Skip to content

Commit

Permalink
feat(minato): relation stage 2 (#99)
Browse files Browse the repository at this point in the history
  • Loading branch information
Hieuzest authored Jun 4, 2024
1 parent df47d9f commit 308229e
Show file tree
Hide file tree
Showing 8 changed files with 1,463 additions and 355 deletions.
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

0 comments on commit 308229e

Please sign in to comment.