turbine-orm 0.35.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -16
- package/dist/cjs/cli/index.js +109 -16
- package/dist/cjs/cli/migrate.js +78 -3
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.js +333 -22
- package/dist/cjs/cli/ui.js +7 -1
- package/dist/cjs/dialect.js +1 -1
- package/dist/cjs/generate.js +23 -2
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/mssql.js +22 -5
- package/dist/cjs/powdb.js +41 -1
- package/dist/cjs/powql.js +80 -25
- package/dist/cjs/query/aggregates.js +683 -0
- package/dist/cjs/query/batched-loader.js +2 -0
- package/dist/cjs/query/builder.js +297 -4504
- package/dist/cjs/query/filters.js +12 -0
- package/dist/cjs/query/relations.js +1698 -0
- package/dist/cjs/query/where-compile.js +180 -0
- package/dist/cjs/query/where.js +1491 -0
- package/dist/cjs/query/writes.js +680 -0
- package/dist/cjs/schema-builder.js +6 -0
- package/dist/cjs/schema-metadata.js +4 -0
- package/dist/cjs/schema-sql.js +265 -3
- package/dist/cjs/sqlite.js +1 -1
- package/dist/cli/index.d.ts +8 -2
- package/dist/cli/index.js +111 -18
- package/dist/cli/migrate.d.ts +24 -1
- package/dist/cli/migrate.js +77 -3
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +46 -13
- package/dist/cli/studio.js +331 -23
- package/dist/cli/ui.js +7 -1
- package/dist/dialect.d.ts +15 -6
- package/dist/dialect.js +1 -1
- package/dist/generate.js +23 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mssql.js +22 -5
- package/dist/powdb.d.ts +20 -0
- package/dist/powdb.js +40 -0
- package/dist/powql.d.ts +33 -1
- package/dist/powql.js +80 -25
- package/dist/query/aggregates.d.ts +74 -0
- package/dist/query/aggregates.js +641 -0
- package/dist/query/batched-loader.d.ts +6 -0
- package/dist/query/batched-loader.js +2 -0
- package/dist/query/builder.d.ts +62 -829
- package/dist/query/builder.js +302 -4509
- package/dist/query/deferred.d.ts +7 -0
- package/dist/query/filters.d.ts +7 -0
- package/dist/query/filters.js +11 -0
- package/dist/query/relations.d.ts +441 -0
- package/dist/query/relations.js +1627 -0
- package/dist/query/types.d.ts +15 -0
- package/dist/query/where-compile.d.ts +139 -0
- package/dist/query/where-compile.js +175 -0
- package/dist/query/where.d.ts +494 -0
- package/dist/query/where.js +1431 -0
- package/dist/query/writes.d.ts +131 -0
- package/dist/query/writes.js +626 -0
- package/dist/schema-builder.d.ts +18 -3
- package/dist/schema-builder.js +6 -0
- package/dist/schema-metadata.js +4 -0
- package/dist/schema-sql.d.ts +60 -3
- package/dist/schema-sql.js +261 -4
- package/dist/schema.d.ts +10 -0
- package/dist/sqlite.js +1 -1
- package/package.json +2 -2
package/dist/generate.js
CHANGED
|
@@ -161,8 +161,13 @@ export function generateTypes(schema, options) {
|
|
|
161
161
|
for (const col of table.columns) {
|
|
162
162
|
const pkNote = table.primaryKey.includes(col.name) ? ' (primary key)' : '';
|
|
163
163
|
const nullNote = col.nullable ? ' (nullable)' : '';
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
// PII columns are excluded from default projections, so the field is
|
|
165
|
+
// absent unless the query names it in `select` or passes `includePii`.
|
|
166
|
+
// The emitted type marks it optional so it tells the truth about absence.
|
|
167
|
+
const piiNote = col.pii ? ' (PII: absent unless selected or includePii)' : '';
|
|
168
|
+
const optional = col.pii ? '?' : '';
|
|
169
|
+
lines.push(` /** Column: ${col.name}, ${col.pgType}${pkNote}${nullNote}${piiNote} */`);
|
|
170
|
+
lines.push(` ${col.field}${optional}: ${columnTsType(col, schema.enums)};`);
|
|
166
171
|
}
|
|
167
172
|
lines.push('}');
|
|
168
173
|
lines.push('');
|
|
@@ -524,6 +529,17 @@ export function generateMetadata(schema, options) {
|
|
|
524
529
|
lines.push(` { name: '${escSQ(idx.name)}', columns: [${idx.columns.map((c) => `'${escSQ(c)}'`).join(', ')}], unique: ${idx.unique}, definition: ${JSON.stringify(idx.definition)} },`);
|
|
525
530
|
}
|
|
526
531
|
lines.push(' ],');
|
|
532
|
+
// checks: introspected named CHECK constraints. Emitted only when present
|
|
533
|
+
// (byte-stable for check-less tables) and sorted by name so `--no-timestamp`
|
|
534
|
+
// output is deterministic regardless of catalog row order.
|
|
535
|
+
if (table.checks && table.checks.length > 0) {
|
|
536
|
+
const sortedChecks = [...table.checks].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
537
|
+
lines.push(' checks: [');
|
|
538
|
+
for (const chk of sortedChecks) {
|
|
539
|
+
lines.push(` { name: '${escSQ(chk.name)}', expression: ${JSON.stringify(chk.expression)} },`);
|
|
540
|
+
}
|
|
541
|
+
lines.push(' ],');
|
|
542
|
+
}
|
|
527
543
|
// isView — read-only marker; the runtime write guard reads it.
|
|
528
544
|
if (table.isView)
|
|
529
545
|
lines.push(' isView: true,');
|
|
@@ -727,6 +743,11 @@ function serializeColumn(col) {
|
|
|
727
743
|
if (col.generationExpression !== undefined) {
|
|
728
744
|
parts.push(`generationExpression: '${escSQ(col.generationExpression)}'`);
|
|
729
745
|
}
|
|
746
|
+
// PII marker: emitted only when set, so untagged schemas stay byte-identical.
|
|
747
|
+
// Introspection never sets this (code-first declaration), but a metadata
|
|
748
|
+
// object built from `defineSchema` (pii: true) carries it through codegen.
|
|
749
|
+
if (col.pii)
|
|
750
|
+
parts.push(`pii: true`);
|
|
730
751
|
if (col.maxLength !== undefined)
|
|
731
752
|
parts.push(`maxLength: ${col.maxLength}`);
|
|
732
753
|
return `{ ${parts.join(', ')} }`;
|
package/dist/index.d.ts
CHANGED
|
@@ -49,7 +49,7 @@ export type { CheckMetadata, ColumnMetadata, IndexMetadata, ReferentialAction, R
|
|
|
49
49
|
export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, } from './schema.js';
|
|
50
50
|
export { applyManyToManyRelations, type CheckDef, ColumnBuilder, type ColumnConfig, type ColumnDef, type ColumnIndexDef, type ColumnType, type ColumnTypeName, column, type DefineSchemaOptions, type DocFieldIndexDef, defineSchema, isDocFieldIndexDef, type ManyToManyDef, type ReferenceDef, type SchemaDef, type SchemaIndexDef, type TableDef, table, } from './schema-builder.js';
|
|
51
51
|
export { schemaDefToMetadata } from './schema-metadata.js';
|
|
52
|
-
export { type AlterColumnDef, type AlterDef, type DiffResult, type PushResult, type SchemaSqlOptions, schemaDiff, schemaPush, schemaToSQL, schemaToSQLString, } from './schema-sql.js';
|
|
52
|
+
export { type AlterColumnDef, type AlterDef, DestructivePushRefusal, type DiffResult, type PushResult, type SchemaSqlOptions, schemaDiff, schemaPush, schemaToSQL, schemaToSQLString, } from './schema-sql.js';
|
|
53
53
|
export { type DefinedSeed, defineSeed, type SeedFunction } from './seed.js';
|
|
54
54
|
export { type TurbineHttpOptions, turbineHttp } from './serverless.js';
|
|
55
55
|
export { buildTypedSql, TypedSqlQuery } from './typed-sql.js';
|
package/dist/index.js
CHANGED
|
@@ -59,7 +59,7 @@ table, } from './schema-builder.js';
|
|
|
59
59
|
// Schema metadata bridge — defineSchema() → SchemaMetadata without a live DB
|
|
60
60
|
export { schemaDefToMetadata } from './schema-metadata.js';
|
|
61
61
|
// Schema SQL — generate DDL, diff, and push
|
|
62
|
-
export { schemaDiff, schemaPush, schemaToSQL, schemaToSQLString, } from './schema-sql.js';
|
|
62
|
+
export { DestructivePushRefusal, schemaDiff, schemaPush, schemaToSQL, schemaToSQLString, } from './schema-sql.js';
|
|
63
63
|
// Seed helper
|
|
64
64
|
export { defineSeed } from './seed.js';
|
|
65
65
|
// Serverless / edge factory
|
package/dist/mssql.js
CHANGED
|
@@ -448,6 +448,23 @@ function mssqlColumnType(type, maxLength) {
|
|
|
448
448
|
// ---------------------------------------------------------------------------
|
|
449
449
|
// mssqlDialect — the full Dialect contract for SQL Server 2016+
|
|
450
450
|
// ---------------------------------------------------------------------------
|
|
451
|
+
/**
|
|
452
|
+
* Render the SQL Server `OUTPUT` clause for a write's returning selection.
|
|
453
|
+
* `'*'` → ` OUTPUT INSERTED.*` (byte-identical to the historical default); a
|
|
454
|
+
* quoted column list → ` OUTPUT INSERTED.[c1], INSERTED.[c2]`; each column
|
|
455
|
+
* carries its own `INSERTED.`/`DELETED.` prefix (a bare comma list is invalid
|
|
456
|
+
* T-SQL). Used to exclude PII columns from a write's returned row. Empty
|
|
457
|
+
* selection → no clause.
|
|
458
|
+
*/
|
|
459
|
+
function mssqlOutput(returning, alias) {
|
|
460
|
+
if (!returning)
|
|
461
|
+
return '';
|
|
462
|
+
if (returning === '*')
|
|
463
|
+
return ` OUTPUT ${alias}.*`;
|
|
464
|
+
if (returning.length === 0)
|
|
465
|
+
return '';
|
|
466
|
+
return ` OUTPUT ${returning.map((col) => `${alias}.${col}`).join(', ')}`;
|
|
467
|
+
}
|
|
451
468
|
/**
|
|
452
469
|
* SQL Server 2016+ implementation of the {@link Dialect} contract. Bracket
|
|
453
470
|
* identifier quoting (`[…]`), named `@pN` placeholders, the `FOR JSON PATH`
|
|
@@ -511,7 +528,7 @@ export const mssqlDialect = {
|
|
|
511
528
|
return '';
|
|
512
529
|
},
|
|
513
530
|
buildInsertStatement(input) {
|
|
514
|
-
const out = input.returning
|
|
531
|
+
const out = mssqlOutput(input.returning, 'INSERTED');
|
|
515
532
|
return `INSERT INTO ${input.table} (${input.columns.join(', ')})${out} VALUES (${input.valuePlaceholders.join(', ')})`;
|
|
516
533
|
},
|
|
517
534
|
buildBulkInsertStatement(input) {
|
|
@@ -531,7 +548,7 @@ export const mssqlDialect = {
|
|
|
531
548
|
const placeholders = input.rowValues
|
|
532
549
|
.map((row) => `(${row.map(() => this.paramPlaceholder(++n)).join(', ')})`)
|
|
533
550
|
.join(', ');
|
|
534
|
-
const out = input.returning
|
|
551
|
+
const out = mssqlOutput(input.returning, 'INSERTED');
|
|
535
552
|
// skipDuplicates has no single-statement equivalent here; ignored (documented).
|
|
536
553
|
return {
|
|
537
554
|
sql: `INSERT INTO ${input.table} (${input.columns.join(', ')})${out} VALUES ${placeholders}`,
|
|
@@ -546,7 +563,7 @@ export const mssqlDialect = {
|
|
|
546
563
|
const on = input.conflictColumns.map((c) => `T.${c} = S.${c}`).join(' AND ');
|
|
547
564
|
const insertCols = input.insertColumns.join(', ');
|
|
548
565
|
const sourceVals = input.insertColumns.map((c) => `S.${c}`).join(', ');
|
|
549
|
-
const out = input.returning
|
|
566
|
+
const out = mssqlOutput(input.returning, 'INSERTED');
|
|
550
567
|
return (`MERGE INTO ${input.table} AS T ` +
|
|
551
568
|
`USING (VALUES (${input.valuePlaceholders.join(', ')})) AS S (${insertCols}) ` +
|
|
552
569
|
`ON (${on}) ` +
|
|
@@ -557,11 +574,11 @@ export const mssqlDialect = {
|
|
|
557
574
|
// UPDATE/DELETE inject OUTPUT mid-statement (between SET and WHERE / FROM and
|
|
558
575
|
// WHERE) — a trailing clause would be invalid T-SQL.
|
|
559
576
|
buildUpdateStatement(input) {
|
|
560
|
-
const out = input.returning
|
|
577
|
+
const out = mssqlOutput(input.returning, 'INSERTED');
|
|
561
578
|
return `UPDATE ${input.table} SET ${input.setClauses.join(', ')}${out}${input.whereSql}`;
|
|
562
579
|
},
|
|
563
580
|
buildDeleteStatement(input) {
|
|
564
|
-
const out = input.returning
|
|
581
|
+
const out = mssqlOutput(input.returning, 'DELETED');
|
|
565
582
|
return `DELETE FROM ${input.table}${out}${input.whereSql}`;
|
|
566
583
|
},
|
|
567
584
|
// SQL Server has no LIMIT — emit OFFSET/FETCH, injecting a stable ORDER BY when
|
package/dist/powdb.d.ts
CHANGED
|
@@ -580,6 +580,26 @@ interface EmbeddedModule {
|
|
|
580
580
|
* as data and cannot break out of the literal or inject a second statement.
|
|
581
581
|
*/
|
|
582
582
|
export declare function encodePowqlLiteral(value: unknown): string;
|
|
583
|
+
/**
|
|
584
|
+
* The newest PowDB engine LINE (major.minor) whose lexer escape handling
|
|
585
|
+
* {@link encodePowqlString} is VERIFIED against by reading
|
|
586
|
+
* `crates/query/src/lexer.rs`. The legacy materialize path
|
|
587
|
+
* ({@link materializePowql}) inlines encoded string literals directly into query
|
|
588
|
+
* text, so it is only injection-safe while the lexer recognizes exactly the
|
|
589
|
+
* escape set the escaper emits (`\"`, `\\`, `\n`, `\t`, everything else raw). If
|
|
590
|
+
* a future engine line teaches the lexer new escapes (e.g. `\u`, `\x`), a string
|
|
591
|
+
* that the escaper leaves raw could be re-interpreted by the lexer and break out
|
|
592
|
+
* of the literal, turning the fallback into an injection primitive.
|
|
593
|
+
*
|
|
594
|
+
* CONTRACT: bump this ceiling ONLY after re-verifying the escape handling in
|
|
595
|
+
* `crates/query/src/lexer.rs` for the newer line AND confirming
|
|
596
|
+
* {@link encodePowqlString} still escapes every breakout vector the lexer
|
|
597
|
+
* recognizes. The legacy path guards on this value (see
|
|
598
|
+
* {@link PowdbEmbeddedPool.exec}): an embedded addon whose engine line exceeds
|
|
599
|
+
* the ceiling yet still routes through the string wire is refused rather than
|
|
600
|
+
* materialized.
|
|
601
|
+
*/
|
|
602
|
+
export declare const POWQL_LEXER_TESTED_CEILING = "0.15";
|
|
583
603
|
/**
|
|
584
604
|
* Substitute every `$N` placeholder in a generator-produced PowQL template with
|
|
585
605
|
* the encoded literal of `params[N-1]`. Safe because the template is produced by
|
package/dist/powdb.js
CHANGED
|
@@ -1372,6 +1372,26 @@ export function encodePowqlLiteral(value) {
|
|
|
1372
1372
|
return encodePowqlString(value);
|
|
1373
1373
|
throw new ValidationError(`[turbine] Value of type ${typeof value} cannot be encoded as a PowDB literal.`);
|
|
1374
1374
|
}
|
|
1375
|
+
/**
|
|
1376
|
+
* The newest PowDB engine LINE (major.minor) whose lexer escape handling
|
|
1377
|
+
* {@link encodePowqlString} is VERIFIED against by reading
|
|
1378
|
+
* `crates/query/src/lexer.rs`. The legacy materialize path
|
|
1379
|
+
* ({@link materializePowql}) inlines encoded string literals directly into query
|
|
1380
|
+
* text, so it is only injection-safe while the lexer recognizes exactly the
|
|
1381
|
+
* escape set the escaper emits (`\"`, `\\`, `\n`, `\t`, everything else raw). If
|
|
1382
|
+
* a future engine line teaches the lexer new escapes (e.g. `\u`, `\x`), a string
|
|
1383
|
+
* that the escaper leaves raw could be re-interpreted by the lexer and break out
|
|
1384
|
+
* of the literal, turning the fallback into an injection primitive.
|
|
1385
|
+
*
|
|
1386
|
+
* CONTRACT: bump this ceiling ONLY after re-verifying the escape handling in
|
|
1387
|
+
* `crates/query/src/lexer.rs` for the newer line AND confirming
|
|
1388
|
+
* {@link encodePowqlString} still escapes every breakout vector the lexer
|
|
1389
|
+
* recognizes. The legacy path guards on this value (see
|
|
1390
|
+
* {@link PowdbEmbeddedPool.exec}): an embedded addon whose engine line exceeds
|
|
1391
|
+
* the ceiling yet still routes through the string wire is refused rather than
|
|
1392
|
+
* materialized.
|
|
1393
|
+
*/
|
|
1394
|
+
export const POWQL_LEXER_TESTED_CEILING = '0.15';
|
|
1375
1395
|
/** Escape a string into a PowQL `"…"` literal, matching the engine lexer's escape rules. */
|
|
1376
1396
|
function encodePowqlString(s) {
|
|
1377
1397
|
let out = '"';
|
|
@@ -1474,6 +1494,26 @@ export class PowdbEmbeddedPool {
|
|
|
1474
1494
|
// Legacy string wire (addon < 0.14): the engine takes no params array, so
|
|
1475
1495
|
// materialize each `$N` into a PowQL literal. Byte-for-byte unchanged, kept
|
|
1476
1496
|
// live and tested as the pre-0.14 fallback.
|
|
1497
|
+
//
|
|
1498
|
+
// Safety assertion (see {@link POWQL_LEXER_TESTED_CEILING}): an addon whose
|
|
1499
|
+
// engine line EXCEEDS the escaper's verified lexer ceiling must never be
|
|
1500
|
+
// materialized: a newer lexer may recognize escapes the escaper leaves raw,
|
|
1501
|
+
// making the inline path an injection primitive. Reachability: addons >= 0.14
|
|
1502
|
+
// expose `queryWithParams` and take the native path above, so the ONLY way to
|
|
1503
|
+
// reach this branch with a newer-than-ceiling engine is the anomalous
|
|
1504
|
+
// newer-addon-without-native case (feature-detect failed). Refuse it rather
|
|
1505
|
+
// than inline-encode against an unverified lexer.
|
|
1506
|
+
const engineSem = parsePowdbSemver(this.capabilities.engineVersion);
|
|
1507
|
+
const ceiling = parsePowdbSemver(POWQL_LEXER_TESTED_CEILING);
|
|
1508
|
+
if (engineSem &&
|
|
1509
|
+
ceiling &&
|
|
1510
|
+
(engineSem.major > ceiling.major || (engineSem.major === ceiling.major && engineSem.minor > ceiling.minor))) {
|
|
1511
|
+
throw new ValidationError(`[turbine] Refusing the PowDB legacy string wire: this embedded addon reports engine ` +
|
|
1512
|
+
`${this.capabilities.engineVersion}, which is newer than the escaper's verified lexer range ` +
|
|
1513
|
+
`(<= ${POWQL_LEXER_TESTED_CEILING}) AND such an addon exposes the parameterized native API, so ` +
|
|
1514
|
+
`reaching the legacy materialize path means the queryWithParams feature-detect failed. Upgrade ` +
|
|
1515
|
+
`turbine-orm so its PowQL escaper is verified against this engine line before running on it.`);
|
|
1516
|
+
}
|
|
1477
1517
|
const materialized = materializePowql(powql, params);
|
|
1478
1518
|
return adaptResult(normalizeEmbeddedResult(this.db.query(materialized)));
|
|
1479
1519
|
}
|
package/dist/powql.d.ts
CHANGED
|
@@ -175,8 +175,40 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
175
175
|
private resolveRelationCondition;
|
|
176
176
|
/** Resolve a manyToMany filter through the junction to `{ sourceRefField: { in|notIn: [...] } }`. */
|
|
177
177
|
private resolveManyToManyCondition;
|
|
178
|
-
/**
|
|
178
|
+
/**
|
|
179
|
+
* Resolve the set of columns to project, honouring `select` / `omit` and the
|
|
180
|
+
* query-level `includePii` opt-in. PII-tagged (`defineSchema` `pii: true`)
|
|
181
|
+
* columns are EXCLUDED from a default (or omit-only) projection unless
|
|
182
|
+
* `includePii` is true; an explicit `select` naming a PII column IS the opt-in
|
|
183
|
+
* and returns it regardless. Untagged tables project exactly as before.
|
|
184
|
+
*/
|
|
179
185
|
private projectedColumns;
|
|
186
|
+
/**
|
|
187
|
+
* The snake_case names of this table's PII-tagged columns. Empty for a table
|
|
188
|
+
* with no `pii: true` column, so untagged tables keep their prior projection.
|
|
189
|
+
*/
|
|
190
|
+
private piiColumnNames;
|
|
191
|
+
/**
|
|
192
|
+
* The camelCase field names of this table's PII-tagged columns: the read
|
|
193
|
+
* policy applied to a write's returned row (create/update/upsert/delete accept
|
|
194
|
+
* no `includePii`/`select`, so their result always drops PII; you may still
|
|
195
|
+
* write PII fields freely).
|
|
196
|
+
*
|
|
197
|
+
* SPEC LIMITATION (PowQL): the driver contract
|
|
198
|
+
* (`docs/integrations/powql-for-drivers.md`) exposes `returning` only as a
|
|
199
|
+
* bare keyword that hands back every column; it accepts NO column list, so
|
|
200
|
+
* (unlike the SQL engines, which emit an explicit non-PII `RETURNING`/`OUTPUT`
|
|
201
|
+
* projection) the create/update/delete `returning` paths cannot exclude PII at
|
|
202
|
+
* the query-language level and must strip it here after the fact. This is the
|
|
203
|
+
* client-side strip of last resort, not defense-in-depth, for those paths; we
|
|
204
|
+
* do NOT reverse-engineer an undocumented projection form. The upsert path is
|
|
205
|
+
* different: it has no `returning` and reselects by PK through the read
|
|
206
|
+
* projection ({@link projectedColumns}), which already omits PII, so PII never
|
|
207
|
+
* crosses the wire there. If a future spec revision lets `returning` take a
|
|
208
|
+
* projection, switch the write paths to emit the non-PII list and this strip
|
|
209
|
+
* becomes a no-op like {@link parseWriteRow} on the SQL engines.
|
|
210
|
+
*/
|
|
211
|
+
private stripWritePii;
|
|
180
212
|
/** `{ .c1, .c2, … }` projection clause. */
|
|
181
213
|
private projection;
|
|
182
214
|
/**
|
package/dist/powql.js
CHANGED
|
@@ -617,10 +617,17 @@ export class PowqlInterface {
|
|
|
617
617
|
// -------------------------------------------------------------------------
|
|
618
618
|
// Projection / order
|
|
619
619
|
// -------------------------------------------------------------------------
|
|
620
|
-
/**
|
|
621
|
-
|
|
620
|
+
/**
|
|
621
|
+
* Resolve the set of columns to project, honouring `select` / `omit` and the
|
|
622
|
+
* query-level `includePii` opt-in. PII-tagged (`defineSchema` `pii: true`)
|
|
623
|
+
* columns are EXCLUDED from a default (or omit-only) projection unless
|
|
624
|
+
* `includePii` is true; an explicit `select` naming a PII column IS the opt-in
|
|
625
|
+
* and returns it regardless. Untagged tables project exactly as before.
|
|
626
|
+
*/
|
|
627
|
+
projectedColumns(select, omit, includePii) {
|
|
622
628
|
let cols = this.meta.columns.map((c) => c.name);
|
|
623
|
-
|
|
629
|
+
const hasSelect = select && Object.keys(select).length;
|
|
630
|
+
if (hasSelect) {
|
|
624
631
|
const picked = new Set(Object.entries(select)
|
|
625
632
|
.filter(([, v]) => v)
|
|
626
633
|
.map(([k]) => this.column(k).name));
|
|
@@ -629,6 +636,14 @@ export class PowqlInterface {
|
|
|
629
636
|
picked.add(pk);
|
|
630
637
|
cols = cols.filter((c) => picked.has(c));
|
|
631
638
|
}
|
|
639
|
+
else if (!includePii) {
|
|
640
|
+
// Default / omit-only projection: drop PII columns (kept above only when a
|
|
641
|
+
// caller names them in `select`). PK is never PII in practice; if one is
|
|
642
|
+
// tagged it is still dropped here, so tag sensitive data, not keys.
|
|
643
|
+
const pii = this.piiColumnNames();
|
|
644
|
+
if (pii.size)
|
|
645
|
+
cols = cols.filter((c) => !pii.has(c));
|
|
646
|
+
}
|
|
632
647
|
if (omit && Object.keys(omit).length) {
|
|
633
648
|
const dropped = new Set(Object.entries(omit)
|
|
634
649
|
.filter(([, v]) => v)
|
|
@@ -637,6 +652,47 @@ export class PowqlInterface {
|
|
|
637
652
|
}
|
|
638
653
|
return cols;
|
|
639
654
|
}
|
|
655
|
+
/**
|
|
656
|
+
* The snake_case names of this table's PII-tagged columns. Empty for a table
|
|
657
|
+
* with no `pii: true` column, so untagged tables keep their prior projection.
|
|
658
|
+
*/
|
|
659
|
+
piiColumnNames() {
|
|
660
|
+
const out = new Set();
|
|
661
|
+
for (const col of this.meta.columns) {
|
|
662
|
+
if (col.pii)
|
|
663
|
+
out.add(col.name);
|
|
664
|
+
}
|
|
665
|
+
return out;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* The camelCase field names of this table's PII-tagged columns: the read
|
|
669
|
+
* policy applied to a write's returned row (create/update/upsert/delete accept
|
|
670
|
+
* no `includePii`/`select`, so their result always drops PII; you may still
|
|
671
|
+
* write PII fields freely).
|
|
672
|
+
*
|
|
673
|
+
* SPEC LIMITATION (PowQL): the driver contract
|
|
674
|
+
* (`docs/integrations/powql-for-drivers.md`) exposes `returning` only as a
|
|
675
|
+
* bare keyword that hands back every column; it accepts NO column list, so
|
|
676
|
+
* (unlike the SQL engines, which emit an explicit non-PII `RETURNING`/`OUTPUT`
|
|
677
|
+
* projection) the create/update/delete `returning` paths cannot exclude PII at
|
|
678
|
+
* the query-language level and must strip it here after the fact. This is the
|
|
679
|
+
* client-side strip of last resort, not defense-in-depth, for those paths; we
|
|
680
|
+
* do NOT reverse-engineer an undocumented projection form. The upsert path is
|
|
681
|
+
* different: it has no `returning` and reselects by PK through the read
|
|
682
|
+
* projection ({@link projectedColumns}), which already omits PII, so PII never
|
|
683
|
+
* crosses the wire there. If a future spec revision lets `returning` take a
|
|
684
|
+
* projection, switch the write paths to emit the non-PII list and this strip
|
|
685
|
+
* becomes a no-op like {@link parseWriteRow} on the SQL engines.
|
|
686
|
+
*/
|
|
687
|
+
stripWritePii(entity) {
|
|
688
|
+
if (!entity)
|
|
689
|
+
return entity;
|
|
690
|
+
for (const col of this.meta.columns) {
|
|
691
|
+
if (col.pii)
|
|
692
|
+
delete entity[col.field];
|
|
693
|
+
}
|
|
694
|
+
return entity;
|
|
695
|
+
}
|
|
640
696
|
/** `{ .c1, .c2, … }` projection clause. */
|
|
641
697
|
projection(cols) {
|
|
642
698
|
return `{ ${cols.map((c) => `.${c}`).join(', ')} }`;
|
|
@@ -841,10 +897,7 @@ export class PowqlInterface {
|
|
|
841
897
|
const { rows, native, resolvedWhere } = await this.runFind(args, 'findMany');
|
|
842
898
|
const entities = this.shape(rows, native);
|
|
843
899
|
if (args.with) {
|
|
844
|
-
await this.loadRelations(entities, args.with, args.timeout, 0, {
|
|
845
|
-
args,
|
|
846
|
-
resolvedWhere,
|
|
847
|
-
});
|
|
900
|
+
await this.loadRelations(entities, args.with, args.timeout, 0, { args, resolvedWhere }, args.includePii === true);
|
|
848
901
|
}
|
|
849
902
|
return entities;
|
|
850
903
|
});
|
|
@@ -861,7 +914,7 @@ export class PowqlInterface {
|
|
|
861
914
|
}
|
|
862
915
|
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
|
|
863
916
|
const where = this.buildWhere(resolvedWhere, params);
|
|
864
|
-
const cols = this.projectedColumns(args.select, args.omit);
|
|
917
|
+
const cols = this.projectedColumns(args.select, args.omit, args.includePii === true);
|
|
865
918
|
const distinct = args.distinct?.length ? ' distinct' : '';
|
|
866
919
|
const filter = where ? ` filter ${where}` : '';
|
|
867
920
|
const order = this.buildOrder(args.orderBy, params);
|
|
@@ -913,7 +966,7 @@ export class PowqlInterface {
|
|
|
913
966
|
return null;
|
|
914
967
|
const entities = this.shape(rows, native);
|
|
915
968
|
if (args.with)
|
|
916
|
-
await this.loadRelations(entities, args.with, args.timeout);
|
|
969
|
+
await this.loadRelations(entities, args.with, args.timeout, 0, undefined, args.includePii === true);
|
|
917
970
|
return entities[0];
|
|
918
971
|
});
|
|
919
972
|
}
|
|
@@ -924,7 +977,7 @@ export class PowqlInterface {
|
|
|
924
977
|
return null;
|
|
925
978
|
const entities = this.shape(rows, native);
|
|
926
979
|
if (args.with)
|
|
927
|
-
await this.loadRelations(entities, args.with, args.timeout);
|
|
980
|
+
await this.loadRelations(entities, args.with, args.timeout, 0, undefined, args.includePii === true);
|
|
928
981
|
return entities[0];
|
|
929
982
|
});
|
|
930
983
|
}
|
|
@@ -954,7 +1007,7 @@ export class PowqlInterface {
|
|
|
954
1007
|
* the default `'batched'` strategy) keeps the loaders. Output is byte-equal
|
|
955
1008
|
* either way (the join reuses the same stitch / shape helpers).
|
|
956
1009
|
*/
|
|
957
|
-
async loadRelations(parents, withClause, timeout, depth = 0, parent) {
|
|
1010
|
+
async loadRelations(parents, withClause, timeout, depth = 0, parent, includePii = false) {
|
|
958
1011
|
if (depth >= 10) {
|
|
959
1012
|
throw new ValidationError(`[turbine] Nested 'with' on PowDB exceeded depth 10 (relation cycle?).`);
|
|
960
1013
|
}
|
|
@@ -974,7 +1027,7 @@ export class PowqlInterface {
|
|
|
974
1027
|
throw new ValidationError(`[turbine] Unknown relation "${relName}" on "${this.table}".`);
|
|
975
1028
|
if (strategyIsJoin && parent && this.joinEligible(rel, opt, parent.args, parents.length)) {
|
|
976
1029
|
if (this.capabilities.serverJoins) {
|
|
977
|
-
await this.loadRelationViaJoin(parents, rel, relName, opt, parent, timeout);
|
|
1030
|
+
await this.loadRelationViaJoin(parents, rel, relName, opt, parent, timeout, includePii);
|
|
978
1031
|
continue;
|
|
979
1032
|
}
|
|
980
1033
|
// An otherwise-eligible relation the engine cannot join: a PER-QUERY
|
|
@@ -986,7 +1039,7 @@ export class PowqlInterface {
|
|
|
986
1039
|
}
|
|
987
1040
|
}
|
|
988
1041
|
if (rel.type === 'manyToMany') {
|
|
989
|
-
await this.loadManyToMany(parents, rel, relName, opt, timeout);
|
|
1042
|
+
await this.loadManyToMany(parents, rel, relName, opt, timeout, includePii);
|
|
990
1043
|
continue;
|
|
991
1044
|
}
|
|
992
1045
|
const fk = normalizeKeyColumns(rel.foreignKey);
|
|
@@ -1043,6 +1096,7 @@ export class PowqlInterface {
|
|
|
1043
1096
|
where: childWhere,
|
|
1044
1097
|
with: options.with,
|
|
1045
1098
|
timeout: options.timeout ?? timeout,
|
|
1099
|
+
includePii,
|
|
1046
1100
|
}));
|
|
1047
1101
|
for (const child of children) {
|
|
1048
1102
|
const k = this.joinKey(child[childKeyField]);
|
|
@@ -1079,7 +1133,7 @@ export class PowqlInterface {
|
|
|
1079
1133
|
* single-key N+1 loaders; the junction's source/target columns must be single
|
|
1080
1134
|
* (composite junction keys would need PowQL tuple-`in`, which it lacks).
|
|
1081
1135
|
*/
|
|
1082
|
-
async loadManyToMany(parents, rel, relName, opt, timeout) {
|
|
1136
|
+
async loadManyToMany(parents, rel, relName, opt, timeout, includePii = false) {
|
|
1083
1137
|
const through = rel.through;
|
|
1084
1138
|
if (!through)
|
|
1085
1139
|
throw new ValidationError(`[turbine] manyToMany relation "${relName}" is missing its junction (\`through\`).`);
|
|
@@ -1143,6 +1197,7 @@ export class PowqlInterface {
|
|
|
1143
1197
|
where,
|
|
1144
1198
|
with: options.with,
|
|
1145
1199
|
timeout: options.timeout ?? timeout,
|
|
1200
|
+
includePii,
|
|
1146
1201
|
}));
|
|
1147
1202
|
for (const t of targets)
|
|
1148
1203
|
targetByPk.set(String(t[targetPkField]), t);
|
|
@@ -1260,9 +1315,9 @@ export class PowqlInterface {
|
|
|
1260
1315
|
return tableMeta.indexes.some((idx) => idx.unique && !idx.docPath && idx.columns.length === 1 && idx.columns[0] === col);
|
|
1261
1316
|
}
|
|
1262
1317
|
/** Dispatch one eligible relation to the correct native-join loader. */
|
|
1263
|
-
async loadRelationViaJoin(parents, rel, relName, opt, parent, timeout) {
|
|
1318
|
+
async loadRelationViaJoin(parents, rel, relName, opt, parent, timeout, includePii = false) {
|
|
1264
1319
|
if (rel.type === 'manyToMany') {
|
|
1265
|
-
await this.loadManyToManyViaJoin(parents, rel, relName, opt, parent, timeout);
|
|
1320
|
+
await this.loadManyToManyViaJoin(parents, rel, relName, opt, parent, timeout, includePii);
|
|
1266
1321
|
return;
|
|
1267
1322
|
}
|
|
1268
1323
|
const options = (opt === true ? {} : opt);
|
|
@@ -1280,7 +1335,7 @@ export class PowqlInterface {
|
|
|
1280
1335
|
const childKeyCol = rel.type === 'belongsTo' ? rk[0] : fk[0];
|
|
1281
1336
|
const parentKeyField = this.meta.reverseColumnMap[parentKeyCol] ?? parentKeyCol;
|
|
1282
1337
|
const params = [];
|
|
1283
|
-
const childCols = this.joinChildCols(targetQi, options);
|
|
1338
|
+
const childCols = this.joinChildCols(targetQi, options, includePii);
|
|
1284
1339
|
const filter = await this.joinFilter(targetQi, parent.resolvedWhere, options.where, 'c', params, options.timeout ?? timeout);
|
|
1285
1340
|
const order = targetQi.buildOrder(options.orderBy, params, 'c');
|
|
1286
1341
|
const limitClause = options.limit !== undefined ? ` limit ${this.param(options.limit, params)}` : '';
|
|
@@ -1304,7 +1359,7 @@ export class PowqlInterface {
|
|
|
1304
1359
|
* → the already-fetched side (alias `p`), correlating `__tpk` from the
|
|
1305
1360
|
* junction's source key. Always a list, stitched exactly like the loader.
|
|
1306
1361
|
*/
|
|
1307
|
-
async loadManyToManyViaJoin(parents, rel, relName, opt, parent, timeout) {
|
|
1362
|
+
async loadManyToManyViaJoin(parents, rel, relName, opt, parent, timeout, includePii = false) {
|
|
1308
1363
|
const through = rel.through;
|
|
1309
1364
|
if (!through)
|
|
1310
1365
|
throw new ValidationError(`[turbine] manyToMany relation "${relName}" is missing its junction (\`through\`).`);
|
|
@@ -1319,7 +1374,7 @@ export class PowqlInterface {
|
|
|
1319
1374
|
const targetPkCol = targetMeta.primaryKey[0];
|
|
1320
1375
|
const parentRefField = this.meta.reverseColumnMap[sourceRefCol] ?? sourceRefCol;
|
|
1321
1376
|
const params = [];
|
|
1322
|
-
const childCols = this.joinChildCols(targetQi, options);
|
|
1377
|
+
const childCols = this.joinChildCols(targetQi, options, includePii);
|
|
1323
1378
|
const filter = await this.joinFilter(targetQi, parent.resolvedWhere, options.where, 't', params, options.timeout ?? timeout);
|
|
1324
1379
|
const proj = this.joinProjection(childCols, `j.${quotePowqlIdent(sourceJCol)}`, 't');
|
|
1325
1380
|
const powql = `${targetQi.qt} as t ` +
|
|
@@ -1338,8 +1393,8 @@ export class PowqlInterface {
|
|
|
1338
1393
|
* with a loud guard: a real column named `__tpk` would collide with the
|
|
1339
1394
|
* reserved correlation alias, so refuse rather than silently mis-stitch.
|
|
1340
1395
|
*/
|
|
1341
|
-
joinChildCols(targetQi, options) {
|
|
1342
|
-
const cols = targetQi.projectedColumns(options.select, options.omit);
|
|
1396
|
+
joinChildCols(targetQi, options, includePii = false) {
|
|
1397
|
+
const cols = targetQi.projectedColumns(options.select, options.omit, includePii);
|
|
1343
1398
|
if (cols.includes('__tpk')) {
|
|
1344
1399
|
throw new ValidationError(`[turbine] relation target "${targetQi.table}" has a column named "__tpk", which collides with the reserved ` +
|
|
1345
1400
|
`join correlation alias. Rename the column or load this relation with relationLoadStrategy: 'batched'.`);
|
|
@@ -1468,7 +1523,7 @@ export class PowqlInterface {
|
|
|
1468
1523
|
.join(', ');
|
|
1469
1524
|
// `returning` surfaces the inserted row (all columns, schema order) in one round-trip.
|
|
1470
1525
|
const { rows, native } = await this.exec(`insert ${this.qt} { ${body} } returning`, params, args.timeout, 'create');
|
|
1471
|
-
const row = rows.length ? this.shape(rows, native)[0] : null;
|
|
1526
|
+
const row = rows.length ? this.stripWritePii(this.shape(rows, native)[0]) : null;
|
|
1472
1527
|
if (!row)
|
|
1473
1528
|
throw new NotFoundError({ table: this.table, where: data });
|
|
1474
1529
|
return row;
|
|
@@ -1486,7 +1541,7 @@ export class PowqlInterface {
|
|
|
1486
1541
|
});
|
|
1487
1542
|
// Multi-row insert with `returning` hands back every inserted row in one round-trip.
|
|
1488
1543
|
const { rows, native } = await this.exec(`insert ${this.qt} ${tuples.join(', ')} returning`, params, args.timeout, 'createMany');
|
|
1489
|
-
return this.shape(rows, native);
|
|
1544
|
+
return this.shape(rows, native).map((r) => this.stripWritePii(r));
|
|
1490
1545
|
});
|
|
1491
1546
|
}
|
|
1492
1547
|
async update(args) {
|
|
@@ -1501,7 +1556,7 @@ export class PowqlInterface {
|
|
|
1501
1556
|
const setClause = this.buildUpdateAssignments(args.data, params);
|
|
1502
1557
|
// `returning` hands back the post-update row(s); take the first (single-row contract).
|
|
1503
1558
|
const { rows, native } = await this.exec(`${this.qt} filter ${where} update { ${setClause} } returning`, params, args.timeout, 'update');
|
|
1504
|
-
const row = rows.length ? this.shape(rows, native)[0] : null;
|
|
1559
|
+
const row = rows.length ? this.stripWritePii(this.shape(rows, native)[0]) : null;
|
|
1505
1560
|
if (!row)
|
|
1506
1561
|
throw new NotFoundError({ table: this.table, where: args.where });
|
|
1507
1562
|
return row;
|
|
@@ -1647,7 +1702,7 @@ export class PowqlInterface {
|
|
|
1647
1702
|
this.assertCompiledWhere(where, false, 'delete');
|
|
1648
1703
|
// `returning` hands back the deleted row(s) — no separate pre-image reselect needed.
|
|
1649
1704
|
const { rows, native } = await this.exec(`${this.qt} filter ${where} delete returning`, params, args.timeout, 'delete');
|
|
1650
|
-
const row = rows.length ? this.shape(rows, native)[0] : null;
|
|
1705
|
+
const row = rows.length ? this.stripWritePii(this.shape(rows, native)[0]) : null;
|
|
1651
1706
|
if (!row)
|
|
1652
1707
|
throw new NotFoundError({ table: this.table, where: args.where });
|
|
1653
1708
|
return row;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turbine-orm: aggregate / groupBy compilation (extracted from builder.ts)
|
|
3
|
+
*
|
|
4
|
+
* buildAggregate + buildGroupBy and their helpers (HAVING clauses, groupBy
|
|
5
|
+
* ordering, DISTINCT-ON sources, JSON-path aggregate targets). All functions
|
|
6
|
+
* take a {@link BuilderCtx} first argument; WHERE compilation is reused from
|
|
7
|
+
* where.ts (via `whereMod`), and the shared orderBy / row-parse primitives
|
|
8
|
+
* stay class-resident, reached through the ctx. See builder.ts for the thin
|
|
9
|
+
* delegating methods (buildGroupBy / buildAggregate).
|
|
10
|
+
*/
|
|
11
|
+
import type { DeferredQuery } from './deferred.js';
|
|
12
|
+
import type { AggregateArgs, AggregateResult, GroupByArgs, GroupByOrderBy, HavingClause, HavingFilter } from './types.js';
|
|
13
|
+
import type { BuilderCtx } from './where.js';
|
|
14
|
+
export declare function buildGroupBy<T extends object>(qi: BuilderCtx, args: GroupByArgs<T>): DeferredQuery<Record<string, unknown>[]>;
|
|
15
|
+
/**
|
|
16
|
+
* Compile a groupBy `orderBy` into an ORDER BY body. Unlike findMany ORDER BY
|
|
17
|
+
* ({@link buildOrderBy}, which validates keys against the table's physical
|
|
18
|
+
* columns), groupBy ordering targets the columns the RESULT actually
|
|
19
|
+
* contains: plain by-fields, JSON group-key aliases, and requested aggregates
|
|
20
|
+
* (`_count` / `_sum` / `_avg` / `_min` / `_max`). Each key re-emits the exact
|
|
21
|
+
* SELECT expression that produced it (`byOrderExprs` / `aggOrderExprs`),
|
|
22
|
+
* mirroring how HAVING re-emits aggregate expressions, so no dialect ever has
|
|
23
|
+
* to accept a SELECT-alias reference in ORDER BY, and any already-bound
|
|
24
|
+
* JSON-path placeholder is reused verbatim (ORDER BY is the last clause, so
|
|
25
|
+
* no `$n` renumbering). An aggregate key that was not requested, or an unknown
|
|
26
|
+
* by-key, throws {@link ValidationError} E003 listing the valid keys.
|
|
27
|
+
*/
|
|
28
|
+
export declare function buildGroupByOrderBy(qi: BuilderCtx, orderBy: GroupByOrderBy, byOrderExprs: Map<string, string>, aggOrderExprs: Map<string, string>): string;
|
|
29
|
+
/**
|
|
30
|
+
* Validate a JSON-path target (group key or aggregate target) in groupBy:
|
|
31
|
+
* the field must resolve to a real json/jsonb column and the path must be a
|
|
32
|
+
* non-empty array of keys/indexes. Returns the resolved snake_case column.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveJsonPathTarget(qi: BuilderCtx, context: string, field: string, path: (string | number)[]): string;
|
|
35
|
+
/**
|
|
36
|
+
* Build the `distinctOn` row source for groupBy (PostgreSQL only: other
|
|
37
|
+
* engines throw {@link UnsupportedFeatureError} E017):
|
|
38
|
+
*
|
|
39
|
+
* ```sql
|
|
40
|
+
* (SELECT DISTINCT ON ("c1") * FROM "table"<WHERE> ORDER BY "c1", <orderBy>) AS "table"
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* The wrapper is aliased as the table name so every outer expression (group
|
|
44
|
+
* keys, aggregates, HAVING, ORDER BY) is byte-identical to the plain path.
|
|
45
|
+
* `distinctOn.orderBy` is required (it decides which row survives) and
|
|
46
|
+
* supports plain columns, {@link OrderBySpec} nulls, and JSON-path specs;
|
|
47
|
+
* JSON paths push their text[] param here, after the WHERE params.
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildDistinctOnSource<T extends object>(qi: BuilderCtx, distinctOn: NonNullable<GroupByArgs<T>['distinctOn']>, whereSql: string, params: unknown[]): string;
|
|
50
|
+
/**
|
|
51
|
+
* Build the SQL fragments for a {@link HavingClause}.
|
|
52
|
+
*
|
|
53
|
+
* Each aggregate expression (`COUNT(*)`, `SUM("col")`, etc.) is constructed
|
|
54
|
+
* from a **schema-validated, quoted** column identifier: `qi.toColumn()`
|
|
55
|
+
* throws {@link ValidationError} for unknown fields and `qi.q()` quotes via
|
|
56
|
+
* the dialect, so no unvalidated identifier ever reaches the SQL string. Every
|
|
57
|
+
* comparison value is pushed onto the shared `params` array and referenced by
|
|
58
|
+
* a `$N` placeholder via {@link buildHavingNumericClauses} — there is no string
|
|
59
|
+
* interpolation of user values.
|
|
60
|
+
*
|
|
61
|
+
* `jsonAggExprs` (from {@link buildGroupBy}) maps `alias:aggKey` to the
|
|
62
|
+
* exact aggregate expression a JSON-path aggregate emitted in SELECT
|
|
63
|
+
* (including its already-bound path placeholder), so HAVING on a JSON-path
|
|
64
|
+
* aggregate alias reuses the same expression instead of resolving the alias
|
|
65
|
+
* as a column.
|
|
66
|
+
*/
|
|
67
|
+
export declare function buildHavingClauses<T extends object>(qi: BuilderCtx, having: HavingClause<T>, params: unknown[], jsonAggExprs?: Map<string, string>): string[];
|
|
68
|
+
/**
|
|
69
|
+
* Convert a single having filter into one or more parameterized SQL
|
|
70
|
+
* comparisons against the given aggregate expression. A bare number is
|
|
71
|
+
* shorthand for equality. Unknown operator keys throw {@link ValidationError}.
|
|
72
|
+
*/
|
|
73
|
+
export declare function buildHavingNumericClauses(qi: BuilderCtx, expr: string, filter: HavingFilter, params: unknown[]): string[];
|
|
74
|
+
export declare function buildAggregate<T extends object>(qi: BuilderCtx, args: AggregateArgs<T>): DeferredQuery<AggregateResult<T>>;
|