uql-orm 0.81.0 → 0.83.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 (113) 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/cockroachdb/crdbQuerierPool.js +2 -2
  9. package/dist/dialect/abstractSqlDialect.d.ts +16 -6
  10. package/dist/dialect/abstractSqlDialect.js +110 -41
  11. package/dist/dialect/hydrateColumn.js +2 -12
  12. package/dist/dialect/mysqlLikeSqlDialect.d.ts +2 -0
  13. package/dist/dialect/mysqlLikeSqlDialect.js +5 -0
  14. package/dist/dialect/operators.d.ts +7 -1
  15. package/dist/dialect/operators.js +13 -1
  16. package/dist/dialect/pgLikeSqlDialect.d.ts +1 -0
  17. package/dist/dialect/pgLikeSqlDialect.js +13 -4
  18. package/dist/entity/metadata/definition.d.ts +1 -2
  19. package/dist/entity/metadata/definition.js +37 -39
  20. package/dist/http/handler.js +5 -4
  21. package/dist/http/query.d.ts +1 -1
  22. package/dist/http/query.js +2 -2
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.js +1 -0
  25. package/dist/maria/mariadbQuerierPool.js +4 -2
  26. package/dist/migrate/acquireQuerierForMigrations.js +2 -1
  27. package/dist/migrate/assertCliConfig.js +7 -6
  28. package/dist/migrate/bin.js +0 -0
  29. package/dist/migrate/builder/expressions.d.ts +2 -0
  30. package/dist/migrate/builder/expressions.js +20 -10
  31. package/dist/migrate/builder/tableBuilder.js +1 -1
  32. package/dist/migrate/cli-config.js +5 -4
  33. package/dist/migrate/ddl/indexDdl.js +4 -3
  34. package/dist/migrate/ddl/mysqlIndexDdl.js +5 -4
  35. package/dist/migrate/ddl/pgIndexDdl.js +2 -1
  36. package/dist/migrate/ddl/sqliteIndexDdl.js +2 -1
  37. package/dist/migrate/ddl/tableDdl.js +2 -1
  38. package/dist/migrate/generator/mongoCommand.js +2 -1
  39. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +1 -1
  40. package/dist/migrate/generator/mongoSchemaGenerator.js +7 -6
  41. package/dist/migrate/indexPredicate.js +2 -1
  42. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +2 -1
  43. package/dist/migrate/introspection/mongoIntrospector.d.ts +1 -1
  44. package/dist/migrate/introspection/mongoIntrospector.js +3 -2
  45. package/dist/migrate/introspection/mssqlIntrospector.js +13 -1
  46. package/dist/migrate/introspection/mysqlIntrospector.d.ts +2 -0
  47. package/dist/migrate/introspection/mysqlIntrospector.js +6 -2
  48. package/dist/migrate/introspection/postgresIntrospector.js +1 -1
  49. package/dist/migrate/migrationTarget.js +2 -1
  50. package/dist/migrate/migrator.js +2 -1
  51. package/dist/migrate/schemaGenerator.js +5 -4
  52. package/dist/migrate/storage/databaseStorage.js +1 -1
  53. package/dist/migrate/triggerSql.d.ts +1 -1
  54. package/dist/migrate/triggerSql.js +77 -61
  55. package/dist/mongo/mongoDialect.d.ts +1 -3
  56. package/dist/mongo/mongoDialect.js +9 -14
  57. package/dist/mongo/mongodbQuerier.js +7 -10
  58. package/dist/mssql/mssqlQuerier.d.ts +2 -0
  59. package/dist/mssql/mssqlQuerier.js +8 -5
  60. package/dist/mysql/mysql2QuerierPool.d.ts +1 -0
  61. package/dist/mysql/mysql2QuerierPool.js +20 -2
  62. package/dist/neon/neonQuerierPool.js +2 -2
  63. package/dist/pglite/pgliteQuerierPool.js +10 -4
  64. package/dist/postgres/pgQuerierPool.js +2 -2
  65. package/dist/postgres/{pgNumericTypes.d.ts → pgWireTypes.d.ts} +4 -3
  66. package/dist/postgres/{pgNumericTypes.js → pgWireTypes.js} +7 -3
  67. package/dist/querier/abstractQuerier.d.ts +9 -4
  68. package/dist/querier/abstractQuerier.js +26 -19
  69. package/dist/querier/abstractQuerierPool.d.ts +3 -3
  70. package/dist/querier/abstractSqlQuerier.d.ts +2 -2
  71. package/dist/querier/abstractSqlQuerier.js +1 -1
  72. package/dist/querier/abstractSqlQuerierPool.d.ts +2 -2
  73. package/dist/querier/queryError.d.ts +2 -2
  74. package/dist/schema/canonicalType.d.ts +3 -0
  75. package/dist/schema/canonicalType.js +31 -9
  76. package/dist/schema/schemaASTBuilder.js +2 -1
  77. package/dist/schema/schemaASTDiffer.js +4 -2
  78. package/dist/sqlite/sqliteDialect.d.ts +1 -3
  79. package/dist/sqlite/sqliteDialect.js +3 -6
  80. package/dist/type/dialect.d.ts +23 -1
  81. package/dist/type/entity.d.ts +16 -12
  82. package/dist/type/logger.d.ts +2 -2
  83. package/dist/type/querier.d.ts +3 -3
  84. package/dist/type/query.d.ts +3 -13
  85. package/dist/type/queryAggregate.d.ts +4 -10
  86. package/dist/type/queryRaw.d.ts +17 -3
  87. package/dist/type/queryRaw.js +2 -1
  88. package/dist/type/queryWhere.d.ts +7 -7
  89. package/dist/type/universalQuerier.d.ts +3 -3
  90. package/dist/type/vector.d.ts +2 -1
  91. package/dist/type/vector.js +2 -1
  92. package/dist/util/date.d.ts +11 -0
  93. package/dist/util/date.js +19 -0
  94. package/dist/util/dialect.util.d.ts +13 -5
  95. package/dist/util/dialect.util.js +28 -20
  96. package/dist/util/field.util.d.ts +4 -4
  97. package/dist/util/field.util.js +10 -2
  98. package/dist/util/fieldOption.util.d.ts +5 -3
  99. package/dist/util/fieldOption.util.js +6 -5
  100. package/dist/util/hook.util.d.ts +1 -1
  101. package/dist/util/hook.util.js +8 -1
  102. package/dist/util/index.d.ts +1 -0
  103. package/dist/util/index.js +1 -0
  104. package/dist/util/logger.d.ts +3 -3
  105. package/dist/util/object.util.js +3 -2
  106. package/dist/util/raw.d.ts +6 -7
  107. package/dist/util/raw.js +10 -12
  108. package/dist/util/sqlLiteral.d.ts +8 -1
  109. package/dist/util/sqlLiteral.js +14 -9
  110. package/dist/util/triggerWrite.d.ts +15 -0
  111. package/dist/util/triggerWrite.js +20 -0
  112. package/package.json +1 -1
  113. package/skills/uql-orm/SKILL.md +4 -4
@@ -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';
@@ -9,8 +9,10 @@ export class MariadbQuerierPool extends AbstractSqlQuerierPool {
9
9
  constructor(opts, extra) {
10
10
  super(new MariaDialect(dialectOptionsFrom(extra)), extra);
11
11
  // BIGINT stays the driver's `bigint`, which `MariadbQuerier` decodes by the rule every driver here
12
- // shares (`decodeWideNumber`) - not `bigIntAsNumber`, which rounds past 2^53 without a word.
13
- this.pool = createPool(opts);
12
+ // shares (`decodeWideNumber`) - not `bigIntAsNumber`, which rounds past 2^53 without a word. A date
13
+ // reads as its UTC text, which hydration decodes, since the connector would take it for local time
14
+ // whatever `timezone` says; that only sets the session's zone, UTC, so `NOW()` agrees.
15
+ this.pool = createPool({ timezone: 'Z', dateStrings: true, ...opts });
14
16
  // `mariadb` fires 'error' at runtime without declaring it, hence the cast; this makes it visible.
15
17
  attachPoolErrorHandler(this.pool, 'Idle MariaDB pool connection encountered an error', extra?.logger);
16
18
  }
@@ -1,4 +1,5 @@
1
1
  import { isMongoQuerier, isSqlQuerier, } from '../type/index.js';
2
+ import { UqlUsageError } from '../util/uqlError.js';
2
3
  /**
3
4
  * Querier used for schema migrations and the migration journal.
4
5
  *
@@ -34,7 +35,7 @@ export function withMongoQuerierForMigrations(pool, requiredBy, task) {
34
35
  function withQuerierOfKind(pool, isKind, error, task) {
35
36
  return withQuerierForMigrations(pool, (querier) => {
36
37
  if (!isKind(querier)) {
37
- throw new TypeError(error);
38
+ throw new UqlUsageError(error);
38
39
  }
39
40
  return task(querier);
40
41
  });
@@ -1,30 +1,31 @@
1
+ import { UqlUsageError } from '../util/uqlError.js';
1
2
  /**
2
3
  * Validates shape required for the migrations CLI (real `QuerierPool`, not config stubs).
3
4
  */
4
5
  export function assertCliConfig(config) {
5
6
  if (config === null || typeof config !== 'object') {
6
- throw new TypeError('Config must be a non-null object');
7
+ throw new UqlUsageError('Config must be a non-null object');
7
8
  }
8
9
  const c = config;
9
10
  const pool = c['pool'];
10
11
  if (pool === null || typeof pool !== 'object') {
11
- throw new TypeError('Config.pool is required and must be an object');
12
+ throw new UqlUsageError('Config.pool is required and must be an object');
12
13
  }
13
14
  const p = pool;
14
15
  for (const key of ['getQuerier', 'transaction', 'withQuerier']) {
15
16
  if (typeof p[key] !== 'function') {
16
- throw new TypeError(`Config.pool.${key} must be a function`);
17
+ throw new UqlUsageError(`Config.pool.${key} must be a function`);
17
18
  }
18
19
  }
19
20
  if (p['end'] !== undefined && typeof p['end'] !== 'function') {
20
- throw new TypeError('Config.pool.end must be a function when provided');
21
+ throw new UqlUsageError('Config.pool.end must be a function when provided');
21
22
  }
22
23
  const dialect = p['dialect'];
23
24
  if (dialect === null || typeof dialect !== 'object') {
24
- throw new TypeError('Config.pool.dialect is required and must be an object');
25
+ throw new UqlUsageError('Config.pool.dialect is required and must be an object');
25
26
  }
26
27
  const dialectName = dialect['dialectName'];
27
28
  if (typeof dialectName !== 'string') {
28
- throw new TypeError('Config.pool.dialect.dialectName must be a string');
29
+ throw new UqlUsageError('Config.pool.dialect.dialectName must be a string');
29
30
  }
30
31
  }
File without changes
@@ -13,6 +13,8 @@ export type DialectDefaults = {
13
13
  readonly expressions: SqlExpressionMap;
14
14
  /** Column types whose `DEFAULT` this engine takes only as a parenthesized expression. */
15
15
  readonly wrapTypes?: RegExp;
16
+ /** Column types whose `CURRENT_TIMESTAMP` default must repeat their precision, captured by the pattern. */
17
+ readonly preciseTypes?: RegExp;
16
18
  };
17
19
  /**
18
20
  * Looked up by name rather than carried on the dialect, which keeps DDL data out of the query
@@ -1,3 +1,4 @@
1
+ import { UqlUsageError } from '../../util/uqlError.js';
1
2
  const ANSI = {
2
3
  now: 'CURRENT_TIMESTAMP',
3
4
  currentDate: 'CURRENT_DATE',
@@ -14,6 +15,8 @@ const MYSQL = {
14
15
  };
15
16
  /** MySQL 8.0.13+ rejects `DEFAULT 'x'` on these but accepts `DEFAULT ('x')`, whatever the value. */
16
17
  const MYSQL_LARGE_TYPES = /^\s*(TINY|MEDIUM|LONG)?(TEXT|BLOB)|^\s*(JSON|GEOMETRY)\b/i;
18
+ /** A `DATETIME(3)` or `TIMESTAMP(3)`, whose fractional-second precision is captured. */
19
+ const MYSQL_PRECISE_TYPES = /^\s*(?:DATETIME|TIMESTAMP)\((\d)\)/i;
17
20
  /**
18
21
  * Looked up by name rather than carried on the dialect, which keeps DDL data out of the query
19
22
  * bundle - the same split that keeps `CANONICAL_TO_SQL` in `schema/canonicalType.ts`. `uuidv7()` is
@@ -23,8 +26,12 @@ const MYSQL_LARGE_TYPES = /^\s*(TINY|MEDIUM|LONG)?(TEXT|BLOB)|^\s*(JSON|GEOMETRY
23
26
  export const DIALECT_DEFAULTS = {
24
27
  postgres: { expressions: { ...PG, uuidv7: 'uuidv7()' } },
25
28
  cockroachdb: { expressions: PG },
26
- mysql: { expressions: MYSQL, wrapTypes: MYSQL_LARGE_TYPES },
27
- mariadb: { expressions: { ...MYSQL, uuidv7: 'UUID_v7()' }, wrapTypes: MYSQL_LARGE_TYPES },
29
+ mysql: { expressions: MYSQL, wrapTypes: MYSQL_LARGE_TYPES, preciseTypes: MYSQL_PRECISE_TYPES },
30
+ mariadb: {
31
+ expressions: { ...MYSQL, uuidv7: 'UUID_v7()' },
32
+ wrapTypes: MYSQL_LARGE_TYPES,
33
+ preciseTypes: MYSQL_PRECISE_TYPES,
34
+ },
28
35
  sqlite: { expressions: ANSI },
29
36
  // `SYSUTCDATETIME()` over `CURRENT_TIMESTAMP`, which is local time in the server's zone. No
30
37
  // `uuidv7`: `NEWSEQUENTIALID()` is an ordered v4 GUID, so it carries no readable timestamp and
@@ -79,7 +86,7 @@ export const expr = {
79
86
  * the result needs wrapping, which MySQL demands on its large types whatever the value.
80
87
  */
81
88
  export function formatDefaultValue(value, dialect, columnType) {
82
- const sql = defaultLiteral(value, dialect);
89
+ const sql = defaultLiteral(value, dialect, columnType);
83
90
  const { wrapTypes } = DIALECT_DEFAULTS[dialect.dialectName];
84
91
  return columnType !== undefined && wrapTypes?.test(columnType) ? `(${sql})` : sql;
85
92
  }
@@ -113,23 +120,26 @@ export function sameDefault(desired, current, dialect) {
113
120
  * cannot serve stay here: a boolean is `1` where booleans are integers, and a plain object or array
114
121
  * is JSON rather than the throw and the IN-list `escape` gives them.
115
122
  */
116
- function defaultLiteral(value, dialect) {
123
+ function defaultLiteral(value, dialect, columnType) {
117
124
  if (value === undefined || value === null) {
118
125
  return 'NULL';
119
126
  }
120
127
  if (SqlExpression.isExpression(value)) {
121
- return expressionSql(value, dialect);
128
+ return expressionSql(value, dialect, columnType);
122
129
  }
123
130
  if (typeof value === 'boolean') {
124
131
  return dialect.booleanLiteral === 'native' ? (value ? 'TRUE' : 'FALSE') : value ? '1' : '0';
125
132
  }
126
133
  return dialect.escape(typeof value === 'object' && !(value instanceof Date) ? JSON.stringify(value) : value);
127
134
  }
128
- function expressionSql(expression, dialect) {
129
- const { expressions } = DIALECT_DEFAULTS[dialect.dialectName];
130
- const sql = expression.kind === 'raw' ? expression.sql : expressions[expression.kind];
135
+ function expressionSql(expression, dialect, columnType) {
136
+ const { expressions, preciseTypes } = DIALECT_DEFAULTS[dialect.dialectName];
137
+ const raw = expression.kind === 'raw';
138
+ const sql = raw ? expression.sql : expressions[expression.kind];
131
139
  if (sql == null) {
132
- throw new TypeError(`${dialect.dialectName} has no '${expression.kind}' default; pass expr.raw(...) with SQL this engine accepts`);
140
+ throw new UqlUsageError(`${dialect.dialectName} has no '${expression.kind}' default; pass expr.raw(...) with SQL this engine accepts`);
133
141
  }
134
- return sql;
142
+ // A raw default is the caller's SQL, never rewritten.
143
+ const precision = raw || columnType === undefined ? undefined : preciseTypes?.exec(columnType)?.[1];
144
+ return precision === undefined ? sql : sql.replaceAll('CURRENT_TIMESTAMP', `CURRENT_TIMESTAMP(${precision})`);
135
145
  }
@@ -144,7 +144,7 @@ export class TableBuilder {
144
144
  return this.timestampNow('updatedAt');
145
145
  }
146
146
  timestampNow(name) {
147
- return this.add(name, { category: 'timestamp' }, { defaultValue: expr.now() });
147
+ return this.timestamptz(name, { defaultValue: expr.now() });
148
148
  }
149
149
  timestamps() {
150
150
  this.createdAt();
@@ -1,13 +1,14 @@
1
1
  import { stat } from 'node:fs/promises';
2
2
  import { resolve } from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
+ import { UqlUsageError } from '../util/uqlError.js';
4
5
  /**
5
6
  * Loads the config with a plain `import()`, leaving TypeScript to the runtime: uql bundles no transpiler,
6
7
  * since only the project knows its decorator spec. Node's type stripping handles no decorators.
7
8
  */
8
9
  async function importConfig(path) {
9
10
  const mod = (await import(pathToFileURL(path).href).catch((cause) => {
10
- throw new TypeError(`Could not import ${path}: ${cause?.message}\n` +
11
+ throw new UqlUsageError(`Could not import ${path}: ${cause?.message}\n` +
11
12
  'If it reaches entity classes, their decorators need a runtime that transforms TypeScript, not ' +
12
13
  'just one that strips its types. Run the CLI with `bun`, or with `node --import tsx` ' +
13
14
  '(`npm i -D tsx`). A JavaScript config, or passing the config inline, needs neither.', { cause });
@@ -21,14 +22,14 @@ export async function loadConfig(customPath) {
21
22
  .then(() => true)
22
23
  .catch(() => false);
23
24
  if (!exists) {
24
- throw new TypeError(`Could not find uql configuration file at ${customPath}`);
25
+ throw new UqlUsageError(`Could not find uql configuration file at ${customPath}`);
25
26
  }
26
27
  try {
27
28
  const config = await importConfig(fullPath);
28
29
  return config;
29
30
  }
30
31
  catch (error) {
31
- throw new TypeError(`Could not load configuration file at ${customPath}: ${error.message}`);
32
+ throw new UqlUsageError(`Could not load configuration file at ${customPath}: ${error.message}`);
32
33
  }
33
34
  }
34
35
  const configPaths = ['uql.config.ts', 'uql.config.js', 'uql.config.mjs', '.uqlrc.ts', '.uqlrc.js'];
@@ -42,5 +43,5 @@ export async function loadConfig(customPath) {
42
43
  return config;
43
44
  }
44
45
  }
45
- throw new TypeError('Could not find uql configuration file. Create a uql.config.ts or uql.config.js file in your project root.');
46
+ throw new UqlUsageError('Could not find uql configuration file. Create a uql.config.ts or uql.config.js file in your project root.');
46
47
  }
@@ -2,6 +2,7 @@ import { jsonTypeMode } from '../../dialect/jsonSql.js';
2
2
  import { INDEX_TYPES } from '../../schema/types.js';
3
3
  import { INDEX_FEATURE_LABELS, } from '../../type/index.js';
4
4
  import { fulltextConfig, getKeys } from '../../util/index.js';
5
+ import { UqlUsageError } from '../../util/uqlError.js';
5
6
  /**
6
7
  * What in an index asks for each feature. A `Record` over the feature union rather than a list, so a
7
8
  * feature added to {@link INDEX_FEATURE_LABELS} cannot reach a dialect without the test that decides
@@ -20,14 +21,14 @@ const INDEX_FEATURE_PROBES = {
20
21
  /** Refuses an index of a type `types` lacks, `hints` naming what to declare instead. */
21
22
  export function assertIndexType(index, types, dialectName, hints = new Map()) {
22
23
  if (index.type && !types.has(index.type)) {
23
- throw new TypeError(`${dialectName} has no ${index.type} index (index "${index.name}")` + (hints.get(index.type) ?? ''));
24
+ throw new UqlUsageError(`${dialectName} has no ${index.type} index (index "${index.name}")` + (hints.get(index.type) ?? ''));
24
25
  }
25
26
  }
26
27
  /** Refuses an index asking for a feature `features` lacks. */
27
28
  export function assertIndexFeatures(index, features, dialectName) {
28
29
  for (const feature of getKeys(INDEX_FEATURE_PROBES)) {
29
30
  if (INDEX_FEATURE_PROBES[feature](index) && !features.has(feature)) {
30
- throw new TypeError(`${dialectName} does not support ${INDEX_FEATURE_LABELS[feature]} (index "${index.name}")`);
31
+ throw new UqlUsageError(`${dialectName} does not support ${INDEX_FEATURE_LABELS[feature]} (index "${index.name}")`);
31
32
  }
32
33
  }
33
34
  }
@@ -122,7 +123,7 @@ export class IndexDdl {
122
123
  * every other dialect refuses `jsonArray` in {@link assertIndexFeatures} and never reaches this.
123
124
  */
124
125
  jsonArrayIndexExpr(_escapedColumn, _json) {
125
- throw new TypeError(`${this.dialect.dialectName} has no multi-valued index`);
126
+ throw new UqlUsageError(`${this.dialect.dialectName} has no multi-valued index`);
126
127
  }
127
128
  /** Postgres-wire dialects put a vector or user-declared operator class here. */
128
129
  indexColumnOpsClass(_entry, _index) {
@@ -1,5 +1,6 @@
1
1
  import { jsonTypeMode } from '../../dialect/jsonSql.js';
2
2
  import { indexDistance, isVectorIndexType, unsupportedVectorMetric, VECTOR_INDEX_TYPES } from '../../type/vector.js';
3
+ import { UqlUsageError } from '../../util/uqlError.js';
3
4
  import { IndexDdl } from './indexDdl.js';
4
5
  /**
5
6
  * A full-text index is its own keyword here (`CREATE FULLTEXT INDEX ... (cols)`); `USING fulltext` is
@@ -38,14 +39,14 @@ export class MySqlIndexDdl extends MysqlLikeIndexDdl {
38
39
  jsonPathIndexExpr(escapedColumn, json) {
39
40
  const mode = jsonTypeMode(json.type);
40
41
  if (mode === 'json') {
41
- throw new TypeError(`mysql cannot index the boolean JSON path '${json.path}', which compares as JSON`);
42
+ throw new UqlUsageError(`mysql cannot index the boolean JSON path '${json.path}', which compares as JSON`);
42
43
  }
43
44
  const expr = super.jsonPathIndexExpr(escapedColumn, json);
44
45
  if (mode === 'numeric') {
45
46
  return expr;
46
47
  }
47
48
  if (!json.length) {
48
- throw new TypeError(`a MySQL index over the string JSON path '${json.path}' needs a length`);
49
+ throw new UqlUsageError(`a MySQL index over the string JSON path '${json.path}' needs a length`);
49
50
  }
50
51
  return `CAST(${expr} AS CHAR(${json.length}) CHARACTER SET utf8mb4) COLLATE utf8mb4_bin`;
51
52
  }
@@ -132,13 +133,13 @@ function arrayCastType(json) {
132
133
  const type = json.type;
133
134
  const cast = ARRAY_CASTS.get(typeof type === 'string' ? type.toLowerCase() : type);
134
135
  if (!cast) {
135
- throw new TypeError(`mysql has no array cast for ${typeof type === 'string' ? type : type.name} elements`);
136
+ throw new UqlUsageError(`mysql has no array cast for ${typeof type === 'string' ? type : type.name} elements`);
136
137
  }
137
138
  if (cast !== 'CHAR' && cast !== 'BINARY') {
138
139
  return cast;
139
140
  }
140
141
  if (!json.length) {
141
- throw new TypeError(`a multi-valued index over ${cast === 'CHAR' ? 'string' : 'binary'} elements needs a length`);
142
+ throw new UqlUsageError(`a multi-valued index over ${cast === 'CHAR' ? 'string' : 'binary'} elements needs a length`);
142
143
  }
143
144
  return `${cast}(${json.length})`;
144
145
  }
@@ -1,4 +1,5 @@
1
1
  import { indexDistance, unsupportedVectorMetric } from '../../type/vector.js';
2
+ import { UqlUsageError } from '../../util/uqlError.js';
2
3
  import { IndexDdl } from './indexDdl.js';
3
4
  /** `CREATE INDEX ... USING hnsw ("embedding" vector_cosine_ops) WITH (m = ...)`, pgvector's form. */
4
5
  export class PgIndexDdl extends IndexDdl {
@@ -49,7 +50,7 @@ export class PgIndexDdl extends IndexDdl {
49
50
  const opsClass = `${vectorType}_${metric}_ops`;
50
51
  // IVFFlat has neither a sparsevec nor an L1 operator class; HNSW has all of them (pgvector 0.8.2).
51
52
  if (index.type === 'ivfflat' && (vectorType === 'sparsevec' || distance === 'l1')) {
52
- throw new TypeError(`ivfflat has no ${opsClass} operator class (index "${index.name}"); use hnsw`);
53
+ throw new UqlUsageError(`ivfflat has no ${opsClass} operator class (index "${index.name}"); use hnsw`);
53
54
  }
54
55
  return ` ${opsClass}`;
55
56
  }