@mikro-orm/core 7.0.0-dev.99 → 7.0.0-rc.1
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 +34 -17
- package/EntityManager.js +95 -103
- package/MikroORM.d.ts +5 -5
- package/MikroORM.js +25 -20
- package/cache/FileCacheAdapter.js +11 -3
- package/connections/Connection.d.ts +3 -2
- package/connections/Connection.js +4 -3
- package/drivers/DatabaseDriver.d.ts +11 -11
- package/drivers/DatabaseDriver.js +91 -25
- package/drivers/IDatabaseDriver.d.ts +50 -20
- package/entity/BaseEntity.d.ts +61 -1
- package/entity/Collection.d.ts +8 -1
- package/entity/Collection.js +12 -13
- package/entity/EntityAssigner.js +9 -9
- package/entity/EntityFactory.d.ts +6 -1
- package/entity/EntityFactory.js +40 -22
- package/entity/EntityHelper.d.ts +2 -2
- package/entity/EntityHelper.js +27 -4
- package/entity/EntityLoader.d.ts +5 -4
- package/entity/EntityLoader.js +193 -80
- package/entity/EntityRepository.d.ts +27 -7
- package/entity/EntityRepository.js +8 -2
- package/entity/PolymorphicRef.d.ts +12 -0
- package/entity/PolymorphicRef.js +18 -0
- package/entity/WrappedEntity.d.ts +2 -2
- package/entity/WrappedEntity.js +1 -1
- package/entity/defineEntity.d.ts +89 -50
- package/entity/defineEntity.js +12 -0
- package/entity/index.d.ts +1 -0
- package/entity/index.js +1 -0
- package/entity/utils.d.ts +6 -1
- package/entity/utils.js +33 -0
- package/entity/validators.js +2 -2
- package/enums.d.ts +2 -2
- package/enums.js +1 -0
- package/errors.d.ts +16 -8
- package/errors.js +40 -13
- package/hydration/ObjectHydrator.js +63 -21
- package/index.d.ts +1 -1
- package/logging/colors.d.ts +1 -1
- package/logging/colors.js +7 -6
- package/logging/inspect.js +1 -6
- package/metadata/EntitySchema.d.ts +43 -13
- package/metadata/EntitySchema.js +82 -27
- package/metadata/MetadataDiscovery.d.ts +60 -3
- package/metadata/MetadataDiscovery.js +665 -154
- package/metadata/MetadataProvider.js +3 -1
- package/metadata/MetadataStorage.d.ts +13 -6
- package/metadata/MetadataStorage.js +64 -19
- package/metadata/MetadataValidator.d.ts +32 -2
- package/metadata/MetadataValidator.js +196 -31
- package/metadata/discover-entities.js +5 -5
- package/metadata/types.d.ts +111 -14
- package/naming-strategy/AbstractNamingStrategy.d.ts +11 -3
- package/naming-strategy/AbstractNamingStrategy.js +12 -0
- package/naming-strategy/EntityCaseNamingStrategy.d.ts +3 -3
- package/naming-strategy/EntityCaseNamingStrategy.js +6 -5
- package/naming-strategy/MongoNamingStrategy.d.ts +3 -3
- package/naming-strategy/MongoNamingStrategy.js +6 -6
- package/naming-strategy/NamingStrategy.d.ts +17 -3
- package/naming-strategy/UnderscoreNamingStrategy.d.ts +3 -3
- package/naming-strategy/UnderscoreNamingStrategy.js +6 -6
- package/package.json +2 -2
- package/platforms/Platform.d.ts +4 -2
- package/platforms/Platform.js +5 -2
- package/serialization/EntitySerializer.d.ts +3 -0
- package/serialization/EntitySerializer.js +15 -13
- package/serialization/EntityTransformer.js +6 -6
- package/serialization/SerializationContext.d.ts +6 -6
- package/typings.d.ts +325 -110
- package/typings.js +84 -17
- package/unit-of-work/ChangeSet.d.ts +4 -3
- package/unit-of-work/ChangeSet.js +2 -3
- package/unit-of-work/ChangeSetComputer.d.ts +3 -6
- package/unit-of-work/ChangeSetComputer.js +34 -13
- package/unit-of-work/ChangeSetPersister.d.ts +12 -10
- package/unit-of-work/ChangeSetPersister.js +55 -25
- package/unit-of-work/CommitOrderCalculator.d.ts +12 -10
- package/unit-of-work/CommitOrderCalculator.js +13 -13
- package/unit-of-work/IdentityMap.d.ts +12 -0
- package/unit-of-work/IdentityMap.js +39 -1
- package/unit-of-work/UnitOfWork.d.ts +21 -3
- package/unit-of-work/UnitOfWork.js +203 -56
- package/utils/AbstractSchemaGenerator.js +17 -8
- package/utils/AsyncContext.d.ts +6 -0
- package/utils/AsyncContext.js +42 -0
- package/utils/Configuration.d.ts +52 -11
- package/utils/Configuration.js +12 -8
- package/utils/Cursor.js +21 -8
- package/utils/DataloaderUtils.js +13 -11
- package/utils/EntityComparator.d.ts +14 -7
- package/utils/EntityComparator.js +132 -46
- package/utils/QueryHelper.d.ts +16 -6
- package/utils/QueryHelper.js +53 -18
- package/utils/RawQueryFragment.d.ts +28 -23
- package/utils/RawQueryFragment.js +34 -56
- package/utils/RequestContext.js +2 -2
- package/utils/TransactionContext.js +2 -2
- package/utils/TransactionManager.js +1 -1
- package/utils/Utils.d.ts +7 -26
- package/utils/Utils.js +25 -79
- package/utils/clone.js +7 -21
- package/utils/env-vars.d.ts +4 -0
- package/utils/env-vars.js +13 -3
- package/utils/fs-utils.d.ts +21 -0
- package/utils/fs-utils.js +106 -11
- package/utils/upsert-utils.d.ts +4 -4
|
@@ -17,26 +17,26 @@ export var NodeState;
|
|
|
17
17
|
*/
|
|
18
18
|
export class CommitOrderCalculator {
|
|
19
19
|
/** Matrix of nodes, keys are provided hashes and values are the node definition objects. */
|
|
20
|
-
nodes =
|
|
20
|
+
nodes = new Map();
|
|
21
21
|
/** Volatile variable holding calculated nodes during sorting process. */
|
|
22
22
|
sortedNodeList = [];
|
|
23
23
|
/**
|
|
24
24
|
* Checks for node existence in graph.
|
|
25
25
|
*/
|
|
26
26
|
hasNode(hash) {
|
|
27
|
-
return
|
|
27
|
+
return this.nodes.has(hash);
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
30
|
* Adds a new node to the graph, assigning its hash.
|
|
31
31
|
*/
|
|
32
32
|
addNode(hash) {
|
|
33
|
-
this.nodes
|
|
33
|
+
this.nodes.set(hash, { hash, state: 0 /* NodeState.NOT_VISITED */, dependencies: new Map() });
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
36
|
* Adds a new dependency (edge) to the graph using their hashes.
|
|
37
37
|
*/
|
|
38
38
|
addDependency(from, to, weight) {
|
|
39
|
-
this.nodes
|
|
39
|
+
this.nodes.get(from).dependencies.set(to, { from, to, weight });
|
|
40
40
|
}
|
|
41
41
|
discoverProperty(prop, entityName) {
|
|
42
42
|
const toOneOwner = (prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner) || prop.kind === ReferenceKind.MANY_TO_ONE;
|
|
@@ -44,8 +44,8 @@ export class CommitOrderCalculator {
|
|
|
44
44
|
if (!toOneOwner && !toManyOwner) {
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
47
|
-
const propertyType = prop.targetMeta?.root.
|
|
48
|
-
if (
|
|
47
|
+
const propertyType = prop.targetMeta?.root._id;
|
|
48
|
+
if (propertyType == null || !this.hasNode(propertyType)) {
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
51
|
this.addDependency(propertyType, entityName, prop.nullable || prop.persist === false ? 0 : 1);
|
|
@@ -57,14 +57,14 @@ export class CommitOrderCalculator {
|
|
|
57
57
|
* @internal Highly performance-sensitive method.
|
|
58
58
|
*/
|
|
59
59
|
sort() {
|
|
60
|
-
for (const vertex of
|
|
60
|
+
for (const vertex of this.nodes.values()) {
|
|
61
61
|
if (vertex.state !== 0 /* NodeState.NOT_VISITED */) {
|
|
62
62
|
continue;
|
|
63
63
|
}
|
|
64
64
|
this.visit(vertex);
|
|
65
65
|
}
|
|
66
66
|
const sortedList = this.sortedNodeList.reverse();
|
|
67
|
-
this.nodes =
|
|
67
|
+
this.nodes = new Map();
|
|
68
68
|
this.sortedNodeList = [];
|
|
69
69
|
return sortedList;
|
|
70
70
|
}
|
|
@@ -75,8 +75,8 @@ export class CommitOrderCalculator {
|
|
|
75
75
|
*/
|
|
76
76
|
visit(node) {
|
|
77
77
|
node.state = 1 /* NodeState.IN_PROGRESS */;
|
|
78
|
-
for (const edge of
|
|
79
|
-
const target = this.nodes
|
|
78
|
+
for (const edge of node.dependencies.values()) {
|
|
79
|
+
const target = this.nodes.get(edge.to);
|
|
80
80
|
switch (target.state) {
|
|
81
81
|
case 2 /* NodeState.VISITED */: break; // Do nothing, since node was already visited
|
|
82
82
|
case 1 /* NodeState.IN_PROGRESS */:
|
|
@@ -94,11 +94,11 @@ export class CommitOrderCalculator {
|
|
|
94
94
|
* Visits all target's dependencies if in cycle with given node
|
|
95
95
|
*/
|
|
96
96
|
visitOpenNode(node, target, edge) {
|
|
97
|
-
if (!target.dependencies
|
|
97
|
+
if (!target.dependencies.has(node.hash) || target.dependencies.get(node.hash).weight >= edge.weight) {
|
|
98
98
|
return;
|
|
99
99
|
}
|
|
100
|
-
for (const edge of
|
|
101
|
-
const targetNode = this.nodes
|
|
100
|
+
for (const edge of target.dependencies.values()) {
|
|
101
|
+
const targetNode = this.nodes.get(edge.to);
|
|
102
102
|
if (targetNode.state === 0 /* NodeState.NOT_VISITED */) {
|
|
103
103
|
this.visit(targetNode);
|
|
104
104
|
}
|
|
@@ -3,7 +3,14 @@ export declare class IdentityMap {
|
|
|
3
3
|
private readonly defaultSchema?;
|
|
4
4
|
constructor(defaultSchema?: string | undefined);
|
|
5
5
|
private readonly registry;
|
|
6
|
+
/** Tracks alternate key hashes for each entity so we can clean them up on delete */
|
|
7
|
+
private readonly alternateKeys;
|
|
6
8
|
store<T>(item: T): void;
|
|
9
|
+
/**
|
|
10
|
+
* Stores an entity under an alternate key (non-PK property).
|
|
11
|
+
* This allows looking up entities by unique properties that are not the primary key.
|
|
12
|
+
*/
|
|
13
|
+
storeByKey<T>(item: T, key: string, value: string, schema?: string): void;
|
|
7
14
|
delete<T>(item: T): void;
|
|
8
15
|
getByHash<T>(meta: EntityMetadata<T>, hash: string): T | undefined;
|
|
9
16
|
getStore<T>(meta: EntityMetadata<T>): Map<string, T>;
|
|
@@ -16,4 +23,9 @@ export declare class IdentityMap {
|
|
|
16
23
|
*/
|
|
17
24
|
get<T>(hash: string): T | undefined;
|
|
18
25
|
private getPkHash;
|
|
26
|
+
/**
|
|
27
|
+
* Creates a hash for an alternate key lookup.
|
|
28
|
+
* Format: `[key]value` or `schema:[key]value`
|
|
29
|
+
*/
|
|
30
|
+
getKeyHash(key: string, value: string, schema?: string): string;
|
|
19
31
|
}
|
|
@@ -4,11 +4,38 @@ export class IdentityMap {
|
|
|
4
4
|
this.defaultSchema = defaultSchema;
|
|
5
5
|
}
|
|
6
6
|
registry = new Map();
|
|
7
|
+
/** Tracks alternate key hashes for each entity so we can clean them up on delete */
|
|
8
|
+
alternateKeys = new WeakMap();
|
|
7
9
|
store(item) {
|
|
8
10
|
this.getStore(item.__meta.root).set(this.getPkHash(item), item);
|
|
9
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Stores an entity under an alternate key (non-PK property).
|
|
14
|
+
* This allows looking up entities by unique properties that are not the primary key.
|
|
15
|
+
*/
|
|
16
|
+
storeByKey(item, key, value, schema) {
|
|
17
|
+
const hash = this.getKeyHash(key, value, schema);
|
|
18
|
+
this.getStore(item.__meta.root).set(hash, item);
|
|
19
|
+
// Track this alternate key so we can clean it up when the entity is deleted
|
|
20
|
+
let keys = this.alternateKeys.get(item);
|
|
21
|
+
if (!keys) {
|
|
22
|
+
keys = new Set();
|
|
23
|
+
this.alternateKeys.set(item, keys);
|
|
24
|
+
}
|
|
25
|
+
keys.add(hash);
|
|
26
|
+
}
|
|
10
27
|
delete(item) {
|
|
11
|
-
|
|
28
|
+
const meta = item.__meta.root;
|
|
29
|
+
const store = this.getStore(meta);
|
|
30
|
+
store.delete(this.getPkHash(item));
|
|
31
|
+
// Also delete any alternate key entries for this entity
|
|
32
|
+
const altKeys = this.alternateKeys.get(item);
|
|
33
|
+
if (altKeys) {
|
|
34
|
+
for (const hash of altKeys) {
|
|
35
|
+
store.delete(hash);
|
|
36
|
+
}
|
|
37
|
+
this.alternateKeys.delete(item);
|
|
38
|
+
}
|
|
12
39
|
}
|
|
13
40
|
getByHash(meta, hash) {
|
|
14
41
|
const store = this.getStore(meta);
|
|
@@ -69,4 +96,15 @@ export class IdentityMap {
|
|
|
69
96
|
}
|
|
70
97
|
return hash;
|
|
71
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Creates a hash for an alternate key lookup.
|
|
101
|
+
* Format: `[key]value` or `schema:[key]value`
|
|
102
|
+
*/
|
|
103
|
+
getKeyHash(key, value, schema) {
|
|
104
|
+
const hash = `[${key}]${value}`;
|
|
105
|
+
if (schema) {
|
|
106
|
+
return schema + ':' + hash;
|
|
107
|
+
}
|
|
108
|
+
return hash;
|
|
109
|
+
}
|
|
72
110
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnyEntity, EntityData, EntityMetadata, EntityProperty, FilterQuery, Primary } from '../typings.js';
|
|
1
|
+
import type { AnyEntity, EntityData, EntityMetadata, EntityName, EntityProperty, FilterQuery, Primary } from '../typings.js';
|
|
2
2
|
import { Collection } from '../entity/Collection.js';
|
|
3
3
|
import { Reference } from '../entity/Reference.js';
|
|
4
4
|
import { ChangeSet, ChangeSetType } from './ChangeSet.js';
|
|
@@ -45,8 +45,21 @@ export declare class UnitOfWork {
|
|
|
45
45
|
/**
|
|
46
46
|
* Returns entity from the identity map. For composite keys, you need to pass an array of PKs in the same order as they are defined in `meta.primaryKeys`.
|
|
47
47
|
*/
|
|
48
|
-
getById<T extends object>(entityName:
|
|
49
|
-
|
|
48
|
+
getById<T extends object>(entityName: EntityName<T>, id: Primary<T> | Primary<T>[], schema?: string, convertCustomTypes?: boolean): T | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Returns entity from the identity map by an alternate key (non-PK property).
|
|
51
|
+
* @param convertCustomTypes - If true, the value is in database format and will be converted to JS format for lookup.
|
|
52
|
+
* If false (default), the value is assumed to be in JS format already.
|
|
53
|
+
*/
|
|
54
|
+
getByKey<T extends object>(entityName: EntityName<T>, key: string, value: unknown, schema?: string, convertCustomTypes?: boolean): T | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Stores an entity in the identity map under an alternate key (non-PK property).
|
|
57
|
+
* Also sets the property value on the entity.
|
|
58
|
+
* @param convertCustomTypes - If true, the value is in database format and will be converted to JS format.
|
|
59
|
+
* If false (default), the value is assumed to be in JS format already.
|
|
60
|
+
*/
|
|
61
|
+
storeByKey<T extends object>(entity: T, key: string, value: unknown, schema?: string, convertCustomTypes?: boolean): void;
|
|
62
|
+
tryGetById<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, schema?: string, strict?: boolean): T | null;
|
|
50
63
|
/**
|
|
51
64
|
* Returns map of all managed entities.
|
|
52
65
|
*/
|
|
@@ -83,6 +96,11 @@ export declare class UnitOfWork {
|
|
|
83
96
|
getOrphanRemoveStack(): Set<AnyEntity>;
|
|
84
97
|
getChangeSetPersister(): ChangeSetPersister;
|
|
85
98
|
private findNewEntities;
|
|
99
|
+
/**
|
|
100
|
+
* For TPT inheritance, creates separate changesets for each table in the hierarchy.
|
|
101
|
+
* Uses the same entity instance for all changesets - only the metadata and payload differ.
|
|
102
|
+
*/
|
|
103
|
+
private createTPTChangeSets;
|
|
86
104
|
/**
|
|
87
105
|
* Returns `true` when the change set should be skipped as it will be empty after the extra update.
|
|
88
106
|
*/
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
1
|
import { Collection } from '../entity/Collection.js';
|
|
3
2
|
import { EntityHelper } from '../entity/EntityHelper.js';
|
|
4
3
|
import { helper } from '../entity/wrap.js';
|
|
@@ -13,8 +12,9 @@ import { Cascade, DeferMode, EventType, LockMode, ReferenceKind } from '../enums
|
|
|
13
12
|
import { OptimisticLockError, ValidationError } from '../errors.js';
|
|
14
13
|
import { TransactionEventBroadcaster } from '../events/TransactionEventBroadcaster.js';
|
|
15
14
|
import { IdentityMap } from './IdentityMap.js';
|
|
15
|
+
import { createAsyncContext } from '../utils/AsyncContext.js';
|
|
16
16
|
// to deal with validation for flush inside flush hooks and `Promise.all`
|
|
17
|
-
const insideFlush =
|
|
17
|
+
const insideFlush = createAsyncContext();
|
|
18
18
|
export class UnitOfWork {
|
|
19
19
|
em;
|
|
20
20
|
/** map of references to managed entities */
|
|
@@ -42,8 +42,8 @@ export class UnitOfWork {
|
|
|
42
42
|
this.identityMap = new IdentityMap(this.platform.getDefaultSchemaName());
|
|
43
43
|
this.eventManager = this.em.getEventManager();
|
|
44
44
|
this.comparator = this.em.getComparator();
|
|
45
|
-
this.changeSetComputer = new ChangeSetComputer(this.
|
|
46
|
-
this.changeSetPersister = new ChangeSetPersister(this.em
|
|
45
|
+
this.changeSetComputer = new ChangeSetComputer(this.em, this.collectionUpdates);
|
|
46
|
+
this.changeSetPersister = new ChangeSetPersister(this.em);
|
|
47
47
|
}
|
|
48
48
|
merge(entity, visited) {
|
|
49
49
|
const wrapped = helper(entity);
|
|
@@ -77,7 +77,10 @@ export class UnitOfWork {
|
|
|
77
77
|
continue;
|
|
78
78
|
}
|
|
79
79
|
if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && Utils.isPlainObject(data[prop.name])) {
|
|
80
|
-
|
|
80
|
+
// Skip polymorphic relations - they use PolymorphicRef wrapper
|
|
81
|
+
if (!prop.polymorphic) {
|
|
82
|
+
data[prop.name] = Utils.getPrimaryKeyValues(data[prop.name], prop.targetMeta, true);
|
|
83
|
+
}
|
|
81
84
|
}
|
|
82
85
|
else if (prop.kind === ReferenceKind.EMBEDDED && !prop.object && Utils.isPlainObject(data[prop.name])) {
|
|
83
86
|
for (const p of prop.targetMeta.props) {
|
|
@@ -169,6 +172,40 @@ export class UnitOfWork {
|
|
|
169
172
|
}
|
|
170
173
|
return this.identityMap.getByHash(meta, hash);
|
|
171
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Returns entity from the identity map by an alternate key (non-PK property).
|
|
177
|
+
* @param convertCustomTypes - If true, the value is in database format and will be converted to JS format for lookup.
|
|
178
|
+
* If false (default), the value is assumed to be in JS format already.
|
|
179
|
+
*/
|
|
180
|
+
getByKey(entityName, key, value, schema, convertCustomTypes) {
|
|
181
|
+
const meta = this.metadata.find(entityName).root;
|
|
182
|
+
schema ??= meta.schema ?? this.em.config.getSchema();
|
|
183
|
+
const prop = meta.properties[key];
|
|
184
|
+
// Convert from DB format to JS format if needed
|
|
185
|
+
if (convertCustomTypes && prop?.customType) {
|
|
186
|
+
value = prop.customType.convertToJSValue(value, this.platform, { mode: 'hydration' });
|
|
187
|
+
}
|
|
188
|
+
const hash = this.identityMap.getKeyHash(key, '' + value, schema);
|
|
189
|
+
return this.identityMap.getByHash(meta, hash);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Stores an entity in the identity map under an alternate key (non-PK property).
|
|
193
|
+
* Also sets the property value on the entity.
|
|
194
|
+
* @param convertCustomTypes - If true, the value is in database format and will be converted to JS format.
|
|
195
|
+
* If false (default), the value is assumed to be in JS format already.
|
|
196
|
+
*/
|
|
197
|
+
storeByKey(entity, key, value, schema, convertCustomTypes) {
|
|
198
|
+
const meta = entity.__meta.root;
|
|
199
|
+
schema ??= meta.schema ?? this.em.config.getSchema();
|
|
200
|
+
const prop = meta.properties[key];
|
|
201
|
+
// Convert from DB format to JS format if needed
|
|
202
|
+
if (convertCustomTypes && prop?.customType) {
|
|
203
|
+
value = prop.customType.convertToJSValue(value, this.platform, { mode: 'hydration' });
|
|
204
|
+
}
|
|
205
|
+
// Set the property on the entity
|
|
206
|
+
entity[key] = value;
|
|
207
|
+
this.identityMap.storeByKey(entity, key, '' + value, schema);
|
|
208
|
+
}
|
|
172
209
|
tryGetById(entityName, where, schema, strict = true) {
|
|
173
210
|
const pk = Utils.extractPK(where, this.metadata.find(entityName), strict);
|
|
174
211
|
if (!pk) {
|
|
@@ -207,7 +244,7 @@ export class UnitOfWork {
|
|
|
207
244
|
if (insideFlush.getStore()) {
|
|
208
245
|
return false;
|
|
209
246
|
}
|
|
210
|
-
if (this.queuedActions.has(meta.
|
|
247
|
+
if (this.queuedActions.has(meta.class) || this.queuedActions.has(meta.root.class)) {
|
|
211
248
|
return true;
|
|
212
249
|
}
|
|
213
250
|
if (meta.discriminatorMap && Object.values(meta.discriminatorMap).some(v => this.queuedActions.has(v))) {
|
|
@@ -220,7 +257,7 @@ export class UnitOfWork {
|
|
|
220
257
|
}
|
|
221
258
|
computeChangeSet(entity, type) {
|
|
222
259
|
const wrapped = helper(entity);
|
|
223
|
-
if (type) {
|
|
260
|
+
if (type === ChangeSetType.DELETE || type === ChangeSetType.DELETE_EARLY) {
|
|
224
261
|
this.changeSets.set(entity, new ChangeSet(entity, type, {}, wrapped.__meta));
|
|
225
262
|
return;
|
|
226
263
|
}
|
|
@@ -228,6 +265,10 @@ export class UnitOfWork {
|
|
|
228
265
|
if (!cs || this.checkUniqueProps(cs)) {
|
|
229
266
|
return;
|
|
230
267
|
}
|
|
268
|
+
/* v8 ignore next */
|
|
269
|
+
if (type) {
|
|
270
|
+
cs.type = type;
|
|
271
|
+
}
|
|
231
272
|
this.initIdentifier(entity);
|
|
232
273
|
this.changeSets.set(entity, cs);
|
|
233
274
|
this.persistStack.delete(entity);
|
|
@@ -251,7 +292,7 @@ export class UnitOfWork {
|
|
|
251
292
|
}
|
|
252
293
|
const wrapped = helper(entity);
|
|
253
294
|
this.persistStack.add(entity);
|
|
254
|
-
this.queuedActions.add(wrapped.__meta.
|
|
295
|
+
this.queuedActions.add(wrapped.__meta.class);
|
|
255
296
|
this.removeStack.delete(entity);
|
|
256
297
|
if (!wrapped.__managed && wrapped.hasPrimaryKey()) {
|
|
257
298
|
this.identityMap.store(entity);
|
|
@@ -264,7 +305,7 @@ export class UnitOfWork {
|
|
|
264
305
|
// allow removing not managed entities if they are not part of the persist stack
|
|
265
306
|
if (helper(entity).__managed || !this.persistStack.has(entity)) {
|
|
266
307
|
this.removeStack.add(entity);
|
|
267
|
-
this.queuedActions.add(helper(entity).__meta.
|
|
308
|
+
this.queuedActions.add(helper(entity).__meta.class);
|
|
268
309
|
}
|
|
269
310
|
else {
|
|
270
311
|
this.persistStack.delete(entity);
|
|
@@ -355,10 +396,10 @@ export class UnitOfWork {
|
|
|
355
396
|
}
|
|
356
397
|
}
|
|
357
398
|
async lock(entity, options) {
|
|
358
|
-
if (!this.getById(entity.constructor
|
|
399
|
+
if (!this.getById(entity.constructor, helper(entity).__primaryKeys, helper(entity).__schema)) {
|
|
359
400
|
throw ValidationError.entityNotManaged(entity);
|
|
360
401
|
}
|
|
361
|
-
const meta = this.metadata.find(entity.constructor
|
|
402
|
+
const meta = this.metadata.find(entity.constructor);
|
|
362
403
|
if (options.lockMode === LockMode.OPTIMISTIC) {
|
|
363
404
|
await this.lockOptimistic(entity, meta, options.lockVersion);
|
|
364
405
|
}
|
|
@@ -382,7 +423,7 @@ export class UnitOfWork {
|
|
|
382
423
|
if (Utils.isCollection(rel)) {
|
|
383
424
|
rel.removeWithoutPropagation(entity);
|
|
384
425
|
}
|
|
385
|
-
else if (rel && (prop.mapToPk ? helper(this.em.getReference(prop.
|
|
426
|
+
else if (rel && (prop.mapToPk ? helper(this.em.getReference(prop.targetMeta.class, rel)).getSerializedPrimaryKey() === serializedPK : rel === entity)) {
|
|
386
427
|
if (prop.formula) {
|
|
387
428
|
delete referrer[prop.name];
|
|
388
429
|
}
|
|
@@ -424,13 +465,13 @@ export class UnitOfWork {
|
|
|
424
465
|
const inserts = {};
|
|
425
466
|
for (const cs of this.changeSets.values()) {
|
|
426
467
|
if (cs.type === ChangeSetType.CREATE) {
|
|
427
|
-
inserts[cs.meta.
|
|
428
|
-
inserts[cs.meta.
|
|
468
|
+
inserts[cs.meta.uniqueName] ??= [];
|
|
469
|
+
inserts[cs.meta.uniqueName].push(cs);
|
|
429
470
|
}
|
|
430
471
|
}
|
|
431
472
|
for (const cs of this.changeSets.values()) {
|
|
432
473
|
if (cs.type === ChangeSetType.UPDATE) {
|
|
433
|
-
this.findEarlyUpdates(cs, inserts[cs.meta.
|
|
474
|
+
this.findEarlyUpdates(cs, inserts[cs.meta.uniqueName]);
|
|
434
475
|
}
|
|
435
476
|
}
|
|
436
477
|
for (const entity of this.removeStack) {
|
|
@@ -441,7 +482,7 @@ export class UnitOfWork {
|
|
|
441
482
|
}
|
|
442
483
|
const deletePkHash = [wrapped.getSerializedPrimaryKey(), ...this.expandUniqueProps(entity)];
|
|
443
484
|
let type = ChangeSetType.DELETE;
|
|
444
|
-
for (const cs of inserts[wrapped.__meta.
|
|
485
|
+
for (const cs of inserts[wrapped.__meta.uniqueName] ?? []) {
|
|
445
486
|
if (deletePkHash.some(hash => hash === cs.getSerializedPrimaryKey() || this.expandUniqueProps(cs.entity).find(child => hash === child))) {
|
|
446
487
|
type = ChangeSetType.DELETE_EARLY;
|
|
447
488
|
}
|
|
@@ -460,7 +501,7 @@ export class UnitOfWork {
|
|
|
460
501
|
}
|
|
461
502
|
for (const cs of this.changeSets.values()) {
|
|
462
503
|
for (const prop of props) {
|
|
463
|
-
if (prop.name in cs.payload && cs.
|
|
504
|
+
if (prop.name in cs.payload && cs.rootMeta === changeSet.rootMeta && cs.type === changeSet.type) {
|
|
464
505
|
conflicts = true;
|
|
465
506
|
if (changeSet.payload[prop.name] == null) {
|
|
466
507
|
type = ChangeSetType.UPDATE_EARLY;
|
|
@@ -479,9 +520,10 @@ export class UnitOfWork {
|
|
|
479
520
|
}
|
|
480
521
|
scheduleOrphanRemoval(entity, visited) {
|
|
481
522
|
if (entity) {
|
|
482
|
-
helper(entity)
|
|
523
|
+
const wrapped = helper(entity);
|
|
524
|
+
wrapped.__em = this.em;
|
|
483
525
|
this.orphanRemoveStack.add(entity);
|
|
484
|
-
this.queuedActions.add(
|
|
526
|
+
this.queuedActions.add(wrapped.__meta.class);
|
|
485
527
|
this.cascade(entity, Cascade.SCHEDULE_ORPHAN_REMOVAL, visited);
|
|
486
528
|
}
|
|
487
529
|
}
|
|
@@ -517,7 +559,68 @@ export class UnitOfWork {
|
|
|
517
559
|
}
|
|
518
560
|
const changeSet = this.changeSetComputer.computeChangeSet(entity);
|
|
519
561
|
if (changeSet && !this.checkUniqueProps(changeSet)) {
|
|
520
|
-
|
|
562
|
+
// For TPT child entities, create changesets for each table in hierarchy
|
|
563
|
+
if (wrapped.__meta.inheritanceType === 'tpt' && wrapped.__meta.tptParent) {
|
|
564
|
+
this.createTPTChangeSets(entity, changeSet);
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
this.changeSets.set(entity, changeSet);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* For TPT inheritance, creates separate changesets for each table in the hierarchy.
|
|
573
|
+
* Uses the same entity instance for all changesets - only the metadata and payload differ.
|
|
574
|
+
*/
|
|
575
|
+
createTPTChangeSets(entity, originalChangeSet) {
|
|
576
|
+
const meta = helper(entity).__meta;
|
|
577
|
+
const isCreate = originalChangeSet.type === ChangeSetType.CREATE;
|
|
578
|
+
let current = meta;
|
|
579
|
+
let leafCs;
|
|
580
|
+
const parentChangeSets = [];
|
|
581
|
+
while (current) {
|
|
582
|
+
const isRoot = !current.tptParent;
|
|
583
|
+
const payload = {};
|
|
584
|
+
for (const prop of current.ownProps) {
|
|
585
|
+
if (prop.name in originalChangeSet.payload) {
|
|
586
|
+
payload[prop.name] = originalChangeSet.payload[prop.name];
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
// For CREATE on non-root tables, include the PK (EntityIdentifier for deferred resolution)
|
|
590
|
+
if (isCreate && !isRoot) {
|
|
591
|
+
const wrapped = helper(entity);
|
|
592
|
+
const identifier = wrapped.__identifier;
|
|
593
|
+
const identifiers = Array.isArray(identifier) ? identifier : [identifier];
|
|
594
|
+
for (let i = 0; i < current.primaryKeys.length; i++) {
|
|
595
|
+
const pk = current.primaryKeys[i];
|
|
596
|
+
payload[pk] = identifiers[i] ?? originalChangeSet.payload[pk];
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (!isCreate && Object.keys(payload).length === 0) {
|
|
600
|
+
current = current.tptParent;
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
const cs = new ChangeSet(entity, originalChangeSet.type, payload, current);
|
|
604
|
+
if (current === meta) {
|
|
605
|
+
cs.originalEntity = originalChangeSet.originalEntity;
|
|
606
|
+
leafCs = cs;
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
parentChangeSets.push(cs);
|
|
610
|
+
}
|
|
611
|
+
current = current.tptParent;
|
|
612
|
+
}
|
|
613
|
+
// When only parent properties changed (UPDATE), leaf payload is empty—create a stub anchor
|
|
614
|
+
if (!leafCs && parentChangeSets.length > 0) {
|
|
615
|
+
leafCs = new ChangeSet(entity, originalChangeSet.type, {}, meta);
|
|
616
|
+
leafCs.originalEntity = originalChangeSet.originalEntity;
|
|
617
|
+
}
|
|
618
|
+
// Store the leaf changeset in the main map (entity as key), with parent CSs attached
|
|
619
|
+
if (leafCs) {
|
|
620
|
+
if (parentChangeSets.length > 0) {
|
|
621
|
+
leafCs.tptChangeSets = parentChangeSets;
|
|
622
|
+
}
|
|
623
|
+
this.changeSets.set(entity, leafCs);
|
|
521
624
|
}
|
|
522
625
|
}
|
|
523
626
|
/**
|
|
@@ -630,7 +733,7 @@ export class UnitOfWork {
|
|
|
630
733
|
const copy = this.comparator.prepareEntity(changeSet.entity);
|
|
631
734
|
await this.eventManager.dispatchEvent(type, { entity: changeSet.entity, meta, em: this.em, changeSet });
|
|
632
735
|
const current = this.comparator.prepareEntity(changeSet.entity);
|
|
633
|
-
const diff = this.comparator.diffEntities(changeSet.
|
|
736
|
+
const diff = this.comparator.diffEntities(changeSet.meta.class, copy, current);
|
|
634
737
|
Object.assign(changeSet.payload, diff);
|
|
635
738
|
const wrapped = helper(changeSet.entity);
|
|
636
739
|
if (wrapped.__identifier) {
|
|
@@ -739,26 +842,26 @@ export class UnitOfWork {
|
|
|
739
842
|
}
|
|
740
843
|
fixMissingReference(entity, prop) {
|
|
741
844
|
const reference = entity[prop.name];
|
|
742
|
-
const
|
|
743
|
-
if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) &&
|
|
744
|
-
if (!Utils.isEntity(
|
|
745
|
-
entity[prop.name] = this.em.getReference(prop.
|
|
845
|
+
const target = Reference.unwrapReference(reference);
|
|
846
|
+
if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && target && !prop.mapToPk) {
|
|
847
|
+
if (!Utils.isEntity(target)) {
|
|
848
|
+
entity[prop.name] = this.em.getReference(prop.targetMeta.class, target, { wrapped: !!prop.ref });
|
|
746
849
|
}
|
|
747
|
-
else if (!helper(
|
|
748
|
-
const pk = helper(
|
|
749
|
-
entity[prop.name] = this.em.getReference(prop.
|
|
850
|
+
else if (!helper(target).__initialized && !helper(target).__em) {
|
|
851
|
+
const pk = helper(target).getPrimaryKey();
|
|
852
|
+
entity[prop.name] = this.em.getReference(prop.targetMeta.class, pk, { wrapped: !!prop.ref });
|
|
750
853
|
}
|
|
751
854
|
}
|
|
752
|
-
// perf: set the `Collection._property` to skip the getter, as it can be slow when there
|
|
753
|
-
if (Utils.isCollection(
|
|
754
|
-
|
|
855
|
+
// perf: set the `Collection._property` to skip the getter, as it can be slow when there are a lot of relations
|
|
856
|
+
if (Utils.isCollection(target)) {
|
|
857
|
+
target.property = prop;
|
|
755
858
|
}
|
|
756
859
|
const isCollection = [ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind);
|
|
757
|
-
if (isCollection && Array.isArray(
|
|
860
|
+
if (isCollection && Array.isArray(target)) {
|
|
758
861
|
const collection = new Collection(entity);
|
|
759
862
|
collection.property = prop;
|
|
760
863
|
entity[prop.name] = collection;
|
|
761
|
-
collection.set(
|
|
864
|
+
collection.set(target);
|
|
762
865
|
}
|
|
763
866
|
}
|
|
764
867
|
async persistToDatabase(groups, ctx) {
|
|
@@ -768,30 +871,30 @@ export class UnitOfWork {
|
|
|
768
871
|
const commitOrder = this.getCommitOrder();
|
|
769
872
|
const commitOrderReversed = [...commitOrder].reverse();
|
|
770
873
|
// early delete - when we recreate entity in the same UoW, we need to issue those delete queries before inserts
|
|
771
|
-
for (const
|
|
772
|
-
await this.commitDeleteChangeSets(groups[ChangeSetType.DELETE_EARLY].get(
|
|
874
|
+
for (const meta of commitOrderReversed) {
|
|
875
|
+
await this.commitDeleteChangeSets(groups[ChangeSetType.DELETE_EARLY].get(meta) ?? [], ctx);
|
|
773
876
|
}
|
|
774
877
|
// early update - when we recreate entity in the same UoW, we need to issue those delete queries before inserts
|
|
775
|
-
for (const
|
|
776
|
-
await this.commitUpdateChangeSets(groups[ChangeSetType.UPDATE_EARLY].get(
|
|
878
|
+
for (const meta of commitOrder) {
|
|
879
|
+
await this.commitUpdateChangeSets(groups[ChangeSetType.UPDATE_EARLY].get(meta) ?? [], ctx);
|
|
777
880
|
}
|
|
778
881
|
// extra updates
|
|
779
882
|
await this.commitExtraUpdates(ChangeSetType.UPDATE_EARLY, ctx);
|
|
780
883
|
// create
|
|
781
|
-
for (const
|
|
782
|
-
await this.commitCreateChangeSets(groups[ChangeSetType.CREATE].get(
|
|
884
|
+
for (const meta of commitOrder) {
|
|
885
|
+
await this.commitCreateChangeSets(groups[ChangeSetType.CREATE].get(meta) ?? [], ctx);
|
|
783
886
|
}
|
|
784
887
|
// update
|
|
785
|
-
for (const
|
|
786
|
-
await this.commitUpdateChangeSets(groups[ChangeSetType.UPDATE].get(
|
|
888
|
+
for (const meta of commitOrder) {
|
|
889
|
+
await this.commitUpdateChangeSets(groups[ChangeSetType.UPDATE].get(meta) ?? [], ctx);
|
|
787
890
|
}
|
|
788
891
|
// extra updates
|
|
789
892
|
await this.commitExtraUpdates(ChangeSetType.UPDATE, ctx);
|
|
790
893
|
// collection updates
|
|
791
894
|
await this.commitCollectionUpdates(ctx);
|
|
792
895
|
// delete - entity deletions need to be in reverse commit order
|
|
793
|
-
for (const
|
|
794
|
-
await this.commitDeleteChangeSets(groups[ChangeSetType.DELETE].get(
|
|
896
|
+
for (const meta of commitOrderReversed) {
|
|
897
|
+
await this.commitDeleteChangeSets(groups[ChangeSetType.DELETE].get(meta) ?? [], ctx);
|
|
795
898
|
}
|
|
796
899
|
// take snapshots of all persisted collections
|
|
797
900
|
const visited = new Set();
|
|
@@ -827,16 +930,28 @@ export class UnitOfWork {
|
|
|
827
930
|
if (Utils.isCollection(ref)) {
|
|
828
931
|
ref.getItems(false).some(item => {
|
|
829
932
|
const cs = this.changeSets.get(Reference.unwrapReference(item));
|
|
830
|
-
const isScheduledForInsert = cs
|
|
933
|
+
const isScheduledForInsert = cs?.type === ChangeSetType.CREATE && !cs.persisted;
|
|
831
934
|
if (isScheduledForInsert) {
|
|
832
935
|
this.scheduleExtraUpdate(changeSet, [prop]);
|
|
833
936
|
return true;
|
|
834
937
|
}
|
|
835
938
|
return false;
|
|
836
939
|
});
|
|
940
|
+
continue;
|
|
837
941
|
}
|
|
838
|
-
const
|
|
839
|
-
|
|
942
|
+
const refEntity = Reference.unwrapReference(ref);
|
|
943
|
+
// For mapToPk properties, the value is a primitive (string/array), not an entity
|
|
944
|
+
if (!Utils.isEntity(refEntity)) {
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
// For TPT entities, check if the ROOT table's changeset has been persisted
|
|
948
|
+
// (since the FK is to the root table, not the concrete entity's table)
|
|
949
|
+
let cs = this.changeSets.get(refEntity);
|
|
950
|
+
if (cs?.tptChangeSets?.length) {
|
|
951
|
+
// Root table changeset is the last one (ordered immediate parent → root)
|
|
952
|
+
cs = cs.tptChangeSets[cs.tptChangeSets.length - 1];
|
|
953
|
+
}
|
|
954
|
+
const isScheduledForInsert = cs?.type === ChangeSetType.CREATE && !cs.persisted;
|
|
840
955
|
if (isScheduledForInsert) {
|
|
841
956
|
this.scheduleExtraUpdate(changeSet, [prop]);
|
|
842
957
|
}
|
|
@@ -958,12 +1073,23 @@ export class UnitOfWork {
|
|
|
958
1073
|
[ChangeSetType.UPDATE_EARLY]: new Map(),
|
|
959
1074
|
[ChangeSetType.DELETE_EARLY]: new Map(),
|
|
960
1075
|
};
|
|
961
|
-
|
|
1076
|
+
const addToGroup = (cs) => {
|
|
1077
|
+
// Skip stub TPT changesets with empty payload (e.g. leaf with no own-property changes on UPDATE)
|
|
1078
|
+
if ((cs.type === ChangeSetType.UPDATE || cs.type === ChangeSetType.UPDATE_EARLY) && !Utils.hasObjectKeys(cs.payload)) {
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
962
1081
|
const group = groups[cs.type];
|
|
963
|
-
const
|
|
1082
|
+
const groupKey = cs.meta.inheritanceType === 'tpt' ? cs.meta : cs.rootMeta;
|
|
1083
|
+
const classGroup = group.get(groupKey) ?? [];
|
|
964
1084
|
classGroup.push(cs);
|
|
965
|
-
if (!group.has(
|
|
966
|
-
group.set(
|
|
1085
|
+
if (!group.has(groupKey)) {
|
|
1086
|
+
group.set(groupKey, classGroup);
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
for (const cs of this.changeSets.values()) {
|
|
1090
|
+
addToGroup(cs);
|
|
1091
|
+
for (const parentCs of cs.tptChangeSets ?? []) {
|
|
1092
|
+
addToGroup(parentCs);
|
|
967
1093
|
}
|
|
968
1094
|
}
|
|
969
1095
|
return groups;
|
|
@@ -971,14 +1097,35 @@ export class UnitOfWork {
|
|
|
971
1097
|
getCommitOrder() {
|
|
972
1098
|
const calc = new CommitOrderCalculator();
|
|
973
1099
|
const set = new Set();
|
|
974
|
-
this.changeSets.forEach(cs =>
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1100
|
+
this.changeSets.forEach(cs => {
|
|
1101
|
+
if (cs.meta.inheritanceType === 'tpt') {
|
|
1102
|
+
set.add(cs.meta);
|
|
1103
|
+
for (const parentCs of cs.tptChangeSets ?? []) {
|
|
1104
|
+
set.add(parentCs.meta);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
else {
|
|
1108
|
+
set.add(cs.rootMeta);
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1111
|
+
set.forEach(meta => calc.addNode(meta._id));
|
|
1112
|
+
for (const meta of set) {
|
|
1113
|
+
for (const prop of meta.relations) {
|
|
1114
|
+
if (prop.polymorphTargets) {
|
|
1115
|
+
for (const targetMeta of prop.polymorphTargets) {
|
|
1116
|
+
calc.discoverProperty({ ...prop, targetMeta }, meta._id);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
else {
|
|
1120
|
+
calc.discoverProperty(prop, meta._id);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
// For TPT, parent table must be inserted BEFORE child tables
|
|
1124
|
+
if (meta.inheritanceType === 'tpt' && meta.tptParent && set.has(meta.tptParent)) {
|
|
1125
|
+
calc.addDependency(meta.tptParent._id, meta._id, 1);
|
|
979
1126
|
}
|
|
980
1127
|
}
|
|
981
|
-
return calc.sort();
|
|
1128
|
+
return calc.sort().map(id => this.metadata.getById(id));
|
|
982
1129
|
}
|
|
983
1130
|
resetTransaction(oldTx) {
|
|
984
1131
|
if (oldTx) {
|