@mikro-orm/core 7.3.0-dev.17 → 7.3.0-dev.19

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.
@@ -183,10 +183,11 @@ export class MetadataDiscovery {
183
183
  }
184
184
  discovered.forEach(meta => meta.sync(true));
185
185
  this.#metadataProvider.combineCache();
186
+ const referencingProperties = this.indexReferencingProperties(filtered);
186
187
  return discovered.map(meta => {
187
188
  meta = this.#metadata.get(meta.class);
188
189
  meta.sync(true);
189
- this.findReferencingProperties(meta, filtered);
190
+ this.findReferencingProperties(meta, referencingProperties);
190
191
  if (meta.inheritanceType === 'tpt') {
191
192
  this.computeTPTOwnProps(meta);
192
193
  }
@@ -217,18 +218,20 @@ export class MetadataDiscovery {
217
218
  .replace(/Array<(.*)>/, '$1') // unwrap array
218
219
  .replace(/\[]$/, '') // remove array suffix
219
220
  .replace(/\((.*)\)/, '$1'); // unwrap union types
221
+ const discoveredNames = new Set(this.#discovered.map(m => m.className));
220
222
  // Names can be ambiguous when a minifier mangles two classes to the same name,
221
223
  // so class references also need to be checked by identity.
224
+ const discoveredClasses = new Set(this.#discovered.map(m => m.class));
222
225
  const discoveredByIdentity = (target) => {
223
226
  const cls = EntitySchema.is(target) ? target.meta.class : target;
224
- return typeof cls !== 'function' || this.#discovered.some(m => m.class === cls);
227
+ return typeof cls !== 'function' || discoveredClasses.has(cls);
225
228
  };
226
229
  const missing = [];
227
230
  this.#discovered.forEach(meta => Object.values(meta.properties).forEach(prop => {
228
231
  const indirect = (prop.kind === ReferenceKind.MANY_TO_MANY ? prop.pivotEntity : prop.through);
229
232
  if (indirect) {
230
233
  const target = typeof indirect === 'function' && !indirect.prototype ? indirect() : indirect;
231
- if (!this.#discovered.find(m => m.className === Utils.className(target)) || !discoveredByIdentity(target)) {
234
+ if (!discoveredNames.has(Utils.className(target)) || !discoveredByIdentity(target)) {
232
235
  missing.push(target);
233
236
  }
234
237
  }
@@ -236,7 +239,7 @@ export class MetadataDiscovery {
236
239
  const target = typeof prop.entity === 'function' && !prop.entity.prototype ? prop.entity() : prop.type;
237
240
  if (!unwrap(prop.type)
238
241
  .split(/ ?\| ?/)
239
- .every(type => this.#discovered.find(m => m.className === type)) ||
242
+ .every(type => discoveredNames.has(type)) ||
240
243
  !Utils.asArray(target).every(discoveredByIdentity)) {
241
244
  missing.push(...Utils.asArray(target));
242
245
  }
@@ -302,7 +305,8 @@ export class MetadataDiscovery {
302
305
  if (validate) {
303
306
  this.#validator.validateDiscovered(this.#discovered, this.#config.get('discovery'));
304
307
  }
305
- return this.#discovered.filter(meta => found.find(m => m.name === meta.className));
308
+ const foundNames = new Set(found.map(m => m.name));
309
+ return this.#discovered.filter(meta => foundNames.has(meta.className));
306
310
  }
307
311
  reset(entityName) {
308
312
  const exists = this.#discovered.findIndex(m => m.class === entityName || m.className === Utils.className(entityName));
@@ -463,7 +467,8 @@ export class MetadataDiscovery {
463
467
  }
464
468
  }
465
469
  initManyToOneFieldName(prop, name) {
466
- const meta2 = prop.targetMeta;
470
+ // STI property renaming runs before `initRelation` populates `targetMeta`
471
+ const meta2 = prop.targetMeta ?? this.#metadata.getByClassName(prop.type);
467
472
  const ret = [];
468
473
  // with `targetKey` on a composite PK target, derive the FK field name from that property
469
474
  // instead of the PKs (simple PK targets keep the PK based naming for backwards compatibility)
@@ -480,8 +485,53 @@ export class MetadataDiscovery {
480
485
  const meta2 = prop.targetMeta;
481
486
  return meta2.primaryKeys.map(() => this.#namingStrategy.propertyToColumnName(name));
482
487
  }
488
+ /**
489
+ * Walks up the TPT hierarchy to the entity whose M:N declaration this property inherits. A child redeclaring
490
+ * the property with a different target, pivot table, join columns or fixed order keeps its own pivot table.
491
+ */
492
+ getPivotDeclaringMeta(meta, prop) {
493
+ const keys = [
494
+ 'type',
495
+ 'pivotTable',
496
+ 'pivotEntity',
497
+ 'joinColumns',
498
+ 'inverseJoinColumns',
499
+ 'fixedOrder',
500
+ 'fixedOrderColumn',
501
+ ];
502
+ // Rails-style polymorphic owners already share an explicit pivot table, each under its own discriminator value
503
+ if (prop.polymorphic && !QueryHelper.isUnionTargetPolymorphic(prop)) {
504
+ return meta;
505
+ }
506
+ while (meta.tptParent) {
507
+ const parentProp = meta.tptParent.properties[prop.name];
508
+ if (parentProp?.kind !== ReferenceKind.MANY_TO_MANY) {
509
+ break;
510
+ }
511
+ // resolve the parent first so explicit options are compared against its final mapping regardless of discovery order
512
+ this.initManyToManyFields(meta.tptParent, parentProp);
513
+ if (keys.some(key => prop[key] != null && !Utils.equals(prop[key], parentProp[key]))) {
514
+ break;
515
+ }
516
+ meta = meta.tptParent;
517
+ }
518
+ return meta;
519
+ }
483
520
  initManyToManyFields(meta, prop) {
484
521
  const meta2 = prop.targetMeta;
522
+ // inverse sides derive their mapping from the owner via `mappedBy`
523
+ const declaringMeta = prop.mappedBy ? meta : this.getPivotDeclaringMeta(meta, prop);
524
+ if (declaringMeta !== meta) {
525
+ const prop2 = declaringMeta.properties[prop.name];
526
+ prop.pivotTable = prop2.pivotTable;
527
+ prop.pivotEntity = prop2.pivotEntity;
528
+ prop.fixedOrder = prop2.fixedOrder;
529
+ prop.fixedOrderColumn = prop2.fixedOrderColumn;
530
+ prop.joinColumns = prop2.joinColumns;
531
+ prop.inverseJoinColumns = prop2.inverseJoinColumns;
532
+ prop.referencedColumnNames = prop2.referencedColumnNames;
533
+ return;
534
+ }
485
535
  Utils.defaultValue(prop, 'fixedOrder', !!prop.fixedOrderColumn);
486
536
  const pivotMeta = this.#metadata.find(prop.pivotEntity);
487
537
  const props = Object.values(pivotMeta?.properties ?? {});
@@ -523,12 +573,16 @@ export class MetadataDiscovery {
523
573
  prop.referencedColumnNames ??= Utils.flatten(meta.primaryKeys.map(primaryKey => meta.properties[primaryKey].fieldNames));
524
574
  // Union-target polymorphic M:N: owner side is fixed (real FK), target side uses discriminator-derived names.
525
575
  const isUnionTargetMN = QueryHelper.isUnionTargetPolymorphic(prop);
576
+ // both sides of a TPT hierarchy would collide on the root name, so name them after the entities themselves
577
+ const sameTPTRoot = meta.inheritanceType === 'tpt' && meta.root.class === meta2.root.class;
578
+ const ownerName = sameTPTRoot ? meta.className : meta.root.className;
579
+ const targetName = sameTPTRoot ? meta2.className : meta2.root.className;
526
580
  if (prop.polymorphic && prop.discriminator && !isUnionTargetMN) {
527
581
  // Rails-style: owner side is polymorphic, uses discriminator base name (e.g. taggable_id instead of post_id)
528
582
  prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.#namingStrategy.joinKeyColumnName(prop.discriminator, referencedColumnName, prop.referencedColumnNames.length > 1));
529
583
  }
530
584
  else {
531
- prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.#namingStrategy.joinKeyColumnName(meta.root.className, referencedColumnName, meta.compositePK));
585
+ prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.#namingStrategy.joinKeyColumnName(ownerName, referencedColumnName, meta.compositePK));
532
586
  }
533
587
  if (isUnionTargetMN) {
534
588
  // Target side uses discriminator base name (e.g. attachable_id — shared across Image/Video)
@@ -536,7 +590,7 @@ export class MetadataDiscovery {
536
590
  prop.inverseJoinColumns ??= targetPkCols.map(fieldName => this.#namingStrategy.joinKeyColumnName(prop.discriminator, fieldName, targetPkCols.length > 1));
537
591
  }
538
592
  else {
539
- prop.inverseJoinColumns ??= this.initManyToOneFieldName(prop, meta2.root.className);
593
+ prop.inverseJoinColumns ??= this.initManyToOneFieldName(prop, targetName);
540
594
  }
541
595
  }
542
596
  isExplicitTableName(meta) {
@@ -635,8 +689,13 @@ export class MetadataDiscovery {
635
689
  return Object.values(meta.properties)
636
690
  .filter(prop => prop.kind === ReferenceKind.MANY_TO_MANY && prop.owner && prop.pivotTable)
637
691
  .map(prop => {
638
- const pivotMeta = this.definePivotTableEntity(meta, prop);
692
+ // an inherited M:N lives in the pivot table of the declaring TPT entity, whose table the pivot FK references
693
+ const declaringMeta = this.getPivotDeclaringMeta(meta, prop);
694
+ const ownerProp = declaringMeta.properties[prop.name];
695
+ const pivotMeta = this.definePivotTableEntity(declaringMeta, ownerProp);
639
696
  prop.pivotEntity = pivotMeta.class;
697
+ prop.inversedBy ??= ownerProp.inversedBy;
698
+ prop.fixedOrderColumn = ownerProp.fixedOrderColumn;
640
699
  if (prop.inversedBy) {
641
700
  prop.targetMeta.properties[prop.inversedBy].pivotEntity = pivotMeta.class;
642
701
  const targetRoot = prop.targetMeta.root;
@@ -649,14 +708,22 @@ export class MetadataDiscovery {
649
708
  // - regular inverse (Tag.posts mappedBy Post.tags) — handled by inversedBy above
650
709
  // - union-target inverse (Image.posts mappedBy Post.attachments) — on each polymorph target
651
710
  // - merged inverse (Tag.owners mappedBy [Post,Video].tags) — union collection on the target
652
- const inverseCandidates = QueryHelper.isUnionTargetPolymorphic(prop)
653
- ? prop.polymorphTargets
654
- : [prop.targetMeta];
711
+ // inherited inverse copies on TPT descendants of the target need the pivot as well
712
+ const withTPTDescendants = (m) => [
713
+ m,
714
+ ...(m.tptChildren ?? []).flatMap(withTPTDescendants),
715
+ ];
716
+ const targets = QueryHelper.isUnionTargetPolymorphic(prop) ? prop.polymorphTargets : [prop.targetMeta];
717
+ const inverseCandidates = targets.flatMap(withTPTDescendants);
655
718
  for (const targetMeta of inverseCandidates) {
656
719
  for (const inverseProp of Object.values(targetMeta.properties)) {
657
720
  if (inverseProp.kind === ReferenceKind.MANY_TO_MANY &&
658
721
  inverseProp.mappedBy === prop.name &&
659
- !inverseProp.pivotEntity) {
722
+ !inverseProp.pivotEntity &&
723
+ // a descendant can declare its own inverse of another owner's same-named M:N
724
+ targets.some(target => target.properties[inverseProp.name]?.mappedBy === prop.name) &&
725
+ // the target itself can carry an inherited inverse of a parent M:N it redeclares
726
+ (inverseProp.targetMeta?.properties[prop.name]?.pivotTable ?? prop.pivotTable) === prop.pivotTable) {
660
727
  inverseProp.pivotEntity = pivotMeta.class;
661
728
  inverseProp.pivotTable = pivotMeta.tableName;
662
729
  }
@@ -667,20 +734,47 @@ export class MetadataDiscovery {
667
734
  }
668
735
  return [];
669
736
  }
670
- findReferencingProperties(meta, metadata) {
737
+ /**
738
+ * Groups the relations of all entities by target class name in one pass, so `findReferencingProperties()`
739
+ * does not go over the relations of every entity for each entity. Lists keep the order of `metadata` and `relations`.
740
+ */
741
+ indexReferencingProperties(metadata) {
742
+ const index = { byType: new Map(), withTargetKey: new Map() };
743
+ const add = (map, key, value) => {
744
+ const list = map.get(key);
745
+ if (list) {
746
+ list.push(value);
747
+ }
748
+ else {
749
+ map.set(key, [value]);
750
+ }
751
+ };
671
752
  for (const meta2 of metadata) {
672
753
  for (const prop2 of meta2.relations) {
673
- if (prop2.kind !== ReferenceKind.SCALAR && prop2.type === meta.className) {
674
- meta.referencingProperties.push({ meta: meta2, prop: prop2 });
754
+ if (prop2.kind !== ReferenceKind.SCALAR) {
755
+ add(index.byType, prop2.type, [meta2, prop2]);
675
756
  }
676
- if (prop2.targetKey &&
677
- (prop2.type === meta.className || prop2.polymorphTargets?.some(t => t.className === meta.className)) &&
678
- !(meta.primaryKeys.length === 1 && meta.primaryKeys[0] === prop2.targetKey) &&
679
- !meta.root.targetKeys?.includes(prop2.targetKey)) {
680
- (meta.root.targetKeys ??= []).push(prop2.targetKey);
757
+ if (prop2.targetKey) {
758
+ const targets = new Set([prop2.type, ...(prop2.polymorphTargets?.map(t => t.className) ?? [])]);
759
+ for (const target of targets) {
760
+ add(index.withTargetKey, target, prop2);
761
+ }
681
762
  }
682
763
  }
683
764
  }
765
+ return index;
766
+ }
767
+ findReferencingProperties(meta, index) {
768
+ for (const [meta2, prop2] of index.byType.get(meta.className) ?? []) {
769
+ meta.referencingProperties.push({ meta: meta2, prop: prop2 });
770
+ }
771
+ for (const prop2 of index.withTargetKey.get(meta.className) ?? []) {
772
+ const targetKey = prop2.targetKey;
773
+ if (!(meta.primaryKeys.length === 1 && meta.primaryKeys[0] === targetKey) &&
774
+ !meta.root.targetKeys?.includes(targetKey)) {
775
+ (meta.root.targetKeys ??= []).push(targetKey);
776
+ }
777
+ }
684
778
  }
685
779
  initFactoryField(meta, prop) {
686
780
  ['mappedBy', 'inversedBy', 'pivotEntity'].forEach(type => {
@@ -785,11 +879,14 @@ export class MetadataDiscovery {
785
879
  else {
786
880
  pivotMeta2.compositePK = true;
787
881
  }
882
+ // STI entities share one table, so any m:n within the hierarchy is self-referencing, named after the root
883
+ const stiRoot = meta.root.inheritanceType === 'sti' ? meta.root : undefined;
884
+ const baseMeta = stiRoot ?? meta;
788
885
  // handle self-referenced m:n with same default field names
789
- if (meta.className === targetType &&
886
+ if ((meta.className === targetType || stiRoot === targetMeta.root) &&
790
887
  prop.joinColumns.every((joinColumn, idx) => joinColumn === prop.inverseJoinColumns[idx])) {
791
888
  // use tableName only when explicitly provided by user, otherwise use className for backwards compatibility
792
- const baseName = this.isExplicitTableName(meta) ? meta.tableName : meta.className;
889
+ const baseName = this.isExplicitTableName(baseMeta) ? baseMeta.tableName : baseMeta.className;
793
890
  prop.joinColumns = prop.referencedColumnNames.map(name => this.#namingStrategy.joinKeyColumnName(baseName + '_1', name, meta.compositePK));
794
891
  prop.inverseJoinColumns = prop.referencedColumnNames.map(name => this.#namingStrategy.joinKeyColumnName(baseName + '_2', name, meta.compositePK));
795
892
  if (prop.inversedBy) {
@@ -797,13 +894,23 @@ export class MetadataDiscovery {
797
894
  prop2.inverseJoinColumns = prop.joinColumns;
798
895
  prop2.joinColumns = prop.inverseJoinColumns;
799
896
  }
800
- // propagate updated joinColumns to all child entities that inherit this property (STI)
801
- for (const childMeta of this.#discovered.filter(m => m.root === meta && m !== meta)) {
897
+ // propagate updated joinColumns to all entities that inherit this property or its inverse (STI, or TPT sharing the pivot)
898
+ const inherits = (m) => m.root === baseMeta || (m.inheritanceType === 'tpt' && m.root === meta.root);
899
+ // includes `meta` itself: an STI root carries the inverse copy of an M:N declared on a child
900
+ for (const childMeta of this.#discovered.filter(inherits)) {
802
901
  const childProp = childMeta.properties[prop.name];
803
- if (childProp) {
902
+ // a TPT subclass redeclaring its own pivot table keeps its own columns
903
+ const sharesPivot = childMeta.inheritanceType !== 'tpt' || childProp?.pivotTable === prop.pivotTable;
904
+ if (childProp && sharesPivot) {
804
905
  childProp.joinColumns = prop.joinColumns;
805
906
  childProp.inverseJoinColumns = prop.inverseJoinColumns;
806
907
  }
908
+ const childProp2 = prop.inversedBy ? childMeta.properties[prop.inversedBy] : undefined;
909
+ // the inherited inverse can stay on this pivot even when the child redeclares the owner side
910
+ if (childProp2 && (childMeta.inheritanceType !== 'tpt' || childProp2.pivotTable === prop.pivotTable)) {
911
+ childProp2.inverseJoinColumns = prop.joinColumns;
912
+ childProp2.joinColumns = prop.inverseJoinColumns;
913
+ }
807
914
  }
808
915
  }
809
916
  // Union-target polymorphic M:N: discriminator + target FK share the pivot across multiple target types
@@ -1281,16 +1388,37 @@ export class MetadataDiscovery {
1281
1388
  // the prefix of the parent cannot be a boolean; it already passed here
1282
1389
  const prefix = this.getPrefix(embeddedProp, parentProperty);
1283
1390
  const glue = object ? '~' : '_';
1284
- for (const prop of Object.values(embeddable.properties)) {
1285
- const name = (embeddedProp.embeddedPath?.join(glue) ?? embeddedProp.fieldNames[0] + glue) + prop.name;
1391
+ const props = Object.values(embeddable.properties);
1392
+ // variants can declare a same-named property of a different kind, the virtual entity lacks it when the variants are shared with another union
1393
+ for (const prop of embeddable.polymorphs?.flatMap(m => Object.values(m.properties)) ?? []) {
1394
+ const same = props.find(p => p.name === prop.name && Utils.isSameKind(p, prop));
1395
+ if (!same && !prop.embedded) {
1396
+ props.push({ ...prop }); // a copy, the variant's metadata is shared with its other owners
1397
+ }
1398
+ else if (same && same.kind !== ReferenceKind.SCALAR && same.type !== prop.type) {
1399
+ // a column shared by relations to different targets cannot have a FK constraint
1400
+ same.createForeignKeyConstraint = false;
1401
+ }
1402
+ }
1403
+ for (const prop of props) {
1404
+ const sameName = props.filter(p => p.name === prop.name);
1405
+ // same-kind variants of a conflicting property share one owner property, as they share the column
1406
+ const first = sameName.find(p => Utils.isSameKind(p, prop));
1407
+ const conflict = first !== sameName[0];
1408
+ const name = (embeddedProp.embeddedPath?.join(glue) ?? embeddedProp.fieldNames[0] + glue) +
1409
+ prop.name +
1410
+ (conflict ? `_${props.indexOf(first)}` : '');
1286
1411
  meta.properties[name] = Utils.copy(prop);
1287
1412
  meta.properties[name].name = name;
1413
+ // STI column tracking between the variants does not apply to the owner's columns
1414
+ delete meta.properties[name].stiFieldNames;
1288
1415
  meta.properties[name].embedded = [embeddedProp.name, prop.name];
1289
1416
  meta.propertyOrder.set(name, (order += 0.01));
1290
- embeddedProp.embeddedProps[prop.name] = meta.properties[name];
1417
+ embeddedProp.embeddedProps[conflict ? name : prop.name] = meta.properties[name];
1291
1418
  meta.properties[name].persist ??= embeddedProp.persist;
1292
1419
  const refInArray = array && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && prop.owner;
1293
- if (embeddedProp.nullable || refInArray) {
1420
+ // each variant fills only its own column of a conflicting property
1421
+ if (embeddedProp.nullable || refInArray || sameName.some(p => !Utils.isSameKind(p, prop))) {
1294
1422
  meta.properties[name].nullable = true;
1295
1423
  }
1296
1424
  // polymorphic relations derive their column names from the discriminator, so prefix it too
@@ -1463,6 +1591,13 @@ export class MetadataDiscovery {
1463
1591
  rootProp.stiFieldNameMap[meta.discriminatorValue] = prop.fieldNames[0];
1464
1592
  // subtypes that only differ in type share the column
1465
1593
  rootProp.stiFieldNames = Utils.unique([...rootProp.stiFieldNames, ...prop.fieldNames]);
1594
+ // a column shared by relations to different targets cannot have a FK constraint
1595
+ for (const p of Object.values(meta.root.properties)) {
1596
+ const shared = p.type !== newProp.type && compareArrays(p.fieldNames ?? [], newProp.fieldNames);
1597
+ if (shared && p.kind !== ReferenceKind.SCALAR && Utils.isSameKind(p, newProp)) {
1598
+ p.createForeignKeyConstraint = newProp.createForeignKeyConstraint = false;
1599
+ }
1600
+ }
1466
1601
  newProp.nullable = true;
1467
1602
  newProp.name = name;
1468
1603
  newProp.hydrate = false;
@@ -1507,6 +1642,9 @@ export class MetadataDiscovery {
1507
1642
  if (isInverseSideRelation && rootProp && rootProp.type !== prop.type) {
1508
1643
  return;
1509
1644
  }
1645
+ // a sibling matching the root property must not drop the column variants of already renamed siblings
1646
+ newProp.stiFieldNames ??= rootProp?.stiFieldNames;
1647
+ newProp.stiFieldNameMap ??= rootProp?.stiFieldNameMap;
1510
1648
  meta.root.addProperty(newProp);
1511
1649
  });
1512
1650
  meta.tableName = meta.root.tableName;
@@ -1627,7 +1765,7 @@ export class MetadataDiscovery {
1627
1765
  if (meta.inheritanceType !== 'tpt') {
1628
1766
  return;
1629
1767
  }
1630
- const belongsToTable = (prop) => prop.persist !== false || prop.primary;
1768
+ const belongsToTable = (prop) => prop.persist !== false || prop.primary || !!prop.formula;
1631
1769
  // Use meta.properties (object) since meta.props (array) may not be populated yet
1632
1770
  const allProps = Object.values(meta.properties);
1633
1771
  if (!meta.tptParent) {
@@ -1873,9 +2011,9 @@ export class MetadataDiscovery {
1873
2011
  // guard preserves the legacy convention where JSON defaults are passed pre-stringified.
1874
2012
  // Array defaults are converted unconditionally (e.g. `default: []` on a JSON-backed embedded array).
1875
2013
  if (prop.customType && (Array.isArray(prop.default) || prop.customType.compareAsType() !== 'any')) {
1876
- val = prop.customType.convertToDatabaseValue(prop.default, this.#platform);
2014
+ val = prop.customType.convertToDatabaseValue(prop.default, this.#platform, { mode: 'discovery' });
1877
2015
  }
1878
- prop.defaultRaw = typeof val === 'string' ? `'${val}'` : '' + val;
2016
+ prop.defaultRaw = typeof val === 'string' ? this.#platform.quoteDefaultValue(val) : '' + val;
1879
2017
  }
1880
2018
  inferTypeFromDefault(prop) {
1881
2019
  if ((prop.defaultRaw == null && prop.default == null) || prop.type !== 'any') {
@@ -9,6 +9,7 @@ export declare class MetadataStorage {
9
9
  /** Returns the global metadata dictionary, or a specific entry by entity name and path (keyed by the class reference when `target` is provided). */
10
10
  static getMetadata(): Dictionary<EntityMetadata>;
11
11
  static getMetadata<T = any>(entity: string, path: string, target?: EntityCtor): EntityMetadata<T>;
12
+ private static hashPath;
12
13
  /** Checks whether an entity with the given class name exists in the global metadata. */
13
14
  static isKnownEntity(name: string): boolean;
14
15
  /** Clears all entries from the global metadata registry. */
@@ -13,6 +13,10 @@ export class MetadataStorage {
13
13
  static PATH_SYMBOL = Symbol.for('@mikro-orm/core/MetadataStorage.PATH_SYMBOL');
14
14
  static META_SYMBOL = Symbol.for('@mikro-orm/core/MetadataStorage.META_SYMBOL');
15
15
  static #metadata = getGlobalStorage('metadata');
16
+ /** Lives in the global storage next to the registry, so it stays in sync when several copies of this module share it (e.g. after `vi.resetModules()`). */
17
+ static #classNames = getGlobalStorage('metadata-class-names');
18
+ /** Hashes of entity file paths, decorators call `getMetadata()` with the same path for every decorated member. */
19
+ static #pathHashes = new Map();
16
20
  #metadataMap = new Map();
17
21
  #idMap;
18
22
  #classNameMap;
@@ -31,7 +35,7 @@ export class MetadataStorage {
31
35
  }
32
36
  }
33
37
  static getMetadata(entity, path, target) {
34
- const key = entity && path ? entity + '-' + Utils.hash(path) : null;
38
+ const key = entity && path ? entity + '-' + MetadataStorage.hashPath(path) : null;
35
39
  // Key the registry by the class reference when available, so two classes minified
36
40
  // to the same mangled name don't collide on the `className-path` key.
37
41
  if (key && target) {
@@ -43,23 +47,36 @@ export class MetadataStorage {
43
47
  }
44
48
  // Keep the name-keyed entry in sync, the class-keyed metadata survives `MetadataStorage.clear()`.
45
49
  MetadataStorage.#metadata[key] = target[MetadataStorage.META_SYMBOL];
50
+ MetadataStorage.#classNames.names?.add(target[MetadataStorage.META_SYMBOL].className);
46
51
  return target[MetadataStorage.META_SYMBOL];
47
52
  }
48
53
  if (key && !MetadataStorage.#metadata[key]) {
49
- MetadataStorage.#metadata[key] = new EntityMetadata({ className: entity, path });
54
+ const meta = new EntityMetadata({ className: entity, path });
55
+ MetadataStorage.#metadata[key] = meta;
56
+ MetadataStorage.#classNames.names?.add(meta.className);
50
57
  }
51
58
  if (key) {
52
59
  return MetadataStorage.#metadata[key];
53
60
  }
54
61
  return MetadataStorage.#metadata;
55
62
  }
63
+ static hashPath(path) {
64
+ let hash = MetadataStorage.#pathHashes.get(path);
65
+ if (hash === undefined) {
66
+ hash = Utils.hash(path);
67
+ MetadataStorage.#pathHashes.set(path, hash);
68
+ }
69
+ return hash;
70
+ }
56
71
  /** Checks whether an entity with the given class name exists in the global metadata. */
57
72
  static isKnownEntity(name) {
58
- return !!Object.values(this.#metadata).find(meta => meta.className === name);
73
+ this.#classNames.names ??= new Set(Object.values(this.#metadata).map(meta => meta.className));
74
+ return this.#classNames.names.has(name);
59
75
  }
60
76
  /** Clears all entries from the global metadata registry. */
61
77
  static clear() {
62
78
  Object.keys(this.#metadata).forEach(k => delete this.#metadata[k]);
79
+ delete this.#classNames.names;
63
80
  }
64
81
  /** Returns the map of all registered entity metadata. */
65
82
  getAll() {
@@ -118,12 +118,13 @@ export class MetadataValidator {
118
118
  return Utils.className(p);
119
119
  };
120
120
  const pivotProps = new Map();
121
+ const discoveredNames = new Set(discovered.map(m => m.className));
121
122
  // check for not discovered entities
122
123
  discovered.forEach(meta => Object.values(meta.properties).forEach(prop => {
123
124
  if (prop.kind !== ReferenceKind.SCALAR &&
124
125
  !unwrap(prop.type)
125
126
  .split(/ ?\| ?/)
126
- .every(type => discovered.find(m => m.className === type))) {
127
+ .every(type => discoveredNames.has(type))) {
127
128
  throw MetadataError.fromUnknownEntity(prop.type, `${meta.className}.${prop.name}`);
128
129
  }
129
130
  if (prop.pivotEntity) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/core",
3
- "version": "7.3.0-dev.17",
3
+ "version": "7.3.0-dev.19",
4
4
  "description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
5
5
  "keywords": [
6
6
  "data-mapper",
@@ -204,6 +204,8 @@ export declare abstract class Platform {
204
204
  supportsUnionWhere(): boolean;
205
205
  /** Returns the SQL type declaration used for array storage. */
206
206
  getArrayDeclarationSQL(): string;
207
+ /** Whether array columns support the `&&` (overlap) operator. */
208
+ supportsArrayOverlap(): boolean;
207
209
  /** Serializes a string array into its database storage format. */
208
210
  marshallArray(values: string[]): string;
209
211
  /** Deserializes a database-stored array string back into a string array. */
@@ -274,6 +276,8 @@ export declare abstract class Platform {
274
276
  }, quote?: string): string;
275
277
  /** Quotes a literal value for safe embedding in SQL. */
276
278
  quoteValue(value: any): string;
279
+ /** Quotes a string default value for DDL; dialects that escape with backslashes override this. */
280
+ quoteDefaultValue(value: string): string;
277
281
  escape(value: any): string;
278
282
  /** Replaces `?` placeholders in SQL with quoted parameter values. */
279
283
  formatQuery(sql: string, params: readonly any[]): string;
@@ -360,6 +360,10 @@ export class Platform {
360
360
  getArrayDeclarationSQL() {
361
361
  return 'text';
362
362
  }
363
+ /** Whether array columns support the `&&` (overlap) operator. */
364
+ supportsArrayOverlap() {
365
+ return false;
366
+ }
363
367
  /** Serializes a string array into its database storage format. */
364
368
  marshallArray(values) {
365
369
  return values.map(v => (/[,",\\]/.test(v) ? JSON.stringify(v) : v)).join(',');
@@ -573,6 +577,10 @@ export class Platform {
573
577
  quoteValue(value) {
574
578
  return value;
575
579
  }
580
+ /** Quotes a string default value for DDL; dialects that escape with backslashes override this. */
581
+ quoteDefaultValue(value) {
582
+ return `'${value.replaceAll("'", "''")}'`;
583
+ }
576
584
  /* v8 ignore next */
577
585
  escape(value) {
578
586
  return value;
@@ -4,20 +4,50 @@ import { ReferenceKind } from '../enums.js';
4
4
  import { Reference } from '../entity/Reference.js';
5
5
  import { SerializationContext } from './SerializationContext.js';
6
6
  import { isRaw } from '../utils/RawQueryFragment.js';
7
- /** Returns true when any entry in `items` is `propName`, the wildcard `*`, or a dot-path under `propName`. */
8
- function matchesPath(items, propName) {
9
- return items.some(item => item === propName || item === '*' || item.startsWith(propName + '.'));
7
+ // Keyed by option objects created for a single `serialize()` call, so caller-owned options are never cached.
8
+ const preparedOptions = new WeakMap();
9
+ function createPathLookup(items) {
10
+ const heads = new Set();
11
+ let all = false;
12
+ for (const item of items) {
13
+ const dot = item.indexOf('.');
14
+ heads.add(dot === -1 ? item : item.substring(0, dot));
15
+ all ||= item === '*';
16
+ }
17
+ return { all, heads };
18
+ }
19
+ /**
20
+ * Copies caller options once per `serialize()` call, so the derived child options and path lookups can be cached
21
+ * for every entity of the call without ever caching an options object the caller might mutate later.
22
+ */
23
+ function prepareOptions(options) {
24
+ if (preparedOptions.has(options)) {
25
+ return options;
26
+ }
27
+ const prepared = {
28
+ ...options,
29
+ populate: options.populate ? Utils.asArray(options.populate) : options.populate,
30
+ };
31
+ preparedOptions.set(prepared, { children: new Map() });
32
+ return prepared;
33
+ }
34
+ function pathLookup(options, key) {
35
+ return (preparedOptions.get(options)[key] ??= createPathLookup(options[key]));
36
+ }
37
+ /** Returns true when any entry in `options[key]` is `propName`, the wildcard `*`, or a dot-path under `propName`. */
38
+ function matchesPath(options, key, propName) {
39
+ const lookup = pathLookup(options, key);
40
+ return lookup.all || lookup.heads.has(propName);
10
41
  }
11
42
  function isVisible(meta, propName, options) {
12
43
  const prop = meta.properties[propName];
13
44
  if (options.groups && prop?.groups) {
14
45
  return prop.groups.some(g => options.groups.includes(g));
15
46
  }
16
- if (Array.isArray(options.fields) && options.fields.length > 0 && !matchesPath(options.fields, propName)) {
47
+ if (Array.isArray(options.fields) && options.fields.length > 0 && !matchesPath(options, 'fields', propName)) {
17
48
  return false;
18
49
  }
19
- if (Array.isArray(options.populate) &&
20
- options.populate.find(item => item === propName || item.startsWith(propName + '.'))) {
50
+ if (Array.isArray(options.populate) && pathLookup(options, 'populate').heads.has(propName)) {
21
51
  return true;
22
52
  }
23
53
  if (options.exclude?.find(item => item === propName)) {
@@ -30,7 +60,7 @@ function isVisible(meta, propName, options) {
30
60
  function isPopulated(propName, options) {
31
61
  if (typeof options.populate !== 'boolean' &&
32
62
  Array.isArray(options.populate) &&
33
- matchesPath(options.populate, propName)) {
63
+ matchesPath(options, 'populate', propName)) {
34
64
  return true;
35
65
  }
36
66
  if (typeof options.populate === 'boolean') {
@@ -45,10 +75,10 @@ export class EntitySerializer {
45
75
  const wrapped = helper(entity);
46
76
  const meta = wrapped.__meta;
47
77
  let contextCreated = false;
78
+ options = prepareOptions(options);
48
79
  if (!wrapped.__serializationContext.root) {
49
80
  const root = new SerializationContext();
50
81
  SerializationContext.propagate(root, entity, (meta, prop) => meta.properties[prop]?.kind !== ReferenceKind.SCALAR);
51
- options.populate = (options.populate ? Utils.asArray(options.populate) : options.populate);
52
82
  contextCreated = true;
53
83
  }
54
84
  const root = wrapped.__serializationContext.root;
@@ -145,7 +175,8 @@ export class EntitySerializer {
145
175
  if (entity[prop] instanceof Function) {
146
176
  const returnValue = entity[prop]();
147
177
  if (!options.ignoreSerializers && serializer) {
148
- return serializer(returnValue, this.extractChildOptions(options, prop));
178
+ // copy, so a serializer mutating its options cannot leak into the cached child options
179
+ return serializer(returnValue, { ...this.extractChildOptions(options, prop) });
149
180
  }
150
181
  return returnValue;
151
182
  }
@@ -186,7 +217,12 @@ export class EntitySerializer {
186
217
  return prop.customType.toJSON(value, platform);
187
218
  }
188
219
  static extractChildOptions(options, prop) {
189
- return {
220
+ const prepared = preparedOptions.get(options);
221
+ const cached = prepared.children.get(prop);
222
+ if (cached) {
223
+ return cached;
224
+ }
225
+ const child = {
190
226
  ...options,
191
227
  populate: Array.isArray(options.populate)
192
228
  ? Utils.extractChildElements(options.populate, prop, '*')
@@ -194,6 +230,9 @@ export class EntitySerializer {
194
230
  exclude: Array.isArray(options.exclude) ? Utils.extractChildElements(options.exclude, prop) : options.exclude,
195
231
  fields: Array.isArray(options.fields) ? this.extractChildFields(options.fields, prop) : options.fields,
196
232
  };
233
+ prepared.children.set(prop, child);
234
+ preparedOptions.set(child, { children: new Map() });
235
+ return child;
197
236
  }
198
237
  /**
199
238
  * Extracts the nested `fields` whitelist for a child property. A bare parent name (`fields: ['books']`) or a
@@ -269,8 +308,9 @@ export class EntitySerializer {
269
308
  * ```
270
309
  */
271
310
  export function serialize(entities, options) {
311
+ const prepared = prepareOptions(options ?? {});
272
312
  if (Array.isArray(entities)) {
273
- return entities.map(e => EntitySerializer.serialize(e, options));
313
+ return entities.map(e => EntitySerializer.serialize(e, prepared));
274
314
  }
275
- return EntitySerializer.serialize(entities, options);
315
+ return EntitySerializer.serialize(entities, prepared);
276
316
  }