@mikro-orm/core 7.0.0-dev.225 → 7.0.0-dev.227

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.
@@ -1,5 +1,6 @@
1
1
  import { EntityMetadata, } from '../typings.js';
2
2
  import { compareArrays, Utils } from '../utils/Utils.js';
3
+ import { QueryHelper } from '../utils/QueryHelper.js';
3
4
  import { MetadataValidator } from './MetadataValidator.js';
4
5
  import { MetadataProvider } from './MetadataProvider.js';
5
6
  import { MetadataStorage } from './MetadataStorage.js';
@@ -131,6 +132,7 @@ export class MetadataDiscovery {
131
132
  filtered.forEach(meta => Object.values(meta.properties).forEach(prop => cb(meta, prop)));
132
133
  };
133
134
  forEachProp((m, p) => this.initFactoryField(m, p));
135
+ forEachProp((m, p) => this.initPolymorphicRelation(m, p, filtered));
134
136
  forEachProp((_m, p) => this.initRelation(p));
135
137
  forEachProp((m, p) => this.initEmbeddables(m, p));
136
138
  forEachProp((_m, p) => this.initFieldName(p));
@@ -351,6 +353,12 @@ export class MetadataDiscovery {
351
353
  if (!prop.joinColumns || !prop.columnTypes || prop.ownColumns || ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
352
354
  continue;
353
355
  }
356
+ // For polymorphic relations, ownColumns should include all fieldNames
357
+ // (discriminator + ID columns) since they are all owned by this relation
358
+ if (prop.polymorphic) {
359
+ prop.ownColumns = prop.fieldNames;
360
+ continue;
361
+ }
354
362
  if (prop.joinColumns.length > 1) {
355
363
  prop.ownColumns = prop.joinColumns.filter(col => {
356
364
  return !meta.props.find(p => p.name !== prop.name && (!p.fieldNames || p.fieldNames.includes(col)));
@@ -381,7 +389,7 @@ export class MetadataDiscovery {
381
389
  if (prop.kind === ReferenceKind.SCALAR || prop.kind === ReferenceKind.EMBEDDED) {
382
390
  prop.fieldNames = [this.namingStrategy.propertyToColumnName(prop.name, object)];
383
391
  }
384
- else if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
392
+ else if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && !prop.polymorphic) {
385
393
  prop.fieldNames = this.initManyToOneFieldName(prop, prop.name);
386
394
  }
387
395
  else if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.owner) {
@@ -436,17 +444,36 @@ export class MetadataDiscovery {
436
444
  prop.fixedOrderColumn = prop2.fixedOrderColumn;
437
445
  prop.joinColumns = prop2.inverseJoinColumns;
438
446
  prop.inverseJoinColumns = prop2.joinColumns;
447
+ prop.polymorphic = prop2.polymorphic;
448
+ prop.discriminator = prop2.discriminator;
449
+ prop.discriminatorColumn = prop2.discriminatorColumn;
450
+ prop.discriminatorValue = prop2.discriminatorValue;
439
451
  }
440
452
  prop.referencedColumnNames ??= Utils.flatten(meta.primaryKeys.map(primaryKey => meta.properties[primaryKey].fieldNames));
441
- const ownerTableName = this.isExplicitTableName(meta.root) ? meta.root.tableName : undefined;
453
+ // For polymorphic M:N, use discriminator base name for FK column (e.g., taggable_id instead of post_id)
454
+ if (prop.polymorphic && prop.discriminator) {
455
+ prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.namingStrategy.joinKeyColumnName(prop.discriminator, referencedColumnName, prop.referencedColumnNames.length > 1));
456
+ }
457
+ else {
458
+ const ownerTableName = this.isExplicitTableName(meta.root) ? meta.root.tableName : undefined;
459
+ prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.namingStrategy.joinKeyColumnName(meta.root.className, referencedColumnName, meta.compositePK, ownerTableName));
460
+ }
442
461
  const inverseTableName = this.isExplicitTableName(meta2.root) ? meta2.root.tableName : undefined;
443
- prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.namingStrategy.joinKeyColumnName(meta.root.className, referencedColumnName, meta.compositePK, ownerTableName));
444
462
  prop.inverseJoinColumns ??= this.initManyToOneFieldName(prop, meta2.root.className, inverseTableName);
445
463
  }
446
464
  isExplicitTableName(meta) {
447
465
  return meta.tableName !== this.namingStrategy.classToTableName(meta.className);
448
466
  }
449
467
  initManyToOneFields(prop) {
468
+ if (prop.polymorphic && prop.polymorphTargets) {
469
+ const fieldNames1 = prop.targetMeta.getPrimaryProps().flatMap(pk => pk.fieldNames);
470
+ const idColumns = fieldNames1.map(fieldName => this.namingStrategy.joinKeyColumnName(prop.discriminator, fieldName, fieldNames1.length > 1));
471
+ prop.fieldNames ??= [prop.discriminatorColumn, ...idColumns];
472
+ prop.joinColumns ??= idColumns;
473
+ prop.referencedColumnNames ??= fieldNames1;
474
+ prop.referencedTableName ??= prop.targetMeta.tableName;
475
+ return;
476
+ }
450
477
  const meta2 = prop.targetMeta;
451
478
  let fieldNames;
452
479
  // If targetKey is specified, use that property's field names instead of PKs
@@ -605,6 +632,18 @@ export class MetadataDiscovery {
605
632
  if (pivotMeta) {
606
633
  prop.pivotEntity = pivotMeta.class;
607
634
  this.ensureCorrectFKOrderInPivotEntity(pivotMeta, prop);
635
+ if (prop.polymorphic && prop.discriminatorValue) {
636
+ pivotMeta.polymorphicDiscriminatorMap ??= {};
637
+ pivotMeta.polymorphicDiscriminatorMap[prop.discriminatorValue] = meta.class;
638
+ // For composite PK entities sharing a polymorphic pivot table,
639
+ // we need to add columns for each entity type's PKs
640
+ this.addPolymorphicPivotColumns(pivotMeta, meta, prop);
641
+ // Add virtual M:1 relation for this polymorphic owner (for join loading)
642
+ const ownerRelationName = `${prop.discriminator}_${meta.tableName}`;
643
+ if (!pivotMeta.properties[ownerRelationName]) {
644
+ pivotMeta.properties[ownerRelationName] = this.definePolymorphicOwnerRelation(prop, ownerRelationName, meta);
645
+ }
646
+ }
608
647
  return pivotMeta;
609
648
  }
610
649
  let tableName = prop.pivotTable;
@@ -650,10 +689,127 @@ export class MetadataDiscovery {
650
689
  }
651
690
  }
652
691
  }
653
- pivotMeta2.properties[meta.name + '_owner'] = this.definePivotProperty(prop, meta.name + '_owner', meta.class, targetType + '_inverse', true, meta.className === targetType);
654
- pivotMeta2.properties[targetType + '_inverse'] = this.definePivotProperty(prop, targetType + '_inverse', targetMeta.class, meta.name + '_owner', false, meta.className === targetType);
692
+ // For polymorphic M:N, create discriminator column and polymorphic FK
693
+ if (prop.polymorphic && prop.discriminatorColumn) {
694
+ this.definePolymorphicPivotProperties(pivotMeta2, meta, prop, targetMeta);
695
+ }
696
+ else {
697
+ pivotMeta2.properties[meta.name + '_owner'] = this.definePivotProperty(prop, meta.name + '_owner', meta.class, targetType + '_inverse', true, meta.className === targetType);
698
+ pivotMeta2.properties[targetType + '_inverse'] = this.definePivotProperty(prop, targetType + '_inverse', targetMeta.class, meta.name + '_owner', false, meta.className === targetType);
699
+ }
655
700
  return this.metadata.set(pivotMeta2.class, EntitySchema.fromMetadata(pivotMeta2).init().meta);
656
701
  }
702
+ /**
703
+ * Create a scalar property for a pivot table column.
704
+ */
705
+ createPivotScalarProperty(name, columnTypes, fieldNames = [name], options = {}) {
706
+ return {
707
+ name,
708
+ fieldNames,
709
+ columnTypes,
710
+ type: options.type ?? 'number',
711
+ kind: ReferenceKind.SCALAR,
712
+ primary: options.primary ?? false,
713
+ nullable: options.nullable ?? true,
714
+ ...(options.persist !== undefined && { persist: options.persist }),
715
+ };
716
+ }
717
+ /**
718
+ * Get column types for an entity's primary keys, initializing them if needed.
719
+ */
720
+ getPrimaryKeyColumnTypes(meta) {
721
+ const columnTypes = [];
722
+ for (const pk of meta.primaryKeys) {
723
+ const pkProp = meta.properties[pk];
724
+ this.initCustomType(meta, pkProp);
725
+ this.initColumnType(pkProp);
726
+ columnTypes.push(...pkProp.columnTypes);
727
+ }
728
+ return columnTypes;
729
+ }
730
+ /**
731
+ * Add missing FK columns for a polymorphic entity to an existing pivot table.
732
+ */
733
+ addPolymorphicPivotColumns(pivotMeta, meta, prop) {
734
+ const existingFieldNames = new Set(Object.values(pivotMeta.properties).flatMap(p => p.fieldNames ?? []));
735
+ const columnTypes = this.getPrimaryKeyColumnTypes(meta);
736
+ for (let i = 0; i < prop.joinColumns.length; i++) {
737
+ const joinColumn = prop.joinColumns[i];
738
+ if (!existingFieldNames.has(joinColumn)) {
739
+ pivotMeta.properties[joinColumn] = this.createPivotScalarProperty(joinColumn, [columnTypes[i]]);
740
+ }
741
+ }
742
+ }
743
+ /**
744
+ * Define properties for a polymorphic pivot table.
745
+ */
746
+ definePolymorphicPivotProperties(pivotMeta, meta, prop, targetMeta) {
747
+ const discriminatorColumn = prop.discriminatorColumn;
748
+ const isCompositePK = meta.compositePK;
749
+ // For composite PK polymorphic M:N, we need fixedOrder (auto-increment PK)
750
+ if (isCompositePK && !prop.fixedOrder) {
751
+ prop.fixedOrder = true;
752
+ const primaryProp = this.defineFixedOrderProperty(prop, targetMeta);
753
+ pivotMeta.properties[primaryProp.name] = primaryProp;
754
+ pivotMeta.compositePK = false;
755
+ }
756
+ const discriminatorProp = this.createPivotScalarProperty(discriminatorColumn, [this.platform.getVarcharTypeDeclarationSQL(prop)], [discriminatorColumn], { type: 'string', primary: !isCompositePK, nullable: false });
757
+ this.initFieldName(discriminatorProp);
758
+ pivotMeta.properties[discriminatorColumn] = discriminatorProp;
759
+ const columnTypes = this.getPrimaryKeyColumnTypes(meta);
760
+ if (isCompositePK) {
761
+ // Create separate properties for each PK column (nullable for other entity types)
762
+ for (let i = 0; i < prop.joinColumns.length; i++) {
763
+ pivotMeta.properties[prop.joinColumns[i]] = this.createPivotScalarProperty(prop.joinColumns[i], [columnTypes[i]]);
764
+ }
765
+ // Virtual property combining all columns (for compatibility)
766
+ pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, columnTypes, [...prop.joinColumns], { type: meta.className, persist: false });
767
+ }
768
+ else {
769
+ pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, columnTypes, [...prop.joinColumns], { type: meta.className, primary: true, nullable: false });
770
+ }
771
+ pivotMeta.properties[targetMeta.className + '_inverse'] = this.definePivotProperty(prop, targetMeta.className + '_inverse', targetMeta.class, prop.discriminator, false, false);
772
+ // Create virtual M:1 relation to the polymorphic owner for single-query join loading
773
+ const ownerRelationName = `${prop.discriminator}_${meta.tableName}`;
774
+ pivotMeta.properties[ownerRelationName] = this.definePolymorphicOwnerRelation(prop, ownerRelationName, meta);
775
+ pivotMeta.polymorphicDiscriminatorMap ??= {};
776
+ pivotMeta.polymorphicDiscriminatorMap[prop.discriminatorValue] = meta.class;
777
+ }
778
+ /**
779
+ * Create a virtual M:1 relation from pivot to a polymorphic owner entity.
780
+ * This enables single-query join loading for inverse-side polymorphic M:N.
781
+ */
782
+ definePolymorphicOwnerRelation(prop, name, targetMeta) {
783
+ const ret = {
784
+ name,
785
+ type: targetMeta.className,
786
+ target: targetMeta.class,
787
+ kind: ReferenceKind.MANY_TO_ONE,
788
+ nullable: true,
789
+ owner: true,
790
+ primary: false,
791
+ createForeignKeyConstraint: false,
792
+ persist: false,
793
+ index: false,
794
+ };
795
+ ret.targetMeta = targetMeta;
796
+ ret.fieldNames = ret.joinColumns = ret.ownColumns = [...prop.joinColumns];
797
+ ret.referencedColumnNames = [];
798
+ ret.inverseJoinColumns = [];
799
+ for (const primaryKey of targetMeta.primaryKeys) {
800
+ const pkProp = targetMeta.properties[primaryKey];
801
+ ret.referencedColumnNames.push(...pkProp.fieldNames);
802
+ ret.inverseJoinColumns.push(...pkProp.fieldNames);
803
+ ret.length = pkProp.length;
804
+ ret.precision = pkProp.precision;
805
+ ret.scale = pkProp.scale;
806
+ }
807
+ const schema = targetMeta.schema ?? this.config.get('schema') ?? this.platform.getDefaultSchemaName();
808
+ ret.referencedTableName = schema && schema !== '*' ? schema + '.' + targetMeta.tableName : targetMeta.tableName;
809
+ this.initColumnType(ret);
810
+ this.initRelation(ret);
811
+ return ret;
812
+ }
657
813
  defineFixedOrderProperty(prop, targetMeta) {
658
814
  const pk = prop.fixedOrderColumn || this.namingStrategy.referenceColumnName();
659
815
  const primaryProp = {
@@ -833,6 +989,54 @@ export class MetadataDiscovery {
833
989
  polymorphs.forEach(meta => meta.root = embeddable);
834
990
  }
835
991
  }
992
+ initPolymorphicRelation(meta, prop, discovered) {
993
+ if (!prop.discriminator && !prop.discriminatorColumn && !prop.discriminatorMap && !Array.isArray(prop.target)) {
994
+ return;
995
+ }
996
+ prop.polymorphic = true;
997
+ prop.discriminator ??= prop.name;
998
+ prop.discriminatorColumn ??= this.namingStrategy.discriminatorColumnName(prop.discriminator);
999
+ prop.createForeignKeyConstraint = false;
1000
+ const isToOne = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind);
1001
+ if (isToOne) {
1002
+ const types = prop.type.split(/ ?\| ?/);
1003
+ prop.polymorphTargets = discovered.filter(m => types.includes(m.className) && !m.embeddable);
1004
+ prop.targetMeta = prop.polymorphTargets[0];
1005
+ prop.referencedPKs = prop.targetMeta?.primaryKeys;
1006
+ }
1007
+ if (prop.discriminatorMap) {
1008
+ const normalizedMap = {};
1009
+ for (const [key, value] of Object.entries(prop.discriminatorMap)) {
1010
+ const targetMeta = this.metadata.getByClassName(value, false);
1011
+ if (!targetMeta) {
1012
+ throw MetadataError.fromUnknownEntity(value, `${meta.className}.${prop.name} discriminatorMap`);
1013
+ }
1014
+ normalizedMap[key] = targetMeta.class;
1015
+ if (targetMeta.class === meta.class) {
1016
+ prop.discriminatorValue = key;
1017
+ }
1018
+ }
1019
+ prop.discriminatorMap = normalizedMap;
1020
+ }
1021
+ else if (isToOne) {
1022
+ prop.discriminatorMap = {};
1023
+ const tableNameToTarget = new Map();
1024
+ for (const target of prop.polymorphTargets) {
1025
+ const existing = tableNameToTarget.get(target.tableName);
1026
+ if (existing) {
1027
+ throw MetadataError.incompatiblePolymorphicTargets(meta, prop, existing, target, `both use table '${target.tableName}'. Use separate properties instead of a single polymorphic relation.`);
1028
+ }
1029
+ tableNameToTarget.set(target.tableName, target);
1030
+ prop.discriminatorMap[target.tableName] = target.class;
1031
+ }
1032
+ }
1033
+ else {
1034
+ prop.discriminatorValue ??= meta.tableName;
1035
+ if (!prop.discriminatorMap) {
1036
+ prop.discriminatorMap = { [prop.discriminatorValue]: meta.class };
1037
+ }
1038
+ }
1039
+ }
836
1040
  initEmbeddables(meta, embeddedProp, visited = new Set()) {
837
1041
  if (embeddedProp.kind !== ReferenceKind.EMBEDDED || visited.has(embeddedProp)) {
838
1042
  return;
@@ -963,7 +1167,7 @@ export class MetadataDiscovery {
963
1167
  meta.root.discriminatorMap[name] = m.class;
964
1168
  }
965
1169
  }
966
- meta.discriminatorValue = Object.entries(meta.root.discriminatorMap).find(([, cls]) => cls === meta.class)?.[0];
1170
+ meta.discriminatorValue = QueryHelper.findDiscriminatorValue(meta.root.discriminatorMap, meta.class);
967
1171
  if (!meta.root.properties[meta.root.discriminatorColumn]) {
968
1172
  this.createDiscriminatorProperty(meta.root);
969
1173
  }
@@ -1245,7 +1449,7 @@ export class MetadataDiscovery {
1245
1449
  if (Type.isMappedType(prop.customType) && prop.kind === ReferenceKind.SCALAR && !isArray) {
1246
1450
  prop.type = prop.customType.name;
1247
1451
  }
1248
- if (!prop.customType && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind) && prop.targetMeta.compositePK) {
1452
+ if (!prop.customType && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind) && !prop.polymorphic && prop.targetMeta.compositePK) {
1249
1453
  prop.customTypes = [];
1250
1454
  for (const pk of prop.targetMeta.getPrimaryProps()) {
1251
1455
  if (pk.customType) {
@@ -1274,7 +1478,7 @@ export class MetadataDiscovery {
1274
1478
  }
1275
1479
  }
1276
1480
  initRelation(prop) {
1277
- if (prop.kind === ReferenceKind.SCALAR) {
1481
+ if (prop.kind === ReferenceKind.SCALAR || prop.polymorphTargets) {
1278
1482
  return;
1279
1483
  }
1280
1484
  // 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
@@ -1285,8 +1489,13 @@ export class MetadataDiscovery {
1285
1489
  if (meta2.view) {
1286
1490
  prop.createForeignKeyConstraint = false;
1287
1491
  }
1492
+ // Auto-generate formula for persist: false relations, but only for single-column FKs
1493
+ // Composite FK relations need standard JOIN conditions, not formula-based
1288
1494
  if (!prop.formula && prop.persist === false && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && !prop.embedded) {
1289
- prop.formula = table => `${table}.${this.platform.quoteIdentifier(prop.fieldNames[0])}`;
1495
+ this.initFieldName(prop);
1496
+ if (prop.fieldNames?.length === 1) {
1497
+ prop.formula = table => `${table}.${this.platform.quoteIdentifier(prop.fieldNames[0])}`;
1498
+ }
1290
1499
  }
1291
1500
  }
1292
1501
  initColumnType(prop) {
@@ -1335,6 +1544,9 @@ export class MetadataDiscovery {
1335
1544
  const referencedProps = prop.targetKey
1336
1545
  ? [targetMeta.properties[prop.targetKey]]
1337
1546
  : targetMeta.getPrimaryProps();
1547
+ if (prop.polymorphic && prop.polymorphTargets) {
1548
+ prop.columnTypes.push(this.platform.getVarcharTypeDeclarationSQL(prop));
1549
+ }
1338
1550
  for (const referencedProp of referencedProps) {
1339
1551
  this.initCustomType(targetMeta, referencedProp);
1340
1552
  this.initColumnType(referencedProp);
@@ -1383,8 +1595,7 @@ export class MetadataDiscovery {
1383
1595
  return;
1384
1596
  }
1385
1597
  if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
1386
- const meta2 = prop.targetMeta;
1387
- prop.unsigned = meta2.getPrimaryProps().some(pk => {
1598
+ prop.unsigned = prop.targetMeta.getPrimaryProps().some(pk => {
1388
1599
  this.initUnsigned(pk);
1389
1600
  return pk.unsigned;
1390
1601
  });
@@ -9,6 +9,12 @@ export declare class MetadataValidator {
9
9
  validateDiscovered(discovered: EntityMetadata[], options: MetadataDiscoveryOptions): void;
10
10
  private validateReference;
11
11
  private validateTargetKey;
12
+ /**
13
+ * Checks if a property has a unique constraint (either via `unique: true` or single-property `@Unique` decorator).
14
+ * Composite unique constraints are not sufficient for targetKey.
15
+ */
16
+ private isPropertyUnique;
17
+ private validatePolymorphicTargets;
12
18
  private validateBidirectional;
13
19
  private validateOwningSide;
14
20
  private validateInverseSide;
@@ -107,6 +107,11 @@ export class MetadataValidator {
107
107
  if (!prop.type) {
108
108
  throw MetadataError.fromWrongTypeDefinition(meta, prop);
109
109
  }
110
+ // Polymorphic relations have multiple targets, validate PK compatibility
111
+ if (prop.polymorphic && prop.polymorphTargets) {
112
+ this.validatePolymorphicTargets(meta, prop);
113
+ return;
114
+ }
110
115
  const targetMeta = prop.targetMeta;
111
116
  // references do have type of known entity
112
117
  if (!targetMeta) {
@@ -133,11 +138,56 @@ export class MetadataValidator {
133
138
  if (!targetProp) {
134
139
  throw MetadataError.targetKeyNotFound(meta, prop);
135
140
  }
136
- // targetKey must point to a unique property
137
- if (!targetProp.unique && !targetMeta.uniques?.some(u => u.properties?.includes(prop.targetKey))) {
141
+ // targetKey must point to a unique property (composite unique is not sufficient)
142
+ if (!this.isPropertyUnique(targetProp, targetMeta)) {
138
143
  throw MetadataError.targetKeyNotUnique(meta, prop);
139
144
  }
140
145
  }
146
+ /**
147
+ * Checks if a property has a unique constraint (either via `unique: true` or single-property `@Unique` decorator).
148
+ * Composite unique constraints are not sufficient for targetKey.
149
+ */
150
+ isPropertyUnique(prop, meta) {
151
+ if (prop.unique) {
152
+ return true;
153
+ }
154
+ // Check for single-property unique constraint via @Unique decorator
155
+ return !!meta.uniques?.some(u => {
156
+ const props = Utils.asArray(u.properties);
157
+ return props.length === 1 && props[0] === prop.name && !u.options;
158
+ });
159
+ }
160
+ validatePolymorphicTargets(meta, prop) {
161
+ const targets = prop.polymorphTargets;
162
+ // Validate targetKey exists and is compatible across all targets
163
+ if (prop.targetKey) {
164
+ for (const target of targets) {
165
+ const targetProp = target.properties[prop.targetKey];
166
+ if (!targetProp) {
167
+ throw MetadataError.targetKeyNotFound(meta, prop, target);
168
+ }
169
+ // targetKey must point to a unique property (composite unique is not sufficient)
170
+ if (!this.isPropertyUnique(targetProp, target)) {
171
+ throw MetadataError.targetKeyNotUnique(meta, prop, target);
172
+ }
173
+ }
174
+ }
175
+ const firstPKs = targets[0].getPrimaryProps();
176
+ for (let i = 1; i < targets.length; i++) {
177
+ const target = targets[i];
178
+ const targetPKs = target.getPrimaryProps();
179
+ if (targetPKs.length !== firstPKs.length) {
180
+ throw MetadataError.incompatiblePolymorphicTargets(meta, prop, targets[0], target, 'different number of primary keys');
181
+ }
182
+ for (let j = 0; j < firstPKs.length; j++) {
183
+ const firstPK = firstPKs[j];
184
+ const targetPK = targetPKs[j];
185
+ if (firstPK.runtimeType !== targetPK.runtimeType) {
186
+ throw MetadataError.incompatiblePolymorphicTargets(meta, prop, targets[0], target, `incompatible primary key types: ${firstPK.name} (${firstPK.runtimeType}) vs ${targetPK.name} (${targetPK.runtimeType})`);
187
+ }
188
+ }
189
+ }
190
+ }
141
191
  validateBidirectional(meta, prop) {
142
192
  if (prop.inversedBy) {
143
193
  this.validateOwningSide(meta, prop);
@@ -151,6 +201,27 @@ export class MetadataValidator {
151
201
  }
152
202
  }
153
203
  validateOwningSide(meta, prop) {
204
+ // For polymorphic relations, inversedBy may point to multiple entity types
205
+ if (prop.polymorphic && prop.polymorphTargets?.length) {
206
+ // For polymorphic relations, validate inversedBy against each target
207
+ // The inverse property should exist on the target entities and reference back to this property
208
+ for (const targetMeta of prop.polymorphTargets) {
209
+ const inverse = targetMeta.properties[prop.inversedBy];
210
+ // The inverse property is optional - some targets may not have it
211
+ if (!inverse) {
212
+ continue;
213
+ }
214
+ // Validate the inverse property
215
+ if (inverse.targetMeta?.root.class !== meta.root.class) {
216
+ throw MetadataError.fromWrongReference(meta, prop, 'inversedBy', inverse);
217
+ }
218
+ // inverse side is not defined as owner
219
+ if (inverse.inversedBy || inverse.owner) {
220
+ throw MetadataError.fromWrongOwnership(meta, prop, 'inversedBy');
221
+ }
222
+ }
223
+ return;
224
+ }
154
225
  const inverse = prop.targetMeta.properties[prop.inversedBy];
155
226
  // has correct `inversedBy` on owning side
156
227
  if (!inverse) {
@@ -173,7 +244,9 @@ export class MetadataValidator {
173
244
  throw MetadataError.fromWrongReference(meta, prop, 'mappedBy');
174
245
  }
175
246
  // has correct `mappedBy` reference type
176
- if (owner.type !== meta.className && owner.targetMeta?.root.class !== meta.root.class) {
247
+ // For polymorphic relations, check if this entity is one of the polymorphic targets
248
+ const isValidPolymorphicInverse = owner.polymorphic && owner.polymorphTargets?.some(target => target.class === meta.root.class);
249
+ if (!isValidPolymorphicInverse && owner.type !== meta.className && owner.targetMeta?.root.class !== meta.root.class) {
177
250
  throw MetadataError.fromWrongReference(meta, prop, 'mappedBy', owner);
178
251
  }
179
252
  // owning side is not defined as inverse
@@ -321,8 +321,8 @@ export interface PropertyOptions<Owner> {
321
321
  ignoreSchemaChanges?: ('type' | 'extra' | 'default')[];
322
322
  }
323
323
  export interface ReferenceOptions<Owner, Target> extends PropertyOptions<Owner> {
324
- /** Set target entity type. */
325
- entity?: () => EntityName<Target>;
324
+ /** Set target entity type. For polymorphic relations, pass an array of entity types. */
325
+ entity?: () => EntityName<Target> | EntityName<Target>[];
326
326
  /** Set what actions on owning entity should be cascaded to the relationship. Defaults to [Cascade.PERSIST, Cascade.MERGE] (see {@doclink cascading}). */
327
327
  cascade?: Cascade[];
328
328
  /** Always load the relationship. Discouraged for use with to-many relations for performance reasons. */
@@ -337,7 +337,20 @@ export interface ReferenceOptions<Owner, Target> extends PropertyOptions<Owner>
337
337
  * @ignore
338
338
  */
339
339
  export type ColumnType = 'int' | 'int4' | 'integer' | 'bigint' | 'int8' | 'int2' | 'tinyint' | 'smallint' | 'mediumint' | 'double' | 'double precision' | 'real' | 'float8' | 'decimal' | 'numeric' | 'float' | 'float4' | 'datetime' | 'time' | 'time with time zone' | 'timestamp' | 'timestamp with time zone' | 'timetz' | 'timestamptz' | 'date' | 'interval' | 'character varying' | 'varchar' | 'char' | 'character' | 'uuid' | 'text' | 'tinytext' | 'mediumtext' | 'longtext' | 'boolean' | 'bool' | 'bit' | 'enum' | 'blob' | 'tinyblob' | 'mediumblob' | 'longblob' | 'bytea' | 'point' | 'line' | 'lseg' | 'box' | 'circle' | 'path' | 'polygon' | 'geometry' | 'tsvector' | 'tsquery' | 'json' | 'jsonb';
340
- export interface ManyToOneOptions<Owner, Target> extends ReferenceOptions<Owner, Target> {
340
+ interface PolymorphicOptions {
341
+ /**
342
+ * For polymorphic relations. Specifies the property name that stores the entity type discriminator.
343
+ * Defaults to the property name. Only used when `entity` returns an array of types.
344
+ * For M:N relations, this is the column name in the pivot table.
345
+ */
346
+ discriminator?: string;
347
+ /**
348
+ * For polymorphic relations. Custom mapping of discriminator values to entity class names.
349
+ * If not provided, table names are used as discriminator values.
350
+ */
351
+ discriminatorMap?: Dictionary<string>;
352
+ }
353
+ export interface ManyToOneOptions<Owner, Target> extends ReferenceOptions<Owner, Target>, PolymorphicOptions {
341
354
  /** Point to the inverse side property name. */
342
355
  inversedBy?: (string & keyof Target) | ((e: Target) => any);
343
356
  /** Wrap the entity in {@apilink Reference} wrapper. */
@@ -391,7 +404,7 @@ export interface OneToManyOptions<Owner, Target> extends ReferenceOptions<Owner,
391
404
  /** Point to the owning side property name. */
392
405
  mappedBy: (string & keyof Target) | ((e: Target) => any);
393
406
  }
394
- export interface OneToOneOptions<Owner, Target> extends Partial<Omit<OneToManyOptions<Owner, Target>, 'orderBy'>> {
407
+ export interface OneToOneOptions<Owner, Target> extends Partial<Omit<OneToManyOptions<Owner, Target>, 'orderBy'>>, PolymorphicOptions {
395
408
  /** Set this side as owning. Owning side is where the foreign key is defined. This option is not required if you use `inversedBy` or `mappedBy` to distinguish owning and inverse side. */
396
409
  owner?: boolean;
397
410
  /** Point to the inverse side property name. */
@@ -417,7 +430,7 @@ export interface OneToOneOptions<Owner, Target> extends Partial<Omit<OneToManyOp
417
430
  /** Enable/disable foreign key constraint creation on this relation */
418
431
  createForeignKeyConstraint?: boolean;
419
432
  }
420
- export interface ManyToManyOptions<Owner, Target> extends ReferenceOptions<Owner, Target> {
433
+ export interface ManyToManyOptions<Owner, Target> extends ReferenceOptions<Owner, Target>, PolymorphicOptions {
421
434
  /** Set this side as owning. Owning side is where the foreign key is defined. This option is not required if you use `inversedBy` or `mappedBy` to distinguish owning and inverse side. */
422
435
  owner?: boolean;
423
436
  /** Point to the inverse side property name. */
@@ -465,6 +478,9 @@ export interface EmbeddedOptions<Owner, Target> extends PropertyOptions<Owner> {
465
478
  export interface EmbeddableOptions<Owner> {
466
479
  /** Specify constructor parameters to be used in `em.create` or when `forceConstructor` is enabled. Those should be names of declared entity properties in the same order as your constructor uses them. The ORM tries to infer those automatically, use this option in case the inference fails. */
467
480
  constructorParams?: (Owner extends EntityClass<infer P> ? keyof P : string)[];
481
+ /** For polymorphic embeddables. Specify the property name that stores the discriminator value. Alias for `discriminatorColumn`. */
482
+ discriminator?: (Owner extends EntityClass<infer P> ? keyof P : string) | AnyString;
483
+ /** For polymorphic embeddables. @deprecated Use `discriminator` instead. */
468
484
  discriminatorColumn?: (Owner extends EntityClass<infer P> ? keyof P : string) | AnyString;
469
485
  discriminatorMap?: Dictionary<string>;
470
486
  discriminatorValue?: number | string;
@@ -30,6 +30,10 @@ export declare abstract class AbstractNamingStrategy implements NamingStrategy {
30
30
  * @inheritDoc
31
31
  */
32
32
  manyToManyPropertyName(ownerEntityName: string, targetEntityName: string, pivotTableName: string, ownerTableName: string, schemaName?: string): string;
33
+ /**
34
+ * @inheritDoc
35
+ */
36
+ discriminatorColumnName(baseName: string): string;
33
37
  abstract classToTableName(entityName: string, tableName?: string): string;
34
38
  abstract joinColumnName(propertyName: string): string;
35
39
  abstract joinKeyColumnName(entityName: string, referencedColumnName?: string, composite?: boolean, tableName?: string): string;
@@ -85,4 +85,10 @@ export class AbstractNamingStrategy {
85
85
  manyToManyPropertyName(ownerEntityName, targetEntityName, pivotTableName, ownerTableName, schemaName) {
86
86
  return this.columnNameToProperty(pivotTableName.replace(new RegExp('^' + ownerTableName + '_'), ''));
87
87
  }
88
+ /**
89
+ * @inheritDoc
90
+ */
91
+ discriminatorColumnName(baseName) {
92
+ return this.propertyToColumnName(baseName + 'Type');
93
+ }
88
94
  }
@@ -95,4 +95,8 @@ export interface NamingStrategy {
95
95
  * @param schemaName - The schema name (if any)
96
96
  */
97
97
  manyToManyPropertyName(ownerEntityName: string, targetEntityName: string, pivotTableName: string, ownerTableName: string, schemaName?: string): string;
98
+ /**
99
+ * Returns the discriminator column name for polymorphic relations.
100
+ */
101
+ discriminatorColumnName(baseName: string): string;
98
102
  }
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.225",
4
+ "version": "7.0.0-dev.227",
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
@@ -295,7 +295,14 @@ export type EntityName<T = any> = EntityClass<T> | EntityCtor<T> | EntitySchema<
295
295
  export type GetRepository<Entity extends {
296
296
  [k: PropertyKey]: any;
297
297
  }, Fallback> = Entity[typeof EntityRepositoryType] extends EntityRepository<any> | undefined ? NonNullable<Entity[typeof EntityRepositoryType]> : Fallback;
298
- export type EntityDataPropValue<T> = T | Primary<T>;
298
+ type PolymorphicPrimaryInner<T> = T extends object ? Primary<T> extends readonly [infer First, infer Second, ...infer Rest] ? readonly [string, First, Second, ...Rest] | [string, First, Second, ...Rest] : readonly [string, Primary<T>] | [string, Primary<T>] : never;
299
+ /**
300
+ * Tuple format for polymorphic FK values: [discriminator, ...pkValues]
301
+ * Distributes over unions, so `Post | Comment` becomes `['post', number] | ['comment', number]`
302
+ * For composite keys like [tenantId, orgId], becomes ['discriminator', tenantId, orgId]
303
+ */
304
+ export type PolymorphicPrimary<T> = true extends IsUnion<T> ? PolymorphicPrimaryInner<T> : never;
305
+ export type EntityDataPropValue<T> = T | Primary<T> | PolymorphicPrimary<T>;
299
306
  type ExpandEntityProp<T, C extends boolean = false> = T extends Record<string, any> ? {
300
307
  [K in keyof T as CleanKeys<T, K>]?: EntityDataProp<ExpandProperty<T[K]>, C> | EntityDataPropValue<ExpandProperty<T[K]>> | null;
301
308
  } | EntityDataPropValue<ExpandProperty<T>> : T;
@@ -339,9 +346,9 @@ export type EntityData<T, C extends boolean = false> = {
339
346
  [K in EntityKey<T>]?: EntityDataItem<T[K] & {}, C>;
340
347
  };
341
348
  export type RequiredEntityData<T, I = never, C extends boolean = false> = {
342
- [K in keyof T as RequiredKeys<T, K, I>]: T[K] | RequiredEntityDataProp<T[K], T, C> | Primary<T[K]> | Raw;
349
+ [K in keyof T as RequiredKeys<T, K, I>]: T[K] | RequiredEntityDataProp<T[K], T, C> | Primary<T[K]> | PolymorphicPrimary<T[K]> | Raw;
343
350
  } & {
344
- [K in keyof T as OptionalKeys<T, K, I>]?: T[K] | RequiredEntityDataProp<T[K], T, C> | Primary<T[K]> | Raw | null;
351
+ [K in keyof T as OptionalKeys<T, K, I>]?: T[K] | RequiredEntityDataProp<T[K], T, C> | Primary<T[K]> | PolymorphicPrimary<T[K]> | Raw | null;
345
352
  };
346
353
  export type EntityDictionary<T> = EntityData<T> & Record<any, any>;
347
354
  type ExtractEagerProps<T> = T extends {
@@ -453,6 +460,11 @@ export interface EntityProperty<Owner = any, Target = any> {
453
460
  embeddable: EntityClass<Owner>;
454
461
  embeddedProps: Dictionary<EntityProperty>;
455
462
  discriminatorColumn?: string;
463
+ discriminator?: string;
464
+ polymorphic?: boolean;
465
+ polymorphTargets?: EntityMetadata[];
466
+ discriminatorMap?: Dictionary<EntityClass<Target>>;
467
+ discriminatorValue?: string;
456
468
  object?: boolean;
457
469
  index?: boolean | string;
458
470
  unique?: boolean | string;
@@ -614,6 +626,8 @@ export interface EntityMetadata<Entity = any, Class extends EntityCtor<Entity> =
614
626
  polymorphs?: EntityMetadata[];
615
627
  root: EntityMetadata<Entity>;
616
628
  definedProperties: Dictionary;
629
+ /** For polymorphic M:N pivot tables, maps discriminator values to entity classes */
630
+ polymorphicDiscriminatorMap?: Dictionary<EntityClass>;
617
631
  hasTriggers?: boolean;
618
632
  /** @internal can be used for computed numeric cache keys */
619
633
  readonly _id: number;
@@ -853,10 +867,10 @@ type ExtractStringKeys<T> = {
853
867
  type StringKeys<T, E extends string = never> = T extends object ? ExtractStringKeys<ExtractType<T>> | E : never;
854
868
  type GetStringKey<T, K extends StringKeys<T, string>, E extends string> = K extends keyof T ? ExtractType<T[K]> : (K extends E ? keyof T : never);
855
869
  type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
856
- type CollectionKeys<T> = T extends object ? {
857
- [K in keyof T]-?: T[K] extends CollectionShape ? IsAny<T[K]> extends true ? never : K & string : never;
870
+ type RelationKeys<T> = T extends object ? {
871
+ [K in keyof T]-?: CleanKeys<T, K, true>;
858
872
  }[keyof T] & {} : never;
859
- export type AutoPath<O, P extends string | boolean, E extends string = never, D extends Prev[number] = 9> = P extends boolean ? P : [D] extends [never] ? never : P extends any ? P extends string ? P extends `${infer A}.${infer B}` ? A extends StringKeys<O, E> ? `${A}.${AutoPath<NonNullable<GetStringKey<O, A, E>>, B, E, Prev[D]>}` : never : P extends StringKeys<O, E> ? (NonNullable<GetStringKey<O, P & StringKeys<O, E>, E>> extends unknown ? Exclude<P, `${string}.`> : never) | (StringKeys<NonNullable<GetStringKey<O, P & StringKeys<O, E>, E>>, E> extends never ? never : `${P & string}.`) : StringKeys<O, E> | `${CollectionKeys<O>}:ref` : never : never;
873
+ export type AutoPath<O, P extends string | boolean, E extends string = never, D extends Prev[number] = 9> = P extends boolean ? P : [D] extends [never] ? never : P extends any ? P extends string ? P extends `${infer A}.${infer B}` ? A extends StringKeys<O, E> ? `${A}.${AutoPath<NonNullable<GetStringKey<O, A, E>>, B, E, Prev[D]>}` : never : P extends StringKeys<O, E> ? (NonNullable<GetStringKey<O, P & StringKeys<O, E>, E>> extends unknown ? Exclude<P, `${string}.`> : never) | (StringKeys<NonNullable<GetStringKey<O, P & StringKeys<O, E>, E>>, E> extends never ? never : `${P & string}.`) : StringKeys<O, E> | `${RelationKeys<O>}:ref` : never : never;
860
874
  export type UnboxArray<T> = T extends any[] ? ArrayElement<T> : T;
861
875
  export type ArrayElement<ArrayType extends unknown[]> = ArrayType extends (infer ElementType)[] ? ElementType : never;
862
876
  export type ExpandProperty<T> = T extends ReferenceShape<infer U> ? NonNullable<U> : T extends CollectionShape<infer U> ? NonNullable<U> : T extends (infer U)[] ? NonNullable<U> : NonNullable<T>;