@mikro-orm/core 7.1.0-dev.6 → 7.1.0-dev.7

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.
@@ -48,14 +48,14 @@ export declare class Collection<T extends object, O extends object = object> {
48
48
  /** Serializes the collection items to plain JSON objects. Returns an empty array if not initialized. */
49
49
  toJSON<TT extends T>(): EntityDTO<TT>[];
50
50
  /** Adds one or more items to the collection, propagating the change to the inverse side. Returns the number of items added. */
51
- add<TT extends T>(entity: TT | Reference<TT> | Iterable<TT | Reference<TT>>, ...entities: (TT | Reference<TT>)[]): number;
51
+ add<TT extends T>(entity: TT | Reference<TT> | Iterable<TT | Reference<TT>>, ...entities: (T | Reference<T>)[]): number;
52
52
  /**
53
53
  * Remove specified item(s) from the collection. Note that removing item from collection does not necessarily imply deleting the target entity,
54
54
  * it means we are disconnecting the relation - removing items from collection, not removing entities from database - `Collection.remove()`
55
55
  * is not the same as `em.remove()`. If we want to delete the entity by removing it from collection, we need to enable `orphanRemoval: true`,
56
56
  * which tells the ORM we don't want orphaned entities to exist, so we know those should be removed.
57
57
  */
58
- remove<TT extends T>(entity: TT | Reference<TT> | Iterable<TT | Reference<TT>> | ((item: TT) => boolean), ...entities: (TT | Reference<TT>)[]): number;
58
+ remove<TT extends T>(entity: TT | Reference<TT> | Iterable<TT | Reference<TT>> | ((item: T) => boolean), ...entities: (T | Reference<T>)[]): number;
59
59
  /** Checks whether the collection contains the given item. */
60
60
  contains<TT extends T>(item: TT | Reference<TT>, check?: boolean): boolean;
61
61
  /** Returns the number of items in the collection. Throws if the collection is not initialized. */
@@ -587,6 +587,7 @@ export class EntityLoader {
587
587
  }
588
588
  const map = await this.#driver.loadFromPivotTable(prop, ids, where, orderBy, this.#em.getTransactionContext(), options2, pivotJoin);
589
589
  const children = [];
590
+ const isUnionTargetMN = QueryHelper.isUnionTargetPolymorphic(prop);
590
591
  for (let i = 0; i < filtered.length; i++) {
591
592
  const entity = filtered[i];
592
593
  const items = map[Utils.getPrimaryKeyHash(ids[i])].map(item => {
@@ -596,7 +597,11 @@ export class EntityLoader {
596
597
  schema: options.schema ?? this.#em.config.get('schema'),
597
598
  });
598
599
  }
599
- const entity = this.#em.getEntityFactory().create(prop.targetMeta.class, item, {
600
+ // Union-target items carry their concrete class via `constructor` — dispatch to the right factory call.
601
+ const targetClass = isUnionTargetMN && item.constructor !== Object
602
+ ? item.constructor
603
+ : prop.targetMeta.class;
604
+ const entity = this.#em.getEntityFactory().create(targetClass, item, {
600
605
  refresh,
601
606
  merge: true,
602
607
  convertCustomTypes: true,
@@ -54,6 +54,16 @@ export declare class MetadataDiscovery {
54
54
  * Define properties for a polymorphic pivot table.
55
55
  */
56
56
  private definePolymorphicPivotProperties;
57
+ /**
58
+ * Mirror of definePolymorphicPivotProperties for union-target M:N
59
+ * (e.g. Post.attachments -> Image | Video via shared pivot with a target-side discriminator).
60
+ *
61
+ * Pivot shape:
62
+ * (owner_fk..., discriminator_column, target_fk...)
63
+ * - owner side is a normal M:1 to the single owner entity
64
+ * - target side is a discriminator column + per-target-type virtual M:1 relations
65
+ */
66
+ private defineUnionTargetPolymorphicPivotProperties;
57
67
  /**
58
68
  * Create a virtual M:1 relation from pivot to a polymorphic owner entity.
59
69
  * This enables single-query join loading for inverse-side polymorphic M:N.
@@ -479,17 +479,28 @@ export class MetadataDiscovery {
479
479
  prop.polymorphic = prop2.polymorphic;
480
480
  prop.discriminator = prop2.discriminator;
481
481
  prop.discriminatorColumn = prop2.discriminatorColumn;
482
- prop.discriminatorValue = prop2.discriminatorValue;
482
+ // For a union-target pivot each inverse side sits on one specific target class, so its
483
+ // discriminator value is that class's tableName. For Rails-style, prop2 has a single fixed value.
484
+ prop.discriminatorValue = QueryHelper.isUnionTargetPolymorphic(prop2) ? meta.tableName : prop2.discriminatorValue;
483
485
  }
484
486
  prop.referencedColumnNames ??= Utils.flatten(meta.primaryKeys.map(primaryKey => meta.properties[primaryKey].fieldNames));
485
- // For polymorphic M:N, use discriminator base name for FK column (e.g., taggable_id instead of post_id)
486
- if (prop.polymorphic && prop.discriminator) {
487
+ // Union-target polymorphic M:N: owner side is fixed (real FK), target side uses discriminator-derived names.
488
+ const isUnionTargetMN = QueryHelper.isUnionTargetPolymorphic(prop);
489
+ if (prop.polymorphic && prop.discriminator && !isUnionTargetMN) {
490
+ // Rails-style: owner side is polymorphic, uses discriminator base name (e.g. taggable_id instead of post_id)
487
491
  prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.#namingStrategy.joinKeyColumnName(prop.discriminator, referencedColumnName, prop.referencedColumnNames.length > 1));
488
492
  }
489
493
  else {
490
494
  prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.#namingStrategy.joinKeyColumnName(meta.root.className, referencedColumnName, meta.compositePK));
491
495
  }
492
- prop.inverseJoinColumns ??= this.initManyToOneFieldName(prop, meta2.root.className);
496
+ if (isUnionTargetMN) {
497
+ // Target side uses discriminator base name (e.g. attachable_id — shared across Image/Video)
498
+ const targetPkCols = Utils.flatten(meta2.primaryKeys.map(pk => meta2.properties[pk].fieldNames));
499
+ prop.inverseJoinColumns ??= targetPkCols.map(fieldName => this.#namingStrategy.joinKeyColumnName(prop.discriminator, fieldName, targetPkCols.length > 1));
500
+ }
501
+ else {
502
+ prop.inverseJoinColumns ??= this.initManyToOneFieldName(prop, meta2.root.className);
503
+ }
493
504
  }
494
505
  isExplicitTableName(meta) {
495
506
  return meta.tableName !== this.#namingStrategy.classToTableName(meta.className);
@@ -583,6 +594,24 @@ export class MetadataDiscovery {
583
594
  if (prop.inversedBy) {
584
595
  prop.targetMeta.properties[prop.inversedBy].pivotEntity = pivotMeta.class;
585
596
  }
597
+ // Propagate pivotEntity to ALL inverse collections using mappedBy pointing at this
598
+ // owner prop. Covers three cases:
599
+ // - regular inverse (Tag.posts mappedBy Post.tags) — handled by inversedBy above
600
+ // - union-target inverse (Image.posts mappedBy Post.attachments) — on each polymorph target
601
+ // - merged inverse (Tag.owners mappedBy [Post,Video].tags) — union collection on the target
602
+ const inverseCandidates = QueryHelper.isUnionTargetPolymorphic(prop)
603
+ ? prop.polymorphTargets
604
+ : [prop.targetMeta];
605
+ for (const targetMeta of inverseCandidates) {
606
+ for (const inverseProp of Object.values(targetMeta.properties)) {
607
+ if (inverseProp.kind === ReferenceKind.MANY_TO_MANY &&
608
+ inverseProp.mappedBy === prop.name &&
609
+ !inverseProp.pivotEntity) {
610
+ inverseProp.pivotEntity = pivotMeta.class;
611
+ inverseProp.pivotTable = pivotMeta.tableName;
612
+ }
613
+ }
614
+ }
586
615
  return pivotMeta;
587
616
  });
588
617
  }
@@ -721,8 +750,12 @@ export class MetadataDiscovery {
721
750
  }
722
751
  }
723
752
  }
724
- // For polymorphic M:N, create discriminator column and polymorphic FK
725
- if (prop.polymorphic && prop.discriminatorColumn) {
753
+ // Union-target polymorphic M:N: discriminator + target FK share the pivot across multiple target types
754
+ if (prop.discriminatorColumn && QueryHelper.isUnionTargetPolymorphic(prop)) {
755
+ this.defineUnionTargetPolymorphicPivotProperties(pivotMeta2, meta, prop);
756
+ }
757
+ else if (prop.polymorphic && prop.discriminatorColumn) {
758
+ // Rails-style polymorphic M:N: multiple owners share the pivot, single target type
726
759
  this.definePolymorphicPivotProperties(pivotMeta2, meta, prop, targetMeta);
727
760
  }
728
761
  else {
@@ -809,6 +842,33 @@ export class MetadataDiscovery {
809
842
  pivotMeta.polymorphicDiscriminatorMap ??= {};
810
843
  pivotMeta.polymorphicDiscriminatorMap[prop.discriminatorValue] = meta.class;
811
844
  }
845
+ /**
846
+ * Mirror of definePolymorphicPivotProperties for union-target M:N
847
+ * (e.g. Post.attachments -> Image | Video via shared pivot with a target-side discriminator).
848
+ *
849
+ * Pivot shape:
850
+ * (owner_fk..., discriminator_column, target_fk...)
851
+ * - owner side is a normal M:1 to the single owner entity
852
+ * - target side is a discriminator column + per-target-type virtual M:1 relations
853
+ */
854
+ defineUnionTargetPolymorphicPivotProperties(pivotMeta, meta, prop) {
855
+ const discriminatorColumn = prop.discriminatorColumn;
856
+ const targets = prop.polymorphTargets;
857
+ pivotMeta.properties[meta.name + '_owner'] = this.definePivotProperty(prop, meta.name + '_owner', meta.class, prop.discriminator, true, false);
858
+ const discriminatorProp = this.createPivotScalarProperty(discriminatorColumn, [this.#platform.getVarcharTypeDeclarationSQL(prop)], [discriminatorColumn], { type: 'string', primary: true, nullable: false });
859
+ this.initFieldName(discriminatorProp);
860
+ pivotMeta.properties[discriminatorColumn] = discriminatorProp;
861
+ const firstTargetColumnTypes = this.getPrimaryKeyColumnTypes(targets[0]);
862
+ pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, firstTargetColumnTypes, [...prop.inverseJoinColumns], { type: targets[0].className, primary: true, nullable: false });
863
+ pivotMeta.polymorphicDiscriminatorMap ??= {};
864
+ for (const targetMeta of targets) {
865
+ const relationName = `${prop.discriminator}_${targetMeta.tableName}`;
866
+ const relation = this.definePolymorphicOwnerRelation(prop, relationName, targetMeta);
867
+ relation.joinColumns = relation.fieldNames = relation.ownColumns = [...prop.inverseJoinColumns];
868
+ pivotMeta.properties[relationName] = relation;
869
+ pivotMeta.polymorphicDiscriminatorMap[targetMeta.tableName] = targetMeta.class;
870
+ }
871
+ }
812
872
  /**
813
873
  * Create a virtual M:1 relation from pivot to a polymorphic owner entity.
814
874
  * This enables single-query join loading for inverse-side polymorphic M:N.
@@ -1045,11 +1105,15 @@ export class MetadataDiscovery {
1045
1105
  prop.discriminatorColumn ??= this.#namingStrategy.discriminatorColumnName(prop.discriminator);
1046
1106
  prop.createForeignKeyConstraint = false;
1047
1107
  const isToOne = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind);
1048
- if (isToOne) {
1108
+ const isUnionTargetMN = prop.kind === ReferenceKind.MANY_TO_MANY && Array.isArray(prop.target);
1109
+ if (isToOne || isUnionTargetMN) {
1049
1110
  const types = prop.type.split(/ ?\| ?/);
1050
1111
  prop.polymorphTargets = discovered.filter(m => types.includes(m.className) && !m.embeddable);
1051
1112
  prop.targetMeta = prop.polymorphTargets[0];
1052
1113
  prop.referencedPKs = prop.targetMeta?.primaryKeys;
1114
+ if (isUnionTargetMN && prop.polymorphTargets.length < 2) {
1115
+ throw new MetadataError(`${meta.className}.${prop.name} union-target polymorphic M:N requires at least two target entity types; use a regular M:N relation for a single target.`);
1116
+ }
1053
1117
  }
1054
1118
  if (prop.discriminatorMap) {
1055
1119
  const normalizedMap = {};
@@ -1065,7 +1129,7 @@ export class MetadataDiscovery {
1065
1129
  }
1066
1130
  prop.discriminatorMap = normalizedMap;
1067
1131
  }
1068
- else if (isToOne) {
1132
+ else if (isToOne || isUnionTargetMN) {
1069
1133
  prop.discriminatorMap = {};
1070
1134
  const tableNameToTarget = new Map();
1071
1135
  for (const target of prop.polymorphTargets) {
@@ -171,6 +171,15 @@ export class MetadataValidator {
171
171
  }
172
172
  validatePolymorphicTargets(meta, prop) {
173
173
  const targets = prop.polymorphTargets;
174
+ // Union-target M:N stores one scalar target FK per pivot row, so composite-PK targets
175
+ // can't round-trip through this schema.
176
+ if (prop.kind === ReferenceKind.MANY_TO_MANY && targets.length > 1) {
177
+ for (const target of targets) {
178
+ if (target.compositePK) {
179
+ throw MetadataError.incompatiblePolymorphicTargets(meta, prop, targets[0], target, `${target.className} has a composite primary key; union-target polymorphic M:N does not support composite-PK targets.`);
180
+ }
181
+ }
182
+ }
174
183
  // Validate targetKey exists and is compatible across all targets
175
184
  if (prop.targetKey) {
176
185
  for (const target of targets) {
@@ -340,7 +340,7 @@ export interface PropertyOptions<Owner> {
340
340
  }
341
341
  export interface ReferenceOptions<Owner, Target> extends PropertyOptions<Owner> {
342
342
  /** Set target entity type. For polymorphic relations, pass an array of entity types. */
343
- entity?: () => EntityName<Target> | EntityName<Target>[];
343
+ entity?: () => EntityName<Target> | EntityName[];
344
344
  /** Set what actions on owning entity should be cascaded to the relationship. Defaults to [Cascade.PERSIST, Cascade.MERGE] (see {@doclink cascading}). */
345
345
  cascade?: Cascade[];
346
346
  /** Always load the relationship. Discouraged for use with to-many relations for performance reasons. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/core",
3
- "version": "7.1.0-dev.6",
3
+ "version": "7.1.0-dev.7",
4
4
  "description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
5
5
  "keywords": [
6
6
  "data-mapper",
package/typings.d.ts CHANGED
@@ -1110,6 +1110,11 @@ export interface IMigrator {
1110
1110
  * Executes down migrations to the given point. Without parameter it will migrate one version down.
1111
1111
  */
1112
1112
  down(options?: string | string[] | Omit<MigrateOptions, 'from'>): Promise<MigrationInfo[]>;
1113
+ /**
1114
+ * Combines multiple executed migrations into a single migration file.
1115
+ * Concatenates source code without touching the database schema.
1116
+ */
1117
+ rollup(migrations?: string[]): Promise<MigrationResult>;
1113
1118
  /**
1114
1119
  * Registers event handler.
1115
1120
  */
@@ -1,4 +1,4 @@
1
- import type { Constructor, IMigrationGenerator, IMigrationRunner, IMigrator, IMigratorStorage, MaybePromise, Migration, MigrationInfo, MigrationRow, MigratorEvent } from '../typings.js';
1
+ import type { Constructor, IMigrationGenerator, IMigrationRunner, IMigrator, IMigratorStorage, MaybePromise, Migration, MigrationInfo, MigrationResult, MigrationRow, MigratorEvent } from '../typings.js';
2
2
  import type { Transaction } from '../connections/Connection.js';
3
3
  import type { Configuration, MigrationsOptions } from './Configuration.js';
4
4
  import type { EntityManagerType, IDatabaseDriver } from '../drivers/IDatabaseDriver.js';
@@ -70,6 +70,16 @@ export declare abstract class AbstractMigrator<D extends IDatabaseDriver> implem
70
70
  * @inheritDoc
71
71
  */
72
72
  down(options?: string | string[] | Omit<MigrateOptions, 'from'>): Promise<MigrationInfo[]>;
73
+ /**
74
+ * @inheritDoc
75
+ */
76
+ rollup(migrations?: string[]): Promise<MigrationResult>;
77
+ /**
78
+ * Extracts the body of a method from migration source code using brace counting.
79
+ * Returns the raw lines between the opening and closing braces, or empty string if not found.
80
+ * @internal
81
+ */
82
+ private extractMethodBody;
73
83
  abstract getStorage(): IMigratorStorage;
74
84
  /**
75
85
  * @inheritDoc
@@ -63,6 +63,209 @@ export class AbstractMigrator {
63
63
  async down(options) {
64
64
  return this.runMigrations('down', options);
65
65
  }
66
+ /**
67
+ * @inheritDoc
68
+ */
69
+ async rollup(migrations) {
70
+ await this.init();
71
+ const { fs } = await import('@mikro-orm/core/fs-utils');
72
+ const all = await this.discoverMigrations();
73
+ const executedSet = new Set(await this.storage.executed());
74
+ let toRollup;
75
+ if (migrations && migrations.length > 0) {
76
+ const requested = new Set(migrations.map(m => this.getMigrationFilename(m)));
77
+ toRollup = all.filter(m => requested.has(m.name));
78
+ const found = new Set(toRollup.map(m => m.name));
79
+ const notFound = [...requested].filter(name => !found.has(name));
80
+ if (notFound.length > 0) {
81
+ throw new Error(`Migrations not found: ${notFound.join(', ')}`);
82
+ }
83
+ const notExecuted = toRollup.filter(m => !executedSet.has(m.name));
84
+ if (notExecuted.length > 0) {
85
+ throw new Error(`Cannot roll up migrations that have not been executed: ${notExecuted.map(m => m.name).join(', ')}`);
86
+ }
87
+ }
88
+ else {
89
+ toRollup = all.filter(m => executedSet.has(m.name));
90
+ }
91
+ if (toRollup.length < 2) {
92
+ throw new Error('At least 2 executed migrations are required for rollup');
93
+ }
94
+ const withoutPath = toRollup.filter(m => !m.path);
95
+ if (withoutPath.length > 0) {
96
+ throw new Error(`Cannot roll up migrations without file paths (class-based migrations): ${withoutPath.map(m => m.name).join(', ')}`);
97
+ }
98
+ const upBodies = [];
99
+ const downBodies = [];
100
+ const placeholder = `__mikro_orm_rollup_${Date.now()}__`;
101
+ for (const migration of toRollup) {
102
+ const source = await fs.readFile(migration.path);
103
+ const upBody = this.extractMethodBody(source, 'up');
104
+ const downBody = this.extractMethodBody(source, 'down');
105
+ if (upBody) {
106
+ upBodies.push(` // --- merged from ${migration.name} ---\n${upBody}`);
107
+ }
108
+ if (downBody) {
109
+ downBodies.unshift(` // --- merged from ${migration.name} ---\n${downBody}`);
110
+ }
111
+ }
112
+ const diff = {
113
+ up: [placeholder],
114
+ down: downBodies.length > 0 ? [placeholder] : [],
115
+ };
116
+ const [templateCode, fileName] = await this.generator.generate(diff);
117
+ const placeholderRe = new RegExp(`^.*${placeholder}.*$`, 'm');
118
+ let code = templateCode.replace(placeholderRe, upBodies.join('\n'));
119
+ if (downBodies.length > 0) {
120
+ code = code.replace(placeholderRe, downBodies.join('\n'));
121
+ }
122
+ await fs.writeFile(fs.normalizePath(this.absolutePath, fileName), code, { flush: true });
123
+ const updateStorage = async () => {
124
+ for (const migration of toRollup) {
125
+ await this.storage.unlogMigration({ name: migration.name });
126
+ }
127
+ await this.storage.logMigration({ name: fileName.replace(/\.[jt]s$/, '') });
128
+ };
129
+ if (this.options.transactional) {
130
+ await this.driver.getConnection().transactional(async (trx) => {
131
+ this.storage.setMasterMigration(trx);
132
+ try {
133
+ await updateStorage();
134
+ }
135
+ finally {
136
+ this.storage.unsetMasterMigration();
137
+ }
138
+ });
139
+ }
140
+ else {
141
+ await updateStorage();
142
+ }
143
+ await Promise.all(toRollup.map(migration => fs.unlink(migration.path)));
144
+ return { fileName, code, diff: { up: [], down: [] } };
145
+ }
146
+ /**
147
+ * Extracts the body of a method from migration source code using brace counting.
148
+ * Returns the raw lines between the opening and closing braces, or empty string if not found.
149
+ * @internal
150
+ */
151
+ extractMethodBody(source, methodName) {
152
+ const lines = source.split('\n');
153
+ // match method declarations, not occurrences in comments/strings — require preceding whitespace or keyword
154
+ const methodPattern = new RegExp(`^\\s+(?:override\\s+|async\\s+)*${methodName}\\s*\\(`);
155
+ let methodLine = -1;
156
+ for (let i = 0; i < lines.length; i++) {
157
+ if (methodPattern.test(lines[i])) {
158
+ methodLine = i;
159
+ break;
160
+ }
161
+ }
162
+ if (methodLine === -1) {
163
+ return '';
164
+ }
165
+ let braceCount = 0;
166
+ let bodyStart = -1;
167
+ let bodyEnd = -1;
168
+ let bodyStartCol = -1;
169
+ let bodyEndCol = -1;
170
+ let inBacktick = false;
171
+ let inBlockComment = false;
172
+ // stack tracks brace depth at which each template expression `${...}` was entered
173
+ const templateExprStack = [];
174
+ for (let i = methodLine; i < lines.length; i++) {
175
+ const line = lines[i];
176
+ for (let j = 0; j < line.length; j++) {
177
+ // handle multi-line block comments
178
+ if (inBlockComment) {
179
+ if (line[j] === '*' && j + 1 < line.length && line[j + 1] === '/') {
180
+ inBlockComment = false;
181
+ j++;
182
+ }
183
+ continue;
184
+ }
185
+ // handle multi-line template literals
186
+ if (inBacktick) {
187
+ if (line[j] === '\\') {
188
+ j++;
189
+ }
190
+ else if (line[j] === '`') {
191
+ inBacktick = false;
192
+ }
193
+ else if (line[j] === '$' && j + 1 < line.length && line[j + 1] === '{') {
194
+ // entering template expression — resume brace counting
195
+ templateExprStack.push(braceCount);
196
+ inBacktick = false;
197
+ j++; // skip the {
198
+ braceCount++;
199
+ }
200
+ continue;
201
+ }
202
+ const ch = line[j];
203
+ // single/double quoted strings (single-line only)
204
+ if (ch === "'" || ch === '"') {
205
+ const quote = ch;
206
+ j++;
207
+ while (j < line.length) {
208
+ if (line[j] === '\\') {
209
+ j++;
210
+ }
211
+ else if (line[j] === quote) {
212
+ break;
213
+ }
214
+ j++;
215
+ }
216
+ continue;
217
+ }
218
+ // template literal start
219
+ if (ch === '`') {
220
+ inBacktick = true;
221
+ continue;
222
+ }
223
+ // single-line comment
224
+ if (ch === '/' && j + 1 < line.length && line[j + 1] === '/') {
225
+ break;
226
+ }
227
+ // block comment start
228
+ if (ch === '/' && j + 1 < line.length && line[j + 1] === '*') {
229
+ inBlockComment = true;
230
+ j++;
231
+ continue;
232
+ }
233
+ if (ch === '{') {
234
+ if (braceCount === 0) {
235
+ bodyStart = i;
236
+ bodyStartCol = j + 1;
237
+ }
238
+ braceCount++;
239
+ }
240
+ else if (ch === '}') {
241
+ braceCount--;
242
+ // closing a template expression — re-enter backtick mode
243
+ if (templateExprStack.length > 0 && braceCount === templateExprStack[templateExprStack.length - 1]) {
244
+ templateExprStack.pop();
245
+ inBacktick = true;
246
+ continue;
247
+ }
248
+ if (braceCount === 0) {
249
+ bodyEnd = i;
250
+ bodyEndCol = j;
251
+ break;
252
+ }
253
+ }
254
+ }
255
+ if (bodyEnd !== -1) {
256
+ break;
257
+ }
258
+ }
259
+ if (bodyStart === -1 || bodyEnd === -1) {
260
+ return '';
261
+ }
262
+ // single-line method body: extract content between braces on the same line
263
+ if (bodyStart === bodyEnd) {
264
+ const content = lines[bodyStart].slice(bodyStartCol, bodyEndCol).trim();
265
+ return content ? ` ${content}` : '';
266
+ }
267
+ return lines.slice(bodyStart + 1, bodyEnd).join('\n');
268
+ }
66
269
  /**
67
270
  * @inheritDoc
68
271
  */
@@ -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.6';
144
+ static #ORM_VERSION = '7.1.0-dev.7';
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;