Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(minato): relation stage 2 #99

Merged
merged 19 commits into from
Jun 4, 2024
Merged
635 changes: 434 additions & 201 deletions packages/core/src/database.ts

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions packages/core/src/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { Awaitable, Dict, mapValues, remove } from 'cosmokit'
import { Context, Logger } from 'cordis'
import { Eval, Update } from './eval.ts'
import { Direction, Modifier, Selection } from './selection.ts'
import { Field, Model } from './model.ts'
import { Field, Model, Relation } from './model.ts'
import { Database } from './database.ts'
import { Type } from './type.ts'
import { FlatKeys } from './utils.ts'

export namespace Driver {
export interface Stats {
Expand All @@ -17,13 +18,14 @@ export namespace Driver {
size: number
}

export type Cursor<K extends string = never> = K[] | CursorOptions<K>
export type Cursor<T = any, S = any, K extends FlatKeys<T> = any> = K[] | CursorOptions<T, S, K>

export interface CursorOptions<K extends string> {
export interface CursorOptions<T = any, S = any, K extends FlatKeys<T> = any> {
limit?: number
offset?: number
fields?: K[]
sort?: Dict<Direction, K>
include?: Relation.Include<T, S>
}

export interface WriteResult {
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export function isEvalExpr(value: any): value is Eval.Expr {
return value && Object.keys(value).some(key => key.startsWith('$'))
}

export const isUpdateExpr: (value: any) => boolean = isEvalExpr

export function isAggrExpr(expr: Eval.Expr): boolean {
return expr['$'] || expr['$select']
}
Expand All @@ -31,7 +33,7 @@ type UnevalObject<S> = {
export type Uneval<U, A extends boolean> =
| U extends Values<AtomicTypes> ? Eval.Term<U, A>
: U extends (infer T extends object)[] ? Relation.Modifier<T> | Eval.Array<T, A>
: U extends object ? Eval.Expr<U, A> | UnevalObject<Flatten<U>>
: U extends object ? Eval.Expr<U, A> | UnevalObject<Flatten<U>> | Relation.Modifier<U>
: any

export type Eval<U> =
Expand Down
50 changes: 35 additions & 15 deletions packages/core/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Type } from './type.ts'
import { Driver } from './driver.ts'
import { Query } from './query.ts'
import { Selection } from './selection.ts'
import { Create } from './database.ts'

const Primary = Symbol('minato.primary')
export type Primary = (string | number) & { [Primary]: true }
Expand All @@ -22,6 +23,7 @@ export namespace Relation {
table: T
references: Keys<S[T]>[]
fields: K[]
shared: Record<K, Keys<S[T]>>
required: boolean
}

Expand All @@ -31,55 +33,73 @@ export namespace Relation {
target?: string
references?: MaybeArray<string>
fields?: MaybeArray<K>
shared?: MaybeArray<K> | Partial<Record<K, string>>
}

export type Include<T, S> = boolean | {
[P in keyof T]?: T[P] extends MaybeArray<infer U extends S> | undefined ? Include<U, S> : never
[P in keyof T]?: T[P] extends MaybeArray<infer U> | undefined ? U extends S ? Include<U, S> : never : never
}

export type SetExpr<S extends object = any> = Row.Computed<S, Update<S>> | {
export type SetExpr<S extends object = any> = ((row: Row<S>) => Update<S>) | {
where: Query.Expr<Flatten<S>> | Selection.Callback<S, boolean>
update: Row.Computed<S, Update<S>>
}

export interface Modifier<S extends object = any> {
$create?: MaybeArray<DeepPartial<S>>
$set?: MaybeArray<SetExpr<S>>
$remove?: Query.Expr<Flatten<S>> | Selection.Callback<S, boolean>
$connect?: Query.Expr<Flatten<S>> | Selection.Callback<S, boolean>
$disconnect?: Query.Expr<Flatten<S>> | Selection.Callback<S, boolean>
export interface Modifier<T extends object = any, S extends any = any> {
$create?: MaybeArray<Create<T, S>>
$upsert?: MaybeArray<DeepPartial<T>>
$set?: MaybeArray<SetExpr<T>>
$remove?: Query.Expr<Flatten<T>> | Selection.Callback<T, boolean>
$connect?: Query.Expr<Flatten<T>> | Selection.Callback<T, boolean>
$disconnect?: Query.Expr<Flatten<T>> | Selection.Callback<T, boolean>
}

export function buildAssociationTable(...tables: [string, string]) {
return '_' + tables.sort().join('To')
return '_' + tables.sort().join('_')
}

export function buildAssociationKey(key: string, table: string) {
return `${table}_${key}`
return `${table}.${key}`
}

export function parse(def: Definition, key: string, model: Model, relmodel: Model): [Config, Config] {
const fields = def.fields ?? ((model.name === relmodel.name || def.type === 'manyToOne'
export function buildSharedKey(field: string, reference: string) {
return [field, reference].sort().join('_')
}

export function parse(def: Definition, key: string, model: Model, relmodel: Model, subprimary?: boolean): [Config, Config] {
const shared = !def.shared ? {}
: typeof def.shared === 'string' ? { [def.shared]: def.shared }
: Array.isArray(def.shared) ? Object.fromEntries(def.shared.map(x => [x, x]))
: def.shared
const fields = def.fields ?? ((subprimary || model.name === relmodel.name || def.type === 'manyToOne'
|| (def.type === 'oneToOne' && !makeArray(relmodel.primary).every(key => !relmodel.fields[key]?.nullable)))
? makeArray(relmodel.primary).map(x => `${key}.${x}`) : model.primary)
const relation: Config = {
type: def.type,
table: def.table ?? relmodel.name,
fields: makeArray(fields),
shared: shared as any,
references: makeArray(def.references ?? relmodel.primary),
required: def.type !== 'manyToOne' && model.name !== relmodel.name
&& makeArray(fields).every(key => !model.fields[key]?.nullable || makeArray(model.primary).includes(key)),
}
// remove shared keys from fields and references
Object.entries(shared).forEach(([k, v]) => {
relation.fields = relation.fields.filter(x => x !== k)
relation.references = relation.references.filter(x => x !== v)
})
const inverse: Config = {
type: relation.type === 'oneToMany' ? 'manyToOne'
: relation.type === 'manyToOne' ? 'oneToMany'
: relation.type,
table: model.name,
fields: relation.references,
references: relation.fields,
required: relation.type !== 'oneToMany' && !relation.required
shared: Object.fromEntries(Object.entries(shared).map(([k, v]) => [v, k])),
required: relation.type !== 'oneToMany'
&& relation.references.every(key => !relmodel.fields[key]?.nullable || makeArray(relmodel.primary).includes(key)),
}
if (inverse.required) relation.required = false
return [relation, inverse]
}
}
Expand Down Expand Up @@ -158,7 +178,7 @@ export namespace Field {
| Literal<O[K], N>
| Definition<O[K], N>
| Transform<O[K], any, N>
| (O[K] extends object ? Relation.Definition<FlatKeys<O>> : never)
| (O[K] extends object | undefined ? Relation.Definition<FlatKeys<O>> : never)
}

export type Extension<O = any, N = any> = MapField<Flatten<O>, N>
Expand Down Expand Up @@ -215,7 +235,7 @@ export namespace Field {
}

export function available(field?: Field) {
return !!field && !field.deprecated && !field.relation
return !!field && !field.deprecated && !field.relation && field.deftype !== 'expr'
}
}

Expand Down
19 changes: 12 additions & 7 deletions packages/core/src/selection.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { defineProperty, Dict, filterKeys } from 'cosmokit'
import { defineProperty, Dict, filterKeys, mapValues } from 'cosmokit'
import { Driver } from './driver.ts'
import { Eval, executeEval, isAggrExpr, isEvalExpr } from './eval.ts'
import { Field, Model } from './model.ts'
import { Query } from './query.ts'
import { FlatKeys, FlatPick, Flatten, Keys, randomId, Row } from './utils.ts'
import { FlatKeys, FlatPick, Flatten, getCell, Keys, randomId, Row } from './utils.ts'
import { Type } from './type.ts'

declare module './eval.ts' {
Expand Down Expand Up @@ -61,7 +61,7 @@ const createRow = (ref: string, expr = {}, prefix = '', model?: Model) => new Pr
}

const row = createRow(ref, Eval('', [ref, `${prefix}${key}`], type), `${prefix}${key}.`, model)
if (Object.keys(model?.fields!).some(k => k.startsWith(`${prefix}${key}.`))) {
if (!field && Object.keys(model?.fields!).some(k => k.startsWith(`${prefix}${key}.`))) {
return createRow(ref, Eval.object(row), `${prefix}${key}.`, model)
} else {
return row
Expand Down Expand Up @@ -253,15 +253,20 @@ export class Selection<S = any> extends Executable<S, S[]> {
): Selection<S & { [P in K]: U}> {
const fields = Object.fromEntries(Object.entries(this.model.fields)
.filter(([key, field]) => Field.available(field) && !key.startsWith(name + '.'))
.map(([key]) => [key, (row) => key.split('.').reduce((r, k) => r[k], row[this.ref])]))
.map(([key]) => [key, (row) => getCell(row[this.ref], key)]))
const joinFields = Object.fromEntries(Object.entries(selection.model.fields)
.filter(([key, field]) => Field.available(field) || Field.available(this.model.fields[`${name}.${key}`]))
.map(([key]) => [key,
(row) => Field.available(this.model.fields[`${name}.${key}`]) ? getCell(row[this.ref], `${name}.${key}`) : getCell(row[name], key),
]))
if (optional) {
return this.driver.database
.join({ [this.ref]: this as Selection, [name]: selection }, (t: any) => callback(t[this.ref], t[name]), { [this.ref]: false, [name]: true })
.project({ ...fields, [name]: (row) => Eval.ignoreNull(row[name]) }) as any
.project({ ...fields, [name]: (row) => Eval.ignoreNull(Eval.object(mapValues(joinFields, x => x(row)))) }) as any
} else {
return this.driver.database
.join({ [this.ref]: this as Selection, [name]: selection }, (t: any) => callback(t[this.ref], t[name]))
.project({ ...fields, [name]: (row) => Eval.ignoreNull(row[name]) }) as any
.project({ ...fields, [name]: (row) => Eval.ignoreNull(Eval.object(mapValues(joinFields, x => x(row)))) }) as any
}
}

Expand All @@ -282,7 +287,7 @@ export class Selection<S = any> extends Executable<S, S[]> {
}

execute(): Promise<S[]>
execute<K extends FlatKeys<S> = any>(cursor?: Driver.Cursor<K>): Promise<FlatPick<S, K>[]>
execute<K extends FlatKeys<S> = any>(cursor?: Driver.Cursor<S, any, K>): Promise<FlatPick<S, K>[]>
execute<T>(callback: Selection.Callback<S, T, true>): Promise<T>
async execute(cursor?: any) {
if (typeof cursor === 'function') {
Expand Down
7 changes: 5 additions & 2 deletions packages/memory/tests/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,13 @@ describe('@minatojs/driver-memory', () => {
},
relation: {
select: {
ignoreNullObject: false,
nullableComparator: false,
},
create: {
nullableComparator: false,
},
modify: {
ignoreNullObject: false,
nullableComparator: false,
},
},
})
Expand Down
2 changes: 1 addition & 1 deletion packages/tests/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ function ModelOperations(database: Database<Tables, Types>) {
inner: {
num: 'unsigned',
text: 'string',
json: 'json',
json: 'object',
embed: {
type: 'object',
inner: {
Expand Down
Loading