@mikro-orm/core 7.3.0-dev.1 → 7.3.0-dev.11
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 +4 -0
- package/entity/EntityFactory.d.ts +1 -0
- package/entity/EntityFactory.js +32 -3
- package/entity/EntityLoader.js +7 -6
- package/entity/WrappedEntity.js +6 -1
- package/entity/defineEntity.d.ts +8 -1
- package/entity/defineEntity.js +8 -0
- package/errors.d.ts +7 -0
- package/errors.js +15 -0
- package/hydration/ObjectHydrator.js +2 -2
- package/metadata/MetadataDiscovery.js +27 -5
- package/metadata/MetadataStorage.d.ts +7 -0
- package/metadata/MetadataStorage.js +27 -0
- package/metadata/types.d.ts +12 -0
- package/package.json +1 -1
- package/platforms/Platform.d.ts +2 -0
- package/platforms/Platform.js +18 -0
- package/typings.d.ts +5 -1
- package/unit-of-work/ChangeSetComputer.js +20 -6
- package/unit-of-work/IdentityMap.d.ts +3 -2
- package/unit-of-work/IdentityMap.js +30 -7
- package/unit-of-work/UnitOfWork.js +25 -2
- package/utils/EntityComparator.js +43 -15
- package/utils/QueryHelper.d.ts +26 -0
- package/utils/QueryHelper.js +141 -11
- package/utils/TransactionManager.js +4 -0
- package/utils/Utils.js +1 -1
package/EntityManager.js
CHANGED
|
@@ -789,6 +789,10 @@ export class EntityManager {
|
|
|
789
789
|
.getHydrator(this.metadata)
|
|
790
790
|
.hydrate(ref, helper(ref).__meta, data, em.#entityFactory, 'full', false, false);
|
|
791
791
|
Utils.merge(helper(ref).__originalEntityData, this.#comparator.prepareEntity(e));
|
|
792
|
+
// index a refreshed `targetKey` value, `getByKey` ignores the old one as stale
|
|
793
|
+
if (helper(ref).__meta.root.targetKeys) {
|
|
794
|
+
em.#unitOfWork.getIdentityMap().store(ref);
|
|
795
|
+
}
|
|
792
796
|
found ||= ref === entity;
|
|
793
797
|
}
|
|
794
798
|
if (!found) {
|
|
@@ -51,6 +51,7 @@ export declare class EntityFactory {
|
|
|
51
51
|
assignDefaultValues<T extends object>(entity: T, meta: EntityMetadata<T>, onCreateOnly?: boolean): void;
|
|
52
52
|
private hydrate;
|
|
53
53
|
private findEntity;
|
|
54
|
+
private findEntityByPrimaryKey;
|
|
54
55
|
private processDiscriminatorColumn;
|
|
55
56
|
/**
|
|
56
57
|
* denormalize PK to value required by driver (e.g. ObjectId)
|
package/entity/EntityFactory.js
CHANGED
|
@@ -55,6 +55,10 @@ export class EntityFactory {
|
|
|
55
55
|
Utils.dropUndefinedProperties(data);
|
|
56
56
|
this.mergeData(meta2, exists, data, options);
|
|
57
57
|
wrapped.__processing = false;
|
|
58
|
+
// index a merged `targetKey` value, `getByKey` ignores the old one as stale
|
|
59
|
+
if (meta2.root.targetKeys) {
|
|
60
|
+
this.unitOfWork.getIdentityMap().store(exists);
|
|
61
|
+
}
|
|
58
62
|
wrapped.__initialized ||= !!options.initialized;
|
|
59
63
|
if (wrapped.isInitialized()) {
|
|
60
64
|
if (!wasInitialized) {
|
|
@@ -232,9 +236,9 @@ export class EntityFactory {
|
|
|
232
236
|
options.convertCustomTypes ??= true;
|
|
233
237
|
const meta = this.#metadata.get(entityName);
|
|
234
238
|
const schema = this.#driver.getSchemaName(meta, options);
|
|
235
|
-
// Handle alternate key lookup
|
|
236
|
-
if (options.key) {
|
|
237
|
-
const value =
|
|
239
|
+
// Handle alternate key lookup, a `key` pointing at the single PK is a plain PK reference
|
|
240
|
+
if (options.key && !(meta.primaryKeys.length === 1 && meta.primaryKeys[0] === options.key)) {
|
|
241
|
+
const value = Array.isArray(id) ? id[0] : Utils.isPlainObject(id) ? id[options.key] : id;
|
|
238
242
|
const exists = this.unitOfWork.getByKey(entityName, options.key, value, schema, options.convertCustomTypes);
|
|
239
243
|
if (exists) {
|
|
240
244
|
return exists;
|
|
@@ -242,6 +246,8 @@ export class EntityFactory {
|
|
|
242
246
|
// Create entity stub - storeByKey will set the alternate key property and store in identity map
|
|
243
247
|
const entity = this.create(entityName, {}, { ...options, initialized: false });
|
|
244
248
|
this.unitOfWork.storeByKey(entity, options.key, value, schema, options.convertCustomTypes);
|
|
249
|
+
// snapshot the stub like a PK reference, so flush treats it as existing instead of inserting it
|
|
250
|
+
helper(entity).__originalEntityData = this.#comparator.prepareEntity(entity);
|
|
245
251
|
return entity;
|
|
246
252
|
}
|
|
247
253
|
if (meta.simplePK) {
|
|
@@ -310,6 +316,12 @@ export class EntityFactory {
|
|
|
310
316
|
if (options.merge && !options.newEntity) {
|
|
311
317
|
this.#hydrator.hydrateReference(entity, meta, data, this, options.convertCustomTypes, options.schema, options.parentSchema);
|
|
312
318
|
this.unitOfWork.register(entity);
|
|
319
|
+
// relations hydrated together with this entity (joined strategy) resolve it via `targetKey` before it is hydrated
|
|
320
|
+
for (const key of meta.root.targetKeys ?? []) {
|
|
321
|
+
if (data[key] != null) {
|
|
322
|
+
this.unitOfWork.storeByKey(entity, key, data[key], schema, options.convertCustomTypes);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
313
325
|
}
|
|
314
326
|
if (options.initialized) {
|
|
315
327
|
EntityHelper.ensurePropagation(entity);
|
|
@@ -379,6 +391,23 @@ export class EntityFactory {
|
|
|
379
391
|
}
|
|
380
392
|
findEntity(data, meta, options) {
|
|
381
393
|
const schema = this.#driver.getSchemaName(meta, options);
|
|
394
|
+
const exists = this.findEntityByPrimaryKey(data, meta, schema, options);
|
|
395
|
+
// a new entity must not adopt a reference stub, that one is never inserted
|
|
396
|
+
if (exists || Array.isArray(data) || options.newEntity) {
|
|
397
|
+
return exists;
|
|
398
|
+
}
|
|
399
|
+
// a reference created via `targetKey` is only known by that key until the entity is loaded
|
|
400
|
+
for (const key of meta.root.targetKeys ?? []) {
|
|
401
|
+
if (data[key] != null) {
|
|
402
|
+
const ref = this.unitOfWork.getByKey(meta.class, key, data[key], schema, options.convertCustomTypes);
|
|
403
|
+
if (ref && !helper(ref).__initialized) {
|
|
404
|
+
return ref;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return undefined;
|
|
409
|
+
}
|
|
410
|
+
findEntityByPrimaryKey(data, meta, schema, options) {
|
|
382
411
|
if (meta.simplePK) {
|
|
383
412
|
return this.unitOfWork.getById(meta.class, data[meta.primaryKeys[0]], schema);
|
|
384
413
|
}
|
package/entity/EntityLoader.js
CHANGED
|
@@ -197,9 +197,9 @@ export class EntityLoader {
|
|
|
197
197
|
this.initializeCollections(filtered, prop, field, items, customOrder, isPartial, hasLimit);
|
|
198
198
|
return items;
|
|
199
199
|
}
|
|
200
|
-
async populateScalar(meta, filtered, options) {
|
|
201
|
-
const pk = Utils.getPrimaryKeyHash(meta.primaryKeys);
|
|
202
|
-
const ids = Utils.unique(filtered.map(e => Utils.getPrimaryKeyValues(e, meta, true)));
|
|
200
|
+
async populateScalar(meta, filtered, options, key) {
|
|
201
|
+
const pk = (key ?? Utils.getPrimaryKeyHash(meta.primaryKeys));
|
|
202
|
+
const ids = Utils.unique(filtered.map(e => (key ? e[key] : Utils.getPrimaryKeyValues(e, meta, true))));
|
|
203
203
|
const where = this.mergePrimaryCondition(ids, pk, options, meta, this.#metadata, this.#driver.getPlatform());
|
|
204
204
|
const { filters, convertCustomTypes, lockMode, strategy, populateWhere, connectionType, logging, fields } = options;
|
|
205
205
|
await this.#em.find(meta.class, where, {
|
|
@@ -221,7 +221,8 @@ export class EntityLoader {
|
|
|
221
221
|
const needsFkLoad = [];
|
|
222
222
|
for (const entity of entities) {
|
|
223
223
|
const refValue = entity[prop.name];
|
|
224
|
-
|
|
224
|
+
// a `targetKey` reference is identified by its key, it might not have the PK yet
|
|
225
|
+
if (refValue && helper(refValue).getTargetKeyValue(prop.targetKey) != null) {
|
|
225
226
|
if ((ref && !options.refresh) || // :ref hint - already have reference
|
|
226
227
|
(!ref && helper(refValue).__initialized && !options.refresh) // already loaded
|
|
227
228
|
) {
|
|
@@ -246,7 +247,7 @@ export class EntityLoader {
|
|
|
246
247
|
if (!ref) {
|
|
247
248
|
for (const entity of needsFkLoad) {
|
|
248
249
|
const refValue = entity[prop.name];
|
|
249
|
-
if (refValue && helper(refValue).
|
|
250
|
+
if (refValue && helper(refValue).getTargetKeyValue(prop.targetKey) != null) {
|
|
250
251
|
toPopulate.push(entity);
|
|
251
252
|
}
|
|
252
253
|
}
|
|
@@ -268,7 +269,7 @@ export class EntityLoader {
|
|
|
268
269
|
const allItems = [];
|
|
269
270
|
await Promise.all([...groups].map(async ([discriminator, children]) => {
|
|
270
271
|
const targetMeta = this.#metadata.find(prop.discriminatorMap[discriminator]);
|
|
271
|
-
await this.populateScalar(targetMeta, children, options);
|
|
272
|
+
await this.populateScalar(targetMeta, children, options, prop.targetKey);
|
|
272
273
|
allItems.push(...children);
|
|
273
274
|
}));
|
|
274
275
|
return allItems;
|
package/entity/WrappedEntity.js
CHANGED
|
@@ -83,7 +83,12 @@ export class WrappedEntity {
|
|
|
83
83
|
if (!this.__em) {
|
|
84
84
|
throw ValidationError.entityNotManaged(this.entity);
|
|
85
85
|
}
|
|
86
|
-
|
|
86
|
+
// a reference known only by its `targetKey` has no PK, so look it up by the key instead
|
|
87
|
+
const key = this.hasPrimaryKey()
|
|
88
|
+
? undefined
|
|
89
|
+
: this.__meta.root.targetKeys?.find(k => this.entity[k] != null);
|
|
90
|
+
const where = key ? { [key]: this.entity[key] } : this.entity;
|
|
91
|
+
return this.__em.findOne(this.entity.constructor, where, {
|
|
87
92
|
...options,
|
|
88
93
|
refresh: true,
|
|
89
94
|
schema: this.__schema,
|
package/entity/defineEntity.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EntityManager } from '../EntityManager.js';
|
|
2
|
-
import type { ColumnType, PropertyOptions, ReferenceOptions, EnumOptions, EmbeddedOptions, ManyToOneOptions, OneToManyOptions, OneToOneOptions, ManyToManyOptions, IndexColumnOptions } from '../metadata/types.js';
|
|
2
|
+
import type { ColumnType, PropertyOptions, ReferenceOptions, EnumOptions, EmbeddedOptions, ManyToOneOptions, OneToManyOptions, OneToOneOptions, ManyToManyOptions, IndexColumnOptions, SequenceOptions } from '../metadata/types.js';
|
|
3
3
|
import type { AnyString, GeneratedColumnCallback, Constructor, CheckCallback, FilterQuery, EntityName, Dictionary, EntityMetadata, PrimaryKeyProp, EntityRepositoryType, Hidden, Opt, Primary, EntityClass, EntitySchemaWithMeta, InferEntity, MaybeReturnType, Ref, LazyRef, IndexCallback, TriggerCallback, FormulaCallback, EntityCtor, IsNever, IWrappedEntity, DefineConfig, Config, MaybePromise, IndexHints, OptionalProps, ExtractDefineEntityProperties } from '../typings.js';
|
|
4
4
|
import type { Raw } from '../utils/RawQueryFragment.js';
|
|
5
5
|
import type { ScalarReference } from './Reference.js';
|
|
@@ -130,6 +130,7 @@ export interface PropertyChain<in out Value, in out Options> {
|
|
|
130
130
|
unique(unique?: boolean): PropertyChain<Value, Options>;
|
|
131
131
|
comment(comment: string): PropertyChain<Value, Options>;
|
|
132
132
|
collation(collation: string): PropertyChain<Value, Options>;
|
|
133
|
+
sequence(sequence: SequenceOptions | number): PropertyChain<Value, Options>;
|
|
133
134
|
accessor(accessor?: string | boolean): PropertyChain<Value, Options>;
|
|
134
135
|
eager(eager?: boolean): HasKind<Options, 'm:1' | '1:m' | '1:1' | 'm:n'> extends true ? PropertyChain<Value, Options> : never;
|
|
135
136
|
cascade(...cascade: Cascade[]): HasKind<Options, 'm:1' | '1:m' | '1:1' | 'm:n'> extends true ? PropertyChain<Value, Options> : never;
|
|
@@ -486,6 +487,12 @@ export declare class UniversalPropertyOptionsBuilder<in out Value, in out Option
|
|
|
486
487
|
* Emits a `COLLATE` clause on the column definition; the accepted collation names are dialect-specific.
|
|
487
488
|
*/
|
|
488
489
|
collation(collation: string): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
|
|
490
|
+
/**
|
|
491
|
+
* Set the start value and increment of an autoincrement column. (SQL only)
|
|
492
|
+
* A number is a shorthand for `{ startWith }`.
|
|
493
|
+
* Applied only when the column is created (and by `orm.schema.clear()`), never diffed by the schema generator.
|
|
494
|
+
*/
|
|
495
|
+
sequence(sequence: SequenceOptions | number): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
|
|
489
496
|
/** mysql only */
|
|
490
497
|
extra(extra: string): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
|
|
491
498
|
/**
|
package/entity/defineEntity.js
CHANGED
|
@@ -323,6 +323,14 @@ export class UniversalPropertyOptionsBuilder {
|
|
|
323
323
|
collation(collation) {
|
|
324
324
|
return this.assignOptions({ collation });
|
|
325
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* Set the start value and increment of an autoincrement column. (SQL only)
|
|
328
|
+
* A number is a shorthand for `{ startWith }`.
|
|
329
|
+
* Applied only when the column is created (and by `orm.schema.clear()`), never diffed by the schema generator.
|
|
330
|
+
*/
|
|
331
|
+
sequence(sequence) {
|
|
332
|
+
return this.assignOptions({ sequence: typeof sequence === 'number' ? { startWith: sequence } : sequence });
|
|
333
|
+
}
|
|
326
334
|
/** mysql only */
|
|
327
335
|
extra(extra) {
|
|
328
336
|
return this.assignOptions({ extra });
|
package/errors.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export declare class ValidationError<T extends AnyEntity = AnyEntity> extends Er
|
|
|
10
10
|
static fromWrongPropertyType(entity: AnyEntity, property: string, expectedType: string, givenType: string, givenValue: string): ValidationError;
|
|
11
11
|
static fromWrongRepositoryType(entityName: string, repoType: string, method: string): ValidationError;
|
|
12
12
|
static fromMergeWithoutPK(meta: EntityMetadata): ValidationError;
|
|
13
|
+
static referenceWithoutPK(entity: AnyEntity): ValidationError;
|
|
13
14
|
static transactionRequired(): ValidationError;
|
|
14
15
|
static entityNotManaged(entity: AnyEntity): ValidationError;
|
|
15
16
|
static notEntity(owner: AnyEntity, prop: EntityProperty, data: any): ValidationError;
|
|
@@ -90,6 +91,12 @@ export declare class MetadataError<T extends AnyEntity = AnyEntity> extends Vali
|
|
|
90
91
|
static triggersNotSupportedByDriver(meta: EntityMetadata): MetadataError;
|
|
91
92
|
/** Thrown when row level security is declared on an entity using a driver that does not support it. */
|
|
92
93
|
static rowLevelSecurityNotSupportedByDriver(meta: EntityMetadata): MetadataError;
|
|
94
|
+
/** Thrown when the `sequence` option is declared on a property that is not autoincrement. */
|
|
95
|
+
static sequenceWithoutAutoincrement(meta: EntityMetadata, prop: EntityProperty): MetadataError;
|
|
96
|
+
/** Thrown when the `sequence` option is combined with an identity option list in `generated`. */
|
|
97
|
+
static sequenceWithIdentityOptions(meta: EntityMetadata, prop: EntityProperty): MetadataError;
|
|
98
|
+
/** Thrown when `sequence.incrementBy` is declared on a platform without a per-column increment. */
|
|
99
|
+
static sequenceIncrementNotSupported(meta: EntityMetadata, prop: EntityProperty, platform: string): MetadataError;
|
|
93
100
|
/** Thrown when row level security is declared on a non-root entity of an STI hierarchy. */
|
|
94
101
|
static rowLevelSecurityOnNonRootStiEntity(meta: EntityMetadata): MetadataError;
|
|
95
102
|
/** Thrown when two policies on the same entity are given the same explicit name. */
|
package/errors.js
CHANGED
|
@@ -28,6 +28,9 @@ export class ValidationError extends Error {
|
|
|
28
28
|
static fromMergeWithoutPK(meta) {
|
|
29
29
|
return new ValidationError(`You cannot merge entity '${meta.className}' without identifier!`);
|
|
30
30
|
}
|
|
31
|
+
static referenceWithoutPK(entity) {
|
|
32
|
+
return new ValidationError(`Entity '${entity.constructor.name}' is only known by an alternate key and has no primary key, load it first`, entity);
|
|
33
|
+
}
|
|
31
34
|
static transactionRequired() {
|
|
32
35
|
return new ValidationError('An open transaction is required for this operation');
|
|
33
36
|
}
|
|
@@ -293,6 +296,18 @@ export class MetadataError extends ValidationError {
|
|
|
293
296
|
static rowLevelSecurityNotSupportedByDriver(meta) {
|
|
294
297
|
return new MetadataError(`Entity ${meta.className} declares row level security which is not supported by the current driver. Row level security is only available with the PostgreSQL driver. To test a PostgreSQL app without a server, use the \`@mikro-orm/pglite\` driver instead of sqlite.`);
|
|
295
298
|
}
|
|
299
|
+
/** Thrown when the `sequence` option is declared on a property that is not autoincrement. */
|
|
300
|
+
static sequenceWithoutAutoincrement(meta, prop) {
|
|
301
|
+
return MetadataError.fromMessage(meta, prop, `defines the 'sequence' option, but is not an autoincrement property`);
|
|
302
|
+
}
|
|
303
|
+
/** Thrown when the `sequence` option is combined with an identity option list in `generated`. */
|
|
304
|
+
static sequenceWithIdentityOptions(meta, prop) {
|
|
305
|
+
return MetadataError.fromMessage(meta, prop, `defines the 'sequence' option together with identity options in 'generated', use only one of them`);
|
|
306
|
+
}
|
|
307
|
+
/** Thrown when `sequence.incrementBy` is declared on a platform without a per-column increment. */
|
|
308
|
+
static sequenceIncrementNotSupported(meta, prop, platform) {
|
|
309
|
+
return MetadataError.fromMessage(meta, prop, `defines 'sequence.incrementBy', but ${platform} does not support a per-column increment`);
|
|
310
|
+
}
|
|
296
311
|
/** Thrown when row level security is declared on a non-root entity of an STI hierarchy. */
|
|
297
312
|
static rowLevelSecurityOnNonRootStiEntity(meta) {
|
|
298
313
|
return new MetadataError(`Entity ${meta.className} declares row level security, but it is part of a single table inheritance hierarchy. Declare policies on the root entity ${meta.root.className} instead, as the whole hierarchy shares a single table.`);
|
|
@@ -207,7 +207,7 @@ export class ObjectHydrator extends Hydrator {
|
|
|
207
207
|
if (prop.customType?.ensureComparable(meta, prop)) {
|
|
208
208
|
registerCustomType(prop, this.safeKey(prop.name), 'convertToDatabaseValue', context);
|
|
209
209
|
ret.push(` if (data${dataKey} != null && typeof data${dataKey} !== 'object' && convertCustomTypes) {`);
|
|
210
|
-
ret.push(` data${dataKey} = convertToDatabaseValue_${this.safeKey(prop.name)}(entity${entityKey}.__helper.
|
|
210
|
+
ret.push(` data${dataKey} = convertToDatabaseValue_${this.safeKey(prop.name)}(entity${entityKey}.__helper.getTargetKeyValue(${prop.targetKey ? this.quote(prop.targetKey) : ''}));`);
|
|
211
211
|
ret.push(` }`);
|
|
212
212
|
}
|
|
213
213
|
return ret;
|
|
@@ -404,7 +404,7 @@ export class ObjectHydrator extends Hydrator {
|
|
|
404
404
|
const code = `// compiled hydrator for entity ${meta.className} (${type + normalizeAccessors ? ' normalized' : ''})\n` +
|
|
405
405
|
`return function(entity, data, factory, newEntity, convertCustomTypes, schema, parentSchema, normalizeAccessors) {\n` +
|
|
406
406
|
`${lines.join('\n')}\n}`;
|
|
407
|
-
const fnKey = `hydrator-${meta.
|
|
407
|
+
const fnKey = `hydrator-${meta.compiledName}-${type}-${normalizeAccessors}`;
|
|
408
408
|
const hydrator = Utils.createFunction(context, code, this.config.get('compiledFunctions'), fnKey);
|
|
409
409
|
this.#hydrators[key].set(meta.class, hydrator);
|
|
410
410
|
return hydrator;
|
|
@@ -547,7 +547,7 @@ export class MetadataDiscovery {
|
|
|
547
547
|
const fieldNames1 = prop.targetMeta.getPrimaryProps().flatMap(pk => pk.fieldNames);
|
|
548
548
|
const idColumns = fieldNames1.map(fieldName => this.#namingStrategy.joinKeyColumnName(prop.discriminator, fieldName, fieldNames1.length > 1));
|
|
549
549
|
prop.fieldNames ??= [prop.discriminatorColumn, ...idColumns];
|
|
550
|
-
prop.joinColumns ??=
|
|
550
|
+
prop.joinColumns ??= prop.fieldNames.slice(1);
|
|
551
551
|
prop.referencedColumnNames ??= fieldNames1;
|
|
552
552
|
prop.referencedTableName ??= prop.targetMeta.tableName;
|
|
553
553
|
return;
|
|
@@ -564,7 +564,10 @@ export class MetadataDiscovery {
|
|
|
564
564
|
}
|
|
565
565
|
Utils.defaultValue(prop, 'referencedTableName', meta2.tableName);
|
|
566
566
|
if (!prop.joinColumns) {
|
|
567
|
-
|
|
567
|
+
// must match the real FK columns, which ignore `targetKey` naming for simple PK targets
|
|
568
|
+
prop.joinColumns = prop.targetKey
|
|
569
|
+
? prop.fieldNames
|
|
570
|
+
: fieldNames.map(fieldName => this.#namingStrategy.joinKeyColumnName(prop.name, fieldName, fieldNames.length > 1));
|
|
568
571
|
}
|
|
569
572
|
if (!prop.referencedColumnNames) {
|
|
570
573
|
prop.referencedColumnNames = fieldNames;
|
|
@@ -670,6 +673,12 @@ export class MetadataDiscovery {
|
|
|
670
673
|
if (prop2.kind !== ReferenceKind.SCALAR && prop2.type === meta.className) {
|
|
671
674
|
meta.referencingProperties.push({ meta: meta2, prop: prop2 });
|
|
672
675
|
}
|
|
676
|
+
if (prop2.targetKey &&
|
|
677
|
+
(prop2.type === meta.className || prop2.polymorphTargets?.some(t => t.className === meta.className)) &&
|
|
678
|
+
!(meta.primaryKeys.length === 1 && meta.primaryKeys[0] === prop2.targetKey) &&
|
|
679
|
+
!meta.root.targetKeys?.includes(prop2.targetKey)) {
|
|
680
|
+
(meta.root.targetKeys ??= []).push(prop2.targetKey);
|
|
681
|
+
}
|
|
673
682
|
}
|
|
674
683
|
}
|
|
675
684
|
}
|
|
@@ -1175,12 +1184,24 @@ export class MetadataDiscovery {
|
|
|
1175
1184
|
if (!prop.discriminator && !prop.discriminatorColumn && !prop.discriminatorMap && !Array.isArray(prop.target)) {
|
|
1176
1185
|
return;
|
|
1177
1186
|
}
|
|
1187
|
+
const isToOne = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind);
|
|
1188
|
+
const isUnionTargetMN = prop.kind === ReferenceKind.MANY_TO_MANY && Array.isArray(prop.target);
|
|
1178
1189
|
prop.polymorphic = true;
|
|
1179
1190
|
prop.discriminator ??= prop.name;
|
|
1191
|
+
// explicit `fieldNames` use the `[discriminatorColumn, ...fkIdColumns]` layout
|
|
1192
|
+
if (isToOne && prop.fieldNames && prop.fieldNames.length > 1) {
|
|
1193
|
+
prop.discriminatorColumn ??= prop.fieldNames[0];
|
|
1194
|
+
// `EntitySchema` mirrors `fieldNames` into `joinColumns`, let `initManyToOneFields` derive them after embedded prefixing
|
|
1195
|
+
if (Utils.equals(prop.joinColumns, prop.fieldNames)) {
|
|
1196
|
+
delete prop.joinColumns;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1180
1199
|
prop.discriminatorColumn ??= this.#namingStrategy.discriminatorColumnName(prop.discriminator);
|
|
1181
1200
|
prop.createForeignKeyConstraint = false;
|
|
1182
|
-
|
|
1183
|
-
|
|
1201
|
+
// a single field name can only be the FK column, e.g. mirrored from `joinColumn`
|
|
1202
|
+
if (isToOne && prop.fieldNames?.length === 1) {
|
|
1203
|
+
prop.fieldNames = [prop.discriminatorColumn, ...prop.fieldNames];
|
|
1204
|
+
}
|
|
1184
1205
|
if (isToOne || isUnionTargetMN) {
|
|
1185
1206
|
const types = prop.type.split(/ ?\| ?/);
|
|
1186
1207
|
prop.polymorphTargets = discovered.filter(m => types.includes(m.className) && !m.embeddable);
|
|
@@ -1440,7 +1461,8 @@ export class MetadataDiscovery {
|
|
|
1440
1461
|
}
|
|
1441
1462
|
}
|
|
1442
1463
|
rootProp.stiFieldNameMap[meta.discriminatorValue] = prop.fieldNames[0];
|
|
1443
|
-
|
|
1464
|
+
// subtypes that only differ in type share the column
|
|
1465
|
+
rootProp.stiFieldNames = Utils.unique([...rootProp.stiFieldNames, ...prop.fieldNames]);
|
|
1444
1466
|
newProp.nullable = true;
|
|
1445
1467
|
newProp.name = name;
|
|
1446
1468
|
newProp.hydrate = false;
|
|
@@ -32,6 +32,13 @@ export declare class MetadataStorage {
|
|
|
32
32
|
getById<T>(id: number): EntityMetadata<T>;
|
|
33
33
|
/** Returns metadata by class name, optionally throwing if not found. */
|
|
34
34
|
getByClassName<T = any, V extends boolean = true>(className: string, validate?: V): V extends true ? EntityMetadata<T> : EntityMetadata<T> | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Returns the first registered metadata with the given table name, in registration order (entities sharing
|
|
37
|
+
* a table, like STI children, resolve to the one registered first). The index is built on first use, as table
|
|
38
|
+
* names are only final once discovery has finished.
|
|
39
|
+
* @internal
|
|
40
|
+
*/
|
|
41
|
+
getByTableName<T = any>(tableName: string): EntityMetadata<T> | undefined;
|
|
35
42
|
/** Returns metadata by unique name, optionally throwing if not found. */
|
|
36
43
|
getByUniqueName<T = any, V extends boolean = true>(uniqueName: string, validate?: V): V extends true ? EntityMetadata<T> : EntityMetadata<T> | undefined;
|
|
37
44
|
private validate;
|
|
@@ -18,6 +18,8 @@ export class MetadataStorage {
|
|
|
18
18
|
#classNameMap;
|
|
19
19
|
#uniqueNameMap;
|
|
20
20
|
#ambiguousNames = new Set();
|
|
21
|
+
/** Built on first use by `getByTableName()`, dropped whenever an entity is registered or removed. */
|
|
22
|
+
#tableNameMap;
|
|
21
23
|
constructor(metadata = {}) {
|
|
22
24
|
this.#idMap = {};
|
|
23
25
|
this.#uniqueNameMap = {};
|
|
@@ -102,6 +104,7 @@ export class MetadataStorage {
|
|
|
102
104
|
/** Registers metadata for the given entity. */
|
|
103
105
|
set(entityName, meta) {
|
|
104
106
|
this.#metadataMap.set(entityName, meta);
|
|
107
|
+
this.#tableNameMap = undefined;
|
|
105
108
|
this.#idMap[meta._id] = meta;
|
|
106
109
|
this.#uniqueNameMap[meta.uniqueName] = meta;
|
|
107
110
|
const className = Utils.className(entityName);
|
|
@@ -110,6 +113,12 @@ export class MetadataStorage {
|
|
|
110
113
|
if (existing && existing !== meta && existing.class !== meta.class) {
|
|
111
114
|
this.#ambiguousNames.add(className);
|
|
112
115
|
}
|
|
116
|
+
// colliding names are numbered in storage order, which follows the discovery config, not module evaluation
|
|
117
|
+
const sameName = this.#ambiguousNames.has(className)
|
|
118
|
+
? [...new Set(this.#metadataMap.values())].filter(m => m.className === meta.className)
|
|
119
|
+
: [meta];
|
|
120
|
+
const ordinal = sameName.indexOf(meta);
|
|
121
|
+
meta.compiledName = ordinal > 0 ? `${meta.className}#${ordinal}` : meta.className;
|
|
113
122
|
this.#classNameMap[className] = meta;
|
|
114
123
|
return meta;
|
|
115
124
|
}
|
|
@@ -118,6 +127,7 @@ export class MetadataStorage {
|
|
|
118
127
|
const meta = this.find(entityName);
|
|
119
128
|
if (meta) {
|
|
120
129
|
this.#metadataMap.delete(meta.class);
|
|
130
|
+
this.#tableNameMap = undefined;
|
|
121
131
|
delete this.#idMap[meta._id];
|
|
122
132
|
delete this.#uniqueNameMap[meta.uniqueName];
|
|
123
133
|
delete this.#classNameMap[meta.className];
|
|
@@ -145,6 +155,23 @@ export class MetadataStorage {
|
|
|
145
155
|
getByClassName(className, validate = true) {
|
|
146
156
|
return this.validate(this.#classNameMap[className], className, validate);
|
|
147
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Returns the first registered metadata with the given table name, in registration order (entities sharing
|
|
160
|
+
* a table, like STI children, resolve to the one registered first). The index is built on first use, as table
|
|
161
|
+
* names are only final once discovery has finished.
|
|
162
|
+
* @internal
|
|
163
|
+
*/
|
|
164
|
+
getByTableName(tableName) {
|
|
165
|
+
if (!this.#tableNameMap) {
|
|
166
|
+
this.#tableNameMap = new Map();
|
|
167
|
+
for (const meta of this.#metadataMap.values()) {
|
|
168
|
+
if (!this.#tableNameMap.has(meta.tableName)) {
|
|
169
|
+
this.#tableNameMap.set(meta.tableName, meta);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return this.#tableNameMap.get(tableName);
|
|
174
|
+
}
|
|
148
175
|
/** Returns metadata by unique name, optionally throwing if not found. */
|
|
149
176
|
getByUniqueName(uniqueName, validate = true) {
|
|
150
177
|
return this.validate(this.#uniqueNameMap[uniqueName], uniqueName, validate);
|
package/metadata/types.d.ts
CHANGED
|
@@ -176,6 +176,12 @@ export interface PropertyOptions<Owner> {
|
|
|
176
176
|
* Explicitly specify the auto increment of the primary key.
|
|
177
177
|
*/
|
|
178
178
|
autoincrement?: boolean;
|
|
179
|
+
/**
|
|
180
|
+
* Start value and increment of an autoincrement column. (SQL only)
|
|
181
|
+
* Applied only when the column is created (and by `orm.schema.clear()`), never diffed by the schema generator.
|
|
182
|
+
* `incrementBy` is not supported on MySQL, MariaDB and SQLite.
|
|
183
|
+
*/
|
|
184
|
+
sequence?: SequenceOptions;
|
|
179
185
|
/**
|
|
180
186
|
* Add the property to the `returning` clause of inserts, updates and upserts, reloading the database value into the entity.
|
|
181
187
|
*/
|
|
@@ -587,6 +593,12 @@ export interface EnumOptions<T> extends PropertyOptions<T> {
|
|
|
587
593
|
/** for postgres, by default it uses text column with check constraint */
|
|
588
594
|
nativeEnumName?: string;
|
|
589
595
|
}
|
|
596
|
+
export interface SequenceOptions {
|
|
597
|
+
/** The first value the autoincrement column generates. */
|
|
598
|
+
startWith?: number;
|
|
599
|
+
/** The step between generated values. Not supported on MySQL, MariaDB and SQLite. */
|
|
600
|
+
incrementBy?: number;
|
|
601
|
+
}
|
|
590
602
|
export interface PrimaryKeyOptions<T> extends PropertyOptions<T> {
|
|
591
603
|
}
|
|
592
604
|
export interface SerializedPrimaryKeyOptions<T> extends PropertyOptions<T> {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/core",
|
|
3
|
-
"version": "7.3.0-dev.
|
|
3
|
+
"version": "7.3.0-dev.11",
|
|
4
4
|
"description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"data-mapper",
|
package/platforms/Platform.d.ts
CHANGED
|
@@ -325,6 +325,8 @@ export declare abstract class Platform {
|
|
|
325
325
|
supportsDownMigrations(): boolean;
|
|
326
326
|
/** Whether the platform supports deferred unique constraints. */
|
|
327
327
|
supportsDeferredUniqueConstraints(): boolean;
|
|
328
|
+
/** Whether autoincrement columns support a per-column increment (`sequence.incrementBy`). */
|
|
329
|
+
supportsAutoincrementIncrement(): boolean;
|
|
328
330
|
/** Whether the platform supports row level security (PostgreSQL). */
|
|
329
331
|
supportsRowLevelSecurity(): boolean;
|
|
330
332
|
/** Whether the driver can apply the session context on every pooled connection acquire (`sessionContext: 'connection'`). */
|
package/platforms/Platform.js
CHANGED
|
@@ -738,6 +738,10 @@ export class Platform {
|
|
|
738
738
|
supportsDeferredUniqueConstraints() {
|
|
739
739
|
return true;
|
|
740
740
|
}
|
|
741
|
+
/** Whether autoincrement columns support a per-column increment (`sequence.incrementBy`). */
|
|
742
|
+
supportsAutoincrementIncrement() {
|
|
743
|
+
return true;
|
|
744
|
+
}
|
|
741
745
|
/** Whether the platform supports row level security (PostgreSQL). */
|
|
742
746
|
supportsRowLevelSecurity() {
|
|
743
747
|
return false;
|
|
@@ -758,6 +762,20 @@ export class Platform {
|
|
|
758
762
|
if (meta.partitionBy && !this.supportsPartitionedTables()) {
|
|
759
763
|
throw new MetadataError(`Entity ${meta.className} uses partitionBy, but ${this.constructor.name} does not support partitioned tables`);
|
|
760
764
|
}
|
|
765
|
+
for (const prop of Object.values(meta.properties)) {
|
|
766
|
+
if (!prop.sequence) {
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
if (!prop.autoincrement) {
|
|
770
|
+
throw MetadataError.sequenceWithoutAutoincrement(meta, prop);
|
|
771
|
+
}
|
|
772
|
+
if (typeof prop.generated === 'string' && /identity\s*\(/i.test(prop.generated)) {
|
|
773
|
+
throw MetadataError.sequenceWithIdentityOptions(meta, prop);
|
|
774
|
+
}
|
|
775
|
+
if (prop.sequence.incrementBy != null && !this.supportsAutoincrementIncrement()) {
|
|
776
|
+
throw MetadataError.sequenceIncrementNotSupported(meta, prop, this.constructor.name);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
761
779
|
const declaresRls = meta.policies.length > 0 || !!meta.rowLevelSecurity;
|
|
762
780
|
if (declaresRls && !this.supportsRowLevelSecurity()) {
|
|
763
781
|
throw MetadataError.rowLevelSecurityNotSupportedByDriver(meta);
|
package/typings.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ import type { SerializeOptions } from './serialization/EntitySerializer.js';
|
|
|
12
12
|
import type { MetadataStorage } from './metadata/MetadataStorage.js';
|
|
13
13
|
import type { Routine } from './metadata/Routine.js';
|
|
14
14
|
import type { EntitySchema } from './metadata/EntitySchema.js';
|
|
15
|
-
import type { EntityPartitionBy, IndexColumnOptions } from './metadata/types.js';
|
|
15
|
+
import type { EntityPartitionBy, IndexColumnOptions, SequenceOptions } from './metadata/types.js';
|
|
16
16
|
import type { Type, types } from './types/index.js';
|
|
17
17
|
import type { Platform } from './platforms/Platform.js';
|
|
18
18
|
import type { Configuration } from './utils/Configuration.js';
|
|
@@ -986,6 +986,7 @@ export interface EntityProperty<Owner = any, Target = any> {
|
|
|
986
986
|
hasConvertToJSValueSQL: boolean;
|
|
987
987
|
hasConvertToDatabaseValueSQL: boolean;
|
|
988
988
|
autoincrement?: boolean;
|
|
989
|
+
sequence?: SequenceOptions;
|
|
989
990
|
returning?: boolean;
|
|
990
991
|
primary?: boolean;
|
|
991
992
|
serializedPrimaryKey: boolean;
|
|
@@ -1175,6 +1176,7 @@ export interface EntityMetadata<Entity = any, Class extends EntityCtor<Entity> =
|
|
|
1175
1176
|
meta: EntityMetadata<Entity>;
|
|
1176
1177
|
prop: EntityProperty<Entity>;
|
|
1177
1178
|
}[];
|
|
1179
|
+
targetKeys?: EntityKey<Entity>[];
|
|
1178
1180
|
comparableProps: EntityProperty<Entity>[];
|
|
1179
1181
|
trackingProps: EntityProperty<Entity>[];
|
|
1180
1182
|
hydrateProps: EntityProperty<Entity>[];
|
|
@@ -1258,6 +1260,8 @@ export interface EntityMetadata<Entity = any, Class extends EntityCtor<Entity> =
|
|
|
1258
1260
|
orderBy?: QueryOrderMap<Entity> | QueryOrderMap<Entity>[];
|
|
1259
1261
|
/** @internal can be used for computed numeric cache keys */
|
|
1260
1262
|
readonly _id: number;
|
|
1263
|
+
/** @internal name that does not depend on module evaluation order (unlike `_id`), used to key `compiledFunctions` */
|
|
1264
|
+
compiledName?: string;
|
|
1261
1265
|
}
|
|
1262
1266
|
/** Options for `ISchemaGenerator.create()`. */
|
|
1263
1267
|
export interface CreateSchemaOptions {
|
|
@@ -7,6 +7,7 @@ import { Reference } from '../entity/Reference.js';
|
|
|
7
7
|
import { PolymorphicRef } from '../entity/PolymorphicRef.js';
|
|
8
8
|
import { ReferenceKind } from '../enums.js';
|
|
9
9
|
import { isRaw } from '../utils/RawQueryFragment.js';
|
|
10
|
+
import { ValidationError } from '../errors.js';
|
|
10
11
|
/** @internal Computes change sets by comparing entity state against original snapshots. */
|
|
11
12
|
export class ChangeSetComputer {
|
|
12
13
|
#comparator;
|
|
@@ -48,6 +49,10 @@ export class ChangeSetComputer {
|
|
|
48
49
|
if (Utils.equals(data, wrapped.__originalEntityData)) {
|
|
49
50
|
return null;
|
|
50
51
|
}
|
|
52
|
+
// a reference known only by its `targetKey` has no PK, so the update would have no condition
|
|
53
|
+
if (!wrapped.hasPrimaryKey()) {
|
|
54
|
+
throw ValidationError.referenceWithoutPK(entity);
|
|
55
|
+
}
|
|
51
56
|
}
|
|
52
57
|
const changeSet = new ChangeSet(entity, type, this.computePayload(entity), meta);
|
|
53
58
|
changeSet.originalEntity = wrapped.__originalEntityData;
|
|
@@ -162,21 +167,30 @@ export class ChangeSetComputer {
|
|
|
162
167
|
const target = Reference.unwrapReference(rawTarget);
|
|
163
168
|
const needsProcessing = target != null && (prop.targetKey != null || !target.__helper.hasPrimaryKey());
|
|
164
169
|
if (needsProcessing) {
|
|
170
|
+
// a reference known only by its `targetKey` has no PK, and is never inserted to generate one
|
|
171
|
+
if (!prop.targetKey && !target.__helper.__initialized) {
|
|
172
|
+
throw ValidationError.referenceWithoutPK(target);
|
|
173
|
+
}
|
|
165
174
|
let value = prop.targetKey ? target[prop.targetKey] : target.__helper.__identifier;
|
|
166
|
-
|
|
167
|
-
if (prop.targetKey
|
|
168
|
-
const targetProp =
|
|
175
|
+
// use the actual target's meta, a polymorphic relation has several
|
|
176
|
+
if (prop.targetKey) {
|
|
177
|
+
const targetProp = target.__meta.properties[prop.targetKey];
|
|
169
178
|
if (targetProp?.customType) {
|
|
170
179
|
value = targetProp.customType.convertToDatabaseValue(value, this.#platform, { mode: 'serialization' });
|
|
171
180
|
}
|
|
172
181
|
}
|
|
173
182
|
if (prop.polymorphic) {
|
|
174
183
|
const discriminator = QueryHelper.findDiscriminatorValue(prop.discriminatorMap, target.constructor);
|
|
175
|
-
|
|
184
|
+
value = new PolymorphicRef(discriminator, value);
|
|
176
185
|
}
|
|
177
|
-
|
|
178
|
-
|
|
186
|
+
// an unchanged `targetKey` FK is not part of the diff
|
|
187
|
+
if (prop.targetKey &&
|
|
188
|
+
changeSet.type === ChangeSetType.UPDATE &&
|
|
189
|
+
!(prop.name in changeSet.payload) &&
|
|
190
|
+
Utils.equals(changeSet.originalEntity?.[prop.name], value)) {
|
|
191
|
+
return;
|
|
179
192
|
}
|
|
193
|
+
Utils.setPayloadProperty(changeSet.payload, changeSet.meta, prop, value, idx);
|
|
180
194
|
}
|
|
181
195
|
});
|
|
182
196
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { AnyEntity, EntityMetadata } from '../typings.js';
|
|
2
|
-
/** @internal Stores managed entity instances keyed by their primary key hash, ensuring each row is loaded once. */
|
|
2
|
+
/** @internal Stores managed entity instances keyed by their primary key hash (and `targetKey` values), ensuring each row is loaded once. */
|
|
3
3
|
export declare class IdentityMap {
|
|
4
4
|
#private;
|
|
5
5
|
constructor(defaultSchema?: string);
|
|
6
|
-
/** Stores an entity in the identity map under its primary key hash. */
|
|
6
|
+
/** Stores an entity in the identity map under its primary key hash and the `targetKey` values referencing it. */
|
|
7
7
|
store<T>(item: T): void;
|
|
8
8
|
/**
|
|
9
9
|
* Stores an entity under an alternate key (non-PK property).
|
|
@@ -25,6 +25,7 @@ export declare class IdentityMap {
|
|
|
25
25
|
/** Returns all entities currently in the identity map. */
|
|
26
26
|
values(): AnyEntity[];
|
|
27
27
|
[Symbol.iterator](): IterableIterator<AnyEntity>;
|
|
28
|
+
private getEntities;
|
|
28
29
|
/** Returns all hash keys currently in the identity map. */
|
|
29
30
|
keys(): string[];
|
|
30
31
|
/**
|
|
@@ -1,15 +1,29 @@
|
|
|
1
|
-
/** @internal Stores managed entity instances keyed by their primary key hash, ensuring each row is loaded once. */
|
|
1
|
+
/** @internal Stores managed entity instances keyed by their primary key hash (and `targetKey` values), ensuring each row is loaded once. */
|
|
2
2
|
export class IdentityMap {
|
|
3
3
|
#defaultSchema;
|
|
4
4
|
#registry = new Map();
|
|
5
5
|
/** Tracks alternate key hashes for each entity so we can clean them up on delete */
|
|
6
6
|
#alternateKeys = new WeakMap();
|
|
7
|
+
#storesWithAlternateKeys = new WeakSet();
|
|
7
8
|
constructor(defaultSchema) {
|
|
8
9
|
this.#defaultSchema = defaultSchema;
|
|
9
10
|
}
|
|
10
|
-
/** Stores an entity in the identity map under its primary key hash. */
|
|
11
|
+
/** Stores an entity in the identity map under its primary key hash and the `targetKey` values referencing it. */
|
|
11
12
|
store(item) {
|
|
12
|
-
|
|
13
|
+
const meta = item.__meta.root;
|
|
14
|
+
const wrapped = item.__helper;
|
|
15
|
+
// PK-less references (known only by a `targetKey`) would share one slot that outlives their later removal
|
|
16
|
+
if (!meta.targetKeys || wrapped.hasPrimaryKey()) {
|
|
17
|
+
this.getStore(meta).set(this.getPkHash(item), item);
|
|
18
|
+
}
|
|
19
|
+
// references resolved via `targetKey` look the entity up by that key instead of the PK
|
|
20
|
+
for (const key of meta.targetKeys ?? []) {
|
|
21
|
+
const value = item[key];
|
|
22
|
+
if (value != null) {
|
|
23
|
+
const schema = wrapped.__schema ?? meta.schema ?? this.#defaultSchema;
|
|
24
|
+
this.storeByKey(item, key, '' + value, schema);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
13
27
|
}
|
|
14
28
|
/**
|
|
15
29
|
* Stores an entity under an alternate key (non-PK property).
|
|
@@ -17,7 +31,9 @@ export class IdentityMap {
|
|
|
17
31
|
*/
|
|
18
32
|
storeByKey(item, key, value, schema) {
|
|
19
33
|
const hash = this.getKeyHash(key, value, schema);
|
|
20
|
-
this.getStore(item.__meta.root)
|
|
34
|
+
const store = this.getStore(item.__meta.root);
|
|
35
|
+
store.set(hash, item);
|
|
36
|
+
this.#storesWithAlternateKeys.add(store);
|
|
21
37
|
// Track this alternate key so we can clean it up when the entity is deleted
|
|
22
38
|
let keys = this.#alternateKeys.get(item);
|
|
23
39
|
if (!keys) {
|
|
@@ -35,7 +51,10 @@ export class IdentityMap {
|
|
|
35
51
|
const altKeys = this.#alternateKeys.get(item);
|
|
36
52
|
if (altKeys) {
|
|
37
53
|
for (const hash of altKeys) {
|
|
38
|
-
|
|
54
|
+
// the hash might be owned by another entity by now, e.g. after swapping unique values
|
|
55
|
+
if (store.get(hash) === item) {
|
|
56
|
+
store.delete(hash);
|
|
57
|
+
}
|
|
39
58
|
}
|
|
40
59
|
this.#alternateKeys.delete(item);
|
|
41
60
|
}
|
|
@@ -69,7 +88,7 @@ export class IdentityMap {
|
|
|
69
88
|
values() {
|
|
70
89
|
const ret = [];
|
|
71
90
|
for (const store of this.#registry.values()) {
|
|
72
|
-
for (const item of
|
|
91
|
+
for (const item of this.getEntities(store)) {
|
|
73
92
|
ret.push(item);
|
|
74
93
|
}
|
|
75
94
|
}
|
|
@@ -77,11 +96,15 @@ export class IdentityMap {
|
|
|
77
96
|
}
|
|
78
97
|
*[Symbol.iterator]() {
|
|
79
98
|
for (const store of this.#registry.values()) {
|
|
80
|
-
for (const item of
|
|
99
|
+
for (const item of this.getEntities(store)) {
|
|
81
100
|
yield item;
|
|
82
101
|
}
|
|
83
102
|
}
|
|
84
103
|
}
|
|
104
|
+
getEntities(store) {
|
|
105
|
+
// an entity indexed by alternate keys occupies several slots of its store
|
|
106
|
+
return this.#storesWithAlternateKeys.has(store) ? new Set(store.values()) : store.values();
|
|
107
|
+
}
|
|
85
108
|
/** Returns all hash keys currently in the identity map. */
|
|
86
109
|
keys() {
|
|
87
110
|
const ret = [];
|
|
@@ -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);
|
|
@@ -1118,6 +1136,11 @@ export class UnitOfWork {
|
|
|
1118
1136
|
for (const changeSet of changeSets) {
|
|
1119
1137
|
const wrapped = helper(changeSet.entity);
|
|
1120
1138
|
wrapped.__originalEntityData = this.#comparator.prepareEntity(changeSet.entity);
|
|
1139
|
+
// re-index so the entity is found by its new `targetKey` values instead of the old ones
|
|
1140
|
+
if (changeSet.meta.root.targetKeys?.some(key => key in changeSet.payload)) {
|
|
1141
|
+
this.#identityMap.delete(changeSet.entity);
|
|
1142
|
+
this.#identityMap.store(changeSet.entity);
|
|
1143
|
+
}
|
|
1121
1144
|
if (!wrapped.__initialized) {
|
|
1122
1145
|
for (const prop of changeSet.meta.relations) {
|
|
1123
1146
|
if ([ReferenceKind.MANY_TO_MANY, ReferenceKind.ONE_TO_MANY].includes(prop.kind) &&
|
|
@@ -95,7 +95,7 @@ export class EntityComparator {
|
|
|
95
95
|
lines.push(` return entity${this.wrap(pk)};`);
|
|
96
96
|
}
|
|
97
97
|
const code = `// compiled pk getter for entity ${meta.className}\n` + `return function(entity) {\n${lines.join('\n')}\n}`;
|
|
98
|
-
const fnKey = `pkGetter-${meta.
|
|
98
|
+
const fnKey = `pkGetter-${meta.compiledName}`;
|
|
99
99
|
const pkSerializer = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
|
|
100
100
|
this.#pkGetters.set(meta, pkSerializer);
|
|
101
101
|
return pkSerializer;
|
|
@@ -147,7 +147,7 @@ export class EntityComparator {
|
|
|
147
147
|
}
|
|
148
148
|
const code = `// compiled pk getter (with converted custom types) for entity ${meta.className}\n` +
|
|
149
149
|
`return function(entity) {\n${lines.join('\n')}\n}`;
|
|
150
|
-
const fnKey = `pkGetterConverted-${meta.
|
|
150
|
+
const fnKey = `pkGetterConverted-${meta.compiledName}`;
|
|
151
151
|
const pkSerializer = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
|
|
152
152
|
this.#pkGettersConverted.set(meta, pkSerializer);
|
|
153
153
|
return pkSerializer;
|
|
@@ -206,7 +206,7 @@ export class EntityComparator {
|
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
208
|
const code = `// compiled pk serializer for entity ${meta.className}\n` + `return function(entity) {\n${lines.join('\n')}\n}`;
|
|
209
|
-
const fnKey = `pkSerializer-${meta.
|
|
209
|
+
const fnKey = `pkSerializer-${meta.compiledName}`;
|
|
210
210
|
const pkSerializer = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
|
|
211
211
|
this.#pkSerializers.set(meta, pkSerializer);
|
|
212
212
|
return pkSerializer;
|
|
@@ -236,7 +236,7 @@ export class EntityComparator {
|
|
|
236
236
|
})
|
|
237
237
|
.forEach(prop => lines.push(this.getPropertySnapshot(meta, prop, context, this.wrap(prop.name), this.wrap(prop.name), [prop.name])));
|
|
238
238
|
const code = `return function(entity) {\n const ret = {};\n${lines.join('\n')}\n return ret;\n}`;
|
|
239
|
-
const fnKey = `snapshotGenerator-${meta.
|
|
239
|
+
const fnKey = `snapshotGenerator-${meta.compiledName}`;
|
|
240
240
|
const snapshotGenerator = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
|
|
241
241
|
this.#snapshotGenerators.set(meta, snapshotGenerator);
|
|
242
242
|
return snapshotGenerator;
|
|
@@ -350,14 +350,27 @@ export class EntityComparator {
|
|
|
350
350
|
if (prop.embedded && (meta.embeddable || meta.properties[prop.embedded[0]].object)) {
|
|
351
351
|
continue;
|
|
352
352
|
}
|
|
353
|
+
let defined = `typeof ${this.propName(prop.fieldNames[0])} !== 'undefined'`;
|
|
354
|
+
// STI subtypes with conflicting columns share the property, so another subtype's null column must not override it
|
|
355
|
+
if (prop.renamedFrom) {
|
|
356
|
+
lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
|
|
357
|
+
defined = `${this.propName(prop.fieldNames[0])} != null`;
|
|
358
|
+
// a column shared with a differently typed subtype is left for each subtype's hydrator to convert
|
|
359
|
+
if (meta.props.some(p => p !== prop && p.name === prop.name && p.fieldNames?.[0] === prop.fieldNames[0])) {
|
|
360
|
+
lines.push(`${padding} if (${defined}) {`);
|
|
361
|
+
lines.push(`${padding} ret${this.wrap(prop.name)} = ${this.propName(prop.fieldNames[0])};`);
|
|
362
|
+
lines.push(`${padding} }`);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
353
366
|
if (prop.runtimeType === 'boolean') {
|
|
354
|
-
lines.push(`${padding} if (
|
|
367
|
+
lines.push(`${padding} if (${defined}) {`);
|
|
355
368
|
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
369
|
lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
|
|
357
370
|
lines.push(`${padding} }`);
|
|
358
371
|
}
|
|
359
372
|
else if (prop.runtimeType === 'Date' && !this.#platform.isNumericProperty(prop)) {
|
|
360
|
-
lines.push(`${padding} if (
|
|
373
|
+
lines.push(`${padding} if (${defined}) {`);
|
|
361
374
|
context.set('parseDate', (value) => this.#platform.parseDate(value));
|
|
362
375
|
parseDate('ret' + this.wrap(prop.name), this.propName(prop.fieldNames[0]), padding);
|
|
363
376
|
lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
|
|
@@ -372,13 +385,13 @@ export class EntityComparator {
|
|
|
372
385
|
}
|
|
373
386
|
return item == null ? item : this.getResultMapper(prop.targetMeta)(item);
|
|
374
387
|
});
|
|
375
|
-
lines.push(`${padding} if (
|
|
388
|
+
lines.push(`${padding} if (${defined}) {`);
|
|
376
389
|
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
390
|
lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
|
|
378
391
|
lines.push(`${padding} }`);
|
|
379
392
|
}
|
|
380
393
|
else if (prop.kind !== ReferenceKind.EMBEDDED) {
|
|
381
|
-
lines.push(`${padding} if (
|
|
394
|
+
lines.push(`${padding} if (${defined}) {`);
|
|
382
395
|
lines.push(`${padding} ret${this.wrap(prop.name)} = ${this.propName(prop.fieldNames[0])};`);
|
|
383
396
|
lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
|
|
384
397
|
lines.push(`${padding} }`);
|
|
@@ -420,7 +433,7 @@ export class EntityComparator {
|
|
|
420
433
|
lines.push(` for (let k in result) { if (Object.hasOwn(result, k) && !mapped[k] && ret[k] === undefined${knownKeysGuard}) ret[k] = result[k]; }`);
|
|
421
434
|
const code = `// compiled mapper for entity ${meta.className}\n` +
|
|
422
435
|
`return function(result) {\n const ret = {};\n${lines.join('\n')}\n return ret;\n}`;
|
|
423
|
-
const fnKey = `resultMapper-${meta.
|
|
436
|
+
const fnKey = `resultMapper-${meta.compiledName}`;
|
|
424
437
|
const resultMapper = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
|
|
425
438
|
this.#mappers.set(meta, resultMapper);
|
|
426
439
|
return resultMapper;
|
|
@@ -608,9 +621,24 @@ export class EntityComparator {
|
|
|
608
621
|
ret += ` if (discriminator !== undefined) break;\n`;
|
|
609
622
|
ret += ` __cls${level} = Object.getPrototypeOf(__cls${level});\n`;
|
|
610
623
|
ret += ` }\n`;
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
624
|
+
if (prop.targetKey) {
|
|
625
|
+
// the FK holds the `targetKey` value in the format of the actual target's column
|
|
626
|
+
const targetKey = prop.targetKey;
|
|
627
|
+
const targetKeyValueKey = `targetKeyValue_${this.safeKey(prop.name)}`;
|
|
628
|
+
context.set(targetKeyValueKey, (val) => {
|
|
629
|
+
const customType = val.__meta.properties[targetKey]?.customType;
|
|
630
|
+
const value = val[targetKey];
|
|
631
|
+
return customType
|
|
632
|
+
? customType.convertToDatabaseValue(value, this.#platform, { mode: 'serialization' })
|
|
633
|
+
: value;
|
|
634
|
+
});
|
|
635
|
+
ret += ` const pk = ${targetKeyValueKey}(val${level});\n`;
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
ret += ` const pk = val${level}?.__helper?.__identifier && !val${level}?.__helper?.hasPrimaryKey()\n`;
|
|
639
|
+
ret += ` ? val${level}.__helper.__identifier\n`;
|
|
640
|
+
ret += ` : toArray(val${level}?.__helper?.getPrimaryKey(true));\n`;
|
|
641
|
+
}
|
|
614
642
|
ret += ` ret${dataKey} = new PolymorphicRef(discriminator, pk);\n`;
|
|
615
643
|
ret += ` }\n`;
|
|
616
644
|
}
|
|
@@ -624,8 +652,8 @@ export class EntityComparator {
|
|
|
624
652
|
ret += ` } else if (typeof entity${entityKey} !== 'undefined') {\n`;
|
|
625
653
|
ret += ` const val${level} = entity${entityKey}${unwrap};\n`;
|
|
626
654
|
if (targetProp?.customType) {
|
|
627
|
-
//
|
|
628
|
-
const convertorKey = this.registerCustomType(targetProp, context);
|
|
655
|
+
// 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`
|
|
656
|
+
const convertorKey = this.registerCustomType({ ...prop, customType: targetProp.customType }, context);
|
|
629
657
|
ret += ` ret${dataKey} = convertToDatabaseValue_${convertorKey}(val${level}${targetAccess});\n`;
|
|
630
658
|
}
|
|
631
659
|
else {
|
|
@@ -688,7 +716,7 @@ export class EntityComparator {
|
|
|
688
716
|
lines.push(`}`);
|
|
689
717
|
const code = `// compiled comparator for entity ${meta.className}\n` +
|
|
690
718
|
`return function(last, current, options) {\n const diff = {};\n${lines.join('\n')}\n return diff;\n}`;
|
|
691
|
-
const fnKey = `comparator-${meta.
|
|
719
|
+
const fnKey = `comparator-${meta.compiledName}`;
|
|
692
720
|
const comparator = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
|
|
693
721
|
this.#comparators.set(meta, comparator);
|
|
694
722
|
return comparator;
|
package/utils/QueryHelper.d.ts
CHANGED
|
@@ -35,9 +35,35 @@ 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>;
|
|
47
|
+
/** @internal Splits a polymorphic target key like `imageable[Article]` into the property and target class names. */
|
|
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[];
|
|
51
|
+
/** @internal Resolves the target entity of a condition key, including polymorphic target keys like `imageable[Article]`. */
|
|
52
|
+
static findTargetMeta(prop: EntityProperty | null | undefined, key: string): EntityMetadata | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Routes conditions on polymorphic to-one relations to the targets that define all the queried properties,
|
|
55
|
+
* e.g. `{ imageable: { title } }` becomes `{ 'imageable[Article]': { title } }`, or an `$or` of such keys
|
|
56
|
+
* when more targets match. The first target alone keeps the regular relation join.
|
|
57
|
+
*/
|
|
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;
|
|
65
|
+
/** Property names used in a condition, including those nested in group operators. */
|
|
66
|
+
private static collectConditionKeys;
|
|
41
67
|
static getActiveFilters<T>(meta: EntityMetadata<T>, options: FilterOptions | undefined, filters: Dictionary<FilterDef>): FilterDef[];
|
|
42
68
|
/** @internal Sentinel wrapping for arguments accessed while statically resolving an `rls` filter condition. */
|
|
43
69
|
static readonly RLS_SENTINEL_PREFIX = "__mikro_rls_arg__";
|
package/utils/QueryHelper.js
CHANGED
|
@@ -4,7 +4,7 @@ import { ARRAY_OPERATORS, GroupOperator, JSON_KEY_OPERATORS, ReferenceKind } fro
|
|
|
4
4
|
import { JsonType } from '../types/JsonType.js';
|
|
5
5
|
import { helper } from '../entity/wrap.js';
|
|
6
6
|
import { isRaw, Raw } from './RawQueryFragment.js';
|
|
7
|
-
import { MetadataError } from '../errors.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
|
}
|
|
@@ -158,17 +155,21 @@ export class QueryHelper {
|
|
|
158
155
|
})));
|
|
159
156
|
}
|
|
160
157
|
Object.keys(where).forEach(k => {
|
|
161
|
-
const prop = meta.properties[k];
|
|
162
|
-
const meta2 = metadata.find(prop?.
|
|
158
|
+
const prop = meta.properties[QueryHelper.splitPolymorphicKey(k)[0]];
|
|
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).
|
|
166
166
|
// The criteria layer would otherwise emit a malformed predicate such as
|
|
167
167
|
// `(joinCol1, joinCol2) = scalar`. Limited to owning relations — `joinColumns` on the
|
|
168
168
|
// inverse side (1:m) describes the owning entity's FK columns, not the LHS tuple.
|
|
169
|
+
// Polymorphic relations are skipped too, a scalar would be compared to the discriminator column.
|
|
169
170
|
if (prop?.owner &&
|
|
170
171
|
[ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) &&
|
|
171
|
-
prop.joinColumns.length !== meta2.primaryKeys.length) {
|
|
172
|
+
(prop.polymorphic || prop.joinColumns.length !== meta2.primaryKeys.length)) {
|
|
172
173
|
return;
|
|
173
174
|
}
|
|
174
175
|
where[k] = Utils.getPrimaryKeyValues(where[k], meta2, true);
|
|
@@ -176,6 +177,37 @@ export class QueryHelper {
|
|
|
176
177
|
});
|
|
177
178
|
return false;
|
|
178
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
|
+
}
|
|
179
211
|
/** Copies the plain object and array structure of the condition, keeping the leaf values (e.g. entities) by reference. */
|
|
180
212
|
static cloneWhere(where) {
|
|
181
213
|
if (Array.isArray(where)) {
|
|
@@ -230,6 +262,11 @@ export class QueryHelper {
|
|
|
230
262
|
if (!Utils.isPlainObject(where)) {
|
|
231
263
|
return where;
|
|
232
264
|
}
|
|
265
|
+
if (meta) {
|
|
266
|
+
where = (options.type === 'orderBy'
|
|
267
|
+
? QueryHelper.splitPolymorphicOrderBy(meta, where)
|
|
268
|
+
: QueryHelper.splitPolymorphicConditions(meta, where));
|
|
269
|
+
}
|
|
233
270
|
return Utils.getObjectQueryKeys(where).reduce((o, key) => {
|
|
234
271
|
let value = where[key];
|
|
235
272
|
const customExpression = Raw.isKnownFragmentSymbol(key);
|
|
@@ -277,10 +314,11 @@ export class QueryHelper {
|
|
|
277
314
|
return o;
|
|
278
315
|
}
|
|
279
316
|
if (Utils.isPlainObject(value)) {
|
|
317
|
+
const targetMeta = customExpression ? undefined : QueryHelper.findTargetMeta(prop, key);
|
|
280
318
|
o[key] = QueryHelper.processWhere({
|
|
281
319
|
...options,
|
|
282
320
|
where: value,
|
|
283
|
-
entityName:
|
|
321
|
+
entityName: targetMeta?.class ?? entityName,
|
|
284
322
|
root: false,
|
|
285
323
|
});
|
|
286
324
|
}
|
|
@@ -290,6 +328,95 @@ export class QueryHelper {
|
|
|
290
328
|
return o;
|
|
291
329
|
}, {});
|
|
292
330
|
}
|
|
331
|
+
/** @internal Splits a polymorphic target key like `imageable[Article]` into the property and target class names. */
|
|
332
|
+
static splitPolymorphicKey(key) {
|
|
333
|
+
const match = /^(\w+)\[([\w,]+)]$/.exec(key);
|
|
334
|
+
return match ? [match[1], match[2]] : [key];
|
|
335
|
+
}
|
|
336
|
+
/** @internal Resolves the targets of a polymorphic target key like `imageable[Article]` or `imageable[Product,Article]`. */
|
|
337
|
+
static findTargetMetas(prop, key) {
|
|
338
|
+
const [, targetNames] = QueryHelper.splitPolymorphicKey(key);
|
|
339
|
+
const targets = targetNames?.split(',').map(name => prop?.polymorphTargets?.find(t => t.className === name));
|
|
340
|
+
return targets?.every(Boolean) ? targets : [];
|
|
341
|
+
}
|
|
342
|
+
/** @internal Resolves the target entity of a condition key, including polymorphic target keys like `imageable[Article]`. */
|
|
343
|
+
static findTargetMeta(prop, key) {
|
|
344
|
+
return QueryHelper.splitPolymorphicKey(key)[1] ? QueryHelper.findTargetMetas(prop, key)[0] : prop?.targetMeta;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Routes conditions on polymorphic to-one relations to the targets that define all the queried properties,
|
|
348
|
+
* e.g. `{ imageable: { title } }` becomes `{ 'imageable[Article]': { title } }`, or an `$or` of such keys
|
|
349
|
+
* when more targets match. The first target alone keeps the regular relation join.
|
|
350
|
+
*/
|
|
351
|
+
static splitPolymorphicConditions(meta, payload) {
|
|
352
|
+
let ret = payload;
|
|
353
|
+
for (const key of Object.keys(payload)) {
|
|
354
|
+
const prop = meta.properties[key];
|
|
355
|
+
const value = payload[key];
|
|
356
|
+
if (!prop?.polymorphTargets ||
|
|
357
|
+
![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) ||
|
|
358
|
+
!Utils.isPlainObject(value)) {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
const keys = this.collectConditionKeys(value);
|
|
362
|
+
const targets = prop.polymorphTargets.filter(target => keys.every(k => k in target.properties));
|
|
363
|
+
const primaryKeys = keys.every(k => targets.every(target => target.primaryKeys.includes(k)));
|
|
364
|
+
// PK conditions compare the FK columns without checking the type, so only when every target defines them
|
|
365
|
+
if (keys.length === 0 ||
|
|
366
|
+
targets.length === 0 ||
|
|
367
|
+
(primaryKeys && targets.length === prop.polymorphTargets.length) ||
|
|
368
|
+
(!primaryKeys && targets.length === 1 && targets[0] === prop.targetMeta)) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const branches = targets.map(target => ({ [`${key}[${target.className}]`]: value }));
|
|
372
|
+
ret = { ...ret };
|
|
373
|
+
delete ret[key];
|
|
374
|
+
if (branches.length === 1) {
|
|
375
|
+
Object.assign(ret, branches[0]);
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
ret.$and = [...(ret.$and ?? []), { $or: branches }];
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return ret;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Routes ordering by properties of polymorphic to-one relations to the targets that define them, e.g.
|
|
385
|
+
* `{ imageable: { name: 'asc' } }` becomes `{ 'imageable[Product,Article]': { name: 'asc' } }`, ordered by
|
|
386
|
+
* the first non-null value. Nested relations and formulas are ordered via the first target defining them.
|
|
387
|
+
*/
|
|
388
|
+
static splitPolymorphicOrderBy(meta, orderBy) {
|
|
389
|
+
const ret = {};
|
|
390
|
+
for (const key of Utils.getObjectQueryKeys(orderBy)) {
|
|
391
|
+
const prop = meta.properties[key];
|
|
392
|
+
const value = orderBy[key];
|
|
393
|
+
if (!prop?.polymorphTargets ||
|
|
394
|
+
![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) ||
|
|
395
|
+
!Utils.isPlainObject(value)) {
|
|
396
|
+
ret[key] = value;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
for (const field of Utils.getObjectQueryKeys(value)) {
|
|
400
|
+
const found = prop.polymorphTargets.filter(target => field in target.properties);
|
|
401
|
+
const sharedPK = found.length === prop.polymorphTargets.length && found.every(target => target.primaryKeys.includes(field));
|
|
402
|
+
const single = Utils.isPlainObject(value[field]) || found.some(target => target.properties[field].formula);
|
|
403
|
+
const targets = found.length === 0 ? [prop.targetMeta] : single ? [found[0]] : found;
|
|
404
|
+
// shared PKs are ordered by the FK column without a join
|
|
405
|
+
const targetKey = sharedPK ? key : `${key}[${targets.map(target => target.className).join(',')}]`;
|
|
406
|
+
ret[targetKey] = { ...ret[targetKey], [field]: value[field] };
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return ret;
|
|
410
|
+
}
|
|
411
|
+
/** Property names used in a condition, including those nested in group operators. */
|
|
412
|
+
static collectConditionKeys(payload) {
|
|
413
|
+
return Object.keys(payload).flatMap(k => {
|
|
414
|
+
if (k in GroupOperator || k === '$not') {
|
|
415
|
+
return Utils.asArray(payload[k]).flatMap(item => Utils.isPlainObject(item) ? this.collectConditionKeys(item) : []);
|
|
416
|
+
}
|
|
417
|
+
return Utils.isOperator(k) ? [] : [k];
|
|
418
|
+
});
|
|
419
|
+
}
|
|
293
420
|
static getActiveFilters(meta, options, filters) {
|
|
294
421
|
if (options === false) {
|
|
295
422
|
return [];
|
|
@@ -450,7 +577,7 @@ export class QueryHelper {
|
|
|
450
577
|
}
|
|
451
578
|
static findProperty(fieldName, options) {
|
|
452
579
|
const parts = fieldName.split('.');
|
|
453
|
-
const propName = parts.pop();
|
|
580
|
+
const propName = QueryHelper.splitPolymorphicKey(parts.pop())[0];
|
|
454
581
|
const alias = parts.length > 0 ? parts.join('.') : undefined;
|
|
455
582
|
const entityName = alias ? options.aliasMap?.[alias] : options.entityName;
|
|
456
583
|
const meta = entityName ? options.metadata.find(entityName) : undefined;
|
|
@@ -494,6 +621,9 @@ export class QueryHelper {
|
|
|
494
621
|
if (expanded !== w) {
|
|
495
622
|
where[k] = expanded;
|
|
496
623
|
}
|
|
624
|
+
else if (Array.isArray(w)) {
|
|
625
|
+
where[k] = w.map(expandItem);
|
|
626
|
+
}
|
|
497
627
|
else if (Utils.isPlainObject(w)) {
|
|
498
628
|
for (const op of Object.keys(w)) {
|
|
499
629
|
if (!Utils.isOperator(op, false)) {
|
|
@@ -189,6 +189,10 @@ export class TransactionManager {
|
|
|
189
189
|
if (wrapped.__originalEntityData) {
|
|
190
190
|
parentWrapped.__originalEntityData = originalEntityData;
|
|
191
191
|
}
|
|
192
|
+
// index a merged `targetKey` value, `getByKey` ignores the old one as stale
|
|
193
|
+
if (meta.root.targetKeys) {
|
|
194
|
+
parentUoW.getIdentityMap().store(parentEntity);
|
|
195
|
+
}
|
|
192
196
|
}
|
|
193
197
|
else {
|
|
194
198
|
parentUoW.merge(entity, new Set([entity]));
|
package/utils/Utils.js
CHANGED
|
@@ -153,7 +153,7 @@ export function parseJsonSafe(value) {
|
|
|
153
153
|
/** Collection of general-purpose utility methods used throughout the ORM. */
|
|
154
154
|
export class Utils {
|
|
155
155
|
static PK_SEPARATOR = '~~~';
|
|
156
|
-
static #ORM_VERSION = '7.3.0-dev.
|
|
156
|
+
static #ORM_VERSION = '7.3.0-dev.11';
|
|
157
157
|
/** Default session variable name backing an RLS filter argument (`current_setting('mikro.<filter>.<arg>')`). */
|
|
158
158
|
static getRlsSettingName(filterName, argName) {
|
|
159
159
|
return `mikro.${filterName}.${argName}`;
|