From fa73be1a8464e81f6c686456337419c697362ac4 Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Mon, 2 Sep 2024 13:50:38 +0800 Subject: [PATCH 1/5] feat(core): impl FieldCallback for project & groupBy, support spreading Row & Cell --- packages/core/src/eval.ts | 4 ++-- packages/core/src/selection.ts | 37 +++++++++++++++++++++++++++------ packages/tests/src/object.ts | 15 +++++++++++++ packages/tests/src/selection.ts | 13 ++++++++++++ 4 files changed, 61 insertions(+), 8 deletions(-) diff --git a/packages/core/src/eval.ts b/packages/core/src/eval.ts index fa870b32..f53756ea 100644 --- a/packages/core/src/eval.ts +++ b/packages/core/src/eval.ts @@ -318,9 +318,9 @@ 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 + return Object.assign(Eval('object', fields, Type.Object(mapValues(fields, (value) => Type.fromTerm(value)))), fields) } Eval.array = unary('array', (expr, table) => Array.isArray(table) diff --git a/packages/core/src/selection.ts b/packages/core/src/selection.ts index 84db53cc..af798f4a 100644 --- a/packages/core/src/selection.ts +++ b/packages/core/src/selection.ts @@ -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' @@ -80,7 +80,13 @@ class Executable { 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 isQuery(query: any): query is Query { + if (typeof query !== 'function') return false + const fields = query(this.row) + return isEvalExpr(omit(fields, ['$object'])) } protected resolveQuery(query?: Query): Query.Expr @@ -99,17 +105,20 @@ class Executable { return query } - protected resolveField(field: FieldLike): Eval.Expr { + protected resolveField(field: FieldLike | 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>) { + protected resolveFields(fields: string | string[] | Dict> | 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) @@ -123,6 +132,7 @@ class Executable { 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]) @@ -151,6 +161,8 @@ type FieldMap>> = { [K in keyof M]: FieldType } +type FieldCallback = (row: Row) => U + export namespace Selection { export type Callback = (row: Row) => Eval.Expr @@ -217,6 +229,12 @@ export class Selection extends Executable { query?: Selection.Callback, ): Selection & FieldMap> + groupBy, U extends object>( + fields: K | K[], + extra?: FieldCallback, + query?: Selection.Callback, + ): Selection & U> + groupBy>>(fields: K, query?: Selection.Callback): Selection> groupBy>, U extends Dict>>( fields: K, @@ -224,10 +242,16 @@ export class Selection extends Executable { query?: Selection.Callback, ): Selection> + groupBy>, U extends any>( + fields: K, + extra?: FieldCallback, + query?: Selection.Callback, + ): Selection & 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.isQuery(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) @@ -240,7 +264,8 @@ export class Selection extends Executable { project>(fields: K | readonly K[]): Selection> project>>(fields: U): Selection> - project(fields: Keys[] | Dict>) { + project(fields: FieldCallback): Selection + project(fields: Keys[] | Dict> | Selection.Callback) { this.args[0].fields = this.resolveFields(fields) return new Selection(this.driver, this) } diff --git a/packages/tests/src/object.ts b/packages/tests/src/object.ts index f4f7c1de..ccfa6d26 100644 --- a/packages/tests/src/object.ts +++ b/packages/tests/src/object.ts @@ -235,6 +235,21 @@ 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', + ...row.meta, + ...row, + })).execute()).to.eventually.have.deep.members([{ + t: table[1].meta, + ...table[1].meta, + ...table[1], + }]) + }) } } diff --git a/packages/tests/src/selection.ts b/packages/tests/src/selection.ts index 8f058281..f537807b 100644 --- a/packages/tests/src/selection.ts +++ b/packages/tests/src/selection.ts @@ -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 () => { From 7fde9d8cbf2550dc72b18e7761a00bf0ca54d252 Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Mon, 2 Sep 2024 16:53:04 +0800 Subject: [PATCH 2/5] fix: types --- packages/core/src/selection.ts | 16 ++++++++++------ packages/tests/src/object.ts | 4 ++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/core/src/selection.ts b/packages/core/src/selection.ts index af798f4a..e48a6b8c 100644 --- a/packages/core/src/selection.ts +++ b/packages/core/src/selection.ts @@ -161,7 +161,11 @@ type FieldMap>> = { [K in keyof M]: FieldType } -type FieldCallback = (row: Row) => U +type FieldCallback> = any> = (row: Row) => M + +type EvalMap>> = { + [K in keyof M]: Eval +} export namespace Selection { export type Callback = (row: Row) => Eval.Expr @@ -233,7 +237,7 @@ export class Selection extends Executable { fields: K | K[], extra?: FieldCallback, query?: Selection.Callback, - ): Selection & U> + ): Selection & EvalMap> groupBy>>(fields: K, query?: Selection.Callback): Selection> groupBy>, U extends Dict>>( @@ -242,11 +246,11 @@ export class Selection extends Executable { query?: Selection.Callback, ): Selection> - groupBy>, U extends any>( + groupBy>, U extends object>( fields: K, extra?: FieldCallback, query?: Selection.Callback, - ): Selection & U> + ): Selection & EvalMap> groupBy(fields: any, ...args: any[]) { this.args[0].fields = this.resolveFields(fields) @@ -264,8 +268,8 @@ export class Selection extends Executable { project>(fields: K | readonly K[]): Selection> project>>(fields: U): Selection> - project(fields: FieldCallback): Selection - project(fields: Keys[] | Dict> | Selection.Callback) { + project(fields: FieldCallback): Selection> + project(fields: Keys[] | Dict> | FieldCallback) { this.args[0].fields = this.resolveFields(fields) return new Selection(this.driver, this) } diff --git a/packages/tests/src/object.ts b/packages/tests/src/object.ts index ccfa6d26..4e43b2ea 100644 --- a/packages/tests/src/object.ts +++ b/packages/tests/src/object.ts @@ -242,10 +242,14 @@ namespace ObjectOperations { '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], }]) From 4df9639ea02c02038fb3e1a906ab679e0a7e721c Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Sat, 14 Sep 2024 19:13:09 +0800 Subject: [PATCH 3/5] feat: support spread in nested $.object --- packages/core/src/eval.ts | 4 +++- packages/tests/src/json.ts | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/core/src/eval.ts b/packages/core/src/eval.ts index f53756ea..5eac8ff9 100644 --- a/packages/core/src/eval.ts +++ b/packages/core/src/eval.ts @@ -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' @@ -320,6 +320,8 @@ Eval.object = (fields: any) => { .map(([k]) => [k.slice(prefix.length), fields[k.slice(prefix.length)]])) return Object.assign(Eval('object', fields, Type.Object(mapValues(fields, (value) => Type.fromTerm(value)))), fields) } + // 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) } diff --git a/packages/tests/src/json.ts b/packages/tests/src/json.ts index 1b0fe56f..d516fbcb 100644 --- a/packages/tests/src/json.ts +++ b/packages/tests/src/json.ts @@ -155,6 +155,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({ From 8835533841e5b37937eaa7c42b3cf0bda235b09f Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Fri, 27 Sep 2024 03:18:54 +0800 Subject: [PATCH 4/5] fix: relation type --- packages/core/src/model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index ebc2bf2e..059943da 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -37,7 +37,7 @@ export namespace Relation { } export type Include = boolean | { - [P in keyof T]?: T[P] extends MaybeArray | undefined ? U extends S ? Include : Query.Expr> : never + [P in keyof T]?: T[P] extends MaybeArray | undefined ? U extends S ? Include : (U extends (infer I)[] ? Query.Expr : never) : never } export type SetExpr = ((row: Row) => Update) | { From 0cfb9ea02f761144e79ed1ae81f02b1e3c890995 Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Fri, 11 Oct 2024 10:28:54 +0800 Subject: [PATCH 5/5] chore: rename isQuery to isCallback --- packages/core/src/selection.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/selection.ts b/packages/core/src/selection.ts index 2a0d5733..8e71220a 100644 --- a/packages/core/src/selection.ts +++ b/packages/core/src/selection.ts @@ -95,7 +95,7 @@ class Executable { defineProperty(this, 'row', createRow(this.ref, Eval.object(createRow(this.ref, {}, '', this.model)), '', this.model)) } - protected isQuery(query: any): query is Query { + protected isCallaback(query: any): query is Selection.Callback { if (typeof query !== 'function') return false const fields = query(this.row) return isEvalExpr(omit(fields, ['$object'])) @@ -267,7 +267,7 @@ export class Selection extends Executable { groupBy(fields: any, ...args: any[]) { this.args[0].fields = this.resolveFields(fields) this.args[0].group = Object.keys(this.args[0].fields!) - const extra = this.isQuery(args[0]) ? 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)