@mikro-orm/core 7.2.8-dev.2 → 7.2.8-dev.3

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 CHANGED
@@ -19,23 +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
- // JSON loses RegExp patterns, `raw()` params and `raw()` keys (symbols); `this[key]` is the value before `toJSON()`
23
- function cacheKeyReplacer(key, value) {
24
- const original = this[key];
25
- if (original instanceof RegExp) {
26
- return { regexp: original.source, flags: original.flags };
27
- }
28
- if (isRaw(original)) {
29
- return { sql: original.sql, params: original.params };
30
- }
31
- if (Raw.hasObjectFragments(value)) {
32
- return Utils.getObjectQueryKeys(value).map(k => [
33
- Raw.getKnownFragment(k) ?? k,
34
- value[k],
35
- ]);
36
- }
37
- return value;
38
- }
22
+ import { scopeCacheKey, stringifyCacheKey } from './cache/cache-key.js';
39
23
  /**
40
24
  * The EntityManager is the central access point to ORM functionality. It is a facade to all different ORM subsystems
41
25
  * such as UnitOfWork, Query Language, and Repository API.
@@ -2475,11 +2459,7 @@ export class EntityManager {
2475
2459
  const em = this.getContext();
2476
2460
  // a named cache key (`config[0]`) discards the computed `key`, which already carries the session context — so
2477
2461
  // scope it here too, otherwise a fork's rows would be served to another session context under the same name
2478
- const cacheKey = Array.isArray(config)
2479
- ? em.#sessionContext
2480
- ? `${config[0]}|${JSON.stringify(em.#sessionContext)}`
2481
- : config[0]
2482
- : JSON.stringify(key, cacheKeyReplacer);
2462
+ const cacheKey = Array.isArray(config) ? scopeCacheKey(config[0], em.#sessionContext) : stringifyCacheKey(key);
2483
2463
  const cached = await em.#resultCache.get(cacheKey);
2484
2464
  if (!cached) {
2485
2465
  return { key: cacheKey, data: cached };
@@ -2532,7 +2512,7 @@ export class EntityManager {
2532
2512
  await em.#resultCache.remove(cacheKey);
2533
2513
  // named keys are scoped by the session context (see `tryCache`), so clear this context's variant too
2534
2514
  if (em.#sessionContext) {
2535
- await em.#resultCache.remove(`${cacheKey}|${JSON.stringify(em.#sessionContext)}`);
2515
+ await em.#resultCache.remove(scopeCacheKey(cacheKey, em.#sessionContext));
2536
2516
  }
2537
2517
  }
2538
2518
  /**
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/core",
3
- "version": "7.2.8-dev.2",
3
+ "version": "7.2.8-dev.3",
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",
@@ -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}|${JSON.stringify(opts ?? {})}`;
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 = JSON.parse(key.substring(key.indexOf('|') + 1));
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}|${JSON.stringify(opts ?? {})}`;
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 = JSON.parse(key.substring(key.indexOf('|') + 1));
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}|${JSON.stringify(opts ?? {})}`;
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}|${JSON.stringify(opts ?? {})}`;
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}|${JSON.stringify(opts ?? {})}`;
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(JSON.stringify(pk), pk);
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.2';
156
+ static #ORM_VERSION = '7.2.8-dev.3';
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}`;