@mikro-orm/core 7.2.0-dev.13 → 7.2.0-dev.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/EntityManager.d.ts +30 -2
- package/EntityManager.js +214 -43
- package/MikroORM.js +3 -0
- package/connections/Connection.d.ts +3 -1
- package/drivers/IDatabaseDriver.d.ts +1 -0
- package/entity/Collection.js +4 -2
- package/entity/EntityFactory.js +6 -0
- package/entity/EntityLoader.js +12 -7
- package/entity/EntityRepository.js +5 -1
- package/entity/defineEntity.d.ts +34 -13
- package/entity/defineEntity.js +1 -1
- package/enums.d.ts +3 -1
- package/errors.d.ts +32 -0
- package/errors.js +75 -0
- package/events/EventManager.js +6 -3
- package/exceptions.d.ts +5 -0
- package/exceptions.js +5 -0
- package/hydration/ObjectHydrator.d.ts +2 -0
- package/hydration/ObjectHydrator.js +12 -8
- package/index.d.ts +1 -1
- package/metadata/MetadataDiscovery.d.ts +1 -0
- package/metadata/MetadataDiscovery.js +70 -11
- package/metadata/MetadataStorage.js +17 -1
- package/metadata/types.d.ts +5 -1
- package/package.json +1 -1
- package/platforms/Platform.d.ts +12 -2
- package/platforms/Platform.js +43 -1
- package/types/Type.js +4 -4
- package/typings.d.ts +44 -2
- package/typings.js +24 -1
- package/unit-of-work/ChangeSetPersister.js +17 -13
- package/unit-of-work/UnitOfWork.js +11 -4
- package/utils/Configuration.d.ts +15 -1
- package/utils/Configuration.js +11 -1
- package/utils/DataloaderUtils.js +2 -1
- package/utils/EntityComparator.d.ts +2 -0
- package/utils/EntityComparator.js +7 -3
- package/utils/QueryHelper.d.ts +12 -0
- package/utils/QueryHelper.js +75 -4
- package/utils/TransactionManager.js +1 -1
- package/utils/Utils.d.ts +14 -2
- package/utils/Utils.js +24 -2
- package/utils/env-vars.js +1 -0
- package/utils/index.d.ts +1 -0
- package/utils/index.js +1 -0
- package/utils/rls-utils.d.ts +35 -0
- package/utils/rls-utils.js +97 -0
- package/utils/upsert-utils.d.ts +9 -1
- package/utils/upsert-utils.js +26 -3
package/EntityManager.d.ts
CHANGED
|
@@ -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
|
*/
|
|
@@ -700,6 +726,8 @@ export interface ForkOptions {
|
|
|
700
726
|
keepTransactionContext?: boolean;
|
|
701
727
|
/** default schema to use for this fork */
|
|
702
728
|
schema?: string;
|
|
729
|
+
/** database session context (row level security) for this fork; inherited from the parent when not set */
|
|
730
|
+
session?: SessionContext;
|
|
703
731
|
/** default logger context, can be overridden via {@apilink FindOptions} */
|
|
704
732
|
loggerContext?: Dictionary;
|
|
705
733
|
/**
|
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;
|
|
@@ -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);
|
|
@@ -260,7 +267,15 @@ export class EntityManager {
|
|
|
260
267
|
addFilter(options) {
|
|
261
268
|
options = { ...options };
|
|
262
269
|
if (options.entity) {
|
|
263
|
-
options.entity = Utils.asArray(options.entity).map(n => Utils.
|
|
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);
|
|
264
279
|
}
|
|
265
280
|
options.default ??= true;
|
|
266
281
|
this.getContext(false).#filters[options.name] = options;
|
|
@@ -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()
|
|
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
|
*/
|
|
@@ -793,6 +922,7 @@ export class EntityManager {
|
|
|
793
922
|
}
|
|
794
923
|
const meta = this.metadata.get(entityName);
|
|
795
924
|
const convertCustomTypes = !Utils.isEntity(data);
|
|
925
|
+
let generatedFields = [];
|
|
796
926
|
if (Utils.isEntity(data)) {
|
|
797
927
|
entity = data;
|
|
798
928
|
if (helper(entity).__managed && helper(entity).__em === em && !this.config.get('upsertManaged')) {
|
|
@@ -800,6 +930,7 @@ export class EntityManager {
|
|
|
800
930
|
return entity;
|
|
801
931
|
}
|
|
802
932
|
where = helper(entity).getPrimaryKey();
|
|
933
|
+
generatedFields = getOnCreateGeneratedFields(meta, entity);
|
|
803
934
|
em.#entityFactory.assignDefaultValues(entity, meta);
|
|
804
935
|
data = em.#comparator.prepareEntity(entity);
|
|
805
936
|
}
|
|
@@ -812,6 +943,7 @@ export class EntityManager {
|
|
|
812
943
|
return em.assign(exists, data);
|
|
813
944
|
}
|
|
814
945
|
}
|
|
946
|
+
generatedFields = getOnCreateGeneratedFields(meta, data);
|
|
815
947
|
em.#entityFactory.assignDefaultValues(data, meta, true);
|
|
816
948
|
for (const key of Object.keys(data)) {
|
|
817
949
|
const prop = meta.properties[key];
|
|
@@ -820,6 +952,10 @@ export class EntityManager {
|
|
|
820
952
|
}
|
|
821
953
|
}
|
|
822
954
|
}
|
|
955
|
+
// `onCreate` generated values are for the insert clause only, they must not overwrite an existing row
|
|
956
|
+
if (generatedFields.length > 0 && !options.onConflictMergeFields) {
|
|
957
|
+
options.onConflictExcludeFields = [...(options.onConflictExcludeFields ?? []), ...generatedFields];
|
|
958
|
+
}
|
|
823
959
|
where = getWhereCondition(meta, options.onConflictFields, data, where).where;
|
|
824
960
|
data = QueryHelper.processObjectParams(data);
|
|
825
961
|
validateParams(data, 'insert data');
|
|
@@ -829,12 +965,12 @@ export class EntityManager {
|
|
|
829
965
|
data = em.#comparator.prepareEntity(entity);
|
|
830
966
|
}
|
|
831
967
|
}
|
|
832
|
-
const ret = await em.driver.nativeUpdate(entityName, where, data, {
|
|
833
|
-
ctx: em.#transactionContext,
|
|
968
|
+
const ret = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.driver.nativeUpdate(entityName, where, data, {
|
|
834
969
|
upsert: true,
|
|
835
970
|
convertCustomTypes,
|
|
836
971
|
...options,
|
|
837
|
-
|
|
972
|
+
ctx,
|
|
973
|
+
}));
|
|
838
974
|
em.#unitOfWork.getChangeSetPersister().mapReturnedValues(entity, data, ret.row, meta, true);
|
|
839
975
|
entity ??= em.#entityFactory.create(entityName, data, {
|
|
840
976
|
refresh: true,
|
|
@@ -866,13 +1002,13 @@ export class EntityManager {
|
|
|
866
1002
|
where[meta.primaryKeys[0]] = ret.insertId;
|
|
867
1003
|
}
|
|
868
1004
|
}
|
|
869
|
-
const data2 = await this.driver.findOne(meta.class, where, {
|
|
1005
|
+
const data2 = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => this.driver.findOne(meta.class, where, {
|
|
870
1006
|
fields: returning.concat(...(options.onConflictMergeFields ?? [])),
|
|
871
|
-
ctx
|
|
1007
|
+
ctx,
|
|
872
1008
|
convertCustomTypes: true,
|
|
873
1009
|
connectionType: 'write',
|
|
874
1010
|
schema: options.schema,
|
|
875
|
-
});
|
|
1011
|
+
}));
|
|
876
1012
|
em.getHydrator().hydrate(entity, meta, data2, em.#entityFactory, 'full', false, true);
|
|
877
1013
|
}
|
|
878
1014
|
// recompute the data as there might be some values missing (e.g. those with db column defaults)
|
|
@@ -939,6 +1075,7 @@ export class EntityManager {
|
|
|
939
1075
|
}
|
|
940
1076
|
const meta = this.metadata.get(entityName);
|
|
941
1077
|
const convertCustomTypes = !Utils.isEntity(data[0]);
|
|
1078
|
+
const generatedFields = new Set();
|
|
942
1079
|
const allData = [];
|
|
943
1080
|
const allWhere = [];
|
|
944
1081
|
const entities = new Map();
|
|
@@ -956,6 +1093,7 @@ export class EntityManager {
|
|
|
956
1093
|
continue;
|
|
957
1094
|
}
|
|
958
1095
|
where = helper(entity).getPrimaryKey();
|
|
1096
|
+
getOnCreateGeneratedFields(meta, entity).forEach(field => generatedFields.add(field));
|
|
959
1097
|
em.#entityFactory.assignDefaultValues(entity, meta);
|
|
960
1098
|
entitiesByAllDataIdx.set(allData.length, entity);
|
|
961
1099
|
row = em.#comparator.prepareEntity(entity);
|
|
@@ -972,6 +1110,7 @@ export class EntityManager {
|
|
|
972
1110
|
continue;
|
|
973
1111
|
}
|
|
974
1112
|
}
|
|
1113
|
+
getOnCreateGeneratedFields(meta, row).forEach(field => generatedFields.add(field));
|
|
975
1114
|
em.#entityFactory.assignDefaultValues(row, meta, true);
|
|
976
1115
|
for (const key of Object.keys(row)) {
|
|
977
1116
|
const prop = meta.properties[key];
|
|
@@ -1000,6 +1139,10 @@ export class EntityManager {
|
|
|
1000
1139
|
if (entities.size === data.length) {
|
|
1001
1140
|
return [...entities.keys()];
|
|
1002
1141
|
}
|
|
1142
|
+
// `onCreate` generated values are for the insert clause only, they must not overwrite existing rows
|
|
1143
|
+
if (generatedFields.size > 0 && !options.onConflictMergeFields) {
|
|
1144
|
+
options.onConflictExcludeFields = [...(options.onConflictExcludeFields ?? []), ...generatedFields];
|
|
1145
|
+
}
|
|
1003
1146
|
if (em.eventManager.hasListeners(EventType.beforeUpsert, meta)) {
|
|
1004
1147
|
for (const dto of data) {
|
|
1005
1148
|
const entity = entitiesByData.get(dto) ?? dto;
|
|
@@ -1009,12 +1152,12 @@ export class EntityManager {
|
|
|
1009
1152
|
allData[idx] = em.#comparator.prepareEntity(entity);
|
|
1010
1153
|
}
|
|
1011
1154
|
}
|
|
1012
|
-
const res = await em.driver.nativeUpdateMany(entityName, allWhere, allData, {
|
|
1013
|
-
ctx: em.#transactionContext,
|
|
1155
|
+
const res = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.driver.nativeUpdateMany(entityName, allWhere, allData, {
|
|
1014
1156
|
upsert: true,
|
|
1015
1157
|
convertCustomTypes,
|
|
1016
1158
|
...options,
|
|
1017
|
-
|
|
1159
|
+
ctx,
|
|
1160
|
+
}));
|
|
1018
1161
|
entities.clear();
|
|
1019
1162
|
entitiesByData.clear();
|
|
1020
1163
|
const loadPK = new Map();
|
|
@@ -1057,16 +1200,16 @@ export class EntityManager {
|
|
|
1057
1200
|
where.$or[idx][prop] = item[prop];
|
|
1058
1201
|
});
|
|
1059
1202
|
});
|
|
1060
|
-
const data2 = await this.driver.find(meta.class, where, {
|
|
1203
|
+
const data2 = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => this.driver.find(meta.class, where, {
|
|
1061
1204
|
fields: returning
|
|
1062
1205
|
.concat(...add)
|
|
1063
1206
|
.concat(...(Array.isArray(uniqueFields) ? uniqueFields : []))
|
|
1064
1207
|
.concat(...(options.onConflictMergeFields ?? [])),
|
|
1065
|
-
ctx
|
|
1208
|
+
ctx,
|
|
1066
1209
|
convertCustomTypes: true,
|
|
1067
1210
|
connectionType: 'write',
|
|
1068
1211
|
schema: options.schema,
|
|
1069
|
-
});
|
|
1212
|
+
}));
|
|
1070
1213
|
for (const [entity, cond] of loadPK.entries()) {
|
|
1071
1214
|
const row = data2.find(row => {
|
|
1072
1215
|
const tmp = {};
|
|
@@ -1164,6 +1307,7 @@ export class EntityManager {
|
|
|
1164
1307
|
}
|
|
1165
1308
|
const em = this.getContext(false);
|
|
1166
1309
|
em.#transactionContext = await em.getConnection('write').begin({
|
|
1310
|
+
sessionContext: em.getTransactionSessionContext(),
|
|
1167
1311
|
...options,
|
|
1168
1312
|
eventBroadcaster: new TransactionEventBroadcaster(em, { topLevelTransaction: !options.ctx }),
|
|
1169
1313
|
});
|
|
@@ -1231,15 +1375,12 @@ export class EntityManager {
|
|
|
1231
1375
|
const meta = helper(data).__meta;
|
|
1232
1376
|
const payload = em.#comparator.prepareEntity(data);
|
|
1233
1377
|
const cs = new ChangeSet(data, ChangeSetType.CREATE, payload, meta);
|
|
1234
|
-
await em.#unitOfWork.getChangeSetPersister().executeInserts([cs], {
|
|
1378
|
+
await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.#unitOfWork.getChangeSetPersister().executeInserts([cs], { ...options, ctx }));
|
|
1235
1379
|
return cs.getPrimaryKey();
|
|
1236
1380
|
}
|
|
1237
1381
|
data = QueryHelper.processObjectParams(data);
|
|
1238
1382
|
validateParams(data, 'insert data');
|
|
1239
|
-
const res = await em.driver.nativeInsert(entityName, data, {
|
|
1240
|
-
ctx: em.#transactionContext,
|
|
1241
|
-
...options,
|
|
1242
|
-
});
|
|
1383
|
+
const res = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.driver.nativeInsert(entityName, data, { ...options, ctx }));
|
|
1243
1384
|
return res.insertId;
|
|
1244
1385
|
}
|
|
1245
1386
|
/**
|
|
@@ -1281,10 +1422,7 @@ export class EntityManager {
|
|
|
1281
1422
|
options ??= {};
|
|
1282
1423
|
options = em.prepareOptions(options);
|
|
1283
1424
|
const meta = em.metadata.get(entityName);
|
|
1284
|
-
const res = await em.driver.nativeClone(entityName, where, overrides, {
|
|
1285
|
-
ctx: em.#transactionContext,
|
|
1286
|
-
...options,
|
|
1287
|
-
});
|
|
1425
|
+
const res = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.driver.nativeClone(entityName, where, overrides, { ...options, ctx }));
|
|
1288
1426
|
const pk = res.insertId ?? res.row?.[meta.primaryKeys[0]];
|
|
1289
1427
|
return em.findOneOrFail(entityName, pk, {
|
|
1290
1428
|
schema: options.schema,
|
|
@@ -1319,15 +1457,12 @@ export class EntityManager {
|
|
|
1319
1457
|
const payload = em.#comparator.prepareEntity(row);
|
|
1320
1458
|
return new ChangeSet(row, ChangeSetType.CREATE, payload, meta);
|
|
1321
1459
|
});
|
|
1322
|
-
await em.#unitOfWork.getChangeSetPersister().executeInserts(css, {
|
|
1460
|
+
await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.#unitOfWork.getChangeSetPersister().executeInserts(css, { ...options, ctx }));
|
|
1323
1461
|
return css.map(cs => cs.getPrimaryKey());
|
|
1324
1462
|
}
|
|
1325
1463
|
data = data.map(row => QueryHelper.processObjectParams(row));
|
|
1326
1464
|
data.forEach(row => validateParams(row, 'insert data'));
|
|
1327
|
-
const res = await em.driver.nativeInsertMany(entityName, data, {
|
|
1328
|
-
ctx: em.#transactionContext,
|
|
1329
|
-
...options,
|
|
1330
|
-
});
|
|
1465
|
+
const res = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.driver.nativeInsertMany(entityName, data, { ...options, ctx }));
|
|
1331
1466
|
if (res.insertedIds) {
|
|
1332
1467
|
return res.insertedIds;
|
|
1333
1468
|
}
|
|
@@ -1344,11 +1479,11 @@ export class EntityManager {
|
|
|
1344
1479
|
where = await em.processWhere(entityName, where, { ...options, convertCustomTypes: false }, 'update');
|
|
1345
1480
|
validateParams(data, 'update data');
|
|
1346
1481
|
validateParams(where, 'update condition');
|
|
1347
|
-
const res = await em.driver.nativeUpdate(entityName, where, data, {
|
|
1348
|
-
ctx: em.#transactionContext,
|
|
1482
|
+
const res = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.driver.nativeUpdate(entityName, where, data, {
|
|
1349
1483
|
em,
|
|
1350
1484
|
...options,
|
|
1351
|
-
|
|
1485
|
+
ctx,
|
|
1486
|
+
}));
|
|
1352
1487
|
return res.affectedRows;
|
|
1353
1488
|
}
|
|
1354
1489
|
/**
|
|
@@ -1380,7 +1515,7 @@ export class EntityManager {
|
|
|
1380
1515
|
throw new Error(`Routine '${routine.name}' is not registered in the 'routines' config option.`);
|
|
1381
1516
|
}
|
|
1382
1517
|
const conn = em.driver.getConnection('write');
|
|
1383
|
-
return conn.callRoutine(routine, args,
|
|
1518
|
+
return em.withSessionContext(em.#transactionContext, ctx => conn.callRoutine(routine, args, ctx));
|
|
1384
1519
|
}
|
|
1385
1520
|
/**
|
|
1386
1521
|
* Fires native delete query. Calling this has no side effects on the context (identity map).
|
|
@@ -1391,11 +1526,11 @@ export class EntityManager {
|
|
|
1391
1526
|
await em.processUnionWhere(entityName, options, 'delete');
|
|
1392
1527
|
where = (await em.processWhere(entityName, where, options, 'delete'));
|
|
1393
1528
|
validateParams(where, 'delete condition');
|
|
1394
|
-
const res = await em.driver.nativeDelete(entityName, where, {
|
|
1395
|
-
ctx: em.#transactionContext,
|
|
1529
|
+
const res = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.driver.nativeDelete(entityName, where, {
|
|
1396
1530
|
em,
|
|
1397
1531
|
...options,
|
|
1398
|
-
|
|
1532
|
+
ctx,
|
|
1533
|
+
}));
|
|
1399
1534
|
return res.affectedRows;
|
|
1400
1535
|
}
|
|
1401
1536
|
/**
|
|
@@ -1534,7 +1669,7 @@ export class EntityManager {
|
|
|
1534
1669
|
if (cached?.data !== undefined) {
|
|
1535
1670
|
return cached.data;
|
|
1536
1671
|
}
|
|
1537
|
-
const count = await em.driver.count(entityName, where, {
|
|
1672
|
+
const count = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.driver.count(entityName, where, { em, ...options, ctx }));
|
|
1538
1673
|
await em.storeCache(options.cache, cached, () => +count);
|
|
1539
1674
|
return +count;
|
|
1540
1675
|
}
|
|
@@ -1649,8 +1784,9 @@ export class EntityManager {
|
|
|
1649
1784
|
}
|
|
1650
1785
|
// For TPT inheritance, check the entity's own properties, not just the root's
|
|
1651
1786
|
// For STI, meta.properties includes all properties anyway
|
|
1652
|
-
|
|
1653
|
-
|
|
1787
|
+
// use an own-property check so inherited `Object.prototype` members (e.g. `__proto__`, `constructor`) are not treated as populatable
|
|
1788
|
+
const ret = Object.hasOwn(meta.properties, p);
|
|
1789
|
+
if (ret && parts.length > 0) {
|
|
1654
1790
|
return this.canPopulate(meta.properties[p].targetMeta.class, parts.join('.'));
|
|
1655
1791
|
}
|
|
1656
1792
|
return ret;
|
|
@@ -1698,6 +1834,18 @@ export class EntityManager {
|
|
|
1698
1834
|
}
|
|
1699
1835
|
fork.#filters = { ...em.#filters };
|
|
1700
1836
|
fork.#filterParams = Utils.copy(em.#filterParams);
|
|
1837
|
+
if (options.session) {
|
|
1838
|
+
// the fork's session replaces the parent context, but the copied `rls` filter params must stay consistent with
|
|
1839
|
+
// it — re-stage their variables underneath the explicit `session.variables`, which win on any conflict
|
|
1840
|
+
const variables = {};
|
|
1841
|
+
for (const name of Object.keys(fork.#filterParams)) {
|
|
1842
|
+
Object.assign(variables, computeRlsFilterVariables(findRlsFilterDefs(fork.metadata, name), fork.#filterParams[name]));
|
|
1843
|
+
}
|
|
1844
|
+
fork.mergeSessionContext({ ...options.session, variables: { ...variables, ...options.session.variables } });
|
|
1845
|
+
}
|
|
1846
|
+
else if (em.#sessionContext) {
|
|
1847
|
+
fork.#sessionContext = Utils.copy(em.#sessionContext);
|
|
1848
|
+
}
|
|
1701
1849
|
fork.loggerContext = Utils.merge({}, em.loggerContext, options.loggerContext);
|
|
1702
1850
|
fork.#schema = options.schema ?? em.#schema;
|
|
1703
1851
|
fork.signal = options.signal ?? em.signal;
|
|
@@ -2109,7 +2257,19 @@ export class EntityManager {
|
|
|
2109
2257
|
]) {
|
|
2110
2258
|
delete opts[k];
|
|
2111
2259
|
}
|
|
2112
|
-
|
|
2260
|
+
// the table name (plus discriminator value for STI) is stable across builds and processes,
|
|
2261
|
+
// unlike class names, which minifiers can mangle to the same short name for two entities
|
|
2262
|
+
const meta = this.metadata.find(entityName);
|
|
2263
|
+
const entityKey = meta?.tableName
|
|
2264
|
+
? [meta.schema, meta.tableName, meta.discriminatorValue]
|
|
2265
|
+
: Utils.className(entityName);
|
|
2266
|
+
const key = [entityKey, method, opts, where];
|
|
2267
|
+
// session context (row level security) scopes cached rows per tenant/role, avoiding cross-context serves
|
|
2268
|
+
const sessionContext = this.getContext(false).#sessionContext;
|
|
2269
|
+
if (sessionContext) {
|
|
2270
|
+
key.push(sessionContext);
|
|
2271
|
+
}
|
|
2272
|
+
return key;
|
|
2113
2273
|
}
|
|
2114
2274
|
/**
|
|
2115
2275
|
* @internal
|
|
@@ -2120,7 +2280,13 @@ export class EntityManager {
|
|
|
2120
2280
|
return undefined;
|
|
2121
2281
|
}
|
|
2122
2282
|
const em = this.getContext();
|
|
2123
|
-
|
|
2283
|
+
// a named cache key (`config[0]`) discards the computed `key`, which already carries the session context — so
|
|
2284
|
+
// scope it here too, otherwise a fork's rows would be served to another session context under the same name
|
|
2285
|
+
const cacheKey = Array.isArray(config)
|
|
2286
|
+
? em.#sessionContext
|
|
2287
|
+
? `${config[0]}|${JSON.stringify(em.#sessionContext)}`
|
|
2288
|
+
: config[0]
|
|
2289
|
+
: JSON.stringify(key);
|
|
2124
2290
|
const cached = await em.#resultCache.get(cacheKey);
|
|
2125
2291
|
if (!cached) {
|
|
2126
2292
|
return { key: cacheKey, data: cached };
|
|
@@ -2168,7 +2334,12 @@ export class EntityManager {
|
|
|
2168
2334
|
* ```
|
|
2169
2335
|
*/
|
|
2170
2336
|
async clearCache(cacheKey) {
|
|
2171
|
-
|
|
2337
|
+
const em = this.getContext();
|
|
2338
|
+
await em.#resultCache.remove(cacheKey);
|
|
2339
|
+
// named keys are scoped by the session context (see `tryCache`), so clear this context's variant too
|
|
2340
|
+
if (em.#sessionContext) {
|
|
2341
|
+
await em.#resultCache.remove(`${cacheKey}|${JSON.stringify(em.#sessionContext)}`);
|
|
2342
|
+
}
|
|
2172
2343
|
}
|
|
2173
2344
|
/**
|
|
2174
2345
|
* Returns the default schema of this EntityManager. Respects the context, so global EM will give you the contextual schema
|
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) {
|
|
@@ -195,6 +196,8 @@ export class MikroORM {
|
|
|
195
196
|
meta.root = this.#metadata.get(meta.root.class);
|
|
196
197
|
}
|
|
197
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);
|
|
198
201
|
}
|
|
199
202
|
/**
|
|
200
203
|
* Gets the SchemaGenerator.
|
|
@@ -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';
|
|
@@ -66,6 +66,7 @@ export declare abstract class Connection {
|
|
|
66
66
|
ctx?: Transaction;
|
|
67
67
|
eventBroadcaster?: TransactionEventBroadcaster;
|
|
68
68
|
loggerContext?: LogContext;
|
|
69
|
+
sessionContext?: SessionContext;
|
|
69
70
|
}): Promise<T>;
|
|
70
71
|
/** Begins a new database transaction and returns the transaction context. */
|
|
71
72
|
begin(options?: {
|
|
@@ -74,6 +75,7 @@ export declare abstract class Connection {
|
|
|
74
75
|
ctx?: Transaction;
|
|
75
76
|
eventBroadcaster?: TransactionEventBroadcaster;
|
|
76
77
|
loggerContext?: LogContext;
|
|
78
|
+
sessionContext?: SessionContext;
|
|
77
79
|
}): Promise<Transaction>;
|
|
78
80
|
/** Commits the given transaction. */
|
|
79
81
|
commit(ctx: Transaction, eventBroadcaster?: TransactionEventBroadcaster, loggerContext?: LogContext): Promise<void>;
|
|
@@ -345,6 +345,7 @@ export interface CountByOptions<T extends object> {
|
|
|
345
345
|
filters?: FilterOptions;
|
|
346
346
|
having?: FilterQuery<T>;
|
|
347
347
|
schema?: string;
|
|
348
|
+
connectionType?: ConnectionType;
|
|
348
349
|
flushMode?: FlushMode | `${FlushMode}`;
|
|
349
350
|
loggerContext?: LogContext;
|
|
350
351
|
logging?: LoggingOptions;
|
package/entity/Collection.js
CHANGED
|
@@ -117,9 +117,11 @@ export class Collection {
|
|
|
117
117
|
opts.orderBy = QueryHelper.mergeOrderBy(opts.orderBy, this.property.orderBy, this.property.targetMeta?.orderBy);
|
|
118
118
|
options.populate = (await em.preparePopulate(this.property.targetMeta.class, options));
|
|
119
119
|
const cond = (await em.applyFilters(this.property.targetMeta.class, where, options.filters ?? {}, 'read'));
|
|
120
|
-
|
|
120
|
+
// fall back to the ambient transaction context, or `withSessionContext` would wrap the pivot load in a
|
|
121
|
+
// second concurrent transaction (a deadlock with a single-connection pool) instead of joining the open one
|
|
122
|
+
const map = await em.withSessionContext(ctx ?? em.getTransactionContext(), trx => em
|
|
121
123
|
.getDriver()
|
|
122
|
-
.loadFromPivotTable(this.property, [helper(this.owner).__primaryKeys], cond, opts.orderBy,
|
|
124
|
+
.loadFromPivotTable(this.property, [helper(this.owner).__primaryKeys], cond, opts.orderBy, trx, options));
|
|
123
125
|
items = map[helper(this.owner).getSerializedPrimaryKey()].map((item) => em.merge(this.property.targetMeta.class, item, { convertCustomTypes: true }));
|
|
124
126
|
await em.populate(items, options.populate, options);
|
|
125
127
|
}
|
package/entity/EntityFactory.js
CHANGED
|
@@ -77,6 +77,12 @@ export class EntityFactory {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
data = { ...data };
|
|
80
|
+
if (options.newEntity && meta2.root.inheritanceType === 'sti' && meta2.discriminatorValue != null) {
|
|
81
|
+
const prop = meta2.properties[meta2.root.discriminatorColumn];
|
|
82
|
+
if (prop && prop.userDefined !== false) {
|
|
83
|
+
data[prop.name] ??= meta2.discriminatorValue;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
80
86
|
const entity = exists ?? this.createEntity(data, meta2, options);
|
|
81
87
|
wrapped = helper(entity);
|
|
82
88
|
wrapped.__processing = true;
|