@mikro-orm/core 7.2.2-dev.8 → 7.2.2
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 +53 -26
- 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/errors.d.ts +1 -0
- package/errors.js +3 -0
- package/hydration/ObjectHydrator.js +2 -2
- package/metadata/MetadataDiscovery.js +27 -5
- package/metadata/MetadataStorage.d.ts +7 -0
- package/metadata/MetadataStorage.js +27 -0
- 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/EntityManager.d.ts
CHANGED
|
@@ -580,6 +580,11 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
|
|
|
580
580
|
* Checks whether this EntityManager is currently operating inside a database transaction.
|
|
581
581
|
*/
|
|
582
582
|
isInTransaction(): boolean;
|
|
583
|
+
/**
|
|
584
|
+
* Checks whether transactions are disabled for this EntityManager.
|
|
585
|
+
* @internal
|
|
586
|
+
*/
|
|
587
|
+
isTransactionDisabled(): boolean;
|
|
583
588
|
/**
|
|
584
589
|
* Gets the transaction context (driver dependent object used to make sure queries are executed on same connection).
|
|
585
590
|
*/
|
|
@@ -633,7 +638,7 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
|
|
|
633
638
|
/**
|
|
634
639
|
* @internal
|
|
635
640
|
*/
|
|
636
|
-
tryCache<T extends object, R>(entityName: EntityName<T>, config: boolean | number | [string, number] | undefined, key: unknown, refresh?: boolean, merge?: boolean): Promise<{
|
|
641
|
+
tryCache<T extends object, R>(entityName: EntityName<T>, config: boolean | number | [string, number] | undefined, key: unknown, refresh?: boolean, merge?: boolean, schema?: string): Promise<{
|
|
637
642
|
data?: R | null;
|
|
638
643
|
key: string;
|
|
639
644
|
} | undefined>;
|
package/EntityManager.js
CHANGED
|
@@ -133,7 +133,7 @@ export class EntityManager {
|
|
|
133
133
|
options.populate = (await em.preparePopulate(entityName, options));
|
|
134
134
|
const populate = options.populate;
|
|
135
135
|
const cacheKey = em.cacheKey(entityName, options, 'em.find', where);
|
|
136
|
-
const cached = await em.tryCache(entityName, options.cache, cacheKey, options.refresh, true);
|
|
136
|
+
const cached = await em.tryCache(entityName, options.cache, cacheKey, options.refresh, true, options.schema);
|
|
137
137
|
if (cached?.data) {
|
|
138
138
|
await em.#entityLoader.populate(entityName, cached.data, populate, {
|
|
139
139
|
...options,
|
|
@@ -789,6 +789,10 @@ export class EntityManager {
|
|
|
789
789
|
.getHydrator(this.metadata)
|
|
790
790
|
.hydrate(ref, helper(ref).__meta, data, em.#entityFactory, 'full', false, false);
|
|
791
791
|
Utils.merge(helper(ref).__originalEntityData, this.#comparator.prepareEntity(e));
|
|
792
|
+
// index a refreshed `targetKey` value, `getByKey` ignores the old one as stale
|
|
793
|
+
if (helper(ref).__meta.root.targetKeys) {
|
|
794
|
+
em.#unitOfWork.getIdentityMap().store(ref);
|
|
795
|
+
}
|
|
792
796
|
found ||= ref === entity;
|
|
793
797
|
}
|
|
794
798
|
if (!found) {
|
|
@@ -834,7 +838,7 @@ export class EntityManager {
|
|
|
834
838
|
validateParams(where);
|
|
835
839
|
options.populate = (await em.preparePopulate(entityName, options));
|
|
836
840
|
const cacheKey = em.cacheKey(entityName, options, 'em.findOne', where);
|
|
837
|
-
const cached = await em.tryCache(entityName, options.cache, cacheKey, options.refresh, true);
|
|
841
|
+
const cached = await em.tryCache(entityName, options.cache, cacheKey, options.refresh, true, options.schema);
|
|
838
842
|
if (cached?.data !== undefined) {
|
|
839
843
|
if (cached.data) {
|
|
840
844
|
await em.#entityLoader.populate(entityName, [cached.data], options.populate, {
|
|
@@ -1110,7 +1114,18 @@ export class EntityManager {
|
|
|
1110
1114
|
const ret = [];
|
|
1111
1115
|
for (const idx of shapes.values()) {
|
|
1112
1116
|
const res = await this.upsertMany(entityName, idx.map(i => data[i]), options);
|
|
1113
|
-
|
|
1117
|
+
const seen = new Set();
|
|
1118
|
+
let j = 0;
|
|
1119
|
+
// `res` holds every entity once, so skip the rows resolving to an entity an earlier row already took
|
|
1120
|
+
for (const i of idx) {
|
|
1121
|
+
const row = data[i];
|
|
1122
|
+
const entity = Utils.isEntity(row)
|
|
1123
|
+
? row
|
|
1124
|
+
: em.#unitOfWork.tryGetById(entityName, row, options.schema, false);
|
|
1125
|
+
if (!seen.has(entity)) {
|
|
1126
|
+
seen.add((ret[i] = res[j++]));
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1114
1129
|
}
|
|
1115
1130
|
// rows resolving to the same entity are returned once, as in the single statement path
|
|
1116
1131
|
return [...new Set(ret.filter(entity => entity))];
|
|
@@ -1123,9 +1138,10 @@ export class EntityManager {
|
|
|
1123
1138
|
const generatedFields = new Set();
|
|
1124
1139
|
const allData = [];
|
|
1125
1140
|
const allWhere = [];
|
|
1141
|
+
const dataIndexes = [];
|
|
1142
|
+
const result = [];
|
|
1126
1143
|
const entities = new Map();
|
|
1127
1144
|
const entitiesByData = new Map();
|
|
1128
|
-
const entitiesByAllDataIdx = new Map();
|
|
1129
1145
|
for (let i = 0; i < data.length; i++) {
|
|
1130
1146
|
let row = data[i];
|
|
1131
1147
|
let where;
|
|
@@ -1133,14 +1149,12 @@ export class EntityManager {
|
|
|
1133
1149
|
const entity = row;
|
|
1134
1150
|
if (helper(entity).__managed && helper(entity).__em === em && !this.config.get('upsertManaged')) {
|
|
1135
1151
|
em.#entityFactory.mergeData(meta, entity, row, { initialized: true });
|
|
1136
|
-
|
|
1137
|
-
entitiesByData.set(row, entity);
|
|
1152
|
+
result[i] = entity;
|
|
1138
1153
|
continue;
|
|
1139
1154
|
}
|
|
1140
1155
|
where = helper(entity).getPrimaryKey();
|
|
1141
1156
|
getOnCreateGeneratedFields(meta, entity).forEach(field => generatedFields.add(field));
|
|
1142
1157
|
em.#entityFactory.assignDefaultValues(entity, meta);
|
|
1143
|
-
entitiesByAllDataIdx.set(allData.length, entity);
|
|
1144
1158
|
row = em.#comparator.prepareEntity(entity);
|
|
1145
1159
|
}
|
|
1146
1160
|
else {
|
|
@@ -1150,8 +1164,7 @@ export class EntityManager {
|
|
|
1150
1164
|
const exists = em.#unitOfWork.getById(entityName, where, options.schema);
|
|
1151
1165
|
if (exists) {
|
|
1152
1166
|
em.assign(exists, row);
|
|
1153
|
-
|
|
1154
|
-
entitiesByData.set(row, exists);
|
|
1167
|
+
result[i] = exists;
|
|
1155
1168
|
continue;
|
|
1156
1169
|
}
|
|
1157
1170
|
}
|
|
@@ -1184,22 +1197,25 @@ export class EntityManager {
|
|
|
1184
1197
|
validateParams(row, 'insert data');
|
|
1185
1198
|
allData.push(row);
|
|
1186
1199
|
allWhere.push(where);
|
|
1200
|
+
dataIndexes.push(i);
|
|
1187
1201
|
}
|
|
1188
|
-
if (
|
|
1189
|
-
return [...
|
|
1202
|
+
if (allData.length === 0) {
|
|
1203
|
+
return [...new Set(result)];
|
|
1190
1204
|
}
|
|
1191
1205
|
// `onCreate` generated values are for the insert clause only, they must not overwrite existing rows
|
|
1192
1206
|
if (generatedFields.size > 0 && !options.onConflictMergeFields) {
|
|
1193
1207
|
options.onConflictExcludeFields = [...(options.onConflictExcludeFields ?? []), ...generatedFields];
|
|
1194
1208
|
}
|
|
1195
1209
|
if (em.eventManager.hasListeners(EventType.beforeUpsert, meta)) {
|
|
1196
|
-
for (const
|
|
1197
|
-
const entity =
|
|
1210
|
+
for (const index of dataIndexes) {
|
|
1211
|
+
const entity = data[index];
|
|
1198
1212
|
await em.eventManager.dispatchEvent(EventType.beforeUpsert, { entity, em, meta }, meta);
|
|
1199
1213
|
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1214
|
+
dataIndexes.forEach((index, idx) => {
|
|
1215
|
+
if (Utils.isEntity(data[index])) {
|
|
1216
|
+
allData[idx] = em.#comparator.prepareEntity(data[index]);
|
|
1217
|
+
}
|
|
1218
|
+
});
|
|
1203
1219
|
}
|
|
1204
1220
|
const res = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.driver.nativeUpdateMany(entityName, allWhere, allData, {
|
|
1205
1221
|
upsert: true,
|
|
@@ -1207,15 +1223,16 @@ export class EntityManager {
|
|
|
1207
1223
|
...options,
|
|
1208
1224
|
ctx,
|
|
1209
1225
|
}));
|
|
1210
|
-
entities.clear();
|
|
1211
|
-
entitiesByData.clear();
|
|
1212
1226
|
const loadPK = new Map();
|
|
1213
1227
|
allData.forEach((row, i) => {
|
|
1214
|
-
|
|
1228
|
+
// Managed inputs skipped by the SQL batch still occupy positions in `data`.
|
|
1229
|
+
const index = dataIndexes[i];
|
|
1230
|
+
const item = data[index];
|
|
1231
|
+
em.#unitOfWork.getChangeSetPersister().mapReturnedValues(Utils.isEntity(item) ? item : null, Utils.isEntity(item) ? {} : item,
|
|
1215
1232
|
// A suppressed conflict omits its row; positional mapping would hydrate a different entity.
|
|
1216
1233
|
res.rows?.length === allData.length ? res.rows[i] : undefined, meta, true);
|
|
1217
|
-
const entity = Utils.isEntity(
|
|
1218
|
-
?
|
|
1234
|
+
const entity = Utils.isEntity(item)
|
|
1235
|
+
? item
|
|
1219
1236
|
: em.#entityFactory.create(entityName, row, {
|
|
1220
1237
|
refresh: true,
|
|
1221
1238
|
initialized: true,
|
|
@@ -1226,18 +1243,19 @@ export class EntityManager {
|
|
|
1226
1243
|
}
|
|
1227
1244
|
entities.set(entity, row);
|
|
1228
1245
|
entitiesByData.set(row, entity);
|
|
1246
|
+
result[index] = entity;
|
|
1229
1247
|
});
|
|
1230
1248
|
// skip the reload only when RETURNING brought back a row for every input
|
|
1231
1249
|
// (onConflictWhere can suppress writes, leaving some rows out)
|
|
1232
1250
|
// oxfmt-ignore
|
|
1233
1251
|
const uniqueFields = options.onConflictFields ?? (Utils.isPlainObject(allWhere[0]) ? Object.keys(allWhere[0]).flatMap(key => Utils.splitPrimaryKeys(key)) : meta.primaryKeys);
|
|
1234
1252
|
const platform = this.getPlatform();
|
|
1235
|
-
const returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options, platform);
|
|
1253
|
+
const returning = getOnConflictReturningFields(meta, data[dataIndexes[0]], uniqueFields, options, platform);
|
|
1236
1254
|
if (options.onConflictWhere && res.rows?.length !== allData.length) {
|
|
1237
1255
|
returning.push(...meta.comparableProps.filter(p => !p.lazy && !p.embeddable).map(p => p.name));
|
|
1238
1256
|
}
|
|
1239
1257
|
const reloadFields = returning.length > 0 &&
|
|
1240
|
-
!((platform.usesReturningStatement() || platform.usesOutputStatement()) && res.rows?.length ===
|
|
1258
|
+
!((platform.usesReturningStatement() || platform.usesOutputStatement()) && res.rows?.length === allData.length);
|
|
1241
1259
|
if (options.onConflictAction === 'ignore' || (!res.rows?.length && loadPK.size > 0) || reloadFields) {
|
|
1242
1260
|
const unique = meta.hydrateProps.filter(p => !p.lazy).map(p => p.name);
|
|
1243
1261
|
const add = new Set(propIndex !== false && propIndex >= 0
|
|
@@ -1247,7 +1265,8 @@ export class EntityManager {
|
|
|
1247
1265
|
Utils.keys(cond).forEach(key => add.add(key));
|
|
1248
1266
|
}
|
|
1249
1267
|
const where = { $or: [] };
|
|
1250
|
-
|
|
1268
|
+
dataIndexes.forEach((index, idx) => {
|
|
1269
|
+
const item = data[index];
|
|
1251
1270
|
where.$or[idx] = {};
|
|
1252
1271
|
const props = Array.isArray(uniqueFields) ? uniqueFields : Object.keys(item);
|
|
1253
1272
|
props.forEach(prop => {
|
|
@@ -1317,7 +1336,7 @@ export class EntityManager {
|
|
|
1317
1336
|
await em.eventManager.dispatchEvent(EventType.afterUpsert, { entity, em, meta }, meta);
|
|
1318
1337
|
}
|
|
1319
1338
|
}
|
|
1320
|
-
return [...
|
|
1339
|
+
return [...new Set(result)];
|
|
1321
1340
|
}
|
|
1322
1341
|
/**
|
|
1323
1342
|
* Runs your callback wrapped inside a database transaction.
|
|
@@ -1974,6 +1993,13 @@ export class EntityManager {
|
|
|
1974
1993
|
isInTransaction() {
|
|
1975
1994
|
return !!this.getContext(false).#transactionContext;
|
|
1976
1995
|
}
|
|
1996
|
+
/**
|
|
1997
|
+
* Checks whether transactions are disabled for this EntityManager.
|
|
1998
|
+
* @internal
|
|
1999
|
+
*/
|
|
2000
|
+
isTransactionDisabled() {
|
|
2001
|
+
return !!this.getContext(false).#disableTransactions;
|
|
2002
|
+
}
|
|
1977
2003
|
/**
|
|
1978
2004
|
* Gets the transaction context (driver dependent object used to make sure queries are executed on same connection).
|
|
1979
2005
|
*/
|
|
@@ -2329,7 +2355,7 @@ export class EntityManager {
|
|
|
2329
2355
|
/**
|
|
2330
2356
|
* @internal
|
|
2331
2357
|
*/
|
|
2332
|
-
async tryCache(entityName, config, key, refresh, merge) {
|
|
2358
|
+
async tryCache(entityName, config, key, refresh, merge, schema) {
|
|
2333
2359
|
config ??= this.config.get('resultCache').global;
|
|
2334
2360
|
if (!config) {
|
|
2335
2361
|
return undefined;
|
|
@@ -2351,6 +2377,7 @@ export class EntityManager {
|
|
|
2351
2377
|
merge: true,
|
|
2352
2378
|
convertCustomTypes: false,
|
|
2353
2379
|
refresh,
|
|
2380
|
+
schema,
|
|
2354
2381
|
recomputeSnapshot: true,
|
|
2355
2382
|
};
|
|
2356
2383
|
if (Array.isArray(cached) && merge) {
|
|
@@ -51,6 +51,7 @@ export declare class EntityFactory {
|
|
|
51
51
|
assignDefaultValues<T extends object>(entity: T, meta: EntityMetadata<T>, onCreateOnly?: boolean): void;
|
|
52
52
|
private hydrate;
|
|
53
53
|
private findEntity;
|
|
54
|
+
private findEntityByPrimaryKey;
|
|
54
55
|
private processDiscriminatorColumn;
|
|
55
56
|
/**
|
|
56
57
|
* denormalize PK to value required by driver (e.g. ObjectId)
|
package/entity/EntityFactory.js
CHANGED
|
@@ -55,6 +55,10 @@ export class EntityFactory {
|
|
|
55
55
|
Utils.dropUndefinedProperties(data);
|
|
56
56
|
this.mergeData(meta2, exists, data, options);
|
|
57
57
|
wrapped.__processing = false;
|
|
58
|
+
// index a merged `targetKey` value, `getByKey` ignores the old one as stale
|
|
59
|
+
if (meta2.root.targetKeys) {
|
|
60
|
+
this.unitOfWork.getIdentityMap().store(exists);
|
|
61
|
+
}
|
|
58
62
|
wrapped.__initialized ||= !!options.initialized;
|
|
59
63
|
if (wrapped.isInitialized()) {
|
|
60
64
|
if (!wasInitialized) {
|
|
@@ -232,9 +236,9 @@ export class EntityFactory {
|
|
|
232
236
|
options.convertCustomTypes ??= true;
|
|
233
237
|
const meta = this.#metadata.get(entityName);
|
|
234
238
|
const schema = this.#driver.getSchemaName(meta, options);
|
|
235
|
-
// Handle alternate key lookup
|
|
236
|
-
if (options.key) {
|
|
237
|
-
const value =
|
|
239
|
+
// Handle alternate key lookup, a `key` pointing at the single PK is a plain PK reference
|
|
240
|
+
if (options.key && !(meta.primaryKeys.length === 1 && meta.primaryKeys[0] === options.key)) {
|
|
241
|
+
const value = Array.isArray(id) ? id[0] : Utils.isPlainObject(id) ? id[options.key] : id;
|
|
238
242
|
const exists = this.unitOfWork.getByKey(entityName, options.key, value, schema, options.convertCustomTypes);
|
|
239
243
|
if (exists) {
|
|
240
244
|
return exists;
|
|
@@ -242,6 +246,8 @@ export class EntityFactory {
|
|
|
242
246
|
// Create entity stub - storeByKey will set the alternate key property and store in identity map
|
|
243
247
|
const entity = this.create(entityName, {}, { ...options, initialized: false });
|
|
244
248
|
this.unitOfWork.storeByKey(entity, options.key, value, schema, options.convertCustomTypes);
|
|
249
|
+
// snapshot the stub like a PK reference, so flush treats it as existing instead of inserting it
|
|
250
|
+
helper(entity).__originalEntityData = this.#comparator.prepareEntity(entity);
|
|
245
251
|
return entity;
|
|
246
252
|
}
|
|
247
253
|
if (meta.simplePK) {
|
|
@@ -310,6 +316,12 @@ export class EntityFactory {
|
|
|
310
316
|
if (options.merge && !options.newEntity) {
|
|
311
317
|
this.#hydrator.hydrateReference(entity, meta, data, this, options.convertCustomTypes, options.schema, options.parentSchema);
|
|
312
318
|
this.unitOfWork.register(entity);
|
|
319
|
+
// relations hydrated together with this entity (joined strategy) resolve it via `targetKey` before it is hydrated
|
|
320
|
+
for (const key of meta.root.targetKeys ?? []) {
|
|
321
|
+
if (data[key] != null) {
|
|
322
|
+
this.unitOfWork.storeByKey(entity, key, data[key], schema, options.convertCustomTypes);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
313
325
|
}
|
|
314
326
|
if (options.initialized) {
|
|
315
327
|
EntityHelper.ensurePropagation(entity);
|
|
@@ -379,6 +391,23 @@ export class EntityFactory {
|
|
|
379
391
|
}
|
|
380
392
|
findEntity(data, meta, options) {
|
|
381
393
|
const schema = this.#driver.getSchemaName(meta, options);
|
|
394
|
+
const exists = this.findEntityByPrimaryKey(data, meta, schema, options);
|
|
395
|
+
// a new entity must not adopt a reference stub, that one is never inserted
|
|
396
|
+
if (exists || Array.isArray(data) || options.newEntity) {
|
|
397
|
+
return exists;
|
|
398
|
+
}
|
|
399
|
+
// a reference created via `targetKey` is only known by that key until the entity is loaded
|
|
400
|
+
for (const key of meta.root.targetKeys ?? []) {
|
|
401
|
+
if (data[key] != null) {
|
|
402
|
+
const ref = this.unitOfWork.getByKey(meta.class, key, data[key], schema, options.convertCustomTypes);
|
|
403
|
+
if (ref && !helper(ref).__initialized) {
|
|
404
|
+
return ref;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return undefined;
|
|
409
|
+
}
|
|
410
|
+
findEntityByPrimaryKey(data, meta, schema, options) {
|
|
382
411
|
if (meta.simplePK) {
|
|
383
412
|
return this.unitOfWork.getById(meta.class, data[meta.primaryKeys[0]], schema);
|
|
384
413
|
}
|
package/entity/EntityLoader.js
CHANGED
|
@@ -197,9 +197,9 @@ export class EntityLoader {
|
|
|
197
197
|
this.initializeCollections(filtered, prop, field, items, customOrder, isPartial, hasLimit);
|
|
198
198
|
return items;
|
|
199
199
|
}
|
|
200
|
-
async populateScalar(meta, filtered, options) {
|
|
201
|
-
const pk = Utils.getPrimaryKeyHash(meta.primaryKeys);
|
|
202
|
-
const ids = Utils.unique(filtered.map(e => Utils.getPrimaryKeyValues(e, meta, true)));
|
|
200
|
+
async populateScalar(meta, filtered, options, key) {
|
|
201
|
+
const pk = (key ?? Utils.getPrimaryKeyHash(meta.primaryKeys));
|
|
202
|
+
const ids = Utils.unique(filtered.map(e => (key ? e[key] : Utils.getPrimaryKeyValues(e, meta, true))));
|
|
203
203
|
const where = this.mergePrimaryCondition(ids, pk, options, meta, this.#metadata, this.#driver.getPlatform());
|
|
204
204
|
const { filters, convertCustomTypes, lockMode, strategy, populateWhere, connectionType, logging, fields } = options;
|
|
205
205
|
await this.#em.find(meta.class, where, {
|
|
@@ -221,7 +221,8 @@ export class EntityLoader {
|
|
|
221
221
|
const needsFkLoad = [];
|
|
222
222
|
for (const entity of entities) {
|
|
223
223
|
const refValue = entity[prop.name];
|
|
224
|
-
|
|
224
|
+
// a `targetKey` reference is identified by its key, it might not have the PK yet
|
|
225
|
+
if (refValue && helper(refValue).getTargetKeyValue(prop.targetKey) != null) {
|
|
225
226
|
if ((ref && !options.refresh) || // :ref hint - already have reference
|
|
226
227
|
(!ref && helper(refValue).__initialized && !options.refresh) // already loaded
|
|
227
228
|
) {
|
|
@@ -246,7 +247,7 @@ export class EntityLoader {
|
|
|
246
247
|
if (!ref) {
|
|
247
248
|
for (const entity of needsFkLoad) {
|
|
248
249
|
const refValue = entity[prop.name];
|
|
249
|
-
if (refValue && helper(refValue).
|
|
250
|
+
if (refValue && helper(refValue).getTargetKeyValue(prop.targetKey) != null) {
|
|
250
251
|
toPopulate.push(entity);
|
|
251
252
|
}
|
|
252
253
|
}
|
|
@@ -268,7 +269,7 @@ export class EntityLoader {
|
|
|
268
269
|
const allItems = [];
|
|
269
270
|
await Promise.all([...groups].map(async ([discriminator, children]) => {
|
|
270
271
|
const targetMeta = this.#metadata.find(prop.discriminatorMap[discriminator]);
|
|
271
|
-
await this.populateScalar(targetMeta, children, options);
|
|
272
|
+
await this.populateScalar(targetMeta, children, options, prop.targetKey);
|
|
272
273
|
allItems.push(...children);
|
|
273
274
|
}));
|
|
274
275
|
return allItems;
|
package/entity/WrappedEntity.js
CHANGED
|
@@ -83,7 +83,12 @@ export class WrappedEntity {
|
|
|
83
83
|
if (!this.__em) {
|
|
84
84
|
throw ValidationError.entityNotManaged(this.entity);
|
|
85
85
|
}
|
|
86
|
-
|
|
86
|
+
// a reference known only by its `targetKey` has no PK, so look it up by the key instead
|
|
87
|
+
const key = this.hasPrimaryKey()
|
|
88
|
+
? undefined
|
|
89
|
+
: this.__meta.root.targetKeys?.find(k => this.entity[k] != null);
|
|
90
|
+
const where = key ? { [key]: this.entity[key] } : this.entity;
|
|
91
|
+
return this.__em.findOne(this.entity.constructor, where, {
|
|
87
92
|
...options,
|
|
88
93
|
refresh: true,
|
|
89
94
|
schema: this.__schema,
|
package/errors.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export declare class ValidationError<T extends AnyEntity = AnyEntity> extends Er
|
|
|
10
10
|
static fromWrongPropertyType(entity: AnyEntity, property: string, expectedType: string, givenType: string, givenValue: string): ValidationError;
|
|
11
11
|
static fromWrongRepositoryType(entityName: string, repoType: string, method: string): ValidationError;
|
|
12
12
|
static fromMergeWithoutPK(meta: EntityMetadata): ValidationError;
|
|
13
|
+
static referenceWithoutPK(entity: AnyEntity): ValidationError;
|
|
13
14
|
static transactionRequired(): ValidationError;
|
|
14
15
|
static entityNotManaged(entity: AnyEntity): ValidationError;
|
|
15
16
|
static notEntity(owner: AnyEntity, prop: EntityProperty, data: any): ValidationError;
|
package/errors.js
CHANGED
|
@@ -28,6 +28,9 @@ export class ValidationError extends Error {
|
|
|
28
28
|
static fromMergeWithoutPK(meta) {
|
|
29
29
|
return new ValidationError(`You cannot merge entity '${meta.className}' without identifier!`);
|
|
30
30
|
}
|
|
31
|
+
static referenceWithoutPK(entity) {
|
|
32
|
+
return new ValidationError(`Entity '${entity.constructor.name}' is only known by an alternate key and has no primary key, load it first`, entity);
|
|
33
|
+
}
|
|
31
34
|
static transactionRequired() {
|
|
32
35
|
return new ValidationError('An open transaction is required for this operation');
|
|
33
36
|
}
|
|
@@ -207,7 +207,7 @@ export class ObjectHydrator extends Hydrator {
|
|
|
207
207
|
if (prop.customType?.ensureComparable(meta, prop)) {
|
|
208
208
|
registerCustomType(prop, this.safeKey(prop.name), 'convertToDatabaseValue', context);
|
|
209
209
|
ret.push(` if (data${dataKey} != null && typeof data${dataKey} !== 'object' && convertCustomTypes) {`);
|
|
210
|
-
ret.push(` data${dataKey} = convertToDatabaseValue_${this.safeKey(prop.name)}(entity${entityKey}.__helper.
|
|
210
|
+
ret.push(` data${dataKey} = convertToDatabaseValue_${this.safeKey(prop.name)}(entity${entityKey}.__helper.getTargetKeyValue(${prop.targetKey ? this.quote(prop.targetKey) : ''}));`);
|
|
211
211
|
ret.push(` }`);
|
|
212
212
|
}
|
|
213
213
|
return ret;
|
|
@@ -407,7 +407,7 @@ export class ObjectHydrator extends Hydrator {
|
|
|
407
407
|
const code = `// compiled hydrator for entity ${meta.className} (${type + normalizeAccessors ? ' normalized' : ''})\n` +
|
|
408
408
|
`return function(entity, data, factory, newEntity, convertCustomTypes, schema, parentSchema, normalizeAccessors) {\n` +
|
|
409
409
|
`${lines.join('\n')}\n}`;
|
|
410
|
-
const fnKey = `hydrator-${meta.
|
|
410
|
+
const fnKey = `hydrator-${meta.compiledName}-${type}-${normalizeAccessors}`;
|
|
411
411
|
const hydrator = Utils.createFunction(context, code, this.config.get('compiledFunctions'), fnKey);
|
|
412
412
|
this.#hydrators[key].set(meta.class, hydrator);
|
|
413
413
|
return hydrator;
|
|
@@ -547,7 +547,7 @@ export class MetadataDiscovery {
|
|
|
547
547
|
const fieldNames1 = prop.targetMeta.getPrimaryProps().flatMap(pk => pk.fieldNames);
|
|
548
548
|
const idColumns = fieldNames1.map(fieldName => this.#namingStrategy.joinKeyColumnName(prop.discriminator, fieldName, fieldNames1.length > 1));
|
|
549
549
|
prop.fieldNames ??= [prop.discriminatorColumn, ...idColumns];
|
|
550
|
-
prop.joinColumns ??=
|
|
550
|
+
prop.joinColumns ??= prop.fieldNames.slice(1);
|
|
551
551
|
prop.referencedColumnNames ??= fieldNames1;
|
|
552
552
|
prop.referencedTableName ??= prop.targetMeta.tableName;
|
|
553
553
|
return;
|
|
@@ -564,7 +564,10 @@ export class MetadataDiscovery {
|
|
|
564
564
|
}
|
|
565
565
|
Utils.defaultValue(prop, 'referencedTableName', meta2.tableName);
|
|
566
566
|
if (!prop.joinColumns) {
|
|
567
|
-
|
|
567
|
+
// must match the real FK columns, which ignore `targetKey` naming for simple PK targets
|
|
568
|
+
prop.joinColumns = prop.targetKey
|
|
569
|
+
? prop.fieldNames
|
|
570
|
+
: fieldNames.map(fieldName => this.#namingStrategy.joinKeyColumnName(prop.name, fieldName, fieldNames.length > 1));
|
|
568
571
|
}
|
|
569
572
|
if (!prop.referencedColumnNames) {
|
|
570
573
|
prop.referencedColumnNames = fieldNames;
|
|
@@ -670,6 +673,12 @@ export class MetadataDiscovery {
|
|
|
670
673
|
if (prop2.kind !== ReferenceKind.SCALAR && prop2.type === meta.className) {
|
|
671
674
|
meta.referencingProperties.push({ meta: meta2, prop: prop2 });
|
|
672
675
|
}
|
|
676
|
+
if (prop2.targetKey &&
|
|
677
|
+
(prop2.type === meta.className || prop2.polymorphTargets?.some(t => t.className === meta.className)) &&
|
|
678
|
+
!(meta.primaryKeys.length === 1 && meta.primaryKeys[0] === prop2.targetKey) &&
|
|
679
|
+
!meta.root.targetKeys?.includes(prop2.targetKey)) {
|
|
680
|
+
(meta.root.targetKeys ??= []).push(prop2.targetKey);
|
|
681
|
+
}
|
|
673
682
|
}
|
|
674
683
|
}
|
|
675
684
|
}
|
|
@@ -1174,12 +1183,24 @@ export class MetadataDiscovery {
|
|
|
1174
1183
|
if (!prop.discriminator && !prop.discriminatorColumn && !prop.discriminatorMap && !Array.isArray(prop.target)) {
|
|
1175
1184
|
return;
|
|
1176
1185
|
}
|
|
1186
|
+
const isToOne = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind);
|
|
1187
|
+
const isUnionTargetMN = prop.kind === ReferenceKind.MANY_TO_MANY && Array.isArray(prop.target);
|
|
1177
1188
|
prop.polymorphic = true;
|
|
1178
1189
|
prop.discriminator ??= prop.name;
|
|
1190
|
+
// explicit `fieldNames` use the `[discriminatorColumn, ...fkIdColumns]` layout
|
|
1191
|
+
if (isToOne && prop.fieldNames && prop.fieldNames.length > 1) {
|
|
1192
|
+
prop.discriminatorColumn ??= prop.fieldNames[0];
|
|
1193
|
+
// `EntitySchema` mirrors `fieldNames` into `joinColumns`, let `initManyToOneFields` derive them after embedded prefixing
|
|
1194
|
+
if (Utils.equals(prop.joinColumns, prop.fieldNames)) {
|
|
1195
|
+
delete prop.joinColumns;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1179
1198
|
prop.discriminatorColumn ??= this.#namingStrategy.discriminatorColumnName(prop.discriminator);
|
|
1180
1199
|
prop.createForeignKeyConstraint = false;
|
|
1181
|
-
|
|
1182
|
-
|
|
1200
|
+
// a single field name can only be the FK column, e.g. mirrored from `joinColumn`
|
|
1201
|
+
if (isToOne && prop.fieldNames?.length === 1) {
|
|
1202
|
+
prop.fieldNames = [prop.discriminatorColumn, ...prop.fieldNames];
|
|
1203
|
+
}
|
|
1183
1204
|
if (isToOne || isUnionTargetMN) {
|
|
1184
1205
|
const types = prop.type.split(/ ?\| ?/);
|
|
1185
1206
|
prop.polymorphTargets = discovered.filter(m => types.includes(m.className) && !m.embeddable);
|
|
@@ -1439,7 +1460,8 @@ export class MetadataDiscovery {
|
|
|
1439
1460
|
}
|
|
1440
1461
|
}
|
|
1441
1462
|
rootProp.stiFieldNameMap[meta.discriminatorValue] = prop.fieldNames[0];
|
|
1442
|
-
|
|
1463
|
+
// subtypes that only differ in type share the column
|
|
1464
|
+
rootProp.stiFieldNames = Utils.unique([...rootProp.stiFieldNames, ...prop.fieldNames]);
|
|
1443
1465
|
newProp.nullable = true;
|
|
1444
1466
|
newProp.name = name;
|
|
1445
1467
|
newProp.hydrate = false;
|
|
@@ -32,6 +32,13 @@ export declare class MetadataStorage {
|
|
|
32
32
|
getById<T>(id: number): EntityMetadata<T>;
|
|
33
33
|
/** Returns metadata by class name, optionally throwing if not found. */
|
|
34
34
|
getByClassName<T = any, V extends boolean = true>(className: string, validate?: V): V extends true ? EntityMetadata<T> : EntityMetadata<T> | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Returns the first registered metadata with the given table name, in registration order (entities sharing
|
|
37
|
+
* a table, like STI children, resolve to the one registered first). The index is built on first use, as table
|
|
38
|
+
* names are only final once discovery has finished.
|
|
39
|
+
* @internal
|
|
40
|
+
*/
|
|
41
|
+
getByTableName<T = any>(tableName: string): EntityMetadata<T> | undefined;
|
|
35
42
|
/** Returns metadata by unique name, optionally throwing if not found. */
|
|
36
43
|
getByUniqueName<T = any, V extends boolean = true>(uniqueName: string, validate?: V): V extends true ? EntityMetadata<T> : EntityMetadata<T> | undefined;
|
|
37
44
|
private validate;
|
|
@@ -18,6 +18,8 @@ export class MetadataStorage {
|
|
|
18
18
|
#classNameMap;
|
|
19
19
|
#uniqueNameMap;
|
|
20
20
|
#ambiguousNames = new Set();
|
|
21
|
+
/** Built on first use by `getByTableName()`, dropped whenever an entity is registered or removed. */
|
|
22
|
+
#tableNameMap;
|
|
21
23
|
constructor(metadata = {}) {
|
|
22
24
|
this.#idMap = {};
|
|
23
25
|
this.#uniqueNameMap = {};
|
|
@@ -102,6 +104,7 @@ export class MetadataStorage {
|
|
|
102
104
|
/** Registers metadata for the given entity. */
|
|
103
105
|
set(entityName, meta) {
|
|
104
106
|
this.#metadataMap.set(entityName, meta);
|
|
107
|
+
this.#tableNameMap = undefined;
|
|
105
108
|
this.#idMap[meta._id] = meta;
|
|
106
109
|
this.#uniqueNameMap[meta.uniqueName] = meta;
|
|
107
110
|
const className = Utils.className(entityName);
|
|
@@ -110,6 +113,12 @@ export class MetadataStorage {
|
|
|
110
113
|
if (existing && existing !== meta && existing.class !== meta.class) {
|
|
111
114
|
this.#ambiguousNames.add(className);
|
|
112
115
|
}
|
|
116
|
+
// colliding names are numbered in storage order, which follows the discovery config, not module evaluation
|
|
117
|
+
const sameName = this.#ambiguousNames.has(className)
|
|
118
|
+
? [...new Set(this.#metadataMap.values())].filter(m => m.className === meta.className)
|
|
119
|
+
: [meta];
|
|
120
|
+
const ordinal = sameName.indexOf(meta);
|
|
121
|
+
meta.compiledName = ordinal > 0 ? `${meta.className}#${ordinal}` : meta.className;
|
|
113
122
|
this.#classNameMap[className] = meta;
|
|
114
123
|
return meta;
|
|
115
124
|
}
|
|
@@ -118,6 +127,7 @@ export class MetadataStorage {
|
|
|
118
127
|
const meta = this.find(entityName);
|
|
119
128
|
if (meta) {
|
|
120
129
|
this.#metadataMap.delete(meta.class);
|
|
130
|
+
this.#tableNameMap = undefined;
|
|
121
131
|
delete this.#idMap[meta._id];
|
|
122
132
|
delete this.#uniqueNameMap[meta.uniqueName];
|
|
123
133
|
delete this.#classNameMap[meta.className];
|
|
@@ -145,6 +155,23 @@ export class MetadataStorage {
|
|
|
145
155
|
getByClassName(className, validate = true) {
|
|
146
156
|
return this.validate(this.#classNameMap[className], className, validate);
|
|
147
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Returns the first registered metadata with the given table name, in registration order (entities sharing
|
|
160
|
+
* a table, like STI children, resolve to the one registered first). The index is built on first use, as table
|
|
161
|
+
* names are only final once discovery has finished.
|
|
162
|
+
* @internal
|
|
163
|
+
*/
|
|
164
|
+
getByTableName(tableName) {
|
|
165
|
+
if (!this.#tableNameMap) {
|
|
166
|
+
this.#tableNameMap = new Map();
|
|
167
|
+
for (const meta of this.#metadataMap.values()) {
|
|
168
|
+
if (!this.#tableNameMap.has(meta.tableName)) {
|
|
169
|
+
this.#tableNameMap.set(meta.tableName, meta);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return this.#tableNameMap.get(tableName);
|
|
174
|
+
}
|
|
148
175
|
/** Returns metadata by unique name, optionally throwing if not found. */
|
|
149
176
|
getByUniqueName(uniqueName, validate = true) {
|
|
150
177
|
return this.validate(this.#uniqueNameMap[uniqueName], uniqueName, validate);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/core",
|
|
3
|
-
"version": "7.2.2
|
|
3
|
+
"version": "7.2.2",
|
|
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]));
|
package/utils/Utils.js
CHANGED
|
@@ -153,7 +153,7 @@ export function parseJsonSafe(value) {
|
|
|
153
153
|
/** Collection of general-purpose utility methods used throughout the ORM. */
|
|
154
154
|
export class Utils {
|
|
155
155
|
static PK_SEPARATOR = '~~~';
|
|
156
|
-
static #ORM_VERSION = '7.2.2
|
|
156
|
+
static #ORM_VERSION = '7.2.2';
|
|
157
157
|
/** Default session variable name backing an RLS filter argument (`current_setting('mikro.<filter>.<arg>')`). */
|
|
158
158
|
static getRlsSettingName(filterName, argName) {
|
|
159
159
|
return `mikro.${filterName}.${argName}`;
|