uql-orm 0.24.1 → 0.24.3
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/README.md +1 -1
- package/dist/dialect/abstractSqlDialect.d.ts +3 -3
- package/dist/dialect/abstractSqlDialect.js +9 -12
- package/dist/entity/decorator/members.d.ts +21 -15
- package/dist/entity/decorator/members.js +9 -0
- package/dist/entity/metadata/definition.js +108 -98
- package/dist/querier/abstractQuerier.js +2 -2
- package/dist/querier/abstractSqlQuerier.js +2 -3
- package/dist/schema/schemaASTBuilder.js +4 -5
- package/dist/type/entity.d.ts +38 -17
- package/dist/type/queryRaw.d.ts +10 -3
- package/dist/type/utility.d.ts +7 -1
- package/dist/util/dialect.util.d.ts +10 -2
- package/dist/util/relationQuery.util.d.ts +0 -2
- package/dist/util/relationQuery.util.js +4 -9
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ from the browser to the server. The same object runs on every supported database
|
|
|
51
51
|
## Why UQL?
|
|
52
52
|
|
|
53
53
|
- **The fastest.** Wins [all 8 categories](https://uql-orm.dev/benchmark) of our [open benchmark](https://github.com/rogerpadilla/ts-orm-benchmark), beating even query builders like Knex and Kysely: ~2.4× faster than the runner-up on average, over 4.6M ops/s on simple SELECTs.
|
|
54
|
-
- **Light.** Zero dependencies,
|
|
54
|
+
- **Light.** Zero runtime dependencies, 288 kB on the wire, every dialect included.
|
|
55
55
|
- **Queries are data, not method chains.** Plain JSON in, typed rows out. There's no DSL to learn and nothing to compile.
|
|
56
56
|
- **Type-safe to the leaf.** Operators are gated per field type, and JSON/JSONB dot-paths resolve each path's value type, so `{ age: { $like: 'x' } }` or a typo'd path is a compile error instead of a runtime surprise.
|
|
57
57
|
- **No codegen.** Entities are TypeScript classes, so your code *is* the schema. No `.prisma` file to regenerate, no generated client to keep in sync.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type EntityMeta, type FieldKey, type FieldOptions, type IsolationLevel, type JsonColumnType, type JsonUpdateOp, type Query, type QueryAggMap, type QueryAggregate, type QueryComparisonOptions, type QueryConflictPaths, type QueryContext, type QueryDialect, type QueryExclude, type QueryGroupMap, type QueryHavingMap, type QueryOptions, type QueryPager, type QueryPopulate, QueryRaw, type QueryRawFnOptions, type QuerySearch, type QuerySelect, type QuerySelectOptions, type QuerySelectValue, type QuerySizeComparisonOps, type QuerySortMap, type QueryTextSearchOptions, type QueryWhere, type QueryWhereArray, type QueryWhereFieldOperatorMap, type QueryWhereMap, type QueryWhereOptions, type
|
|
1
|
+
import { type EntityMeta, type FieldKey, type FieldOptions, type IsolationLevel, type JsonColumnType, type JsonUpdateOp, type Query, type QueryAggMap, type QueryAggregate, type QueryComparisonOptions, type QueryConflictPaths, type QueryContext, type QueryDialect, type QueryExclude, type QueryGroupMap, type QueryHavingMap, type QueryOptions, type QueryPager, type QueryPopulate, QueryRaw, type QueryRawFnOptions, type QuerySearch, type QuerySelect, type QuerySelectOptions, type QuerySelectValue, type QuerySizeComparisonOps, type QuerySortMap, type QueryTextSearchOptions, type QueryWhere, type QueryWhereArray, type QueryWhereFieldOperatorMap, type QueryWhereMap, type QueryWhereOptions, type RelationMeta, type SqlDialectName, type SqlQueryDialect, type Type, type UpdatePayload } from '../type/index.js';
|
|
2
2
|
import { IndexSqlDialect } from './indexSqlDialect.js';
|
|
3
3
|
/** How a column's values are bound: see {@link AbstractSqlDialect.persistKind}. */
|
|
4
4
|
type PersistKind = 'plain' | 'json' | 'vector';
|
|
@@ -340,9 +340,9 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
|
|
|
340
340
|
*/
|
|
341
341
|
private appendRelationSubquery;
|
|
342
342
|
/** Filter by relation: a parent matches when {@link appendRelationSubquery} finds one target row. */
|
|
343
|
-
protected compareRelation<E>(ctx: QueryContext, entity: Type<E>,
|
|
343
|
+
protected compareRelation<E>(ctx: QueryContext, entity: Type<E>, val: QueryWhereMap<unknown>, rel: RelationMeta, opts: QueryComparisonOptions): void;
|
|
344
344
|
/** Filter by relation size: the same subquery, counting instead of testing for existence. */
|
|
345
|
-
protected compareRelationSize<E>(ctx: QueryContext, entity: Type<E>,
|
|
345
|
+
protected compareRelationSize<E>(ctx: QueryContext, entity: Type<E>, sizeVal: number | QuerySizeComparisonOps, rel: RelationMeta, opts: QueryComparisonOptions): void;
|
|
346
346
|
/**
|
|
347
347
|
* Build a complete `$size` comparison expression.
|
|
348
348
|
* Handles both single and multiple comparison operators by repeating the size expression.
|
|
@@ -215,7 +215,7 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
215
215
|
const joinAlias = this.escapeId(joinRelAlias, true);
|
|
216
216
|
ctx.append(` ${joinType} JOIN ${relEntityName} ${joinAlias} ON `);
|
|
217
217
|
let refAppended = false;
|
|
218
|
-
for (const it of relOpts.references
|
|
218
|
+
for (const it of relOpts.references) {
|
|
219
219
|
if (refAppended)
|
|
220
220
|
ctx.append(' AND ');
|
|
221
221
|
const relField = relMeta.fields[it.foreign];
|
|
@@ -246,7 +246,7 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
246
246
|
const prefix = opts.prefix;
|
|
247
247
|
for (const relKey of relKeys) {
|
|
248
248
|
const relOpts = meta.relations[relKey];
|
|
249
|
-
if (!relOpts
|
|
249
|
+
if (!relOpts)
|
|
250
250
|
continue;
|
|
251
251
|
const isFirstLevel = prefix === tableName;
|
|
252
252
|
const joinRelAlias = isFirstLevel ? relKey : prefix ? `${prefix}.${relKey}` : relKey;
|
|
@@ -334,10 +334,10 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
334
334
|
if (rel) {
|
|
335
335
|
const sizeVal = parseRelationSize(val);
|
|
336
336
|
if (sizeVal !== undefined) {
|
|
337
|
-
this.compareRelationSize(ctx, entity,
|
|
337
|
+
this.compareRelationSize(ctx, entity, sizeVal, rel, opts);
|
|
338
338
|
return;
|
|
339
339
|
}
|
|
340
|
-
this.compareRelation(ctx, entity,
|
|
340
|
+
this.compareRelation(ctx, entity, val, rel, opts);
|
|
341
341
|
return;
|
|
342
342
|
}
|
|
343
343
|
const value = this.normalizeWhereValue(val);
|
|
@@ -1304,12 +1304,9 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
1304
1304
|
* invisible here just as it is to a joined `$populate`. The caller's filter bypass is deliberately
|
|
1305
1305
|
* not propagated (`withDeleted()` does not reach into relations), matching `selectRelationJoins`.
|
|
1306
1306
|
*/
|
|
1307
|
-
appendRelationSubquery(ctx, entity,
|
|
1307
|
+
appendRelationSubquery(ctx, entity, rel, opts, projection, val) {
|
|
1308
1308
|
const meta = getMeta(entity);
|
|
1309
1309
|
const parentTable = this.resolveTableName(entity, meta);
|
|
1310
|
-
if (!rel.references?.length) {
|
|
1311
|
-
throw new TypeError(`Relation '${key}' on '${parentTable}' has no references defined`);
|
|
1312
|
-
}
|
|
1313
1310
|
const references = rel.references;
|
|
1314
1311
|
const escapedParentId = this.escapedParentColumn(parentTable, meta, opts, meta.id);
|
|
1315
1312
|
const relatedEntity = rel.entity();
|
|
@@ -1345,13 +1342,13 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
1345
1342
|
ctx.append(')');
|
|
1346
1343
|
}
|
|
1347
1344
|
/** Filter by relation: a parent matches when {@link appendRelationSubquery} finds one target row. */
|
|
1348
|
-
compareRelation(ctx, entity,
|
|
1345
|
+
compareRelation(ctx, entity, val, rel, opts) {
|
|
1349
1346
|
ctx.append('EXISTS ');
|
|
1350
|
-
this.appendRelationSubquery(ctx, entity,
|
|
1347
|
+
this.appendRelationSubquery(ctx, entity, rel, opts, '1', val);
|
|
1351
1348
|
}
|
|
1352
1349
|
/** Filter by relation size: the same subquery, counting instead of testing for existence. */
|
|
1353
|
-
compareRelationSize(ctx, entity,
|
|
1354
|
-
this.buildSizeComparison(ctx, () => this.appendRelationSubquery(ctx, entity,
|
|
1350
|
+
compareRelationSize(ctx, entity, sizeVal, rel, opts) {
|
|
1351
|
+
this.buildSizeComparison(ctx, () => this.appendRelationSubquery(ctx, entity, rel, opts, 'COUNT(*)', {}), sizeVal);
|
|
1355
1352
|
}
|
|
1356
1353
|
/**
|
|
1357
1354
|
* Build a complete `$size` comparison expression.
|
|
@@ -1,6 +1,17 @@
|
|
|
1
|
-
import type { EntityGetter, FieldOptions, FieldType, RelationManyToManyOptions, RelationManyToOneOptions, RelationOneToManyOptions, RelationOneToOneOptions, TsTypeOf } from '../../type/index.js';
|
|
1
|
+
import type { EntityGetter, FieldOptions, FieldType, IdValue, RelationManyToManyOptions, RelationManyToOneOptions, RelationOneToManyOptions, RelationOneToOneOptions, TsTypeOf } from '../../type/index.js';
|
|
2
2
|
/** A member decorator that also constrains the property it may be applied to. */
|
|
3
3
|
type MemberDecorator<V> = (value: undefined, context: ClassFieldDecoratorContext<unknown, V>) => void;
|
|
4
|
+
/**
|
|
5
|
+
* The property type a set of field options describes, in the same order schema generation resolves the
|
|
6
|
+
* column: a declared `type` wins, and otherwise the column - and so the property - is the referenced
|
|
7
|
+
* primary key's own type. Which is what makes `@Field({ references: () => User })` on a `number`, where
|
|
8
|
+
* `User.id` is a `uuid`, a compile error rather than a column that disagrees with its property.
|
|
9
|
+
*/
|
|
10
|
+
type DeclaredValue<O> = O extends {
|
|
11
|
+
readonly type: infer T extends FieldType;
|
|
12
|
+
} ? TsTypeOf<T> : O extends {
|
|
13
|
+
readonly references: EntityGetter<infer E>;
|
|
14
|
+
} ? IdValue<E> : never;
|
|
4
15
|
/**
|
|
5
16
|
* Declares a persisted field.
|
|
6
17
|
*
|
|
@@ -8,27 +19,22 @@ type MemberDecorator<V> = (value: undefined, context: ClassFieldDecoratorContext
|
|
|
8
19
|
* which is what makes the now-mandatory `type` worth stating.
|
|
9
20
|
*
|
|
10
21
|
* @example `@Field({ type: String }) name?: string;`
|
|
11
|
-
* @example `@Field({ references: () =>
|
|
12
|
-
*/
|
|
13
|
-
export declare function Field<T extends FieldType>(opts: FieldOptions & {
|
|
14
|
-
readonly type: T;
|
|
15
|
-
}): MemberDecorator<TsTypeOf<T> | undefined>;
|
|
16
|
-
/**
|
|
17
|
-
* A foreign key may omit `type`: schema generation resolves it from the referenced primary key, so the
|
|
18
|
-
* column picks up that key's `columnType`, length and chained references rather than a guess.
|
|
22
|
+
* @example `@Field({ references: () => User }) userId?: string;` (where `User.id` is a `uuid`)
|
|
19
23
|
*/
|
|
20
|
-
export declare function Field
|
|
21
|
-
|
|
22
|
-
}
|
|
24
|
+
export declare function Field<O extends FieldOptions<DeclaredValue<O>> & ({
|
|
25
|
+
type: FieldType;
|
|
26
|
+
} | {
|
|
27
|
+
references: EntityGetter;
|
|
28
|
+
})>(opts: O): MemberDecorator<DeclaredValue<O> | undefined>;
|
|
23
29
|
/**
|
|
24
30
|
* Declares the primary key, checked the same way as `@Field`.
|
|
25
31
|
*
|
|
26
32
|
* @example `@Id({ type: Number }) id?: number;`
|
|
27
33
|
* @example `@Id({ type: 'uuid', onInsert: uuidv7 }) id?: string;`
|
|
28
34
|
*/
|
|
29
|
-
export declare function Id<
|
|
30
|
-
|
|
31
|
-
}): MemberDecorator<
|
|
35
|
+
export declare function Id<O extends FieldOptions<DeclaredValue<O>> & {
|
|
36
|
+
type: FieldType;
|
|
37
|
+
}>(opts: O): MemberDecorator<DeclaredValue<O> | undefined>;
|
|
32
38
|
/**
|
|
33
39
|
* `E` comes from the mandatory `entity` getter, so the context can insist the property really holds that
|
|
34
40
|
* entity: `@ManyToOne({ entity: () => Other })` on a `Company` field stops compiling, and a to-many
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { memberRegistrations } from './bag.js';
|
|
2
|
+
/**
|
|
3
|
+
* Declares a persisted field.
|
|
4
|
+
*
|
|
5
|
+
* `@Field({ type: String })` on a `number` property is a compile error rather than a silent TEXT column,
|
|
6
|
+
* which is what makes the now-mandatory `type` worth stating.
|
|
7
|
+
*
|
|
8
|
+
* @example `@Field({ type: String }) name?: string;`
|
|
9
|
+
* @example `@Field({ references: () => User }) userId?: string;` (where `User.id` is a `uuid`)
|
|
10
|
+
*/
|
|
2
11
|
export function Field(opts) {
|
|
3
12
|
return (_value, context) => {
|
|
4
13
|
memberRegistrations(context.metadata).fields[String(context.name)] = opts;
|
|
@@ -38,8 +38,11 @@ export function defineRelation(entity, key, opts) {
|
|
|
38
38
|
throw new TypeError(`'${entity.name}.${key}' needs an 'entity' getter, e.g. '@ManyToOne({ entity: () => Company })'.`);
|
|
39
39
|
}
|
|
40
40
|
const meta = ensureMeta(entity);
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// Registration writes the authored shape into a map declared as resolved: `getMeta` runs
|
|
42
|
+
// `fillRelations`, which settles `entity`, `references` and `mappedBy` or throws. Bridging the two
|
|
43
|
+
// shapes here is what lets every consumer read `RelationMeta` without asserting.
|
|
44
|
+
const relations = meta.relations;
|
|
45
|
+
relations[key] = { ...relations[key], ...opts };
|
|
43
46
|
return meta;
|
|
44
47
|
}
|
|
45
48
|
export function defineHook(entity, methodName, event) {
|
|
@@ -182,122 +185,131 @@ export function getMeta(entity) {
|
|
|
182
185
|
}
|
|
183
186
|
function fillRelations(meta) {
|
|
184
187
|
for (const relKey in meta.relations) {
|
|
188
|
+
// The authored view: `mappedBy` may still be the callback and `references` unset until this settles them.
|
|
185
189
|
const relOpts = meta.relations[relKey];
|
|
186
190
|
if (!relOpts)
|
|
187
191
|
continue;
|
|
188
|
-
|
|
189
|
-
// references were manually specified
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
+
const at = `'${meta.entity.name}.${relKey}'`;
|
|
192
193
|
if (relOpts.mappedBy) {
|
|
193
|
-
|
|
194
|
-
continue;
|
|
194
|
+
fillInverseSide(at, relOpts);
|
|
195
195
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (relOpts.cardinality === 'mm') {
|
|
199
|
-
const idKey = meta.id;
|
|
200
|
-
const relIdKey = relMeta.id;
|
|
201
|
-
const idName = meta.fields[idKey]?.name ?? idKey;
|
|
202
|
-
const relIdName = relMeta.fields[relIdKey]?.name ?? relIdKey;
|
|
203
|
-
const source = lowerFirst(meta.name ?? '') + upperFirst(idName);
|
|
204
|
-
const target = lowerFirst(relMeta.name ?? '') + upperFirst(relIdName);
|
|
205
|
-
relOpts.references = [
|
|
206
|
-
{ local: source, foreign: idKey },
|
|
207
|
-
{ local: target, foreign: relIdKey },
|
|
208
|
-
];
|
|
196
|
+
else if (!relOpts.references) {
|
|
197
|
+
fillOwningSide(at, meta, relKey, relOpts);
|
|
209
198
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const fkKey = `${relKey}Id`;
|
|
213
|
-
relOpts.references = [{ local: fkKey, foreign: relIdKey }];
|
|
214
|
-
// Auto-create the FK column when only the relation is declared (no explicit `@Field`).
|
|
215
|
-
// Mirror an explicit `@Field({ references })` column: carry `references` and mark the type
|
|
216
|
-
// as inferred so schema generation resolves the exact referenced primary-key type
|
|
217
|
-
// (columnType, length, chained keys) via `resolveColumnCanonicalType`.
|
|
218
|
-
if (!meta.fields[fkKey]) {
|
|
219
|
-
const relatedIdField = relMeta.fields[relIdKey];
|
|
220
|
-
meta.fields[fkKey] = {
|
|
221
|
-
...meta.fields[fkKey],
|
|
222
|
-
name: fkKey,
|
|
223
|
-
type: relatedIdField?.type ?? Number,
|
|
224
|
-
references: relOpts.entity,
|
|
225
|
-
typeFromReference: true,
|
|
226
|
-
};
|
|
227
|
-
}
|
|
199
|
+
if (!relOpts.references?.length) {
|
|
200
|
+
throw new TypeError(`${at} has no columns to join on.`);
|
|
228
201
|
}
|
|
202
|
+
// Hand-written `references` land here too: naming the columns says which they are, not that they exist.
|
|
229
203
|
if (relOpts.through) {
|
|
230
|
-
const
|
|
231
|
-
const throughMeta = fillThroughRelations(throughEntity);
|
|
204
|
+
const junction = getMeta(relOpts.through());
|
|
232
205
|
for (const { local } of relOpts.references) {
|
|
233
|
-
if (
|
|
206
|
+
if (junction.fields[local])
|
|
234
207
|
continue;
|
|
235
|
-
throw new TypeError(
|
|
236
|
-
"
|
|
208
|
+
throw new TypeError(`${at} joins through '${junction.entity.name}', which has no '${local}' field. Declare it, or name ` +
|
|
209
|
+
"the join columns with 'references'.");
|
|
237
210
|
}
|
|
238
211
|
}
|
|
239
212
|
}
|
|
213
|
+
fillForeignKeyRelations(meta);
|
|
240
214
|
return meta;
|
|
241
215
|
}
|
|
242
|
-
function
|
|
216
|
+
function fillOwningSide(at, meta, relKey, relOpts) {
|
|
217
|
+
const relMeta = ensureMeta(relOpts.entity());
|
|
218
|
+
const relIdKey = relMeta.id;
|
|
219
|
+
if (relOpts.through) {
|
|
220
|
+
// Both columns live on the junction, whatever the cardinality: `fillToManyThroughRelation`,
|
|
221
|
+
// `deleteRelations` and every dialect read them as junction columns.
|
|
222
|
+
relOpts.references = [
|
|
223
|
+
{ local: junctionColumn(meta, meta.id), foreign: meta.id },
|
|
224
|
+
{ local: junctionColumn(relMeta, relIdKey), foreign: relIdKey },
|
|
225
|
+
];
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (relOpts.cardinality === '1m' || relOpts.cardinality === 'mm') {
|
|
229
|
+
throw new TypeError(`${at} is a to-many relation with no way to join: it needs 'mappedBy' (the field on the other side), ` +
|
|
230
|
+
"'through' (a junction entity), or 'references' (the columns).");
|
|
231
|
+
}
|
|
232
|
+
const fkKey = `${relKey}Id`;
|
|
233
|
+
relOpts.references = [{ local: fkKey, foreign: relIdKey }];
|
|
234
|
+
// `typeFromReference` so schema generation resolves the referenced primary key's exact type
|
|
235
|
+
// (columnType, length, chained keys) rather than trusting the fallback, as it does for an
|
|
236
|
+
// explicit `@Field({ references })`.
|
|
237
|
+
if (!meta.fields[fkKey]) {
|
|
238
|
+
meta.fields[fkKey] = {
|
|
239
|
+
name: fkKey,
|
|
240
|
+
type: relMeta.fields[relIdKey]?.type ?? Number,
|
|
241
|
+
references: relOpts.entity,
|
|
242
|
+
typeFromReference: true,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function fillInverseSide(at, relOpts) {
|
|
243
247
|
const relEntity = relOpts.entity();
|
|
244
248
|
const relMeta = getMeta(relEntity);
|
|
245
|
-
const mappedBy =
|
|
249
|
+
const mappedBy = getMappedByKey(relOpts);
|
|
246
250
|
relOpts.mappedBy = mappedBy;
|
|
251
|
+
if (relOpts.references)
|
|
252
|
+
return;
|
|
247
253
|
if (relMeta.fields[mappedBy]) {
|
|
248
254
|
relOpts.references = [{ local: relMeta.id, foreign: mappedBy }];
|
|
249
255
|
return;
|
|
250
256
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (
|
|
255
|
-
|
|
256
|
-
relOpts.through = mappedByRelation.through;
|
|
257
|
-
return;
|
|
257
|
+
// Authored view again: with each side mapped by the other, the target is still mid-resolution here and
|
|
258
|
+
// its own `references` are unset, which is what the second throw reports.
|
|
259
|
+
const owner = relMeta.relations[mappedBy];
|
|
260
|
+
if (!owner) {
|
|
261
|
+
throw new TypeError(`${at} is mapped by '${mappedBy}', which is neither a field nor a relation of '${relEntity.name}'.`);
|
|
258
262
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
meta.relations = getKeys(meta.fields).reduce((relations, key) => {
|
|
270
|
-
const field = meta.fields[key];
|
|
271
|
-
if (!field)
|
|
272
|
-
return relations;
|
|
273
|
-
if (field.references) {
|
|
274
|
-
const relEntity = field.references();
|
|
275
|
-
const relMeta = ensureMeta(relEntity);
|
|
276
|
-
const relIdKey = relMeta.id;
|
|
277
|
-
const relKey = key.slice(0, -relIdKey.length);
|
|
278
|
-
const relOpts = {
|
|
279
|
-
entity: field.references,
|
|
280
|
-
cardinality: 'm1',
|
|
281
|
-
references: [{ local: key, foreign: relIdKey }],
|
|
282
|
-
};
|
|
283
|
-
relations[relKey] = relOpts;
|
|
284
|
-
}
|
|
285
|
-
return relations;
|
|
286
|
-
}, {});
|
|
287
|
-
return meta;
|
|
263
|
+
if (!owner.references?.length) {
|
|
264
|
+
throw new TypeError(`${at} is mapped by '${relEntity.name}.${mappedBy}', an inverse side too, so neither owns the foreign key.`);
|
|
265
|
+
}
|
|
266
|
+
// Two different flips: a junction pair is `[thisSide, otherSide]`, so the array reverses; a plain
|
|
267
|
+
// foreign key is one pair whose ends swap.
|
|
268
|
+
relOpts.references =
|
|
269
|
+
relOpts.cardinality === 'm1' || relOpts.cardinality === 'mm'
|
|
270
|
+
? owner.references.toReversed()
|
|
271
|
+
: owner.references.map(({ local, foreign }) => ({ local: foreign, foreign: local }));
|
|
272
|
+
relOpts.through = owner.through;
|
|
288
273
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
274
|
+
/**
|
|
275
|
+
* A field carrying `references` is a foreign key, and a foreign key is a many-to-one whether or not
|
|
276
|
+
* anyone declared the relation. Deriving it everywhere is what lets a junction be written as two plain
|
|
277
|
+
* columns: it needs the relations for `$populate` and for its DDL constraints, and it used to get them
|
|
278
|
+
* only because some *other* entity pointed `through` at it. Gaps only, so a declared relation keeps its
|
|
279
|
+
* own cardinality and `cascade`.
|
|
280
|
+
*/
|
|
281
|
+
function fillForeignKeyRelations(meta) {
|
|
282
|
+
const joined = new Set(getKeys(meta.relations).flatMap((key) => meta.relations[key]?.references.map(({ local }) => local) ?? []));
|
|
283
|
+
for (const fieldKey of getKeys(meta.fields)) {
|
|
284
|
+
const references = meta.fields[fieldKey]?.references;
|
|
285
|
+
if (!references || joined.has(fieldKey))
|
|
286
|
+
continue;
|
|
287
|
+
const foreign = ensureMeta(references()).id;
|
|
288
|
+
// The relation takes the column's name minus the key it points at (`itemId` -> `item`); a column
|
|
289
|
+
// named anything else has no name to take, so it stays a plain foreign key.
|
|
290
|
+
const suffix = upperFirst(foreign);
|
|
291
|
+
if (!fieldKey.endsWith(suffix))
|
|
292
|
+
continue;
|
|
293
|
+
const relKey = fieldKey.slice(0, -suffix.length);
|
|
294
|
+
if (!relKey || meta.fields[relKey] || meta.relations[relKey])
|
|
295
|
+
continue;
|
|
296
|
+
meta.relations[relKey] = {
|
|
297
|
+
entity: references,
|
|
298
|
+
cardinality: 'm1',
|
|
299
|
+
references: [{ local: fieldKey, foreign }],
|
|
300
|
+
};
|
|
295
301
|
}
|
|
296
|
-
return relOpts.mappedBy;
|
|
297
302
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
return
|
|
303
|
+
/** `<entityName><IdColumn>`, not the `<relationKey>Id` an owning to-one derives: a junction row has no relation key to borrow from. */
|
|
304
|
+
function junctionColumn(meta, idKey) {
|
|
305
|
+
return lowerFirst(meta.name ?? '') + upperFirst(meta.fields[idKey]?.name ?? idKey);
|
|
306
|
+
}
|
|
307
|
+
/** A callback only reads one property off the key map, and that property is the key, so one serves every entity. */
|
|
308
|
+
const RELATION_KEY_MAP = new Proxy({}, { get: (_, key) => key });
|
|
309
|
+
function getMappedByKey(relOpts) {
|
|
310
|
+
return typeof relOpts.mappedBy === 'function'
|
|
311
|
+
? relOpts.mappedBy(RELATION_KEY_MAP)
|
|
312
|
+
: relOpts.mappedBy;
|
|
301
313
|
}
|
|
302
314
|
function getIdKey(meta) {
|
|
303
315
|
const id = getKeys(meta.fields).find((key) => meta.fields[key]?.isId);
|
|
@@ -305,12 +317,10 @@ function getIdKey(meta) {
|
|
|
305
317
|
}
|
|
306
318
|
function extendMeta(target, source) {
|
|
307
319
|
const sourceFields = { ...source.fields };
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
delete sourceFields[sourceId];
|
|
313
|
-
}
|
|
320
|
+
const sourceId = getIdKey(source);
|
|
321
|
+
// A subclass that declares its own primary key drops the parent's, so exactly one stays marked.
|
|
322
|
+
if (sourceId && getIdKey(target)) {
|
|
323
|
+
delete sourceFields[sourceId];
|
|
314
324
|
}
|
|
315
325
|
target.fields = { ...sourceFields, ...target.fields };
|
|
316
326
|
target.relations = { ...source.relations, ...target.relations };
|
|
@@ -40,7 +40,7 @@ export class AbstractQuerier {
|
|
|
40
40
|
}
|
|
41
41
|
forEachRequestedRelation(meta, q.$populate, (relKey, relValue) => {
|
|
42
42
|
const relOpts = meta.relations[relKey];
|
|
43
|
-
if (!relOpts
|
|
43
|
+
if (!relOpts)
|
|
44
44
|
return;
|
|
45
45
|
const relEntity = relOpts.entity();
|
|
46
46
|
const parsed = parseRelationQueryValue(relValue);
|
|
@@ -205,7 +205,7 @@ export class AbstractQuerier {
|
|
|
205
205
|
const localField = relOpts.references[0].local;
|
|
206
206
|
const throughEntity = relOpts.through();
|
|
207
207
|
const throughMeta = getMeta(throughEntity);
|
|
208
|
-
const targetRelKey = getKeys(throughMeta.relations).find((key) => throughMeta.relations[key]
|
|
208
|
+
const targetRelKey = getKeys(throughMeta.relations).find((key) => throughMeta.relations[key]?.references.some(({ local }) => local === relOpts.references[1].local));
|
|
209
209
|
const ids = payload.map((it) => it[meta.id]);
|
|
210
210
|
const throughFounds = await this.findMany(throughEntity, {
|
|
211
211
|
...relationQuery,
|
|
@@ -119,10 +119,9 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
119
119
|
}
|
|
120
120
|
for (const key in meta.relations) {
|
|
121
121
|
const rel = meta.relations[key];
|
|
122
|
-
|
|
123
|
-
if (!relEntity) {
|
|
122
|
+
if (!rel)
|
|
124
123
|
continue;
|
|
125
|
-
|
|
124
|
+
const relEntity = rel.entity();
|
|
126
125
|
const value = row[key];
|
|
127
126
|
if (Array.isArray(value)) {
|
|
128
127
|
for (const it of value) {
|
|
@@ -152,7 +152,7 @@ export class SchemaASTBuilder {
|
|
|
152
152
|
const relations = meta.relations;
|
|
153
153
|
for (const key of Object.keys(relations)) {
|
|
154
154
|
const relation = relations[key];
|
|
155
|
-
if (!relation
|
|
155
|
+
if (!relation)
|
|
156
156
|
continue;
|
|
157
157
|
const relatedEntity = relation.entity();
|
|
158
158
|
const relatedMeta = getMeta(relatedEntity);
|
|
@@ -164,11 +164,10 @@ export class SchemaASTBuilder {
|
|
|
164
164
|
// `references` describe how to join back (its own primary key against the owner's FK column) -
|
|
165
165
|
// reading those as a foreign key emitted a reversed constraint (`User(id) REFERENCES
|
|
166
166
|
// user_profile(creatorId)`), which SQLite rejects outright as a foreign key mismatch.
|
|
167
|
-
const ownsForeignKey = relation.cardinality === 'm1' || (relation.cardinality === '11' &&
|
|
167
|
+
const ownsForeignKey = relation.cardinality === 'm1' || (relation.cardinality === '11' && !relation.mappedBy);
|
|
168
168
|
if (ownsForeignKey) {
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
const foreignPropName = references[0].foreign;
|
|
169
|
+
const localPropName = relation.references[0].local;
|
|
170
|
+
const foreignPropName = relation.references[0].foreign;
|
|
172
171
|
const localField = meta.fields[localPropName];
|
|
173
172
|
if (!localField)
|
|
174
173
|
continue;
|
package/dist/type/entity.d.ts
CHANGED
|
@@ -206,9 +206,11 @@ export type FieldType = StringConstructor | NumberConstructor | BooleanConstruct
|
|
|
206
206
|
*/
|
|
207
207
|
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;
|
|
208
208
|
/**
|
|
209
|
-
* Configurable options for a field
|
|
209
|
+
* Configurable options for a field, carrying `V`, the value the column holds: what a generator returns
|
|
210
|
+
* and what a default is has to be that value, checked the same way the declared `type` is. `Scalar` by
|
|
211
|
+
* by default, for the places that handle a field without knowing which one it is.
|
|
210
212
|
*/
|
|
211
|
-
export type FieldOptions = {
|
|
213
|
+
export type FieldOptions<V = TsTypeOf<FieldType>> = {
|
|
212
214
|
readonly name?: string;
|
|
213
215
|
readonly isId?: true;
|
|
214
216
|
readonly type?: FieldType;
|
|
@@ -238,8 +240,8 @@ export type FieldOptions = {
|
|
|
238
240
|
readonly virtual?: QueryRaw;
|
|
239
241
|
readonly updatable?: boolean;
|
|
240
242
|
readonly eager?: boolean;
|
|
241
|
-
readonly onInsert?: OnFieldCallback
|
|
242
|
-
readonly onUpdate?: OnFieldCallback
|
|
243
|
+
readonly onInsert?: OnFieldCallback<V>;
|
|
244
|
+
readonly onUpdate?: OnFieldCallback<V>;
|
|
243
245
|
/**
|
|
244
246
|
* Marks this field as the soft-delete field. Its presence makes the entity "soft deletable":
|
|
245
247
|
* a `delete` becomes an `UPDATE` that stamps this field instead of removing the row, and reads
|
|
@@ -251,7 +253,7 @@ export type FieldOptions = {
|
|
|
251
253
|
* @example `@Field({ softDelete: true }) deletedAt?: Date;`
|
|
252
254
|
* @example `@Field({ softDelete: () => Date.now() }) deletedAt?: number;`
|
|
253
255
|
*/
|
|
254
|
-
readonly softDelete?: OnFieldCallback
|
|
256
|
+
readonly softDelete?: true | OnFieldCallback<V>;
|
|
255
257
|
/**
|
|
256
258
|
* SQL column type for migrations. If not specified, inferred from TypeScript type.
|
|
257
259
|
*/
|
|
@@ -279,7 +281,7 @@ export type FieldOptions = {
|
|
|
279
281
|
/**
|
|
280
282
|
* Default value for the column
|
|
281
283
|
*/
|
|
282
|
-
readonly defaultValue?:
|
|
284
|
+
readonly defaultValue?: V | Record<string, unknown>;
|
|
283
285
|
/**
|
|
284
286
|
* Whether the column is auto-incrementing (for integer IDs).
|
|
285
287
|
*/
|
|
@@ -293,7 +295,7 @@ export type FieldOptions = {
|
|
|
293
295
|
*/
|
|
294
296
|
readonly comment?: string;
|
|
295
297
|
};
|
|
296
|
-
export type OnFieldCallback =
|
|
298
|
+
export type OnFieldCallback<V = TsTypeOf<FieldType>> = V | QueryRaw | (() => V | QueryRaw);
|
|
297
299
|
/**
|
|
298
300
|
* The TypeScript types a field may be declared as, given the `type` it registers: the inverse of
|
|
299
301
|
* {@link TypeFor}.
|
|
@@ -316,9 +318,9 @@ export type TsTypeOf<T> = T extends StringConstructor ? string : T extends Numbe
|
|
|
316
318
|
* `@Field({ references: () => Company })` would silently downgrade a `uuid` key to TEXT on every
|
|
317
319
|
* column pointing at it.
|
|
318
320
|
*/
|
|
319
|
-
export type FieldOptionsFor<V> = (FieldOptions & {
|
|
321
|
+
export type FieldOptionsFor<V> = (FieldOptions<NonNullable<V>> & {
|
|
320
322
|
readonly type: TypeFor<V>;
|
|
321
|
-
}) | (FieldOptions & {
|
|
323
|
+
}) | (FieldOptions<NonNullable<V>> & {
|
|
322
324
|
readonly references: EntityGetter;
|
|
323
325
|
readonly type?: TypeFor<V>;
|
|
324
326
|
});
|
|
@@ -331,12 +333,12 @@ export type RelationTarget<V> = NonNullable<Unpacked<NonNullable<V>>>;
|
|
|
331
333
|
* {@link RelationOptions} for a relation field declared as `V`, with `entity` required and pinned to
|
|
332
334
|
* `V`'s own type, and the cardinality restricted to the ones that field shape can hold. Together those
|
|
333
335
|
* reject `@ManyToOne({ entity: () => Other })` on a `Company` field, and any to-many cardinality on a
|
|
334
|
-
* field that is not an array.
|
|
336
|
+
* field that is not an array. An array field additionally needs a {@link RelationJoin}.
|
|
335
337
|
*/
|
|
336
338
|
export type RelationOptionsFor<V> = Omit<RelationOptions<RelationTarget<V>>, 'entity' | 'cardinality'> & {
|
|
337
339
|
readonly entity: EntityGetter<RelationTarget<V>>;
|
|
338
340
|
readonly cardinality: NonNullable<V> extends readonly unknown[] ? '1m' | 'mm' : '11' | 'm1';
|
|
339
|
-
};
|
|
341
|
+
} & (NonNullable<V> extends readonly unknown[] ? RelationJoin<RelationTarget<V>> : unknown);
|
|
340
342
|
/**
|
|
341
343
|
* The method names of an entity, so hook registrations name a method that exists.
|
|
342
344
|
*/
|
|
@@ -357,18 +359,37 @@ export type RelationOptions<E = any> = {
|
|
|
357
359
|
through?: EntityGetter;
|
|
358
360
|
references?: RelationReferences;
|
|
359
361
|
};
|
|
362
|
+
/**
|
|
363
|
+
* A relation once `getMeta` has resolved it: `entity` and `references` are settled and `mappedBy` is
|
|
364
|
+
* the key its callback named. Consumers read this shape rather than {@link RelationOptions}, so they
|
|
365
|
+
* need no assertions - `fillRelations` establishes the invariant once, and throws where it cannot.
|
|
366
|
+
*/
|
|
367
|
+
export type RelationMeta<E = any> = Omit<RelationOptions<E>, 'entity' | 'mappedBy' | 'references'> & {
|
|
368
|
+
entity: EntityGetter<E>;
|
|
369
|
+
mappedBy?: Key<E>;
|
|
370
|
+
references: RelationReferences;
|
|
371
|
+
};
|
|
372
|
+
/** How a to-many owner reaches its children: a junction entity, or the join columns by name. */
|
|
373
|
+
type RelationOwnerJoin<E> = Required<Pick<RelationOptions<E>, 'through'>> | Required<Pick<RelationOptions<E>, 'references'>>;
|
|
374
|
+
/**
|
|
375
|
+
* Every way a to-many can say where its rows are. Required because nothing about the field implies it:
|
|
376
|
+
* without one of the three, resolution has no columns to join on and throws.
|
|
377
|
+
*/
|
|
378
|
+
type RelationJoin<E> = RelationOwnerJoin<E> | Required<Pick<RelationOptions<E>, 'mappedBy'>>;
|
|
360
379
|
type RelationOptionsOwner<E> = Pick<RelationOptions<E>, 'entity' | 'references' | 'cascade'>;
|
|
361
380
|
type RelationOptionsInverseSide<E> = Required<Pick<RelationOptions<E>, 'entity' | 'mappedBy'>> & Pick<RelationOptions<E>, 'cascade'>;
|
|
362
|
-
type RelationOptionsThroughOwner<E> = Required<Pick<RelationOptions<E>, 'entity'>> & Pick<RelationOptions<E>, '
|
|
381
|
+
type RelationOptionsThroughOwner<E> = Required<Pick<RelationOptions<E>, 'entity'>> & Pick<RelationOptions<E>, 'cascade'> & RelationOwnerJoin<E>;
|
|
363
382
|
/**
|
|
364
383
|
* The key names of `E` as values, so `mappedBy` can be written as `(user) => user.company` instead of
|
|
365
384
|
* a string literal and survive a rename.
|
|
366
385
|
*
|
|
367
386
|
* Mapping over `Key<E>` rather than `keyof E` is what makes the callback usable: a homomorphic
|
|
368
387
|
* `[K in keyof E]` inherits the entity's optional modifiers, so `user.company` is
|
|
369
|
-
* `'company' | undefined` and {@link RelationKeyMapper} rejects it - every callback needed a `!`.
|
|
370
|
-
*
|
|
371
|
-
*
|
|
388
|
+
* `'company' | undefined` and {@link RelationKeyMapper} rejects it - every callback needed a `!`.
|
|
389
|
+
*
|
|
390
|
+
* At runtime a callback only ever reads one property off the map, so a single `Proxy` returning its
|
|
391
|
+
* own key stands in for every entity's: see `RELATION_KEY_MAP`. A key that names neither a field nor
|
|
392
|
+
* a relation of the target is rejected when the entity resolves.
|
|
372
393
|
*/
|
|
373
394
|
export type RelationKeyMap<E> = {
|
|
374
395
|
readonly [K in Key<E>]: K;
|
|
@@ -493,9 +514,9 @@ export type EntityMeta<E> = {
|
|
|
493
514
|
[key: string]: FieldOptions | undefined;
|
|
494
515
|
};
|
|
495
516
|
relations: {
|
|
496
|
-
[K in RelationKey<E>]?:
|
|
517
|
+
[K in RelationKey<E>]?: RelationMeta;
|
|
497
518
|
} & {
|
|
498
|
-
[key: string]:
|
|
519
|
+
[key: string]: RelationMeta | undefined;
|
|
499
520
|
};
|
|
500
521
|
/** Composite indexes defined via @Index decorator */
|
|
501
522
|
indexes?: EntityIndexMeta[];
|
package/dist/type/queryRaw.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { QueryContext, QueryDialect } from './dialect.js';
|
|
2
2
|
import type { Scalar } from './utility.js';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* What may be passed towards a `raw` callback. Every key is optional here because the callers along the
|
|
5
|
+
* way fill them in progressively; what reaches the callback is the complete set - see {@link QueryRawFn}.
|
|
5
6
|
*/
|
|
6
7
|
export type QueryRawFnOptions = {
|
|
7
8
|
/**
|
|
@@ -22,9 +23,15 @@ export type QueryRawFnOptions = {
|
|
|
22
23
|
ctx?: QueryContext;
|
|
23
24
|
};
|
|
24
25
|
/**
|
|
25
|
-
*
|
|
26
|
+
* A `raw` callback: write into `ctx`, or return a string or number to have it appended. Anything else
|
|
27
|
+
* it returns is ignored, which is why the return type is `unknown` rather than `void | Scalar` - the
|
|
28
|
+
* latter rejected `({ ctx }) => ctx.append(...)`, the form every virtual field is written in, because
|
|
29
|
+
* TypeScript's "returning a value where void is expected" allowance does not apply to a union.
|
|
30
|
+
*
|
|
31
|
+
* `Required`, and the parameter not optional, because the one place that calls it (`getRawValue`)
|
|
32
|
+
* passes all four every time.
|
|
26
33
|
*/
|
|
27
|
-
export type QueryRawFn = (opts
|
|
34
|
+
export type QueryRawFn = (opts: Required<QueryRawFnOptions>) => unknown;
|
|
28
35
|
export declare const RAW_VALUE: unique symbol;
|
|
29
36
|
export declare const RAW_ALIAS: unique symbol;
|
|
30
37
|
export declare class QueryRaw {
|
package/dist/type/utility.d.ts
CHANGED
|
@@ -6,8 +6,14 @@ export type MongoId = {
|
|
|
6
6
|
/**
|
|
7
7
|
* Every value type storable in an entity column. Superset of {@link QueryComparableScalar}
|
|
8
8
|
* and {@link PrimaryKey}.
|
|
9
|
+
*
|
|
10
|
+
* `Uint8Array` rather than `Buffer`, which every `Buffer` still satisfies: naming an ambient Node
|
|
11
|
+
* global here made the whole key-checking layer depend on `@types/node` being in scope. Without it
|
|
12
|
+
* `Buffer` resolves to nothing, this union collapses to `any`, and `FieldKey` - the basis of
|
|
13
|
+
* `$select`, `$where`, `$sort`, `@Index` and `defineEntity({ fields })` - silently stops checking
|
|
14
|
+
* anything. A browser or edge project would have got no type safety and no error saying so.
|
|
9
15
|
*/
|
|
10
|
-
export type Scalar = string | number | boolean | bigint | Date | RegExp |
|
|
16
|
+
export type Scalar = string | number | boolean | bigint | Date | RegExp | Uint8Array | MongoId;
|
|
11
17
|
/**
|
|
12
18
|
* Scalar types with a meaningful ordering, accepted by `$lt`/`$lte`/`$gt`/`$gte`/`$between`.
|
|
13
19
|
*/
|
|
@@ -14,12 +14,20 @@ export declare function filterFieldKeys<E>(meta: EntityMeta<E>, payload: E, call
|
|
|
14
14
|
* chunk-size estimate inspects the raw payload).
|
|
15
15
|
*/
|
|
16
16
|
export declare function getInsertFieldKeys<E>(meta: EntityMeta<E>, payloads: E[]): FieldKey<E>[];
|
|
17
|
-
export declare function getFieldCallbackValue(val: OnFieldCallback): QueryRaw |
|
|
17
|
+
export declare function getFieldCallbackValue(val: OnFieldCallback): string | number | bigint | boolean | Date | QueryRaw | readonly {
|
|
18
|
+
readonly __json?: never;
|
|
19
|
+
}[] | readonly number[] | Uint8Array<ArrayBufferLike> | {
|
|
20
|
+
readonly __json?: never;
|
|
21
|
+
};
|
|
18
22
|
/**
|
|
19
23
|
* Resolves the value stamped on the soft-delete field when deleting a row.
|
|
20
24
|
* `true` stamps the current timestamp (`new Date()`); any other marker is an {@link OnFieldCallback}.
|
|
21
25
|
*/
|
|
22
|
-
export declare function getSoftDeleteValue(field: FieldOptions): QueryRaw |
|
|
26
|
+
export declare function getSoftDeleteValue(field: FieldOptions): string | number | bigint | boolean | Date | QueryRaw | readonly {
|
|
27
|
+
readonly __json?: never;
|
|
28
|
+
}[] | readonly number[] | Uint8Array<ArrayBufferLike> | {
|
|
29
|
+
readonly __json?: never;
|
|
30
|
+
};
|
|
23
31
|
export declare function fillOnFields<E>(meta: EntityMeta<E>, payload: E | E[], callbackKey: CallbackKey): E[];
|
|
24
32
|
/**
|
|
25
33
|
* The relation keys present in `payload` whose cascade configuration allows `action`. Only
|
|
@@ -10,8 +10,6 @@ export declare function isPopulatingRelations<E>(meta: EntityMeta<E>, populate?:
|
|
|
10
10
|
export type RelationQuery<E extends object = object> = Query<E> & {
|
|
11
11
|
$required?: boolean;
|
|
12
12
|
};
|
|
13
|
-
export declare function getRelationQueryValue<E>(relKey: RelationKey<E>, populate?: QueryPopulate<E>): unknown;
|
|
14
|
-
export declare function isRelationQueryObject<E extends object = object>(value: unknown): value is RelationQuery<E>;
|
|
15
13
|
export type ParsedRelationQuery<E extends object = object> = {
|
|
16
14
|
query: RelationQuery<E>;
|
|
17
15
|
required: boolean;
|
|
@@ -37,13 +37,8 @@ const RELATION_QUERY_ALLOWED_KEYS = new Set([
|
|
|
37
37
|
...RELATION_QUERY_NUMBER_KEYS,
|
|
38
38
|
'$where',
|
|
39
39
|
]);
|
|
40
|
-
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
export function isRelationQueryObject(value) {
|
|
44
|
-
if (!isRecord(value))
|
|
45
|
-
return false;
|
|
46
|
-
return isValidRelationQueryShape(value);
|
|
40
|
+
function isRelationQueryObject(value) {
|
|
41
|
+
return isRecord(value) && isValidRelationQueryShape(value);
|
|
47
42
|
}
|
|
48
43
|
export function parseRelationQueryValue(value) {
|
|
49
44
|
if (isRelationQueryObject(value)) {
|
|
@@ -63,11 +58,11 @@ export function parseRelationQueryValue(value) {
|
|
|
63
58
|
}
|
|
64
59
|
/** Parses the relation payload for `relKey` */
|
|
65
60
|
export function parseRelationAtKey(relKey, populate) {
|
|
66
|
-
return parseRelationQueryValue(
|
|
61
|
+
return parseRelationQueryValue(populate?.[relKey]);
|
|
67
62
|
}
|
|
68
63
|
export function forEachRequestedRelation(meta, populate, fn) {
|
|
69
64
|
for (const relKey of getRelationRequestSummary(meta, populate).requestedKeys) {
|
|
70
|
-
fn(relKey,
|
|
65
|
+
fn(relKey, populate?.[relKey]);
|
|
71
66
|
}
|
|
72
67
|
}
|
|
73
68
|
function isRecord(value) {
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"homepage": "https://uql-orm.dev",
|
|
4
4
|
"description": "Extremely fast, type-safe TypeScript ORM - one API for every database",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "0.24.
|
|
6
|
+
"version": "0.24.3",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=24"
|
|
@@ -198,5 +198,5 @@
|
|
|
198
198
|
"publishConfig": {
|
|
199
199
|
"access": "public"
|
|
200
200
|
},
|
|
201
|
-
"gitHead": "
|
|
201
|
+
"gitHead": "4301613bcab77a626513197f1e815f972aa21bba"
|
|
202
202
|
}
|