uql-orm 0.68.1 → 0.70.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/dist/browser/querier/httpQuerier.d.ts +7 -7
- package/dist/browser/type/clientQuerier.d.ts +5 -5
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +6 -6
- package/dist/cockroachdb/cockroachDialect.js +2 -2
- package/dist/dialect/abstractDialect.d.ts +8 -2
- package/dist/dialect/abstractDialect.js +17 -1
- package/dist/dialect/abstractSqlDialect.d.ts +42 -4
- package/dist/dialect/abstractSqlDialect.js +164 -48
- package/dist/dialect/aliases.d.ts +15 -4
- package/dist/dialect/aliases.js +15 -4
- package/dist/dialect/mysqlLikeSqlDialect.js +3 -2
- package/dist/dialect/pgLikeSqlDialect.js +1 -0
- package/dist/dialect/queryJoins.d.ts +8 -1
- package/dist/dialect/queryJoins.js +33 -10
- package/dist/entity/decorator/members.d.ts +44 -2
- package/dist/entity/decorator/members.js +0 -5
- package/dist/entity/metadata/definition.d.ts +8 -3
- package/dist/entity/metadata/definition.js +10 -3
- package/dist/migrate/codegen/entityCodeGenerator.js +4 -2
- package/dist/migrate/introspection/postgresIntrospector.d.ts +6 -0
- package/dist/migrate/introspection/postgresIntrospector.js +7 -1
- package/dist/migrate/migrator.d.ts +2 -1
- package/dist/migrate/migrator.js +5 -3
- package/dist/mongo/mongoDialect.d.ts +49 -19
- package/dist/mongo/mongoDialect.js +238 -81
- package/dist/mongo/mongodbQuerier.d.ts +2 -4
- package/dist/mongo/mongodbQuerier.js +16 -14
- package/dist/mssql/mssqlDialect.js +3 -2
- package/dist/postgres/postgresDialect.js +2 -2
- package/dist/querier/abstractQuerier.d.ts +16 -11
- package/dist/querier/abstractQuerier.js +28 -8
- package/dist/querier/abstractQuerierPool.d.ts +9 -9
- package/dist/querier/abstractSqlQuerier.d.ts +1 -1
- package/dist/querier/abstractSqlQuerier.js +3 -3
- package/dist/sqlite/sqliteDialect.js +1 -0
- package/dist/turso/tursoDialect.d.ts +1 -1
- package/dist/turso/tursoDialect.js +6 -2
- package/dist/type/dialect.d.ts +35 -2
- package/dist/type/entity.d.ts +120 -4
- package/dist/type/migration.d.ts +7 -0
- package/dist/type/query.d.ts +7 -10
- package/dist/type/queryAggregate.d.ts +77 -42
- package/dist/type/queryAggregate.js +4 -21
- package/dist/type/queryRaw.d.ts +19 -1
- package/dist/type/queryRaw.js +18 -0
- package/dist/type/universalQuerier.d.ts +9 -9
- package/dist/util/dialect.util.d.ts +6 -2
- package/dist/util/dialect.util.js +21 -7
- package/dist/util/field.util.d.ts +15 -1
- package/dist/util/field.util.js +18 -1
- package/dist/util/object.util.d.ts +1 -4
- package/dist/util/object.util.js +0 -3
- package/dist/util/raw.d.ts +2 -2
- package/dist/util/raw.js +29 -2
- package/package.json +1 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { fieldOf, getMeta, relationOf, soleIdOf } from '../entity/index.js';
|
|
2
2
|
import { COUNT_RESULT_KEY, parseQueryLock, QueryRaw, RAW_ALIAS, VECTOR_QUERY_KEYS, } from '../type/index.js';
|
|
3
3
|
import { isInlinedExpression } from '../util/field.util.js';
|
|
4
|
-
import { asSelectMap, assertNonNegativeInteger, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, columnFamily, countedRelations, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorKey, isVectorSearch, normalizeScalarFieldSelection, parentJoins, targetKeyColumns, parseGroupMap, parseRelationAtKey, parseRelationSize, parseSortByCount, populatesRelations, raw, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
4
|
+
import { asSelectMap, assertNonNegativeInteger, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, columnFamily, countedRelations, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorKey, isVectorSearch, normalizeScalarFieldSelection, parentJoins, targetKeyColumns, parseGroupMap, parseRelationAtKey, parseRelationSize, parseSortByCount, populatesRelations, aggregateOf, raw, refs, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
5
5
|
import { escapeAnsiSqlLiteral } from '../util/sqlLiteral.js';
|
|
6
|
-
import {
|
|
6
|
+
import { AGGREGATE_PAGE_ALIAS, AGGREGATE_VALUE_ALIAS, ROWS_ALIAS, JSON_ELEM_ALIAS, JSON_PULL_ALIAS, relationSortColumn, } from './aliases.js';
|
|
7
7
|
import { holdsOperator, isJsonScalar, jsonCompareMode, jsonElemExists, jsonPath, } from './jsonSql.js';
|
|
8
8
|
import { SqlQueryContext } from './queryContext.js';
|
|
9
|
-
import { NO_JOINS, resolveQueryJoins, resolveSortableJoin, } from './queryJoins.js';
|
|
9
|
+
import { groupPathField, NO_JOINS, resolveGroupJoins, resolveQueryJoins, resolveSortableJoin, } from './queryJoins.js';
|
|
10
10
|
import { resolveVectorCast } from './vectorCast.js';
|
|
11
11
|
import { VectorSqlDialect } from './vectorSqlDialect.js';
|
|
12
12
|
/** The key a term answers under in a populated relation's row, which a raw expression has only once aliased. */
|
|
@@ -795,7 +795,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
795
795
|
}
|
|
796
796
|
columns.push({
|
|
797
797
|
key: keyPath,
|
|
798
|
-
expr: this.buildFragment(ctx, (fragmentCtx) => this.appendRelationSubquery(fragmentCtx, meta, key, relation, { prefix }, '
|
|
798
|
+
expr: this.buildFragment(ctx, (fragmentCtx) => this.appendRelationSubquery(fragmentCtx, meta, key, relation, { prefix }, { op: '$count' })),
|
|
799
799
|
direction: this.resolveSortDirection(countDirection),
|
|
800
800
|
output: false,
|
|
801
801
|
});
|
|
@@ -916,7 +916,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
916
916
|
count(ctx, entity, q, opts) {
|
|
917
917
|
const { $where, $skip, $limit } = q;
|
|
918
918
|
if ($skip === undefined && $limit === undefined) {
|
|
919
|
-
this.select(ctx, entity, { $select: [raw `COUNT(*)`.as(
|
|
919
|
+
this.select(ctx, entity, { $select: [raw `COUNT(*)`.as(AGGREGATE_VALUE_ALIAS)] });
|
|
920
920
|
this.search(ctx, entity, { $where }, opts);
|
|
921
921
|
return;
|
|
922
922
|
}
|
|
@@ -936,9 +936,9 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
936
936
|
}
|
|
937
937
|
/** `SELECT COUNT(*)` over the rows `rows` appends, as a derived table. */
|
|
938
938
|
countRows(ctx, rows) {
|
|
939
|
-
ctx.append(`SELECT COUNT(*) ${this.escapeId(
|
|
939
|
+
ctx.append(`SELECT COUNT(*) ${this.escapeId(AGGREGATE_VALUE_ALIAS, true)} FROM (`);
|
|
940
940
|
rows();
|
|
941
|
-
ctx.append(`) ${this.escapeId(
|
|
941
|
+
ctx.append(`) ${this.escapeId(ROWS_ALIAS, true)}`);
|
|
942
942
|
}
|
|
943
943
|
/**
|
|
944
944
|
* The statistic the engine already keeps, as a `count` column. Overridden by the dialects that
|
|
@@ -958,35 +958,40 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
958
958
|
};
|
|
959
959
|
aggregate(ctx, entity, q, opts = {}) {
|
|
960
960
|
const meta = getMeta(entity);
|
|
961
|
-
const
|
|
961
|
+
const entries = parseGroupMap(q.$group, q.$select);
|
|
962
|
+
if (!entries.length) {
|
|
963
|
+
throw new TypeError('aggregate requires at least one $group column or $select function');
|
|
964
|
+
}
|
|
965
|
+
const table = this.tableRef(meta, this.readOptions(ctx, meta).alias);
|
|
966
|
+
const joins = resolveGroupJoins(meta, q.$group, (path) => ctx.claimAlias(path));
|
|
967
|
+
const prefix = joins.size ? table.alias : undefined;
|
|
968
|
+
const reads = entries.map((entry) => ({ entry, value: this.aggregateValue(ctx, entity, entry, joins, prefix) }));
|
|
969
|
+
// Only bare columns are read inline: SQL Server refuses a subquery inside an aggregate or a
|
|
970
|
+
// `GROUP BY`, and a filter's bound values would repeat in `HAVING`. A derived table answers by alias.
|
|
971
|
+
const derived = reads.some(({ value }) => !value.bare);
|
|
972
|
+
const named = (sql, alias) => sql === this.escapeId(alias) ? sql : `${sql} ${this.escapeId(alias)}`;
|
|
962
973
|
const groupKeys = [];
|
|
963
974
|
const selectParts = [];
|
|
964
975
|
// Every column the statement emits, mapped to the SQL that references it. `$having` and `$sort`
|
|
965
976
|
// may name these and nothing else, so one map answers both "is this legal?" and "what do I
|
|
966
977
|
// emit for it?".
|
|
967
978
|
const emittedColumns = {};
|
|
968
|
-
for (const entry of
|
|
979
|
+
for (const { entry, value } of reads) {
|
|
980
|
+
const column = derived ? this.escapeId(entry.alias) : value.sql;
|
|
981
|
+
const expr = entry.kind === 'key' ? column : this.aggregateFn(entry.op, value.sql === '*' ? '*' : column, entry.distinct);
|
|
969
982
|
if (entry.kind === 'key') {
|
|
970
|
-
|
|
971
|
-
const columnName = this.resolveColumnName(entry.alias, field);
|
|
972
|
-
const escaped = this.escapeId(columnName);
|
|
973
|
-
groupKeys.push(escaped);
|
|
974
|
-
emittedColumns[entry.alias] = escaped;
|
|
975
|
-
selectParts.push(columnName !== entry.alias ? `${escaped} ${this.escapeId(entry.alias)}` : escaped);
|
|
976
|
-
}
|
|
977
|
-
else {
|
|
978
|
-
const sqlFn = AbstractSqlDialect.AGGREGATE_FN[entry.op];
|
|
979
|
-
const sqlArg = entry.fieldRef === '*' ? '*' : this.escapeId(this.columnOf(meta, entry.fieldRef));
|
|
980
|
-
const expr = `${sqlFn}(${entry.distinct ? 'DISTINCT ' : ''}${sqlArg})`;
|
|
981
|
-
emittedColumns[entry.alias] = expr;
|
|
982
|
-
selectParts.push(`${expr} ${this.escapeId(entry.alias)}`);
|
|
983
|
+
groupKeys.push(expr);
|
|
983
984
|
}
|
|
985
|
+
emittedColumns[entry.alias] = expr;
|
|
986
|
+
selectParts.push(named(expr, entry.alias));
|
|
984
987
|
}
|
|
985
|
-
|
|
986
|
-
|
|
988
|
+
const columns = reads.flatMap(({ entry, value }) => (value.sql === '*' ? [] : [named(value.sql, entry.alias)]));
|
|
989
|
+
ctx.append(`SELECT ${selectParts.join(', ')} FROM ${derived ? `(SELECT ${columns.join(', ')} FROM ` : ''}${table.ref}`);
|
|
990
|
+
this.selectRelationJoins(ctx, meta, table.alias, joins);
|
|
991
|
+
this.where(ctx, entity, q.$where, { ...opts, prefix });
|
|
992
|
+
if (derived) {
|
|
993
|
+
ctx.append(`) ${this.escapeId(ROWS_ALIAS, true)}`);
|
|
987
994
|
}
|
|
988
|
-
ctx.append(`SELECT ${selectParts.join(', ')} FROM ${tableName}`);
|
|
989
|
-
this.where(ctx, entity, q.$where, opts);
|
|
990
995
|
if (groupKeys.length) {
|
|
991
996
|
ctx.append(` GROUP BY ${groupKeys.join(', ')}`);
|
|
992
997
|
}
|
|
@@ -996,6 +1001,34 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
996
1001
|
const sorted = this.aggregateSort(ctx, q.$sort, emittedColumns);
|
|
997
1002
|
this.pager(ctx, q, sorted);
|
|
998
1003
|
}
|
|
1004
|
+
/**
|
|
1005
|
+
* What one entry reads: a grouped field, through the join its path passes, or an aggregate's argument,
|
|
1006
|
+
* narrowed by its own `$where` to `CASE WHEN … THEN … END`. Bare where it is a column or `'*'`.
|
|
1007
|
+
*/
|
|
1008
|
+
aggregateValue(ctx, entity, entry, joins, prefix) {
|
|
1009
|
+
if (entry.kind === 'key') {
|
|
1010
|
+
const { key, join } = groupPathField(joins, entry.path);
|
|
1011
|
+
return join
|
|
1012
|
+
? this.aggregateOperand(ctx, join.entity, key, join.alias)
|
|
1013
|
+
: this.aggregateOperand(ctx, entity, key, prefix);
|
|
1014
|
+
}
|
|
1015
|
+
const arg = entry.fieldRef === '*' ? undefined : this.aggregateOperand(ctx, entity, entry.fieldRef, prefix);
|
|
1016
|
+
const { where } = entry;
|
|
1017
|
+
const condition = where &&
|
|
1018
|
+
this.buildFragment(ctx, (fragment) => this.renderWhere(fragment, entity, where, { clause: false, prefix }));
|
|
1019
|
+
if (!condition) {
|
|
1020
|
+
return arg ?? { sql: '*', bare: true };
|
|
1021
|
+
}
|
|
1022
|
+
return { sql: `CASE WHEN ${condition} THEN ${arg?.sql ?? '1'} END`, bare: false };
|
|
1023
|
+
}
|
|
1024
|
+
/** A field as an aggregate reads it: its column, or the expression an inlined one stands for. */
|
|
1025
|
+
aggregateOperand(ctx, entity, key, prefix) {
|
|
1026
|
+
const field = getMeta(entity).fields[key];
|
|
1027
|
+
return {
|
|
1028
|
+
sql: this.resolveOperandField(ctx, entity, key, { prefix }),
|
|
1029
|
+
bare: field === undefined || !isInlinedExpression(field),
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
999
1032
|
/**
|
|
1000
1033
|
* ORDER BY for aggregate queries - handles both entity-field and alias references. A grouped
|
|
1001
1034
|
* statement has no joins to address, so a relation key is rejected rather than emitted as an alias
|
|
@@ -1296,7 +1329,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1296
1329
|
}
|
|
1297
1330
|
const decoded = [];
|
|
1298
1331
|
for (const [key, field] of Object.entries(meta.fields)) {
|
|
1299
|
-
const kind = this.
|
|
1332
|
+
const kind = this.fieldKind(meta, field);
|
|
1300
1333
|
if (kind) {
|
|
1301
1334
|
decoded.push([key, kind]);
|
|
1302
1335
|
}
|
|
@@ -1304,28 +1337,46 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1304
1337
|
this.hydratable.set(entity, [meta.revision, decoded]);
|
|
1305
1338
|
return decoded;
|
|
1306
1339
|
}
|
|
1307
|
-
/**
|
|
1308
|
-
* The same for an aggregate's row, per query: a count or total is a number however the engine widened
|
|
1309
|
-
* it, and `$min`/`$max` or a grouped column decodes as its field does.
|
|
1310
|
-
*/
|
|
1340
|
+
/** The same for an aggregate's row, per query: each alias decodes by {@link aggregateKind}. */
|
|
1311
1341
|
hydratableAggregates(entity, q) {
|
|
1312
|
-
const
|
|
1342
|
+
const meta = getMeta(entity);
|
|
1343
|
+
const joins = resolveGroupJoins(meta, q.$group);
|
|
1313
1344
|
const decoded = [];
|
|
1314
1345
|
for (const entry of parseGroupMap(q.$group, q.$select)) {
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
}
|
|
1319
|
-
// `$min`/`$max` read the field they aggregate; a `$group` column is that field. Only `$count`
|
|
1320
|
-
// takes `'*'`, and it went down the numeric path above, so there is always a field to look up.
|
|
1321
|
-
const key = entry.kind === 'fn' ? entry.fieldRef : entry.alias;
|
|
1322
|
-
const kind = this.hydrateKind(fields[key]);
|
|
1346
|
+
const kind = entry.kind === 'fn'
|
|
1347
|
+
? this.aggregateKind(entry.op, this.fieldKind(meta, meta.fields[entry.fieldRef]))
|
|
1348
|
+
: this.groupedKind(meta, joins, entry.path);
|
|
1323
1349
|
if (kind) {
|
|
1324
1350
|
decoded.push([entry.alias, kind]);
|
|
1325
1351
|
}
|
|
1326
1352
|
}
|
|
1327
1353
|
return decoded;
|
|
1328
1354
|
}
|
|
1355
|
+
/**
|
|
1356
|
+
* What an aggregate's value decodes as, which the engine widens beyond the column it read: a tally
|
|
1357
|
+
* (`COUNT` to Postgres's `bigint`) and a mean (`AVG` to `numeric`) are numbers whatever they counted,
|
|
1358
|
+
* and the rest read as the column they aggregate - so a `SUM` over a wide integer stays exact rather
|
|
1359
|
+
* than rounding through a float, which is what every driver's BIGINT decoding promises.
|
|
1360
|
+
*
|
|
1361
|
+
* One rule for both readers: a relation aggregate a field declares, and an aggregate a query names.
|
|
1362
|
+
*/
|
|
1363
|
+
aggregateKind(op, fieldKind) {
|
|
1364
|
+
return op === '$count' || op === '$avg' ? 'number' : fieldKind;
|
|
1365
|
+
}
|
|
1366
|
+
/** What a grouped path decodes as: the kind of the field it reaches, through its join or on the entity. */
|
|
1367
|
+
groupedKind(meta, joins, path) {
|
|
1368
|
+
const { key, join } = groupPathField(joins, path);
|
|
1369
|
+
return join ? this.fieldKind(join.meta, join.meta.fields[key]) : this.fieldKind(meta, meta.fields[key]);
|
|
1370
|
+
}
|
|
1371
|
+
/** What a field decodes as: its column's kind, or a relation aggregate's {@link aggregateKind} over the target's column. */
|
|
1372
|
+
fieldKind(meta, field) {
|
|
1373
|
+
const spec = aggregateOf(field);
|
|
1374
|
+
if (!spec) {
|
|
1375
|
+
return this.hydrateKind(field);
|
|
1376
|
+
}
|
|
1377
|
+
const target = getMeta(relationOf(meta, spec.relation).entity());
|
|
1378
|
+
return this.aggregateKind(spec.op, spec.field ? this.fieldKind(target, target.fields[spec.field]) : undefined);
|
|
1379
|
+
}
|
|
1329
1380
|
/** What one column decodes as, the inverse of {@link persistKind}. `BigInt` first, since it shares the numeric family. */
|
|
1330
1381
|
hydrateKind(field) {
|
|
1331
1382
|
const type = field?.type;
|
|
@@ -1382,8 +1433,6 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1382
1433
|
* `$pull` reads the column and every later one its expression once, so no value binds twice.
|
|
1383
1434
|
*/
|
|
1384
1435
|
formatJsonUpdate(ctx, escapedCol, value, field) {
|
|
1385
|
-
// Centralizes the one narrowing cast: the payload's keys are typed against the entity's JSON
|
|
1386
|
-
// payload, which the dialects do not need - they only build SQL from keys and values.
|
|
1387
1436
|
const { $pull, $set, $push, $unset } = value;
|
|
1388
1437
|
let expr = escapedCol;
|
|
1389
1438
|
if (hasKeys($pull)) {
|
|
@@ -1519,14 +1568,18 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1519
1568
|
* invisible here just as it is to a joined `$populate`. The caller's filter bypass is deliberately
|
|
1520
1569
|
* not propagated (`withDeleted()` does not reach into relations), matching `selectRelationJoins`.
|
|
1521
1570
|
*/
|
|
1522
|
-
appendRelationSubquery(ctx, meta, relKey, rel, opts,
|
|
1571
|
+
appendRelationSubquery(ctx, meta, relKey, rel, opts, read) {
|
|
1523
1572
|
const relatedEntity = rel.entity();
|
|
1524
1573
|
const relatedMeta = getMeta(relatedEntity);
|
|
1525
1574
|
const parent = opts.prefix ?? this.resolveTableAlias(meta);
|
|
1526
1575
|
// Resolved before any SQL is emitted: it also decides whether the junction form reaches the target.
|
|
1527
|
-
const targetWhere = this.scopedWhere(relatedMeta,
|
|
1528
|
-
ctx.append(`(SELECT ${projection} FROM `);
|
|
1576
|
+
const targetWhere = this.scopedWhere(relatedMeta, read.query?.$where ?? {});
|
|
1529
1577
|
if (rel.through) {
|
|
1578
|
+
// The rows here are the junction's own, so a column of the far side is read as a page instead.
|
|
1579
|
+
if (read.field) {
|
|
1580
|
+
throw new TypeError(`cannot read ${read.op}('${read.field}') over the many-to-many '${relKey}' without a page: its rows are the junction's, so name a '$sort' and a '$limit' to read the target's own`);
|
|
1581
|
+
}
|
|
1582
|
+
ctx.append(`(SELECT ${read.op === 'exists' ? '1' : 'COUNT(*)'} FROM `);
|
|
1530
1583
|
const junction = this.junctionRows(ctx, meta, rel, rel.through(), parent);
|
|
1531
1584
|
ctx.append(junction.from);
|
|
1532
1585
|
if (hasKeys(targetWhere)) {
|
|
@@ -1539,13 +1592,76 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1539
1592
|
}
|
|
1540
1593
|
}
|
|
1541
1594
|
else {
|
|
1595
|
+
// The alias is claimed before the SELECT is written, since an aggregate names a column of it.
|
|
1542
1596
|
const related = this.tableRef(relatedMeta, ctx.claimAlias(relKey, parent));
|
|
1597
|
+
ctx.append(`(SELECT ${this.aggregateProjection(read, related.alias, relatedMeta)} FROM `);
|
|
1543
1598
|
ctx.append(related.ref);
|
|
1544
1599
|
ctx.append(` WHERE ${this.correlation(meta, rel, parent, related.alias, relatedMeta)}`);
|
|
1545
1600
|
this.renderWhere(ctx, relatedEntity, targetWhere, { prefix: related.alias, clause: 'AND' });
|
|
1546
1601
|
}
|
|
1547
1602
|
ctx.append(')');
|
|
1548
1603
|
}
|
|
1604
|
+
/**
|
|
1605
|
+
* What a relation subquery selects: the literals a relation operator reads, or an aggregate over one
|
|
1606
|
+
* of the target's columns. `count` and `sum` answer `0` on a parent with no rows, which is what makes
|
|
1607
|
+
* them the two a trigger could keep; the rest answer `NULL`, and the field's type says so.
|
|
1608
|
+
*/
|
|
1609
|
+
aggregateProjection(projection, alias, relatedMeta) {
|
|
1610
|
+
if (projection.op === 'exists') {
|
|
1611
|
+
return '1';
|
|
1612
|
+
}
|
|
1613
|
+
return this.aggregateCall(projection.op, projection.field ? this.escapedColumn(alias, relatedMeta, projection.field) : '');
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* A relation aggregate as the correlated subquery a `computed` field reads, the same one `$count`
|
|
1617
|
+
* emits: `(user) => user.resources.count()` renders here, wherever the field is named.
|
|
1618
|
+
*/
|
|
1619
|
+
appendRelationAggregate(ctx, entity, aggregate, prefix) {
|
|
1620
|
+
const meta = getMeta(entity);
|
|
1621
|
+
const rel = relationOf(meta, aggregate.relation);
|
|
1622
|
+
const parent = prefix || this.resolveTableAlias(meta);
|
|
1623
|
+
const { $limit, $skip } = aggregate.query ?? {};
|
|
1624
|
+
if ($limit === undefined && $skip === undefined) {
|
|
1625
|
+
this.appendRelationSubquery(ctx, meta, aggregate.relation, rel, { prefix: parent }, aggregate);
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
// Capped: the rows it reads are a page of the relation, so they are read first - ordered, since an
|
|
1629
|
+
// order is what picks them - and the aggregate runs over that page.
|
|
1630
|
+
const pageAlias = this.escapeId(AGGREGATE_PAGE_ALIAS);
|
|
1631
|
+
const value = this.escapeId(AGGREGATE_VALUE_ALIAS);
|
|
1632
|
+
ctx.append(`(SELECT ${this.aggregateCall(aggregate.op, `${pageAlias}.${value}`)} FROM (`);
|
|
1633
|
+
this.appendRelationPage(ctx, meta, rel, aggregate, parent);
|
|
1634
|
+
ctx.append(`) ${pageAlias})`);
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1637
|
+
* The page a capped aggregate reads: an ordinary read of the related entity under its own alias,
|
|
1638
|
+
* narrowed to the parent's rows, carrying out the one column the aggregate runs over. Its `$sort`,
|
|
1639
|
+
* `$limit` and `$skip` are the relation's own, and its filters apply as they do to any read.
|
|
1640
|
+
*/
|
|
1641
|
+
appendRelationPage(ctx, meta, rel, aggregate, parent) {
|
|
1642
|
+
const entity = rel.entity();
|
|
1643
|
+
const alias = ctx.claimAlias(aggregate.relation, parent);
|
|
1644
|
+
const correlation = raw(({ ctx: pageCtx }) => this.appendCorrelation(pageCtx, meta, rel, parent, alias));
|
|
1645
|
+
const { $where, ...page } = aggregate.query ?? {};
|
|
1646
|
+
// `1` where nothing is aggregated: a tally counts the rows the page holds, whatever they carry.
|
|
1647
|
+
const read = aggregate.field ? refs(entity)[aggregate.field] : raw `1`;
|
|
1648
|
+
const query = {
|
|
1649
|
+
...page,
|
|
1650
|
+
$select: [read.as(AGGREGATE_VALUE_ALIAS)],
|
|
1651
|
+
$where: { ...$where, $and: [...($where?.$and ?? []), correlation] },
|
|
1652
|
+
};
|
|
1653
|
+
const joins = resolveQueryJoins(getMeta(entity), query, (path) => ctx.claimAlias(path));
|
|
1654
|
+
this.read(ctx, entity, query, { alias }, joins);
|
|
1655
|
+
}
|
|
1656
|
+
/** A relation aggregate over an operand, reading `0` on a parent with no rows where its type says so. */
|
|
1657
|
+
aggregateCall(op, operand) {
|
|
1658
|
+
const call = this.aggregateFn(op, op === '$count' ? '*' : operand);
|
|
1659
|
+
return op === '$sum' ? `COALESCE(${call}, 0)` : call;
|
|
1660
|
+
}
|
|
1661
|
+
/** One aggregate function call, the one spelling every statement that aggregates writes. */
|
|
1662
|
+
aggregateFn(op, operand, distinct) {
|
|
1663
|
+
return `${AbstractSqlDialect.AGGREGATE_FN[op]}(${distinct ? 'DISTINCT ' : ''}${operand})`;
|
|
1664
|
+
}
|
|
1549
1665
|
/**
|
|
1550
1666
|
* One equality per key of the parent, anded: a composite correlates on every column, and matching on
|
|
1551
1667
|
* part of one would find the rows of a different parent. `parentJoins` keeps the two ends the right
|
|
@@ -1571,7 +1687,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1571
1687
|
/** Each relation a read's `$count` tallies, as a correlated count of its select list. */
|
|
1572
1688
|
selectRelationCounts(ctx, meta, count, parent) {
|
|
1573
1689
|
return countedRelations(meta, count).map(({ relKey, relation, where }) => {
|
|
1574
|
-
const sql = this.buildFragment(ctx, (fragmentCtx) => this.appendRelationSubquery(fragmentCtx, meta, relKey, relation, { prefix: parent }, '
|
|
1690
|
+
const sql = this.buildFragment(ctx, (fragmentCtx) => this.appendRelationSubquery(fragmentCtx, meta, relKey, relation, { prefix: parent }, { op: '$count', query: { $where: where } }));
|
|
1575
1691
|
return { sql, key: `${COUNT_RESULT_KEY}.${relKey}` };
|
|
1576
1692
|
});
|
|
1577
1693
|
}
|
|
@@ -1670,11 +1786,11 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1670
1786
|
/** Filter by relation: a parent matches when {@link appendRelationSubquery} finds one target row. */
|
|
1671
1787
|
compareRelation(ctx, entity, relKey, val, rel, opts) {
|
|
1672
1788
|
ctx.append('EXISTS ');
|
|
1673
|
-
this.appendRelationSubquery(ctx, getMeta(entity), relKey, rel, opts, '
|
|
1789
|
+
this.appendRelationSubquery(ctx, getMeta(entity), relKey, rel, opts, { op: 'exists', query: { $where: val } });
|
|
1674
1790
|
}
|
|
1675
1791
|
/** Filter by relation size: the same subquery, counting instead of testing for existence. */
|
|
1676
1792
|
compareRelationSize(ctx, entity, relKey, sizeVal, rel, opts) {
|
|
1677
|
-
const count = (fragmentCtx) => this.appendRelationSubquery(fragmentCtx, getMeta(entity), relKey, rel, opts, '
|
|
1793
|
+
const count = (fragmentCtx) => this.appendRelationSubquery(fragmentCtx, getMeta(entity), relKey, rel, opts, { op: '$count' });
|
|
1678
1794
|
ctx.append(this.sizeCondition(ctx, count, sizeVal));
|
|
1679
1795
|
}
|
|
1680
1796
|
/**
|
|
@@ -1,9 +1,20 @@
|
|
|
1
|
-
/** The column every internally-built count answers in: `COUNT(*)`, a grouped tally, a `$count` stage. */
|
|
2
|
-
export declare const COUNT_ALIAS = "_uql_count";
|
|
3
1
|
/** The column a paged read carries its own unpaged total in, from `COUNT(*) OVER ()`. */
|
|
4
2
|
export declare const TOTAL_ALIAS = "_uql_total";
|
|
5
|
-
/**
|
|
6
|
-
|
|
3
|
+
/**
|
|
4
|
+
* The derived table a statement wraps the rows it counts or aggregates in: a page, a `$distinct` set, or
|
|
5
|
+
* rows computing an inlined field an aggregate names. MySQL requires the alias.
|
|
6
|
+
*/
|
|
7
|
+
export declare const ROWS_ALIAS = "_uql_rows";
|
|
8
|
+
/** The derived table a capped relation aggregate reads: the page is taken first, then aggregated over. */
|
|
9
|
+
export declare const AGGREGATE_PAGE_ALIAS = "_uql_page";
|
|
10
|
+
/**
|
|
11
|
+
* The one value a statement that aggregates answers under: a `COUNT(*)` or an estimate, the column a
|
|
12
|
+
* capped page carries out for the aggregate wrapping it, and the field a MongoDB `$group` or `$count`
|
|
13
|
+
* leaves its result in. One name, since both ends of each are written and read here.
|
|
14
|
+
*/
|
|
15
|
+
export declare const AGGREGATE_VALUE_ALIAS = "_uql_value";
|
|
16
|
+
/** Beside each MongoDB `$sum`, how many values it read: `$sum` answers 0 over none, where SQL answers null. */
|
|
17
|
+
export declare const SUM_COUNT_ALIAS = "_uql_count";
|
|
7
18
|
/** The row a Postgres relation aggregates whole: a LATERAL projection of the columns it answers under. */
|
|
8
19
|
export declare const RELATION_ROW_ALIAS = "_uql_row";
|
|
9
20
|
/** The alias an exploded JSON array element is read through, `_uql_elem_2` and on where one nests in another. */
|
package/dist/dialect/aliases.js
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
// Every identifier UQL invents, `_uql`-prefixed to stay off a user's own, collected in one place:
|
|
2
2
|
// the ends writing and reading one sit in different modules, and a drift between them fails silently.
|
|
3
|
-
/** The column every internally-built count answers in: `COUNT(*)`, a grouped tally, a `$count` stage. */
|
|
4
|
-
export const COUNT_ALIAS = '_uql_count';
|
|
5
3
|
/** The column a paged read carries its own unpaged total in, from `COUNT(*) OVER ()`. */
|
|
6
4
|
export const TOTAL_ALIAS = '_uql_total';
|
|
7
|
-
/**
|
|
8
|
-
|
|
5
|
+
/**
|
|
6
|
+
* The derived table a statement wraps the rows it counts or aggregates in: a page, a `$distinct` set, or
|
|
7
|
+
* rows computing an inlined field an aggregate names. MySQL requires the alias.
|
|
8
|
+
*/
|
|
9
|
+
export const ROWS_ALIAS = '_uql_rows';
|
|
10
|
+
/** The derived table a capped relation aggregate reads: the page is taken first, then aggregated over. */
|
|
11
|
+
export const AGGREGATE_PAGE_ALIAS = '_uql_page';
|
|
12
|
+
/**
|
|
13
|
+
* The one value a statement that aggregates answers under: a `COUNT(*)` or an estimate, the column a
|
|
14
|
+
* capped page carries out for the aggregate wrapping it, and the field a MongoDB `$group` or `$count`
|
|
15
|
+
* leaves its result in. One name, since both ends of each are written and read here.
|
|
16
|
+
*/
|
|
17
|
+
export const AGGREGATE_VALUE_ALIAS = '_uql_value';
|
|
18
|
+
/** Beside each MongoDB `$sum`, how many values it read: `$sum` answers 0 over none, where SQL answers null. */
|
|
19
|
+
export const SUM_COUNT_ALIAS = '_uql_count';
|
|
9
20
|
/** The row a Postgres relation aggregates whole: a LATERAL projection of the columns it answers under. */
|
|
10
21
|
export const RELATION_ROW_ALIAS = '_uql_row';
|
|
11
22
|
/** The alias an exploded JSON array element is read through, `_uql_elem_2` and on where one nests in another. */
|
|
@@ -2,7 +2,7 @@ import { getMeta } from '../entity/index.js';
|
|
|
2
2
|
import { textSearchFields } from '../util/index.js';
|
|
3
3
|
import { escapeMysqlSqlLiteral, escapeSingleQuotes } from '../util/sqlLiteral.js';
|
|
4
4
|
import { AbstractSqlDialect, } from './abstractSqlDialect.js';
|
|
5
|
-
import {
|
|
5
|
+
import { AGGREGATE_VALUE_ALIAS } from './aliases.js';
|
|
6
6
|
import { BYTES_PREFIX } from './hydrateColumn.js';
|
|
7
7
|
import { jsonSetCall, jsonPath, jsonRemoveCall, jsonSetTarget } from './jsonSql.js';
|
|
8
8
|
import { aggregatesRelations } from './queryJoins.js';
|
|
@@ -27,6 +27,7 @@ export const MYSQL_FEATURES = {
|
|
|
27
27
|
stringSizing: 'varchar',
|
|
28
28
|
supportsUnsigned: true,
|
|
29
29
|
serverSideCursors: false,
|
|
30
|
+
correlatedWrites: true,
|
|
30
31
|
rowLocks: true,
|
|
31
32
|
rowLockWithWindow: true,
|
|
32
33
|
rowLockOf: true,
|
|
@@ -49,7 +50,7 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
49
50
|
estimatedCount(ctx, entity) {
|
|
50
51
|
const meta = getMeta(entity);
|
|
51
52
|
const schema = this.resolveSchema(meta);
|
|
52
|
-
ctx.append(`SELECT TABLE_ROWS ${this.escapeId(
|
|
53
|
+
ctx.append(`SELECT TABLE_ROWS ${this.escapeId(AGGREGATE_VALUE_ALIAS, true)} FROM information_schema.TABLES WHERE TABLE_SCHEMA = `);
|
|
53
54
|
if (schema) {
|
|
54
55
|
ctx.addValue(schema);
|
|
55
56
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EntityMeta, Query, QuerySortMap, RelationMeta, RelationQuery, Type } from '../type/index.js';
|
|
1
|
+
import type { EntityMeta, Query, QueryGroupMap, QuerySortMap, RelationMeta, RelationQuery, Type } from '../type/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* One relation a statement joins, keyed by the alias its columns are addressed by (`tax`,
|
|
4
4
|
* `tax.category`). `projected` tells a `$populate` join, whose columns are selected, from one only
|
|
@@ -39,6 +39,13 @@ export type QuerySortOptions = {
|
|
|
39
39
|
* columns, the `ORDER BY` and the lock agree. `claimAlias` names each join's table, parents first.
|
|
40
40
|
*/
|
|
41
41
|
export declare function resolveQueryJoins<E>(meta: EntityMeta<E>, q: Query<E>, claimAlias?: (path: string) => string): QueryJoins;
|
|
42
|
+
/** What an aggregate joins: each to-one relation a `$group` path passes through. */
|
|
43
|
+
export declare function resolveGroupJoins<E>(meta: EntityMeta<E>, group: QueryGroupMap<E> | undefined, claimAlias?: (path: string) => string): QueryJoins;
|
|
44
|
+
/** The field a grouped `path` reads, and the join it reads it through: none for the entity's own. */
|
|
45
|
+
export declare function groupPathField(joins: QueryJoins, path: readonly string[]): {
|
|
46
|
+
readonly key: string;
|
|
47
|
+
readonly join: QueryJoin | undefined;
|
|
48
|
+
};
|
|
42
49
|
/**
|
|
43
50
|
* Whether a join drops parents that have no match, which is the one thing a join does to *how many*
|
|
44
51
|
* rows a read returns rather than how wide they are. A count that skips the joins has to be told, or
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getMeta, relationOf } from '../entity/index.js';
|
|
2
|
-
import { getKeys, getRelationRequestSummary, isToManyRelation, parseRelationAtKey } from '../util/index.js';
|
|
2
|
+
import { getKeys, getRelationRequestSummary, isRecord, isToManyRelation, parseRelationAtKey } from '../util/index.js';
|
|
3
3
|
export const NO_JOINS = new Map();
|
|
4
4
|
/**
|
|
5
5
|
* What the statement joins, from `$populate` and from a `$sort` by a to-one relation's field, so the
|
|
@@ -11,9 +11,31 @@ export function resolveQueryJoins(meta, q, claimAlias = (path) => path) {
|
|
|
11
11
|
}
|
|
12
12
|
const joins = new Map();
|
|
13
13
|
addPopulateJoins(joins, claimAlias, meta, q.$populate);
|
|
14
|
-
|
|
14
|
+
addPathJoins(joins, claimAlias, meta, q.$sort);
|
|
15
15
|
return joins;
|
|
16
16
|
}
|
|
17
|
+
/** What an aggregate joins: each to-one relation a `$group` path passes through. */
|
|
18
|
+
export function resolveGroupJoins(meta, group, claimAlias = (path) => path) {
|
|
19
|
+
const joins = new Map();
|
|
20
|
+
for (const ref of Object.values(group ?? {})) {
|
|
21
|
+
if (isRecord(ref)) {
|
|
22
|
+
addPathJoins(joins, claimAlias, meta, ref);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return joins;
|
|
26
|
+
}
|
|
27
|
+
/** The field a grouped `path` reads, and the join it reads it through: none for the entity's own. */
|
|
28
|
+
export function groupPathField(joins, path) {
|
|
29
|
+
const key = path[path.length - 1];
|
|
30
|
+
if (path.length === 1) {
|
|
31
|
+
return { key, join: undefined };
|
|
32
|
+
}
|
|
33
|
+
const join = joins.get(path.slice(0, -1).join('.'));
|
|
34
|
+
if (!join) {
|
|
35
|
+
throw new TypeError(`cannot $group by '${path.join('.')}': only a to-one relation's field groups, since a to-many multiplies the rows it joins`);
|
|
36
|
+
}
|
|
37
|
+
return { key, join };
|
|
38
|
+
}
|
|
17
39
|
/**
|
|
18
40
|
* Whether a join drops parents that have no match, which is the one thing a join does to *how many*
|
|
19
41
|
* rows a read returns rather than how wide they are. A count that skips the joins has to be told, or
|
|
@@ -71,21 +93,22 @@ function addPopulateJoins(joins, claimAlias, meta, populate, parent) {
|
|
|
71
93
|
addPopulateJoins(joins, claimAlias, join.meta, query.$populate, join);
|
|
72
94
|
}
|
|
73
95
|
}
|
|
74
|
-
|
|
75
|
-
|
|
96
|
+
/** The to-one relations a nested map of fields passes through, a `$sort` or a `$group` path, as joins adding no columns. */
|
|
97
|
+
function addPathJoins(joins, claimAlias, meta, map, parent) {
|
|
98
|
+
if (!map) {
|
|
76
99
|
return;
|
|
77
100
|
}
|
|
78
|
-
for (const key of getKeys(
|
|
101
|
+
for (const key of getKeys(map)) {
|
|
79
102
|
const relation = meta.relations[key];
|
|
80
|
-
const value =
|
|
103
|
+
const value = map[key];
|
|
81
104
|
// A to-many, or a value that is not a map of the relation's own fields, cannot be joined and is
|
|
82
|
-
// reported where the
|
|
105
|
+
// reported where the statement names it - the one place that knows how to.
|
|
83
106
|
if (!relation || isToManyRelation(relation) || !isSortMap(value)) {
|
|
84
107
|
continue;
|
|
85
108
|
}
|
|
86
109
|
const join = addJoin(joins, claimAlias, parent, key, relation, {}, false, false);
|
|
87
|
-
// `E` stated: inferred from
|
|
88
|
-
|
|
110
|
+
// `E` stated: inferred from the nested map, it lands on the nested relation's target.
|
|
111
|
+
addPathJoins(joins, claimAlias, join.meta, value, join);
|
|
89
112
|
}
|
|
90
113
|
}
|
|
91
114
|
/** The join a sort may address at `path` with the relation's own sort map, or why it may not; `unjoinable` is the dialect's remedy. */
|
|
@@ -102,7 +125,7 @@ export function resolveSortableJoin(relation, path, value, joins, unjoinable) {
|
|
|
102
125
|
}
|
|
103
126
|
return { join, sort: value };
|
|
104
127
|
}
|
|
105
|
-
/** A nested
|
|
128
|
+
/** A nested map of fields, as opposed to a `$sort` direction or vector search, or a `$group` field's `true`. */
|
|
106
129
|
function isSortMap(value) {
|
|
107
130
|
return typeof value === 'object' && value !== null && !Array.isArray(value) && !('$vector' in value);
|
|
108
131
|
}
|
|
@@ -1,7 +1,19 @@
|
|
|
1
|
-
import type { EntityGetter, FieldOptions, FieldType, HasCompositeKey, IdValue, NamedIdKey, RejectKeys, RelationManyToManyOptions, RelationManyToOneOptions, RelationOneToManyOptions, RelationOneToOneOptions, TsTypeOf } from '../../type/index.js';
|
|
1
|
+
import type { AggregateValue, ComputedRefs, EntityAggregate, EntityGetter, Except, FieldOptions, FieldType, HasCompositeKey, IdValue, NamedIdKey, RejectKeys, RelationManyToManyOptions, RelationManyToOneOptions, RelationOneToManyOptions, RelationOneToOneOptions, RelationAggregate, TsTypeOf, Writable } from '../../type/index.js';
|
|
2
2
|
import type { RejectIncompatible } from '../../util/index.js';
|
|
3
3
|
/** A member decorator that also constrains the property it may be applied to, on a class `O`. */
|
|
4
4
|
type MemberDecorator<V, O = unknown> = (value: undefined, context: ClassFieldDecoratorContext<O, V>) => void;
|
|
5
|
+
/**
|
|
6
|
+
* A {@link MemberDecorator} whose property has to admit `M` as well as hold `V`. A decorator context is
|
|
7
|
+
* covariant in its value, so on its own it takes a property narrower than the field reads: a `string`
|
|
8
|
+
* where a nullable column reads `string | null`, a `number` where `max()` reads `number | null`.
|
|
9
|
+
*
|
|
10
|
+
* `M` is what each arm insists on rather than the whole of `V`, because a column type that names a
|
|
11
|
+
* family - `jsonb`, whose document the property shapes, or `numeric`, either number kind - is meant to
|
|
12
|
+
* be narrowed. A column insists on its `null`; an aggregate, whose value is exact, insists on all of it.
|
|
13
|
+
*/
|
|
14
|
+
type AdmittingDecorator<V, M, O> = <P extends V | undefined>(value: undefined, context: ClassFieldDecoratorContext<O, P> & ([Writable<M>] extends [Writable<P>] ? unknown : {
|
|
15
|
+
readonly __propertyMustAdmit: M;
|
|
16
|
+
})) => void;
|
|
5
17
|
/**
|
|
6
18
|
* The property type a set of field options describes: the declared `type`, narrowed by `enum` to the
|
|
7
19
|
* values that type admits (so `enum: [2]` stays off a `String`), or else the referenced key's own type,
|
|
@@ -16,6 +28,16 @@ type DeclaredValue<O> = O extends {
|
|
|
16
28
|
} ? HasCompositeKey<E> extends true ? {
|
|
17
29
|
readonly __compositeKeyNeedsAColumnPerKey: true;
|
|
18
30
|
} : IdValue<E> : never;
|
|
31
|
+
/**
|
|
32
|
+
* The `null` a column reads back: every one holds it unless `nullable: false` says otherwise, so the
|
|
33
|
+
* property admits it too. A key holds none, whether `@Id` or `@Field({ isId: true })` declares it, since
|
|
34
|
+
* it is NOT NULL on every engine.
|
|
35
|
+
*/
|
|
36
|
+
type NullOf<O> = O extends {
|
|
37
|
+
readonly nullable: false;
|
|
38
|
+
} | {
|
|
39
|
+
readonly isId: true;
|
|
40
|
+
} ? never : null;
|
|
19
41
|
/** The enum's members, or a named complaint where they widened for lack of `as const`, which would check nothing. */
|
|
20
42
|
type EnumValue<Members, Declared> = Declared extends Members ? {
|
|
21
43
|
readonly __enumNeedsAsConst: true;
|
|
@@ -29,7 +51,27 @@ export declare function Field<This, O extends FieldOptions<DeclaredValue<O>, Thi
|
|
|
29
51
|
type: FieldType;
|
|
30
52
|
} | {
|
|
31
53
|
references: EntityGetter;
|
|
32
|
-
}) & RejectKeys<Exclude<keyof O, keyof FieldOptions>> & RejectIncompatible<O>>(opts: O):
|
|
54
|
+
}) & RejectKeys<Exclude<keyof O, keyof FieldOptions>> & RejectIncompatible<O>>(opts: O): AdmittingDecorator<DeclaredValue<O> | NullOf<O>, NullOf<O>, This>;
|
|
55
|
+
/**
|
|
56
|
+
* Declares a field a relation aggregate computes, `@Field({ computed: (user) => user.resources.count() })`.
|
|
57
|
+
* The aggregate says what the field holds, so it takes no `type`, and only `count` and `sum` - the two a
|
|
58
|
+
* row change turns into a delta - may be `stored`.
|
|
59
|
+
* @example `@Field({ computed: (user) => user.resources.count() }) readonly resourceCount?: number;`
|
|
60
|
+
*/
|
|
61
|
+
export declare function Field<This, O extends AggregateOptions<This> & RejectKeys<Exclude<keyof O, keyof FieldOptions>>>(opts: O): AdmittingDecorator<AggregateValue<O>, AggregateValue<O>, This>;
|
|
62
|
+
/**
|
|
63
|
+
* A field the aggregate itself types: `stored: true` only where a trigger could keep it, and every other
|
|
64
|
+
* option as a column takes it.
|
|
65
|
+
*/
|
|
66
|
+
type AggregateOptions<E> = (Except<FieldOptions<never, E>, 'computed' | 'stored' | 'type'> & {
|
|
67
|
+
readonly computed: EntityAggregate<E>;
|
|
68
|
+
readonly stored?: false;
|
|
69
|
+
}) | (Except<FieldOptions<never, E>, 'computed' | 'stored' | 'type'> & {
|
|
70
|
+
readonly computed: {
|
|
71
|
+
agg(refs: ComputedRefs<E>): RelationAggregate<unknown, true>;
|
|
72
|
+
}['agg'];
|
|
73
|
+
readonly stored: true;
|
|
74
|
+
});
|
|
33
75
|
/**
|
|
34
76
|
* A key the type level cannot name, reported on each `@Id` that leaves it unnamed. Where no `idKey`
|
|
35
77
|
* brand and no conventional name applies, `IdKey` falls back to every field, and `IdValue`,
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import { relationRegistration } from '../metadata/definition.js';
|
|
2
2
|
import { memberRegistrations } from './bag.js';
|
|
3
|
-
/**
|
|
4
|
-
* Declares a persisted field, its `type` checked against the property's.
|
|
5
|
-
* @example `@Field({ type: String }) name?: string;`
|
|
6
|
-
* @example `@Field({ references: () => User }) userId?: string;`
|
|
7
|
-
*/
|
|
8
3
|
export function Field(opts) {
|
|
9
4
|
return (_value, context) => {
|
|
10
5
|
memberRegistrations(context.metadata).fields[String(context.name)] = opts;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { EntityIndexInput, EntityMembers, EntityMeta, EntityOptions, FieldMeta, FieldOptions, FilterName, FilterOptions, HookEvent, IdKey, RelationKey, RelationMeta, RelationOptions, RelationRegistration, Type, WrittenId } from '../../type/index.js';
|
|
2
2
|
export declare function defineField<E>(entity: Type<E>, key: string, opts?: FieldOptions): EntityMeta<E>;
|
|
3
3
|
export declare function defineId<E>(entity: Type<E>, key: string, opts: FieldOptions): EntityMeta<E>;
|
|
4
4
|
/** `T` is the relation's target, independent of the owner `E`. */
|
|
@@ -39,10 +39,14 @@ export declare function soleIdOf<E>(meta: EntityMeta<E>, what: string): IdKey<E>
|
|
|
39
39
|
export declare function fieldOf<E>(meta: EntityMeta<E>, key: string): FieldMeta;
|
|
40
40
|
/** The relation `key` names, for a caller that took `key` from the metadata itself. */
|
|
41
41
|
export declare function relationOf<E>(meta: EntityMeta<E>, key: RelationKey<E>): RelationMeta;
|
|
42
|
+
/** A row of `E` as far as reading its key goes: a record or a write, whatever its values. */
|
|
43
|
+
type KeyedRow<E> = {
|
|
44
|
+
readonly [K in keyof E]?: unknown;
|
|
45
|
+
};
|
|
42
46
|
/** Whether the row names every column of its primary key, `0` and `''` included. */
|
|
43
|
-
export declare function namesKey<E>(meta: EntityMeta<E>, row:
|
|
47
|
+
export declare function namesKey<E>(meta: EntityMeta<E>, row: KeyedRow<E>): boolean;
|
|
44
48
|
/** A row's primary key: its value, or a map of every column on a composite, checked at run time. */
|
|
45
|
-
export declare function idOf<E>(meta: EntityMeta<E>, row:
|
|
49
|
+
export declare function idOf<E>(meta: EntityMeta<E>, row: KeyedRow<E>): WrittenId<E>;
|
|
46
50
|
/**
|
|
47
51
|
* Forgets an entity, and reports whether there was one - for a registry that grows at runtime, where a
|
|
48
52
|
* deleted content type would otherwise keep its metadata for the life of the process. Nothing rewrites
|
|
@@ -58,3 +62,4 @@ export declare function getMeta<E>(entity: Type<E>): EntityMeta<E>;
|
|
|
58
62
|
* schema build constrains and a junction joins by, settling the relations holding them first.
|
|
59
63
|
*/
|
|
60
64
|
export declare function foreignKeysOf<E>(meta: EntityMeta<E>): RelationMeta[];
|
|
65
|
+
export {};
|