@mikro-orm/core 7.2.2-dev.3 → 7.2.2-dev.30
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 +6 -1
- package/EntityManager.js +81 -27
- package/entity/EntityFactory.d.ts +1 -0
- package/entity/EntityFactory.js +32 -3
- package/entity/EntityLoader.js +7 -6
- package/entity/WrappedEntity.js +6 -1
- package/entity/defineEntity.d.ts +1 -1
- package/entity/defineEntity.js +1 -1
- package/errors.d.ts +1 -0
- package/errors.js +3 -0
- package/hydration/ObjectHydrator.js +2 -2
- package/metadata/MetadataDiscovery.d.ts +2 -0
- package/metadata/MetadataDiscovery.js +38 -5
- package/metadata/MetadataStorage.d.ts +7 -0
- package/metadata/MetadataStorage.js +27 -0
- package/metadata/types.d.ts +1 -1
- package/package.json +1 -1
- package/typings.d.ts +3 -0
- package/unit-of-work/ChangeSetComputer.js +20 -6
- package/unit-of-work/ChangeSetPersister.js +9 -7
- package/unit-of-work/IdentityMap.d.ts +3 -2
- package/unit-of-work/IdentityMap.js +30 -7
- package/unit-of-work/UnitOfWork.js +29 -3
- package/utils/EntityComparator.js +43 -15
- package/utils/QueryHelper.d.ts +6 -0
- package/utils/QueryHelper.js +51 -8
- package/utils/TransactionContext.d.ts +1 -0
- package/utils/TransactionContext.js +9 -3
- package/utils/TransactionManager.js +6 -2
- package/utils/Utils.js +1 -1
- package/utils/upsert-utils.d.ts +2 -1
- package/utils/upsert-utils.js +9 -5
package/metadata/types.d.ts
CHANGED
|
@@ -175,7 +175,7 @@ export interface PropertyOptions<Owner> {
|
|
|
175
175
|
*/
|
|
176
176
|
autoincrement?: boolean;
|
|
177
177
|
/**
|
|
178
|
-
* Add the property to the `returning`
|
|
178
|
+
* Add the property to the `returning` clause of inserts, updates and upserts, reloading the database value into the entity.
|
|
179
179
|
*/
|
|
180
180
|
returning?: boolean;
|
|
181
181
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/core",
|
|
3
|
-
"version": "7.2.2-dev.
|
|
3
|
+
"version": "7.2.2-dev.30",
|
|
4
4
|
"description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"data-mapper",
|
package/typings.d.ts
CHANGED
|
@@ -1173,6 +1173,7 @@ export interface EntityMetadata<Entity = any, Class extends EntityCtor<Entity> =
|
|
|
1173
1173
|
meta: EntityMetadata<Entity>;
|
|
1174
1174
|
prop: EntityProperty<Entity>;
|
|
1175
1175
|
}[];
|
|
1176
|
+
targetKeys?: EntityKey<Entity>[];
|
|
1176
1177
|
comparableProps: EntityProperty<Entity>[];
|
|
1177
1178
|
trackingProps: EntityProperty<Entity>[];
|
|
1178
1179
|
hydrateProps: EntityProperty<Entity>[];
|
|
@@ -1254,6 +1255,8 @@ export interface EntityMetadata<Entity = any, Class extends EntityCtor<Entity> =
|
|
|
1254
1255
|
orderBy?: QueryOrderMap<Entity> | QueryOrderMap<Entity>[];
|
|
1255
1256
|
/** @internal can be used for computed numeric cache keys */
|
|
1256
1257
|
readonly _id: number;
|
|
1258
|
+
/** @internal name that does not depend on module evaluation order (unlike `_id`), used to key `compiledFunctions` */
|
|
1259
|
+
compiledName?: string;
|
|
1257
1260
|
}
|
|
1258
1261
|
/** Options for `ISchemaGenerator.create()`. */
|
|
1259
1262
|
export interface CreateSchemaOptions {
|
|
@@ -7,6 +7,7 @@ import { Reference } from '../entity/Reference.js';
|
|
|
7
7
|
import { PolymorphicRef } from '../entity/PolymorphicRef.js';
|
|
8
8
|
import { ReferenceKind } from '../enums.js';
|
|
9
9
|
import { isRaw } from '../utils/RawQueryFragment.js';
|
|
10
|
+
import { ValidationError } from '../errors.js';
|
|
10
11
|
/** @internal Computes change sets by comparing entity state against original snapshots. */
|
|
11
12
|
export class ChangeSetComputer {
|
|
12
13
|
#comparator;
|
|
@@ -48,6 +49,10 @@ export class ChangeSetComputer {
|
|
|
48
49
|
if (Utils.equals(data, wrapped.__originalEntityData)) {
|
|
49
50
|
return null;
|
|
50
51
|
}
|
|
52
|
+
// a reference known only by its `targetKey` has no PK, so the update would have no condition
|
|
53
|
+
if (!wrapped.hasPrimaryKey()) {
|
|
54
|
+
throw ValidationError.referenceWithoutPK(entity);
|
|
55
|
+
}
|
|
51
56
|
}
|
|
52
57
|
const changeSet = new ChangeSet(entity, type, this.computePayload(entity), meta);
|
|
53
58
|
changeSet.originalEntity = wrapped.__originalEntityData;
|
|
@@ -162,21 +167,30 @@ export class ChangeSetComputer {
|
|
|
162
167
|
const target = Reference.unwrapReference(rawTarget);
|
|
163
168
|
const needsProcessing = target != null && (prop.targetKey != null || !target.__helper.hasPrimaryKey());
|
|
164
169
|
if (needsProcessing) {
|
|
170
|
+
// a reference known only by its `targetKey` has no PK, and is never inserted to generate one
|
|
171
|
+
if (!prop.targetKey && !target.__helper.__initialized) {
|
|
172
|
+
throw ValidationError.referenceWithoutPK(target);
|
|
173
|
+
}
|
|
165
174
|
let value = prop.targetKey ? target[prop.targetKey] : target.__helper.__identifier;
|
|
166
|
-
|
|
167
|
-
if (prop.targetKey
|
|
168
|
-
const targetProp =
|
|
175
|
+
// use the actual target's meta, a polymorphic relation has several
|
|
176
|
+
if (prop.targetKey) {
|
|
177
|
+
const targetProp = target.__meta.properties[prop.targetKey];
|
|
169
178
|
if (targetProp?.customType) {
|
|
170
179
|
value = targetProp.customType.convertToDatabaseValue(value, this.#platform, { mode: 'serialization' });
|
|
171
180
|
}
|
|
172
181
|
}
|
|
173
182
|
if (prop.polymorphic) {
|
|
174
183
|
const discriminator = QueryHelper.findDiscriminatorValue(prop.discriminatorMap, target.constructor);
|
|
175
|
-
|
|
184
|
+
value = new PolymorphicRef(discriminator, value);
|
|
176
185
|
}
|
|
177
|
-
|
|
178
|
-
|
|
186
|
+
// an unchanged `targetKey` FK is not part of the diff
|
|
187
|
+
if (prop.targetKey &&
|
|
188
|
+
changeSet.type === ChangeSetType.UPDATE &&
|
|
189
|
+
!(prop.name in changeSet.payload) &&
|
|
190
|
+
Utils.equals(changeSet.originalEntity?.[prop.name], value)) {
|
|
191
|
+
return;
|
|
179
192
|
}
|
|
193
|
+
Utils.setPayloadProperty(changeSet.payload, changeSet.meta, prop, value, idx);
|
|
180
194
|
}
|
|
181
195
|
});
|
|
182
196
|
}
|
|
@@ -207,7 +207,7 @@ export class ChangeSetPersister {
|
|
|
207
207
|
const keys = meta.getOwnConcurrencyCheckKeys();
|
|
208
208
|
for (const key of keys) {
|
|
209
209
|
cond[key] = changeSet.originalEntity[key];
|
|
210
|
-
if (changeSet.payload
|
|
210
|
+
if (Object.hasOwn(changeSet.payload, key)) {
|
|
211
211
|
tmp.push(key);
|
|
212
212
|
}
|
|
213
213
|
}
|
|
@@ -322,17 +322,19 @@ export class ChangeSetPersister {
|
|
|
322
322
|
}
|
|
323
323
|
// skip entity references as they don't have version values loaded
|
|
324
324
|
changeSets = changeSets.filter(cs => helper(cs.entity).__initialized);
|
|
325
|
-
const
|
|
325
|
+
const fields = meta.primaryKeys.concat(...concurrencyCheckKeys);
|
|
326
326
|
const $or = changeSets.map(cs => {
|
|
327
|
-
const cond = Utils.getPrimaryKeyCond(cs.originalEntity, primaryKeys);
|
|
327
|
+
const cond = Utils.getPrimaryKeyCond(cs.originalEntity, meta.primaryKeys);
|
|
328
|
+
for (const key of concurrencyCheckKeys) {
|
|
329
|
+
cond[key] = cs.originalEntity[key];
|
|
330
|
+
}
|
|
328
331
|
if (meta.ownsVersionProperty()) {
|
|
329
|
-
// @ts-ignore
|
|
330
332
|
cond[meta.versionProperty] = this.#platform.convertVersionValue(cs.entity[meta.versionProperty], meta.properties[meta.versionProperty]);
|
|
331
333
|
}
|
|
332
334
|
return cond;
|
|
333
335
|
});
|
|
334
336
|
options = this.prepareOptions(meta, options, {
|
|
335
|
-
fields
|
|
337
|
+
fields,
|
|
336
338
|
orderBy: meta.primaryKeys.reduce((o, pk) => {
|
|
337
339
|
o[pk] = 'asc';
|
|
338
340
|
return o;
|
|
@@ -344,8 +346,8 @@ export class ChangeSetPersister {
|
|
|
344
346
|
if (res.length !== changeSets.length) {
|
|
345
347
|
// a FK pointing to a composite PK is an array, so the values need to be compared deeply
|
|
346
348
|
const compare = (a, b, keys) => keys.every(k => equals(a[k], b[k]));
|
|
347
|
-
const entity = changeSets.find(
|
|
348
|
-
return !res.some(row => compare(
|
|
349
|
+
const entity = changeSets.find((_, index) => {
|
|
350
|
+
return !res.some(row => compare($or[index], row, meta.primaryKeys));
|
|
349
351
|
}).entity;
|
|
350
352
|
throw OptimisticLockError.lockFailed(entity);
|
|
351
353
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { AnyEntity, EntityMetadata } from '../typings.js';
|
|
2
|
-
/** @internal Stores managed entity instances keyed by their primary key hash, ensuring each row is loaded once. */
|
|
2
|
+
/** @internal Stores managed entity instances keyed by their primary key hash (and `targetKey` values), ensuring each row is loaded once. */
|
|
3
3
|
export declare class IdentityMap {
|
|
4
4
|
#private;
|
|
5
5
|
constructor(defaultSchema?: string);
|
|
6
|
-
/** Stores an entity in the identity map under its primary key hash. */
|
|
6
|
+
/** Stores an entity in the identity map under its primary key hash and the `targetKey` values referencing it. */
|
|
7
7
|
store<T>(item: T): void;
|
|
8
8
|
/**
|
|
9
9
|
* Stores an entity under an alternate key (non-PK property).
|
|
@@ -25,6 +25,7 @@ export declare class IdentityMap {
|
|
|
25
25
|
/** Returns all entities currently in the identity map. */
|
|
26
26
|
values(): AnyEntity[];
|
|
27
27
|
[Symbol.iterator](): IterableIterator<AnyEntity>;
|
|
28
|
+
private getEntities;
|
|
28
29
|
/** Returns all hash keys currently in the identity map. */
|
|
29
30
|
keys(): string[];
|
|
30
31
|
/**
|
|
@@ -1,15 +1,29 @@
|
|
|
1
|
-
/** @internal Stores managed entity instances keyed by their primary key hash, ensuring each row is loaded once. */
|
|
1
|
+
/** @internal Stores managed entity instances keyed by their primary key hash (and `targetKey` values), ensuring each row is loaded once. */
|
|
2
2
|
export class IdentityMap {
|
|
3
3
|
#defaultSchema;
|
|
4
4
|
#registry = new Map();
|
|
5
5
|
/** Tracks alternate key hashes for each entity so we can clean them up on delete */
|
|
6
6
|
#alternateKeys = new WeakMap();
|
|
7
|
+
#storesWithAlternateKeys = new WeakSet();
|
|
7
8
|
constructor(defaultSchema) {
|
|
8
9
|
this.#defaultSchema = defaultSchema;
|
|
9
10
|
}
|
|
10
|
-
/** Stores an entity in the identity map under its primary key hash. */
|
|
11
|
+
/** Stores an entity in the identity map under its primary key hash and the `targetKey` values referencing it. */
|
|
11
12
|
store(item) {
|
|
12
|
-
|
|
13
|
+
const meta = item.__meta.root;
|
|
14
|
+
const wrapped = item.__helper;
|
|
15
|
+
// PK-less references (known only by a `targetKey`) would share one slot that outlives their later removal
|
|
16
|
+
if (!meta.targetKeys || wrapped.hasPrimaryKey()) {
|
|
17
|
+
this.getStore(meta).set(this.getPkHash(item), item);
|
|
18
|
+
}
|
|
19
|
+
// references resolved via `targetKey` look the entity up by that key instead of the PK
|
|
20
|
+
for (const key of meta.targetKeys ?? []) {
|
|
21
|
+
const value = item[key];
|
|
22
|
+
if (value != null) {
|
|
23
|
+
const schema = wrapped.__schema ?? meta.schema ?? this.#defaultSchema;
|
|
24
|
+
this.storeByKey(item, key, '' + value, schema);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
13
27
|
}
|
|
14
28
|
/**
|
|
15
29
|
* Stores an entity under an alternate key (non-PK property).
|
|
@@ -17,7 +31,9 @@ export class IdentityMap {
|
|
|
17
31
|
*/
|
|
18
32
|
storeByKey(item, key, value, schema) {
|
|
19
33
|
const hash = this.getKeyHash(key, value, schema);
|
|
20
|
-
this.getStore(item.__meta.root)
|
|
34
|
+
const store = this.getStore(item.__meta.root);
|
|
35
|
+
store.set(hash, item);
|
|
36
|
+
this.#storesWithAlternateKeys.add(store);
|
|
21
37
|
// Track this alternate key so we can clean it up when the entity is deleted
|
|
22
38
|
let keys = this.#alternateKeys.get(item);
|
|
23
39
|
if (!keys) {
|
|
@@ -35,7 +51,10 @@ export class IdentityMap {
|
|
|
35
51
|
const altKeys = this.#alternateKeys.get(item);
|
|
36
52
|
if (altKeys) {
|
|
37
53
|
for (const hash of altKeys) {
|
|
38
|
-
|
|
54
|
+
// the hash might be owned by another entity by now, e.g. after swapping unique values
|
|
55
|
+
if (store.get(hash) === item) {
|
|
56
|
+
store.delete(hash);
|
|
57
|
+
}
|
|
39
58
|
}
|
|
40
59
|
this.#alternateKeys.delete(item);
|
|
41
60
|
}
|
|
@@ -69,7 +88,7 @@ export class IdentityMap {
|
|
|
69
88
|
values() {
|
|
70
89
|
const ret = [];
|
|
71
90
|
for (const store of this.#registry.values()) {
|
|
72
|
-
for (const item of
|
|
91
|
+
for (const item of this.getEntities(store)) {
|
|
73
92
|
ret.push(item);
|
|
74
93
|
}
|
|
75
94
|
}
|
|
@@ -77,11 +96,15 @@ export class IdentityMap {
|
|
|
77
96
|
}
|
|
78
97
|
*[Symbol.iterator]() {
|
|
79
98
|
for (const store of this.#registry.values()) {
|
|
80
|
-
for (const item of
|
|
99
|
+
for (const item of this.getEntities(store)) {
|
|
81
100
|
yield item;
|
|
82
101
|
}
|
|
83
102
|
}
|
|
84
103
|
}
|
|
104
|
+
getEntities(store) {
|
|
105
|
+
// an entity indexed by alternate keys occupies several slots of its store
|
|
106
|
+
return this.#storesWithAlternateKeys.has(store) ? new Set(store.values()) : store.values();
|
|
107
|
+
}
|
|
85
108
|
/** Returns all hash keys currently in the identity map. */
|
|
86
109
|
keys() {
|
|
87
110
|
const ret = [];
|
|
@@ -89,7 +89,10 @@ export class UnitOfWork {
|
|
|
89
89
|
Utils.isPlainObject(data[prop.name])) {
|
|
90
90
|
// Skip polymorphic relations - they use PolymorphicRef wrapper
|
|
91
91
|
if (!prop.polymorphic) {
|
|
92
|
-
|
|
92
|
+
// with `targetKey`, the FK holds that value instead of the PK
|
|
93
|
+
data[prop.name] = prop.targetKey
|
|
94
|
+
? data[prop.name][prop.targetKey]
|
|
95
|
+
: Utils.getPrimaryKeyValues(data[prop.name], prop.targetMeta, true);
|
|
93
96
|
}
|
|
94
97
|
}
|
|
95
98
|
else if (prop.kind === ReferenceKind.EMBEDDED && !prop.object && Utils.isPlainObject(data[prop.name])) {
|
|
@@ -205,7 +208,18 @@ export class UnitOfWork {
|
|
|
205
208
|
value = prop.customType.convertToJSValue(value, this.#platform, { mode: 'hydration' });
|
|
206
209
|
}
|
|
207
210
|
const hash = this.#identityMap.getKeyHash(key, '' + value, schema);
|
|
208
|
-
|
|
211
|
+
const entity = this.#identityMap.getByHash(meta, hash);
|
|
212
|
+
// the entry is stale when the key changed via refresh or merge, unless only the unflushed value differs
|
|
213
|
+
if (entity && '' + entity[key] !== '' + value) {
|
|
214
|
+
// the snapshot holds the database value
|
|
215
|
+
const dbValue = prop?.customType
|
|
216
|
+
? prop.customType.convertToDatabaseValue(value, this.#platform, { mode: 'serialization' })
|
|
217
|
+
: value;
|
|
218
|
+
if ('' + helper(entity).__originalEntityData?.[key] !== '' + dbValue) {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return entity;
|
|
209
223
|
}
|
|
210
224
|
/**
|
|
211
225
|
* Stores an entity in the identity map under an alternate key (non-PK property).
|
|
@@ -368,6 +382,10 @@ export class UnitOfWork {
|
|
|
368
382
|
}
|
|
369
383
|
/** Marks an entity for removal, cascading to related entities. */
|
|
370
384
|
remove(entity, visited, options = {}) {
|
|
385
|
+
// a reference known only by its `targetKey` has no PK, so the delete would match nothing
|
|
386
|
+
if (!helper(entity).__initialized && !helper(entity).hasPrimaryKey()) {
|
|
387
|
+
throw ValidationError.referenceWithoutPK(entity);
|
|
388
|
+
}
|
|
371
389
|
// allow removing not managed entities if they are not part of the persist stack
|
|
372
390
|
if (helper(entity).__managed || !this.#persistStack.has(entity)) {
|
|
373
391
|
this.#removeStack.add(entity);
|
|
@@ -467,7 +485,10 @@ export class UnitOfWork {
|
|
|
467
485
|
}
|
|
468
486
|
const groups = this.getChangeSetGroups();
|
|
469
487
|
const platform = this.#em.getPlatform();
|
|
470
|
-
const runInTransaction = !this.#em.isInTransaction() &&
|
|
488
|
+
const runInTransaction = !this.#em.isInTransaction() &&
|
|
489
|
+
!this.#em.isTransactionDisabled() &&
|
|
490
|
+
platform.supportsTransactions() &&
|
|
491
|
+
this.#em.config.get('implicitTransactions');
|
|
471
492
|
if (runInTransaction) {
|
|
472
493
|
const loggerContext = Utils.merge({ id: this.#em._id }, this.#em.getLoggerContext({ disableContextResolution: true }));
|
|
473
494
|
await this.#em.getConnection('write').transactional(trx => this.persistToDatabase(groups, trx), {
|
|
@@ -1115,6 +1136,11 @@ export class UnitOfWork {
|
|
|
1115
1136
|
for (const changeSet of changeSets) {
|
|
1116
1137
|
const wrapped = helper(changeSet.entity);
|
|
1117
1138
|
wrapped.__originalEntityData = this.#comparator.prepareEntity(changeSet.entity);
|
|
1139
|
+
// re-index so the entity is found by its new `targetKey` values instead of the old ones
|
|
1140
|
+
if (changeSet.meta.root.targetKeys?.some(key => key in changeSet.payload)) {
|
|
1141
|
+
this.#identityMap.delete(changeSet.entity);
|
|
1142
|
+
this.#identityMap.store(changeSet.entity);
|
|
1143
|
+
}
|
|
1118
1144
|
if (!wrapped.__initialized) {
|
|
1119
1145
|
for (const prop of changeSet.meta.relations) {
|
|
1120
1146
|
if ([ReferenceKind.MANY_TO_MANY, ReferenceKind.ONE_TO_MANY].includes(prop.kind) &&
|
|
@@ -95,7 +95,7 @@ export class EntityComparator {
|
|
|
95
95
|
lines.push(` return entity${this.wrap(pk)};`);
|
|
96
96
|
}
|
|
97
97
|
const code = `// compiled pk getter for entity ${meta.className}\n` + `return function(entity) {\n${lines.join('\n')}\n}`;
|
|
98
|
-
const fnKey = `pkGetter-${meta.
|
|
98
|
+
const fnKey = `pkGetter-${meta.compiledName}`;
|
|
99
99
|
const pkSerializer = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
|
|
100
100
|
this.#pkGetters.set(meta, pkSerializer);
|
|
101
101
|
return pkSerializer;
|
|
@@ -147,7 +147,7 @@ export class EntityComparator {
|
|
|
147
147
|
}
|
|
148
148
|
const code = `// compiled pk getter (with converted custom types) for entity ${meta.className}\n` +
|
|
149
149
|
`return function(entity) {\n${lines.join('\n')}\n}`;
|
|
150
|
-
const fnKey = `pkGetterConverted-${meta.
|
|
150
|
+
const fnKey = `pkGetterConverted-${meta.compiledName}`;
|
|
151
151
|
const pkSerializer = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
|
|
152
152
|
this.#pkGettersConverted.set(meta, pkSerializer);
|
|
153
153
|
return pkSerializer;
|
|
@@ -206,7 +206,7 @@ export class EntityComparator {
|
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
208
|
const code = `// compiled pk serializer for entity ${meta.className}\n` + `return function(entity) {\n${lines.join('\n')}\n}`;
|
|
209
|
-
const fnKey = `pkSerializer-${meta.
|
|
209
|
+
const fnKey = `pkSerializer-${meta.compiledName}`;
|
|
210
210
|
const pkSerializer = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
|
|
211
211
|
this.#pkSerializers.set(meta, pkSerializer);
|
|
212
212
|
return pkSerializer;
|
|
@@ -236,7 +236,7 @@ export class EntityComparator {
|
|
|
236
236
|
})
|
|
237
237
|
.forEach(prop => lines.push(this.getPropertySnapshot(meta, prop, context, this.wrap(prop.name), this.wrap(prop.name), [prop.name])));
|
|
238
238
|
const code = `return function(entity) {\n const ret = {};\n${lines.join('\n')}\n return ret;\n}`;
|
|
239
|
-
const fnKey = `snapshotGenerator-${meta.
|
|
239
|
+
const fnKey = `snapshotGenerator-${meta.compiledName}`;
|
|
240
240
|
const snapshotGenerator = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
|
|
241
241
|
this.#snapshotGenerators.set(meta, snapshotGenerator);
|
|
242
242
|
return snapshotGenerator;
|
|
@@ -350,14 +350,27 @@ export class EntityComparator {
|
|
|
350
350
|
if (prop.embedded && (meta.embeddable || meta.properties[prop.embedded[0]].object)) {
|
|
351
351
|
continue;
|
|
352
352
|
}
|
|
353
|
+
let defined = `typeof ${this.propName(prop.fieldNames[0])} !== 'undefined'`;
|
|
354
|
+
// STI subtypes with conflicting columns share the property, so another subtype's null column must not override it
|
|
355
|
+
if (prop.renamedFrom) {
|
|
356
|
+
lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
|
|
357
|
+
defined = `${this.propName(prop.fieldNames[0])} != null`;
|
|
358
|
+
// a column shared with a differently typed subtype is left for each subtype's hydrator to convert
|
|
359
|
+
if (meta.props.some(p => p !== prop && p.name === prop.name && p.fieldNames?.[0] === prop.fieldNames[0])) {
|
|
360
|
+
lines.push(`${padding} if (${defined}) {`);
|
|
361
|
+
lines.push(`${padding} ret${this.wrap(prop.name)} = ${this.propName(prop.fieldNames[0])};`);
|
|
362
|
+
lines.push(`${padding} }`);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
353
366
|
if (prop.runtimeType === 'boolean') {
|
|
354
|
-
lines.push(`${padding} if (
|
|
367
|
+
lines.push(`${padding} if (${defined}) {`);
|
|
355
368
|
lines.push(`${padding} ret${this.wrap(prop.name)} = ${this.propName(prop.fieldNames[0])} == null ? ${this.propName(prop.fieldNames[0])} : !!${this.propName(prop.fieldNames[0])};`);
|
|
356
369
|
lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
|
|
357
370
|
lines.push(`${padding} }`);
|
|
358
371
|
}
|
|
359
372
|
else if (prop.runtimeType === 'Date' && !this.#platform.isNumericProperty(prop)) {
|
|
360
|
-
lines.push(`${padding} if (
|
|
373
|
+
lines.push(`${padding} if (${defined}) {`);
|
|
361
374
|
context.set('parseDate', (value) => this.#platform.parseDate(value));
|
|
362
375
|
parseDate('ret' + this.wrap(prop.name), this.propName(prop.fieldNames[0]), padding);
|
|
363
376
|
lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
|
|
@@ -372,13 +385,13 @@ export class EntityComparator {
|
|
|
372
385
|
}
|
|
373
386
|
return item == null ? item : this.getResultMapper(prop.targetMeta)(item);
|
|
374
387
|
});
|
|
375
|
-
lines.push(`${padding} if (
|
|
388
|
+
lines.push(`${padding} if (${defined}) {`);
|
|
376
389
|
lines.push(`${padding} ret${this.wrap(prop.name)} = ${this.propName(prop.fieldNames[0])} == null ? ${this.propName(prop.fieldNames[0])} : mapEmbeddedResult_${idx}(${this.propName(prop.fieldNames[0])});`);
|
|
377
390
|
lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
|
|
378
391
|
lines.push(`${padding} }`);
|
|
379
392
|
}
|
|
380
393
|
else if (prop.kind !== ReferenceKind.EMBEDDED) {
|
|
381
|
-
lines.push(`${padding} if (
|
|
394
|
+
lines.push(`${padding} if (${defined}) {`);
|
|
382
395
|
lines.push(`${padding} ret${this.wrap(prop.name)} = ${this.propName(prop.fieldNames[0])};`);
|
|
383
396
|
lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
|
|
384
397
|
lines.push(`${padding} }`);
|
|
@@ -420,7 +433,7 @@ export class EntityComparator {
|
|
|
420
433
|
lines.push(` for (let k in result) { if (Object.hasOwn(result, k) && !mapped[k] && ret[k] === undefined${knownKeysGuard}) ret[k] = result[k]; }`);
|
|
421
434
|
const code = `// compiled mapper for entity ${meta.className}\n` +
|
|
422
435
|
`return function(result) {\n const ret = {};\n${lines.join('\n')}\n return ret;\n}`;
|
|
423
|
-
const fnKey = `resultMapper-${meta.
|
|
436
|
+
const fnKey = `resultMapper-${meta.compiledName}`;
|
|
424
437
|
const resultMapper = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
|
|
425
438
|
this.#mappers.set(meta, resultMapper);
|
|
426
439
|
return resultMapper;
|
|
@@ -608,9 +621,24 @@ export class EntityComparator {
|
|
|
608
621
|
ret += ` if (discriminator !== undefined) break;\n`;
|
|
609
622
|
ret += ` __cls${level} = Object.getPrototypeOf(__cls${level});\n`;
|
|
610
623
|
ret += ` }\n`;
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
624
|
+
if (prop.targetKey) {
|
|
625
|
+
// the FK holds the `targetKey` value in the format of the actual target's column
|
|
626
|
+
const targetKey = prop.targetKey;
|
|
627
|
+
const targetKeyValueKey = `targetKeyValue_${this.safeKey(prop.name)}`;
|
|
628
|
+
context.set(targetKeyValueKey, (val) => {
|
|
629
|
+
const customType = val.__meta.properties[targetKey]?.customType;
|
|
630
|
+
const value = val[targetKey];
|
|
631
|
+
return customType
|
|
632
|
+
? customType.convertToDatabaseValue(value, this.#platform, { mode: 'serialization' })
|
|
633
|
+
: value;
|
|
634
|
+
});
|
|
635
|
+
ret += ` const pk = ${targetKeyValueKey}(val${level});\n`;
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
ret += ` const pk = val${level}?.__helper?.__identifier && !val${level}?.__helper?.hasPrimaryKey()\n`;
|
|
639
|
+
ret += ` ? val${level}.__helper.__identifier\n`;
|
|
640
|
+
ret += ` : toArray(val${level}?.__helper?.getPrimaryKey(true));\n`;
|
|
641
|
+
}
|
|
614
642
|
ret += ` ret${dataKey} = new PolymorphicRef(discriminator, pk);\n`;
|
|
615
643
|
ret += ` }\n`;
|
|
616
644
|
}
|
|
@@ -624,8 +652,8 @@ export class EntityComparator {
|
|
|
624
652
|
ret += ` } else if (typeof entity${entityKey} !== 'undefined') {\n`;
|
|
625
653
|
ret += ` const val${level} = entity${entityKey}${unwrap};\n`;
|
|
626
654
|
if (targetProp?.customType) {
|
|
627
|
-
//
|
|
628
|
-
const convertorKey = this.registerCustomType(targetProp, context);
|
|
655
|
+
// keyed by the relation name to avoid clashing with a same-named owner prop, the relation itself lacks the custom type with an explicit `columnType`
|
|
656
|
+
const convertorKey = this.registerCustomType({ ...prop, customType: targetProp.customType }, context);
|
|
629
657
|
ret += ` ret${dataKey} = convertToDatabaseValue_${convertorKey}(val${level}${targetAccess});\n`;
|
|
630
658
|
}
|
|
631
659
|
else {
|
|
@@ -688,7 +716,7 @@ export class EntityComparator {
|
|
|
688
716
|
lines.push(`}`);
|
|
689
717
|
const code = `// compiled comparator for entity ${meta.className}\n` +
|
|
690
718
|
`return function(last, current, options) {\n const diff = {};\n${lines.join('\n')}\n return diff;\n}`;
|
|
691
|
-
const fnKey = `comparator-${meta.
|
|
719
|
+
const fnKey = `comparator-${meta.compiledName}`;
|
|
692
720
|
const comparator = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
|
|
693
721
|
this.#comparators.set(meta, comparator);
|
|
694
722
|
return comparator;
|
package/utils/QueryHelper.d.ts
CHANGED
|
@@ -35,6 +35,12 @@ export declare class QueryHelper {
|
|
|
35
35
|
*/
|
|
36
36
|
static liftGroupOperators<T extends object>(where: Dictionary, meta: EntityMetadata<T>, metadata: MetadataStorage, key?: string): string | undefined;
|
|
37
37
|
static inlinePrimaryKeyObjects<T extends object>(where: Dictionary, meta: EntityMetadata<T>, metadata: MetadataStorage, key?: string): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* A bare value on a polymorphic relation would be compared to the discriminator column, so PK values are turned
|
|
40
|
+
* into a PK condition instead, e.g. `1` becomes `{ id: 1 }` and `[{ id: 1 }, { id: 2 }]` becomes `{ id: { $in: [1, 2] } }`.
|
|
41
|
+
* Null checks and `[type, id]` tuples keep their meaning.
|
|
42
|
+
*/
|
|
43
|
+
private static inlinePolymorphicPrimaryKeys;
|
|
38
44
|
/** Copies the plain object and array structure of the condition, keeping the leaf values (e.g. entities) by reference. */
|
|
39
45
|
private static cloneWhere;
|
|
40
46
|
static processWhere<T extends object>(options: ProcessWhereOptions<T>): FilterQuery<T>;
|
package/utils/QueryHelper.js
CHANGED
|
@@ -4,7 +4,7 @@ import { ARRAY_OPERATORS, GroupOperator, JSON_KEY_OPERATORS, ReferenceKind } fro
|
|
|
4
4
|
import { JsonType } from '../types/JsonType.js';
|
|
5
5
|
import { helper } from '../entity/wrap.js';
|
|
6
6
|
import { isRaw, Raw } from './RawQueryFragment.js';
|
|
7
|
-
import { MetadataError } from '../errors.js';
|
|
7
|
+
import { MetadataError, ValidationError } from '../errors.js';
|
|
8
8
|
/** @internal */
|
|
9
9
|
export class QueryHelper {
|
|
10
10
|
static SUPPORTED_OPERATORS = ['>', '<', '<=', '>=', '!', '!='];
|
|
@@ -102,18 +102,23 @@ export class QueryHelper {
|
|
|
102
102
|
for (const k of keys) {
|
|
103
103
|
const value = where[k];
|
|
104
104
|
const prop = meta.properties[k];
|
|
105
|
-
|
|
106
|
-
// participate in the standard single-column FK expansion. Query by discriminator
|
|
107
|
-
// column directly instead, e.g. { likeableType: 'post', likeableId: 1 }.
|
|
108
|
-
if (!prop || ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) || prop.polymorphic) {
|
|
105
|
+
if (!prop || ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
|
|
109
106
|
continue;
|
|
110
107
|
}
|
|
111
108
|
const op = this.liftGroupOperators(value, prop.targetMeta, metadata, k);
|
|
112
109
|
if (op) {
|
|
113
110
|
delete where[k];
|
|
114
|
-
|
|
111
|
+
const group = value[op].map((v) => {
|
|
115
112
|
return { [k]: v };
|
|
116
113
|
});
|
|
114
|
+
if (op in where) {
|
|
115
|
+
// Sibling groups are conjunctive, even when both use `$or`.
|
|
116
|
+
where.$and ??= [];
|
|
117
|
+
where.$and.push({ [op]: group });
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
where[op] = group;
|
|
121
|
+
}
|
|
117
122
|
}
|
|
118
123
|
}
|
|
119
124
|
return undefined;
|
|
@@ -122,7 +127,7 @@ export class QueryHelper {
|
|
|
122
127
|
if (Array.isArray(where)) {
|
|
123
128
|
where.forEach((item, i) => {
|
|
124
129
|
if (this.inlinePrimaryKeyObjects(item, meta, metadata, key)) {
|
|
125
|
-
where[i] = Utils.getPrimaryKeyValues(item, meta,
|
|
130
|
+
where[i] = Utils.getPrimaryKeyValues(item, meta, true);
|
|
126
131
|
}
|
|
127
132
|
});
|
|
128
133
|
}
|
|
@@ -152,15 +157,19 @@ export class QueryHelper {
|
|
|
152
157
|
Object.keys(where).forEach(k => {
|
|
153
158
|
const prop = meta.properties[k];
|
|
154
159
|
const meta2 = metadata.find(prop?.targetMeta?.class) || meta;
|
|
160
|
+
if (prop?.polymorphic) {
|
|
161
|
+
where[k] = this.inlinePolymorphicPrimaryKeys(where[k], meta2, prop);
|
|
162
|
+
}
|
|
155
163
|
if (this.inlinePrimaryKeyObjects(where[k], meta2, metadata, k)) {
|
|
156
164
|
// Skip the PK collapse when an owning M:1/1:1 relation's FK column count does not match
|
|
157
165
|
// the target's PK column count (e.g. FK references target PK + an extra unique column).
|
|
158
166
|
// The criteria layer would otherwise emit a malformed predicate such as
|
|
159
167
|
// `(joinCol1, joinCol2) = scalar`. Limited to owning relations — `joinColumns` on the
|
|
160
168
|
// inverse side (1:m) describes the owning entity's FK columns, not the LHS tuple.
|
|
169
|
+
// Polymorphic relations are skipped too, a scalar would be compared to the discriminator column.
|
|
161
170
|
if (prop?.owner &&
|
|
162
171
|
[ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) &&
|
|
163
|
-
prop.joinColumns.length !== meta2.primaryKeys.length) {
|
|
172
|
+
(prop.polymorphic || prop.joinColumns.length !== meta2.primaryKeys.length)) {
|
|
164
173
|
return;
|
|
165
174
|
}
|
|
166
175
|
where[k] = Utils.getPrimaryKeyValues(where[k], meta2, true);
|
|
@@ -168,6 +177,37 @@ export class QueryHelper {
|
|
|
168
177
|
});
|
|
169
178
|
return false;
|
|
170
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* A bare value on a polymorphic relation would be compared to the discriminator column, so PK values are turned
|
|
182
|
+
* into a PK condition instead, e.g. `1` becomes `{ id: 1 }` and `[{ id: 1 }, { id: 2 }]` becomes `{ id: { $in: [1, 2] } }`.
|
|
183
|
+
* Null checks and `[type, id]` tuples keep their meaning.
|
|
184
|
+
*/
|
|
185
|
+
static inlinePolymorphicPrimaryKeys(value, meta, prop) {
|
|
186
|
+
const pk = meta.primaryKeys[0];
|
|
187
|
+
const isScalar = (v) => v != null && !Array.isArray(v) && Utils.isPrimaryKey(v);
|
|
188
|
+
const isPrimaryKeyObject = (v) => Utils.isPlainObject(v) && Utils.getObjectKeysSize(v) === 1 && pk in v;
|
|
189
|
+
// lists of plain values are how `[type, id]` tuples get normalized (`{ $in: [type, id] }`), so only PK objects are inlined
|
|
190
|
+
const isList = (v) => Array.isArray(v) && v.length > 0 && v.every(isPrimaryKeyObject);
|
|
191
|
+
let cond;
|
|
192
|
+
if (isScalar(value)) {
|
|
193
|
+
cond = value;
|
|
194
|
+
}
|
|
195
|
+
else if (isList(value)) {
|
|
196
|
+
cond = { $in: value.map(v => v[pk]) };
|
|
197
|
+
}
|
|
198
|
+
else if (Utils.isPlainObject(value) &&
|
|
199
|
+
Utils.hasObjectKeys(value) &&
|
|
200
|
+
Object.entries(value).every(([op, v]) => Utils.isOperator(op) && !(op in GroupOperator) && op !== '$not' && (isScalar(v) || isList(v)))) {
|
|
201
|
+
cond = Object.fromEntries(Object.entries(value).map(([op, v]) => [op, isList(v) ? v.map(i => i[pk]) : v]));
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
if (meta.compositePK) {
|
|
207
|
+
throw new ValidationError(`Polymorphic relation ${prop.name} targets entities with a composite primary key, use PK objects or entity references in the condition instead.`);
|
|
208
|
+
}
|
|
209
|
+
return { [pk]: cond };
|
|
210
|
+
}
|
|
171
211
|
/** Copies the plain object and array structure of the condition, keeping the leaf values (e.g. entities) by reference. */
|
|
172
212
|
static cloneWhere(where) {
|
|
173
213
|
if (Array.isArray(where)) {
|
|
@@ -486,6 +526,9 @@ export class QueryHelper {
|
|
|
486
526
|
if (expanded !== w) {
|
|
487
527
|
where[k] = expanded;
|
|
488
528
|
}
|
|
529
|
+
else if (Array.isArray(w)) {
|
|
530
|
+
where[k] = w.map(expandItem);
|
|
531
|
+
}
|
|
489
532
|
else if (Utils.isPlainObject(w)) {
|
|
490
533
|
for (const op of Object.keys(w)) {
|
|
491
534
|
if (!Utils.isOperator(op, false)) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { EntityManager } from '../EntityManager.js';
|
|
2
2
|
/** Uses `AsyncLocalStorage` to maintain a transaction-scoped EntityManager context across async operations. */
|
|
3
3
|
export declare class TransactionContext {
|
|
4
|
+
#private;
|
|
4
5
|
readonly em: EntityManager;
|
|
5
6
|
private static storage;
|
|
6
7
|
readonly id: number;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
var _a;
|
|
1
2
|
import { createAsyncContext } from './AsyncContext.js';
|
|
2
3
|
/** Uses `AsyncLocalStorage` to maintain a transaction-scoped EntityManager context across async operations. */
|
|
3
4
|
export class TransactionContext {
|
|
4
5
|
em;
|
|
5
6
|
static storage = createAsyncContext();
|
|
7
|
+
#parent = _a.currentTransactionContext();
|
|
6
8
|
id;
|
|
7
9
|
constructor(em) {
|
|
8
10
|
this.em = em;
|
|
@@ -12,7 +14,7 @@ export class TransactionContext {
|
|
|
12
14
|
* Creates new TransactionContext instance and runs the code inside its domain.
|
|
13
15
|
*/
|
|
14
16
|
static create(em, next) {
|
|
15
|
-
const context = new
|
|
17
|
+
const context = new _a(em);
|
|
16
18
|
return this.storage.run(context, next);
|
|
17
19
|
}
|
|
18
20
|
/**
|
|
@@ -25,7 +27,11 @@ export class TransactionContext {
|
|
|
25
27
|
* Returns current EntityManager (if available).
|
|
26
28
|
*/
|
|
27
29
|
static getEntityManager(name = 'default') {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
let context = _a.currentTransactionContext();
|
|
31
|
+
while (context && context.em.name !== name) {
|
|
32
|
+
context = context.#parent;
|
|
33
|
+
}
|
|
34
|
+
return context?.em;
|
|
30
35
|
}
|
|
31
36
|
}
|
|
37
|
+
_a = TransactionContext;
|
|
@@ -81,10 +81,10 @@ export class TransactionManager {
|
|
|
81
81
|
*/
|
|
82
82
|
async executeWithoutTransaction(em, cb, options) {
|
|
83
83
|
const suspended = this.suspendTransaction(em);
|
|
84
|
-
const fork = this.createFork(em,
|
|
84
|
+
const fork = this.createFork(em, options);
|
|
85
85
|
const propagateToUpperContext = this.shouldPropagateToUpperContext(em);
|
|
86
86
|
try {
|
|
87
|
-
return await this.executeTransactionFlow(fork, cb, propagateToUpperContext, em);
|
|
87
|
+
return await TransactionContext.create(fork, () => this.executeTransactionFlow(fork, cb, propagateToUpperContext, em));
|
|
88
88
|
}
|
|
89
89
|
finally {
|
|
90
90
|
this.resumeTransaction(em, suspended);
|
|
@@ -189,6 +189,10 @@ export class TransactionManager {
|
|
|
189
189
|
if (wrapped.__originalEntityData) {
|
|
190
190
|
parentWrapped.__originalEntityData = originalEntityData;
|
|
191
191
|
}
|
|
192
|
+
// index a merged `targetKey` value, `getByKey` ignores the old one as stale
|
|
193
|
+
if (meta.root.targetKeys) {
|
|
194
|
+
parentUoW.getIdentityMap().store(parentEntity);
|
|
195
|
+
}
|
|
192
196
|
}
|
|
193
197
|
else {
|
|
194
198
|
parentUoW.merge(entity, new Set([entity]));
|