Skip to content

Commit

Permalink
Merge branch 'master' into feat-project-row
Browse files Browse the repository at this point in the history
  • Loading branch information
Hieuzest committed Sep 26, 2024
2 parents 4df9639 + b2d7cd6 commit 1ea37e3
Show file tree
Hide file tree
Showing 27 changed files with 314 additions and 102 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"@types/mocha": "^9.1.1",
"@types/node": "^22.1.0",
"c8": "^7.14.0",
"esbuild": "^0.23.0",
"esbuild": "^0.23.1",
"esbuild-register": "^3.5.0",
"eslint": "^8.57.0",
"eslint-plugin-mocha": "^10.4.1",
Expand Down
10 changes: 5 additions & 5 deletions packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "minato",
"version": "3.5.0",
"version": "3.6.0",
"description": "Type Driven Database Framework",
"type": "module",
"main": "lib/index.cjs",
Expand Down Expand Up @@ -44,14 +44,14 @@
"sqlite",
"mongo",
"postgres",
"cordis",
"plugin"
"cordis"
],
"cordis": {
"ecosystem": {
"pattern": [
"@minatojs/driver-*",
"@minatojs/plugin-*",
"minato-driver-*",
"minato-plugin-*"
]
},
Expand All @@ -62,10 +62,10 @@
}
},
"devDependencies": {
"cordis": "^3.18.0"
"cordis": "^3.18.1"
},
"peerDependencies": {
"cordis": "^3.18.0"
"cordis": "^3.18.1"
},
"dependencies": {
"cosmokit": "^1.6.2"
Expand Down
43 changes: 26 additions & 17 deletions packages/core/src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export class Database<S = {}, N = {}, C extends Context = Context> extends Servi
}

private getDriver(table: string | Selection): Driver<any, C> {
if (table instanceof Selection) return table.driver as any
if (Selection.is(table)) return table.driver as any
const model: Model = this.tables[table]
if (!model) throw new Error(`cannot resolve table "${table}"`)
return model.ctx?.get('database')?._driver as any
Expand Down Expand Up @@ -150,15 +150,16 @@ export class Database<S = {}, N = {}, C extends Context = Context> extends Servi
if (!Relation.Type.includes(def.type)) return
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`)
const relmodel = this.tables[relation.table]
if (!relmodel) throw new Error(`relation table ${relation.table} does not exist`)
;(model.fields[key] = Field.parse('expr')).relation = relation
if (def.target) {
(this.tables[relation.table].fields[def.target] ??= Field.parse('expr')).relation = inverse
(relmodel.fields[def.target] ??= Field.parse('expr')).relation = inverse
}

if (relation.type === 'oneToOne' || relation.type === 'manyToOne') {
relation.fields.forEach((x, i) => {
model.fields[x] ??= { ...this.tables[relation.table].fields[relation.references[i]] } as any
model.fields[x] ??= { ...relmodel.fields[relation.references[i]] } as any
if (!relation.required) {
model.fields[x]!.nullable = true
model.fields[x]!.initial = null
Expand All @@ -169,7 +170,7 @@ export class Database<S = {}, N = {}, C extends Context = Context> extends Servi
if (this.tables[assocTable]) return
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)
const references = relation.references.map(x => [Relation.buildAssociationKey(x, relation.table), relmodel.fields[x]?.deftype] as const)
this.extend(assocTable as any, {
...Object.fromEntries([...shared, ...fields, ...references]),
[name]: {
Expand All @@ -190,8 +191,8 @@ export class Database<S = {}, N = {}, C extends Context = Context> extends Servi
}
})
// use relation field as primary
if (Array.isArray(model.primary)) {
model.primary = deduplicate(model.primary.map(key => model.fields[key]!.relation?.fields || key).flat())
if (Array.isArray(model.primary) || model.fields[model.primary]!.relation) {
model.primary = deduplicate(makeArray(model.primary).map(key => model.fields[key]!.relation?.fields || key).flat())
}
model.unique = model.unique.map(keys => typeof keys === 'string' ? model.fields[keys]!.relation?.fields || keys
: keys.map(key => model.fields[key]!.relation?.fields || key).flat())
Expand All @@ -203,11 +204,11 @@ export class Database<S = {}, N = {}, C extends Context = Context> extends Servi
private _parseField(field: any, transformers: Driver.Transformer[] = [], setInitial?: (value) => void, setField?: (value) => void): Type {
if (field === 'object') {
setInitial?.({})
setField?.({ type: 'json', initial: {} })
setField?.({ initial: {}, deftype: 'json', type: Type.Object() })
return Type.Object()
} else if (field === 'array') {
setInitial?.([])
setField?.({ type: 'json', initial: [] })
setField?.({ initial: [], deftype: 'json', type: Type.Array() })
return Type.Array()
} else if (typeof field === 'string' && this.types[field]) {
transformers.push({
Expand Down Expand Up @@ -316,7 +317,7 @@ export class Database<S = {}, N = {}, C extends Context = Context> extends Servi
select(table: any, query?: any, include?: any) {
let sel = new Selection(this.getDriver(table), table, query)
if (typeof table !== 'string') return sel
const whereOnly = include === null
const whereOnly = include === null, isAssoc = !!include?.$assoc
const rawquery = typeof query === 'function' ? query : () => query
const modelFields = this.tables[table].fields
if (include) include = filterKeys(include, (key) => !!modelFields[key]?.relation)
Expand Down Expand Up @@ -363,13 +364,20 @@ export class Database<S = {}, N = {}, C extends Context = Context> extends Servi
for (const key in include) {
if (!include[key] || !modelFields[key]?.relation) continue
const relation: Relation.Config<S> = modelFields[key]!.relation as any
const relmodel = this.tables[relation.table]
if (relation.type === 'oneToOne' || relation.type === 'manyToOne') {
sel = whereOnly ? sel : sel.join(key, this.select(relation.table, {}, include[key]), (self, other) => Eval.and(
sel = whereOnly ? sel : sel.join(key, this.select(relation.table,
typeof include[key] === 'object' ? filterKeys(include[key], (k) => !relmodel.fields[k]?.relation) : {} as any,
typeof include[key] === 'object' ? filterKeys(include[key], (k) => !!relmodel.fields[k]?.relation) : include[key],
), (self, other) => Eval.and(
...relation.fields.map((k, i) => Eval.eq(self[k], other[relation.references[i]])),
), true)
), !isAssoc)
sel = applyQuery(sel, key)
} else if (relation.type === 'oneToMany') {
sel = whereOnly ? sel : sel.join(key, this.select(relation.table, {}, include[key]), (self, other) => Eval.and(
sel = whereOnly ? sel : sel.join(key, this.select(relation.table,
typeof include[key] === 'object' ? filterKeys(include[key], (k) => !relmodel.fields[k]?.relation) : {} as any,
typeof include[key] === 'object' ? filterKeys(include[key], (k) => !!relmodel.fields[k]?.relation) : include[key],
), (self, other) => Eval.and(
...relation.fields.map((k, i) => Eval.eq(self[k], other[relation.references[i]])),
), true)
sel = applyQuery(sel, key)
Expand All @@ -386,10 +394,11 @@ export class Database<S = {}, N = {}, C extends Context = Context> extends Servi
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]])),
), true)
sel = whereOnly ? sel : sel.join(key, this.select(assocTable, {}, { $assoc: true, [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]])),
), true)
sel = applyQuery(sel, key)
sel = whereOnly ? sel : sel.groupBy([
...Object.entries(modelFields).filter(([k, field]) => !extraFields.some(x => k.startsWith(`${x}.`)) && Field.available(field)).map(([k]) => k),
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ export abstract class Driver<T = any, C extends Context = Context> {
throw new TypeError(`unknown table name "${table}"`)
}

if (table instanceof Selection) {
if (!table.args[0].fields && (typeof table.table === 'string' || table.table instanceof Selection)) {
if (Selection.is(table)) {
if (!table.args[0].fields && (typeof table.table === 'string' || Selection.is(table.table))) {
return table.model
}
const model = new Model('temp')
Expand Down
9 changes: 7 additions & 2 deletions packages/core/src/eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ export namespace Eval {
object<T extends any>(row: Row.Cell<T>): Expr<T, false>
object<T extends any>(row: Row<T>): Expr<T, false>
array<T>(value: Expr<T, false>): Expr<T[], true>

get<T extends object, K extends keyof T, A extends boolean>(x: Term<T, A>, key: K): Expr<T[K], A>
get<T extends any, A extends boolean>(x: Array<T, A>, index: Term<number, A>): Expr<T, A>
}
}

Expand Down Expand Up @@ -192,9 +195,11 @@ operators.$switch = (args, data) => {
return executeEval(data, args.default)
}

Eval.ignoreNull = (expr) => (expr[Type.kType]!.ignoreNull = true, expr)
// special forms
Eval.ignoreNull = (expr) => (expr['$ignoreNull'] = true, 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) => ({ $expr: expr, ...query }) as any
Eval.exec = unary('exec', (expr, data) => (expr.driver as any).executeSelection(expr, data), (expr) => Type.fromTerm(expr.args[0]))

// univeral
Eval.if = multary('if', ([cond, vThen, vElse], data) => executeEval(data, cond) ? executeEval(data, vThen)
Expand Down Expand Up @@ -329,7 +334,7 @@ Eval.array = unary('array', (expr, table) => Array.isArray(table)
? 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]))
Eval.get = multary('get', ([x, key], data) => executeEval(data, x)?.[executeEval(data, key)], (x, key) => Type.getInner(Type.fromTerm(x), key) ?? Type.Any)

export { Eval as $ }

Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { clone, deepEqual, filterKeys, isNullable, makeArray, mapValues, MaybeArray } from 'cosmokit'
import { clone, deepEqual, defineProperty, filterKeys, isNullable, makeArray, mapValues, MaybeArray } from 'cosmokit'
import { Context } from 'cordis'
import { Eval, Update } from './eval.ts'
import { DeepPartial, FlatKeys, Flatten, isFlat, Keys, Row, unravel } from './utils.ts'
Expand Down Expand Up @@ -37,7 +37,7 @@ export namespace Relation {
}

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

export type SetExpr<S extends object = any> = ((row: Row<S>) => Update<S>) | {
Expand Down Expand Up @@ -262,7 +262,7 @@ export class Model<S = any> {
fields: Field.Config<S> = {}
migrations = new Map<Model.Migration, string[]>()

private type: Type<S> | undefined
declare private type: Type<S> | undefined

constructor(public name: string) {
this.autoInc = false
Expand Down Expand Up @@ -442,7 +442,7 @@ export class Model<S = any> {
getType(): Type<S>
getType(key: string): Type | undefined
getType(key?: string): Type | undefined {
this.type ??= Type.Object(mapValues(this.fields!, field => Type.fromField(field!))) as any
if (!this.type) defineProperty(this, 'type', Type.Object(mapValues(this.fields, field => Type.fromField(field!))) as any)
return key ? Type.getInner(this.type, key) : this.type
}
}
22 changes: 20 additions & 2 deletions packages/core/src/selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,27 @@ namespace Executable {
}
}

const createRow = (ref: string, expr = {}, prefix = '', model?: Model) => new Proxy(expr, {
const createRow = (ref: string, expr = {}, prefix = '', model?: Model, intermediate?: Eval.Expr) => new Proxy(expr, {
get(target, key) {
if (key === '$prefix') return prefix
if (key === '$model') return model
if (typeof key === 'symbol' || key in target || key.startsWith('$')) return Reflect.get(target, key)

if (intermediate) {
if (Type.isArray(expr?.[Type.kType]) && Number.isInteger(+key)) {
return createRow(ref, Eval.get(expr as any, +key), '', model, Eval.get(expr as any, +key))
} else {
return createRow(ref, Eval.get(intermediate as any, `${prefix}${key}`), `${prefix}${key}.`, model, intermediate)
}
}

let type: Type
const field = model?.fields[prefix + key as string]
if (Type.getInner(expr?.[Type.kType], key)) {
if (Type.isArray(expr?.[Type.kType]) && Number.isInteger(+key)) {
// indexing array
type = Type.getInner(expr?.[Type.kType]) ?? Type.fromField('expr')
return createRow(ref, Eval.get(expr as any, +key), '', model, Eval.get(expr as any, +key))
} else if (Type.getInner(expr?.[Type.kType], key)) {
// type may conatins object layout
type = Type.getInner(expr?.[Type.kType], key)!
} else if (field) {
Expand Down Expand Up @@ -346,6 +358,12 @@ export class Selection<S = any> extends Executable<S, S[]> {
}
}

export namespace Selection {
export function is(sel: any): sel is Selection {
return sel && !!sel.tables as any
}
}

export function executeSort(data: any[], modifier: Modifier, name: string) {
const { limit, offset, sort } = modifier

Expand Down
19 changes: 10 additions & 9 deletions packages/core/src/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ export interface Type<T = any, N = any> {
export namespace Type {
export const kType = Symbol.for('minato.type')

export const Boolean: Type<boolean> = defineProperty({ type: 'boolean' }, kType, true) as any
export const Number: Type<number> = defineProperty({ type: 'double' }, kType, true)
export const String: Type<string> = defineProperty({ type: 'string' }, kType, true)
export const Any: Type = fromField('expr')
export const Boolean: Type<boolean> = fromField('boolean')
export const Number: Type<number> = fromField('double')
export const String: Type<string> = fromField('string')

type Extract<T> =
| T extends Type<infer I> ? I
Expand Down Expand Up @@ -76,20 +77,20 @@ export namespace Type {
return value?.[kType] === true
}

export function isArray(type: Type) {
return (type.type === 'json') && type.array
export function isArray(type?: Type) {
return (type?.type === 'json') && type?.array
}

export function getInner(type?: Type, key?: string): Type | undefined {
if (!type?.inner) return
if (isArray(type) && isNullable(key)) return type.inner
if (isArray(type)) return type.inner
if (isNullable(key)) return
if (type.inner[key]) return type.inner[key]
if (key.includes('.')) return key.split('.').reduce((t, k) => getInner(t, k), type)
return Object(globalThis.Object.fromEntries(globalThis.Object.entries(type.inner)
const fields = globalThis.Object.entries(type.inner)
.filter(([k]) => k.startsWith(`${key}.`))
.map(([k, v]) => [k.slice(key.length + 1), v]),
))
.map(([k, v]) => [k.slice(key.length + 1), v])
return fields.length ? Object(globalThis.Object.fromEntries(fields)) : undefined
}

export function transform(value: any, type: Type, callback: (value: any, type?: Type) => any) {
Expand Down
10 changes: 5 additions & 5 deletions packages/memory/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@minatojs/driver-memory",
"version": "3.5.0",
"version": "3.6.0",
"description": "In-memory Driver for Minato",
"type": "module",
"main": "lib/index.cjs",
Expand Down Expand Up @@ -37,12 +37,12 @@
"memory"
],
"peerDependencies": {
"minato": "^3.5.0"
"minato": "^3.6.0"
},
"devDependencies": {
"@minatojs/tests": "^2.5.0",
"cordis": "^3.18.0",
"minato": "^3.5.0"
"@minatojs/tests": "^2.6.0",
"cordis": "^3.18.1",
"minato": "^3.6.0"
},
"dependencies": {
"cosmokit": "^1.6.2"
Expand Down
4 changes: 2 additions & 2 deletions packages/memory/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export class MemoryDriver extends Driver<MemoryDriver.Config> {
return this._store[sel] ||= []
}

if (!(sel instanceof Selection)) {
if (!Selection.is(sel)) {
throw new Error('Should not reach here')
}

Expand All @@ -38,7 +38,7 @@ export class MemoryDriver extends Driver<MemoryDriver.Config> {

let data: any[]

if (typeof table === 'object' && !(table instanceof Selection)) {
if (typeof table === 'object' && !Selection.is(table)) {
const entries = Object.entries(table).map(([name, sel]) => [name, this.table(sel, env)] as const)
const catesian = (entries: (readonly [string, any[]])[]): any[] => {
if (!entries.length) return []
Expand Down
6 changes: 6 additions & 0 deletions packages/memory/tests/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,18 @@ describe('@minatojs/driver-memory', () => {
update: {
index: false,
},
json: {
query: {
nullableComparator: false,
},
},
model: {
fields: {
cast: false,
typeModel: false,
},
object: {
nullableComparator: false,
typeModel: false,
},
},
Expand Down
Loading

0 comments on commit 1ea37e3

Please sign in to comment.