@mikro-orm/core 7.2.2-dev.9 → 7.3.0-dev.1

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) {
@@ -190,7 +190,7 @@ export class EntityFactory {
190
190
  const initialized = options.initialized || helper(entity).__initialized;
191
191
  this.hydrate(entity, meta, diff2, initialized ? { ...options, initialized } : options);
192
192
  // we need to update the entity data only with keys that were not present before
193
- const nullVal = this.#config.get('forceUndefined') ? undefined : null;
193
+ const nullVal = (meta.forceUndefined ?? this.#config.get('forceUndefined')) ? undefined : null;
194
194
  Utils.keys(diff2).forEach(key => {
195
195
  const prop = meta.properties[key];
196
196
  if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) &&
@@ -336,7 +336,7 @@ export class EntityFactory {
336
336
  prop.default == null &&
337
337
  !prop.defaultRaw &&
338
338
  entity[prop.name] === undefined) {
339
- entity[prop.name] = (this.#config.get('forceUndefined') ? undefined : null);
339
+ entity[prop.name] = ((meta.forceUndefined ?? this.#config.get('forceUndefined')) ? undefined : null);
340
340
  }
341
341
  if (prop.kind === ReferenceKind.EMBEDDED && entity[prop.name]) {
342
342
  const items = prop.array ? entity[prop.name] : [entity[prop.name]];
@@ -458,7 +458,8 @@ export class EntityLoader {
458
458
  if ([ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind) &&
459
459
  items.length !== children.length &&
460
460
  Utils.isEmpty(options.where)) {
461
- const nullVal = this.#em.config.get('forceUndefined') ? undefined : null;
461
+ const ownerMeta = this.#metadata.find(prop.entity());
462
+ const nullVal = (ownerMeta.forceUndefined ?? this.#em.config.get('forceUndefined')) ? undefined : null;
462
463
  const itemsMap = new Set();
463
464
  const childrenMap = new Set();
464
465
  // `e` may be an unresolved reference here, so read the targetKey value directly instead of
@@ -631,7 +631,7 @@ type PartialWhere<TProperties, TBase> = string | FilterQuery<{
631
631
  [K in AllKeys<TProperties, TBase> & string]?: unknown;
632
632
  }>;
633
633
  /** Metadata descriptor for `defineEntity()`, combining entity options with property definitions. */
634
- 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> 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'> {
634
+ 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'> {
635
635
  name: TName;
636
636
  tableName?: TTableName;
637
637
  extends?: {
@@ -656,6 +656,8 @@ export interface EntityMetadataWithProperties<TName extends string, TTableName e
656
656
  };
657
657
  }>;
658
658
  forceObject?: TForceObject;
659
+ /** Hydrate `null` as `undefined` for this entity and infer nullable properties as `T | undefined`. */
660
+ forceUndefined?: TForceUndefined;
659
661
  embeddable?: TEmbeddable;
660
662
  inheritance?: 'tpt';
661
663
  orderBy?: {
@@ -707,7 +709,7 @@ export interface EntityMetadataWithProperties<TName extends string, TTableName e
707
709
  }[];
708
710
  }
709
711
  /** Defines an entity schema using property builders, with full type inference from the property definitions. */
710
- export declare function defineEntity<const TName extends string, const TTableName extends string, const TProperties extends Record<string, any>, const TPK extends (keyof TProperties)[] | undefined = undefined, const TBase = never, const TRepository = never, const TForceObject extends boolean = false, const TDiscriminatorColumn extends string | undefined = undefined, const TDiscriminatorValue extends string | number | undefined = undefined, const TBaseDiscriminatorColumn extends string | undefined = undefined, const TEmbeddable extends boolean = false>(meta: EntityMetadataWithProperties<TName, TTableName, TProperties, TPK, TBase, TRepository, TForceObject, TDiscriminatorColumn, TDiscriminatorValue, TBaseDiscriminatorColumn, TEmbeddable>): EntitySchemaWithMeta<TName, TTableName, InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject, TBaseDiscriminatorColumn, TDiscriminatorValue, TEmbeddable>, TBase, TProperties, EntityCtor<InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject, TBaseDiscriminatorColumn, TDiscriminatorValue, TEmbeddable>>, TDiscriminatorColumn>;
712
+ export declare function defineEntity<const TName extends string, const TTableName extends string, const TProperties extends Record<string, any>, const TPK extends (keyof TProperties)[] | undefined = undefined, const TBase = never, const TRepository = never, const TForceObject extends boolean = false, const TDiscriminatorColumn extends string | undefined = undefined, const TDiscriminatorValue extends string | number | undefined = undefined, const TBaseDiscriminatorColumn extends string | undefined = undefined, const TEmbeddable extends boolean = false, const TForceUndefined extends boolean = false>(meta: EntityMetadataWithProperties<TName, TTableName, TProperties, TPK, TBase, TRepository, TForceObject, TDiscriminatorColumn, TDiscriminatorValue, TBaseDiscriminatorColumn, TEmbeddable, TForceUndefined>): EntitySchemaWithMeta<TName, TTableName, MaybeForceUndefined<InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject, TBaseDiscriminatorColumn, TDiscriminatorValue, TEmbeddable, TForceUndefined>, EffectiveForceUndefined<TForceUndefined, TBase>>, TBase, TProperties, EntityCtor<MaybeForceUndefined<InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject, TBaseDiscriminatorColumn, TDiscriminatorValue, TEmbeddable, TForceUndefined>, EffectiveForceUndefined<TForceUndefined, TBase>>>, TDiscriminatorColumn>;
711
713
  export declare namespace defineEntity {
712
714
  export { propertyBuilders as properties };
713
715
  }
@@ -744,8 +746,17 @@ type InferColumnType<T extends string> = T extends 'int' | 'int4' | 'integer' |
744
746
  type BaseEntityMethodKeys = 'toObject' | 'toPOJO' | 'serialize' | 'assign' | 'populate' | 'init' | 'toReference';
745
747
  interface BaseEntityMethods<in out Entity extends object> extends Pick<IWrappedEntity<Entity>, BaseEntityMethodKeys> {
746
748
  }
749
+ type MaybeForceUndefined<Entity, ForceUndefined> = [ForceUndefined] extends [true] ? {
750
+ [K in keyof Entity]: SwapNull<Entity[K]>;
751
+ } : Entity;
752
+ type SwapNull<Value> = [Value] extends [ScalarReference<infer Inner>] ? [Extract<keyof Value, symbol>] extends [never] ? ScalarReference<SwapNull<Inner>> : ScalarReference<SwapNull<Inner>> & Pick<Value, Extract<keyof Value, symbol>> : null extends Value ? Exclude<Value, null> | undefined : Value;
753
+ type EffectiveForceUndefined<Flag, Base> = [Flag] extends [true] ? true : IsNever<Base> extends true ? false : [Base] extends [{
754
+ [Config]?: infer C;
755
+ }] ? [NonNullable<C>] extends [{
756
+ forceUndefined: true;
757
+ }] ? true : false : false;
747
758
  /** Infers the entity type from a `defineEntity()` properties map, resolving builders, base classes, and primary keys. */
748
- export type InferEntityFromProperties<Properties extends Record<string, any>, PK extends (keyof Properties)[] | undefined = undefined, Base = never, Repository = never, ForceObject extends boolean = false, BaseDiscriminatorColumn extends string | undefined = undefined, DiscriminatorValue extends string | number | undefined = undefined, Embeddable extends boolean = false> = (IsNever<Base> extends true ? {} : Base extends {
759
+ export type InferEntityFromProperties<Properties extends Record<string, any>, PK extends (keyof Properties)[] | undefined = undefined, Base = never, Repository = never, ForceObject extends boolean = false, BaseDiscriminatorColumn extends string | undefined = undefined, DiscriminatorValue extends string | number | undefined = undefined, Embeddable extends boolean = false, ForceUndefined extends boolean = false> = (IsNever<Base> extends true ? {} : Base extends {
749
760
  toObject(...args: any[]): any;
750
761
  } ? BaseEntityMethods<{
751
762
  -readonly [K in keyof Properties]: InferBuilderValue<MaybeReturnType<Properties[K]>>;
@@ -763,6 +774,10 @@ export type InferEntityFromProperties<Properties extends Record<string, any>, PK
763
774
  [Config]?: DefineConfig<{
764
775
  forceObject: true;
765
776
  }>;
777
+ } : {}) & (ForceUndefined extends true ? {
778
+ [Config]?: DefineConfig<{
779
+ forceUndefined: true;
780
+ }>;
766
781
  } : {}) & {
767
782
  [IndexHints]?: [Omit<ExtractBaseProperties<Base>, keyof Properties> & Properties];
768
783
  };
@@ -50,6 +50,8 @@ export class ObjectHydrator extends Hydrator {
50
50
  const lines = [];
51
51
  const context = new Map();
52
52
  const props = this.getProperties(meta, type);
53
+ const forceUndefined = meta.forceUndefined ?? this.config.get('forceUndefined');
54
+ const nullVal = forceUndefined ? 'undefined' : 'null';
53
55
  context.set('isPrimaryKey', Utils.isPrimaryKey);
54
56
  context.set('isEntity', EntityHelper.isEntity);
55
57
  context.set('isScalarReference', ScalarReference.isScalarReference);
@@ -76,7 +78,6 @@ export class ObjectHydrator extends Hydrator {
76
78
  .join('_');
77
79
  const ret = [];
78
80
  const idx = this.#tmpIndex++;
79
- const nullVal = this.config.get('forceUndefined') ? 'undefined' : 'null';
80
81
  if (prop.getter && !prop.setter && prop.persist === false) {
81
82
  return [];
82
83
  }
@@ -142,7 +143,6 @@ export class ObjectHydrator extends Hydrator {
142
143
  const hydrateToOne = (prop, dataKey, entityKey) => {
143
144
  const ret = [];
144
145
  const method = type === 'reference' ? 'createReference' : 'create';
145
- const nullVal = this.config.get('forceUndefined') ? 'undefined' : 'null';
146
146
  ret.push(` if (data${dataKey} === null) {\n entity${entityKey} = ${nullVal};`);
147
147
  ret.push(` } else if (typeof data${dataKey} !== 'undefined') {`);
148
148
  // For polymorphic: instanceof check; for regular: isPrimaryKey() check
@@ -341,8 +341,6 @@ export class ObjectHydrator extends Hydrator {
341
341
  ...hydrateProperty(childProp, prop.object, [...path, childProp.embedded[1]], childDataKey).map(l => ' ' + l));
342
342
  });
343
343
  }
344
- /* v8 ignore next */
345
- const nullVal = this.config.get('forceUndefined') ? 'undefined' : 'null';
346
344
  if (prop.object) {
347
345
  ret.push(` } else if (data${dataKey} === null) {`);
348
346
  }
@@ -365,8 +363,7 @@ export class ObjectHydrator extends Hydrator {
365
363
  ret.push(...hydrateEmbedded(prop, [...path, `[idx_${idx}]`], `${dataKey}[idx_${idx}]`).map(l => ' ' + l));
366
364
  ret.push(` });`);
367
365
  ret.push(` } else if (data${dataKey} === null) {`);
368
- /* v8 ignore next */
369
- ret.push(` entity${entityKey} = ${this.config.get('forceUndefined') ? 'undefined' : 'null'};`);
366
+ ret.push(` entity${entityKey} = ${nullVal};`);
370
367
  ret.push(` }`);
371
368
  return ret;
372
369
  };
@@ -396,7 +393,7 @@ export class ObjectHydrator extends Hydrator {
396
393
  // ReferenceKind.SCALAR
397
394
  ret.push(...hydrateScalar(prop, path, dataKey));
398
395
  }
399
- if (this.config.get('forceUndefined')) {
396
+ if (forceUndefined) {
400
397
  ret.push(` if (data${dataKey} === null) entity${entityKey} = undefined;`);
401
398
  }
402
399
  return ret;
@@ -1062,6 +1062,7 @@ export class MetadataDiscovery {
1062
1062
  });
1063
1063
  ownProps.forEach(prop => (meta.properties[prop.name] = prop));
1064
1064
  meta.filters = { ...base.filters, ...meta.filters };
1065
+ meta.forceUndefined ??= base.forceUndefined;
1065
1066
  if (!meta.discriminatorValue) {
1066
1067
  Object.values(base.properties)
1067
1068
  .filter(prop => !old.includes(prop.name))
@@ -85,6 +85,8 @@ export type EntityOptions<T, E = T extends EntityClass<infer P> ? P : T> = {
85
85
  abstract?: boolean;
86
86
  /** Disables change tracking - such entities are ignored during flush. */
87
87
  readonly?: boolean;
88
+ /** Hydrate `null` values as `undefined` for this entity, overriding the global `forceUndefined` option. */
89
+ forceUndefined?: boolean;
88
90
  /** Marks entity as {@doclink virtual-entities | virtual}. This is set automatically when you use `expression` option (unless `view` is set). */
89
91
  virtual?: boolean;
90
92
  /**
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.1",
4
4
  "description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
5
5
  "keywords": [
6
6
  "data-mapper",
package/typings.d.ts CHANGED
@@ -246,6 +246,8 @@ export type CleanTypeConfig<T> = Compute<Pick<T, Extract<keyof T, keyof TypeConf
246
246
  /** Configuration options that can be set on an entity via the `[Config]` symbol. */
247
247
  export interface TypeConfig {
248
248
  forceObject?: boolean;
249
+ /** Type-level marker of the `forceUndefined` entity option: `defineEntity` infers nullable properties as `T | undefined` instead of `T | null | undefined`. */
250
+ forceUndefined?: boolean;
249
251
  }
250
252
  declare const __selectedType: unique symbol;
251
253
  declare const __loadedType: unique symbol;
@@ -1223,6 +1225,8 @@ export interface EntityMetadata<Entity = any, Class extends EntityCtor<Entity> =
1223
1225
  selfReferencing?: boolean;
1224
1226
  hasUniqueProps?: boolean;
1225
1227
  readonly?: boolean;
1228
+ /** Per-entity override of the global `forceUndefined` option. */
1229
+ forceUndefined?: boolean;
1226
1230
  polymorphs?: EntityMetadata[];
1227
1231
  root: EntityMetadata<Entity>;
1228
1232
  definedProperties: Dictionary;
@@ -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
  }
@@ -79,7 +79,7 @@ export class UnitOfWork {
79
79
  * @internal
80
80
  */
81
81
  normalizeEntityData(meta, data) {
82
- const forceUndefined = this.#em.config.get('forceUndefined');
82
+ const forceUndefined = meta.forceUndefined ?? this.#em.config.get('forceUndefined');
83
83
  for (const key of Utils.keys(data)) {
84
84
  const prop = meta.properties[key];
85
85
  if (!prop) {
@@ -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.1';
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}`;