@mikro-orm/core 7.2.2-dev.9 → 7.3.0-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.
@@ -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,
@@ -834,7 +834,7 @@ export class EntityManager {
834
834
  validateParams(where);
835
835
  options.populate = (await em.preparePopulate(entityName, options));
836
836
  const cacheKey = em.cacheKey(entityName, options, 'em.findOne', where);
837
- const cached = await em.tryCache(entityName, options.cache, cacheKey, options.refresh, true);
837
+ const cached = await em.tryCache(entityName, options.cache, cacheKey, options.refresh, true, options.schema);
838
838
  if (cached?.data !== undefined) {
839
839
  if (cached.data) {
840
840
  await em.#entityLoader.populate(entityName, [cached.data], options.populate, {
@@ -1110,7 +1110,18 @@ export class EntityManager {
1110
1110
  const ret = [];
1111
1111
  for (const idx of shapes.values()) {
1112
1112
  const res = await this.upsertMany(entityName, idx.map(i => data[i]), options);
1113
- idx.forEach((i, j) => (ret[i] = res[j]));
1113
+ const seen = new Set();
1114
+ let j = 0;
1115
+ // `res` holds every entity once, so skip the rows resolving to an entity an earlier row already took
1116
+ for (const i of idx) {
1117
+ const row = data[i];
1118
+ const entity = Utils.isEntity(row)
1119
+ ? row
1120
+ : em.#unitOfWork.tryGetById(entityName, row, options.schema, false);
1121
+ if (!seen.has(entity)) {
1122
+ seen.add((ret[i] = res[j++]));
1123
+ }
1124
+ }
1114
1125
  }
1115
1126
  // rows resolving to the same entity are returned once, as in the single statement path
1116
1127
  return [...new Set(ret.filter(entity => entity))];
@@ -1123,9 +1134,10 @@ export class EntityManager {
1123
1134
  const generatedFields = new Set();
1124
1135
  const allData = [];
1125
1136
  const allWhere = [];
1137
+ const dataIndexes = [];
1138
+ const result = [];
1126
1139
  const entities = new Map();
1127
1140
  const entitiesByData = new Map();
1128
- const entitiesByAllDataIdx = new Map();
1129
1141
  for (let i = 0; i < data.length; i++) {
1130
1142
  let row = data[i];
1131
1143
  let where;
@@ -1133,14 +1145,12 @@ export class EntityManager {
1133
1145
  const entity = row;
1134
1146
  if (helper(entity).__managed && helper(entity).__em === em && !this.config.get('upsertManaged')) {
1135
1147
  em.#entityFactory.mergeData(meta, entity, row, { initialized: true });
1136
- entities.set(entity, row);
1137
- entitiesByData.set(row, entity);
1148
+ result[i] = entity;
1138
1149
  continue;
1139
1150
  }
1140
1151
  where = helper(entity).getPrimaryKey();
1141
1152
  getOnCreateGeneratedFields(meta, entity).forEach(field => generatedFields.add(field));
1142
1153
  em.#entityFactory.assignDefaultValues(entity, meta);
1143
- entitiesByAllDataIdx.set(allData.length, entity);
1144
1154
  row = em.#comparator.prepareEntity(entity);
1145
1155
  }
1146
1156
  else {
@@ -1150,8 +1160,7 @@ export class EntityManager {
1150
1160
  const exists = em.#unitOfWork.getById(entityName, where, options.schema);
1151
1161
  if (exists) {
1152
1162
  em.assign(exists, row);
1153
- entities.set(exists, row);
1154
- entitiesByData.set(row, exists);
1163
+ result[i] = exists;
1155
1164
  continue;
1156
1165
  }
1157
1166
  }
@@ -1184,22 +1193,25 @@ export class EntityManager {
1184
1193
  validateParams(row, 'insert data');
1185
1194
  allData.push(row);
1186
1195
  allWhere.push(where);
1196
+ dataIndexes.push(i);
1187
1197
  }
1188
- if (entities.size === data.length) {
1189
- return [...entities.keys()];
1198
+ if (allData.length === 0) {
1199
+ return [...new Set(result)];
1190
1200
  }
1191
1201
  // `onCreate` generated values are for the insert clause only, they must not overwrite existing rows
1192
1202
  if (generatedFields.size > 0 && !options.onConflictMergeFields) {
1193
1203
  options.onConflictExcludeFields = [...(options.onConflictExcludeFields ?? []), ...generatedFields];
1194
1204
  }
1195
1205
  if (em.eventManager.hasListeners(EventType.beforeUpsert, meta)) {
1196
- for (const dto of data) {
1197
- const entity = entitiesByData.get(dto) ?? dto;
1206
+ for (const index of dataIndexes) {
1207
+ const entity = data[index];
1198
1208
  await em.eventManager.dispatchEvent(EventType.beforeUpsert, { entity, em, meta }, meta);
1199
1209
  }
1200
- for (const [idx, entity] of entitiesByAllDataIdx) {
1201
- allData[idx] = em.#comparator.prepareEntity(entity);
1202
- }
1210
+ dataIndexes.forEach((index, idx) => {
1211
+ if (Utils.isEntity(data[index])) {
1212
+ allData[idx] = em.#comparator.prepareEntity(data[index]);
1213
+ }
1214
+ });
1203
1215
  }
1204
1216
  const res = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.driver.nativeUpdateMany(entityName, allWhere, allData, {
1205
1217
  upsert: true,
@@ -1207,15 +1219,16 @@ export class EntityManager {
1207
1219
  ...options,
1208
1220
  ctx,
1209
1221
  }));
1210
- entities.clear();
1211
- entitiesByData.clear();
1212
1222
  const loadPK = new Map();
1213
1223
  allData.forEach((row, i) => {
1214
- em.#unitOfWork.getChangeSetPersister().mapReturnedValues(Utils.isEntity(data[i]) ? data[i] : null, Utils.isEntity(data[i]) ? {} : data[i],
1224
+ // Managed inputs skipped by the SQL batch still occupy positions in `data`.
1225
+ const index = dataIndexes[i];
1226
+ const item = data[index];
1227
+ em.#unitOfWork.getChangeSetPersister().mapReturnedValues(Utils.isEntity(item) ? item : null, Utils.isEntity(item) ? {} : item,
1215
1228
  // A suppressed conflict omits its row; positional mapping would hydrate a different entity.
1216
1229
  res.rows?.length === allData.length ? res.rows[i] : undefined, meta, true);
1217
- const entity = Utils.isEntity(data[i])
1218
- ? data[i]
1230
+ const entity = Utils.isEntity(item)
1231
+ ? item
1219
1232
  : em.#entityFactory.create(entityName, row, {
1220
1233
  refresh: true,
1221
1234
  initialized: true,
@@ -1226,18 +1239,19 @@ export class EntityManager {
1226
1239
  }
1227
1240
  entities.set(entity, row);
1228
1241
  entitiesByData.set(row, entity);
1242
+ result[index] = entity;
1229
1243
  });
1230
1244
  // skip the reload only when RETURNING brought back a row for every input
1231
1245
  // (onConflictWhere can suppress writes, leaving some rows out)
1232
1246
  // oxfmt-ignore
1233
1247
  const uniqueFields = options.onConflictFields ?? (Utils.isPlainObject(allWhere[0]) ? Object.keys(allWhere[0]).flatMap(key => Utils.splitPrimaryKeys(key)) : meta.primaryKeys);
1234
1248
  const platform = this.getPlatform();
1235
- const returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options, platform);
1249
+ const returning = getOnConflictReturningFields(meta, data[dataIndexes[0]], uniqueFields, options, platform);
1236
1250
  if (options.onConflictWhere && res.rows?.length !== allData.length) {
1237
1251
  returning.push(...meta.comparableProps.filter(p => !p.lazy && !p.embeddable).map(p => p.name));
1238
1252
  }
1239
1253
  const reloadFields = returning.length > 0 &&
1240
- !((platform.usesReturningStatement() || platform.usesOutputStatement()) && res.rows?.length === data.length);
1254
+ !((platform.usesReturningStatement() || platform.usesOutputStatement()) && res.rows?.length === allData.length);
1241
1255
  if (options.onConflictAction === 'ignore' || (!res.rows?.length && loadPK.size > 0) || reloadFields) {
1242
1256
  const unique = meta.hydrateProps.filter(p => !p.lazy).map(p => p.name);
1243
1257
  const add = new Set(propIndex !== false && propIndex >= 0
@@ -1247,7 +1261,8 @@ export class EntityManager {
1247
1261
  Utils.keys(cond).forEach(key => add.add(key));
1248
1262
  }
1249
1263
  const where = { $or: [] };
1250
- data.forEach((item, idx) => {
1264
+ dataIndexes.forEach((index, idx) => {
1265
+ const item = data[index];
1251
1266
  where.$or[idx] = {};
1252
1267
  const props = Array.isArray(uniqueFields) ? uniqueFields : Object.keys(item);
1253
1268
  props.forEach(prop => {
@@ -1317,7 +1332,7 @@ export class EntityManager {
1317
1332
  await em.eventManager.dispatchEvent(EventType.afterUpsert, { entity, em, meta }, meta);
1318
1333
  }
1319
1334
  }
1320
- return [...entities.keys()];
1335
+ return [...new Set(result)];
1321
1336
  }
1322
1337
  /**
1323
1338
  * Runs your callback wrapped inside a database transaction.
@@ -1974,6 +1989,13 @@ export class EntityManager {
1974
1989
  isInTransaction() {
1975
1990
  return !!this.getContext(false).#transactionContext;
1976
1991
  }
1992
+ /**
1993
+ * Checks whether transactions are disabled for this EntityManager.
1994
+ * @internal
1995
+ */
1996
+ isTransactionDisabled() {
1997
+ return !!this.getContext(false).#disableTransactions;
1998
+ }
1977
1999
  /**
1978
2000
  * Gets the transaction context (driver dependent object used to make sure queries are executed on same connection).
1979
2001
  */
@@ -2329,7 +2351,7 @@ export class EntityManager {
2329
2351
  /**
2330
2352
  * @internal
2331
2353
  */
2332
- async tryCache(entityName, config, key, refresh, merge) {
2354
+ async tryCache(entityName, config, key, refresh, merge, schema) {
2333
2355
  config ??= this.config.get('resultCache').global;
2334
2356
  if (!config) {
2335
2357
  return undefined;
@@ -2351,6 +2373,7 @@ export class EntityManager {
2351
2373
  merge: true,
2352
2374
  convertCustomTypes: false,
2353
2375
  refresh,
2376
+ schema,
2354
2377
  recomputeSnapshot: true,
2355
2378
  };
2356
2379
  if (Array.isArray(cached) && merge) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/core",
3
- "version": "7.2.2-dev.9",
3
+ "version": "7.3.0-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",
@@ -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[key]) {
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 primaryKeys = meta.primaryKeys.concat(...concurrencyCheckKeys);
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: primaryKeys,
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(cs => {
348
- return !res.some(row => compare(Utils.getPrimaryKeyCond(cs.entity, primaryKeys), row, primaryKeys));
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
  }
@@ -467,7 +467,10 @@ export class UnitOfWork {
467
467
  }
468
468
  const groups = this.getChangeSetGroups();
469
469
  const platform = this.#em.getPlatform();
470
- const runInTransaction = !this.#em.isInTransaction() && platform.supportsTransactions() && this.#em.config.get('implicitTransactions');
470
+ const runInTransaction = !this.#em.isInTransaction() &&
471
+ !this.#em.isTransactionDisabled() &&
472
+ platform.supportsTransactions() &&
473
+ this.#em.config.get('implicitTransactions');
471
474
  if (runInTransaction) {
472
475
  const loggerContext = Utils.merge({ id: this.#em._id }, this.#em.getLoggerContext({ disableContextResolution: true }));
473
476
  await this.#em.getConnection('write').transactional(trx => this.persistToDatabase(groups, trx), {
@@ -111,9 +111,17 @@ export class QueryHelper {
111
111
  const op = this.liftGroupOperators(value, prop.targetMeta, metadata, k);
112
112
  if (op) {
113
113
  delete where[k];
114
- where[op] = value[op].map((v) => {
114
+ const group = value[op].map((v) => {
115
115
  return { [k]: v };
116
116
  });
117
+ if (op in where) {
118
+ // Sibling groups are conjunctive, even when both use `$or`.
119
+ where.$and ??= [];
120
+ where.$and.push({ [op]: group });
121
+ }
122
+ else {
123
+ where[op] = group;
124
+ }
117
125
  }
118
126
  }
119
127
  return undefined;
@@ -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 TransactionContext(em);
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
- const context = TransactionContext.currentTransactionContext();
29
- return context?.em.name === name ? context.em : undefined;
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, { ...options, disableTransactions: true });
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);
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-dev.9';
156
+ static #ORM_VERSION = '7.3.0-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}`;