@mikro-orm/core 7.2.4-dev.9 → 7.2.5-dev.0
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 +91 -17
- package/entity/EntityLoader.js +5 -4
- package/entity/defineEntity.d.ts +6 -6
- package/package.json +1 -1
- package/utils/EntityComparator.d.ts +5 -0
- package/utils/EntityComparator.js +11 -0
- package/utils/Utils.js +1 -1
- package/utils/upsert-utils.js +2 -1
package/EntityManager.js
CHANGED
|
@@ -1025,6 +1025,15 @@ export class EntityManager {
|
|
|
1025
1025
|
where[meta.primaryKeys[0]] = ret.insertId;
|
|
1026
1026
|
}
|
|
1027
1027
|
}
|
|
1028
|
+
// a `null` unique value cannot identify the row, and as it never conflicts, the row was inserted
|
|
1029
|
+
if (!Utils.hasObjectKeys(where)) {
|
|
1030
|
+
const insertId = meta.simplePK && ret.insertId != null ? { [meta.primaryKeys[0]]: ret.insertId } : {};
|
|
1031
|
+
Object.assign(where, Utils.getPrimaryKeyCond(entity, meta.primaryKeys) ?? insertId);
|
|
1032
|
+
}
|
|
1033
|
+
// an empty condition would hydrate the entity from an arbitrary row, mongo still gets one for nested conflict fields
|
|
1034
|
+
if (!Utils.hasObjectKeys(where) && platform.usesPivotTable()) {
|
|
1035
|
+
throw new Error(`Cannot find the upserted ${meta.className} row, as neither its primary key nor a unique value is known`);
|
|
1036
|
+
}
|
|
1028
1037
|
const data2 = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => this.driver.findOne(meta.class, where, {
|
|
1029
1038
|
fields: returning.concat(...(options.onConflictMergeFields ?? [])),
|
|
1030
1039
|
ctx,
|
|
@@ -1032,6 +1041,9 @@ export class EntityManager {
|
|
|
1032
1041
|
connectionType: 'write',
|
|
1033
1042
|
schema: options.schema,
|
|
1034
1043
|
}));
|
|
1044
|
+
if (!data2) {
|
|
1045
|
+
throw new Error(`Cannot find the upserted ${meta.className} row for condition ${JSON.stringify(where)}`);
|
|
1046
|
+
}
|
|
1035
1047
|
em.getHydrator().hydrate(entity, meta, data2, em.#entityFactory, 'full', false, true);
|
|
1036
1048
|
}
|
|
1037
1049
|
// recompute the data as there might be some values missing (e.g. those with db column defaults)
|
|
@@ -1183,8 +1195,10 @@ export class EntityManager {
|
|
|
1183
1195
|
unique.findIndex(p => data[p] ?? data[p.substring(0, p.indexOf('.'))] != null);
|
|
1184
1196
|
const tmp = getWhereCondition(meta, options.onConflictFields, row, where);
|
|
1185
1197
|
propIndex = tmp.propIndex;
|
|
1198
|
+
// a partial composite PK cannot identify the row
|
|
1199
|
+
const partialPK = meta.compositePK && !Utils.isPlainObject(tmp.where) && meta.primaryKeys.some(pk => row[pk] == null);
|
|
1186
1200
|
where = QueryHelper.processWhere({
|
|
1187
|
-
where: tmp.where,
|
|
1201
|
+
where: partialPK ? {} : tmp.where,
|
|
1188
1202
|
entityName,
|
|
1189
1203
|
metadata: this.metadata,
|
|
1190
1204
|
platform: this.getPlatform(),
|
|
@@ -1199,6 +1213,25 @@ export class EntityManager {
|
|
|
1199
1213
|
allWhere.push(where);
|
|
1200
1214
|
dataIndexes.push(i);
|
|
1201
1215
|
}
|
|
1216
|
+
// mongo inserts the rows with an empty condition as part of the batch and reports their ids
|
|
1217
|
+
const positional = !this.getPlatform().usesPivotTable() ||
|
|
1218
|
+
((this.getPlatform().usesReturningStatement() || this.getPlatform().usesOutputStatement()) &&
|
|
1219
|
+
options.onConflictAction !== 'ignore' &&
|
|
1220
|
+
!options.onConflictWhere);
|
|
1221
|
+
const hasUniqueKey = !!options.onConflictFields || meta.uniques.length > 0 || meta.props.some(p => p.unique && !p.primary);
|
|
1222
|
+
// only an autoincrement PK can be derived from the `insertId` of the batch
|
|
1223
|
+
const autoincrement = meta.getPrimaryProps().some(p => p.autoincrement);
|
|
1224
|
+
// a TPT child table does not return its rows, so they are never mapped by position
|
|
1225
|
+
const peel = !!meta.tptParent || (!positional && (hasUniqueKey || !autoincrement));
|
|
1226
|
+
// a row with a `null` unique value has nothing to be reloaded by, the single row path maps its PK from the insert
|
|
1227
|
+
for (let idx = 0; peel && idx < allWhere.length; idx++) {
|
|
1228
|
+
if (!Utils.hasObjectKeys(allWhere[idx])) {
|
|
1229
|
+
result[dataIndexes[idx]] = await em.upsert(entityName, data[dataIndexes[idx]], options);
|
|
1230
|
+
allData.splice(idx, 1);
|
|
1231
|
+
allWhere.splice(idx, 1);
|
|
1232
|
+
dataIndexes.splice(idx--, 1);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1202
1235
|
if (allData.length === 0) {
|
|
1203
1236
|
return [...new Set(result)];
|
|
1204
1237
|
}
|
|
@@ -1248,7 +1281,11 @@ export class EntityManager {
|
|
|
1248
1281
|
// skip the reload only when RETURNING brought back a row for every input
|
|
1249
1282
|
// (onConflictWhere can suppress writes, leaving some rows out)
|
|
1250
1283
|
// oxfmt-ignore
|
|
1251
|
-
const
|
|
1284
|
+
const getUniqueFields = (where) => options.onConflictFields ?? (Utils.isPlainObject(where) ? Object.keys(where).flatMap(key => Utils.splitPrimaryKeys(key)) : meta.primaryKeys);
|
|
1285
|
+
// a row without a condition has no unique fields to derive
|
|
1286
|
+
const uniqueFields = getUniqueFields(allWhere.find(cond => Utils.hasObjectKeys(cond)) ?? allWhere[0]);
|
|
1287
|
+
// an empty condition identifies no row, it must not turn into a reload condition matching every row
|
|
1288
|
+
const getRowUniqueFields = (where) => Utils.isPlainObject(where) && !Utils.hasObjectKeys(where) ? uniqueFields : getUniqueFields(where);
|
|
1252
1289
|
const platform = this.getPlatform();
|
|
1253
1290
|
const returning = getOnConflictReturningFields(meta, data[dataIndexes[0]], uniqueFields, options, platform);
|
|
1254
1291
|
if (options.onConflictWhere && res.rows?.length !== allData.length) {
|
|
@@ -1265,10 +1302,14 @@ export class EntityManager {
|
|
|
1265
1302
|
Utils.keys(cond).forEach(key => add.add(key));
|
|
1266
1303
|
}
|
|
1267
1304
|
const where = { $or: [] };
|
|
1305
|
+
// a row without a condition was inserted, only the primary key reported for it can identify it
|
|
1306
|
+
const insertedPKs = allData.map((row, idx) => Utils.hasObjectKeys(allWhere[idx]) ? null : Utils.getPrimaryKeyCond(entitiesByData.get(row), meta.primaryKeys));
|
|
1268
1307
|
dataIndexes.forEach((index, idx) => {
|
|
1269
1308
|
const item = data[index];
|
|
1270
|
-
where.$or[idx] = {};
|
|
1271
|
-
|
|
1309
|
+
where.$or[idx] = { ...insertedPKs[idx] };
|
|
1310
|
+
// the inputs of one batch can be identified by different unique keys
|
|
1311
|
+
const fields = insertedPKs[idx] ? [] : getRowUniqueFields(allWhere[idx]);
|
|
1312
|
+
const props = Array.isArray(fields) ? fields : Object.keys(item);
|
|
1272
1313
|
props.forEach(prop => {
|
|
1273
1314
|
where.$or[idx][prop] = item[prop];
|
|
1274
1315
|
});
|
|
@@ -1283,8 +1324,8 @@ export class EntityManager {
|
|
|
1283
1324
|
connectionType: 'write',
|
|
1284
1325
|
schema: options.schema,
|
|
1285
1326
|
}));
|
|
1286
|
-
|
|
1287
|
-
const
|
|
1327
|
+
if (loadPK.size > 0) {
|
|
1328
|
+
const comparableRows = data2.map(row => {
|
|
1288
1329
|
const tmp = {};
|
|
1289
1330
|
add.forEach(k => {
|
|
1290
1331
|
const prop = meta.properties[k];
|
|
@@ -1292,25 +1333,58 @@ export class EntityManager {
|
|
|
1292
1333
|
tmp[k] = prop?.customType ? prop.customType.convertToDatabaseValue(row[k], this.getPlatform()) : row[k];
|
|
1293
1334
|
}
|
|
1294
1335
|
});
|
|
1295
|
-
return
|
|
1336
|
+
return tmp;
|
|
1296
1337
|
});
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1338
|
+
// rows carry the keys of every condition in the batch, compare only this one's
|
|
1339
|
+
const isMatch = (cond, idx) => this.#comparator.matching(entityName, cond, Object.fromEntries(Object.entries(comparableRows[idx]).filter(([k]) => k in cond)));
|
|
1340
|
+
// When every condition key is a comparable prop checked with `!==`, a matching row has the same primitive
|
|
1341
|
+
// values, so rows can be grouped by those values once instead of comparing every row for every entity.
|
|
1342
|
+
// Props outside `comparableProps` are skipped by the comparator, so they never qualify for grouping.
|
|
1343
|
+
const groupableProps = new Set(meta.comparableProps.filter(prop => this.#comparator.isStrictlyCompared(prop)));
|
|
1344
|
+
const groups = new Map();
|
|
1345
|
+
// `null` and `undefined` share a key, which can only make a group wider than what `matching` accepts
|
|
1346
|
+
const groupKey = (data, keys) => keys.map(k => (data[k] == null ? 'null' : `${typeof data[k]}:${data[k]}`)).join('\0');
|
|
1347
|
+
const findRowIndex = (cond) => {
|
|
1348
|
+
const keys = Utils.keys(cond);
|
|
1349
|
+
if (!keys.every(k => groupableProps.has(meta.properties[k]))) {
|
|
1350
|
+
return comparableRows.findIndex((_, idx) => isMatch(cond, idx));
|
|
1351
|
+
}
|
|
1352
|
+
const signature = keys.join('\0');
|
|
1353
|
+
if (!groups.has(signature)) {
|
|
1354
|
+
const rowsByKey = new Map();
|
|
1355
|
+
comparableRows.forEach((tmp, idx) => {
|
|
1356
|
+
const rowKey = groupKey(tmp, keys);
|
|
1357
|
+
const indexes = rowsByKey.get(rowKey) ?? [];
|
|
1358
|
+
indexes.push(idx);
|
|
1359
|
+
rowsByKey.set(rowKey, indexes);
|
|
1360
|
+
});
|
|
1361
|
+
groups.set(signature, rowsByKey);
|
|
1362
|
+
}
|
|
1363
|
+
const group = groups.get(signature);
|
|
1364
|
+
return group.get(groupKey(cond, keys))?.find(idx => isMatch(cond, idx)) ?? -1;
|
|
1365
|
+
};
|
|
1366
|
+
for (const [entity, cond] of loadPK.entries()) {
|
|
1367
|
+
const row = data2[findRowIndex(cond)];
|
|
1368
|
+
/* v8 ignore next */
|
|
1369
|
+
if (!row) {
|
|
1370
|
+
throw new Error(`Cannot find matching entity for condition ${JSON.stringify(cond)}`);
|
|
1371
|
+
}
|
|
1372
|
+
em.getHydrator().hydrate(entity, meta, row, em.#entityFactory, 'full', false, true);
|
|
1300
1373
|
}
|
|
1301
|
-
em.getHydrator().hydrate(entity, meta, row, em.#entityFactory, 'full', false, true);
|
|
1302
1374
|
}
|
|
1303
1375
|
if (loadPK.size !== data2.length && Array.isArray(uniqueFields)) {
|
|
1304
1376
|
for (let i = 0; i < allData.length; i++) {
|
|
1305
1377
|
const data = allData[i];
|
|
1306
|
-
const
|
|
1307
|
-
|
|
1308
|
-
a
|
|
1309
|
-
|
|
1310
|
-
|
|
1378
|
+
const fields = (insertedPKs[i] ? meta.primaryKeys : getRowUniqueFields(allWhere[i]));
|
|
1379
|
+
const cond = insertedPKs[i] ??
|
|
1380
|
+
fields.reduce((a, b) => {
|
|
1381
|
+
// @ts-ignore
|
|
1382
|
+
a[b] = data[b];
|
|
1383
|
+
return a;
|
|
1384
|
+
}, {});
|
|
1311
1385
|
const entity = entitiesByData.get(data);
|
|
1312
1386
|
const row = data2.find(item => {
|
|
1313
|
-
const pk =
|
|
1387
|
+
const pk = fields.reduce((a, b) => {
|
|
1314
1388
|
// @ts-ignore
|
|
1315
1389
|
a[b] = item[b];
|
|
1316
1390
|
return a;
|
package/entity/EntityLoader.js
CHANGED
|
@@ -325,11 +325,12 @@ export class EntityLoader {
|
|
|
325
325
|
}
|
|
326
326
|
else {
|
|
327
327
|
// owning side of M:N without pivot table needs to be reordered
|
|
328
|
+
const childIndex = new Map(children.map((child, idx) => [child, idx]));
|
|
328
329
|
for (const entity of filtered) {
|
|
329
|
-
|
|
330
|
-
const items =
|
|
331
|
-
if (
|
|
332
|
-
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));
|
|
333
334
|
}
|
|
334
335
|
entity[field].hydrate(items, true, partial, readonly);
|
|
335
336
|
}
|
package/entity/defineEntity.d.ts
CHANGED
|
@@ -641,7 +641,7 @@ export interface EntityMetadataWithProperties<TName extends string, TTableName e
|
|
|
641
641
|
'~discriminatorColumn'?: TBaseDiscriminatorColumn;
|
|
642
642
|
});
|
|
643
643
|
properties: TProperties | ((properties: PropertyBuilders) => TProperties);
|
|
644
|
-
primaryKeys?: TPK & InferPrimaryKeyConstraint<TProperties
|
|
644
|
+
primaryKeys?: TPK & NoInfer<InferPrimaryKeyConstraint<TProperties>>[];
|
|
645
645
|
hooks?: DefineEntityHooks;
|
|
646
646
|
repository?: () => TRepository;
|
|
647
647
|
filters?: Dictionary<{
|
|
@@ -667,16 +667,16 @@ export interface EntityMetadataWithProperties<TName extends string, TTableName e
|
|
|
667
667
|
/** Alias for `discriminator`. */
|
|
668
668
|
discriminatorColumn?: TDiscriminatorColumn;
|
|
669
669
|
discriminatorValue?: TDiscriminatorValue;
|
|
670
|
-
versionProperty?: AllKeys<TProperties, TBase
|
|
671
|
-
concurrencyCheckKeys?: Set<AllKeys<TProperties, TBase
|
|
672
|
-
serializedPrimaryKey?: AllKeys<TProperties, TBase
|
|
670
|
+
versionProperty?: NoInfer<AllKeys<TProperties, TBase>>;
|
|
671
|
+
concurrencyCheckKeys?: Set<NoInfer<AllKeys<TProperties, TBase>>>;
|
|
672
|
+
serializedPrimaryKey?: NoInfer<AllKeys<TProperties, TBase>>;
|
|
673
673
|
indexes?: {
|
|
674
674
|
properties?: NoInfer<AllKeys<TProperties, TBase>> | NoInfer<AllKeys<TProperties, TBase>>[];
|
|
675
675
|
name?: string;
|
|
676
676
|
type?: string;
|
|
677
677
|
options?: Dictionary;
|
|
678
678
|
expression?: string | IndexCallback<InferEntityFromProperties<TProperties, TPK, TBase>>;
|
|
679
|
-
where?: PartialWhere<TProperties, TBase
|
|
679
|
+
where?: NoInfer<PartialWhere<TProperties, TBase>>;
|
|
680
680
|
columns?: IndexColumnOptions[];
|
|
681
681
|
include?: NoInfer<AllKeys<TProperties, TBase>> | NoInfer<AllKeys<TProperties, TBase>>[];
|
|
682
682
|
fillFactor?: number;
|
|
@@ -689,7 +689,7 @@ export interface EntityMetadataWithProperties<TName extends string, TTableName e
|
|
|
689
689
|
name?: string;
|
|
690
690
|
options?: Dictionary;
|
|
691
691
|
expression?: string | IndexCallback<InferEntityFromProperties<TProperties, TPK, TBase>>;
|
|
692
|
-
where?: PartialWhere<TProperties, TBase
|
|
692
|
+
where?: NoInfer<PartialWhere<TProperties, TBase>>;
|
|
693
693
|
deferMode?: DeferMode | `${DeferMode}`;
|
|
694
694
|
columns?: IndexColumnOptions[];
|
|
695
695
|
include?: NoInfer<AllKeys<TProperties, TBase>> | NoInfer<AllKeys<TProperties, TBase>>[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/core",
|
|
3
|
-
"version": "7.2.
|
|
3
|
+
"version": "7.2.5-dev.0",
|
|
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",
|
|
@@ -19,6 +19,11 @@ export declare class EntityComparator {
|
|
|
19
19
|
diffEntities<T extends object>(entityName: EntityName<T>, a: EntityData<T>, b: EntityData<T>, options?: {
|
|
20
20
|
includeInverseSides?: boolean;
|
|
21
21
|
}): EntityData<T>;
|
|
22
|
+
/**
|
|
23
|
+
* Returns true if the comparator checks this property with `!==`, so matching values are identical primitives.
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
isStrictlyCompared<T>(prop: EntityProperty<T>): boolean;
|
|
22
27
|
/** Returns true if two entity snapshots are identical (no differences). */
|
|
23
28
|
matching<T extends object>(entityName: EntityName<T>, a: EntityData<T>, b: EntityData<T>): boolean;
|
|
24
29
|
/**
|
|
@@ -29,6 +29,17 @@ export class EntityComparator {
|
|
|
29
29
|
const comparator = this.getEntityComparator(entityName);
|
|
30
30
|
return Utils.callCompiledFunction(comparator, a, b, options);
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Returns true if the comparator checks this property with `!==`, so matching values are identical primitives.
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
isStrictlyCompared(prop) {
|
|
37
|
+
if (prop.kind !== ReferenceKind.SCALAR || prop.customType?.compareValues) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const type = (prop.customType ? prop.customType.compareAsType() : prop.type).toLowerCase();
|
|
41
|
+
return ['string', 'number', 'bigint'].includes(type);
|
|
42
|
+
}
|
|
32
43
|
/** Returns true if two entity snapshots are identical (no differences). */
|
|
33
44
|
matching(entityName, a, b) {
|
|
34
45
|
const diff = this.diffEntities(entityName, a, b);
|
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.
|
|
156
|
+
static #ORM_VERSION = '7.2.5-dev.0';
|
|
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}`;
|
package/utils/upsert-utils.js
CHANGED
|
@@ -103,7 +103,8 @@ export function getOnConflictReturningFields(meta, data, uniqueFields, options,
|
|
|
103
103
|
if (p.lazy || p.embeddable) {
|
|
104
104
|
return false;
|
|
105
105
|
}
|
|
106
|
-
|
|
106
|
+
// a PK missing in the data is generated by the database
|
|
107
|
+
if (p.autoincrement || (p.primary && data[p.name] == null)) {
|
|
107
108
|
return true;
|
|
108
109
|
}
|
|
109
110
|
// for a raw `onConflictFields` fragment we can't introspect which columns form the conflict
|