@mikro-orm/core 7.0.0-dev.156 → 7.0.0-dev.158
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.d.ts +18 -2
- package/drivers/IDatabaseDriver.d.ts +5 -0
- package/entity/EntityFactory.d.ts +6 -1
- package/entity/EntityFactory.js +12 -0
- package/entity/EntityLoader.d.ts +1 -1
- package/entity/EntityLoader.js +33 -6
- package/entity/EntityRepository.d.ts +18 -2
- package/entity/defineEntity.d.ts +2 -0
- package/entity/defineEntity.js +4 -0
- package/errors.d.ts +3 -0
- package/errors.js +9 -0
- package/hydration/ObjectHydrator.js +4 -2
- package/metadata/MetadataDiscovery.js +32 -15
- package/metadata/MetadataValidator.d.ts +1 -0
- package/metadata/MetadataValidator.js +19 -0
- package/metadata/types.d.ts +4 -0
- package/package.json +1 -1
- package/typings.d.ts +1 -0
- package/unit-of-work/ChangeSetComputer.js +13 -2
- package/unit-of-work/IdentityMap.d.ts +12 -0
- package/unit-of-work/IdentityMap.js +39 -1
- package/unit-of-work/UnitOfWork.d.ts +13 -0
- package/unit-of-work/UnitOfWork.js +34 -0
- package/utils/EntityComparator.js +17 -0
- package/utils/Utils.js +1 -1
package/EntityManager.d.ts
CHANGED
|
@@ -382,10 +382,26 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
|
|
|
382
382
|
* Shortcut for `wrap(entity).assign(data, { em })`
|
|
383
383
|
*/
|
|
384
384
|
assign<Entity extends object, Naked extends FromEntityType<Entity> = FromEntityType<Entity>, Convert extends boolean = false, Data extends EntityData<Naked, Convert> | Partial<EntityDTO<Naked>> = EntityData<Naked, Convert> | Partial<EntityDTO<Naked>>>(entity: Entity | Partial<Entity>, data: Data & IsSubset<EntityData<Naked, Convert>, Data>, options?: AssignOptions<Convert>): MergeSelected<Entity, Naked, keyof Data & string>;
|
|
385
|
+
/**
|
|
386
|
+
* Gets a reference to the entity identified by the given type and alternate key property without actually loading it.
|
|
387
|
+
* The key option specifies which property to use for identity map lookup instead of the primary key.
|
|
388
|
+
*/
|
|
389
|
+
getReference<Entity extends object, K extends string & keyof Entity>(entityName: EntityName<Entity>, id: Entity[K], options: Omit<GetReferenceOptions, 'key' | 'wrapped'> & {
|
|
390
|
+
key: K;
|
|
391
|
+
wrapped: true;
|
|
392
|
+
}): Ref<Entity>;
|
|
393
|
+
/**
|
|
394
|
+
* Gets a reference to the entity identified by the given type and alternate key property without actually loading it.
|
|
395
|
+
* The key option specifies which property to use for identity map lookup instead of the primary key.
|
|
396
|
+
*/
|
|
397
|
+
getReference<Entity extends object, K extends string & keyof Entity>(entityName: EntityName<Entity>, id: Entity[K], options: Omit<GetReferenceOptions, 'key'> & {
|
|
398
|
+
key: K;
|
|
399
|
+
wrapped?: false;
|
|
400
|
+
}): Entity;
|
|
385
401
|
/**
|
|
386
402
|
* Gets a reference to the entity identified by the given type and identifier without actually loading it, if the entity is not yet loaded
|
|
387
403
|
*/
|
|
388
|
-
getReference<Entity extends object>(entityName: EntityName<Entity>, id: Primary<Entity>, options: Omit<GetReferenceOptions, 'wrapped'> & {
|
|
404
|
+
getReference<Entity extends object>(entityName: EntityName<Entity>, id: Primary<Entity>, options: Omit<GetReferenceOptions, 'wrapped' | 'key'> & {
|
|
389
405
|
wrapped: true;
|
|
390
406
|
}): Ref<Entity>;
|
|
391
407
|
/**
|
|
@@ -395,7 +411,7 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
|
|
|
395
411
|
/**
|
|
396
412
|
* Gets a reference to the entity identified by the given type and identifier without actually loading it, if the entity is not yet loaded
|
|
397
413
|
*/
|
|
398
|
-
getReference<Entity extends object>(entityName: EntityName<Entity>, id: Primary<Entity>, options: Omit<GetReferenceOptions, 'wrapped'> & {
|
|
414
|
+
getReference<Entity extends object>(entityName: EntityName<Entity>, id: Primary<Entity>, options: Omit<GetReferenceOptions, 'wrapped' | 'key'> & {
|
|
399
415
|
wrapped: false;
|
|
400
416
|
}): Entity;
|
|
401
417
|
/**
|
|
@@ -247,4 +247,9 @@ export interface GetReferenceOptions {
|
|
|
247
247
|
wrapped?: boolean;
|
|
248
248
|
convertCustomTypes?: boolean;
|
|
249
249
|
schema?: string;
|
|
250
|
+
/**
|
|
251
|
+
* Property name to use for identity map lookup instead of the primary key.
|
|
252
|
+
* This is useful for creating references by unique non-PK properties.
|
|
253
|
+
*/
|
|
254
|
+
key?: string;
|
|
250
255
|
}
|
|
@@ -16,6 +16,11 @@ export interface FactoryOptions {
|
|
|
16
16
|
schema?: string;
|
|
17
17
|
parentSchema?: string;
|
|
18
18
|
normalizeAccessors?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Property name to use for identity map lookup instead of the primary key.
|
|
21
|
+
* This is useful for creating references by unique non-PK properties.
|
|
22
|
+
*/
|
|
23
|
+
key?: string;
|
|
19
24
|
}
|
|
20
25
|
export declare class EntityFactory {
|
|
21
26
|
private readonly em;
|
|
@@ -29,7 +34,7 @@ export declare class EntityFactory {
|
|
|
29
34
|
constructor(em: EntityManager);
|
|
30
35
|
create<T extends object, P extends string = string>(entityName: EntityName<T>, data: EntityData<T>, options?: FactoryOptions): New<T, P>;
|
|
31
36
|
mergeData<T extends object>(meta: EntityMetadata<T>, entity: T, data: EntityData<T>, options?: FactoryOptions): void;
|
|
32
|
-
createReference<T extends object>(entityName: EntityName<T>, id: Primary<T> | Primary<T>[] | Record<string, Primary<T>>, options?: Pick<FactoryOptions, 'merge' | 'convertCustomTypes' | 'schema'>): T;
|
|
37
|
+
createReference<T extends object>(entityName: EntityName<T>, id: Primary<T> | Primary<T>[] | Record<string, Primary<T>>, options?: Pick<FactoryOptions, 'merge' | 'convertCustomTypes' | 'schema' | 'key'>): T;
|
|
33
38
|
createEmbeddable<T extends object>(entityName: EntityName<T>, data: EntityData<T>, options?: Pick<FactoryOptions, 'newEntity' | 'convertCustomTypes'>): T;
|
|
34
39
|
getComparator(): EntityComparator;
|
|
35
40
|
private createEntity;
|
package/entity/EntityFactory.js
CHANGED
|
@@ -164,6 +164,18 @@ export class EntityFactory {
|
|
|
164
164
|
options.convertCustomTypes ??= true;
|
|
165
165
|
const meta = this.metadata.get(entityName);
|
|
166
166
|
const schema = this.driver.getSchemaName(meta, options);
|
|
167
|
+
// Handle alternate key lookup
|
|
168
|
+
if (options.key) {
|
|
169
|
+
const value = '' + (Array.isArray(id) ? id[0] : Utils.isPlainObject(id) ? id[options.key] : id);
|
|
170
|
+
const exists = this.unitOfWork.getByKey(entityName, options.key, value, schema, options.convertCustomTypes);
|
|
171
|
+
if (exists) {
|
|
172
|
+
return exists;
|
|
173
|
+
}
|
|
174
|
+
// Create entity stub - storeByKey will set the alternate key property and store in identity map
|
|
175
|
+
const entity = this.create(entityName, {}, { ...options, initialized: false });
|
|
176
|
+
this.unitOfWork.storeByKey(entity, options.key, value, schema, options.convertCustomTypes);
|
|
177
|
+
return entity;
|
|
178
|
+
}
|
|
167
179
|
if (meta.simplePK) {
|
|
168
180
|
const exists = this.unitOfWork.getById(entityName, id, schema);
|
|
169
181
|
if (exists) {
|
package/entity/EntityLoader.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export type EntityLoaderOptions<Entity, Fields extends string = PopulatePath.ALL
|
|
|
15
15
|
convertCustomTypes?: boolean;
|
|
16
16
|
ignoreLazyScalarProperties?: boolean;
|
|
17
17
|
filters?: FilterOptions;
|
|
18
|
-
strategy?: LoadStrategy
|
|
18
|
+
strategy?: LoadStrategy | `${LoadStrategy}`;
|
|
19
19
|
lockMode?: Exclude<LockMode, LockMode.OPTIMISTIC>;
|
|
20
20
|
schema?: string;
|
|
21
21
|
connectionType?: ConnectionType;
|
package/entity/EntityLoader.js
CHANGED
|
@@ -211,7 +211,8 @@ export class EntityLoader {
|
|
|
211
211
|
async findChildren(entities, prop, populate, options, ref) {
|
|
212
212
|
const children = Utils.unique(this.getChildReferences(entities, prop, options, ref));
|
|
213
213
|
const meta = prop.targetMeta;
|
|
214
|
-
|
|
214
|
+
// When targetKey is set, use it for FK lookup instead of the PK
|
|
215
|
+
let fk = prop.targetKey ?? Utils.getPrimaryKeyHash(meta.primaryKeys);
|
|
215
216
|
let schema = options.schema;
|
|
216
217
|
const partial = !Utils.isEmpty(prop.where) || !Utils.isEmpty(options.where);
|
|
217
218
|
if (prop.kind === ReferenceKind.ONE_TO_MANY || (prop.kind === ReferenceKind.MANY_TO_MANY && !prop.owner)) {
|
|
@@ -228,7 +229,8 @@ export class EntityLoader {
|
|
|
228
229
|
if (!schema && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind)) {
|
|
229
230
|
schema = children.find(e => e.__helper.__schema)?.__helper.__schema;
|
|
230
231
|
}
|
|
231
|
-
|
|
232
|
+
// When targetKey is set, get the targetKey value instead of PK
|
|
233
|
+
const ids = Utils.unique(children.map(e => prop.targetKey ? e[prop.targetKey] : e.__helper.getPrimaryKey()));
|
|
232
234
|
let where = this.mergePrimaryCondition(ids, fk, options, meta, this.metadata, this.driver.getPlatform());
|
|
233
235
|
const fields = this.buildFields(options.fields, prop, ref);
|
|
234
236
|
/* eslint-disable prefer-const */
|
|
@@ -255,19 +257,44 @@ export class EntityLoader {
|
|
|
255
257
|
// @ts-ignore not a public option, will be propagated to the populate call
|
|
256
258
|
visited: options.visited,
|
|
257
259
|
});
|
|
260
|
+
// For targetKey relations, wire up loaded entities to parent references
|
|
261
|
+
// This is needed because the references were created under alternate key,
|
|
262
|
+
// but loaded entities are stored under PK, so they don't automatically merge
|
|
263
|
+
if (prop.targetKey && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind)) {
|
|
264
|
+
const itemsByKey = new Map();
|
|
265
|
+
for (const item of items) {
|
|
266
|
+
itemsByKey.set('' + item[prop.targetKey], item);
|
|
267
|
+
}
|
|
268
|
+
for (const entity of entities) {
|
|
269
|
+
const ref = entity[prop.name];
|
|
270
|
+
/* v8 ignore next */
|
|
271
|
+
if (!ref) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const keyValue = '' + (Reference.isReference(ref) ? ref.unwrap()[prop.targetKey] : ref[prop.targetKey]);
|
|
275
|
+
const loadedItem = itemsByKey.get(keyValue);
|
|
276
|
+
if (loadedItem) {
|
|
277
|
+
entity[prop.name] = (Reference.isReference(ref) ? Reference.create(loadedItem) : loadedItem);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
258
281
|
if ([ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind) && items.length !== children.length) {
|
|
259
282
|
const nullVal = this.em.config.get('forceUndefined') ? undefined : null;
|
|
260
283
|
const itemsMap = new Set();
|
|
261
284
|
const childrenMap = new Set();
|
|
285
|
+
// Use targetKey value if set, otherwise use serialized PK
|
|
286
|
+
const getKey = (e) => prop.targetKey ? '' + e[prop.targetKey] : helper(e).getSerializedPrimaryKey();
|
|
262
287
|
for (const item of items) {
|
|
263
|
-
|
|
288
|
+
/* v8 ignore next */
|
|
289
|
+
itemsMap.add(getKey(item));
|
|
264
290
|
}
|
|
265
291
|
for (const child of children) {
|
|
266
|
-
childrenMap.add(
|
|
292
|
+
childrenMap.add(getKey(child));
|
|
267
293
|
}
|
|
268
294
|
for (const entity of entities) {
|
|
269
|
-
const
|
|
270
|
-
|
|
295
|
+
const ref = entity[prop.name] ?? {};
|
|
296
|
+
const key = helper(ref) ? getKey(ref) : undefined;
|
|
297
|
+
if (key && childrenMap.has(key) && !itemsMap.has(key)) {
|
|
271
298
|
entity[prop.name] = nullVal;
|
|
272
299
|
helper(entity).__originalEntityData[prop.name] = null;
|
|
273
300
|
}
|
|
@@ -111,10 +111,26 @@ export declare class EntityRepository<Entity extends object> {
|
|
|
111
111
|
map(result: EntityDictionary<Entity>, options?: {
|
|
112
112
|
schema?: string;
|
|
113
113
|
}): Entity;
|
|
114
|
+
/**
|
|
115
|
+
* Gets a reference to the entity identified by the given type and alternate key property without actually loading it.
|
|
116
|
+
* The key option specifies which property to use for identity map lookup instead of the primary key.
|
|
117
|
+
*/
|
|
118
|
+
getReference<K extends string & keyof Entity>(id: Entity[K], options: Omit<GetReferenceOptions, 'key' | 'wrapped'> & {
|
|
119
|
+
key: K;
|
|
120
|
+
wrapped: true;
|
|
121
|
+
}): Ref<Entity>;
|
|
122
|
+
/**
|
|
123
|
+
* Gets a reference to the entity identified by the given type and alternate key property without actually loading it.
|
|
124
|
+
* The key option specifies which property to use for identity map lookup instead of the primary key.
|
|
125
|
+
*/
|
|
126
|
+
getReference<K extends string & keyof Entity>(id: Entity[K], options: Omit<GetReferenceOptions, 'key'> & {
|
|
127
|
+
key: K;
|
|
128
|
+
wrapped?: false;
|
|
129
|
+
}): Entity;
|
|
114
130
|
/**
|
|
115
131
|
* Gets a reference to the entity identified by the given type and identifier without actually loading it, if the entity is not yet loaded
|
|
116
132
|
*/
|
|
117
|
-
getReference(id: Primary<Entity>, options: Omit<GetReferenceOptions, 'wrapped'> & {
|
|
133
|
+
getReference(id: Primary<Entity>, options: Omit<GetReferenceOptions, 'wrapped' | 'key'> & {
|
|
118
134
|
wrapped: true;
|
|
119
135
|
}): Ref<Entity>;
|
|
120
136
|
/**
|
|
@@ -124,7 +140,7 @@ export declare class EntityRepository<Entity extends object> {
|
|
|
124
140
|
/**
|
|
125
141
|
* Gets a reference to the entity identified by the given type and identifier without actually loading it, if the entity is not yet loaded
|
|
126
142
|
*/
|
|
127
|
-
getReference(id: Primary<Entity>, options: Omit<GetReferenceOptions, 'wrapped'> & {
|
|
143
|
+
getReference(id: Primary<Entity>, options: Omit<GetReferenceOptions, 'wrapped' | 'key'> & {
|
|
128
144
|
wrapped: false;
|
|
129
145
|
}): Entity;
|
|
130
146
|
/**
|
package/entity/defineEntity.d.ts
CHANGED
|
@@ -339,6 +339,8 @@ export declare class UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys
|
|
|
339
339
|
referenceColumnName(referenceColumnName: string): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
|
|
340
340
|
/** Override the default database column name on the target entity (see {@doclink naming-strategy | Naming Strategy}). This option is suitable for composite keys, where one property is represented by multiple columns. */
|
|
341
341
|
referencedColumnNames(...referencedColumnNames: string[]): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
|
|
342
|
+
/** Specify the property name on the target entity that this FK references instead of the primary key. */
|
|
343
|
+
targetKey(targetKey: string): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
|
|
342
344
|
/** What to do when the target entity gets deleted. */
|
|
343
345
|
deleteRule(deleteRule: 'cascade' | 'no action' | 'set null' | 'set default' | AnyString): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
|
|
344
346
|
/** What to do when the reference to the target entity gets updated. */
|
package/entity/defineEntity.js
CHANGED
|
@@ -409,6 +409,10 @@ export class UniversalPropertyOptionsBuilder {
|
|
|
409
409
|
referencedColumnNames(...referencedColumnNames) {
|
|
410
410
|
return this.assignOptions({ referencedColumnNames });
|
|
411
411
|
}
|
|
412
|
+
/** Specify the property name on the target entity that this FK references instead of the primary key. */
|
|
413
|
+
targetKey(targetKey) {
|
|
414
|
+
return this.assignOptions({ targetKey });
|
|
415
|
+
}
|
|
412
416
|
/** What to do when the target entity gets deleted. */
|
|
413
417
|
deleteRule(deleteRule) {
|
|
414
418
|
return this.assignOptions({ deleteRule });
|
package/errors.d.ts
CHANGED
|
@@ -62,6 +62,9 @@ export declare class MetadataError<T extends AnyEntity = AnyEntity> extends Vali
|
|
|
62
62
|
static nonPersistentCompositeProp(meta: EntityMetadata, prop: EntityProperty): MetadataError<Partial<any>>;
|
|
63
63
|
static propertyTargetsEntityType(meta: EntityMetadata, prop: EntityProperty, target: EntityMetadata): MetadataError<Partial<any>>;
|
|
64
64
|
static fromMissingOption(meta: EntityMetadata, prop: EntityProperty, option: string): MetadataError<Partial<any>>;
|
|
65
|
+
static targetKeyOnManyToMany(meta: EntityMetadata, prop: EntityProperty): MetadataError<Partial<any>>;
|
|
66
|
+
static targetKeyNotUnique(meta: EntityMetadata, prop: EntityProperty): MetadataError<Partial<any>>;
|
|
67
|
+
static targetKeyNotFound(meta: EntityMetadata, prop: EntityProperty): MetadataError<Partial<any>>;
|
|
65
68
|
static dangerousPropertyName(meta: EntityMetadata, prop: EntityProperty): MetadataError<Partial<any>>;
|
|
66
69
|
private static fromMessage;
|
|
67
70
|
}
|
package/errors.js
CHANGED
|
@@ -213,6 +213,15 @@ export class MetadataError extends ValidationError {
|
|
|
213
213
|
static fromMissingOption(meta, prop, option) {
|
|
214
214
|
return this.fromMessage(meta, prop, `is missing '${option}' option`);
|
|
215
215
|
}
|
|
216
|
+
static targetKeyOnManyToMany(meta, prop) {
|
|
217
|
+
return this.fromMessage(meta, prop, `uses 'targetKey' option which is not supported for ManyToMany relations`);
|
|
218
|
+
}
|
|
219
|
+
static targetKeyNotUnique(meta, prop) {
|
|
220
|
+
return this.fromMessage(meta, prop, `has 'targetKey' set to '${prop.targetKey}', but ${prop.type}.${prop.targetKey} is not marked as unique. The target property must have a unique constraint.`);
|
|
221
|
+
}
|
|
222
|
+
static targetKeyNotFound(meta, prop) {
|
|
223
|
+
return this.fromMessage(meta, prop, `has 'targetKey' set to '${prop.targetKey}', but ${prop.type}.${prop.targetKey} does not exist`);
|
|
224
|
+
}
|
|
216
225
|
static dangerousPropertyName(meta, prop) {
|
|
217
226
|
return this.fromMessage(meta, prop, `uses a dangerous property name '${prop.name}' which could lead to prototype pollution. Please use a different property name.`);
|
|
218
227
|
}
|
|
@@ -137,11 +137,13 @@ export class ObjectHydrator extends Hydrator {
|
|
|
137
137
|
ret.push(` if (isPrimaryKey(data${dataKey}, true)) {`);
|
|
138
138
|
const targetKey = this.safeKey(`${prop.targetMeta.tableName}_${this.tmpIndex++}`);
|
|
139
139
|
context.set(targetKey, prop.targetMeta.class);
|
|
140
|
+
// When targetKey is set, pass the key option to createReference so it uses the alternate key
|
|
141
|
+
const keyOption = prop.targetKey ? `, key: '${prop.targetKey}'` : '';
|
|
140
142
|
if (prop.ref) {
|
|
141
|
-
ret.push(` entity${entityKey} = Reference.create(factory.createReference(${targetKey}, data${dataKey}, { merge: true, convertCustomTypes, normalizeAccessors, schema }));`);
|
|
143
|
+
ret.push(` entity${entityKey} = Reference.create(factory.createReference(${targetKey}, data${dataKey}, { merge: true, convertCustomTypes, normalizeAccessors, schema${keyOption} }));`);
|
|
142
144
|
}
|
|
143
145
|
else {
|
|
144
|
-
ret.push(` entity${entityKey} = factory.createReference(${targetKey}, data${dataKey}, { merge: true, convertCustomTypes, normalizeAccessors, schema });`);
|
|
146
|
+
ret.push(` entity${entityKey} = factory.createReference(${targetKey}, data${dataKey}, { merge: true, convertCustomTypes, normalizeAccessors, schema${keyOption} });`);
|
|
145
147
|
}
|
|
146
148
|
ret.push(` } else if (data${dataKey} && typeof data${dataKey} === 'object') {`);
|
|
147
149
|
if (prop.ref) {
|
|
@@ -438,7 +438,15 @@ export class MetadataDiscovery {
|
|
|
438
438
|
}
|
|
439
439
|
initManyToOneFields(prop) {
|
|
440
440
|
const meta2 = prop.targetMeta;
|
|
441
|
-
|
|
441
|
+
let fieldNames;
|
|
442
|
+
// If targetKey is specified, use that property's field names instead of PKs
|
|
443
|
+
if (prop.targetKey) {
|
|
444
|
+
const targetProp = meta2.properties[prop.targetKey];
|
|
445
|
+
fieldNames = targetProp.fieldNames;
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
fieldNames = Utils.flatten(meta2.primaryKeys.map(primaryKey => meta2.properties[primaryKey].fieldNames));
|
|
449
|
+
}
|
|
442
450
|
Utils.defaultValue(prop, 'referencedTableName', meta2.tableName);
|
|
443
451
|
if (!prop.joinColumns) {
|
|
444
452
|
prop.joinColumns = fieldNames.map(fieldName => this.namingStrategy.joinKeyColumnName(prop.name, fieldName, fieldNames.length > 1));
|
|
@@ -1238,7 +1246,8 @@ export class MetadataDiscovery {
|
|
|
1238
1246
|
}
|
|
1239
1247
|
// when the target is a polymorphic embedded entity, `prop.target` is an array of classes, we need to get the metadata by the type name instead
|
|
1240
1248
|
const meta2 = this.metadata.find(prop.target) ?? this.metadata.getByClassName(prop.type);
|
|
1241
|
-
|
|
1249
|
+
// If targetKey is specified, use that property instead of PKs for referencedPKs
|
|
1250
|
+
prop.referencedPKs = prop.targetKey ? [prop.targetKey] : meta2.primaryKeys;
|
|
1242
1251
|
prop.targetMeta = meta2;
|
|
1243
1252
|
if (!prop.formula && prop.persist === false && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && !prop.embedded) {
|
|
1244
1253
|
prop.formula = table => `${table}.${this.platform.quoteIdentifier(prop.fieldNames[0])}`;
|
|
@@ -1246,10 +1255,14 @@ export class MetadataDiscovery {
|
|
|
1246
1255
|
}
|
|
1247
1256
|
initColumnType(prop) {
|
|
1248
1257
|
this.initUnsigned(prop);
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
prop.
|
|
1252
|
-
|
|
1258
|
+
// Get the target properties for FK relations - use targetKey property if specified, otherwise PKs
|
|
1259
|
+
const targetProps = prop.targetMeta
|
|
1260
|
+
? (prop.targetKey ? [prop.targetMeta.properties[prop.targetKey]] : prop.targetMeta.getPrimaryProps())
|
|
1261
|
+
: [];
|
|
1262
|
+
targetProps.map(targetProp => {
|
|
1263
|
+
prop.length ??= targetProp.length;
|
|
1264
|
+
prop.precision ??= targetProp.precision;
|
|
1265
|
+
prop.scale ??= targetProp.scale;
|
|
1253
1266
|
});
|
|
1254
1267
|
if (prop.kind === ReferenceKind.SCALAR && (prop.type == null || prop.type === 'object') && prop.columnTypes?.[0]) {
|
|
1255
1268
|
delete prop.type;
|
|
@@ -1282,17 +1295,21 @@ export class MetadataDiscovery {
|
|
|
1282
1295
|
}
|
|
1283
1296
|
const targetMeta = prop.targetMeta;
|
|
1284
1297
|
prop.columnTypes = [];
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1298
|
+
// Use targetKey property if specified, otherwise use primary key properties
|
|
1299
|
+
const referencedProps = prop.targetKey
|
|
1300
|
+
? [targetMeta.properties[prop.targetKey]]
|
|
1301
|
+
: targetMeta.getPrimaryProps();
|
|
1302
|
+
for (const referencedProp of referencedProps) {
|
|
1303
|
+
this.initCustomType(targetMeta, referencedProp);
|
|
1304
|
+
this.initColumnType(referencedProp);
|
|
1305
|
+
const mappedType = this.getMappedType(referencedProp);
|
|
1306
|
+
let columnTypes = referencedProp.columnTypes;
|
|
1307
|
+
if (referencedProp.autoincrement) {
|
|
1308
|
+
columnTypes = [mappedType.getColumnType({ ...referencedProp, autoincrement: false }, this.platform)];
|
|
1292
1309
|
}
|
|
1293
1310
|
prop.columnTypes.push(...columnTypes);
|
|
1294
|
-
if (!targetMeta.compositePK) {
|
|
1295
|
-
prop.customType =
|
|
1311
|
+
if (!targetMeta.compositePK || prop.targetKey) {
|
|
1312
|
+
prop.customType = referencedProp.customType;
|
|
1296
1313
|
}
|
|
1297
1314
|
}
|
|
1298
1315
|
}
|
|
@@ -8,6 +8,7 @@ export declare class MetadataValidator {
|
|
|
8
8
|
validateEntityDefinition<T>(metadata: MetadataStorage, name: EntityName<T>, options: MetadataDiscoveryOptions): void;
|
|
9
9
|
validateDiscovered(discovered: EntityMetadata[], options: MetadataDiscoveryOptions): void;
|
|
10
10
|
private validateReference;
|
|
11
|
+
private validateTargetKey;
|
|
11
12
|
private validateBidirectional;
|
|
12
13
|
private validateOwningSide;
|
|
13
14
|
private validateInverseSide;
|
|
@@ -110,6 +110,25 @@ export class MetadataValidator {
|
|
|
110
110
|
if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && prop.persist === false && targetMeta.compositePK && options.checkNonPersistentCompositeProps) {
|
|
111
111
|
throw MetadataError.nonPersistentCompositeProp(meta, prop);
|
|
112
112
|
}
|
|
113
|
+
this.validateTargetKey(meta, prop, targetMeta);
|
|
114
|
+
}
|
|
115
|
+
validateTargetKey(meta, prop, targetMeta) {
|
|
116
|
+
if (!prop.targetKey) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// targetKey is not supported for ManyToMany relations
|
|
120
|
+
if (prop.kind === ReferenceKind.MANY_TO_MANY) {
|
|
121
|
+
throw MetadataError.targetKeyOnManyToMany(meta, prop);
|
|
122
|
+
}
|
|
123
|
+
// targetKey must point to an existing property
|
|
124
|
+
const targetProp = targetMeta.properties[prop.targetKey];
|
|
125
|
+
if (!targetProp) {
|
|
126
|
+
throw MetadataError.targetKeyNotFound(meta, prop);
|
|
127
|
+
}
|
|
128
|
+
// targetKey must point to a unique property
|
|
129
|
+
if (!targetProp.unique && !targetMeta.uniques?.some(u => u.properties?.includes(prop.targetKey))) {
|
|
130
|
+
throw MetadataError.targetKeyNotUnique(meta, prop);
|
|
131
|
+
}
|
|
113
132
|
}
|
|
114
133
|
validateBidirectional(meta, prop) {
|
|
115
134
|
if (prop.inversedBy) {
|
package/metadata/types.d.ts
CHANGED
|
@@ -342,6 +342,8 @@ export interface ManyToOneOptions<Owner, Target> extends ReferenceOptions<Owner,
|
|
|
342
342
|
referenceColumnName?: string;
|
|
343
343
|
/** Override the default database column name on the target entity (see {@doclink naming-strategy | Naming Strategy}). This option is suitable for composite keys, where one property is represented by multiple columns. */
|
|
344
344
|
referencedColumnNames?: string[];
|
|
345
|
+
/** Specify the property name on the target entity that this FK references instead of the primary key. */
|
|
346
|
+
targetKey?: string & keyof Target;
|
|
345
347
|
/** What to do when the target entity gets deleted. */
|
|
346
348
|
deleteRule?: 'cascade' | 'no action' | 'set null' | 'set default' | AnyString;
|
|
347
349
|
/** What to do when the reference to the target entity gets updated. */
|
|
@@ -388,6 +390,8 @@ export interface OneToOneOptions<Owner, Target> extends Partial<Omit<OneToManyOp
|
|
|
388
390
|
mapToPk?: boolean;
|
|
389
391
|
/** When a part of a composite column is shared in other properties, use this option to specify what columns are considered as owned by this property. This is useful when your composite property is nullable, but parts of it are not. */
|
|
390
392
|
ownColumns?: string[];
|
|
393
|
+
/** Specify the property name on the target entity that this FK references instead of the primary key. */
|
|
394
|
+
targetKey?: string & keyof Target;
|
|
391
395
|
/** What to do when the target entity gets deleted. */
|
|
392
396
|
deleteRule?: 'cascade' | 'no action' | 'set null' | 'set default' | AnyString;
|
|
393
397
|
/** What to do when the reference to the target entity gets updated. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "7.0.0-dev.
|
|
4
|
+
"version": "7.0.0-dev.158",
|
|
5
5
|
"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.",
|
|
6
6
|
"exports": {
|
|
7
7
|
"./package.json": "./package.json",
|
package/typings.d.ts
CHANGED
|
@@ -422,6 +422,7 @@ export interface EntityProperty<Owner = any, Target = any> {
|
|
|
422
422
|
referencedColumnNames: string[];
|
|
423
423
|
referencedTableName: string;
|
|
424
424
|
referencedPKs: EntityKey<Owner>[];
|
|
425
|
+
targetKey?: string;
|
|
425
426
|
serializer?: (value: any, options?: SerializeOptions<any>) => any;
|
|
426
427
|
serializedName?: string;
|
|
427
428
|
comment?: string;
|
|
@@ -132,7 +132,16 @@ export class ChangeSetComputer {
|
|
|
132
132
|
const targets = Utils.unwrapProperty(changeSet.entity, changeSet.meta, prop);
|
|
133
133
|
targets.forEach(([target, idx]) => {
|
|
134
134
|
if (!target.__helper.hasPrimaryKey()) {
|
|
135
|
-
|
|
135
|
+
// When targetKey is set, use that property value instead of the PK identifier
|
|
136
|
+
let value = prop.targetKey ? target[prop.targetKey] : target.__helper.__identifier;
|
|
137
|
+
/* v8 ignore next */
|
|
138
|
+
if (prop.targetKey && prop.targetMeta) {
|
|
139
|
+
const targetProp = prop.targetMeta.properties[prop.targetKey];
|
|
140
|
+
if (targetProp?.customType) {
|
|
141
|
+
value = targetProp.customType.convertToDatabaseValue(value, this.platform, { mode: 'serialization' });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
Utils.setPayloadProperty(changeSet.payload, changeSet.meta, prop, value, idx);
|
|
136
145
|
}
|
|
137
146
|
});
|
|
138
147
|
}
|
|
@@ -145,7 +154,9 @@ export class ChangeSetComputer {
|
|
|
145
154
|
this.collectionUpdates.add(target);
|
|
146
155
|
}
|
|
147
156
|
if (prop.owner && !this.platform.usesPivotTable()) {
|
|
148
|
-
changeSet.payload[prop.name] = target.getItems(false).map((item) =>
|
|
157
|
+
changeSet.payload[prop.name] = target.getItems(false).map((item) => {
|
|
158
|
+
return item.__helper.__identifier ?? item.__helper.getPrimaryKey();
|
|
159
|
+
});
|
|
149
160
|
}
|
|
150
161
|
}
|
|
151
162
|
}
|
|
@@ -3,7 +3,14 @@ export declare class IdentityMap {
|
|
|
3
3
|
private readonly defaultSchema?;
|
|
4
4
|
constructor(defaultSchema?: string | undefined);
|
|
5
5
|
private readonly registry;
|
|
6
|
+
/** Tracks alternate key hashes for each entity so we can clean them up on delete */
|
|
7
|
+
private readonly alternateKeys;
|
|
6
8
|
store<T>(item: T): void;
|
|
9
|
+
/**
|
|
10
|
+
* Stores an entity under an alternate key (non-PK property).
|
|
11
|
+
* This allows looking up entities by unique properties that are not the primary key.
|
|
12
|
+
*/
|
|
13
|
+
storeByKey<T>(item: T, key: string, value: string, schema?: string): void;
|
|
7
14
|
delete<T>(item: T): void;
|
|
8
15
|
getByHash<T>(meta: EntityMetadata<T>, hash: string): T | undefined;
|
|
9
16
|
getStore<T>(meta: EntityMetadata<T>): Map<string, T>;
|
|
@@ -16,4 +23,9 @@ export declare class IdentityMap {
|
|
|
16
23
|
*/
|
|
17
24
|
get<T>(hash: string): T | undefined;
|
|
18
25
|
private getPkHash;
|
|
26
|
+
/**
|
|
27
|
+
* Creates a hash for an alternate key lookup.
|
|
28
|
+
* Format: `[key]value` or `schema:[key]value`
|
|
29
|
+
*/
|
|
30
|
+
getKeyHash(key: string, value: string, schema?: string): string;
|
|
19
31
|
}
|
|
@@ -4,11 +4,38 @@ export class IdentityMap {
|
|
|
4
4
|
this.defaultSchema = defaultSchema;
|
|
5
5
|
}
|
|
6
6
|
registry = new Map();
|
|
7
|
+
/** Tracks alternate key hashes for each entity so we can clean them up on delete */
|
|
8
|
+
alternateKeys = new WeakMap();
|
|
7
9
|
store(item) {
|
|
8
10
|
this.getStore(item.__meta.root).set(this.getPkHash(item), item);
|
|
9
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Stores an entity under an alternate key (non-PK property).
|
|
14
|
+
* This allows looking up entities by unique properties that are not the primary key.
|
|
15
|
+
*/
|
|
16
|
+
storeByKey(item, key, value, schema) {
|
|
17
|
+
const hash = this.getKeyHash(key, value, schema);
|
|
18
|
+
this.getStore(item.__meta.root).set(hash, item);
|
|
19
|
+
// Track this alternate key so we can clean it up when the entity is deleted
|
|
20
|
+
let keys = this.alternateKeys.get(item);
|
|
21
|
+
if (!keys) {
|
|
22
|
+
keys = new Set();
|
|
23
|
+
this.alternateKeys.set(item, keys);
|
|
24
|
+
}
|
|
25
|
+
keys.add(hash);
|
|
26
|
+
}
|
|
10
27
|
delete(item) {
|
|
11
|
-
|
|
28
|
+
const meta = item.__meta.root;
|
|
29
|
+
const store = this.getStore(meta);
|
|
30
|
+
store.delete(this.getPkHash(item));
|
|
31
|
+
// Also delete any alternate key entries for this entity
|
|
32
|
+
const altKeys = this.alternateKeys.get(item);
|
|
33
|
+
if (altKeys) {
|
|
34
|
+
for (const hash of altKeys) {
|
|
35
|
+
store.delete(hash);
|
|
36
|
+
}
|
|
37
|
+
this.alternateKeys.delete(item);
|
|
38
|
+
}
|
|
12
39
|
}
|
|
13
40
|
getByHash(meta, hash) {
|
|
14
41
|
const store = this.getStore(meta);
|
|
@@ -69,4 +96,15 @@ export class IdentityMap {
|
|
|
69
96
|
}
|
|
70
97
|
return hash;
|
|
71
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Creates a hash for an alternate key lookup.
|
|
101
|
+
* Format: `[key]value` or `schema:[key]value`
|
|
102
|
+
*/
|
|
103
|
+
getKeyHash(key, value, schema) {
|
|
104
|
+
const hash = `[${key}]${value}`;
|
|
105
|
+
if (schema) {
|
|
106
|
+
return schema + ':' + hash;
|
|
107
|
+
}
|
|
108
|
+
return hash;
|
|
109
|
+
}
|
|
72
110
|
}
|
|
@@ -46,6 +46,19 @@ export declare class UnitOfWork {
|
|
|
46
46
|
* Returns entity from the identity map. For composite keys, you need to pass an array of PKs in the same order as they are defined in `meta.primaryKeys`.
|
|
47
47
|
*/
|
|
48
48
|
getById<T extends object>(entityName: EntityName<T>, id: Primary<T> | Primary<T>[], schema?: string, convertCustomTypes?: boolean): T | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Returns entity from the identity map by an alternate key (non-PK property).
|
|
51
|
+
* @param convertCustomTypes - If true, the value is in database format and will be converted to JS format for lookup.
|
|
52
|
+
* If false (default), the value is assumed to be in JS format already.
|
|
53
|
+
*/
|
|
54
|
+
getByKey<T extends object>(entityName: EntityName<T>, key: string, value: unknown, schema?: string, convertCustomTypes?: boolean): T | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Stores an entity in the identity map under an alternate key (non-PK property).
|
|
57
|
+
* Also sets the property value on the entity.
|
|
58
|
+
* @param convertCustomTypes - If true, the value is in database format and will be converted to JS format.
|
|
59
|
+
* If false (default), the value is assumed to be in JS format already.
|
|
60
|
+
*/
|
|
61
|
+
storeByKey<T extends object>(entity: T, key: string, value: unknown, schema?: string, convertCustomTypes?: boolean): void;
|
|
49
62
|
tryGetById<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, schema?: string, strict?: boolean): T | null;
|
|
50
63
|
/**
|
|
51
64
|
* Returns map of all managed entities.
|
|
@@ -169,6 +169,40 @@ export class UnitOfWork {
|
|
|
169
169
|
}
|
|
170
170
|
return this.identityMap.getByHash(meta, hash);
|
|
171
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Returns entity from the identity map by an alternate key (non-PK property).
|
|
174
|
+
* @param convertCustomTypes - If true, the value is in database format and will be converted to JS format for lookup.
|
|
175
|
+
* If false (default), the value is assumed to be in JS format already.
|
|
176
|
+
*/
|
|
177
|
+
getByKey(entityName, key, value, schema, convertCustomTypes) {
|
|
178
|
+
const meta = this.metadata.find(entityName).root;
|
|
179
|
+
schema ??= meta.schema ?? this.em.config.getSchema();
|
|
180
|
+
const prop = meta.properties[key];
|
|
181
|
+
// Convert from DB format to JS format if needed
|
|
182
|
+
if (convertCustomTypes && prop?.customType) {
|
|
183
|
+
value = prop.customType.convertToJSValue(value, this.platform, { mode: 'hydration' });
|
|
184
|
+
}
|
|
185
|
+
const hash = this.identityMap.getKeyHash(key, '' + value, schema);
|
|
186
|
+
return this.identityMap.getByHash(meta, hash);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Stores an entity in the identity map under an alternate key (non-PK property).
|
|
190
|
+
* Also sets the property value on the entity.
|
|
191
|
+
* @param convertCustomTypes - If true, the value is in database format and will be converted to JS format.
|
|
192
|
+
* If false (default), the value is assumed to be in JS format already.
|
|
193
|
+
*/
|
|
194
|
+
storeByKey(entity, key, value, schema, convertCustomTypes) {
|
|
195
|
+
const meta = entity.__meta.root;
|
|
196
|
+
schema ??= meta.schema ?? this.em.config.getSchema();
|
|
197
|
+
const prop = meta.properties[key];
|
|
198
|
+
// Convert from DB format to JS format if needed
|
|
199
|
+
if (convertCustomTypes && prop?.customType) {
|
|
200
|
+
value = prop.customType.convertToJSValue(value, this.platform, { mode: 'hydration' });
|
|
201
|
+
}
|
|
202
|
+
// Set the property on the entity
|
|
203
|
+
entity[key] = value;
|
|
204
|
+
this.identityMap.storeByKey(entity, key, '' + value, schema);
|
|
205
|
+
}
|
|
172
206
|
tryGetById(entityName, where, schema, strict = true) {
|
|
173
207
|
const pk = Utils.extractPK(where, this.metadata.find(entityName), strict);
|
|
174
208
|
if (!pk) {
|
|
@@ -510,6 +510,23 @@ export class EntityComparator {
|
|
|
510
510
|
ret += ` ret${dataKey} = entity${entityKey};\n`;
|
|
511
511
|
}
|
|
512
512
|
}
|
|
513
|
+
else if (prop.targetKey) {
|
|
514
|
+
// When targetKey is set, extract that property value instead of the PK
|
|
515
|
+
const targetProp = prop.targetMeta?.properties[prop.targetKey];
|
|
516
|
+
ret += ` if (entity${entityKey} === null) {\n`;
|
|
517
|
+
ret += ` ret${dataKey} = null;\n`;
|
|
518
|
+
ret += ` } else if (typeof entity${entityKey} !== 'undefined') {\n`;
|
|
519
|
+
ret += ` const val${level} = entity${entityKey}${unwrap};\n`;
|
|
520
|
+
if (targetProp?.customType) {
|
|
521
|
+
// If targetKey property has a custom type, convert to database value
|
|
522
|
+
const convertorKey = this.registerCustomType(targetProp, context);
|
|
523
|
+
ret += ` ret${dataKey} = convertToDatabaseValue_${convertorKey}(val${level}?.${prop.targetKey});\n`;
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
ret += ` ret${dataKey} = val${level}?.${prop.targetKey};\n`;
|
|
527
|
+
}
|
|
528
|
+
ret += ` }\n`;
|
|
529
|
+
}
|
|
513
530
|
else {
|
|
514
531
|
const toArray = (val) => {
|
|
515
532
|
if (Utils.isPlainObject(val)) {
|
package/utils/Utils.js
CHANGED
|
@@ -123,7 +123,7 @@ export function parseJsonSafe(value) {
|
|
|
123
123
|
}
|
|
124
124
|
export class Utils {
|
|
125
125
|
static PK_SEPARATOR = '~~~';
|
|
126
|
-
static #ORM_VERSION = '7.0.0-dev.
|
|
126
|
+
static #ORM_VERSION = '7.0.0-dev.158';
|
|
127
127
|
/**
|
|
128
128
|
* Checks if the argument is instance of `Object`. Returns false for arrays.
|
|
129
129
|
*/
|