@mikro-orm/core 7.1.0-dev.9 → 7.1.1-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/EntityManager.d.ts +47 -3
- package/EntityManager.js +150 -23
- package/README.md +2 -1
- package/connections/Connection.d.ts +67 -1
- package/connections/Connection.js +64 -0
- package/drivers/IDatabaseDriver.d.ts +20 -6
- package/entity/BaseEntity.d.ts +68 -1
- package/entity/BaseEntity.js +18 -0
- package/entity/Collection.d.ts +1 -1
- package/entity/Collection.js +4 -1
- package/entity/EntityAssigner.js +8 -0
- package/entity/EntityFactory.js +5 -3
- package/entity/EntityLoader.d.ts +8 -1
- package/entity/EntityLoader.js +97 -29
- package/entity/Reference.d.ts +42 -1
- package/entity/Reference.js +9 -0
- package/entity/defineEntity.d.ts +69 -16
- package/entity/defineEntity.js +17 -0
- package/entity/utils.js +4 -5
- package/enums.d.ts +8 -1
- package/index.d.ts +1 -1
- package/metadata/EntitySchema.js +18 -0
- package/metadata/MetadataDiscovery.d.ts +1 -0
- package/metadata/MetadataDiscovery.js +79 -15
- package/metadata/MetadataValidator.d.ts +26 -0
- package/metadata/MetadataValidator.js +228 -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 +75 -8
- package/package.json +1 -1
- package/platforms/Platform.d.ts +13 -3
- package/platforms/Platform.js +51 -6
- package/serialization/EntitySerializer.js +4 -3
- package/typings.d.ts +323 -21
- package/typings.js +32 -12
- package/unit-of-work/UnitOfWork.js +43 -5
- package/utils/AbstractMigrator.d.ts +9 -4
- package/utils/AbstractMigrator.js +60 -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/EntityComparator.js +1 -1
- package/utils/TransactionManager.js +2 -0
- package/utils/Utils.js +1 -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
|
@@ -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();
|
|
@@ -5,14 +5,15 @@ import type { EntityManagerType, IDatabaseDriver } from '../drivers/IDatabaseDri
|
|
|
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
|
*/
|
|
@@ -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
|
|
@@ -317,26 +331,26 @@ export class AbstractMigrator {
|
|
|
317
331
|
}
|
|
318
332
|
}
|
|
319
333
|
resolve(params) {
|
|
320
|
-
const createMigrationHandler = async (method) => {
|
|
334
|
+
const createMigrationHandler = async (method, afterRun) => {
|
|
321
335
|
const { fs } = await import('@mikro-orm/core/fs-utils');
|
|
322
336
|
const migration = await fs.dynamicImport(params.path);
|
|
323
337
|
const MigrationClass = Object.values(migration).find(cls => typeof cls === 'function' && typeof cls.constructor === 'function');
|
|
324
338
|
const instance = new MigrationClass(this.driver, this.config);
|
|
325
|
-
await this.runner.run(instance, method);
|
|
339
|
+
await this.runner.run(instance, method, afterRun);
|
|
326
340
|
};
|
|
327
341
|
return {
|
|
328
342
|
name: this.storage.getMigrationName(params.name),
|
|
329
343
|
path: params.path,
|
|
330
|
-
up:
|
|
331
|
-
down:
|
|
344
|
+
up: afterRun => createMigrationHandler('up', afterRun),
|
|
345
|
+
down: afterRun => createMigrationHandler('down', afterRun),
|
|
332
346
|
};
|
|
333
347
|
}
|
|
334
348
|
initialize(MigrationClass, name) {
|
|
335
349
|
const instance = new MigrationClass(this.driver, this.config);
|
|
336
350
|
return {
|
|
337
351
|
name: this.storage.getMigrationName(name),
|
|
338
|
-
up:
|
|
339
|
-
down:
|
|
352
|
+
up: afterRun => this.runner.run(instance, 'up', afterRun),
|
|
353
|
+
down: afterRun => this.runner.run(instance, 'down', afterRun),
|
|
340
354
|
};
|
|
341
355
|
}
|
|
342
356
|
/**
|
|
@@ -415,13 +429,15 @@ export class AbstractMigrator {
|
|
|
415
429
|
for (const migration of toRun) {
|
|
416
430
|
const event = { name: migration.name, path: migration.path };
|
|
417
431
|
await this.emit(eventBefore, event);
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
+
});
|
|
425
441
|
await this.emit(eventAfter, event);
|
|
426
442
|
result.push(event);
|
|
427
443
|
}
|
|
@@ -487,13 +503,17 @@ export class AbstractMigrator {
|
|
|
487
503
|
return /^\d{14}$/.exec(name) ? this.options.fileName(name) : name;
|
|
488
504
|
}
|
|
489
505
|
prefix(options) {
|
|
506
|
+
const base = {};
|
|
507
|
+
if (this.options.schema) {
|
|
508
|
+
base.schema = this.options.schema;
|
|
509
|
+
}
|
|
490
510
|
if (typeof options === 'string' || Array.isArray(options)) {
|
|
491
|
-
return { migrations: Utils.asArray(options).map(name => this.getMigrationFilename(name)) };
|
|
511
|
+
return { ...base, migrations: Utils.asArray(options).map(name => this.getMigrationFilename(name)) };
|
|
492
512
|
}
|
|
493
513
|
if (!options) {
|
|
494
|
-
return
|
|
514
|
+
return base;
|
|
495
515
|
}
|
|
496
|
-
const result =
|
|
516
|
+
const result = base;
|
|
497
517
|
if (options.migrations) {
|
|
498
518
|
result.migrations = options.migrations.map(name => this.getMigrationFilename(name));
|
|
499
519
|
}
|
|
@@ -506,23 +526,35 @@ export class AbstractMigrator {
|
|
|
506
526
|
else if (options.to === 0) {
|
|
507
527
|
result.to = 0;
|
|
508
528
|
}
|
|
529
|
+
if (options.schema !== undefined) {
|
|
530
|
+
result.schema = options.schema;
|
|
531
|
+
}
|
|
509
532
|
return result;
|
|
510
533
|
}
|
|
511
534
|
async runMigrations(method, options) {
|
|
512
535
|
await this.init();
|
|
513
|
-
|
|
514
|
-
|
|
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));
|
|
515
547
|
}
|
|
516
|
-
|
|
517
|
-
|
|
548
|
+
finally {
|
|
549
|
+
this.runner.unsetRunSchema?.();
|
|
550
|
+
this.storage.unsetRunSchema?.();
|
|
518
551
|
}
|
|
519
|
-
return this.driver.getConnection().transactional(trx => this.runInTransaction(trx, method, options));
|
|
520
552
|
}
|
|
521
553
|
async runInTransaction(trx, method, options) {
|
|
522
554
|
this.runner.setMasterMigration(trx);
|
|
523
555
|
this.storage.setMasterMigration(trx);
|
|
524
556
|
try {
|
|
525
|
-
return await this.executeMigrations(method,
|
|
557
|
+
return await this.executeMigrations(method, options);
|
|
526
558
|
}
|
|
527
559
|
finally {
|
|
528
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
|
});
|
package/utils/Configuration.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { type Logger, type LoggerNamespace, type LoggerOptions } from '../loggin
|
|
|
6
6
|
import type { EntityManager } from '../EntityManager.js';
|
|
7
7
|
import type { Platform } from '../platforms/Platform.js';
|
|
8
8
|
import type { EntitySchema } from '../metadata/EntitySchema.js';
|
|
9
|
+
import { Routine } from '../metadata/Routine.js';
|
|
9
10
|
import { MetadataProvider } from '../metadata/MetadataProvider.js';
|
|
10
11
|
import type { MetadataStorage } from '../metadata/MetadataStorage.js';
|
|
11
12
|
import type { EventSubscriber } from '../events/EventSubscriber.js';
|
|
@@ -27,6 +28,10 @@ export declare class Configuration<D extends IDatabaseDriver = IDatabaseDriver,
|
|
|
27
28
|
get<T extends keyof Options<D, EM>, U extends Options<D, EM>[T]>(key: T, defaultValue?: U): U;
|
|
28
29
|
/** Returns all configuration options. */
|
|
29
30
|
getAll(): Options<D, EM>;
|
|
31
|
+
/** Validates the `routines` config option on first access and throws on duplicate `(schema, name)` pairs or non-Routine entries. */
|
|
32
|
+
getRoutines(): readonly Routine[];
|
|
33
|
+
hasRoutine(routine: Routine): boolean;
|
|
34
|
+
private normaliseRoutines;
|
|
30
35
|
/**
|
|
31
36
|
* Overrides specified configuration value.
|
|
32
37
|
*/
|
|
@@ -238,6 +243,24 @@ export type MigrationsOptions = {
|
|
|
238
243
|
fileName?: (timestamp: string, name?: string) => string;
|
|
239
244
|
/** List of migration classes or objects to use instead of file-based discovery. */
|
|
240
245
|
migrationsList?: (MigrationObject | Constructor<Migration>)[];
|
|
246
|
+
/**
|
|
247
|
+
* Target schema used when running migrations. When set, the driver's "set current schema" statement
|
|
248
|
+
* (e.g. `SET search_path` on PostgreSQL) is issued before each migration, and the migration tracking
|
|
249
|
+
* table lives in this schema. Can be overridden per call via `migrator.up({ schema })`.
|
|
250
|
+
*
|
|
251
|
+
* Intended for per-deployment-one-schema setups (e.g. PR previews) and for applying a single
|
|
252
|
+
* unqualified migration to multiple schemas (see `includeWildcardSchema`). Not supported on MSSQL.
|
|
253
|
+
*/
|
|
254
|
+
schema?: string;
|
|
255
|
+
/**
|
|
256
|
+
* When set, entities with a wildcard schema (`schema: '*'`) are included in generated migrations.
|
|
257
|
+
* The emitted SQL is unqualified — and therefore safe to apply against any schema at runtime via
|
|
258
|
+
* `migrator.up({ schema })` — only when neither `options.schema` nor the ORM's `config.schema`
|
|
259
|
+
* is set. If `config.schema` is set, wildcard tables are qualified with it (useful for local
|
|
260
|
+
* dev runs); in that case, generate migrations from an environment where `config.schema` is unset.
|
|
261
|
+
* @default false
|
|
262
|
+
*/
|
|
263
|
+
includeWildcardSchema?: boolean;
|
|
241
264
|
};
|
|
242
265
|
/**
|
|
243
266
|
* Configuration options for database seeders.
|
|
@@ -390,6 +413,13 @@ export interface Options<Driver extends IDatabaseDriver = IDatabaseDriver, EM ex
|
|
|
390
413
|
* Can be class references or instances.
|
|
391
414
|
*/
|
|
392
415
|
subscribers: Iterable<EventSubscriber | Constructor<EventSubscriber>>;
|
|
416
|
+
/**
|
|
417
|
+
* Stored procedures and functions, declared as {@link Routine} instances.
|
|
418
|
+
*
|
|
419
|
+
* @example
|
|
420
|
+
* routines: [HashUser, AddRecord]
|
|
421
|
+
*/
|
|
422
|
+
routines: Iterable<Routine>;
|
|
393
423
|
/**
|
|
394
424
|
* Global entity filters to apply.
|
|
395
425
|
* Filters are applied by default unless explicitly disabled.
|
package/utils/Configuration.js
CHANGED
|
@@ -4,6 +4,8 @@ import { NullHighlighter } from '../utils/NullHighlighter.js';
|
|
|
4
4
|
import { DefaultLogger } from '../logging/DefaultLogger.js';
|
|
5
5
|
import { colors } from '../logging/colors.js';
|
|
6
6
|
import { Utils } from '../utils/Utils.js';
|
|
7
|
+
import { Routine } from '../metadata/Routine.js';
|
|
8
|
+
import { MetadataValidator } from '../metadata/MetadataValidator.js';
|
|
7
9
|
import { MetadataProvider } from '../metadata/MetadataProvider.js';
|
|
8
10
|
import { NotFoundError } from '../errors.js';
|
|
9
11
|
import { RequestContext } from './RequestContext.js';
|
|
@@ -17,6 +19,7 @@ const DEFAULTS = {
|
|
|
17
19
|
entitiesTs: [],
|
|
18
20
|
extensions: [],
|
|
19
21
|
subscribers: [],
|
|
22
|
+
routines: [],
|
|
20
23
|
filters: {},
|
|
21
24
|
discovery: {
|
|
22
25
|
warnWhenNoEntities: true,
|
|
@@ -137,6 +140,8 @@ export class Configuration {
|
|
|
137
140
|
#platform;
|
|
138
141
|
#cache = new Map();
|
|
139
142
|
#extensions = new Map();
|
|
143
|
+
#routines = [];
|
|
144
|
+
#routinesNormalised = false;
|
|
140
145
|
constructor(options, validate = true) {
|
|
141
146
|
if (options.dynamicImportProvider) {
|
|
142
147
|
globalThis.dynamicImportProvider = options.dynamicImportProvider;
|
|
@@ -181,6 +186,42 @@ export class Configuration {
|
|
|
181
186
|
getAll() {
|
|
182
187
|
return this.#options;
|
|
183
188
|
}
|
|
189
|
+
/** Validates the `routines` config option on first access and throws on duplicate `(schema, name)` pairs or non-Routine entries. */
|
|
190
|
+
getRoutines() {
|
|
191
|
+
this.normaliseRoutines();
|
|
192
|
+
return this.#routines;
|
|
193
|
+
}
|
|
194
|
+
hasRoutine(routine) {
|
|
195
|
+
this.normaliseRoutines();
|
|
196
|
+
return this.#routines.includes(routine);
|
|
197
|
+
}
|
|
198
|
+
normaliseRoutines() {
|
|
199
|
+
if (this.#routinesNormalised) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const validator = new MetadataValidator();
|
|
203
|
+
const seenKeys = new Set();
|
|
204
|
+
// Stage in a local array so a mid-loop throw doesn't leave `#routines` partially populated;
|
|
205
|
+
// a retry after the user fixes the offending entry would otherwise duplicate earlier ones.
|
|
206
|
+
const collected = [];
|
|
207
|
+
for (const item of this.#options.routines ?? []) {
|
|
208
|
+
if (!Routine.is(item)) {
|
|
209
|
+
throw new Error(`'routines' entry is not a stored routine declaration. Use a Routine class instance.`);
|
|
210
|
+
}
|
|
211
|
+
validator.validateRoutineDefinition(item);
|
|
212
|
+
// Case-fold to match `SchemaComparator.compareRoutines`, which lower-cases the key so
|
|
213
|
+
// dialect-specific folding (Oracle uppercases, PG lowercases) round-trips; otherwise a
|
|
214
|
+
// user could register `'Foo'` and `'foo'` here and have the comparator silently collapse them.
|
|
215
|
+
const key = ((item.schema ? `${item.schema}.` : '') + item.name).toLowerCase();
|
|
216
|
+
if (seenKeys.has(key)) {
|
|
217
|
+
throw new Error(`Duplicate routine '${key}' declared more than once in the 'routines' config. Routine names must be unique within a schema.`);
|
|
218
|
+
}
|
|
219
|
+
seenKeys.add(key);
|
|
220
|
+
collected.push(item);
|
|
221
|
+
}
|
|
222
|
+
this.#routines.push(...collected);
|
|
223
|
+
this.#routinesNormalised = true;
|
|
224
|
+
}
|
|
184
225
|
/**
|
|
185
226
|
* Overrides specified configuration value.
|
|
186
227
|
*/
|
|
@@ -324,6 +365,10 @@ export class Configuration {
|
|
|
324
365
|
metadataCache.enabled ??= useCache;
|
|
325
366
|
this.#options.clientUrl ??= this.#platform.getDefaultClientUrl();
|
|
326
367
|
this.#options.implicitTransactions ??= this.#platform.usesImplicitTransactions();
|
|
368
|
+
// Eagerly trigger routine validation so errors surface at init time, not on first call.
|
|
369
|
+
if (validate) {
|
|
370
|
+
this.getRoutines();
|
|
371
|
+
}
|
|
327
372
|
if (validate && metadataCache.enabled && !metadataCache.adapter) {
|
|
328
373
|
throw new Error('No metadata cache adapter specified, please fill in `metadataCache.adapter` option or use the async MikroORM.init() method which can autoload it.');
|
|
329
374
|
}
|
|
@@ -536,7 +536,7 @@ export class EntityComparator {
|
|
|
536
536
|
ret += ` && entity${path.map(k => this.wrap(k)).join('')}?.isInitialized()`;
|
|
537
537
|
}
|
|
538
538
|
ret += `) {\n`;
|
|
539
|
-
if (['number', 'string', 'boolean'].includes(prop.type.toLowerCase())) {
|
|
539
|
+
if (!prop.array && ['number', 'string', 'boolean'].includes(prop.type.toLowerCase())) {
|
|
540
540
|
return ret + ` ret${dataKey} = entity${entityKey}${unwrap};\n }\n`;
|
|
541
541
|
}
|
|
542
542
|
if (prop.kind === ReferenceKind.EMBEDDED) {
|
|
@@ -136,6 +136,8 @@ export class TransactionManager {
|
|
|
136
136
|
cloneEventManager: true,
|
|
137
137
|
disableTransactions: options.ignoreNestedTransactions,
|
|
138
138
|
loggerContext: options.loggerContext,
|
|
139
|
+
signal: options.signal,
|
|
140
|
+
inflightQueryAbortStrategy: options.inflightQueryAbortStrategy,
|
|
139
141
|
});
|
|
140
142
|
}
|
|
141
143
|
/**
|
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.
|
|
144
|
+
static #ORM_VERSION = '7.1.1-dev.0';
|
|
145
145
|
/**
|
|
146
146
|
* Checks if the argument is instance of `Object`. Returns false for arrays.
|
|
147
147
|
*/
|
package/utils/index.d.ts
CHANGED
package/utils/index.js
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a partition name for collision detection. Mirrors PostgreSQL identifier
|
|
3
|
+
* resolution: unquoted segments fold to lower case, quoted segments preserve case and
|
|
4
|
+
* un-escape embedded `""`. Returns a canonical `schema.name` form so both schema-qualified
|
|
5
|
+
* and bare names compare consistently (`.name` vs `schema.name` stay distinguishable).
|
|
6
|
+
*
|
|
7
|
+
* `Part_1`, `part_1`, and `"part_1"` all normalize to the same value, catching collisions
|
|
8
|
+
* that would otherwise only surface as runtime PG "relation already exists" errors.
|
|
9
|
+
*/
|
|
10
|
+
export declare function normalizePartitionNameForComparison(name: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Split a comma-list of identifiers, respecting double-quoted identifiers (which may contain
|
|
13
|
+
* commas and use `""` to escape embedded quotes). Returns `null` when the input is not a clean
|
|
14
|
+
* comma-list of identifiers (e.g. contains function calls or literals), so callers treat it
|
|
15
|
+
* as an opaque expression.
|
|
16
|
+
*/
|
|
17
|
+
export declare function splitCommaSeparatedIdentifiers(value: string): string[] | null;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
function normalizeIdentifierSegment(segment) {
|
|
2
|
+
const trimmed = segment.trim();
|
|
3
|
+
if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
4
|
+
return trimmed.slice(1, -1).replaceAll('""', '"');
|
|
5
|
+
}
|
|
6
|
+
return trimmed.toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Normalize a partition name for collision detection. Mirrors PostgreSQL identifier
|
|
10
|
+
* resolution: unquoted segments fold to lower case, quoted segments preserve case and
|
|
11
|
+
* un-escape embedded `""`. Returns a canonical `schema.name` form so both schema-qualified
|
|
12
|
+
* and bare names compare consistently (`.name` vs `schema.name` stay distinguishable).
|
|
13
|
+
*
|
|
14
|
+
* `Part_1`, `part_1`, and `"part_1"` all normalize to the same value, catching collisions
|
|
15
|
+
* that would otherwise only surface as runtime PG "relation already exists" errors.
|
|
16
|
+
*/
|
|
17
|
+
export function normalizePartitionNameForComparison(name) {
|
|
18
|
+
const trimmed = name.trim();
|
|
19
|
+
let depth = 0;
|
|
20
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
21
|
+
const ch = trimmed[i];
|
|
22
|
+
if (ch === '"') {
|
|
23
|
+
if (trimmed[i + 1] === '"') {
|
|
24
|
+
i++;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
depth = depth === 0 ? 1 : 0;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (ch === '.' && depth === 0) {
|
|
31
|
+
return `${normalizeIdentifierSegment(trimmed.slice(0, i))}.${normalizeIdentifierSegment(trimmed.slice(i + 1))}`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return `.${normalizeIdentifierSegment(trimmed)}`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Split a comma-list of identifiers, respecting double-quoted identifiers (which may contain
|
|
38
|
+
* commas and use `""` to escape embedded quotes). Returns `null` when the input is not a clean
|
|
39
|
+
* comma-list of identifiers (e.g. contains function calls or literals), so callers treat it
|
|
40
|
+
* as an opaque expression.
|
|
41
|
+
*/
|
|
42
|
+
export function splitCommaSeparatedIdentifiers(value) {
|
|
43
|
+
const segments = [];
|
|
44
|
+
let buffer = '';
|
|
45
|
+
let inQuote = false;
|
|
46
|
+
for (let i = 0; i < value.length; i++) {
|
|
47
|
+
const ch = value[i];
|
|
48
|
+
if (ch === '"') {
|
|
49
|
+
if (inQuote && value[i + 1] === '"') {
|
|
50
|
+
buffer += '""';
|
|
51
|
+
i++;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
inQuote = !inQuote;
|
|
55
|
+
buffer += ch;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!inQuote) {
|
|
59
|
+
if (ch === ',') {
|
|
60
|
+
segments.push(buffer);
|
|
61
|
+
buffer = '';
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (!/[\w.\s]/.test(ch)) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
buffer += ch;
|
|
69
|
+
}
|
|
70
|
+
if (inQuote) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
segments.push(buffer);
|
|
74
|
+
const trimmed = segments.map(s => s.trim());
|
|
75
|
+
if (trimmed.some(s => s === '')) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return trimmed;
|
|
79
|
+
}
|
package/utils/upsert-utils.d.ts
CHANGED
|
@@ -10,3 +10,5 @@ export declare function getWhereCondition<T extends object>(meta: EntityMetadata
|
|
|
10
10
|
where: FilterQuery<T>;
|
|
11
11
|
propIndex: number | false;
|
|
12
12
|
};
|
|
13
|
+
/** @internal */
|
|
14
|
+
export declare function resetUntouchedCollections<Entity extends object>(meta: EntityMetadata<Entity>, entity: Entity): void;
|
package/utils/upsert-utils.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { isRaw } from '../utils/RawQueryFragment.js';
|
|
2
2
|
import { Utils } from './Utils.js';
|
|
3
|
+
import { ReferenceKind } from '../enums.js';
|
|
4
|
+
import { Collection } from '../entity/Collection.js';
|
|
5
|
+
import { helper } from '../entity/wrap.js';
|
|
3
6
|
function expandEmbeddedProperties(prop, key) {
|
|
4
7
|
if (prop.object) {
|
|
5
8
|
return [prop.name];
|
|
@@ -52,7 +55,9 @@ export function getOnConflictFields(meta, data, uniqueFields, options) {
|
|
|
52
55
|
});
|
|
53
56
|
}
|
|
54
57
|
const keys = Object.keys(data).flatMap(f => {
|
|
55
|
-
|
|
58
|
+
// skip explicitly listed unique fields; for raw onConflictFields we can't introspect the
|
|
59
|
+
// fragment, so merge all data keys (the user is responsible for not corrupting them).
|
|
60
|
+
if (Array.isArray(uniqueFields) && uniqueFields.includes(f)) {
|
|
56
61
|
return [];
|
|
57
62
|
}
|
|
58
63
|
const prop = meta?.properties[f];
|
|
@@ -142,3 +147,23 @@ export function getWhereCondition(meta, onConflictFields, data, where) {
|
|
|
142
147
|
}
|
|
143
148
|
return { where, propIndex };
|
|
144
149
|
}
|
|
150
|
+
/** @internal */
|
|
151
|
+
export function resetUntouchedCollections(meta, entity) {
|
|
152
|
+
// for entities passed in via `em.create()` and then upserted, collection-kind relations
|
|
153
|
+
// were initialized as empty-but-initialized by the hydrator (newEntity=true). once the
|
|
154
|
+
// upsert resolves the entity to a possibly existing row, that state is stale and would
|
|
155
|
+
// cause `em.populate()` to skip those collections. replace them with a fresh
|
|
156
|
+
// uninitialized collection so populate can load them.
|
|
157
|
+
if (helper(entity).__originalEntityData) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
for (const prop of meta.relations) {
|
|
161
|
+
if (prop.kind !== ReferenceKind.MANY_TO_MANY && prop.kind !== ReferenceKind.ONE_TO_MANY) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const collection = entity[prop.name];
|
|
165
|
+
if (Utils.isCollection(collection) && collection.isInitialized() && !collection.isDirty()) {
|
|
166
|
+
Collection.create(entity, prop.name, undefined, false);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|