@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/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, {
|
|
@@ -994,13 +998,14 @@ export class EntityManager {
|
|
|
994
998
|
});
|
|
995
999
|
const uniqueFields = options.onConflictFields ??
|
|
996
1000
|
(Utils.isPlainObject(where) ? Object.keys(where) : meta.primaryKeys);
|
|
997
|
-
const
|
|
1001
|
+
const platform = this.getPlatform();
|
|
1002
|
+
const returning = getOnConflictReturningFields(meta, data, uniqueFields, options, platform);
|
|
998
1003
|
if (options.onConflictWhere && !ret.row) {
|
|
999
1004
|
returning.push(...Object.keys(data));
|
|
1000
1005
|
}
|
|
1001
1006
|
if (options.onConflictAction === 'ignore' ||
|
|
1002
1007
|
!helper(entity).hasPrimaryKey() ||
|
|
1003
|
-
(returning.length > 0 && !(
|
|
1008
|
+
(returning.length > 0 && !((platform.usesReturningStatement() || platform.usesOutputStatement()) && ret.row))) {
|
|
1004
1009
|
const where = {};
|
|
1005
1010
|
if (Array.isArray(uniqueFields)) {
|
|
1006
1011
|
for (const prop of uniqueFields) {
|
|
@@ -1091,6 +1096,41 @@ export class EntityManager {
|
|
|
1091
1096
|
}
|
|
1092
1097
|
return ret;
|
|
1093
1098
|
}
|
|
1099
|
+
// merge fields are derived from the first row, rows of a different shape need their own statement
|
|
1100
|
+
if (!options.onConflictMergeFields && options.onConflictAction !== 'ignore') {
|
|
1101
|
+
const pks = this.metadata.get(entityName).primaryKeys;
|
|
1102
|
+
const shapes = new Map();
|
|
1103
|
+
data.forEach((row, i) => {
|
|
1104
|
+
// entity instances carry their unset properties as `undefined` keys, while in plain rows those are merged as `null`
|
|
1105
|
+
const shape = Object.keys(row)
|
|
1106
|
+
.filter(key => !pks.includes(key) && !(Utils.isEntity(row) && row[key] === undefined))
|
|
1107
|
+
.sort()
|
|
1108
|
+
.join();
|
|
1109
|
+
const idx = shapes.get(shape) ?? [];
|
|
1110
|
+
shapes.set(shape, idx);
|
|
1111
|
+
idx.push(i);
|
|
1112
|
+
});
|
|
1113
|
+
if (shapes.size > 1) {
|
|
1114
|
+
const ret = [];
|
|
1115
|
+
for (const idx of shapes.values()) {
|
|
1116
|
+
const res = await this.upsertMany(entityName, idx.map(i => data[i]), options);
|
|
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
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
// rows resolving to the same entity are returned once, as in the single statement path
|
|
1131
|
+
return [...new Set(ret.filter(entity => entity))];
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1094
1134
|
// the rows are replaced with their processed copies below, keep the caller's array intact
|
|
1095
1135
|
data = [...data];
|
|
1096
1136
|
const meta = this.metadata.get(entityName);
|
|
@@ -1098,9 +1138,10 @@ export class EntityManager {
|
|
|
1098
1138
|
const generatedFields = new Set();
|
|
1099
1139
|
const allData = [];
|
|
1100
1140
|
const allWhere = [];
|
|
1141
|
+
const dataIndexes = [];
|
|
1142
|
+
const result = [];
|
|
1101
1143
|
const entities = new Map();
|
|
1102
1144
|
const entitiesByData = new Map();
|
|
1103
|
-
const entitiesByAllDataIdx = new Map();
|
|
1104
1145
|
for (let i = 0; i < data.length; i++) {
|
|
1105
1146
|
let row = data[i];
|
|
1106
1147
|
let where;
|
|
@@ -1108,14 +1149,12 @@ export class EntityManager {
|
|
|
1108
1149
|
const entity = row;
|
|
1109
1150
|
if (helper(entity).__managed && helper(entity).__em === em && !this.config.get('upsertManaged')) {
|
|
1110
1151
|
em.#entityFactory.mergeData(meta, entity, row, { initialized: true });
|
|
1111
|
-
|
|
1112
|
-
entitiesByData.set(row, entity);
|
|
1152
|
+
result[i] = entity;
|
|
1113
1153
|
continue;
|
|
1114
1154
|
}
|
|
1115
1155
|
where = helper(entity).getPrimaryKey();
|
|
1116
1156
|
getOnCreateGeneratedFields(meta, entity).forEach(field => generatedFields.add(field));
|
|
1117
1157
|
em.#entityFactory.assignDefaultValues(entity, meta);
|
|
1118
|
-
entitiesByAllDataIdx.set(allData.length, entity);
|
|
1119
1158
|
row = em.#comparator.prepareEntity(entity);
|
|
1120
1159
|
}
|
|
1121
1160
|
else {
|
|
@@ -1125,8 +1164,7 @@ export class EntityManager {
|
|
|
1125
1164
|
const exists = em.#unitOfWork.getById(entityName, where, options.schema);
|
|
1126
1165
|
if (exists) {
|
|
1127
1166
|
em.assign(exists, row);
|
|
1128
|
-
|
|
1129
|
-
entitiesByData.set(row, exists);
|
|
1167
|
+
result[i] = exists;
|
|
1130
1168
|
continue;
|
|
1131
1169
|
}
|
|
1132
1170
|
}
|
|
@@ -1159,22 +1197,25 @@ export class EntityManager {
|
|
|
1159
1197
|
validateParams(row, 'insert data');
|
|
1160
1198
|
allData.push(row);
|
|
1161
1199
|
allWhere.push(where);
|
|
1200
|
+
dataIndexes.push(i);
|
|
1162
1201
|
}
|
|
1163
|
-
if (
|
|
1164
|
-
return [...
|
|
1202
|
+
if (allData.length === 0) {
|
|
1203
|
+
return [...new Set(result)];
|
|
1165
1204
|
}
|
|
1166
1205
|
// `onCreate` generated values are for the insert clause only, they must not overwrite existing rows
|
|
1167
1206
|
if (generatedFields.size > 0 && !options.onConflictMergeFields) {
|
|
1168
1207
|
options.onConflictExcludeFields = [...(options.onConflictExcludeFields ?? []), ...generatedFields];
|
|
1169
1208
|
}
|
|
1170
1209
|
if (em.eventManager.hasListeners(EventType.beforeUpsert, meta)) {
|
|
1171
|
-
for (const
|
|
1172
|
-
const entity =
|
|
1210
|
+
for (const index of dataIndexes) {
|
|
1211
|
+
const entity = data[index];
|
|
1173
1212
|
await em.eventManager.dispatchEvent(EventType.beforeUpsert, { entity, em, meta }, meta);
|
|
1174
1213
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1214
|
+
dataIndexes.forEach((index, idx) => {
|
|
1215
|
+
if (Utils.isEntity(data[index])) {
|
|
1216
|
+
allData[idx] = em.#comparator.prepareEntity(data[index]);
|
|
1217
|
+
}
|
|
1218
|
+
});
|
|
1178
1219
|
}
|
|
1179
1220
|
const res = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.driver.nativeUpdateMany(entityName, allWhere, allData, {
|
|
1180
1221
|
upsert: true,
|
|
@@ -1182,15 +1223,16 @@ export class EntityManager {
|
|
|
1182
1223
|
...options,
|
|
1183
1224
|
ctx,
|
|
1184
1225
|
}));
|
|
1185
|
-
entities.clear();
|
|
1186
|
-
entitiesByData.clear();
|
|
1187
1226
|
const loadPK = new Map();
|
|
1188
1227
|
allData.forEach((row, i) => {
|
|
1189
|
-
|
|
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,
|
|
1190
1232
|
// A suppressed conflict omits its row; positional mapping would hydrate a different entity.
|
|
1191
1233
|
res.rows?.length === allData.length ? res.rows[i] : undefined, meta, true);
|
|
1192
|
-
const entity = Utils.isEntity(
|
|
1193
|
-
?
|
|
1234
|
+
const entity = Utils.isEntity(item)
|
|
1235
|
+
? item
|
|
1194
1236
|
: em.#entityFactory.create(entityName, row, {
|
|
1195
1237
|
refresh: true,
|
|
1196
1238
|
initialized: true,
|
|
@@ -1201,16 +1243,19 @@ export class EntityManager {
|
|
|
1201
1243
|
}
|
|
1202
1244
|
entities.set(entity, row);
|
|
1203
1245
|
entitiesByData.set(row, entity);
|
|
1246
|
+
result[index] = entity;
|
|
1204
1247
|
});
|
|
1205
1248
|
// skip the reload only when RETURNING brought back a row for every input
|
|
1206
1249
|
// (onConflictWhere can suppress writes, leaving some rows out)
|
|
1207
1250
|
// oxfmt-ignore
|
|
1208
1251
|
const uniqueFields = options.onConflictFields ?? (Utils.isPlainObject(allWhere[0]) ? Object.keys(allWhere[0]).flatMap(key => Utils.splitPrimaryKeys(key)) : meta.primaryKeys);
|
|
1209
|
-
const
|
|
1252
|
+
const platform = this.getPlatform();
|
|
1253
|
+
const returning = getOnConflictReturningFields(meta, data[dataIndexes[0]], uniqueFields, options, platform);
|
|
1210
1254
|
if (options.onConflictWhere && res.rows?.length !== allData.length) {
|
|
1211
1255
|
returning.push(...meta.comparableProps.filter(p => !p.lazy && !p.embeddable).map(p => p.name));
|
|
1212
1256
|
}
|
|
1213
|
-
const reloadFields = returning.length > 0 &&
|
|
1257
|
+
const reloadFields = returning.length > 0 &&
|
|
1258
|
+
!((platform.usesReturningStatement() || platform.usesOutputStatement()) && res.rows?.length === allData.length);
|
|
1214
1259
|
if (options.onConflictAction === 'ignore' || (!res.rows?.length && loadPK.size > 0) || reloadFields) {
|
|
1215
1260
|
const unique = meta.hydrateProps.filter(p => !p.lazy).map(p => p.name);
|
|
1216
1261
|
const add = new Set(propIndex !== false && propIndex >= 0
|
|
@@ -1220,7 +1265,8 @@ export class EntityManager {
|
|
|
1220
1265
|
Utils.keys(cond).forEach(key => add.add(key));
|
|
1221
1266
|
}
|
|
1222
1267
|
const where = { $or: [] };
|
|
1223
|
-
|
|
1268
|
+
dataIndexes.forEach((index, idx) => {
|
|
1269
|
+
const item = data[index];
|
|
1224
1270
|
where.$or[idx] = {};
|
|
1225
1271
|
const props = Array.isArray(uniqueFields) ? uniqueFields : Object.keys(item);
|
|
1226
1272
|
props.forEach(prop => {
|
|
@@ -1290,7 +1336,7 @@ export class EntityManager {
|
|
|
1290
1336
|
await em.eventManager.dispatchEvent(EventType.afterUpsert, { entity, em, meta }, meta);
|
|
1291
1337
|
}
|
|
1292
1338
|
}
|
|
1293
|
-
return [...
|
|
1339
|
+
return [...new Set(result)];
|
|
1294
1340
|
}
|
|
1295
1341
|
/**
|
|
1296
1342
|
* Runs your callback wrapped inside a database transaction.
|
|
@@ -1947,6 +1993,13 @@ export class EntityManager {
|
|
|
1947
1993
|
isInTransaction() {
|
|
1948
1994
|
return !!this.getContext(false).#transactionContext;
|
|
1949
1995
|
}
|
|
1996
|
+
/**
|
|
1997
|
+
* Checks whether transactions are disabled for this EntityManager.
|
|
1998
|
+
* @internal
|
|
1999
|
+
*/
|
|
2000
|
+
isTransactionDisabled() {
|
|
2001
|
+
return !!this.getContext(false).#disableTransactions;
|
|
2002
|
+
}
|
|
1950
2003
|
/**
|
|
1951
2004
|
* Gets the transaction context (driver dependent object used to make sure queries are executed on same connection).
|
|
1952
2005
|
*/
|
|
@@ -2302,7 +2355,7 @@ export class EntityManager {
|
|
|
2302
2355
|
/**
|
|
2303
2356
|
* @internal
|
|
2304
2357
|
*/
|
|
2305
|
-
async tryCache(entityName, config, key, refresh, merge) {
|
|
2358
|
+
async tryCache(entityName, config, key, refresh, merge, schema) {
|
|
2306
2359
|
config ??= this.config.get('resultCache').global;
|
|
2307
2360
|
if (!config) {
|
|
2308
2361
|
return undefined;
|
|
@@ -2324,6 +2377,7 @@ export class EntityManager {
|
|
|
2324
2377
|
merge: true,
|
|
2325
2378
|
convertCustomTypes: false,
|
|
2326
2379
|
refresh,
|
|
2380
|
+
schema,
|
|
2327
2381
|
recomputeSnapshot: true,
|
|
2328
2382
|
};
|
|
2329
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/entity/defineEntity.d.ts
CHANGED
|
@@ -267,7 +267,7 @@ export declare class UniversalPropertyOptionsBuilder<in out Value, in out Option
|
|
|
267
267
|
autoincrement: false;
|
|
268
268
|
}, IncludeKeys>, IncludeKeys>;
|
|
269
269
|
/**
|
|
270
|
-
* Add the property to the `returning`
|
|
270
|
+
* Add the property to the `returning` clause of inserts, updates and upserts, reloading the database value into the entity.
|
|
271
271
|
*/
|
|
272
272
|
returning(returning?: boolean): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
|
|
273
273
|
/**
|
package/entity/defineEntity.js
CHANGED
|
@@ -88,7 +88,7 @@ export class UniversalPropertyOptionsBuilder {
|
|
|
88
88
|
return this.assignOptions({ autoincrement });
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
|
-
* Add the property to the `returning`
|
|
91
|
+
* Add the property to the `returning` clause of inserts, updates and upserts, reloading the database value into the entity.
|
|
92
92
|
*/
|
|
93
93
|
returning(returning = true) {
|
|
94
94
|
return this.assignOptions({ returning });
|
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;
|
|
@@ -128,6 +128,8 @@ export declare class MetadataDiscovery {
|
|
|
128
128
|
private initRelation;
|
|
129
129
|
/** Resolves the `through` option of a virtual to-one relation into a read-only formula property. */
|
|
130
130
|
private initThroughRelation;
|
|
131
|
+
/** `returning` is only honoured for physical columns that can be hydrated back into the entity. */
|
|
132
|
+
private initReturning;
|
|
131
133
|
private initColumnType;
|
|
132
134
|
private getMappedType;
|
|
133
135
|
private getPrefix;
|
|
@@ -174,6 +174,7 @@ export class MetadataDiscovery {
|
|
|
174
174
|
this.initRelation(p);
|
|
175
175
|
this.initThroughRelation(m, p);
|
|
176
176
|
this.initColumnType(p);
|
|
177
|
+
this.initReturning(p);
|
|
177
178
|
});
|
|
178
179
|
forEachProp((m, p) => this.initIndexes(m, p));
|
|
179
180
|
filtered.forEach(meta => this.autoWireBidirectionalProperties(meta));
|
|
@@ -546,7 +547,7 @@ export class MetadataDiscovery {
|
|
|
546
547
|
const fieldNames1 = prop.targetMeta.getPrimaryProps().flatMap(pk => pk.fieldNames);
|
|
547
548
|
const idColumns = fieldNames1.map(fieldName => this.#namingStrategy.joinKeyColumnName(prop.discriminator, fieldName, fieldNames1.length > 1));
|
|
548
549
|
prop.fieldNames ??= [prop.discriminatorColumn, ...idColumns];
|
|
549
|
-
prop.joinColumns ??=
|
|
550
|
+
prop.joinColumns ??= prop.fieldNames.slice(1);
|
|
550
551
|
prop.referencedColumnNames ??= fieldNames1;
|
|
551
552
|
prop.referencedTableName ??= prop.targetMeta.tableName;
|
|
552
553
|
return;
|
|
@@ -563,7 +564,10 @@ export class MetadataDiscovery {
|
|
|
563
564
|
}
|
|
564
565
|
Utils.defaultValue(prop, 'referencedTableName', meta2.tableName);
|
|
565
566
|
if (!prop.joinColumns) {
|
|
566
|
-
|
|
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));
|
|
567
571
|
}
|
|
568
572
|
if (!prop.referencedColumnNames) {
|
|
569
573
|
prop.referencedColumnNames = fieldNames;
|
|
@@ -669,6 +673,12 @@ export class MetadataDiscovery {
|
|
|
669
673
|
if (prop2.kind !== ReferenceKind.SCALAR && prop2.type === meta.className) {
|
|
670
674
|
meta.referencingProperties.push({ meta: meta2, prop: prop2 });
|
|
671
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
|
+
}
|
|
672
682
|
}
|
|
673
683
|
}
|
|
674
684
|
}
|
|
@@ -1173,12 +1183,24 @@ export class MetadataDiscovery {
|
|
|
1173
1183
|
if (!prop.discriminator && !prop.discriminatorColumn && !prop.discriminatorMap && !Array.isArray(prop.target)) {
|
|
1174
1184
|
return;
|
|
1175
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);
|
|
1176
1188
|
prop.polymorphic = true;
|
|
1177
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
|
+
}
|
|
1178
1198
|
prop.discriminatorColumn ??= this.#namingStrategy.discriminatorColumnName(prop.discriminator);
|
|
1179
1199
|
prop.createForeignKeyConstraint = false;
|
|
1180
|
-
|
|
1181
|
-
|
|
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
|
+
}
|
|
1182
1204
|
if (isToOne || isUnionTargetMN) {
|
|
1183
1205
|
const types = prop.type.split(/ ?\| ?/);
|
|
1184
1206
|
prop.polymorphTargets = discovered.filter(m => types.includes(m.className) && !m.embeddable);
|
|
@@ -1438,7 +1460,8 @@ export class MetadataDiscovery {
|
|
|
1438
1460
|
}
|
|
1439
1461
|
}
|
|
1440
1462
|
rootProp.stiFieldNameMap[meta.discriminatorValue] = prop.fieldNames[0];
|
|
1441
|
-
|
|
1463
|
+
// subtypes that only differ in type share the column
|
|
1464
|
+
rootProp.stiFieldNames = Utils.unique([...rootProp.stiFieldNames, ...prop.fieldNames]);
|
|
1442
1465
|
newProp.nullable = true;
|
|
1443
1466
|
newProp.name = name;
|
|
1444
1467
|
newProp.hydrate = false;
|
|
@@ -2129,6 +2152,16 @@ export class MetadataDiscovery {
|
|
|
2129
2152
|
prop.persist = false;
|
|
2130
2153
|
prop.formula = columns => this.#platform.getThroughRelationFormula(prop, columns);
|
|
2131
2154
|
}
|
|
2155
|
+
/** `returning` is only honoured for physical columns that can be hydrated back into the entity. */
|
|
2156
|
+
initReturning(prop) {
|
|
2157
|
+
const owning = prop.kind === ReferenceKind.SCALAR ||
|
|
2158
|
+
prop.kind === ReferenceKind.MANY_TO_ONE ||
|
|
2159
|
+
(prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner) ||
|
|
2160
|
+
(prop.kind === ReferenceKind.EMBEDDED && prop.object);
|
|
2161
|
+
if (prop.returning && (!owning || prop.persist === false || prop.hydrate === false || prop.formula)) {
|
|
2162
|
+
prop.returning = false;
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2132
2165
|
initColumnType(prop) {
|
|
2133
2166
|
this.initUnsigned(prop);
|
|
2134
2167
|
// Get the target properties for FK relations - use targetKey property if specified, otherwise PKs
|
|
@@ -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);
|