@mikro-orm/core 7.1.0-dev.3 → 7.1.0-dev.30

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 (54) hide show
  1. package/EntityManager.d.ts +44 -11
  2. package/EntityManager.js +139 -31
  3. package/drivers/IDatabaseDriver.d.ts +39 -1
  4. package/entity/BaseEntity.d.ts +68 -1
  5. package/entity/BaseEntity.js +18 -0
  6. package/entity/Collection.d.ts +6 -3
  7. package/entity/Collection.js +15 -4
  8. package/entity/EntityFactory.js +20 -1
  9. package/entity/EntityLoader.d.ts +3 -1
  10. package/entity/EntityLoader.js +39 -16
  11. package/entity/EntityRepository.d.ts +27 -9
  12. package/entity/EntityRepository.js +12 -0
  13. package/entity/Reference.d.ts +42 -1
  14. package/entity/Reference.js +9 -0
  15. package/entity/defineEntity.d.ts +105 -23
  16. package/entity/defineEntity.js +17 -6
  17. package/entity/utils.js +4 -5
  18. package/errors.d.ts +2 -0
  19. package/errors.js +4 -0
  20. package/index.d.ts +2 -2
  21. package/index.js +1 -1
  22. package/metadata/EntitySchema.js +3 -0
  23. package/metadata/MetadataDiscovery.d.ts +12 -0
  24. package/metadata/MetadataDiscovery.js +185 -16
  25. package/metadata/MetadataValidator.d.ts +24 -0
  26. package/metadata/MetadataValidator.js +202 -1
  27. package/metadata/types.d.ts +77 -4
  28. package/naming-strategy/AbstractNamingStrategy.d.ts +1 -1
  29. package/naming-strategy/NamingStrategy.d.ts +1 -1
  30. package/package.json +1 -1
  31. package/platforms/Platform.d.ts +8 -1
  32. package/platforms/Platform.js +40 -4
  33. package/serialization/EntitySerializer.js +2 -1
  34. package/typings.d.ts +176 -18
  35. package/typings.js +22 -5
  36. package/unit-of-work/UnitOfWork.js +3 -0
  37. package/utils/AbstractMigrator.d.ts +20 -5
  38. package/utils/AbstractMigrator.js +263 -28
  39. package/utils/AbstractSchemaGenerator.d.ts +1 -1
  40. package/utils/AbstractSchemaGenerator.js +4 -1
  41. package/utils/Configuration.d.ts +25 -0
  42. package/utils/Configuration.js +1 -0
  43. package/utils/DataloaderUtils.d.ts +10 -1
  44. package/utils/DataloaderUtils.js +78 -0
  45. package/utils/EntityComparator.js +1 -1
  46. package/utils/QueryHelper.d.ts +16 -0
  47. package/utils/QueryHelper.js +15 -0
  48. package/utils/Utils.js +1 -1
  49. package/utils/fs-utils.d.ts +2 -0
  50. package/utils/fs-utils.js +7 -1
  51. package/utils/index.d.ts +1 -0
  52. package/utils/index.js +1 -0
  53. package/utils/partition-utils.d.ts +17 -0
  54. package/utils/partition-utils.js +79 -0
@@ -38,18 +38,32 @@ export class AbstractMigrator {
38
38
  /**
39
39
  * @inheritDoc
40
40
  */
41
- async getExecuted() {
41
+ async getExecuted(options) {
42
42
  await this.init();
43
- return this.storage.getExecutedMigrations();
43
+ const schema = options?.schema ?? this.options.schema;
44
+ this.storage.setRunSchema?.(schema);
45
+ try {
46
+ return await this.storage.getExecutedMigrations();
47
+ }
48
+ finally {
49
+ this.storage.unsetRunSchema?.();
50
+ }
44
51
  }
45
52
  /**
46
53
  * @inheritDoc
47
54
  */
48
- async getPending() {
55
+ async getPending(options) {
49
56
  await this.init();
50
- const all = await this.discoverMigrations();
51
- const executed = new Set(await this.storage.executed());
52
- return all.filter(m => !executed.has(m.name)).map(m => ({ name: m.name, path: m.path }));
57
+ const schema = options?.schema ?? this.options.schema;
58
+ this.storage.setRunSchema?.(schema);
59
+ try {
60
+ const all = await this.discoverMigrations();
61
+ const executed = new Set(await this.storage.executed());
62
+ return all.filter(m => !executed.has(m.name)).map(m => ({ name: m.name, path: m.path }));
63
+ }
64
+ finally {
65
+ this.storage.unsetRunSchema?.();
66
+ }
53
67
  }
54
68
  /**
55
69
  * @inheritDoc
@@ -63,6 +77,209 @@ export class AbstractMigrator {
63
77
  async down(options) {
64
78
  return this.runMigrations('down', options);
65
79
  }
80
+ /**
81
+ * @inheritDoc
82
+ */
83
+ async rollup(migrations) {
84
+ await this.init();
85
+ const { fs } = await import('@mikro-orm/core/fs-utils');
86
+ const all = await this.discoverMigrations();
87
+ const executedSet = new Set(await this.storage.executed());
88
+ let toRollup;
89
+ if (migrations && migrations.length > 0) {
90
+ const requested = new Set(migrations.map(m => this.getMigrationFilename(m)));
91
+ toRollup = all.filter(m => requested.has(m.name));
92
+ const found = new Set(toRollup.map(m => m.name));
93
+ const notFound = [...requested].filter(name => !found.has(name));
94
+ if (notFound.length > 0) {
95
+ throw new Error(`Migrations not found: ${notFound.join(', ')}`);
96
+ }
97
+ const notExecuted = toRollup.filter(m => !executedSet.has(m.name));
98
+ if (notExecuted.length > 0) {
99
+ throw new Error(`Cannot roll up migrations that have not been executed: ${notExecuted.map(m => m.name).join(', ')}`);
100
+ }
101
+ }
102
+ else {
103
+ toRollup = all.filter(m => executedSet.has(m.name));
104
+ }
105
+ if (toRollup.length < 2) {
106
+ throw new Error('At least 2 executed migrations are required for rollup');
107
+ }
108
+ const withoutPath = toRollup.filter(m => !m.path);
109
+ if (withoutPath.length > 0) {
110
+ throw new Error(`Cannot roll up migrations without file paths (class-based migrations): ${withoutPath.map(m => m.name).join(', ')}`);
111
+ }
112
+ const upBodies = [];
113
+ const downBodies = [];
114
+ const placeholder = `__mikro_orm_rollup_${Date.now()}__`;
115
+ for (const migration of toRollup) {
116
+ const source = await fs.readFile(migration.path);
117
+ const upBody = this.extractMethodBody(source, 'up');
118
+ const downBody = this.extractMethodBody(source, 'down');
119
+ if (upBody) {
120
+ upBodies.push(` // --- merged from ${migration.name} ---\n${upBody}`);
121
+ }
122
+ if (downBody) {
123
+ downBodies.unshift(` // --- merged from ${migration.name} ---\n${downBody}`);
124
+ }
125
+ }
126
+ const diff = {
127
+ up: [placeholder],
128
+ down: downBodies.length > 0 ? [placeholder] : [],
129
+ };
130
+ const [templateCode, fileName] = await this.generator.generate(diff);
131
+ const placeholderRe = new RegExp(`^.*${placeholder}.*$`, 'm');
132
+ let code = templateCode.replace(placeholderRe, upBodies.join('\n'));
133
+ if (downBodies.length > 0) {
134
+ code = code.replace(placeholderRe, downBodies.join('\n'));
135
+ }
136
+ await fs.writeFile(fs.normalizePath(this.absolutePath, fileName), code, { flush: true });
137
+ const updateStorage = async () => {
138
+ for (const migration of toRollup) {
139
+ await this.storage.unlogMigration({ name: migration.name });
140
+ }
141
+ await this.storage.logMigration({ name: fileName.replace(/\.[jt]s$/, '') });
142
+ };
143
+ if (this.options.transactional) {
144
+ await this.driver.getConnection().transactional(async (trx) => {
145
+ this.storage.setMasterMigration(trx);
146
+ try {
147
+ await updateStorage();
148
+ }
149
+ finally {
150
+ this.storage.unsetMasterMigration();
151
+ }
152
+ });
153
+ }
154
+ else {
155
+ await updateStorage();
156
+ }
157
+ await Promise.all(toRollup.map(migration => fs.unlink(migration.path)));
158
+ return { fileName, code, diff: { up: [], down: [] } };
159
+ }
160
+ /**
161
+ * Extracts the body of a method from migration source code using brace counting.
162
+ * Returns the raw lines between the opening and closing braces, or empty string if not found.
163
+ * @internal
164
+ */
165
+ extractMethodBody(source, methodName) {
166
+ const lines = source.split('\n');
167
+ // match method declarations, not occurrences in comments/strings — require preceding whitespace or keyword
168
+ const methodPattern = new RegExp(`^\\s+(?:override\\s+|async\\s+)*${methodName}\\s*\\(`);
169
+ let methodLine = -1;
170
+ for (let i = 0; i < lines.length; i++) {
171
+ if (methodPattern.test(lines[i])) {
172
+ methodLine = i;
173
+ break;
174
+ }
175
+ }
176
+ if (methodLine === -1) {
177
+ return '';
178
+ }
179
+ let braceCount = 0;
180
+ let bodyStart = -1;
181
+ let bodyEnd = -1;
182
+ let bodyStartCol = -1;
183
+ let bodyEndCol = -1;
184
+ let inBacktick = false;
185
+ let inBlockComment = false;
186
+ // stack tracks brace depth at which each template expression `${...}` was entered
187
+ const templateExprStack = [];
188
+ for (let i = methodLine; i < lines.length; i++) {
189
+ const line = lines[i];
190
+ for (let j = 0; j < line.length; j++) {
191
+ // handle multi-line block comments
192
+ if (inBlockComment) {
193
+ if (line[j] === '*' && j + 1 < line.length && line[j + 1] === '/') {
194
+ inBlockComment = false;
195
+ j++;
196
+ }
197
+ continue;
198
+ }
199
+ // handle multi-line template literals
200
+ if (inBacktick) {
201
+ if (line[j] === '\\') {
202
+ j++;
203
+ }
204
+ else if (line[j] === '`') {
205
+ inBacktick = false;
206
+ }
207
+ else if (line[j] === '$' && j + 1 < line.length && line[j + 1] === '{') {
208
+ // entering template expression — resume brace counting
209
+ templateExprStack.push(braceCount);
210
+ inBacktick = false;
211
+ j++; // skip the {
212
+ braceCount++;
213
+ }
214
+ continue;
215
+ }
216
+ const ch = line[j];
217
+ // single/double quoted strings (single-line only)
218
+ if (ch === "'" || ch === '"') {
219
+ const quote = ch;
220
+ j++;
221
+ while (j < line.length) {
222
+ if (line[j] === '\\') {
223
+ j++;
224
+ }
225
+ else if (line[j] === quote) {
226
+ break;
227
+ }
228
+ j++;
229
+ }
230
+ continue;
231
+ }
232
+ // template literal start
233
+ if (ch === '`') {
234
+ inBacktick = true;
235
+ continue;
236
+ }
237
+ // single-line comment
238
+ if (ch === '/' && j + 1 < line.length && line[j + 1] === '/') {
239
+ break;
240
+ }
241
+ // block comment start
242
+ if (ch === '/' && j + 1 < line.length && line[j + 1] === '*') {
243
+ inBlockComment = true;
244
+ j++;
245
+ continue;
246
+ }
247
+ if (ch === '{') {
248
+ if (braceCount === 0) {
249
+ bodyStart = i;
250
+ bodyStartCol = j + 1;
251
+ }
252
+ braceCount++;
253
+ }
254
+ else if (ch === '}') {
255
+ braceCount--;
256
+ // closing a template expression — re-enter backtick mode
257
+ if (templateExprStack.length > 0 && braceCount === templateExprStack[templateExprStack.length - 1]) {
258
+ templateExprStack.pop();
259
+ inBacktick = true;
260
+ continue;
261
+ }
262
+ if (braceCount === 0) {
263
+ bodyEnd = i;
264
+ bodyEndCol = j;
265
+ break;
266
+ }
267
+ }
268
+ }
269
+ if (bodyEnd !== -1) {
270
+ break;
271
+ }
272
+ }
273
+ if (bodyStart === -1 || bodyEnd === -1) {
274
+ return '';
275
+ }
276
+ // single-line method body: extract content between braces on the same line
277
+ if (bodyStart === bodyEnd) {
278
+ const content = lines[bodyStart].slice(bodyStartCol, bodyEndCol).trim();
279
+ return content ? ` ${content}` : '';
280
+ }
281
+ return lines.slice(bodyStart + 1, bodyEnd).join('\n');
282
+ }
66
283
  /**
67
284
  * @inheritDoc
68
285
  */
@@ -114,26 +331,26 @@ export class AbstractMigrator {
114
331
  }
115
332
  }
116
333
  resolve(params) {
117
- const createMigrationHandler = async (method) => {
334
+ const createMigrationHandler = async (method, afterRun) => {
118
335
  const { fs } = await import('@mikro-orm/core/fs-utils');
119
336
  const migration = await fs.dynamicImport(params.path);
120
337
  const MigrationClass = Object.values(migration).find(cls => typeof cls === 'function' && typeof cls.constructor === 'function');
121
338
  const instance = new MigrationClass(this.driver, this.config);
122
- await this.runner.run(instance, method);
339
+ await this.runner.run(instance, method, afterRun);
123
340
  };
124
341
  return {
125
342
  name: this.storage.getMigrationName(params.name),
126
343
  path: params.path,
127
- up: () => createMigrationHandler('up'),
128
- down: () => createMigrationHandler('down'),
344
+ up: afterRun => createMigrationHandler('up', afterRun),
345
+ down: afterRun => createMigrationHandler('down', afterRun),
129
346
  };
130
347
  }
131
348
  initialize(MigrationClass, name) {
132
349
  const instance = new MigrationClass(this.driver, this.config);
133
350
  return {
134
351
  name: this.storage.getMigrationName(name),
135
- up: () => this.runner.run(instance, 'up'),
136
- down: () => this.runner.run(instance, 'down'),
352
+ up: afterRun => this.runner.run(instance, 'up', afterRun),
353
+ down: afterRun => this.runner.run(instance, 'down', afterRun),
137
354
  };
138
355
  }
139
356
  /**
@@ -212,13 +429,15 @@ export class AbstractMigrator {
212
429
  for (const migration of toRun) {
213
430
  const event = { name: migration.name, path: migration.path };
214
431
  await this.emit(eventBefore, event);
215
- await migration[method]();
216
- if (method === 'up') {
217
- await this.storage.logMigration({ name: migration.name });
218
- }
219
- else {
220
- await this.storage.unlogMigration({ name: migration.name });
221
- }
432
+ // log inside the runner's tx (if any) so a logMigration failure rolls the migration back
433
+ await migration[method](async (tx) => {
434
+ if (method === 'up') {
435
+ await this.storage.logMigration({ name: migration.name }, tx);
436
+ }
437
+ else {
438
+ await this.storage.unlogMigration({ name: migration.name }, tx);
439
+ }
440
+ });
222
441
  await this.emit(eventAfter, event);
223
442
  result.push(event);
224
443
  }
@@ -284,13 +503,17 @@ export class AbstractMigrator {
284
503
  return /^\d{14}$/.exec(name) ? this.options.fileName(name) : name;
285
504
  }
286
505
  prefix(options) {
506
+ const base = {};
507
+ if (this.options.schema) {
508
+ base.schema = this.options.schema;
509
+ }
287
510
  if (typeof options === 'string' || Array.isArray(options)) {
288
- return { migrations: Utils.asArray(options).map(name => this.getMigrationFilename(name)) };
511
+ return { ...base, migrations: Utils.asArray(options).map(name => this.getMigrationFilename(name)) };
289
512
  }
290
513
  if (!options) {
291
- return {};
514
+ return base;
292
515
  }
293
- const result = {};
516
+ const result = base;
294
517
  if (options.migrations) {
295
518
  result.migrations = options.migrations.map(name => this.getMigrationFilename(name));
296
519
  }
@@ -303,23 +526,35 @@ export class AbstractMigrator {
303
526
  else if (options.to === 0) {
304
527
  result.to = 0;
305
528
  }
529
+ if (options.schema !== undefined) {
530
+ result.schema = options.schema;
531
+ }
306
532
  return result;
307
533
  }
308
534
  async runMigrations(method, options) {
309
535
  await this.init();
310
- if (!this.options.transactional || !this.options.allOrNothing) {
311
- return this.executeMigrations(method, this.prefix(options));
536
+ const normalized = this.prefix(options);
537
+ this.runner.setRunSchema?.(normalized.schema);
538
+ this.storage.setRunSchema?.(normalized.schema);
539
+ try {
540
+ if (!this.options.transactional || !this.options.allOrNothing) {
541
+ return await this.executeMigrations(method, normalized);
542
+ }
543
+ if (Utils.isObject(options) && options.transaction) {
544
+ return await this.runInTransaction(options.transaction, method, normalized);
545
+ }
546
+ return await this.driver.getConnection().transactional(trx => this.runInTransaction(trx, method, normalized));
312
547
  }
313
- if (Utils.isObject(options) && options.transaction) {
314
- return this.runInTransaction(options.transaction, method, options);
548
+ finally {
549
+ this.runner.unsetRunSchema?.();
550
+ this.storage.unsetRunSchema?.();
315
551
  }
316
- return this.driver.getConnection().transactional(trx => this.runInTransaction(trx, method, options));
317
552
  }
318
553
  async runInTransaction(trx, method, options) {
319
554
  this.runner.setMasterMigration(trx);
320
555
  this.storage.setMasterMigration(trx);
321
556
  try {
322
- return await this.executeMigrations(method, this.prefix(options));
557
+ return await this.executeMigrations(method, options);
323
558
  }
324
559
  finally {
325
560
  this.runner.unsetMasterMigration();
@@ -34,6 +34,6 @@ export declare abstract class AbstractSchemaGenerator<D extends IDatabaseDriver>
34
34
  dropDatabase(name?: string): Promise<void>;
35
35
  execute(query: string): Promise<void>;
36
36
  ensureIndexes(): Promise<void>;
37
- protected getOrderedMetadata(schema?: string): EntityMetadata[];
37
+ protected getOrderedMetadata(schema?: string, includeWildcardSchema?: boolean): EntityMetadata[];
38
38
  protected notImplemented(): never;
39
39
  }
@@ -89,7 +89,7 @@ export class AbstractSchemaGenerator {
89
89
  async ensureIndexes() {
90
90
  this.notImplemented();
91
91
  }
92
- getOrderedMetadata(schema) {
92
+ getOrderedMetadata(schema, includeWildcardSchema = false) {
93
93
  const metadata = [...this.metadata.getAll().values()].filter(meta => {
94
94
  const isRootEntity = meta.root.class === meta.class;
95
95
  const isTPTChild = meta.inheritanceType === 'tpt' && meta.tptParent;
@@ -116,6 +116,9 @@ export class AbstractSchemaGenerator {
116
116
  .sort()
117
117
  .map(cls => this.metadata.getById(cls))
118
118
  .filter(meta => {
119
+ if (includeWildcardSchema && meta.schema === '*') {
120
+ return true;
121
+ }
119
122
  const targetSchema = meta.schema ?? this.config.get('schema', this.platform.getDefaultSchemaName());
120
123
  return schema ? [schema, '*'].includes(targetSchema) : meta.schema !== '*';
121
124
  });
@@ -238,6 +238,24 @@ export type MigrationsOptions = {
238
238
  fileName?: (timestamp: string, name?: string) => string;
239
239
  /** List of migration classes or objects to use instead of file-based discovery. */
240
240
  migrationsList?: (MigrationObject | Constructor<Migration>)[];
241
+ /**
242
+ * Target schema used when running migrations. When set, the driver's "set current schema" statement
243
+ * (e.g. `SET search_path` on PostgreSQL) is issued before each migration, and the migration tracking
244
+ * table lives in this schema. Can be overridden per call via `migrator.up({ schema })`.
245
+ *
246
+ * Intended for per-deployment-one-schema setups (e.g. PR previews) and for applying a single
247
+ * unqualified migration to multiple schemas (see `includeWildcardSchema`). Not supported on MSSQL.
248
+ */
249
+ schema?: string;
250
+ /**
251
+ * When set, entities with a wildcard schema (`schema: '*'`) are included in generated migrations.
252
+ * The emitted SQL is unqualified — and therefore safe to apply against any schema at runtime via
253
+ * `migrator.up({ schema })` — only when neither `options.schema` nor the ORM's `config.schema`
254
+ * is set. If `config.schema` is set, wildcard tables are qualified with it (useful for local
255
+ * dev runs); in that case, generate migrations from an environment where `config.schema` is unset.
256
+ * @default false
257
+ */
258
+ includeWildcardSchema?: boolean;
241
259
  };
242
260
  /**
243
261
  * Configuration options for database seeders.
@@ -529,6 +547,13 @@ export interface Options<Driver extends IDatabaseDriver = IDatabaseDriver, EM ex
529
547
  * @default false
530
548
  */
531
549
  forceUndefined: boolean;
550
+ /**
551
+ * Initialize nullable properties to `null` (or `undefined` when `forceUndefined` is set)
552
+ * during `em.create()` when they are not provided in the data. Without this option,
553
+ * nullable properties remain `undefined` until the entity is loaded from the database.
554
+ * @default false
555
+ */
556
+ initNullableProperties: boolean;
532
557
  /**
533
558
  * Property `onCreate` hooks are normally executed during `flush` operation.
534
559
  * With this option, they will be processed early inside `em.create()` method.
@@ -62,6 +62,7 @@ const DEFAULTS = {
62
62
  upsertManaged: true,
63
63
  forceEntityConstructor: false,
64
64
  forceUndefined: false,
65
+ initNullableProperties: false,
65
66
  forceUtcTimezone: true,
66
67
  processOnCreateHooksEarly: true,
67
68
  ensureDatabase: true,
@@ -1,5 +1,5 @@
1
1
  import type { Constructor, Primary, Ref } from '../typings.js';
2
- import { Collection, type InitCollectionOptions } from '../entity/Collection.js';
2
+ import { Collection, type InitCollectionOptions, type LoadCountOptions } from '../entity/Collection.js';
3
3
  import { type EntityManager } from '../EntityManager.js';
4
4
  import { type LoadReferenceOptions } from '../entity/Reference.js';
5
5
  type BatchLoadFn<K, V> = (keys: readonly K[]) => PromiseLike<ArrayLike<V | Error>>;
@@ -44,6 +44,15 @@ export declare class DataloaderUtils {
44
44
  * makes one query per entity and maps each input collection to the corresponding result.
45
45
  */
46
46
  static getManyToManyColBatchLoadFn(em: EntityManager): BatchLoadFn<[Collection<any>, Omit<InitCollectionOptions<any, any>, 'dataloader'>?], any>;
47
+ /**
48
+ * Returns the count dataloader batchLoadFn, which aggregates `Collection.loadCount()` calls
49
+ * by entity and relation, issues a single grouped count query per entity+options combination
50
+ * via `em.countBy()`, and maps each input collection to the corresponding count.
51
+ *
52
+ * For 1:M relations, groups by the FK property on the target entity.
53
+ * For M:N relations, groups by the owner FK on the pivot entity.
54
+ */
55
+ static getCountBatchLoadFn(em: EntityManager): BatchLoadFn<[Collection<any>, Omit<LoadCountOptions<any>, 'dataloader' | 'refresh'>?], number>;
47
56
  static getDataLoader(): Promise<Constructor<{
48
57
  load: (...args: unknown[]) => Promise<unknown>;
49
58
  }>>;
@@ -1,6 +1,7 @@
1
1
  import { Collection } from '../entity/Collection.js';
2
2
  import { helper } from '../entity/wrap.js';
3
3
  import { Reference } from '../entity/Reference.js';
4
+ import { ReferenceKind } from '../enums.js';
4
5
  import { Utils } from './Utils.js';
5
6
  export class DataloaderUtils {
6
7
  static DataLoader;
@@ -216,6 +217,83 @@ export class DataloaderUtils {
216
217
  return ret;
217
218
  };
218
219
  }
220
+ /**
221
+ * Returns the count dataloader batchLoadFn, which aggregates `Collection.loadCount()` calls
222
+ * by entity and relation, issues a single grouped count query per entity+options combination
223
+ * via `em.countBy()`, and maps each input collection to the corresponding count.
224
+ *
225
+ * For 1:M relations, groups by the FK property on the target entity.
226
+ * For M:N relations, groups by the owner FK on the pivot entity.
227
+ */
228
+ static getCountBatchLoadFn(em) {
229
+ return async (collsWithOpts) => {
230
+ const groups = new Map();
231
+ const keys = [];
232
+ for (const [col, opts] of collsWithOpts) {
233
+ const prop = col.property;
234
+ let fkProp;
235
+ let countByClass;
236
+ let targetFilterProp;
237
+ if (prop.kind === ReferenceKind.ONE_TO_MANY) {
238
+ fkProp = prop.mappedBy;
239
+ countByClass = prop.targetMeta.class;
240
+ }
241
+ else {
242
+ // M:N: group by the owner FK on the pivot entity
243
+ const pivotMeta = em.getMetadata().get(prop.pivotEntity);
244
+ const ownerPivotProp = pivotMeta.relations[prop.owner ? 0 : 1];
245
+ const targetPivotProp = pivotMeta.relations[prop.owner ? 1 : 0];
246
+ fkProp = ownerPivotProp.name;
247
+ countByClass = pivotMeta.class;
248
+ targetFilterProp = targetPivotProp.name;
249
+ }
250
+ // Include the owner-side uniqueName in the key so that two distinct owner entity types
251
+ // sharing a relation with the same property name (e.g. `Author.books` and `Publisher.books`)
252
+ // don't collide into a single batch that would use one owner's FK mapping for the other.
253
+ const ownerUniqueName = helper(col.owner).__meta.uniqueName;
254
+ const key = `${ownerUniqueName}.${prop.name}|${JSON.stringify(opts ?? {})}`;
255
+ keys.push(key);
256
+ let group = groups.get(key);
257
+ if (!group) {
258
+ group = { fkProp, countByClass, targetFilterProp, ownerPKs: new Map(), opts: opts ?? {} };
259
+ groups.set(key, group);
260
+ }
261
+ const pk = helper(col.owner).getPrimaryKey();
262
+ group.ownerPKs.set(JSON.stringify(pk), pk);
263
+ }
264
+ const promises = Array.from(groups.entries()).map(async ([key, group]) => {
265
+ const allPKs = Array.from(group.ownerPKs.values());
266
+ const { where, ...countOpts } = group.opts;
267
+ const conditions = [{ [group.fkProp]: { $in: allPKs } }];
268
+ if (where) {
269
+ conditions.push(where);
270
+ }
271
+ // For M:N, apply the target entity's filters through the pivot's target relation
272
+ if (group.targetFilterProp) {
273
+ const targetMeta = em.getMetadata().find(group.countByClass);
274
+ const targetRelMeta = targetMeta.properties[group.targetFilterProp]?.targetMeta;
275
+ if (targetRelMeta) {
276
+ const filterCond = await em.applyFilters(targetRelMeta.class, {}, countOpts.filters, 'read');
277
+ if (filterCond && Object.keys(filterCond).length > 0) {
278
+ conditions.push({ [group.targetFilterProp]: filterCond });
279
+ }
280
+ }
281
+ }
282
+ const fkWhere = conditions.length === 1 ? conditions[0] : { $and: conditions };
283
+ const counts = await em.countBy(group.countByClass, group.fkProp, {
284
+ where: fkWhere,
285
+ ...countOpts,
286
+ });
287
+ return [key, counts];
288
+ });
289
+ const resultsMap = new Map(await Promise.all(promises));
290
+ return collsWithOpts.map(([col], i) => {
291
+ const pk = helper(col.owner).getPrimaryKey();
292
+ const counts = resultsMap.get(keys[i]);
293
+ return counts?.[String(pk)] ?? 0;
294
+ });
295
+ };
296
+ }
219
297
  static async getDataLoader() {
220
298
  if (this.DataLoader) {
221
299
  return this.DataLoader;
@@ -536,7 +536,7 @@ export class EntityComparator {
536
536
  ret += ` && entity${path.map(k => this.wrap(k)).join('')}?.isInitialized()`;
537
537
  }
538
538
  ret += `) {\n`;
539
- if (['number', 'string', 'boolean'].includes(prop.type.toLowerCase())) {
539
+ if (!prop.array && ['number', 'string', 'boolean'].includes(prop.type.toLowerCase())) {
540
540
  return ret + ` ret${dataKey} = entity${entityKey}${unwrap};\n }\n`;
541
541
  }
542
542
  if (prop.kind === ReferenceKind.EMBEDDED) {
@@ -6,6 +6,22 @@ import type { FilterOptions } from '../drivers/IDatabaseDriver.js';
6
6
  /** @internal */
7
7
  export declare class QueryHelper {
8
8
  static readonly SUPPORTED_OPERATORS: string[];
9
+ /**
10
+ * True when the property has multiple polymorph target types. Covers two structurally-equivalent
11
+ * shapes routed through the same loading path:
12
+ * 1. Union-target owner side — `Post.attachments: Collection<Image | Video>` (one owner, many
13
+ * target types, shared pivot with target-side discriminator).
14
+ * 2. Merged inverse of Rails-style polymorphic M:N — `Tag.owners: Collection<Post | Video>`
15
+ * (many owner types pointing at one target, viewed from the target where "owners" looks
16
+ * like a union of multiple types).
17
+ *
18
+ * Both cases are loaded via `loadFromUnionTargetPolymorphicPivotTable`, which buckets pivot rows
19
+ * by discriminator and hydrates each target class separately.
20
+ */
21
+ static isUnionTargetPolymorphic(prop: {
22
+ polymorphic?: boolean;
23
+ polymorphTargets?: readonly unknown[];
24
+ }): boolean;
9
25
  /**
10
26
  * Finds the discriminator value (key) for a given entity class in a discriminator map.
11
27
  * Walks up the prototype chain so TPT subclasses resolve to their root's key.
@@ -7,6 +7,21 @@ import { isRaw, Raw } from './RawQueryFragment.js';
7
7
  /** @internal */
8
8
  export class QueryHelper {
9
9
  static SUPPORTED_OPERATORS = ['>', '<', '<=', '>=', '!', '!='];
10
+ /**
11
+ * True when the property has multiple polymorph target types. Covers two structurally-equivalent
12
+ * shapes routed through the same loading path:
13
+ * 1. Union-target owner side — `Post.attachments: Collection<Image | Video>` (one owner, many
14
+ * target types, shared pivot with target-side discriminator).
15
+ * 2. Merged inverse of Rails-style polymorphic M:N — `Tag.owners: Collection<Post | Video>`
16
+ * (many owner types pointing at one target, viewed from the target where "owners" looks
17
+ * like a union of multiple types).
18
+ *
19
+ * Both cases are loaded via `loadFromUnionTargetPolymorphicPivotTable`, which buckets pivot rows
20
+ * by discriminator and hydrates each target class separately.
21
+ */
22
+ static isUnionTargetPolymorphic(prop) {
23
+ return !!prop.polymorphic && (prop.polymorphTargets?.length ?? 0) > 1;
24
+ }
10
25
  /**
11
26
  * Finds the discriminator value (key) for a given entity class in a discriminator map.
12
27
  * Walks up the prototype chain so TPT subclasses resolve to their root's key.
package/utils/Utils.js CHANGED
@@ -141,7 +141,7 @@ export function parseJsonSafe(value) {
141
141
  /** Collection of general-purpose utility methods used throughout the ORM. */
142
142
  export class Utils {
143
143
  static PK_SEPARATOR = '~~~';
144
- static #ORM_VERSION = '7.1.0-dev.3';
144
+ static #ORM_VERSION = '7.1.0-dev.30';
145
145
  /**
146
146
  * Checks if the argument is instance of `Object`. Returns false for arrays.
147
147
  */
@@ -13,7 +13,9 @@ export interface FsUtils {
13
13
  normalizePath(...parts: string[]): string;
14
14
  relativePath(path: string, relativeTo: string): string;
15
15
  absolutePath(path: string, baseDir?: string): string;
16
+ readFile(path: string): Promise<string>;
16
17
  writeFile(path: string, data: string, options?: Record<string, any>): Promise<void>;
18
+ unlink(path: string): Promise<void>;
17
19
  dynamicImport<T = any>(id: string): Promise<T>;
18
20
  }
19
21
  export declare const fs: FsUtils;
package/utils/fs-utils.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { existsSync, globSync as nodeGlobSync, mkdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
2
- import { writeFile as nodeWriteFile } from 'node:fs/promises';
2
+ import { readFile as nodeReadFile, unlink as nodeUnlink, writeFile as nodeWriteFile } from 'node:fs/promises';
3
3
  import { isAbsolute, join, normalize, relative } from 'node:path';
4
4
  import { fileURLToPath, pathToFileURL } from 'node:url';
5
5
  import { Utils } from './Utils.js';
@@ -181,9 +181,15 @@ export const fs = {
181
181
  }
182
182
  return this.normalizePath(path);
183
183
  },
184
+ async readFile(path) {
185
+ return nodeReadFile(path, 'utf-8');
186
+ },
184
187
  async writeFile(path, data, options) {
185
188
  await nodeWriteFile(path, data, options);
186
189
  },
190
+ async unlink(path) {
191
+ await nodeUnlink(path);
192
+ },
187
193
  async dynamicImport(id) {
188
194
  /* v8 ignore next */
189
195
  const specifier = id.startsWith('file://') ? id : pathToFileURL(id).href;
package/utils/index.d.ts CHANGED
@@ -10,3 +10,4 @@ export * from './EntityComparator.js';
10
10
  export * from './RawQueryFragment.js';
11
11
  export * from './env-vars.js';
12
12
  export * from './upsert-utils.js';
13
+ export * from './partition-utils.js';