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

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 (104) hide show
  1. package/EntityManager.d.ts +34 -17
  2. package/EntityManager.js +88 -99
  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 +22 -16
  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 +88 -49
  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/validators.js +2 -2
  32. package/enums.d.ts +2 -2
  33. package/enums.js +1 -0
  34. package/errors.d.ts +16 -8
  35. package/errors.js +40 -13
  36. package/hydration/ObjectHydrator.js +63 -21
  37. package/index.d.ts +1 -1
  38. package/logging/colors.d.ts +1 -1
  39. package/logging/colors.js +7 -6
  40. package/logging/inspect.js +1 -6
  41. package/metadata/EntitySchema.d.ts +43 -13
  42. package/metadata/EntitySchema.js +82 -27
  43. package/metadata/MetadataDiscovery.d.ts +60 -3
  44. package/metadata/MetadataDiscovery.js +665 -154
  45. package/metadata/MetadataProvider.js +3 -1
  46. package/metadata/MetadataStorage.d.ts +13 -6
  47. package/metadata/MetadataStorage.js +64 -19
  48. package/metadata/MetadataValidator.d.ts +32 -2
  49. package/metadata/MetadataValidator.js +196 -31
  50. package/metadata/discover-entities.js +5 -5
  51. package/metadata/types.d.ts +111 -14
  52. package/naming-strategy/AbstractNamingStrategy.d.ts +11 -3
  53. package/naming-strategy/AbstractNamingStrategy.js +12 -0
  54. package/naming-strategy/EntityCaseNamingStrategy.d.ts +3 -3
  55. package/naming-strategy/EntityCaseNamingStrategy.js +6 -5
  56. package/naming-strategy/MongoNamingStrategy.d.ts +3 -3
  57. package/naming-strategy/MongoNamingStrategy.js +6 -6
  58. package/naming-strategy/NamingStrategy.d.ts +17 -3
  59. package/naming-strategy/UnderscoreNamingStrategy.d.ts +3 -3
  60. package/naming-strategy/UnderscoreNamingStrategy.js +6 -6
  61. package/package.json +2 -2
  62. package/platforms/Platform.d.ts +4 -2
  63. package/platforms/Platform.js +5 -2
  64. package/serialization/EntitySerializer.d.ts +3 -0
  65. package/serialization/EntitySerializer.js +15 -13
  66. package/serialization/EntityTransformer.js +6 -6
  67. package/serialization/SerializationContext.d.ts +6 -6
  68. package/typings.d.ts +320 -109
  69. package/typings.js +84 -17
  70. package/unit-of-work/ChangeSet.d.ts +4 -3
  71. package/unit-of-work/ChangeSet.js +2 -3
  72. package/unit-of-work/ChangeSetComputer.js +27 -6
  73. package/unit-of-work/ChangeSetPersister.d.ts +5 -0
  74. package/unit-of-work/ChangeSetPersister.js +45 -15
  75. package/unit-of-work/CommitOrderCalculator.d.ts +12 -10
  76. package/unit-of-work/CommitOrderCalculator.js +13 -13
  77. package/unit-of-work/IdentityMap.d.ts +12 -0
  78. package/unit-of-work/IdentityMap.js +39 -1
  79. package/unit-of-work/UnitOfWork.d.ts +21 -3
  80. package/unit-of-work/UnitOfWork.js +201 -54
  81. package/utils/AbstractSchemaGenerator.js +17 -8
  82. package/utils/AsyncContext.d.ts +6 -0
  83. package/utils/AsyncContext.js +42 -0
  84. package/utils/Configuration.d.ts +52 -11
  85. package/utils/Configuration.js +12 -8
  86. package/utils/Cursor.js +21 -8
  87. package/utils/DataloaderUtils.js +13 -11
  88. package/utils/EntityComparator.d.ts +14 -7
  89. package/utils/EntityComparator.js +132 -46
  90. package/utils/QueryHelper.d.ts +16 -6
  91. package/utils/QueryHelper.js +53 -18
  92. package/utils/RawQueryFragment.d.ts +28 -23
  93. package/utils/RawQueryFragment.js +34 -56
  94. package/utils/RequestContext.js +2 -2
  95. package/utils/TransactionContext.js +2 -2
  96. package/utils/TransactionManager.js +1 -1
  97. package/utils/Utils.d.ts +7 -26
  98. package/utils/Utils.js +25 -79
  99. package/utils/clone.js +7 -21
  100. package/utils/env-vars.d.ts +4 -0
  101. package/utils/env-vars.js +13 -3
  102. package/utils/fs-utils.d.ts +21 -0
  103. package/utils/fs-utils.js +104 -9
  104. 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 } 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
@@ -144,7 +144,7 @@ export interface FindOptions<Entity, Hint extends string = never, Fields extends
144
144
  flags?: QueryFlag[];
145
145
  /** sql only */
146
146
  groupBy?: string | string[];
147
- having?: QBFilterQuery<Entity>;
147
+ having?: FilterQuery<Entity>;
148
148
  /** sql only */
149
149
  strategy?: LoadStrategy | `${LoadStrategy}`;
150
150
  flushMode?: FlushMode | `${FlushMode}`;
@@ -166,7 +166,7 @@ export interface FindOptions<Entity, Hint extends string = never, Fields extends
166
166
  /** @internal used to apply filters to the auto-joined relations */
167
167
  em?: EntityManager;
168
168
  }
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'> {
169
+ 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
170
  includeCount?: I;
171
171
  }
172
172
  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 +189,11 @@ export interface NativeInsertUpdateManyOptions<T> extends NativeInsertUpdateOpti
189
189
  processCollections?: boolean;
190
190
  }
191
191
  export interface UpsertOptions<Entity, Fields extends string = never> extends Omit<NativeInsertUpdateOptions<Entity>, 'upsert'> {
192
- onConflictFields?: (keyof Entity)[] | RawQueryFragment;
192
+ onConflictFields?: (keyof Entity)[] | Raw;
193
193
  onConflictAction?: 'ignore' | 'merge';
194
194
  onConflictMergeFields?: AutoPath<Entity, Fields, `${PopulatePath.ALL}`>[];
195
195
  onConflictExcludeFields?: AutoPath<Entity, Fields, `${PopulatePath.ALL}`>[];
196
+ onConflictWhere?: FilterQuery<Entity>;
196
197
  disableIdentityMap?: boolean;
197
198
  }
198
199
  export interface UpsertManyOptions<Entity, Fields extends string = never> extends UpsertOptions<Entity, Fields> {
@@ -202,7 +203,7 @@ export interface CountOptions<T extends object, P extends string = never> {
202
203
  filters?: FilterOptions;
203
204
  schema?: string;
204
205
  groupBy?: string | readonly string[];
205
- having?: QBFilterQuery<T>;
206
+ having?: FilterQuery<T>;
206
207
  cache?: boolean | number | [string, number];
207
208
  populate?: Populate<T, P>;
208
209
  populateWhere?: ObjectQuery<T> | PopulateHint | `${PopulateHint}`;
@@ -247,4 +248,9 @@ export interface GetReferenceOptions {
247
248
  wrapped?: boolean;
248
249
  convertCustomTypes?: boolean;
249
250
  schema?: string;
251
+ /**
252
+ * Property name to use for identity map lookup instead of the primary key.
253
+ * This is useful for creating references by unique non-PK properties.
254
+ */
255
+ key?: string;
250
256
  }
@@ -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>>;
@@ -62,7 +62,6 @@ export declare class Collection<T extends object, O extends object = object> {
62
62
  init<TT extends T, P extends string = never>(options?: InitCollectionOptions<TT, P>): Promise<LoadedCollection<Loaded<TT, P>>>;
63
63
  private getEntityManager;
64
64
  private createCondition;
65
- private createOrderBy;
66
65
  private createManyToManyCondition;
67
66
  private createLoadCountCondition;
68
67
  private checkInitialized;
@@ -104,10 +103,18 @@ export declare class Collection<T extends object, O extends object = object> {
104
103
  * Tests for the existence of an element that satisfies the given predicate.
105
104
  */
106
105
  exists(cb: (item: T) => boolean): boolean;
106
+ /**
107
+ * Returns the first element of this collection that satisfies the predicate.
108
+ */
109
+ find<S extends T>(cb: (item: T, index: number) => item is S): S | undefined;
107
110
  /**
108
111
  * Returns the first element of this collection that satisfies the predicate.
109
112
  */
110
113
  find(cb: (item: T, index: number) => boolean): T | undefined;
114
+ /**
115
+ * Extracts a subset of the collection items.
116
+ */
117
+ filter<S extends T>(cb: (item: T, index: number) => item is S): S[];
111
118
  /**
112
119
  * Extracts a subset of the collection items.
113
120
  */