@mikro-orm/core 7.2.0-dev.2 → 7.2.0-dev.21

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.
Files changed (86) hide show
  1. package/EntityManager.d.ts +42 -8
  2. package/EntityManager.js +256 -70
  3. package/MikroORM.d.ts +4 -0
  4. package/MikroORM.js +9 -0
  5. package/cache/CacheAdapter.d.ts +6 -4
  6. package/cache/FileCacheAdapter.d.ts +1 -1
  7. package/cache/FileCacheAdapter.js +7 -2
  8. package/connections/Connection.d.ts +10 -1
  9. package/connections/Connection.js +9 -0
  10. package/drivers/DatabaseDriver.d.ts +17 -1
  11. package/drivers/DatabaseDriver.js +203 -41
  12. package/drivers/IDatabaseDriver.d.ts +1 -0
  13. package/entity/Collection.js +4 -2
  14. package/entity/EntityFactory.js +6 -0
  15. package/entity/EntityLoader.d.ts +7 -1
  16. package/entity/EntityLoader.js +46 -11
  17. package/entity/EntityRepository.d.ts +4 -5
  18. package/entity/EntityRepository.js +7 -2
  19. package/entity/defineEntity.d.ts +48 -14
  20. package/entity/defineEntity.js +32 -1
  21. package/enums.d.ts +5 -1
  22. package/enums.js +2 -0
  23. package/errors.d.ts +36 -0
  24. package/errors.js +90 -0
  25. package/events/EventManager.js +6 -3
  26. package/exceptions.d.ts +5 -0
  27. package/exceptions.js +5 -0
  28. package/hydration/ObjectHydrator.d.ts +2 -0
  29. package/hydration/ObjectHydrator.js +15 -8
  30. package/index.d.ts +1 -1
  31. package/metadata/EntitySchema.js +5 -2
  32. package/metadata/MetadataDiscovery.d.ts +3 -0
  33. package/metadata/MetadataDiscovery.js +161 -28
  34. package/metadata/MetadataProvider.js +1 -1
  35. package/metadata/MetadataStorage.d.ts +4 -3
  36. package/metadata/MetadataStorage.js +32 -2
  37. package/metadata/Routine.js +4 -1
  38. package/metadata/types.d.ts +19 -3
  39. package/naming-strategy/AbstractNamingStrategy.js +2 -1
  40. package/naming-strategy/NamingStrategy.d.ts +2 -1
  41. package/package.json +1 -1
  42. package/platforms/Platform.d.ts +24 -3
  43. package/platforms/Platform.js +63 -1
  44. package/types/BigIntType.d.ts +1 -0
  45. package/types/BigIntType.js +23 -0
  46. package/types/DateTimeType.d.ts +1 -0
  47. package/types/DateTimeType.js +8 -0
  48. package/types/StringType.d.ts +14 -3
  49. package/types/StringType.js +34 -4
  50. package/types/TextType.d.ts +2 -4
  51. package/types/TextType.js +2 -8
  52. package/types/Type.d.ts +11 -0
  53. package/types/Type.js +4 -4
  54. package/types/index.d.ts +2 -2
  55. package/typings.d.ts +58 -2
  56. package/typings.js +24 -1
  57. package/unit-of-work/ChangeSet.js +10 -7
  58. package/unit-of-work/ChangeSetPersister.js +32 -20
  59. package/unit-of-work/UnitOfWork.js +11 -4
  60. package/utils/AbstractMigrator.d.ts +1 -1
  61. package/utils/AbstractMigrator.js +3 -2
  62. package/utils/Configuration.d.ts +15 -1
  63. package/utils/Configuration.js +13 -2
  64. package/utils/Cursor.d.ts +2 -0
  65. package/utils/Cursor.js +44 -39
  66. package/utils/DataloaderUtils.js +2 -1
  67. package/utils/EntityComparator.d.ts +2 -0
  68. package/utils/EntityComparator.js +11 -4
  69. package/utils/QueryHelper.d.ts +17 -0
  70. package/utils/QueryHelper.js +100 -4
  71. package/utils/RawQueryFragment.d.ts +6 -0
  72. package/utils/RawQueryFragment.js +15 -6
  73. package/utils/RequestContext.d.ts +2 -2
  74. package/utils/RequestContext.js +11 -2
  75. package/utils/TransactionManager.d.ts +6 -0
  76. package/utils/TransactionManager.js +36 -3
  77. package/utils/Utils.d.ts +14 -2
  78. package/utils/Utils.js +36 -6
  79. package/utils/clone.js +6 -0
  80. package/utils/env-vars.js +1 -0
  81. package/utils/index.d.ts +1 -0
  82. package/utils/index.js +1 -0
  83. package/utils/rls-utils.d.ts +35 -0
  84. package/utils/rls-utils.js +97 -0
  85. package/utils/upsert-utils.d.ts +9 -1
  86. package/utils/upsert-utils.js +26 -3
@@ -165,10 +165,14 @@ export class MetadataDiscovery {
165
165
  filtered.forEach(meta => this.initAutoincrement(meta)); // once again after we init custom types
166
166
  filtered.forEach(meta => this.initCheckConstraints(meta));
167
167
  filtered.forEach(meta => this.initTriggers(meta));
168
- forEachProp((_m, p) => {
168
+ // filter names are load-bearing for RLS (policy and session variable names), backfill from the dictionary key
169
+ filtered.forEach(meta => Object.entries(meta.filters).forEach(([key, filter]) => (filter.name ??= key)));
170
+ filtered.forEach(meta => this.initPolicies(meta));
171
+ forEachProp((m, p) => {
169
172
  this.initDefaultValue(p);
170
173
  this.inferTypeFromDefault(p);
171
174
  this.initRelation(p);
175
+ this.initThroughRelation(m, p);
172
176
  this.initColumnType(p);
173
177
  });
174
178
  forEachProp((m, p) => this.initIndexes(m, p));
@@ -212,14 +216,18 @@ export class MetadataDiscovery {
212
216
  .replace(/Array<(.*)>/, '$1') // unwrap array
213
217
  .replace(/\[]$/, '') // remove array suffix
214
218
  .replace(/\((.*)\)/, '$1'); // unwrap union types
219
+ // Names can be ambiguous when a minifier mangles two classes to the same name,
220
+ // so class references also need to be checked by identity.
221
+ const discoveredByIdentity = (target) => {
222
+ const cls = EntitySchema.is(target) ? target.meta.class : target;
223
+ return typeof cls !== 'function' || this.#discovered.some(m => m.class === cls);
224
+ };
215
225
  const missing = [];
216
226
  this.#discovered.forEach(meta => Object.values(meta.properties).forEach(prop => {
217
- if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.pivotEntity) {
218
- const pivotEntity = prop.pivotEntity;
219
- const target = typeof pivotEntity === 'function' && !pivotEntity.prototype
220
- ? pivotEntity()
221
- : pivotEntity;
222
- if (!this.#discovered.find(m => m.className === Utils.className(target))) {
227
+ const indirect = (prop.kind === ReferenceKind.MANY_TO_MANY ? prop.pivotEntity : prop.through);
228
+ if (indirect) {
229
+ const target = typeof indirect === 'function' && !indirect.prototype ? indirect() : indirect;
230
+ if (!this.#discovered.find(m => m.className === Utils.className(target)) || !discoveredByIdentity(target)) {
223
231
  missing.push(target);
224
232
  }
225
233
  }
@@ -227,7 +235,8 @@ export class MetadataDiscovery {
227
235
  const target = typeof prop.entity === 'function' && !prop.entity.prototype ? prop.entity() : prop.type;
228
236
  if (!unwrap(prop.type)
229
237
  .split(/ ?\| ?/)
230
- .every(type => this.#discovered.find(m => m.className === type))) {
238
+ .every(type => this.#discovered.find(m => m.className === type)) ||
239
+ !Utils.asArray(target).every(discoveredByIdentity)) {
231
240
  missing.push(...Utils.asArray(target));
232
241
  }
233
242
  }
@@ -275,9 +284,11 @@ export class MetadataDiscovery {
275
284
  continue;
276
285
  }
277
286
  parent = Object.getPrototypeOf(meta.class);
278
- // Skip if parent is the auto-generated base class for the same entity (from setClass usage)
287
+ // Skip if parent is the auto-generated base class for the same entity (from setClass usage).
288
+ // A parent carrying its own decorator metadata is a real base class even when a minifier
289
+ // mangles it to the same name as the child.
279
290
  if (parent.name !== '' &&
280
- parent.name !== meta.className &&
291
+ (parent.name !== meta.className || Object.hasOwn(parent, MetadataStorage.META_SYMBOL)) &&
281
292
  !this.#metadata.has(parent) &&
282
293
  parent !== BaseEntity) {
283
294
  this.discoverReferences([parent], false);
@@ -311,7 +322,12 @@ export class MetadataDiscovery {
311
322
  const cls = entity;
312
323
  const path = cls[MetadataStorage.PATH_SYMBOL];
313
324
  if (path) {
314
- const meta = Utils.copy(MetadataStorage.getMetadata(cls.name, path), false);
325
+ // Prefer the metadata stored on the class reference, the `className-path` key can
326
+ // collide when a minifier mangles two classes to the same name.
327
+ const stored = Object.hasOwn(cls, MetadataStorage.META_SYMBOL)
328
+ ? cls[MetadataStorage.META_SYMBOL]
329
+ : MetadataStorage.getMetadata(cls.name, path);
330
+ const meta = Utils.copy(stored, false);
315
331
  meta.path = path;
316
332
  this.#metadata.set(cls, meta);
317
333
  }
@@ -427,7 +443,18 @@ export class MetadataDiscovery {
427
443
  if (prop.kind === ReferenceKind.SCALAR || prop.kind === ReferenceKind.EMBEDDED) {
428
444
  prop.fieldNames = [this.#namingStrategy.propertyToColumnName(prop.name, object)];
429
445
  }
430
- else if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && !prop.polymorphic) {
446
+ else if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && prop.polymorphic) {
447
+ if (prop.targetMeta) {
448
+ // same layout as `initManyToOneFields` builds later: `[discriminatorColumn, ...fkIdColumns]`
449
+ const pkFields = prop.targetMeta.getPrimaryProps().flatMap(pk => {
450
+ this.initFieldName(pk);
451
+ return pk.fieldNames;
452
+ });
453
+ const idColumns = pkFields.map(fieldName => this.#namingStrategy.joinKeyColumnName(prop.discriminator, fieldName, pkFields.length > 1));
454
+ prop.fieldNames = [prop.discriminatorColumn, ...idColumns];
455
+ }
456
+ }
457
+ else if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
431
458
  prop.fieldNames = this.initManyToOneFieldName(prop, prop.name);
432
459
  }
433
460
  else if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.owner) {
@@ -437,10 +464,13 @@ export class MetadataDiscovery {
437
464
  initManyToOneFieldName(prop, name) {
438
465
  const meta2 = prop.targetMeta;
439
466
  const ret = [];
440
- for (const primaryKey of meta2.primaryKeys) {
441
- this.initFieldName(meta2.properties[primaryKey]);
442
- for (const fieldName of meta2.properties[primaryKey].fieldNames) {
443
- ret.push(this.#namingStrategy.joinKeyColumnName(name, fieldName, meta2.compositePK));
467
+ // with `targetKey` on a composite PK target, derive the FK field name from that property
468
+ // instead of the PKs (simple PK targets keep the PK based naming for backwards compatibility)
469
+ const referencedKeys = prop.targetKey && meta2.compositePK ? [prop.targetKey] : meta2.primaryKeys;
470
+ for (const referencedKey of referencedKeys) {
471
+ this.initFieldName(meta2.properties[referencedKey]);
472
+ for (const fieldName of meta2.properties[referencedKey].fieldNames) {
473
+ ret.push(this.#namingStrategy.joinKeyColumnName(name, fieldName, !prop.targetKey && meta2.compositePK));
444
474
  }
445
475
  }
446
476
  return ret;
@@ -834,7 +864,7 @@ export class MetadataDiscovery {
834
864
  pivotMeta.properties[primaryProp.name] = primaryProp;
835
865
  pivotMeta.compositePK = false;
836
866
  }
837
- const discriminatorProp = this.createPivotScalarProperty(discriminatorColumn, [this.#platform.getVarcharTypeDeclarationSQL(prop)], [discriminatorColumn], { type: 'string', primary: !isCompositePK, nullable: false });
867
+ const discriminatorProp = this.createPivotScalarProperty(discriminatorColumn, [this.#platform.getVarcharTypeDeclarationSQL(prop)], [discriminatorColumn], { type: 'string', primary: !prop.fixedOrder, nullable: false });
838
868
  this.initFieldName(discriminatorProp);
839
869
  pivotMeta.properties[discriminatorColumn] = discriminatorProp;
840
870
  const columnTypes = this.getPrimaryKeyColumnTypes(meta);
@@ -849,7 +879,7 @@ export class MetadataDiscovery {
849
879
  pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, columnTypes, [...prop.joinColumns], { type: meta.className, persist: false });
850
880
  }
851
881
  else {
852
- pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, columnTypes, [...prop.joinColumns], { type: meta.className, primary: true, nullable: false });
882
+ pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, columnTypes, [...prop.joinColumns], { type: meta.className, primary: !prop.fixedOrder, nullable: false });
853
883
  }
854
884
  pivotMeta.properties[targetMeta.className + '_inverse'] = this.definePivotProperty(prop, targetMeta.className + '_inverse', targetMeta.class, prop.discriminator, false, false);
855
885
  // Create virtual M:1 relation to the polymorphic owner for single-query join loading
@@ -871,11 +901,11 @@ export class MetadataDiscovery {
871
901
  const discriminatorColumn = prop.discriminatorColumn;
872
902
  const targets = prop.polymorphTargets;
873
903
  pivotMeta.properties[meta.name + '_owner'] = this.definePivotProperty(prop, meta.name + '_owner', meta.class, prop.discriminator, true, false);
874
- const discriminatorProp = this.createPivotScalarProperty(discriminatorColumn, [this.#platform.getVarcharTypeDeclarationSQL(prop)], [discriminatorColumn], { type: 'string', primary: true, nullable: false });
904
+ const discriminatorProp = this.createPivotScalarProperty(discriminatorColumn, [this.#platform.getVarcharTypeDeclarationSQL(prop)], [discriminatorColumn], { type: 'string', primary: !prop.fixedOrder, nullable: false });
875
905
  this.initFieldName(discriminatorProp);
876
906
  pivotMeta.properties[discriminatorColumn] = discriminatorProp;
877
907
  const firstTargetColumnTypes = this.getPrimaryKeyColumnTypes(targets[0]);
878
- pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, firstTargetColumnTypes, [...prop.inverseJoinColumns], { type: targets[0].className, primary: true, nullable: false });
908
+ pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, firstTargetColumnTypes, [...prop.inverseJoinColumns], { type: targets[0].className, primary: !prop.fixedOrder, nullable: false });
879
909
  pivotMeta.polymorphicDiscriminatorMap ??= {};
880
910
  for (const targetMeta of targets) {
881
911
  const relationName = `${prop.discriminator}_${targetMeta.tableName}`;
@@ -942,6 +972,11 @@ export class MetadataDiscovery {
942
972
  return primaryProp;
943
973
  }
944
974
  definePivotProperty(prop, name, type, inverse, owner, selfReferencing) {
975
+ let index = prop.index ?? this.#platform.indexForeignKeys();
976
+ if (owner && prop.index) {
977
+ // owner join columns are the leading prefix of the composite PK, so an explicit `index` only applies to them with `fixedOrder`; a custom index name always belongs to the inverse side
978
+ index = prop.fixedOrder ? true : this.#platform.indexForeignKeys();
979
+ }
945
980
  const ret = {
946
981
  name,
947
982
  type: Utils.className(type),
@@ -950,7 +985,7 @@ export class MetadataDiscovery {
950
985
  cascade: [Cascade.ALL],
951
986
  fixedOrder: prop.fixedOrder,
952
987
  fixedOrderColumn: prop.fixedOrderColumn,
953
- index: this.#platform.indexForeignKeys(),
988
+ index,
954
989
  primary: !prop.fixedOrder,
955
990
  autoincrement: false,
956
991
  updateRule: prop.updateRule,
@@ -1036,11 +1071,18 @@ export class MetadataDiscovery {
1036
1071
  }
1037
1072
  // TPT children have their own tables that don't contain the parent's columns,
1038
1073
  // so propagating parent indexes/uniques/checks/triggers would target missing columns.
1074
+ // deep equality, as the subclass items might be copies of the base class ones (e.g. with TC39 decorators)
1039
1075
  if (meta.inheritanceType !== 'tpt' || !meta.tptParent) {
1040
- meta.indexes = Utils.unique([...base.indexes, ...meta.indexes]);
1041
- meta.uniques = Utils.unique([...base.uniques, ...meta.uniques]);
1042
- meta.checks = Utils.unique([...base.checks, ...meta.checks]);
1043
- meta.triggers = Utils.unique([...base.triggers, ...meta.triggers]);
1076
+ meta.indexes = Utils.unique([...base.indexes, ...meta.indexes], Utils.equals);
1077
+ meta.uniques = Utils.unique([...base.uniques, ...meta.uniques], Utils.equals);
1078
+ meta.checks = Utils.unique([...base.checks, ...meta.checks], Utils.equals);
1079
+ meta.triggers = Utils.unique([...base.triggers, ...meta.triggers], Utils.equals);
1080
+ }
1081
+ // Policies pass down only from inlined abstract bases; STI children share the root table
1082
+ // and TPT children own their tables, so both declare policies directly on the root/child.
1083
+ if (base.abstract && base.inheritanceType !== 'sti' && (meta.inheritanceType !== 'tpt' || !meta.tptParent)) {
1084
+ meta.policies = Utils.unique([...base.policies, ...meta.policies]);
1085
+ meta.rowLevelSecurity ??= base.rowLevelSecurity;
1044
1086
  }
1045
1087
  const pks = Object.values(meta.properties)
1046
1088
  .filter(p => p.primary)
@@ -1184,7 +1226,11 @@ export class MetadataDiscovery {
1184
1226
  return;
1185
1227
  }
1186
1228
  visited.add(embeddedProp);
1187
- const embeddable = this.#discovered.find(m => m.name === embeddedProp.type);
1229
+ // Prefer resolution via the class reference, the name can be ambiguous when a minifier
1230
+ // mangles two classes to the same name. Only named metadata counts, an auto-discovered
1231
+ // class without the `@Embeddable()` decorator should still fail as unknown below.
1232
+ const embeddable = this.#discovered.find(m => m.name && m.class === embeddedProp.target) ??
1233
+ this.#discovered.find(m => m.name === embeddedProp.type);
1188
1234
  if (!embeddable) {
1189
1235
  throw MetadataError.fromUnknownEntity(embeddedProp.type, `${meta.className}.${embeddedProp.name}`);
1190
1236
  }
@@ -1224,8 +1270,17 @@ export class MetadataDiscovery {
1224
1270
  if (embeddedProp.nullable || refInArray) {
1225
1271
  meta.properties[name].nullable = true;
1226
1272
  }
1273
+ // polymorphic relations derive their column names from the discriminator, so prefix it too
1274
+ if (meta.properties[name].polymorphic && !object) {
1275
+ meta.properties[name].discriminator = prefix + meta.properties[name].discriminator;
1276
+ meta.properties[name].discriminatorColumn = prefix + meta.properties[name].discriminatorColumn;
1277
+ }
1227
1278
  if (meta.properties[name].fieldNames) {
1228
- meta.properties[name].fieldNames[0] = prefix + meta.properties[name].fieldNames[0];
1279
+ const { fieldNames, polymorphic } = meta.properties[name];
1280
+ // polymorphic `fieldNames` hold `[discriminatorColumn, ...fkIdColumns]`, so prefix all of them
1281
+ for (let i = 0; i < (polymorphic ? fieldNames.length : 1); i++) {
1282
+ fieldNames[i] = prefix + fieldNames[i];
1283
+ }
1229
1284
  }
1230
1285
  else {
1231
1286
  const name2 = meta.properties[name].name;
@@ -1654,6 +1709,9 @@ export class MetadataDiscovery {
1654
1709
  if (prop.persist === false || prop.nativeEnumName || !prop.items?.every(item => typeof item === 'string')) {
1655
1710
  continue;
1656
1711
  }
1712
+ if (prop.customType instanceof t.json || ['json', 'jsonb'].includes(prop.columnTypes?.[0])) {
1713
+ continue;
1714
+ }
1657
1715
  this.initFieldName(prop);
1658
1716
  let expression = null;
1659
1717
  if (prop.enum) {
@@ -1693,6 +1751,28 @@ export class MetadataDiscovery {
1693
1751
  }
1694
1752
  meta.hasTriggers = true;
1695
1753
  }
1754
+ initPolicies(meta) {
1755
+ if (meta.policies.length === 0) {
1756
+ return;
1757
+ }
1758
+ const columns = meta.createSchemaColumnMappingObject();
1759
+ const table = this.createSchemaTable(meta);
1760
+ // resolve callbacks into a copy — the defs can be shared with siblings via an inlined abstract base,
1761
+ // and resolving in place would bake the first child's table into every other child's policy
1762
+ meta.policies = meta.policies.map(policy => {
1763
+ if (!(policy.using instanceof Function) && !(policy.check instanceof Function)) {
1764
+ return policy;
1765
+ }
1766
+ const resolved = { ...policy };
1767
+ if (resolved.using instanceof Function) {
1768
+ resolved.using = resolved.using(columns, table);
1769
+ }
1770
+ if (resolved.check instanceof Function) {
1771
+ resolved.check = resolved.check(columns, table);
1772
+ }
1773
+ return resolved;
1774
+ });
1775
+ }
1696
1776
  initGeneratedColumn(meta, prop) {
1697
1777
  if (!prop.generated && prop.columnTypes) {
1698
1778
  const match = /(.*) generated always as (.*)/i.exec(prop.columnTypes[0]);
@@ -1997,6 +2077,58 @@ export class MetadataDiscovery {
1997
2077
  }
1998
2078
  }
1999
2079
  }
2080
+ /** Resolves the `through` option of a virtual to-one relation into a read-only formula property. */
2081
+ initThroughRelation(meta, prop) {
2082
+ // already resolved, or not a through relation at all
2083
+ if (prop.through?.ownerProperty || !prop.through) {
2084
+ return;
2085
+ }
2086
+ if (![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
2087
+ throw MetadataError.throughRelationInvalidKind(meta, prop);
2088
+ }
2089
+ const targetMeta = prop.targetMeta;
2090
+ // the subquery selects a single column
2091
+ if (targetMeta.compositePK) {
2092
+ throw MetadataError.throughRelationCompositeTarget(meta, prop);
2093
+ }
2094
+ const through = prop.through;
2095
+ const throughMeta = this.#metadata.get(!through.prototype ? through() : through);
2096
+ // a property is considered to point at an entity when it targets it or one of its parents
2097
+ const pointsTo = (p, m) => {
2098
+ const candidate = this.#metadata.find(p.target);
2099
+ /* v8 ignore next 3 */
2100
+ if (!candidate) {
2101
+ return false;
2102
+ }
2103
+ return candidate.class === m.class || m.class.prototype instanceof candidate.class;
2104
+ };
2105
+ const fks = Object.values(throughMeta.properties).filter(p => p.kind === ReferenceKind.MANY_TO_ONE);
2106
+ const ownerProp = fks.find(p => pointsTo(p, meta));
2107
+ if (!ownerProp) {
2108
+ throw MetadataError.throughRelationMissingProperty(meta, prop, throughMeta, 'owner');
2109
+ }
2110
+ let targetProperty;
2111
+ const selectsTarget = throughMeta.class === targetMeta.class || throughMeta.class.prototype instanceof targetMeta.class;
2112
+ if (!selectsTarget) {
2113
+ const targetProp = fks.find(p => p !== ownerProp && pointsTo(p, targetMeta));
2114
+ if (!targetProp) {
2115
+ throw MetadataError.throughRelationMissingProperty(meta, prop, throughMeta, 'target');
2116
+ }
2117
+ targetProperty = targetProp.name;
2118
+ }
2119
+ prop.through = {
2120
+ entity: throughMeta.class,
2121
+ where: prop.where,
2122
+ orderBy: prop.orderBy ? Utils.asArray(prop.orderBy) : undefined,
2123
+ ownerProperty: ownerProp.name,
2124
+ targetProperty,
2125
+ };
2126
+ // the condition and ordering apply to the `through` entity, not to the target, so they must not leak into the target joins
2127
+ delete prop.where;
2128
+ delete prop.orderBy;
2129
+ prop.persist = false;
2130
+ prop.formula = columns => this.#platform.getThroughRelationFormula(prop, columns);
2131
+ }
2000
2132
  initColumnType(prop) {
2001
2133
  this.initUnsigned(prop);
2002
2134
  // Get the target properties for FK relations - use targetKey property if specified, otherwise PKs
@@ -2057,6 +2189,7 @@ export class MetadataDiscovery {
2057
2189
  prop.columnTypes.push(...columnTypes);
2058
2190
  if (!targetMeta.compositePK || prop.targetKey) {
2059
2191
  prop.customType = referencedProp.customType;
2192
+ prop.collation ??= referencedProp.collation;
2060
2193
  }
2061
2194
  }
2062
2195
  }
@@ -2112,7 +2245,7 @@ export class MetadataDiscovery {
2112
2245
  shouldForceConstructorUsage(meta) {
2113
2246
  const forceConstructor = this.#config.get('forceEntityConstructor');
2114
2247
  if (Array.isArray(forceConstructor)) {
2115
- return forceConstructor.some(cls => Utils.className(cls) === meta.className);
2248
+ return forceConstructor.some(cls => Utils.matchesEntity(cls, meta));
2116
2249
  }
2117
2250
  return forceConstructor;
2118
2251
  }
@@ -92,7 +92,7 @@ export class MetadataProvider {
92
92
  if (!this.useCache()) {
93
93
  return undefined;
94
94
  }
95
- const cache = meta.path && this.config.getMetadataCacheAdapter().get(this.getCacheKey(meta));
95
+ const cache = meta.path && this.config.getMetadataCacheAdapter().get(this.getCacheKey(meta), meta.path);
96
96
  if (cache) {
97
97
  this.loadFromCache(meta, cache);
98
98
  meta.root = root;
@@ -1,13 +1,14 @@
1
- import { type Dictionary, EntityMetadata, type EntityName } from '../typings.js';
1
+ import { type Dictionary, type EntityCtor, EntityMetadata, type EntityName } from '../typings.js';
2
2
  import type { EntityManager } from '../EntityManager.js';
3
3
  /** Registry that stores and provides access to entity metadata by class, name, or id. */
4
4
  export declare class MetadataStorage {
5
5
  #private;
6
6
  static readonly PATH_SYMBOL: unique symbol;
7
+ static readonly META_SYMBOL: unique symbol;
7
8
  constructor(metadata?: Dictionary<EntityMetadata>);
8
- /** Returns the global metadata dictionary, or a specific entry by entity name and path. */
9
+ /** Returns the global metadata dictionary, or a specific entry by entity name and path (keyed by the class reference when `target` is provided). */
9
10
  static getMetadata(): Dictionary<EntityMetadata>;
10
- static getMetadata<T = any>(entity: string, path: string): EntityMetadata<T>;
11
+ static getMetadata<T = any>(entity: string, path: string, target?: EntityCtor): EntityMetadata<T>;
11
12
  /** Checks whether an entity with the given class name exists in the global metadata. */
12
13
  static isKnownEntity(name: string): boolean;
13
14
  /** Clears all entries from the global metadata registry. */
@@ -11,11 +11,13 @@ function getGlobalStorage(namespace) {
11
11
  /** Registry that stores and provides access to entity metadata by class, name, or id. */
12
12
  export class MetadataStorage {
13
13
  static PATH_SYMBOL = Symbol.for('@mikro-orm/core/MetadataStorage.PATH_SYMBOL');
14
+ static META_SYMBOL = Symbol.for('@mikro-orm/core/MetadataStorage.META_SYMBOL');
14
15
  static #metadata = getGlobalStorage('metadata');
15
16
  #metadataMap = new Map();
16
17
  #idMap;
17
18
  #classNameMap;
18
19
  #uniqueNameMap;
20
+ #ambiguousNames = new Set();
19
21
  constructor(metadata = {}) {
20
22
  this.#idMap = {};
21
23
  this.#uniqueNameMap = {};
@@ -26,8 +28,21 @@ export class MetadataStorage {
26
28
  this.#metadataMap.set(meta.class, meta);
27
29
  }
28
30
  }
29
- static getMetadata(entity, path) {
31
+ static getMetadata(entity, path, target) {
30
32
  const key = entity && path ? entity + '-' + Utils.hash(path) : null;
33
+ // Key the registry by the class reference when available, so two classes minified
34
+ // to the same mangled name don't collide on the `className-path` key.
35
+ if (key && target) {
36
+ if (!Object.hasOwn(target, MetadataStorage.META_SYMBOL)) {
37
+ Object.defineProperty(target, MetadataStorage.META_SYMBOL, {
38
+ value: new EntityMetadata({ className: entity, path }),
39
+ writable: true,
40
+ });
41
+ }
42
+ // Keep the name-keyed entry in sync, the class-keyed metadata survives `MetadataStorage.clear()`.
43
+ MetadataStorage.#metadata[key] = target[MetadataStorage.META_SYMBOL];
44
+ return target[MetadataStorage.META_SYMBOL];
45
+ }
31
46
  if (key && !MetadataStorage.#metadata[key]) {
32
47
  MetadataStorage.#metadata[key] = new EntityMetadata({ className: entity, path });
33
48
  }
@@ -50,6 +65,10 @@ export class MetadataStorage {
50
65
  }
51
66
  /** Returns metadata for the given entity, optionally initializing it if not found. */
52
67
  get(entityName, init = false) {
68
+ // string lookups cannot be resolved when several classes were minified to the same name
69
+ if (typeof entityName === 'string' && this.#ambiguousNames.has(entityName)) {
70
+ throw MetadataError.ambiguousEntityName(entityName);
71
+ }
53
72
  const exists = this.find(entityName);
54
73
  if (exists) {
55
74
  return exists;
@@ -85,7 +104,13 @@ export class MetadataStorage {
85
104
  this.#metadataMap.set(entityName, meta);
86
105
  this.#idMap[meta._id] = meta;
87
106
  this.#uniqueNameMap[meta.uniqueName] = meta;
88
- this.#classNameMap[Utils.className(entityName)] = meta;
107
+ const className = Utils.className(entityName);
108
+ const existing = this.#classNameMap[className];
109
+ // track name collisions caused by minifiers mangling two classes to the same name
110
+ if (existing && existing !== meta && existing.class !== meta.class) {
111
+ this.#ambiguousNames.add(className);
112
+ }
113
+ this.#classNameMap[className] = meta;
89
114
  return meta;
90
115
  }
91
116
  /** Removes metadata for the given entity from all internal maps. */
@@ -96,6 +121,11 @@ export class MetadataStorage {
96
121
  delete this.#idMap[meta._id];
97
122
  delete this.#uniqueNameMap[meta.uniqueName];
98
123
  delete this.#classNameMap[meta.className];
124
+ // the name may still be ambiguous among the remaining metas
125
+ const remaining = new Set([...this.#metadataMap.values()].filter(m => m.className === meta.className).map(m => m.class));
126
+ if (remaining.size <= 1) {
127
+ this.#ambiguousNames.delete(meta.className);
128
+ }
99
129
  }
100
130
  }
101
131
  /** Decorates all entity prototypes with helper methods (e.g. init, toJSON). */
@@ -135,6 +135,9 @@ export class Routine {
135
135
  return new Routine(config);
136
136
  }
137
137
  static is(item) {
138
- return item instanceof Routine;
138
+ if (item instanceof Routine) {
139
+ return true;
140
+ }
141
+ return item != null && typeof item === 'object' && item.constructor?.name === 'Routine' && 'type' in item;
139
142
  }
140
143
  }
@@ -1,4 +1,4 @@
1
- import type { AnyEntity, Constructor, EntityName, AnyString, CheckCallback, GeneratedColumnCallback, FormulaCallback, FilterQuery, Dictionary, AutoPath, EntityClass, IndexCallback, ObjectQuery, Raw, SchemaColumns, TriggerDef } from '../typings.js';
1
+ import type { AnyEntity, Constructor, EntityName, AnyString, CheckCallback, GeneratedColumnCallback, FormulaCallback, FilterQuery, Dictionary, AutoPath, EntityClass, IndexCallback, ObjectQuery, Raw, SchemaColumns, TriggerDef, PolicyDef } from '../typings.js';
2
2
  import type { Cascade, LoadStrategy, DeferMode, QueryOrderMap, EmbeddedPrefixMode } from '../enums.js';
3
3
  import type { Type, types } from '../types/index.js';
4
4
  import type { EntityManager } from '../EntityManager.js';
@@ -104,6 +104,10 @@ export type EntityOptions<T, E = T extends EntityClass<infer P> ? P : T> = {
104
104
  hasTriggers?: boolean;
105
105
  /** Database triggers to create for this entity's table. (SQL drivers only) */
106
106
  triggers?: TriggerDef<E>[];
107
+ /** PostgreSQL row level security policies for this entity's table. Declaring policies implicitly enables RLS. */
108
+ policies?: PolicyDef<E>[];
109
+ /** Enables PostgreSQL row level security on this entity's table. `'force'` also enforces it for the table owner. Set to `false` to keep declared policies staged while leaving RLS disabled. */
110
+ rowLevelSecurity?: boolean | 'force';
107
111
  /**
108
112
  * PostgreSQL partitioning definition for this table.
109
113
  *
@@ -427,9 +431,15 @@ interface PolymorphicOptions {
427
431
  */
428
432
  discriminatorMap?: Dictionary<string>;
429
433
  }
430
- export interface ManyToOneOptions<Owner, Target> extends ReferenceOptions<Owner, Target>, PolymorphicOptions {
434
+ export interface ManyToOneOptions<Owner, Target, Through = Target> extends ReferenceOptions<Owner, Target>, PolymorphicOptions {
431
435
  /** Point to the inverse side property name. */
432
436
  inversedBy?: (string & keyof Target) | ((e: Target) => any);
437
+ /** Resolve this read-only relation via a subquery on another entity: a pivot entity with FKs to both sides, or the target itself to pick a single item out of a to-many relation (see {@doclink relationships#to-one-relations-through-another-entity | To-one relations through another entity}). */
438
+ through?: () => EntityName<Through>;
439
+ /** Condition applied on the `through` entity. */
440
+ where?: FilterQuery<Through>;
441
+ /** Ordering applied on the `through` entity, the first matching row is used. */
442
+ orderBy?: QueryOrderMap<Through> | QueryOrderMap<Through>[];
433
443
  /** Wrap the entity in {@apilink Reference} wrapper. */
434
444
  ref?: boolean;
435
445
  /** Use this relation as a primary key. */
@@ -481,9 +491,15 @@ export interface OneToManyOptions<Owner, Target> extends ReferenceOptions<Owner,
481
491
  /** Point to the owning side property name. */
482
492
  mappedBy: (string & keyof Target) | ((e: Target) => any);
483
493
  }
484
- export interface OneToOneOptions<Owner, Target> extends Partial<Omit<OneToManyOptions<Owner, Target>, 'orderBy'>>, PolymorphicOptions {
494
+ export interface OneToOneOptions<Owner, Target, Through = Target> extends Partial<Omit<OneToManyOptions<Owner, Target>, 'orderBy' | 'where'>>, PolymorphicOptions {
485
495
  /** 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. */
486
496
  owner?: boolean;
497
+ /** Resolve this read-only relation via a subquery on another entity: a pivot entity with FKs to both sides, or the target itself to pick a single item out of a to-many relation (see {@doclink relationships#to-one-relations-through-another-entity | To-one relations through another entity}). */
498
+ through?: () => EntityName<Through>;
499
+ /** Condition for {@doclink collections#declarative-partial-loading | Declarative partial loading}, or the condition applied on the `through` entity. */
500
+ where?: FilterQuery<Through>;
501
+ /** Ordering applied on the `through` entity, the first matching row is used. */
502
+ orderBy?: QueryOrderMap<Through> | QueryOrderMap<Through>[];
487
503
  /** Point to the inverse side property name. */
488
504
  inversedBy?: (string & keyof Target) | ((e: Target) => any);
489
505
  /** Wrap the entity in {@apilink Reference} wrapper. */
@@ -10,7 +10,8 @@ export class AbstractNamingStrategy {
10
10
  classToMigrationName(timestamp, customMigrationName) {
11
11
  let migrationName = `Migration${timestamp}`;
12
12
  if (customMigrationName) {
13
- migrationName += `_${customMigrationName}`;
13
+ // the name becomes part of a class identifier
14
+ migrationName += `_${customMigrationName.replace(/[^$\p{ID_Continue}]+/gu, '_')}`;
14
15
  }
15
16
  return migrationName;
16
17
  }
@@ -9,7 +9,8 @@ export interface NamingStrategy {
9
9
  */
10
10
  classToTableName(entityName: string, tableName?: string): string;
11
11
  /**
12
- * Return a migration name. This name should allow ordering.
12
+ * Return a migration name. This name should allow ordering, and has to be a valid class identifier,
13
+ * as it is used as the class name in the generated migration file.
13
14
  */
14
15
  classToMigrationName(timestamp: string, customMigrationName?: string): string;
15
16
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/core",
3
- "version": "7.2.0-dev.2",
3
+ "version": "7.2.0-dev.21",
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",
@@ -1,6 +1,6 @@
1
1
  import { EntityRepository } from '../entity/EntityRepository.js';
2
2
  import { type NamingStrategy } from '../naming-strategy/NamingStrategy.js';
3
- import type { Constructor, EntityMetadata, EntityProperty, IPrimaryKey, ISchemaGenerator, PopulateOptions, Primary, SimpleColumnMeta, FilterQuery, EntityValue, EntityKey } from '../typings.js';
3
+ import type { Constructor, EntityMetadata, EntityProperty, IPrimaryKey, ISchemaGenerator, PopulateOptions, Primary, SimpleColumnMeta, FilterQuery, EntityValue, EntityKey, FormulaColumns } from '../typings.js';
4
4
  import { ExceptionConverter } from './ExceptionConverter.js';
5
5
  import type { EntityManager } from '../EntityManager.js';
6
6
  import type { Configuration } from '../utils/Configuration.js';
@@ -211,8 +211,9 @@ export declare abstract class Platform {
211
211
  getBlobDeclarationSQL(): string;
212
212
  getJsonDeclarationSQL(): string;
213
213
  getSearchJsonPropertySQL(path: string, type: string, aliased: boolean): string | Raw;
214
- getSearchJsonPropertyKey(path: string[], type: string, aliased: boolean, value?: unknown): string | Raw;
215
- processJsonCondition<T extends object>(o: FilterQuery<T>, value: EntityValue<T>, path: EntityKey<T>[], alias: boolean): FilterQuery<T>;
214
+ /** When `aliased` is a string, it holds an explicit alias the key was prefixed with. */
215
+ getSearchJsonPropertyKey(path: string[], type: string, aliased: boolean | string, value?: unknown): string | Raw;
216
+ processJsonCondition<T extends object>(o: FilterQuery<T>, value: EntityValue<T>, path: EntityKey<T>[], alias: boolean | string): FilterQuery<T>;
216
217
  protected getJsonValueType(value: unknown): string;
217
218
  getJsonIndexDefinition(index: {
218
219
  columnNames: string[];
@@ -227,6 +228,12 @@ export declare abstract class Platform {
227
228
  formatIndexHint(indexNames: string[]): string | undefined;
228
229
  /** Whether the driver automatically parses JSON columns into JS objects. */
229
230
  convertsJsonAutomatically(): boolean;
231
+ /** Whether date values inside JSON documents keep their native type (e.g. BSON dates), instead of being serialized to ISO strings. */
232
+ preservesDatesInsideJson(): boolean;
233
+ /** Whether `nulls first`/`nulls last` can be requested in an `orderBy`. */
234
+ supportsNullsOrdering(): boolean;
235
+ /** Where nulls land when an `orderBy` requests no explicit placement: lowest (`asc` puts them first) or highest. */
236
+ sortsNullsLowest(): boolean;
230
237
  /** Converts a JS value to its JSON database representation (typically JSON.stringify). */
231
238
  convertJsonToDatabaseValue(value: unknown, context?: TransformContext): unknown;
232
239
  /** Converts a database JSON value to its JS representation. */
@@ -272,6 +279,11 @@ export declare abstract class Platform {
272
279
  formatQuery(sql: string, params: readonly any[]): string;
273
280
  /** Deep-clones embeddable data and tags it for JSON serialization. */
274
281
  cloneEmbeddable<T>(data: T): T;
282
+ /**
283
+ * Builds the correlated subquery used as the formula of a virtual to-one relation defined via `through`.
284
+ * @internal
285
+ */
286
+ getThroughRelationFormula(prop: EntityProperty, columns: FormulaColumns<any>): string;
275
287
  /** Initializes the platform with the ORM configuration. */
276
288
  setConfig(config: Configuration): void;
277
289
  /** Returns the current ORM configuration. */
@@ -313,6 +325,15 @@ export declare abstract class Platform {
313
325
  supportsDownMigrations(): boolean;
314
326
  /** Whether the platform supports deferred unique constraints. */
315
327
  supportsDeferredUniqueConstraints(): boolean;
328
+ /** Whether the platform supports row level security (PostgreSQL). */
329
+ supportsRowLevelSecurity(): boolean;
330
+ /** Whether the driver can apply the session context on every pooled connection acquire (`sessionContext: 'connection'`). */
331
+ supportsConnectionSessionContext(): boolean;
332
+ /**
333
+ * SQL cast suffix (e.g. `'::uuid'`, or `''` when none is needed) applied when an RLS filter reads a session
334
+ * variable via `current_setting()` as the given column type, or `null` if the type has no automatic cast.
335
+ */
336
+ getCurrentSettingCast(mappedType: Type<unknown>): string | null;
316
337
  /** Platform-specific validation of entity metadata. */
317
338
  validateMetadata(meta: EntityMetadata): void;
318
339
  /**