@mikro-orm/core 7.0.0-dev.99 → 7.0.0-rc.1

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 (107) hide show
  1. package/EntityManager.d.ts +34 -17
  2. package/EntityManager.js +95 -103
  3. package/MikroORM.d.ts +5 -5
  4. package/MikroORM.js +25 -20
  5. package/cache/FileCacheAdapter.js +11 -3
  6. package/connections/Connection.d.ts +3 -2
  7. package/connections/Connection.js +4 -3
  8. package/drivers/DatabaseDriver.d.ts +11 -11
  9. package/drivers/DatabaseDriver.js +91 -25
  10. package/drivers/IDatabaseDriver.d.ts +50 -20
  11. package/entity/BaseEntity.d.ts +61 -1
  12. package/entity/Collection.d.ts +8 -1
  13. package/entity/Collection.js +12 -13
  14. package/entity/EntityAssigner.js +9 -9
  15. package/entity/EntityFactory.d.ts +6 -1
  16. package/entity/EntityFactory.js +40 -22
  17. package/entity/EntityHelper.d.ts +2 -2
  18. package/entity/EntityHelper.js +27 -4
  19. package/entity/EntityLoader.d.ts +5 -4
  20. package/entity/EntityLoader.js +193 -80
  21. package/entity/EntityRepository.d.ts +27 -7
  22. package/entity/EntityRepository.js +8 -2
  23. package/entity/PolymorphicRef.d.ts +12 -0
  24. package/entity/PolymorphicRef.js +18 -0
  25. package/entity/WrappedEntity.d.ts +2 -2
  26. package/entity/WrappedEntity.js +1 -1
  27. package/entity/defineEntity.d.ts +89 -50
  28. package/entity/defineEntity.js +12 -0
  29. package/entity/index.d.ts +1 -0
  30. package/entity/index.js +1 -0
  31. package/entity/utils.d.ts +6 -1
  32. package/entity/utils.js +33 -0
  33. package/entity/validators.js +2 -2
  34. package/enums.d.ts +2 -2
  35. package/enums.js +1 -0
  36. package/errors.d.ts +16 -8
  37. package/errors.js +40 -13
  38. package/hydration/ObjectHydrator.js +63 -21
  39. package/index.d.ts +1 -1
  40. package/logging/colors.d.ts +1 -1
  41. package/logging/colors.js +7 -6
  42. package/logging/inspect.js +1 -6
  43. package/metadata/EntitySchema.d.ts +43 -13
  44. package/metadata/EntitySchema.js +82 -27
  45. package/metadata/MetadataDiscovery.d.ts +60 -3
  46. package/metadata/MetadataDiscovery.js +665 -154
  47. package/metadata/MetadataProvider.js +3 -1
  48. package/metadata/MetadataStorage.d.ts +13 -6
  49. package/metadata/MetadataStorage.js +64 -19
  50. package/metadata/MetadataValidator.d.ts +32 -2
  51. package/metadata/MetadataValidator.js +196 -31
  52. package/metadata/discover-entities.js +5 -5
  53. package/metadata/types.d.ts +111 -14
  54. package/naming-strategy/AbstractNamingStrategy.d.ts +11 -3
  55. package/naming-strategy/AbstractNamingStrategy.js +12 -0
  56. package/naming-strategy/EntityCaseNamingStrategy.d.ts +3 -3
  57. package/naming-strategy/EntityCaseNamingStrategy.js +6 -5
  58. package/naming-strategy/MongoNamingStrategy.d.ts +3 -3
  59. package/naming-strategy/MongoNamingStrategy.js +6 -6
  60. package/naming-strategy/NamingStrategy.d.ts +17 -3
  61. package/naming-strategy/UnderscoreNamingStrategy.d.ts +3 -3
  62. package/naming-strategy/UnderscoreNamingStrategy.js +6 -6
  63. package/package.json +2 -2
  64. package/platforms/Platform.d.ts +4 -2
  65. package/platforms/Platform.js +5 -2
  66. package/serialization/EntitySerializer.d.ts +3 -0
  67. package/serialization/EntitySerializer.js +15 -13
  68. package/serialization/EntityTransformer.js +6 -6
  69. package/serialization/SerializationContext.d.ts +6 -6
  70. package/typings.d.ts +325 -110
  71. package/typings.js +84 -17
  72. package/unit-of-work/ChangeSet.d.ts +4 -3
  73. package/unit-of-work/ChangeSet.js +2 -3
  74. package/unit-of-work/ChangeSetComputer.d.ts +3 -6
  75. package/unit-of-work/ChangeSetComputer.js +34 -13
  76. package/unit-of-work/ChangeSetPersister.d.ts +12 -10
  77. package/unit-of-work/ChangeSetPersister.js +55 -25
  78. package/unit-of-work/CommitOrderCalculator.d.ts +12 -10
  79. package/unit-of-work/CommitOrderCalculator.js +13 -13
  80. package/unit-of-work/IdentityMap.d.ts +12 -0
  81. package/unit-of-work/IdentityMap.js +39 -1
  82. package/unit-of-work/UnitOfWork.d.ts +21 -3
  83. package/unit-of-work/UnitOfWork.js +203 -56
  84. package/utils/AbstractSchemaGenerator.js +17 -8
  85. package/utils/AsyncContext.d.ts +6 -0
  86. package/utils/AsyncContext.js +42 -0
  87. package/utils/Configuration.d.ts +52 -11
  88. package/utils/Configuration.js +12 -8
  89. package/utils/Cursor.js +21 -8
  90. package/utils/DataloaderUtils.js +13 -11
  91. package/utils/EntityComparator.d.ts +14 -7
  92. package/utils/EntityComparator.js +132 -46
  93. package/utils/QueryHelper.d.ts +16 -6
  94. package/utils/QueryHelper.js +53 -18
  95. package/utils/RawQueryFragment.d.ts +28 -23
  96. package/utils/RawQueryFragment.js +34 -56
  97. package/utils/RequestContext.js +2 -2
  98. package/utils/TransactionContext.js +2 -2
  99. package/utils/TransactionManager.js +1 -1
  100. package/utils/Utils.d.ts +7 -26
  101. package/utils/Utils.js +25 -79
  102. package/utils/clone.js +7 -21
  103. package/utils/env-vars.d.ts +4 -0
  104. package/utils/env-vars.js +13 -3
  105. package/utils/fs-utils.d.ts +21 -0
  106. package/utils/fs-utils.js +106 -11
  107. package/utils/upsert-utils.d.ts +4 -4
package/MikroORM.js CHANGED
@@ -4,31 +4,35 @@ import { Configuration } from './utils/Configuration.js';
4
4
  import { loadEnvironmentVars } from './utils/env-vars.js';
5
5
  import { Utils } from './utils/Utils.js';
6
6
  import { colors } from './logging/colors.js';
7
- async function registerExtension(name, mod, extensions) {
8
- /* v8 ignore next */
9
- const resolved = await mod.catch(() => null);
10
- const module = resolved?.[name];
11
- /* v8 ignore else */
12
- if (module) {
13
- extensions.push(module);
7
+ async function tryRegisterExtension(name, pkg, extensions) {
8
+ try {
9
+ const url = import.meta.resolve(pkg);
10
+ const mod = await import(url);
11
+ if (mod[name]) {
12
+ extensions.push(mod[name]);
13
+ }
14
+ }
15
+ catch {
16
+ // not installed
14
17
  }
15
18
  }
16
19
  /** @internal */
17
- export async function lookupExtensions(options) {
20
+ export async function loadOptionalDependencies(options) {
21
+ await import('@mikro-orm/core/fs-utils').then(m => m.fs.init()).catch(() => null);
18
22
  const extensions = options.extensions ?? [];
19
23
  const exists = (name) => extensions.some(ext => ext.name === name);
20
24
  if (!exists('SeedManager')) {
21
- await registerExtension('SeedManager', import((() => '@mikro-orm/seeder')()), extensions);
25
+ await tryRegisterExtension('SeedManager', '@mikro-orm/seeder', extensions);
22
26
  }
23
27
  if (!exists('Migrator')) {
24
- await registerExtension('Migrator', import((() => '@mikro-orm/migrations')()), extensions);
28
+ await tryRegisterExtension('Migrator', '@mikro-orm/migrations', extensions);
25
29
  }
26
30
  /* v8 ignore if */
27
31
  if (!exists('Migrator')) {
28
- await registerExtension('Migrator', import((() => '@mikro-orm/migrations-mongodb')()), extensions);
32
+ await tryRegisterExtension('Migrator', '@mikro-orm/migrations-mongodb', extensions);
29
33
  }
30
34
  if (!exists('EntityGenerator')) {
31
- await registerExtension('EntityGenerator', import((() => '@mikro-orm/entity-generator')()), extensions);
35
+ await tryRegisterExtension('EntityGenerator', '@mikro-orm/entity-generator', extensions);
32
36
  }
33
37
  options.extensions = extensions;
34
38
  const metadataCacheEnabled = options.metadataCache?.enabled || options.metadataProvider?.useCache?.();
@@ -85,7 +89,7 @@ export class MikroORM {
85
89
  options = { ...options };
86
90
  options.discovery ??= {};
87
91
  options.discovery.skipSyncDiscovery ??= true;
88
- await lookupExtensions(options);
92
+ await loadOptionalDependencies(options);
89
93
  const orm = new this(options);
90
94
  const preferTs = orm.config.get('preferTs', Utils.detectTypeScriptSupport());
91
95
  orm.metadata = await orm.discovery.discover(preferTs);
@@ -94,13 +98,15 @@ export class MikroORM {
94
98
  }
95
99
  /**
96
100
  * Synchronous variant of the `init` method with some limitations:
97
- * - database connection will be established when you first interact with the database (or you can use `orm.connect()` explicitly)
98
- * - no loading of the `config` file, `options` parameter is mandatory
99
- * - no support for folder based discovery
101
+ * - folder-based discovery not supported
102
+ * - ORM extensions are not autoloaded
103
+ * - when metadata cache is enabled, `FileCacheAdapter` needs to be explicitly set in the config
100
104
  */
101
105
  constructor(options) {
102
106
  const env = loadEnvironmentVars();
103
- options = Utils.merge(options, env);
107
+ options = options.preferEnvVars
108
+ ? Utils.merge(options, env)
109
+ : Utils.merge(env, options);
104
110
  this.config = new Configuration(options);
105
111
  const discovery = this.config.get('discovery');
106
112
  this.driver = this.config.getDriver();
@@ -158,7 +164,6 @@ export class MikroORM {
158
164
  */
159
165
  getMetadata(entityName) {
160
166
  if (entityName) {
161
- entityName = Utils.className(entityName);
162
167
  return this.metadata.get(entityName);
163
168
  }
164
169
  return this.metadata;
@@ -181,8 +186,8 @@ export class MikroORM {
181
186
  const tmp = this.discovery.discoverReferences(Utils.asArray(entities));
182
187
  const metadata = this.discovery.processDiscoveredEntities(tmp);
183
188
  for (const meta of metadata) {
184
- this.metadata.set(meta.className, meta);
185
- meta.root = this.metadata.get(meta.root.className);
189
+ this.metadata.set(meta.class, meta);
190
+ meta.root = this.metadata.get(meta.root.class);
186
191
  }
187
192
  this.metadata.decorate(this.em);
188
193
  }
@@ -53,7 +53,15 @@ export class FileCacheAdapter {
53
53
  clear() {
54
54
  const path = this.path('*');
55
55
  const files = fs.glob(path);
56
- files.forEach(file => unlinkSync(file));
56
+ for (const file of files) {
57
+ /* v8 ignore next */
58
+ try {
59
+ unlinkSync(file);
60
+ }
61
+ catch {
62
+ // ignore if file is already gone
63
+ }
64
+ }
57
65
  this.cache = {};
58
66
  }
59
67
  combine() {
@@ -63,7 +71,7 @@ export class FileCacheAdapter {
63
71
  let path = typeof this.options.combined === 'string'
64
72
  ? this.options.combined
65
73
  : './metadata.json';
66
- path = Utils.normalizePath(this.options.cacheDir, path);
74
+ path = fs.normalizePath(this.options.cacheDir, path);
67
75
  this.options.combined = path; // override in the options, so we can log it from the CLI in `cache:generate` command
68
76
  writeFileSync(path, JSON.stringify(this.cache, null, this.pretty ? 2 : undefined));
69
77
  return path;
@@ -73,7 +81,7 @@ export class FileCacheAdapter {
73
81
  return `${this.options.cacheDir}/${name}.json`;
74
82
  }
75
83
  getHash(origin) {
76
- origin = Utils.absolutePath(origin, this.baseDir);
84
+ origin = fs.absolutePath(origin, this.baseDir);
77
85
  if (!existsSync(origin)) {
78
86
  return null;
79
87
  }
@@ -43,9 +43,10 @@ export declare abstract class Connection {
43
43
  */
44
44
  ensureConnection(): Promise<void>;
45
45
  /**
46
- * Load schema dump from file and execute it. Not supported by MongoDB driver.
46
+ * Execute raw SQL queries, handy from running schema dump loaded from a file.
47
+ * This method doesn't support transactions, as opposed to `orm.schema.execute()`, which is used internally.
47
48
  */
48
- loadFile(path: string): Promise<void>;
49
+ executeDump(dump: string): Promise<void>;
49
50
  protected onConnect(): Promise<void>;
50
51
  transactional<T>(cb: (trx: Transaction) => Promise<T>, options?: {
51
52
  isolationLevel?: IsolationLevel | `${IsolationLevel}`;
@@ -40,10 +40,11 @@ export class Connection {
40
40
  }
41
41
  }
42
42
  /**
43
- * Load schema dump from file and execute it. Not supported by MongoDB driver.
43
+ * Execute raw SQL queries, handy from running schema dump loaded from a file.
44
+ * This method doesn't support transactions, as opposed to `orm.schema.execute()`, which is used internally.
44
45
  */
45
- async loadFile(path) {
46
- throw new Error(`Loading SQL files is not supported by current driver`);
46
+ async executeDump(dump) {
47
+ throw new Error(`Executing SQL dumps is not supported by current driver`);
47
48
  }
48
49
  async onConnect() {
49
50
  const schemaGenerator = this.config.getExtension('@mikro-orm/schema-generator');
@@ -22,18 +22,18 @@ export declare abstract class DatabaseDriver<C extends Connection> implements ID
22
22
  protected comparator: EntityComparator;
23
23
  protected metadata: MetadataStorage;
24
24
  protected constructor(config: Configuration, dependencies: string[]);
25
- abstract find<T extends object, P extends string = never, F extends string = '*', E extends string = never>(entityName: string, where: FilterQuery<T>, options?: FindOptions<T, P, F, E>): Promise<EntityData<T>[]>;
26
- abstract findOne<T extends object, P extends string = never, F extends string = '*', E extends string = never>(entityName: string, where: FilterQuery<T>, options?: FindOneOptions<T, P, F, E>): Promise<EntityData<T> | null>;
27
- abstract nativeInsert<T extends object>(entityName: string, data: EntityDictionary<T>, options?: NativeInsertUpdateOptions<T>): Promise<QueryResult<T>>;
28
- abstract nativeInsertMany<T extends object>(entityName: string, data: EntityDictionary<T>[], options?: NativeInsertUpdateManyOptions<T>, transform?: (sql: string) => string): Promise<QueryResult<T>>;
29
- abstract nativeUpdate<T extends object>(entityName: string, where: FilterQuery<T>, data: EntityDictionary<T>, options?: NativeInsertUpdateOptions<T>): Promise<QueryResult<T>>;
30
- nativeUpdateMany<T extends object>(entityName: string, where: FilterQuery<T>[], data: EntityDictionary<T>[], options?: NativeInsertUpdateManyOptions<T>): Promise<QueryResult<T>>;
31
- abstract nativeDelete<T extends object>(entityName: string, where: FilterQuery<T>, options?: DeleteOptions<T>): Promise<QueryResult<T>>;
32
- abstract count<T extends object, P extends string = never>(entityName: string, where: FilterQuery<T>, options?: CountOptions<T, P>): Promise<number>;
25
+ abstract find<T extends object, P extends string = never, F extends string = '*', E extends string = never>(entityName: EntityName<T>, where: FilterQuery<T>, options?: FindOptions<T, P, F, E>): Promise<EntityData<T>[]>;
26
+ abstract findOne<T extends object, P extends string = never, F extends string = '*', E extends string = never>(entityName: EntityName<T>, where: FilterQuery<T>, options?: FindOneOptions<T, P, F, E>): Promise<EntityData<T> | null>;
27
+ abstract nativeInsert<T extends object>(entityName: EntityName<T>, data: EntityDictionary<T>, options?: NativeInsertUpdateOptions<T>): Promise<QueryResult<T>>;
28
+ abstract nativeInsertMany<T extends object>(entityName: EntityName<T>, data: EntityDictionary<T>[], options?: NativeInsertUpdateManyOptions<T>, transform?: (sql: string) => string): Promise<QueryResult<T>>;
29
+ abstract nativeUpdate<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, data: EntityDictionary<T>, options?: NativeInsertUpdateOptions<T>): Promise<QueryResult<T>>;
30
+ nativeUpdateMany<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>[], data: EntityDictionary<T>[], options?: NativeInsertUpdateManyOptions<T>): Promise<QueryResult<T>>;
31
+ abstract nativeDelete<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, options?: DeleteOptions<T>): Promise<QueryResult<T>>;
32
+ abstract count<T extends object, P extends string = never>(entityName: EntityName<T>, where: FilterQuery<T>, options?: CountOptions<T, P>): Promise<number>;
33
33
  createEntityManager(useContext?: boolean): this[typeof EntityManagerType];
34
34
  findVirtual<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, options: FindOptions<T, any, any, any>): Promise<EntityData<T>[]>;
35
- countVirtual<T extends object>(entityName: string, where: FilterQuery<T>, options: CountOptions<T, any>): Promise<number>;
36
- aggregate(entityName: string, pipeline: any[]): Promise<any[]>;
35
+ countVirtual<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, options: CountOptions<T, any>): Promise<number>;
36
+ aggregate(entityName: EntityName, pipeline: any[]): Promise<any[]>;
37
37
  loadFromPivotTable<T extends object, O extends object>(prop: EntityProperty, owners: Primary<O>[][], where?: FilterQuery<any>, orderBy?: OrderDefinition<T>, ctx?: Transaction, options?: FindOptions<T, any, any, any>, pivotJoin?: boolean): Promise<Dictionary<T[]>>;
38
38
  syncCollections<T extends object, O extends object>(collections: Iterable<Collection<T, O>>, options?: DriverMethodOptions): Promise<void>;
39
39
  mapResult<T extends object>(result: EntityDictionary<T>, meta?: EntityMetadata<T>, populate?: PopulateOptions<T>[]): EntityData<T> | null;
@@ -58,7 +58,7 @@ export declare abstract class DatabaseDriver<C extends Connection> implements ID
58
58
  /** @internal */
59
59
  mapDataToFieldNames(data: Dictionary, stringifyJsonArrays: boolean, properties?: Record<string, EntityProperty>, convertCustomTypes?: boolean, object?: boolean): Dictionary;
60
60
  protected inlineEmbeddables<T extends object>(meta: EntityMetadata<T>, data: T, where?: boolean): void;
61
- protected getPrimaryKeyFields(entityName: string): string[];
61
+ protected getPrimaryKeyFields<T>(meta: EntityMetadata<T>): string[];
62
62
  protected createReplicas(cb: (c: ConnectionOptions) => C): C[];
63
63
  lockPessimistic<T extends object>(entity: T, options: LockOptions): Promise<void>;
64
64
  abstract stream<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, options: StreamOptions<T>): AsyncIterableIterator<T>;
@@ -8,6 +8,7 @@ import { EntityManager } from '../EntityManager.js';
8
8
  import { CursorError, ValidationError } from '../errors.js';
9
9
  import { DriverException } from '../exceptions.js';
10
10
  import { helper } from '../entity/wrap.js';
11
+ import { PolymorphicRef } from '../entity/PolymorphicRef.js';
11
12
  import { JsonType } from '../types/JsonType.js';
12
13
  import { MikroORM } from '../MikroORM.js';
13
14
  export class DatabaseDriver {
@@ -59,7 +60,7 @@ export class DatabaseDriver {
59
60
  {
60
61
  const pk = coll.property.targetMeta.primaryKeys[0];
61
62
  const data = { [coll.property.name]: coll.getIdentifiers(pk) };
62
- await this.nativeUpdate(coll.owner.constructor.name, helper(coll.owner).getPrimaryKey(), data, options);
63
+ await this.nativeUpdate(coll.owner.constructor, helper(coll.owner).getPrimaryKey(), data, options);
63
64
  }
64
65
  }
65
66
  }
@@ -67,7 +68,7 @@ export class DatabaseDriver {
67
68
  if (!result || !meta) {
68
69
  return result ?? null;
69
70
  }
70
- return this.comparator.mapResult(meta.className, result);
71
+ return this.comparator.mapResult(meta, result);
71
72
  }
72
73
  async connect(options) {
73
74
  await this.connection.connect(options);
@@ -152,7 +153,7 @@ export class DatabaseDriver {
152
153
  }
153
154
  const createOrderBy = (prop, direction) => {
154
155
  if (Utils.isPlainObject(direction)) {
155
- const value = Utils.keys(direction).reduce((o, key) => {
156
+ const value = Utils.getObjectQueryKeys(direction).reduce((o, key) => {
156
157
  Object.assign(o, createOrderBy(key, direction[key]));
157
158
  return o;
158
159
  }, {});
@@ -168,22 +169,51 @@ export class DatabaseDriver {
168
169
  };
169
170
  }
170
171
  createCursorCondition(definition, offsets, inverse, meta) {
171
- const createCondition = (prop, direction, offset, eq = false) => {
172
- if (offset === null) {
173
- throw CursorError.missingValue(meta.className, prop);
174
- }
172
+ const createCondition = (prop, direction, offset, eq = false, path = prop) => {
175
173
  if (Utils.isPlainObject(direction)) {
174
+ if (offset === undefined) {
175
+ throw CursorError.missingValue(meta.className, path);
176
+ }
176
177
  const value = Utils.keys(direction).reduce((o, key) => {
177
- if (Utils.isEmpty(offset[key])) {
178
- throw CursorError.missingValue(meta.className, `${prop}.${key}`);
179
- }
180
- Object.assign(o, createCondition(key, direction[key], offset[key], eq));
178
+ Object.assign(o, createCondition(key, direction[key], offset?.[key], eq, `${path}.${key}`));
181
179
  return o;
182
180
  }, {});
183
- return ({ [prop]: value });
181
+ return { [prop]: value };
184
182
  }
185
- const desc = direction === QueryOrderNumeric.DESC || direction.toString().toLowerCase() === 'desc';
186
- const operator = Utils.xor(desc, inverse) ? '$lt' : '$gt';
183
+ const isDesc = direction === QueryOrderNumeric.DESC || direction.toString().toLowerCase() === 'desc';
184
+ const dirStr = direction.toString().toLowerCase();
185
+ let nullsFirst;
186
+ if (dirStr.includes('nulls first')) {
187
+ nullsFirst = true;
188
+ }
189
+ else if (dirStr.includes('nulls last')) {
190
+ nullsFirst = false;
191
+ }
192
+ else {
193
+ // Default: NULLS LAST for ASC, NULLS FIRST for DESC (matches most databases)
194
+ nullsFirst = isDesc;
195
+ }
196
+ const operator = Utils.xor(isDesc, inverse) ? '$lt' : '$gt';
197
+ // For leaf-level properties, undefined means missing value
198
+ if (offset === undefined) {
199
+ throw CursorError.missingValue(meta.className, path);
200
+ }
201
+ // Handle null offset (intentional null cursor value)
202
+ if (offset === null) {
203
+ if (eq) {
204
+ // Equal to null
205
+ return { [prop]: null };
206
+ }
207
+ // Strict comparison with null cursor value
208
+ // hasItemsAfterNull: forward + nullsFirst, or backward + nullsLast
209
+ const hasItemsAfterNull = Utils.xor(nullsFirst, inverse);
210
+ if (hasItemsAfterNull) {
211
+ return { [prop]: { $ne: null } };
212
+ }
213
+ // No items after null in this direction, return impossible condition
214
+ return { [prop]: [] };
215
+ }
216
+ // Non-null offset
187
217
  return { [prop]: { [operator + (eq ? 'e' : '')]: offset } };
188
218
  };
189
219
  const [order, ...otherOrders] = definition;
@@ -231,6 +261,43 @@ export class DatabaseDriver {
231
261
  }
232
262
  return;
233
263
  }
264
+ // Handle polymorphic relations - convert tuple or PolymorphicRef to separate columns
265
+ // Tuple format: ['discriminator', id] or ['discriminator', id1, id2] for composite keys
266
+ // Must be checked BEFORE joinColumns array handling since polymorphic uses fieldNames (includes discriminator)
267
+ if (prop.polymorphic && prop.fieldNames && prop.fieldNames.length >= 2) {
268
+ let discriminator;
269
+ let ids;
270
+ if (Array.isArray(data[k]) && typeof data[k][0] === 'string' && prop.discriminatorMap?.[data[k][0]]) {
271
+ // Tuple format: ['discriminator', ...ids]
272
+ const [disc, ...rest] = data[k];
273
+ discriminator = disc;
274
+ ids = rest;
275
+ }
276
+ else if (data[k] instanceof PolymorphicRef) {
277
+ // PolymorphicRef wrapper (internal use)
278
+ discriminator = data[k].discriminator;
279
+ const polyId = data[k].id;
280
+ // Handle object-style composite key IDs like { tenantId: 1, orgId: 100 }
281
+ if (polyId && typeof polyId === 'object' && !Array.isArray(polyId)) {
282
+ const targetEntity = prop.discriminatorMap?.[discriminator];
283
+ const targetMeta = this.metadata.get(targetEntity);
284
+ ids = targetMeta.primaryKeys.map(pk => polyId[pk]);
285
+ }
286
+ else {
287
+ ids = Utils.asArray(polyId);
288
+ }
289
+ }
290
+ if (discriminator) {
291
+ const discriminatorColumn = prop.fieldNames[0];
292
+ const idColumns = prop.fieldNames.slice(1);
293
+ delete data[k];
294
+ data[discriminatorColumn] = discriminator;
295
+ idColumns.forEach((col, idx) => {
296
+ data[col] = ids[idx];
297
+ });
298
+ return;
299
+ }
300
+ }
234
301
  if (prop.joinColumns && Array.isArray(data[k])) {
235
302
  const copy = Utils.flatten(data[k]);
236
303
  delete data[k];
@@ -277,38 +344,38 @@ export class DatabaseDriver {
277
344
  // explicitly allow `$exists`, `$eq` and `$ne` operators here as they can't be misused this way
278
345
  const operator = Object.keys(data[prop.name]).some(f => Utils.isOperator(f) && !['$exists', '$ne', '$eq'].includes(f));
279
346
  if (operator) {
280
- throw ValidationError.cannotUseOperatorsInsideEmbeddables(meta.className, prop.name, data);
347
+ throw ValidationError.cannotUseOperatorsInsideEmbeddables(meta.class, prop.name, data);
281
348
  }
282
349
  if (prop.object && where) {
283
350
  const inline = (payload, sub, path) => {
284
351
  if (sub.kind === ReferenceKind.EMBEDDED && Utils.isObject(payload[sub.embedded[1]])) {
285
352
  return Object.keys(payload[sub.embedded[1]]).forEach(kkk => {
286
353
  if (!sub.embeddedProps[kkk]) {
287
- throw ValidationError.invalidEmbeddableQuery(meta.className, kkk, sub.type);
354
+ throw ValidationError.invalidEmbeddableQuery(meta.class, kkk, sub.type);
288
355
  }
289
- inline(payload[sub.embedded[1]], sub.embeddedProps[kkk], [...path, sub.embedded[1]]);
356
+ inline(payload[sub.embedded[1]], sub.embeddedProps[kkk], [...path, sub.fieldNames[0]]);
290
357
  });
291
358
  }
292
- data[`${path.join('.')}.${sub.embedded[1]}`] = payload[sub.embedded[1]];
359
+ data[`${path.join('.')}.${sub.fieldNames[0]}`] = payload[sub.embedded[1]];
293
360
  };
294
361
  const parentPropName = kk.substring(0, kk.indexOf('.'));
295
362
  // we might be using some native JSON operator, e.g. with mongodb's `$geoWithin` or `$exists`
296
363
  if (props[kk]) {
297
364
  /* v8 ignore next */
298
- inline(data[prop.name], props[kk] || props[parentPropName], [prop.name]);
365
+ inline(data[prop.name], props[kk] || props[parentPropName], [prop.fieldNames[0]]);
299
366
  }
300
367
  else if (props[parentPropName]) {
301
- data[`${prop.name}.${kk}`] = data[prop.name][kk];
368
+ data[`${prop.fieldNames[0]}.${kk}`] = data[prop.name][kk];
302
369
  }
303
370
  else {
304
371
  unknownProp = true;
305
372
  }
306
373
  }
307
374
  else if (props[kk]) {
308
- data[props[kk].name] = data[prop.name][props[kk].embedded[1]];
375
+ data[props[kk].fieldNames[0]] = data[prop.name][props[kk].embedded[1]];
309
376
  }
310
377
  else {
311
- throw ValidationError.invalidEmbeddableQuery(meta.className, kk, prop.type);
378
+ throw ValidationError.invalidEmbeddableQuery(meta.class, kk, prop.type);
312
379
  }
313
380
  });
314
381
  if (!unknownProp) {
@@ -317,9 +384,8 @@ export class DatabaseDriver {
317
384
  }
318
385
  });
319
386
  }
320
- getPrimaryKeyFields(entityName) {
321
- const meta = this.metadata.find(entityName);
322
- return meta ? Utils.flatten(meta.getPrimaryProps().map(pk => pk.fieldNames)) : [this.config.getNamingStrategy().referenceColumnName()];
387
+ getPrimaryKeyFields(meta) {
388
+ return meta.getPrimaryProps().flatMap(pk => pk.fieldNames);
323
389
  }
324
390
  createReplicas(cb) {
325
391
  const replicas = this.config.get('replicas', []);
@@ -1,4 +1,4 @@
1
- import type { ConnectionType, EntityData, EntityMetadata, EntityProperty, FilterQuery, Primary, Dictionary, QBFilterQuery, IPrimaryKey, PopulateOptions, EntityDictionary, AutoPath, ObjectQuery, FilterObject, Populate, EntityName } from '../typings.js';
1
+ import type { ConnectionType, EntityData, EntityMetadata, EntityProperty, FilterQuery, Primary, Dictionary, IPrimaryKey, PopulateOptions, EntityDictionary, AutoPath, ObjectQuery, FilterObject, Populate, EntityName, PopulateHintOptions, Prefixes } from '../typings.js';
2
2
  import type { Connection, QueryResult, Transaction } from '../connections/Connection.js';
3
3
  import type { FlushMode, LockMode, QueryOrderMap, QueryFlag, LoadStrategy, PopulateHint, PopulatePath } from '../enums.js';
4
4
  import type { Platform } from '../platforms/Platform.js';
@@ -8,7 +8,7 @@ import type { EntityManager } from '../EntityManager.js';
8
8
  import type { DriverException } from '../exceptions.js';
9
9
  import type { Configuration } from '../utils/Configuration.js';
10
10
  import type { LoggingOptions, LogContext } from '../logging/Logger.js';
11
- import type { RawQueryFragment } from '../utils/RawQueryFragment.js';
11
+ import type { Raw } from '../utils/RawQueryFragment.js';
12
12
  export declare const EntityManagerType: unique symbol;
13
13
  export interface IDatabaseDriver<C extends Connection = Connection> {
14
14
  [EntityManagerType]: EntityManager<this>;
@@ -25,21 +25,21 @@ export interface IDatabaseDriver<C extends Connection = Connection> {
25
25
  /**
26
26
  * Finds selection of entities
27
27
  */
28
- find<T extends object, P extends string = never, F extends string = '*', E extends string = never>(entityName: string, where: FilterQuery<T>, options?: FindOptions<T, P, F, E>): Promise<EntityData<T>[]>;
28
+ find<T extends object, P extends string = never, F extends string = '*', E extends string = never>(entityName: EntityName<T>, where: FilterQuery<T>, options?: FindOptions<T, P, F, E>): Promise<EntityData<T>[]>;
29
29
  /**
30
30
  * Finds single entity (table row, document)
31
31
  */
32
- findOne<T extends object, P extends string = never, F extends string = '*', E extends string = never>(entityName: string, where: FilterQuery<T>, options?: FindOneOptions<T, P, F, E>): Promise<EntityData<T> | null>;
33
- findVirtual<T extends object>(entityName: string, where: FilterQuery<T>, options: FindOptions<T, any, any, any>): Promise<EntityData<T>[]>;
32
+ findOne<T extends object, P extends string = never, F extends string = '*', E extends string = never>(entityName: EntityName<T>, where: FilterQuery<T>, options?: FindOneOptions<T, P, F, E>): Promise<EntityData<T> | null>;
33
+ findVirtual<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, options: FindOptions<T, any, any, any>): Promise<EntityData<T>[]>;
34
34
  stream<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, options: StreamOptions<T>): AsyncIterableIterator<T>;
35
- nativeInsert<T extends object>(entityName: string, data: EntityDictionary<T>, options?: NativeInsertUpdateOptions<T>): Promise<QueryResult<T>>;
36
- nativeInsertMany<T extends object>(entityName: string, data: EntityDictionary<T>[], options?: NativeInsertUpdateManyOptions<T>, transform?: (sql: string) => string): Promise<QueryResult<T>>;
37
- nativeUpdate<T extends object>(entityName: string, where: FilterQuery<T>, data: EntityDictionary<T>, options?: NativeInsertUpdateOptions<T>): Promise<QueryResult<T>>;
38
- nativeUpdateMany<T extends object>(entityName: string, where: FilterQuery<T>[], data: EntityDictionary<T>[], options?: NativeInsertUpdateManyOptions<T>): Promise<QueryResult<T>>;
39
- nativeDelete<T extends object>(entityName: string, where: FilterQuery<T>, options?: NativeDeleteOptions<T>): Promise<QueryResult<T>>;
35
+ nativeInsert<T extends object>(entityName: EntityName<T>, data: EntityDictionary<T>, options?: NativeInsertUpdateOptions<T>): Promise<QueryResult<T>>;
36
+ nativeInsertMany<T extends object>(entityName: EntityName<T>, data: EntityDictionary<T>[], options?: NativeInsertUpdateManyOptions<T>, transform?: (sql: string) => string): Promise<QueryResult<T>>;
37
+ nativeUpdate<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, data: EntityDictionary<T>, options?: NativeInsertUpdateOptions<T>): Promise<QueryResult<T>>;
38
+ nativeUpdateMany<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>[], data: EntityDictionary<T>[], options?: NativeInsertUpdateManyOptions<T>): Promise<QueryResult<T>>;
39
+ nativeDelete<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, options?: NativeDeleteOptions<T>): Promise<QueryResult<T>>;
40
40
  syncCollections<T extends object, O extends object>(collections: Iterable<Collection<T, O>>, options?: DriverMethodOptions): Promise<void>;
41
- count<T extends object, P extends string = never>(entityName: string, where: FilterQuery<T>, options?: CountOptions<T, P>): Promise<number>;
42
- aggregate(entityName: string, pipeline: any[]): Promise<any[]>;
41
+ count<T extends object, P extends string = never>(entityName: EntityName<T>, where: FilterQuery<T>, options?: CountOptions<T, P>): Promise<number>;
42
+ aggregate(entityName: EntityName, pipeline: any[]): Promise<any[]>;
43
43
  mapResult<T extends object>(result: EntityDictionary<T>, meta: EntityMetadata<T>, populate?: PopulateOptions<T>[]): EntityData<T> | null;
44
44
  /**
45
45
  * When driver uses pivot tables for M:N, this method will load identifiers for given collections from them
@@ -107,6 +107,10 @@ export interface FindOptions<Entity, Hint extends string = never, Fields extends
107
107
  populateFilter?: ObjectQuery<Entity>;
108
108
  /** Used for ordering of the populate queries. If not specified, the value of `options.orderBy` is used. */
109
109
  populateOrderBy?: OrderDefinition<Entity>;
110
+ /** Per-relation overrides for populate loading behavior. Keys are populate paths (same as used in `populate`). */
111
+ populateHints?: [Hint] extends [never] ? never : {
112
+ [K in Prefixes<Hint>]?: PopulateHintOptions;
113
+ };
110
114
  /** Ordering of the results.Can be an object or array of objects, keys are property names, values are ordering (asc/desc) */
111
115
  orderBy?: OrderDefinition<Entity>;
112
116
  /** Control result caching for this query. Result cache is by default disabled, not to be confused with the identity map. */
@@ -144,7 +148,7 @@ export interface FindOptions<Entity, Hint extends string = never, Fields extends
144
148
  flags?: QueryFlag[];
145
149
  /** sql only */
146
150
  groupBy?: string | string[];
147
- having?: QBFilterQuery<Entity>;
151
+ having?: FilterQuery<Entity>;
148
152
  /** sql only */
149
153
  strategy?: LoadStrategy | `${LoadStrategy}`;
150
154
  flushMode?: FlushMode | `${FlushMode}`;
@@ -155,18 +159,24 @@ export interface FindOptions<Entity, Hint extends string = never, Fields extends
155
159
  lockTableAliases?: string[];
156
160
  ctx?: Transaction;
157
161
  connectionType?: ConnectionType;
158
- /** sql only */
159
- indexHint?: string;
162
+ /** SQL: appended to FROM clause (e.g. `'force index(my_index)'`); MongoDB: index name or spec passed as `hint`. */
163
+ indexHint?: string | Dictionary;
160
164
  /** sql only */
161
165
  comments?: string | string[];
162
166
  /** sql only */
163
167
  hintComments?: string | string[];
168
+ /** SQL: collation name string applied as COLLATE to ORDER BY; MongoDB: CollationOptions object. */
169
+ collation?: CollationOptions | string;
170
+ /** mongodb only */
171
+ maxTimeMS?: number;
172
+ /** mongodb only */
173
+ allowDiskUse?: boolean;
164
174
  loggerContext?: LogContext;
165
175
  logging?: LoggingOptions;
166
176
  /** @internal used to apply filters to the auto-joined relations */
167
177
  em?: EntityManager;
168
178
  }
169
- export interface FindByCursorOptions<T extends object, P extends string = never, F extends string = '*', E extends string = never, I extends boolean = true> extends Omit<FindOptions<T, P, F, E>, 'limit' | 'offset'> {
179
+ export interface FindByCursorOptions<T extends object, P extends string = never, F extends string = '*', E extends string = never, I extends boolean = true> extends Omit<FindAllOptions<T, P, F, E>, 'limit' | 'offset'> {
170
180
  includeCount?: I;
171
181
  }
172
182
  export interface FindOneOptions<T, P extends string = never, F extends string = '*', E extends string = never> extends Omit<FindOptions<T, P, F, E>, 'limit' | 'lockMode'> {
@@ -189,10 +199,11 @@ export interface NativeInsertUpdateManyOptions<T> extends NativeInsertUpdateOpti
189
199
  processCollections?: boolean;
190
200
  }
191
201
  export interface UpsertOptions<Entity, Fields extends string = never> extends Omit<NativeInsertUpdateOptions<Entity>, 'upsert'> {
192
- onConflictFields?: (keyof Entity)[] | RawQueryFragment;
202
+ onConflictFields?: (keyof Entity)[] | Raw;
193
203
  onConflictAction?: 'ignore' | 'merge';
194
204
  onConflictMergeFields?: AutoPath<Entity, Fields, `${PopulatePath.ALL}`>[];
195
205
  onConflictExcludeFields?: AutoPath<Entity, Fields, `${PopulatePath.ALL}`>[];
206
+ onConflictWhere?: FilterQuery<Entity>;
196
207
  disableIdentityMap?: boolean;
197
208
  }
198
209
  export interface UpsertManyOptions<Entity, Fields extends string = never> extends UpsertOptions<Entity, Fields> {
@@ -202,7 +213,7 @@ export interface CountOptions<T extends object, P extends string = never> {
202
213
  filters?: FilterOptions;
203
214
  schema?: string;
204
215
  groupBy?: string | readonly string[];
205
- having?: QBFilterQuery<T>;
216
+ having?: FilterQuery<T>;
206
217
  cache?: boolean | number | [string, number];
207
218
  populate?: Populate<T, P>;
208
219
  populateWhere?: ObjectQuery<T> | PopulateHint | `${PopulateHint}`;
@@ -210,12 +221,16 @@ export interface CountOptions<T extends object, P extends string = never> {
210
221
  ctx?: Transaction;
211
222
  connectionType?: ConnectionType;
212
223
  flushMode?: FlushMode | `${FlushMode}`;
213
- /** sql only */
214
- indexHint?: string;
224
+ /** SQL: appended to FROM clause (e.g. `'force index(my_index)'`); MongoDB: index name or spec passed as `hint`. */
225
+ indexHint?: string | Dictionary;
215
226
  /** sql only */
216
227
  comments?: string | string[];
217
228
  /** sql only */
218
229
  hintComments?: string | string[];
230
+ /** SQL: collation name string applied as COLLATE; MongoDB: CollationOptions object. */
231
+ collation?: CollationOptions | string;
232
+ /** mongodb only */
233
+ maxTimeMS?: number;
219
234
  loggerContext?: LogContext;
220
235
  logging?: LoggingOptions;
221
236
  /** @internal used to apply filters to the auto-joined relations */
@@ -243,8 +258,23 @@ export interface DriverMethodOptions {
243
258
  schema?: string;
244
259
  loggerContext?: LogContext;
245
260
  }
261
+ export interface CollationOptions {
262
+ locale: string;
263
+ caseLevel?: boolean;
264
+ caseFirst?: string;
265
+ strength?: number;
266
+ numericOrdering?: boolean;
267
+ alternate?: string;
268
+ maxVariable?: string;
269
+ backwards?: boolean;
270
+ }
246
271
  export interface GetReferenceOptions {
247
272
  wrapped?: boolean;
248
273
  convertCustomTypes?: boolean;
249
274
  schema?: string;
275
+ /**
276
+ * Property name to use for identity map lookup instead of the primary key.
277
+ * This is useful for creating references by unique non-PK properties.
278
+ */
279
+ key?: string;
250
280
  }
@@ -4,13 +4,73 @@ import { type AssignOptions } from './EntityAssigner.js';
4
4
  import type { EntityLoaderOptions } from './EntityLoader.js';
5
5
  import { type SerializeOptions } from '../serialization/EntitySerializer.js';
6
6
  import type { FindOneOptions } from '../drivers/IDatabaseDriver.js';
7
+ import type { PopulatePath } from '../enums.js';
7
8
  export declare abstract class BaseEntity {
8
9
  isInitialized(): boolean;
9
10
  populated(populated?: boolean): void;
10
- populate<Entity extends this = this, Hint extends string = never>(populate: AutoPath<Entity, Hint>[] | false, options?: EntityLoaderOptions<Entity>): Promise<Loaded<Entity, Hint>>;
11
+ populate<Entity extends this = this, Hint extends string = never>(populate: AutoPath<Entity, Hint, PopulatePath.ALL>[] | false, options?: EntityLoaderOptions<Entity>): Promise<Loaded<Entity, Hint>>;
11
12
  toReference<Entity extends this = this>(): Ref<Entity> & LoadedReference<Loaded<Entity, AddEager<Entity>>>;
13
+ /**
14
+ * Converts the entity to a plain object representation.
15
+ *
16
+ * **Note on typing with `Loaded` entities:** When called on a `Loaded<Entity, 'relation'>` type,
17
+ * the return type will be `EntityDTO<Entity>` (with relations as primary keys), not
18
+ * `EntityDTO<Loaded<Entity, 'relation'>>` (with loaded relations as nested objects).
19
+ * This is a TypeScript limitation - the `this` type resolves to the class, not the `Loaded` wrapper.
20
+ *
21
+ * For correct typing that reflects loaded relations, use `wrap()`:
22
+ * ```ts
23
+ * const result = await em.find(User, {}, { populate: ['profile'] });
24
+ * // Type: EntityDTO<User> (profile is number)
25
+ * const obj1 = result[0].toObject();
26
+ * // Type: EntityDTO<Loaded<User, 'profile'>> (profile is nested object)
27
+ * const obj2 = wrap(result[0]).toObject();
28
+ * ```
29
+ *
30
+ * Runtime values are correct in both cases - only the static types differ.
31
+ */
12
32
  toObject<Entity extends this = this>(): EntityDTO<Entity>;
33
+ /**
34
+ * Converts the entity to a plain object representation.
35
+ *
36
+ * **Note on typing with `Loaded` entities:** When called on a `Loaded<Entity, 'relation'>` type,
37
+ * the return type will be `EntityDTO<Entity>` (with relations as primary keys), not
38
+ * `EntityDTO<Loaded<Entity, 'relation'>>` (with loaded relations as nested objects).
39
+ * This is a TypeScript limitation - the `this` type resolves to the class, not the `Loaded` wrapper.
40
+ *
41
+ * For correct typing that reflects loaded relations, use `wrap()`:
42
+ * ```ts
43
+ * const result = await em.find(User, {}, { populate: ['profile'] });
44
+ * // Type: EntityDTO<User> (profile is number)
45
+ * const obj1 = result[0].toObject();
46
+ * // Type: EntityDTO<Loaded<User, 'profile'>> (profile is nested object)
47
+ * const obj2 = wrap(result[0]).toObject();
48
+ * ```
49
+ *
50
+ * Runtime values are correct in both cases - only the static types differ.
51
+ */
13
52
  toObject<Entity extends this = this>(ignoreFields: never[]): EntityDTO<Entity>;
53
+ /**
54
+ * Converts the entity to a plain object representation.
55
+ *
56
+ * **Note on typing with `Loaded` entities:** When called on a `Loaded<Entity, 'relation'>` type,
57
+ * the return type will be `EntityDTO<Entity>` (with relations as primary keys), not
58
+ * `EntityDTO<Loaded<Entity, 'relation'>>` (with loaded relations as nested objects).
59
+ * This is a TypeScript limitation - the `this` type resolves to the class, not the `Loaded` wrapper.
60
+ *
61
+ * For correct typing that reflects loaded relations, use `wrap()`:
62
+ * ```ts
63
+ * const result = await em.find(User, {}, { populate: ['profile'] });
64
+ * // Type: EntityDTO<User> (profile is number)
65
+ * const obj1 = result[0].toObject();
66
+ * // Type: EntityDTO<Loaded<User, 'profile'>> (profile is nested object)
67
+ * const obj2 = wrap(result[0]).toObject();
68
+ * ```
69
+ *
70
+ * Runtime values are correct in both cases - only the static types differ.
71
+ *
72
+ * @param ignoreFields - Array of field names to omit from the result.
73
+ */
14
74
  toObject<Entity extends this = this, Ignored extends EntityKey<Entity> = never>(ignoreFields: Ignored[]): Omit<EntityDTO<Entity>, Ignored>;
15
75
  toPOJO<Entity extends this = this>(): EntityDTO<Entity>;
16
76
  serialize<Entity extends this = this, Naked extends FromEntityType<Entity> = FromEntityType<Entity>, Hint extends string = never, Exclude extends string = never>(options?: SerializeOptions<Naked, Hint, Exclude>): EntityDTO<Loaded<Naked, Hint>>;