@mikro-orm/core 7.2.8-dev.0 → 7.2.8-dev.10
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.js +15 -8
- package/cache/cache-key.d.ts +5 -0
- package/cache/cache-key.js +30 -0
- package/drivers/IDatabaseDriver.d.ts +2 -0
- package/package.json +1 -1
- package/unit-of-work/UnitOfWork.js +4 -2
- package/utils/DataloaderUtils.js +19 -8
- package/utils/Utils.js +1 -1
package/EntityManager.js
CHANGED
|
@@ -19,6 +19,7 @@ import { TransactionEventBroadcaster } from './events/TransactionEventBroadcaste
|
|
|
19
19
|
import { MetadataError, OptimisticLockError, ValidationError } from './errors.js';
|
|
20
20
|
import { applyPopulateHints, getLoadingStrategy } from './entity/utils.js';
|
|
21
21
|
import { TransactionManager } from './utils/TransactionManager.js';
|
|
22
|
+
import { scopeCacheKey, stringifyCacheKey } from './cache/cache-key.js';
|
|
22
23
|
/**
|
|
23
24
|
* The EntityManager is the central access point to ORM functionality. It is a facade to all different ORM subsystems
|
|
24
25
|
* such as UnitOfWork, Query Language, and Repository API.
|
|
@@ -211,9 +212,14 @@ export class EntityManager {
|
|
|
211
212
|
await em.tryFlush(entityName, options);
|
|
212
213
|
const where = (await em.processWhere(entityName, options.where ?? {}, options, 'read'));
|
|
213
214
|
validateParams(where);
|
|
214
|
-
options.orderBy = options.orderBy || {};
|
|
215
|
-
options.populate = (await em.preparePopulate(entityName, options));
|
|
216
215
|
const meta = this.metadata.get(entityName);
|
|
216
|
+
if (meta.orderBy) {
|
|
217
|
+
options.orderBy = QueryHelper.mergeOrderBy(options.orderBy, meta.orderBy);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
options.orderBy ??= {};
|
|
221
|
+
}
|
|
222
|
+
options.populate = (await em.preparePopulate(entityName, options));
|
|
217
223
|
em.validateIndexUsage(meta, options.where ?? {}, options);
|
|
218
224
|
options = { ...options };
|
|
219
225
|
// save the original hint value so we know it was infer/all
|
|
@@ -857,6 +863,10 @@ export class EntityManager {
|
|
|
857
863
|
return em.lockAndPopulate(meta, entity, where, options);
|
|
858
864
|
}
|
|
859
865
|
validateParams(where);
|
|
866
|
+
// before the cache key so cached results share the merged orderBy
|
|
867
|
+
if (meta.orderBy) {
|
|
868
|
+
options.orderBy = QueryHelper.mergeOrderBy(options.orderBy, meta.orderBy);
|
|
869
|
+
}
|
|
860
870
|
options.populate = (await em.preparePopulate(entityName, options));
|
|
861
871
|
const cacheKey = em.cacheKey(entityName, options, 'em.findOne', where);
|
|
862
872
|
const cached = await em.tryCache(entityName, options.cache, cacheKey, options.refresh, true, options.schema);
|
|
@@ -1831,6 +1841,7 @@ export class EntityManager {
|
|
|
1831
1841
|
options.populateWhere = this.createPopulateWhere({ ...where }, options);
|
|
1832
1842
|
options.populateFilter = await this.getJoinedFilters(meta, options);
|
|
1833
1843
|
validateParams(where);
|
|
1844
|
+
em.validateIndexUsage(meta, where, options);
|
|
1834
1845
|
delete options.orderBy;
|
|
1835
1846
|
await em.processUnionWhere(entityName, options, 'read');
|
|
1836
1847
|
const cacheKey = em.cacheKey(entityName, options, 'em.count', where);
|
|
@@ -2458,11 +2469,7 @@ export class EntityManager {
|
|
|
2458
2469
|
const em = this.getContext();
|
|
2459
2470
|
// a named cache key (`config[0]`) discards the computed `key`, which already carries the session context — so
|
|
2460
2471
|
// scope it here too, otherwise a fork's rows would be served to another session context under the same name
|
|
2461
|
-
const cacheKey = Array.isArray(config)
|
|
2462
|
-
? em.#sessionContext
|
|
2463
|
-
? `${config[0]}|${JSON.stringify(em.#sessionContext)}`
|
|
2464
|
-
: config[0]
|
|
2465
|
-
: JSON.stringify(key);
|
|
2472
|
+
const cacheKey = Array.isArray(config) ? scopeCacheKey(config[0], em.#sessionContext) : stringifyCacheKey(key);
|
|
2466
2473
|
const cached = await em.#resultCache.get(cacheKey);
|
|
2467
2474
|
if (!cached) {
|
|
2468
2475
|
return { key: cacheKey, data: cached };
|
|
@@ -2515,7 +2522,7 @@ export class EntityManager {
|
|
|
2515
2522
|
await em.#resultCache.remove(cacheKey);
|
|
2516
2523
|
// named keys are scoped by the session context (see `tryCache`), so clear this context's variant too
|
|
2517
2524
|
if (em.#sessionContext) {
|
|
2518
|
-
await em.#resultCache.remove(
|
|
2525
|
+
await em.#resultCache.remove(scopeCacheKey(cacheKey, em.#sessionContext));
|
|
2519
2526
|
}
|
|
2520
2527
|
}
|
|
2521
2528
|
/**
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { SessionContext } from '../typings.js';
|
|
2
|
+
/** Serializes a result cache or dataloader batch key, telling apart all values a query can contain. */
|
|
3
|
+
export declare function stringifyCacheKey(key: unknown): string;
|
|
4
|
+
/** Scopes a named result cache key by the session context, so rows are not served across session contexts. */
|
|
5
|
+
export declare function scopeCacheKey(name: string, sessionContext?: SessionContext): string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { isRaw, isSubquery, Raw } from '../utils/RawQueryFragment.js';
|
|
2
|
+
import { Utils } from '../utils/Utils.js';
|
|
3
|
+
// JSON loses RegExp patterns, `raw()` params and `raw()` keys (symbols), and throws on bigints and sub-queries; `this[key]` is the value before `toJSON()`
|
|
4
|
+
function replacer(key, value) {
|
|
5
|
+
const original = isSubquery(this[key]) ? this[key].toRaw() : this[key];
|
|
6
|
+
if (typeof original === 'bigint') {
|
|
7
|
+
return { bigint: original.toString() };
|
|
8
|
+
}
|
|
9
|
+
if (original instanceof RegExp) {
|
|
10
|
+
return { regexp: original.source, flags: original.flags };
|
|
11
|
+
}
|
|
12
|
+
if (isRaw(original)) {
|
|
13
|
+
return { sql: original.sql, params: original.params };
|
|
14
|
+
}
|
|
15
|
+
if (Raw.hasObjectFragments(value)) {
|
|
16
|
+
return Utils.getObjectQueryKeys(value).map(k => [
|
|
17
|
+
Raw.getKnownFragment(k) ?? k,
|
|
18
|
+
value[k],
|
|
19
|
+
]);
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
/** Serializes a result cache or dataloader batch key, telling apart all values a query can contain. */
|
|
24
|
+
export function stringifyCacheKey(key) {
|
|
25
|
+
return JSON.stringify(key, replacer);
|
|
26
|
+
}
|
|
27
|
+
/** Scopes a named result cache key by the session context, so rows are not served across session contexts. */
|
|
28
|
+
export function scopeCacheKey(name, sessionContext) {
|
|
29
|
+
return sessionContext ? `${name}|${JSON.stringify(sessionContext)}` : name;
|
|
30
|
+
}
|
|
@@ -326,6 +326,8 @@ export interface CountOptions<T extends object, P extends string = never> extend
|
|
|
326
326
|
flushMode?: FlushMode | `${FlushMode}`;
|
|
327
327
|
/** SQL: appended to FROM clause (e.g. `'force index(my_index)'`); MongoDB: index name or spec passed as `hint`. */
|
|
328
328
|
indexHint?: string | Dictionary;
|
|
329
|
+
/** @see FindOptions.using */
|
|
330
|
+
using?: IndexName<T> | IndexName<T>[];
|
|
329
331
|
/** sql only */
|
|
330
332
|
comments?: string | string[];
|
|
331
333
|
/** sql only */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/core",
|
|
3
|
-
"version": "7.2.8-dev.
|
|
3
|
+
"version": "7.2.8-dev.10",
|
|
4
4
|
"description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"data-mapper",
|
|
@@ -395,7 +395,7 @@ export class UnitOfWork {
|
|
|
395
395
|
this.#persistStack.delete(entity);
|
|
396
396
|
this.#identityMap.delete(entity);
|
|
397
397
|
}
|
|
398
|
-
//
|
|
398
|
+
// unlink loaded inverse-side items only when the FK is `set null`, other delete rules are left to the database
|
|
399
399
|
const entityMeta = helper(entity).__meta;
|
|
400
400
|
for (const prop of entityMeta.bidirectionalRelations) {
|
|
401
401
|
const inverseProp = prop.mappedBy || prop.inversedBy;
|
|
@@ -410,7 +410,9 @@ export class UnitOfWork {
|
|
|
410
410
|
: inverseTarget.class !== entityMeta.class) {
|
|
411
411
|
continue;
|
|
412
412
|
}
|
|
413
|
-
if (prop.kind === ReferenceKind.ONE_TO_MANY &&
|
|
413
|
+
if (prop.kind === ReferenceKind.ONE_TO_MANY &&
|
|
414
|
+
prop2.deleteRule === 'set null' &&
|
|
415
|
+
Utils.isCollection(relation)) {
|
|
414
416
|
for (const item of relation.getItems(false)) {
|
|
415
417
|
delete item[inverseProp];
|
|
416
418
|
}
|
package/utils/DataloaderUtils.js
CHANGED
|
@@ -3,6 +3,9 @@ import { helper } from '../entity/wrap.js';
|
|
|
3
3
|
import { Reference } from '../entity/Reference.js';
|
|
4
4
|
import { ReferenceKind } from '../enums.js';
|
|
5
5
|
import { Utils } from './Utils.js';
|
|
6
|
+
import { stringifyCacheKey } from '../cache/cache-key.js';
|
|
7
|
+
// the original options of each group key, JSON cannot restore values like RegExp or `raw()` from the key
|
|
8
|
+
const groupOptions = new WeakMap();
|
|
6
9
|
export class DataloaderUtils {
|
|
7
10
|
static DataLoader;
|
|
8
11
|
/**
|
|
@@ -11,6 +14,8 @@ export class DataloaderUtils {
|
|
|
11
14
|
*/
|
|
12
15
|
static groupPrimaryKeysByEntityAndOpts(refsWithOpts) {
|
|
13
16
|
const map = new Map();
|
|
17
|
+
const options = new Map();
|
|
18
|
+
groupOptions.set(map, options);
|
|
14
19
|
for (const [ref, opts] of refsWithOpts) {
|
|
15
20
|
/* The key is a combination of the uniqueName (a unique table name based identifier) and a stringified version if the load options because we want
|
|
16
21
|
to map each combination of entities/options into separate find queries in order to return accurate results.
|
|
@@ -25,7 +30,8 @@ export class DataloaderUtils {
|
|
|
25
30
|
Thus such approach should probably be configurable, if not opt-in.
|
|
26
31
|
NOTE: meta + opts multi maps (https://github.com/martian17/ds-js) might be a more elegant way
|
|
27
32
|
to implement this but not necessarily faster. */
|
|
28
|
-
const key = `${helper(ref).__meta.uniqueName}|${
|
|
33
|
+
const key = `${helper(ref).__meta.uniqueName}|${stringifyCacheKey(opts ?? {})}`;
|
|
34
|
+
options.set(key, opts ?? {});
|
|
29
35
|
let primaryKeysSet = map.get(key);
|
|
30
36
|
if (primaryKeysSet == null) {
|
|
31
37
|
primaryKeysSet = new Set();
|
|
@@ -44,7 +50,7 @@ export class DataloaderUtils {
|
|
|
44
50
|
const groupedIdsMap = DataloaderUtils.groupPrimaryKeysByEntityAndOpts(refsWithOpts);
|
|
45
51
|
const promises = Array.from(groupedIdsMap).map(([key, idsSet]) => {
|
|
46
52
|
const uniqueName = key.substring(0, key.indexOf('|'));
|
|
47
|
-
const opts =
|
|
53
|
+
const opts = groupOptions.get(groupedIdsMap).get(key);
|
|
48
54
|
const meta = em.getMetadata().getByUniqueName(uniqueName);
|
|
49
55
|
return em.find(meta.class, Array.from(idsSet), opts);
|
|
50
56
|
});
|
|
@@ -64,6 +70,8 @@ export class DataloaderUtils {
|
|
|
64
70
|
*/
|
|
65
71
|
static groupInversedOrMappedKeysByEntityAndOpts(collsWithOpts) {
|
|
66
72
|
const entitiesMap = new Map();
|
|
73
|
+
const options = new Map();
|
|
74
|
+
groupOptions.set(entitiesMap, options);
|
|
67
75
|
for (const [col, opts] of collsWithOpts) {
|
|
68
76
|
/*
|
|
69
77
|
We first get the entity name of the Collection and together with its options (see groupPrimaryKeysByEntityAndOpts
|
|
@@ -72,7 +80,8 @@ export class DataloaderUtils {
|
|
|
72
80
|
The value is another Map which we can use to filter the find query to get results pertaining to the collections that have been dataloaded:
|
|
73
81
|
its keys are the props we are going to filter to and its values are the corresponding PKs.
|
|
74
82
|
*/
|
|
75
|
-
const key = `${col.property.targetMeta.uniqueName}|${
|
|
83
|
+
const key = `${col.property.targetMeta.uniqueName}|${stringifyCacheKey(opts ?? {})}`;
|
|
84
|
+
options.set(key, opts ?? {});
|
|
76
85
|
let filterMap = entitiesMap.get(key); // We are going to use this map to filter the entities pertaining to the collections that have been dataloaded.
|
|
77
86
|
if (filterMap == null) {
|
|
78
87
|
filterMap = new Map();
|
|
@@ -100,7 +109,9 @@ export class DataloaderUtils {
|
|
|
100
109
|
static entitiesAndOptsMapToQueries(entitiesAndOptsMap, em) {
|
|
101
110
|
return Array.from(entitiesAndOptsMap, async ([key, filterMap]) => {
|
|
102
111
|
const uniqueName = key.substring(0, key.indexOf('|'));
|
|
103
|
-
const opts =
|
|
112
|
+
const opts = groupOptions
|
|
113
|
+
.get(entitiesAndOptsMap)
|
|
114
|
+
?.get(key) ?? JSON.parse(key.substring(key.indexOf('|') + 1));
|
|
104
115
|
const meta = em.getMetadata().getByUniqueName(uniqueName);
|
|
105
116
|
const res = await em.find(meta.class, opts?.where != null && Object.keys(opts.where).length > 0
|
|
106
117
|
? {
|
|
@@ -168,7 +179,7 @@ export class DataloaderUtils {
|
|
|
168
179
|
// We need to filter the results in order to map each input collection
|
|
169
180
|
// to a subset of each query matching the collection items.
|
|
170
181
|
return collsWithOpts.map(([col, opts]) => {
|
|
171
|
-
const key = `${col.property.targetMeta.uniqueName}|${
|
|
182
|
+
const key = `${col.property.targetMeta.uniqueName}|${stringifyCacheKey(opts ?? {})}`;
|
|
172
183
|
const entities = resultsMap.get(key);
|
|
173
184
|
if (entities == null) {
|
|
174
185
|
// Should never happen
|
|
@@ -187,7 +198,7 @@ export class DataloaderUtils {
|
|
|
187
198
|
return async (collsWithOpts) => {
|
|
188
199
|
const groups = new Map();
|
|
189
200
|
for (const [col, opts] of collsWithOpts) {
|
|
190
|
-
const key = `${col.property.targetMeta.uniqueName}.${col.property.name}|${
|
|
201
|
+
const key = `${col.property.targetMeta.uniqueName}.${col.property.name}|${stringifyCacheKey(opts ?? {})}`;
|
|
191
202
|
const value = groups.get(key) ?? [];
|
|
192
203
|
value.push([col, opts ?? {}]);
|
|
193
204
|
groups.set(key, value);
|
|
@@ -252,7 +263,7 @@ export class DataloaderUtils {
|
|
|
252
263
|
// sharing a relation with the same property name (e.g. `Author.books` and `Publisher.books`)
|
|
253
264
|
// don't collide into a single batch that would use one owner's FK mapping for the other.
|
|
254
265
|
const ownerUniqueName = helper(col.owner).__meta.uniqueName;
|
|
255
|
-
const key = `${ownerUniqueName}.${prop.name}|${
|
|
266
|
+
const key = `${ownerUniqueName}.${prop.name}|${stringifyCacheKey(opts ?? {})}`;
|
|
256
267
|
keys.push(key);
|
|
257
268
|
let group = groups.get(key);
|
|
258
269
|
if (!group) {
|
|
@@ -260,7 +271,7 @@ export class DataloaderUtils {
|
|
|
260
271
|
groups.set(key, group);
|
|
261
272
|
}
|
|
262
273
|
const pk = helper(col.owner).getPrimaryKey();
|
|
263
|
-
group.ownerPKs.set(
|
|
274
|
+
group.ownerPKs.set(stringifyCacheKey(pk), pk);
|
|
264
275
|
}
|
|
265
276
|
const promises = Array.from(groups.entries()).map(async ([key, group]) => {
|
|
266
277
|
const allPKs = Array.from(group.ownerPKs.values());
|
package/utils/Utils.js
CHANGED
|
@@ -153,7 +153,7 @@ export function parseJsonSafe(value) {
|
|
|
153
153
|
/** Collection of general-purpose utility methods used throughout the ORM. */
|
|
154
154
|
export class Utils {
|
|
155
155
|
static PK_SEPARATOR = '~~~';
|
|
156
|
-
static #ORM_VERSION = '7.2.8-dev.
|
|
156
|
+
static #ORM_VERSION = '7.2.8-dev.10';
|
|
157
157
|
/** Default session variable name backing an RLS filter argument (`current_setting('mikro.<filter>.<arg>')`). */
|
|
158
158
|
static getRlsSettingName(filterName, argName) {
|
|
159
159
|
return `mikro.${filterName}.${argName}`;
|