@mikro-orm/core 7.3.0-dev.17 → 7.3.0-dev.18
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.js +127 -32
- package/drivers/DatabaseDriver.d.ts +2 -0
- package/drivers/DatabaseDriver.js +16 -3
- package/entity/EntityAssigner.js +15 -3
- package/entity/EntityFactory.d.ts +7 -0
- package/entity/EntityFactory.js +37 -0
- package/entity/EntityLoader.js +24 -9
- package/entity/defineEntity.d.ts +22 -17
- package/entity/validators.js +4 -1
- package/hydration/ObjectHydrator.js +4 -0
- package/metadata/MetadataDiscovery.d.ts +5 -0
- package/metadata/MetadataDiscovery.js +96 -25
- package/metadata/MetadataStorage.d.ts +1 -0
- package/metadata/MetadataStorage.js +20 -3
- package/metadata/MetadataValidator.js +2 -1
- package/package.json +1 -1
- package/platforms/Platform.d.ts +4 -0
- package/platforms/Platform.js +8 -0
- package/serialization/EntitySerializer.js +52 -12
- package/typings.d.ts +21 -5
- package/typings.js +5 -2
- package/unit-of-work/ChangeSetComputer.js +2 -1
- package/unit-of-work/ChangeSetPersister.js +24 -5
- package/unit-of-work/CommitOrderCalculator.js +2 -1
- package/unit-of-work/UnitOfWork.js +20 -5
- package/utils/EntityComparator.d.ts +5 -0
- package/utils/EntityComparator.js +46 -6
- package/utils/QueryHelper.js +10 -2
- package/utils/RawQueryFragment.d.ts +5 -1
- package/utils/RawQueryFragment.js +27 -19
- package/utils/Utils.d.ts +2 -0
- package/utils/Utils.js +24 -8
- package/utils/clone.js +5 -7
- package/utils/fs-utils.js +19 -15
- package/utils/upsert-utils.js +2 -1
package/EntityManager.js
CHANGED
|
@@ -743,10 +743,15 @@ export class EntityManager {
|
|
|
743
743
|
if (Utils.isEmpty(options.orderBy) && !Raw.hasObjectFragments(options.orderBy)) {
|
|
744
744
|
throw new Error('Explicit `orderBy` option required');
|
|
745
745
|
}
|
|
746
|
+
const meta = this.metadata.get(entityName);
|
|
747
|
+
// same merge as em.find, so the cursor and the query see the same orderBy
|
|
748
|
+
if (meta.orderBy) {
|
|
749
|
+
options.orderBy = QueryHelper.mergeOrderBy(options.orderBy, meta.orderBy);
|
|
750
|
+
}
|
|
746
751
|
const [entities, count] = options.includeCount !== false
|
|
747
752
|
? await em.findAndCount(entityName, options.where, options)
|
|
748
753
|
: [await em.find(entityName, options.where, options)];
|
|
749
|
-
return new Cursor(entities, count, options,
|
|
754
|
+
return new Cursor(entities, count, options, meta);
|
|
750
755
|
}
|
|
751
756
|
/**
|
|
752
757
|
* Refreshes the persistent state of an entity from the database, overriding any local changes that have not yet been
|
|
@@ -781,14 +786,38 @@ export class EntityManager {
|
|
|
781
786
|
em.#unitOfWork.unsetIdentity(entity);
|
|
782
787
|
return null;
|
|
783
788
|
}
|
|
789
|
+
// `serialize()` applies `Type.toJSON()` and `serializedName`, take only relations and embeddables from it, scalars from the entity
|
|
790
|
+
const getData = (e, data = helper(e).serialize({
|
|
791
|
+
ignoreSerializers: true,
|
|
792
|
+
includeHidden: true,
|
|
793
|
+
convertCustomTypes: false,
|
|
794
|
+
})) => {
|
|
795
|
+
for (const prop of helper(e).__meta.props) {
|
|
796
|
+
if (prop.kind === ReferenceKind.SCALAR && !prop.ref) {
|
|
797
|
+
if (helper(e).__initialized || prop.primary) {
|
|
798
|
+
data[prop.name] = e[prop.name];
|
|
799
|
+
}
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
if (prop.serializedName && prop.serializedName in data) {
|
|
803
|
+
data[prop.name] = data[prop.serializedName];
|
|
804
|
+
}
|
|
805
|
+
if (prop.kind === ReferenceKind.EMBEDDED && e[prop.name] != null) {
|
|
806
|
+
Utils.asArray(e[prop.name]).forEach((item, i) => getData(item, prop.array ? data[prop.name][i] : data[prop.name]));
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return data;
|
|
810
|
+
};
|
|
811
|
+
const hydrate = (target, source) => {
|
|
812
|
+
em.config
|
|
813
|
+
.getHydrator(this.metadata)
|
|
814
|
+
.hydrate(target, helper(target).__meta, getData(source), em.#entityFactory, 'full', false, false);
|
|
815
|
+
Utils.merge(helper(target).__originalEntityData, this.#comparator.prepareEntity(source));
|
|
816
|
+
};
|
|
784
817
|
let found = false;
|
|
785
818
|
for (const e of fork.#unitOfWork.getIdentityMap()) {
|
|
786
819
|
const ref = em.getReference(e.constructor, helper(e).getPrimaryKey());
|
|
787
|
-
|
|
788
|
-
em.config
|
|
789
|
-
.getHydrator(this.metadata)
|
|
790
|
-
.hydrate(ref, helper(ref).__meta, data, em.#entityFactory, 'full', false, false);
|
|
791
|
-
Utils.merge(helper(ref).__originalEntityData, this.#comparator.prepareEntity(e));
|
|
820
|
+
hydrate(ref, e);
|
|
792
821
|
// index a refreshed `targetKey` value, `getByKey` ignores the old one as stale
|
|
793
822
|
if (helper(ref).__meta.root.targetKeys) {
|
|
794
823
|
em.#unitOfWork.getIdentityMap().store(ref);
|
|
@@ -796,15 +825,7 @@ export class EntityManager {
|
|
|
796
825
|
found ||= ref === entity;
|
|
797
826
|
}
|
|
798
827
|
if (!found) {
|
|
799
|
-
|
|
800
|
-
ignoreSerializers: true,
|
|
801
|
-
includeHidden: true,
|
|
802
|
-
convertCustomTypes: false,
|
|
803
|
-
});
|
|
804
|
-
em.config
|
|
805
|
-
.getHydrator(this.metadata)
|
|
806
|
-
.hydrate(entity, wrapped.__meta, data, em.#entityFactory, 'full', false, false);
|
|
807
|
-
Utils.merge(wrapped.__originalEntityData, this.#comparator.prepareEntity(reloaded));
|
|
828
|
+
hydrate(entity, reloaded);
|
|
808
829
|
}
|
|
809
830
|
return entity;
|
|
810
831
|
}
|
|
@@ -1025,6 +1046,15 @@ export class EntityManager {
|
|
|
1025
1046
|
where[meta.primaryKeys[0]] = ret.insertId;
|
|
1026
1047
|
}
|
|
1027
1048
|
}
|
|
1049
|
+
// a `null` unique value cannot identify the row, and as it never conflicts, the row was inserted
|
|
1050
|
+
if (!Utils.hasObjectKeys(where)) {
|
|
1051
|
+
const insertId = meta.simplePK && ret.insertId != null ? { [meta.primaryKeys[0]]: ret.insertId } : {};
|
|
1052
|
+
Object.assign(where, Utils.getPrimaryKeyCond(entity, meta.primaryKeys) ?? insertId);
|
|
1053
|
+
}
|
|
1054
|
+
// an empty condition would hydrate the entity from an arbitrary row, mongo still gets one for nested conflict fields
|
|
1055
|
+
if (!Utils.hasObjectKeys(where) && platform.usesPivotTable()) {
|
|
1056
|
+
throw new Error(`Cannot find the upserted ${meta.className} row, as neither its primary key nor a unique value is known`);
|
|
1057
|
+
}
|
|
1028
1058
|
const data2 = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => this.driver.findOne(meta.class, where, {
|
|
1029
1059
|
fields: returning.concat(...(options.onConflictMergeFields ?? [])),
|
|
1030
1060
|
ctx,
|
|
@@ -1032,6 +1062,9 @@ export class EntityManager {
|
|
|
1032
1062
|
connectionType: 'write',
|
|
1033
1063
|
schema: options.schema,
|
|
1034
1064
|
}));
|
|
1065
|
+
if (!data2) {
|
|
1066
|
+
throw new Error(`Cannot find the upserted ${meta.className} row for condition ${JSON.stringify(where)}`);
|
|
1067
|
+
}
|
|
1035
1068
|
em.getHydrator().hydrate(entity, meta, data2, em.#entityFactory, 'full', false, true);
|
|
1036
1069
|
}
|
|
1037
1070
|
// recompute the data as there might be some values missing (e.g. those with db column defaults)
|
|
@@ -1183,8 +1216,10 @@ export class EntityManager {
|
|
|
1183
1216
|
unique.findIndex(p => data[p] ?? data[p.substring(0, p.indexOf('.'))] != null);
|
|
1184
1217
|
const tmp = getWhereCondition(meta, options.onConflictFields, row, where);
|
|
1185
1218
|
propIndex = tmp.propIndex;
|
|
1219
|
+
// a partial composite PK cannot identify the row
|
|
1220
|
+
const partialPK = meta.compositePK && !Utils.isPlainObject(tmp.where) && meta.primaryKeys.some(pk => row[pk] == null);
|
|
1186
1221
|
where = QueryHelper.processWhere({
|
|
1187
|
-
where: tmp.where,
|
|
1222
|
+
where: partialPK ? {} : tmp.where,
|
|
1188
1223
|
entityName,
|
|
1189
1224
|
metadata: this.metadata,
|
|
1190
1225
|
platform: this.getPlatform(),
|
|
@@ -1199,6 +1234,25 @@ export class EntityManager {
|
|
|
1199
1234
|
allWhere.push(where);
|
|
1200
1235
|
dataIndexes.push(i);
|
|
1201
1236
|
}
|
|
1237
|
+
// mongo inserts the rows with an empty condition as part of the batch and reports their ids
|
|
1238
|
+
const positional = !this.getPlatform().usesPivotTable() ||
|
|
1239
|
+
((this.getPlatform().usesReturningStatement() || this.getPlatform().usesOutputStatement()) &&
|
|
1240
|
+
options.onConflictAction !== 'ignore' &&
|
|
1241
|
+
!options.onConflictWhere);
|
|
1242
|
+
const hasUniqueKey = !!options.onConflictFields || meta.uniques.length > 0 || meta.props.some(p => p.unique && !p.primary);
|
|
1243
|
+
// only an autoincrement PK can be derived from the `insertId` of the batch
|
|
1244
|
+
const autoincrement = meta.getPrimaryProps().some(p => p.autoincrement);
|
|
1245
|
+
// a TPT child table does not return its rows, so they are never mapped by position
|
|
1246
|
+
const peel = !!meta.tptParent || (!positional && (hasUniqueKey || !autoincrement));
|
|
1247
|
+
// a row with a `null` unique value has nothing to be reloaded by, the single row path maps its PK from the insert
|
|
1248
|
+
for (let idx = 0; peel && idx < allWhere.length; idx++) {
|
|
1249
|
+
if (!Utils.hasObjectKeys(allWhere[idx])) {
|
|
1250
|
+
result[dataIndexes[idx]] = await em.upsert(entityName, data[dataIndexes[idx]], options);
|
|
1251
|
+
allData.splice(idx, 1);
|
|
1252
|
+
allWhere.splice(idx, 1);
|
|
1253
|
+
dataIndexes.splice(idx--, 1);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1202
1256
|
if (allData.length === 0) {
|
|
1203
1257
|
return [...new Set(result)];
|
|
1204
1258
|
}
|
|
@@ -1248,7 +1302,11 @@ export class EntityManager {
|
|
|
1248
1302
|
// skip the reload only when RETURNING brought back a row for every input
|
|
1249
1303
|
// (onConflictWhere can suppress writes, leaving some rows out)
|
|
1250
1304
|
// oxfmt-ignore
|
|
1251
|
-
const
|
|
1305
|
+
const getUniqueFields = (where) => options.onConflictFields ?? (Utils.isPlainObject(where) ? Object.keys(where).flatMap(key => Utils.splitPrimaryKeys(key)) : meta.primaryKeys);
|
|
1306
|
+
// a row without a condition has no unique fields to derive
|
|
1307
|
+
const uniqueFields = getUniqueFields(allWhere.find(cond => Utils.hasObjectKeys(cond)) ?? allWhere[0]);
|
|
1308
|
+
// an empty condition identifies no row, it must not turn into a reload condition matching every row
|
|
1309
|
+
const getRowUniqueFields = (where) => Utils.isPlainObject(where) && !Utils.hasObjectKeys(where) ? uniqueFields : getUniqueFields(where);
|
|
1252
1310
|
const platform = this.getPlatform();
|
|
1253
1311
|
const returning = getOnConflictReturningFields(meta, data[dataIndexes[0]], uniqueFields, options, platform);
|
|
1254
1312
|
if (options.onConflictWhere && res.rows?.length !== allData.length) {
|
|
@@ -1265,10 +1323,14 @@ export class EntityManager {
|
|
|
1265
1323
|
Utils.keys(cond).forEach(key => add.add(key));
|
|
1266
1324
|
}
|
|
1267
1325
|
const where = { $or: [] };
|
|
1326
|
+
// a row without a condition was inserted, only the primary key reported for it can identify it
|
|
1327
|
+
const insertedPKs = allData.map((row, idx) => Utils.hasObjectKeys(allWhere[idx]) ? null : Utils.getPrimaryKeyCond(entitiesByData.get(row), meta.primaryKeys));
|
|
1268
1328
|
dataIndexes.forEach((index, idx) => {
|
|
1269
1329
|
const item = data[index];
|
|
1270
|
-
where.$or[idx] = {};
|
|
1271
|
-
|
|
1330
|
+
where.$or[idx] = { ...insertedPKs[idx] };
|
|
1331
|
+
// the inputs of one batch can be identified by different unique keys
|
|
1332
|
+
const fields = insertedPKs[idx] ? [] : getRowUniqueFields(allWhere[idx]);
|
|
1333
|
+
const props = Array.isArray(fields) ? fields : Object.keys(item);
|
|
1272
1334
|
props.forEach(prop => {
|
|
1273
1335
|
where.$or[idx][prop] = item[prop];
|
|
1274
1336
|
});
|
|
@@ -1283,8 +1345,8 @@ export class EntityManager {
|
|
|
1283
1345
|
connectionType: 'write',
|
|
1284
1346
|
schema: options.schema,
|
|
1285
1347
|
}));
|
|
1286
|
-
|
|
1287
|
-
const
|
|
1348
|
+
if (loadPK.size > 0) {
|
|
1349
|
+
const comparableRows = data2.map(row => {
|
|
1288
1350
|
const tmp = {};
|
|
1289
1351
|
add.forEach(k => {
|
|
1290
1352
|
const prop = meta.properties[k];
|
|
@@ -1292,25 +1354,58 @@ export class EntityManager {
|
|
|
1292
1354
|
tmp[k] = prop?.customType ? prop.customType.convertToDatabaseValue(row[k], this.getPlatform()) : row[k];
|
|
1293
1355
|
}
|
|
1294
1356
|
});
|
|
1295
|
-
return
|
|
1357
|
+
return tmp;
|
|
1296
1358
|
});
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1359
|
+
// rows carry the keys of every condition in the batch, compare only this one's
|
|
1360
|
+
const isMatch = (cond, idx) => this.#comparator.matching(entityName, cond, Object.fromEntries(Object.entries(comparableRows[idx]).filter(([k]) => k in cond)));
|
|
1361
|
+
// When every condition key is a comparable prop checked with `!==`, a matching row has the same primitive
|
|
1362
|
+
// values, so rows can be grouped by those values once instead of comparing every row for every entity.
|
|
1363
|
+
// Props outside `comparableProps` are skipped by the comparator, so they never qualify for grouping.
|
|
1364
|
+
const groupableProps = new Set(meta.comparableProps.filter(prop => this.#comparator.isStrictlyCompared(prop)));
|
|
1365
|
+
const groups = new Map();
|
|
1366
|
+
// `null` and `undefined` share a key, which can only make a group wider than what `matching` accepts
|
|
1367
|
+
const groupKey = (data, keys) => keys.map(k => (data[k] == null ? 'null' : `${typeof data[k]}:${data[k]}`)).join('\0');
|
|
1368
|
+
const findRowIndex = (cond) => {
|
|
1369
|
+
const keys = Utils.keys(cond);
|
|
1370
|
+
if (!keys.every(k => groupableProps.has(meta.properties[k]))) {
|
|
1371
|
+
return comparableRows.findIndex((_, idx) => isMatch(cond, idx));
|
|
1372
|
+
}
|
|
1373
|
+
const signature = keys.join('\0');
|
|
1374
|
+
if (!groups.has(signature)) {
|
|
1375
|
+
const rowsByKey = new Map();
|
|
1376
|
+
comparableRows.forEach((tmp, idx) => {
|
|
1377
|
+
const rowKey = groupKey(tmp, keys);
|
|
1378
|
+
const indexes = rowsByKey.get(rowKey) ?? [];
|
|
1379
|
+
indexes.push(idx);
|
|
1380
|
+
rowsByKey.set(rowKey, indexes);
|
|
1381
|
+
});
|
|
1382
|
+
groups.set(signature, rowsByKey);
|
|
1383
|
+
}
|
|
1384
|
+
const group = groups.get(signature);
|
|
1385
|
+
return group.get(groupKey(cond, keys))?.find(idx => isMatch(cond, idx)) ?? -1;
|
|
1386
|
+
};
|
|
1387
|
+
for (const [entity, cond] of loadPK.entries()) {
|
|
1388
|
+
const row = data2[findRowIndex(cond)];
|
|
1389
|
+
/* v8 ignore next */
|
|
1390
|
+
if (!row) {
|
|
1391
|
+
throw new Error(`Cannot find matching entity for condition ${JSON.stringify(cond)}`);
|
|
1392
|
+
}
|
|
1393
|
+
em.getHydrator().hydrate(entity, meta, row, em.#entityFactory, 'full', false, true);
|
|
1300
1394
|
}
|
|
1301
|
-
em.getHydrator().hydrate(entity, meta, row, em.#entityFactory, 'full', false, true);
|
|
1302
1395
|
}
|
|
1303
1396
|
if (loadPK.size !== data2.length && Array.isArray(uniqueFields)) {
|
|
1304
1397
|
for (let i = 0; i < allData.length; i++) {
|
|
1305
1398
|
const data = allData[i];
|
|
1306
|
-
const
|
|
1307
|
-
|
|
1308
|
-
a
|
|
1309
|
-
|
|
1310
|
-
|
|
1399
|
+
const fields = (insertedPKs[i] ? meta.primaryKeys : getRowUniqueFields(allWhere[i]));
|
|
1400
|
+
const cond = insertedPKs[i] ??
|
|
1401
|
+
fields.reduce((a, b) => {
|
|
1402
|
+
// @ts-ignore
|
|
1403
|
+
a[b] = data[b];
|
|
1404
|
+
return a;
|
|
1405
|
+
}, {});
|
|
1311
1406
|
const entity = entitiesByData.get(data);
|
|
1312
1407
|
const row = data2.find(item => {
|
|
1313
|
-
const pk =
|
|
1408
|
+
const pk = fields.reduce((a, b) => {
|
|
1314
1409
|
// @ts-ignore
|
|
1315
1410
|
a[b] = item[b];
|
|
1316
1411
|
return a;
|
|
@@ -81,6 +81,8 @@ export declare abstract class DatabaseDriver<C extends Connection> implements ID
|
|
|
81
81
|
*/
|
|
82
82
|
private mapCursorOffset;
|
|
83
83
|
protected createCursorCondition<T extends object>(definition: (readonly [keyof T & string, QueryOrder])[], offsets: Dictionary[], inverse: boolean, meta: EntityMetadata<T>, fromJson?: boolean): FilterQuery<T>;
|
|
84
|
+
/** @internal Polymorphic embeddable variants can declare same-named properties with different field names. */
|
|
85
|
+
protected variantProps(prop: EntityProperty, data?: Dictionary): Dictionary<EntityProperty>;
|
|
84
86
|
/** @internal */
|
|
85
87
|
mapDataToFieldNames(data: Dictionary, stringifyJsonArrays: boolean, properties?: Record<string, EntityProperty>, convertCustomTypes?: boolean, object?: boolean): Dictionary;
|
|
86
88
|
protected inlineEmbeddables<T extends object>(meta: EntityMetadata<T>, data: T, where?: boolean): void;
|
|
@@ -403,6 +403,19 @@ export class DatabaseDriver {
|
|
|
403
403
|
}
|
|
404
404
|
return { ...atOrPast, ...past };
|
|
405
405
|
}
|
|
406
|
+
/** @internal Polymorphic embeddable variants can declare same-named properties with different field names. */
|
|
407
|
+
variantProps(prop, data) {
|
|
408
|
+
const meta = prop.targetMeta;
|
|
409
|
+
// eslint-disable-next-line eqeqeq
|
|
410
|
+
const variant = meta?.polymorphs?.find(m => m.discriminatorValue == data?.[meta.discriminatorColumn]);
|
|
411
|
+
const props = variant ? { ...prop.embeddedProps } : prop.embeddedProps;
|
|
412
|
+
for (const p of variant ? Object.values(props) : []) {
|
|
413
|
+
if (variant.properties[p.embedded[1]] && Utils.isSameKind(variant.properties[p.embedded[1]], p)) {
|
|
414
|
+
props[p.embedded[1]] = p;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return props;
|
|
418
|
+
}
|
|
406
419
|
/** @internal */
|
|
407
420
|
mapDataToFieldNames(data, stringifyJsonArrays, properties, convertCustomTypes, object) {
|
|
408
421
|
if (!properties || data == null) {
|
|
@@ -417,17 +430,17 @@ export class DatabaseDriver {
|
|
|
417
430
|
if (prop.embeddedProps && !prop.object && !object) {
|
|
418
431
|
const copy = data[k];
|
|
419
432
|
delete data[k];
|
|
420
|
-
Object.assign(data, this.mapDataToFieldNames(copy, stringifyJsonArrays, prop
|
|
433
|
+
Object.assign(data, this.mapDataToFieldNames(copy, stringifyJsonArrays, this.variantProps(prop, copy), convertCustomTypes));
|
|
421
434
|
return;
|
|
422
435
|
}
|
|
423
436
|
if (prop.embeddedProps && (object || prop.object)) {
|
|
424
437
|
const copy = data[k];
|
|
425
438
|
delete data[k];
|
|
426
439
|
if (prop.array) {
|
|
427
|
-
data[prop.fieldNames[0]] = copy?.map((item) => this.mapDataToFieldNames(item, stringifyJsonArrays, prop
|
|
440
|
+
data[prop.fieldNames[0]] = copy?.map((item) => this.mapDataToFieldNames(item, stringifyJsonArrays, this.variantProps(prop, item), convertCustomTypes, true));
|
|
428
441
|
}
|
|
429
442
|
else {
|
|
430
|
-
data[prop.fieldNames[0]] = this.mapDataToFieldNames(copy, stringifyJsonArrays, prop
|
|
443
|
+
data[prop.fieldNames[0]] = this.mapDataToFieldNames(copy, stringifyJsonArrays, this.variantProps(prop, copy), convertCustomTypes, true);
|
|
431
444
|
}
|
|
432
445
|
if (stringifyJsonArrays && prop.array && !object) {
|
|
433
446
|
data[prop.fieldNames[0]] = this.platform.convertJsonToDatabaseValue(data[prop.fieldNames[0]]);
|
package/entity/EntityAssigner.js
CHANGED
|
@@ -234,14 +234,26 @@ export class EntityAssigner {
|
|
|
234
234
|
entity[propName].push(...Object.values(tmp));
|
|
235
235
|
});
|
|
236
236
|
}
|
|
237
|
+
const polymorphs = prop.targetMeta?.polymorphs;
|
|
238
|
+
// eslint-disable-next-line eqeqeq
|
|
239
|
+
const variant = polymorphs?.find(m => m.discriminatorValue == value[prop.targetMeta.discriminatorColumn]);
|
|
237
240
|
const create = () => EntityAssigner.validateEM(em) &&
|
|
238
|
-
em.getEntityFactory().createEmbeddable(prop.targetMeta.class, value, {
|
|
241
|
+
em.getEntityFactory().createEmbeddable((variant ?? prop.targetMeta).class, value, {
|
|
239
242
|
convertCustomTypes: options.convertCustomTypes,
|
|
240
243
|
newEntity: options.mergeEmbeddedProperties ? !('propName' in entity) : true,
|
|
241
244
|
});
|
|
242
|
-
|
|
245
|
+
// a different polymorphic variant cannot be merged into the current one
|
|
246
|
+
const merge = options.mergeEmbeddedProperties && (!variant || entity[propName]?.constructor === variant.class);
|
|
247
|
+
entity[propName] = (merge ? entity[propName] || create() : create());
|
|
248
|
+
const props = polymorphs ? { ...prop.embeddedProps } : prop.embeddedProps;
|
|
249
|
+
// polymorphic variants can declare same-named properties of different kinds or relation targets
|
|
250
|
+
for (const p of polymorphs ? entity[propName].__meta.props : []) {
|
|
251
|
+
if (props[p.name] && props[p.name].targetMeta?.class !== p.targetMeta?.class) {
|
|
252
|
+
props[p.name] = { ...p, nullable: prop.nullable || p.nullable };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
243
255
|
Object.keys(value).forEach(key => {
|
|
244
|
-
EntityAssigner.assignProperty(entity[propName], key,
|
|
256
|
+
EntityAssigner.assignProperty(entity[propName], key, props, value, options);
|
|
245
257
|
});
|
|
246
258
|
}
|
|
247
259
|
static createCollectionItem(item, em, prop, invalid, options) {
|
|
@@ -38,6 +38,13 @@ export declare class EntityFactory {
|
|
|
38
38
|
constructor(em: EntityManager);
|
|
39
39
|
/** Creates a new entity instance or returns an existing one from the identity map, hydrating it with the provided data. */
|
|
40
40
|
create<T extends object, P extends string = string>(entityName: EntityName<T>, data: EntityData<T>, options?: FactoryOptions): New<T, P>;
|
|
41
|
+
/**
|
|
42
|
+
* Whether merging `data` into an initialized entity would change nothing: every key is a scalar the entity already
|
|
43
|
+
* holds by identity, and keys `mergeData` always re-applies (formula, version, generated, discriminator) match the snapshot.
|
|
44
|
+
*/
|
|
45
|
+
private isNoopMerge;
|
|
46
|
+
/** Whether hydrating `value` into `prop` would leave `current` as is, mirroring the hydrator's Date and boolean coercion. */
|
|
47
|
+
private holdsValue;
|
|
41
48
|
/** Merges new data into an existing entity, preserving user-modified properties. */
|
|
42
49
|
mergeData<T extends object>(meta: EntityMetadata<T>, entity: T, data: EntityData<T>, options?: FactoryOptions): void;
|
|
43
50
|
/** Creates or retrieves an uninitialized entity reference by its primary key or alternate key. */
|
package/entity/EntityFactory.js
CHANGED
|
@@ -149,8 +149,45 @@ export class EntityFactory {
|
|
|
149
149
|
wrapped.__processing = false;
|
|
150
150
|
return entity;
|
|
151
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Whether merging `data` into an initialized entity would change nothing: every key is a scalar the entity already
|
|
154
|
+
* holds by identity, and keys `mergeData` always re-applies (formula, version, generated, discriminator) match the snapshot.
|
|
155
|
+
*/
|
|
156
|
+
isNoopMerge(meta, entity, data, options) {
|
|
157
|
+
const wrapped = helper(entity);
|
|
158
|
+
if (!wrapped.__initialized || options.normalizeAccessors) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
for (const key of Utils.keys(data)) {
|
|
162
|
+
const prop = meta.properties[key];
|
|
163
|
+
const value = data[key];
|
|
164
|
+
// custom types are left to the full merge, as their JS and database values can differ
|
|
165
|
+
if (!prop || prop.customType || value === undefined || !this.holdsValue(prop, entity[key], value)) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
const reapplied = prop.formula || prop.version || prop.generated || key === meta.root.discriminatorColumn;
|
|
169
|
+
if (reapplied && (wrapped.__originalEntityData?.[key] !== value || !wrapped.__loadedProperties.has(key))) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
/** Whether hydrating `value` into `prop` would leave `current` as is, mirroring the hydrator's Date and boolean coercion. */
|
|
176
|
+
holdsValue(prop, current, value) {
|
|
177
|
+
if (current instanceof Date && value instanceof Date) {
|
|
178
|
+
return current.getTime() === value.getTime();
|
|
179
|
+
}
|
|
180
|
+
if (prop.runtimeType === 'boolean' && value != null) {
|
|
181
|
+
return current === !!value;
|
|
182
|
+
}
|
|
183
|
+
return current === value;
|
|
184
|
+
}
|
|
152
185
|
/** Merges new data into an existing entity, preserving user-modified properties. */
|
|
153
186
|
mergeData(meta, entity, data, options = {}) {
|
|
187
|
+
// joined rows often repeat exactly what the entity already holds, skip the snapshot and diffing then
|
|
188
|
+
if (this.isNoopMerge(meta, entity, data, options)) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
154
191
|
// merge unchanged properties automatically
|
|
155
192
|
data = QueryHelper.processParams(data);
|
|
156
193
|
const existsData = this.#comparator.prepareEntity(entity);
|
package/entity/EntityLoader.js
CHANGED
|
@@ -312,18 +312,25 @@ export class EntityLoader {
|
|
|
312
312
|
}
|
|
313
313
|
initializeManyToMany(filtered, children, prop, field, customOrder, partial, readonly) {
|
|
314
314
|
if (prop.mappedBy) {
|
|
315
|
+
// group children by owner in one pass
|
|
316
|
+
const map = new Map(filtered.map(entity => [entity, []]));
|
|
317
|
+
for (const child of children) {
|
|
318
|
+
for (const owner of child[prop.mappedBy].getItems(false)) {
|
|
319
|
+
map.get(owner)?.push(child);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
315
322
|
for (const entity of filtered) {
|
|
316
|
-
|
|
317
|
-
entity[field].hydrate(items, true, partial, readonly);
|
|
323
|
+
entity[field].hydrate(map.get(entity), true, partial, readonly);
|
|
318
324
|
}
|
|
319
325
|
}
|
|
320
326
|
else {
|
|
321
327
|
// owning side of M:N without pivot table needs to be reordered
|
|
328
|
+
const childIndex = new Map(children.map((child, idx) => [child, idx]));
|
|
322
329
|
for (const entity of filtered) {
|
|
323
|
-
|
|
324
|
-
const items =
|
|
325
|
-
if (
|
|
326
|
-
items.sort((a, b) =>
|
|
330
|
+
// keep the order of references, or the order of loaded children when a custom order is used
|
|
331
|
+
const items = entity[prop.name].getItems(false).filter(item => childIndex.has(item));
|
|
332
|
+
if (customOrder) {
|
|
333
|
+
items.sort((a, b) => childIndex.get(a) - childIndex.get(b));
|
|
327
334
|
}
|
|
328
335
|
entity[field].hydrate(items, true, partial, readonly);
|
|
329
336
|
}
|
|
@@ -673,12 +680,20 @@ export class EntityLoader {
|
|
|
673
680
|
const subCond = Utils.isPlainObject(where[prop.name]) ? { ...where[prop.name] } : {};
|
|
674
681
|
const meta2 = prop.targetMeta;
|
|
675
682
|
const pk = Utils.getPrimaryKeyHash(meta2.primaryKeys);
|
|
683
|
+
// `$not` wrapping entity conditions stays on the target entity, only operators on the relation itself map to its PK
|
|
684
|
+
const isRelationOperator = (key, value) => {
|
|
685
|
+
if (key === '$not' && Utils.isPlainObject(value)) {
|
|
686
|
+
return Utils.getObjectQueryKeys(value).every(k => Utils.isOperator(k, false));
|
|
687
|
+
}
|
|
688
|
+
return Utils.isOperator(key, false);
|
|
689
|
+
};
|
|
676
690
|
['$and', '$or'].forEach(op => {
|
|
677
691
|
if (where[op]) {
|
|
678
692
|
const child = where[op]
|
|
679
693
|
.map((cond) => cond[prop.name])
|
|
680
694
|
.filter((sub) => sub != null &&
|
|
681
|
-
!(Utils.isPlainObject(sub) &&
|
|
695
|
+
!(Utils.isPlainObject(sub) &&
|
|
696
|
+
Utils.getObjectQueryKeys(sub).every(key => isRelationOperator(key, sub[key]))))
|
|
682
697
|
.map((cond) => {
|
|
683
698
|
if (Utils.isPrimaryKey(cond)) {
|
|
684
699
|
return { [pk]: cond };
|
|
@@ -691,7 +706,7 @@ export class EntityLoader {
|
|
|
691
706
|
}
|
|
692
707
|
}
|
|
693
708
|
});
|
|
694
|
-
const operators = Object.keys(subCond).filter(key =>
|
|
709
|
+
const operators = Object.keys(subCond).filter(key => isRelationOperator(key, subCond[key]));
|
|
695
710
|
if (operators.length > 0) {
|
|
696
711
|
subCond[pk] = Utils.isPlainObject(subCond[pk]) ? { ...subCond[pk] } : (subCond[pk] ?? {});
|
|
697
712
|
operators.forEach(op => {
|
|
@@ -809,7 +824,7 @@ export class EntityLoader {
|
|
|
809
824
|
if ([ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(wrapped.__meta.properties[f].kind)) {
|
|
810
825
|
return entity[f].getItems(false).every((item) => this.isPropertyLoaded(item, r.join('.')));
|
|
811
826
|
}
|
|
812
|
-
return this.isPropertyLoaded(entity[f], r.join('.'));
|
|
827
|
+
return this.isPropertyLoaded(Reference.unwrapReference(entity[f]), r.join('.'));
|
|
813
828
|
}
|
|
814
829
|
filterReferences(entities, field, options, ref, populate) {
|
|
815
830
|
if (ref) {
|
package/entity/defineEntity.d.ts
CHANGED
|
@@ -638,6 +638,21 @@ type AllKeys<TProperties, TBase> = keyof TProperties | (IsNever<TBase> extends t
|
|
|
638
638
|
type PartialWhere<TProperties, TBase> = string | FilterQuery<{
|
|
639
639
|
[K in AllKeys<TProperties, TBase> & string]?: unknown;
|
|
640
640
|
}>;
|
|
641
|
+
type DefineEntityFilterMap = Dictionary<{
|
|
642
|
+
name: string;
|
|
643
|
+
cond: Dictionary | ((args: Dictionary, type: 'read' | 'update' | 'delete', em: any, options?: FindOptions<any, any, any, any> | FindOneOptions<any, any, any, any>, entityName?: string) => MaybePromise<FilterQuery<any>>);
|
|
644
|
+
default?: boolean;
|
|
645
|
+
entity?: EntityName<any> | EntityName<any>[];
|
|
646
|
+
args?: boolean;
|
|
647
|
+
strict?: boolean;
|
|
648
|
+
rls?: boolean | {
|
|
649
|
+
setting?: string;
|
|
650
|
+
};
|
|
651
|
+
}>;
|
|
652
|
+
type DefineEntityFilterMapLoose = Dictionary<{
|
|
653
|
+
name: string;
|
|
654
|
+
cond: Dictionary;
|
|
655
|
+
}>;
|
|
641
656
|
/** Metadata descriptor for `defineEntity()`, combining entity options with property definitions. */
|
|
642
657
|
export interface EntityMetadataWithProperties<TName extends string, TTableName extends string, TProperties extends Record<string, any>, TPK extends (keyof TProperties)[] | undefined = undefined, TBase = never, TRepository = never, TForceObject extends boolean = false, TDiscriminatorColumn extends string | undefined = undefined, TDiscriminatorValue extends string | number | undefined = undefined, TBaseDiscriminatorColumn extends string | undefined = undefined, TEmbeddable extends boolean = false, TForceUndefined extends boolean = false> extends Omit<Partial<EntityMetadata<InferEntityFromProperties<TProperties, TPK, TBase, TRepository>>>, 'properties' | 'extends' | 'primaryKeys' | 'hooks' | 'discriminator' | 'discriminatorColumn' | 'discriminatorValue' | 'versionProperty' | 'concurrencyCheckKeys' | 'serializedPrimaryKey' | 'indexes' | 'uniques' | 'triggers' | 'repository' | 'filters' | 'orderBy'> {
|
|
643
658
|
name: TName;
|
|
@@ -649,20 +664,10 @@ export interface EntityMetadataWithProperties<TName extends string, TTableName e
|
|
|
649
664
|
'~discriminatorColumn'?: TBaseDiscriminatorColumn;
|
|
650
665
|
});
|
|
651
666
|
properties: TProperties | ((properties: PropertyBuilders) => TProperties);
|
|
652
|
-
primaryKeys?: TPK & InferPrimaryKeyConstraint<TProperties
|
|
667
|
+
primaryKeys?: TPK & NoInfer<InferPrimaryKeyConstraint<TProperties>>[];
|
|
653
668
|
hooks?: DefineEntityHooks;
|
|
654
669
|
repository?: () => TRepository;
|
|
655
|
-
filters?:
|
|
656
|
-
name: string;
|
|
657
|
-
cond: Dictionary | ((args: Dictionary, type: 'read' | 'update' | 'delete', em: any, options?: FindOptions<any, any, any, any> | FindOneOptions<any, any, any, any>, entityName?: string) => MaybePromise<FilterQuery<any>>);
|
|
658
|
-
default?: boolean;
|
|
659
|
-
entity?: EntityName<any> | EntityName<any>[];
|
|
660
|
-
args?: boolean;
|
|
661
|
-
strict?: boolean;
|
|
662
|
-
rls?: boolean | {
|
|
663
|
-
setting?: string;
|
|
664
|
-
};
|
|
665
|
-
}>;
|
|
670
|
+
filters?: DefineEntityFilterMap | IsNever<TRepository, never, DefineEntityFilterMapLoose>;
|
|
666
671
|
forceObject?: TForceObject;
|
|
667
672
|
/** Hydrate `null` as `undefined` for this entity and infer nullable properties as `T | undefined`. */
|
|
668
673
|
forceUndefined?: TForceUndefined;
|
|
@@ -677,16 +682,16 @@ export interface EntityMetadataWithProperties<TName extends string, TTableName e
|
|
|
677
682
|
/** Alias for `discriminator`. */
|
|
678
683
|
discriminatorColumn?: TDiscriminatorColumn;
|
|
679
684
|
discriminatorValue?: TDiscriminatorValue;
|
|
680
|
-
versionProperty?: AllKeys<TProperties, TBase
|
|
681
|
-
concurrencyCheckKeys?: Set<AllKeys<TProperties, TBase
|
|
682
|
-
serializedPrimaryKey?: AllKeys<TProperties, TBase
|
|
685
|
+
versionProperty?: NoInfer<AllKeys<TProperties, TBase>>;
|
|
686
|
+
concurrencyCheckKeys?: Set<NoInfer<AllKeys<TProperties, TBase>>>;
|
|
687
|
+
serializedPrimaryKey?: NoInfer<AllKeys<TProperties, TBase>>;
|
|
683
688
|
indexes?: {
|
|
684
689
|
properties?: NoInfer<AllKeys<TProperties, TBase>> | NoInfer<AllKeys<TProperties, TBase>>[];
|
|
685
690
|
name?: string;
|
|
686
691
|
type?: string;
|
|
687
692
|
options?: Dictionary;
|
|
688
693
|
expression?: string | IndexCallback<InferEntityFromProperties<TProperties, TPK, TBase>>;
|
|
689
|
-
where?: PartialWhere<TProperties, TBase
|
|
694
|
+
where?: NoInfer<PartialWhere<TProperties, TBase>>;
|
|
690
695
|
columns?: IndexColumnOptions[];
|
|
691
696
|
include?: NoInfer<AllKeys<TProperties, TBase>> | NoInfer<AllKeys<TProperties, TBase>>[];
|
|
692
697
|
fillFactor?: number;
|
|
@@ -699,7 +704,7 @@ export interface EntityMetadataWithProperties<TName extends string, TTableName e
|
|
|
699
704
|
name?: string;
|
|
700
705
|
options?: Dictionary;
|
|
701
706
|
expression?: string | IndexCallback<InferEntityFromProperties<TProperties, TPK, TBase>>;
|
|
702
|
-
where?: PartialWhere<TProperties, TBase
|
|
707
|
+
where?: NoInfer<PartialWhere<TProperties, TBase>>;
|
|
703
708
|
deferMode?: DeferMode | `${DeferMode}`;
|
|
704
709
|
columns?: IndexColumnOptions[];
|
|
705
710
|
include?: NoInfer<AllKeys<TProperties, TBase>> | NoInfer<AllKeys<TProperties, TBase>>[];
|
package/entity/validators.js
CHANGED
|
@@ -24,7 +24,10 @@ export function validateProperty(prop, givenValue, entity) {
|
|
|
24
24
|
}
|
|
25
25
|
function getValue(o, prop) {
|
|
26
26
|
if (prop.embedded && prop.embedded[0] in o) {
|
|
27
|
-
|
|
27
|
+
const embeddable = o[prop.embedded[0]];
|
|
28
|
+
const variantProp = embeddable?.__meta?.properties[prop.embedded[1]];
|
|
29
|
+
// a same-named property of a different kind in another polymorphic variant
|
|
30
|
+
return variantProp && !Utils.isSameKind(variantProp, prop) ? undefined : embeddable?.[prop.embedded[1]];
|
|
28
31
|
}
|
|
29
32
|
/* v8 ignore next */
|
|
30
33
|
if (prop.ref) {
|
|
@@ -313,6 +313,10 @@ export class ObjectHydrator extends Hydrator {
|
|
|
313
313
|
? dataKey + this.wrap(childProp.embedded[1])
|
|
314
314
|
: this.wrap(childProp.name);
|
|
315
315
|
const prop2 = childMeta.properties[childProp.embedded[1]];
|
|
316
|
+
// a same-named property of a different kind in another variant
|
|
317
|
+
if (prop2 && !Utils.isSameKind(prop2, childProp)) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
316
320
|
const prop3 = {
|
|
317
321
|
...prop2,
|
|
318
322
|
name: childProp.name,
|
|
@@ -34,6 +34,11 @@ export declare class MetadataDiscovery {
|
|
|
34
34
|
private initManyToOneFields;
|
|
35
35
|
private initOneToManyFields;
|
|
36
36
|
private processEntity;
|
|
37
|
+
/**
|
|
38
|
+
* Groups the relations of all entities by target class name in one pass, so `findReferencingProperties()`
|
|
39
|
+
* does not go over the relations of every entity for each entity. Lists keep the order of `metadata` and `relations`.
|
|
40
|
+
*/
|
|
41
|
+
private indexReferencingProperties;
|
|
37
42
|
private findReferencingProperties;
|
|
38
43
|
private initFactoryField;
|
|
39
44
|
private ensureCorrectFKOrderInPivotEntity;
|