@mikro-orm/core 7.2.0-dev.3 → 7.2.0-dev.5
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/cache/CacheAdapter.d.ts +6 -4
- package/cache/FileCacheAdapter.d.ts +1 -1
- package/cache/FileCacheAdapter.js +6 -1
- package/drivers/DatabaseDriver.d.ts +7 -0
- package/drivers/DatabaseDriver.js +72 -9
- package/hydration/ObjectHydrator.js +3 -0
- package/metadata/EntitySchema.js +5 -2
- package/metadata/MetadataDiscovery.js +28 -10
- package/metadata/MetadataProvider.js +1 -1
- package/metadata/MetadataStorage.d.ts +4 -3
- package/metadata/MetadataStorage.js +15 -1
- package/metadata/Routine.js +4 -1
- package/naming-strategy/AbstractNamingStrategy.js +2 -1
- package/naming-strategy/NamingStrategy.d.ts +2 -1
- package/package.json +1 -1
- package/platforms/Platform.d.ts +2 -0
- package/platforms/Platform.js +4 -0
- package/typings.d.ts +2 -0
- package/unit-of-work/ChangeSet.js +10 -7
- package/unit-of-work/ChangeSetPersister.js +15 -7
- package/utils/AbstractMigrator.d.ts +1 -1
- package/utils/AbstractMigrator.js +3 -2
- package/utils/Configuration.js +2 -1
- package/utils/Cursor.js +1 -6
- package/utils/EntityComparator.js +4 -1
- package/utils/QueryHelper.d.ts +5 -0
- package/utils/QueryHelper.js +25 -0
- package/utils/TransactionManager.d.ts +6 -0
- package/utils/TransactionManager.js +35 -2
- package/utils/Utils.js +6 -3
- package/utils/clone.js +6 -0
package/cache/CacheAdapter.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/** Interface for async-capable cache storage used by result cache and metadata cache. */
|
|
2
2
|
export interface CacheAdapter {
|
|
3
3
|
/**
|
|
4
|
-
* Gets the items under `name` key from the cache.
|
|
4
|
+
* Gets the items under `name` key from the cache. When `origin` is provided, adapters that
|
|
5
|
+
* track the entry origin should ignore entries cached from a different source file.
|
|
5
6
|
*/
|
|
6
|
-
get<T = any>(name: string): T | Promise<T | undefined> | undefined;
|
|
7
|
+
get<T = any>(name: string, origin?: string): T | Promise<T | undefined> | undefined;
|
|
7
8
|
/**
|
|
8
9
|
* Sets the item to the cache. `origin` is used for cache invalidation and should reflect the change in data.
|
|
9
10
|
*/
|
|
@@ -24,9 +25,10 @@ export interface CacheAdapter {
|
|
|
24
25
|
/** Synchronous variant of CacheAdapter, used for metadata cache where async access is not needed. */
|
|
25
26
|
export interface SyncCacheAdapter extends CacheAdapter {
|
|
26
27
|
/**
|
|
27
|
-
* Gets the items under `name` key from the cache.
|
|
28
|
+
* Gets the items under `name` key from the cache. When `origin` is provided, adapters that
|
|
29
|
+
* track the entry origin should ignore entries cached from a different source file.
|
|
28
30
|
*/
|
|
29
|
-
get<T = any>(name: string): T | undefined;
|
|
31
|
+
get<T = any>(name: string, origin?: string): T | undefined;
|
|
30
32
|
/**
|
|
31
33
|
* Sets the item to the cache. `origin` is used for cache invalidation and should reflect the change in data.
|
|
32
34
|
*/
|
|
@@ -16,12 +16,17 @@ export class FileCacheAdapter {
|
|
|
16
16
|
/**
|
|
17
17
|
* @inheritDoc
|
|
18
18
|
*/
|
|
19
|
-
get(name) {
|
|
19
|
+
get(name, origin) {
|
|
20
20
|
const path = this.path(name);
|
|
21
21
|
if (!existsSync(path)) {
|
|
22
22
|
return null;
|
|
23
23
|
}
|
|
24
24
|
const payload = fs.readJSONSync(path);
|
|
25
|
+
// Two classes with the same name share the cache key, ignore entries cached
|
|
26
|
+
// from a different source file.
|
|
27
|
+
if (origin && fs.absolutePath(payload.origin, this.#baseDir) !== fs.absolutePath(origin, this.#baseDir)) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
25
30
|
const hash = this.getHash(payload.origin);
|
|
26
31
|
if (!hash || payload.hash !== hash) {
|
|
27
32
|
return null;
|
|
@@ -64,6 +64,13 @@ export declare abstract class DatabaseDriver<C extends Connection> implements ID
|
|
|
64
64
|
orderBy: OrderDefinition<T>[];
|
|
65
65
|
where: FilterQuery<T>;
|
|
66
66
|
};
|
|
67
|
+
/**
|
|
68
|
+
* Restores the JS value of a single cursor offset: ISO strings become `Date` instances based on the
|
|
69
|
+
* property type (never based on the string shape alone), and custom types are restored via
|
|
70
|
+
* `convertToJSValue`. Values compared against a JSON document keep their serialized form instead,
|
|
71
|
+
* unless the platform preserves native date types inside JSON documents (mongo).
|
|
72
|
+
*/
|
|
73
|
+
private mapCursorOffset;
|
|
67
74
|
protected createCursorCondition<T extends object>(definition: (readonly [keyof T & string, QueryOrder])[], offsets: Dictionary[], inverse: boolean, meta: EntityMetadata<T>): FilterQuery<T>;
|
|
68
75
|
/** @internal */
|
|
69
76
|
mapDataToFieldNames(data: Dictionary, stringifyJsonArrays: boolean, properties?: Record<string, EntityProperty>, convertCustomTypes?: boolean, object?: boolean): Dictionary;
|
|
@@ -7,8 +7,11 @@ import { EntityManager } from '../EntityManager.js';
|
|
|
7
7
|
import { CursorError, ValidationError } from '../errors.js';
|
|
8
8
|
import { DriverException } from '../exceptions.js';
|
|
9
9
|
import { helper } from '../entity/wrap.js';
|
|
10
|
+
import { Reference } from '../entity/Reference.js';
|
|
10
11
|
import { PolymorphicRef } from '../entity/PolymorphicRef.js';
|
|
11
12
|
import { JsonType } from '../types/JsonType.js';
|
|
13
|
+
import { DateTimeType } from '../types/DateTimeType.js';
|
|
14
|
+
import { QueryHelper } from '../utils/QueryHelper.js';
|
|
12
15
|
import { MikroORM } from '../MikroORM.js';
|
|
13
16
|
/** Abstract base class for all database drivers, implementing common driver logic. */
|
|
14
17
|
export class DatabaseDriver {
|
|
@@ -141,13 +144,24 @@ export class DatabaseDriver {
|
|
|
141
144
|
return !!val && typeof val === 'object' && key in val;
|
|
142
145
|
};
|
|
143
146
|
const createCursor = (val, key, inverse = false) => {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
+
const def = Reference.unwrapReference((isCursor(val, key) ? val[key] : val));
|
|
148
|
+
let offsets;
|
|
149
|
+
// entity (and reference) instances are supported as cursors too, their properties are read the same way
|
|
150
|
+
if (Utils.isPlainObject(def) || Utils.isEntity(def)) {
|
|
151
|
+
// POJO values are already JS values, extract them ordered per the definition,
|
|
152
|
+
// without the JSON round trip `Cursor.for` + `Cursor.decode` would impose
|
|
153
|
+
offsets = definition.map(([key]) => {
|
|
154
|
+
if (def[key] === undefined) {
|
|
155
|
+
throw CursorError.missingValue(meta.className, key);
|
|
156
|
+
}
|
|
157
|
+
return def[key];
|
|
158
|
+
});
|
|
147
159
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
160
|
+
else {
|
|
161
|
+
/* v8 ignore next */
|
|
162
|
+
offsets = def ? Cursor.decode(def) : [];
|
|
163
|
+
}
|
|
164
|
+
if (definition.length > 0 && definition.length === offsets.length) {
|
|
151
165
|
return this.createCursorCondition(definition, offsets, inverse, meta);
|
|
152
166
|
}
|
|
153
167
|
/* v8 ignore next */
|
|
@@ -174,19 +188,67 @@ export class DatabaseDriver {
|
|
|
174
188
|
const dir = Utils.xor(desc, isLast) ? 'desc' : 'asc';
|
|
175
189
|
return { [prop]: dir };
|
|
176
190
|
};
|
|
191
|
+
// the cursor condition is created at the driver level, after the EM already converted custom types
|
|
192
|
+
// in the user `where`, so we need to run the same conversion over it explicitly
|
|
193
|
+
const where = QueryHelper.processWhere({
|
|
194
|
+
where: ($and.length > 1 ? { $and } : { ...$and[0] }),
|
|
195
|
+
entityName: meta.class,
|
|
196
|
+
metadata: this.metadata,
|
|
197
|
+
platform: this.platform,
|
|
198
|
+
convertCustomTypes: options.convertCustomTypes,
|
|
199
|
+
});
|
|
177
200
|
return {
|
|
178
201
|
orderBy: definition.map(([prop, direction]) => createOrderBy(prop, direction)),
|
|
179
|
-
where
|
|
202
|
+
where,
|
|
180
203
|
};
|
|
181
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Restores the JS value of a single cursor offset: ISO strings become `Date` instances based on the
|
|
207
|
+
* property type (never based on the string shape alone), and custom types are restored via
|
|
208
|
+
* `convertToJSValue`. Values compared against a JSON document keep their serialized form instead,
|
|
209
|
+
* unless the platform preserves native date types inside JSON documents (mongo).
|
|
210
|
+
*/
|
|
211
|
+
mapCursorOffset(prop, value, insideJson) {
|
|
212
|
+
if (Utils.isScalarReference(value)) {
|
|
213
|
+
value = value.unwrap();
|
|
214
|
+
}
|
|
215
|
+
// scalar direction on a relation orders by its primary key
|
|
216
|
+
if (Utils.isEntity(value, true)) {
|
|
217
|
+
value = helper(value).getPrimaryKey();
|
|
218
|
+
}
|
|
219
|
+
if (value == null) {
|
|
220
|
+
return value;
|
|
221
|
+
}
|
|
222
|
+
if (insideJson && !this.platform.preservesDatesInsideJson()) {
|
|
223
|
+
// compared against the JSON document, which holds the serialized form
|
|
224
|
+
if (value instanceof Date) {
|
|
225
|
+
return value.toISOString();
|
|
226
|
+
}
|
|
227
|
+
// restore the JS value from the serialized form, `processWhere` then converts it to
|
|
228
|
+
// the database form, which is what the JSON document holds for custom typed props
|
|
229
|
+
return prop?.customType ? prop.customType.convertToJSValue(value, this.platform) : value;
|
|
230
|
+
}
|
|
231
|
+
if (typeof value === 'string' &&
|
|
232
|
+
(prop?.runtimeType === 'Date' ||
|
|
233
|
+
(prop?.customType && this.platform.getMappedType(prop.columnTypes?.[0] ?? '') instanceof DateTimeType))) {
|
|
234
|
+
value = new Date(value);
|
|
235
|
+
}
|
|
236
|
+
return prop?.customType ? prop.customType.convertToJSValue(value, this.platform) : value;
|
|
237
|
+
}
|
|
182
238
|
createCursorCondition(definition, offsets, inverse, meta) {
|
|
183
|
-
const createCondition = (prop, direction, offset, eq = false, path = prop) => {
|
|
239
|
+
const createCondition = (prop, direction, offset, eq = false, path = prop, properties = meta.properties, insideJson = false) => {
|
|
240
|
+
const propMeta = properties[prop];
|
|
184
241
|
if (Utils.isPlainObject(direction)) {
|
|
185
242
|
if (offset === undefined) {
|
|
186
243
|
throw CursorError.missingValue(meta.className, path);
|
|
187
244
|
}
|
|
245
|
+
// POJO cursors can carry entity, reference or embeddable class instances, read their properties directly
|
|
246
|
+
offset = Reference.unwrapReference(offset);
|
|
247
|
+
const childProps = propMeta?.kind === ReferenceKind.EMBEDDED ? propMeta.embeddedProps : propMeta?.targetMeta?.properties;
|
|
248
|
+
insideJson ||=
|
|
249
|
+
(propMeta?.kind === ReferenceKind.EMBEDDED && !!propMeta.object) || propMeta?.customType instanceof JsonType;
|
|
188
250
|
const value = Utils.keys(direction).reduce((o, key) => {
|
|
189
|
-
Object.assign(o, createCondition(key, direction[key], offset?.[key], eq, `${path}.${key}
|
|
251
|
+
Object.assign(o, createCondition(key, direction[key], offset?.[key], eq, `${path}.${key}`, childProps ?? {}, insideJson));
|
|
190
252
|
return o;
|
|
191
253
|
}, {});
|
|
192
254
|
return { [prop]: value };
|
|
@@ -209,6 +271,7 @@ export class DatabaseDriver {
|
|
|
209
271
|
if (offset === undefined) {
|
|
210
272
|
throw CursorError.missingValue(meta.className, path);
|
|
211
273
|
}
|
|
274
|
+
offset = this.mapCursorOffset(propMeta, offset, insideJson);
|
|
212
275
|
// Handle null offset (intentional null cursor value)
|
|
213
276
|
if (offset === null) {
|
|
214
277
|
if (eq) {
|
|
@@ -364,6 +364,9 @@ export class ObjectHydrator extends Hydrator {
|
|
|
364
364
|
ret.push(` data${dataKey}.forEach((_, idx_${idx}) => {`);
|
|
365
365
|
ret.push(...hydrateEmbedded(prop, [...path, `[idx_${idx}]`], `${dataKey}[idx_${idx}]`).map(l => ' ' + l));
|
|
366
366
|
ret.push(` });`);
|
|
367
|
+
ret.push(` } else if (data${dataKey} === null) {`);
|
|
368
|
+
/* v8 ignore next */
|
|
369
|
+
ret.push(` entity${entityKey} = ${this.config.get('forceUndefined') ? 'undefined' : 'null'};`);
|
|
367
370
|
ret.push(` }`);
|
|
368
371
|
return ret;
|
|
369
372
|
};
|
package/metadata/EntitySchema.js
CHANGED
|
@@ -4,6 +4,8 @@ import { Cascade, ReferenceKind } from '../enums.js';
|
|
|
4
4
|
import { Type } from '../types/Type.js';
|
|
5
5
|
import { Utils } from '../utils/Utils.js';
|
|
6
6
|
import { EnumArrayType } from '../types/EnumArrayType.js';
|
|
7
|
+
// mirrors `MetadataStorage.META_SYMBOL`, we can't import it here due to a module cycle
|
|
8
|
+
const META_SYMBOL = Symbol.for('@mikro-orm/core/MetadataStorage.META_SYMBOL');
|
|
7
9
|
/** Class-less entity definition that provides a programmatic API for defining entities without decorators. */
|
|
8
10
|
export class EntitySchema {
|
|
9
11
|
/**
|
|
@@ -220,8 +222,9 @@ export class EntitySchema {
|
|
|
220
222
|
// Only set extends if the parent is NOT the auto-generated class for this same entity.
|
|
221
223
|
// When the user extends the auto-generated class (from defineEntity without a class option)
|
|
222
224
|
// and registers their custom class via setClass, we don't want to discover the
|
|
223
|
-
// auto-generated class as a separate parent entity.
|
|
224
|
-
|
|
225
|
+
// auto-generated class as a separate parent entity. A parent carrying its own decorator
|
|
226
|
+
// metadata is a real base class even when a minifier mangles it to the same name as the child.
|
|
227
|
+
if (base !== BaseEntity && (base.name !== this._meta.className || Object.hasOwn(base, META_SYMBOL))) {
|
|
225
228
|
this._meta.extends ??= base.name ? base : undefined;
|
|
226
229
|
}
|
|
227
230
|
}
|
|
@@ -212,6 +212,12 @@ export class MetadataDiscovery {
|
|
|
212
212
|
.replace(/Array<(.*)>/, '$1') // unwrap array
|
|
213
213
|
.replace(/\[]$/, '') // remove array suffix
|
|
214
214
|
.replace(/\((.*)\)/, '$1'); // unwrap union types
|
|
215
|
+
// Names can be ambiguous when a minifier mangles two classes to the same name,
|
|
216
|
+
// so class references also need to be checked by identity.
|
|
217
|
+
const discoveredByIdentity = (target) => {
|
|
218
|
+
const cls = EntitySchema.is(target) ? target.meta.class : target;
|
|
219
|
+
return typeof cls !== 'function' || this.#discovered.some(m => m.class === cls);
|
|
220
|
+
};
|
|
215
221
|
const missing = [];
|
|
216
222
|
this.#discovered.forEach(meta => Object.values(meta.properties).forEach(prop => {
|
|
217
223
|
if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.pivotEntity) {
|
|
@@ -219,7 +225,7 @@ export class MetadataDiscovery {
|
|
|
219
225
|
const target = typeof pivotEntity === 'function' && !pivotEntity.prototype
|
|
220
226
|
? pivotEntity()
|
|
221
227
|
: pivotEntity;
|
|
222
|
-
if (!this.#discovered.find(m => m.className === Utils.className(target))) {
|
|
228
|
+
if (!this.#discovered.find(m => m.className === Utils.className(target)) || !discoveredByIdentity(target)) {
|
|
223
229
|
missing.push(target);
|
|
224
230
|
}
|
|
225
231
|
}
|
|
@@ -227,7 +233,8 @@ export class MetadataDiscovery {
|
|
|
227
233
|
const target = typeof prop.entity === 'function' && !prop.entity.prototype ? prop.entity() : prop.type;
|
|
228
234
|
if (!unwrap(prop.type)
|
|
229
235
|
.split(/ ?\| ?/)
|
|
230
|
-
.every(type => this.#discovered.find(m => m.className === type))
|
|
236
|
+
.every(type => this.#discovered.find(m => m.className === type)) ||
|
|
237
|
+
!Utils.asArray(target).every(discoveredByIdentity)) {
|
|
231
238
|
missing.push(...Utils.asArray(target));
|
|
232
239
|
}
|
|
233
240
|
}
|
|
@@ -275,9 +282,11 @@ export class MetadataDiscovery {
|
|
|
275
282
|
continue;
|
|
276
283
|
}
|
|
277
284
|
parent = Object.getPrototypeOf(meta.class);
|
|
278
|
-
// Skip if parent is the auto-generated base class for the same entity (from setClass usage)
|
|
285
|
+
// Skip if parent is the auto-generated base class for the same entity (from setClass usage).
|
|
286
|
+
// A parent carrying its own decorator metadata is a real base class even when a minifier
|
|
287
|
+
// mangles it to the same name as the child.
|
|
279
288
|
if (parent.name !== '' &&
|
|
280
|
-
parent.name !== meta.className &&
|
|
289
|
+
(parent.name !== meta.className || Object.hasOwn(parent, MetadataStorage.META_SYMBOL)) &&
|
|
281
290
|
!this.#metadata.has(parent) &&
|
|
282
291
|
parent !== BaseEntity) {
|
|
283
292
|
this.discoverReferences([parent], false);
|
|
@@ -311,7 +320,12 @@ export class MetadataDiscovery {
|
|
|
311
320
|
const cls = entity;
|
|
312
321
|
const path = cls[MetadataStorage.PATH_SYMBOL];
|
|
313
322
|
if (path) {
|
|
314
|
-
|
|
323
|
+
// Prefer the metadata stored on the class reference, the `className-path` key can
|
|
324
|
+
// collide when a minifier mangles two classes to the same name.
|
|
325
|
+
const stored = Object.hasOwn(cls, MetadataStorage.META_SYMBOL)
|
|
326
|
+
? cls[MetadataStorage.META_SYMBOL]
|
|
327
|
+
: MetadataStorage.getMetadata(cls.name, path);
|
|
328
|
+
const meta = Utils.copy(stored, false);
|
|
315
329
|
meta.path = path;
|
|
316
330
|
this.#metadata.set(cls, meta);
|
|
317
331
|
}
|
|
@@ -834,7 +848,7 @@ export class MetadataDiscovery {
|
|
|
834
848
|
pivotMeta.properties[primaryProp.name] = primaryProp;
|
|
835
849
|
pivotMeta.compositePK = false;
|
|
836
850
|
}
|
|
837
|
-
const discriminatorProp = this.createPivotScalarProperty(discriminatorColumn, [this.#platform.getVarcharTypeDeclarationSQL(prop)], [discriminatorColumn], { type: 'string', primary: !
|
|
851
|
+
const discriminatorProp = this.createPivotScalarProperty(discriminatorColumn, [this.#platform.getVarcharTypeDeclarationSQL(prop)], [discriminatorColumn], { type: 'string', primary: !prop.fixedOrder, nullable: false });
|
|
838
852
|
this.initFieldName(discriminatorProp);
|
|
839
853
|
pivotMeta.properties[discriminatorColumn] = discriminatorProp;
|
|
840
854
|
const columnTypes = this.getPrimaryKeyColumnTypes(meta);
|
|
@@ -849,7 +863,7 @@ export class MetadataDiscovery {
|
|
|
849
863
|
pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, columnTypes, [...prop.joinColumns], { type: meta.className, persist: false });
|
|
850
864
|
}
|
|
851
865
|
else {
|
|
852
|
-
pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, columnTypes, [...prop.joinColumns], { type: meta.className, primary:
|
|
866
|
+
pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, columnTypes, [...prop.joinColumns], { type: meta.className, primary: !prop.fixedOrder, nullable: false });
|
|
853
867
|
}
|
|
854
868
|
pivotMeta.properties[targetMeta.className + '_inverse'] = this.definePivotProperty(prop, targetMeta.className + '_inverse', targetMeta.class, prop.discriminator, false, false);
|
|
855
869
|
// Create virtual M:1 relation to the polymorphic owner for single-query join loading
|
|
@@ -871,11 +885,11 @@ export class MetadataDiscovery {
|
|
|
871
885
|
const discriminatorColumn = prop.discriminatorColumn;
|
|
872
886
|
const targets = prop.polymorphTargets;
|
|
873
887
|
pivotMeta.properties[meta.name + '_owner'] = this.definePivotProperty(prop, meta.name + '_owner', meta.class, prop.discriminator, true, false);
|
|
874
|
-
const discriminatorProp = this.createPivotScalarProperty(discriminatorColumn, [this.#platform.getVarcharTypeDeclarationSQL(prop)], [discriminatorColumn], { type: 'string', primary:
|
|
888
|
+
const discriminatorProp = this.createPivotScalarProperty(discriminatorColumn, [this.#platform.getVarcharTypeDeclarationSQL(prop)], [discriminatorColumn], { type: 'string', primary: !prop.fixedOrder, nullable: false });
|
|
875
889
|
this.initFieldName(discriminatorProp);
|
|
876
890
|
pivotMeta.properties[discriminatorColumn] = discriminatorProp;
|
|
877
891
|
const firstTargetColumnTypes = this.getPrimaryKeyColumnTypes(targets[0]);
|
|
878
|
-
pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, firstTargetColumnTypes, [...prop.inverseJoinColumns], { type: targets[0].className, primary:
|
|
892
|
+
pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, firstTargetColumnTypes, [...prop.inverseJoinColumns], { type: targets[0].className, primary: !prop.fixedOrder, nullable: false });
|
|
879
893
|
pivotMeta.polymorphicDiscriminatorMap ??= {};
|
|
880
894
|
for (const targetMeta of targets) {
|
|
881
895
|
const relationName = `${prop.discriminator}_${targetMeta.tableName}`;
|
|
@@ -1184,7 +1198,11 @@ export class MetadataDiscovery {
|
|
|
1184
1198
|
return;
|
|
1185
1199
|
}
|
|
1186
1200
|
visited.add(embeddedProp);
|
|
1187
|
-
|
|
1201
|
+
// Prefer resolution via the class reference, the name can be ambiguous when a minifier
|
|
1202
|
+
// mangles two classes to the same name. Only named metadata counts, an auto-discovered
|
|
1203
|
+
// class without the `@Embeddable()` decorator should still fail as unknown below.
|
|
1204
|
+
const embeddable = this.#discovered.find(m => m.name && m.class === embeddedProp.target) ??
|
|
1205
|
+
this.#discovered.find(m => m.name === embeddedProp.type);
|
|
1188
1206
|
if (!embeddable) {
|
|
1189
1207
|
throw MetadataError.fromUnknownEntity(embeddedProp.type, `${meta.className}.${embeddedProp.name}`);
|
|
1190
1208
|
}
|
|
@@ -92,7 +92,7 @@ export class MetadataProvider {
|
|
|
92
92
|
if (!this.useCache()) {
|
|
93
93
|
return undefined;
|
|
94
94
|
}
|
|
95
|
-
const cache = meta.path && this.config.getMetadataCacheAdapter().get(this.getCacheKey(meta));
|
|
95
|
+
const cache = meta.path && this.config.getMetadataCacheAdapter().get(this.getCacheKey(meta), meta.path);
|
|
96
96
|
if (cache) {
|
|
97
97
|
this.loadFromCache(meta, cache);
|
|
98
98
|
meta.root = root;
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { type Dictionary, EntityMetadata, type EntityName } from '../typings.js';
|
|
1
|
+
import { type Dictionary, type EntityCtor, EntityMetadata, type EntityName } from '../typings.js';
|
|
2
2
|
import type { EntityManager } from '../EntityManager.js';
|
|
3
3
|
/** Registry that stores and provides access to entity metadata by class, name, or id. */
|
|
4
4
|
export declare class MetadataStorage {
|
|
5
5
|
#private;
|
|
6
6
|
static readonly PATH_SYMBOL: unique symbol;
|
|
7
|
+
static readonly META_SYMBOL: unique symbol;
|
|
7
8
|
constructor(metadata?: Dictionary<EntityMetadata>);
|
|
8
|
-
/** Returns the global metadata dictionary, or a specific entry by entity name and path. */
|
|
9
|
+
/** Returns the global metadata dictionary, or a specific entry by entity name and path (keyed by the class reference when `target` is provided). */
|
|
9
10
|
static getMetadata(): Dictionary<EntityMetadata>;
|
|
10
|
-
static getMetadata<T = any>(entity: string, path: string): EntityMetadata<T>;
|
|
11
|
+
static getMetadata<T = any>(entity: string, path: string, target?: EntityCtor): EntityMetadata<T>;
|
|
11
12
|
/** Checks whether an entity with the given class name exists in the global metadata. */
|
|
12
13
|
static isKnownEntity(name: string): boolean;
|
|
13
14
|
/** Clears all entries from the global metadata registry. */
|
|
@@ -11,6 +11,7 @@ function getGlobalStorage(namespace) {
|
|
|
11
11
|
/** Registry that stores and provides access to entity metadata by class, name, or id. */
|
|
12
12
|
export class MetadataStorage {
|
|
13
13
|
static PATH_SYMBOL = Symbol.for('@mikro-orm/core/MetadataStorage.PATH_SYMBOL');
|
|
14
|
+
static META_SYMBOL = Symbol.for('@mikro-orm/core/MetadataStorage.META_SYMBOL');
|
|
14
15
|
static #metadata = getGlobalStorage('metadata');
|
|
15
16
|
#metadataMap = new Map();
|
|
16
17
|
#idMap;
|
|
@@ -26,8 +27,21 @@ export class MetadataStorage {
|
|
|
26
27
|
this.#metadataMap.set(meta.class, meta);
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
|
-
static getMetadata(entity, path) {
|
|
30
|
+
static getMetadata(entity, path, target) {
|
|
30
31
|
const key = entity && path ? entity + '-' + Utils.hash(path) : null;
|
|
32
|
+
// Key the registry by the class reference when available, so two classes minified
|
|
33
|
+
// to the same mangled name don't collide on the `className-path` key.
|
|
34
|
+
if (key && target) {
|
|
35
|
+
if (!Object.hasOwn(target, MetadataStorage.META_SYMBOL)) {
|
|
36
|
+
Object.defineProperty(target, MetadataStorage.META_SYMBOL, {
|
|
37
|
+
value: new EntityMetadata({ className: entity, path }),
|
|
38
|
+
writable: true,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
// Keep the name-keyed entry in sync, the class-keyed metadata survives `MetadataStorage.clear()`.
|
|
42
|
+
MetadataStorage.#metadata[key] = target[MetadataStorage.META_SYMBOL];
|
|
43
|
+
return target[MetadataStorage.META_SYMBOL];
|
|
44
|
+
}
|
|
31
45
|
if (key && !MetadataStorage.#metadata[key]) {
|
|
32
46
|
MetadataStorage.#metadata[key] = new EntityMetadata({ className: entity, path });
|
|
33
47
|
}
|
package/metadata/Routine.js
CHANGED
|
@@ -135,6 +135,9 @@ export class Routine {
|
|
|
135
135
|
return new Routine(config);
|
|
136
136
|
}
|
|
137
137
|
static is(item) {
|
|
138
|
-
|
|
138
|
+
if (item instanceof Routine) {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
return item != null && typeof item === 'object' && item.constructor?.name === 'Routine' && 'type' in item;
|
|
139
142
|
}
|
|
140
143
|
}
|
|
@@ -10,7 +10,8 @@ export class AbstractNamingStrategy {
|
|
|
10
10
|
classToMigrationName(timestamp, customMigrationName) {
|
|
11
11
|
let migrationName = `Migration${timestamp}`;
|
|
12
12
|
if (customMigrationName) {
|
|
13
|
-
|
|
13
|
+
// the name becomes part of a class identifier
|
|
14
|
+
migrationName += `_${customMigrationName.replace(/[^$\p{ID_Continue}]+/gu, '_')}`;
|
|
14
15
|
}
|
|
15
16
|
return migrationName;
|
|
16
17
|
}
|
|
@@ -9,7 +9,8 @@ export interface NamingStrategy {
|
|
|
9
9
|
*/
|
|
10
10
|
classToTableName(entityName: string, tableName?: string): string;
|
|
11
11
|
/**
|
|
12
|
-
* Return a migration name. This name should allow ordering
|
|
12
|
+
* Return a migration name. This name should allow ordering, and has to be a valid class identifier,
|
|
13
|
+
* as it is used as the class name in the generated migration file.
|
|
13
14
|
*/
|
|
14
15
|
classToMigrationName(timestamp: string, customMigrationName?: string): string;
|
|
15
16
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/core",
|
|
3
|
-
"version": "7.2.0-dev.
|
|
3
|
+
"version": "7.2.0-dev.5",
|
|
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
|
@@ -227,6 +227,8 @@ export declare abstract class Platform {
|
|
|
227
227
|
formatIndexHint(indexNames: string[]): string | undefined;
|
|
228
228
|
/** Whether the driver automatically parses JSON columns into JS objects. */
|
|
229
229
|
convertsJsonAutomatically(): boolean;
|
|
230
|
+
/** Whether date values inside JSON documents keep their native type (e.g. BSON dates), instead of being serialized to ISO strings. */
|
|
231
|
+
preservesDatesInsideJson(): boolean;
|
|
230
232
|
/** Converts a JS value to its JSON database representation (typically JSON.stringify). */
|
|
231
233
|
convertJsonToDatabaseValue(value: unknown, context?: TransformContext): unknown;
|
|
232
234
|
/** Converts a database JSON value to its JS representation. */
|
package/platforms/Platform.js
CHANGED
|
@@ -465,6 +465,10 @@ export class Platform {
|
|
|
465
465
|
convertsJsonAutomatically() {
|
|
466
466
|
return true;
|
|
467
467
|
}
|
|
468
|
+
/** Whether date values inside JSON documents keep their native type (e.g. BSON dates), instead of being serialized to ISO strings. */
|
|
469
|
+
preservesDatesInsideJson() {
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
468
472
|
/** Converts a JS value to its JSON database representation (typically JSON.stringify). */
|
|
469
473
|
convertJsonToDatabaseValue(value, context) {
|
|
470
474
|
return JSON.stringify(value);
|
package/typings.d.ts
CHANGED
|
@@ -1470,6 +1470,8 @@ export interface IMigrationGenerator {
|
|
|
1470
1470
|
}
|
|
1471
1471
|
/** Interface that all migration classes must implement. */
|
|
1472
1472
|
export interface Migration {
|
|
1473
|
+
/** Stable migration name, used instead of the class name (which minifiers can mangle). */
|
|
1474
|
+
name?: string;
|
|
1473
1475
|
up(): Promise<void> | void;
|
|
1474
1476
|
down(): Promise<void> | void;
|
|
1475
1477
|
isTransactional(): boolean;
|
|
@@ -29,15 +29,18 @@ export class ChangeSet {
|
|
|
29
29
|
else {
|
|
30
30
|
this.primaryKey = this.originalEntity[this.meta.primaryKeys[0]];
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
});
|
|
32
|
+
const primaryProp = this.meta.getPrimaryProp();
|
|
33
|
+
const relationPK = !this.meta.compositePK && !!primaryProp.targetMeta?.compositePK;
|
|
34
|
+
// arrays are already the ordered tuple of the target's primary keys
|
|
35
|
+
if (relationPK && Utils.isPlainObject(this.primaryKey)) {
|
|
36
|
+
const pk = this.primaryKey;
|
|
37
|
+
this.primaryKey = primaryProp.targetMeta.primaryKeys.map(childPK => pk[childPK]);
|
|
39
38
|
}
|
|
40
39
|
if (object && this.primaryKey != null) {
|
|
40
|
+
// the whole tuple belongs to the single relation PK, it must not be spread over the (single) PK prop
|
|
41
|
+
if (relationPK) {
|
|
42
|
+
return { [primaryProp.name]: this.primaryKey };
|
|
43
|
+
}
|
|
41
44
|
return Utils.primaryKeyToObject(this.meta, this.primaryKey);
|
|
42
45
|
}
|
|
43
46
|
return this.primaryKey ?? null;
|
|
@@ -3,7 +3,7 @@ import { PolymorphicRef } from '../entity/PolymorphicRef.js';
|
|
|
3
3
|
import { helper } from '../entity/wrap.js';
|
|
4
4
|
import { ChangeSetType } from './ChangeSet.js';
|
|
5
5
|
import { isRaw } from '../utils/RawQueryFragment.js';
|
|
6
|
-
import { Utils } from '../utils/Utils.js';
|
|
6
|
+
import { equals, Utils } from '../utils/Utils.js';
|
|
7
7
|
import { OptimisticLockError, ValidationError } from '../errors.js';
|
|
8
8
|
import { ReferenceKind } from '../enums.js';
|
|
9
9
|
/** @internal Executes change sets against the database, handling inserts, updates, and deletes. */
|
|
@@ -230,11 +230,15 @@ export class ChangeSetPersister {
|
|
|
230
230
|
}
|
|
231
231
|
const res = await this.#driver.nativeUpdateMany(meta.class, cond, payload, options);
|
|
232
232
|
const map = new Map();
|
|
233
|
-
|
|
233
|
+
// returning rows are not mapped yet, so they are keyed by field names - we need to build the hash
|
|
234
|
+
// from those to be able to match them with `getSerializedPrimaryKey()` of the entity
|
|
235
|
+
const pkFields = meta.getPrimaryProps().flatMap(prop => prop.fieldNames);
|
|
236
|
+
res.rows?.forEach(item => map.set(Utils.getPrimaryKeyHash(pkFields.map(field => item[field])), item));
|
|
234
237
|
for (const changeSet of changeSets) {
|
|
235
238
|
if (res.rows) {
|
|
236
239
|
const row = map.get(helper(changeSet.entity).getSerializedPrimaryKey());
|
|
237
|
-
|
|
240
|
+
// STI batches can mix child types, so map through the change set's own metadata
|
|
241
|
+
this.mapReturnedValues(changeSet.entity, changeSet.payload, row, changeSet.meta);
|
|
238
242
|
}
|
|
239
243
|
changeSet.persisted = true;
|
|
240
244
|
}
|
|
@@ -334,7 +338,8 @@ export class ChangeSetPersister {
|
|
|
334
338
|
});
|
|
335
339
|
const res = await this.#driver.find(meta.root.class, { $or }, options);
|
|
336
340
|
if (res.length !== changeSets.length) {
|
|
337
|
-
|
|
341
|
+
// a FK pointing to a composite PK is an array, so the values need to be compared deeply
|
|
342
|
+
const compare = (a, b, keys) => keys.every(k => equals(a[k], b[k]));
|
|
338
343
|
const entity = changeSets.find(cs => {
|
|
339
344
|
return !res.some(row => compare(Utils.getPrimaryKeyCond(cs.entity, primaryKeys), row, primaryKeys));
|
|
340
345
|
}).entity;
|
|
@@ -375,7 +380,8 @@ export class ChangeSetPersister {
|
|
|
375
380
|
changeSets.forEach(cs => {
|
|
376
381
|
Utils.keys(cs.payload).forEach(k => {
|
|
377
382
|
if (isRaw(cs.payload[k]) && isRaw(cs.entity[k])) {
|
|
378
|
-
|
|
383
|
+
// STI batches can mix child types, so the property might not exist on `meta`
|
|
384
|
+
returning.add(cs.meta.properties[k]);
|
|
379
385
|
}
|
|
380
386
|
});
|
|
381
387
|
});
|
|
@@ -400,12 +406,14 @@ export class ChangeSetPersister {
|
|
|
400
406
|
options = this.prepareOptions(meta, options, {
|
|
401
407
|
fields: Utils.unique(reloadProps.map(prop => prop.name)),
|
|
402
408
|
});
|
|
403
|
-
|
|
409
|
+
// a mixed STI batch shares one table but the child discriminator would filter out the siblings
|
|
410
|
+
const target = changeSets.some(cs => cs.meta !== meta) && meta.root.discriminatorColumn ? meta.root : meta;
|
|
411
|
+
const data = await this.#driver.find(target.class, { [pk]: { $in: pks } }, options);
|
|
404
412
|
const map = new Map();
|
|
405
413
|
data.forEach(item => map.set(Utils.getCompositeKeyHash(item, meta, false, this.#platform, true), item));
|
|
406
414
|
for (const changeSet of changeSets) {
|
|
407
415
|
const data = map.get(helper(changeSet.entity).getSerializedPrimaryKey());
|
|
408
|
-
this.#hydrator.hydrate(changeSet.entity, meta, data, this.#factory, 'full', false, true);
|
|
416
|
+
this.#hydrator.hydrate(changeSet.entity, changeSet.meta, data, this.#factory, 'full', false, true);
|
|
409
417
|
Object.assign(changeSet.payload, data); // merge to the changeset payload, so it gets saved to the entity snapshot
|
|
410
418
|
}
|
|
411
419
|
}
|
|
@@ -103,7 +103,7 @@ export declare abstract class AbstractMigrator<D extends IDatabaseDriver> implem
|
|
|
103
103
|
name: string;
|
|
104
104
|
path: string;
|
|
105
105
|
}): RunnableMigration;
|
|
106
|
-
protected initialize(MigrationClass: Constructor<Migration>, name
|
|
106
|
+
protected initialize(MigrationClass: Constructor<Migration>, name?: string): RunnableMigration;
|
|
107
107
|
/**
|
|
108
108
|
* Checks if `src` folder exists, it so, tries to adjust the migrations and seeders paths automatically to use it.
|
|
109
109
|
* If there is a `dist` or `build` folder, it will be used for the JS variant (`path` option), while the `src` folder will be
|
|
@@ -358,7 +358,8 @@ export class AbstractMigrator {
|
|
|
358
358
|
initialize(MigrationClass, name) {
|
|
359
359
|
const instance = new MigrationClass(this.driver, this.config);
|
|
360
360
|
return {
|
|
361
|
-
name
|
|
361
|
+
// the constructor name is the last resort, minifiers can mangle it (e.g. when bundling)
|
|
362
|
+
name: this.storage.getMigrationName(name ?? instance.name ?? MigrationClass.name),
|
|
362
363
|
up: afterRun => this.runner.run(instance, 'up', afterRun),
|
|
363
364
|
down: afterRun => this.runner.run(instance, 'down', afterRun),
|
|
364
365
|
};
|
|
@@ -409,7 +410,7 @@ export class AbstractMigrator {
|
|
|
409
410
|
if (this.options.migrationsList) {
|
|
410
411
|
return this.options.migrationsList.map(migration => {
|
|
411
412
|
if (typeof migration === 'function') {
|
|
412
|
-
return this.initialize(migration
|
|
413
|
+
return this.initialize(migration);
|
|
413
414
|
}
|
|
414
415
|
return this.initialize(migration.class, migration.name);
|
|
415
416
|
});
|
package/utils/Configuration.js
CHANGED
|
@@ -86,7 +86,8 @@ const DEFAULTS = {
|
|
|
86
86
|
snapshot: true,
|
|
87
87
|
snapshotOnMigrate: true,
|
|
88
88
|
emit: 'ts',
|
|
89
|
-
|
|
89
|
+
// mirrors `NamingStrategy.classToMigrationName`, so the file name matches the class it declares
|
|
90
|
+
fileName: (timestamp, name) => `Migration${timestamp}${name ? '_' + name.replace(/[^$\p{ID_Continue}]+/gu, '_') : ''}`,
|
|
90
91
|
},
|
|
91
92
|
schemaGenerator: {
|
|
92
93
|
createForeignKeyConstraints: true,
|
package/utils/Cursor.js
CHANGED
|
@@ -150,12 +150,7 @@ export class Cursor {
|
|
|
150
150
|
return Buffer.from(JSON.stringify(value)).toString('base64url');
|
|
151
151
|
}
|
|
152
152
|
static decode(value) {
|
|
153
|
-
return JSON.parse(Buffer.from(value, 'base64url').toString('utf8'))
|
|
154
|
-
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}/.exec(value)) {
|
|
155
|
-
return new Date(value);
|
|
156
|
-
}
|
|
157
|
-
return value;
|
|
158
|
-
});
|
|
153
|
+
return JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
|
|
159
154
|
}
|
|
160
155
|
static getDefinition(meta, orderBy) {
|
|
161
156
|
return Utils.asArray(orderBy).flatMap(order => {
|
|
@@ -164,7 +164,9 @@ export class EntityComparator {
|
|
|
164
164
|
const lines = [];
|
|
165
165
|
const context = new Map();
|
|
166
166
|
context.set('isEntityOrRef', (val) => Utils.isEntity(val, true));
|
|
167
|
-
context.set('getCompositeKeyValue', (val) =>
|
|
167
|
+
context.set('getCompositeKeyValue', (val) =>
|
|
168
|
+
// deep flatten, nested composite PKs produce nested arrays that would be comma-joined by the hash
|
|
169
|
+
Utils.flatten(Utils.getCompositeKeyValue(val, meta, 'convertToDatabaseValue', this.#platform), true));
|
|
168
170
|
context.set('getPrimaryKeyHash', (val) => Utils.getPrimaryKeyHash(Utils.asArray(val)));
|
|
169
171
|
if (meta.primaryKeys.length > 1) {
|
|
170
172
|
lines.push(` const pks = entity.__helper.__pk ? getCompositeKeyValue(entity.__helper.__pk) : [`);
|
|
@@ -447,6 +449,7 @@ export class EntityComparator {
|
|
|
447
449
|
const ret = [];
|
|
448
450
|
const padding = ' '.repeat(level * 2);
|
|
449
451
|
const idx = this.#tmpIndex++;
|
|
452
|
+
ret.push(`${padding}if (entity${entityKey} === null) ret${dataKey} = null;`);
|
|
450
453
|
ret.push(`${padding}if (Array.isArray(entity${entityKey})) {`);
|
|
451
454
|
ret.push(`${padding} ret${dataKey} = [];`);
|
|
452
455
|
ret.push(`${padding} entity${entityKey}.forEach((_, idx_${idx}) => {`);
|
package/utils/QueryHelper.d.ts
CHANGED
|
@@ -40,6 +40,11 @@ export declare class QueryHelper {
|
|
|
40
40
|
static mergePropertyFilters(propFilters: FilterOptions | undefined, options: FilterOptions | undefined): FilterOptions | undefined;
|
|
41
41
|
static isFilterActive<T>(meta: EntityMetadata<T>, filterName: string, filter: FilterDef, options: Dictionary<boolean | Dictionary>): boolean;
|
|
42
42
|
static processCustomType<T extends object>(prop: EntityProperty<T>, cond: FilterQuery<T>, platform: Platform, key?: string, fromQuery?: boolean): FilterQuery<T>;
|
|
43
|
+
/**
|
|
44
|
+
* Composite PK conditions are keyed by a hash of all the PK names, which `findProperty` cannot
|
|
45
|
+
* resolve, so the custom types have to be applied positionally instead.
|
|
46
|
+
*/
|
|
47
|
+
private static processCompositeCustomTypes;
|
|
43
48
|
private static isSupportedOperator;
|
|
44
49
|
private static processJsonCondition;
|
|
45
50
|
static findProperty<T>(fieldName: string, options: ProcessWhereOptions<T>): EntityProperty<T> | undefined;
|
package/utils/QueryHelper.js
CHANGED
|
@@ -227,6 +227,13 @@ export class QueryHelper {
|
|
|
227
227
|
if (prop?.customType && convertCustomTypes && !isRaw(value)) {
|
|
228
228
|
value = QueryHelper.processCustomType(prop, value, platform, undefined, true);
|
|
229
229
|
}
|
|
230
|
+
else if (!prop &&
|
|
231
|
+
meta?.compositePK &&
|
|
232
|
+
convertCustomTypes &&
|
|
233
|
+
Array.isArray(value) &&
|
|
234
|
+
key === Utils.getPrimaryKeyHash(meta.primaryKeys)) {
|
|
235
|
+
value = QueryHelper.processCompositeCustomTypes(value, meta, platform);
|
|
236
|
+
}
|
|
230
237
|
// oxfmt-ignore
|
|
231
238
|
const isJsonProperty = prop?.customType instanceof JsonType && !isRaw(value) && (Utils.isPlainObject(value) ? !['$eq', '$elemMatch'].includes(Object.keys(value)[0]) : !Array.isArray(value));
|
|
232
239
|
if (isJsonProperty && prop?.kind !== ReferenceKind.EMBEDDED) {
|
|
@@ -321,6 +328,24 @@ export class QueryHelper {
|
|
|
321
328
|
}
|
|
322
329
|
return prop.customType.convertToDatabaseValue(cond, platform, { fromQuery, key, mode: 'query' });
|
|
323
330
|
}
|
|
331
|
+
/**
|
|
332
|
+
* Composite PK conditions are keyed by a hash of all the PK names, which `findProperty` cannot
|
|
333
|
+
* resolve, so the custom types have to be applied positionally instead.
|
|
334
|
+
*/
|
|
335
|
+
static processCompositeCustomTypes(value, meta, platform) {
|
|
336
|
+
const props = meta.primaryKeys.map(pk => meta.properties[pk]);
|
|
337
|
+
if (!props.some(prop => prop.customType)) {
|
|
338
|
+
return value;
|
|
339
|
+
}
|
|
340
|
+
// the tuple can be longer than the PK when the user passes a malformed condition
|
|
341
|
+
const convert = (tuple) => tuple.map((val, idx) => {
|
|
342
|
+
if (!props[idx]?.customType) {
|
|
343
|
+
return val;
|
|
344
|
+
}
|
|
345
|
+
return QueryHelper.processCustomType(props[idx], val, platform, undefined, true);
|
|
346
|
+
});
|
|
347
|
+
return value.every(val => Array.isArray(val)) ? value.map(val => convert(val)) : convert(value);
|
|
348
|
+
}
|
|
324
349
|
static isSupportedOperator(key) {
|
|
325
350
|
return !!QueryHelper.SUPPORTED_OPERATORS.find(op => key === op);
|
|
326
351
|
}
|
|
@@ -50,6 +50,12 @@ export declare class TransactionManager {
|
|
|
50
50
|
* Merges entities from fork to parent EntityManager.
|
|
51
51
|
*/
|
|
52
52
|
private mergeEntitiesToParent;
|
|
53
|
+
/**
|
|
54
|
+
* Returns the property names a given property is tracked and snapshotted under, paired with their values.
|
|
55
|
+
* Inlined embeddables are hydrated as a single object, so only their leaves tell whether it is complete.
|
|
56
|
+
*/
|
|
57
|
+
private getTrackedValues;
|
|
58
|
+
private restore;
|
|
53
59
|
/**
|
|
54
60
|
* Registers a deletion handler to unset entity identities after flush.
|
|
55
61
|
*/
|
|
@@ -169,19 +169,52 @@ export class TransactionManager {
|
|
|
169
169
|
if (!wrapped.__initialized && parentWrapped.__initialized) {
|
|
170
170
|
continue;
|
|
171
171
|
}
|
|
172
|
-
|
|
173
|
-
|
|
172
|
+
const parentData = parentWrapped.__data;
|
|
173
|
+
const parentSnapshot = parentWrapped.__originalEntityData;
|
|
174
|
+
parentWrapped.__data = { ...wrapped.__data };
|
|
175
|
+
const originalEntityData = { ...wrapped.__originalEntityData };
|
|
174
176
|
for (const prop of meta.hydrateProps) {
|
|
177
|
+
const tracked = this.getTrackedValues(prop, entity[prop.name]);
|
|
178
|
+
// the fork entity can be partially loaded, and propagating a property it does not know about
|
|
179
|
+
// would clobber the parent state, so we restore both its value and its snapshot entries
|
|
180
|
+
if (!tracked.every(([key, value]) => value !== undefined || wrapped.__loadedProperties.has(key))) {
|
|
181
|
+
this.restore(parentWrapped.__data, parentData, prop.name);
|
|
182
|
+
tracked.forEach(([key]) => this.restore(originalEntityData, parentSnapshot, key));
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
175
185
|
if (prop.kind === ReferenceKind.SCALAR) {
|
|
176
186
|
parentEntity[prop.name] = entity[prop.name];
|
|
177
187
|
}
|
|
178
188
|
}
|
|
189
|
+
if (wrapped.__originalEntityData) {
|
|
190
|
+
parentWrapped.__originalEntityData = originalEntityData;
|
|
191
|
+
}
|
|
179
192
|
}
|
|
180
193
|
else {
|
|
181
194
|
parentUoW.merge(entity, new Set([entity]));
|
|
182
195
|
}
|
|
183
196
|
}
|
|
184
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Returns the property names a given property is tracked and snapshotted under, paired with their values.
|
|
200
|
+
* Inlined embeddables are hydrated as a single object, so only their leaves tell whether it is complete.
|
|
201
|
+
*/
|
|
202
|
+
getTrackedValues(prop, value) {
|
|
203
|
+
if (prop.kind === ReferenceKind.EMBEDDED && !prop.object) {
|
|
204
|
+
return Object.values(prop.embeddedProps).flatMap(child => {
|
|
205
|
+
return this.getTrackedValues(child, value?.[child.embedded[1]]);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return [[prop.name, value]];
|
|
209
|
+
}
|
|
210
|
+
restore(target, source, key) {
|
|
211
|
+
if (source && key in source) {
|
|
212
|
+
target[key] = source[key];
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
delete target[key];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
185
218
|
/**
|
|
186
219
|
* Registers a deletion handler to unset entity identities after flush.
|
|
187
220
|
*/
|
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.0-dev.
|
|
156
|
+
static #ORM_VERSION = '7.2.0-dev.5';
|
|
157
157
|
/**
|
|
158
158
|
* Checks if the argument is instance of `Object`. Returns false for arrays.
|
|
159
159
|
*/
|
|
@@ -402,7 +402,8 @@ export class Utils {
|
|
|
402
402
|
static getCompositeKeyHash(data, meta, convertCustomTypes = false, platform, flat = false) {
|
|
403
403
|
let pks = this.getCompositeKeyValue(data, meta, convertCustomTypes, platform);
|
|
404
404
|
if (flat) {
|
|
405
|
-
|
|
405
|
+
// deep flatten, nested composite PKs produce nested arrays that would be comma-joined by the hash
|
|
406
|
+
pks = Utils.flatten(pks, true);
|
|
406
407
|
}
|
|
407
408
|
return Utils.getPrimaryKeyHash(pks);
|
|
408
409
|
}
|
|
@@ -462,7 +463,9 @@ export class Utils {
|
|
|
462
463
|
}
|
|
463
464
|
static getPrimaryKeyCond(entity, primaryKeys) {
|
|
464
465
|
const cond = primaryKeys.reduce((o, pk) => {
|
|
465
|
-
|
|
466
|
+
const value = entity[pk];
|
|
467
|
+
// FKs pointing to a composite PK are arrays, which `extractPK` rejects
|
|
468
|
+
o[pk] = Utils.isPrimaryKey(value, true) ? value : Utils.extractPK(value);
|
|
466
469
|
return o;
|
|
467
470
|
}, {});
|
|
468
471
|
if (Object.values(cond).some(v => v === null)) {
|
package/utils/clone.js
CHANGED
|
@@ -112,6 +112,12 @@ export function clone(parent, respectCustomCloneMethod = true) {
|
|
|
112
112
|
});
|
|
113
113
|
}
|
|
114
114
|
for (const i in parent) {
|
|
115
|
+
// an own `__proto__` key (as produced by `JSON.parse`) has no own counterpart on
|
|
116
|
+
// `child` to shadow the inherited accessor, so assigning it would replace the
|
|
117
|
+
// clone's prototype instead of copying the value
|
|
118
|
+
if (i === '__proto__') {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
115
121
|
let attrs;
|
|
116
122
|
if (proto) {
|
|
117
123
|
attrs = getPropertyDescriptor(proto, i);
|