uql-orm 0.41.1 → 0.42.1

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 (69) hide show
  1. package/dist/browser/querier/httpQuerier.d.ts +7 -7
  2. package/dist/browser/querier/httpQuerier.js +21 -6
  3. package/dist/browser/type/clientQuerier.d.ts +3 -3
  4. package/dist/browser/uql-browser.min.js +2 -2
  5. package/dist/browser/uql-browser.min.js.map +4 -4
  6. package/dist/dialect/abstractSqlDialect.d.ts +32 -3
  7. package/dist/dialect/abstractSqlDialect.js +58 -22
  8. package/dist/dialect/mysqlLikeSqlDialect.d.ts +3 -2
  9. package/dist/dialect/mysqlLikeSqlDialect.js +5 -3
  10. package/dist/dialect/pgLikeSqlDialect.d.ts +1 -1
  11. package/dist/dialect/pgLikeSqlDialect.js +2 -1
  12. package/dist/entity/decorator/entity.d.ts +1 -1
  13. package/dist/entity/decorator/entity.js +1 -1
  14. package/dist/entity/decorator/members.d.ts +10 -2
  15. package/dist/entity/index.d.ts +1 -1
  16. package/dist/entity/index.js +1 -1
  17. package/dist/entity/metadata/definition.d.ts +18 -1
  18. package/dist/entity/metadata/definition.js +91 -36
  19. package/dist/http/handler.js +4 -4
  20. package/dist/maria/mariaDialect.d.ts +2 -2
  21. package/dist/maria/mariaDialect.js +3 -2
  22. package/dist/migrate/builder/tableBuilder.js +5 -4
  23. package/dist/migrate/drift/driftDetector.js +16 -0
  24. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +7 -0
  25. package/dist/migrate/generator/mongoSchemaGenerator.js +24 -28
  26. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +9 -1
  27. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +11 -2
  28. package/dist/migrate/introspection/baseSqlIntrospector.js +6 -3
  29. package/dist/migrate/introspection/postgresIntrospector.js +1 -1
  30. package/dist/migrate/introspection/sqliteIntrospector.js +7 -3
  31. package/dist/migrate/migrator.js +6 -0
  32. package/dist/migrate/schemaGenerator.d.ts +44 -34
  33. package/dist/migrate/schemaGenerator.js +158 -141
  34. package/dist/mongo/mongoDialect.js +39 -14
  35. package/dist/mongo/mongodbQuerier.js +6 -3
  36. package/dist/postgres/postgresDialect.js +1 -1
  37. package/dist/querier/abstractQuerier.d.ts +16 -12
  38. package/dist/querier/abstractQuerier.js +88 -40
  39. package/dist/querier/abstractQuerierPool.d.ts +8 -8
  40. package/dist/querier/abstractSqlQuerier.d.ts +1 -1
  41. package/dist/querier/abstractSqlQuerier.js +12 -7
  42. package/dist/querier/relationCount.js +44 -33
  43. package/dist/schema/indexDifferences.d.ts +28 -0
  44. package/dist/schema/indexDifferences.js +46 -0
  45. package/dist/schema/schemaASTBuilder.js +38 -31
  46. package/dist/schema/schemaASTDiffer.d.ts +27 -1
  47. package/dist/schema/schemaASTDiffer.js +54 -18
  48. package/dist/schema/types.d.ts +46 -7
  49. package/dist/sqlite/sqliteDialect.d.ts +2 -1
  50. package/dist/sqlite/sqliteDialect.js +4 -1
  51. package/dist/type/dialect.d.ts +6 -0
  52. package/dist/type/entity.d.ts +57 -14
  53. package/dist/type/migration.d.ts +19 -0
  54. package/dist/type/queryWhere.d.ts +7 -2
  55. package/dist/type/universalQuerier.d.ts +8 -8
  56. package/dist/util/dialect.util.js +15 -14
  57. package/dist/util/field.util.d.ts +11 -1
  58. package/dist/util/field.util.js +12 -0
  59. package/dist/util/index.d.ts +1 -0
  60. package/dist/util/index.js +1 -0
  61. package/dist/util/object.util.d.ts +6 -0
  62. package/dist/util/object.util.js +18 -0
  63. package/dist/util/relationQuery.util.d.ts +48 -7
  64. package/dist/util/relationQuery.util.js +70 -9
  65. package/dist/util/rowKey.util.d.ts +9 -0
  66. package/dist/util/rowKey.util.js +24 -0
  67. package/dist/util/sql.util.d.ts +24 -7
  68. package/dist/util/sql.util.js +75 -10
  69. package/package.json +1 -1
@@ -13,6 +13,7 @@ export class SqliteDialect extends AbstractSqlDialect {
13
13
  dropTableCascade: false,
14
14
  renameColumn: true,
15
15
  foreignKeyAlter: false, // SQLite does not support adding FKs to existing tables
16
+ primaryKeyAlter: false, // nor changing a key: the only route is rebuilding the table
16
17
  columnComment: false, // SQLite does not support column comments
17
18
  vectorIndexRequiresNotNull: false,
18
19
  vectorSupportsLength: false,
@@ -21,7 +22,9 @@ export class SqliteDialect extends AbstractSqlDialect {
21
22
  };
22
23
  dialectName = 'sqlite';
23
24
  escapeIdChar = '`';
24
- serialPrimaryKey = 'INTEGER PRIMARY KEY AUTOINCREMENT';
25
+ serialType = 'INTEGER PRIMARY KEY AUTOINCREMENT';
26
+ // `AUTOINCREMENT` is only legal in that exact phrase, so the key cannot be lifted to table level.
27
+ serialDeclaresPrimaryKey = true;
25
28
  tableOptions = '';
26
29
  beginTransactionCommand = 'BEGIN TRANSACTION';
27
30
  commitTransactionCommand = 'COMMIT';
@@ -91,6 +91,12 @@ export interface EngineFeatures {
91
91
  readonly dropTableCascade: boolean;
92
92
  readonly renameColumn: boolean;
93
93
  readonly foreignKeyAlter: boolean;
94
+ /**
95
+ * Whether a table's primary key can be changed on an existing table. False on SQLite, whose only
96
+ * route is rebuilding the table - so a migration that would change one is refused by name rather
97
+ * than emitting DDL the engine rejects.
98
+ */
99
+ readonly primaryKeyAlter: boolean;
94
100
  /** Whether the dialect supports inline COMMENT on columns (MySQL/MariaDB). */
95
101
  readonly columnComment: boolean;
96
102
  /**
@@ -182,7 +182,7 @@ export type FieldValue<E> = E[FieldKey<E>];
182
182
  /**
183
183
  * Infers the name of the key identifier on an entity
184
184
  */
185
- export type IdKey<E> = E extends {
185
+ export type IdKey<E> = (E extends {
186
186
  [idKey]?: infer K;
187
187
  } ? K & FieldKey<E> : E extends {
188
188
  _id?: unknown;
@@ -190,15 +190,39 @@ export type IdKey<E> = E extends {
190
190
  id?: unknown;
191
191
  } ? 'id' & FieldKey<E> : E extends {
192
192
  uuid?: unknown;
193
- } ? 'uuid' & FieldKey<E> : FieldKey<E>;
193
+ } ? 'uuid' & FieldKey<E> : FieldKey<E>) & string;
194
194
  /**
195
195
  * Infers the value of the key identifier on an entity.
196
196
  *
197
+ * A composite key is addressed by an object carrying every key, which is also the `$where` map it
198
+ * reduces to - so both spellings are one type. Completeness is checked at run time by
199
+ * `assertIdValue`: TypeScript cannot accumulate `@Id` across properties into the class type, so it
200
+ * cannot know how many keys there are.
201
+ *
197
202
  * Nullable, because an entity declares its id optional - nothing has assigned one before the
198
203
  * insert. That puts `undefined` inside every by-id method's parameter, where it would mean "no
199
204
  * filter"; `assertIdValue` is what rejects it.
200
205
  */
201
206
  export type IdValue<E> = E[IdKey<E>];
207
+ /**
208
+ * How a row is addressed by its primary key: the value for a single key, an object carrying every
209
+ * key for a composite - which is also the `$where` map it reduces to, so both spellings are one type.
210
+ *
211
+ * Distinct from {@link IdValue}, the *column's* value, which stays a scalar: an id column holds a
212
+ * number, never an object. The two read alike on a single-key entity and are not the same thing -
213
+ * `findOneById` takes an `EntityId`, while the inserts return `IdValue | undefined`, which is why a
214
+ * composite insert reports no id rather than the map.
215
+ *
216
+ * The keys stay optional, and completeness is checked at run time by `assertIdValue`. Requiring them
217
+ * needs `IdKey` to be precise, which it is not: with no `id`/`_id`/`uuid` and no `idKey` brand it
218
+ * falls back to every field, so `IdKey<Membership>` accepts `'role'` and requiring the map would
219
+ * demand fields that are not keys. Making it conditional on the brand was tried and reverted - a
220
+ * conditional type does not reduce for an unresolved `E`, which left `QueryWhere<E>` opaque and broke
221
+ * assignability across the dialects.
222
+ */
223
+ export type EntityId<E> = IdValue<E> | {
224
+ [K in IdKey<E>]?: E[K];
225
+ };
202
226
  /**
203
227
  * Infers the values of the relations on an entity
204
228
  */
@@ -254,22 +278,34 @@ export type FieldType = StringConstructor | NumberConstructor | BooleanConstruct
254
278
  */
255
279
  export type TypeFor<V, T = NonNullable<V>> = IsJson<T> extends true ? JsonColumnType : IsJson<NonNullable<Unpacked<T>>> extends true ? JsonColumnType : T extends readonly number[] ? VectorColumnType : T extends string ? StringConstructor | StringColumnType : T extends number ? NumberConstructor | NumericColumnType : T extends bigint ? BigIntConstructor | NumericColumnType : T extends boolean ? BooleanConstructor | BooleanColumnType : T extends Date ? DateConstructor | DateColumnType : T extends Uint8Array ? BlobColumnType : FieldType;
256
280
  /**
257
- * Configurable options for a field, carrying `V`, the value the column holds: what a generator returns
258
- * and what a default is has to be that value, checked the same way the declared `type` is. `Scalar` by
259
- * by default, for the places that handle a field without knowing which one it is.
281
+ * A field as the registry holds it: what the user authored, plus what registration worked out.
282
+ *
283
+ * Separate from {@link FieldOptions} so neither of these can be written in a decorator. They used to
284
+ * live there behind an `@internal` tag and a "do not set this" note, which is a comment standing in
285
+ * for a type boundary.
260
286
  */
261
- export type FieldOptions<V = TsTypeOf<FieldType>> = {
262
- readonly name?: string;
263
- readonly isId?: true;
264
- readonly type?: FieldType;
287
+ export type FieldMeta<V = TsTypeOf<FieldType>> = FieldOptions<V> & {
265
288
  /**
266
289
  * Set by `defineField` when the field gave `references` but no `type`, so schema generation resolves
267
290
  * the column from the referenced primary key rather than from whatever ended up in `type`. That is
268
291
  * what keeps a `uuid` primary key from becoming TEXT on every foreign key pointing at it.
269
- * Internal bookkeeping - do not set this from a decorator.
270
- * @internal
271
292
  */
272
293
  readonly typeFromReference?: boolean;
294
+ /**
295
+ * Which key of the referenced entity this column points at, where that entity has more than one.
296
+ * Set by `fillOwningSide`; without it a composite target's columns would all take the first key's type.
297
+ */
298
+ readonly referencedKey?: string;
299
+ };
300
+ /**
301
+ * Configurable options for a field, carrying `V`, the value the column holds: what a generator returns
302
+ * and what a default is has to be that value, checked the same way the declared `type` is. `Scalar` by
303
+ * default, for the places that handle a field without knowing which one it is.
304
+ */
305
+ export type FieldOptions<V = TsTypeOf<FieldType>> = {
306
+ readonly name?: string;
307
+ readonly isId?: true;
308
+ readonly type?: FieldType;
273
309
  /**
274
310
  * Dimensions for vector fields. Used in schema generation.
275
311
  * @example `@Field({ type: 'vector', dimensions: 1536 })`
@@ -697,14 +733,21 @@ export type EntityMeta<E> = {
697
733
  name?: string;
698
734
  /** Set only when the entity named one; unset defers to the pool where it is used. See `AbstractDialect.resolveSchema`. */
699
735
  schema?: string;
700
- id: IdKey<E>;
736
+ /**
737
+ * Every key of the primary key, in declaration order. One unless the entity declares a composite.
738
+ *
739
+ * The only stored form: a single `id` beside it could only ever be right for a single-key entity,
740
+ * so every reader had to know whether it was safe. Asking whether *this* field is part of the key
741
+ * is `fields[key].isId`, which is O(1) and the source this list is derived from.
742
+ */
743
+ ids: readonly IdKey<E>[];
701
744
  softDelete?: FieldKey<E>;
702
745
  /** Named, default-on `$where` filters applied to every query unless bypassed. */
703
746
  filters?: Record<string, FilterOptions<E>>;
704
747
  fields: {
705
- [K in FieldKey<E>]?: FieldOptions;
748
+ [K in FieldKey<E>]?: FieldMeta;
706
749
  } & {
707
- [key: string]: FieldOptions | undefined;
750
+ [key: string]: FieldMeta | undefined;
708
751
  };
709
752
  relations: {
710
753
  [K in RelationKey<E>]?: RelationMeta;
@@ -123,7 +123,14 @@ export interface ColumnSchema {
123
123
  export interface TableSchema {
124
124
  readonly name: string;
125
125
  readonly columns: ColumnSchema[];
126
+ /** The key's columns **in order**, which is what a composite is: `(a, b)` is not `(b, a)`. */
126
127
  readonly primaryKey?: string[];
128
+ /**
129
+ * What the engine calls the key's constraint, where it names one at all - Postgres's `Member_pkey`,
130
+ * MySQL's literal `PRIMARY`, nothing on SQLite. Only a `DROP` needs it, and only the name the
131
+ * database actually reported will do: a derived one would name a constraint that is not there.
132
+ */
133
+ readonly primaryKeyName?: string;
127
134
  readonly indexes?: IndexSchema[];
128
135
  readonly foreignKeys?: ForeignKeySchema[];
129
136
  }
@@ -176,6 +183,18 @@ export interface SchemaDiff {
176
183
  */
177
184
  readonly schema?: string;
178
185
  readonly type: 'create' | 'alter' | 'drop';
186
+ /**
187
+ * The key the table has against the key the entity declares, set only when they differ.
188
+ *
189
+ * Compared by columns, never by name: the engine named the existing one, so requiring a derived
190
+ * name to match would rewrite the primary key of every table on the first migration after
191
+ * upgrading. `fromName` is what the database reported, and the only name a `DROP` can use.
192
+ */
193
+ readonly primaryKey?: {
194
+ readonly from: string[];
195
+ readonly to: string[];
196
+ readonly fromName?: string;
197
+ };
179
198
  readonly columnsToAdd?: ColumnSchema[];
180
199
  readonly columnsToAlter?: {
181
200
  from: ColumnSchema;
@@ -1,4 +1,4 @@
1
- import type { FieldKey, IdValue, JsonFieldPaths, JsonFieldPathValue, RelationKey, RelationTarget } from './entity.js';
1
+ import type { EntityId, FieldKey, JsonFieldPaths, JsonFieldPathValue, RelationKey, RelationTarget } from './entity.js';
2
2
  import type { QueryRaw } from './queryRaw.js';
3
3
  import type { ExpandScalar, IsMany, QueryComparableScalar, Scalar } from './utility.js';
4
4
  import type { QueryVectorQuery } from './vector.js';
@@ -316,5 +316,10 @@ export type QueryWhereArray<E> = (QueryWhereMap<E> | QueryRaw)[];
316
316
  /**
317
317
  * query filter.
318
318
  */
319
- export type QueryWhere<E> = IdValue<E> | IdValue<E>[] | QueryWhereMap<E> | QueryWhereArray<E> | QueryRaw;
319
+ /**
320
+ * `EntityId` rather than `IdValue`: a by-id method reduces to `$where: id`, and a composite key is
321
+ * addressed by an object carrying every key. That object is a where map naming those columns, so the
322
+ * two spellings meet here rather than needing a conversion.
323
+ */
324
+ export type QueryWhere<E> = EntityId<E> | EntityId<E>[] | QueryWhereMap<E> | QueryWhereArray<E> | QueryRaw;
320
325
  export {};
@@ -1,4 +1,4 @@
1
- import type { EntityData, FieldKey, IdValue, RelationKey, UpdatePayload } from './entity.js';
1
+ import type { EntityData, EntityId, FieldKey, IdValue, RelationKey, UpdatePayload } from './entity.js';
2
2
  import type { QueryConflictPaths, QueryFilter, QueryFindResult, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch, QueryStreamProjected, QueryUpdateResult } from './query.js';
3
3
  import type { QueryAggMap, QueryAggregate, QueryAggregateResult, QueryGroupMap } from './queryAggregate.js';
4
4
  import type { Type } from './utility.js';
@@ -22,7 +22,7 @@ export interface SharedQuerier<W extends QuerierTransport, O, DO = O> {
22
22
  * @param q the additional criteria options
23
23
  * @return the record
24
24
  */
25
- findOneById<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(entity: Type<E>, id: IdValue<E>, q?: QueryOneProjected<E, S, V, X, P, C>, opts?: O): QuerierResult<W, QueryFindResult<E, S, V, X, P, C> | undefined>;
25
+ findOneById<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(entity: Type<E>, id: EntityId<E>, q?: QueryOneProjected<E, S, V, X, P, C>, opts?: O): QuerierResult<W, QueryFindResult<E, S, V, X, P, C> | undefined>;
26
26
  /**
27
27
  * obtains the first record matching the given search parameters.
28
28
  * @param entity the target entity
@@ -68,7 +68,7 @@ export interface SharedQuerier<W extends QuerierTransport, O, DO = O> {
68
68
  * @param payload the data to be persisted
69
69
  * @return the number of affected records
70
70
  */
71
- updateOneById<E extends object>(entity: Type<E>, id: IdValue<E>, payload: UpdatePayload<E>, opts?: O): QuerierResult<W, number>;
71
+ updateOneById<E extends object>(entity: Type<E>, id: EntityId<E>, payload: UpdatePayload<E>, opts?: O): QuerierResult<W, number>;
72
72
  /**
73
73
  * updates many records partially.
74
74
  * @param entity the entity to persist on
@@ -83,7 +83,7 @@ export interface SharedQuerier<W extends QuerierTransport, O, DO = O> {
83
83
  * @param id the primary key of the record
84
84
  * @return the number of affected records
85
85
  */
86
- deleteOneById<E extends object>(entity: Type<E>, id: IdValue<E>, opts?: DO): QuerierResult<W, number>;
86
+ deleteOneById<E extends object>(entity: Type<E>, id: EntityId<E>, opts?: DO): QuerierResult<W, number>;
87
87
  /**
88
88
  * delete or SoftDelete records.
89
89
  * @param entity the entity to persist on
@@ -130,7 +130,7 @@ export interface UniversalQuerier extends SharedQuerier<'server', QueryOptions>
130
130
  * @param payload the data to be persisted
131
131
  * @return the IDs
132
132
  */
133
- insertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
133
+ insertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<(IdValue<E> | undefined)[]>;
134
134
  /**
135
135
  * Insert or update a record based on the conflict paths.
136
136
  * @param entity the entity to persist on
@@ -153,19 +153,19 @@ export interface UniversalQuerier extends SharedQuerier<'server', QueryOptions>
153
153
  * @param payload the data to be persisted
154
154
  * @return the ID
155
155
  */
156
- saveOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<IdValue<E>>;
156
+ saveOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<IdValue<E> | undefined>;
157
157
  /**
158
158
  * Insert or update records.
159
159
  * @param entity the entity to persist on
160
160
  * @param payload the data to be persisted
161
161
  * @return the IDs
162
162
  */
163
- saveMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
163
+ saveMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<(IdValue<E> | undefined)[]>;
164
164
  /**
165
165
  * Restore soft-deleted records (sets the soft-delete field back to `null`). Throws if the
166
166
  * entity has no soft-delete field.
167
167
  */
168
- restoreOneById<E extends object>(entity: Type<E>, id: IdValue<E>): Promise<number>;
168
+ restoreOneById<E extends object>(entity: Type<E>, id: EntityId<E>): Promise<number>;
169
169
  restoreMany<E extends object>(entity: Type<E>, q: QuerySearch<E>): Promise<number>;
170
170
  /**
171
171
  * runs an aggregate query (GROUP BY with aggregate functions).
@@ -1,7 +1,8 @@
1
1
  import { getContext, UqlSecurityError } from '../context/context.js';
2
+ import { soleIdOf } from '../entity/metadata/definition.js';
2
3
  import { QueryRaw, resolveAggregateOp, SOFT_DELETE_FILTER, } from '../type/index.js';
3
4
  import { VECTOR_INDEX_TYPES } from '../type/vector.js';
4
- import { entityName, getFieldKeys, getKeys, hasKeys, someKey } from './object.util.js';
5
+ import { entityName, getFieldKeys, getKeys, hasKeys, isScalarId, someKey } from './object.util.js';
5
6
  export function filterFieldKeys(meta, payload, callbackKey) {
6
7
  return getKeys(payload).filter((key) => {
7
8
  const fieldOpts = meta.fields[key];
@@ -112,7 +113,7 @@ export function isPagedQuery(q) {
112
113
  * compiler - so it is spelled once here rather than in each querier.
113
114
  */
114
115
  export function idOnlyQuery(meta, q) {
115
- return { ...q, $select: { [meta.id]: true } };
116
+ return { ...q, $select: Object.fromEntries(meta.ids.map((key) => [key, true])) };
116
117
  }
117
118
  /**
118
119
  * The map form of a `$select` value, or `undefined` for the raw-array form. Centralizes the one
@@ -235,10 +236,18 @@ export function buildQueryWhereAsMap(meta, filter = {}) {
235
236
  if (filter instanceof QueryRaw) {
236
237
  return { $and: [filter] };
237
238
  }
238
- if (isIdValue(filter)) {
239
- return {
240
- [meta.id]: filter,
241
- };
239
+ if (Array.isArray(filter)) {
240
+ // A list of bare ids is an `IN` over the one key column; a list of anything else is a list of
241
+ // `$where`s, which is an OR - and that is how a composite's id objects name a settled set of rows.
242
+ return filter.every(isScalarId)
243
+ ? { [soleIdOf(meta, 'addressing by a bare id value')]: filter }
244
+ : { $or: filter };
245
+ }
246
+ if (isScalarId(filter)) {
247
+ // A scalar can only name one column, so on a composite it would address every row agreeing on
248
+ // that one. A composite is addressed by a map, which falls through below as the `$where` it
249
+ // already is - an id object and a where map are the same shape by design.
250
+ return { [soleIdOf(meta, 'addressing by a bare id value')]: filter };
242
251
  }
243
252
  return filter;
244
253
  }
@@ -311,14 +320,6 @@ export function applyFilters(meta, whereMap, opts) {
311
320
  }
312
321
  return result;
313
322
  }
314
- function isIdValue(filter) {
315
- const type = typeof filter;
316
- return (type === 'string' ||
317
- type === 'number' ||
318
- type === 'bigint' ||
319
- typeof filter.toHexString === 'function' ||
320
- Array.isArray(filter));
321
- }
322
323
  /**
323
324
  * The `$size` of a relation condition (`{ comments: { $size: { $gte: 2 } } }`), or `undefined` when the
324
325
  * condition constrains the target's fields instead. Shared so every dialect agrees on which of the two
@@ -1,4 +1,4 @@
1
- import type { FieldOptions } from '../type/index.js';
1
+ import type { EntityMeta, FieldOptions } from '../type/index.js';
2
2
  /**
3
3
  * Checks if a field type is numeric (Number, BigInt, or explicit numeric logical types)
4
4
  */
@@ -11,6 +11,16 @@ export declare function isBooleanType(type: unknown): boolean;
11
11
  * Checks if a field type is JSON
12
12
  */
13
13
  export declare function isJsonType(type: unknown): boolean;
14
+ /**
15
+ * Whether the field is the entity's *whole* primary key - the only kind a serial can stand in for,
16
+ * and the only one that may state `PRIMARY KEY` in its own column definition.
17
+ *
18
+ * One column of a composite is a value the caller supplies, and the table states the key over every
19
+ * column at once. Asked in one place because the two schema paths - the AST that builds a
20
+ * `CREATE TABLE` and the diff that builds an `ALTER` - have to answer it the same way, and each
21
+ * answering for itself is what put a serial `PRIMARY KEY` on both columns of a composite.
22
+ */
23
+ export declare function isSoleIdField<E>(meta: EntityMeta<E>, field: FieldOptions): boolean;
14
24
  /**
15
25
  * Checks if a field should be treated as auto-incrementing.
16
26
  */
@@ -52,6 +52,18 @@ export function isJsonType(type) {
52
52
  }
53
53
  return false;
54
54
  }
55
+ /**
56
+ * Whether the field is the entity's *whole* primary key - the only kind a serial can stand in for,
57
+ * and the only one that may state `PRIMARY KEY` in its own column definition.
58
+ *
59
+ * One column of a composite is a value the caller supplies, and the table states the key over every
60
+ * column at once. Asked in one place because the two schema paths - the AST that builds a
61
+ * `CREATE TABLE` and the diff that builds an `ALTER` - have to answer it the same way, and each
62
+ * answering for itself is what put a serial `PRIMARY KEY` on both columns of a composite.
63
+ */
64
+ export function isSoleIdField(meta, field) {
65
+ return field.isId === true && meta.ids.length === 1;
66
+ }
55
67
  /**
56
68
  * Checks if a field should be treated as auto-incrementing.
57
69
  */
@@ -6,6 +6,7 @@ export * from './ddlExpression.util.js';
6
6
  export * from './logger.js';
7
7
  export * from './object.util.js';
8
8
  export * from './raw.js';
9
+ export * from './rowKey.util.js';
9
10
  export * from './relationQuery.util.js';
10
11
  export * from './sql.util.js';
11
12
  export * from './string.util.js';
@@ -6,6 +6,7 @@ export * from './ddlExpression.util.js';
6
6
  export * from './logger.js';
7
7
  export * from './object.util.js';
8
8
  export * from './raw.js';
9
+ export * from './rowKey.util.js';
9
10
  export * from './relationQuery.util.js';
10
11
  export * from './sql.util.js';
11
12
  export * from './string.util.js';
@@ -29,3 +29,9 @@ export declare function entityName<E>(meta: EntityMeta<E>): string;
29
29
  export declare function getFieldKeys<E>(fields: {
30
30
  [K in FieldKey<E>]?: FieldOptions;
31
31
  }): FieldKey<E>[];
32
+ /**
33
+ * Whether `value` addresses a row by itself rather than naming columns: every primitive, and the
34
+ * object ids a driver deals in (`ObjectId`, `Date`, bytes). Only a plain object names columns, which
35
+ * is what a `$where` map and a composite key's id object both are; an array is a list of either.
36
+ */
37
+ export declare function isScalarId(value: unknown): boolean;
@@ -63,3 +63,21 @@ export function entityName(meta) {
63
63
  export function getFieldKeys(fields) {
64
64
  return getKeys(fields).filter((field) => fields[field].eager ?? true);
65
65
  }
66
+ /**
67
+ * Whether `value` addresses a row by itself rather than naming columns: every primitive, and the
68
+ * object ids a driver deals in (`ObjectId`, `Date`, bytes). Only a plain object names columns, which
69
+ * is what a `$where` map and a composite key's id object both are; an array is a list of either.
70
+ */
71
+ export function isScalarId(value) {
72
+ if (typeof value !== 'object' || value === null) {
73
+ return true;
74
+ }
75
+ if (Array.isArray(value)) {
76
+ return false;
77
+ }
78
+ // `null` as well as `Object.prototype`: an object with no prototype is what a query-string parser
79
+ // hands back (`qs`, express's `req.params`), and reading one as a bare id would name one column
80
+ // with a map of several.
81
+ const proto = Object.getPrototypeOf(value);
82
+ return proto !== Object.prototype && proto !== null;
83
+ }
@@ -9,15 +9,56 @@ export type RelationRequestSummary<E> = {
9
9
  * the one field it reads, so it answers for a relation being declared as well as for a resolved one.
10
10
  */
11
11
  export declare function isToManyRelation(relation: Pick<RelationMeta, 'cardinality'>): boolean;
12
+ /** One column of a parent's key, paired with the column matching it on the table being joined. */
13
+ export type ParentJoin = {
14
+ readonly parent: string;
15
+ readonly joined: string;
16
+ };
17
+ /**
18
+ * How a relation joins to its parent: `parent` is a column of the parent's own table, `joined` the
19
+ * column matching it on the table the relation reads - a junction's own column for a relation that
20
+ * goes through one, the child's foreign key otherwise.
21
+ *
22
+ * The two are spelled from opposite ends of `references` (`local` names a column of the table the
23
+ * relation is declared on, `foreign` a column of the other one), and getting that backwards reads a
24
+ * real column of the wrong table, so it is answered once here. One pair per key of the parent.
25
+ */
26
+ export declare function parentJoins(relOpts: Pick<RelationMeta, 'references' | 'through'>, parentKeyCount: number): ParentJoin[];
27
+ /**
28
+ * The junction columns holding the target's key, the other half of {@link parentJoins}.
29
+ *
30
+ * `parentKeyCount` is required: the target's columns start after the parent's, so guessing the
31
+ * boundary returned the parent's *second* column as the target's - a real column of the wrong side,
32
+ * which is the mistake this module exists to prevent.
33
+ */
34
+ export declare function targetKeyColumns(relOpts: Pick<RelationMeta, 'references'>, parentKeyCount: number): string[];
35
+ /** `{ joined column: true }`: the projection or grouping that keeps a parent's key on the rows read. */
36
+ export declare function joinedColumns(joins: readonly ParentJoin[]): Record<string, true>;
37
+ /**
38
+ * A parent row keyed by the columns a relation joins *from*, and a child or tally row keyed by the
39
+ * columns it carries that key in. The two halves of matching children to parents: they must agree on
40
+ * every column, so each is read through `joins` rather than through the parent's own key list - which
41
+ * is the same set only for a to-many, and silently a different one otherwise.
42
+ */
43
+ export declare function parentRowKey(joins: readonly ParentJoin[], parent: unknown): string;
44
+ export declare function joinedRowKey(joins: readonly ParentJoin[], row: unknown): string;
45
+ /**
46
+ * `{ joined column: every parent's value for it }`, the filter that fetches a whole page of parents'
47
+ * children in one statement.
48
+ *
49
+ * A composite over-selects, because the lists are independent and a pairing no parent has can still
50
+ * match. Regrouping the rows keys on every column, so those rows find no parent and are dropped -
51
+ * cheaper than the row-value comparison no engine spells the same way.
52
+ */
53
+ export declare function parentsIn(joins: readonly ParentJoin[], parents: readonly unknown[]): Record<string, unknown[]>;
12
54
  /**
13
- * The column holding the parent's id: a junction's own for a relation that goes through one, the
14
- * child's foreign key otherwise. The two are spelled from opposite ends - `local` names a column of
15
- * the table the relation is declared to write, `foreign` a column of the other one - and getting
16
- * that backwards reads a real column of the wrong table, so it is answered once here.
55
+ * The `$where` naming exactly the children of the rows `parentIds` identifies: an `IN` over the one
56
+ * column a single key contributes, an OR of key maps for several.
57
+ *
58
+ * Exact, unlike {@link parentsIn}: a read absorbs over-selection by regrouping its rows, and a write
59
+ * has nothing to regroup - a pairing no parent has would delete a child of a parent that survives.
17
60
  */
18
- export declare function parentKeyColumn(relOpts: Pick<RelationMeta, 'references' | 'through'>): string;
19
- /** The column on a junction table holding the target's id, the other half of {@link parentKeyColumn}. */
20
- export declare function targetKeyColumn(relOpts: Pick<RelationMeta, 'references'>): string;
61
+ export declare function childrenOf(joins: readonly ParentJoin[], parentIds: readonly unknown[]): Record<string, unknown>;
21
62
  /**
22
63
  * What a joined relation cannot carry, and why. A to-many is loaded by a query of its own, which is
23
64
  * what gives these four a meaning there; a to-one is one row of the parent's, so every backend used
@@ -1,5 +1,6 @@
1
1
  import { QUERY_BOOLEAN_CLAUSES, QUERY_NUMBER_CLAUSES, QUERY_OBJECT_CLAUSES, QUERY_ROOT_NUMBER_CLAUSES, } from '../type/query.js';
2
2
  import { getKeys, someKey } from './object.util.js';
3
+ import { rowKey } from './rowKey.util.js';
3
4
  /**
4
5
  * Whether a relation holds many rows per parent, so it cannot be joined into the parent's row. Takes
5
6
  * the one field it reads, so it answers for a relation being declared as well as for a resolved one.
@@ -8,17 +9,77 @@ export function isToManyRelation(relation) {
8
9
  return relation.cardinality === '1m' || relation.cardinality === 'mm';
9
10
  }
10
11
  /**
11
- * The column holding the parent's id: a junction's own for a relation that goes through one, the
12
- * child's foreign key otherwise. The two are spelled from opposite ends - `local` names a column of
13
- * the table the relation is declared to write, `foreign` a column of the other one - and getting
14
- * that backwards reads a real column of the wrong table, so it is answered once here.
12
+ * How a relation joins to its parent: `parent` is a column of the parent's own table, `joined` the
13
+ * column matching it on the table the relation reads - a junction's own column for a relation that
14
+ * goes through one, the child's foreign key otherwise.
15
+ *
16
+ * The two are spelled from opposite ends of `references` (`local` names a column of the table the
17
+ * relation is declared on, `foreign` a column of the other one), and getting that backwards reads a
18
+ * real column of the wrong table, so it is answered once here. One pair per key of the parent.
15
19
  */
16
- export function parentKeyColumn(relOpts) {
17
- return relOpts.through ? relOpts.references[0].local : relOpts.references[0].foreign;
20
+ export function parentJoins(relOpts, parentKeyCount) {
21
+ if (!relOpts.through) {
22
+ return relOpts.references.map(({ local, foreign }) => ({ parent: local, joined: foreign }));
23
+ }
24
+ // A junction's pairs are the parent's followed by the target's, and how many the parent has is
25
+ // something its caller already knows - so the boundary is passed rather than stored on a relation.
26
+ return relOpts.references.slice(0, parentKeyCount).map(({ local, foreign }) => ({ parent: foreign, joined: local }));
27
+ }
28
+ /**
29
+ * The junction columns holding the target's key, the other half of {@link parentJoins}.
30
+ *
31
+ * `parentKeyCount` is required: the target's columns start after the parent's, so guessing the
32
+ * boundary returned the parent's *second* column as the target's - a real column of the wrong side,
33
+ * which is the mistake this module exists to prevent.
34
+ */
35
+ export function targetKeyColumns(relOpts, parentKeyCount) {
36
+ return relOpts.references.slice(parentKeyCount).map(({ local }) => local);
37
+ }
38
+ /** `{ joined column: true }`: the projection or grouping that keeps a parent's key on the rows read. */
39
+ export function joinedColumns(joins) {
40
+ return Object.fromEntries(joins.map(({ joined }) => [joined, true]));
41
+ }
42
+ /**
43
+ * A parent row keyed by the columns a relation joins *from*, and a child or tally row keyed by the
44
+ * columns it carries that key in. The two halves of matching children to parents: they must agree on
45
+ * every column, so each is read through `joins` rather than through the parent's own key list - which
46
+ * is the same set only for a to-many, and silently a different one otherwise.
47
+ */
48
+ export function parentRowKey(joins, parent) {
49
+ return rowKey(joins.map(({ parent: key }) => read(parent, key)));
50
+ }
51
+ export function joinedRowKey(joins, row) {
52
+ return rowKey(joins.map(({ joined }) => read(row, joined)));
53
+ }
54
+ /**
55
+ * `{ joined column: every parent's value for it }`, the filter that fetches a whole page of parents'
56
+ * children in one statement.
57
+ *
58
+ * A composite over-selects, because the lists are independent and a pairing no parent has can still
59
+ * match. Regrouping the rows keys on every column, so those rows find no parent and are dropped -
60
+ * cheaper than the row-value comparison no engine spells the same way.
61
+ */
62
+ export function parentsIn(joins, parents) {
63
+ return Object.fromEntries(joins.map(({ parent, joined }) => [joined, parents.map((it) => read(it, parent))]));
64
+ }
65
+ /**
66
+ * The `$where` naming exactly the children of the rows `parentIds` identifies: an `IN` over the one
67
+ * column a single key contributes, an OR of key maps for several.
68
+ *
69
+ * Exact, unlike {@link parentsIn}: a read absorbs over-selection by regrouping its rows, and a write
70
+ * has nothing to regroup - a pairing no parent has would delete a child of a parent that survives.
71
+ */
72
+ export function childrenOf(joins, parentIds) {
73
+ const [first] = joins;
74
+ if (joins.length === 1) {
75
+ return { [first.joined]: parentIds };
76
+ }
77
+ return {
78
+ $or: parentIds.map((id) => Object.fromEntries(joins.map(({ parent, joined }) => [joined, read(id, parent)]))),
79
+ };
18
80
  }
19
- /** The column on a junction table holding the target's id, the other half of {@link parentKeyColumn}. */
20
- export function targetKeyColumn(relOpts) {
21
- return relOpts.references[1].local;
81
+ function read(row, key) {
82
+ return row[key];
22
83
  }
23
84
  /**
24
85
  * What a joined relation cannot carry, and why. A to-many is loaded by a query of its own, which is
@@ -0,0 +1,9 @@
1
+ /**
2
+ * A row's key as a string, for matching rows to each other in a `Map`.
3
+ *
4
+ * Values are normalized before joining, not stringified: `String(date)` is locale- and
5
+ * timezone-dependent, so two equal dates could key apart, and a `Uint8Array` stringifies to its
6
+ * bytes with commas. Every part is included, so two rows agreeing on one column of a composite key
7
+ * are not treated as one row.
8
+ */
9
+ export declare function rowKey(values: readonly unknown[]): string;
@@ -0,0 +1,24 @@
1
+ /** Separates the parts of a composite key: a unit separator, which no column value carries. */
2
+ const KEY_SEPARATOR = '\u001f';
3
+ /**
4
+ * A row's key as a string, for matching rows to each other in a `Map`.
5
+ *
6
+ * Values are normalized before joining, not stringified: `String(date)` is locale- and
7
+ * timezone-dependent, so two equal dates could key apart, and a `Uint8Array` stringifies to its
8
+ * bytes with commas. Every part is included, so two rows agreeing on one column of a composite key
9
+ * are not treated as one row.
10
+ */
11
+ export function rowKey(values) {
12
+ return values.map(keyPart).join(KEY_SEPARATOR);
13
+ }
14
+ function keyPart(value) {
15
+ if (value instanceof Date) {
16
+ return value.toISOString();
17
+ }
18
+ // Hex by hand rather than through `Buffer`, which is undefined on the browser and edge runtimes
19
+ // this module reaches through `AbstractQuerier`.
20
+ if (value instanceof Uint8Array) {
21
+ return Array.from(value, (byte) => byte.toString(16).padStart(2, '0')).join('');
22
+ }
23
+ return String(value);
24
+ }