@mikro-orm/core 7.1.16-dev.1 → 7.1.16-dev.11

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 (61) hide show
  1. package/EntityManager.d.ts +41 -7
  2. package/EntityManager.js +203 -42
  3. package/MikroORM.d.ts +4 -0
  4. package/MikroORM.js +9 -0
  5. package/README.md +1 -0
  6. package/cache/FileCacheAdapter.js +1 -1
  7. package/connections/Connection.d.ts +10 -1
  8. package/connections/Connection.js +9 -0
  9. package/drivers/DatabaseDriver.d.ts +14 -5
  10. package/drivers/DatabaseDriver.js +151 -52
  11. package/entity/Collection.js +4 -2
  12. package/entity/EntityLoader.d.ts +7 -1
  13. package/entity/EntityLoader.js +35 -5
  14. package/entity/EntityRepository.d.ts +4 -5
  15. package/entity/EntityRepository.js +2 -1
  16. package/entity/defineEntity.d.ts +23 -5
  17. package/entity/defineEntity.js +31 -0
  18. package/enums.d.ts +5 -1
  19. package/enums.js +2 -0
  20. package/errors.d.ts +35 -0
  21. package/errors.js +87 -0
  22. package/exceptions.d.ts +5 -0
  23. package/exceptions.js +5 -0
  24. package/index.d.ts +1 -1
  25. package/metadata/MetadataDiscovery.d.ts +3 -0
  26. package/metadata/MetadataDiscovery.js +94 -7
  27. package/metadata/types.d.ts +19 -3
  28. package/package.json +1 -1
  29. package/platforms/Platform.d.ts +19 -1
  30. package/platforms/Platform.js +56 -0
  31. package/types/BigIntType.d.ts +1 -0
  32. package/types/BigIntType.js +23 -0
  33. package/types/DateTimeType.d.ts +1 -0
  34. package/types/DateTimeType.js +8 -0
  35. package/types/StringType.d.ts +14 -3
  36. package/types/StringType.js +34 -4
  37. package/types/TextType.d.ts +2 -4
  38. package/types/TextType.js +2 -8
  39. package/types/Type.d.ts +11 -0
  40. package/types/index.d.ts +2 -2
  41. package/typings.d.ts +48 -0
  42. package/typings.js +1 -0
  43. package/unit-of-work/UnitOfWork.js +1 -0
  44. package/utils/Configuration.d.ts +21 -1
  45. package/utils/Configuration.js +12 -1
  46. package/utils/Cursor.d.ts +2 -0
  47. package/utils/Cursor.js +43 -33
  48. package/utils/QueryHelper.d.ts +12 -0
  49. package/utils/QueryHelper.js +63 -0
  50. package/utils/RawQueryFragment.d.ts +6 -0
  51. package/utils/RawQueryFragment.js +15 -6
  52. package/utils/RequestContext.d.ts +2 -2
  53. package/utils/RequestContext.js +11 -2
  54. package/utils/TransactionManager.js +1 -1
  55. package/utils/Utils.d.ts +2 -0
  56. package/utils/Utils.js +12 -3
  57. package/utils/env-vars.js +2 -0
  58. package/utils/index.d.ts +1 -0
  59. package/utils/index.js +1 -0
  60. package/utils/rls-utils.d.ts +35 -0
  61. package/utils/rls-utils.js +97 -0
@@ -7,7 +7,7 @@ import { EntityLoader, type EntityLoaderOptions } from './entity/EntityLoader.js
7
7
  import { Reference } from './entity/Reference.js';
8
8
  import { UnitOfWork } from './unit-of-work/UnitOfWork.js';
9
9
  import type { CountByOptions, CountOptions, DeleteOptions, FilterOptions, FindAllOptions, FindByCursorOptions, FindOneOptions, FindOneOrFailOptions, FindOptions, GetReferenceOptions, IDatabaseDriver, LockOptions, NativeInsertUpdateOptions, StreamOptions, UpdateOptions, UpsertManyOptions, UpsertOptions } from './drivers/IDatabaseDriver.js';
10
- import type { AnyString, ArrayElement, AutoPath, ConnectionType, Dictionary, EntityClass, EntityData, EntityDictionary, EntityDTO, EntityKey, EntityMetadata, EntityName, FilterDef, FilterQuery, FromEntityType, GetRepository, IHydrator, IsSubset, Loaded, MergeLoaded, MergeSelected, ObjectQuery, PopulateOptions, Primary, Ref, RequiredEntityData, RoutineArgs, RoutineReturn, UnboxArray, IndexFilterQuery, WithUsingOptions } from './typings.js';
10
+ import type { AnyString, ArrayElement, AutoPath, ConnectionType, Dictionary, EntityClass, EntityData, EntityDictionary, EntityDTO, EntityKey, EntityMetadata, EntityName, FilterDef, FilterQuery, FromEntityType, GetRepository, IHydrator, IsSubset, Loaded, MergeLoaded, MergeSelected, ObjectQuery, PopulateOptions, Primary, Ref, RequiredEntityData, RoutineArgs, RoutineReturn, SessionContext, UnboxArray, IndexFilterQuery, WithUsingOptions } from './typings.js';
11
11
  import type { Routine } from './metadata/Routine.js';
12
12
  import { FlushMode, LockMode, PopulatePath, type TransactionOptions } from './enums.js';
13
13
  import type { MetadataStorage } from './metadata/MetadataStorage.js';
@@ -95,7 +95,7 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
95
95
  /**
96
96
  * Registers global filter to this entity manager. Global filters are enabled by default (unless disabled via last parameter).
97
97
  */
98
- addFilter<T extends EntityName | readonly EntityName[]>(options: FilterDef<T>): void;
98
+ addFilter<T extends EntityName | readonly EntityName[]>(options: Omit<FilterDef<T>, 'rls'>): void;
99
99
  /**
100
100
  * Sets filter parameter values globally inside context defined by this entity manager.
101
101
  * If you want to set shared value for all contexts, be sure to use the root entity manager.
@@ -105,6 +105,32 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
105
105
  * Returns filter parameters for given filter set in this context.
106
106
  */
107
107
  getFilterParams<T extends Dictionary = Dictionary>(name: string): T;
108
+ /**
109
+ * Sets the database session context (row level security) for this entity manager. Session variables are merged
110
+ * with any previously set ones, while the role is replaced when one is provided. The variables are applied via `set_config()` and are
111
+ * typically referenced by RLS policies through `current_setting()`.
112
+ */
113
+ setSessionContext(context: SessionContext): void;
114
+ private validateSessionContextStaging;
115
+ /** Merges into this exact instance — `fork()` must bypass context resolution to target the new fork. */
116
+ private mergeSessionContext;
117
+ /**
118
+ * Returns the database session context (row level security) set for this entity manager, or `undefined` if none.
119
+ */
120
+ getSessionContext(): SessionContext | undefined;
121
+ /**
122
+ * Clears the database session context, since `setSessionContext()` only ever merges variables and updates the role.
123
+ */
124
+ clearSessionContext(): void;
125
+ /** @internal session context to apply on `begin()` under the `'transaction'` strategy (`undefined` otherwise). */
126
+ getTransactionSessionContext(): SessionContext | undefined;
127
+ /**
128
+ * Wraps a driver call in a short implicit transaction when the session context needs to apply, so `set local`
129
+ * takes effect. Resolves to a plain call when already inside a transaction or when no session context is set.
130
+ *
131
+ * @internal
132
+ */
133
+ withSessionContext<T>(ctx: Transaction | undefined, cb: (ctx?: Transaction) => Promise<T>): Promise<T>;
108
134
  /**
109
135
  * Sets logger context for this entity manager.
110
136
  */
@@ -375,12 +401,10 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
375
401
  */
376
402
  nativeDelete<Entity extends object>(entityName: EntityName<Entity>, where: FilterQuery<NoInfer<Entity>>, options?: DeleteOptions<Entity>): Promise<number>;
377
403
  /**
378
- * Maps raw database result to an entity and merges it to this EntityManager.
404
+ * Maps raw database result to an entity and merges it to this EntityManager by default.
405
+ * Use `disableIdentityMap` to return an isolated entity without affecting the current context.
379
406
  */
380
- map<Entity extends object>(entityName: EntityName<Entity>, result: EntityDictionary<Entity>, options?: {
381
- schema?: string;
382
- mapped?: boolean;
383
- }): Entity;
407
+ map<Entity extends object>(entityName: EntityName<Entity>, result: EntityDictionary<Entity>, options?: MapOptions): Entity;
384
408
  /**
385
409
  * Merges given entity to this EntityManager so it becomes managed. You can force refreshing of existing entities
386
410
  * via second parameter. By default, it will return already loaded entities without modifying them.
@@ -667,6 +691,14 @@ export interface CreateOptions<Convert extends boolean> {
667
691
  */
668
692
  processOnCreateHooksEarly?: boolean;
669
693
  }
694
+ export interface MapOptions {
695
+ /** schema to use when mapping the entity */
696
+ schema?: string;
697
+ /** set to true when the result is already mapped to entity property names */
698
+ mapped?: boolean;
699
+ /** map the entity in an isolated context without adding it to the identity map */
700
+ disableIdentityMap?: boolean;
701
+ }
670
702
  export interface MergeOptions {
671
703
  refresh?: boolean;
672
704
  convertCustomTypes?: boolean;
@@ -694,6 +726,8 @@ export interface ForkOptions {
694
726
  keepTransactionContext?: boolean;
695
727
  /** default schema to use for this fork */
696
728
  schema?: string;
729
+ /** database session context (row level security) for this fork; inherited from the parent when not set */
730
+ session?: SessionContext;
697
731
  /** default logger context, can be overridden via {@apilink FindOptions} */
698
732
  loggerContext?: Dictionary;
699
733
  /**
package/EntityManager.js CHANGED
@@ -1,4 +1,5 @@
1
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;
@@ -199,6 +201,11 @@ export class EntityManager {
199
201
  */
200
202
  async *stream(entityName, options = {}) {
201
203
  const em = this.getContext();
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
+ }
202
209
  options = em.prepareOptions(options);
203
210
  options.strategy = 'joined';
204
211
  await em.tryFlush(entityName, options);
@@ -262,6 +269,14 @@ export class EntityManager {
262
269
  if (options.entity) {
263
270
  options.entity = Utils.asArray(options.entity).map(n => Utils.classOrName(n));
264
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);
279
+ }
265
280
  options.default ??= true;
266
281
  this.getContext(false).#filters[options.name] = options;
267
282
  }
@@ -270,7 +285,37 @@ export class EntityManager {
270
285
  * If you want to set shared value for all contexts, be sure to use the root entity manager.
271
286
  */
272
287
  setFilterParams(name, args) {
273
- 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
+ }
274
319
  }
275
320
  /**
276
321
  * Returns filter parameters for given filter set in this context.
@@ -278,6 +323,90 @@ export class EntityManager {
278
323
  getFilterParams(name) {
279
324
  return this.getContext().#filterParams[name];
280
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
+ }
281
410
  /**
282
411
  * Sets logger context for this entity manager.
283
412
  */
@@ -383,6 +512,7 @@ export class EntityManager {
383
512
  filters,
384
513
  populate: hint.children,
385
514
  populateWhere: PopulateHint.ALL,
515
+ populateFilter: undefined,
386
516
  });
387
517
  if (Utils.hasObjectKeys(where)) {
388
518
  ret[field] = ret[field] ? { $and: [where, ret[field]] } : where;
@@ -836,12 +966,12 @@ export class EntityManager {
836
966
  data = em.#comparator.prepareEntity(entity);
837
967
  }
838
968
  }
839
- const ret = await em.driver.nativeUpdate(entityName, where, data, {
840
- ctx: em.#transactionContext,
969
+ const ret = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.driver.nativeUpdate(entityName, where, data, {
841
970
  upsert: true,
842
971
  convertCustomTypes,
843
972
  ...options,
844
- });
973
+ ctx,
974
+ }));
845
975
  em.#unitOfWork.getChangeSetPersister().mapReturnedValues(entity, data, ret.row, meta, true);
846
976
  entity ??= em.#entityFactory.create(entityName, data, {
847
977
  refresh: true,
@@ -873,13 +1003,13 @@ export class EntityManager {
873
1003
  where[meta.primaryKeys[0]] = ret.insertId;
874
1004
  }
875
1005
  }
876
- 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, {
877
1007
  fields: returning.concat(...(options.onConflictMergeFields ?? [])),
878
- ctx: em.#transactionContext,
1008
+ ctx,
879
1009
  convertCustomTypes: true,
880
1010
  connectionType: 'write',
881
1011
  schema: options.schema,
882
- });
1012
+ }));
883
1013
  em.getHydrator().hydrate(entity, meta, data2, em.#entityFactory, 'full', false, true);
884
1014
  }
885
1015
  // recompute the data as there might be some values missing (e.g. those with db column defaults)
@@ -1023,12 +1153,12 @@ export class EntityManager {
1023
1153
  allData[idx] = em.#comparator.prepareEntity(entity);
1024
1154
  }
1025
1155
  }
1026
- const res = await em.driver.nativeUpdateMany(entityName, allWhere, allData, {
1027
- ctx: em.#transactionContext,
1156
+ const res = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.driver.nativeUpdateMany(entityName, allWhere, allData, {
1028
1157
  upsert: true,
1029
1158
  convertCustomTypes,
1030
1159
  ...options,
1031
- });
1160
+ ctx,
1161
+ }));
1032
1162
  entities.clear();
1033
1163
  entitiesByData.clear();
1034
1164
  const loadPK = new Map();
@@ -1071,16 +1201,16 @@ export class EntityManager {
1071
1201
  where.$or[idx][prop] = item[prop];
1072
1202
  });
1073
1203
  });
1074
- 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, {
1075
1205
  fields: returning
1076
1206
  .concat(...add)
1077
1207
  .concat(...(Array.isArray(uniqueFields) ? uniqueFields : []))
1078
1208
  .concat(...(options.onConflictMergeFields ?? [])),
1079
- ctx: em.#transactionContext,
1209
+ ctx,
1080
1210
  convertCustomTypes: true,
1081
1211
  connectionType: 'write',
1082
1212
  schema: options.schema,
1083
- });
1213
+ }));
1084
1214
  for (const [entity, cond] of loadPK.entries()) {
1085
1215
  const row = data2.find(row => {
1086
1216
  const tmp = {};
@@ -1178,6 +1308,7 @@ export class EntityManager {
1178
1308
  }
1179
1309
  const em = this.getContext(false);
1180
1310
  em.#transactionContext = await em.getConnection('write').begin({
1311
+ sessionContext: em.getTransactionSessionContext(),
1181
1312
  ...options,
1182
1313
  eventBroadcaster: new TransactionEventBroadcaster(em, { topLevelTransaction: !options.ctx }),
1183
1314
  });
@@ -1245,15 +1376,12 @@ export class EntityManager {
1245
1376
  const meta = helper(data).__meta;
1246
1377
  const payload = em.#comparator.prepareEntity(data);
1247
1378
  const cs = new ChangeSet(data, ChangeSetType.CREATE, payload, meta);
1248
- 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 }));
1249
1380
  return cs.getPrimaryKey();
1250
1381
  }
1251
1382
  data = QueryHelper.processObjectParams(data);
1252
1383
  validateParams(data, 'insert data');
1253
- const res = await em.driver.nativeInsert(entityName, data, {
1254
- ctx: em.#transactionContext,
1255
- ...options,
1256
- });
1384
+ const res = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.driver.nativeInsert(entityName, data, { ...options, ctx }));
1257
1385
  return res.insertId;
1258
1386
  }
1259
1387
  /**
@@ -1295,10 +1423,7 @@ export class EntityManager {
1295
1423
  options ??= {};
1296
1424
  options = em.prepareOptions(options);
1297
1425
  const meta = em.metadata.get(entityName);
1298
- const res = await em.driver.nativeClone(entityName, where, overrides, {
1299
- ctx: em.#transactionContext,
1300
- ...options,
1301
- });
1426
+ const res = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.driver.nativeClone(entityName, where, overrides, { ...options, ctx }));
1302
1427
  const pk = res.insertId ?? res.row?.[meta.primaryKeys[0]];
1303
1428
  return em.findOneOrFail(entityName, pk, {
1304
1429
  schema: options.schema,
@@ -1333,15 +1458,12 @@ export class EntityManager {
1333
1458
  const payload = em.#comparator.prepareEntity(row);
1334
1459
  return new ChangeSet(row, ChangeSetType.CREATE, payload, meta);
1335
1460
  });
1336
- 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 }));
1337
1462
  return css.map(cs => cs.getPrimaryKey());
1338
1463
  }
1339
1464
  data = data.map(row => QueryHelper.processObjectParams(row));
1340
1465
  data.forEach(row => validateParams(row, 'insert data'));
1341
- const res = await em.driver.nativeInsertMany(entityName, data, {
1342
- ctx: em.#transactionContext,
1343
- ...options,
1344
- });
1466
+ const res = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.driver.nativeInsertMany(entityName, data, { ...options, ctx }));
1345
1467
  if (res.insertedIds) {
1346
1468
  return res.insertedIds;
1347
1469
  }
@@ -1358,11 +1480,11 @@ export class EntityManager {
1358
1480
  where = await em.processWhere(entityName, where, { ...options, convertCustomTypes: false }, 'update');
1359
1481
  validateParams(data, 'update data');
1360
1482
  validateParams(where, 'update condition');
1361
- const res = await em.driver.nativeUpdate(entityName, where, data, {
1362
- ctx: em.#transactionContext,
1483
+ const res = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.driver.nativeUpdate(entityName, where, data, {
1363
1484
  em,
1364
1485
  ...options,
1365
- });
1486
+ ctx,
1487
+ }));
1366
1488
  return res.affectedRows;
1367
1489
  }
1368
1490
  /**
@@ -1394,7 +1516,7 @@ export class EntityManager {
1394
1516
  throw new Error(`Routine '${routine.name}' is not registered in the 'routines' config option.`);
1395
1517
  }
1396
1518
  const conn = em.driver.getConnection('write');
1397
- return conn.callRoutine(routine, args, em.#transactionContext);
1519
+ return em.withSessionContext(em.#transactionContext, ctx => conn.callRoutine(routine, args, ctx));
1398
1520
  }
1399
1521
  /**
1400
1522
  * Fires native delete query. Calling this has no side effects on the context (identity map).
@@ -1405,18 +1527,26 @@ export class EntityManager {
1405
1527
  await em.processUnionWhere(entityName, options, 'delete');
1406
1528
  where = (await em.processWhere(entityName, where, options, 'delete'));
1407
1529
  validateParams(where, 'delete condition');
1408
- const res = await em.driver.nativeDelete(entityName, where, {
1409
- ctx: em.#transactionContext,
1530
+ const res = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.driver.nativeDelete(entityName, where, {
1410
1531
  em,
1411
1532
  ...options,
1412
- });
1533
+ ctx,
1534
+ }));
1413
1535
  return res.affectedRows;
1414
1536
  }
1415
1537
  /**
1416
- * 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.
1417
1540
  */
1418
1541
  map(entityName, result, options = {}) {
1419
- 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;
1420
1550
  const meta = this.metadata.get(entityName);
1421
1551
  const data = (mapped ? result : this.driver.mapResult(result, meta));
1422
1552
  for (const k of Object.keys(data)) {
@@ -1540,7 +1670,7 @@ export class EntityManager {
1540
1670
  if (cached?.data !== undefined) {
1541
1671
  return cached.data;
1542
1672
  }
1543
- 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 }));
1544
1674
  await em.storeCache(options.cache, cached, () => +count);
1545
1675
  return +count;
1546
1676
  }
@@ -1705,6 +1835,18 @@ export class EntityManager {
1705
1835
  }
1706
1836
  fork.#filters = { ...em.#filters };
1707
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
+ }
1708
1850
  fork.loggerContext = Utils.merge({}, em.loggerContext, options.loggerContext);
1709
1851
  fork.#schema = options.schema ?? em.#schema;
1710
1852
  fork.signal = options.signal ?? em.signal;
@@ -2119,8 +2261,16 @@ export class EntityManager {
2119
2261
  // the table name (plus discriminator value for STI) is stable across builds and processes,
2120
2262
  // unlike class names, which minifiers can mangle to the same short name for two entities
2121
2263
  const meta = this.metadata.find(entityName);
2122
- const key = meta?.tableName ? [meta.schema, meta.tableName, meta.discriminatorValue] : Utils.className(entityName);
2123
- return [key, method, opts, where];
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;
2124
2274
  }
2125
2275
  /**
2126
2276
  * @internal
@@ -2131,7 +2281,13 @@ export class EntityManager {
2131
2281
  return undefined;
2132
2282
  }
2133
2283
  const em = this.getContext();
2134
- 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);
2135
2291
  const cached = await em.#resultCache.get(cacheKey);
2136
2292
  if (!cached) {
2137
2293
  return { key: cacheKey, data: cached };
@@ -2179,7 +2335,12 @@ export class EntityManager {
2179
2335
  * ```
2180
2336
  */
2181
2337
  async clearCache(cacheKey) {
2182
- 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
+ }
2183
2344
  }
2184
2345
  /**
2185
2346
  * Returns the default schema of this EntityManager. Respects the context, so global EM will give you the contextual schema
package/MikroORM.d.ts CHANGED
@@ -82,6 +82,10 @@ export declare class MikroORM<Driver extends IDatabaseDriver = IDatabaseDriver,
82
82
  * Closes the database connection.
83
83
  */
84
84
  close(force?: boolean): Promise<void>;
85
+ /**
86
+ * Closes the database connection, allows using the ORM instance with `await using`.
87
+ */
88
+ [Symbol.asyncDispose](): Promise<void>;
85
89
  /**
86
90
  * Gets the `MetadataStorage`.
87
91
  */
package/MikroORM.js CHANGED
@@ -2,6 +2,7 @@ import { MetadataDiscovery } from './metadata/MetadataDiscovery.js';
2
2
  import { MetadataStorage } from './metadata/MetadataStorage.js';
3
3
  import { Configuration } from './utils/Configuration.js';
4
4
  import { loadEnvironmentVars } from './utils/env-vars.js';
5
+ import { clearRlsFilterDefsCache } from './utils/rls-utils.js';
5
6
  import { Utils } from './utils/Utils.js';
6
7
  import { colors } from './logging/colors.js';
7
8
  async function tryRegisterExtension(name, pkg, extensions) {
@@ -159,6 +160,12 @@ export class MikroORM {
159
160
  await this.config.getMetadataCacheAdapter()?.close?.();
160
161
  await this.config.getResultCacheAdapter()?.close?.();
161
162
  }
163
+ /**
164
+ * Closes the database connection, allows using the ORM instance with `await using`.
165
+ */
166
+ async [Symbol.asyncDispose]() {
167
+ await this.close();
168
+ }
162
169
  /**
163
170
  * Gets the `MetadataStorage` (without parameters) or `EntityMetadata` instance when provided with the `entityName` parameter.
164
171
  */
@@ -189,6 +196,8 @@ export class MikroORM {
189
196
  meta.root = this.#metadata.get(meta.root.class);
190
197
  }
191
198
  this.#metadata.decorate(this.em);
199
+ // the newly discovered entities may declare `rls` filters the cached lookup was built without
200
+ clearRlsFilterDefsCache(this.#metadata);
192
201
  }
193
202
  /**
194
203
  * Gets the SchemaGenerator.
package/README.md CHANGED
@@ -24,6 +24,7 @@ npm install @mikro-orm/mysql # MySQL
24
24
  npm install @mikro-orm/mariadb # MariaDB
25
25
  npm install @mikro-orm/sqlite # SQLite
26
26
  npm install @mikro-orm/libsql # libSQL / Turso
27
+ npm install @mikro-orm/sql-js # sql.js (in-memory SQLite in WASM)
27
28
  npm install @mikro-orm/mongodb # MongoDB
28
29
  npm install @mikro-orm/mssql # MS SQL Server
29
30
  npm install @mikro-orm/oracledb # Oracle
@@ -76,7 +76,7 @@ export class FileCacheAdapter {
76
76
  let path = typeof this.#options.combined === 'string' ? this.#options.combined : './metadata.json';
77
77
  path = fs.normalizePath(this.#options.cacheDir, path);
78
78
  this.#options.combined = path; // override in the options, so we can log it from the CLI in `cache:generate` command
79
- writeFileSync(path, JSON.stringify(this.#cache, null, this.#pretty ? 2 : undefined));
79
+ writeFileSync(path, JSON.stringify(this.#cache, null, this.#pretty ? 2 : undefined), { flush: true });
80
80
  return path;
81
81
  }
82
82
  path(name) {
@@ -1,7 +1,7 @@
1
1
  import { type Configuration, type ConnectionOptions } from '../utils/Configuration.js';
2
2
  import type { LogContext, Logger } from '../logging/Logger.js';
3
3
  import type { MetadataStorage } from '../metadata/MetadataStorage.js';
4
- import type { ConnectionType, Dictionary, MaybePromise, Primary, RoutineProperty } from '../typings.js';
4
+ import type { ConnectionType, Dictionary, MaybePromise, Primary, RoutineProperty, SessionContext } from '../typings.js';
5
5
  import type { Routine } from '../metadata/Routine.js';
6
6
  import type { Platform } from '../platforms/Platform.js';
7
7
  import type { Type } from '../types/Type.js';
@@ -51,6 +51,13 @@ export declare abstract class Connection {
51
51
  * This method doesn't support transactions, as opposed to `orm.schema.execute()`, which is used internally.
52
52
  */
53
53
  executeDump(dump: string): Promise<void>;
54
+ /**
55
+ * Returns the underlying database client the connection drives — e.g. the `pg` pool, the
56
+ * `better-sqlite3` database, or the `PGlite` instance — for vendor APIs MikroORM does not wrap.
57
+ * Each driver narrows the return type to its own client. Its lifecycle belongs to the ORM, so
58
+ * leave closing it to `orm.close()` unless you supplied the client yourself via `driverOptions`.
59
+ */
60
+ getNativeClient(): Promise<unknown>;
54
61
  protected onConnect(): Promise<void>;
55
62
  /** Executes a callback inside a transaction, committing on success and rolling back on failure. */
56
63
  transactional<T>(cb: (trx: Transaction) => Promise<T>, options?: {
@@ -59,6 +66,7 @@ export declare abstract class Connection {
59
66
  ctx?: Transaction;
60
67
  eventBroadcaster?: TransactionEventBroadcaster;
61
68
  loggerContext?: LogContext;
69
+ sessionContext?: SessionContext;
62
70
  }): Promise<T>;
63
71
  /** Begins a new database transaction and returns the transaction context. */
64
72
  begin(options?: {
@@ -67,6 +75,7 @@ export declare abstract class Connection {
67
75
  ctx?: Transaction;
68
76
  eventBroadcaster?: TransactionEventBroadcaster;
69
77
  loggerContext?: LogContext;
78
+ sessionContext?: SessionContext;
70
79
  }): Promise<Transaction>;
71
80
  /** Commits the given transaction. */
72
81
  commit(ctx: Transaction, eventBroadcaster?: TransactionEventBroadcaster, loggerContext?: LogContext): Promise<void>;
@@ -65,6 +65,15 @@ export class Connection {
65
65
  async executeDump(dump) {
66
66
  throw new Error(`Executing SQL dumps is not supported by current driver`);
67
67
  }
68
+ /**
69
+ * Returns the underlying database client the connection drives — e.g. the `pg` pool, the
70
+ * `better-sqlite3` database, or the `PGlite` instance — for vendor APIs MikroORM does not wrap.
71
+ * Each driver narrows the return type to its own client. Its lifecycle belongs to the ORM, so
72
+ * leave closing it to `orm.close()` unless you supplied the client yourself via `driverOptions`.
73
+ */
74
+ async getNativeClient() {
75
+ throw new Error(`Accessing the native client is not supported by current driver`);
76
+ }
68
77
  async onConnect() {
69
78
  const schemaGenerator = this.config.getExtension('@mikro-orm/schema-generator');
70
79
  if (this.type === 'write' && schemaGenerator) {