@mikro-orm/core 7.2.0-dev.1 → 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 (43) hide show
  1. package/EntityManager.d.ts +1 -1
  2. package/EntityManager.js +31 -25
  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/hydration/ObjectHydrator.js +3 -0
  13. package/metadata/EntitySchema.js +5 -2
  14. package/metadata/MetadataDiscovery.js +34 -11
  15. package/metadata/MetadataProvider.js +1 -1
  16. package/metadata/MetadataStorage.d.ts +4 -3
  17. package/metadata/MetadataStorage.js +15 -1
  18. package/metadata/Routine.js +4 -1
  19. package/naming-strategy/AbstractNamingStrategy.js +2 -1
  20. package/naming-strategy/NamingStrategy.d.ts +2 -1
  21. package/package.json +1 -1
  22. package/platforms/Platform.d.ts +2 -0
  23. package/platforms/Platform.js +4 -0
  24. package/typings.d.ts +2 -0
  25. package/unit-of-work/ChangeSet.js +10 -7
  26. package/unit-of-work/ChangeSetPersister.js +15 -7
  27. package/utils/AbstractMigrator.d.ts +1 -1
  28. package/utils/AbstractMigrator.js +3 -2
  29. package/utils/Configuration.d.ts +6 -0
  30. package/utils/Configuration.js +3 -1
  31. package/utils/Cursor.js +1 -6
  32. package/utils/EntityComparator.js +4 -1
  33. package/utils/QueryHelper.d.ts +5 -0
  34. package/utils/QueryHelper.js +25 -0
  35. package/utils/RawQueryFragment.d.ts +6 -0
  36. package/utils/RawQueryFragment.js +15 -6
  37. package/utils/RequestContext.d.ts +2 -2
  38. package/utils/RequestContext.js +11 -2
  39. package/utils/TransactionManager.d.ts +6 -0
  40. package/utils/TransactionManager.js +35 -2
  41. package/utils/Utils.js +6 -3
  42. package/utils/clone.js +6 -0
  43. package/utils/env-vars.js +1 -0
@@ -601,7 +601,7 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
601
601
  * some additional lazy properties, if so, we reload and merge the data from database
602
602
  */
603
603
  protected shouldRefresh<T extends object, P extends string = never, F extends string = never, E extends string = never>(meta: EntityMetadata<T>, entity: T, options: FindOneOptions<T, P, F, E>): boolean;
604
- protected prepareOptions(options: (FindOptions<any, any, any, any> | FindOneOptions<any, any, any, any> | CountOptions<any, any> | CountByOptions<any>) & AbortQueryOptions): void;
604
+ protected prepareOptions<Options extends (FindOptions<any, any, any, any> | FindOneOptions<any, any, any, any> | CountOptions<any, any> | CountByOptions<any>) & AbortQueryOptions>(options: Options): Options;
605
605
  /**
606
606
  * @internal
607
607
  */
package/EntityManager.js CHANGED
@@ -116,7 +116,7 @@ export class EntityManager {
116
116
  return ret;
117
117
  }
118
118
  const em = this.getContext();
119
- em.prepareOptions(options);
119
+ options = em.prepareOptions(options);
120
120
  const meta = this.metadata.get(entityName);
121
121
  em.validateIndexUsage(meta, where, options);
122
122
  await em.tryFlush(entityName, options);
@@ -199,7 +199,7 @@ export class EntityManager {
199
199
  */
200
200
  async *stream(entityName, options = {}) {
201
201
  const em = this.getContext();
202
- em.prepareOptions(options);
202
+ options = em.prepareOptions(options);
203
203
  options.strategy = 'joined';
204
204
  await em.tryFlush(entityName, options);
205
205
  const where = (await em.processWhere(entityName, options.where ?? {}, options, 'read'));
@@ -258,6 +258,7 @@ export class EntityManager {
258
258
  * Registers global filter to this entity manager. Global filters are enabled by default (unless disabled via last parameter).
259
259
  */
260
260
  addFilter(options) {
261
+ options = { ...options };
261
262
  if (options.entity) {
262
263
  options.entity = Utils.asArray(options.entity).map(n => Utils.className(n));
263
264
  }
@@ -528,7 +529,7 @@ export class EntityManager {
528
529
  async findAndCount(entityName, where, options = {}) {
529
530
  const em = this.getContext(false);
530
531
  await em.tryFlush(entityName, options);
531
- options.flushMode = 'commit'; // do not try to auto flush again
532
+ options = { ...options, flushMode: 'commit' }; // do not try to auto flush again
532
533
  return Promise.all([
533
534
  em.find(entityName, where, options),
534
535
  em.count(entityName, where, options),
@@ -592,6 +593,7 @@ export class EntityManager {
592
593
  */
593
594
  async findByCursor(entityName, options) {
594
595
  const em = this.getContext(false);
596
+ options = { ...options };
595
597
  options.overfetch ??= true;
596
598
  options.where ??= {};
597
599
  if (Utils.isEmpty(options.orderBy) && !Raw.hasObjectFragments(options.orderBy)) {
@@ -610,10 +612,10 @@ export class EntityManager {
610
612
  async refreshOrFail(entity, options = {}) {
611
613
  const ret = await this.refresh(entity, options);
612
614
  if (!ret) {
613
- options.failHandler ??= this.config.get('findOneOrFailHandler');
615
+ const failHandler = options.failHandler ?? this.config.get('findOneOrFailHandler');
614
616
  const wrapped = helper(entity);
615
617
  const where = wrapped.getPrimaryKey();
616
- throw options.failHandler(wrapped.__meta.className, where);
618
+ throw failHandler(wrapped.__meta.className, where);
617
619
  }
618
620
  return ret;
619
621
  }
@@ -667,7 +669,7 @@ export class EntityManager {
667
669
  return ret;
668
670
  }
669
671
  const em = this.getContext();
670
- em.prepareOptions(options);
672
+ options = em.prepareOptions(options);
671
673
  let entity = em.#unitOfWork.tryGetById(entityName, where, options.schema);
672
674
  // query for a not managed entity which is already in the identity map as it
673
675
  // was provided with a PK this entity does not exist in the db, there can't
@@ -739,11 +741,11 @@ export class EntityManager {
739
741
  }
740
742
  if (!entity || isStrictViolation) {
741
743
  const key = options.strict ? 'findExactlyOneOrFailHandler' : 'findOneOrFailHandler';
742
- options.failHandler ??= this.config.get(key);
744
+ const failHandler = options.failHandler ?? this.config.get(key);
743
745
  const name = Utils.className(entityName);
744
746
  /* v8 ignore next */
745
747
  where = Utils.isEntity(where) ? helper(where).getPrimaryKey() : where;
746
- throw options.failHandler(name, where);
748
+ throw failHandler(name, where);
747
749
  }
748
750
  return entity;
749
751
  }
@@ -778,7 +780,7 @@ export class EntityManager {
778
780
  return ret;
779
781
  }
780
782
  const em = this.getContext(false);
781
- em.prepareOptions(options);
783
+ options = em.prepareOptions(options);
782
784
  let entityName;
783
785
  let where;
784
786
  let entity = null;
@@ -916,7 +918,7 @@ export class EntityManager {
916
918
  return ret;
917
919
  }
918
920
  const em = this.getContext(false);
919
- em.prepareOptions(options);
921
+ options = em.prepareOptions(options);
920
922
  let entityName;
921
923
  let propIndex;
922
924
  if (data === undefined) {
@@ -1202,7 +1204,7 @@ export class EntityManager {
1202
1204
  */
1203
1205
  async lock(entity, lockMode, options = {}) {
1204
1206
  options = Utils.isPlainObject(options) ? options : { lockVersion: options };
1205
- this.getContext(false).prepareOptions(options);
1207
+ options = this.getContext(false).prepareOptions(options);
1206
1208
  await this.getUnitOfWork().lock(entity, { lockMode, ...options });
1207
1209
  }
1208
1210
  /**
@@ -1210,7 +1212,7 @@ export class EntityManager {
1210
1212
  */
1211
1213
  async insert(entityNameOrEntity, data, options = {}) {
1212
1214
  const em = this.getContext(false);
1213
- em.prepareOptions(options);
1215
+ options = em.prepareOptions(options);
1214
1216
  let entityName;
1215
1217
  if (data === undefined) {
1216
1218
  entityName = entityNameOrEntity.constructor;
@@ -1277,7 +1279,7 @@ export class EntityManager {
1277
1279
  overrides = overridesOrOptions;
1278
1280
  }
1279
1281
  options ??= {};
1280
- em.prepareOptions(options);
1282
+ options = em.prepareOptions(options);
1281
1283
  const meta = em.metadata.get(entityName);
1282
1284
  const res = await em.driver.nativeClone(entityName, where, overrides, {
1283
1285
  ctx: em.#transactionContext,
@@ -1293,7 +1295,7 @@ export class EntityManager {
1293
1295
  */
1294
1296
  async insertMany(entityNameOrEntities, data, options = {}) {
1295
1297
  const em = this.getContext(false);
1296
- em.prepareOptions(options);
1298
+ options = em.prepareOptions(options);
1297
1299
  let entityName;
1298
1300
  if (data === undefined) {
1299
1301
  entityName = entityNameOrEntities[0].constructor;
@@ -1336,7 +1338,7 @@ export class EntityManager {
1336
1338
  */
1337
1339
  async nativeUpdate(entityName, where, data, options = {}) {
1338
1340
  const em = this.getContext(false);
1339
- em.prepareOptions(options);
1341
+ options = em.prepareOptions(options);
1340
1342
  await em.processUnionWhere(entityName, options, 'update');
1341
1343
  data = QueryHelper.processObjectParams(data);
1342
1344
  where = await em.processWhere(entityName, where, { ...options, convertCustomTypes: false }, 'update');
@@ -1385,7 +1387,7 @@ export class EntityManager {
1385
1387
  */
1386
1388
  async nativeDelete(entityName, where, options = {}) {
1387
1389
  const em = this.getContext(false);
1388
- em.prepareOptions(options);
1390
+ options = em.prepareOptions(options);
1389
1391
  await em.processUnionWhere(entityName, options, 'delete');
1390
1392
  where = (await em.processWhere(entityName, where, options, 'delete'));
1391
1393
  validateParams(where, 'delete condition');
@@ -1428,6 +1430,7 @@ export class EntityManager {
1428
1430
  return this.merge(entityName.constructor, entityName, data);
1429
1431
  }
1430
1432
  const em = options.disableContextResolution ? this : this.getContext();
1433
+ options = { ...options };
1431
1434
  options.schema ??= em.#schema;
1432
1435
  options.validate ??= true;
1433
1436
  options.cascade ??= true;
@@ -1461,6 +1464,7 @@ export class EntityManager {
1461
1464
  */
1462
1465
  create(entityName, data, options = {}) {
1463
1466
  const em = this.getContext();
1467
+ options = { ...options };
1464
1468
  options.schema ??= em.#schema;
1465
1469
  const entity = em.#entityFactory.create(entityName, data, {
1466
1470
  ...options,
@@ -1484,6 +1488,7 @@ export class EntityManager {
1484
1488
  * Gets a reference to the entity identified by the given type and identifier without actually loading it, if the entity is not yet loaded
1485
1489
  */
1486
1490
  getReference(entityName, id, options = {}) {
1491
+ options = { ...options };
1487
1492
  options.schema ??= this.schema;
1488
1493
  options.convertCustomTypes ??= false;
1489
1494
  const meta = this.metadata.get(entityName);
@@ -1504,13 +1509,10 @@ export class EntityManager {
1504
1509
  */
1505
1510
  async count(entityName, where = {}, options = {}) {
1506
1511
  const em = this.getContext(false);
1507
- // Shallow copy options since the object will be modified when deleting orderBy
1508
- options = { ...options };
1509
- em.prepareOptions(options);
1512
+ options = em.prepareOptions(options);
1510
1513
  await em.tryFlush(entityName, options);
1511
1514
  where = await em.processWhere(entityName, where, options, 'read');
1512
1515
  options.populate = (await em.preparePopulate(entityName, options));
1513
- options = { ...options };
1514
1516
  // save the original hint value so we know it was infer/all
1515
1517
  const meta = em.metadata.find(entityName);
1516
1518
  options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
@@ -1654,7 +1656,7 @@ export class EntityManager {
1654
1656
  return entities;
1655
1657
  }
1656
1658
  const em = this.getContext();
1657
- em.prepareOptions(options);
1659
+ options = em.prepareOptions(options);
1658
1660
  const entityName = arr[0].constructor;
1659
1661
  const preparedPopulate = await em.preparePopulate(entityName, { populate: populate, filters: options.filters, populateHints: options.populateHints }, options.validate);
1660
1662
  await em.#entityLoader.populate(entityName, arr, preparedPopulate, options);
@@ -1665,6 +1667,7 @@ export class EntityManager {
1665
1667
  */
1666
1668
  fork(options = {}) {
1667
1669
  const em = options.disableContextResolution ? this : this.getContext(false);
1670
+ options = { ...options };
1668
1671
  options.clear ??= true;
1669
1672
  options.useContext ??= false;
1670
1673
  options.freshEventManager ??= false;
@@ -2073,10 +2076,13 @@ export class EntityManager {
2073
2076
  if (!Utils.isEmpty(options.fields) && !Utils.isEmpty(options.exclude)) {
2074
2077
  throw new ValidationError(`Cannot combine 'fields' and 'exclude' option.`);
2075
2078
  }
2076
- options.schema ??= this.#schema;
2077
- options.signal ??= this.signal;
2078
- options.inflightQueryAbortStrategy ??= this.inflightQueryAbortStrategy;
2079
- options.logging = options.loggerContext = Utils.merge({ id: this.id }, this.loggerContext, options.loggerContext, options.logging);
2079
+ // the options object belongs to the caller, everything below works on our own copy
2080
+ const opts = { ...options };
2081
+ opts.schema ??= this.#schema;
2082
+ opts.signal ??= this.signal;
2083
+ opts.inflightQueryAbortStrategy ??= this.inflightQueryAbortStrategy;
2084
+ opts.logging = opts.loggerContext = Utils.merge({ id: this.id }, this.loggerContext, opts.loggerContext, opts.logging);
2085
+ return opts;
2080
2086
  }
2081
2087
  /**
2082
2088
  * @internal
package/MikroORM.d.ts CHANGED
@@ -82,6 +82,10 @@ export declare class MikroORM<Driver extends IDatabaseDriver = IDatabaseDriver,
82
82
  * Closes the database connection.
83
83
  */
84
84
  close(force?: boolean): Promise<void>;
85
+ /**
86
+ * Closes the database connection, allows using the ORM instance with `await using`.
87
+ */
88
+ [Symbol.asyncDispose](): Promise<void>;
85
89
  /**
86
90
  * Gets the `MetadataStorage`.
87
91
  */
package/MikroORM.js CHANGED
@@ -159,6 +159,12 @@ export class MikroORM {
159
159
  await this.config.getMetadataCacheAdapter()?.close?.();
160
160
  await this.config.getResultCacheAdapter()?.close?.();
161
161
  }
162
+ /**
163
+ * Closes the database connection, allows using the ORM instance with `await using`.
164
+ */
165
+ async [Symbol.asyncDispose]() {
166
+ await this.close();
167
+ }
162
168
  /**
163
169
  * Gets the `MetadataStorage` (without parameters) or `EntityMetadata` instance when provided with the `entityName` parameter.
164
170
  */
@@ -1,9 +1,10 @@
1
1
  /** Interface for async-capable cache storage used by result cache and metadata cache. */
2
2
  export interface CacheAdapter {
3
3
  /**
4
- * Gets the items under `name` key from the cache.
4
+ * Gets the items under `name` key from the cache. When `origin` is provided, adapters that
5
+ * track the entry origin should ignore entries cached from a different source file.
5
6
  */
6
- get<T = any>(name: string): T | Promise<T | undefined> | undefined;
7
+ get<T = any>(name: string, origin?: string): T | Promise<T | undefined> | undefined;
7
8
  /**
8
9
  * Sets the item to the cache. `origin` is used for cache invalidation and should reflect the change in data.
9
10
  */
@@ -24,9 +25,10 @@ export interface CacheAdapter {
24
25
  /** Synchronous variant of CacheAdapter, used for metadata cache where async access is not needed. */
25
26
  export interface SyncCacheAdapter extends CacheAdapter {
26
27
  /**
27
- * Gets the items under `name` key from the cache.
28
+ * Gets the items under `name` key from the cache. When `origin` is provided, adapters that
29
+ * track the entry origin should ignore entries cached from a different source file.
28
30
  */
29
- get<T = any>(name: string): T | undefined;
31
+ get<T = any>(name: string, origin?: string): T | undefined;
30
32
  /**
31
33
  * Sets the item to the cache. `origin` is used for cache invalidation and should reflect the change in data.
32
34
  */
@@ -8,7 +8,7 @@ export declare class FileCacheAdapter implements SyncCacheAdapter {
8
8
  /**
9
9
  * @inheritDoc
10
10
  */
11
- get(name: string): any;
11
+ get(name: string, origin?: string): any;
12
12
  /**
13
13
  * @inheritDoc
14
14
  */
@@ -16,12 +16,17 @@ export class FileCacheAdapter {
16
16
  /**
17
17
  * @inheritDoc
18
18
  */
19
- get(name) {
19
+ get(name, origin) {
20
20
  const path = this.path(name);
21
21
  if (!existsSync(path)) {
22
22
  return null;
23
23
  }
24
24
  const payload = fs.readJSONSync(path);
25
+ // Two classes with the same name share the cache key, ignore entries cached
26
+ // from a different source file.
27
+ if (origin && fs.absolutePath(payload.origin, this.#baseDir) !== fs.absolutePath(origin, this.#baseDir)) {
28
+ return null;
29
+ }
25
30
  const hash = this.getHash(payload.origin);
26
31
  if (!hash || payload.hash !== hash) {
27
32
  return null;
@@ -71,7 +76,7 @@ export class FileCacheAdapter {
71
76
  let path = typeof this.#options.combined === 'string' ? this.#options.combined : './metadata.json';
72
77
  path = fs.normalizePath(this.#options.cacheDir, path);
73
78
  this.#options.combined = path; // override in the options, so we can log it from the CLI in `cache:generate` command
74
- writeFileSync(path, JSON.stringify(this.#cache, null, this.#pretty ? 2 : undefined));
79
+ writeFileSync(path, JSON.stringify(this.#cache, null, this.#pretty ? 2 : undefined), { flush: true });
75
80
  return path;
76
81
  }
77
82
  path(name) {
@@ -51,6 +51,13 @@ export declare abstract class Connection {
51
51
  * This method doesn't support transactions, as opposed to `orm.schema.execute()`, which is used internally.
52
52
  */
53
53
  executeDump(dump: string): Promise<void>;
54
+ /**
55
+ * Returns the underlying database client the connection drives — e.g. the `pg` pool, the
56
+ * `better-sqlite3` database, or the `PGlite` instance — for vendor APIs MikroORM does not wrap.
57
+ * Each driver narrows the return type to its own client. Its lifecycle belongs to the ORM, so
58
+ * leave closing it to `orm.close()` unless you supplied the client yourself via `driverOptions`.
59
+ */
60
+ getNativeClient(): Promise<unknown>;
54
61
  protected onConnect(): Promise<void>;
55
62
  /** Executes a callback inside a transaction, committing on success and rolling back on failure. */
56
63
  transactional<T>(cb: (trx: Transaction) => Promise<T>, options?: {
@@ -65,6 +65,15 @@ export class Connection {
65
65
  async executeDump(dump) {
66
66
  throw new Error(`Executing SQL dumps is not supported by current driver`);
67
67
  }
68
+ /**
69
+ * Returns the underlying database client the connection drives — e.g. the `pg` pool, the
70
+ * `better-sqlite3` database, or the `PGlite` instance — for vendor APIs MikroORM does not wrap.
71
+ * Each driver narrows the return type to its own client. Its lifecycle belongs to the ORM, so
72
+ * leave closing it to `orm.close()` unless you supplied the client yourself via `driverOptions`.
73
+ */
74
+ async getNativeClient() {
75
+ throw new Error(`Accessing the native client is not supported by current driver`);
76
+ }
68
77
  async onConnect() {
69
78
  const schemaGenerator = this.config.getExtension('@mikro-orm/schema-generator');
70
79
  if (this.type === 'write' && schemaGenerator) {
@@ -64,6 +64,13 @@ export declare abstract class DatabaseDriver<C extends Connection> implements ID
64
64
  orderBy: OrderDefinition<T>[];
65
65
  where: FilterQuery<T>;
66
66
  };
67
+ /**
68
+ * Restores the JS value of a single cursor offset: ISO strings become `Date` instances based on the
69
+ * property type (never based on the string shape alone), and custom types are restored via
70
+ * `convertToJSValue`. Values compared against a JSON document keep their serialized form instead,
71
+ * unless the platform preserves native date types inside JSON documents (mongo).
72
+ */
73
+ private mapCursorOffset;
67
74
  protected createCursorCondition<T extends object>(definition: (readonly [keyof T & string, QueryOrder])[], offsets: Dictionary[], inverse: boolean, meta: EntityMetadata<T>): FilterQuery<T>;
68
75
  /** @internal */
69
76
  mapDataToFieldNames(data: Dictionary, stringifyJsonArrays: boolean, properties?: Record<string, EntityProperty>, convertCustomTypes?: boolean, object?: boolean): Dictionary;
@@ -7,8 +7,11 @@ import { EntityManager } from '../EntityManager.js';
7
7
  import { CursorError, ValidationError } from '../errors.js';
8
8
  import { DriverException } from '../exceptions.js';
9
9
  import { helper } from '../entity/wrap.js';
10
+ import { Reference } from '../entity/Reference.js';
10
11
  import { PolymorphicRef } from '../entity/PolymorphicRef.js';
11
12
  import { JsonType } from '../types/JsonType.js';
13
+ import { DateTimeType } from '../types/DateTimeType.js';
14
+ import { QueryHelper } from '../utils/QueryHelper.js';
12
15
  import { MikroORM } from '../MikroORM.js';
13
16
  /** Abstract base class for all database drivers, implementing common driver logic. */
14
17
  export class DatabaseDriver {
@@ -141,13 +144,24 @@ export class DatabaseDriver {
141
144
  return !!val && typeof val === 'object' && key in val;
142
145
  };
143
146
  const createCursor = (val, key, inverse = false) => {
144
- let def = isCursor(val, key) ? val[key] : val;
145
- if (Utils.isPlainObject(def)) {
146
- def = Cursor.for(meta, def, orderBy);
147
+ const def = Reference.unwrapReference((isCursor(val, key) ? val[key] : val));
148
+ let offsets;
149
+ // entity (and reference) instances are supported as cursors too, their properties are read the same way
150
+ if (Utils.isPlainObject(def) || Utils.isEntity(def)) {
151
+ // POJO values are already JS values, extract them ordered per the definition,
152
+ // without the JSON round trip `Cursor.for` + `Cursor.decode` would impose
153
+ offsets = definition.map(([key]) => {
154
+ if (def[key] === undefined) {
155
+ throw CursorError.missingValue(meta.className, key);
156
+ }
157
+ return def[key];
158
+ });
147
159
  }
148
- /* v8 ignore next */
149
- const offsets = def ? Cursor.decode(def) : [];
150
- if (definition.length === offsets.length) {
160
+ else {
161
+ /* v8 ignore next */
162
+ offsets = def ? Cursor.decode(def) : [];
163
+ }
164
+ if (definition.length > 0 && definition.length === offsets.length) {
151
165
  return this.createCursorCondition(definition, offsets, inverse, meta);
152
166
  }
153
167
  /* v8 ignore next */
@@ -174,19 +188,67 @@ export class DatabaseDriver {
174
188
  const dir = Utils.xor(desc, isLast) ? 'desc' : 'asc';
175
189
  return { [prop]: dir };
176
190
  };
191
+ // the cursor condition is created at the driver level, after the EM already converted custom types
192
+ // in the user `where`, so we need to run the same conversion over it explicitly
193
+ const where = QueryHelper.processWhere({
194
+ where: ($and.length > 1 ? { $and } : { ...$and[0] }),
195
+ entityName: meta.class,
196
+ metadata: this.metadata,
197
+ platform: this.platform,
198
+ convertCustomTypes: options.convertCustomTypes,
199
+ });
177
200
  return {
178
201
  orderBy: definition.map(([prop, direction]) => createOrderBy(prop, direction)),
179
- where: ($and.length > 1 ? { $and } : { ...$and[0] }),
202
+ where,
180
203
  };
181
204
  }
205
+ /**
206
+ * Restores the JS value of a single cursor offset: ISO strings become `Date` instances based on the
207
+ * property type (never based on the string shape alone), and custom types are restored via
208
+ * `convertToJSValue`. Values compared against a JSON document keep their serialized form instead,
209
+ * unless the platform preserves native date types inside JSON documents (mongo).
210
+ */
211
+ mapCursorOffset(prop, value, insideJson) {
212
+ if (Utils.isScalarReference(value)) {
213
+ value = value.unwrap();
214
+ }
215
+ // scalar direction on a relation orders by its primary key
216
+ if (Utils.isEntity(value, true)) {
217
+ value = helper(value).getPrimaryKey();
218
+ }
219
+ if (value == null) {
220
+ return value;
221
+ }
222
+ if (insideJson && !this.platform.preservesDatesInsideJson()) {
223
+ // compared against the JSON document, which holds the serialized form
224
+ if (value instanceof Date) {
225
+ return value.toISOString();
226
+ }
227
+ // restore the JS value from the serialized form, `processWhere` then converts it to
228
+ // the database form, which is what the JSON document holds for custom typed props
229
+ return prop?.customType ? prop.customType.convertToJSValue(value, this.platform) : value;
230
+ }
231
+ if (typeof value === 'string' &&
232
+ (prop?.runtimeType === 'Date' ||
233
+ (prop?.customType && this.platform.getMappedType(prop.columnTypes?.[0] ?? '') instanceof DateTimeType))) {
234
+ value = new Date(value);
235
+ }
236
+ return prop?.customType ? prop.customType.convertToJSValue(value, this.platform) : value;
237
+ }
182
238
  createCursorCondition(definition, offsets, inverse, meta) {
183
- const createCondition = (prop, direction, offset, eq = false, path = prop) => {
239
+ const createCondition = (prop, direction, offset, eq = false, path = prop, properties = meta.properties, insideJson = false) => {
240
+ const propMeta = properties[prop];
184
241
  if (Utils.isPlainObject(direction)) {
185
242
  if (offset === undefined) {
186
243
  throw CursorError.missingValue(meta.className, path);
187
244
  }
245
+ // POJO cursors can carry entity, reference or embeddable class instances, read their properties directly
246
+ offset = Reference.unwrapReference(offset);
247
+ const childProps = propMeta?.kind === ReferenceKind.EMBEDDED ? propMeta.embeddedProps : propMeta?.targetMeta?.properties;
248
+ insideJson ||=
249
+ (propMeta?.kind === ReferenceKind.EMBEDDED && !!propMeta.object) || propMeta?.customType instanceof JsonType;
188
250
  const value = Utils.keys(direction).reduce((o, key) => {
189
- Object.assign(o, createCondition(key, direction[key], offset?.[key], eq, `${path}.${key}`));
251
+ Object.assign(o, createCondition(key, direction[key], offset?.[key], eq, `${path}.${key}`, childProps ?? {}, insideJson));
190
252
  return o;
191
253
  }, {});
192
254
  return { [prop]: value };
@@ -209,6 +271,7 @@ export class DatabaseDriver {
209
271
  if (offset === undefined) {
210
272
  throw CursorError.missingValue(meta.className, path);
211
273
  }
274
+ offset = this.mapCursorOffset(propMeta, offset, insideJson);
212
275
  // Handle null offset (intentional null cursor value)
213
276
  if (offset === null) {
214
277
  if (eq) {
@@ -364,6 +364,9 @@ export class ObjectHydrator extends Hydrator {
364
364
  ret.push(` data${dataKey}.forEach((_, idx_${idx}) => {`);
365
365
  ret.push(...hydrateEmbedded(prop, [...path, `[idx_${idx}]`], `${dataKey}[idx_${idx}]`).map(l => ' ' + l));
366
366
  ret.push(` });`);
367
+ ret.push(` } else if (data${dataKey} === null) {`);
368
+ /* v8 ignore next */
369
+ ret.push(` entity${entityKey} = ${this.config.get('forceUndefined') ? 'undefined' : 'null'};`);
367
370
  ret.push(` }`);
368
371
  return ret;
369
372
  };
@@ -4,6 +4,8 @@ import { Cascade, ReferenceKind } from '../enums.js';
4
4
  import { Type } from '../types/Type.js';
5
5
  import { Utils } from '../utils/Utils.js';
6
6
  import { EnumArrayType } from '../types/EnumArrayType.js';
7
+ // mirrors `MetadataStorage.META_SYMBOL`, we can't import it here due to a module cycle
8
+ const META_SYMBOL = Symbol.for('@mikro-orm/core/MetadataStorage.META_SYMBOL');
7
9
  /** Class-less entity definition that provides a programmatic API for defining entities without decorators. */
8
10
  export class EntitySchema {
9
11
  /**
@@ -220,8 +222,9 @@ export class EntitySchema {
220
222
  // Only set extends if the parent is NOT the auto-generated class for this same entity.
221
223
  // When the user extends the auto-generated class (from defineEntity without a class option)
222
224
  // and registers their custom class via setClass, we don't want to discover the
223
- // auto-generated class as a separate parent entity.
224
- if (base !== BaseEntity && base.name !== this._meta.className) {
225
+ // auto-generated class as a separate parent entity. A parent carrying its own decorator
226
+ // metadata is a real base class even when a minifier mangles it to the same name as the child.
227
+ if (base !== BaseEntity && (base.name !== this._meta.className || Object.hasOwn(base, META_SYMBOL))) {
225
228
  this._meta.extends ??= base.name ? base : undefined;
226
229
  }
227
230
  }
@@ -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
  }