@mikro-orm/core 7.2.7-dev.0 → 7.2.7-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 +6 -1
- package/entity/EntityLoader.js +10 -2
- package/package.json +1 -1
- package/platforms/Platform.d.ts +2 -0
- package/platforms/Platform.js +4 -0
- package/serialization/EntitySerializer.js +52 -12
- package/typings.d.ts +21 -5
- package/unit-of-work/UnitOfWork.js +13 -5
- package/utils/Utils.js +9 -3
package/EntityManager.js
CHANGED
|
@@ -743,10 +743,15 @@ export class EntityManager {
|
|
|
743
743
|
if (Utils.isEmpty(options.orderBy) && !Raw.hasObjectFragments(options.orderBy)) {
|
|
744
744
|
throw new Error('Explicit `orderBy` option required');
|
|
745
745
|
}
|
|
746
|
+
const meta = this.metadata.get(entityName);
|
|
747
|
+
// same merge as em.find, so the cursor and the query see the same orderBy
|
|
748
|
+
if (meta.orderBy) {
|
|
749
|
+
options.orderBy = QueryHelper.mergeOrderBy(options.orderBy, meta.orderBy);
|
|
750
|
+
}
|
|
746
751
|
const [entities, count] = options.includeCount !== false
|
|
747
752
|
? await em.findAndCount(entityName, options.where, options)
|
|
748
753
|
: [await em.find(entityName, options.where, options)];
|
|
749
|
-
return new Cursor(entities, count, options,
|
|
754
|
+
return new Cursor(entities, count, options, meta);
|
|
750
755
|
}
|
|
751
756
|
/**
|
|
752
757
|
* Refreshes the persistent state of an entity from the database, overriding any local changes that have not yet been
|
package/entity/EntityLoader.js
CHANGED
|
@@ -679,12 +679,20 @@ export class EntityLoader {
|
|
|
679
679
|
const subCond = Utils.isPlainObject(where[prop.name]) ? { ...where[prop.name] } : {};
|
|
680
680
|
const meta2 = prop.targetMeta;
|
|
681
681
|
const pk = Utils.getPrimaryKeyHash(meta2.primaryKeys);
|
|
682
|
+
// `$not` wrapping entity conditions stays on the target entity, only operators on the relation itself map to its PK
|
|
683
|
+
const isRelationOperator = (key, value) => {
|
|
684
|
+
if (key === '$not' && Utils.isPlainObject(value)) {
|
|
685
|
+
return Utils.getObjectQueryKeys(value).every(k => Utils.isOperator(k, false));
|
|
686
|
+
}
|
|
687
|
+
return Utils.isOperator(key, false);
|
|
688
|
+
};
|
|
682
689
|
['$and', '$or'].forEach(op => {
|
|
683
690
|
if (where[op]) {
|
|
684
691
|
const child = where[op]
|
|
685
692
|
.map((cond) => cond[prop.name])
|
|
686
693
|
.filter((sub) => sub != null &&
|
|
687
|
-
!(Utils.isPlainObject(sub) &&
|
|
694
|
+
!(Utils.isPlainObject(sub) &&
|
|
695
|
+
Utils.getObjectQueryKeys(sub).every(key => isRelationOperator(key, sub[key]))))
|
|
688
696
|
.map((cond) => {
|
|
689
697
|
if (Utils.isPrimaryKey(cond)) {
|
|
690
698
|
return { [pk]: cond };
|
|
@@ -697,7 +705,7 @@ export class EntityLoader {
|
|
|
697
705
|
}
|
|
698
706
|
}
|
|
699
707
|
});
|
|
700
|
-
const operators = Object.keys(subCond).filter(key =>
|
|
708
|
+
const operators = Object.keys(subCond).filter(key => isRelationOperator(key, subCond[key]));
|
|
701
709
|
if (operators.length > 0) {
|
|
702
710
|
subCond[pk] = Utils.isPlainObject(subCond[pk]) ? { ...subCond[pk] } : (subCond[pk] ?? {});
|
|
703
711
|
operators.forEach(op => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/core",
|
|
3
|
-
"version": "7.2.7-dev.
|
|
3
|
+
"version": "7.2.7-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",
|
package/platforms/Platform.d.ts
CHANGED
|
@@ -204,6 +204,8 @@ export declare abstract class Platform {
|
|
|
204
204
|
supportsUnionWhere(): boolean;
|
|
205
205
|
/** Returns the SQL type declaration used for array storage. */
|
|
206
206
|
getArrayDeclarationSQL(): string;
|
|
207
|
+
/** Whether array columns support the `&&` (overlap) operator. */
|
|
208
|
+
supportsArrayOverlap(): boolean;
|
|
207
209
|
/** Serializes a string array into its database storage format. */
|
|
208
210
|
marshallArray(values: string[]): string;
|
|
209
211
|
/** Deserializes a database-stored array string back into a string array. */
|
package/platforms/Platform.js
CHANGED
|
@@ -360,6 +360,10 @@ export class Platform {
|
|
|
360
360
|
getArrayDeclarationSQL() {
|
|
361
361
|
return 'text';
|
|
362
362
|
}
|
|
363
|
+
/** Whether array columns support the `&&` (overlap) operator. */
|
|
364
|
+
supportsArrayOverlap() {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
363
367
|
/** Serializes a string array into its database storage format. */
|
|
364
368
|
marshallArray(values) {
|
|
365
369
|
return values.map(v => (/[,",\\]/.test(v) ? JSON.stringify(v) : v)).join(',');
|
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
313
|
+
return entities.map(e => EntitySerializer.serialize(e, prepared));
|
|
274
314
|
}
|
|
275
|
-
return EntitySerializer.serialize(entities,
|
|
315
|
+
return EntitySerializer.serialize(entities, prepared);
|
|
276
316
|
}
|
package/typings.d.ts
CHANGED
|
@@ -356,12 +356,15 @@ type ElemMatchCondition<T extends Record<string, any>> = {
|
|
|
356
356
|
$and?: ElemMatchCondition<T>[];
|
|
357
357
|
$not?: ElemMatchCondition<T>;
|
|
358
358
|
};
|
|
359
|
-
type
|
|
359
|
+
type ArrayFilter<T> = T extends readonly (infer E)[] ? [E] extends [Scalar] ? {
|
|
360
|
+
$in?: readonly (readonly ExpandScalar<E>[])[];
|
|
361
|
+
$nin?: readonly (readonly ExpandScalar<E>[])[];
|
|
362
|
+
} : E extends Record<string, any> ? {
|
|
360
363
|
$elemMatch: ElemMatchCondition<E>;
|
|
361
364
|
} : never : never;
|
|
362
365
|
/** Object form of a filter query, mapping entity keys to their filter conditions. */
|
|
363
366
|
export type FilterObject<T> = {
|
|
364
|
-
-readonly [K in EntityKey<T>]?: ExpandQuery<ExpandProperty<FilterObjectProp<T, K>>> | ExpandQueryMerged<ExpandProperty<FilterObjectProp<T, K>>> | FilterValue<ExpandProperty<FilterObjectProp<T, K>>> |
|
|
367
|
+
-readonly [K in EntityKey<T>]?: ExpandQuery<ExpandProperty<FilterObjectProp<T, K>>> | ExpandQueryMerged<ExpandProperty<FilterObjectProp<T, K>>> | FilterValue<ExpandProperty<FilterObjectProp<T, K>>> | ArrayFilter<FilterObjectProp<T, K>> | null;
|
|
365
368
|
};
|
|
366
369
|
/** Recursively expands a type into its `FilterQuery` form for nested object filtering. */
|
|
367
370
|
export type ExpandQuery<T> = T extends object ? (T extends Scalar ? never : FilterQuery<T>) : FilterValue<T>;
|
|
@@ -381,7 +384,7 @@ export type FilterQuery<T> = ObjectQuery<T> | NonNullable<ExpandScalar<Primary<T
|
|
|
381
384
|
* Used when `using` option is set in `FindOptions` to enforce type-safe index usage.
|
|
382
385
|
*/
|
|
383
386
|
export type IndexFilterQuery<T, Using extends string> = [Using] extends [never] ? FilterQuery<T> : (OperatorMap<T> & {
|
|
384
|
-
-readonly [K in Extract<EntityKey<T>, IndexColumns<T, Using>>]?: ExpandQuery<ExpandProperty<FilterObjectProp<T, K>>> | ExpandQueryMerged<ExpandProperty<FilterObjectProp<T, K>>> | FilterValue<ExpandProperty<FilterObjectProp<T, K>>> |
|
|
387
|
+
-readonly [K in Extract<EntityKey<T>, IndexColumns<T, Using>>]?: ExpandQuery<ExpandProperty<FilterObjectProp<T, K>>> | ExpandQueryMerged<ExpandProperty<FilterObjectProp<T, K>>> | FilterValue<ExpandProperty<FilterObjectProp<T, K>>> | ArrayFilter<FilterObjectProp<T, K>> | null;
|
|
385
388
|
}) | NonNullable<ExpandScalar<Primary<T>>> | IndexFilterQuery<T, Using>[];
|
|
386
389
|
/** Replaces `where` and `using` on an options type with index-aware variants when `Using` is specified. */
|
|
387
390
|
export type WithUsingOptions<Opts, Entity, Using extends string> = Omit<Opts, 'where' | 'using'> & {
|
|
@@ -562,6 +565,13 @@ export declare namespace LazyRef {
|
|
|
562
565
|
[__lazyRef]?: (arg: T) => T;
|
|
563
566
|
}
|
|
564
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;
|
|
565
575
|
/** Alias for `Reference<T> & { id: number }` (see {@apilink Ref}). */
|
|
566
576
|
export type EntityRef<T extends object> = true extends IsUnknown<PrimaryProperty<T>> ? Reference<T> : IsAny<T> extends true ? Reference<T> : {
|
|
567
577
|
[K in PrimaryProperty<T> & keyof T]: T[K];
|
|
@@ -1615,7 +1625,8 @@ export type UnboxArray<T> = T extends any[] ? ArrayElement<T> : T;
|
|
|
1615
1625
|
/** Extracts the element type from an array type. */
|
|
1616
1626
|
export type ArrayElement<ArrayType extends unknown[]> = ArrayType extends (infer ElementType)[] ? ElementType : never;
|
|
1617
1627
|
/** Unwraps a property type from its wrapper (Reference, Collection, or array) to the inner entity type. */
|
|
1618
|
-
export type ExpandProperty<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>;
|
|
1619
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;
|
|
1620
1631
|
type IsTrue<T> = IsNever<T> extends true ? false : T extends boolean ? (T extends true ? true : false) : false;
|
|
1621
1632
|
type StringLiteral<T> = T extends string ? (string extends T ? never : T) : never;
|
|
@@ -1650,7 +1661,12 @@ export type MergeSelected<T, U, F extends string> = IsLoadedType<T> extends true
|
|
|
1650
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>;
|
|
1651
1662
|
/** Extracts the nullability modifiers (`null`, `undefined`, or both) from a type `T`. */
|
|
1652
1663
|
export type AddOptional<T> = undefined | null extends T ? null | undefined : null extends T ? null : undefined extends T ? undefined : never;
|
|
1653
|
-
|
|
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>>;
|
|
1654
1670
|
/** Extracts the eager-loaded property names declared via `[EagerProps]` as a string union. */
|
|
1655
1671
|
export type AddEager<T> = ExtractEagerProps<T> & string;
|
|
1656
1672
|
/** Combines an explicit populate hint `L` with the entity's eagerly loaded properties. */
|
|
@@ -600,19 +600,27 @@ export class UnitOfWork {
|
|
|
600
600
|
this.findEarlyUpdates(cs, inserts[cs.meta.uniqueName]);
|
|
601
601
|
}
|
|
602
602
|
}
|
|
603
|
+
// hashes of the inserts' PKs and unique values per type, built lazily, so matching a removed entity is a set lookup
|
|
604
|
+
const insertHashes = {};
|
|
603
605
|
for (const entity of this.#removeStack) {
|
|
604
606
|
const wrapped = helper(entity);
|
|
605
607
|
/* v8 ignore next */
|
|
606
608
|
if (wrapped.__processing) {
|
|
607
609
|
continue;
|
|
608
610
|
}
|
|
609
|
-
const
|
|
610
|
-
let
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
611
|
+
const uniqueName = wrapped.__meta.uniqueName;
|
|
612
|
+
let hashes = insertHashes[uniqueName];
|
|
613
|
+
if (!hashes) {
|
|
614
|
+
hashes = insertHashes[uniqueName] = new Set();
|
|
615
|
+
for (const cs of inserts[uniqueName] ?? []) {
|
|
616
|
+
hashes.add(cs.getSerializedPrimaryKey());
|
|
617
|
+
for (const hash of this.expandUniqueProps(cs.entity)) {
|
|
618
|
+
hashes.add(hash);
|
|
619
|
+
}
|
|
614
620
|
}
|
|
615
621
|
}
|
|
622
|
+
const deletePkHash = [wrapped.getSerializedPrimaryKey(), ...this.expandUniqueProps(entity)];
|
|
623
|
+
const type = deletePkHash.some(hash => hashes.has(hash)) ? ChangeSetType.DELETE_EARLY : ChangeSetType.DELETE;
|
|
616
624
|
this.computeChangeSet(entity, type);
|
|
617
625
|
}
|
|
618
626
|
}
|
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.
|
|
156
|
+
static #ORM_VERSION = '7.2.7-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}`;
|
|
@@ -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
|
-
|
|
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 */
|