Skip to content

Commit

Permalink
Merge branch 'master' into feat-get
Browse files Browse the repository at this point in the history
  • Loading branch information
Hieuzest authored Sep 16, 2024
2 parents 21e80ea + c71ad3e commit a4dfb6f
Show file tree
Hide file tree
Showing 12 changed files with 102 additions and 35 deletions.
15 changes: 8 additions & 7 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 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
7 changes: 6 additions & 1 deletion packages/core/src/eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,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 @@ -330,10 +332,13 @@ 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)))

<<<<<<< feat-get
Eval.get = multary('get', ([x, key], data) => executeEval(data, x)?.[executeEval(data, key)], (x, key) => Type.getInner(Type.fromTerm(x), key) ?? Type.Any)

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

=======
>>>>>>> master
export { Eval as $ }

export type Update<T = any> = UnevalObject<Flatten<T>>
Expand Down
6 changes: 3 additions & 3 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 @@ -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
}
}
6 changes: 6 additions & 0 deletions packages/core/src/selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,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
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
25 changes: 13 additions & 12 deletions packages/mongo/src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,21 @@ import { Eval, Field, flatten, isAggrExpr, isComparable, isEvalExpr, isFlat, mak
import { Filter, FilterOperators, ObjectId } from 'mongodb'
import MongoDriver from '.'

function createFieldFilter(query: Query.Field, key: string) {
function createFieldFilter(query: Query.Field, key: string, type?: Type) {
const filters: Filter<any>[] = []
const result: Filter<any> = {}
const child = transformFieldQuery(query, key, filters)
const child = transformFieldQuery(query, key, filters, type)
if (child === false) return false
if (child !== true) result[key] = child
if (filters.length) result.$and = filters
if (Object.keys(result).length) return result
return true
}

function transformFieldQuery(query: Query.Field, key: string, filters: Filter<any>[]) {
function transformFieldQuery(query: Query.Field, key: string, filters: Filter<any>[], type?: Type) {
// shorthand syntax
if (isComparable(query) || query instanceof ObjectId) {
if (type?.type === 'primary' && typeof query === 'string') query = new ObjectId(query)
return { $eq: query }
} else if (Array.isArray(query)) {
if (!query.length) return false
Expand All @@ -32,21 +33,21 @@ function transformFieldQuery(query: Query.Field, key: string, filters: Filter<an
for (const prop in query) {
if (prop === '$and') {
for (const item of query[prop]!) {
const child = createFieldFilter(item, key)
const child = createFieldFilter(item, key, type)
if (child === false) return false
if (child !== true) filters.push(child)
}
} else if (prop === '$or') {
const $or: Filter<any>[] = []
if (!query[prop]!.length) return false
const always = query[prop]!.some((item) => {
const child = createFieldFilter(item, key)
const child = createFieldFilter(item, key, type)
if (typeof child === 'boolean') return child
$or.push(child)
})
if (!always) filters.push({ $or })
} else if (prop === '$not') {
const child = createFieldFilter(query[prop], key)
const child = createFieldFilter(query[prop], key, type)
if (child === true) return false
if (child !== false) filters.push({ $nor: [child] })
} else if (prop === '$el') {
Expand Down Expand Up @@ -396,7 +397,7 @@ export class Builder {
return this.transformEvalExpr(expr, group)
}

public query(query: Query.Expr) {
public query(sel: Selection.Immutable, query: Query.Expr) {
const filter: Filter<any> = {}
const additional: Filter<any>[] = []
for (const key in query) {
Expand All @@ -406,15 +407,15 @@ export class Builder {
// { $and: [] } matches everything
// { $or: [] } matches nothing
if (value.length) {
filter[key] = value.map(query => this.query(query))
filter[key] = value.map(query => this.query(sel, query))
} else if (key === '$or') {
return
}
} else if (key === '$not') {
// MongoError: unknown top level operator: $not
// https://stackoverflow.com/questions/25270396/mongodb-how-to-invert-query-with-not
// this may solve this problem but lead to performance degradation
const query = this.query(value)
const query = this.query(sel, value)
if (query) filter.$nor = [query]
} else if (key === '$expr') {
additional.push({ $expr: this.eval(value) })
Expand All @@ -423,7 +424,7 @@ export class Builder {
const flattenQuery = ignore(value) ? { [key]: value } : flatten(value, `${key}.`, ignore)
for (const key in flattenQuery) {
const value = flattenQuery[key], actualKey = this.getActualKey(key)
const query = transformFieldQuery(value, actualKey, additional)
const query = transformFieldQuery(value, actualKey, additional, sel.model.fields[key]?.type)
if (query === false) return
if (query !== true) filter[actualKey] = query
}
Expand Down Expand Up @@ -511,7 +512,7 @@ export class Builder {
if (typeof table === 'string') {
this.table = table
this.refVirtualKeys[sel.ref] = this.virtualKey = (sel.driver as MongoDriver).getVirtualKey(table)!
} else if (table instanceof Selection) {
} else if (Selection.is(table)) {
const predecessor = this.createSubquery(table)
if (!predecessor) return
this.table = predecessor.table
Expand Down Expand Up @@ -558,7 +559,7 @@ export class Builder {
}

// where
const filter = this.query(query)
const filter = this.query(sel, query)
if (!filter) return
if (Object.keys(filter).length) {
this.pipeline.push(...this.flushLookups(), { $match: filter })
Expand Down
10 changes: 8 additions & 2 deletions packages/mongo/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BSONType, ClientSession, Collection, Db, IndexDescription, Long, MongoClient, MongoClientOptions, MongoError } from 'mongodb'
import { BSONType, ClientSession, Collection, Db, IndexDescription, Long, MongoClient, MongoClientOptions, MongoError, ObjectId } from 'mongodb'
import { Binary, Dict, isNullable, makeArray, mapValues, noop, omit, pick } from 'cosmokit'
import { Driver, Eval, executeUpdate, Field, hasSubquery, Query, RuntimeError, Selection, z } from 'minato'
import { URLSearchParams } from 'url'
Expand Down Expand Up @@ -72,6 +72,12 @@ export class MongoDriver extends Driver<MongoDriver.Config> {
dump: value => isNullable(value) ? value : value as any,
load: value => isNullable(value) ? value : BigInt(value as any),
})

this.define<ObjectId | string, ObjectId>({
types: ['primary' as any],
dump: value => typeof value === 'string' ? new ObjectId(value) : value,
load: value => value,
})
}

stop() {
Expand Down Expand Up @@ -310,7 +316,7 @@ export class MongoDriver extends Driver<MongoDriver.Config> {
}

private transformQuery(sel: Selection.Immutable, query: Query.Expr, table: string) {
return new Builder(this, Object.keys(sel.tables), this.getVirtualKey(table)).query(query)
return new Builder(this, Object.keys(sel.tables), this.getVirtualKey(table)).query(sel, query)
}

async get(sel: Selection.Immutable) {
Expand Down
6 changes: 6 additions & 0 deletions packages/mongo/tests/migration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ describe('@minatojs/driver-mongo/migrate-virtualKey', () => {
foreign: 'primary',
})

await database.remove('temp2', {})

const table: Bar[] = []
table.push(await database.create('temp2', {
text: 'awesome foo',
Expand All @@ -138,6 +140,10 @@ describe('@minatojs/driver-mongo/migrate-virtualKey', () => {
table.push(await database.create('temp2', { text: 'awesome baz' }))
await expect(database.get('temp2', {})).to.eventually.deep.eq(table)

await expect(database.get('temp2', table[0].id?.toString() as any)).to.eventually.deep.eq([table[0]])
await expect(database.get('temp2', { id: table[0].id?.toString() as any })).to.eventually.deep.eq([table[0]])
await expect(database.get('temp2', row => $.eq(row.id, $.literal(table[0].id?.toString(), 'primary') as any))).to.eventually.deep.eq([table[0]])

await (Object.values(database.drivers)[0] as Driver).drop('_fields')
await resetConfig(true)
await expect(database.get('temp2', {})).to.eventually.deep.eq(table)
Expand Down
2 changes: 1 addition & 1 deletion packages/mysql/src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class MySQLBuilder extends Builder {
if (compat.maria || compat.mysql57) {
return this.asEncoded(`json_object(${args.map(arg => this.parseEval(arg, false)).flatMap((x, i) => [`${i}`, x]).join(', ')})`, true)
} else {
return `${args.map(arg => this.parseEval(arg, false)).join(', ')}`
return `${args.map(arg => this.parseEval(arg)).join(', ')}`
}
}

Expand Down
6 changes: 3 additions & 3 deletions packages/sql-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export class Builder {
this.evalOperators = {
// universal
$: (key) => this.getRecursive(key),
$select: (args) => `${args.map(arg => this.parseEval(arg, false)).join(', ')}`,
$select: (args) => `${args.map(arg => this.parseEval(arg)).join(', ')}`,
$if: (args) => `if(${args.map(arg => this.parseEval(arg)).join(', ')})`,
$ifNull: (args) => `ifnull(${args.map(arg => this.parseEval(arg)).join(', ')})`,

Expand Down Expand Up @@ -511,7 +511,7 @@ export class Builder {
let prefix: string | undefined
if (typeof table === 'string') {
prefix = this.escapeId(table)
} else if (table instanceof Selection) {
} else if (Selection.is(table)) {
prefix = this.get(table, true)
if (!prefix) return
} else {
Expand Down Expand Up @@ -558,7 +558,7 @@ export class Builder {
suffix = ` WHERE ${filter}` + suffix
}

if (inline && !args[0].fields && !suffix && (typeof table === 'string' || table instanceof Selection)) {
if (inline && !args[0].fields && !suffix && (typeof table === 'string' || Selection.is(table))) {
return (addref && isBracketed(prefix)) ? `${prefix} ${ref}` : prefix
}

Expand Down
Loading

0 comments on commit a4dfb6f

Please sign in to comment.