@mikro-orm/core 7.1.0-dev.5 → 7.1.0-dev.50
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/EntityManager.d.ts +87 -12
- package/EntityManager.js +252 -40
- package/README.md +2 -1
- package/connections/Connection.d.ts +67 -1
- package/connections/Connection.js +64 -0
- package/drivers/IDatabaseDriver.d.ts +45 -7
- package/entity/BaseEntity.d.ts +68 -1
- package/entity/BaseEntity.js +18 -0
- package/entity/Collection.d.ts +6 -3
- package/entity/Collection.js +15 -4
- package/entity/EntityAssigner.js +8 -0
- package/entity/EntityFactory.js +5 -3
- package/entity/EntityLoader.d.ts +8 -1
- package/entity/EntityLoader.js +103 -30
- package/entity/EntityRepository.d.ts +27 -9
- package/entity/EntityRepository.js +12 -0
- package/entity/Reference.d.ts +42 -1
- package/entity/Reference.js +9 -0
- package/entity/defineEntity.d.ts +101 -21
- package/entity/defineEntity.js +17 -6
- package/entity/utils.js +4 -5
- package/enums.d.ts +8 -1
- package/errors.d.ts +2 -0
- package/errors.js +4 -0
- package/index.d.ts +2 -2
- package/index.js +1 -1
- package/metadata/EntitySchema.js +18 -0
- package/metadata/MetadataDiscovery.d.ts +12 -0
- package/metadata/MetadataDiscovery.js +173 -22
- package/metadata/MetadataValidator.d.ts +26 -0
- package/metadata/MetadataValidator.js +237 -1
- package/metadata/Routine.d.ts +74 -0
- package/metadata/Routine.js +140 -0
- package/metadata/index.d.ts +1 -0
- package/metadata/index.js +1 -0
- package/metadata/types.d.ts +78 -9
- package/naming-strategy/AbstractNamingStrategy.d.ts +1 -1
- package/naming-strategy/NamingStrategy.d.ts +1 -1
- package/package.json +1 -1
- package/platforms/Platform.d.ts +18 -3
- package/platforms/Platform.js +58 -6
- package/serialization/EntitySerializer.js +4 -3
- package/typings.d.ts +399 -22
- package/typings.js +43 -12
- package/unit-of-work/UnitOfWork.js +43 -5
- package/utils/AbstractMigrator.d.ts +20 -5
- package/utils/AbstractMigrator.js +263 -28
- package/utils/AbstractSchemaGenerator.d.ts +1 -1
- package/utils/AbstractSchemaGenerator.js +4 -1
- package/utils/Configuration.d.ts +30 -0
- package/utils/Configuration.js +45 -0
- package/utils/DataloaderUtils.d.ts +10 -1
- package/utils/DataloaderUtils.js +78 -0
- package/utils/EntityComparator.js +1 -1
- package/utils/QueryHelper.d.ts +16 -0
- package/utils/QueryHelper.js +15 -0
- package/utils/TransactionManager.js +2 -0
- package/utils/Utils.js +1 -1
- package/utils/fs-utils.d.ts +2 -0
- package/utils/fs-utils.js +7 -1
- package/utils/index.d.ts +1 -0
- package/utils/index.js +1 -0
- package/utils/partition-utils.d.ts +17 -0
- package/utils/partition-utils.js +79 -0
- package/utils/upsert-utils.d.ts +2 -0
- package/utils/upsert-utils.js +26 -1
package/typings.js
CHANGED
|
@@ -21,6 +21,16 @@ export const Config = Symbol('Config');
|
|
|
21
21
|
/** Symbol used to declare the entity name as a string literal type (used by `defineEntity`). */
|
|
22
22
|
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
|
23
23
|
export const EntityName = Symbol('EntityName');
|
|
24
|
+
/**
|
|
25
|
+
* Symbol used to declare index-to-column mappings on an entity type.
|
|
26
|
+
* For decorator entities, declare as a phantom property:
|
|
27
|
+
* ```typescript
|
|
28
|
+
* [IndexHints]?: { idx_email: 'email'; idx_name_age: 'name' | 'age' };
|
|
29
|
+
* ```
|
|
30
|
+
* For `defineEntity` entities, index hints are inferred automatically from
|
|
31
|
+
* named indexes (property-level `.index('name')` and entity-level `indexes`/`uniques`).
|
|
32
|
+
*/
|
|
33
|
+
export const IndexHints = Symbol('IndexHints');
|
|
24
34
|
/**
|
|
25
35
|
* Runtime metadata for an entity, holding its properties, relations, indexes, hooks, and more.
|
|
26
36
|
* Created during metadata discovery and used throughout the ORM lifecycle.
|
|
@@ -38,6 +48,7 @@ export class EntityMetadata {
|
|
|
38
48
|
this.indexes = [];
|
|
39
49
|
this.uniques = [];
|
|
40
50
|
this.checks = [];
|
|
51
|
+
this.triggers = [];
|
|
41
52
|
this.referencingProperties = [];
|
|
42
53
|
this.concurrencyCheckKeys = new Set();
|
|
43
54
|
Object.assign(this, meta);
|
|
@@ -122,16 +133,37 @@ export class EntityMetadata {
|
|
|
122
133
|
/**
|
|
123
134
|
* Creates a column mapping for schema callbacks (indexes, checks, generated columns).
|
|
124
135
|
* For TPT entities, only includes properties that belong to the current table (ownProps).
|
|
136
|
+
* Embedded properties expose their sub-columns via nested access (e.g. `cols.address.city`),
|
|
137
|
+
* while still coercing to the embedded column prefix when used as a string (GH #7712).
|
|
125
138
|
*/
|
|
126
139
|
createSchemaColumnMappingObject() {
|
|
127
140
|
// For TPT entities, only include properties that belong to this entity's table
|
|
128
141
|
const props = this.inheritanceType === 'tpt' && this.ownProps ? this.ownProps : Object.values(this.properties);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
142
|
+
const result = {};
|
|
143
|
+
for (const prop of props) {
|
|
144
|
+
if (!prop.fieldNames) {
|
|
145
|
+
continue;
|
|
132
146
|
}
|
|
133
|
-
|
|
134
|
-
|
|
147
|
+
if (prop.kind === ReferenceKind.EMBEDDED && prop.embeddedProps) {
|
|
148
|
+
result[prop.name] = EntityMetadata.buildEmbeddedColumnMapping(prop);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
result[prop.name] = prop.fieldNames[0];
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
/** @internal Recursively builds a column mapping for an embedded property's sub-columns. */
|
|
156
|
+
static buildEmbeddedColumnMapping(embeddedProp) {
|
|
157
|
+
const result = {};
|
|
158
|
+
for (const [name, sub] of Object.entries(embeddedProp.embeddedProps)) {
|
|
159
|
+
result[name] =
|
|
160
|
+
sub.kind === ReferenceKind.EMBEDDED && sub.embeddedProps
|
|
161
|
+
? EntityMetadata.buildEmbeddedColumnMapping(sub)
|
|
162
|
+
: sub.fieldNames[0];
|
|
163
|
+
}
|
|
164
|
+
const prefix = embeddedProp.fieldNames[0];
|
|
165
|
+
Object.defineProperty(result, 'toString', { value: () => prefix, enumerable: false });
|
|
166
|
+
return result;
|
|
135
167
|
}
|
|
136
168
|
get tableName() {
|
|
137
169
|
return this.collection;
|
|
@@ -155,7 +187,7 @@ export class EntityMetadata {
|
|
|
155
187
|
if (prop.inherited || (prop.persist === false && prop.userDefined !== false)) {
|
|
156
188
|
return false;
|
|
157
189
|
}
|
|
158
|
-
return prop.kind === ReferenceKind.SCALAR && ['string', 'number', 'boolean', 'Date'].includes(prop.type);
|
|
190
|
+
return (prop.kind === ReferenceKind.SCALAR && !prop.array && ['string', 'number', 'boolean', 'Date'].includes(prop.type));
|
|
159
191
|
});
|
|
160
192
|
this.hydrateProps = this.props.filter(prop => {
|
|
161
193
|
// `prop.userDefined` is either `undefined` or `false`
|
|
@@ -182,9 +214,10 @@ export class EntityMetadata {
|
|
|
182
214
|
// Virtual entities evaluate expressions at query time, view entities create actual database views.
|
|
183
215
|
this.virtual = !!this.expression && !this.view;
|
|
184
216
|
if (config) {
|
|
217
|
+
const platform = config.getPlatform();
|
|
185
218
|
for (const prop of this.props) {
|
|
186
219
|
if (prop.enum && !prop.nativeEnumName && prop.items?.every(item => typeof item === 'string')) {
|
|
187
|
-
const name =
|
|
220
|
+
const name = platform.getIndexName(this.tableName, prop.fieldNames, 'check');
|
|
188
221
|
const exists = this.checks.findIndex(check => check.name === name);
|
|
189
222
|
if (exists !== -1) {
|
|
190
223
|
this.checks.splice(exists, 1);
|
|
@@ -192,9 +225,7 @@ export class EntityMetadata {
|
|
|
192
225
|
this.checks.push({
|
|
193
226
|
name,
|
|
194
227
|
property: prop.name,
|
|
195
|
-
expression:
|
|
196
|
-
.getPlatform()
|
|
197
|
-
.getEnumCheckConstraintExpression(prop.fieldNames[0], prop.items),
|
|
228
|
+
expression: platform.getEnumCheckConstraintExpression(prop.fieldNames[0], prop.items),
|
|
198
229
|
});
|
|
199
230
|
}
|
|
200
231
|
}
|
|
@@ -250,8 +281,8 @@ export class EntityMetadata {
|
|
|
250
281
|
}, { __gettersDefined: { value: true, enumerable: false } });
|
|
251
282
|
}
|
|
252
283
|
initIndexes(prop) {
|
|
253
|
-
const simpleIndex = this.indexes.find(index => index.properties === prop.name && !index.options && !index.type && !index.expression);
|
|
254
|
-
const simpleUnique = this.uniques.find(index => index.properties === prop.name && !index.options);
|
|
284
|
+
const simpleIndex = this.indexes.find(index => index.properties === prop.name && !index.options && !index.type && !index.expression && index.where == null);
|
|
285
|
+
const simpleUnique = this.uniques.find(index => index.properties === prop.name && !index.options && !index.expression && index.where == null);
|
|
255
286
|
const owner = prop.kind === ReferenceKind.MANY_TO_ONE;
|
|
256
287
|
if (!prop.index && simpleIndex) {
|
|
257
288
|
Utils.defaultValue(simpleIndex, 'name', true);
|
|
@@ -36,6 +36,13 @@ export class UnitOfWork {
|
|
|
36
36
|
#flushQueue = [];
|
|
37
37
|
#working = false;
|
|
38
38
|
#em;
|
|
39
|
+
/**
|
|
40
|
+
* UoW lives in `@mikro-orm/core` alongside `EntityManager` and reaches `getAbortOptions`
|
|
41
|
+
* (TS-protected) via cast — TS has no `friend` keyword, so this is the documented escape hatch.
|
|
42
|
+
*/
|
|
43
|
+
get #abortOptions() {
|
|
44
|
+
return this.#em.getAbortOptions();
|
|
45
|
+
}
|
|
39
46
|
constructor(em) {
|
|
40
47
|
this.#em = em;
|
|
41
48
|
this.#metadata = this.#em.getMetadata();
|
|
@@ -371,16 +378,43 @@ export class UnitOfWork {
|
|
|
371
378
|
this.#identityMap.delete(entity);
|
|
372
379
|
}
|
|
373
380
|
// remove from referencing relations that are nullable
|
|
374
|
-
|
|
381
|
+
const entityMeta = helper(entity).__meta;
|
|
382
|
+
for (const prop of entityMeta.bidirectionalRelations) {
|
|
375
383
|
const inverseProp = prop.mappedBy || prop.inversedBy;
|
|
376
384
|
const relation = Reference.unwrapReference(entity[prop.name]);
|
|
377
385
|
const prop2 = prop.targetMeta.properties[inverseProp];
|
|
386
|
+
// skip relations that this entity's class can't actually participate in.
|
|
387
|
+
// happens with STI: a child meta picks up a sibling's inverse-side relation via the root,
|
|
388
|
+
// but the owning side targets the sibling — no collection can reference this entity.
|
|
389
|
+
const inverseTarget = prop2.targetMeta;
|
|
390
|
+
if (inverseTarget.abstract
|
|
391
|
+
? inverseTarget.root.class !== entityMeta.root.class
|
|
392
|
+
: inverseTarget.class !== entityMeta.class) {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
378
395
|
if (prop.kind === ReferenceKind.ONE_TO_MANY && prop2.nullable && Utils.isCollection(relation)) {
|
|
379
396
|
for (const item of relation.getItems(false)) {
|
|
380
397
|
delete item[inverseProp];
|
|
381
398
|
}
|
|
382
399
|
continue;
|
|
383
400
|
}
|
|
401
|
+
// inlined pivot M:N: scrub the deleted entity from each owning-side document's array;
|
|
402
|
+
// the owner's collection update rides along inside its UPDATE changeset in the same flush
|
|
403
|
+
if (prop.kind === ReferenceKind.MANY_TO_MANY &&
|
|
404
|
+
prop.mappedBy &&
|
|
405
|
+
!this.#platform.usesPivotTable() &&
|
|
406
|
+
Utils.isCollection(relation)) {
|
|
407
|
+
if (!relation.isInitialized(true)) {
|
|
408
|
+
throw ValidationError.cannotModifyInverseCollection(entity, prop);
|
|
409
|
+
}
|
|
410
|
+
for (const owner of relation.getItems(false)) {
|
|
411
|
+
const ownerColl = owner[inverseProp];
|
|
412
|
+
if (Utils.isCollection(ownerColl)) {
|
|
413
|
+
ownerColl.removeWithoutPropagation(entity);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
384
418
|
const target = relation?.[inverseProp];
|
|
385
419
|
if (relation && Utils.isCollection(target)) {
|
|
386
420
|
target.removeWithoutPropagation(entity);
|
|
@@ -463,7 +497,7 @@ export class UnitOfWork {
|
|
|
463
497
|
if (options.lockMode === LockMode.OPTIMISTIC) {
|
|
464
498
|
await this.lockOptimistic(entity, meta, options.lockVersion);
|
|
465
499
|
}
|
|
466
|
-
else if (options.lockMode != null) {
|
|
500
|
+
else if (options.lockMode != null && options.lockMode !== LockMode.NONE) {
|
|
467
501
|
await this.lockPessimistic(entity, options);
|
|
468
502
|
}
|
|
469
503
|
}
|
|
@@ -474,6 +508,9 @@ export class UnitOfWork {
|
|
|
474
508
|
}
|
|
475
509
|
unsetIdentity(entity) {
|
|
476
510
|
this.#identityMap.delete(entity);
|
|
511
|
+
this.#persistStack.delete(entity);
|
|
512
|
+
this.#removeStack.delete(entity);
|
|
513
|
+
this.#orphanRemoveStack.delete(entity);
|
|
477
514
|
const wrapped = helper(entity);
|
|
478
515
|
const serializedPK = wrapped.getSerializedPrimaryKey();
|
|
479
516
|
// remove references of this entity in all managed entities, otherwise flushing could reinsert the entity
|
|
@@ -999,7 +1036,7 @@ export class UnitOfWork {
|
|
|
999
1036
|
this.findExtraUpdates(changeSet, props);
|
|
1000
1037
|
await this.runHooks(EventType.beforeCreate, changeSet, true);
|
|
1001
1038
|
}
|
|
1002
|
-
await this.#changeSetPersister.executeInserts(changeSets, { ctx });
|
|
1039
|
+
await this.#changeSetPersister.executeInserts(changeSets, { ctx, ...this.#abortOptions });
|
|
1003
1040
|
for (const changeSet of changeSets) {
|
|
1004
1041
|
// For TPT entities, use the full entity snapshot instead of the partial changeset payload,
|
|
1005
1042
|
// since each table's changeset only contains its own properties. Without this, the snapshot
|
|
@@ -1068,7 +1105,7 @@ export class UnitOfWork {
|
|
|
1068
1105
|
for (const changeSet of changeSets) {
|
|
1069
1106
|
await this.runHooks(EventType.beforeUpdate, changeSet, true);
|
|
1070
1107
|
}
|
|
1071
|
-
await this.#changeSetPersister.executeUpdates(changeSets, batched, { ctx });
|
|
1108
|
+
await this.#changeSetPersister.executeUpdates(changeSets, batched, { ctx, ...this.#abortOptions });
|
|
1072
1109
|
for (const changeSet of changeSets) {
|
|
1073
1110
|
const wrapped = helper(changeSet.entity);
|
|
1074
1111
|
wrapped.__originalEntityData = this.#comparator.prepareEntity(changeSet.entity);
|
|
@@ -1091,7 +1128,7 @@ export class UnitOfWork {
|
|
|
1091
1128
|
for (const changeSet of changeSets) {
|
|
1092
1129
|
await this.runHooks(EventType.beforeDelete, changeSet, true);
|
|
1093
1130
|
}
|
|
1094
|
-
await this.#changeSetPersister.executeDeletes(changeSets, { ctx });
|
|
1131
|
+
await this.#changeSetPersister.executeDeletes(changeSets, { ctx, ...this.#abortOptions });
|
|
1095
1132
|
for (const changeSet of changeSets) {
|
|
1096
1133
|
this.unsetIdentity(changeSet.entity);
|
|
1097
1134
|
await this.runHooks(EventType.afterDelete, changeSet);
|
|
@@ -1129,6 +1166,7 @@ export class UnitOfWork {
|
|
|
1129
1166
|
ctx,
|
|
1130
1167
|
schema: this.#em.schema,
|
|
1131
1168
|
loggerContext,
|
|
1169
|
+
...this.#abortOptions,
|
|
1132
1170
|
});
|
|
1133
1171
|
for (const coll of this.#collectionUpdates) {
|
|
1134
1172
|
coll.takeSnapshot();
|
|
@@ -1,18 +1,19 @@
|
|
|
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';
|
|
5
5
|
interface RunnableMigration {
|
|
6
6
|
name: string;
|
|
7
7
|
path?: string;
|
|
8
|
-
up: () => MaybePromise<void>;
|
|
9
|
-
down: () => MaybePromise<void>;
|
|
8
|
+
up: (afterRun?: (tx?: Transaction) => Promise<void>) => MaybePromise<void>;
|
|
9
|
+
down: (afterRun?: (tx?: Transaction) => Promise<void>) => MaybePromise<void>;
|
|
10
10
|
}
|
|
11
11
|
type MigrateOptions = {
|
|
12
12
|
from?: string | number;
|
|
13
13
|
to?: string | number;
|
|
14
14
|
migrations?: string[];
|
|
15
15
|
transaction?: Transaction;
|
|
16
|
+
schema?: string;
|
|
16
17
|
};
|
|
17
18
|
export declare abstract class AbstractMigrator<D extends IDatabaseDriver> implements IMigrator {
|
|
18
19
|
#private;
|
|
@@ -57,11 +58,15 @@ export declare abstract class AbstractMigrator<D extends IDatabaseDriver> implem
|
|
|
57
58
|
/**
|
|
58
59
|
* @inheritDoc
|
|
59
60
|
*/
|
|
60
|
-
getExecuted(
|
|
61
|
+
getExecuted(options?: {
|
|
62
|
+
schema?: string;
|
|
63
|
+
}): Promise<MigrationRow[]>;
|
|
61
64
|
/**
|
|
62
65
|
* @inheritDoc
|
|
63
66
|
*/
|
|
64
|
-
getPending(
|
|
67
|
+
getPending(options?: {
|
|
68
|
+
schema?: string;
|
|
69
|
+
}): Promise<MigrationInfo[]>;
|
|
65
70
|
/**
|
|
66
71
|
* @inheritDoc
|
|
67
72
|
*/
|
|
@@ -70,6 +75,16 @@ export declare abstract class AbstractMigrator<D extends IDatabaseDriver> implem
|
|
|
70
75
|
* @inheritDoc
|
|
71
76
|
*/
|
|
72
77
|
down(options?: string | string[] | Omit<MigrateOptions, 'from'>): Promise<MigrationInfo[]>;
|
|
78
|
+
/**
|
|
79
|
+
* @inheritDoc
|
|
80
|
+
*/
|
|
81
|
+
rollup(migrations?: string[]): Promise<MigrationResult>;
|
|
82
|
+
/**
|
|
83
|
+
* Extracts the body of a method from migration source code using brace counting.
|
|
84
|
+
* Returns the raw lines between the opening and closing braces, or empty string if not found.
|
|
85
|
+
* @internal
|
|
86
|
+
*/
|
|
87
|
+
private extractMethodBody;
|
|
73
88
|
abstract getStorage(): IMigratorStorage;
|
|
74
89
|
/**
|
|
75
90
|
* @inheritDoc
|
|
@@ -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
|
-
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
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:
|
|
128
|
-
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:
|
|
136
|
-
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
311
|
-
|
|
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
|
-
|
|
314
|
-
|
|
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,
|
|
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
|
});
|