Comparing version 3.4.0 to 3.4.1
@@ -140,8 +140,9 @@ import { Extract, Dict, Awaitable, MaybeArray, Intersect } from 'cosmokit'; | ||
} | ||
type Cursor<K extends string = never> = K[] | CursorOptions<K>; | ||
interface CursorOptions<K extends string> { | ||
type Cursor<K extends string = string, S = any, T extends Keys<S> = any> = K[] | CursorOptions<K, S, T>; | ||
interface CursorOptions<K extends string = string, S = any, T extends Keys<S> = any> { | ||
limit?: number; | ||
offset?: number; | ||
fields?: K[]; | ||
sort?: Dict<Direction, K>; | ||
sort?: Partial<Dict<Direction, FlatKeys<S[T]>>>; | ||
include?: Relation.Include<S[T], Values<S>>; | ||
} | ||
@@ -211,2 +212,3 @@ interface WriteResult { | ||
fields: K[]; | ||
shared: Record<K, Keys<S[T]>>; | ||
required: boolean; | ||
@@ -220,20 +222,23 @@ } | ||
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]): string; | ||
export function buildAssociationKey(key: string, table: string): string; | ||
export function parse(def: Definition, key: string, model: Model, relmodel: Model): [Config, Config]; | ||
export function buildSharedKey(field: string, reference: string): string; | ||
export function parse(def: Definition, key: string, model: Model, relmodel: Model, subprimary?: boolean): [Config, Config]; | ||
} | ||
@@ -284,3 +289,3 @@ export interface Field<T = any> { | ||
type MapField<O = any, N = any> = { | ||
[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); | ||
[K in keyof O]?: Literal<O[K], N> | Definition<O[K], N> | Transform<O[K], any, N> | (O[K] extends object | undefined ? Relation.Definition<FlatKeys<O>> : never); | ||
}; | ||
@@ -360,2 +365,3 @@ export type Extension<O = any, N = any> = MapField<Flatten<O>, N>; | ||
export function isEvalExpr(value: any): value is Eval.Expr; | ||
export const isUpdateExpr: (value: any) => boolean; | ||
export function isAggrExpr(expr: Eval.Expr): boolean; | ||
@@ -366,3 +372,3 @@ export function hasSubquery(value: any): boolean; | ||
}; | ||
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>> : any; | ||
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>> | Relation.Modifier<U> : any; | ||
export type Eval<U> = U extends Values<AtomicTypes> ? U : U extends Eval.Expr<infer T> ? T : never; | ||
@@ -397,3 +403,3 @@ declare const kExpr: unique symbol; | ||
select(...args: Any[]): Expr<any[], false>; | ||
query<T extends object>(row: Row<T>, query: Query.Expr<T>): Expr<boolean, false>; | ||
query<T extends object>(row: Row<T>, query: Query.Expr<T>, expr?: Term<boolean>): Expr<boolean, false>; | ||
if<T extends Comparable, A extends boolean>(cond: Any<A>, vThen: Term<T, A>, vElse: Term<T, A>): Expr<T, A>; | ||
@@ -508,3 +514,3 @@ ifNull<T extends Comparable, A extends boolean>(...args: Term<T, A>[]): Expr<T, A>; | ||
export function flatten(source: object, prefix?: string, ignore?: (value: any) => boolean): {}; | ||
export function getCell(row: any, key: any): any; | ||
export function getCell(row: any, path: any): any; | ||
export function isEmpty(value: any): boolean; | ||
@@ -531,2 +537,16 @@ type TableLike<S> = Keys<S> | Selection; | ||
} | ||
type CreateMap<T, S> = { | ||
[K in keyof T]?: Create<T[K], S>; | ||
}; | ||
export type Create<T, S> = T extends Values<AtomicTypes> ? T : T extends (infer I extends Values<S>)[] ? CreateMap<I, S>[] | { | ||
$literal?: DeepPartial<I>; | ||
$create?: MaybeArray<CreateMap<I, S>>; | ||
$upsert?: MaybeArray<CreateMap<I, S>>; | ||
$connect?: Query.Expr<Flatten<I>>; | ||
} : T extends Values<S> ? CreateMap<T, S> | { | ||
$literal?: DeepPartial<T>; | ||
$create?: CreateMap<T, S>; | ||
$upsert?: CreateMap<T, S>; | ||
$connect?: Query.Expr<Flatten<T>>; | ||
} : T extends (infer U)[] ? DeepPartial<U>[] : T extends object ? CreateMap<T, S> : T; | ||
export class Database<S = {}, N = {}, C extends Context = Context> extends Service<undefined, C> { | ||
@@ -549,3 +569,3 @@ static [Service.provide]: string; | ||
private prepare; | ||
extend<K extends Keys<S>, T extends Field.Extension<S[K], N>>(name: K, fields: T, config?: Partial<Model.Config<Keys<T>>>): void; | ||
extend<K extends Keys<S>>(name: K, fields: Field.Extension<S[K], N>, config?: Partial<Model.Config<FlatKeys<S[K]>>>): void; | ||
private _parseField; | ||
@@ -557,11 +577,11 @@ private parseField; | ||
select<T>(table: Selection<T>, query?: Query<T>): Selection<T>; | ||
select<K extends Keys<S>>(table: K, query?: Query<S[K]>, cursor?: Relation.Include<S[K], Values<S>> | null): Selection<S[K]>; | ||
select<K extends Keys<S>>(table: K, query?: Query<S[K]>, include?: Relation.Include<S[K], Values<S>> | null): Selection<S[K]>; | ||
join<const X extends Join1.Input<S>>(tables: X, callback?: Join1.Predicate<S, X>, optional?: boolean[]): Selection<Join1.Output<S, X>>; | ||
join<X extends Join2.Input<S>>(tables: X, callback?: Join2.Predicate<S, X>, optional?: Dict<boolean, Keys<X>>): Selection<Join2.Output<S, X>>; | ||
get<K extends Keys<S>>(table: K, query: Query<S[K]>): Promise<S[K][]>; | ||
get<K extends Keys<S>, P extends FlatKeys<S[K]> = any>(table: K, query: Query<S[K]>, cursor?: Driver.Cursor<P>): Promise<FlatPick<S[K], P>[]>; | ||
get<K extends Keys<S>, P extends FlatKeys<S[K]> = any>(table: K, query: Query<S[K]>, cursor?: Driver.Cursor<P, S, K>): Promise<FlatPick<S[K], P>[]>; | ||
eval<K extends Keys<S>, T>(table: K, expr: Selection.Callback<S[K], T, true>, query?: Query<S[K]>): Promise<T>; | ||
set<K extends Keys<S>>(table: K, query: Query<S[K]>, update: Row.Computed<S[K], Update<S[K]>>): Promise<Driver.WriteResult>; | ||
remove<K extends Keys<S>>(table: K, query: Query<S[K]>): Promise<Driver.WriteResult>; | ||
create<K extends Keys<S>>(table: K, data: DeepPartial<S[K]>): Promise<S[K]>; | ||
create<K extends Keys<S>>(table: K, data: Create<S[K], S>): Promise<S[K]>; | ||
upsert<K extends Keys<S>>(table: K, upsert: Row.Computed<S[K], Update<S[K]>[]>, keys?: MaybeArray<FlatKeys<S[K], Indexable>>): Promise<Driver.WriteResult>; | ||
@@ -577,3 +597,5 @@ makeProxy(marker: any, getDriver?: (driver: Driver<any, C>, database: this) => Driver<any, C>): this; | ||
private transformRelationQuery; | ||
private createOrUpdate; | ||
private processRelationUpdate; | ||
private hasRelation; | ||
} | ||
@@ -580,0 +602,0 @@ export namespace RuntimeError { |
{ | ||
"name": "minato", | ||
"version": "3.4.0", | ||
"version": "3.4.1", | ||
"description": "Type Driven Database Framework", | ||
@@ -65,3 +65,3 @@ "type": "module", | ||
"peerDependencies": { | ||
"cordis": "^3.16.1" | ||
"cordis": "^3.17.1" | ||
}, | ||
@@ -68,0 +68,0 @@ "dependencies": { |
@@ -1,8 +0,8 @@ | ||
import { defineProperty, Dict, filterKeys, makeArray, mapValues, MaybeArray, noop, omit } from 'cosmokit' | ||
import { deduplicate, defineProperty, Dict, filterKeys, isNullable, makeArray, mapValues, MaybeArray, noop, omit, pick, remove } from 'cosmokit' | ||
import { Context, Service, Spread } from 'cordis' | ||
import { DeepPartial, FlatKeys, FlatPick, getCell, Indexable, Keys, randomId, Row, unravel, Values } from './utils.ts' | ||
import { AtomicTypes, DeepPartial, FlatKeys, FlatPick, Flatten, getCell, Indexable, Keys, randomId, Row, unravel, Values } from './utils.ts' | ||
import { Selection } from './selection.ts' | ||
import { Field, Model, Relation } from './model.ts' | ||
import { Driver } from './driver.ts' | ||
import { Eval, Update } from './eval.ts' | ||
import { Eval, isUpdateExpr, Update } from './eval.ts' | ||
import { Query } from './query.ts' | ||
@@ -47,5 +47,39 @@ import { Type } from './type.ts' | ||
type CreateMap<T, S> = { [K in keyof T]?: Create<T[K], S> } | ||
export type Create<T, S> = | ||
| T extends Values<AtomicTypes> ? T | ||
: T extends (infer I extends Values<S>)[] ? CreateMap<I, S>[] | | ||
{ | ||
$literal?: DeepPartial<I> | ||
$create?: MaybeArray<CreateMap<I, S>> | ||
$upsert?: MaybeArray<CreateMap<I, S>> | ||
$connect?: Query.Expr<Flatten<I>> | ||
} | ||
: T extends Values<S> ? CreateMap<T, S> | | ||
{ | ||
$literal?: DeepPartial<T> | ||
$create?: CreateMap<T, S> | ||
$upsert?: CreateMap<T, S> | ||
$connect?: Query.Expr<Flatten<T>> | ||
} | ||
: T extends (infer U)[] ? DeepPartial<U>[] | ||
: T extends object ? CreateMap<T, S> | ||
: T | ||
function mergeQuery<T>(base: Query.FieldExpr<T>, query: Query.Expr<Flatten<T>> | ((row: Row<T>) => Query.Expr<Flatten<T>>)): Selection.Callback<T, boolean> { | ||
if (typeof query === 'function') { | ||
return (row: any) => { | ||
const q = query(row) | ||
return { $expr: true, ...base, ...(q.$expr ? q : { $expr: q }) } as any | ||
} | ||
} else { | ||
return (_: any) => ({ $expr: true, ...base, ...query }) as any | ||
} | ||
} | ||
export class Database<S = {}, N = {}, C extends Context = Context> extends Service<undefined, C> { | ||
static [Service.provide] = 'model' | ||
static [Service.immediate] = true | ||
static readonly transact = Symbol('minato.transact') | ||
@@ -101,3 +135,3 @@ static readonly migrate = Symbol('minato.migrate') | ||
extend<K extends Keys<S>, T extends Field.Extension<S[K], N>>(name: K, fields: T, config: Partial<Model.Config<Keys<T>>> = {}) { | ||
extend<K extends Keys<S>>(name: K, fields: Field.Extension<S[K], N>, config: Partial<Model.Config<FlatKeys<S[K]>>> = {}) { | ||
let model = this.tables[name] | ||
@@ -114,7 +148,8 @@ if (!model) { | ||
if (makeArray(model.primary).every(key => key in fields)) { | ||
defineProperty(model, 'ctx', this[Context.origin]) | ||
defineProperty(model, 'ctx', this.ctx) | ||
} | ||
Object.entries(fields).forEach(([key, def]: [string, Relation.Definition]) => { | ||
if (!Relation.Type.includes(def.type)) return | ||
const [relation, inverse] = Relation.parse(def, key, model, this.tables[def.table ?? key]) | ||
const subprimary = !def.fields && makeArray(model.primary).includes(key) | ||
const [relation, inverse] = Relation.parse(def, key, model, this.tables[def.table ?? key], subprimary) | ||
if (!this.tables[relation.table]) throw new Error(`relation table ${relation.table} does not exist`) | ||
@@ -137,11 +172,12 @@ ;(model.fields[key] = Field.parse('expr')).relation = relation | ||
if (this.tables[assocTable]) return | ||
const fields = relation.fields.map(x => [Relation.buildAssociationKey(x, name), model.fields[x]?.deftype] as const) | ||
const shared = Object.entries(relation.shared).map(([x, y]) => [Relation.buildSharedKey(x, y), model.fields[x]!.deftype] as const) | ||
const fields = relation.fields.map(x => [Relation.buildAssociationKey(x, name), model.fields[x]!.deftype] as const) | ||
const references = relation.references.map((x, i) => [Relation.buildAssociationKey(x, relation.table), fields[i][1]] as const) | ||
this.extend(assocTable as any, { | ||
...Object.fromEntries([...fields, ...references]), | ||
...Object.fromEntries([...shared, ...fields, ...references]), | ||
[name]: { | ||
type: 'manyToOne', | ||
table: name, | ||
fields: fields.map(x => x[0]), | ||
references: relation.references, | ||
fields: [...shared, ...fields].map(x => x[0]), | ||
references: [...Object.keys(relation.shared), ...relation.fields], | ||
}, | ||
@@ -151,10 +187,14 @@ [relation.table]: { | ||
table: relation.table, | ||
fields: references.map(x => x[0]), | ||
references: relation.fields, | ||
fields: [...shared, ...references].map(x => x[0]), | ||
references: [...Object.values(relation.shared), ...relation.references], | ||
}, | ||
} as any, { | ||
primary: [...fields.map(x => x[0]), ...references.map(x => x[0])], | ||
primary: [...shared, ...fields, ...references].map(x => x[0]) as any, | ||
}) | ||
} | ||
}) | ||
// use relation field as primary | ||
if (Array.isArray(model.primary) && model.primary.every(key => model.fields[key]?.relation)) { | ||
model.primary = deduplicate(model.primary.map(key => model.fields[key]!.relation!.fields).flat()) | ||
} | ||
this.prepareTasks[name] = this.prepare(name) | ||
@@ -254,3 +294,3 @@ ;(this.ctx as Context).emit('model', name) | ||
this[Context.current].effect(() => { | ||
this.ctx.effect(() => { | ||
this.types[name] = { ...field } | ||
@@ -275,12 +315,12 @@ this.types[name].deftype ??= this.types[field.type]?.deftype ?? type.type as any | ||
query?: Query<S[K]>, | ||
cursor?: Relation.Include<S[K], Values<S>> | null, | ||
include?: Relation.Include<S[K], Values<S>> | null, | ||
): Selection<S[K]> | ||
select(table: any, query?: any, cursor?: any) { | ||
select(table: any, query?: any, include?: any) { | ||
let sel = new Selection(this.getDriver(table), table, query) | ||
if (typeof table !== 'string') return sel | ||
const whereOnly = cursor === null | ||
const whereOnly = include === null | ||
const rawquery = typeof query === 'function' ? query : () => query | ||
const modelFields = this.tables[table].fields | ||
if (cursor) cursor = filterKeys(cursor, (key) => !!modelFields[key]?.relation) | ||
if (include) include = filterKeys(include, (key) => !!modelFields[key]?.relation) | ||
for (const key in { ...sel.query, ...sel.query.$not }) { | ||
@@ -304,4 +344,4 @@ if (modelFields[key]?.relation) { | ||
} | ||
if (!cursor || !Object.getOwnPropertyNames(cursor).includes(key)) { | ||
(cursor ??= {})[key] = true | ||
if (!include || !Object.getOwnPropertyNames(include).includes(key)) { | ||
(include ??= {})[key] = true | ||
} | ||
@@ -311,9 +351,9 @@ } | ||
sel.query = omit(sel.query, Object.keys(cursor ?? {})) | ||
sel.query = omit(sel.query, Object.keys(include ?? {})) | ||
if (Object.keys(sel.query.$not ?? {}).length) { | ||
sel.query.$not = omit(sel.query.$not!, Object.keys(cursor ?? {})) | ||
sel.query.$not = omit(sel.query.$not!, Object.keys(include ?? {})) | ||
if (Object.keys(sel.query.$not).length === 0) Reflect.deleteProperty(sel.query, '$not') | ||
} | ||
if (cursor && typeof cursor === 'object') { | ||
if (include && typeof include === 'object') { | ||
if (typeof table !== 'string') throw new Error('cannot include relations on derived selection') | ||
@@ -328,7 +368,7 @@ const extraFields: string[] = [] | ||
} | ||
for (const key in cursor) { | ||
if (!cursor[key] || !modelFields[key]?.relation) continue | ||
for (const key in include) { | ||
if (!include[key] || !modelFields[key]?.relation) continue | ||
const relation: Relation.Config<S> = modelFields[key]!.relation as any | ||
if (relation.type === 'oneToOne' || relation.type === 'manyToOne') { | ||
sel = whereOnly ? sel : sel.join(key, this.select(relation.table, {}, cursor[key]), (self, other) => Eval.and( | ||
sel = whereOnly ? sel : sel.join(key, this.select(relation.table, {}, include[key]), (self, other) => Eval.and( | ||
...relation.fields.map((k, i) => Eval.eq(self[k], other[relation.references[i]])), | ||
@@ -338,3 +378,3 @@ ), true) | ||
} else if (relation.type === 'oneToMany') { | ||
sel = whereOnly ? sel : sel.join(key, this.select(relation.table, {}, cursor[key]), (self, other) => Eval.and( | ||
sel = whereOnly ? sel : sel.join(key, this.select(relation.table, {}, include[key]), (self, other) => Eval.and( | ||
...relation.fields.map((k, i) => Eval.eq(self[k], other[relation.references[i]])), | ||
@@ -344,3 +384,3 @@ ), true) | ||
sel = whereOnly ? sel : sel.groupBy([ | ||
...Object.entries(modelFields).filter(([, field]) => Field.available(field)).map(([k]) => k), | ||
...Object.entries(modelFields).filter(([k, field]) => !extraFields.some(x => k.startsWith(`${x}.`)) && Field.available(field)).map(([k]) => k), | ||
...extraFields, | ||
@@ -353,3 +393,8 @@ ], { | ||
const references = relation.fields.map(x => Relation.buildAssociationKey(x, table)) | ||
sel = whereOnly ? sel : sel.join(key, this.select(assocTable, {}, { [relation.table]: cursor[key] } as any), (self, other) => Eval.and( | ||
const shared = Object.entries(relation.shared).map(([x, y]) => [Relation.buildSharedKey(x, y), { | ||
field: x, | ||
reference: y, | ||
}] as const) | ||
sel = whereOnly ? sel : sel.join(key, this.select(assocTable, {}, { [relation.table]: include[key] } as any), (self, other) => Eval.and( | ||
...shared.map(([k, v]) => Eval.eq(self[v.field], other[k])), | ||
...relation.fields.map((k, i) => Eval.eq(self[k], other[references[i]])), | ||
@@ -359,3 +404,3 @@ ), true) | ||
sel = whereOnly ? sel : sel.groupBy([ | ||
...Object.entries(modelFields).filter(([, field]) => Field.available(field)).map(([k]) => k), | ||
...Object.entries(modelFields).filter(([k, field]) => !extraFields.some(x => k.startsWith(`${x}.`)) && Field.available(field)).map(([k]) => k), | ||
...extraFields, | ||
@@ -415,8 +460,9 @@ ], { | ||
query: Query<S[K]>, | ||
cursor?: Driver.Cursor<P>, | ||
cursor?: Driver.Cursor<P, S, K>, | ||
): Promise<FlatPick<S[K], P>[]> | ||
async get<K extends Keys<S>>(table: K, query: Query<S[K]>, cursor?: any) { | ||
const fields = Array.isArray(cursor) ? cursor : cursor?.fields | ||
return this.select(table, query, fields && Object.fromEntries(fields.map(x => [x, true])) as any).execute(cursor) as any | ||
let fields = Array.isArray(cursor) ? cursor : cursor?.fields | ||
fields = fields ? Object.fromEntries(fields.map(x => [x, true])) : cursor?.include | ||
return this.select(table, query, fields).execute(cursor) as any | ||
} | ||
@@ -434,3 +480,3 @@ | ||
const rawupdate = typeof update === 'function' ? update : () => update | ||
const sel = this.select(table, query, null) | ||
let sel = this.select(table, query, null) | ||
if (typeof update === 'function') update = update(sel.row) | ||
@@ -448,24 +494,7 @@ const primary = makeArray(sel.model.primary) | ||
const rows = await database.get(table, query) | ||
let baseUpdate = omit(update, relations.map(([key]) => key) as any) | ||
sel = database.select(table, query, null) | ||
let baseUpdate = omit(rawupdate(sel.row), relations.map(([key]) => key) as any) | ||
baseUpdate = sel.model.format(baseUpdate) | ||
for (const [key, relation] of relations) { | ||
if (relation.type === 'oneToOne') { | ||
if (update[key] === null) { | ||
await Promise.all(rows.map(row => database.remove(relation.table, | ||
Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])) as any, | ||
))) | ||
} else { | ||
await database.upsert(relation.table, rows.map(row => ({ | ||
...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), | ||
...rawupdate(row as any)[key], | ||
})), relation.references as any) | ||
} | ||
} else if (relation.type === 'manyToOne') { | ||
await database.upsert(relation.table, rows.map(row => ({ | ||
...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), | ||
...rawupdate(row as any)[key], | ||
})), relation.references as any) | ||
} else if (relation.type === 'oneToMany' || relation.type === 'manyToMany') { | ||
await Promise.all(rows.map(row => this.processRelationUpdate(table, row, key, rawupdate(row as any)[key]))) | ||
} | ||
for (const [key] of relations) { | ||
await Promise.all(rows.map(row => database.processRelationUpdate(table, row, key, rawupdate(row as any)[key]))) | ||
} | ||
@@ -486,59 +515,18 @@ return Object.keys(baseUpdate).length === 0 ? {} : await sel._action('set', baseUpdate).execute() | ||
async create<K extends Keys<S>>(table: K, data: DeepPartial<S[K]>): Promise<S[K]> | ||
async create<K extends Keys<S>>(table: K, data: Create<S[K], S>): Promise<S[K]> | ||
async create<K extends Keys<S>>(table: K, data: any): Promise<S[K]> { | ||
const sel = this.select(table) | ||
const { primary, autoInc, fields } = sel.model | ||
if (!autoInc) { | ||
const keys = makeArray(primary) | ||
if (keys.some(key => getCell(data, key) === undefined)) { | ||
throw new Error('missing primary key') | ||
} | ||
} | ||
const tasks: any[] = [] | ||
for (const key in data) { | ||
if (data[key] && this.tables[table].fields[key]?.relation) { | ||
const relation = this.tables[table].fields[key].relation | ||
if (relation.type === 'oneToOne' && relation.required) { | ||
const mergedData = { ...data[key] } | ||
for (const k in relation.fields) { | ||
mergedData[relation.references[k]] = getCell(data, relation.fields[k]) | ||
} | ||
tasks.push([relation.table, [mergedData], relation.references]) | ||
} else if (relation.type === 'oneToMany' && Array.isArray(data[key])) { | ||
const mergedData = data[key].map(row => { | ||
const mergedData = { ...row } | ||
for (const k in relation.fields) { | ||
mergedData[relation.references[k]] = getCell(data, relation.fields[k]) | ||
} | ||
return mergedData | ||
}) | ||
tasks.push([relation.table, mergedData]) | ||
} else { | ||
// handle shadowed fields | ||
data = { | ||
...omit(data, [key]), | ||
...Object.fromEntries(Object.entries(data[key]).map(([k, v]) => { | ||
if (!fields[`${key}.${k}`]) { | ||
throw new Error(`field ${key}.${k} does not exist`) | ||
} | ||
return [`${key}.${k}`, v] | ||
})), | ||
} | ||
continue | ||
if (!this.hasRelation(table, data)) { | ||
const { primary, autoInc } = sel.model | ||
if (!autoInc) { | ||
const keys = makeArray(primary) | ||
if (keys.some(key => getCell(data, key) === undefined)) { | ||
throw new Error('missing primary key') | ||
} | ||
data = omit(data, [key]) as any | ||
} | ||
return sel._action('create', sel.model.create(data)).execute() | ||
} else { | ||
return this.ensureTransaction(database => database.createOrUpdate(table, data, false)) | ||
} | ||
if (tasks.length) { | ||
return this.ensureTransaction(async (database) => { | ||
for (const [table, data, keys] of tasks) { | ||
await database.upsert(table, data, keys) | ||
} | ||
return database.create(table, data) | ||
}) | ||
} | ||
return sel._action('create', sel.model.create(data)).execute() | ||
} | ||
@@ -553,60 +541,2 @@ | ||
if (typeof upsert === 'function') upsert = upsert(sel.row) | ||
else { | ||
const buildKey = (relation: Relation.Config) => [relation.table, ...relation.references].join('__') | ||
const tasks: Dict<{ | ||
table: string | ||
upsert: any[] | ||
keys?: string[] | ||
}> = {} | ||
const upsert2 = (upsert as any[]).map(data => { | ||
for (const key in data) { | ||
if (data[key] && this.tables[table].fields[key]?.relation) { | ||
const relation = this.tables[table].fields[key].relation | ||
if (relation.type === 'oneToOne' && relation.required) { | ||
const mergedData = { ...data[key] } | ||
for (const k in relation.fields) { | ||
mergedData[relation.references[k]] = data[relation.fields[k]] | ||
} | ||
;(tasks[buildKey(relation)] ??= { | ||
table: relation.table, | ||
upsert: [], | ||
keys: relation.references, | ||
}).upsert.push(mergedData) | ||
} else if (relation.type === 'oneToMany' && Array.isArray(data[key])) { | ||
const mergedData = data[key].map(row => { | ||
const mergedData = { ...row } | ||
for (const k in relation.fields) { | ||
mergedData[relation.references[k]] = data[relation.fields[k]] | ||
} | ||
return mergedData | ||
}) | ||
;(tasks[relation.table] ??= { table: relation.table, upsert: [] }).upsert.push(...mergedData) | ||
} else { | ||
// handle shadowed fields | ||
data = { | ||
...omit(data, [key]), | ||
...Object.fromEntries(Object.entries(data[key]).map(([k, v]) => { | ||
if (!sel.model.fields[`${key}.${k}`]) throw new Error(`field ${key}.${k} does not exist`) | ||
return [`${key}.${k}`, v] | ||
})), | ||
} | ||
continue | ||
} | ||
data = omit(data, [key]) as any | ||
} | ||
} | ||
return data | ||
}) | ||
if (Object.keys(tasks).length) { | ||
return this.ensureTransaction(async (database) => { | ||
for (const { table, upsert, keys } of Object.values(tasks)) { | ||
await database.upsert(table as any, upsert, keys as any) | ||
} | ||
return database.upsert(table, upsert2) | ||
}) | ||
} | ||
} | ||
upsert = upsert.map(item => sel.model.format(item)) | ||
@@ -765,86 +695,380 @@ keys = makeArray(keys || sel.model.primary) as any | ||
private async processRelationUpdate(table: any, row: any, key: any, modifier: Relation.Modifier) { | ||
const relation: Relation.Config<S> = this.tables[table].fields[key]!.relation! as any | ||
if (Array.isArray(modifier)) { | ||
if (relation.type === 'oneToMany') { | ||
modifier = { $remove: {}, $create: modifier } | ||
} else if (relation.type === 'manyToMany') { | ||
throw new Error('override for manyToMany relation is not supported') | ||
private async createOrUpdate<K extends Keys<S>>(table: K, data: any, upsert: boolean = true): Promise<S[K]> { | ||
const sel = this.select(table) | ||
data = { ...data } | ||
const tasks = [''] | ||
for (const key in data) { | ||
if (data[key] !== undefined && this.tables[table].fields[key]?.relation) { | ||
const relation = this.tables[table].fields[key].relation | ||
if (relation.type === 'oneToOne' && relation.required) tasks.push(key) | ||
else if (relation.type === 'oneToOne') tasks.unshift(key) | ||
else if (relation.type === 'oneToMany') tasks.push(key) | ||
else if (relation.type === 'manyToOne') tasks.unshift(key) | ||
else if (relation.type === 'manyToMany') tasks.push(key) | ||
} | ||
} | ||
if (modifier.$remove) { | ||
if (relation.type === 'oneToMany') { | ||
await this.remove(relation.table, (r: any) => Eval.query(r, { | ||
...Object.fromEntries(relation.references.map((k, i) => [k, row[relation.fields[i]]])), | ||
...(typeof modifier.$remove === 'function' ? { $expr: modifier.$remove(r) } : modifier.$remove), | ||
}) as any) | ||
for (const key of [...tasks]) { | ||
if (!key) { | ||
// create the plain data, with or without upsert | ||
const { primary, autoInc } = sel.model | ||
const keys = makeArray(primary) | ||
if (keys.some(key => isNullable(getCell(data, key)))) { | ||
if (!autoInc) { | ||
throw new Error('missing primary key') | ||
} else { | ||
// nullable relation may pass null here, remove it to enable autoInc | ||
delete data[primary as string] | ||
upsert = false | ||
} | ||
} | ||
if (upsert) { | ||
await sel._action('upsert', [sel.model.format(omit(data, tasks))], keys).execute() | ||
} else { | ||
Object.assign(data, await sel._action('create', sel.model.create(omit(data, tasks))).execute()) | ||
} | ||
continue | ||
} | ||
const value = data[key] | ||
const relation: Relation.Config<S> = this.tables[table].fields[key]!.relation! as any | ||
if (relation.type === 'oneToOne') { | ||
if (value.$literal) { | ||
data[key] = value.$literal | ||
remove(tasks, key) | ||
} else if (value.$create || !isUpdateExpr(value)) { | ||
const result = await this.createOrUpdate(relation.table, { | ||
...Object.fromEntries(relation.references.map((k, i) => [k, getCell(data, relation.fields[i])])), | ||
...value.$create ?? value, | ||
} as any) | ||
if (!relation.required) { | ||
relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(result, k)) | ||
} | ||
} else if (value.$upsert) { | ||
await this.upsert(relation.table, [{ | ||
...Object.fromEntries(relation.references.map((k, i) => [k, getCell(data, relation.fields[i])])), | ||
...value.$upsert, | ||
}]) | ||
if (!relation.required) { | ||
relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(value.$upsert, k)) | ||
} | ||
} else if (value.$connect) { | ||
if (relation.required) { | ||
await this.set(relation.table, | ||
value.$connect, | ||
Object.fromEntries(relation.references.map((k, i) => [k, getCell(data, relation.fields[i])])) as any, | ||
) | ||
} else { | ||
const result = relation.references.every(k => value.$connect![k as any] !== undefined) ? [value.$connect] | ||
: await this.get(relation.table, value.$connect as any) | ||
if (result.length !== 1) throw new Error('related row not found or not unique') | ||
relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(result[0], k)) | ||
} | ||
} | ||
} else if (relation.type === 'manyToOne') { | ||
if (value.$literal) { | ||
data[key] = value.$literal | ||
remove(tasks, key) | ||
} else if (value.$create || !isUpdateExpr(value)) { | ||
const result = await this.createOrUpdate(relation.table, value.$create ?? value) | ||
relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(result, k)) | ||
} else if (value.$upsert) { | ||
await this.upsert(relation.table, [value.$upsert]) | ||
relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(value.$upsert, k)) | ||
} else if (value.$connect) { | ||
const result = relation.references.every(k => value.$connect![k as any] !== undefined) ? [value.$connect] | ||
: await this.get(relation.table, value.$connect as any) | ||
if (result.length !== 1) throw new Error('related row not found or not unique') | ||
relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(result[0], k)) | ||
} | ||
} else if (relation.type === 'oneToMany') { | ||
if (value.$create || Array.isArray(value)) { | ||
for (const item of makeArray(value.$create ?? value)) { | ||
await this.createOrUpdate(relation.table, { | ||
...Object.fromEntries(relation.references.map((k, i) => [k, getCell(data, relation.fields[i])])), | ||
...item, | ||
}) | ||
} | ||
} | ||
if (value.$upsert) { | ||
await this.upsert(relation.table, makeArray(value.$upsert).map(r => ({ | ||
...Object.fromEntries(relation.references.map((k, i) => [k, getCell(data, relation.fields[i])])), | ||
...r, | ||
}))) | ||
} | ||
if (value.$connect) { | ||
await this.set(relation.table, | ||
value.$connect, | ||
Object.fromEntries(relation.references.map((k, i) => [k, getCell(data, relation.fields[i])])) as any, | ||
) | ||
} | ||
} else if (relation.type === 'manyToMany') { | ||
throw new Error('remove for manyToMany relation is not supported') | ||
const assocTable = Relation.buildAssociationTable(relation.table, table) | ||
const fields = relation.fields.map(x => Relation.buildAssociationKey(x, table)) | ||
const references = relation.references.map(x => Relation.buildAssociationKey(x, relation.table)) | ||
const shared = Object.entries(relation.shared).map(([x, y]) => [Relation.buildSharedKey(x, y), { | ||
field: x, | ||
reference: y, | ||
}] as const) | ||
const result: any[] = [] | ||
if (value.$create || Array.isArray(value)) { | ||
for (const item of makeArray(value.$create ?? value)) { | ||
result.push(await this.createOrUpdate(relation.table, { | ||
...Object.fromEntries(shared.map(([, v]) => [v.reference, getCell(item, v.reference) ?? getCell(data, v.field)])), | ||
...item, | ||
})) | ||
} | ||
} | ||
if (value.$upsert) { | ||
const upsert = makeArray(value.$upsert).map(r => ({ | ||
...Object.fromEntries(shared.map(([, v]) => [v.reference, getCell(r, v.reference) ?? getCell(data, v.field)])), | ||
...r, | ||
})) | ||
await this.upsert(relation.table, upsert) | ||
result.push(...upsert) | ||
} | ||
if (value.$connect) { | ||
for (const item of makeArray(value.$connect)) { | ||
if (references.every(k => item[k] !== undefined)) result.push(item) | ||
else result.push(...await this.get(relation.table, item)) | ||
} | ||
} | ||
await this.upsert(assocTable as any, result.map(r => ({ | ||
...Object.fromEntries(shared.map(([k, v]) => [k, getCell(r, v.reference) ?? getCell(data, v.field)])), | ||
...Object.fromEntries(fields.map((k, i) => [k, getCell(data, relation.fields[i])])), | ||
...Object.fromEntries(references.map((k, i) => [k, getCell(r, relation.references[i])])), | ||
} as any))) | ||
} | ||
} | ||
if (modifier.$set) { | ||
if (relation.type === 'oneToMany') { | ||
for (const setexpr of makeArray(modifier.$set) as any[]) { | ||
const [query, update] = setexpr.update ? [setexpr.where, setexpr.update] : [{}, setexpr] | ||
return data | ||
} | ||
private async processRelationUpdate(table: any, row: any, key: any, value: Relation.Modifier) { | ||
const model = this.tables[table], update = Object.create(null) | ||
const relation: Relation.Config<S> = this.tables[table].fields[key]!.relation! as any | ||
if (relation.type === 'oneToOne') { | ||
if (value === null) { | ||
value = relation.required ? { $remove: {} } : { $disconnect: {} } | ||
} | ||
if (typeof value === 'object' && !isUpdateExpr(value)) { | ||
value = { $create: value } | ||
} | ||
if (value.$remove) { | ||
await this.remove(relation.table, Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])) as any) | ||
} | ||
if (value.$disconnect) { | ||
if (relation.required) { | ||
await this.set(relation.table, | ||
(r: any) => Eval.query(r, { | ||
...Object.fromEntries(relation.references.map((k, i) => [k, row[relation.fields[i]]])), | ||
...(typeof query === 'function' ? { $expr: query } : query), | ||
}) as any, | ||
update, | ||
mergeQuery(Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), value.$disconnect), | ||
Object.fromEntries(relation.references.map((k, i) => [k, null])) as any, | ||
) | ||
} else { | ||
Object.assign(update, Object.fromEntries(relation.fields.map((k, i) => [k, null]))) | ||
} | ||
} else if (relation.type === 'manyToMany') { | ||
throw new Error('set for manyToMany relation is not supported') | ||
} | ||
} | ||
if (modifier.$create) { | ||
if (relation.type === 'oneToMany') { | ||
const upsert = makeArray(modifier.$create).map((r: any) => { | ||
const data = { ...r } | ||
for (const k in relation.fields) { | ||
data[relation.references[k]] = row[relation.fields[k]] | ||
} | ||
return data | ||
if (value.$set || typeof value === 'function') { | ||
await this.set( | ||
relation.table, | ||
Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])) as any, | ||
value.$set ?? value as any, | ||
) | ||
} | ||
if (value.$create) { | ||
const result = await this.createOrUpdate(relation.table, { | ||
...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), | ||
...value.$create, | ||
}) | ||
await this.upsert(relation.table, upsert) | ||
} else if (relation.type === 'manyToMany') { | ||
throw new Error('create for manyToMany relation is not supported') | ||
if (!relation.required) { | ||
Object.assign(update, Object.fromEntries(relation.fields.map((k, i) => [k, getCell(result, relation.references[i])]))) | ||
} | ||
} | ||
} | ||
if (modifier.$disconnect) { | ||
if (relation.type === 'oneToMany') { | ||
if (value.$upsert) { | ||
await this.upsert(relation.table, makeArray(value.$upsert).map(r => ({ | ||
...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), | ||
...r, | ||
}))) | ||
if (!relation.required) { | ||
Object.assign(update, Object.fromEntries(relation.fields.map((k, i) => [k, getCell(value.$upsert, relation.references[i])]))) | ||
} | ||
} | ||
if (value.$connect) { | ||
if (relation.required) { | ||
await this.set(relation.table, | ||
value.$connect, | ||
Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])) as any, | ||
) | ||
} else { | ||
const result = await this.get(relation.table, value.$connect as any) | ||
if (result.length !== 1) throw new Error('related row not found or not unique') | ||
Object.assign(update, Object.fromEntries(relation.fields.map((k, i) => [k, getCell(result[0], relation.references[i])]))) | ||
} | ||
} | ||
} else if (relation.type === 'manyToOne') { | ||
if (value === null) { | ||
value = { $disconnect: {} } | ||
} | ||
if (typeof value === 'object' && !isUpdateExpr(value)) { | ||
value = { $create: value } | ||
} | ||
if (value.$remove) { | ||
await this.remove(relation.table, Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])) as any) | ||
} | ||
if (value.$disconnect) { | ||
Object.assign(update, Object.fromEntries(relation.fields.map((k, i) => [k, null]))) | ||
} | ||
if (value.$set || typeof value === 'function') { | ||
await this.set( | ||
relation.table, | ||
Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])) as any, | ||
value.$set ?? value as any, | ||
) | ||
} | ||
if (value.$create) { | ||
const result = await this.createOrUpdate(relation.table, { | ||
...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), | ||
...value.$create, | ||
}) | ||
Object.assign(update, Object.fromEntries(relation.fields.map((k, i) => [k, getCell(result, relation.references[i])]))) | ||
} | ||
if (value.$upsert) { | ||
await this.upsert(relation.table, makeArray(value.$upsert).map(r => ({ | ||
...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), | ||
...r, | ||
}))) | ||
Object.assign(update, Object.fromEntries(relation.fields.map((k, i) => [k, getCell(value.$upsert, relation.references[i])]))) | ||
} | ||
if (value.$connect) { | ||
const result = await this.get(relation.table, value.$connect) | ||
if (result.length !== 1) throw new Error('related row not found or not unique') | ||
Object.assign(update, Object.fromEntries(relation.fields.map((k, i) => [k, getCell(result[0], relation.references[i])]))) | ||
} | ||
} else if (relation.type === 'oneToMany') { | ||
if (Array.isArray(value)) { | ||
const $create: any[] = [], $upsert: any[] = [] | ||
value.forEach(item => this.hasRelation(relation.table, item) ? $create.push(item) : $upsert.push(item)) | ||
value = { $remove: {}, $create, $upsert } | ||
} | ||
if (value.$remove) { | ||
await this.remove(relation.table, mergeQuery(Object.fromEntries(relation.references.map((k, i) => [k, row[relation.fields[i]]])), value.$remove)) | ||
} | ||
if (value.$disconnect) { | ||
await this.set(relation.table, | ||
(r: any) => Eval.query(r, { | ||
...Object.fromEntries(relation.references.map((k, i) => [k, row[relation.fields[i]]])), | ||
...(typeof modifier.$disconnect === 'function' ? { $expr: modifier.$disconnect } : modifier.$disconnect), | ||
} as any), | ||
mergeQuery(Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), value.$disconnect), | ||
Object.fromEntries(relation.references.map((k, i) => [k, null])) as any, | ||
) | ||
} else if (relation.type === 'manyToMany') { | ||
const assocTable = Relation.buildAssociationTable(table, relation.table) as Keys<S> | ||
const fields = relation.fields.map(x => Relation.buildAssociationKey(x, table)) | ||
const references = relation.references.map(x => Relation.buildAssociationKey(x, relation.table)) | ||
} | ||
if (value.$set || typeof value === 'function') { | ||
for (const setexpr of makeArray(value.$set ?? value) as any[]) { | ||
const [query, update] = setexpr.update ? [setexpr.where, setexpr.update] : [{}, setexpr] | ||
await this.set(relation.table, | ||
mergeQuery(Object.fromEntries(relation.references.map((k, i) => [k, row[relation.fields[i]]])), query), | ||
update, | ||
) | ||
} | ||
} | ||
if (value.$create) { | ||
for (const item of makeArray(value.$create)) { | ||
await this.createOrUpdate(relation.table, { | ||
...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), | ||
...item, | ||
}) | ||
} | ||
} | ||
if (value.$upsert) { | ||
await this.upsert(relation.table, makeArray(value.$upsert).map(r => ({ | ||
...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), | ||
...r, | ||
}))) | ||
} | ||
if (value.$connect) { | ||
await this.set(relation.table, | ||
value.$connect, | ||
Object.fromEntries(relation.references.map((k, i) => [k, row[relation.fields[i]]])) as any, | ||
) | ||
} | ||
} else if (relation.type === 'manyToMany') { | ||
const assocTable = Relation.buildAssociationTable(table, relation.table) as Keys<S> | ||
const fields = relation.fields.map(x => Relation.buildAssociationKey(x, table)) | ||
const references = relation.references.map(x => Relation.buildAssociationKey(x, relation.table)) | ||
const shared = Object.entries(relation.shared).map(([x, y]) => [Relation.buildSharedKey(x, y), { | ||
field: x, | ||
reference: y, | ||
}] as const) | ||
if (Array.isArray(value)) { | ||
const $create: any[] = [], $upsert: any[] = [] | ||
value.forEach(item => this.hasRelation(relation.table, item) ? $create.push(item) : $upsert.push(item)) | ||
value = { $disconnect: {}, $create, $upsert } | ||
} | ||
if (value.$remove) { | ||
const rows = await this.select(assocTable, { | ||
...Object.fromEntries(fields.map((k, i) => [k, row[relation.fields[i]]])) as any, | ||
[relation.table]: modifier.$disconnect, | ||
...Object.fromEntries(shared.map(([k, v]) => [k, getCell(row, v.field)])), | ||
...Object.fromEntries(fields.map((k, i) => [k, getCell(row, relation.fields[i])])) as any, | ||
[relation.table]: value.$remove, | ||
}, null).execute() | ||
await this.remove(assocTable, r => Eval.in( | ||
[...fields.map(x => r[x]), ...references.map(x => r[x])], | ||
rows.map(r => [...fields.map(x => r[x]), ...references.map(x => r[x])]), | ||
[...shared.map(([k, v]) => r[k]), ...fields.map(x => r[x]), ...references.map(x => r[x])], | ||
rows.map(r => [...shared.map(([k, v]) => getCell(r, k)), ...fields.map(x => getCell(r, x)), ...references.map(x => getCell(r, x))]), | ||
)) | ||
await this.remove(relation.table, (r) => Eval.in( | ||
[...shared.map(([k, v]) => r[v.reference]), ...relation.references.map(x => r[x])], | ||
rows.map(r => [...shared.map(([k, v]) => getCell(r, k)), ...references.map(x => getCell(r, x))]), | ||
)) | ||
} | ||
} | ||
if (modifier.$connect) { | ||
if (relation.type === 'oneToMany') { | ||
await this.set(relation.table, | ||
modifier.$connect, | ||
Object.fromEntries(relation.references.map((k, i) => [k, row[relation.fields[i]]])) as any, | ||
) | ||
} else if (relation.type === 'manyToMany') { | ||
const assocTable: any = Relation.buildAssociationTable(table, relation.table) | ||
const fields = relation.fields.map(x => Relation.buildAssociationKey(x, table)) | ||
const references = relation.references.map(x => Relation.buildAssociationKey(x, relation.table)) | ||
const rows = await this.get(relation.table, modifier.$connect) | ||
if (value.$disconnect) { | ||
const rows = await this.select(assocTable, { | ||
...Object.fromEntries(shared.map(([k, v]) => [k, getCell(row, v.field)])), | ||
...Object.fromEntries(fields.map((k, i) => [k, getCell(row, relation.fields[i])])) as any, | ||
[relation.table]: value.$disconnect, | ||
}, null).execute() | ||
await this.remove(assocTable, r => Eval.in( | ||
[...shared.map(([k, v]) => r[k]), ...fields.map(x => r[x]), ...references.map(x => r[x])], | ||
rows.map(r => [...shared.map(([k, v]) => getCell(r, k)), ...fields.map(x => getCell(r, x)), ...references.map(x => getCell(r, x))]), | ||
)) | ||
} | ||
if (value.$set) { | ||
for (const setexpr of makeArray(value.$set) as any[]) { | ||
const [query, update] = setexpr.update ? [setexpr.where, setexpr.update] : [{}, setexpr] | ||
const rows = await this.select(assocTable, (r: any) => ({ | ||
...Object.fromEntries(shared.map(([k, v]) => [k, getCell(row, v.field)])), | ||
...Object.fromEntries(fields.map((k, i) => [k, getCell(row, relation.fields[i])])) as any, | ||
[relation.table]: query, | ||
}), null).execute() | ||
await this.set(relation.table, | ||
(r) => Eval.in( | ||
[...shared.map(([k, v]) => r[v.reference]), ...relation.references.map(x => r[x])], | ||
rows.map(r => [...shared.map(([k, v]) => getCell(r, k)), ...references.map(x => getCell(r, x))]), | ||
), | ||
update, | ||
) | ||
} | ||
} | ||
if (value.$create) { | ||
const result: any[] = [] | ||
for (const item of makeArray(value.$create)) { | ||
result.push(await this.createOrUpdate(relation.table, { | ||
...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), | ||
...item, | ||
})) | ||
} | ||
await this.upsert(assocTable, result.map(r => ({ | ||
...Object.fromEntries(shared.map(([k, v]) => [k, getCell(row, v.field)])), | ||
...Object.fromEntries(fields.map((k, i) => [k, row[relation.fields[i]]])), | ||
...Object.fromEntries(references.map((k, i) => [k, r[relation.references[i] as any]])), | ||
})) as any) | ||
} | ||
if (value.$upsert) { | ||
await this.upsert(relation.table, makeArray(value.$upsert).map(r => ({ | ||
...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), | ||
...r, | ||
}))) | ||
await this.upsert(assocTable, makeArray(value.$upsert).map(r => ({ | ||
...Object.fromEntries(shared.map(([k, v]) => [k, getCell(row, v.field)])), | ||
...Object.fromEntries(fields.map((k, i) => [k, row[relation.fields[i]]])), | ||
...Object.fromEntries(references.map((k, i) => [k, r[relation.references[i] as any]])), | ||
})) as any) | ||
} | ||
if (value.$connect) { | ||
const rows = await this.get(relation.table, | ||
mergeQuery(Object.fromEntries(shared.map(([k, v]) => [v.reference, getCell(row, v.field)])), value.$connect)) | ||
await this.upsert(assocTable, rows.map(r => ({ | ||
...Object.fromEntries(shared.map(([k, v]) => [k, getCell(row, v.field)])), | ||
...Object.fromEntries(fields.map((k, i) => [k, row[relation.fields[i]]])), | ||
@@ -855,3 +1079,14 @@ ...Object.fromEntries(references.map((k, i) => [k, r[relation.references[i] as any]])), | ||
} | ||
if (Object.keys(update).length) { | ||
await this.set(table, pick(model.format(row), makeArray(model.primary)), update) | ||
} | ||
} | ||
private hasRelation<K extends Keys<S>>(table: K, data: Create<S[K], S>): boolean | ||
private hasRelation(table: any, data: any) { | ||
for (const key in data) { | ||
if (data[key] !== undefined && this.tables[table].fields[key]?.relation) return true | ||
} | ||
return false | ||
} | ||
} |
import { Awaitable, Dict, mapValues, remove } from 'cosmokit' | ||
import { Context, Logger } from 'cordis' | ||
import { Context, Logger, Service } 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, Keys, Values } from './utils.ts' | ||
@@ -20,9 +21,10 @@ export namespace Driver { | ||
export type Cursor<K extends string = never> = K[] | CursorOptions<K> | ||
export type Cursor<K extends string = string, S = any, T extends Keys<S> = any> = K[] | CursorOptions<K, S, T> | ||
export interface CursorOptions<K extends string> { | ||
export interface CursorOptions<K extends string = string, S = any, T extends Keys<S> = any> { | ||
limit?: number | ||
offset?: number | ||
fields?: K[] | ||
sort?: Dict<Direction, K> | ||
sort?: Partial<Dict<Direction, FlatKeys<S[T]>>> | ||
include?: Relation.Include<S[T], Values<S>> | ||
} | ||
@@ -80,2 +82,6 @@ | ||
database._driver = this | ||
database[Service.tracker] = { | ||
associate: 'database', | ||
property: 'ctx', | ||
} | ||
ctx.set('database', Context.associate(database, 'database')) | ||
@@ -82,0 +88,0 @@ }) |
@@ -11,2 +11,4 @@ import { defineProperty, isNullable, mapValues } from 'cosmokit' | ||
export const isUpdateExpr: (value: any) => boolean = isEvalExpr | ||
export function isAggrExpr(expr: Eval.Expr): boolean { | ||
@@ -35,3 +37,3 @@ return expr['$'] || expr['$select'] | ||
: 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 | ||
@@ -80,3 +82,3 @@ | ||
select(...args: Any[]): Expr<any[], false> | ||
query<T extends object>(row: Row<T>, query: Query.Expr<T>): Expr<boolean, false> | ||
query<T extends object>(row: Row<T>, query: Query.Expr<T>, expr?: Term<boolean>): Expr<boolean, false> | ||
@@ -198,3 +200,3 @@ // univeral | ||
Eval.select = multary('select', (args, table) => args.map(arg => executeEval(table, arg)), Type.Array()) | ||
Eval.query = (row, query) => ({ $expr: true, ...query }) as any | ||
Eval.query = (row, query, expr = true) => ({ $expr: expr, ...query }) as any | ||
@@ -201,0 +203,0 @@ // univeral |
@@ -9,2 +9,3 @@ import { clone, filterKeys, isNullable, makeArray, mapValues, MaybeArray } from 'cosmokit' | ||
import { Selection } from './selection.ts' | ||
import { Create } from './database.ts' | ||
@@ -26,2 +27,3 @@ const Primary = Symbol('minato.primary') | ||
fields: K[] | ||
shared: Record<K, Keys<S[T]>> | ||
required: boolean | ||
@@ -36,9 +38,10 @@ } | ||
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> | ||
@@ -48,20 +51,29 @@ 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))) | ||
@@ -73,2 +85,3 @@ ? makeArray(relmodel.primary).map(x => `${key}.${x}`) : model.primary) | ||
fields: makeArray(fields), | ||
shared: shared as any, | ||
references: makeArray(def.references ?? relmodel.primary), | ||
@@ -78,2 +91,7 @@ required: def.type !== 'manyToOne' && model.name !== relmodel.name | ||
} | ||
// 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 = { | ||
@@ -86,5 +104,7 @@ type: relation.type === 'oneToMany' ? 'manyToOne' | ||
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] | ||
@@ -168,3 +188,3 @@ } | ||
| Transform<O[K], any, N> | ||
| (O[K] extends object ? Relation.Definition<FlatKeys<O>> : never) | ||
| (O[K] extends object | undefined ? Relation.Definition<FlatKeys<O>> : never) | ||
} | ||
@@ -226,3 +246,3 @@ | ||
export function available(field?: Field) { | ||
return !!field && !field.deprecated && !field.relation | ||
return !!field && !field.deprecated && !field.relation && field.deftype !== 'expr' | ||
} | ||
@@ -248,3 +268,3 @@ } | ||
export class Model<S = any> { | ||
ctx?: Context | ||
declare ctx?: Context | ||
fields: Field.Config<S> = {} | ||
@@ -251,0 +271,0 @@ migrations = new Map<Model.Migration, string[]>() |
@@ -1,2 +0,2 @@ | ||
import { defineProperty, Dict, filterKeys } from 'cosmokit' | ||
import { defineProperty, Dict, filterKeys, mapValues } from 'cosmokit' | ||
import { Driver } from './driver.ts' | ||
@@ -6,3 +6,3 @@ import { Eval, executeEval, isAggrExpr, isEvalExpr } from './eval.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' | ||
@@ -65,3 +65,3 @@ | ||
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) | ||
@@ -258,11 +258,16 @@ } else { | ||
.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 | ||
} | ||
@@ -269,0 +274,0 @@ } |
@@ -131,5 +131,10 @@ import { Binary, Intersect, isNullable } from 'cosmokit' | ||
export function getCell(row: any, key: any): any { | ||
if (key in row) return row[key] | ||
return key.split('.').reduce((r, k) => r === undefined ? undefined : r[k], row) | ||
export function getCell(row: any, path: any): any { | ||
if (path in row) return row[path] | ||
if (path.includes('.')) { | ||
const index = path.indexOf('.') | ||
return getCell(row[path.slice(0, index)] ?? {}, path.slice(index + 1)) | ||
} else { | ||
return row[path] | ||
} | ||
} | ||
@@ -136,0 +141,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
474709
7441