uql-orm 0.41.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/querier/httpQuerier.d.ts +4 -4
- package/dist/browser/querier/httpQuerier.js +21 -6
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +4 -4
- package/dist/dialect/abstractSqlDialect.d.ts +1 -1
- package/dist/dialect/abstractSqlDialect.js +26 -21
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +1 -1
- package/dist/dialect/mysqlLikeSqlDialect.js +2 -2
- package/dist/entity/decorator/members.d.ts +29 -3
- package/dist/entity/index.d.ts +1 -1
- package/dist/entity/index.js +1 -1
- package/dist/entity/metadata/definition.d.ts +18 -1
- package/dist/entity/metadata/definition.js +97 -38
- package/dist/http/handler.js +4 -4
- package/dist/maria/mariaDialect.d.ts +2 -2
- package/dist/maria/mariaDialect.js +2 -2
- package/dist/migrate/builder/migrationBuilder.js +2 -2
- package/dist/migrate/builder/tableBuilder.js +2 -2
- package/dist/migrate/schemaGenerator.d.ts +2 -2
- package/dist/migrate/schemaGenerator.js +12 -4
- package/dist/mongo/mongoDialect.js +17 -6
- package/dist/mongo/mongodbQuerier.js +6 -3
- package/dist/querier/abstractQuerier.d.ts +16 -12
- package/dist/querier/abstractQuerier.js +77 -40
- package/dist/querier/abstractQuerierPool.d.ts +8 -8
- package/dist/querier/abstractSqlQuerier.d.ts +1 -1
- package/dist/querier/abstractSqlQuerier.js +12 -7
- package/dist/querier/relationCount.js +40 -31
- package/dist/schema/schemaAST.js +1 -0
- package/dist/schema/schemaASTBuilder.js +37 -22
- package/dist/schema/types.d.ts +24 -0
- package/dist/type/entity.d.ts +85 -15
- package/dist/type/queryWhere.d.ts +7 -2
- package/dist/type/universalQuerier.d.ts +8 -8
- package/dist/util/ddlExpression.util.d.ts +15 -0
- package/dist/util/ddlExpression.util.js +27 -0
- package/dist/util/dialect.util.js +15 -14
- package/dist/util/index.d.ts +2 -1
- package/dist/util/index.js +2 -1
- package/dist/util/object.util.d.ts +6 -0
- package/dist/util/object.util.js +10 -0
- package/dist/util/relationQuery.util.d.ts +38 -7
- package/dist/util/relationQuery.util.js +50 -9
- package/dist/util/rowKey.util.d.ts +9 -0
- package/dist/util/rowKey.util.js +24 -0
- package/dist/util/sql.util.d.ts +2 -0
- package/dist/util/sql.util.js +4 -0
- package/package.json +2 -2
- package/dist/util/indexColumn.util.d.ts +0 -8
- package/dist/util/indexColumn.util.js +0 -30
package/dist/schema/types.d.ts
CHANGED
|
@@ -40,6 +40,26 @@ export interface CanonicalType {
|
|
|
40
40
|
* Actions for foreign key ON DELETE and ON UPDATE clauses.
|
|
41
41
|
*/
|
|
42
42
|
export type ForeignKeyAction = 'CASCADE' | 'SET NULL' | 'SET DEFAULT' | 'RESTRICT' | 'NO ACTION';
|
|
43
|
+
/**
|
|
44
|
+
* The values a column accepts, rendered as `CHECK (col IN (...))`.
|
|
45
|
+
*
|
|
46
|
+
* Strings and numbers only: those are what `IN (...)` can state, and each is escaped by the
|
|
47
|
+
* dialect's own literal rules, so a number stays bare where a string is quoted.
|
|
48
|
+
*/
|
|
49
|
+
export type EnumValues = readonly (string | number)[];
|
|
50
|
+
/**
|
|
51
|
+
* A `CHECK` constraint as the schema holds it, its expression already text. Declared here rather
|
|
52
|
+
* than beside the entity types because a table node also comes from introspection, where there is
|
|
53
|
+
* no entity to have authored one.
|
|
54
|
+
*
|
|
55
|
+
* Only ever compared by presence, never by content: a check is SQL text, and a database reprints
|
|
56
|
+
* text from its parse tree, so `CHECK ("balance" >= 0)` reads back as `CHECK ((balance >= (0)::numeric))`.
|
|
57
|
+
*/
|
|
58
|
+
export interface CheckSchema {
|
|
59
|
+
/** Absent when nothing named it, which the generator fills in with `derivedCheckName`. */
|
|
60
|
+
readonly name?: string;
|
|
61
|
+
readonly expression: string;
|
|
62
|
+
}
|
|
43
63
|
/**
|
|
44
64
|
* Default action for foreign key ON DELETE and ON UPDATE clauses.
|
|
45
65
|
*/
|
|
@@ -84,6 +104,8 @@ export interface ColumnNode {
|
|
|
84
104
|
readonly isAutoIncrement: boolean;
|
|
85
105
|
/** Whether this column has a unique constraint */
|
|
86
106
|
readonly isUnique: boolean;
|
|
107
|
+
/** The values the column accepts. See {@link EnumValues}. */
|
|
108
|
+
readonly enum?: EnumValues;
|
|
87
109
|
/** Column comment/description */
|
|
88
110
|
readonly comment?: string;
|
|
89
111
|
/** Reference to the parent table */
|
|
@@ -115,6 +137,8 @@ export interface TableNode {
|
|
|
115
137
|
readonly primaryKey: ColumnNode[];
|
|
116
138
|
/** Indexes on this table */
|
|
117
139
|
readonly indexes: IndexNode[];
|
|
140
|
+
/** `CHECK` constraints on this table. Optional: a node can be built without ever naming one. */
|
|
141
|
+
readonly checks?: CheckSchema[];
|
|
118
142
|
/** Optional table comment */
|
|
119
143
|
readonly comment?: string;
|
|
120
144
|
/** Relationships pointing TO this table (other tables referencing this one) */
|
package/dist/type/entity.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ForeignKeyAction, IndexType } from '../schema/types.js';
|
|
1
|
+
import type { CheckSchema, EnumValues, ForeignKeyAction, IndexType } from '../schema/types.js';
|
|
2
2
|
import type { FilterOptions } from './query.js';
|
|
3
3
|
import type { QueryRaw } from './queryRaw.js';
|
|
4
4
|
import type { Except, IsMany, Json, Scalar, Type, Unpacked } from './utility.js';
|
|
@@ -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
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
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
|
|
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 })`
|
|
@@ -292,6 +328,20 @@ export type FieldOptions<V = TsTypeOf<FieldType>> = {
|
|
|
292
328
|
* @example `@Field({ references: () => Company, onDelete: 'CASCADE' }) companyId?: string;`
|
|
293
329
|
*/
|
|
294
330
|
readonly onDelete?: ForeignKeyAction;
|
|
331
|
+
/**
|
|
332
|
+
* The values the column accepts, enforced by the database as well as by TypeScript.
|
|
333
|
+
*
|
|
334
|
+
* Emitted as a column `CHECK (col IN (...))` on every SQL dialect rather than a native enum type:
|
|
335
|
+
* one code path, no separate schema object to order, and adding a value stays an ordinary column
|
|
336
|
+
* change instead of Postgres's irreversible `ALTER TYPE ... ADD VALUE`.
|
|
337
|
+
*
|
|
338
|
+
* Not constrained against the field's own type here: the decorator narrows the property to these
|
|
339
|
+
* values instead, which reports a mismatch where the mistake is rather than as an unrelated
|
|
340
|
+
* `never`. `as const` is what makes them literal, and so what makes any of it check.
|
|
341
|
+
*
|
|
342
|
+
* @example `@Field({ type: String, enum: ['draft', 'paid'] as const })`
|
|
343
|
+
*/
|
|
344
|
+
readonly enum?: EnumValues;
|
|
295
345
|
readonly virtual?: QueryRaw;
|
|
296
346
|
readonly updatable?: boolean;
|
|
297
347
|
readonly eager?: boolean;
|
|
@@ -683,14 +733,21 @@ export type EntityMeta<E> = {
|
|
|
683
733
|
name?: string;
|
|
684
734
|
/** Set only when the entity named one; unset defers to the pool where it is used. See `AbstractDialect.resolveSchema`. */
|
|
685
735
|
schema?: string;
|
|
686
|
-
|
|
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>[];
|
|
687
744
|
softDelete?: FieldKey<E>;
|
|
688
745
|
/** Named, default-on `$where` filters applied to every query unless bypassed. */
|
|
689
746
|
filters?: Record<string, FilterOptions<E>>;
|
|
690
747
|
fields: {
|
|
691
|
-
[K in FieldKey<E>]?:
|
|
748
|
+
[K in FieldKey<E>]?: FieldMeta;
|
|
692
749
|
} & {
|
|
693
|
-
[key: string]:
|
|
750
|
+
[key: string]: FieldMeta | undefined;
|
|
694
751
|
};
|
|
695
752
|
relations: {
|
|
696
753
|
[K in RelationKey<E>]?: RelationMeta;
|
|
@@ -699,6 +756,8 @@ export type EntityMeta<E> = {
|
|
|
699
756
|
};
|
|
700
757
|
/** Composite indexes defined via @Index decorator */
|
|
701
758
|
indexes?: EntityIndexMeta[];
|
|
759
|
+
/** `CHECK` constraints, their expressions already reduced to text. */
|
|
760
|
+
checks?: CheckSchema[];
|
|
702
761
|
/** Lifecycle hooks registered via @BeforeInsert, @AfterUpdate, etc. */
|
|
703
762
|
hooks?: Partial<Record<HookEvent, HookRegistration[]>>;
|
|
704
763
|
processed?: boolean;
|
|
@@ -709,6 +768,15 @@ export type EntityMeta<E> = {
|
|
|
709
768
|
* Optional `fields`, `relations`, `indexes`, and `hooks` register metadata in one call for
|
|
710
769
|
* decorator-free setups. Omit them when using `@Field` / `@ManyToOne` / etc.
|
|
711
770
|
*/
|
|
771
|
+
/**
|
|
772
|
+
* A table-level `CHECK`. The expression is `raw` with no interpolation, like an index expression:
|
|
773
|
+
* this is DDL, so there is no placeholder a bound value could go into.
|
|
774
|
+
*/
|
|
775
|
+
export type CheckOptions = {
|
|
776
|
+
/** Derived from the table and the constraint's position when absent. */
|
|
777
|
+
readonly name?: string;
|
|
778
|
+
readonly expression: QueryRaw;
|
|
779
|
+
};
|
|
712
780
|
export type EntityOptions<E = unknown> = {
|
|
713
781
|
readonly name?: string;
|
|
714
782
|
/**
|
|
@@ -726,6 +794,8 @@ export type EntityOptions<E = unknown> = {
|
|
|
726
794
|
readonly [K in RelationKey<E>]?: RelationOptionsFor<E[K]>;
|
|
727
795
|
};
|
|
728
796
|
readonly indexes?: readonly EntityIndexInput<FieldKey<E>, E>[];
|
|
797
|
+
/** Table-level `CHECK` constraints. See {@link CheckOptions}. */
|
|
798
|
+
readonly checks?: readonly CheckOptions[];
|
|
729
799
|
/** Map hook events to method names on the entity class. */
|
|
730
800
|
readonly hooks?: Partial<Record<HookEvent, readonly MethodKey<E>[]>>;
|
|
731
801
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type IndexColumnInput, type IndexColumnSchema, QueryRaw } from '../type/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* SQL bound for DDL, as the text a generator renders. `raw` with no interpolation, or a bare string
|
|
4
|
+
* where one is still accepted: DDL is evaluated once at creation time, so there is no query context
|
|
5
|
+
* for the callback form and no placeholder a `CREATE` statement could bind a value into.
|
|
6
|
+
*
|
|
7
|
+
* `what` names the thing being declared, so the error says which one the caller got wrong.
|
|
8
|
+
*/
|
|
9
|
+
export declare function ddlText(value: string | QueryRaw, what: string): string;
|
|
10
|
+
export declare function ddlText(value: string | QueryRaw | undefined, what: string): string | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Reduces an authored index entry to its normalized form, so the three shapes users write - a column
|
|
13
|
+
* name, an expression, or an options object - reach the dialects as one.
|
|
14
|
+
*/
|
|
15
|
+
export declare function normalizeIndexColumn(entry: IndexColumnInput): IndexColumnSchema;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { QueryRaw, RAW_VALUE } from '../type/index.js';
|
|
2
|
+
export function ddlText(value, what) {
|
|
3
|
+
if (!(value instanceof QueryRaw)) {
|
|
4
|
+
return value;
|
|
5
|
+
}
|
|
6
|
+
const sql = value[RAW_VALUE];
|
|
7
|
+
if (typeof sql !== 'string') {
|
|
8
|
+
throw new TypeError(`${what} needs raw() with no interpolation, not a function or a bound value`);
|
|
9
|
+
}
|
|
10
|
+
return sql;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Reduces an authored index entry to its normalized form, so the three shapes users write - a column
|
|
14
|
+
* name, an expression, or an options object - reach the dialects as one.
|
|
15
|
+
*/
|
|
16
|
+
export function normalizeIndexColumn(entry) {
|
|
17
|
+
if (typeof entry === 'string') {
|
|
18
|
+
return { column: entry };
|
|
19
|
+
}
|
|
20
|
+
if (entry instanceof QueryRaw) {
|
|
21
|
+
return { column: ddlText(entry, 'an index expression'), expression: true };
|
|
22
|
+
}
|
|
23
|
+
const { column, ...rest } = entry;
|
|
24
|
+
return column instanceof QueryRaw
|
|
25
|
+
? { ...rest, column: ddlText(column, 'an index expression'), expression: true }
|
|
26
|
+
: { ...rest, column };
|
|
27
|
+
}
|
|
@@ -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:
|
|
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 (
|
|
239
|
-
|
|
240
|
-
|
|
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
|
package/dist/util/index.d.ts
CHANGED
|
@@ -2,10 +2,11 @@ export * from './dialect.util.js';
|
|
|
2
2
|
export * from './field.util.js';
|
|
3
3
|
export * from './filters.util.js';
|
|
4
4
|
export * from './hook.util.js';
|
|
5
|
-
export * from './
|
|
5
|
+
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';
|
package/dist/util/index.js
CHANGED
|
@@ -2,10 +2,11 @@ export * from './dialect.util.js';
|
|
|
2
2
|
export * from './field.util.js';
|
|
3
3
|
export * from './filters.util.js';
|
|
4
4
|
export * from './hook.util.js';
|
|
5
|
-
export * from './
|
|
5
|
+
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;
|
package/dist/util/object.util.js
CHANGED
|
@@ -63,3 +63,13 @@ 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
|
+
return (typeof value !== 'object' ||
|
|
73
|
+
value === null ||
|
|
74
|
+
(!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype));
|
|
75
|
+
}
|
|
@@ -9,15 +9,46 @@ 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
|
+
/**
|
|
36
|
+
* `{ joined column: every parent's value for it }`, the filter that fetches a whole page of parents'
|
|
37
|
+
* children in one statement.
|
|
38
|
+
*
|
|
39
|
+
* A composite over-selects, because the lists are independent and a pairing no parent has can still
|
|
40
|
+
* match. Regrouping the rows keys on every column, so those rows find no parent and are dropped -
|
|
41
|
+
* cheaper than the row-value comparison no engine spells the same way.
|
|
42
|
+
*/
|
|
43
|
+
export declare function parentsIn(joins: readonly ParentJoin[], parents: readonly unknown[]): Record<string, unknown[]>;
|
|
12
44
|
/**
|
|
13
|
-
* The
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
45
|
+
* The `$where` naming exactly the children of the rows `parentIds` identifies: an `IN` over the one
|
|
46
|
+
* column a single key contributes, an OR of key maps for several.
|
|
47
|
+
*
|
|
48
|
+
* Exact, unlike {@link parentsIn}: a read absorbs over-selection by regrouping its rows, and a write
|
|
49
|
+
* has nothing to regroup - a pairing no parent has would delete a child of a parent that survives.
|
|
17
50
|
*/
|
|
18
|
-
export declare function
|
|
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;
|
|
51
|
+
export declare function childrenOf(joins: readonly ParentJoin[], parentIds: readonly unknown[]): Record<string, unknown>;
|
|
21
52
|
/**
|
|
22
53
|
* What a joined relation cannot carry, and why. A to-many is loaded by a query of its own, which is
|
|
23
54
|
* what gives these four a meaning there; a to-one is one row of the parent's, so every backend used
|
|
@@ -8,17 +8,58 @@ export function isToManyRelation(relation) {
|
|
|
8
8
|
return relation.cardinality === '1m' || relation.cardinality === 'mm';
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* How a relation joins to its parent: `parent` is a column of the parent's own table, `joined` the
|
|
12
|
+
* column matching it on the table the relation reads - a junction's own column for a relation that
|
|
13
|
+
* goes through one, the child's foreign key otherwise.
|
|
14
|
+
*
|
|
15
|
+
* The two are spelled from opposite ends of `references` (`local` names a column of the table the
|
|
16
|
+
* relation is declared on, `foreign` a column of the other one), and getting that backwards reads a
|
|
17
|
+
* real column of the wrong table, so it is answered once here. One pair per key of the parent.
|
|
15
18
|
*/
|
|
16
|
-
export function
|
|
17
|
-
|
|
19
|
+
export function parentJoins(relOpts, parentKeyCount) {
|
|
20
|
+
if (!relOpts.through) {
|
|
21
|
+
return relOpts.references.map(({ local, foreign }) => ({ parent: local, joined: foreign }));
|
|
22
|
+
}
|
|
23
|
+
// A junction's pairs are the parent's followed by the target's, and how many the parent has is
|
|
24
|
+
// something its caller already knows - so the boundary is passed rather than stored on a relation.
|
|
25
|
+
return relOpts.references.slice(0, parentKeyCount).map(({ local, foreign }) => ({ parent: foreign, joined: local }));
|
|
26
|
+
}
|
|
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 function targetKeyColumns(relOpts, parentKeyCount) {
|
|
35
|
+
return relOpts.references.slice(parentKeyCount).map(({ local }) => local);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* `{ joined column: every parent's value for it }`, the filter that fetches a whole page of parents'
|
|
39
|
+
* children in one statement.
|
|
40
|
+
*
|
|
41
|
+
* A composite over-selects, because the lists are independent and a pairing no parent has can still
|
|
42
|
+
* match. Regrouping the rows keys on every column, so those rows find no parent and are dropped -
|
|
43
|
+
* cheaper than the row-value comparison no engine spells the same way.
|
|
44
|
+
*/
|
|
45
|
+
export function parentsIn(joins, parents) {
|
|
46
|
+
return Object.fromEntries(joins.map(({ parent, joined }) => [joined, parents.map((it) => it[parent])]));
|
|
18
47
|
}
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
48
|
+
/**
|
|
49
|
+
* The `$where` naming exactly the children of the rows `parentIds` identifies: an `IN` over the one
|
|
50
|
+
* column a single key contributes, an OR of key maps for several.
|
|
51
|
+
*
|
|
52
|
+
* Exact, unlike {@link parentsIn}: a read absorbs over-selection by regrouping its rows, and a write
|
|
53
|
+
* has nothing to regroup - a pairing no parent has would delete a child of a parent that survives.
|
|
54
|
+
*/
|
|
55
|
+
export function childrenOf(joins, parentIds) {
|
|
56
|
+
const [first] = joins;
|
|
57
|
+
if (joins.length === 1) {
|
|
58
|
+
return { [first.joined]: parentIds };
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
$or: parentIds.map((id) => Object.fromEntries(joins.map(({ parent, joined }) => [joined, id[parent]]))),
|
|
62
|
+
};
|
|
22
63
|
}
|
|
23
64
|
/**
|
|
24
65
|
* 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
|
+
}
|
package/dist/util/sql.util.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export declare function qualifyName(name: string, schema?: string): string;
|
|
|
24
24
|
* `table` is the table's own name, never qualified - the result is a single identifier.
|
|
25
25
|
*/
|
|
26
26
|
export declare function derivedIndexName(table: string, columns: readonly string[]): string;
|
|
27
|
+
/** The constraint name a check gets when nothing named it: `ck_Order_1`, by declaration order. */
|
|
28
|
+
export declare function derivedCheckName(table: string, position: number): string;
|
|
27
29
|
/** The constraint name a foreign key gets when nothing named it: `fk_Order_customerId`. */
|
|
28
30
|
export declare function derivedForeignKeyName(table: string, columns: readonly string[]): string;
|
|
29
31
|
/**
|
package/dist/util/sql.util.js
CHANGED
|
@@ -67,6 +67,10 @@ export function qualifyName(name, schema) {
|
|
|
67
67
|
export function derivedIndexName(table, columns) {
|
|
68
68
|
return `idx_${table}_${columns.join('_')}`;
|
|
69
69
|
}
|
|
70
|
+
/** The constraint name a check gets when nothing named it: `ck_Order_1`, by declaration order. */
|
|
71
|
+
export function derivedCheckName(table, position) {
|
|
72
|
+
return `ck_${table}_${position}`;
|
|
73
|
+
}
|
|
70
74
|
/** The constraint name a foreign key gets when nothing named it: `fk_Order_customerId`. */
|
|
71
75
|
export function derivedForeignKeyName(table, columns) {
|
|
72
76
|
return `fk_${table}_${columns.join('_')}`;
|