@mikro-orm/core 7.3.0-dev.2 → 7.3.0-dev.20
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/EntityManager.js +131 -32
- package/drivers/DatabaseDriver.d.ts +2 -0
- package/drivers/DatabaseDriver.js +16 -3
- package/entity/EntityAssigner.js +15 -3
- package/entity/EntityFactory.d.ts +8 -0
- package/entity/EntityFactory.js +69 -3
- package/entity/EntityLoader.js +31 -15
- package/entity/WrappedEntity.js +6 -1
- package/entity/defineEntity.d.ts +32 -19
- package/entity/defineEntity.js +9 -0
- package/entity/validators.js +4 -1
- package/errors.d.ts +7 -0
- package/errors.js +15 -0
- package/hydration/ObjectHydrator.js +6 -2
- package/metadata/MetadataDiscovery.d.ts +10 -0
- package/metadata/MetadataDiscovery.js +178 -30
- package/metadata/MetadataStorage.d.ts +8 -0
- package/metadata/MetadataStorage.js +47 -3
- package/metadata/MetadataValidator.js +2 -1
- package/metadata/types.d.ts +12 -0
- package/package.json +1 -1
- package/platforms/Platform.d.ts +6 -0
- package/platforms/Platform.js +26 -0
- package/serialization/EntitySerializer.js +52 -12
- package/types/DecimalType.js +34 -1
- package/types/DoubleType.d.ts +7 -3
- package/types/DoubleType.js +8 -3
- package/typings.d.ts +26 -6
- package/typings.js +5 -2
- package/unit-of-work/ChangeSetComputer.js +22 -7
- package/unit-of-work/ChangeSetPersister.js +24 -5
- package/unit-of-work/CommitOrderCalculator.js +2 -1
- package/unit-of-work/IdentityMap.d.ts +3 -2
- package/unit-of-work/IdentityMap.js +30 -7
- package/unit-of-work/UnitOfWork.js +45 -7
- package/utils/Configuration.d.ts +12 -1
- package/utils/EntityComparator.d.ts +5 -0
- package/utils/EntityComparator.js +87 -19
- package/utils/QueryHelper.d.ts +14 -0
- package/utils/QueryHelper.js +90 -13
- package/utils/RawQueryFragment.d.ts +5 -1
- package/utils/RawQueryFragment.js +27 -19
- package/utils/TransactionManager.js +4 -0
- package/utils/Utils.d.ts +2 -0
- package/utils/Utils.js +24 -8
- package/utils/clone.js +5 -7
- package/utils/env-vars.js +2 -0
- package/utils/fs-utils.js +19 -15
- package/utils/upsert-utils.js +2 -1
|
@@ -89,7 +89,10 @@ export class UnitOfWork {
|
|
|
89
89
|
Utils.isPlainObject(data[prop.name])) {
|
|
90
90
|
// Skip polymorphic relations - they use PolymorphicRef wrapper
|
|
91
91
|
if (!prop.polymorphic) {
|
|
92
|
-
|
|
92
|
+
// with `targetKey`, the FK holds that value instead of the PK
|
|
93
|
+
data[prop.name] = prop.targetKey
|
|
94
|
+
? data[prop.name][prop.targetKey]
|
|
95
|
+
: Utils.getPrimaryKeyValues(data[prop.name], prop.targetMeta, true);
|
|
93
96
|
}
|
|
94
97
|
}
|
|
95
98
|
else if (prop.kind === ReferenceKind.EMBEDDED && !prop.object && Utils.isPlainObject(data[prop.name])) {
|
|
@@ -205,7 +208,18 @@ export class UnitOfWork {
|
|
|
205
208
|
value = prop.customType.convertToJSValue(value, this.#platform, { mode: 'hydration' });
|
|
206
209
|
}
|
|
207
210
|
const hash = this.#identityMap.getKeyHash(key, '' + value, schema);
|
|
208
|
-
|
|
211
|
+
const entity = this.#identityMap.getByHash(meta, hash);
|
|
212
|
+
// the entry is stale when the key changed via refresh or merge, unless only the unflushed value differs
|
|
213
|
+
if (entity && '' + entity[key] !== '' + value) {
|
|
214
|
+
// the snapshot holds the database value
|
|
215
|
+
const dbValue = prop?.customType
|
|
216
|
+
? prop.customType.convertToDatabaseValue(value, this.#platform, { mode: 'serialization' })
|
|
217
|
+
: value;
|
|
218
|
+
if ('' + helper(entity).__originalEntityData?.[key] !== '' + dbValue) {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return entity;
|
|
209
223
|
}
|
|
210
224
|
/**
|
|
211
225
|
* Stores an entity in the identity map under an alternate key (non-PK property).
|
|
@@ -368,6 +382,10 @@ export class UnitOfWork {
|
|
|
368
382
|
}
|
|
369
383
|
/** Marks an entity for removal, cascading to related entities. */
|
|
370
384
|
remove(entity, visited, options = {}) {
|
|
385
|
+
// a reference known only by its `targetKey` has no PK, so the delete would match nothing
|
|
386
|
+
if (!helper(entity).__initialized && !helper(entity).hasPrimaryKey()) {
|
|
387
|
+
throw ValidationError.referenceWithoutPK(entity);
|
|
388
|
+
}
|
|
371
389
|
// allow removing not managed entities if they are not part of the persist stack
|
|
372
390
|
if (helper(entity).__managed || !this.#persistStack.has(entity)) {
|
|
373
391
|
this.#removeStack.add(entity);
|
|
@@ -582,19 +600,27 @@ export class UnitOfWork {
|
|
|
582
600
|
this.findEarlyUpdates(cs, inserts[cs.meta.uniqueName]);
|
|
583
601
|
}
|
|
584
602
|
}
|
|
603
|
+
// hashes of the inserts' PKs and unique values per type, built lazily, so matching a removed entity is a set lookup
|
|
604
|
+
const insertHashes = {};
|
|
585
605
|
for (const entity of this.#removeStack) {
|
|
586
606
|
const wrapped = helper(entity);
|
|
587
607
|
/* v8 ignore next */
|
|
588
608
|
if (wrapped.__processing) {
|
|
589
609
|
continue;
|
|
590
610
|
}
|
|
591
|
-
const
|
|
592
|
-
let
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
611
|
+
const uniqueName = wrapped.__meta.uniqueName;
|
|
612
|
+
let hashes = insertHashes[uniqueName];
|
|
613
|
+
if (!hashes) {
|
|
614
|
+
hashes = insertHashes[uniqueName] = new Set();
|
|
615
|
+
for (const cs of inserts[uniqueName] ?? []) {
|
|
616
|
+
hashes.add(cs.getSerializedPrimaryKey());
|
|
617
|
+
for (const hash of this.expandUniqueProps(cs.entity)) {
|
|
618
|
+
hashes.add(hash);
|
|
619
|
+
}
|
|
596
620
|
}
|
|
597
621
|
}
|
|
622
|
+
const deletePkHash = [wrapped.getSerializedPrimaryKey(), ...this.expandUniqueProps(entity)];
|
|
623
|
+
const type = deletePkHash.some(hash => hashes.has(hash)) ? ChangeSetType.DELETE_EARLY : ChangeSetType.DELETE;
|
|
598
624
|
this.computeChangeSet(entity, type);
|
|
599
625
|
}
|
|
600
626
|
}
|
|
@@ -1118,6 +1144,11 @@ export class UnitOfWork {
|
|
|
1118
1144
|
for (const changeSet of changeSets) {
|
|
1119
1145
|
const wrapped = helper(changeSet.entity);
|
|
1120
1146
|
wrapped.__originalEntityData = this.#comparator.prepareEntity(changeSet.entity);
|
|
1147
|
+
// re-index so the entity is found by its new `targetKey` values instead of the old ones
|
|
1148
|
+
if (changeSet.meta.root.targetKeys?.some(key => key in changeSet.payload)) {
|
|
1149
|
+
this.#identityMap.delete(changeSet.entity);
|
|
1150
|
+
this.#identityMap.store(changeSet.entity);
|
|
1151
|
+
}
|
|
1121
1152
|
if (!wrapped.__initialized) {
|
|
1122
1153
|
for (const prop of changeSet.meta.relations) {
|
|
1123
1154
|
if ([ReferenceKind.MANY_TO_MANY, ReferenceKind.ONE_TO_MANY].includes(prop.kind) &&
|
|
@@ -1260,6 +1291,13 @@ export class UnitOfWork {
|
|
|
1260
1291
|
calc.discoverProperty({ ...prop, targetMeta }, meta._id);
|
|
1261
1292
|
}
|
|
1262
1293
|
}
|
|
1294
|
+
else if (prop.targetMeta?.inheritanceType === 'tpt') {
|
|
1295
|
+
calc.discoverProperty(prop, meta._id);
|
|
1296
|
+
// a TPT subclass delete has only a leaf change set that also removes the target row; weak edge so it can't break insert order
|
|
1297
|
+
for (const targetMeta of this.#metadata.find(prop.targetMeta.class).allTPTDescendants ?? []) {
|
|
1298
|
+
calc.discoverProperty({ ...prop, targetMeta, nullable: true }, meta._id);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1263
1301
|
else {
|
|
1264
1302
|
calc.discoverProperty(prop, meta._id);
|
|
1265
1303
|
}
|
package/utils/Configuration.d.ts
CHANGED
|
@@ -218,6 +218,17 @@ export type MigrationsOptions = {
|
|
|
218
218
|
* @default true
|
|
219
219
|
*/
|
|
220
220
|
allOrNothing?: boolean;
|
|
221
|
+
/**
|
|
222
|
+
* Serialize concurrent `migrator.up()`/`migrator.down()` runs (e.g. several replicas migrating on start) with a
|
|
223
|
+
* database lock, held on a pooled connection of its own for the whole run (so `pool.max` needs to be at least 2).
|
|
224
|
+
* Supported on PostgreSQL, MySQL, MariaDB and MSSQL, ignored elsewhere.
|
|
225
|
+
* @default false
|
|
226
|
+
*/
|
|
227
|
+
lock?: boolean;
|
|
228
|
+
/**
|
|
229
|
+
* How long to wait for the migration lock (in milliseconds) before failing. Waits indefinitely by default.
|
|
230
|
+
*/
|
|
231
|
+
lockTimeout?: number;
|
|
221
232
|
/**
|
|
222
233
|
* Allow dropping tables during schema diff.
|
|
223
234
|
* @default true
|
|
@@ -593,7 +604,7 @@ export interface Options<Driver extends IDatabaseDriver = IDatabaseDriver, EM ex
|
|
|
593
604
|
/**
|
|
594
605
|
* Force `Date` values to be stored in UTC for datetime columns without timezone.
|
|
595
606
|
* Works for MySQL (`datetime` type), PostgreSQL (`timestamp` type), and MSSQL (`datetime`/`datetime2` types).
|
|
596
|
-
* SQLite does this by default.
|
|
607
|
+
* SQLite does this by default. MySQL and MariaDB also get their session time zone set on every new connection.
|
|
597
608
|
* @default true
|
|
598
609
|
*/
|
|
599
610
|
forceUtcTimezone: boolean;
|
|
@@ -19,6 +19,11 @@ export declare class EntityComparator {
|
|
|
19
19
|
diffEntities<T extends object>(entityName: EntityName<T>, a: EntityData<T>, b: EntityData<T>, options?: {
|
|
20
20
|
includeInverseSides?: boolean;
|
|
21
21
|
}): EntityData<T>;
|
|
22
|
+
/**
|
|
23
|
+
* Returns true if the comparator checks this property with `!==`, so matching values are identical primitives.
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
isStrictlyCompared<T>(prop: EntityProperty<T>): boolean;
|
|
22
27
|
/** Returns true if two entity snapshots are identical (no differences). */
|
|
23
28
|
matching<T extends object>(entityName: EntityName<T>, a: EntityData<T>, b: EntityData<T>): boolean;
|
|
24
29
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { clone } from './clone.js';
|
|
2
2
|
import { ReferenceKind } from '../enums.js';
|
|
3
3
|
import { compareArrays, compareBooleans, compareBuffers, compareObjects, equals, parseJsonSafe, Utils, } from './Utils.js';
|
|
4
|
+
import { DateType } from '../types/DateType.js';
|
|
4
5
|
import { JsonType } from '../types/JsonType.js';
|
|
5
6
|
import { Raw } from './RawQueryFragment.js';
|
|
6
7
|
import { EntityIdentifier } from '../entity/EntityIdentifier.js';
|
|
@@ -29,6 +30,17 @@ export class EntityComparator {
|
|
|
29
30
|
const comparator = this.getEntityComparator(entityName);
|
|
30
31
|
return Utils.callCompiledFunction(comparator, a, b, options);
|
|
31
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Returns true if the comparator checks this property with `!==`, so matching values are identical primitives.
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
isStrictlyCompared(prop) {
|
|
38
|
+
if (prop.kind !== ReferenceKind.SCALAR || prop.customType?.compareValues) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const type = (prop.customType ? prop.customType.compareAsType() : prop.type).toLowerCase();
|
|
42
|
+
return ['string', 'number', 'bigint'].includes(type);
|
|
43
|
+
}
|
|
32
44
|
/** Returns true if two entity snapshots are identical (no differences). */
|
|
33
45
|
matching(entityName, a, b) {
|
|
34
46
|
const diff = this.diffEntities(entityName, a, b);
|
|
@@ -95,7 +107,7 @@ export class EntityComparator {
|
|
|
95
107
|
lines.push(` return entity${this.wrap(pk)};`);
|
|
96
108
|
}
|
|
97
109
|
const code = `// compiled pk getter for entity ${meta.className}\n` + `return function(entity) {\n${lines.join('\n')}\n}`;
|
|
98
|
-
const fnKey = `pkGetter-${meta.
|
|
110
|
+
const fnKey = `pkGetter-${meta.compiledName}`;
|
|
99
111
|
const pkSerializer = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
|
|
100
112
|
this.#pkGetters.set(meta, pkSerializer);
|
|
101
113
|
return pkSerializer;
|
|
@@ -147,7 +159,7 @@ export class EntityComparator {
|
|
|
147
159
|
}
|
|
148
160
|
const code = `// compiled pk getter (with converted custom types) for entity ${meta.className}\n` +
|
|
149
161
|
`return function(entity) {\n${lines.join('\n')}\n}`;
|
|
150
|
-
const fnKey = `pkGetterConverted-${meta.
|
|
162
|
+
const fnKey = `pkGetterConverted-${meta.compiledName}`;
|
|
151
163
|
const pkSerializer = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
|
|
152
164
|
this.#pkGettersConverted.set(meta, pkSerializer);
|
|
153
165
|
return pkSerializer;
|
|
@@ -206,7 +218,7 @@ export class EntityComparator {
|
|
|
206
218
|
}
|
|
207
219
|
}
|
|
208
220
|
const code = `// compiled pk serializer for entity ${meta.className}\n` + `return function(entity) {\n${lines.join('\n')}\n}`;
|
|
209
|
-
const fnKey = `pkSerializer-${meta.
|
|
221
|
+
const fnKey = `pkSerializer-${meta.compiledName}`;
|
|
210
222
|
const pkSerializer = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
|
|
211
223
|
this.#pkSerializers.set(meta, pkSerializer);
|
|
212
224
|
return pkSerializer;
|
|
@@ -236,7 +248,7 @@ export class EntityComparator {
|
|
|
236
248
|
})
|
|
237
249
|
.forEach(prop => lines.push(this.getPropertySnapshot(meta, prop, context, this.wrap(prop.name), this.wrap(prop.name), [prop.name])));
|
|
238
250
|
const code = `return function(entity) {\n const ret = {};\n${lines.join('\n')}\n return ret;\n}`;
|
|
239
|
-
const fnKey = `snapshotGenerator-${meta.
|
|
251
|
+
const fnKey = `snapshotGenerator-${meta.compiledName}`;
|
|
240
252
|
const snapshotGenerator = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
|
|
241
253
|
this.#snapshotGenerators.set(meta, snapshotGenerator);
|
|
242
254
|
return snapshotGenerator;
|
|
@@ -350,19 +362,48 @@ export class EntityComparator {
|
|
|
350
362
|
if (prop.embedded && (meta.embeddable || meta.properties[prop.embedded[0]].object)) {
|
|
351
363
|
continue;
|
|
352
364
|
}
|
|
365
|
+
let defined = `typeof ${this.propName(prop.fieldNames[0])} !== 'undefined'`;
|
|
366
|
+
// STI subtypes with conflicting columns share the property, so another subtype's null column must not override it
|
|
367
|
+
if (prop.renamedFrom) {
|
|
368
|
+
lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
|
|
369
|
+
defined = `${this.propName(prop.fieldNames[0])} != null`;
|
|
370
|
+
const sharesColumn = meta.props.some(p => p !== prop && p.name === prop.name && p.fieldNames?.[0] === prop.fieldNames[0]);
|
|
371
|
+
// hydrators expect mapped embeddable keys, so a shared object embeddable is mapped only for its own subtypes
|
|
372
|
+
if (sharesColumn && prop.kind === ReferenceKind.EMBEDDED && prop.object && meta.discriminatorMap) {
|
|
373
|
+
const owners = Object.entries(meta.discriminatorMap)
|
|
374
|
+
.filter(([, cls]) => this.#metadata.find(cls)?.properties[prop.name]?.targetMeta === prop.targetMeta)
|
|
375
|
+
.map(([value]) => `${this.propName(meta.discriminatorColumn)} == ${JSON.stringify(value)}`);
|
|
376
|
+
defined += ` && (${owners.join(' || ')})`;
|
|
377
|
+
}
|
|
378
|
+
else if (sharesColumn) {
|
|
379
|
+
// a column shared with a differently typed subtype is left for each subtype's hydrator to convert
|
|
380
|
+
lines.push(`${padding} if (${defined}) {`);
|
|
381
|
+
lines.push(`${padding} ret${this.wrap(prop.name)} = ${this.propName(prop.fieldNames[0])};`);
|
|
382
|
+
lines.push(`${padding} }`);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
353
386
|
if (prop.runtimeType === 'boolean') {
|
|
354
|
-
lines.push(`${padding} if (
|
|
387
|
+
lines.push(`${padding} if (${defined}) {`);
|
|
355
388
|
lines.push(`${padding} ret${this.wrap(prop.name)} = ${this.propName(prop.fieldNames[0])} == null ? ${this.propName(prop.fieldNames[0])} : !!${this.propName(prop.fieldNames[0])};`);
|
|
356
389
|
lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
|
|
357
390
|
lines.push(`${padding} }`);
|
|
358
391
|
}
|
|
359
392
|
else if (prop.runtimeType === 'Date' && !this.#platform.isNumericProperty(prop)) {
|
|
360
|
-
lines.push(`${padding} if (
|
|
393
|
+
lines.push(`${padding} if (${defined}) {`);
|
|
361
394
|
context.set('parseDate', (value) => this.#platform.parseDate(value));
|
|
362
395
|
parseDate('ret' + this.wrap(prop.name), this.propName(prop.fieldNames[0]), padding);
|
|
363
396
|
lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
|
|
364
397
|
lines.push(`${padding} }`);
|
|
365
398
|
}
|
|
399
|
+
else if (prop.customType instanceof DateType) {
|
|
400
|
+
// tedious returns DATE columns as `Date`, map them to the string the entity holds, so the snapshot matches it
|
|
401
|
+
context.set('convertDateToJSValue', (value) => this.#platform.convertDateToJSValue(value));
|
|
402
|
+
lines.push(`${padding} if (${defined}) {`);
|
|
403
|
+
lines.push(`${padding} ret${this.wrap(prop.name)} = ${this.propName(prop.fieldNames[0])} instanceof Date ? convertDateToJSValue(${this.propName(prop.fieldNames[0])}) : ${this.propName(prop.fieldNames[0])};`);
|
|
404
|
+
lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
|
|
405
|
+
lines.push(`${padding} }`);
|
|
406
|
+
}
|
|
366
407
|
else if (prop.kind === ReferenceKind.EMBEDDED && (prop.object || meta.embeddable)) {
|
|
367
408
|
const idx = this.#tmpIndex++;
|
|
368
409
|
context.set(`mapEmbeddedResult_${idx}`, (data) => {
|
|
@@ -372,13 +413,13 @@ export class EntityComparator {
|
|
|
372
413
|
}
|
|
373
414
|
return item == null ? item : this.getResultMapper(prop.targetMeta)(item);
|
|
374
415
|
});
|
|
375
|
-
lines.push(`${padding} if (
|
|
416
|
+
lines.push(`${padding} if (${defined}) {`);
|
|
376
417
|
lines.push(`${padding} ret${this.wrap(prop.name)} = ${this.propName(prop.fieldNames[0])} == null ? ${this.propName(prop.fieldNames[0])} : mapEmbeddedResult_${idx}(${this.propName(prop.fieldNames[0])});`);
|
|
377
418
|
lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
|
|
378
419
|
lines.push(`${padding} }`);
|
|
379
420
|
}
|
|
380
421
|
else if (prop.kind !== ReferenceKind.EMBEDDED) {
|
|
381
|
-
lines.push(`${padding} if (
|
|
422
|
+
lines.push(`${padding} if (${defined}) {`);
|
|
382
423
|
lines.push(`${padding} ret${this.wrap(prop.name)} = ${this.propName(prop.fieldNames[0])};`);
|
|
383
424
|
lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
|
|
384
425
|
lines.push(`${padding} }`);
|
|
@@ -420,7 +461,7 @@ export class EntityComparator {
|
|
|
420
461
|
lines.push(` for (let k in result) { if (Object.hasOwn(result, k) && !mapped[k] && ret[k] === undefined${knownKeysGuard}) ret[k] = result[k]; }`);
|
|
421
462
|
const code = `// compiled mapper for entity ${meta.className}\n` +
|
|
422
463
|
`return function(result) {\n const ret = {};\n${lines.join('\n')}\n return ret;\n}`;
|
|
423
|
-
const fnKey = `resultMapper-${meta.
|
|
464
|
+
const fnKey = `resultMapper-${meta.compiledName}`;
|
|
424
465
|
const resultMapper = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
|
|
425
466
|
this.#mappers.set(meta, resultMapper);
|
|
426
467
|
return resultMapper;
|
|
@@ -499,11 +540,22 @@ export class EntityComparator {
|
|
|
499
540
|
}
|
|
500
541
|
return true;
|
|
501
542
|
}
|
|
543
|
+
// a same-named property of a different kind in another polymorphic variant applies only to its own variants
|
|
544
|
+
const guard = (childProp, code) => {
|
|
545
|
+
const variants = prop.targetMeta?.polymorphs?.filter(m => m.properties[childProp.embedded[1]]) ?? [];
|
|
546
|
+
const owners = variants.filter(m => Utils.isSameKind(m.properties[childProp.embedded[1]], childProp));
|
|
547
|
+
if (owners.length === variants.length) {
|
|
548
|
+
return code;
|
|
549
|
+
}
|
|
550
|
+
const discriminator = `entity${[...path, prop.targetMeta.discriminatorColumn].map(k => this.wrap(k)).join('')}`;
|
|
551
|
+
const conds = owners.map(m => `${discriminator} == ${JSON.stringify(m.discriminatorValue)}`);
|
|
552
|
+
return `${padding} if (${conds.join(' || ')}) {\n${code}\n${padding} }`;
|
|
553
|
+
};
|
|
554
|
+
const childProps = meta.props.filter(p => p.embedded?.[0] === prop.name &&
|
|
555
|
+
// object for JSON embeddable
|
|
556
|
+
(p.object || p.persist !== false));
|
|
502
557
|
ret +=
|
|
503
|
-
|
|
504
|
-
.filter(p => p.embedded?.[0] === prop.name &&
|
|
505
|
-
// object for JSON embeddable
|
|
506
|
-
(p.object || p.persist !== false))
|
|
558
|
+
childProps
|
|
507
559
|
.map(childProp => {
|
|
508
560
|
const childDataKey = meta.embeddable || prop.object ? dataKey + this.wrap(childProp.embedded[1]) : this.wrap(childProp.name);
|
|
509
561
|
const childEntityKey = [...path, childProp.embedded[1]].map(k => this.wrap(k)).join('');
|
|
@@ -526,6 +578,7 @@ export class EntityComparator {
|
|
|
526
578
|
}
|
|
527
579
|
return `${padding} if (${childCond}) ret${childDataKey} = clone(entity${childEntityKey});`;
|
|
528
580
|
})
|
|
581
|
+
.map((code, i) => guard(childProps[i], code))
|
|
529
582
|
.join('\n') + `\n`;
|
|
530
583
|
if (this.shouldSerialize(prop, dataKey)) {
|
|
531
584
|
return `${ret + padding} ret${dataKey} = cloneEmbeddable(ret${dataKey});\n${padding}}`;
|
|
@@ -608,9 +661,24 @@ export class EntityComparator {
|
|
|
608
661
|
ret += ` if (discriminator !== undefined) break;\n`;
|
|
609
662
|
ret += ` __cls${level} = Object.getPrototypeOf(__cls${level});\n`;
|
|
610
663
|
ret += ` }\n`;
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
664
|
+
if (prop.targetKey) {
|
|
665
|
+
// the FK holds the `targetKey` value in the format of the actual target's column
|
|
666
|
+
const targetKey = prop.targetKey;
|
|
667
|
+
const targetKeyValueKey = `targetKeyValue_${this.safeKey(prop.name)}`;
|
|
668
|
+
context.set(targetKeyValueKey, (val) => {
|
|
669
|
+
const customType = val.__meta.properties[targetKey]?.customType;
|
|
670
|
+
const value = val[targetKey];
|
|
671
|
+
return customType
|
|
672
|
+
? customType.convertToDatabaseValue(value, this.#platform, { mode: 'serialization' })
|
|
673
|
+
: value;
|
|
674
|
+
});
|
|
675
|
+
ret += ` const pk = ${targetKeyValueKey}(val${level});\n`;
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
ret += ` const pk = val${level}?.__helper?.__identifier && !val${level}?.__helper?.hasPrimaryKey()\n`;
|
|
679
|
+
ret += ` ? val${level}.__helper.__identifier\n`;
|
|
680
|
+
ret += ` : toArray(val${level}?.__helper?.getPrimaryKey(true));\n`;
|
|
681
|
+
}
|
|
614
682
|
ret += ` ret${dataKey} = new PolymorphicRef(discriminator, pk);\n`;
|
|
615
683
|
ret += ` }\n`;
|
|
616
684
|
}
|
|
@@ -624,8 +692,8 @@ export class EntityComparator {
|
|
|
624
692
|
ret += ` } else if (typeof entity${entityKey} !== 'undefined') {\n`;
|
|
625
693
|
ret += ` const val${level} = entity${entityKey}${unwrap};\n`;
|
|
626
694
|
if (targetProp?.customType) {
|
|
627
|
-
//
|
|
628
|
-
const convertorKey = this.registerCustomType(targetProp, context);
|
|
695
|
+
// keyed by the relation name to avoid clashing with a same-named owner prop, the relation itself lacks the custom type with an explicit `columnType`
|
|
696
|
+
const convertorKey = this.registerCustomType({ ...prop, customType: targetProp.customType }, context);
|
|
629
697
|
ret += ` ret${dataKey} = convertToDatabaseValue_${convertorKey}(val${level}${targetAccess});\n`;
|
|
630
698
|
}
|
|
631
699
|
else {
|
|
@@ -688,7 +756,7 @@ export class EntityComparator {
|
|
|
688
756
|
lines.push(`}`);
|
|
689
757
|
const code = `// compiled comparator for entity ${meta.className}\n` +
|
|
690
758
|
`return function(last, current, options) {\n const diff = {};\n${lines.join('\n')}\n return diff;\n}`;
|
|
691
|
-
const fnKey = `comparator-${meta.
|
|
759
|
+
const fnKey = `comparator-${meta.compiledName}`;
|
|
692
760
|
const comparator = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
|
|
693
761
|
this.#comparators.set(meta, comparator);
|
|
694
762
|
return comparator;
|
package/utils/QueryHelper.d.ts
CHANGED
|
@@ -35,11 +35,19 @@ export declare class QueryHelper {
|
|
|
35
35
|
*/
|
|
36
36
|
static liftGroupOperators<T extends object>(where: Dictionary, meta: EntityMetadata<T>, metadata: MetadataStorage, key?: string): string | undefined;
|
|
37
37
|
static inlinePrimaryKeyObjects<T extends object>(where: Dictionary, meta: EntityMetadata<T>, metadata: MetadataStorage, key?: string): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* A bare value on a polymorphic relation would be compared to the discriminator column, so PK values are turned
|
|
40
|
+
* into a PK condition instead, e.g. `1` becomes `{ id: 1 }` and `[{ id: 1 }, { id: 2 }]` becomes `{ id: { $in: [1, 2] } }`.
|
|
41
|
+
* Null checks and `[type, id]` tuples keep their meaning.
|
|
42
|
+
*/
|
|
43
|
+
private static inlinePolymorphicPrimaryKeys;
|
|
38
44
|
/** Copies the plain object and array structure of the condition, keeping the leaf values (e.g. entities) by reference. */
|
|
39
45
|
private static cloneWhere;
|
|
40
46
|
static processWhere<T extends object>(options: ProcessWhereOptions<T>): FilterQuery<T>;
|
|
41
47
|
/** @internal Splits a polymorphic target key like `imageable[Article]` into the property and target class names. */
|
|
42
48
|
static splitPolymorphicKey(key: string): [string, string?];
|
|
49
|
+
/** @internal Resolves the targets of a polymorphic target key like `imageable[Article]` or `imageable[Product,Article]`. */
|
|
50
|
+
static findTargetMetas(prop: EntityProperty | null | undefined, key: string): EntityMetadata[];
|
|
43
51
|
/** @internal Resolves the target entity of a condition key, including polymorphic target keys like `imageable[Article]`. */
|
|
44
52
|
static findTargetMeta(prop: EntityProperty | null | undefined, key: string): EntityMetadata | undefined;
|
|
45
53
|
/**
|
|
@@ -48,6 +56,12 @@ export declare class QueryHelper {
|
|
|
48
56
|
* when more targets match. The first target alone keeps the regular relation join.
|
|
49
57
|
*/
|
|
50
58
|
private static splitPolymorphicConditions;
|
|
59
|
+
/**
|
|
60
|
+
* Routes ordering by properties of polymorphic to-one relations to the targets that define them, e.g.
|
|
61
|
+
* `{ imageable: { name: 'asc' } }` becomes `{ 'imageable[Product,Article]': { name: 'asc' } }`, ordered by
|
|
62
|
+
* the first non-null value. Nested relations and formulas are ordered via the first target defining them.
|
|
63
|
+
*/
|
|
64
|
+
private static splitPolymorphicOrderBy;
|
|
51
65
|
/** Property names used in a condition, including those nested in group operators. */
|
|
52
66
|
private static collectConditionKeys;
|
|
53
67
|
static getActiveFilters<T>(meta: EntityMetadata<T>, options: FilterOptions | undefined, filters: Dictionary<FilterDef>): FilterDef[];
|
package/utils/QueryHelper.js
CHANGED
|
@@ -3,8 +3,8 @@ import { DANGEROUS_PROPERTY_NAMES, Utils } from './Utils.js';
|
|
|
3
3
|
import { ARRAY_OPERATORS, GroupOperator, JSON_KEY_OPERATORS, ReferenceKind } from '../enums.js';
|
|
4
4
|
import { JsonType } from '../types/JsonType.js';
|
|
5
5
|
import { helper } from '../entity/wrap.js';
|
|
6
|
-
import { isRaw, Raw } from './RawQueryFragment.js';
|
|
7
|
-
import { MetadataError } from '../errors.js';
|
|
6
|
+
import { isRaw, isSubquery, Raw } from './RawQueryFragment.js';
|
|
7
|
+
import { MetadataError, ValidationError } from '../errors.js';
|
|
8
8
|
/** @internal */
|
|
9
9
|
export class QueryHelper {
|
|
10
10
|
static SUPPORTED_OPERATORS = ['>', '<', '<=', '>=', '!', '!='];
|
|
@@ -102,10 +102,7 @@ export class QueryHelper {
|
|
|
102
102
|
for (const k of keys) {
|
|
103
103
|
const value = where[k];
|
|
104
104
|
const prop = meta.properties[k];
|
|
105
|
-
|
|
106
|
-
// participate in the standard single-column FK expansion. Query by discriminator
|
|
107
|
-
// column directly instead, e.g. { likeableType: 'post', likeableId: 1 }.
|
|
108
|
-
if (!prop || ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) || prop.polymorphic) {
|
|
105
|
+
if (!prop || ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
|
|
109
106
|
continue;
|
|
110
107
|
}
|
|
111
108
|
const op = this.liftGroupOperators(value, prop.targetMeta, metadata, k);
|
|
@@ -130,7 +127,7 @@ export class QueryHelper {
|
|
|
130
127
|
if (Array.isArray(where)) {
|
|
131
128
|
where.forEach((item, i) => {
|
|
132
129
|
if (this.inlinePrimaryKeyObjects(item, meta, metadata, key)) {
|
|
133
|
-
where[i] = Utils.getPrimaryKeyValues(item, meta,
|
|
130
|
+
where[i] = Utils.getPrimaryKeyValues(item, meta, true);
|
|
134
131
|
}
|
|
135
132
|
});
|
|
136
133
|
}
|
|
@@ -160,6 +157,9 @@ export class QueryHelper {
|
|
|
160
157
|
Object.keys(where).forEach(k => {
|
|
161
158
|
const prop = meta.properties[QueryHelper.splitPolymorphicKey(k)[0]];
|
|
162
159
|
const meta2 = metadata.find(QueryHelper.findTargetMeta(prop, k)?.class) || meta;
|
|
160
|
+
if (prop?.polymorphic) {
|
|
161
|
+
where[k] = this.inlinePolymorphicPrimaryKeys(where[k], meta2, prop);
|
|
162
|
+
}
|
|
163
163
|
if (this.inlinePrimaryKeyObjects(where[k], meta2, metadata, k)) {
|
|
164
164
|
// Skip the PK collapse when an owning M:1/1:1 relation's FK column count does not match
|
|
165
165
|
// the target's PK column count (e.g. FK references target PK + an extra unique column).
|
|
@@ -177,6 +177,37 @@ export class QueryHelper {
|
|
|
177
177
|
});
|
|
178
178
|
return false;
|
|
179
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* A bare value on a polymorphic relation would be compared to the discriminator column, so PK values are turned
|
|
182
|
+
* into a PK condition instead, e.g. `1` becomes `{ id: 1 }` and `[{ id: 1 }, { id: 2 }]` becomes `{ id: { $in: [1, 2] } }`.
|
|
183
|
+
* Null checks and `[type, id]` tuples keep their meaning.
|
|
184
|
+
*/
|
|
185
|
+
static inlinePolymorphicPrimaryKeys(value, meta, prop) {
|
|
186
|
+
const pk = meta.primaryKeys[0];
|
|
187
|
+
const isScalar = (v) => v != null && !Array.isArray(v) && Utils.isPrimaryKey(v);
|
|
188
|
+
const isPrimaryKeyObject = (v) => Utils.isPlainObject(v) && Utils.getObjectKeysSize(v) === 1 && pk in v;
|
|
189
|
+
// lists of plain values are how `[type, id]` tuples get normalized (`{ $in: [type, id] }`), so only PK objects are inlined
|
|
190
|
+
const isList = (v) => Array.isArray(v) && v.length > 0 && v.every(isPrimaryKeyObject);
|
|
191
|
+
let cond;
|
|
192
|
+
if (isScalar(value)) {
|
|
193
|
+
cond = value;
|
|
194
|
+
}
|
|
195
|
+
else if (isList(value)) {
|
|
196
|
+
cond = { $in: value.map(v => v[pk]) };
|
|
197
|
+
}
|
|
198
|
+
else if (Utils.isPlainObject(value) &&
|
|
199
|
+
Utils.hasObjectKeys(value) &&
|
|
200
|
+
Object.entries(value).every(([op, v]) => Utils.isOperator(op) && !(op in GroupOperator) && op !== '$not' && (isScalar(v) || isList(v)))) {
|
|
201
|
+
cond = Object.fromEntries(Object.entries(value).map(([op, v]) => [op, isList(v) ? v.map(i => i[pk]) : v]));
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
if (meta.compositePK) {
|
|
207
|
+
throw new ValidationError(`Polymorphic relation ${prop.name} targets entities with a composite primary key, use PK objects or entity references in the condition instead.`);
|
|
208
|
+
}
|
|
209
|
+
return { [pk]: cond };
|
|
210
|
+
}
|
|
180
211
|
/** Copies the plain object and array structure of the condition, keeping the leaf values (e.g. entities) by reference. */
|
|
181
212
|
static cloneWhere(where) {
|
|
182
213
|
if (Array.isArray(where)) {
|
|
@@ -231,8 +262,10 @@ export class QueryHelper {
|
|
|
231
262
|
if (!Utils.isPlainObject(where)) {
|
|
232
263
|
return where;
|
|
233
264
|
}
|
|
234
|
-
if (meta
|
|
235
|
-
where =
|
|
265
|
+
if (meta) {
|
|
266
|
+
where = (options.type === 'orderBy'
|
|
267
|
+
? QueryHelper.splitPolymorphicOrderBy(meta, where)
|
|
268
|
+
: QueryHelper.splitPolymorphicConditions(meta, where));
|
|
236
269
|
}
|
|
237
270
|
return Utils.getObjectQueryKeys(where).reduce((o, key) => {
|
|
238
271
|
let value = where[key];
|
|
@@ -280,6 +313,13 @@ export class QueryHelper {
|
|
|
280
313
|
o[key] = { [op]: value };
|
|
281
314
|
return o;
|
|
282
315
|
}
|
|
316
|
+
// `$all` items are matched like separate `$some` payloads, so they need the same processing
|
|
317
|
+
if (key === '$all' && Array.isArray(value)) {
|
|
318
|
+
o[key] = value.map(item => Utils.isPlainObject(item)
|
|
319
|
+
? QueryHelper.processWhere({ ...options, where: item, entityName, root: false })
|
|
320
|
+
: item);
|
|
321
|
+
return o;
|
|
322
|
+
}
|
|
283
323
|
if (Utils.isPlainObject(value)) {
|
|
284
324
|
const targetMeta = customExpression ? undefined : QueryHelper.findTargetMeta(prop, key);
|
|
285
325
|
o[key] = QueryHelper.processWhere({
|
|
@@ -297,13 +337,18 @@ export class QueryHelper {
|
|
|
297
337
|
}
|
|
298
338
|
/** @internal Splits a polymorphic target key like `imageable[Article]` into the property and target class names. */
|
|
299
339
|
static splitPolymorphicKey(key) {
|
|
300
|
-
const match = /^(\w+)\[(\w+)]$/.exec(key);
|
|
340
|
+
const match = /^(\w+)\[([\w,]+)]$/.exec(key);
|
|
301
341
|
return match ? [match[1], match[2]] : [key];
|
|
302
342
|
}
|
|
343
|
+
/** @internal Resolves the targets of a polymorphic target key like `imageable[Article]` or `imageable[Product,Article]`. */
|
|
344
|
+
static findTargetMetas(prop, key) {
|
|
345
|
+
const [, targetNames] = QueryHelper.splitPolymorphicKey(key);
|
|
346
|
+
const targets = targetNames?.split(',').map(name => prop?.polymorphTargets?.find(t => t.className === name));
|
|
347
|
+
return targets?.every(Boolean) ? targets : [];
|
|
348
|
+
}
|
|
303
349
|
/** @internal Resolves the target entity of a condition key, including polymorphic target keys like `imageable[Article]`. */
|
|
304
350
|
static findTargetMeta(prop, key) {
|
|
305
|
-
|
|
306
|
-
return targetName ? prop?.polymorphTargets?.find(t => t.className === targetName) : prop?.targetMeta;
|
|
351
|
+
return QueryHelper.splitPolymorphicKey(key)[1] ? QueryHelper.findTargetMetas(prop, key)[0] : prop?.targetMeta;
|
|
307
352
|
}
|
|
308
353
|
/**
|
|
309
354
|
* Routes conditions on polymorphic to-one relations to the targets that define all the queried properties,
|
|
@@ -342,6 +387,34 @@ export class QueryHelper {
|
|
|
342
387
|
}
|
|
343
388
|
return ret;
|
|
344
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* Routes ordering by properties of polymorphic to-one relations to the targets that define them, e.g.
|
|
392
|
+
* `{ imageable: { name: 'asc' } }` becomes `{ 'imageable[Product,Article]': { name: 'asc' } }`, ordered by
|
|
393
|
+
* the first non-null value. Nested relations and formulas are ordered via the first target defining them.
|
|
394
|
+
*/
|
|
395
|
+
static splitPolymorphicOrderBy(meta, orderBy) {
|
|
396
|
+
const ret = {};
|
|
397
|
+
for (const key of Utils.getObjectQueryKeys(orderBy)) {
|
|
398
|
+
const prop = meta.properties[key];
|
|
399
|
+
const value = orderBy[key];
|
|
400
|
+
if (!prop?.polymorphTargets ||
|
|
401
|
+
![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) ||
|
|
402
|
+
!Utils.isPlainObject(value)) {
|
|
403
|
+
ret[key] = value;
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
for (const field of Utils.getObjectQueryKeys(value)) {
|
|
407
|
+
const found = prop.polymorphTargets.filter(target => field in target.properties);
|
|
408
|
+
const sharedPK = found.length === prop.polymorphTargets.length && found.every(target => target.primaryKeys.includes(field));
|
|
409
|
+
const single = Utils.isPlainObject(value[field]) || found.some(target => target.properties[field].formula);
|
|
410
|
+
const targets = found.length === 0 ? [prop.targetMeta] : single ? [found[0]] : found;
|
|
411
|
+
// shared PKs are ordered by the FK column without a join
|
|
412
|
+
const targetKey = sharedPK ? key : `${key}[${targets.map(target => target.className).join(',')}]`;
|
|
413
|
+
ret[targetKey] = { ...ret[targetKey], [field]: value[field] };
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return ret;
|
|
417
|
+
}
|
|
345
418
|
/** Property names used in a condition, including those nested in group operators. */
|
|
346
419
|
static collectConditionKeys(payload) {
|
|
347
420
|
return Object.keys(payload).flatMap(k => {
|
|
@@ -480,7 +553,8 @@ export class QueryHelper {
|
|
|
480
553
|
if (Array.isArray(cond) && !(key && ARRAY_OPERATORS.includes(key))) {
|
|
481
554
|
return cond.map(v => QueryHelper.processCustomType(prop, v, platform, key, fromQuery));
|
|
482
555
|
}
|
|
483
|
-
|
|
556
|
+
// a query builder renders as a sub-query, not as a value of the property type
|
|
557
|
+
if (isRaw(cond) || isSubquery(cond)) {
|
|
484
558
|
return cond;
|
|
485
559
|
}
|
|
486
560
|
return prop.customType.convertToDatabaseValue(cond, platform, { fromQuery, key, mode: 'query' });
|
|
@@ -555,6 +629,9 @@ export class QueryHelper {
|
|
|
555
629
|
if (expanded !== w) {
|
|
556
630
|
where[k] = expanded;
|
|
557
631
|
}
|
|
632
|
+
else if (Array.isArray(w)) {
|
|
633
|
+
where[k] = w.map(expandItem);
|
|
634
|
+
}
|
|
558
635
|
else if (Utils.isPlainObject(w)) {
|
|
559
636
|
for (const op of Object.keys(w)) {
|
|
560
637
|
if (!Utils.isOperator(op, false)) {
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import type { AnyString, Dictionary, EntityKey } from '../typings.js';
|
|
1
|
+
import type { AnyString, Dictionary, EntityKey, Subquery } from '../typings.js';
|
|
2
2
|
declare const rawFragmentSymbolBrand: unique symbol;
|
|
3
3
|
/** Branded symbol type used as a unique key for tracking raw SQL fragments in object properties. */
|
|
4
4
|
export type RawQueryFragmentSymbol = symbol & {
|
|
5
5
|
readonly [rawFragmentSymbolBrand]: true;
|
|
6
6
|
};
|
|
7
|
+
/** Prototype brand of query builders usable as sub-queries; a symbol so JSON payloads cannot carry it, `Symbol.for` so package copies agree. */
|
|
8
|
+
export declare const SUBQUERY_BRAND: unique symbol;
|
|
7
9
|
/** Checks whether the given value is a `RawQueryFragment` instance. */
|
|
8
10
|
export declare function isRaw(value: unknown): value is RawQueryFragment;
|
|
11
|
+
/** Checks whether the given value is a query builder usable as a sub-query (its prototype carries the `SUBQUERY_BRAND`). */
|
|
12
|
+
export declare function isSubquery(value: unknown): value is Subquery;
|
|
9
13
|
/** Represents a raw SQL fragment with optional parameters, usable as both a value and an object key via Symbol coercion. */
|
|
10
14
|
export declare class RawQueryFragment<Alias extends string = string> {
|
|
11
15
|
#private;
|