uql-orm 0.80.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 (123) 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/dialect/vectorSqlDialect.d.ts +2 -0
  13. package/dist/dialect/vectorSqlDialect.js +4 -0
  14. package/dist/entity/metadata/definition.d.ts +1 -2
  15. package/dist/entity/metadata/definition.js +37 -39
  16. package/dist/http/handler.js +5 -4
  17. package/dist/http/query.d.ts +1 -1
  18. package/dist/http/query.js +2 -2
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.js +1 -0
  21. package/dist/migrate/acquireQuerierForMigrations.js +2 -1
  22. package/dist/migrate/assertCliConfig.js +7 -6
  23. package/dist/migrate/bin.js +0 -0
  24. package/dist/migrate/builder/expressions.d.ts +2 -0
  25. package/dist/migrate/builder/expressions.js +26 -1
  26. package/dist/migrate/cli-config.js +5 -4
  27. package/dist/migrate/cli.js +1 -1
  28. package/dist/migrate/codegen/entityCodeGenerator.js +2 -2
  29. package/dist/migrate/codegen/indexDecoratorSource.d.ts +3 -2
  30. package/dist/migrate/codegen/indexDecoratorSource.js +5 -23
  31. package/dist/migrate/ddl/indexDdl.js +4 -3
  32. package/dist/migrate/ddl/mssqlTableDdl.d.ts +4 -4
  33. package/dist/migrate/ddl/mssqlTableDdl.js +20 -14
  34. package/dist/migrate/ddl/mysqlIndexDdl.d.ts +2 -2
  35. package/dist/migrate/ddl/mysqlIndexDdl.js +12 -10
  36. package/dist/migrate/ddl/pgIndexDdl.d.ts +2 -1
  37. package/dist/migrate/ddl/pgIndexDdl.js +10 -7
  38. package/dist/migrate/ddl/sqliteIndexDdl.js +2 -1
  39. package/dist/migrate/ddl/tableDdl.d.ts +3 -2
  40. package/dist/migrate/ddl/tableDdl.js +11 -8
  41. package/dist/migrate/drift/driftDetector.d.ts +4 -5
  42. package/dist/migrate/drift/driftDetector.js +21 -21
  43. package/dist/migrate/generator/definitionToNode.d.ts +1 -1
  44. package/dist/migrate/generator/definitionToNode.js +9 -20
  45. package/dist/migrate/generator/mongoCommand.js +2 -1
  46. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +1 -1
  47. package/dist/migrate/generator/mongoSchemaGenerator.js +17 -24
  48. package/dist/migrate/index.d.ts +2 -1
  49. package/dist/migrate/index.js +1 -0
  50. package/dist/migrate/indexPredicate.js +2 -1
  51. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +5 -7
  52. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +13 -19
  53. package/dist/migrate/introspection/baseSqlIntrospector.js +7 -18
  54. package/dist/migrate/introspection/mongoIntrospector.d.ts +1 -1
  55. package/dist/migrate/introspection/mongoIntrospector.js +5 -4
  56. package/dist/migrate/introspection/mssqlIntrospector.js +2 -1
  57. package/dist/migrate/introspection/mysqlIntrospector.d.ts +15 -5
  58. package/dist/migrate/introspection/mysqlIntrospector.js +32 -4
  59. package/dist/migrate/introspection/postgresIntrospector.d.ts +29 -21
  60. package/dist/migrate/introspection/postgresIntrospector.js +63 -46
  61. package/dist/migrate/introspection/sqliteIntrospector.js +11 -9
  62. package/dist/migrate/migrationTarget.js +2 -1
  63. package/dist/migrate/migrator.d.ts +5 -0
  64. package/dist/migrate/migrator.js +35 -45
  65. package/dist/migrate/schemaChange.d.ts +18 -0
  66. package/dist/migrate/schemaChange.js +37 -0
  67. package/dist/migrate/schemaGenerator.d.ts +13 -14
  68. package/dist/migrate/schemaGenerator.js +88 -181
  69. package/dist/migrate/triggerSql.d.ts +1 -1
  70. package/dist/migrate/triggerSql.js +77 -61
  71. package/dist/mongo/mongoDialect.d.ts +1 -3
  72. package/dist/mongo/mongoDialect.js +9 -14
  73. package/dist/mongo/mongodbQuerier.js +3 -7
  74. package/dist/querier/abstractQuerier.d.ts +9 -4
  75. package/dist/querier/abstractQuerier.js +26 -19
  76. package/dist/querier/abstractQuerierPool.d.ts +3 -3
  77. package/dist/querier/abstractSqlQuerier.d.ts +2 -2
  78. package/dist/querier/abstractSqlQuerier.js +1 -1
  79. package/dist/querier/abstractSqlQuerierPool.d.ts +2 -2
  80. package/dist/querier/queryError.d.ts +2 -2
  81. package/dist/schema/indexDifferences.d.ts +22 -6
  82. package/dist/schema/indexDifferences.js +23 -8
  83. package/dist/schema/matchByKey.d.ts +10 -0
  84. package/dist/schema/matchByKey.js +18 -0
  85. package/dist/schema/schemaAST.d.ts +6 -2
  86. package/dist/schema/schemaAST.js +7 -3
  87. package/dist/schema/schemaASTBuilder.d.ts +2 -0
  88. package/dist/schema/schemaASTBuilder.js +17 -11
  89. package/dist/schema/schemaASTDiffer.d.ts +2 -3
  90. package/dist/schema/schemaASTDiffer.js +15 -36
  91. package/dist/schema/types.d.ts +14 -15
  92. package/dist/type/dialect.d.ts +23 -1
  93. package/dist/type/entity.d.ts +15 -11
  94. package/dist/type/logger.d.ts +2 -2
  95. package/dist/type/migration.d.ts +32 -50
  96. package/dist/type/querier.d.ts +3 -3
  97. package/dist/type/query.d.ts +3 -13
  98. package/dist/type/queryAggregate.d.ts +4 -10
  99. package/dist/type/queryRaw.d.ts +17 -3
  100. package/dist/type/queryRaw.js +2 -1
  101. package/dist/type/queryWhere.d.ts +7 -7
  102. package/dist/type/universalQuerier.d.ts +3 -3
  103. package/dist/type/vector.d.ts +2 -1
  104. package/dist/type/vector.js +2 -1
  105. package/dist/util/ddlExpression.util.d.ts +5 -1
  106. package/dist/util/ddlExpression.util.js +6 -2
  107. package/dist/util/dialect.util.d.ts +13 -5
  108. package/dist/util/dialect.util.js +28 -20
  109. package/dist/util/field.util.d.ts +4 -4
  110. package/dist/util/field.util.js +10 -2
  111. package/dist/util/hook.util.d.ts +1 -1
  112. package/dist/util/hook.util.js +8 -1
  113. package/dist/util/index.d.ts +1 -0
  114. package/dist/util/index.js +1 -0
  115. package/dist/util/logger.d.ts +3 -3
  116. package/dist/util/object.util.js +3 -2
  117. package/dist/util/raw.d.ts +6 -7
  118. package/dist/util/raw.js +10 -12
  119. package/dist/util/sqlLiteral.js +3 -2
  120. package/dist/util/triggerWrite.d.ts +15 -0
  121. package/dist/util/triggerWrite.js +20 -0
  122. package/package.json +1 -1
  123. package/skills/uql-orm/SKILL.md +3 -3
@@ -118,14 +118,7 @@ export interface ColumnSchema extends Omit<ColumnNode, 'type' | 'table' | 'refer
118
118
  export interface TableSchema {
119
119
  readonly name: string;
120
120
  readonly columns: ColumnSchema[];
121
- /** The key's columns **in order**, which is what a composite is: `(a, b)` is not `(b, a)`. */
122
- readonly primaryKey?: string[];
123
- /**
124
- * What the engine calls the key's constraint, where it names one at all - Postgres's `Member_pkey`,
125
- * MySQL's literal `PRIMARY`, nothing on SQLite. Only a `DROP` needs it, and only the name the
126
- * database actually reported will do: a derived one would name a constraint that is not there.
127
- */
128
- readonly primaryKeyName?: string;
121
+ readonly primaryKey?: PrimaryKeySchema;
129
122
  readonly indexes?: IndexSchema[];
130
123
  readonly foreignKeys?: ForeignKeySchema[];
131
124
  }
@@ -176,8 +169,26 @@ export interface ForeignKeySchema {
176
169
  readonly onUpdate?: ForeignKeyAction;
177
170
  }
178
171
  /**
179
- * Represents a difference between current and desired schema
172
+ * One object's change: added (`to` alone), dropped (`from` alone), or altered (both), each whole so the
173
+ * change is undone by swapping its ends. No engine alters an index, a key or a foreign key in place, so
174
+ * an alter of one is its drop and its add, which safe mode holds back together.
180
175
  */
176
+ export interface Change<T> {
177
+ readonly from?: T;
178
+ readonly to?: T;
179
+ }
180
+ /** A primary key, whichever side it is read from: the entities, the database, or a diff between them. */
181
+ export interface PrimaryKeySchema {
182
+ /** Its columns **in order**, which is what a composite is: `(a, b)` is not `(b, a)`. */
183
+ readonly columns: readonly string[];
184
+ /**
185
+ * What the engine calls its constraint, where it names one at all - Postgres's `Member_pkey`, MySQL's
186
+ * literal `PRIMARY`, nothing on SQLite. Only a `DROP` needs it, and only the name the database
187
+ * reported will do: a derived one would name a constraint that is not there.
188
+ */
189
+ readonly name?: string;
190
+ }
191
+ /** A table's differences from what its entity declares, or the table to create or drop. */
181
192
  export interface SchemaDiff {
182
193
  /** Qualified where the table has a schema, since it is also the key the table is found under. */
183
194
  readonly tableName: string;
@@ -187,36 +198,10 @@ export interface SchemaDiff {
187
198
  */
188
199
  readonly schema?: string;
189
200
  readonly type: 'create' | 'alter' | 'drop';
190
- /**
191
- * The table's key against the entity's, where their columns differ; `fromName` is the name the
192
- * database reported, which is what a `DROP` needs.
193
- */
194
- readonly primaryKey?: {
195
- readonly from: string[];
196
- readonly to: string[];
197
- readonly fromName?: string;
198
- };
199
- readonly columnsToAdd?: ColumnSchema[];
200
- readonly columnsToAlter?: {
201
- from: ColumnSchema;
202
- to: ColumnSchema;
203
- }[];
204
- readonly columnsToDrop?: string[];
205
- readonly indexesToAdd?: IndexSchema[];
206
- /** Whole rather than by name, so the rollback can create each again. */
207
- readonly indexesToDrop?: IndexSchema[];
208
- readonly foreignKeysToAdd?: ForeignKeySchema[];
209
- /** Dropped under the name the *database* reported, which is the only name a `DROP` can use. */
210
- readonly foreignKeysToDrop?: string[];
211
- /**
212
- * A constraint whose referential actions changed. Its own field rather than a pair of entries in
213
- * the two above, because no engine alters an action in place: it is a drop and an add that have to
214
- * travel together, and safe mode has to hold back both or neither.
215
- */
216
- readonly foreignKeysToAlter?: {
217
- readonly from: ForeignKeySchema;
218
- readonly to: ForeignKeySchema;
219
- }[];
201
+ readonly primaryKey?: Change<PrimaryKeySchema>;
202
+ readonly columns?: readonly Change<ColumnSchema>[];
203
+ readonly indexes?: readonly Change<IndexSchema>[];
204
+ readonly foreignKeys?: readonly Change<ForeignKeySchema>[];
220
205
  }
221
206
  /**
222
207
  * What every sync entry point takes: `safe` keeps it additive, `drop` lets it remove a column, and
@@ -254,6 +239,8 @@ export type InstalledTriggers = ReadonlyMap<string, readonly string[]>;
254
239
  * Interface for generating DDL statements from entity metadata
255
240
  */
256
241
  export interface SchemaGenerator {
242
+ /** Whether a column's stored default is the one the entity declares, as the engine reprints it. Absent where columns are not compared. */
243
+ readonly defaultsEqual?: (expected: unknown, actual: unknown) => boolean;
257
244
  /** The whole schema for `entities`, tables then the foreign keys between them, which need every entity at once. */
258
245
  generateCreateSchema(entities: readonly Type<object>[], options?: CreateSchemaOptions): string[];
259
246
  /**
@@ -273,14 +260,8 @@ export interface SchemaGenerator {
273
260
  generateTriggersDown(entity: Type<object>, installed?: InstalledTriggers): string[];
274
261
  /** A `DROP` for each trigger uql owns among `names` on `entity`'s table, whatever the entity declares. */
275
262
  generateTriggerDrops(entity: Type<object>, names: readonly string[]): string[];
276
- /**
277
- * Generate ALTER TABLE statements based on schema diff
278
- */
263
+ /** The statements taking a table through `diff`; its rollback is the diff reversed, see `reverseDiff`. */
279
264
  generateAlterTable(diff: SchemaDiff): string[];
280
- /**
281
- * Generate rollback (down) statements for ALTER TABLE based on schema diff
282
- */
283
- generateAlterTableDown(diff: SchemaDiff): string[];
284
265
  /**
285
266
  * Generate CREATE INDEX statement
286
267
  */
@@ -333,11 +314,12 @@ export interface SchemaGenerator {
333
314
  */
334
315
  export interface SchemaIntrospector {
335
316
  /**
336
- * Every trigger uql installed in this schema, by table and then by name, each with the statements that
337
- * recreate it as it stands. The names say which to drop once an entity no longer declares them; the
338
- * statements are what a rollback puts back, read off the engine rather than recorded anywhere by uql.
317
+ * Every trigger uql installed on `table`, by name, each with the statements that recreate it as it
318
+ * stands. The names say which to drop once an entity no longer declares them; the statements are what
319
+ * a rollback puts back, read off the engine rather than recorded anywhere by uql. One table's alone,
320
+ * so reading it never meets a trigger another writer is dropping from some other table.
339
321
  */
340
- ownedTriggers(): Promise<Map<string, InstalledTriggers>>;
322
+ ownedTriggers(table: string): Promise<InstalledTriggers>;
341
323
  /**
342
324
  * What this introspector can read back about an index, and so all that diffing may compare.
343
325
  * Comparing a feature it cannot read reports the same drift forever: the entity side declares it,
@@ -106,11 +106,11 @@ export interface SqlQuerier extends Querier {
106
106
  /**
107
107
  * Execute a raw SQL query and return results
108
108
  */
109
- all<T>(query: string, values?: unknown[]): Promise<T[]>;
109
+ all<T>(query: string, values?: readonly unknown[]): Promise<T[]>;
110
110
  /**
111
111
  * Execute a raw SQL command (INSERT, UPDATE, DELETE, DDL)
112
112
  */
113
- run(query: string, values?: unknown[]): Promise<QueryUpdateResult>;
113
+ run(query: string, values?: readonly unknown[]): Promise<QueryUpdateResult>;
114
114
  }
115
115
  /**
116
116
  * Type guard to check if a querier supports raw SQL execution
@@ -136,7 +136,7 @@ export declare function isMongoQuerier(querier: Querier): querier is MongoQuerie
136
136
  export type ListenerContext<E extends object = object> = {
137
137
  readonly entity: Type<E>;
138
138
  readonly querier: Querier;
139
- readonly payloads: E[];
139
+ readonly payloads: readonly E[];
140
140
  readonly event: HookEvent;
141
141
  };
142
142
  /**
@@ -1,4 +1,4 @@
1
- import type { FieldKey, JsonFieldPaths, RelationKey, RelationTarget, ToManyRelationKey, WrittenId } from './entity.js';
1
+ import type { FieldKey, FieldKeyOf, JsonFieldPaths, RelationKey, RelationTarget, ToManyRelationKey, WrittenId } from './entity.js';
2
2
  import type { QueryLock } from './queryLock.js';
3
3
  import type { QueryRaw } from './queryRaw.js';
4
4
  import type { QueryWhere } from './queryWhere.js';
@@ -145,17 +145,13 @@ export type QuerySortValue = QuerySortDirection | QueryVectorSearch;
145
145
  export type QuerySortByCount = {
146
146
  $count: QuerySortDirection;
147
147
  };
148
- /** The fields of `E` a vector search can rank by. */
149
- type VectorFieldKey<E> = {
150
- [P in FieldKey<E>]: NonNullable<E[P]> extends readonly number[] ? P : never;
151
- }[FieldKey<E>];
152
148
  /**
153
149
  * Ordering parents by the row of a to-many nearest a vector, per vector field: its distance is the
154
150
  * smallest of theirs. Nothing to `$project`, since no one row of the parent's answers under it. Never
155
151
  * where the target has no vector, since an empty map would admit any value at all.
156
152
  */
157
- export type QuerySortByNearest<E> = [VectorFieldKey<E>] extends [never] ? never : {
158
- [P in VectorFieldKey<E>]?: QueryVectorQuery;
153
+ export type QuerySortByNearest<E> = [FieldKeyOf<E, readonly number[]>] extends [never] ? never : {
154
+ [P in FieldKeyOf<E, readonly number[]>]?: QueryVectorQuery;
159
155
  };
160
156
  /**
161
157
  * Ordering by relevance to the `$text` at the root of `$where`, in either direction as any key sorts. The
@@ -371,12 +367,6 @@ Pick<E, Exclude<ProjectedKeys<E, S, V, X, P>, PopulatedToMany<E, P>> & keyof E>
371
367
  } : E;
372
368
  /** The to-many relations a query populated, which come back as lists rather than as optional ones. */
373
369
  type PopulatedToMany<E, P> = Extract<P, ToManyRelationKey<E>>;
374
- /**
375
- * stringified query.
376
- */
377
- export type QueryStringified = {
378
- [K in keyof Query<unknown>]?: string;
379
- };
380
370
  /** What upserting one row reports. `created` is only knowable for a single statement, so a batch has none. */
381
371
  export type QueryUpsertOneResult<E> = {
382
372
  readonly id?: WrittenId<E>;
@@ -1,4 +1,4 @@
1
- import type { FieldKey, RelationKey, RelationTarget } from './entity.js';
1
+ import type { FieldKey, FieldKeyOf, RelationKey, RelationTarget } from './entity.js';
2
2
  import type { QueryPager, QuerySelect, QuerySortDirection } from './query.js';
3
3
  import type { QueryRaw } from './queryRaw.js';
4
4
  import type { QueryWhere, QueryWhereFieldValue } from './queryWhere.js';
@@ -44,13 +44,6 @@ export declare function resolveAggregateOp(key: string): {
44
44
  export type QueryFieldRef<E, F extends keyof E = FieldKey<E>> = ExactlyOne<Required<QuerySelect<E, F, true>>>;
45
45
  /** The argument of an aggregate function: a field, or `'*'` (only meaningful for `COUNT(*)`). */
46
46
  export type QueryAggregateArg<E> = QueryFieldRef<E> | '*';
47
- /**
48
- * Fields `SUM`/`AVG` can total. Restricted to numeric columns because totalling a text or date one is
49
- * either an engine error or a coercion, and neither produces the value the signature promises.
50
- */
51
- type NumericFieldKey<E> = {
52
- readonly [K in FieldKey<E>]: [NonNullable<E[K]>] extends [number | bigint] ? K : never;
53
- }[FieldKey<E>];
54
47
  /** Every aggregate op, plain and DISTINCT-qualified. */
55
48
  type AggregateOp = QueryAggregateOp | QueryAggregateDistinctOp;
56
49
  /**
@@ -67,9 +60,10 @@ type AveragingOp = OpsOf<'$avg'>;
67
60
  type TotallingOp = SummingOp | AveragingOp;
68
61
  /**
69
62
  * Every aggregate op mapped to the argument it accepts: `$count` a field or `'*'` (`COUNT(*)`),
70
- * the totalling ops a numeric field, `$min`/`$max`/`$countDistinct` any field.
63
+ * the totalling ops a numeric field, since totalling any other is an engine error or a coercion,
64
+ * and `$min`/`$max`/`$countDistinct` any field.
71
65
  */
72
- type QueryAggregateArgMap<E> = Record<'$count', QueryAggregateArg<E>> & Record<TotallingOp, QueryFieldRef<E, NumericFieldKey<E>>> & Record<Exclude<AggregateOp, '$count' | TotallingOp>, QueryFieldRef<E>>;
66
+ type QueryAggregateArgMap<E> = Record<'$count', QueryAggregateArg<E>> & Record<TotallingOp, QueryFieldRef<E, FieldKeyOf<E, number | bigint>>> & Record<Exclude<AggregateOp, '$count' | TotallingOp>, QueryFieldRef<E>>;
73
67
  /**
74
68
  * An aggregate over one field, exactly one op per entry: `{ $sum: { amount: true } }` is `SUM("amount")`,
75
69
  * `{ $countDistinct: { id: true } }` is `COUNT(DISTINCT "id")`, and only `$count` takes `'*'`. Its own
@@ -15,6 +15,11 @@ export type QueryRawRenderOptions = {
15
15
  * computed field's own, or the one whose schema is built. Absent where a statement renders SQL.
16
16
  */
17
17
  entity?: Type<unknown>;
18
+ /**
19
+ * The `FROM` a set-based trigger's body reads its rows through, `FROM inserted` and the like, which a
20
+ * write in it names. Absent where the body reads `NEW` and `OLD` bare, and outside a trigger.
21
+ */
22
+ rows?: string;
18
23
  };
19
24
  /** {@link QueryRawRenderOptions} as the callers along the way fill them in, every one still optional. */
20
25
  export type QueryRawFnOptions = Partial<QueryRawRenderOptions>;
@@ -45,12 +50,21 @@ export declare class QueryRaw {
45
50
  }
46
51
  /**
47
52
  * A field of an entity as SQL, read off `refs(Entity)` or a definition's refs: interpolated into `raw`, it
48
- * renders as the field's column. Its `key` is how an index tells a column from an expression.
53
+ * renders as the field's column. Its `key` is how an index tells a column from an expression, and `V`,
54
+ * the field's type, is what a value slot checks it against: see {@link RawFor}.
49
55
  */
50
- export declare class ColumnRef<K extends string = string> extends QueryRaw {
56
+ export declare class ColumnRef<K extends string = string, V = unknown> extends QueryRaw {
51
57
  readonly key: K;
58
+ readonly __value?: V;
52
59
  constructor(key: K, value: QueryRawFn);
53
60
  }
61
+ /**
62
+ * SQL where a value of type `V` goes: bare SQL, whose type is its author's to know, or a ref to a column
63
+ * holding one, nullability aside. `Raw` is what the transport carries, so the wire's `never` stays one.
64
+ */
65
+ export type RawFor<Raw, V> = Raw & {
66
+ readonly __value?: V | null;
67
+ };
54
68
  /**
55
69
  * A relation aggregate as SQL, read off a `computed` field's refs: `(user) => user.resources.count()`.
56
70
  * It renders as the correlated subquery a `$count` reads, so a field holding one is read, filtered and
@@ -63,7 +77,7 @@ export declare class ColumnRef<K extends string = string> extends QueryRaw {
63
77
  export declare class RelationAggregate<V = unknown, Storable extends boolean = boolean> extends QueryRaw {
64
78
  /** What it reads, kept beside the SQL so a read decodes the value the way the target's field does. */
65
79
  readonly spec: RelationAggregateSpec;
66
- private readonly __value;
80
+ readonly __value?: V;
67
81
  private readonly __storable;
68
82
  constructor(
69
83
  /** What it reads, kept beside the SQL so a read decodes the value the way the target's field does. */
@@ -29,7 +29,8 @@ export class QueryRaw {
29
29
  }
30
30
  /**
31
31
  * A field of an entity as SQL, read off `refs(Entity)` or a definition's refs: interpolated into `raw`, it
32
- * renders as the field's column. Its `key` is how an index tells a column from an expression.
32
+ * renders as the field's column. Its `key` is how an index tells a column from an expression, and `V`,
33
+ * the field's type, is what a value slot checks it against: see {@link RawFor}.
33
34
  */
34
35
  export class ColumnRef extends QueryRaw {
35
36
  key;
@@ -1,6 +1,6 @@
1
1
  import type { FieldKey, JsonFieldPaths, JsonFieldPathValue, RelationKey, RelationTarget } from './entity.js';
2
2
  import type { QuerySelect } from './query.js';
3
- import type { QueryRaw } from './queryRaw.js';
3
+ import type { QueryRaw, RawFor } from './queryRaw.js';
4
4
  import type { AtLeastOne, ExpandScalar, IsMany, QueryComparableScalar, Scalar } from './utility.js';
5
5
  import type { QueryVectorQuery } from './vector.js';
6
6
  /**
@@ -136,7 +136,7 @@ export type QueryWhereFieldOperatorMap<T, Raw = QueryRaw> = {
136
136
  * whether a value is between two values (inclusive). Shorthand for $gte + $lte.
137
137
  * @example { age: { $between: [18, 65] } }
138
138
  */
139
- $between?: [ExpandScalar<T>, ExpandScalar<T>];
139
+ $between?: readonly [ExpandScalar<T>, ExpandScalar<T>];
140
140
  /**
141
141
  * whether a string begins with the given string (case sensitive).
142
142
  */
@@ -176,11 +176,11 @@ export type QueryWhereFieldOperatorMap<T, Raw = QueryRaw> = {
176
176
  /**
177
177
  * whether a value matches any of the given values.
178
178
  */
179
- $in?: ExpandScalar<T>[];
179
+ $in?: readonly ExpandScalar<T>[];
180
180
  /**
181
181
  * whether a value does not match any of the given values.
182
182
  */
183
- $nin?: ExpandScalar<T>[];
183
+ $nin?: readonly ExpandScalar<T>[];
184
184
  /**
185
185
  * whether a value is null.
186
186
  * @example { deletedAt: { $isNull: true } }
@@ -195,7 +195,7 @@ export type QueryWhereFieldOperatorMap<T, Raw = QueryRaw> = {
195
195
  * whether an array contains all the specified values.
196
196
  * @example { tags: { $all: ['typescript', 'orm'] } }
197
197
  */
198
- $all?: unknown extends T ? unknown[] : NonNullable<T> extends readonly (infer U)[] ? ExpandScalar<U>[] : never;
198
+ $all?: unknown extends T ? readonly unknown[] : NonNullable<T> extends readonly (infer U)[] ? readonly ExpandScalar<U>[] : never;
199
199
  /** whether an array has the given length, or one in range: `{ roles: { $size: { $gte: 2 } } }`. */
200
200
  $size?: number | QuerySizeComparisonOps;
201
201
  /**
@@ -284,9 +284,9 @@ type IsUntypedColumn<T> = [Scalar] extends [NonNullable<T>] ? true : false;
284
284
  * A field's filter value: the value, `null` where it is optional, a list as an implicit `$in` (not on
285
285
  * an array field, where it would be ambiguous), or an operator map.
286
286
  */
287
- export type QueryWhereFieldValue<T, Raw = QueryRaw> = T | (undefined extends T ? null : never) | (IsMany<T> extends true ? never : T[]) | QueryWhereFieldOperators<T, Raw> | Raw;
287
+ export type QueryWhereFieldValue<T, Raw = QueryRaw> = T | (undefined extends T ? null : never) | (IsMany<T> extends true ? never : readonly T[]) | QueryWhereFieldOperators<T, Raw> | RawFor<Raw, T>;
288
288
  /**
289
289
  * query filter array - the value every {@link QueryGroupOp} takes.
290
290
  */
291
- export type QueryWhereArray<E, Raw = QueryRaw> = (QueryWhere<E, Raw> | Raw)[];
291
+ export type QueryWhereArray<E, Raw = QueryRaw> = readonly (QueryWhere<E, Raw> | Raw)[];
292
292
  export {};
@@ -65,11 +65,11 @@ export interface UniversalQuerier extends SharedQuerier<'server', QueryOptions>
65
65
  * Ids are exact everywhere but MySQL, which infers them from its header and reports `undefined` rather
66
66
  * than a guess where it cannot: a batch naming some keys, or a key that is not `AUTO_INCREMENT`.
67
67
  */
68
- insertMany<E extends object>(entity: Type<E>, payload: EntityWrite<E>[]): Promise<(WrittenId<E> | undefined)[]>;
68
+ insertMany<E extends object>(entity: Type<E>, payload: readonly EntityWrite<E>[]): Promise<(WrittenId<E> | undefined)[]>;
69
69
  /** Insert or update a record by its conflict paths; resolves to its id and whether it was created. */
70
70
  upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityWrite<E>): Promise<QueryUpsertOneResult<E>>;
71
71
  /** Insert or update records by their conflict paths; resolves to their ids in payload order. */
72
- upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityWrite<E>[]): Promise<QueryUpsertManyResult<E>>;
72
+ upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: readonly EntityWrite<E>[]): Promise<QueryUpsertManyResult<E>>;
73
73
  /**
74
74
  * insert or update a record.
75
75
  * @param entity the entity to persist on
@@ -83,7 +83,7 @@ export interface UniversalQuerier extends SharedQuerier<'server', QueryOptions>
83
83
  * @param payload the data to be persisted
84
84
  * @return the IDs
85
85
  */
86
- saveMany<E extends object>(entity: Type<E>, payload: EntityWrite<E>[]): Promise<(WrittenId<E> | undefined)[]>;
86
+ saveMany<E extends object>(entity: Type<E>, payload: readonly EntityWrite<E>[]): Promise<(WrittenId<E> | undefined)[]>;
87
87
  /**
88
88
  * Restore soft-deleted records (sets the soft-delete field back to `null`). Throws if the
89
89
  * entity has no soft-delete field.
@@ -1,5 +1,6 @@
1
1
  import type { VectorCast } from '../dialect/vectorCast.js';
2
2
  import type { IndexType } from '../schema/types.js';
3
+ import { UqlUsageError } from '../util/uqlError.js';
3
4
  /**
4
5
  * A vector search's metric: `cosine` (the default), `l2`, `inner` product or `l1`. No hamming: every
5
6
  * engine's takes a bit vector, which no field type maps to.
@@ -37,7 +38,7 @@ export type VectorMetric = {
37
38
  readonly index?: string;
38
39
  };
39
40
  /** The error every dialect throws for a metric it lacks. */
40
- export declare function unsupportedVectorMetric(dialectName: string, distance: VectorDistance, indexName?: string): TypeError;
41
+ export declare function unsupportedVectorMetric(dialectName: string, distance: VectorDistance, indexName?: string): UqlUsageError;
41
42
  /**
42
43
  * Vector-specific tuning options shared by `@Index` decorator, entity metadata, and migration schema.
43
44
  */
@@ -1,9 +1,10 @@
1
+ import { UqlUsageError } from '../util/uqlError.js';
1
2
  /** The keys that describe the search rather than bound it, so `$near`'s bounds are what is left. */
2
3
  export const VECTOR_QUERY_KEYS = ['$vector', '$distance'];
3
4
  /** The error every dialect throws for a metric it lacks. */
4
5
  export function unsupportedVectorMetric(dialectName, distance, indexName) {
5
6
  const where = indexName === undefined ? '' : ` (index "${indexName}")`;
6
- return new TypeError(`${dialectName} does not support vector distance metric: ${distance}${where}`);
7
+ return new UqlUsageError(`${dialectName} does not support vector distance metric: ${distance}${where}`);
7
8
  }
8
9
  /** The metric a search or an index measures by where nothing names one: every engine with vectors has it. */
9
10
  export const DEFAULT_VECTOR_DISTANCE = 'cosine';
@@ -4,7 +4,11 @@ import { type EntityIndexColumn, type EntityIndexMeta, type EntityMeta, type Ind
4
4
  * object reach the schema as one: a column read off the refs as its key, any other `raw` as it is.
5
5
  */
6
6
  export declare function normalizeIndexColumn(entry: IndexColumnInput): EntityIndexColumn;
7
- /** Every index an entity declares: each `@Field({ index })` as the one-column `@Index` it is, then its `@Index`es. */
7
+ /**
8
+ * Every index an entity declares: each `@Field({ index })` or `@Field({ unique })` as the one-column
9
+ * `@Index` it is, then its `@Index`es. A unique column is a unique index, the one spelling of uniqueness
10
+ * every engine can add and drop.
11
+ */
8
12
  export declare function declaredIndexes<E>(meta: EntityMeta<E>): EntityIndexMeta<E>[];
9
13
  /** An index entry as the schema holds it, its expression rendered to text by `render`. */
10
14
  export declare function renderIndexColumn(entry: EntityIndexColumn, render: (sql: QueryRaw) => string): IndexColumnSchema;
@@ -9,9 +9,13 @@ export function normalizeIndexColumn(entry) {
9
9
  const { column, ...modifiers } = typeof entry === 'string' || entry instanceof QueryRaw ? { column: entry } : entry;
10
10
  return { ...modifiers, column: column instanceof ColumnRef ? column.key : column };
11
11
  }
12
- /** Every index an entity declares: each `@Field({ index })` as the one-column `@Index` it is, then its `@Index`es. */
12
+ /**
13
+ * Every index an entity declares: each `@Field({ index })` or `@Field({ unique })` as the one-column
14
+ * `@Index` it is, then its `@Index`es. A unique column is a unique index, the one spelling of uniqueness
15
+ * every engine can add and drop.
16
+ */
13
17
  export function declaredIndexes(meta) {
14
- const fieldIndexes = definedEntries(meta.fields).flatMap(([key, field]) => field.index
18
+ const fieldIndexes = definedEntries(meta.fields).flatMap(([key, field]) => field.index || field.unique
15
19
  ? [
16
20
  {
17
21
  columns: [{ column: key }],
@@ -1,5 +1,5 @@
1
1
  import type { IndexType } from '../schema/types.js';
2
- import { type AggregateCall, type CascadeType, type EntityData, type EntityId, type EntityIndexMeta, type EntityMeta, type FieldKey, type FieldOptions, type FieldUpdateOp, type JsonUpdateOp, type OnFieldCallback, type Query, type QueryAggMap, type QueryExclude, type QueryGroupMap, type QueryOptions, QueryRaw, type QuerySearch, type QuerySelect, type QuerySelectValue, type QuerySortDirection, type QuerySizeComparisonOps, type QuerySortMap, type QueryTextSearchOptions, type QueryVectorQuery, type QueryVectorSearch, type QueryWhere, type RelationKey, type UpdatePayload, type VectorDistance } from '../type/index.js';
2
+ import { type AggregateCall, type CascadeType, type EntityData, type EntityId, type EntityIndexMeta, type EntityMeta, type FieldKey, type FieldOptions, type FieldUpdateOp, type JsonUpdateOp, type OnFieldCallback, type Query, type QueryAggMap, type QueryConflictPaths, type QueryExclude, type QueryGroupMap, type QueryOptions, QueryRaw, type QuerySearch, type QuerySelect, type QuerySelectValue, type QuerySortDirection, type QuerySizeComparisonOps, type QuerySortMap, type QueryTextSearchOptions, type QueryVectorQuery, type QueryVectorSearch, type QueryWhere, type QueryWhereArray, type RelationKey, type UpdatePayload, type VectorDistance } from '../type/index.js';
3
3
  export type CallbackKey = keyof Pick<FieldOptions, 'onInsert' | 'onUpdate'>;
4
4
  /** The keys of `payload` a write persists as columns. */
5
5
  export declare function filterFieldKeys<E>(meta: EntityMeta<E>, payload: EntityData<E> | UpdatePayload<E>, callbackKey: CallbackKey): FieldKey<E>[];
@@ -50,10 +50,18 @@ export declare function isCascadable(action: CascadeType, configuration?: boolea
50
50
  */
51
51
  export declare function isPagedQuery<E>(q: QuerySearch<E>): boolean;
52
52
  /**
53
- * `q` selecting nothing but the id: what a write hands its backend's own read builder to settle the
54
- * rows it will name. The cast is unavoidable - a computed key is not a `QuerySelect` key to the
55
- * compiler - so it is spelled once here rather than in each querier.
56
- */
53
+ * Each of `keys` switched on, as a `$select` or conflict paths name them. This and the `where*` builders
54
+ * below hold the casts a statement built in generic code needs: a key read at run time is no key of
55
+ * these maps to the compiler, whose values it works out per entity.
56
+ */
57
+ export declare function keySet<E>(keys: readonly FieldKey<E>[]): QueryConflictPaths<E>;
58
+ /** `where`, or no `$where`, with `key` held to `value` as well: spread, so the two `AND`. */
59
+ export declare function whereWith<E>(key: FieldKey<E>, value: unknown, where?: QueryWhere<E>): QueryWhere<E>;
60
+ /** The `$where` holding each of `keys` to what `valueOf` reads for it. */
61
+ export declare function whereEach<E>(keys: readonly FieldKey<E>[], valueOf: (key: FieldKey<E>) => unknown): QueryWhere<E>;
62
+ /** The `$where` any one of `clauses` satisfies. */
63
+ export declare function whereAnyOf<E>(clauses: QueryWhereArray<E>): QueryWhere<E>;
64
+ /** `q` selecting nothing but the id: what a write hands its backend's own read builder to settle the rows it will name. */
57
65
  export declare function idOnlyQuery<E>(meta: EntityMeta<E>, q: QuerySearch<E>): Query<E>;
58
66
  /**
59
67
  * The map form of a `$select` value, or `undefined` for the raw-array form. Centralizes the one
@@ -2,7 +2,7 @@ import { getContext, UqlSecurityError } from '../context/context.js';
2
2
  import { soleIdOf } from '../entity/metadata/definition.js';
3
3
  import { QueryRaw, resolveAggregateOp, SOFT_DELETE_FILTER, } from '../type/index.js';
4
4
  import { DEFAULT_VECTOR_DISTANCE, VECTOR_INDEX_TYPES } from '../type/vector.js';
5
- import { getFieldKeys, isDatabaseWritten } from './field.util.js';
5
+ import { defaultReadKeys, fieldKeys, isDatabaseWritten } from './field.util.js';
6
6
  import { entityName, getKeys, hasKeys, isOperatorObject, isScalarId, isRecord, isWhereMap, someKey, } from './object.util.js';
7
7
  import { UqlUsageError } from './uqlError.js';
8
8
  /** The keys of `payload` a write persists as columns. */
@@ -50,12 +50,7 @@ export function getInsertFieldKeys(meta, payloads) {
50
50
  for (const record of payloads) {
51
51
  addInsertFieldKeys(meta, record, seen, keys);
52
52
  }
53
- for (const key of getKeys(meta.fields)) {
54
- if (meta.fields[key].onInsert !== undefined && !seen.has(key)) {
55
- keys.push(key);
56
- }
57
- }
58
- return keys;
53
+ return [...keys, ...fieldKeys(meta, (field) => field.onInsert !== undefined).filter((key) => !seen.has(key))];
59
54
  }
60
55
  export function getFieldCallbackValue(val) {
61
56
  return typeof val === 'function' ? val() : val;
@@ -72,7 +67,7 @@ export function fillOnFields(meta, payload, callbackKey) {
72
67
  const payloads = Array.isArray(payload) ? payload : [payload];
73
68
  // By presence, not truthiness, as `addInsertFieldKeys` above reads it: `onInsert: 0` and `onInsert: ''`
74
69
  // are values a caller meant, and a falsy one was silently never filled.
75
- const keys = getKeys(meta.fields).filter((key) => meta.fields[key][callbackKey] !== undefined);
70
+ const keys = fieldKeys(meta, (field) => field[callbackKey] !== undefined);
76
71
  if (keys.length === 0) {
77
72
  return payloads;
78
73
  }
@@ -120,12 +115,28 @@ export function isPagedQuery(q) {
120
115
  return q.$sort !== undefined || q.$limit !== undefined || q.$skip !== undefined;
121
116
  }
122
117
  /**
123
- * `q` selecting nothing but the id: what a write hands its backend's own read builder to settle the
124
- * rows it will name. The cast is unavoidable - a computed key is not a `QuerySelect` key to the
125
- * compiler - so it is spelled once here rather than in each querier.
118
+ * Each of `keys` switched on, as a `$select` or conflict paths name them. This and the `where*` builders
119
+ * below hold the casts a statement built in generic code needs: a key read at run time is no key of
120
+ * these maps to the compiler, whose values it works out per entity.
126
121
  */
122
+ export function keySet(keys) {
123
+ return Object.fromEntries(keys.map((key) => [key, true]));
124
+ }
125
+ /** `where`, or no `$where`, with `key` held to `value` as well: spread, so the two `AND`. */
126
+ export function whereWith(key, value, where) {
127
+ return { ...where, [key]: value };
128
+ }
129
+ /** The `$where` holding each of `keys` to what `valueOf` reads for it. */
130
+ export function whereEach(keys, valueOf) {
131
+ return Object.fromEntries(keys.map((key) => [key, valueOf(key)]));
132
+ }
133
+ /** The `$where` any one of `clauses` satisfies. */
134
+ export function whereAnyOf(clauses) {
135
+ return { $or: clauses };
136
+ }
137
+ /** `q` selecting nothing but the id: what a write hands its backend's own read builder to settle the rows it will name. */
127
138
  export function idOnlyQuery(meta, q) {
128
- return { ...q, $select: Object.fromEntries(meta.ids.map((key) => [key, true])) };
139
+ return { ...q, $select: keySet(meta.ids) };
129
140
  }
130
141
  /**
131
142
  * The map form of a `$select` value, or `undefined` for the raw-array form. Centralizes the one
@@ -165,7 +176,7 @@ export function normalizeScalarFieldSelection(meta, select, exclude) {
165
176
  }
166
177
  }
167
178
  }
168
- const allFields = getFieldKeys(meta.fields);
179
+ const allFields = defaultReadKeys(meta);
169
180
  if (!excludedFields) {
170
181
  return allFields;
171
182
  }
@@ -217,10 +228,7 @@ export function findVectorIndex(meta, key) {
217
228
  * serves no other, else cosine. The one fallback every engine resolves, so none can rank by another.
218
229
  */
219
230
  export function vectorDistanceOf(meta, key, search) {
220
- return (search.$distance ??
221
- meta.fields[key]?.distance ??
222
- findVectorIndex(meta, key)?.distance ??
223
- DEFAULT_VECTOR_DISTANCE);
231
+ return (search.$distance ?? meta.fields[key]?.distance ?? findVectorIndex(meta, key)?.distance ?? DEFAULT_VECTOR_DISTANCE);
224
232
  }
225
233
  /**
226
234
  * Whether a `$where` filters by vector distance anywhere in its tree, `$and`/`$or`/`$not` included.
@@ -271,7 +279,7 @@ export function fieldUpdateOf(key, value) {
271
279
  */
272
280
  export function whereIds(meta, ids) {
273
281
  if (Array.isArray(ids) ? ids.every(isScalarId) : isScalarId(ids)) {
274
- return { [soleIdOf(meta, 'addressing by a bare id value')]: ids };
282
+ return whereWith(soleIdOf(meta, 'addressing by a bare id value'), ids);
275
283
  }
276
284
  return (Array.isArray(ids) ? { $or: ids } : ids);
277
285
  }
@@ -492,11 +500,11 @@ export function fulltextWeights(index) {
492
500
  return undefined;
493
501
  }
494
502
  if (index.type !== 'fulltext') {
495
- throw new TypeError(`a column weight ranks a fulltext index, and this one is ${index.type ?? 'btree'}`);
503
+ throw new UqlUsageError(`a column weight ranks a fulltext index, and this one is ${index.type ?? 'btree'}`);
496
504
  }
497
505
  const weights = index.entries.map(({ weight = 1 }) => {
498
506
  if (!Number.isInteger(weight) || weight < 1 || weight > MAX_TEXT_WEIGHT) {
499
- throw new TypeError(`a column weight is a whole number from 1 to ${MAX_TEXT_WEIGHT}, not ${weight}`);
507
+ throw new UqlUsageError(`a column weight is a whole number from 1 to ${MAX_TEXT_WEIGHT}, not ${weight}`);
500
508
  }
501
509
  return weight;
502
510
  });
@@ -1,4 +1,4 @@
1
- import { type ColumnFamily, type EntityMeta, type FieldKey, type FieldOptions, type StampEvent, type RelationAggregateSpec } from '../type/index.js';
1
+ import { type ColumnFamily, type EntityMeta, type FieldKey, type FieldMeta, type FieldOptions, type StampEvent, type RelationAggregateSpec } from '../type/index.js';
2
2
  /** The family of a logical field type, or `undefined` where it names none. */
3
3
  export declare function columnFamily(type: unknown): ColumnFamily | undefined;
4
4
  /**
@@ -39,11 +39,11 @@ export declare function isSoleIdField<E>(meta: EntityMeta<E>, field: FieldOption
39
39
  * states its width. The one answer the create statement and the diff both read.
40
40
  */
41
41
  export declare function isAutoIncrement(field: FieldOptions, isPrimaryKey: boolean): boolean;
42
+ /** The fields `meta` declares whose options `pick` accepts, in declaration order, each as the entity's own key. */
43
+ export declare function fieldKeys<E>(meta: EntityMeta<E>, pick: (field: FieldMeta) => unknown): FieldKey<E>[];
42
44
  /**
43
45
  * The fields a read answers with where it names none. A relation aggregate is left out unless it asks
44
46
  * for `eager: true`: it reads the related rows, which is what a relation does, and a relation is loaded
45
47
  * only when a query asks for it. Naming one in `$select` reads it, whatever the default.
46
48
  */
47
- export declare function getFieldKeys<E>(fields: {
48
- [K in FieldKey<E>]?: FieldOptions;
49
- }): FieldKey<E>[];
49
+ export declare function defaultReadKeys<E>(meta: EntityMeta<E>): FieldKey<E>[];
@@ -83,11 +83,19 @@ export function isAutoIncrement(field, isPrimaryKey) {
83
83
  return field.autoIncrement;
84
84
  return isPrimaryKey && columnFamily(field.type) === 'numeric' && !field.onInsert && !field.references;
85
85
  }
86
+ /** The fields `meta` declares whose options `pick` accepts, in declaration order, each as the entity's own key. */
87
+ export function fieldKeys(meta, pick) {
88
+ const fields = meta.fields;
89
+ return getKeys(fields).filter((key) => {
90
+ const field = fields[key];
91
+ return field !== undefined && Boolean(pick(field));
92
+ });
93
+ }
86
94
  /**
87
95
  * The fields a read answers with where it names none. A relation aggregate is left out unless it asks
88
96
  * for `eager: true`: it reads the related rows, which is what a relation does, and a relation is loaded
89
97
  * only when a query asks for it. Naming one in `$select` reads it, whatever the default.
90
98
  */
91
- export function getFieldKeys(fields) {
92
- return getKeys(fields).filter((field) => fields[field].eager ?? !aggregateOf(fields[field]));
99
+ export function defaultReadKeys(meta) {
100
+ return fieldKeys(meta, (field) => field.eager ?? !aggregateOf(field));
93
101
  }
@@ -11,4 +11,4 @@ export type HookContext = {
11
11
  * Hooks are invoked with `this` bound to the payload via `call`,
12
12
  * so mutations go directly to the original object.
13
13
  */
14
- export declare function runHooks<E extends object>(entity: Type<E>, event: HookEvent, payloads: E[], ctx: HookContext): Promise<void>;
14
+ export declare function runHooks<E extends object>(entity: Type<E>, event: HookEvent, payloads: readonly E[], ctx: HookContext): Promise<void>;
@@ -1,4 +1,5 @@
1
1
  import { getMeta } from '../entity/index.js';
2
+ import { UqlUsageError } from './uqlError.js';
2
3
  /**
3
4
  * Run all registered hooks for the given event on each payload.
4
5
  * Hooks are invoked with `this` bound to the payload via `call`,
@@ -9,9 +10,15 @@ export async function runHooks(entity, event, payloads, ctx) {
9
10
  const registrations = meta.hooks?.[event];
10
11
  if (!registrations?.length)
11
12
  return;
13
+ // A prototype is typed `any`, so what is read off it is typed here, where it enters.
14
+ const prototype = entity.prototype;
12
15
  for (const payload of payloads) {
13
16
  for (const { methodName } of registrations) {
14
- const result = entity.prototype[methodName].call(payload, ctx);
17
+ const method = prototype[methodName];
18
+ if (typeof method !== 'function') {
19
+ throw new UqlUsageError(`'${entity.name}' runs '${methodName}' on ${event}, but has no such method`);
20
+ }
21
+ const result = method.call(payload, ctx);
15
22
  if (result instanceof Promise)
16
23
  await result;
17
24
  }