@mikro-orm/core 7.2.0-dev.2 → 7.2.0-dev.20

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.
Files changed (86) hide show
  1. package/EntityManager.d.ts +42 -8
  2. package/EntityManager.js +256 -70
  3. package/MikroORM.d.ts +4 -0
  4. package/MikroORM.js +9 -0
  5. package/cache/CacheAdapter.d.ts +6 -4
  6. package/cache/FileCacheAdapter.d.ts +1 -1
  7. package/cache/FileCacheAdapter.js +7 -2
  8. package/connections/Connection.d.ts +10 -1
  9. package/connections/Connection.js +9 -0
  10. package/drivers/DatabaseDriver.d.ts +17 -1
  11. package/drivers/DatabaseDriver.js +203 -41
  12. package/drivers/IDatabaseDriver.d.ts +1 -0
  13. package/entity/Collection.js +4 -2
  14. package/entity/EntityFactory.js +6 -0
  15. package/entity/EntityLoader.d.ts +7 -1
  16. package/entity/EntityLoader.js +46 -11
  17. package/entity/EntityRepository.d.ts +4 -5
  18. package/entity/EntityRepository.js +7 -2
  19. package/entity/defineEntity.d.ts +48 -14
  20. package/entity/defineEntity.js +32 -1
  21. package/enums.d.ts +5 -1
  22. package/enums.js +2 -0
  23. package/errors.d.ts +36 -0
  24. package/errors.js +90 -0
  25. package/events/EventManager.js +6 -3
  26. package/exceptions.d.ts +5 -0
  27. package/exceptions.js +5 -0
  28. package/hydration/ObjectHydrator.d.ts +2 -0
  29. package/hydration/ObjectHydrator.js +15 -8
  30. package/index.d.ts +1 -1
  31. package/metadata/EntitySchema.js +5 -2
  32. package/metadata/MetadataDiscovery.d.ts +3 -0
  33. package/metadata/MetadataDiscovery.js +161 -28
  34. package/metadata/MetadataProvider.js +1 -1
  35. package/metadata/MetadataStorage.d.ts +4 -3
  36. package/metadata/MetadataStorage.js +32 -2
  37. package/metadata/Routine.js +4 -1
  38. package/metadata/types.d.ts +19 -3
  39. package/naming-strategy/AbstractNamingStrategy.js +2 -1
  40. package/naming-strategy/NamingStrategy.d.ts +2 -1
  41. package/package.json +1 -1
  42. package/platforms/Platform.d.ts +24 -3
  43. package/platforms/Platform.js +63 -1
  44. package/types/BigIntType.d.ts +1 -0
  45. package/types/BigIntType.js +23 -0
  46. package/types/DateTimeType.d.ts +1 -0
  47. package/types/DateTimeType.js +8 -0
  48. package/types/StringType.d.ts +14 -3
  49. package/types/StringType.js +34 -4
  50. package/types/TextType.d.ts +2 -4
  51. package/types/TextType.js +2 -8
  52. package/types/Type.d.ts +11 -0
  53. package/types/Type.js +4 -4
  54. package/types/index.d.ts +2 -2
  55. package/typings.d.ts +58 -2
  56. package/typings.js +24 -1
  57. package/unit-of-work/ChangeSet.js +10 -7
  58. package/unit-of-work/ChangeSetPersister.js +32 -20
  59. package/unit-of-work/UnitOfWork.js +11 -4
  60. package/utils/AbstractMigrator.d.ts +1 -1
  61. package/utils/AbstractMigrator.js +3 -2
  62. package/utils/Configuration.d.ts +15 -1
  63. package/utils/Configuration.js +13 -2
  64. package/utils/Cursor.d.ts +2 -0
  65. package/utils/Cursor.js +44 -39
  66. package/utils/DataloaderUtils.js +2 -1
  67. package/utils/EntityComparator.d.ts +2 -0
  68. package/utils/EntityComparator.js +11 -4
  69. package/utils/QueryHelper.d.ts +17 -0
  70. package/utils/QueryHelper.js +100 -4
  71. package/utils/RawQueryFragment.d.ts +6 -0
  72. package/utils/RawQueryFragment.js +15 -6
  73. package/utils/RequestContext.d.ts +2 -2
  74. package/utils/RequestContext.js +11 -2
  75. package/utils/TransactionManager.d.ts +6 -0
  76. package/utils/TransactionManager.js +36 -3
  77. package/utils/Utils.d.ts +14 -2
  78. package/utils/Utils.js +34 -5
  79. package/utils/clone.js +6 -0
  80. package/utils/env-vars.js +1 -0
  81. package/utils/index.d.ts +1 -0
  82. package/utils/index.js +1 -0
  83. package/utils/rls-utils.d.ts +35 -0
  84. package/utils/rls-utils.js +97 -0
  85. package/utils/upsert-utils.d.ts +9 -1
  86. package/utils/upsert-utils.js +26 -3
package/EntityManager.js CHANGED
@@ -1,4 +1,5 @@
1
- import { getOnConflictReturningFields, getWhereCondition, resetUntouchedCollections } from './utils/upsert-utils.js';
1
+ import { getOnConflictReturningFields, getOnCreateGeneratedFields, getWhereCondition, resetUntouchedCollections, } from './utils/upsert-utils.js';
2
+ import { computeRemovedRlsVariables, computeRlsFilterVariables, findRlsFilterDefs } from './utils/rls-utils.js';
2
3
  import { Utils } from './utils/Utils.js';
3
4
  import { Cursor } from './utils/Cursor.js';
4
5
  import { QueryHelper } from './utils/QueryHelper.js';
@@ -15,7 +16,7 @@ import { UnitOfWork } from './unit-of-work/UnitOfWork.js';
15
16
  import { EventType, FlushMode, LoadStrategy, LockMode, PopulateHint, PopulatePath, QueryFlag, ReferenceKind, SCALAR_TYPES, } from './enums.js';
16
17
  import { EventManager } from './events/EventManager.js';
17
18
  import { TransactionEventBroadcaster } from './events/TransactionEventBroadcaster.js';
18
- import { OptimisticLockError, ValidationError } from './errors.js';
19
+ import { MetadataError, OptimisticLockError, ValidationError } from './errors.js';
19
20
  import { applyPopulateHints, getLoadingStrategy } from './entity/utils.js';
20
21
  import { TransactionManager } from './utils/TransactionManager.js';
21
22
  /**
@@ -44,6 +45,7 @@ export class EntityManager {
44
45
  #resultCache;
45
46
  #filters = {};
46
47
  #filterParams = {};
48
+ #sessionContext;
47
49
  loggerContext;
48
50
  #transactionContext;
49
51
  #disableTransactions;
@@ -116,7 +118,7 @@ export class EntityManager {
116
118
  return ret;
117
119
  }
118
120
  const em = this.getContext();
119
- em.prepareOptions(options);
121
+ options = em.prepareOptions(options);
120
122
  const meta = this.metadata.get(entityName);
121
123
  em.validateIndexUsage(meta, where, options);
122
124
  await em.tryFlush(entityName, options);
@@ -199,7 +201,12 @@ export class EntityManager {
199
201
  */
200
202
  async *stream(entityName, options = {}) {
201
203
  const em = this.getContext();
202
- em.prepareOptions(options);
204
+ // a stream never opens the implicit session-context transaction, so under the 'transaction' strategy the staged
205
+ // context would silently never apply outside an ambient transaction and other tenants' rows would leak — fail closed
206
+ if (!options.ctx && !em.#transactionContext && em.getTransactionSessionContext()) {
207
+ throw ValidationError.sessionContextStreamRequiresTransaction();
208
+ }
209
+ options = em.prepareOptions(options);
203
210
  options.strategy = 'joined';
204
211
  await em.tryFlush(entityName, options);
205
212
  const where = (await em.processWhere(entityName, options.where ?? {}, options, 'read'));
@@ -258,8 +265,17 @@ export class EntityManager {
258
265
  * Registers global filter to this entity manager. Global filters are enabled by default (unless disabled via last parameter).
259
266
  */
260
267
  addFilter(options) {
268
+ options = { ...options };
261
269
  if (options.entity) {
262
- options.entity = Utils.asArray(options.entity).map(n => Utils.className(n));
270
+ options.entity = Utils.asArray(options.entity).map(n => Utils.classOrName(n));
271
+ }
272
+ // runtime-registered filters are never part of entity metadata, so no policy can be generated for them — whether or
273
+ // not an `entity` was scoped, `rls` is only valid on filters declared in metadata via `@Filter()`. `rls` is excluded
274
+ // from the type above so TS users fail at compile time; the runtime guard still covers JS callers using `as any`
275
+ if (options.rls) {
276
+ throw options.entity
277
+ ? MetadataError.rlsFilterCannotBeRegisteredAtRuntime(options.name)
278
+ : MetadataError.rlsFilterMustBeEntityScoped(options.name);
263
279
  }
264
280
  options.default ??= true;
265
281
  this.getContext(false).#filters[options.name] = options;
@@ -269,7 +285,37 @@ export class EntityManager {
269
285
  * If you want to set shared value for all contexts, be sure to use the root entity manager.
270
286
  */
271
287
  setFilterParams(name, args) {
272
- this.getContext().#filterParams[name] = args;
288
+ const em = this.getContext();
289
+ // `rls` filters mirror their params as session variables, so the matching DB policies see the same values;
290
+ // the same filter name can be declared on multiple entities, so stage the union across all `rls`-flagged defs
291
+ const filters = findRlsFilterDefs(em.metadata, name);
292
+ if (filters.length === 0) {
293
+ em.#filterParams[name] = args;
294
+ return;
295
+ }
296
+ // fail before storing the params and pruning stale variables below, so an invalid staging attempt leaves both
297
+ // the filter params and the session context untouched
298
+ em.validateSessionContextStaging();
299
+ const variables = computeRlsFilterVariables(filters, args);
300
+ const previousArgs = em.#filterParams[name];
301
+ em.#filterParams[name] = args;
302
+ // this call replaces the filter's params, so drop the exact variables a previous call for this filter staged but
303
+ // this one no longer sets (unless another filter's current params still stage them)
304
+ const staged = em.#sessionContext?.variables;
305
+ if (staged && previousArgs) {
306
+ for (const key of computeRemovedRlsVariables(em.metadata, name, filters, previousArgs, variables, em.#filterParams)) {
307
+ delete staged[key];
308
+ }
309
+ // pruning may have emptied the whole context — drop it, so it does not keep forcing the implicit
310
+ // transaction wrap (and a distinct cache key) while carrying no variables
311
+ if (Object.keys(staged).length === 0 && !em.#sessionContext.role) {
312
+ em.#sessionContext = undefined;
313
+ }
314
+ }
315
+ // an empty context would still switch on the implicit transaction wrapping
316
+ if (Object.keys(variables).length > 0) {
317
+ em.mergeSessionContext({ variables });
318
+ }
273
319
  }
274
320
  /**
275
321
  * Returns filter parameters for given filter set in this context.
@@ -277,6 +323,90 @@ export class EntityManager {
277
323
  getFilterParams(name) {
278
324
  return this.getContext().#filterParams[name];
279
325
  }
326
+ /**
327
+ * Sets the database session context (row level security) for this entity manager. Session variables are merged
328
+ * with any previously set ones, while the role is replaced when one is provided. The variables are applied via `set_config()` and are
329
+ * typically referenced by RLS policies through `current_setting()`.
330
+ */
331
+ setSessionContext(context) {
332
+ // validate the global context like `setFilterParams` — a tenant context set on the global EM
333
+ // would be silently inherited by every later fork
334
+ this.getContext().mergeSessionContext(context);
335
+ }
336
+ validateSessionContextStaging() {
337
+ if (!this.getPlatform().supportsRowLevelSecurity()) {
338
+ throw ValidationError.sessionContextNotSupported();
339
+ }
340
+ // staging inside an open transaction is inert under both strategies (the 'transaction' context is only emitted at
341
+ // top-level begin; the 'connection' context was applied when the pinned connection was reserved), while
342
+ // `getSessionContext()` would still claim it is set — fail closed regardless of strategy
343
+ if (this.#transactionContext) {
344
+ throw ValidationError.sessionContextInsideTransaction();
345
+ }
346
+ if (this.config.get('sessionContext') === 'transaction') {
347
+ // the context is applied on transaction begin, so without implicit transactions writes run untransacted and
348
+ // silently bypass the policies — fail closed instead of leaking a base-role write
349
+ if (this.config.get('implicitTransactions') === false) {
350
+ throw ValidationError.sessionContextRequiresImplicitTransactions();
351
+ }
352
+ // same rationale for disabled transactions (config or fork option): the UoW flush would run untransacted
353
+ // and skip the context while reads still get the per-statement wrap — fail closed on the asymmetry
354
+ if (this.#disableTransactions) {
355
+ throw ValidationError.sessionContextWithDisabledTransactions();
356
+ }
357
+ }
358
+ }
359
+ /** Merges into this exact instance — `fork()` must bypass context resolution to target the new fork. */
360
+ mergeSessionContext(context) {
361
+ this.validateSessionContextStaging();
362
+ const current = this.#sessionContext;
363
+ const merged = {
364
+ variables: { ...current?.variables, ...context.variables },
365
+ role: context.role ?? current?.role,
366
+ };
367
+ // normalize an empty context away — it would still force the implicit transaction wrap and a distinct cache key
368
+ this.#sessionContext = Object.keys(merged.variables).length > 0 || merged.role ? merged : undefined;
369
+ }
370
+ /**
371
+ * Returns the database session context (row level security) set for this entity manager, or `undefined` if none.
372
+ */
373
+ getSessionContext() {
374
+ return this.getContext(false).#sessionContext;
375
+ }
376
+ /**
377
+ * Clears the database session context, since `setSessionContext()` only ever merges variables and updates the role.
378
+ */
379
+ clearSessionContext() {
380
+ const em = this.getContext(false);
381
+ // clearing inside an open transaction would be as inert (and cache-poisoning) as staging there — fail closed too,
382
+ // under both strategies (the 'connection' pinned connection was already reserved with the previous context)
383
+ if (em.#sessionContext && em.#transactionContext) {
384
+ throw ValidationError.sessionContextInsideTransaction('clear');
385
+ }
386
+ em.#sessionContext = undefined;
387
+ }
388
+ /** @internal session context to apply on `begin()` under the `'transaction'` strategy (`undefined` otherwise). */
389
+ getTransactionSessionContext() {
390
+ const em = this.getContext(false);
391
+ if (!em.#sessionContext || em.config.get('sessionContext') !== 'transaction') {
392
+ return undefined;
393
+ }
394
+ return em.#sessionContext;
395
+ }
396
+ /**
397
+ * Wraps a driver call in a short implicit transaction when the session context needs to apply, so `set local`
398
+ * takes effect. Resolves to a plain call when already inside a transaction or when no session context is set.
399
+ *
400
+ * @internal
401
+ */
402
+ async withSessionContext(ctx, cb) {
403
+ const em = this.getContext(false);
404
+ const sessionContext = ctx ? undefined : em.getTransactionSessionContext();
405
+ if (!sessionContext) {
406
+ return cb(ctx);
407
+ }
408
+ return em.getConnection('write').transactional(trx => cb(trx), { sessionContext, loggerContext: em.loggerContext });
409
+ }
280
410
  /**
281
411
  * Sets logger context for this entity manager.
282
412
  */
@@ -382,6 +512,7 @@ export class EntityManager {
382
512
  filters,
383
513
  populate: hint.children,
384
514
  populateWhere: PopulateHint.ALL,
515
+ populateFilter: undefined,
385
516
  });
386
517
  if (Utils.hasObjectKeys(where)) {
387
518
  ret[field] = ret[field] ? { $and: [where, ret[field]] } : where;
@@ -528,7 +659,7 @@ export class EntityManager {
528
659
  async findAndCount(entityName, where, options = {}) {
529
660
  const em = this.getContext(false);
530
661
  await em.tryFlush(entityName, options);
531
- options.flushMode = 'commit'; // do not try to auto flush again
662
+ options = { ...options, flushMode: 'commit' }; // do not try to auto flush again
532
663
  return Promise.all([
533
664
  em.find(entityName, where, options),
534
665
  em.count(entityName, where, options),
@@ -592,6 +723,7 @@ export class EntityManager {
592
723
  */
593
724
  async findByCursor(entityName, options) {
594
725
  const em = this.getContext(false);
726
+ options = { ...options };
595
727
  options.overfetch ??= true;
596
728
  options.where ??= {};
597
729
  if (Utils.isEmpty(options.orderBy) && !Raw.hasObjectFragments(options.orderBy)) {
@@ -610,10 +742,10 @@ export class EntityManager {
610
742
  async refreshOrFail(entity, options = {}) {
611
743
  const ret = await this.refresh(entity, options);
612
744
  if (!ret) {
613
- options.failHandler ??= this.config.get('findOneOrFailHandler');
745
+ const failHandler = options.failHandler ?? this.config.get('findOneOrFailHandler');
614
746
  const wrapped = helper(entity);
615
747
  const where = wrapped.getPrimaryKey();
616
- throw options.failHandler(wrapped.__meta.className, where);
748
+ throw failHandler(wrapped.__meta.className, where);
617
749
  }
618
750
  return ret;
619
751
  }
@@ -667,7 +799,7 @@ export class EntityManager {
667
799
  return ret;
668
800
  }
669
801
  const em = this.getContext();
670
- em.prepareOptions(options);
802
+ options = em.prepareOptions(options);
671
803
  let entity = em.#unitOfWork.tryGetById(entityName, where, options.schema);
672
804
  // query for a not managed entity which is already in the identity map as it
673
805
  // was provided with a PK this entity does not exist in the db, there can't
@@ -739,11 +871,11 @@ export class EntityManager {
739
871
  }
740
872
  if (!entity || isStrictViolation) {
741
873
  const key = options.strict ? 'findExactlyOneOrFailHandler' : 'findOneOrFailHandler';
742
- options.failHandler ??= this.config.get(key);
874
+ const failHandler = options.failHandler ?? this.config.get(key);
743
875
  const name = Utils.className(entityName);
744
876
  /* v8 ignore next */
745
877
  where = Utils.isEntity(where) ? helper(where).getPrimaryKey() : where;
746
- throw options.failHandler(name, where);
878
+ throw failHandler(name, where);
747
879
  }
748
880
  return entity;
749
881
  }
@@ -778,7 +910,7 @@ export class EntityManager {
778
910
  return ret;
779
911
  }
780
912
  const em = this.getContext(false);
781
- em.prepareOptions(options);
913
+ options = em.prepareOptions(options);
782
914
  let entityName;
783
915
  let where;
784
916
  let entity = null;
@@ -791,6 +923,7 @@ export class EntityManager {
791
923
  }
792
924
  const meta = this.metadata.get(entityName);
793
925
  const convertCustomTypes = !Utils.isEntity(data);
926
+ let generatedFields = [];
794
927
  if (Utils.isEntity(data)) {
795
928
  entity = data;
796
929
  if (helper(entity).__managed && helper(entity).__em === em && !this.config.get('upsertManaged')) {
@@ -798,6 +931,7 @@ export class EntityManager {
798
931
  return entity;
799
932
  }
800
933
  where = helper(entity).getPrimaryKey();
934
+ generatedFields = getOnCreateGeneratedFields(meta, entity);
801
935
  em.#entityFactory.assignDefaultValues(entity, meta);
802
936
  data = em.#comparator.prepareEntity(entity);
803
937
  }
@@ -810,6 +944,7 @@ export class EntityManager {
810
944
  return em.assign(exists, data);
811
945
  }
812
946
  }
947
+ generatedFields = getOnCreateGeneratedFields(meta, data);
813
948
  em.#entityFactory.assignDefaultValues(data, meta, true);
814
949
  for (const key of Object.keys(data)) {
815
950
  const prop = meta.properties[key];
@@ -818,6 +953,10 @@ export class EntityManager {
818
953
  }
819
954
  }
820
955
  }
956
+ // `onCreate` generated values are for the insert clause only, they must not overwrite an existing row
957
+ if (generatedFields.length > 0 && !options.onConflictMergeFields) {
958
+ options.onConflictExcludeFields = [...(options.onConflictExcludeFields ?? []), ...generatedFields];
959
+ }
821
960
  where = getWhereCondition(meta, options.onConflictFields, data, where).where;
822
961
  data = QueryHelper.processObjectParams(data);
823
962
  validateParams(data, 'insert data');
@@ -827,12 +966,12 @@ export class EntityManager {
827
966
  data = em.#comparator.prepareEntity(entity);
828
967
  }
829
968
  }
830
- const ret = await em.driver.nativeUpdate(entityName, where, data, {
831
- ctx: em.#transactionContext,
969
+ const ret = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.driver.nativeUpdate(entityName, where, data, {
832
970
  upsert: true,
833
971
  convertCustomTypes,
834
972
  ...options,
835
- });
973
+ ctx,
974
+ }));
836
975
  em.#unitOfWork.getChangeSetPersister().mapReturnedValues(entity, data, ret.row, meta, true);
837
976
  entity ??= em.#entityFactory.create(entityName, data, {
838
977
  refresh: true,
@@ -864,13 +1003,13 @@ export class EntityManager {
864
1003
  where[meta.primaryKeys[0]] = ret.insertId;
865
1004
  }
866
1005
  }
867
- const data2 = await this.driver.findOne(meta.class, where, {
1006
+ const data2 = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => this.driver.findOne(meta.class, where, {
868
1007
  fields: returning.concat(...(options.onConflictMergeFields ?? [])),
869
- ctx: em.#transactionContext,
1008
+ ctx,
870
1009
  convertCustomTypes: true,
871
1010
  connectionType: 'write',
872
1011
  schema: options.schema,
873
- });
1012
+ }));
874
1013
  em.getHydrator().hydrate(entity, meta, data2, em.#entityFactory, 'full', false, true);
875
1014
  }
876
1015
  // recompute the data as there might be some values missing (e.g. those with db column defaults)
@@ -916,7 +1055,7 @@ export class EntityManager {
916
1055
  return ret;
917
1056
  }
918
1057
  const em = this.getContext(false);
919
- em.prepareOptions(options);
1058
+ options = em.prepareOptions(options);
920
1059
  let entityName;
921
1060
  let propIndex;
922
1061
  if (data === undefined) {
@@ -937,6 +1076,7 @@ export class EntityManager {
937
1076
  }
938
1077
  const meta = this.metadata.get(entityName);
939
1078
  const convertCustomTypes = !Utils.isEntity(data[0]);
1079
+ const generatedFields = new Set();
940
1080
  const allData = [];
941
1081
  const allWhere = [];
942
1082
  const entities = new Map();
@@ -954,6 +1094,7 @@ export class EntityManager {
954
1094
  continue;
955
1095
  }
956
1096
  where = helper(entity).getPrimaryKey();
1097
+ getOnCreateGeneratedFields(meta, entity).forEach(field => generatedFields.add(field));
957
1098
  em.#entityFactory.assignDefaultValues(entity, meta);
958
1099
  entitiesByAllDataIdx.set(allData.length, entity);
959
1100
  row = em.#comparator.prepareEntity(entity);
@@ -970,6 +1111,7 @@ export class EntityManager {
970
1111
  continue;
971
1112
  }
972
1113
  }
1114
+ getOnCreateGeneratedFields(meta, row).forEach(field => generatedFields.add(field));
973
1115
  em.#entityFactory.assignDefaultValues(row, meta, true);
974
1116
  for (const key of Object.keys(row)) {
975
1117
  const prop = meta.properties[key];
@@ -998,6 +1140,10 @@ export class EntityManager {
998
1140
  if (entities.size === data.length) {
999
1141
  return [...entities.keys()];
1000
1142
  }
1143
+ // `onCreate` generated values are for the insert clause only, they must not overwrite existing rows
1144
+ if (generatedFields.size > 0 && !options.onConflictMergeFields) {
1145
+ options.onConflictExcludeFields = [...(options.onConflictExcludeFields ?? []), ...generatedFields];
1146
+ }
1001
1147
  if (em.eventManager.hasListeners(EventType.beforeUpsert, meta)) {
1002
1148
  for (const dto of data) {
1003
1149
  const entity = entitiesByData.get(dto) ?? dto;
@@ -1007,12 +1153,12 @@ export class EntityManager {
1007
1153
  allData[idx] = em.#comparator.prepareEntity(entity);
1008
1154
  }
1009
1155
  }
1010
- const res = await em.driver.nativeUpdateMany(entityName, allWhere, allData, {
1011
- ctx: em.#transactionContext,
1156
+ const res = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.driver.nativeUpdateMany(entityName, allWhere, allData, {
1012
1157
  upsert: true,
1013
1158
  convertCustomTypes,
1014
1159
  ...options,
1015
- });
1160
+ ctx,
1161
+ }));
1016
1162
  entities.clear();
1017
1163
  entitiesByData.clear();
1018
1164
  const loadPK = new Map();
@@ -1055,16 +1201,16 @@ export class EntityManager {
1055
1201
  where.$or[idx][prop] = item[prop];
1056
1202
  });
1057
1203
  });
1058
- const data2 = await this.driver.find(meta.class, where, {
1204
+ const data2 = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => this.driver.find(meta.class, where, {
1059
1205
  fields: returning
1060
1206
  .concat(...add)
1061
1207
  .concat(...(Array.isArray(uniqueFields) ? uniqueFields : []))
1062
1208
  .concat(...(options.onConflictMergeFields ?? [])),
1063
- ctx: em.#transactionContext,
1209
+ ctx,
1064
1210
  convertCustomTypes: true,
1065
1211
  connectionType: 'write',
1066
1212
  schema: options.schema,
1067
- });
1213
+ }));
1068
1214
  for (const [entity, cond] of loadPK.entries()) {
1069
1215
  const row = data2.find(row => {
1070
1216
  const tmp = {};
@@ -1162,6 +1308,7 @@ export class EntityManager {
1162
1308
  }
1163
1309
  const em = this.getContext(false);
1164
1310
  em.#transactionContext = await em.getConnection('write').begin({
1311
+ sessionContext: em.getTransactionSessionContext(),
1165
1312
  ...options,
1166
1313
  eventBroadcaster: new TransactionEventBroadcaster(em, { topLevelTransaction: !options.ctx }),
1167
1314
  });
@@ -1202,7 +1349,7 @@ export class EntityManager {
1202
1349
  */
1203
1350
  async lock(entity, lockMode, options = {}) {
1204
1351
  options = Utils.isPlainObject(options) ? options : { lockVersion: options };
1205
- this.getContext(false).prepareOptions(options);
1352
+ options = this.getContext(false).prepareOptions(options);
1206
1353
  await this.getUnitOfWork().lock(entity, { lockMode, ...options });
1207
1354
  }
1208
1355
  /**
@@ -1210,7 +1357,7 @@ export class EntityManager {
1210
1357
  */
1211
1358
  async insert(entityNameOrEntity, data, options = {}) {
1212
1359
  const em = this.getContext(false);
1213
- em.prepareOptions(options);
1360
+ options = em.prepareOptions(options);
1214
1361
  let entityName;
1215
1362
  if (data === undefined) {
1216
1363
  entityName = entityNameOrEntity.constructor;
@@ -1229,15 +1376,12 @@ export class EntityManager {
1229
1376
  const meta = helper(data).__meta;
1230
1377
  const payload = em.#comparator.prepareEntity(data);
1231
1378
  const cs = new ChangeSet(data, ChangeSetType.CREATE, payload, meta);
1232
- await em.#unitOfWork.getChangeSetPersister().executeInserts([cs], { ctx: em.#transactionContext, ...options });
1379
+ await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.#unitOfWork.getChangeSetPersister().executeInserts([cs], { ...options, ctx }));
1233
1380
  return cs.getPrimaryKey();
1234
1381
  }
1235
1382
  data = QueryHelper.processObjectParams(data);
1236
1383
  validateParams(data, 'insert data');
1237
- const res = await em.driver.nativeInsert(entityName, data, {
1238
- ctx: em.#transactionContext,
1239
- ...options,
1240
- });
1384
+ const res = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.driver.nativeInsert(entityName, data, { ...options, ctx }));
1241
1385
  return res.insertId;
1242
1386
  }
1243
1387
  /**
@@ -1277,12 +1421,9 @@ export class EntityManager {
1277
1421
  overrides = overridesOrOptions;
1278
1422
  }
1279
1423
  options ??= {};
1280
- em.prepareOptions(options);
1424
+ options = em.prepareOptions(options);
1281
1425
  const meta = em.metadata.get(entityName);
1282
- const res = await em.driver.nativeClone(entityName, where, overrides, {
1283
- ctx: em.#transactionContext,
1284
- ...options,
1285
- });
1426
+ const res = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.driver.nativeClone(entityName, where, overrides, { ...options, ctx }));
1286
1427
  const pk = res.insertId ?? res.row?.[meta.primaryKeys[0]];
1287
1428
  return em.findOneOrFail(entityName, pk, {
1288
1429
  schema: options.schema,
@@ -1293,7 +1434,7 @@ export class EntityManager {
1293
1434
  */
1294
1435
  async insertMany(entityNameOrEntities, data, options = {}) {
1295
1436
  const em = this.getContext(false);
1296
- em.prepareOptions(options);
1437
+ options = em.prepareOptions(options);
1297
1438
  let entityName;
1298
1439
  if (data === undefined) {
1299
1440
  entityName = entityNameOrEntities[0].constructor;
@@ -1317,15 +1458,12 @@ export class EntityManager {
1317
1458
  const payload = em.#comparator.prepareEntity(row);
1318
1459
  return new ChangeSet(row, ChangeSetType.CREATE, payload, meta);
1319
1460
  });
1320
- await em.#unitOfWork.getChangeSetPersister().executeInserts(css, { ctx: em.#transactionContext, ...options });
1461
+ await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.#unitOfWork.getChangeSetPersister().executeInserts(css, { ...options, ctx }));
1321
1462
  return css.map(cs => cs.getPrimaryKey());
1322
1463
  }
1323
1464
  data = data.map(row => QueryHelper.processObjectParams(row));
1324
1465
  data.forEach(row => validateParams(row, 'insert data'));
1325
- const res = await em.driver.nativeInsertMany(entityName, data, {
1326
- ctx: em.#transactionContext,
1327
- ...options,
1328
- });
1466
+ const res = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.driver.nativeInsertMany(entityName, data, { ...options, ctx }));
1329
1467
  if (res.insertedIds) {
1330
1468
  return res.insertedIds;
1331
1469
  }
@@ -1336,17 +1474,17 @@ export class EntityManager {
1336
1474
  */
1337
1475
  async nativeUpdate(entityName, where, data, options = {}) {
1338
1476
  const em = this.getContext(false);
1339
- em.prepareOptions(options);
1477
+ options = em.prepareOptions(options);
1340
1478
  await em.processUnionWhere(entityName, options, 'update');
1341
1479
  data = QueryHelper.processObjectParams(data);
1342
1480
  where = await em.processWhere(entityName, where, { ...options, convertCustomTypes: false }, 'update');
1343
1481
  validateParams(data, 'update data');
1344
1482
  validateParams(where, 'update condition');
1345
- const res = await em.driver.nativeUpdate(entityName, where, data, {
1346
- ctx: em.#transactionContext,
1483
+ const res = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.driver.nativeUpdate(entityName, where, data, {
1347
1484
  em,
1348
1485
  ...options,
1349
- });
1486
+ ctx,
1487
+ }));
1350
1488
  return res.affectedRows;
1351
1489
  }
1352
1490
  /**
@@ -1378,29 +1516,37 @@ export class EntityManager {
1378
1516
  throw new Error(`Routine '${routine.name}' is not registered in the 'routines' config option.`);
1379
1517
  }
1380
1518
  const conn = em.driver.getConnection('write');
1381
- return conn.callRoutine(routine, args, em.#transactionContext);
1519
+ return em.withSessionContext(em.#transactionContext, ctx => conn.callRoutine(routine, args, ctx));
1382
1520
  }
1383
1521
  /**
1384
1522
  * Fires native delete query. Calling this has no side effects on the context (identity map).
1385
1523
  */
1386
1524
  async nativeDelete(entityName, where, options = {}) {
1387
1525
  const em = this.getContext(false);
1388
- em.prepareOptions(options);
1526
+ options = em.prepareOptions(options);
1389
1527
  await em.processUnionWhere(entityName, options, 'delete');
1390
1528
  where = (await em.processWhere(entityName, where, options, 'delete'));
1391
1529
  validateParams(where, 'delete condition');
1392
- const res = await em.driver.nativeDelete(entityName, where, {
1393
- ctx: em.#transactionContext,
1530
+ const res = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.driver.nativeDelete(entityName, where, {
1394
1531
  em,
1395
1532
  ...options,
1396
- });
1533
+ ctx,
1534
+ }));
1397
1535
  return res.affectedRows;
1398
1536
  }
1399
1537
  /**
1400
- * Maps raw database result to an entity and merges it to this EntityManager.
1538
+ * Maps raw database result to an entity and merges it to this EntityManager by default.
1539
+ * Use `disableIdentityMap` to return an isolated entity without affecting the current context.
1401
1540
  */
1402
1541
  map(entityName, result, options = {}) {
1403
- const { mapped, ...rest } = options;
1542
+ if (options.disableIdentityMap ?? this.config.get('disableIdentityMap')) {
1543
+ const em = this.getContext(false);
1544
+ const fork = em.fork({ keepTransactionContext: true });
1545
+ const ret = fork.map(entityName, result, { ...options, disableIdentityMap: false });
1546
+ fork.clear();
1547
+ return ret;
1548
+ }
1549
+ const { mapped, disableIdentityMap, ...rest } = options;
1404
1550
  const meta = this.metadata.get(entityName);
1405
1551
  const data = (mapped ? result : this.driver.mapResult(result, meta));
1406
1552
  for (const k of Object.keys(data)) {
@@ -1428,6 +1574,7 @@ export class EntityManager {
1428
1574
  return this.merge(entityName.constructor, entityName, data);
1429
1575
  }
1430
1576
  const em = options.disableContextResolution ? this : this.getContext();
1577
+ options = { ...options };
1431
1578
  options.schema ??= em.#schema;
1432
1579
  options.validate ??= true;
1433
1580
  options.cascade ??= true;
@@ -1461,6 +1608,7 @@ export class EntityManager {
1461
1608
  */
1462
1609
  create(entityName, data, options = {}) {
1463
1610
  const em = this.getContext();
1611
+ options = { ...options };
1464
1612
  options.schema ??= em.#schema;
1465
1613
  const entity = em.#entityFactory.create(entityName, data, {
1466
1614
  ...options,
@@ -1484,6 +1632,7 @@ export class EntityManager {
1484
1632
  * Gets a reference to the entity identified by the given type and identifier without actually loading it, if the entity is not yet loaded
1485
1633
  */
1486
1634
  getReference(entityName, id, options = {}) {
1635
+ options = { ...options };
1487
1636
  options.schema ??= this.schema;
1488
1637
  options.convertCustomTypes ??= false;
1489
1638
  const meta = this.metadata.get(entityName);
@@ -1504,13 +1653,10 @@ export class EntityManager {
1504
1653
  */
1505
1654
  async count(entityName, where = {}, options = {}) {
1506
1655
  const em = this.getContext(false);
1507
- // Shallow copy options since the object will be modified when deleting orderBy
1508
- options = { ...options };
1509
- em.prepareOptions(options);
1656
+ options = em.prepareOptions(options);
1510
1657
  await em.tryFlush(entityName, options);
1511
1658
  where = await em.processWhere(entityName, where, options, 'read');
1512
1659
  options.populate = (await em.preparePopulate(entityName, options));
1513
- options = { ...options };
1514
1660
  // save the original hint value so we know it was infer/all
1515
1661
  const meta = em.metadata.find(entityName);
1516
1662
  options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
@@ -1524,7 +1670,7 @@ export class EntityManager {
1524
1670
  if (cached?.data !== undefined) {
1525
1671
  return cached.data;
1526
1672
  }
1527
- const count = await em.driver.count(entityName, where, { ctx: em.#transactionContext, em, ...options });
1673
+ const count = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.driver.count(entityName, where, { em, ...options, ctx }));
1528
1674
  await em.storeCache(options.cache, cached, () => +count);
1529
1675
  return +count;
1530
1676
  }
@@ -1639,8 +1785,9 @@ export class EntityManager {
1639
1785
  }
1640
1786
  // For TPT inheritance, check the entity's own properties, not just the root's
1641
1787
  // For STI, meta.properties includes all properties anyway
1642
- const ret = p in meta.properties;
1643
- if (parts.length > 0) {
1788
+ // use an own-property check so inherited `Object.prototype` members (e.g. `__proto__`, `constructor`) are not treated as populatable
1789
+ const ret = Object.hasOwn(meta.properties, p);
1790
+ if (ret && parts.length > 0) {
1644
1791
  return this.canPopulate(meta.properties[p].targetMeta.class, parts.join('.'));
1645
1792
  }
1646
1793
  return ret;
@@ -1654,7 +1801,7 @@ export class EntityManager {
1654
1801
  return entities;
1655
1802
  }
1656
1803
  const em = this.getContext();
1657
- em.prepareOptions(options);
1804
+ options = em.prepareOptions(options);
1658
1805
  const entityName = arr[0].constructor;
1659
1806
  const preparedPopulate = await em.preparePopulate(entityName, { populate: populate, filters: options.filters, populateHints: options.populateHints }, options.validate);
1660
1807
  await em.#entityLoader.populate(entityName, arr, preparedPopulate, options);
@@ -1665,6 +1812,7 @@ export class EntityManager {
1665
1812
  */
1666
1813
  fork(options = {}) {
1667
1814
  const em = options.disableContextResolution ? this : this.getContext(false);
1815
+ options = { ...options };
1668
1816
  options.clear ??= true;
1669
1817
  options.useContext ??= false;
1670
1818
  options.freshEventManager ??= false;
@@ -1687,6 +1835,18 @@ export class EntityManager {
1687
1835
  }
1688
1836
  fork.#filters = { ...em.#filters };
1689
1837
  fork.#filterParams = Utils.copy(em.#filterParams);
1838
+ if (options.session) {
1839
+ // the fork's session replaces the parent context, but the copied `rls` filter params must stay consistent with
1840
+ // it — re-stage their variables underneath the explicit `session.variables`, which win on any conflict
1841
+ const variables = {};
1842
+ for (const name of Object.keys(fork.#filterParams)) {
1843
+ Object.assign(variables, computeRlsFilterVariables(findRlsFilterDefs(fork.metadata, name), fork.#filterParams[name]));
1844
+ }
1845
+ fork.mergeSessionContext({ ...options.session, variables: { ...variables, ...options.session.variables } });
1846
+ }
1847
+ else if (em.#sessionContext) {
1848
+ fork.#sessionContext = Utils.copy(em.#sessionContext);
1849
+ }
1690
1850
  fork.loggerContext = Utils.merge({}, em.loggerContext, options.loggerContext);
1691
1851
  fork.#schema = options.schema ?? em.#schema;
1692
1852
  fork.signal = options.signal ?? em.signal;
@@ -2073,10 +2233,13 @@ export class EntityManager {
2073
2233
  if (!Utils.isEmpty(options.fields) && !Utils.isEmpty(options.exclude)) {
2074
2234
  throw new ValidationError(`Cannot combine 'fields' and 'exclude' option.`);
2075
2235
  }
2076
- options.schema ??= this.#schema;
2077
- options.signal ??= this.signal;
2078
- options.inflightQueryAbortStrategy ??= this.inflightQueryAbortStrategy;
2079
- options.logging = options.loggerContext = Utils.merge({ id: this.id }, this.loggerContext, options.loggerContext, options.logging);
2236
+ // the options object belongs to the caller, everything below works on our own copy
2237
+ const opts = { ...options };
2238
+ opts.schema ??= this.#schema;
2239
+ opts.signal ??= this.signal;
2240
+ opts.inflightQueryAbortStrategy ??= this.inflightQueryAbortStrategy;
2241
+ opts.logging = opts.loggerContext = Utils.merge({ id: this.id }, this.loggerContext, opts.loggerContext, opts.logging);
2242
+ return opts;
2080
2243
  }
2081
2244
  /**
2082
2245
  * @internal
@@ -2095,7 +2258,19 @@ export class EntityManager {
2095
2258
  ]) {
2096
2259
  delete opts[k];
2097
2260
  }
2098
- return [Utils.className(entityName), method, opts, where];
2261
+ // the table name (plus discriminator value for STI) is stable across builds and processes,
2262
+ // unlike class names, which minifiers can mangle to the same short name for two entities
2263
+ const meta = this.metadata.find(entityName);
2264
+ const entityKey = meta?.tableName
2265
+ ? [meta.schema, meta.tableName, meta.discriminatorValue]
2266
+ : Utils.className(entityName);
2267
+ const key = [entityKey, method, opts, where];
2268
+ // session context (row level security) scopes cached rows per tenant/role, avoiding cross-context serves
2269
+ const sessionContext = this.getContext(false).#sessionContext;
2270
+ if (sessionContext) {
2271
+ key.push(sessionContext);
2272
+ }
2273
+ return key;
2099
2274
  }
2100
2275
  /**
2101
2276
  * @internal
@@ -2106,7 +2281,13 @@ export class EntityManager {
2106
2281
  return undefined;
2107
2282
  }
2108
2283
  const em = this.getContext();
2109
- const cacheKey = Array.isArray(config) ? config[0] : JSON.stringify(key);
2284
+ // a named cache key (`config[0]`) discards the computed `key`, which already carries the session context — so
2285
+ // scope it here too, otherwise a fork's rows would be served to another session context under the same name
2286
+ const cacheKey = Array.isArray(config)
2287
+ ? em.#sessionContext
2288
+ ? `${config[0]}|${JSON.stringify(em.#sessionContext)}`
2289
+ : config[0]
2290
+ : JSON.stringify(key);
2110
2291
  const cached = await em.#resultCache.get(cacheKey);
2111
2292
  if (!cached) {
2112
2293
  return { key: cacheKey, data: cached };
@@ -2154,7 +2335,12 @@ export class EntityManager {
2154
2335
  * ```
2155
2336
  */
2156
2337
  async clearCache(cacheKey) {
2157
- await this.getContext().#resultCache.remove(cacheKey);
2338
+ const em = this.getContext();
2339
+ await em.#resultCache.remove(cacheKey);
2340
+ // named keys are scoped by the session context (see `tryCache`), so clear this context's variant too
2341
+ if (em.#sessionContext) {
2342
+ await em.#resultCache.remove(`${cacheKey}|${JSON.stringify(em.#sessionContext)}`);
2343
+ }
2158
2344
  }
2159
2345
  /**
2160
2346
  * Returns the default schema of this EntityManager. Respects the context, so global EM will give you the contextual schema