@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.
- package/entity/Collection.d.ts +2 -2
- package/entity/EntityLoader.js +6 -1
- package/metadata/MetadataDiscovery.d.ts +10 -0
- package/metadata/MetadataDiscovery.js +72 -8
- package/metadata/MetadataValidator.js +9 -0
- package/metadata/types.d.ts +1 -1
- package/package.json +1 -1
- package/typings.d.ts +5 -0
- package/utils/AbstractMigrator.d.ts +11 -1
- package/utils/AbstractMigrator.js +203 -0
- package/utils/QueryHelper.d.ts +16 -0
- package/utils/QueryHelper.js +15 -0
- package/utils/Utils.js +1 -1
- package/utils/fs-utils.d.ts +2 -0
- package/utils/fs-utils.js +7 -1
package/entity/Collection.d.ts
CHANGED
|
@@ -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: (
|
|
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:
|
|
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. */
|
package/entity/EntityLoader.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
486
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
725
|
-
if (prop.
|
|
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
|
-
|
|
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) {
|
package/metadata/types.d.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
*/
|
package/utils/QueryHelper.d.ts
CHANGED
|
@@ -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.
|
package/utils/QueryHelper.js
CHANGED
|
@@ -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.
|
|
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
|
*/
|
package/utils/fs-utils.d.ts
CHANGED
|
@@ -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;
|