@mikro-orm/core 7.2.0-dev.0 → 7.2.0-dev.10

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 (53) hide show
  1. package/EntityManager.d.ts +1 -1
  2. package/EntityManager.js +32 -26
  3. package/MikroORM.d.ts +4 -0
  4. package/MikroORM.js +6 -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 +7 -0
  9. package/connections/Connection.js +9 -0
  10. package/drivers/DatabaseDriver.d.ts +7 -0
  11. package/drivers/DatabaseDriver.js +72 -9
  12. package/entity/EntityAssigner.js +8 -2
  13. package/entity/EntityFactory.js +1 -1
  14. package/entity/EntityLoader.js +2 -1
  15. package/hydration/ObjectHydrator.js +3 -0
  16. package/metadata/EntitySchema.js +5 -2
  17. package/metadata/MetadataDiscovery.js +34 -11
  18. package/metadata/MetadataProvider.js +1 -1
  19. package/metadata/MetadataStorage.d.ts +4 -3
  20. package/metadata/MetadataStorage.js +15 -1
  21. package/metadata/MetadataValidator.js +1 -13
  22. package/metadata/Routine.js +4 -1
  23. package/naming-strategy/AbstractNamingStrategy.js +2 -1
  24. package/naming-strategy/NamingStrategy.d.ts +2 -1
  25. package/package.json +1 -1
  26. package/platforms/Platform.d.ts +2 -0
  27. package/platforms/Platform.js +4 -0
  28. package/typings.d.ts +2 -0
  29. package/unit-of-work/ChangeSet.js +10 -7
  30. package/unit-of-work/ChangeSetComputer.js +2 -1
  31. package/unit-of-work/ChangeSetPersister.js +15 -7
  32. package/unit-of-work/IdentityMap.d.ts +5 -0
  33. package/unit-of-work/IdentityMap.js +7 -0
  34. package/unit-of-work/UnitOfWork.d.ts +1 -1
  35. package/unit-of-work/UnitOfWork.js +2 -2
  36. package/utils/AbstractMigrator.d.ts +3 -1
  37. package/utils/AbstractMigrator.js +14 -3
  38. package/utils/Configuration.d.ts +6 -0
  39. package/utils/Configuration.js +3 -1
  40. package/utils/Cursor.js +1 -6
  41. package/utils/EntityComparator.js +8 -1
  42. package/utils/QueryHelper.d.ts +5 -0
  43. package/utils/QueryHelper.js +25 -0
  44. package/utils/RawQueryFragment.d.ts +6 -0
  45. package/utils/RawQueryFragment.js +15 -6
  46. package/utils/RequestContext.d.ts +2 -2
  47. package/utils/RequestContext.js +11 -2
  48. package/utils/TransactionManager.d.ts +6 -0
  49. package/utils/TransactionManager.js +36 -3
  50. package/utils/Utils.d.ts +12 -0
  51. package/utils/Utils.js +19 -4
  52. package/utils/clone.js +6 -0
  53. package/utils/env-vars.js +1 -0
@@ -212,6 +212,12 @@ export class MetadataDiscovery {
212
212
  .replace(/Array<(.*)>/, '$1') // unwrap array
213
213
  .replace(/\[]$/, '') // remove array suffix
214
214
  .replace(/\((.*)\)/, '$1'); // unwrap union types
215
+ // Names can be ambiguous when a minifier mangles two classes to the same name,
216
+ // so class references also need to be checked by identity.
217
+ const discoveredByIdentity = (target) => {
218
+ const cls = EntitySchema.is(target) ? target.meta.class : target;
219
+ return typeof cls !== 'function' || this.#discovered.some(m => m.class === cls);
220
+ };
215
221
  const missing = [];
216
222
  this.#discovered.forEach(meta => Object.values(meta.properties).forEach(prop => {
217
223
  if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.pivotEntity) {
@@ -219,7 +225,7 @@ export class MetadataDiscovery {
219
225
  const target = typeof pivotEntity === 'function' && !pivotEntity.prototype
220
226
  ? pivotEntity()
221
227
  : pivotEntity;
222
- if (!this.#discovered.find(m => m.className === Utils.className(target))) {
228
+ if (!this.#discovered.find(m => m.className === Utils.className(target)) || !discoveredByIdentity(target)) {
223
229
  missing.push(target);
224
230
  }
225
231
  }
@@ -227,7 +233,8 @@ export class MetadataDiscovery {
227
233
  const target = typeof prop.entity === 'function' && !prop.entity.prototype ? prop.entity() : prop.type;
228
234
  if (!unwrap(prop.type)
229
235
  .split(/ ?\| ?/)
230
- .every(type => this.#discovered.find(m => m.className === type))) {
236
+ .every(type => this.#discovered.find(m => m.className === type)) ||
237
+ !Utils.asArray(target).every(discoveredByIdentity)) {
231
238
  missing.push(...Utils.asArray(target));
232
239
  }
233
240
  }
@@ -275,9 +282,11 @@ export class MetadataDiscovery {
275
282
  continue;
276
283
  }
277
284
  parent = Object.getPrototypeOf(meta.class);
278
- // Skip if parent is the auto-generated base class for the same entity (from setClass usage)
285
+ // Skip if parent is the auto-generated base class for the same entity (from setClass usage).
286
+ // A parent carrying its own decorator metadata is a real base class even when a minifier
287
+ // mangles it to the same name as the child.
279
288
  if (parent.name !== '' &&
280
- parent.name !== meta.className &&
289
+ (parent.name !== meta.className || Object.hasOwn(parent, MetadataStorage.META_SYMBOL)) &&
281
290
  !this.#metadata.has(parent) &&
282
291
  parent !== BaseEntity) {
283
292
  this.discoverReferences([parent], false);
@@ -311,7 +320,12 @@ export class MetadataDiscovery {
311
320
  const cls = entity;
312
321
  const path = cls[MetadataStorage.PATH_SYMBOL];
313
322
  if (path) {
314
- const meta = Utils.copy(MetadataStorage.getMetadata(cls.name, path), false);
323
+ // Prefer the metadata stored on the class reference, the `className-path` key can
324
+ // collide when a minifier mangles two classes to the same name.
325
+ const stored = Object.hasOwn(cls, MetadataStorage.META_SYMBOL)
326
+ ? cls[MetadataStorage.META_SYMBOL]
327
+ : MetadataStorage.getMetadata(cls.name, path);
328
+ const meta = Utils.copy(stored, false);
315
329
  meta.path = path;
316
330
  this.#metadata.set(cls, meta);
317
331
  }
@@ -834,7 +848,7 @@ export class MetadataDiscovery {
834
848
  pivotMeta.properties[primaryProp.name] = primaryProp;
835
849
  pivotMeta.compositePK = false;
836
850
  }
837
- const discriminatorProp = this.createPivotScalarProperty(discriminatorColumn, [this.#platform.getVarcharTypeDeclarationSQL(prop)], [discriminatorColumn], { type: 'string', primary: !isCompositePK, nullable: false });
851
+ const discriminatorProp = this.createPivotScalarProperty(discriminatorColumn, [this.#platform.getVarcharTypeDeclarationSQL(prop)], [discriminatorColumn], { type: 'string', primary: !prop.fixedOrder, nullable: false });
838
852
  this.initFieldName(discriminatorProp);
839
853
  pivotMeta.properties[discriminatorColumn] = discriminatorProp;
840
854
  const columnTypes = this.getPrimaryKeyColumnTypes(meta);
@@ -849,7 +863,7 @@ export class MetadataDiscovery {
849
863
  pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, columnTypes, [...prop.joinColumns], { type: meta.className, persist: false });
850
864
  }
851
865
  else {
852
- pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, columnTypes, [...prop.joinColumns], { type: meta.className, primary: true, nullable: false });
866
+ pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, columnTypes, [...prop.joinColumns], { type: meta.className, primary: !prop.fixedOrder, nullable: false });
853
867
  }
854
868
  pivotMeta.properties[targetMeta.className + '_inverse'] = this.definePivotProperty(prop, targetMeta.className + '_inverse', targetMeta.class, prop.discriminator, false, false);
855
869
  // Create virtual M:1 relation to the polymorphic owner for single-query join loading
@@ -871,11 +885,11 @@ export class MetadataDiscovery {
871
885
  const discriminatorColumn = prop.discriminatorColumn;
872
886
  const targets = prop.polymorphTargets;
873
887
  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 });
888
+ const discriminatorProp = this.createPivotScalarProperty(discriminatorColumn, [this.#platform.getVarcharTypeDeclarationSQL(prop)], [discriminatorColumn], { type: 'string', primary: !prop.fixedOrder, nullable: false });
875
889
  this.initFieldName(discriminatorProp);
876
890
  pivotMeta.properties[discriminatorColumn] = discriminatorProp;
877
891
  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 });
892
+ pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, firstTargetColumnTypes, [...prop.inverseJoinColumns], { type: targets[0].className, primary: !prop.fixedOrder, nullable: false });
879
893
  pivotMeta.polymorphicDiscriminatorMap ??= {};
880
894
  for (const targetMeta of targets) {
881
895
  const relationName = `${prop.discriminator}_${targetMeta.tableName}`;
@@ -942,6 +956,11 @@ export class MetadataDiscovery {
942
956
  return primaryProp;
943
957
  }
944
958
  definePivotProperty(prop, name, type, inverse, owner, selfReferencing) {
959
+ let index = prop.index ?? this.#platform.indexForeignKeys();
960
+ if (owner && prop.index) {
961
+ // 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
962
+ index = prop.fixedOrder ? true : this.#platform.indexForeignKeys();
963
+ }
945
964
  const ret = {
946
965
  name,
947
966
  type: Utils.className(type),
@@ -950,7 +969,7 @@ export class MetadataDiscovery {
950
969
  cascade: [Cascade.ALL],
951
970
  fixedOrder: prop.fixedOrder,
952
971
  fixedOrderColumn: prop.fixedOrderColumn,
953
- index: this.#platform.indexForeignKeys(),
972
+ index,
954
973
  primary: !prop.fixedOrder,
955
974
  autoincrement: false,
956
975
  updateRule: prop.updateRule,
@@ -1184,7 +1203,11 @@ export class MetadataDiscovery {
1184
1203
  return;
1185
1204
  }
1186
1205
  visited.add(embeddedProp);
1187
- const embeddable = this.#discovered.find(m => m.name === embeddedProp.type);
1206
+ // Prefer resolution via the class reference, the name can be ambiguous when a minifier
1207
+ // mangles two classes to the same name. Only named metadata counts, an auto-discovered
1208
+ // class without the `@Embeddable()` decorator should still fail as unknown below.
1209
+ const embeddable = this.#discovered.find(m => m.name && m.class === embeddedProp.target) ??
1210
+ this.#discovered.find(m => m.name === embeddedProp.type);
1188
1211
  if (!embeddable) {
1189
1212
  throw MetadataError.fromUnknownEntity(embeddedProp.type, `${meta.className}.${embeddedProp.name}`);
1190
1213
  }
@@ -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,6 +11,7 @@ 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;
@@ -26,8 +27,21 @@ export class MetadataStorage {
26
27
  this.#metadataMap.set(meta.class, meta);
27
28
  }
28
29
  }
29
- static getMetadata(entity, path) {
30
+ static getMetadata(entity, path, target) {
30
31
  const key = entity && path ? entity + '-' + Utils.hash(path) : null;
32
+ // Key the registry by the class reference when available, so two classes minified
33
+ // to the same mangled name don't collide on the `className-path` key.
34
+ if (key && target) {
35
+ if (!Object.hasOwn(target, MetadataStorage.META_SYMBOL)) {
36
+ Object.defineProperty(target, MetadataStorage.META_SYMBOL, {
37
+ value: new EntityMetadata({ className: entity, path }),
38
+ writable: true,
39
+ });
40
+ }
41
+ // Keep the name-keyed entry in sync, the class-keyed metadata survives `MetadataStorage.clear()`.
42
+ MetadataStorage.#metadata[key] = target[MetadataStorage.META_SYMBOL];
43
+ return target[MetadataStorage.META_SYMBOL];
44
+ }
31
45
  if (key && !MetadataStorage.#metadata[key]) {
32
46
  MetadataStorage.#metadata[key] = new EntityMetadata({ className: entity, path });
33
47
  }
@@ -1,19 +1,7 @@
1
- import { Utils } from '../utils/Utils.js';
1
+ import { DANGEROUS_PROPERTY_NAMES, Utils } from '../utils/Utils.js';
2
2
  import { normalizePartitionNameForComparison, splitCommaSeparatedIdentifiers } from '../utils/partition-utils.js';
3
3
  import { MetadataError } from '../errors.js';
4
4
  import { ReferenceKind } from '../enums.js';
5
- /**
6
- * List of property names that could lead to prototype pollution vulnerabilities.
7
- * These names should never be used as entity property names because they could
8
- * allow malicious code to modify object prototypes when property values are assigned.
9
- *
10
- * - `__proto__`: Could modify the prototype chain
11
- * - `constructor`: Could modify the constructor property
12
- * - `prototype`: Could modify the prototype object
13
- *
14
- * @internal
15
- */
16
- const DANGEROUS_PROPERTY_NAMES = ['__proto__', 'constructor', 'prototype'];
17
5
  /**
18
6
  * @internal
19
7
  */
@@ -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
  }
@@ -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.0",
3
+ "version": "7.2.0-dev.10",
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",
@@ -227,6 +227,8 @@ export declare abstract class Platform {
227
227
  formatIndexHint(indexNames: string[]): string | undefined;
228
228
  /** Whether the driver automatically parses JSON columns into JS objects. */
229
229
  convertsJsonAutomatically(): boolean;
230
+ /** Whether date values inside JSON documents keep their native type (e.g. BSON dates), instead of being serialized to ISO strings. */
231
+ preservesDatesInsideJson(): boolean;
230
232
  /** Converts a JS value to its JSON database representation (typically JSON.stringify). */
231
233
  convertJsonToDatabaseValue(value: unknown, context?: TransformContext): unknown;
232
234
  /** Converts a database JSON value to its JS representation. */
@@ -465,6 +465,10 @@ export class Platform {
465
465
  convertsJsonAutomatically() {
466
466
  return true;
467
467
  }
468
+ /** Whether date values inside JSON documents keep their native type (e.g. BSON dates), instead of being serialized to ISO strings. */
469
+ preservesDatesInsideJson() {
470
+ return false;
471
+ }
468
472
  /** Converts a JS value to its JSON database representation (typically JSON.stringify). */
469
473
  convertJsonToDatabaseValue(value, context) {
470
474
  return JSON.stringify(value);
package/typings.d.ts CHANGED
@@ -1470,6 +1470,8 @@ export interface IMigrationGenerator {
1470
1470
  }
1471
1471
  /** Interface that all migration classes must implement. */
1472
1472
  export interface Migration {
1473
+ /** Stable migration name, used instead of the class name (which minifiers can mangle). */
1474
+ name?: string;
1473
1475
  up(): Promise<void> | void;
1474
1476
  down(): Promise<void> | void;
1475
1477
  isTransactional(): boolean;
@@ -29,15 +29,18 @@ export class ChangeSet {
29
29
  else {
30
30
  this.primaryKey = this.originalEntity[this.meta.primaryKeys[0]];
31
31
  }
32
- if (!this.meta.compositePK &&
33
- this.meta.getPrimaryProp().targetMeta?.compositePK &&
34
- typeof this.primaryKey === 'object' &&
35
- this.primaryKey !== null) {
36
- this.primaryKey = this.meta.getPrimaryProp().targetMeta.primaryKeys.map(childPK => {
37
- return this.primaryKey[childPK];
38
- });
32
+ const primaryProp = this.meta.getPrimaryProp();
33
+ const relationPK = !this.meta.compositePK && !!primaryProp.targetMeta?.compositePK;
34
+ // arrays are already the ordered tuple of the target's primary keys
35
+ if (relationPK && Utils.isPlainObject(this.primaryKey)) {
36
+ const pk = this.primaryKey;
37
+ this.primaryKey = primaryProp.targetMeta.primaryKeys.map(childPK => pk[childPK]);
39
38
  }
40
39
  if (object && this.primaryKey != null) {
40
+ // the whole tuple belongs to the single relation PK, it must not be spread over the (single) PK prop
41
+ if (relationPK) {
42
+ return { [primaryProp.name]: this.primaryKey };
43
+ }
41
44
  return Utils.primaryKeyToObject(this.meta, this.primaryKey);
42
45
  }
43
46
  return this.primaryKey ?? null;
@@ -138,7 +138,8 @@ export class ChangeSetComputer {
138
138
  return data;
139
139
  }
140
140
  processProperty(changeSet, prop, target) {
141
- if (!target) {
141
+ // check for `null`/`undefined` explicitly, the target can be a falsy raw PK value like `0` (e.g. with `mapToPk`)
142
+ if (target == null) {
142
143
  const targets = Utils.unwrapProperty(changeSet.entity, changeSet.meta, prop);
143
144
  targets.forEach(([t]) => this.processProperty(changeSet, prop, t));
144
145
  return;
@@ -3,7 +3,7 @@ import { PolymorphicRef } from '../entity/PolymorphicRef.js';
3
3
  import { helper } from '../entity/wrap.js';
4
4
  import { ChangeSetType } from './ChangeSet.js';
5
5
  import { isRaw } from '../utils/RawQueryFragment.js';
6
- import { Utils } from '../utils/Utils.js';
6
+ import { equals, Utils } from '../utils/Utils.js';
7
7
  import { OptimisticLockError, ValidationError } from '../errors.js';
8
8
  import { ReferenceKind } from '../enums.js';
9
9
  /** @internal Executes change sets against the database, handling inserts, updates, and deletes. */
@@ -230,11 +230,15 @@ export class ChangeSetPersister {
230
230
  }
231
231
  const res = await this.#driver.nativeUpdateMany(meta.class, cond, payload, options);
232
232
  const map = new Map();
233
- res.rows?.forEach(item => map.set(Utils.getCompositeKeyHash(item, meta, true, this.#platform, true), item));
233
+ // returning rows are not mapped yet, so they are keyed by field names - we need to build the hash
234
+ // from those to be able to match them with `getSerializedPrimaryKey()` of the entity
235
+ const pkFields = meta.getPrimaryProps().flatMap(prop => prop.fieldNames);
236
+ res.rows?.forEach(item => map.set(Utils.getPrimaryKeyHash(pkFields.map(field => item[field])), item));
234
237
  for (const changeSet of changeSets) {
235
238
  if (res.rows) {
236
239
  const row = map.get(helper(changeSet.entity).getSerializedPrimaryKey());
237
- this.mapReturnedValues(changeSet.entity, changeSet.payload, row, meta);
240
+ // STI batches can mix child types, so map through the change set's own metadata
241
+ this.mapReturnedValues(changeSet.entity, changeSet.payload, row, changeSet.meta);
238
242
  }
239
243
  changeSet.persisted = true;
240
244
  }
@@ -334,7 +338,8 @@ export class ChangeSetPersister {
334
338
  });
335
339
  const res = await this.#driver.find(meta.root.class, { $or }, options);
336
340
  if (res.length !== changeSets.length) {
337
- const compare = (a, b, keys) => keys.every(k => a[k] === b[k]);
341
+ // a FK pointing to a composite PK is an array, so the values need to be compared deeply
342
+ const compare = (a, b, keys) => keys.every(k => equals(a[k], b[k]));
338
343
  const entity = changeSets.find(cs => {
339
344
  return !res.some(row => compare(Utils.getPrimaryKeyCond(cs.entity, primaryKeys), row, primaryKeys));
340
345
  }).entity;
@@ -375,7 +380,8 @@ export class ChangeSetPersister {
375
380
  changeSets.forEach(cs => {
376
381
  Utils.keys(cs.payload).forEach(k => {
377
382
  if (isRaw(cs.payload[k]) && isRaw(cs.entity[k])) {
378
- returning.add(meta.properties[k]);
383
+ // STI batches can mix child types, so the property might not exist on `meta`
384
+ returning.add(cs.meta.properties[k]);
379
385
  }
380
386
  });
381
387
  });
@@ -400,12 +406,14 @@ export class ChangeSetPersister {
400
406
  options = this.prepareOptions(meta, options, {
401
407
  fields: Utils.unique(reloadProps.map(prop => prop.name)),
402
408
  });
403
- const data = await this.#driver.find(meta.class, { [pk]: { $in: pks } }, options);
409
+ // a mixed STI batch shares one table but the child discriminator would filter out the siblings
410
+ const target = changeSets.some(cs => cs.meta !== meta) && meta.root.discriminatorColumn ? meta.root : meta;
411
+ const data = await this.#driver.find(target.class, { [pk]: { $in: pks } }, options);
404
412
  const map = new Map();
405
413
  data.forEach(item => map.set(Utils.getCompositeKeyHash(item, meta, false, this.#platform, true), item));
406
414
  for (const changeSet of changeSets) {
407
415
  const data = map.get(helper(changeSet.entity).getSerializedPrimaryKey());
408
- this.#hydrator.hydrate(changeSet.entity, meta, data, this.#factory, 'full', false, true);
416
+ this.#hydrator.hydrate(changeSet.entity, changeSet.meta, data, this.#factory, 'full', false, true);
409
417
  Object.assign(changeSet.payload, data); // merge to the changeset payload, so it gets saved to the entity snapshot
410
418
  }
411
419
  }
@@ -12,6 +12,11 @@ export declare class IdentityMap {
12
12
  storeByKey<T>(item: T, key: string, value: string, schema?: string): void;
13
13
  /** Removes an entity and its alternate key entries from the identity map. */
14
14
  delete<T>(item: T): void;
15
+ /**
16
+ * Retrieves the entity occupying the same slot as the given one, if any. Hashes the entity the same way `store()`
17
+ * does, so it matches regardless of the primary key shape — unlike hashing a primary key value obtained elsewhere.
18
+ */
19
+ getByEntity<T>(item: T): T | undefined;
15
20
  /** Retrieves an entity by its hash key from the identity map. */
16
21
  getByHash<T>(meta: EntityMetadata<T>, hash: string): T | undefined;
17
22
  /** Returns (or creates) the per-entity-class store within the identity map. */
@@ -40,6 +40,13 @@ export class IdentityMap {
40
40
  this.#alternateKeys.delete(item);
41
41
  }
42
42
  }
43
+ /**
44
+ * Retrieves the entity occupying the same slot as the given one, if any. Hashes the entity the same way `store()`
45
+ * does, so it matches regardless of the primary key shape — unlike hashing a primary key value obtained elsewhere.
46
+ */
47
+ getByEntity(item) {
48
+ return this.getStore(item.__meta.root).get(this.getPkHash(item));
49
+ }
43
50
  /** Retrieves an entity by its hash key from the identity map. */
44
51
  getByHash(meta, hash) {
45
52
  const store = this.getStore(meta);
@@ -48,7 +48,7 @@ export declare class UnitOfWork {
48
48
  */
49
49
  storeByKey<T extends object>(entity: T, key: string, value: unknown, schema?: string, convertCustomTypes?: boolean): void;
50
50
  /** Attempts to extract a primary key from the where condition and look up the entity in the identity map. */
51
- tryGetById<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, schema?: string, strict?: boolean): T | null;
51
+ tryGetById<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, schema?: string, strict?: boolean, convertCustomTypes?: boolean): T | null;
52
52
  /**
53
53
  * Returns map of all managed entities.
54
54
  */
@@ -227,12 +227,12 @@ export class UnitOfWork {
227
227
  this.#identityMap.storeByKey(entity, key, '' + value, schema);
228
228
  }
229
229
  /** Attempts to extract a primary key from the where condition and look up the entity in the identity map. */
230
- tryGetById(entityName, where, schema, strict = true) {
230
+ tryGetById(entityName, where, schema, strict = true, convertCustomTypes) {
231
231
  const pk = Utils.extractPK(where, this.#metadata.find(entityName), strict);
232
232
  if (!pk) {
233
233
  return null;
234
234
  }
235
- return this.getById(entityName, pk, schema);
235
+ return this.getById(entityName, pk, schema, convertCustomTypes);
236
236
  }
237
237
  /**
238
238
  * Returns map of all managed entities.
@@ -95,13 +95,15 @@ export declare abstract class AbstractMigrator<D extends IDatabaseDriver> implem
95
95
  */
96
96
  unlogMigration(name: string): Promise<void>;
97
97
  protected init(): Promise<void>;
98
+ /** Resolves the migrations path (including source folder auto-detection) without touching the filesystem. */
99
+ protected resolvePaths(): Promise<void>;
98
100
  protected initPaths(): Promise<void>;
99
101
  protected initServices(): void;
100
102
  protected resolve(params: {
101
103
  name: string;
102
104
  path: string;
103
105
  }): RunnableMigration;
104
- protected initialize(MigrationClass: Constructor<Migration>, name: string): RunnableMigration;
106
+ protected initialize(MigrationClass: Constructor<Migration>, name?: string): RunnableMigration;
105
107
  /**
106
108
  * Checks if `src` folder exists, it so, tries to adjust the migrations and seeders paths automatically to use it.
107
109
  * If there is a `dist` or `build` folder, it will be used for the JS variant (`path` option), while the `src` folder will be
@@ -9,6 +9,7 @@ export class AbstractMigrator {
9
9
  options;
10
10
  absolutePath;
11
11
  initialized = false;
12
+ #pathsEnsured = false;
12
13
  #listeners = new Map();
13
14
  constructor(em) {
14
15
  this.em = em;
@@ -303,7 +304,8 @@ export class AbstractMigrator {
303
304
  this.initialized = true;
304
305
  await this.initPaths();
305
306
  }
306
- async initPaths() {
307
+ /** Resolves the migrations path (including source folder auto-detection) without touching the filesystem. */
308
+ async resolvePaths() {
307
309
  if (this.absolutePath || this.options.migrationsList) {
308
310
  return;
309
311
  }
@@ -312,6 +314,14 @@ export class AbstractMigrator {
312
314
  /* v8 ignore next */
313
315
  const key = this.config.get('preferTs', Utils.detectTypeScriptSupport()) && this.options.pathTs ? 'pathTs' : 'path';
314
316
  this.absolutePath = fs.absolutePath(this.options[key], this.config.get('baseDir'));
317
+ }
318
+ async initPaths() {
319
+ await this.resolvePaths();
320
+ if (this.#pathsEnsured || this.options.migrationsList) {
321
+ return;
322
+ }
323
+ this.#pathsEnsured = true;
324
+ const { fs } = await import('@mikro-orm/core/fs-utils');
315
325
  try {
316
326
  fs.ensureDir(this.absolutePath);
317
327
  }
@@ -348,7 +358,8 @@ export class AbstractMigrator {
348
358
  initialize(MigrationClass, name) {
349
359
  const instance = new MigrationClass(this.driver, this.config);
350
360
  return {
351
- name: this.storage.getMigrationName(name),
361
+ // the constructor name is the last resort, minifiers can mangle it (e.g. when bundling)
362
+ name: this.storage.getMigrationName(name ?? instance.name ?? MigrationClass.name),
352
363
  up: afterRun => this.runner.run(instance, 'up', afterRun),
353
364
  down: afterRun => this.runner.run(instance, 'down', afterRun),
354
365
  };
@@ -399,7 +410,7 @@ export class AbstractMigrator {
399
410
  if (this.options.migrationsList) {
400
411
  return this.options.migrationsList.map(migration => {
401
412
  if (typeof migration === 'function') {
402
- return this.initialize(migration, migration.name);
413
+ return this.initialize(migration);
403
414
  }
404
415
  return this.initialize(migration.class, migration.name);
405
416
  });
@@ -233,6 +233,12 @@ export type MigrationsOptions = {
233
233
  * @default true
234
234
  */
235
235
  snapshot?: boolean;
236
+ /**
237
+ * Update the snapshot from the database schema when running migrations up or down.
238
+ * Disable to keep the snapshot managed solely by `migration:create`.
239
+ * @default true
240
+ */
241
+ snapshotOnMigrate?: boolean;
236
242
  /** Custom name for the snapshot file. */
237
243
  snapshotName?: string;
238
244
  /**
@@ -84,8 +84,10 @@ const DEFAULTS = {
84
84
  dropTables: true,
85
85
  safe: false,
86
86
  snapshot: true,
87
+ snapshotOnMigrate: true,
87
88
  emit: 'ts',
88
- fileName: (timestamp, name) => `Migration${timestamp}${name ? '_' + name : ''}`,
89
+ // mirrors `NamingStrategy.classToMigrationName`, so the file name matches the class it declares
90
+ fileName: (timestamp, name) => `Migration${timestamp}${name ? '_' + name.replace(/[^$\p{ID_Continue}]+/gu, '_') : ''}`,
89
91
  },
90
92
  schemaGenerator: {
91
93
  createForeignKeyConstraints: true,
package/utils/Cursor.js CHANGED
@@ -150,12 +150,7 @@ export class Cursor {
150
150
  return Buffer.from(JSON.stringify(value)).toString('base64url');
151
151
  }
152
152
  static decode(value) {
153
- return JSON.parse(Buffer.from(value, 'base64url').toString('utf8')).map((value) => {
154
- if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}/.exec(value)) {
155
- return new Date(value);
156
- }
157
- return value;
158
- });
153
+ return JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
159
154
  }
160
155
  static getDefinition(meta, orderBy) {
161
156
  return Utils.asArray(orderBy).flatMap(order => {
@@ -164,7 +164,9 @@ export class EntityComparator {
164
164
  const lines = [];
165
165
  const context = new Map();
166
166
  context.set('isEntityOrRef', (val) => Utils.isEntity(val, true));
167
- context.set('getCompositeKeyValue', (val) => Utils.flatten(Utils.getCompositeKeyValue(val, meta, 'convertToDatabaseValue', this.#platform)));
167
+ context.set('getCompositeKeyValue', (val) =>
168
+ // deep flatten, nested composite PKs produce nested arrays that would be comma-joined by the hash
169
+ Utils.flatten(Utils.getCompositeKeyValue(val, meta, 'convertToDatabaseValue', this.#platform), true));
168
170
  context.set('getPrimaryKeyHash', (val) => Utils.getPrimaryKeyHash(Utils.asArray(val)));
169
171
  if (meta.primaryKeys.length > 1) {
170
172
  lines.push(` const pks = entity.__helper.__pk ? getCompositeKeyValue(entity.__helper.__pk) : [`);
@@ -172,6 +174,10 @@ export class EntityComparator {
172
174
  if (meta.properties[pk].kind !== ReferenceKind.SCALAR) {
173
175
  lines.push(` (entity${this.wrap(pk)} != null && isEntityOrRef(entity${this.wrap(pk)})) ? entity${this.wrap(pk)}.__helper.getSerializedPrimaryKey() : entity${this.wrap(pk)},`);
174
176
  }
177
+ else if (meta.properties[pk].customType) {
178
+ const convertorKey = this.registerCustomType(meta.properties[pk], context);
179
+ lines.push(` convertToDatabaseValue_${convertorKey}(entity${this.wrap(pk)}),`);
180
+ }
175
181
  else {
176
182
  lines.push(` entity${this.wrap(pk)},`);
177
183
  }
@@ -443,6 +449,7 @@ export class EntityComparator {
443
449
  const ret = [];
444
450
  const padding = ' '.repeat(level * 2);
445
451
  const idx = this.#tmpIndex++;
452
+ ret.push(`${padding}if (entity${entityKey} === null) ret${dataKey} = null;`);
446
453
  ret.push(`${padding}if (Array.isArray(entity${entityKey})) {`);
447
454
  ret.push(`${padding} ret${dataKey} = [];`);
448
455
  ret.push(`${padding} entity${entityKey}.forEach((_, idx_${idx}) => {`);
@@ -40,6 +40,11 @@ export declare class QueryHelper {
40
40
  static mergePropertyFilters(propFilters: FilterOptions | undefined, options: FilterOptions | undefined): FilterOptions | undefined;
41
41
  static isFilterActive<T>(meta: EntityMetadata<T>, filterName: string, filter: FilterDef, options: Dictionary<boolean | Dictionary>): boolean;
42
42
  static processCustomType<T extends object>(prop: EntityProperty<T>, cond: FilterQuery<T>, platform: Platform, key?: string, fromQuery?: boolean): FilterQuery<T>;
43
+ /**
44
+ * Composite PK conditions are keyed by a hash of all the PK names, which `findProperty` cannot
45
+ * resolve, so the custom types have to be applied positionally instead.
46
+ */
47
+ private static processCompositeCustomTypes;
43
48
  private static isSupportedOperator;
44
49
  private static processJsonCondition;
45
50
  static findProperty<T>(fieldName: string, options: ProcessWhereOptions<T>): EntityProperty<T> | undefined;