@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.
Files changed (107) hide show
  1. package/EntityManager.d.ts +34 -17
  2. package/EntityManager.js +95 -103
  3. package/MikroORM.d.ts +5 -5
  4. package/MikroORM.js +25 -20
  5. package/cache/FileCacheAdapter.js +11 -3
  6. package/connections/Connection.d.ts +3 -2
  7. package/connections/Connection.js +4 -3
  8. package/drivers/DatabaseDriver.d.ts +11 -11
  9. package/drivers/DatabaseDriver.js +91 -25
  10. package/drivers/IDatabaseDriver.d.ts +50 -20
  11. package/entity/BaseEntity.d.ts +61 -1
  12. package/entity/Collection.d.ts +8 -1
  13. package/entity/Collection.js +12 -13
  14. package/entity/EntityAssigner.js +9 -9
  15. package/entity/EntityFactory.d.ts +6 -1
  16. package/entity/EntityFactory.js +40 -22
  17. package/entity/EntityHelper.d.ts +2 -2
  18. package/entity/EntityHelper.js +27 -4
  19. package/entity/EntityLoader.d.ts +5 -4
  20. package/entity/EntityLoader.js +193 -80
  21. package/entity/EntityRepository.d.ts +27 -7
  22. package/entity/EntityRepository.js +8 -2
  23. package/entity/PolymorphicRef.d.ts +12 -0
  24. package/entity/PolymorphicRef.js +18 -0
  25. package/entity/WrappedEntity.d.ts +2 -2
  26. package/entity/WrappedEntity.js +1 -1
  27. package/entity/defineEntity.d.ts +89 -50
  28. package/entity/defineEntity.js +12 -0
  29. package/entity/index.d.ts +1 -0
  30. package/entity/index.js +1 -0
  31. package/entity/utils.d.ts +6 -1
  32. package/entity/utils.js +33 -0
  33. package/entity/validators.js +2 -2
  34. package/enums.d.ts +2 -2
  35. package/enums.js +1 -0
  36. package/errors.d.ts +16 -8
  37. package/errors.js +40 -13
  38. package/hydration/ObjectHydrator.js +63 -21
  39. package/index.d.ts +1 -1
  40. package/logging/colors.d.ts +1 -1
  41. package/logging/colors.js +7 -6
  42. package/logging/inspect.js +1 -6
  43. package/metadata/EntitySchema.d.ts +43 -13
  44. package/metadata/EntitySchema.js +82 -27
  45. package/metadata/MetadataDiscovery.d.ts +60 -3
  46. package/metadata/MetadataDiscovery.js +665 -154
  47. package/metadata/MetadataProvider.js +3 -1
  48. package/metadata/MetadataStorage.d.ts +13 -6
  49. package/metadata/MetadataStorage.js +64 -19
  50. package/metadata/MetadataValidator.d.ts +32 -2
  51. package/metadata/MetadataValidator.js +196 -31
  52. package/metadata/discover-entities.js +5 -5
  53. package/metadata/types.d.ts +111 -14
  54. package/naming-strategy/AbstractNamingStrategy.d.ts +11 -3
  55. package/naming-strategy/AbstractNamingStrategy.js +12 -0
  56. package/naming-strategy/EntityCaseNamingStrategy.d.ts +3 -3
  57. package/naming-strategy/EntityCaseNamingStrategy.js +6 -5
  58. package/naming-strategy/MongoNamingStrategy.d.ts +3 -3
  59. package/naming-strategy/MongoNamingStrategy.js +6 -6
  60. package/naming-strategy/NamingStrategy.d.ts +17 -3
  61. package/naming-strategy/UnderscoreNamingStrategy.d.ts +3 -3
  62. package/naming-strategy/UnderscoreNamingStrategy.js +6 -6
  63. package/package.json +2 -2
  64. package/platforms/Platform.d.ts +4 -2
  65. package/platforms/Platform.js +5 -2
  66. package/serialization/EntitySerializer.d.ts +3 -0
  67. package/serialization/EntitySerializer.js +15 -13
  68. package/serialization/EntityTransformer.js +6 -6
  69. package/serialization/SerializationContext.d.ts +6 -6
  70. package/typings.d.ts +325 -110
  71. package/typings.js +84 -17
  72. package/unit-of-work/ChangeSet.d.ts +4 -3
  73. package/unit-of-work/ChangeSet.js +2 -3
  74. package/unit-of-work/ChangeSetComputer.d.ts +3 -6
  75. package/unit-of-work/ChangeSetComputer.js +34 -13
  76. package/unit-of-work/ChangeSetPersister.d.ts +12 -10
  77. package/unit-of-work/ChangeSetPersister.js +55 -25
  78. package/unit-of-work/CommitOrderCalculator.d.ts +12 -10
  79. package/unit-of-work/CommitOrderCalculator.js +13 -13
  80. package/unit-of-work/IdentityMap.d.ts +12 -0
  81. package/unit-of-work/IdentityMap.js +39 -1
  82. package/unit-of-work/UnitOfWork.d.ts +21 -3
  83. package/unit-of-work/UnitOfWork.js +203 -56
  84. package/utils/AbstractSchemaGenerator.js +17 -8
  85. package/utils/AsyncContext.d.ts +6 -0
  86. package/utils/AsyncContext.js +42 -0
  87. package/utils/Configuration.d.ts +52 -11
  88. package/utils/Configuration.js +12 -8
  89. package/utils/Cursor.js +21 -8
  90. package/utils/DataloaderUtils.js +13 -11
  91. package/utils/EntityComparator.d.ts +14 -7
  92. package/utils/EntityComparator.js +132 -46
  93. package/utils/QueryHelper.d.ts +16 -6
  94. package/utils/QueryHelper.js +53 -18
  95. package/utils/RawQueryFragment.d.ts +28 -23
  96. package/utils/RawQueryFragment.js +34 -56
  97. package/utils/RequestContext.js +2 -2
  98. package/utils/TransactionContext.js +2 -2
  99. package/utils/TransactionManager.js +1 -1
  100. package/utils/Utils.d.ts +7 -26
  101. package/utils/Utils.js +25 -79
  102. package/utils/clone.js +7 -21
  103. package/utils/env-vars.d.ts +4 -0
  104. package/utils/env-vars.js +13 -3
  105. package/utils/fs-utils.d.ts +21 -0
  106. package/utils/fs-utils.js +106 -11
  107. 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 hash in this.nodes;
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[hash] = { hash, state: 0 /* NodeState.NOT_VISITED */, dependencies: {} };
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[from].dependencies[to] = { from, to, weight };
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.className;
48
- if (!propertyType || !this.hasNode(propertyType)) {
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 Object.values(this.nodes)) {
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 Object.values(node.dependencies)) {
79
- const target = this.nodes[edge.to];
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[node.hash] || target.dependencies[node.hash].weight >= edge.weight) {
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 Object.values(target.dependencies)) {
101
- const targetNode = this.nodes[edge.to];
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
- this.getStore(item.__meta.root).delete(this.getPkHash(item));
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: string, id: Primary<T> | Primary<T>[], schema?: string, convertCustomTypes?: boolean): T | undefined;
49
- tryGetById<T extends object>(entityName: string, where: FilterQuery<T>, schema?: string, strict?: boolean): T | null;
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 = new AsyncLocalStorage();
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.collectionUpdates, this.metadata, this.platform, this.em.config, this.em);
46
- this.changeSetPersister = new ChangeSetPersister(this.em.getDriver(), this.metadata, this.em.config.getHydrator(this.metadata), this.em.getEntityFactory(), this.em.config, 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
- data[prop.name] = Utils.getPrimaryKeyValues(data[prop.name], prop.targetMeta, true);
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.className) || this.queuedActions.has(meta.root.className)) {
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.className);
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.className);
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.name, helper(entity).__primaryKeys, helper(entity).__schema)) {
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.name);
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.type, rel)).getSerializedPrimaryKey() === serializedPK : rel === entity)) {
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.className] ??= [];
428
- inserts[cs.meta.className].push(cs);
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.className]);
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.className] ?? []) {
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.rootName === changeSet.rootName && cs.type === changeSet.type) {
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).__em = this.em;
523
+ const wrapped = helper(entity);
524
+ wrapped.__em = this.em;
483
525
  this.orphanRemoveStack.add(entity);
484
- this.queuedActions.add(entity.__meta.className);
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
- this.changeSets.set(entity, changeSet);
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.name, copy, current);
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 kind = Reference.unwrapReference(reference);
743
- if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && kind && !prop.mapToPk) {
744
- if (!Utils.isEntity(kind)) {
745
- entity[prop.name] = this.em.getReference(prop.type, kind, { wrapped: !!prop.ref });
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(kind).__initialized && !helper(kind).__em) {
748
- const pk = helper(kind).getPrimaryKey();
749
- entity[prop.name] = this.em.getReference(prop.type, pk, { wrapped: !!prop.ref });
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 is a lot of relations
753
- if (Utils.isCollection(kind)) {
754
- kind.property = prop;
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(kind)) {
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(kind);
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 name of commitOrderReversed) {
772
- await this.commitDeleteChangeSets(groups[ChangeSetType.DELETE_EARLY].get(name) ?? [], ctx);
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 name of commitOrder) {
776
- await this.commitUpdateChangeSets(groups[ChangeSetType.UPDATE_EARLY].get(name) ?? [], ctx);
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 name of commitOrder) {
782
- await this.commitCreateChangeSets(groups[ChangeSetType.CREATE].get(name) ?? [], ctx);
884
+ for (const meta of commitOrder) {
885
+ await this.commitCreateChangeSets(groups[ChangeSetType.CREATE].get(meta) ?? [], ctx);
783
886
  }
784
887
  // update
785
- for (const name of commitOrder) {
786
- await this.commitUpdateChangeSets(groups[ChangeSetType.UPDATE].get(name) ?? [], ctx);
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 name of commitOrderReversed) {
794
- await this.commitDeleteChangeSets(groups[ChangeSetType.DELETE].get(name) ?? [], ctx);
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 && cs.type === ChangeSetType.CREATE && !cs.persisted;
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 cs = this.changeSets.get(Reference.unwrapReference(ref));
839
- const isScheduledForInsert = cs && cs.type === ChangeSetType.CREATE && !cs.persisted;
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
- for (const cs of this.changeSets.values()) {
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 classGroup = group.get(cs.rootName) ?? [];
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(cs.rootName)) {
966
- group.set(cs.rootName, classGroup);
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 => set.add(cs.rootName));
975
- set.forEach(entityName => calc.addNode(entityName));
976
- for (const entityName of set) {
977
- for (const prop of this.metadata.find(entityName).props) {
978
- calc.discoverProperty(prop, entityName);
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) {