Skip to content

Commit

Permalink
feat(minato): relation (#90)
Browse files Browse the repository at this point in the history
Co-authored-by: Shigma <shigma10826@gmail.com>
  • Loading branch information
Hieuzest and shigma authored May 19, 2024
1 parent f178a94 commit aade306
Show file tree
Hide file tree
Showing 26 changed files with 1,795 additions and 213 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"eslint-plugin-mocha": "^10.4.1",
"mocha": "^9.2.2",
"shx": "^0.3.4",
"typescript": "^5.4.3",
"typescript": "^5.5.0-beta",
"yakumo": "^1.0.0-beta.14",
"yakumo-esbuild": "^1.0.0-beta.5",
"yakumo-mocha": "^1.0.0-beta.2",
Expand Down
476 changes: 453 additions & 23 deletions packages/core/src/database.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/core/src/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export abstract class Driver<T = any, C extends Context = Context> {
for (const key in table) {
const submodel = this.model(table[key])
for (const field in submodel.fields) {
if (submodel.fields[field]!.deprecated) continue
if (!Field.available(submodel.fields[field])) continue
model.fields[`${key}.${field}`] = {
expr: Eval('', [table[key].ref, field], Type.fromField(submodel.fields[field]!)),
type: Type.fromField(submodel.fields[field]!),
Expand Down
60 changes: 41 additions & 19 deletions packages/core/src/eval.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { defineProperty, isNullable, mapValues } from 'cosmokit'
import { Comparable, Flatten, isComparable, makeRegExp, Row } from './utils.ts'
import { AtomicTypes, Comparable, Flatten, isComparable, isEmpty, makeRegExp, Row, Values } from './utils.ts'
import { Type } from './type.ts'
import { Field } from './model.ts'
import { Field, Relation } from './model.ts'
import { Query } from './query.ts'

export function isEvalExpr(value: any): value is Eval.Expr {
return value && Object.keys(value).some(key => key.startsWith('$'))
}

export function isAggrExpr(expr: Eval.Expr): boolean {
return expr['$'] || expr['$select']
}

export function hasSubquery(value: any): boolean {
if (!isEvalExpr(value)) return false
return Object.entries(value).filter(([k]) => k.startsWith('$')).some(([k, v]) => {
Expand All @@ -19,16 +24,18 @@ export function hasSubquery(value: any): boolean {
})
}

type UnevalObject<S> = {
[K in keyof S]?: (undefined extends S[K] ? null : never) | Uneval<Exclude<S[K], undefined>, boolean>
}

export type Uneval<U, A extends boolean> =
| U extends number ? Eval.Term<number, A>
: U extends string ? Eval.Term<string, A>
: U extends boolean ? Eval.Term<boolean, A>
: U extends Date ? Eval.Term<Date, A>
: U extends RegExp ? Eval.Term<RegExp, A>
| 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>>
: any

export type Eval<U> =
| U extends Comparable ? U
| U extends Values<AtomicTypes> ? U
: U extends Eval.Expr<infer T> ? T
: never

Expand Down Expand Up @@ -66,6 +73,10 @@ export namespace Eval {
export interface Static {
<A extends boolean>(key: string, value: any, type: Type): Eval.Expr<any, A>

ignoreNull<T, A extends boolean>(value: Eval.Expr<T, A>): Eval.Expr<T, A>
select(...args: Any[]): Expr<any[], false>
query<T extends object>(row: Row<T>, query: Query.Expr<T>): Expr<boolean, false>

// univeral
if<T extends Comparable, A extends boolean>(cond: Any<A>, vThen: Term<T, A>, vElse: Term<T, A>): Expr<T, A>
ifNull<T extends Comparable, A extends boolean>(...args: Term<T, A>[]): Expr<T, A>
Expand Down Expand Up @@ -105,7 +116,9 @@ export namespace Eval {

// element
in<T extends Comparable, A extends boolean>(x: Term<T, A>, array: Array<T, A>): Expr<boolean, A>
in<T extends Comparable, A extends boolean>(x: Term<T, A>[], array: Array<T[], A>): Expr<boolean, A>
nin<T extends Comparable, A extends boolean>(x: Term<T, A>, array: Array<T, A>): Expr<boolean, A>
nin<T extends Comparable, A extends boolean>(x: Term<T, A>[], array: Array<T[], A>): Expr<boolean, A>

// string
concat: Multi<string, string>
Expand All @@ -128,7 +141,6 @@ export namespace Eval {
min: Aggr<Comparable>
count(value: Any<false>): Expr<number, true>
length(value: Any<false>): Expr<number, true>
size<A extends boolean>(value: (Any | Expr<Any, A>)[] | Expr<Any[], A>): Expr<number, A>
length<A extends boolean>(value: any[] | Expr<any[], A>): Expr<number, A>

object<T extends any>(row: Row.Cell<T>): Expr<T, false>
Expand Down Expand Up @@ -177,6 +189,10 @@ operators.$switch = (args, data) => {
return executeEval(data, args.default)
}

Eval.ignoreNull = (expr) => (expr[Type.kType]!.ignoreNull = true, expr)
Eval.select = multary('select', (args, table) => args.map(arg => executeEval(table, arg)), Type.Array())
Eval.query = (row, query) => ({ $expr: true, ...query }) as any

// univeral
Eval.if = multary('if', ([cond, vThen, vElse], data) => executeEval(data, cond) ? executeEval(data, vThen)
: executeEval(data, vElse), (cond, vThen, vElse) => Type.fromTerm(vThen))
Expand Down Expand Up @@ -209,8 +225,18 @@ Eval.lt = comparator('lt', (left, right) => left < right)
Eval.le = Eval.lte = comparator('lte', (left, right) => left <= right)

// element
Eval.in = multary('in', ([value, array], data) => executeEval(data, array).includes(executeEval(data, value)), Type.Boolean)
Eval.nin = multary('nin', ([value, array], data) => !executeEval(data, array).includes(executeEval(data, value)), Type.Boolean)
Eval.in = (value, array) => Eval('in', [Array.isArray(value) ? Eval.select(...value) : value, array], Type.Boolean)
operators.$in = ([value, array], data) => {
const val = executeEval(data, value), arr = executeEval(data, array)
if (typeof val === 'object') return arr.includes(val) || arr.map(JSON.stringify).includes(JSON.stringify(val))
return arr.includes(val)
}
Eval.nin = (value, array) => Eval('nin', [Array.isArray(value) ? Eval.select(...value) : value, array], Type.Boolean)
operators.$nin = ([value, array], data) => {
const val = executeEval(data, value), arr = executeEval(data, array)
if (typeof val === 'object') return !arr.includes(val) && !arr.map(JSON.stringify).includes(JSON.stringify(val))
return !arr.includes(val)
}

// string
Eval.concat = multary('concat', (args, data) => args.map(arg => executeEval(data, arg)).join(''), Type.String)
Expand Down Expand Up @@ -286,7 +312,7 @@ Eval.object = (fields: any) => {
const modelFields: [string, Field][] = Object.entries(fields.$model.fields)
const prefix: string = fields.$prefix
fields = Object.fromEntries(modelFields
.filter(([, field]) => !field.deprecated)
.filter(([, field]) => Field.available(field))
.filter(([path]) => path.startsWith(prefix))
.map(([k]) => [k.slice(prefix.length), fields[k.slice(prefix.length)]]))
return Eval('object', fields, Type.Object(mapValues(fields, (value) => Type.fromTerm(value))))
Expand All @@ -295,18 +321,14 @@ Eval.object = (fields: any) => {
}

Eval.array = unary('array', (expr, table) => Array.isArray(table)
? table.map(data => executeAggr(expr, data))
: Array.from(executeEval(table, expr)), (expr) => Type.Array(Type.fromTerm(expr)))
? table.map(data => executeAggr(expr, data)).filter(x => !expr[Type.kType]?.ignoreNull || !isEmpty(x))
: Array.from(executeEval(table, expr)).filter(x => !expr[Type.kType]?.ignoreNull || !isEmpty(x)), (expr) => Type.Array(Type.fromTerm(expr)))

Eval.exec = unary('exec', (expr, data) => (expr.driver as any).executeSelection(expr, data), (expr) => Type.fromTerm(expr.args[0]))

export { Eval as $ }

type MapUneval<S> = {
[K in keyof S]?: Uneval<S[K], false>
}

export type Update<T = any> = MapUneval<Flatten<T>>
export type Update<T = any> = UnevalObject<Flatten<T>>

function getRecursive(args: string | string[], data: any): any {
if (typeof args === 'string') {
Expand Down
119 changes: 104 additions & 15 deletions packages/core/src/model.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,89 @@
import { Binary, clone, isNullable, makeArray, mapValues, MaybeArray } from 'cosmokit'
import { Binary, clone, filterKeys, isNullable, makeArray, mapValues, MaybeArray } from 'cosmokit'
import { Context } from 'cordis'
import { Eval, isEvalExpr } from './eval.ts'
import { Flatten, Keys, unravel } from './utils.ts'
import { Eval, isEvalExpr, Update } from './eval.ts'
import { DeepPartial, FlatKeys, Flatten, Keys, Row, unravel } from './utils.ts'
import { Type } from './type.ts'
import { Driver } from './driver.ts'
import { Query } from './query.ts'
import { Selection } from './selection.ts'

export const Primary = Symbol('Primary')
const Primary = Symbol('minato.primary')
export type Primary = (string | number) & { [Primary]: true }

export namespace Relation {
const Marker = Symbol('minato.relation')
export type Marker = { [Marker]: true }

export const Type = ['oneToOne', 'oneToMany', 'manyToOne', 'manyToMany'] as const
export type Type = typeof Type[number]

export interface Config<S extends any = any, T extends Keys<S> = Keys<S>, K extends string = string> {
type: Type
table: T
references: Keys<S[T]>[]
fields: K[]
required: boolean
}

export interface Definition<K extends string = string> {
type: 'oneToOne' | 'manyToOne' | 'manyToMany'
table?: string
target?: string
references?: MaybeArray<string>
fields?: MaybeArray<K>
}

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

export type SetExpr<S extends object = any> = Row.Computed<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 function buildAssociationTable(...tables: [string, string]) {
return '_' + tables.sort().join('To')
}

export function buildAssociationKey(key: string, table: string) {
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'
|| (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),
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)),
}
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
&& relation.references.every(key => !relmodel.fields[key]?.nullable || makeArray(relmodel.primary).includes(key)),
}
return [relation, inverse]
}
}

export interface Field<T = any> {
type: Type<T>
deftype?: Field.Type<T>
Expand All @@ -19,6 +95,7 @@ export interface Field<T = any> {
expr?: Eval.Expr
legacy?: string[]
deprecated?: boolean
relation?: Relation.Config
transformers?: Driver.Transformer[]
}

Expand All @@ -37,8 +114,8 @@ export namespace Field {
: T extends Date ? 'timestamp' | 'date' | 'time'
: T extends ArrayBuffer ? 'binary'
: T extends bigint ? 'bigint'
: T extends unknown[] ? 'list' | 'json'
: T extends object ? 'json'
: T extends unknown[] ? 'list' | 'json' | 'oneToMany' | 'manyToMany'
: T extends object ? 'json' | 'oneToOne' | 'manyToOne'
: 'expr'

type Shorthand<S extends string> = S | `${S}(${any})`
Expand Down Expand Up @@ -77,12 +154,16 @@ export namespace Field {
} & Omit<Field<T>, 'type'>

type MapField<O = any, N = any> = {
[K in keyof O]?: Literal<O[K], N> | Definition<O[K], N> | Transform<O[K], any, N>
[K in keyof O]?:
| Literal<O[K], N>
| Definition<O[K], N>
| Transform<O[K], any, N>
| (O[K] extends object ? Relation.Definition<FlatKeys<O>> : never)
}

export type Extension<O = any, N = any> = MapField<Flatten<O>, N>

const NewType = Symbol('newtype')
const NewType = Symbol('minato.newtype')
export type NewType<T> = string & { [NewType]: T }

export type Config<O = any> = {
Expand Down Expand Up @@ -132,6 +213,10 @@ export namespace Field {
}
return initial
}

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

export namespace Model {
Expand Down Expand Up @@ -239,7 +324,7 @@ export class Model<S = any> {
}

format(source: object, strict = true, prefix = '', result = {} as S) {
const fields = Object.keys(this.fields)
const fields = Object.keys(this.fields).filter(key => !this.fields[key].relation)
Object.entries(source).map(([key, value]) => {
key = prefix + key
if (value === undefined) return
Expand All @@ -251,18 +336,18 @@ export class Model<S = any> {
if (field) {
result[key] = value
} else if (!value || typeof value !== 'object' || isEvalExpr(value) || Object.keys(value).length === 0) {
if (strict) {
if (strict && (typeof value !== 'object' || Object.keys(value).length)) {
throw new TypeError(`unknown field "${key}" in model ${this.name}`)
}
} else {
this.format(value, strict, key + '.', result)
}
})
return prefix === '' ? this.resolveModel(result) : result
return (strict && prefix === '') ? this.resolveModel(result) : result
}

parse(source: object, strict = true, prefix = '', result = {} as S) {
const fields = Object.keys(this.fields)
const fields = Object.keys(this.fields).filter(key => !this.fields[key].relation)
if (strict && prefix === '') {
// initialize object layout
Object.assign(result as any, unravel(Object.fromEntries(fields
Expand Down Expand Up @@ -293,22 +378,26 @@ export class Model<S = any> {
}
}
}
return prefix === '' ? this.resolveModel(result) : result
return (strict && prefix === '') ? this.resolveModel(result) : result
}

create(data?: {}) {
const result = {} as S
const keys = makeArray(this.primary)
for (const key in this.fields) {
const { initial, deprecated } = this.fields[key]!
if (deprecated) continue
if (!Field.available(this.fields[key])) continue
const { initial } = this.fields[key]!
if (!keys.includes(key) && !isNullable(initial)) {
result[key] = clone(initial)
}
}
return this.parse({ ...result, ...data })
}

avaiableFields() {
return filterKeys(this.fields, (_, field) => Field.available(field))
}

getType(): Type<S>
getType(key: string): Type | undefined
getType(key?: string): Type | undefined {
Expand Down
Loading

0 comments on commit aade306

Please sign in to comment.