@mikro-orm/core 7.2.8-dev.1 → 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 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(`${cacheKey}|${JSON.stringify(em.#sessionContext)}`);
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.1",
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
- // remove from referencing relations that are nullable
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 && prop2.nullable && Utils.isCollection(relation)) {
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
  }
@@ -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.1';
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}`;