@mikro-orm/core 7.2.0-dev.2 → 7.2.0-dev.21
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 +42 -8
- package/EntityManager.js +256 -70
- package/MikroORM.d.ts +4 -0
- package/MikroORM.js +9 -0
- package/cache/CacheAdapter.d.ts +6 -4
- package/cache/FileCacheAdapter.d.ts +1 -1
- package/cache/FileCacheAdapter.js +7 -2
- package/connections/Connection.d.ts +10 -1
- package/connections/Connection.js +9 -0
- package/drivers/DatabaseDriver.d.ts +17 -1
- package/drivers/DatabaseDriver.js +203 -41
- package/drivers/IDatabaseDriver.d.ts +1 -0
- package/entity/Collection.js +4 -2
- package/entity/EntityFactory.js +6 -0
- package/entity/EntityLoader.d.ts +7 -1
- package/entity/EntityLoader.js +46 -11
- package/entity/EntityRepository.d.ts +4 -5
- package/entity/EntityRepository.js +7 -2
- package/entity/defineEntity.d.ts +48 -14
- package/entity/defineEntity.js +32 -1
- package/enums.d.ts +5 -1
- package/enums.js +2 -0
- package/errors.d.ts +36 -0
- package/errors.js +90 -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 +15 -8
- package/index.d.ts +1 -1
- package/metadata/EntitySchema.js +5 -2
- package/metadata/MetadataDiscovery.d.ts +3 -0
- package/metadata/MetadataDiscovery.js +161 -28
- package/metadata/MetadataProvider.js +1 -1
- package/metadata/MetadataStorage.d.ts +4 -3
- package/metadata/MetadataStorage.js +32 -2
- package/metadata/Routine.js +4 -1
- package/metadata/types.d.ts +19 -3
- package/naming-strategy/AbstractNamingStrategy.js +2 -1
- package/naming-strategy/NamingStrategy.d.ts +2 -1
- package/package.json +1 -1
- package/platforms/Platform.d.ts +24 -3
- package/platforms/Platform.js +63 -1
- package/types/BigIntType.d.ts +1 -0
- package/types/BigIntType.js +23 -0
- package/types/DateTimeType.d.ts +1 -0
- package/types/DateTimeType.js +8 -0
- package/types/StringType.d.ts +14 -3
- package/types/StringType.js +34 -4
- package/types/TextType.d.ts +2 -4
- package/types/TextType.js +2 -8
- package/types/Type.d.ts +11 -0
- package/types/Type.js +4 -4
- package/types/index.d.ts +2 -2
- package/typings.d.ts +58 -2
- package/typings.js +24 -1
- package/unit-of-work/ChangeSet.js +10 -7
- package/unit-of-work/ChangeSetPersister.js +32 -20
- package/unit-of-work/UnitOfWork.js +11 -4
- package/utils/AbstractMigrator.d.ts +1 -1
- package/utils/AbstractMigrator.js +3 -2
- package/utils/Configuration.d.ts +15 -1
- package/utils/Configuration.js +13 -2
- package/utils/Cursor.d.ts +2 -0
- package/utils/Cursor.js +44 -39
- package/utils/DataloaderUtils.js +2 -1
- package/utils/EntityComparator.d.ts +2 -0
- package/utils/EntityComparator.js +11 -4
- package/utils/QueryHelper.d.ts +17 -0
- package/utils/QueryHelper.js +100 -4
- package/utils/RawQueryFragment.d.ts +6 -0
- package/utils/RawQueryFragment.js +15 -6
- package/utils/RequestContext.d.ts +2 -2
- package/utils/RequestContext.js +11 -2
- package/utils/TransactionManager.d.ts +6 -0
- package/utils/TransactionManager.js +36 -3
- package/utils/Utils.d.ts +14 -2
- package/utils/Utils.js +36 -6
- package/utils/clone.js +6 -0
- 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/utils/QueryHelper.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Reference } from '../entity/Reference.js';
|
|
2
|
-
import { Utils } from './Utils.js';
|
|
2
|
+
import { DANGEROUS_PROPERTY_NAMES, Utils } from './Utils.js';
|
|
3
3
|
import { ARRAY_OPERATORS, GroupOperator, JSON_KEY_OPERATORS, ReferenceKind } from '../enums.js';
|
|
4
4
|
import { JsonType } from '../types/JsonType.js';
|
|
5
5
|
import { helper } from '../entity/wrap.js';
|
|
6
6
|
import { isRaw, Raw } from './RawQueryFragment.js';
|
|
7
|
+
import { MetadataError } from '../errors.js';
|
|
7
8
|
/** @internal */
|
|
8
9
|
export class QueryHelper {
|
|
9
10
|
static SUPPORTED_OPERATORS = ['>', '<', '<=', '>=', '!', '!='];
|
|
@@ -227,10 +228,21 @@ export class QueryHelper {
|
|
|
227
228
|
if (prop?.customType && convertCustomTypes && !isRaw(value)) {
|
|
228
229
|
value = QueryHelper.processCustomType(prop, value, platform, undefined, true);
|
|
229
230
|
}
|
|
231
|
+
else if (!prop &&
|
|
232
|
+
meta?.compositePK &&
|
|
233
|
+
convertCustomTypes &&
|
|
234
|
+
Array.isArray(value) &&
|
|
235
|
+
key === Utils.getPrimaryKeyHash(meta.primaryKeys)) {
|
|
236
|
+
value = QueryHelper.processCompositeCustomTypes(value, meta, platform);
|
|
237
|
+
}
|
|
230
238
|
// oxfmt-ignore
|
|
231
239
|
const isJsonProperty = prop?.customType instanceof JsonType && !isRaw(value) && (Utils.isPlainObject(value) ? !['$eq', '$elemMatch'].includes(Object.keys(value)[0]) : !Array.isArray(value));
|
|
232
240
|
if (isJsonProperty && prop?.kind !== ReferenceKind.EMBEDDED) {
|
|
233
|
-
|
|
241
|
+
// an explicit alias prefix (e.g. `a.meta`) has to survive, otherwise the condition falls back to the root alias
|
|
242
|
+
const explicitAlias = key.includes('.')
|
|
243
|
+
? key.split('.').slice(0, -1).join('.')
|
|
244
|
+
: undefined;
|
|
245
|
+
return this.processJsonCondition(o, value, [prop.fieldNames[0]], platform, aliased && explicitAlias != null ? explicitAlias : aliased);
|
|
234
246
|
}
|
|
235
247
|
// oxfmt-ignore
|
|
236
248
|
if (Array.isArray(value) && !Utils.isOperator(key) && !QueryHelper.isSupportedOperator(key) && !(customExpression && Raw.getKnownFragment(key).params.length > 0) && options.type !== 'orderBy') {
|
|
@@ -262,7 +274,11 @@ export class QueryHelper {
|
|
|
262
274
|
options.forEach(filter => (opts[filter] = true));
|
|
263
275
|
}
|
|
264
276
|
else if (Utils.isPlainObject(options)) {
|
|
265
|
-
Object.keys(options).forEach(filter =>
|
|
277
|
+
Object.keys(options).forEach(filter => {
|
|
278
|
+
if (!DANGEROUS_PROPERTY_NAMES.includes(filter)) {
|
|
279
|
+
opts[filter] = options[filter];
|
|
280
|
+
}
|
|
281
|
+
});
|
|
266
282
|
}
|
|
267
283
|
return Object.keys(filters)
|
|
268
284
|
.filter(f => QueryHelper.isFilterActive(meta, f, filters[f], opts))
|
|
@@ -271,6 +287,68 @@ export class QueryHelper {
|
|
|
271
287
|
return filters[f];
|
|
272
288
|
});
|
|
273
289
|
}
|
|
290
|
+
/** @internal Sentinel wrapping for arguments accessed while statically resolving an `rls` filter condition. */
|
|
291
|
+
static RLS_SENTINEL_PREFIX = '__mikro_rls_arg__';
|
|
292
|
+
/** @internal */
|
|
293
|
+
static RLS_SENTINEL_SUFFIX = '__';
|
|
294
|
+
/**
|
|
295
|
+
* Resolves an `rls` filter's condition to a static `FilterQuery`. Function conditions are called with a proxy `args`
|
|
296
|
+
* that yields a unique sentinel per accessed argument, real `type`/`entityName` strings (validated to not affect the
|
|
297
|
+
* result), and a poison proxy or `undefined` for the remaining runtime-only parameters.
|
|
298
|
+
*
|
|
299
|
+
* @internal
|
|
300
|
+
*/
|
|
301
|
+
static resolveRlsFilterCond(filter, accessed, entityName) {
|
|
302
|
+
if (!(filter.cond instanceof Function)) {
|
|
303
|
+
return filter.cond;
|
|
304
|
+
}
|
|
305
|
+
const args = new Proxy({}, {
|
|
306
|
+
get: (_target, prop) => {
|
|
307
|
+
if (typeof prop === 'symbol') {
|
|
308
|
+
// e.g. coercing `args` itself in a template literal triggers a `Symbol.toPrimitive` lookup
|
|
309
|
+
throw MetadataError.rlsFilterUnsupportedCond(filter.name);
|
|
310
|
+
}
|
|
311
|
+
accessed.add(prop);
|
|
312
|
+
return `${this.RLS_SENTINEL_PREFIX}${prop}${this.RLS_SENTINEL_SUFFIX}`;
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
const poison = new Proxy({}, {
|
|
316
|
+
get: () => {
|
|
317
|
+
throw MetadataError.rlsFilterDependsOnRuntimeState(filter.name);
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
// property access on the poison proxies throws, but equality/truthiness checks (`type === 'read'`,
|
|
321
|
+
// `entityName === 'X'`, `options ? a : b`) cannot be trapped — vary all three across the evaluations and require
|
|
322
|
+
// identical results, so a command-, entity-, or options-dependent condition cannot silently compile one branch.
|
|
323
|
+
// `entityName` uses the real class name (plus two derived-distinct variants) so an `=== '<name>'` check diverges;
|
|
324
|
+
// `options` alternates the poison proxy and `undefined` so a truthiness check flips. `em` stays poison throughout.
|
|
325
|
+
const name = entityName ?? `${this.RLS_SENTINEL_PREFIX}entity${this.RLS_SENTINEL_SUFFIX}`;
|
|
326
|
+
const evaluate = (type, entity, options) => {
|
|
327
|
+
let result;
|
|
328
|
+
try {
|
|
329
|
+
result = filter.cond(args, type, poison, options, entity);
|
|
330
|
+
}
|
|
331
|
+
catch (e) {
|
|
332
|
+
// a raw TypeError from touching the `undefined` options/em must fail closed like the poison proxy does,
|
|
333
|
+
// but the descriptive MetadataErrors thrown above are already correct — let them surface unchanged
|
|
334
|
+
if (e instanceof MetadataError) {
|
|
335
|
+
throw e;
|
|
336
|
+
}
|
|
337
|
+
throw MetadataError.rlsFilterDependsOnRuntimeState(filter.name);
|
|
338
|
+
}
|
|
339
|
+
if (result instanceof Promise) {
|
|
340
|
+
throw MetadataError.rlsFilterDependsOnRuntimeState(filter.name);
|
|
341
|
+
}
|
|
342
|
+
return result;
|
|
343
|
+
};
|
|
344
|
+
const read = evaluate('read', name, poison);
|
|
345
|
+
const update = evaluate('update', `${name}\0a`, undefined);
|
|
346
|
+
const del = evaluate('delete', `${name}\0b`, poison);
|
|
347
|
+
if (JSON.stringify(read) !== JSON.stringify(update) || JSON.stringify(read) !== JSON.stringify(del)) {
|
|
348
|
+
throw MetadataError.rlsFilterDependsOnRuntimeState(filter.name);
|
|
349
|
+
}
|
|
350
|
+
return read;
|
|
351
|
+
}
|
|
274
352
|
static mergePropertyFilters(propFilters, options) {
|
|
275
353
|
if (!options || !propFilters || options === true || propFilters === true) {
|
|
276
354
|
return options ?? propFilters;
|
|
@@ -290,7 +368,7 @@ export class QueryHelper {
|
|
|
290
368
|
return Utils.mergeConfig({}, propFilters, options);
|
|
291
369
|
}
|
|
292
370
|
static isFilterActive(meta, filterName, filter, options) {
|
|
293
|
-
if (filter.entity && !filter.entity.
|
|
371
|
+
if (filter.entity && !Utils.asArray(filter.entity).some(e => Utils.matchesEntity(e, meta))) {
|
|
294
372
|
return false;
|
|
295
373
|
}
|
|
296
374
|
if (options[filterName] === false) {
|
|
@@ -321,6 +399,24 @@ export class QueryHelper {
|
|
|
321
399
|
}
|
|
322
400
|
return prop.customType.convertToDatabaseValue(cond, platform, { fromQuery, key, mode: 'query' });
|
|
323
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* Composite PK conditions are keyed by a hash of all the PK names, which `findProperty` cannot
|
|
404
|
+
* resolve, so the custom types have to be applied positionally instead.
|
|
405
|
+
*/
|
|
406
|
+
static processCompositeCustomTypes(value, meta, platform) {
|
|
407
|
+
const props = meta.primaryKeys.map(pk => meta.properties[pk]);
|
|
408
|
+
if (!props.some(prop => prop.customType)) {
|
|
409
|
+
return value;
|
|
410
|
+
}
|
|
411
|
+
// the tuple can be longer than the PK when the user passes a malformed condition
|
|
412
|
+
const convert = (tuple) => tuple.map((val, idx) => {
|
|
413
|
+
if (!props[idx]?.customType) {
|
|
414
|
+
return val;
|
|
415
|
+
}
|
|
416
|
+
return QueryHelper.processCustomType(props[idx], val, platform, undefined, true);
|
|
417
|
+
});
|
|
418
|
+
return value.every(val => Array.isArray(val)) ? value.map(val => convert(val)) : convert(value);
|
|
419
|
+
}
|
|
324
420
|
static isSupportedOperator(key) {
|
|
325
421
|
return !!QueryHelper.SUPPORTED_OPERATORS.find(op => key === op);
|
|
326
422
|
}
|
|
@@ -61,6 +61,12 @@ export declare const ALIAS_REPLACEMENT_RE = "\\[::alias::\\]";
|
|
|
61
61
|
* await em.find(User, { [raw(alias => `lower(${alias}.name)`)]: name.toLowerCase() });
|
|
62
62
|
* ```
|
|
63
63
|
*
|
|
64
|
+
* Named parameters are supported via an object of parameters, use `:name` for values and `:name:` for identifiers:
|
|
65
|
+
*
|
|
66
|
+
* ```ts
|
|
67
|
+
* raw('select :col: from geo where city = :city or region = :city', { col: 'city', city: 'Brno' });
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
64
70
|
* You can also use the `sql` tagged template function, which works the same, but supports only the simple string signature:
|
|
65
71
|
*
|
|
66
72
|
* ```ts
|
|
@@ -146,6 +146,12 @@ export const ALIAS_REPLACEMENT_RE = '\\[::alias::\\]';
|
|
|
146
146
|
* await em.find(User, { [raw(alias => `lower(${alias}.name)`)]: name.toLowerCase() });
|
|
147
147
|
* ```
|
|
148
148
|
*
|
|
149
|
+
* Named parameters are supported via an object of parameters, use `:name` for values and `:name:` for identifiers:
|
|
150
|
+
*
|
|
151
|
+
* ```ts
|
|
152
|
+
* raw('select :col: from geo where city = :city or region = :city', { col: 'city', city: 'Brno' });
|
|
153
|
+
* ```
|
|
154
|
+
*
|
|
149
155
|
* You can also use the `sql` tagged template function, which works the same, but supports only the simple string signature:
|
|
150
156
|
*
|
|
151
157
|
* ```ts
|
|
@@ -191,13 +197,16 @@ export function raw(sql, params) {
|
|
|
191
197
|
return Utils.getPrimaryKeyHash(sql);
|
|
192
198
|
}
|
|
193
199
|
if (typeof params === 'object' && !Array.isArray(params)) {
|
|
194
|
-
const
|
|
200
|
+
const dict = params;
|
|
195
201
|
const objectParams = [];
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
202
|
+
// single left-to-right scan keeps values in SQL-placeholder order while `::` casts and unknown tokens stay untouched
|
|
203
|
+
sql = sql.replace(/(?<!:):([$\w]+)(:(?!:))?/g, (match, key, identifier) => {
|
|
204
|
+
if (!Object.hasOwn(dict, key)) {
|
|
205
|
+
return match;
|
|
206
|
+
}
|
|
207
|
+
objectParams.push(dict[key]);
|
|
208
|
+
return identifier ? '??' : '?';
|
|
209
|
+
});
|
|
201
210
|
return new RawQueryFragment(sql, objectParams);
|
|
202
211
|
}
|
|
203
212
|
return new RawQueryFragment(sql, params);
|
|
@@ -17,13 +17,13 @@ export declare class RequestContext {
|
|
|
17
17
|
* If the handler is async, the return value needs to be awaited.
|
|
18
18
|
* Uses `AsyncLocalStorage.run()`, suitable for regular express style middlewares with a `next` callback.
|
|
19
19
|
*/
|
|
20
|
-
static create<T>(em: EntityManager | EntityManager[], next: (...args: any[]) => T, options?: CreateContextOptions): T;
|
|
20
|
+
static create<T>(em: EntityManager | EntityManager[], next: (...args: any[]) => T, options?: ((name: string) => CreateContextOptions) | CreateContextOptions): T;
|
|
21
21
|
/**
|
|
22
22
|
* Creates new RequestContext instance and runs the code inside its domain.
|
|
23
23
|
* If the handler is async, the return value needs to be awaited.
|
|
24
24
|
* Uses `AsyncLocalStorage.enterWith()`, suitable for elysia style middlewares without a `next` callback.
|
|
25
25
|
*/
|
|
26
|
-
static enter(em: EntityManager | EntityManager[], options?: CreateContextOptions): void;
|
|
26
|
+
static enter(em: EntityManager | EntityManager[], options?: ((name: string) => CreateContextOptions) | CreateContextOptions): void;
|
|
27
27
|
/**
|
|
28
28
|
* Returns current RequestContext (if available).
|
|
29
29
|
*/
|
package/utils/RequestContext.js
CHANGED
|
@@ -50,10 +50,19 @@ export class RequestContext {
|
|
|
50
50
|
static createContext(em, options = {}) {
|
|
51
51
|
const forks = new Map();
|
|
52
52
|
if (Array.isArray(em)) {
|
|
53
|
-
|
|
53
|
+
if (typeof options === 'function') {
|
|
54
|
+
for (const emInstance of em) {
|
|
55
|
+
forks.set(emInstance.name, emInstance.fork({ useContext: true, ...options(emInstance.name) }));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
for (const emInstance of em) {
|
|
60
|
+
forks.set(emInstance.name, emInstance.fork({ useContext: true, ...options }));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
54
63
|
}
|
|
55
64
|
else {
|
|
56
|
-
forks.set(em.name, em.fork({ useContext: true, ...options }));
|
|
65
|
+
forks.set(em.name, em.fork({ useContext: true, ...(typeof options === 'function' ? options(em.name) : options) }));
|
|
57
66
|
}
|
|
58
67
|
return new RequestContext(forks);
|
|
59
68
|
}
|
|
@@ -50,6 +50,12 @@ export declare class TransactionManager {
|
|
|
50
50
|
* Merges entities from fork to parent EntityManager.
|
|
51
51
|
*/
|
|
52
52
|
private mergeEntitiesToParent;
|
|
53
|
+
/**
|
|
54
|
+
* Returns the property names a given property is tracked and snapshotted under, paired with their values.
|
|
55
|
+
* Inlined embeddables are hydrated as a single object, so only their leaves tell whether it is complete.
|
|
56
|
+
*/
|
|
57
|
+
private getTrackedValues;
|
|
58
|
+
private restore;
|
|
53
59
|
/**
|
|
54
60
|
* Registers a deletion handler to unset entity identities after flush.
|
|
55
61
|
*/
|
|
@@ -169,19 +169,52 @@ export class TransactionManager {
|
|
|
169
169
|
if (!wrapped.__initialized && parentWrapped.__initialized) {
|
|
170
170
|
continue;
|
|
171
171
|
}
|
|
172
|
-
|
|
173
|
-
|
|
172
|
+
const parentData = parentWrapped.__data;
|
|
173
|
+
const parentSnapshot = parentWrapped.__originalEntityData;
|
|
174
|
+
parentWrapped.__data = { ...wrapped.__data };
|
|
175
|
+
const originalEntityData = { ...wrapped.__originalEntityData };
|
|
174
176
|
for (const prop of meta.hydrateProps) {
|
|
177
|
+
const tracked = this.getTrackedValues(prop, entity[prop.name]);
|
|
178
|
+
// the fork entity can be partially loaded, and propagating a property it does not know about
|
|
179
|
+
// would clobber the parent state, so we restore both its value and its snapshot entries
|
|
180
|
+
if (!tracked.every(([key, value]) => value !== undefined || wrapped.__loadedProperties.has(key))) {
|
|
181
|
+
this.restore(parentWrapped.__data, parentData, prop.name);
|
|
182
|
+
tracked.forEach(([key]) => this.restore(originalEntityData, parentSnapshot, key));
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
175
185
|
if (prop.kind === ReferenceKind.SCALAR) {
|
|
176
186
|
parentEntity[prop.name] = entity[prop.name];
|
|
177
187
|
}
|
|
178
188
|
}
|
|
189
|
+
if (wrapped.__originalEntityData) {
|
|
190
|
+
parentWrapped.__originalEntityData = originalEntityData;
|
|
191
|
+
}
|
|
179
192
|
}
|
|
180
193
|
else {
|
|
181
194
|
parentUoW.merge(entity, new Set([entity]));
|
|
182
195
|
}
|
|
183
196
|
}
|
|
184
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Returns the property names a given property is tracked and snapshotted under, paired with their values.
|
|
200
|
+
* Inlined embeddables are hydrated as a single object, so only their leaves tell whether it is complete.
|
|
201
|
+
*/
|
|
202
|
+
getTrackedValues(prop, value) {
|
|
203
|
+
if (prop.kind === ReferenceKind.EMBEDDED && !prop.object) {
|
|
204
|
+
return Object.values(prop.embeddedProps).flatMap(child => {
|
|
205
|
+
return this.getTrackedValues(child, value?.[child.embedded[1]]);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return [[prop.name, value]];
|
|
209
|
+
}
|
|
210
|
+
restore(target, source, key) {
|
|
211
|
+
if (source && key in source) {
|
|
212
|
+
target[key] = source[key];
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
delete target[key];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
185
218
|
/**
|
|
186
219
|
* Registers a deletion handler to unset entity identities after flush.
|
|
187
220
|
*/
|
|
@@ -206,7 +239,7 @@ export class TransactionManager {
|
|
|
206
239
|
return TransactionContext.create(fork, () => fork.getConnection().transactional(async (trx) => {
|
|
207
240
|
fork.setTransactionContext(trx);
|
|
208
241
|
return this.executeTransactionFlow(fork, cb, propagateToUpperContext, em);
|
|
209
|
-
}, { ...options, eventBroadcaster }));
|
|
242
|
+
}, { sessionContext: fork.getTransactionSessionContext(), ...options, eventBroadcaster }));
|
|
210
243
|
}
|
|
211
244
|
/**
|
|
212
245
|
* Executes transaction workflow with entity synchronization.
|
package/utils/Utils.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CompiledFunctions, Dictionary, EntityData, EntityDictionary, EntityKey, EntityMetadata, EntityName, EntityProperty, Primary } from '../typings.js';
|
|
1
|
+
import type { CompiledFunctions, Dictionary, EntityCtor, EntityData, EntityDictionary, EntityKey, EntityMetadata, EntityName, EntityProperty, Primary } from '../typings.js';
|
|
2
2
|
import type { Platform } from '../platforms/Platform.js';
|
|
3
3
|
import { ScalarReference } from '../entity/Reference.js';
|
|
4
4
|
import { Collection } from '../entity/Collection.js';
|
|
@@ -33,6 +33,8 @@ export declare function parseJsonSafe<T = unknown>(value: unknown): T;
|
|
|
33
33
|
export declare class Utils {
|
|
34
34
|
#private;
|
|
35
35
|
static readonly PK_SEPARATOR = "~~~";
|
|
36
|
+
/** Default session variable name backing an RLS filter argument (`current_setting('mikro.<filter>.<arg>')`). */
|
|
37
|
+
static getRlsSettingName(filterName: string, argName: string): string;
|
|
36
38
|
/**
|
|
37
39
|
* Checks if the argument is instance of `Object`. Returns false for arrays.
|
|
38
40
|
*/
|
|
@@ -58,7 +60,7 @@ export declare class Utils {
|
|
|
58
60
|
/**
|
|
59
61
|
* Gets array without duplicates.
|
|
60
62
|
*/
|
|
61
|
-
static unique<T = string>(items: T[]): T[];
|
|
63
|
+
static unique<T = string>(items: T[], equals?: (a: T, b: T) => boolean): T[];
|
|
62
64
|
/**
|
|
63
65
|
* Merges all sources into the target recursively.
|
|
64
66
|
*/
|
|
@@ -129,6 +131,16 @@ export declare class Utils {
|
|
|
129
131
|
* Gets string name of given class.
|
|
130
132
|
*/
|
|
131
133
|
static className<T>(classOrName: string | EntityName<T>): string;
|
|
134
|
+
/**
|
|
135
|
+
* Normalizes an entity reference for identity-safe matching: keeps class references
|
|
136
|
+
* (minifiers can mangle two classes to the same name) and falls back to the class name otherwise.
|
|
137
|
+
*/
|
|
138
|
+
static classOrName<T>(classOrName: string | EntityName<T>): EntityCtor<T> | string;
|
|
139
|
+
/**
|
|
140
|
+
* Checks whether the given entity reference points at the given metadata,
|
|
141
|
+
* comparing classes by identity and strings by class name.
|
|
142
|
+
*/
|
|
143
|
+
static matchesEntity<T>(classOrName: string | EntityName<T>, meta: EntityMetadata<any>): boolean;
|
|
132
144
|
static extractChildElements(items: readonly string[], prefix: string, allSymbol?: string): string[];
|
|
133
145
|
/**
|
|
134
146
|
* Tries to detect TypeScript support.
|
package/utils/Utils.js
CHANGED
|
@@ -153,7 +153,11 @@ export function parseJsonSafe(value) {
|
|
|
153
153
|
/** Collection of general-purpose utility methods used throughout the ORM. */
|
|
154
154
|
export class Utils {
|
|
155
155
|
static PK_SEPARATOR = '~~~';
|
|
156
|
-
static #ORM_VERSION = '7.2.0-dev.
|
|
156
|
+
static #ORM_VERSION = '7.2.0-dev.21';
|
|
157
|
+
/** Default session variable name backing an RLS filter argument (`current_setting('mikro.<filter>.<arg>')`). */
|
|
158
|
+
static getRlsSettingName(filterName, argName) {
|
|
159
|
+
return `mikro.${filterName}.${argName}`;
|
|
160
|
+
}
|
|
157
161
|
/**
|
|
158
162
|
* Checks if the argument is instance of `Object`. Returns false for arrays.
|
|
159
163
|
*/
|
|
@@ -216,10 +220,13 @@ export class Utils {
|
|
|
216
220
|
/**
|
|
217
221
|
* Gets array without duplicates.
|
|
218
222
|
*/
|
|
219
|
-
static unique(items) {
|
|
223
|
+
static unique(items, equals) {
|
|
220
224
|
if (items.length < 2) {
|
|
221
225
|
return items;
|
|
222
226
|
}
|
|
227
|
+
if (equals) {
|
|
228
|
+
return items.filter((a, idx) => items.findIndex(b => equals(a, b)) === idx);
|
|
229
|
+
}
|
|
223
230
|
return [...new Set(items)];
|
|
224
231
|
}
|
|
225
232
|
/**
|
|
@@ -402,7 +409,8 @@ export class Utils {
|
|
|
402
409
|
static getCompositeKeyHash(data, meta, convertCustomTypes = false, platform, flat = false) {
|
|
403
410
|
let pks = this.getCompositeKeyValue(data, meta, convertCustomTypes, platform);
|
|
404
411
|
if (flat) {
|
|
405
|
-
|
|
412
|
+
// deep flatten, nested composite PKs produce nested arrays that would be comma-joined by the hash
|
|
413
|
+
pks = Utils.flatten(pks, true);
|
|
406
414
|
}
|
|
407
415
|
return Utils.getPrimaryKeyHash(pks);
|
|
408
416
|
}
|
|
@@ -462,7 +470,9 @@ export class Utils {
|
|
|
462
470
|
}
|
|
463
471
|
static getPrimaryKeyCond(entity, primaryKeys) {
|
|
464
472
|
const cond = primaryKeys.reduce((o, pk) => {
|
|
465
|
-
|
|
473
|
+
const value = entity[pk];
|
|
474
|
+
// FKs pointing to a composite PK are arrays, which `extractPK` rejects
|
|
475
|
+
o[pk] = Utils.isPrimaryKey(value, true) ? value : Utils.extractPK(value);
|
|
466
476
|
return o;
|
|
467
477
|
}, {});
|
|
468
478
|
if (Object.values(cond).some(v => v === null)) {
|
|
@@ -559,6 +569,21 @@ export class Utils {
|
|
|
559
569
|
}
|
|
560
570
|
return classOrName.name;
|
|
561
571
|
}
|
|
572
|
+
/**
|
|
573
|
+
* Normalizes an entity reference for identity-safe matching: keeps class references
|
|
574
|
+
* (minifiers can mangle two classes to the same name) and falls back to the class name otherwise.
|
|
575
|
+
*/
|
|
576
|
+
static classOrName(classOrName) {
|
|
577
|
+
return typeof classOrName === 'function' ? classOrName : Utils.className(classOrName);
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Checks whether the given entity reference points at the given metadata,
|
|
581
|
+
* comparing classes by identity and strings by class name.
|
|
582
|
+
*/
|
|
583
|
+
static matchesEntity(classOrName, meta) {
|
|
584
|
+
const ref = Utils.classOrName(classOrName);
|
|
585
|
+
return typeof ref === 'function' ? ref === meta.class : ref === meta.className;
|
|
586
|
+
}
|
|
562
587
|
static extractChildElements(items, prefix, allSymbol) {
|
|
563
588
|
return items
|
|
564
589
|
.filter(field => field === allSymbol || field.startsWith(`${prefix}.`))
|
|
@@ -581,7 +606,8 @@ export class Utils {
|
|
|
581
606
|
return (arg.includes('ts-node') || // check for ts-node loader
|
|
582
607
|
arg.includes('@swc-node/register') || // check for swc-node/register loader
|
|
583
608
|
arg.includes('node_modules/tsx/') || // check for tsx loader
|
|
584
|
-
arg.includes('@oxc-node/core') // check for oxc-node loader
|
|
609
|
+
arg.includes('@oxc-node/core') || // check for oxc-node loader
|
|
610
|
+
arg.includes('@nubjs/loader') // check for Nub loader
|
|
585
611
|
);
|
|
586
612
|
}));
|
|
587
613
|
}
|
|
@@ -833,7 +859,11 @@ export class Utils {
|
|
|
833
859
|
return await import(module);
|
|
834
860
|
}
|
|
835
861
|
catch (err) {
|
|
836
|
-
|
|
862
|
+
// only a missing module is expected here, anything else is a real failure inside the module
|
|
863
|
+
if (!['ERR_MODULE_NOT_FOUND', 'MODULE_NOT_FOUND'].includes(err.code)) {
|
|
864
|
+
throw err;
|
|
865
|
+
}
|
|
866
|
+
if (warning) {
|
|
837
867
|
// eslint-disable-next-line no-console
|
|
838
868
|
console.warn(warning);
|
|
839
869
|
}
|
package/utils/clone.js
CHANGED
|
@@ -112,6 +112,12 @@ export function clone(parent, respectCustomCloneMethod = true) {
|
|
|
112
112
|
});
|
|
113
113
|
}
|
|
114
114
|
for (const i in parent) {
|
|
115
|
+
// an own `__proto__` key (as produced by `JSON.parse`) has no own counterpart on
|
|
116
|
+
// `child` to shadow the inherited accessor, so assigning it would replace the
|
|
117
|
+
// clone's prototype instead of copying the value
|
|
118
|
+
if (i === '__proto__') {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
115
121
|
let attrs;
|
|
116
122
|
if (proto) {
|
|
117
123
|
attrs = getPropertyDescriptor(proto, i);
|
package/utils/env-vars.js
CHANGED
|
@@ -88,6 +88,7 @@ export function loadEnvironmentVars() {
|
|
|
88
88
|
read3('createForeignKeyConstraints', bool);
|
|
89
89
|
read3('ignoreTriggers', bool);
|
|
90
90
|
read3('ignoreRoutines', bool);
|
|
91
|
+
read3('ignorePolicies', bool);
|
|
91
92
|
cleanup(ret, 'schemaGenerator');
|
|
92
93
|
ret.seeder = {};
|
|
93
94
|
const read4 = read.bind(null, ret.seeder, 'MIKRO_ORM_SEEDER_');
|
package/utils/index.d.ts
CHANGED
package/utils/index.js
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { MetadataStorage } from '../metadata/MetadataStorage.js';
|
|
2
|
+
import type { Dictionary, FilterDef } from '../typings.js';
|
|
3
|
+
/** An `rls`-flagged filter definition together with the entity it is declared on. @internal */
|
|
4
|
+
export interface RlsFilterEntry {
|
|
5
|
+
filter: FilterDef;
|
|
6
|
+
entityName: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Collects all `rls`-flagged filter definitions with the given name (only entity-scoped filters can be `rls`).
|
|
10
|
+
* The full name -> defs lookup is built once and cached on the shared (immutable) MetadataStorage, so repeated
|
|
11
|
+
* `setFilterParams` calls and forks reuse it instead of walking every entity each time.
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
export declare function findRlsFilterDefs(metadata: MetadataStorage, name: string): RlsFilterEntry[];
|
|
15
|
+
/**
|
|
16
|
+
* Drops the cached `rls` filter lookup — `MikroORM.discoverEntity()` mutates the shared MetadataStorage,
|
|
17
|
+
* so a lookup built before the call would miss the newly discovered filters.
|
|
18
|
+
*
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
export declare function clearRlsFilterDefsCache(metadata: MetadataStorage): void;
|
|
22
|
+
/**
|
|
23
|
+
* Computes the `rls` session variables a set of same-named filter defs stages for the given args, mirroring the
|
|
24
|
+
* policy compilation (`current_setting` names and custom `setting` binding). Shared by staging and `fork({ session })`.
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
export declare function computeRlsFilterVariables(filters: RlsFilterEntry[], args: Dictionary): Dictionary<string | number | boolean | Date>;
|
|
28
|
+
/**
|
|
29
|
+
* Computes which staged session variables a `setFilterParams` call may prune: the variables the OLD args staged for
|
|
30
|
+
* this filter that the new args no longer set, minus any variable another filter's current params still stage
|
|
31
|
+
* (a custom `setting` name can be shared by differently named filters). Recomputing from the old args rather than
|
|
32
|
+
* matching by prefix keeps a filter named `tenant` from also pruning a `tenant.x` filter's `mikro.tenant.x.*` variables.
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
export declare function computeRemovedRlsVariables(metadata: MetadataStorage, name: string, filters: RlsFilterEntry[], previousArgs: Dictionary, nextVariables: Dictionary, allFilterParams: Dictionary<Dictionary>): string[];
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { MetadataError, ValidationError } from '../errors.js';
|
|
2
|
+
import { QueryHelper } from './QueryHelper.js';
|
|
3
|
+
import { Utils } from './Utils.js';
|
|
4
|
+
/** Lazily-built `rls` filter lookup keyed by the shared (immutable) MetadataStorage, so all forks reuse it. */
|
|
5
|
+
const rlsFilterDefs = new WeakMap();
|
|
6
|
+
/**
|
|
7
|
+
* Collects all `rls`-flagged filter definitions with the given name (only entity-scoped filters can be `rls`).
|
|
8
|
+
* The full name -> defs lookup is built once and cached on the shared (immutable) MetadataStorage, so repeated
|
|
9
|
+
* `setFilterParams` calls and forks reuse it instead of walking every entity each time.
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export function findRlsFilterDefs(metadata, name) {
|
|
13
|
+
let cache = rlsFilterDefs.get(metadata);
|
|
14
|
+
if (!cache) {
|
|
15
|
+
cache = new Map();
|
|
16
|
+
for (const meta of metadata) {
|
|
17
|
+
for (const filterName of Object.keys(meta.filters)) {
|
|
18
|
+
const filter = meta.filters[filterName];
|
|
19
|
+
if (!filter.rls) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const defs = cache.get(filterName) ?? [];
|
|
23
|
+
// inheritance shares the same filter object across base and child metadata — keep a single entry
|
|
24
|
+
if (!defs.some(d => d.filter === filter)) {
|
|
25
|
+
defs.push({ filter, entityName: meta.className });
|
|
26
|
+
}
|
|
27
|
+
cache.set(filterName, defs);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
rlsFilterDefs.set(metadata, cache);
|
|
31
|
+
}
|
|
32
|
+
return cache.get(name) ?? [];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Drops the cached `rls` filter lookup — `MikroORM.discoverEntity()` mutates the shared MetadataStorage,
|
|
36
|
+
* so a lookup built before the call would miss the newly discovered filters.
|
|
37
|
+
*
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
export function clearRlsFilterDefsCache(metadata) {
|
|
41
|
+
rlsFilterDefs.delete(metadata);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Computes the `rls` session variables a set of same-named filter defs stages for the given args, mirroring the
|
|
45
|
+
* policy compilation (`current_setting` names and custom `setting` binding). Shared by staging and `fork({ session })`.
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
export function computeRlsFilterVariables(filters, args) {
|
|
49
|
+
const variables = {};
|
|
50
|
+
for (const { filter, entityName } of filters) {
|
|
51
|
+
const setting = typeof filter.rls === 'object' ? filter.rls.setting : undefined;
|
|
52
|
+
let settingArg;
|
|
53
|
+
if (setting) {
|
|
54
|
+
// mirror the policy compilation — a custom `setting` binds the single argument the condition accesses
|
|
55
|
+
const accessed = new Set();
|
|
56
|
+
QueryHelper.resolveRlsFilterCond(filter, accessed, entityName);
|
|
57
|
+
if (accessed.size > 1) {
|
|
58
|
+
throw MetadataError.rlsFilterMultiArgSetting(filter.name, [...accessed]);
|
|
59
|
+
}
|
|
60
|
+
settingArg = [...accessed][0];
|
|
61
|
+
}
|
|
62
|
+
for (const key of Object.keys(args)) {
|
|
63
|
+
const value = args[key];
|
|
64
|
+
// treat `undefined` like an omitted arg — staging it would serialize as the literal string 'undefined'
|
|
65
|
+
if (value === undefined) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// a non-scalar arg has no equivalent in the compiled `= current_setting(...)` comparison — the app-level
|
|
69
|
+
// filter would apply `$in`/`is null` semantics while the policy compares against `String(value)`
|
|
70
|
+
if (value === null || (typeof value === 'object' && !(value instanceof Date))) {
|
|
71
|
+
throw ValidationError.cannotStageNonScalarSessionVariable(filter.name, key);
|
|
72
|
+
}
|
|
73
|
+
const settingName = key === settingArg ? setting : Utils.getRlsSettingName(filter.name, key);
|
|
74
|
+
variables[settingName] = value;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return variables;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Computes which staged session variables a `setFilterParams` call may prune: the variables the OLD args staged for
|
|
81
|
+
* this filter that the new args no longer set, minus any variable another filter's current params still stage
|
|
82
|
+
* (a custom `setting` name can be shared by differently named filters). Recomputing from the old args rather than
|
|
83
|
+
* matching by prefix keeps a filter named `tenant` from also pruning a `tenant.x` filter's `mikro.tenant.x.*` variables.
|
|
84
|
+
* @internal
|
|
85
|
+
*/
|
|
86
|
+
export function computeRemovedRlsVariables(metadata, name, filters, previousArgs, nextVariables, allFilterParams) {
|
|
87
|
+
const removed = Object.keys(computeRlsFilterVariables(filters, previousArgs)).filter(key => !(key in nextVariables));
|
|
88
|
+
const keptByOthers = new Set();
|
|
89
|
+
for (const otherName of removed.length > 0 ? Object.keys(allFilterParams) : []) {
|
|
90
|
+
if (otherName !== name) {
|
|
91
|
+
for (const key of Object.keys(computeRlsFilterVariables(findRlsFilterDefs(metadata, otherName), allFilterParams[otherName]))) {
|
|
92
|
+
keptByOthers.add(key);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return removed.filter(key => !keptByOthers.has(key));
|
|
97
|
+
}
|
package/utils/upsert-utils.d.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import type { EntityData, EntityMetadata, FilterQuery } from '../typings.js';
|
|
1
|
+
import type { EntityData, EntityKey, EntityMetadata, FilterQuery } from '../typings.js';
|
|
2
2
|
import type { UpsertOptions } from '../drivers/IDatabaseDriver.js';
|
|
3
3
|
import { type Raw } from '../utils/RawQueryFragment.js';
|
|
4
4
|
/** @internal */
|
|
5
5
|
export declare function getOnConflictFields<T>(meta: EntityMetadata<T> | undefined, data: EntityData<T>, uniqueFields: (keyof T)[] | Raw, options: UpsertOptions<T>): (keyof T)[];
|
|
6
|
+
/**
|
|
7
|
+
* Detects properties that will get their value generated by an `onCreate` hook during the upsert,
|
|
8
|
+
* i.e. those with an `onCreate` hook and no value provided. Such values are meant for the insert
|
|
9
|
+
* clause only and must not overwrite an existing row via the `on conflict do update set` clause.
|
|
10
|
+
* The property filter mirrors `EntityFactory.assignDefaultValues`.
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export declare function getOnCreateGeneratedFields<T extends object>(meta: EntityMetadata<T>, data: T | EntityData<T>): EntityKey<T>[];
|
|
6
14
|
/** @internal */
|
|
7
15
|
export declare function getOnConflictReturningFields<T, P extends string>(meta: EntityMetadata<T> | undefined, data: EntityData<T>, uniqueFields: (keyof T)[] | Raw, options: UpsertOptions<T, P>): (keyof T)[] | '*';
|
|
8
16
|
/** @internal */
|