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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getMeta } from '../entity/index.js';
|
|
2
|
-
import { asSelectMap, augmentWhere, clone, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, LoggerWrapper,
|
|
1
|
+
import { getMeta, idOf, soleIdOf } from '../entity/index.js';
|
|
2
|
+
import { asSelectMap, augmentWhere, childrenOf, clone, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isScalarId, LoggerWrapper, parentJoins, parentsIn, parseRelationAtKey, parseRelationQueryValue, rowKey, runHooks, someKey, targetKeyColumns, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
3
3
|
import { enrichError } from './queryError.js';
|
|
4
4
|
import { fillRelationCounts, withIdForCounts } from './relationCount.js';
|
|
5
5
|
/**
|
|
@@ -15,6 +15,29 @@ function assertIdValue(entity, id) {
|
|
|
15
15
|
if (id === undefined || id === null) {
|
|
16
16
|
throw new TypeError(`'${entity.name}' was addressed by id, but the id is ${String(id)}`);
|
|
17
17
|
}
|
|
18
|
+
if (isScalarId(id)) {
|
|
19
|
+
// One value names one column, which `buildQueryWhereAsMap` refuses on a composite.
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
// Every key, or the `$where` names only some of the columns and addresses each row that agrees on
|
|
23
|
+
// those - which for an update or a delete is silently far more than the one row asked for. `{}`
|
|
24
|
+
// reaches here too, and would otherwise be an unfiltered statement over the whole table.
|
|
25
|
+
const given = id;
|
|
26
|
+
const { ids } = getMeta(entity);
|
|
27
|
+
const missing = ids.filter((key) => given[key] == null);
|
|
28
|
+
if (missing.length) {
|
|
29
|
+
throw new TypeError(`'${entity.name}' is addressed by an object carrying every key of its primary key (${ids.join(', ')}); missing ${missing.join(', ')}.`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The one column a write path matches a parent's key against.
|
|
34
|
+
*
|
|
35
|
+
* Every caller reaches its parents through a sole id - `soleIdOf` refused a composite before any of
|
|
36
|
+
* them - so the parent contributes exactly one column here, whether the relation goes through a
|
|
37
|
+
* junction or straight to the child.
|
|
38
|
+
*/
|
|
39
|
+
function soleParentColumn(relOpts) {
|
|
40
|
+
return parentJoins(relOpts, 1)[0].joined;
|
|
18
41
|
}
|
|
19
42
|
/**
|
|
20
43
|
* Base class for all database queriers.
|
|
@@ -187,7 +210,11 @@ export class AbstractQuerier {
|
|
|
187
210
|
// Where a snapshot was taken, the statement names those rows rather than resolving `q` a second
|
|
188
211
|
// time. Two reads of one `$limit` with no total order are free to disagree, which would fire the
|
|
189
212
|
// hooks for one row and delete another; naming them also spares the second read.
|
|
190
|
-
|
|
213
|
+
let target = q;
|
|
214
|
+
if (doomed) {
|
|
215
|
+
const meta = getMeta(entity);
|
|
216
|
+
target = { $where: doomed.map((it) => idOf(meta, it)) };
|
|
217
|
+
}
|
|
191
218
|
await this.emitHook(entity, 'beforeDelete', doomed ?? []);
|
|
192
219
|
const changes = await this.internalDeleteMany(entity, target, opts);
|
|
193
220
|
await this.emitHook(entity, 'afterDelete', doomed ?? []);
|
|
@@ -218,12 +245,16 @@ export class AbstractQuerier {
|
|
|
218
245
|
const toInsert = [];
|
|
219
246
|
const toUpdate = [];
|
|
220
247
|
const existingIds = [];
|
|
248
|
+
// Save reads an id as proof the row exists, and inserts the rest. A composite key is supplied by
|
|
249
|
+
// the caller on every row, insert included, so that proof does not exist for one: telling the two
|
|
250
|
+
// apart takes a read, which is `upsertMany`'s job and not this one's.
|
|
251
|
+
const idKey = soleIdOf(meta, 'saving a row');
|
|
221
252
|
for (const it of payload) {
|
|
222
|
-
const id = it[
|
|
253
|
+
const id = it[idKey];
|
|
223
254
|
if (!id) {
|
|
224
255
|
toInsert.push(it);
|
|
225
256
|
}
|
|
226
|
-
else if (!someKey(it, (key) => key !==
|
|
257
|
+
else if (!someKey(it, (key) => key !== idKey)) {
|
|
227
258
|
existingIds.push(id);
|
|
228
259
|
}
|
|
229
260
|
else {
|
|
@@ -233,9 +264,9 @@ export class AbstractQuerier {
|
|
|
233
264
|
const [insertedIds, updatedIds] = await Promise.all([
|
|
234
265
|
toInsert.length ? this.insertMany(entity, toInsert) : [],
|
|
235
266
|
Promise.all(toUpdate.map(async (it) => {
|
|
236
|
-
const id = it[
|
|
267
|
+
const id = it[idKey];
|
|
237
268
|
const data = { ...it };
|
|
238
|
-
delete data[
|
|
269
|
+
delete data[idKey];
|
|
239
270
|
await this.updateOneById(entity, id, data);
|
|
240
271
|
return id;
|
|
241
272
|
})),
|
|
@@ -263,11 +294,11 @@ export class AbstractQuerier {
|
|
|
263
294
|
}
|
|
264
295
|
}
|
|
265
296
|
async fillToManyThroughRelation(payload, meta, relKey, relOpts, relationQuery) {
|
|
266
|
-
const
|
|
297
|
+
const joins = parentJoins(relOpts, meta.ids.length);
|
|
298
|
+
const [targetColumn] = targetKeyColumns(relOpts, meta.ids.length);
|
|
267
299
|
const throughEntity = relOpts.through();
|
|
268
300
|
const throughMeta = getMeta(throughEntity);
|
|
269
|
-
const targetRelKey = getKeys(throughMeta.relations).find((key) => throughMeta.relations[key]?.references.some(({ local }) => local ===
|
|
270
|
-
const ids = payload.map((it) => it[meta.id]);
|
|
301
|
+
const targetRelKey = getKeys(throughMeta.relations).find((key) => throughMeta.relations[key]?.references.some(({ local }) => local === targetColumn));
|
|
271
302
|
// A relation query names the target's columns, not the join table's, so the projection and the
|
|
272
303
|
// filter belong on the populate below - resolved there against the entity that has them. Spread
|
|
273
304
|
// onto the through query they asked `ItemTag` for `Tag`'s columns: `$where`/`$sort` failed with
|
|
@@ -275,53 +306,52 @@ export class AbstractQuerier {
|
|
|
275
306
|
const { $select: _select, $exclude: _exclude, $where: _where, ...throughQuery } = relationQuery;
|
|
276
307
|
const throughFounds = await this.findMany(throughEntity, {
|
|
277
308
|
...throughQuery,
|
|
278
|
-
$select: {
|
|
279
|
-
[localField]: true,
|
|
280
|
-
},
|
|
309
|
+
$select: Object.fromEntries(joins.map(({ joined }) => [joined, true])),
|
|
281
310
|
$populate: {
|
|
282
311
|
[targetRelKey]: {
|
|
283
312
|
...relationQuery,
|
|
284
313
|
$required: true,
|
|
285
314
|
},
|
|
286
315
|
},
|
|
287
|
-
$where:
|
|
288
|
-
[localField]: ids,
|
|
289
|
-
},
|
|
316
|
+
$where: parentsIn(joins, payload),
|
|
290
317
|
});
|
|
291
318
|
const founds = throughFounds.map((it) => ({
|
|
292
319
|
...it[targetRelKey],
|
|
293
|
-
[
|
|
320
|
+
...Object.fromEntries(joins.map(({ joined }) => [joined, it[joined]])),
|
|
294
321
|
}));
|
|
295
|
-
this.putChildrenInParents(payload, founds,
|
|
322
|
+
this.putChildrenInParents(payload, founds, joins, relKey);
|
|
296
323
|
}
|
|
297
324
|
async fillToManyOneToMany(payload, meta, relKey, relOpts, relationQuery, relEntity) {
|
|
298
|
-
const
|
|
325
|
+
const joins = parentJoins(relOpts, meta.ids.length);
|
|
299
326
|
// The FK is what putChildrenInParents groups on, so it outlives the relation's projection
|
|
300
327
|
// either way: added to a whitelisting `$select` (the raw-array form has nothing to augment),
|
|
301
328
|
// dropped from a subtractive `$exclude`. `relationQuery` is already a clone.
|
|
302
329
|
const select = asSelectMap(relationQuery.$select);
|
|
303
|
-
|
|
304
|
-
|
|
330
|
+
const exclude = relationQuery.$exclude;
|
|
331
|
+
for (const { joined } of joins) {
|
|
332
|
+
if (select && !select[joined]) {
|
|
333
|
+
select[joined] = true;
|
|
334
|
+
}
|
|
335
|
+
delete exclude?.[joined];
|
|
305
336
|
}
|
|
306
|
-
|
|
307
|
-
const ids = payload.map((it) => it[meta.id]);
|
|
308
|
-
relationQuery.$where = { ...relationQuery.$where, [foreignField]: ids };
|
|
337
|
+
relationQuery.$where = { ...relationQuery.$where, ...parentsIn(joins, payload) };
|
|
309
338
|
const founds = await this.findMany(relEntity, relationQuery);
|
|
310
|
-
this.putChildrenInParents(payload, founds,
|
|
339
|
+
this.putChildrenInParents(payload, founds, joins, relKey);
|
|
311
340
|
}
|
|
312
|
-
putChildrenInParents(parents, children,
|
|
341
|
+
putChildrenInParents(parents, children, joins, relKey) {
|
|
313
342
|
const childrenByParentId = {};
|
|
314
343
|
for (const child of children) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
childrenByParentId[parentId].push(child);
|
|
344
|
+
// Every joined column, so two children agreeing on one column of a composite key are not
|
|
345
|
+
// gathered under the same parent.
|
|
346
|
+
const parentId = rowKey(joins.map(({ joined }) => child[joined]));
|
|
347
|
+
(childrenByParentId[parentId] ??= []).push(child);
|
|
319
348
|
}
|
|
320
349
|
for (const parent of parents) {
|
|
321
350
|
// `[]` rather than nothing for a parent with no children: a populated to-many is a list the
|
|
322
351
|
// caller asked for, so it maps and counts without a guard, and its type can say so. An
|
|
323
352
|
// unpopulated one stays absent, which is what tells the two apart.
|
|
324
|
-
|
|
353
|
+
const key = rowKey(joins.map(({ parent: parentKey }) => parent[parentKey]));
|
|
354
|
+
parent[relKey] = (childrenByParentId[key] ?? []);
|
|
325
355
|
}
|
|
326
356
|
}
|
|
327
357
|
async insertRelations(entity, payload) {
|
|
@@ -334,7 +364,8 @@ export class AbstractQuerier {
|
|
|
334
364
|
}, []);
|
|
335
365
|
if (!entries.length)
|
|
336
366
|
return;
|
|
337
|
-
|
|
367
|
+
const idKey = soleIdOf(meta, 'saving a relation');
|
|
368
|
+
await Promise.all(entries.map(({ it, relKeys }) => Promise.all(relKeys.map((relKey) => this.saveRelation(entity, [it[idKey]], it[relKey], relKey)))));
|
|
338
369
|
}
|
|
339
370
|
async updateRelations(entity, q, payload, opts) {
|
|
340
371
|
const meta = getMeta(entity);
|
|
@@ -342,8 +373,9 @@ export class AbstractQuerier {
|
|
|
342
373
|
if (!relKeys.length) {
|
|
343
374
|
return;
|
|
344
375
|
}
|
|
376
|
+
const idKey = soleIdOf(meta, 'saving a relation');
|
|
345
377
|
const founds = await this.findMany(entity, idOnlyQuery(meta, q), opts);
|
|
346
|
-
const ids = founds.map((found) => found[
|
|
378
|
+
const ids = founds.map((found) => found[idKey]);
|
|
347
379
|
if (!ids.length) {
|
|
348
380
|
return;
|
|
349
381
|
}
|
|
@@ -351,6 +383,10 @@ export class AbstractQuerier {
|
|
|
351
383
|
await this.saveRelation(entity, ids, payload[relKey], relKey, true);
|
|
352
384
|
}
|
|
353
385
|
}
|
|
386
|
+
/**
|
|
387
|
+
* `EntityId` because a settled set names composite rows as objects, which is also what the parent's
|
|
388
|
+
* own delete takes - and what {@link childrenOf} reads each child's foreign key columns out of.
|
|
389
|
+
*/
|
|
354
390
|
async deleteRelations(entity, ids, opts) {
|
|
355
391
|
const meta = getMeta(entity);
|
|
356
392
|
const relKeys = filterPersistableRelationKeys(meta, meta.relations, 'delete');
|
|
@@ -360,9 +396,9 @@ export class AbstractQuerier {
|
|
|
360
396
|
if (!relOpts)
|
|
361
397
|
continue;
|
|
362
398
|
const relEntity = relOpts.entity();
|
|
363
|
-
const parentKey = parentKeyColumn(relOpts);
|
|
364
399
|
const target = relOpts.through ? relOpts.through() : relEntity;
|
|
365
|
-
|
|
400
|
+
const where = childrenOf(parentJoins(relOpts, meta.ids.length), ids);
|
|
401
|
+
await this.deleteMany(target, { $where: where }, opts);
|
|
366
402
|
}
|
|
367
403
|
}
|
|
368
404
|
/**
|
|
@@ -391,7 +427,8 @@ export class AbstractQuerier {
|
|
|
391
427
|
async saveToMany(relOpts, relEntity, ids, relPayload, isUpdate) {
|
|
392
428
|
const { through } = relOpts;
|
|
393
429
|
if (through) {
|
|
394
|
-
const localField =
|
|
430
|
+
const localField = soleParentColumn(relOpts);
|
|
431
|
+
const [targetColumn] = targetKeyColumns(relOpts, 1);
|
|
395
432
|
const throughEntity = through();
|
|
396
433
|
if (isUpdate) {
|
|
397
434
|
await this.deleteMany(throughEntity, { $where: { [localField]: ids } });
|
|
@@ -401,12 +438,12 @@ export class AbstractQuerier {
|
|
|
401
438
|
// would link every parent to a single shared row instead.
|
|
402
439
|
for (const id of ids) {
|
|
403
440
|
const savedIds = await this.saveMany(relEntity, relPayload);
|
|
404
|
-
await this.insertMany(throughEntity, savedIds.map((relId) => ({ [localField]: id, [
|
|
441
|
+
await this.insertMany(throughEntity, savedIds.map((relId) => ({ [localField]: id, [targetColumn]: relId })));
|
|
405
442
|
}
|
|
406
443
|
}
|
|
407
444
|
return;
|
|
408
445
|
}
|
|
409
|
-
const foreignField =
|
|
446
|
+
const foreignField = soleParentColumn(relOpts);
|
|
410
447
|
if (isUpdate) {
|
|
411
448
|
await this.deleteMany(relEntity, { $where: { [foreignField]: ids } });
|
|
412
449
|
}
|
|
@@ -415,7 +452,7 @@ export class AbstractQuerier {
|
|
|
415
452
|
}
|
|
416
453
|
}
|
|
417
454
|
async saveOneToOne(relEntity, relOpts, ids, relPayload) {
|
|
418
|
-
const foreignField =
|
|
455
|
+
const foreignField = soleParentColumn(relOpts);
|
|
419
456
|
if (relPayload === null) {
|
|
420
457
|
await this.deleteMany(relEntity, { $where: { [foreignField]: ids } });
|
|
421
458
|
return;
|
|
@@ -423,7 +460,7 @@ export class AbstractQuerier {
|
|
|
423
460
|
await this.saveMany(relEntity, ids.map((id) => ({ ...relPayload, [foreignField]: id })));
|
|
424
461
|
}
|
|
425
462
|
async saveManyToOne(entity, relEntity, relOpts, ids, relPayload) {
|
|
426
|
-
// Not `
|
|
463
|
+
// Not `soleParentColumn`: a many-to-one points the other way, so this is the *parent's* own column
|
|
427
464
|
// holding the child's id - the one place `references[0].local` does not name a key of the parent.
|
|
428
465
|
const localField = relOpts.references[0].local;
|
|
429
466
|
// Per parent: each gets its own reference row, so each `SET` carries a different value.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AbstractDialect } from '../dialect/index.js';
|
|
2
|
-
import type { EntityData, ExtraOptions, FieldKey, IdValue, PoolRunOptions, Querier, QuerierPool, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch, QueryStreamProjected, QueryUpdateResult, RelationKey, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
2
|
+
import type { EntityData, EntityId, ExtraOptions, FieldKey, IdValue, PoolRunOptions, Querier, QuerierPool, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch, QueryStreamProjected, QueryUpdateResult, RelationKey, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Base pool: dialect id and behavior come only from the `dialect` instance (see {@link QuerierPool}).
|
|
5
5
|
*/
|
|
@@ -24,7 +24,7 @@ export declare abstract class AbstractQuerierPool<Q extends Querier, D extends A
|
|
|
24
24
|
withQuerier<T>(callback: (querier: Q) => Promise<T>, opts?: PoolRunOptions): Promise<T>;
|
|
25
25
|
/** Run `fn` under `context` (an enclosing {@link withContext}) when provided, else run it as-is. */
|
|
26
26
|
private runScoped;
|
|
27
|
-
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:
|
|
27
|
+
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?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C> | undefined>;
|
|
28
28
|
findOne<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>, q: QueryOneProjected<E, S, V, X, P, C>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C> | undefined>;
|
|
29
29
|
findMany<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>, q: QueryProjected<E, S, V, X, P, C>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C>[]>;
|
|
30
30
|
/**
|
|
@@ -38,16 +38,16 @@ export declare abstract class AbstractQuerierPool<Q extends Querier, D extends A
|
|
|
38
38
|
aggregate<E extends object, const G extends QueryGroupMap<E>, const A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): Promise<QueryAggregateResult<E, G, A>[]>;
|
|
39
39
|
estimatedCount<E extends object>(entity: Type<E>): Promise<number>;
|
|
40
40
|
insertOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<IdValue<E> | undefined>;
|
|
41
|
-
insertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
|
|
42
|
-
updateOneById<E extends object>(entity: Type<E>, id:
|
|
41
|
+
insertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<(IdValue<E> | undefined)[]>;
|
|
42
|
+
updateOneById<E extends object>(entity: Type<E>, id: EntityId<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
43
43
|
updateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
44
44
|
upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>): Promise<QueryUpdateResult>;
|
|
45
45
|
upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>[]): Promise<QueryUpdateResult>;
|
|
46
|
-
saveOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<IdValue<E
|
|
47
|
-
saveMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
|
|
48
|
-
deleteOneById<E extends object>(entity: Type<E>, id:
|
|
46
|
+
saveOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<IdValue<E> | undefined>;
|
|
47
|
+
saveMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<(IdValue<E> | undefined)[]>;
|
|
48
|
+
deleteOneById<E extends object>(entity: Type<E>, id: EntityId<E>, opts?: QueryOptions): Promise<number>;
|
|
49
49
|
deleteMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
|
|
50
|
-
restoreOneById<E extends object>(entity: Type<E>, id:
|
|
50
|
+
restoreOneById<E extends object>(entity: Type<E>, id: EntityId<E>): Promise<number>;
|
|
51
51
|
restoreMany<E extends object>(entity: Type<E>, q: QuerySearch<E>): Promise<number>;
|
|
52
52
|
/**
|
|
53
53
|
* end the pool.
|
|
@@ -105,7 +105,7 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
|
|
|
105
105
|
protected internalCount<E extends object>(entity: Type<E>, q?: QueryFilter<E>, opts?: QueryOptions): Promise<number>;
|
|
106
106
|
estimatedCount<E extends object>(entity: Type<E>): Promise<number>;
|
|
107
107
|
protected internalAggregate<E extends object, G extends QueryGroupMap<E>, A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): Promise<QueryAggregateResult<E, G, A>[]>;
|
|
108
|
-
internalInsertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
|
|
108
|
+
internalInsertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<(IdValue<E> | undefined)[]>;
|
|
109
109
|
internalUpdateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
110
110
|
/** The ids matching `q`, in `q`'s own order and page, so a write can name the rows it settled on. */
|
|
111
111
|
private settleIds;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { COUNT_ALIAS, TOTAL_ALIAS } from '../dialect/aliases.js';
|
|
2
2
|
import { decodeColumn } from '../dialect/hydrateColumn.js';
|
|
3
|
-
import { getMeta } from '../entity/index.js';
|
|
3
|
+
import { getMeta, idOf, soleIdOf } from '../entity/index.js';
|
|
4
4
|
import { buildUpdateResult, cascadesOnDelete, clone, getInsertFieldKeys, getRelationRequestSummary, hasKeys, idOnlyQuery, isAutoIncrement, isPagedQuery, obtainAttrsPaths, throwNoPendingTransaction, throwPendingTransaction, unflatObject, unflatObjects, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
5
5
|
import { AbstractQuerier } from './abstractQuerier.js';
|
|
6
6
|
import { enrichError } from './queryError.js';
|
|
@@ -283,14 +283,19 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
283
283
|
}
|
|
284
284
|
payload = clone(payload);
|
|
285
285
|
const meta = getMeta(entity);
|
|
286
|
-
|
|
287
|
-
|
|
286
|
+
// A composite key is supplied whole by the caller, so nothing is generated and nothing comes
|
|
287
|
+
// back: the rows insert, and their ids are `undefined` for the reason MySQL's are below - there
|
|
288
|
+
// is no id to report. `idOf` names such a row for the caller that wants one.
|
|
289
|
+
const [idKey] = meta.ids;
|
|
290
|
+
const sole = meta.ids.length === 1;
|
|
291
|
+
const idField = sole ? meta.fields[idKey] : undefined;
|
|
288
292
|
// RETURNING-based IDs are exact per row. Header-derived IDs (LAST_INSERT_ID /
|
|
289
293
|
// lastInsertRowid arithmetic) are only sound when the primary key is database-generated
|
|
290
294
|
// and no record supplies an explicit ID (a mixed batch shifts the positional mapping and
|
|
291
295
|
// MySQL stops guaranteeing consecutive values); otherwise generated IDs stay `undefined`.
|
|
292
|
-
const idsReliable =
|
|
293
|
-
(
|
|
296
|
+
const idsReliable = sole &&
|
|
297
|
+
(this.dialect.insertIdSource === 'returning' ||
|
|
298
|
+
(!!idField && isAutoIncrement(idField, true) && payload.every((it) => it[idKey] === undefined)));
|
|
294
299
|
// Inferring multiple ids from the single header id (MySQL) assumes a known stride; a clustered
|
|
295
300
|
// server may set `auto_increment_increment` > 1, so probe it (once, cached) before inferring.
|
|
296
301
|
if (idsReliable && payload.length > 1 && this.dialect.insertIdSource === 'firstId') {
|
|
@@ -309,7 +314,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
309
314
|
if (idsReliable) {
|
|
310
315
|
it[idKey] ??= ids[index];
|
|
311
316
|
}
|
|
312
|
-
payloadIds.push(it[idKey]);
|
|
317
|
+
payloadIds.push(sole ? it[idKey] : undefined);
|
|
313
318
|
});
|
|
314
319
|
}
|
|
315
320
|
await this.insertRelations(entity, payload);
|
|
@@ -339,7 +344,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
339
344
|
const ctx = this.dialect.createContext();
|
|
340
345
|
this.dialect.find(ctx, entity, idOnlyQuery(meta, q), opts);
|
|
341
346
|
const founds = await this.all(ctx.sql, ctx.values);
|
|
342
|
-
return founds.map((
|
|
347
|
+
return founds.map((found) => idOf(meta, found));
|
|
343
348
|
}
|
|
344
349
|
async upsertOne(entity, conflictPaths, payload) {
|
|
345
350
|
return this.upsertMany(entity, conflictPaths, [payload]);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { COUNT_ALIAS } from '../dialect/aliases.js';
|
|
2
|
-
import { getMeta } from '../entity/index.js';
|
|
2
|
+
import { getMeta, soleIdOf } from '../entity/index.js';
|
|
3
3
|
import { COUNT_RESULT_KEY } from '../type/index.js';
|
|
4
|
-
import { asSelectMap, getKeys,
|
|
4
|
+
import { asSelectMap, getKeys, parentJoins, parentsIn, rowKey, targetKeyColumns, } from '../util/index.js';
|
|
5
5
|
/**
|
|
6
6
|
* A `$count` groups its tallies by the parent's id, so the id has to outlive the projection - the
|
|
7
7
|
* same reason populating a relation keeps it. A whitelisting `$select` gains the key and an
|
|
@@ -9,27 +9,33 @@ import { asSelectMap, getKeys, parentKeyColumn, targetKeyColumn } from '../util/
|
|
|
9
9
|
*/
|
|
10
10
|
export function withIdForCounts(entity, q) {
|
|
11
11
|
const meta = getMeta(entity);
|
|
12
|
-
if (!q.$count
|
|
12
|
+
if (!q.$count) {
|
|
13
13
|
return q;
|
|
14
14
|
}
|
|
15
|
+
const { ids } = meta;
|
|
16
|
+
const named = ids.join(', ');
|
|
15
17
|
if (q.$distinct) {
|
|
16
|
-
// The
|
|
17
|
-
// `$distinct` collapses on: adding
|
|
18
|
-
throw new TypeError(`$count cannot be combined with $distinct: the tallies group by each row's '${
|
|
18
|
+
// The keys would have to join the projection for the tallies to group by, and that is the set
|
|
19
|
+
// `$distinct` collapses on: adding them silently stops the grouping collapsing anything at all.
|
|
20
|
+
throw new TypeError(`$count cannot be combined with $distinct: the tallies group by each row's '${named}', which the grouping does not keep.`);
|
|
19
21
|
}
|
|
20
22
|
if (Array.isArray(q.$select)) {
|
|
21
|
-
// Nothing to add the
|
|
23
|
+
// Nothing to add the keys to, and without them every tally would be looked up by `undefined` and
|
|
22
24
|
// come back zero. Refused rather than answered wrong, the way an unorderable clause is.
|
|
23
|
-
throw new TypeError(`$count needs the '${
|
|
25
|
+
throw new TypeError(`$count needs the '${named}' of each row to group its tallies by, which a raw $select cannot carry. Use a $select map, or drop the $count.`);
|
|
24
26
|
}
|
|
25
27
|
const next = { ...q };
|
|
26
28
|
const select = asSelectMap(q.$select);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
const missing = select ? ids.filter((key) => !select[key]) : [];
|
|
30
|
+
if (missing.length) {
|
|
31
|
+
next.$select = { ...select, ...Object.fromEntries(missing.map((key) => [key, true])) };
|
|
29
32
|
}
|
|
30
|
-
|
|
33
|
+
const excluded = ids.filter((key) => q.$exclude?.[key]);
|
|
34
|
+
if (excluded.length) {
|
|
31
35
|
const kept = { ...q.$exclude };
|
|
32
|
-
|
|
36
|
+
for (const key of excluded) {
|
|
37
|
+
delete kept[key];
|
|
38
|
+
}
|
|
33
39
|
next.$exclude = kept;
|
|
34
40
|
}
|
|
35
41
|
return next;
|
|
@@ -44,9 +50,6 @@ export async function fillRelationCounts(querier, entity, payload, count) {
|
|
|
44
50
|
return;
|
|
45
51
|
}
|
|
46
52
|
const meta = getMeta(entity);
|
|
47
|
-
// The ids cross the same boundary the entity does: past here every column is named by metadata,
|
|
48
|
-
// so they are read as {@link CountedRow}'s values rather than as this entity's own id type.
|
|
49
|
-
const ids = payload.map((it) => it[meta.id]);
|
|
50
53
|
const counted = new Map();
|
|
51
54
|
for (const relKey of getKeys(count)) {
|
|
52
55
|
const value = count[relKey];
|
|
@@ -55,10 +58,11 @@ export async function fillRelationCounts(querier, entity, payload, count) {
|
|
|
55
58
|
continue;
|
|
56
59
|
}
|
|
57
60
|
const where = typeof value === 'object' ? value.$where : undefined;
|
|
58
|
-
|
|
61
|
+
const joins = parentJoins(relOpts, meta.ids.length);
|
|
62
|
+
counted.set(relKey, await countPerParent(querier, relOpts, joins, payload, where));
|
|
59
63
|
}
|
|
60
64
|
for (const parent of payload) {
|
|
61
|
-
const id =
|
|
65
|
+
const id = rowKey(meta.ids.map((key) => parent[key]));
|
|
62
66
|
const row = {};
|
|
63
67
|
for (const [relKey, byParent] of counted) {
|
|
64
68
|
// A parent the grouped result has no row for matched nothing, which is a zero rather than a
|
|
@@ -68,37 +72,42 @@ export async function fillRelationCounts(querier, entity, payload, count) {
|
|
|
68
72
|
parent[COUNT_RESULT_KEY] = row;
|
|
69
73
|
}
|
|
70
74
|
}
|
|
71
|
-
async function countPerParent(querier, relOpts,
|
|
75
|
+
async function countPerParent(querier, relOpts, joins, parents, where) {
|
|
72
76
|
const through = relOpts.through;
|
|
73
77
|
if (through) {
|
|
74
|
-
return countThroughPerParent(querier, relOpts, through(),
|
|
78
|
+
return countThroughPerParent(querier, relOpts, through(), joins, parents, where);
|
|
75
79
|
}
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
return groupedCount(querier, relOpts.entity(), joins, {
|
|
81
|
+
...where,
|
|
82
|
+
...parentsIn(joins, parents),
|
|
83
|
+
});
|
|
78
84
|
}
|
|
79
85
|
/**
|
|
80
86
|
* A many-to-many counts its junction rows, one per pairing. A filter names the target's columns,
|
|
81
87
|
* which the junction does not have, so the matching targets are resolved first and the junction
|
|
82
88
|
* counted against them - the one shape here that costs a second statement, and only when filtered.
|
|
83
89
|
*/
|
|
84
|
-
async function countThroughPerParent(querier, relOpts, throughEntity,
|
|
85
|
-
const
|
|
86
|
-
const throughWhere = { [local]: ids };
|
|
90
|
+
async function countThroughPerParent(querier, relOpts, throughEntity, joins, parents, where) {
|
|
91
|
+
const throughWhere = parentsIn(joins, parents);
|
|
87
92
|
if (where) {
|
|
88
93
|
const target = relOpts.entity();
|
|
89
|
-
const targetId = getMeta(target)
|
|
94
|
+
const targetId = soleIdOf(getMeta(target), 'a many-to-many target');
|
|
90
95
|
const targets = await querier.findMany(target, { $select: { [targetId]: true }, $where: where });
|
|
91
|
-
|
|
96
|
+
const [targetColumn] = targetKeyColumns(relOpts, joins.length);
|
|
97
|
+
throughWhere[targetColumn] = targets.map((it) => it[targetId]);
|
|
92
98
|
}
|
|
93
|
-
return groupedCount(querier, throughEntity,
|
|
99
|
+
return groupedCount(querier, throughEntity, joins, throughWhere);
|
|
94
100
|
}
|
|
95
|
-
/** `SELECT <
|
|
96
|
-
async function groupedCount(querier, entity,
|
|
101
|
+
/** `SELECT <keys>, COUNT(*) ... GROUP BY <keys>`, as a lookup from parent key to tally. */
|
|
102
|
+
async function groupedCount(querier, entity, joins, where) {
|
|
97
103
|
const $agg = { [COUNT_ALIAS]: { $count: '*' } };
|
|
98
|
-
const
|
|
104
|
+
const $group = Object.fromEntries(joins.map(({ joined }) => [joined, true]));
|
|
105
|
+
const rows = await querier.aggregate(entity, { $group, $agg, $where: where });
|
|
99
106
|
const byParent = {};
|
|
100
107
|
for (const row of rows) {
|
|
101
|
-
|
|
108
|
+
// Keyed by every joined column, which is how a tally finds the one parent whose whole key it
|
|
109
|
+
// matches - and how the rows an over-selecting `IN` brought back find no parent at all.
|
|
110
|
+
byParent[rowKey(joins.map(({ joined }) => row[joined]))] = Number(row[COUNT_ALIAS]);
|
|
102
111
|
}
|
|
103
112
|
return byParent;
|
|
104
113
|
}
|
package/dist/schema/schemaAST.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - Entity metadata (decorator-based entities)
|
|
6
6
|
* - Database introspection results (TableSchema[])
|
|
7
7
|
*/
|
|
8
|
-
import { getMeta } from '../entity/metadata/definition.js';
|
|
8
|
+
import { getMeta, soleIdOf } from '../entity/metadata/definition.js';
|
|
9
9
|
import { derivedForeignKeyName, derivedIndexName, qualifyName } from '../util/sql.util.js';
|
|
10
10
|
import { fieldOptionsToCanonical } from './canonicalType.js';
|
|
11
11
|
import { createTableNode, SchemaAST } from './schemaAST.js';
|
|
@@ -57,7 +57,9 @@ function resolveColumnCanonicalType(field, seen = new Set()) {
|
|
|
57
57
|
if (!hasExplicitType && field.references && !seen.has(field.references)) {
|
|
58
58
|
seen.add(field.references);
|
|
59
59
|
const referencedMeta = getMeta(field.references());
|
|
60
|
-
|
|
60
|
+
// The column names which key it points at when the target has several; otherwise there is one.
|
|
61
|
+
const referencedKey = field.referencedKey ?? soleIdOf(referencedMeta, 'a foreign key');
|
|
62
|
+
const referencedIdField = referencedMeta.fields[referencedKey];
|
|
61
63
|
if (referencedIdField) {
|
|
62
64
|
return resolveColumnCanonicalType(referencedIdField, seen);
|
|
63
65
|
}
|
|
@@ -71,6 +73,7 @@ function addTableFromEntity(ctx, meta) {
|
|
|
71
73
|
const tableName = ctx.resolveTableName(meta);
|
|
72
74
|
const table = createTableNode(tableName, ctx.resolveSchema(meta));
|
|
73
75
|
const { columns, primaryKey } = table;
|
|
76
|
+
table.checks?.push(...(meta.checks ?? []));
|
|
74
77
|
// Add columns from fields
|
|
75
78
|
const fields = meta.fields;
|
|
76
79
|
for (const key of Object.keys(fields)) {
|
|
@@ -82,7 +85,8 @@ function addTableFromEntity(ctx, meta) {
|
|
|
82
85
|
continue;
|
|
83
86
|
const columnName = ctx.resolveColumnName(key, field);
|
|
84
87
|
const type = resolveColumnCanonicalType(field);
|
|
85
|
-
const isPrimaryKey =
|
|
88
|
+
const isPrimaryKey = field.isId === true;
|
|
89
|
+
const isSoleKey = isPrimaryKey && meta.ids.length === 1;
|
|
86
90
|
const column = {
|
|
87
91
|
name: columnName,
|
|
88
92
|
type,
|
|
@@ -91,15 +95,18 @@ function addTableFromEntity(ctx, meta) {
|
|
|
91
95
|
nullable: isPrimaryKey ? false : (field.nullable ?? true),
|
|
92
96
|
defaultValue: field.defaultValue,
|
|
93
97
|
isPrimaryKey,
|
|
94
|
-
|
|
98
|
+
// Only a sole integer key defaults to auto-increment: a composite's columns are values the
|
|
99
|
+
// caller supplies, and the serial type carries an inline `PRIMARY KEY` the table already states.
|
|
100
|
+
isAutoIncrement: field.autoIncrement ?? (isPrimaryKey && isSoleKey && type.category === 'integer'),
|
|
95
101
|
isUnique: field.unique ?? false,
|
|
96
102
|
comment: field.comment,
|
|
103
|
+
enum: field.enum,
|
|
97
104
|
table,
|
|
98
105
|
referencedBy: [],
|
|
99
106
|
references: undefined,
|
|
100
107
|
};
|
|
101
108
|
columns.set(columnName, column);
|
|
102
|
-
if (
|
|
109
|
+
if (field.isId) {
|
|
103
110
|
primaryKey.push(column);
|
|
104
111
|
}
|
|
105
112
|
}
|
|
@@ -131,27 +138,35 @@ function addRelationshipsFromEntity(ctx, meta) {
|
|
|
131
138
|
// user_profile(creatorId)`), which SQLite rejects outright as a foreign key mismatch.
|
|
132
139
|
const ownsForeignKey = relation.cardinality === 'm1' || (relation.cardinality === '11' && !relation.mappedBy);
|
|
133
140
|
if (ownsForeignKey) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
141
|
+
// Every pair, not just the first: a composite key is one constraint over all its columns, and
|
|
142
|
+
// the engine requires the referenced columns to match a unique constraint as a whole.
|
|
143
|
+
const localColumns = [];
|
|
144
|
+
const foreignColumns = [];
|
|
145
|
+
let firstLocalField;
|
|
146
|
+
for (const { local: localPropName, foreign: foreignPropName } of relation.references) {
|
|
147
|
+
const localField = meta.fields[localPropName];
|
|
148
|
+
const foreignField = relatedMeta.fields[foreignPropName];
|
|
149
|
+
if (!localField || !foreignField) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const localColumn = table.columns.get(ctx.resolveColumnName(localPropName, localField));
|
|
153
|
+
const foreignColumn = relatedTable.columns.get(ctx.resolveColumnName(foreignPropName, foreignField));
|
|
154
|
+
if (!localColumn || !foreignColumn) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
firstLocalField ??= localField;
|
|
158
|
+
localColumns.push(localColumn);
|
|
159
|
+
foreignColumns.push(foreignColumn);
|
|
160
|
+
}
|
|
161
|
+
if (localColumns.length === relation.references.length && firstLocalField) {
|
|
147
162
|
const relNode = {
|
|
148
|
-
name: derivedForeignKeyName(table.name,
|
|
163
|
+
name: derivedForeignKeyName(table.name, localColumns.map((column) => column.name)),
|
|
149
164
|
type: relation.cardinality === 'm1' ? 'ManyToOne' : 'OneToOne',
|
|
150
|
-
from: { table, columns:
|
|
151
|
-
to: { table: relatedTable, columns:
|
|
165
|
+
from: { table, columns: localColumns },
|
|
166
|
+
to: { table: relatedTable, columns: foreignColumns },
|
|
152
167
|
// Falls back to the FK column's own `onDelete`, which is what makes a bare `@Field({
|
|
153
168
|
// references, onDelete })` work with no relation declared at all.
|
|
154
|
-
onDelete: relation.onDelete ??
|
|
169
|
+
onDelete: relation.onDelete ?? firstLocalField.onDelete ?? ctx.defaultForeignKeyAction,
|
|
155
170
|
onUpdate: relation.onUpdate ?? ctx.defaultForeignKeyAction,
|
|
156
171
|
confidence: 1.0,
|
|
157
172
|
inferredFrom: 'entity_decorator',
|