@mikro-orm/core 7.0.0-dev.224 → 7.0.0-dev.226

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 CHANGED
@@ -325,8 +325,10 @@ export class EntityManager {
325
325
  return ret;
326
326
  }
327
327
  async getJoinedFilters(meta, options) {
328
+ // If user provided populateFilter, merge it with computed filters
329
+ const userFilter = options.populateFilter;
328
330
  if (!this.config.get('filtersOnRelations') || !options.populate) {
329
- return undefined;
331
+ return userFilter;
330
332
  }
331
333
  const ret = {};
332
334
  for (const hint of options.populate) {
@@ -360,7 +362,11 @@ export class EntityManager {
360
362
  }
361
363
  }
362
364
  }
363
- return ret;
365
+ // Merge user-provided populateFilter with computed filters
366
+ if (userFilter) {
367
+ Utils.merge(ret, userFilter);
368
+ }
369
+ return Utils.hasObjectKeys(ret) ? ret : undefined;
364
370
  }
365
371
  /**
366
372
  * When filters are active on M:1 or 1:1 relations, we need to ref join them eagerly as they might affect the FK value.
@@ -53,7 +53,15 @@ export class FileCacheAdapter {
53
53
  clear() {
54
54
  const path = this.path('*');
55
55
  const files = fs.glob(path);
56
- files.forEach(file => unlinkSync(file));
56
+ for (const file of files) {
57
+ /* v8 ignore next */
58
+ try {
59
+ unlinkSync(file);
60
+ }
61
+ catch {
62
+ // ignore if file is already gone
63
+ }
64
+ }
57
65
  this.cache = {};
58
66
  }
59
67
  combine() {
@@ -8,6 +8,7 @@ import { EntityManager } from '../EntityManager.js';
8
8
  import { CursorError, ValidationError } from '../errors.js';
9
9
  import { DriverException } from '../exceptions.js';
10
10
  import { helper } from '../entity/wrap.js';
11
+ import { PolymorphicRef } from '../entity/PolymorphicRef.js';
11
12
  import { JsonType } from '../types/JsonType.js';
12
13
  import { MikroORM } from '../MikroORM.js';
13
14
  export class DatabaseDriver {
@@ -260,6 +261,43 @@ export class DatabaseDriver {
260
261
  }
261
262
  return;
262
263
  }
264
+ // Handle polymorphic relations - convert tuple or PolymorphicRef to separate columns
265
+ // Tuple format: ['discriminator', id] or ['discriminator', id1, id2] for composite keys
266
+ // Must be checked BEFORE joinColumns array handling since polymorphic uses fieldNames (includes discriminator)
267
+ if (prop.polymorphic && prop.fieldNames && prop.fieldNames.length >= 2) {
268
+ let discriminator;
269
+ let ids;
270
+ if (Array.isArray(data[k]) && typeof data[k][0] === 'string' && prop.discriminatorMap?.[data[k][0]]) {
271
+ // Tuple format: ['discriminator', ...ids]
272
+ const [disc, ...rest] = data[k];
273
+ discriminator = disc;
274
+ ids = rest;
275
+ }
276
+ else if (data[k] instanceof PolymorphicRef) {
277
+ // PolymorphicRef wrapper (internal use)
278
+ discriminator = data[k].discriminator;
279
+ const polyId = data[k].id;
280
+ // Handle object-style composite key IDs like { tenantId: 1, orgId: 100 }
281
+ if (polyId && typeof polyId === 'object' && !Array.isArray(polyId)) {
282
+ const targetEntity = prop.discriminatorMap?.[discriminator];
283
+ const targetMeta = this.metadata.get(targetEntity);
284
+ ids = targetMeta.primaryKeys.map(pk => polyId[pk]);
285
+ }
286
+ else {
287
+ ids = Utils.asArray(polyId);
288
+ }
289
+ }
290
+ if (discriminator) {
291
+ const discriminatorColumn = prop.fieldNames[0];
292
+ const idColumns = prop.fieldNames.slice(1);
293
+ delete data[k];
294
+ data[discriminatorColumn] = discriminator;
295
+ idColumns.forEach((col, idx) => {
296
+ data[col] = ids[idx];
297
+ });
298
+ return;
299
+ }
300
+ }
263
301
  if (prop.joinColumns && Array.isArray(data[k])) {
264
302
  const copy = Utils.flatten(data[k]);
265
303
  delete data[k];
@@ -176,7 +176,19 @@ export class EntityHelper {
176
176
  });
177
177
  }
178
178
  static propagate(meta, entity, owner, prop, value, old) {
179
- for (const prop2 of prop.targetMeta.bidirectionalRelations) {
179
+ // For polymorphic relations, get bidirectional relations from the actual entity's metadata
180
+ let bidirectionalRelations;
181
+ if (prop.polymorphic && prop.polymorphTargets?.length) {
182
+ // For polymorphic relations, we need to get the bidirectional relations from the actual value's metadata
183
+ if (!value) {
184
+ return; // No value means no propagation needed
185
+ }
186
+ bidirectionalRelations = helper(value).__meta.bidirectionalRelations;
187
+ }
188
+ else {
189
+ bidirectionalRelations = prop.targetMeta.bidirectionalRelations;
190
+ }
191
+ for (const prop2 of bidirectionalRelations) {
180
192
  if ((prop2.inversedBy || prop2.mappedBy) !== prop.name) {
181
193
  continue;
182
194
  }
@@ -43,6 +43,7 @@ export declare class EntityLoader {
43
43
  */
44
44
  private populateMany;
45
45
  private populateScalar;
46
+ private populatePolymorphic;
46
47
  private initializeCollections;
47
48
  private initializeOneToMany;
48
49
  private initializeManyToMany;
@@ -144,6 +144,9 @@ export class EntityLoader {
144
144
  const res = await this.findChildrenFromPivotTable(filtered, prop, options, innerOrderBy, populate, !!ref);
145
145
  return Utils.flatten(res);
146
146
  }
147
+ if (prop.polymorphic && prop.polymorphTargets) {
148
+ return this.populatePolymorphic(entities, prop, options, !!ref);
149
+ }
147
150
  const { items, partial } = await this.findChildren(options.filtered ?? entities, prop, populate, {
148
151
  ...options,
149
152
  where,
@@ -163,6 +166,64 @@ export class EntityLoader {
163
166
  populate: [],
164
167
  });
165
168
  }
169
+ async populatePolymorphic(entities, prop, options, ref) {
170
+ const ownerMeta = this.metadata.get(entities[0].constructor);
171
+ // Separate entities: those with loaded refs vs those needing FK load
172
+ const toPopulate = [];
173
+ const needsFkLoad = [];
174
+ for (const entity of entities) {
175
+ const refValue = entity[prop.name];
176
+ if (refValue && helper(refValue).hasPrimaryKey()) {
177
+ if ((ref && !options.refresh) || // :ref hint - already have reference
178
+ (!ref && helper(refValue).__initialized && !options.refresh) // already loaded
179
+ ) {
180
+ continue;
181
+ }
182
+ toPopulate.push(entity);
183
+ }
184
+ else if (refValue == null && !helper(entity).__loadedProperties.has(prop.name)) {
185
+ // FK columns weren't loaded (partial loading) — need to re-fetch them.
186
+ // If the property IS in __loadedProperties, the FK was loaded and is genuinely null.
187
+ needsFkLoad.push(entity);
188
+ }
189
+ }
190
+ // Load FK columns using populateScalar pattern
191
+ if (needsFkLoad.length > 0) {
192
+ await this.populateScalar(ownerMeta, needsFkLoad, {
193
+ ...options,
194
+ fields: [...ownerMeta.primaryKeys, prop.name],
195
+ });
196
+ // After loading FKs, add to toPopulate if not using :ref hint
197
+ if (!ref) {
198
+ for (const entity of needsFkLoad) {
199
+ const refValue = entity[prop.name];
200
+ if (refValue && helper(refValue).hasPrimaryKey()) {
201
+ toPopulate.push(entity);
202
+ }
203
+ }
204
+ }
205
+ }
206
+ if (toPopulate.length === 0) {
207
+ return [];
208
+ }
209
+ // Group references by target class for batch loading
210
+ const groups = new Map();
211
+ for (const entity of toPopulate) {
212
+ const refValue = Reference.unwrapReference(entity[prop.name]);
213
+ const discriminator = QueryHelper.findDiscriminatorValue(prop.discriminatorMap, helper(refValue).__meta.class);
214
+ const group = groups.get(discriminator) ?? [];
215
+ group.push(refValue);
216
+ groups.set(discriminator, group);
217
+ }
218
+ // Load each group concurrently - identity map handles merging with existing references
219
+ const allItems = [];
220
+ await Promise.all([...groups].map(async ([discriminator, children]) => {
221
+ const targetMeta = this.metadata.find(prop.discriminatorMap[discriminator]);
222
+ await this.populateScalar(targetMeta, children, options);
223
+ allItems.push(...children);
224
+ }));
225
+ return allItems;
226
+ }
166
227
  initializeCollections(filtered, prop, field, children, customOrder, partial) {
167
228
  if (prop.kind === ReferenceKind.ONE_TO_MANY) {
168
229
  this.initializeOneToMany(filtered, children, prop, field, partial);
@@ -215,8 +276,17 @@ export class EntityLoader {
215
276
  let fk = prop.targetKey ?? Utils.getPrimaryKeyHash(meta.primaryKeys);
216
277
  let schema = options.schema;
217
278
  const partial = !Utils.isEmpty(prop.where) || !Utils.isEmpty(options.where);
279
+ let polymorphicOwnerProp;
218
280
  if (prop.kind === ReferenceKind.ONE_TO_MANY || (prop.kind === ReferenceKind.MANY_TO_MANY && !prop.owner)) {
219
- fk = meta.properties[prop.mappedBy].name;
281
+ const ownerProp = meta.properties[prop.mappedBy];
282
+ if (ownerProp.polymorphic && ownerProp.fieldNames.length >= 2) {
283
+ const idColumns = ownerProp.fieldNames.slice(1);
284
+ fk = idColumns.length === 1 ? idColumns[0] : idColumns;
285
+ polymorphicOwnerProp = ownerProp;
286
+ }
287
+ else {
288
+ fk = ownerProp.name;
289
+ }
220
290
  }
221
291
  if (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner && !ref) {
222
292
  children.length = 0;
@@ -229,9 +299,24 @@ export class EntityLoader {
229
299
  if (!schema && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind)) {
230
300
  schema = children.find(e => e.__helper.__schema)?.__helper.__schema;
231
301
  }
232
- // When targetKey is set, get the targetKey value instead of PK
233
302
  const ids = Utils.unique(children.map(e => prop.targetKey ? e[prop.targetKey] : e.__helper.getPrimaryKey()));
234
- let where = this.mergePrimaryCondition(ids, fk, options, meta, this.metadata, this.driver.getPlatform());
303
+ let where;
304
+ if (polymorphicOwnerProp && Array.isArray(fk)) {
305
+ const conditions = ids.map(id => {
306
+ const pkValues = Object.values(id);
307
+ return Object.fromEntries(fk.map((col, idx) => [col, pkValues[idx]]));
308
+ });
309
+ where = (conditions.length === 1 ? conditions[0] : { $or: conditions });
310
+ }
311
+ else {
312
+ where = this.mergePrimaryCondition(ids, fk, options, meta, this.metadata, this.driver.getPlatform());
313
+ }
314
+ if (polymorphicOwnerProp) {
315
+ const parentMeta = this.metadata.find(entities[0].constructor);
316
+ const discriminatorValue = QueryHelper.findDiscriminatorValue(polymorphicOwnerProp.discriminatorMap, parentMeta.class) ?? parentMeta.tableName;
317
+ const discriminatorColumn = polymorphicOwnerProp.fieldNames[0];
318
+ where = { $and: [where, { [discriminatorColumn]: discriminatorValue }] };
319
+ }
235
320
  const fields = this.buildFields(options.fields, prop, ref);
236
321
  /* eslint-disable prefer-const */
237
322
  let { refresh, filters, convertCustomTypes, lockMode, strategy, populateWhere = 'infer', connectionType, logging, } = options;
@@ -362,30 +447,42 @@ export class EntityLoader {
362
447
  for (const entity of entities) {
363
448
  visited.add(entity);
364
449
  }
365
- // skip lazy scalar properties
366
450
  if (!prop.targetMeta) {
367
451
  return;
368
452
  }
369
- await this.populate(prop.targetMeta.class, unique, populate.children ?? populate.all, {
370
- where: await this.extractChildCondition(options, prop, false),
371
- orderBy: innerOrderBy,
372
- fields,
373
- exclude,
374
- validate: false,
375
- lookup: false,
376
- filters,
377
- ignoreLazyScalarProperties,
378
- populateWhere,
379
- connectionType,
380
- logging,
381
- schema,
382
- // @ts-ignore not a public option, will be propagated to the populate call
383
- refresh: refresh && !filtered.every(item => options.visited.has(item)),
384
- // @ts-ignore not a public option, will be propagated to the populate call
385
- visited: options.visited,
386
- // @ts-ignore not a public option
387
- filtered,
388
- });
453
+ const populateChildren = async (targetMeta, items) => {
454
+ await this.populate(targetMeta.class, items, populate.children ?? populate.all, {
455
+ where: await this.extractChildCondition(options, prop, false),
456
+ orderBy: innerOrderBy,
457
+ fields,
458
+ exclude,
459
+ validate: false,
460
+ lookup: false,
461
+ filters,
462
+ ignoreLazyScalarProperties,
463
+ populateWhere,
464
+ connectionType,
465
+ logging,
466
+ schema,
467
+ // @ts-ignore not a public option, will be propagated to the populate call
468
+ refresh: refresh && !filtered.every(item => options.visited.has(item)),
469
+ // @ts-ignore not a public option, will be propagated to the populate call
470
+ visited: options.visited,
471
+ // @ts-ignore not a public option
472
+ filtered,
473
+ });
474
+ };
475
+ if (prop.polymorphic && prop.polymorphTargets) {
476
+ await Promise.all(prop.polymorphTargets.map(async (targetMeta) => {
477
+ const targetChildren = unique.filter(child => helper(child).__meta.className === targetMeta.className);
478
+ if (targetChildren.length > 0) {
479
+ await populateChildren(targetMeta, targetChildren);
480
+ }
481
+ }));
482
+ }
483
+ else {
484
+ await populateChildren(prop.targetMeta, unique);
485
+ }
389
486
  }
390
487
  /** @internal */
391
488
  async findChildrenFromPivotTable(filtered, prop, options, orderBy, populate, pivotJoin) {
@@ -394,7 +491,8 @@ export class EntityLoader {
394
491
  let where = await this.extractChildCondition(options, prop, true);
395
492
  const fields = this.buildFields(options.fields, prop);
396
493
  const exclude = Array.isArray(options.exclude) ? Utils.extractChildElements(options.exclude, prop.name) : options.exclude;
397
- const options2 = { ...options, fields, exclude };
494
+ const populateFilter = options.populateFilter?.[prop.name];
495
+ const options2 = { ...options, fields, exclude, populateFilter };
398
496
  ['limit', 'offset', 'first', 'last', 'before', 'after', 'overfetch'].forEach(prop => delete options2[prop]);
399
497
  options2.populate = (populate?.children ?? []);
400
498
  if (prop.customType) {
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Wrapper class for polymorphic relation reference data.
3
+ * Holds the discriminator value (type identifier) and the primary key value(s).
4
+ * Used internally to track polymorphic FK values before hydration.
5
+ */
6
+ export declare class PolymorphicRef {
7
+ readonly discriminator: string;
8
+ id: unknown;
9
+ constructor(discriminator: string, id: unknown);
10
+ /** Returns `[discriminator, ...idValues]` tuple suitable for column-level expansion. */
11
+ toTuple(): unknown[];
12
+ }
@@ -0,0 +1,18 @@
1
+ import { Utils } from '../utils/Utils.js';
2
+ /**
3
+ * Wrapper class for polymorphic relation reference data.
4
+ * Holds the discriminator value (type identifier) and the primary key value(s).
5
+ * Used internally to track polymorphic FK values before hydration.
6
+ */
7
+ export class PolymorphicRef {
8
+ discriminator;
9
+ id;
10
+ constructor(discriminator, id) {
11
+ this.discriminator = discriminator;
12
+ this.id = id;
13
+ }
14
+ /** Returns `[discriminator, ...idValues]` tuple suitable for column-level expansion. */
15
+ toTuple() {
16
+ return [this.discriminator, ...Utils.asArray(this.id)];
17
+ }
18
+ }
@@ -312,6 +312,10 @@ export declare class UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys
312
312
  owner<T extends boolean = true>(owner?: T): Pick<UniversalPropertyOptionsBuilder<Value, Omit<Options, 'owner'> & {
313
313
  owner: T;
314
314
  }, IncludeKeys>, IncludeKeys>;
315
+ /** For polymorphic relations. Specifies the property name that stores the entity type discriminator. Defaults to the property name. */
316
+ discriminator(discriminator: string): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
317
+ /** For polymorphic relations. Custom mapping of discriminator values to entity class names. */
318
+ discriminatorMap(discriminatorMap: Dictionary<string>): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
315
319
  /** Point to the inverse side property name. */
316
320
  inversedBy(inversedBy: keyof Value | ((e: Value) => any)): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
317
321
  /** Point to the owning side property name. */
@@ -390,11 +394,11 @@ declare const propertyBuilders: {
390
394
  manyToMany: <Target extends EntityTarget>(target: Target) => UniversalPropertyOptionsBuilder<InferEntity<Target>, EmptyOptions & {
391
395
  kind: "m:n";
392
396
  }, IncludeKeysForManyToManyOptions>;
393
- manyToOne: <Target extends EntityTarget>(target: Target) => UniversalPropertyOptionsBuilder<InferEntity<Target>, EmptyOptions & {
397
+ manyToOne: <Target extends EntityTarget | EntityTarget[]>(target: Target) => UniversalPropertyOptionsBuilder<InferEntity<Target extends (infer T)[] ? T : Target>, EmptyOptions & {
394
398
  kind: "m:1";
395
399
  }, IncludeKeysForManyToOneOptions>;
396
400
  oneToMany: <Target extends EntityTarget>(target: Target) => OneToManyOptionsBuilderOnlyMappedBy<InferEntity<Target>>;
397
- oneToOne: <Target extends EntityTarget>(target: Target) => UniversalPropertyOptionsBuilder<InferEntity<Target>, EmptyOptions & {
401
+ oneToOne: <Target extends EntityTarget | EntityTarget[]>(target: Target) => UniversalPropertyOptionsBuilder<InferEntity<Target extends (infer T)[] ? T : Target>, EmptyOptions & {
398
402
  kind: "1:1";
399
403
  }, IncludeKeysForOneToOneOptions>;
400
404
  date: () => UniversalPropertyOptionsBuilder<string, EmptyOptions, IncludeKeysForProperty>;
@@ -468,11 +472,11 @@ export declare namespace defineEntity {
468
472
  manyToMany: <Target extends EntityTarget>(target: Target) => UniversalPropertyOptionsBuilder<InferEntity<Target>, EmptyOptions & {
469
473
  kind: "m:n";
470
474
  }, IncludeKeysForManyToManyOptions>;
471
- manyToOne: <Target extends EntityTarget>(target: Target) => UniversalPropertyOptionsBuilder<InferEntity<Target>, EmptyOptions & {
475
+ manyToOne: <Target extends EntityTarget | EntityTarget[]>(target: Target) => UniversalPropertyOptionsBuilder<InferEntity<Target extends (infer T)[] ? T : Target>, EmptyOptions & {
472
476
  kind: "m:1";
473
477
  }, IncludeKeysForManyToOneOptions>;
474
478
  oneToMany: <Target extends EntityTarget>(target: Target) => OneToManyOptionsBuilderOnlyMappedBy<InferEntity<Target>>;
475
- oneToOne: <Target extends EntityTarget>(target: Target) => UniversalPropertyOptionsBuilder<InferEntity<Target>, EmptyOptions & {
479
+ oneToOne: <Target extends EntityTarget | EntityTarget[]>(target: Target) => UniversalPropertyOptionsBuilder<InferEntity<Target extends (infer T)[] ? T : Target>, EmptyOptions & {
476
480
  kind: "1:1";
477
481
  }, IncludeKeysForOneToOneOptions>;
478
482
  date: () => UniversalPropertyOptionsBuilder<string, EmptyOptions, IncludeKeysForProperty>;
@@ -353,6 +353,14 @@ export class UniversalPropertyOptionsBuilder {
353
353
  owner(owner = true) {
354
354
  return this.assignOptions({ owner });
355
355
  }
356
+ /** For polymorphic relations. Specifies the property name that stores the entity type discriminator. Defaults to the property name. */
357
+ discriminator(discriminator) {
358
+ return this.assignOptions({ discriminator });
359
+ }
360
+ /** For polymorphic relations. Custom mapping of discriminator values to entity class names. */
361
+ discriminatorMap(discriminatorMap) {
362
+ return this.assignOptions({ discriminatorMap });
363
+ }
356
364
  /** Point to the inverse side property name. */
357
365
  inversedBy(inversedBy) {
358
366
  return this.assignOptions({ inversedBy });
package/entity/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './EntityRepository.js';
2
2
  export * from './EntityIdentifier.js';
3
+ export * from './PolymorphicRef.js';
3
4
  export * from './EntityAssigner.js';
4
5
  export * from './EntityHelper.js';
5
6
  export * from './EntityFactory.js';
package/entity/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './EntityRepository.js';
2
2
  export * from './EntityIdentifier.js';
3
+ export * from './PolymorphicRef.js';
3
4
  export * from './EntityAssigner.js';
4
5
  export * from './EntityHelper.js';
5
6
  export * from './EntityFactory.js';
package/errors.d.ts CHANGED
@@ -63,8 +63,9 @@ export declare class MetadataError<T extends AnyEntity = AnyEntity> extends Vali
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
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>>;
66
+ static targetKeyNotUnique(meta: EntityMetadata, prop: EntityProperty, target?: EntityMetadata): MetadataError<Partial<any>>;
67
+ static targetKeyNotFound(meta: EntityMetadata, prop: EntityProperty, target?: EntityMetadata): MetadataError<Partial<any>>;
68
+ static incompatiblePolymorphicTargets(meta: EntityMetadata, prop: EntityProperty, target1: EntityMetadata, target2: EntityMetadata, reason: string): MetadataError<Partial<any>>;
68
69
  static dangerousPropertyName(meta: EntityMetadata, prop: EntityProperty): MetadataError<Partial<any>>;
69
70
  static viewEntityWithoutExpression(meta: EntityMetadata): MetadataError;
70
71
  private static fromMessage;
package/errors.js CHANGED
@@ -216,11 +216,16 @@ export class MetadataError extends ValidationError {
216
216
  static targetKeyOnManyToMany(meta, prop) {
217
217
  return this.fromMessage(meta, prop, `uses 'targetKey' option which is not supported for ManyToMany relations`);
218
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.`);
219
+ static targetKeyNotUnique(meta, prop, target) {
220
+ const targetName = target?.className ?? prop.type;
221
+ return this.fromMessage(meta, prop, `has 'targetKey' set to '${prop.targetKey}', but ${targetName}.${prop.targetKey} is not marked as unique`);
221
222
  }
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`);
223
+ static targetKeyNotFound(meta, prop, target) {
224
+ const targetName = target?.className ?? prop.type;
225
+ return this.fromMessage(meta, prop, `has 'targetKey' set to '${prop.targetKey}', but ${targetName}.${prop.targetKey} does not exist`);
226
+ }
227
+ static incompatiblePolymorphicTargets(meta, prop, target1, target2, reason) {
228
+ return this.fromMessage(meta, prop, `has incompatible polymorphic targets ${target1.className} and ${target2.className}: ${reason}`);
224
229
  }
225
230
  static dangerousPropertyName(meta, prop) {
226
231
  return this.fromMessage(meta, prop, `uses a dangerous property name '${prop.name}' which could lead to prototype pollution. Please use a different property name.`);
@@ -1,9 +1,11 @@
1
1
  import { Hydrator } from './Hydrator.js';
2
2
  import { Collection } from '../entity/Collection.js';
3
3
  import { Reference, ScalarReference } from '../entity/Reference.js';
4
+ import { PolymorphicRef } from '../entity/PolymorphicRef.js';
4
5
  import { parseJsonSafe, Utils } from '../utils/Utils.js';
5
6
  import { ReferenceKind } from '../enums.js';
6
7
  import { Raw } from '../utils/RawQueryFragment.js';
8
+ import { ValidationError } from '../errors.js';
7
9
  export class ObjectHydrator extends Hydrator {
8
10
  hydrators = {
9
11
  'full~true': new Map(),
@@ -49,6 +51,8 @@ export class ObjectHydrator extends Hydrator {
49
51
  context.set('isPrimaryKey', Utils.isPrimaryKey);
50
52
  context.set('Collection', Collection);
51
53
  context.set('Reference', Reference);
54
+ context.set('PolymorphicRef', PolymorphicRef);
55
+ context.set('ValidationError', ValidationError);
52
56
  const registerCustomType = (prop, convertorKey, method, context) => {
53
57
  context.set(`${method}_${convertorKey}`, (val) => {
54
58
  /* v8 ignore next */
@@ -134,23 +138,51 @@ export class ObjectHydrator extends Hydrator {
134
138
  const nullVal = this.config.get('forceUndefined') ? 'undefined' : 'null';
135
139
  ret.push(` if (data${dataKey} === null) {\n entity${entityKey} = ${nullVal};`);
136
140
  ret.push(` } else if (typeof data${dataKey} !== 'undefined') {`);
137
- ret.push(` if (isPrimaryKey(data${dataKey}, true)) {`);
138
- const targetKey = this.safeKey(`${prop.targetMeta.tableName}_${this.tmpIndex++}`);
139
- context.set(targetKey, prop.targetMeta.class);
141
+ // For polymorphic: instanceof check; for regular: isPrimaryKey() check
142
+ const pkCheck = prop.polymorphic ? `data${dataKey} instanceof PolymorphicRef` : `isPrimaryKey(data${dataKey}, true)`;
143
+ ret.push(` if (${pkCheck}) {`);
140
144
  // When targetKey is set, pass the key option to createReference so it uses the alternate key
141
145
  const keyOption = prop.targetKey ? `, key: '${prop.targetKey}'` : '';
142
- if (prop.ref) {
143
- ret.push(` entity${entityKey} = Reference.create(factory.createReference(${targetKey}, data${dataKey}, { merge: true, convertCustomTypes, normalizeAccessors, schema${keyOption} }));`);
146
+ if (prop.polymorphic) {
147
+ // For polymorphic: target class from discriminator map, PK from data.id
148
+ const discriminatorMapKey = this.safeKey(`discriminatorMap_${prop.name}_${this.tmpIndex++}`);
149
+ context.set(discriminatorMapKey, prop.discriminatorMap);
150
+ ret.push(` const targetClass = ${discriminatorMapKey}[data${dataKey}.discriminator];`);
151
+ ret.push(` if (!targetClass) throw new ValidationError(\`Unknown discriminator value '\${data${dataKey}.discriminator}' for polymorphic relation '${prop.name}'. Valid values: \${Object.keys(${discriminatorMapKey}).join(', ')}\`);`);
152
+ if (prop.ref) {
153
+ ret.push(` entity${entityKey} = Reference.create(factory.createReference(targetClass, data${dataKey}.id, { merge: true, convertCustomTypes, normalizeAccessors, schema${keyOption} }));`);
154
+ }
155
+ else {
156
+ ret.push(` entity${entityKey} = factory.createReference(targetClass, data${dataKey}.id, { merge: true, convertCustomTypes, normalizeAccessors, schema${keyOption} });`);
157
+ }
144
158
  }
145
159
  else {
146
- ret.push(` entity${entityKey} = factory.createReference(${targetKey}, data${dataKey}, { merge: true, convertCustomTypes, normalizeAccessors, schema${keyOption} });`);
160
+ // For regular: fixed target class, PK is the data itself
161
+ const targetKey = this.safeKey(`${prop.targetMeta.tableName}_${this.tmpIndex++}`);
162
+ context.set(targetKey, prop.targetMeta.class);
163
+ if (prop.ref) {
164
+ ret.push(` entity${entityKey} = Reference.create(factory.createReference(${targetKey}, data${dataKey}, { merge: true, convertCustomTypes, normalizeAccessors, schema${keyOption} }));`);
165
+ }
166
+ else {
167
+ ret.push(` entity${entityKey} = factory.createReference(${targetKey}, data${dataKey}, { merge: true, convertCustomTypes, normalizeAccessors, schema${keyOption} });`);
168
+ }
147
169
  }
148
170
  ret.push(` } else if (data${dataKey} && typeof data${dataKey} === 'object') {`);
171
+ // For full entity hydration, polymorphic needs to determine target class from entity itself
172
+ let hydrateTargetExpr;
173
+ if (prop.polymorphic) {
174
+ hydrateTargetExpr = `data${dataKey}.constructor`;
175
+ }
176
+ else {
177
+ const targetKey = this.safeKey(`${prop.targetMeta.tableName}_${this.tmpIndex++}`);
178
+ context.set(targetKey, prop.targetMeta.class);
179
+ hydrateTargetExpr = targetKey;
180
+ }
149
181
  if (prop.ref) {
150
- ret.push(` entity${entityKey} = Reference.create(factory.${method}(${targetKey}, data${dataKey}, { initialized: true, merge: true, newEntity, convertCustomTypes, normalizeAccessors, schema }));`);
182
+ ret.push(` entity${entityKey} = Reference.create(factory.${method}(${hydrateTargetExpr}, data${dataKey}, { initialized: true, merge: true, newEntity, convertCustomTypes, normalizeAccessors, schema }));`);
151
183
  }
152
184
  else {
153
- ret.push(` entity${entityKey} = factory.${method}(${targetKey}, data${dataKey}, { initialized: true, merge: true, newEntity, convertCustomTypes, normalizeAccessors, schema });`);
185
+ ret.push(` entity${entityKey} = factory.${method}(${hydrateTargetExpr}, data${dataKey}, { initialized: true, merge: true, newEntity, convertCustomTypes, normalizeAccessors, schema });`);
154
186
  }
155
187
  ret.push(` }`);
156
188
  ret.push(` }`);
@@ -7,7 +7,7 @@ type TypeType = string | NumberConstructor | StringConstructor | BooleanConstruc
7
7
  type TypeDef<Target> = {
8
8
  type: TypeType;
9
9
  } | {
10
- entity: () => EntityName<Target>;
10
+ entity: () => EntityName<Target> | EntityName[];
11
11
  };
12
12
  type EmbeddedTypeDef<Target> = {
13
13
  type: TypeType;
@@ -42,11 +42,33 @@ export declare class MetadataDiscovery {
42
42
  private initFactoryField;
43
43
  private ensureCorrectFKOrderInPivotEntity;
44
44
  private definePivotTableEntity;
45
+ /**
46
+ * Create a scalar property for a pivot table column.
47
+ */
48
+ private createPivotScalarProperty;
49
+ /**
50
+ * Get column types for an entity's primary keys, initializing them if needed.
51
+ */
52
+ private getPrimaryKeyColumnTypes;
53
+ /**
54
+ * Add missing FK columns for a polymorphic entity to an existing pivot table.
55
+ */
56
+ private addPolymorphicPivotColumns;
57
+ /**
58
+ * Define properties for a polymorphic pivot table.
59
+ */
60
+ private definePolymorphicPivotProperties;
61
+ /**
62
+ * Create a virtual M:1 relation from pivot to a polymorphic owner entity.
63
+ * This enables single-query join loading for inverse-side polymorphic M:N.
64
+ */
65
+ private definePolymorphicOwnerRelation;
45
66
  private defineFixedOrderProperty;
46
67
  private definePivotProperty;
47
68
  private autoWireBidirectionalProperties;
48
69
  private defineBaseEntityProperties;
49
70
  private initPolyEmbeddables;
71
+ private initPolymorphicRelation;
50
72
  private initEmbeddables;
51
73
  private initSingleTableInheritance;
52
74
  private createDiscriminatorProperty;