Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): impl FieldCallback, support spreading Row & Cell #110

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions packages/core/src/eval.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineProperty, isNullable, mapValues } from 'cosmokit'
import { defineProperty, filterKeys, isNullable, mapValues } from 'cosmokit'
import { AtomicTypes, Comparable, Flatten, isComparable, isEmpty, makeRegExp, Row, Values } from './utils.ts'
import { Type } from './type.ts'
import { Field, Relation } from './model.ts'
Expand Down Expand Up @@ -323,9 +323,11 @@ Eval.object = (fields: any) => {
.filter(([, field]) => Field.available(field))
.filter(([path]) => path.startsWith(prefix))
.map(([k]) => [k.slice(prefix.length), fields[k.slice(prefix.length)]]))
return Eval('object', fields, Type.Object(mapValues(fields, (value) => Type.fromTerm(value))))
return Object.assign(Eval('object', fields, Type.Object(mapValues(fields, (value) => Type.fromTerm(value)))), fields)
}
return Eval('object', fields, Type.Object(mapValues(fields, (value) => Type.fromTerm(value)))) as any
// filter out nested spread $object
fields = filterKeys(fields, key => key !== '$object')
return Object.assign(Eval('object', fields, Type.Object(mapValues(fields, (value) => Type.fromTerm(value)))), fields)
}

Eval.array = unary('array', (expr, table) => Array.isArray(table)
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/model.ts
Original file line number Diff line number Diff line change
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> : Query.Expr<Flatten<U>> : never
[P in keyof T]?: T[P] extends MaybeArray<infer U> | undefined ? U extends S ? Include<U, S> : (U extends (infer I)[] ? Query.Expr<I> : never) : never
}

export type SetExpr<S extends object = any> = ((row: Row<S>) => Update<S>) | {
Expand Down
41 changes: 35 additions & 6 deletions packages/core/src/selection.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineProperty, Dict, filterKeys, mapValues } from 'cosmokit'
import { defineProperty, Dict, filterKeys, mapValues, omit } from 'cosmokit'
import { Driver } from './driver.ts'
import { Eval, executeEval, isAggrExpr, isEvalExpr } from './eval.ts'
import { Field, Model } from './model.ts'
Expand Down Expand Up @@ -92,7 +92,13 @@ class Executable<S = any, T = any> {
Object.assign(this, payload)
defineProperty(this, 'driver', driver)
defineProperty(this, 'model', driver.model(this.table))
defineProperty(this, 'row', createRow(this.ref, {}, '', this.model))
defineProperty(this, 'row', createRow(this.ref, Eval.object(createRow(this.ref, {}, '', this.model)), '', this.model))
}

protected isCallaback(query: any): query is Selection.Callback<S> {
if (typeof query !== 'function') return false
const fields = query(this.row)
return isEvalExpr(omit(fields, ['$object']))
}

protected resolveQuery(query?: Query<S>): Query.Expr<S>
Expand All @@ -111,17 +117,20 @@ class Executable<S = any, T = any> {
return query
}

protected resolveField(field: FieldLike<S>): Eval.Expr {
protected resolveField(field: FieldLike<S> | Eval.Expr): Eval.Expr {
if (typeof field === 'string') {
return this.row[field]
} else if (typeof field === 'function') {
return field(this.row)
} else if (isEvalExpr(field)) {
return field
} else {
throw new TypeError('invalid field definition')
}
}

protected resolveFields(fields: string | string[] | Dict<FieldLike<S>>) {
protected resolveFields(fields: string | string[] | Dict<FieldLike<S>> | FieldCallback) {
if (typeof fields === 'function') fields = fields(this.row)
if (typeof fields === 'string') fields = [fields]
if (Array.isArray(fields)) {
const modelFields = Object.keys(this.model.fields)
Expand All @@ -135,6 +144,7 @@ class Executable<S = any, T = any> {
return Object.fromEntries(entries)
} else {
const entries = Object.entries(fields).flatMap(([key, field]) => {
if (key.startsWith('$')) return []
const expr = this.resolveField(field)
if (expr['$object'] && !Type.fromTerm(expr).ignoreNull) {
return Object.entries(expr['$object']).map(([key2, expr2]) => [`${key}.${key2}`, expr2])
Expand Down Expand Up @@ -163,6 +173,12 @@ type FieldMap<S, M extends Dict<FieldLike<S>>> = {
[K in keyof M]: FieldType<S, M[K]>
}

type FieldCallback<S = any, M extends Dict<Eval.Term<any>> = any> = (row: Row<S>) => M

type EvalMap<M extends Dict<Eval.Term<any>>> = {
[K in keyof M]: Eval<M[K]>
}

export namespace Selection {
export type Callback<S = any, T = any, A extends boolean = boolean> = (row: Row<S>) => Eval.Expr<T, A>

Expand Down Expand Up @@ -229,17 +245,29 @@ export class Selection<S = any> extends Executable<S, S[]> {
query?: Selection.Callback<S, boolean>,
): Selection<FlatPick<S, K> & FieldMap<S, U>>

groupBy<K extends FlatKeys<S>, U extends object>(
fields: K | K[],
extra?: FieldCallback<S, U>,
query?: Selection.Callback<S, boolean>,
): Selection<FlatPick<S, K> & EvalMap<U>>

groupBy<K extends Dict<FieldLike<S>>>(fields: K, query?: Selection.Callback<S, boolean>): Selection<FieldMap<S, K>>
groupBy<K extends Dict<FieldLike<S>>, U extends Dict<FieldLike<S>>>(
fields: K,
extra?: U,
query?: Selection.Callback<S, boolean>,
): Selection<FieldMap<S, K & U>>

groupBy<K extends Dict<FieldLike<S>>, U extends object>(
fields: K,
extra?: FieldCallback<S, U>,
query?: Selection.Callback<S, boolean>,
): Selection<FieldMap<S, K> & EvalMap<U>>

groupBy(fields: any, ...args: any[]) {
this.args[0].fields = this.resolveFields(fields)
this.args[0].group = Object.keys(this.args[0].fields!)
const extra = typeof args[0] === 'function' ? undefined : args.shift()
const extra = this.isCallaback(args[0]) ? undefined : args.shift()
Object.assign(this.args[0].fields!, this.resolveFields(extra || {}))
if (args[0]) this.having(args[0])
return new Selection(this.driver, this)
Expand All @@ -252,7 +280,8 @@ export class Selection<S = any> extends Executable<S, S[]> {

project<K extends FlatKeys<S>>(fields: K | readonly K[]): Selection<FlatPick<S, K>>
project<U extends Dict<FieldLike<S>>>(fields: U): Selection<FieldMap<S, U>>
project(fields: Keys<S>[] | Dict<FieldLike<S>>) {
project<U extends object>(fields: FieldCallback<S, U>): Selection<EvalMap<U>>
project(fields: Keys<S>[] | Dict<FieldLike<S>> | FieldCallback) {
this.args[0].fields = this.resolveFields(fields)
return new Selection(this.driver, this)
}
Expand Down
18 changes: 18 additions & 0 deletions packages/tests/src/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,24 @@ namespace JsonTests {
])
})

it('$.object using spread', async () => {
const res = await database.select('foo')
.project({
obj: row => $.object({
id2: row.id,
...row,
})
})
.orderBy(row => row.obj.id)
.execute()

expect(res).to.deep.equal([
{ obj: { id2: 1, id: 1, value: 0 } },
{ obj: { id2: 2, id: 2, value: 2 } },
{ obj: { id2: 3, id: 3, value: 2 } },
])
})

it('$.object in json', async () => {
const res = await database.select('bar')
.project({
Expand Down
19 changes: 19 additions & 0 deletions packages/tests/src/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,25 @@ namespace ObjectOperations {
t: 'meta',
}).execute()).to.eventually.have.deep.members([{ t: table[1].meta }])
})

it('accumlate project', async () => {
const table = await setup(database)
await expect(database.select('object', {
'meta.a': '666',
}).project(row => ({
t: 'meta',
t2: row.meta.embed.c,
t3: $.concat(row.meta.a, 'my'),
...row.meta,
...row,
})).execute()).to.eventually.have.deep.members([{
t: table[1].meta,
t2: 'world',
t3: '666my',
...table[1].meta,
...table[1],
}])
})
}
}

Expand Down
13 changes: 13 additions & 0 deletions packages/tests/src/selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,19 @@ namespace SelectionTests {
{ value: 0, sum: 1, count: 1 },
{ value: 2, sum: 5, count: 2 },
])

await expect(database
.select('foo')
.groupBy('value', row => ({
sum: $.sum(row.id),
count: $.count(row.id),
}))
.orderBy('value')
.execute()
).to.eventually.deep.equal([
{ value: 0, sum: 1, count: 1 },
{ value: 2, sum: 5, count: 2 },
])
})

it('having', async () => {
Expand Down