@mikro-orm/core 7.2.7-dev.8 → 7.2.7

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.
@@ -817,11 +817,14 @@ export class MetadataDiscovery {
817
817
  else {
818
818
  pivotMeta2.compositePK = true;
819
819
  }
820
+ // STI entities share one table, so any m:n within the hierarchy is self-referencing, named after the root
821
+ const stiRoot = meta.root.inheritanceType === 'sti' ? meta.root : undefined;
822
+ const baseMeta = stiRoot ?? meta;
820
823
  // handle self-referenced m:n with same default field names
821
- if (meta.className === targetType &&
824
+ if ((meta.className === targetType || stiRoot === targetMeta.root) &&
822
825
  prop.joinColumns.every((joinColumn, idx) => joinColumn === prop.inverseJoinColumns[idx])) {
823
826
  // use tableName only when explicitly provided by user, otherwise use className for backwards compatibility
824
- const baseName = this.isExplicitTableName(meta) ? meta.tableName : meta.className;
827
+ const baseName = this.isExplicitTableName(baseMeta) ? baseMeta.tableName : baseMeta.className;
825
828
  prop.joinColumns = prop.referencedColumnNames.map(name => this.#namingStrategy.joinKeyColumnName(baseName + '_1', name, meta.compositePK));
826
829
  prop.inverseJoinColumns = prop.referencedColumnNames.map(name => this.#namingStrategy.joinKeyColumnName(baseName + '_2', name, meta.compositePK));
827
830
  if (prop.inversedBy) {
@@ -829,13 +832,18 @@ export class MetadataDiscovery {
829
832
  prop2.inverseJoinColumns = prop.joinColumns;
830
833
  prop2.joinColumns = prop.inverseJoinColumns;
831
834
  }
832
- // propagate updated joinColumns to all child entities that inherit this property (STI)
833
- for (const childMeta of this.#discovered.filter(m => m.root === meta && m !== meta)) {
835
+ // propagate updated joinColumns to all entities of the hierarchy that inherit this property or its inverse (STI)
836
+ for (const childMeta of this.#discovered.filter(m => m.root === baseMeta)) {
834
837
  const childProp = childMeta.properties[prop.name];
835
838
  if (childProp) {
836
839
  childProp.joinColumns = prop.joinColumns;
837
840
  childProp.inverseJoinColumns = prop.inverseJoinColumns;
838
841
  }
842
+ const childProp2 = prop.inversedBy ? childMeta.properties[prop.inversedBy] : undefined;
843
+ if (childProp2) {
844
+ childProp2.inverseJoinColumns = prop.joinColumns;
845
+ childProp2.joinColumns = prop.inverseJoinColumns;
846
+ }
839
847
  }
840
848
  }
841
849
  // Union-target polymorphic M:N: discriminator + target FK share the pivot across multiple target types
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/core",
3
- "version": "7.2.7-dev.8",
3
+ "version": "7.2.7",
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",
@@ -4,20 +4,50 @@ import { ReferenceKind } from '../enums.js';
4
4
  import { Reference } from '../entity/Reference.js';
5
5
  import { SerializationContext } from './SerializationContext.js';
6
6
  import { isRaw } from '../utils/RawQueryFragment.js';
7
- /** Returns true when any entry in `items` is `propName`, the wildcard `*`, or a dot-path under `propName`. */
8
- function matchesPath(items, propName) {
9
- return items.some(item => item === propName || item === '*' || item.startsWith(propName + '.'));
7
+ // Keyed by option objects created for a single `serialize()` call, so caller-owned options are never cached.
8
+ const preparedOptions = new WeakMap();
9
+ function createPathLookup(items) {
10
+ const heads = new Set();
11
+ let all = false;
12
+ for (const item of items) {
13
+ const dot = item.indexOf('.');
14
+ heads.add(dot === -1 ? item : item.substring(0, dot));
15
+ all ||= item === '*';
16
+ }
17
+ return { all, heads };
18
+ }
19
+ /**
20
+ * Copies caller options once per `serialize()` call, so the derived child options and path lookups can be cached
21
+ * for every entity of the call without ever caching an options object the caller might mutate later.
22
+ */
23
+ function prepareOptions(options) {
24
+ if (preparedOptions.has(options)) {
25
+ return options;
26
+ }
27
+ const prepared = {
28
+ ...options,
29
+ populate: options.populate ? Utils.asArray(options.populate) : options.populate,
30
+ };
31
+ preparedOptions.set(prepared, { children: new Map() });
32
+ return prepared;
33
+ }
34
+ function pathLookup(options, key) {
35
+ return (preparedOptions.get(options)[key] ??= createPathLookup(options[key]));
36
+ }
37
+ /** Returns true when any entry in `options[key]` is `propName`, the wildcard `*`, or a dot-path under `propName`. */
38
+ function matchesPath(options, key, propName) {
39
+ const lookup = pathLookup(options, key);
40
+ return lookup.all || lookup.heads.has(propName);
10
41
  }
11
42
  function isVisible(meta, propName, options) {
12
43
  const prop = meta.properties[propName];
13
44
  if (options.groups && prop?.groups) {
14
45
  return prop.groups.some(g => options.groups.includes(g));
15
46
  }
16
- if (Array.isArray(options.fields) && options.fields.length > 0 && !matchesPath(options.fields, propName)) {
47
+ if (Array.isArray(options.fields) && options.fields.length > 0 && !matchesPath(options, 'fields', propName)) {
17
48
  return false;
18
49
  }
19
- if (Array.isArray(options.populate) &&
20
- options.populate.find(item => item === propName || item.startsWith(propName + '.'))) {
50
+ if (Array.isArray(options.populate) && pathLookup(options, 'populate').heads.has(propName)) {
21
51
  return true;
22
52
  }
23
53
  if (options.exclude?.find(item => item === propName)) {
@@ -30,7 +60,7 @@ function isVisible(meta, propName, options) {
30
60
  function isPopulated(propName, options) {
31
61
  if (typeof options.populate !== 'boolean' &&
32
62
  Array.isArray(options.populate) &&
33
- matchesPath(options.populate, propName)) {
63
+ matchesPath(options, 'populate', propName)) {
34
64
  return true;
35
65
  }
36
66
  if (typeof options.populate === 'boolean') {
@@ -45,10 +75,10 @@ export class EntitySerializer {
45
75
  const wrapped = helper(entity);
46
76
  const meta = wrapped.__meta;
47
77
  let contextCreated = false;
78
+ options = prepareOptions(options);
48
79
  if (!wrapped.__serializationContext.root) {
49
80
  const root = new SerializationContext();
50
81
  SerializationContext.propagate(root, entity, (meta, prop) => meta.properties[prop]?.kind !== ReferenceKind.SCALAR);
51
- options.populate = (options.populate ? Utils.asArray(options.populate) : options.populate);
52
82
  contextCreated = true;
53
83
  }
54
84
  const root = wrapped.__serializationContext.root;
@@ -145,7 +175,8 @@ export class EntitySerializer {
145
175
  if (entity[prop] instanceof Function) {
146
176
  const returnValue = entity[prop]();
147
177
  if (!options.ignoreSerializers && serializer) {
148
- return serializer(returnValue, this.extractChildOptions(options, prop));
178
+ // copy, so a serializer mutating its options cannot leak into the cached child options
179
+ return serializer(returnValue, { ...this.extractChildOptions(options, prop) });
149
180
  }
150
181
  return returnValue;
151
182
  }
@@ -186,7 +217,12 @@ export class EntitySerializer {
186
217
  return prop.customType.toJSON(value, platform);
187
218
  }
188
219
  static extractChildOptions(options, prop) {
189
- return {
220
+ const prepared = preparedOptions.get(options);
221
+ const cached = prepared.children.get(prop);
222
+ if (cached) {
223
+ return cached;
224
+ }
225
+ const child = {
190
226
  ...options,
191
227
  populate: Array.isArray(options.populate)
192
228
  ? Utils.extractChildElements(options.populate, prop, '*')
@@ -194,6 +230,9 @@ export class EntitySerializer {
194
230
  exclude: Array.isArray(options.exclude) ? Utils.extractChildElements(options.exclude, prop) : options.exclude,
195
231
  fields: Array.isArray(options.fields) ? this.extractChildFields(options.fields, prop) : options.fields,
196
232
  };
233
+ prepared.children.set(prop, child);
234
+ preparedOptions.set(child, { children: new Map() });
235
+ return child;
197
236
  }
198
237
  /**
199
238
  * Extracts the nested `fields` whitelist for a child property. A bare parent name (`fields: ['books']`) or a
@@ -269,8 +308,9 @@ export class EntitySerializer {
269
308
  * ```
270
309
  */
271
310
  export function serialize(entities, options) {
311
+ const prepared = prepareOptions(options ?? {});
272
312
  if (Array.isArray(entities)) {
273
- return entities.map(e => EntitySerializer.serialize(e, options));
313
+ return entities.map(e => EntitySerializer.serialize(e, prepared));
274
314
  }
275
- return EntitySerializer.serialize(entities, options);
315
+ return EntitySerializer.serialize(entities, prepared);
276
316
  }
package/typings.d.ts CHANGED
@@ -565,6 +565,13 @@ export declare namespace LazyRef {
565
565
  [__lazyRef]?: (arg: T) => T;
566
566
  }
567
567
  }
568
+ /**
569
+ * The brand's only key is optional, so an object type without named properties (like `Record<string, number>`)
570
+ * extends `LazyRef.Brand<infer U>` too, with `U` inferred as `unknown`. `LazyRef<T>` needs `T extends object`,
571
+ * so an `unknown` target means the type is not a `LazyRef` and resolves to `Fallback`. `any` stays `any`.
572
+ * Only checked once `unknown extends U` holds, so a real `LazyRef` skips it.
573
+ */
574
+ type NotLazyRef<U, Fallback> = IsAny<U> extends true ? U : Fallback;
568
575
  /** Alias for `Reference<T> & { id: number }` (see {@apilink Ref}). */
569
576
  export type EntityRef<T extends object> = true extends IsUnknown<PrimaryProperty<T>> ? Reference<T> : IsAny<T> extends true ? Reference<T> : {
570
577
  [K in PrimaryProperty<T> & keyof T]: T[K];
@@ -1618,7 +1625,8 @@ export type UnboxArray<T> = T extends any[] ? ArrayElement<T> : T;
1618
1625
  /** Extracts the element type from an array type. */
1619
1626
  export type ArrayElement<ArrayType extends unknown[]> = ArrayType extends (infer ElementType)[] ? ElementType : never;
1620
1627
  /** Unwraps a property type from its wrapper (Reference, Collection, or array) to the inner entity type. */
1621
- export type ExpandProperty<T> = T extends LazyRef.Brand<infer U> ? NonNullable<U> : T extends ReferenceShape<infer U> ? NonNullable<U> : T extends CollectionShape<infer U> ? NonNullable<U> : T extends (infer U)[] ? NonNullable<U> : NonNullable<T>;
1628
+ export type ExpandProperty<T> = ExpandPropertyOf<T, T>;
1629
+ type ExpandPropertyOf<T, R> = T extends LazyRef.Brand<infer U> ? unknown extends U ? NotLazyRef<U, NonNullable<R>> : NonNullable<U> : T extends ReferenceShape<infer U> ? NonNullable<U> : T extends CollectionShape<infer U> ? NonNullable<U> : T extends (infer U)[] ? NonNullable<U> : NonNullable<T>;
1622
1630
  type LoadedLoadable<T, E extends object> = T extends CollectionShape ? LoadedCollection<E> : T extends ScalarReference<infer U> ? LoadedScalarReference<U> : T extends LazyRef.Brand<any> ? E : T extends ReferenceShape ? T & LoadedReference<E> : T extends Scalar ? T : T extends (infer U)[] ? U extends Scalar ? T : E[] : E;
1623
1631
  type IsTrue<T> = IsNever<T> extends true ? false : T extends boolean ? (T extends true ? true : false) : false;
1624
1632
  type StringLiteral<T> = T extends string ? (string extends T ? never : T) : never;
@@ -1653,7 +1661,12 @@ export type MergeSelected<T, U, F extends string> = IsLoadedType<T> extends true
1653
1661
  export type MergeLoaded<T, U, P extends string, F extends string, E extends string, R extends boolean = false> = IsLoadedType<T> extends true ? T & Loaded<U, P, F, E> : Loaded<T, P, F, E>;
1654
1662
  /** Extracts the nullability modifiers (`null`, `undefined`, or both) from a type `T`. */
1655
1663
  export type AddOptional<T> = undefined | null extends T ? null | undefined : null extends T ? null : undefined extends T ? undefined : never;
1656
- type LoadedProp<T, L extends string = never, F extends string = '*', E extends string = never> = LoadedLoadable<T, Loaded<ExtractTypeWithLazyRef<T>, L, F, E>>;
1664
+ /**
1665
+ * Like {@link ExtractTypeWithLazyRef}, but a record type that matched `LazyRef.Brand` resolves to itself (see
1666
+ * {@link NotLazyRef}). `AutoPath` keeps the unguarded version, as it is on its hot path.
1667
+ */
1668
+ type ExtractLoadedType<T> = T extends LazyRef.Brand<infer U> ? (unknown extends U ? NotLazyRef<U, ExtractType<T>> : U) : ExtractType<T>;
1669
+ type LoadedProp<T, L extends string = never, F extends string = '*', E extends string = never> = LoadedLoadable<T, Loaded<ExtractLoadedType<T>, L, F, E>>;
1657
1670
  /** Extracts the eager-loaded property names declared via `[EagerProps]` as a string union. */
1658
1671
  export type AddEager<T> = ExtractEagerProps<T> & string;
1659
1672
  /** Combines an explicit populate hint `L` with the entity's eagerly loaded properties. */
@@ -3,7 +3,7 @@ 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
- import { isRaw, Raw } from './RawQueryFragment.js';
6
+ import { isRaw, isSubquery, Raw } from './RawQueryFragment.js';
7
7
  import { MetadataError, ValidationError } from '../errors.js';
8
8
  /** @internal */
9
9
  export class QueryHelper {
@@ -458,7 +458,8 @@ export class QueryHelper {
458
458
  if (Array.isArray(cond) && !(key && ARRAY_OPERATORS.includes(key))) {
459
459
  return cond.map(v => QueryHelper.processCustomType(prop, v, platform, key, fromQuery));
460
460
  }
461
- if (isRaw(cond)) {
461
+ // a query builder renders as a sub-query, not as a value of the property type
462
+ if (isRaw(cond) || isSubquery(cond)) {
462
463
  return cond;
463
464
  }
464
465
  return prop.customType.convertToDatabaseValue(cond, platform, { fromQuery, key, mode: 'query' });
@@ -1,11 +1,15 @@
1
- import type { AnyString, Dictionary, EntityKey } from '../typings.js';
1
+ import type { AnyString, Dictionary, EntityKey, Subquery } from '../typings.js';
2
2
  declare const rawFragmentSymbolBrand: unique symbol;
3
3
  /** Branded symbol type used as a unique key for tracking raw SQL fragments in object properties. */
4
4
  export type RawQueryFragmentSymbol = symbol & {
5
5
  readonly [rawFragmentSymbolBrand]: true;
6
6
  };
7
+ /** Prototype brand of query builders usable as sub-queries; a symbol so JSON payloads cannot carry it, `Symbol.for` so package copies agree. */
8
+ export declare const SUBQUERY_BRAND: unique symbol;
7
9
  /** Checks whether the given value is a `RawQueryFragment` instance. */
8
10
  export declare function isRaw(value: unknown): value is RawQueryFragment;
11
+ /** Checks whether the given value is a query builder usable as a sub-query (its prototype carries the `SUBQUERY_BRAND`). */
12
+ export declare function isSubquery(value: unknown): value is Subquery;
9
13
  /** Represents a raw SQL fragment with optional parameters, usable as both a value and an object key via Symbol coercion. */
10
14
  export declare class RawQueryFragment<Alias extends string = string> {
11
15
  #private;
@@ -1,11 +1,6 @@
1
1
  import { Utils } from './Utils.js';
2
- // Brand lives on the prototype so JSON payloads — whose proto is
3
- // `Object.prototype` — cannot forge it. String key, not a `Symbol.for(...)`,
4
- // so each CJS/ESM copy of this module independently installs it on its own
5
- // prototype without publishing a global key for the marker that controls raw
6
- // SQL assembly. The string is namespaced so it does not collide with property
7
- // names users might independently install on their own prototypes.
8
- const RAW_FRAGMENT_BRAND = '__mikroOrmRawFragment';
2
+ // Prototype brand of raw fragments; `Symbol.for` so package copies agree.
3
+ const RAW_FRAGMENT_BRAND = Symbol.for('mikro-orm.raw-fragment');
9
4
  // Back-references from a fragment's symbol key to the fragment itself, shared
10
5
  // across CJS/ESM module copies via globalThis: when one copy creates a fragment
11
6
  // via `raw('…')` and stores its symbol in a where-clause object key, the other
@@ -13,6 +8,20 @@ const RAW_FRAGMENT_BRAND = '__mikroOrmRawFragment';
13
8
  const REGISTRY_KEY = Symbol.for('@mikro-orm/core/RawQueryFragment.references');
14
9
  const rawQueryReferences = (globalThis[REGISTRY_KEY] ??=
15
10
  new WeakMap());
11
+ /** Prototype brand of query builders usable as sub-queries; a symbol so JSON payloads cannot carry it, `Symbol.for` so package copies agree. */
12
+ export const SUBQUERY_BRAND = Symbol.for('mikro-orm.subquery');
13
+ function hasPrototypeBrand(value, brand) {
14
+ // Walk the prototype chain starting from the *prototype* (not the value) so
15
+ // own-property spoofing from JSON payloads cannot forge the brand. Stop at
16
+ // `Object.prototype`, which is never branded — that bails plain objects,
17
+ // JSON payloads, and built-ins like `Date`/`Array`/`Map` at depth 1.
18
+ for (let p = Object.getPrototypeOf(value); p != null && p !== Object.prototype; p = Object.getPrototypeOf(p)) {
19
+ if (Object.hasOwn(p, brand)) {
20
+ return true;
21
+ }
22
+ }
23
+ return false;
24
+ }
16
25
  /** Checks whether the given value is a `RawQueryFragment` instance. */
17
26
  export function isRaw(value) {
18
27
  if (value == null || typeof value !== 'object') {
@@ -23,16 +32,11 @@ export function isRaw(value) {
23
32
  if (value instanceof RawQueryFragment) {
24
33
  return true;
25
34
  }
26
- // Walk the prototype chain starting from the *prototype* (not the value) so
27
- // own-property spoofing from JSON payloads cannot forge the brand. Stop at
28
- // `Object.prototype`, which is never branded — that bails plain objects,
29
- // JSON payloads, and built-ins like `Date`/`Array`/`Map` at depth 1.
30
- for (let p = Object.getPrototypeOf(value); p != null && p !== Object.prototype; p = Object.getPrototypeOf(p)) {
31
- if (Object.hasOwn(p, RAW_FRAGMENT_BRAND)) {
32
- return true;
33
- }
34
- }
35
- return false;
35
+ return hasPrototypeBrand(value, RAW_FRAGMENT_BRAND);
36
+ }
37
+ /** Checks whether the given value is a query builder usable as a sub-query (its prototype carries the `SUBQUERY_BRAND`). */
38
+ export function isSubquery(value) {
39
+ return value != null && typeof value === 'object' && hasPrototypeBrand(value, SUBQUERY_BRAND);
36
40
  }
37
41
  /** Represents a raw SQL fragment with optional parameters, usable as both a value and an object key via Symbol coercion. */
38
42
  export class RawQueryFragment {
@@ -121,6 +125,10 @@ export { RawQueryFragment as Raw };
121
125
  export const ALIAS_REPLACEMENT = '[::alias::]';
122
126
  /** @internal */
123
127
  export const ALIAS_REPLACEMENT_RE = '\\[::alias::\\]';
128
+ // a query builder param is embedded as a sub-query instead of being bound as a value
129
+ function toRawParam(param) {
130
+ return isSubquery(param) ? param.toRaw() : param;
131
+ }
124
132
  /**
125
133
  * Creates raw SQL query fragment that can be assigned to a property or part of a filter. This fragment is represented
126
134
  * by `RawQueryFragment` class instance that can be serialized to a string, so it can be used both as an object value
@@ -204,12 +212,12 @@ export function raw(sql, params) {
204
212
  if (!Object.hasOwn(dict, key)) {
205
213
  return match;
206
214
  }
207
- objectParams.push(dict[key]);
215
+ objectParams.push(toRawParam(dict[key]));
208
216
  return identifier ? '??' : '?';
209
217
  });
210
218
  return new RawQueryFragment(sql, objectParams);
211
219
  }
212
- return new RawQueryFragment(sql, params);
220
+ return new RawQueryFragment(sql, params?.map(toRawParam));
213
221
  }
214
222
  /**
215
223
  * Alternative to the `raw()` helper allowing to use it as a tagged template function for the simple cases.
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.7-dev.8';
156
+ static #ORM_VERSION = '7.2.7';
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}`;
@@ -244,7 +244,7 @@ export class Utils {
244
244
  /**
245
245
  * Merges all sources into the target recursively.
246
246
  */
247
- static _merge(target, sources, ignoreUndefined) {
247
+ static _merge(target, sources, ignoreUndefined, copies) {
248
248
  if (!sources.length) {
249
249
  return target;
250
250
  }
@@ -259,7 +259,13 @@ export class Utils {
259
259
  }
260
260
  if (Utils.isPlainObject(value)) {
261
261
  if (!Utils.isObject(target[key])) {
262
- target[key] = Utils.copy(value);
262
+ // copy plain objects but keep class instances by reference, tracking copies so cycles terminate
263
+ const seen = copies ?? new WeakMap();
264
+ if (!seen.has(value)) {
265
+ seen.set(value, {});
266
+ Utils._merge(seen.get(value), [value], ignoreUndefined, seen);
267
+ }
268
+ target[key] = seen.get(value);
263
269
  continue;
264
270
  }
265
271
  /* v8 ignore next */