uql-orm 0.81.0 → 0.82.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.
Files changed (78) hide show
  1. package/README.md +3 -3
  2. package/dist/browser/querier/httpQuerier.d.ts +2 -2
  3. package/dist/browser/querier/httpQuerier.js +2 -1
  4. package/dist/browser/type/clientQuerier.d.ts +2 -2
  5. package/dist/browser/uql-browser.min.js +2 -2
  6. package/dist/browser/uql-browser.min.js.map +9 -8
  7. package/dist/bunSql/bunSql.util.js +2 -1
  8. package/dist/dialect/abstractSqlDialect.d.ts +15 -5
  9. package/dist/dialect/abstractSqlDialect.js +109 -40
  10. package/dist/dialect/operators.d.ts +7 -1
  11. package/dist/dialect/operators.js +13 -1
  12. package/dist/entity/metadata/definition.d.ts +1 -2
  13. package/dist/entity/metadata/definition.js +37 -39
  14. package/dist/http/handler.js +5 -4
  15. package/dist/http/query.d.ts +1 -1
  16. package/dist/http/query.js +2 -2
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.js +1 -0
  19. package/dist/migrate/acquireQuerierForMigrations.js +2 -1
  20. package/dist/migrate/assertCliConfig.js +7 -6
  21. package/dist/migrate/bin.js +0 -0
  22. package/dist/migrate/builder/expressions.js +2 -1
  23. package/dist/migrate/cli-config.js +5 -4
  24. package/dist/migrate/ddl/indexDdl.js +4 -3
  25. package/dist/migrate/ddl/mysqlIndexDdl.js +5 -4
  26. package/dist/migrate/ddl/pgIndexDdl.js +2 -1
  27. package/dist/migrate/ddl/sqliteIndexDdl.js +2 -1
  28. package/dist/migrate/ddl/tableDdl.js +2 -1
  29. package/dist/migrate/generator/mongoCommand.js +2 -1
  30. package/dist/migrate/generator/mongoSchemaGenerator.js +6 -5
  31. package/dist/migrate/indexPredicate.js +2 -1
  32. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +2 -1
  33. package/dist/migrate/introspection/mongoIntrospector.js +2 -1
  34. package/dist/migrate/migrationTarget.js +2 -1
  35. package/dist/migrate/migrator.js +2 -1
  36. package/dist/migrate/schemaGenerator.js +5 -4
  37. package/dist/migrate/triggerSql.d.ts +1 -1
  38. package/dist/migrate/triggerSql.js +77 -61
  39. package/dist/mongo/mongoDialect.d.ts +1 -3
  40. package/dist/mongo/mongoDialect.js +9 -14
  41. package/dist/mongo/mongodbQuerier.js +3 -7
  42. package/dist/querier/abstractQuerier.d.ts +9 -4
  43. package/dist/querier/abstractQuerier.js +26 -19
  44. package/dist/querier/abstractQuerierPool.d.ts +3 -3
  45. package/dist/querier/abstractSqlQuerier.d.ts +2 -2
  46. package/dist/querier/abstractSqlQuerier.js +1 -1
  47. package/dist/querier/abstractSqlQuerierPool.d.ts +2 -2
  48. package/dist/querier/queryError.d.ts +2 -2
  49. package/dist/schema/schemaASTBuilder.js +2 -1
  50. package/dist/type/dialect.d.ts +23 -1
  51. package/dist/type/entity.d.ts +15 -11
  52. package/dist/type/logger.d.ts +2 -2
  53. package/dist/type/querier.d.ts +3 -3
  54. package/dist/type/query.d.ts +3 -13
  55. package/dist/type/queryAggregate.d.ts +4 -10
  56. package/dist/type/queryRaw.d.ts +17 -3
  57. package/dist/type/queryRaw.js +2 -1
  58. package/dist/type/queryWhere.d.ts +7 -7
  59. package/dist/type/universalQuerier.d.ts +3 -3
  60. package/dist/type/vector.d.ts +2 -1
  61. package/dist/type/vector.js +2 -1
  62. package/dist/util/dialect.util.d.ts +13 -5
  63. package/dist/util/dialect.util.js +28 -20
  64. package/dist/util/field.util.d.ts +4 -4
  65. package/dist/util/field.util.js +10 -2
  66. package/dist/util/hook.util.d.ts +1 -1
  67. package/dist/util/hook.util.js +8 -1
  68. package/dist/util/index.d.ts +1 -0
  69. package/dist/util/index.js +1 -0
  70. package/dist/util/logger.d.ts +3 -3
  71. package/dist/util/object.util.js +3 -2
  72. package/dist/util/raw.d.ts +6 -7
  73. package/dist/util/raw.js +10 -12
  74. package/dist/util/sqlLiteral.js +3 -2
  75. package/dist/util/triggerWrite.d.ts +15 -0
  76. package/dist/util/triggerWrite.js +20 -0
  77. package/package.json +1 -1
  78. package/skills/uql-orm/SKILL.md +3 -3
@@ -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, } 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, fieldUpdateOf, fulltextIndexOver, fulltextWeights, isFieldUpdateOp, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorKey, isVectorSearch, normalizeScalarFieldSelection, parentJoins, rankedTextSearch, targetKeyColumns, textSearchFields, textSortOf, textWeightSteps, parseGroupMap, parseRelationAtKey, parseRelationSize, populatesRelations, aggregateOf, raw, refs, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
4
+ import { asSelectMap, assertNonNegativeInteger, assertWhere, definedEntries, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, columnFamily, countedRelations, fieldUpdateOf, fulltextIndexOver, fulltextWeights, isFieldUpdateOp, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorKey, isVectorSearch, normalizeScalarFieldSelection, parentJoins, rankedTextSearch, targetKeyColumns, textSearchFields, textSortOf, textWeightSteps, parseGroupMap, parseRelationAtKey, parseRelationSize, populatesRelations, aggregateOf, raw, refs, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
5
5
  import { escapeAnsiSqlLiteral } from '../util/sqlLiteral.js';
6
6
  import { kindOf, UqlUsageError } from '../util/uqlError.js';
7
7
  import { AGGREGATE_PAGE_ALIAS, AGGREGATE_VALUE_ALIAS, ROWS_ALIAS, JSON_ELEM_ALIAS, JSON_PULL_ALIAS, relationSortColumn, } from './aliases.js';
8
8
  import { holdsOperator, isJsonScalar, jsonCompareMode, jsonElemExists, jsonPath, } from './jsonSql.js';
9
- import { betweenBounds, COMPARE_OPS, EQUALITY_OPS, GROUP_OPS, groupClauses, inOperands, isGroupOp, isOrderedOp, isVectorQuery, LIKE_OPS, ORDERED_OPS, VECTOR_QUERY_KEY_SET, whereOperators, } from './operators.js';
9
+ import { betweenBounds, COMPARE_OPS, EQUALITY_OPS, GROUP_OPS, groupClauses, inOperands, isGroupOp, isOrderedOp, isVectorQuery, LIKE_OPS, namesRows, ORDERED_OPS, VECTOR_QUERY_KEY_SET, whereOperators, } from './operators.js';
10
10
  import { SqlQueryContext } from './queryContext.js';
11
11
  import { groupPathField, NO_JOINS, aggregateColumnField, resolveGroupJoins, resolveQueryJoins, relationSortTerms, resolveSortableJoin, } from './queryJoins.js';
12
12
  import { resolveVectorCast } from './vectorCast.js';
@@ -34,6 +34,13 @@ function orRefuse(value, refusal) {
34
34
  }
35
35
  return value;
36
36
  }
37
+ /**
38
+ * Whether an assignment builds on what each row before it left: `$inc`, `$mul`, `$push`. A set-based
39
+ * `UPDATE` writes a target once however many rows of its set match it, so it would apply one of them.
40
+ */
41
+ function accumulates(value) {
42
+ return isFieldUpdateOp(value) || (isJsonUpdateOp(value) && value.$push !== undefined);
43
+ }
37
44
  /** Conditions joined by `AND`, parenthesized where there is more than one. */
38
45
  function conjunction(parts) {
39
46
  return parts.length > 1 ? `(${parts.join(' AND ')})` : parts.join('');
@@ -178,7 +185,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
178
185
  returningIdExpression(meta) {
179
186
  const [idKey] = meta.ids;
180
187
  return meta.ids.length === 1
181
- ? `${this.returnedRowPrefix}${this.escapeId(this.columnOf(meta, idKey))} ${this.escapeId('id')}`
188
+ ? `${this.returnedRowPrefix}${this.escapedColumnName(meta, idKey)} ${this.escapeId('id')}`
182
189
  : '';
183
190
  }
184
191
  search(ctx, entity, q = {}, opts = {}, joins = NO_JOINS, order) {
@@ -423,7 +430,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
423
430
  join.relation.references.forEach((reference, index) => {
424
431
  if (index > 0)
425
432
  ctx.append(' AND ');
426
- const foreign = this.escapeId(this.columnOf(join.meta, reference.foreign));
433
+ const foreign = this.escapedColumnName(join.meta, reference.foreign);
427
434
  // Two calls rather than one over a union: the parent is either another join's entity or the
428
435
  // queried one, and their metadata types have nothing in common.
429
436
  const local = this.escapeId(join.parent ? this.columnOf(join.parent.meta, reference.local) : this.columnOf(meta, reference.local));
@@ -1254,30 +1261,99 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1254
1261
  const meta = getMeta(entity);
1255
1262
  const [filledPayload] = fillOnFields(meta, payload, 'onUpdate');
1256
1263
  const keys = filterFieldKeys(meta, filledPayload, 'onUpdate');
1257
- const tableName = this.escapedTableName(meta);
1258
- ctx.append(`UPDATE ${tableName} SET `);
1264
+ ctx.append(`UPDATE ${this.escapedTableName(meta)} SET `);
1259
1265
  for (let i = 0; i < keys.length; i++) {
1260
1266
  if (i > 0) {
1261
1267
  ctx.append(', ');
1262
1268
  }
1263
- const key = keys[i];
1264
- const field = meta.fields[key];
1265
- const escapedCol = this.escapedColumnName(meta, key);
1266
- const value = filledPayload[key];
1267
- if (isJsonUpdateOp(value)) {
1268
- this.formatJsonUpdate(ctx, escapedCol, value, field);
1269
- }
1270
- else if (isFieldUpdateOp(value)) {
1271
- const [op, operand] = fieldUpdateOf(key, value);
1272
- ctx.append(`${escapedCol} = COALESCE(${escapedCol}, 0) ${SQL_ARITHMETIC[op]} `);
1273
- ctx.addValue(operand);
1269
+ this.appendAssignment(ctx, meta, keys[i], filledPayload[keys[i]]);
1270
+ }
1271
+ this.search(ctx, entity, q, opts);
1272
+ }
1273
+ /** `col = value`, a JSON operator or a `$inc`/`$mul` spelled as the engine's. */
1274
+ appendAssignment(ctx, meta, key, value) {
1275
+ const field = meta.fields[key];
1276
+ const escapedCol = this.escapedColumnName(meta, key);
1277
+ if (isJsonUpdateOp(value)) {
1278
+ this.formatJsonUpdate(ctx, escapedCol, value, field);
1279
+ }
1280
+ else if (isFieldUpdateOp(value)) {
1281
+ const [op, operand] = fieldUpdateOf(key, value);
1282
+ ctx.append(`${escapedCol} = COALESCE(${escapedCol}, 0) ${SQL_ARITHMETIC[op]} `);
1283
+ ctx.addValue(operand);
1284
+ }
1285
+ else {
1286
+ ctx.append(`${escapedCol} = `);
1287
+ this.formatPersistableValue(ctx, field, value);
1288
+ }
1289
+ }
1290
+ /**
1291
+ * A write in a trigger's body. Only the columns it names, none filled in JavaScript - that would bake
1292
+ * one value into the trigger - no id read back, and no entity filter, which a request resolves and a
1293
+ * trigger has none of. `rows` is the `FROM` a set-based engine's body reads its rows through.
1294
+ */
1295
+ triggerWrite(ctx, write, rows) {
1296
+ const meta = getMeta(write.entity);
1297
+ const table = this.escapedTableName(meta);
1298
+ if (write.kind === 'insert') {
1299
+ const unfilled = definedEntries(meta.fields).find(([key, field]) => field.onInsert !== undefined && field.defaultValue === undefined && write.row[key] === undefined);
1300
+ if (unfilled) {
1301
+ throw new UqlUsageError(`'${meta.name}.${unfilled[0]}' is filled on insert by uql, which a trigger does not run: ` +
1302
+ 'name it, or give it a defaultValue');
1274
1303
  }
1275
- else {
1276
- ctx.append(`${escapedCol} = `);
1277
- this.formatPersistableValue(ctx, field, value);
1304
+ const entries = this.writtenEntries(meta, write.row);
1305
+ const columns = entries.map(([key]) => this.escapedColumnName(meta, key)).join(', ');
1306
+ const [open, close] = rows ? ['SELECT ', ` ${rows};`] : ['VALUES (', ');'];
1307
+ ctx.append(`INSERT INTO ${table} (${columns}) ${open}`);
1308
+ entries.forEach(([key, value], i) => {
1309
+ if (i > 0) {
1310
+ ctx.append(', ');
1311
+ }
1312
+ this.formatPersistableValue(ctx, meta.fields[key], value);
1313
+ });
1314
+ ctx.append(close);
1315
+ return;
1316
+ }
1317
+ assertWhere(meta, write.where);
1318
+ if (!namesRows(write.where)) {
1319
+ throw new UqlUsageError(`a trigger's ${write.kind} over '${meta.name}' names no rows, so it would address every one`);
1320
+ }
1321
+ if (write.kind === 'update') {
1322
+ const entries = this.writtenEntries(meta, write.set);
1323
+ const accumulating = rows && entries.find(([, value]) => accumulates(value));
1324
+ if (accumulating) {
1325
+ throw new UqlUsageError(`'${meta.name}.${accumulating[0]}' cannot accumulate per row in a trigger fired once per statement, ` +
1326
+ 'which applies $inc, $mul and $push once');
1278
1327
  }
1328
+ ctx.append(`UPDATE ${table} SET `);
1329
+ entries.forEach(([key, value], i) => {
1330
+ if (i > 0) {
1331
+ ctx.append(', ');
1332
+ }
1333
+ this.appendAssignment(ctx, meta, key, value);
1334
+ });
1279
1335
  }
1280
- this.search(ctx, entity, q, opts);
1336
+ else {
1337
+ ctx.append(`DELETE FROM ${table}`);
1338
+ }
1339
+ if (rows) {
1340
+ ctx.append(` ${rows}`);
1341
+ }
1342
+ // Qualified by the table: on a set-based engine `inserted` holds the same column names.
1343
+ this.renderWhere(ctx, write.entity, write.where, { escapedPrefix: `${table}.` });
1344
+ ctx.append(';');
1345
+ }
1346
+ /** The values a write names, refusing one the entity has no column for. */
1347
+ writtenEntries(meta, values) {
1348
+ const entries = Object.entries(values).filter(([, value]) => value !== undefined);
1349
+ const unknown = entries.find(([key]) => !meta.fields[key]);
1350
+ if (unknown) {
1351
+ throw new UqlUsageError(`'${meta.name}' has no field '${unknown[0]}' for a trigger to write`);
1352
+ }
1353
+ if (!entries.length) {
1354
+ throw new UqlUsageError(`a trigger's write to '${meta.name}' names no field`);
1355
+ }
1356
+ return entries;
1281
1357
  }
1282
1358
  /**
1283
1359
  * `INSERT ... ON CONFLICT ... DO UPDATE/NOTHING RETURNING`. The assignments are built before the insert
@@ -1312,23 +1388,18 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1312
1388
  return fields
1313
1389
  .filter((col) => !conflictPaths[col])
1314
1390
  .map((col) => {
1315
- const field = meta.fields[col];
1316
- const columnName = this.resolveColumnName(col, field);
1391
+ const column = this.escapedColumnName(meta, col);
1317
1392
  if (Object.hasOwn(sample, col)) {
1318
- return `${this.escapeId(columnName)} = ${callback(this.escapeId(columnName))}`;
1393
+ return `${column} = ${callback(column)}`;
1319
1394
  }
1320
- const text = this.buildFragment(ctx, (fragmentCtx) => this.formatPersistableValue(fragmentCtx, field, filledPayload[col]));
1321
- return `${this.escapeId(columnName)} = ${text}`;
1395
+ const text = this.buildFragment(ctx, (fragmentCtx) => this.formatPersistableValue(fragmentCtx, meta.fields[col], filledPayload[col]));
1396
+ return `${column} = ${text}`;
1322
1397
  })
1323
1398
  .join(', ');
1324
1399
  }
1325
1400
  getUpsertConflictPathsStr(meta, conflictPaths) {
1326
1401
  return getKeys(conflictPaths)
1327
- .map((key) => {
1328
- const field = meta.fields[key];
1329
- const columnName = this.resolveColumnName(key, field);
1330
- return this.escapeId(columnName);
1331
- })
1402
+ .map((key) => this.escapedColumnName(meta, key))
1332
1403
  .join(', ');
1333
1404
  }
1334
1405
  delete(ctx, entity, q, opts = {}) {
@@ -1337,10 +1408,8 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1337
1408
  // Soft-delete (stamp only live rows) unless `hardDelete` is requested or the entity has no
1338
1409
  // soft-delete field (e.g. a cascade onto a non-soft-deletable child).
1339
1410
  if (!opts.hardDelete && meta.softDelete) {
1340
- const field = fieldOf(meta, meta.softDelete);
1341
- const columnName = this.resolveColumnName(meta.softDelete, field);
1342
- ctx.append(`UPDATE ${tableName} SET ${this.escapeId(columnName)} = `);
1343
- this.formatPersistableValue(ctx, field, getSoftDeleteValue(field));
1411
+ ctx.append(`UPDATE ${tableName} SET `);
1412
+ this.appendAssignment(ctx, meta, meta.softDelete, getSoftDeleteValue(fieldOf(meta, meta.softDelete)));
1344
1413
  this.search(ctx, entity, q, opts);
1345
1414
  return;
1346
1415
  }
@@ -1537,19 +1606,19 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1537
1606
  jsonDiffers(elem, operand) {
1538
1607
  return `${elem} <> ${operand}`;
1539
1608
  }
1540
- compileDdl(sql, entity) {
1609
+ compileDdl(sql, entity, { rows, escapedPrefix, operand } = {}) {
1541
1610
  const ctx = this.createContext({ inlineValues: true });
1542
1611
  if (sql instanceof QueryRaw) {
1543
- sql.render({ ctx, dialect: this, prefix: '', escapedPrefix: '', entity });
1612
+ sql.render({ ctx, dialect: this, prefix: '', escapedPrefix: escapedPrefix ?? '', entity, rows });
1544
1613
  }
1545
1614
  else if (entity) {
1546
- this.renderWhere(ctx, entity, sql, { clause: false });
1615
+ this.renderWhere(ctx, entity, sql, { clause: false, escapedPrefix, operand });
1547
1616
  }
1548
1617
  else {
1549
- throw new TypeError('a predicate compiles against the entity it is written for, and none was given');
1618
+ throw new UqlUsageError('a predicate compiles against the entity it is written for, and none was given');
1550
1619
  }
1551
1620
  if (ctx.values.length) {
1552
- throw new TypeError(`DDL has no placeholder to bind a value into, and this SQL left one bound: ${ctx.sql}`);
1621
+ throw new UqlUsageError(`DDL has no placeholder to bind a value into, and this SQL left one bound: ${ctx.sql}`);
1553
1622
  }
1554
1623
  return ctx.sql;
1555
1624
  }
@@ -1,4 +1,4 @@
1
- import { type QueryGroupOp, type QueryOrderedOp, type QueryVectorQuery, type QueryWhereArray, type QueryWhereFieldOp } from '../type/index.js';
1
+ import { type QueryGroupOp, type QueryOrderedOp, type QueryVectorQuery, type QueryWhere, type QueryWhereArray, type QueryWhereFieldOp } from '../type/index.js';
2
2
  /**
3
3
  * How each grouping operator renders: the operator joining its clauses, and whether the group is
4
4
  * negated (`$not` is `NOT (a AND b)`). Total over {@link QueryGroupOp}.
@@ -29,6 +29,12 @@ export declare function isGroupOp(key: string): key is QueryGroupOp;
29
29
  * refuse the same payload rather than one throwing and the other failing further in.
30
30
  */
31
31
  export declare function groupClauses<E>(key: QueryGroupOp, val: QueryWhereArray<E> | undefined): QueryWhereArray<E>;
32
+ /**
33
+ * Whether a `$where` names any rows, as the WHERE it renders would: an `undefined` value, an empty
34
+ * operator map and a group none of whose clauses names one all render nothing, which a write reads as
35
+ * the whole table.
36
+ */
37
+ export declare function namesRows<E>(where: QueryWhere<E> | undefined): boolean;
32
38
  /**
33
39
  * A WHERE value as the operators it applies: an array is `$in`, an operator map its own entries, and
34
40
  * anything else `$eq`. Each key is checked here, once, refused as `refusal` where it is no operator.
@@ -1,5 +1,6 @@
1
- import { VECTOR_QUERY_KEYS, } from '../type/index.js';
1
+ import { QueryRaw, VECTOR_QUERY_KEYS, } from '../type/index.js';
2
2
  import { isOperatorMap } from '../util/dialect.util.js';
3
+ import { hasKeys, isWhereMap, someKey } from '../util/object.util.js';
3
4
  import { kindOf, UqlUsageError } from '../util/uqlError.js';
4
5
  /**
5
6
  * How each grouping operator renders: the operator joining its clauses, and whether the group is
@@ -26,6 +27,17 @@ export function groupClauses(key, val) {
26
27
  }
27
28
  return val ?? [];
28
29
  }
30
+ /**
31
+ * Whether a `$where` names any rows, as the WHERE it renders would: an `undefined` value, an empty
32
+ * operator map and a group none of whose clauses names one all render nothing, which a write reads as
33
+ * the whole table.
34
+ */
35
+ export function namesRows(where) {
36
+ return (where !== undefined &&
37
+ someKey(where, (key) => isGroupOp(key)
38
+ ? groupClauses(key, where[key]).some((clause) => clause instanceof QueryRaw || namesRows(clause))
39
+ : where[key] !== undefined && !(isWhereMap(where[key]) && !hasKeys(where[key]))));
40
+ }
29
41
  /**
30
42
  * A WHERE value as the operators it applies: an array is `$in`, an operator map its own entries, and
31
43
  * anything else `$eq`. Each key is checked here, once, refused as `refusal` where it is no operator.
@@ -45,7 +45,7 @@ export declare function fieldOf<E>(meta: EntityMeta<E>, key: string): FieldMeta;
45
45
  /** The relation `key` names, for a caller that took `key` from the metadata itself. */
46
46
  export declare function relationOf<E>(meta: EntityMeta<E>, key: RelationKey<E>): RelationMeta;
47
47
  /** A row of `E` as far as reading its key goes: a record or a write, whatever its values. */
48
- type KeyedRow<E> = {
48
+ export type KeyedRow<E> = {
49
49
  readonly [K in keyof E]?: unknown;
50
50
  };
51
51
  /** Whether the row names every column of its primary key, `0` and `''` included. */
@@ -67,4 +67,3 @@ export declare function getMeta<E>(entity: Type<E>): EntityMeta<E>;
67
67
  * schema build constrains and a junction joins by, settling the relations holding them first.
68
68
  */
69
69
  export declare function foreignKeysOf<E>(meta: EntityMeta<E>): RelationMeta[];
70
- export {};
@@ -1,6 +1,7 @@
1
1
  import { RelationAggregate, SOFT_DELETE_FILTER } from '../../type/index.js';
2
- import { isInlinedExpression } from '../../util/field.util.js';
3
- import { entitySql, entityWhere, fieldOptionConflict, getKeys, hasKeys, isToManyRelation, memberRefs, fulltextWeights, normalizeIndexColumn, definedEntries, } from '../../util/index.js';
2
+ import { fieldKeys, isInlinedExpression } from '../../util/field.util.js';
3
+ import { entitySql, entityWhere, fieldOptionConflict, hasKeys, isToManyRelation, memberRefs, fulltextWeights, normalizeIndexColumn, definedEntries, whereWith, } from '../../util/index.js';
4
+ import { UqlUsageError } from '../../util/uqlError.js';
4
5
  import { ownRegistrations } from '../decorator/bag.js';
5
6
  /**
6
7
  * A map held on `globalThis` through the global symbol registry, so a single one survives multiple
@@ -22,18 +23,18 @@ export function defineField(entity, key, opts = {}) {
22
23
  // A relation aggregate reads as a correlated subquery, which no engine accepts in a generated column:
23
24
  // keeping one on the row takes the triggers a write fires, which are not built yet.
24
25
  if (opts.stored && sql instanceof RelationAggregate) {
25
- throw new TypeError(`'${entity.name}.${key}' cannot be 'stored': a relation aggregate reads as a subquery, which no ` +
26
+ throw new UqlUsageError(`'${entity.name}.${key}' cannot be 'stored': a relation aggregate reads as a subquery, which no ` +
26
27
  "engine keeps in a generated column. Drop 'stored' to have it read on each query.");
27
28
  }
28
29
  // A stored computed column is a real column and still needs a type; only an inlined one is exempt,
29
30
  // its expression being spliced in rather than declared.
30
31
  if (!opts.type && !opts.references && !isInlinedExpression(opts)) {
31
- throw new TypeError(`'${entity.name}.${key}' needs a 'type'. Declare it - '@Field({ type: String })' - or point the field ` +
32
+ throw new UqlUsageError(`'${entity.name}.${key}' needs a 'type'. Declare it - '@Field({ type: String })' - or point the field ` +
32
33
  "at another entity with 'references', which resolves the column type from its primary key.");
33
34
  }
34
35
  const conflict = fieldOptionConflict(opts);
35
36
  if (conflict) {
36
- throw new TypeError(`'${entity.name}.${key}' ${conflict}.`);
37
+ throw new UqlUsageError(`'${entity.name}.${key}' ${conflict}.`);
37
38
  }
38
39
  const fieldKey = key;
39
40
  // Flagged when the author gave `references` but no `type`, so schema generation knows to resolve the
@@ -75,10 +76,10 @@ function keyMap() {
75
76
  const KEY_MAP = new Proxy({}, { get: (_, key) => key });
76
77
  function addRelation(entity, key, registration) {
77
78
  if (!registration.entity) {
78
- throw new TypeError(`'${entity.name}.${key}' needs an 'entity' getter, e.g. '@ManyToOne({ entity: () => Company })'.`);
79
+ throw new UqlUsageError(`'${entity.name}.${key}' needs an 'entity' getter, e.g. '@ManyToOne({ entity: () => Company })'.`);
79
80
  }
80
81
  if (registration.through && registration.references) {
81
- throw new TypeError(`'${entity.name}.${key}' joins through a junction, whose column referencing each side is the join; ` +
82
+ throw new UqlUsageError(`'${entity.name}.${key}' joins through a junction, whose column referencing each side is the join; ` +
82
83
  "'references' pairs the declaring entity's columns with the target's instead.");
83
84
  }
84
85
  const meta = ensureWritableMeta(entity);
@@ -120,10 +121,10 @@ export function defineTrigger(entity, trigger) {
120
121
  const meta = ensureWritableMeta(entity);
121
122
  // The type already refuses an empty map; this is the same answer for plain JavaScript.
122
123
  if (typeof trigger.run !== 'function' && !hasKeys(trigger.run)) {
123
- throw new TypeError(`'${entity.name}' has a trigger whose body names at least one engine to run on`);
124
+ throw new UqlUsageError(`'${entity.name}' has a trigger whose body names at least one engine to run on`);
124
125
  }
125
126
  if (trigger.name && meta.triggers?.some((it) => it.name === trigger.name)) {
126
- throw new TypeError(`'${entity.name}' already has a trigger named '${trigger.name}'`);
127
+ throw new UqlUsageError(`'${entity.name}' already has a trigger named '${trigger.name}'`);
127
128
  }
128
129
  (meta.triggers ??= []).push({ ...trigger, of: trigger.of?.(memberRefs()).map((ref) => ref.key) });
129
130
  return meta;
@@ -131,12 +132,12 @@ export function defineTrigger(entity, trigger) {
131
132
  export function defineFilter(entity, name, opts) {
132
133
  const meta = ensureWritableMeta(entity);
133
134
  if (name === SOFT_DELETE_FILTER) {
134
- throw TypeError(`'${entity.name}' filter name '${SOFT_DELETE_FILTER}' is reserved; it is auto-registered from @Field({ softDelete })`);
135
+ throw new UqlUsageError(`'${entity.name}' filter name '${SOFT_DELETE_FILTER}' is reserved; it is auto-registered from @Field({ softDelete })`);
135
136
  }
136
137
  // Widened for a caller the types did not reach, which is the only one this can refuse.
137
138
  const { security, onMissing } = opts;
138
139
  if (security && onMissing === 'skip') {
139
- throw TypeError(`'${entity.name}' security filter '${name}' cannot use onMissing: 'skip' (it must fail closed)`);
140
+ throw new UqlUsageError(`'${entity.name}' security filter '${name}' cannot use onMissing: 'skip' (it must fail closed)`);
140
141
  }
141
142
  (meta.filters ??= {})[name] = opts;
142
143
  return meta;
@@ -173,7 +174,7 @@ export function defineEntity(entity, opts = {}) {
173
174
  // statement builds and then fails at the database. `schema` is the way to say it.
174
175
  if (opts.name?.includes('.')) {
175
176
  const [schema, ...rest] = opts.name.split('.');
176
- throw new TypeError(`'${entity.name}' has a dotted name '${opts.name}'. Name the schema separately as ` +
177
+ throw new UqlUsageError(`'${entity.name}' has a dotted name '${opts.name}'. Name the schema separately as ` +
177
178
  `{ schema: '${schema}', name: '${rest.join('.')}' }.`);
178
179
  }
179
180
  const meta = ensureWritableMeta(entity);
@@ -201,7 +202,7 @@ export function defineEntity(entity, opts = {}) {
201
202
  defineFilter(entity, name, filter);
202
203
  }
203
204
  if (!hasKeys(meta.fields)) {
204
- throw TypeError(`'${entity.name}' must have fields`);
205
+ throw new UqlUsageError(`'${entity.name}' must have fields`);
205
206
  }
206
207
  // A later call composes onto the entity, so a name is only ever set, and `derivedName` records that
207
208
  // the class name stood in, which is what a naming strategy derives from.
@@ -221,21 +222,18 @@ export function defineEntity(entity, opts = {}) {
221
222
  // Derive soft-delete from the (inheritance-merged) fields, so own and inherited markers are handled
222
223
  // uniformly. Exactly one field may be marked; it auto-registers the built-in `softDelete` read
223
224
  // filter (a reserved name - see defineFilter - so it never clobbers a user filter).
224
- const softDeleteKeys = getKeys(meta.fields).filter((key) => {
225
- const softDelete = meta.fields[key]?.softDelete;
226
- return softDelete !== undefined && softDelete !== false;
227
- });
225
+ const softDeleteKeys = fieldKeys(meta, ({ softDelete }) => softDelete !== undefined && softDelete !== false);
228
226
  if (softDeleteKeys.length > 1) {
229
- throw TypeError(`'${entity.name}' must have at most one field with 'softDelete'`);
227
+ throw new UqlUsageError(`'${entity.name}' must have at most one field with 'softDelete'`);
230
228
  }
231
229
  if (softDeleteKeys.length) {
232
230
  meta.softDelete = softDeleteKeys[0];
233
- (meta.filters ??= {})[SOFT_DELETE_FILTER] = { where: { [meta.softDelete]: null }, default: true };
231
+ (meta.filters ??= {})[SOFT_DELETE_FILTER] = { where: whereWith(meta.softDelete, null), default: true };
234
232
  }
235
233
  // The optimistic lock, derived the same way and just as singular: one row has one version.
236
- const versionKeys = getKeys(meta.fields).filter((key) => meta.fields[key]?.version);
234
+ const versionKeys = fieldKeys(meta, (field) => field.version);
237
235
  if (versionKeys.length > 1) {
238
- throw TypeError(`'${entity.name}' must have at most one field with 'version'`);
236
+ throw new UqlUsageError(`'${entity.name}' must have at most one field with 'version'`);
239
237
  }
240
238
  if (versionKeys.length) {
241
239
  meta.version = versionKeys[0];
@@ -246,7 +244,7 @@ export function defineEntity(entity, opts = {}) {
246
244
  }
247
245
  const ids = getIdKeys(meta);
248
246
  if (!ids.length) {
249
- throw TypeError(`'${entity.name}' must have at least one id field (use @Id, defineId, or defineEntity({ fields: { ..., isId: true } }))`);
247
+ throw new UqlUsageError(`'${entity.name}' must have at least one id field (use @Id, defineId, or defineEntity({ fields: { ..., isId: true } }))`);
250
248
  }
251
249
  meta.ids = ids;
252
250
  return meta;
@@ -262,7 +260,7 @@ export function assertSoleId(meta, what) {
262
260
  if (ids.length === 1) {
263
261
  return;
264
262
  }
265
- throw new TypeError(ids.length
263
+ throw new UqlUsageError(ids.length
266
264
  ? `'${meta.entity.name}' has a composite primary key (${ids.join(', ')}), which ${what} does not support yet.`
267
265
  : // An entity registered with `@Field` but no `@Entity` never ran the check in `defineEntity`.
268
266
  `'${meta.entity.name}' has no primary key, which ${what} needs.`);
@@ -276,7 +274,7 @@ export function soleIdOf(meta, what) {
276
274
  export function fieldOf(meta, key) {
277
275
  const field = meta.fields[key];
278
276
  if (!field) {
279
- throw new TypeError(`'${meta.entity.name}' has no field '${key}'`);
277
+ throw new UqlUsageError(`'${meta.entity.name}' has no field '${key}'`);
280
278
  }
281
279
  return field;
282
280
  }
@@ -284,7 +282,7 @@ export function fieldOf(meta, key) {
284
282
  export function relationOf(meta, key) {
285
283
  const relation = meta.relations[key];
286
284
  if (!relation) {
287
- throw new TypeError(`'${meta.entity.name}' has no relation '${key}'`);
285
+ throw new UqlUsageError(`'${meta.entity.name}' has no relation '${key}'`);
288
286
  }
289
287
  return relation;
290
288
  }
@@ -342,7 +340,7 @@ export function getMeta(entity) {
342
340
  function registeredMeta(entity) {
343
341
  const meta = metas.get(entity);
344
342
  if (!meta) {
345
- throw TypeError(`'${entity.name}' is not an entity`);
343
+ throw new UqlUsageError(`'${entity.name}' is not an entity`);
346
344
  }
347
345
  return meta;
348
346
  }
@@ -351,7 +349,7 @@ function fillRelations(meta) {
351
349
  const at = `'${meta.entity.name}.${relKey}'`;
352
350
  const references = settledReferences(at, meta, relKey, relation);
353
351
  if (!references.length) {
354
- throw new TypeError(`${at} has no columns to join on.`);
352
+ throw new UqlUsageError(`${at} has no columns to join on.`);
355
353
  }
356
354
  if (!relation.through) {
357
355
  assertJoins(at, meta, relation, references);
@@ -371,7 +369,7 @@ function settledReferences(at, meta, relKey, relOpts) {
371
369
  if (typeof references === 'string') {
372
370
  const target = ensureMeta(relOpts.entity());
373
371
  if (mappedBy || isToManyRelation(relOpts) || target.ids.length > 1) {
374
- throw new TypeError(`${at} names one column, '${references}', which only a to-one holding a foreign key to a one-column key ` +
372
+ throw new UqlUsageError(`${at} names one column, '${references}', which only a to-one holding a foreign key to a one-column key ` +
375
373
  'can: pair the columns, [{ local, foreign }].');
376
374
  }
377
375
  relOpts.references = [{ local: references, foreign: soleIdOf(target, 'a foreign key') }];
@@ -383,7 +381,7 @@ function settledReferences(at, meta, relKey, relOpts) {
383
381
  return fillInverseSide(at, meta, relOpts, mappedBy);
384
382
  if (through)
385
383
  return fillThrough(at, meta, relOpts, through);
386
- throw new TypeError(isToManyRelation(relOpts)
384
+ throw new UqlUsageError(isToManyRelation(relOpts)
387
385
  ? `${at} is a to-many relation with no way to join: it needs 'mappedBy' (the member on the other side), ` +
388
386
  "'through' (a junction entity), or 'references' (the columns)."
389
387
  : `${at} needs 'references', the foreign key column it joins by, or 'mappedBy', the member on the other ` +
@@ -407,7 +405,7 @@ function fillInverseSide(at, meta, relOpts, mappedBy) {
407
405
  const other = `'${relMeta.entity.name}.${mappedBy}'`;
408
406
  if (relMeta.fields[mappedBy]) {
409
407
  if (meta.ids.length > 1) {
410
- throw new TypeError(`${at} is mapped by ${other}, one column, but the primary key of ` +
408
+ throw new UqlUsageError(`${at} is mapped by ${other}, one column, but the primary key of ` +
411
409
  `'${meta.entity.name}' is composite (${meta.ids.join(', ')}). Map it by the relation on the other side ` +
412
410
  'instead, which joins every column of the key.');
413
411
  }
@@ -417,14 +415,14 @@ function fillInverseSide(at, meta, relOpts, mappedBy) {
417
415
  }
418
416
  const owner = relMeta.relations[mappedBy];
419
417
  if (!owner) {
420
- throw new TypeError(`${at} is mapped by '${mappedBy}', which is neither a field nor a relation of '${relMeta.entity.name}'.`);
418
+ throw new UqlUsageError(`${at} is mapped by '${mappedBy}', which is neither a field nor a relation of '${relMeta.entity.name}'.`);
421
419
  }
422
420
  if (owner.mappedBy) {
423
- throw new TypeError(`${at} is mapped by ${other}, an inverse side too, so neither owns the foreign key.`);
421
+ throw new UqlUsageError(`${at} is mapped by ${other}, an inverse side too, so neither owns the foreign key.`);
424
422
  }
425
423
  const ownerTarget = owner.entity();
426
424
  if (!isA(meta.entity, ownerTarget)) {
427
- throw new TypeError(`${at} is mapped by ${other}, a relation to '${ownerTarget.name}', not to '${meta.entity.name}'.`);
425
+ throw new UqlUsageError(`${at} is mapped by ${other}, a relation to '${ownerTarget.name}', not to '${meta.entity.name}'.`);
428
426
  }
429
427
  const ownerReferences = settledReferences(other, relMeta, mappedBy, owner);
430
428
  // Two different flips: a junction's pairs are the owner's group followed by ours, so the two groups
@@ -454,11 +452,11 @@ function assertJoins(at, meta, relOpts, pairs) {
454
452
  const column = `'${side.meta.entity.name}.${key}'`;
455
453
  const field = side.meta.fields[key];
456
454
  if (!field || isInlinedExpression(field)) {
457
- throw new TypeError(`${at} joins ${column}, which is not a column: declare it with '@Field'.`);
455
+ throw new UqlUsageError(`${at} joins ${column}, which is not a column: declare it with '@Field'.`);
458
456
  }
459
457
  const referenced = side.holds && pairs.length === 1 ? field.references?.() : undefined;
460
458
  if (referenced && !isA(side.joins, referenced)) {
461
- throw new TypeError(`${at} joins ${column}, a foreign key to '${referenced.name}', not to '${side.joins.name}'.`);
459
+ throw new UqlUsageError(`${at} joins ${column}, a foreign key to '${referenced.name}', not to '${side.joins.name}'.`);
462
460
  }
463
461
  }
464
462
  }
@@ -491,7 +489,7 @@ export function foreignKeysOf(meta) {
491
489
  if (!target.ids.length)
492
490
  return [];
493
491
  if (target.ids.length > 1) {
494
- throw new TypeError(`'${meta.entity.name}.${key}' cannot reference '${target.entity.name}', whose primary key is composite ` +
492
+ throw new UqlUsageError(`'${meta.entity.name}.${key}' cannot reference '${target.entity.name}', whose primary key is composite ` +
495
493
  `(${target.ids.join(', ')}): a column points at one. Declare a column per key and pair each with it ` +
496
494
  `in a '@ManyToOne' to '${target.entity.name}'.`);
497
495
  }
@@ -509,11 +507,11 @@ function junctionReferences(at, junction, side) {
509
507
  const declare = side.ids.length > 1
510
508
  ? `a column per key, paired in a '@ManyToOne' to '${side.entity.name}'`
511
509
  : `'@Field({ references: () => ${side.entity.name} })'`;
512
- throw new TypeError(`${at} joins through '${junction.entity.name}', which has no column referencing ${referenced}: declare ${declare}.`);
510
+ throw new UqlUsageError(`${at} joins through '${junction.entity.name}', which has no column referencing ${referenced}: declare ${declare}.`);
513
511
  }
514
512
  if (others.length) {
515
513
  const columns = [pair, ...others].map(({ local }) => `'${local}'`).join(' and ');
516
- throw new TypeError(`${at} joins through '${junction.entity.name}', where ${columns} each reference ${referenced}: a junction ` +
514
+ throw new UqlUsageError(`${at} joins through '${junction.entity.name}', where ${columns} each reference ${referenced}: a junction ` +
517
515
  'needs exactly one column per key of each side.');
518
516
  }
519
517
  return { local: pair.local, foreign: key };
@@ -521,7 +519,7 @@ function junctionReferences(at, junction, side) {
521
519
  }
522
520
  /** Every key the entity marks, in declaration order. More than one is a composite primary key. */
523
521
  function getIdKeys(meta) {
524
- return getKeys(meta.fields).filter((key) => meta.fields[key]?.isId);
522
+ return fieldKeys(meta, (field) => field.isId);
525
523
  }
526
524
  /**
527
525
  * Merges `ancestor` and its ancestors into `meta`, nearest first, draining an undecorated base's
@@ -1,6 +1,7 @@
1
1
  import { withContext } from '../context/context.js';
2
2
  import { getEntities, getMeta, soleIdOf } from '../entity/index.js';
3
- import { whereIds } from '../util/dialect.util.js';
3
+ import { whereIds, whereWith } from '../util/dialect.util.js';
4
+ import { UqlUsageError } from '../util/uqlError.js';
4
5
  import { entityPath, matchRoute } from './contract.js';
5
6
  import { parseQueryParams } from './query.js';
6
7
  /** `Company (crm.Company)`: the class, and the table it maps, which is what tells two apart. */
@@ -16,14 +17,14 @@ export function createRequestHandler(opts) {
16
17
  entities = entities.filter((entity) => !exclude.includes(entity));
17
18
  }
18
19
  if (!entities.length) {
19
- throw new TypeError('no entities for the uql middleware');
20
+ throw new UqlUsageError('no entities for the uql middleware');
20
21
  }
21
22
  // All of them at once, so fixing the first collision does not just reveal the next.
22
23
  const byPath = Map.groupBy(entities, pathOf);
23
24
  const collisions = [...byPath].filter(([, clashing]) => clashing.length > 1);
24
25
  if (collisions.length) {
25
26
  const lines = collisions.map(([path, clashing]) => ` /${path} <- ${clashing.map(tableOf).join(', ')}`);
26
- throw new TypeError(`every entity below shares a route with another, so all but the first are unreachable:\n${lines.join('\n')}\n` +
27
+ throw new UqlUsageError(`every entity below shares a route with another, so all but the first are unreachable:\n${lines.join('\n')}\n` +
27
28
  "A route is the kebab-cased class name unless 'entityPath' says otherwise. Name them apart, " +
28
29
  "pass an 'entityPath', or pass only one of them in 'include'.");
29
30
  }
@@ -157,6 +158,6 @@ function ok(body) {
157
158
  return { status: 200, body };
158
159
  }
159
160
  function buildIdQuery(meta, id, query) {
160
- query.$where = { ...query.$where, [soleIdOf(meta, 'the HTTP handler')]: id };
161
+ query.$where = whereWith(soleIdOf(meta, 'the HTTP handler'), id, query.$where);
161
162
  return query;
162
163
  }
@@ -3,7 +3,7 @@ import type { WireQuery } from '../type/index.js';
3
3
  * Parse raw query-string entries (with JSON-stringified values) into a UQL query object.
4
4
  * Symmetric counterpart of {@link stringifyQuery}. Only {@link ALLOWED_QUERY_KEYS} are honored.
5
5
  */
6
- export declare function parseQueryParams(params?: Record<string, unknown>): WireQuery<unknown>;
6
+ export declare function parseQueryParams<E = unknown>(params?: Record<string, unknown>): WireQuery<E>;
7
7
  /**
8
8
  * Serialize a UQL query object into a percent-encoded query string where object values
9
9
  * are JSON-stringified. Symmetric counterpart of {@link parseQueryParams}.
@@ -105,11 +105,11 @@ export function wireJson(value) {
105
105
  return held;
106
106
  }
107
107
  if (RAW_VALUE in held) {
108
- throw new TypeError('raw SQL cannot travel over HTTP: what leaves the browser is JSON');
108
+ throw new UqlUsageError('raw SQL cannot travel over HTTP: what leaves the browser is JSON');
109
109
  }
110
110
  // A blob is a field value, so no type parameter reaches it: this is the only place it is caught.
111
111
  if (held instanceof ArrayBuffer || ArrayBuffer.isView(held)) {
112
- throw new TypeError('binary cannot travel over HTTP: what leaves the browser is JSON');
112
+ throw new UqlUsageError('binary cannot travel over HTTP: what leaves the browser is JSON');
113
113
  }
114
114
  return held;
115
115
  });
package/dist/index.d.ts CHANGED
@@ -8,4 +8,5 @@ export { withDeleted } from './util/filters.util.js';
8
8
  export type { HookContext } from './util/hook.util.js';
9
9
  export { DefaultLogger } from './util/logger.js';
10
10
  export { raw, refs } from './util/raw.js';
11
+ export { deleteFrom, insertInto, updateTable } from './util/triggerWrite.js';
11
12
  export * from './util/uqlError.js';
package/dist/index.js CHANGED
@@ -7,4 +7,5 @@ export * from './type/index.js';
7
7
  export { withDeleted } from './util/filters.util.js';
8
8
  export { DefaultLogger } from './util/logger.js';
9
9
  export { raw, refs } from './util/raw.js';
10
+ export { deleteFrom, insertInto, updateTable } from './util/triggerWrite.js';
10
11
  export * from './util/uqlError.js';