@mikro-orm/core 7.3.0-dev.2 → 7.3.0-dev.20
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 +131 -32
- package/drivers/DatabaseDriver.d.ts +2 -0
- package/drivers/DatabaseDriver.js +16 -3
- package/entity/EntityAssigner.js +15 -3
- package/entity/EntityFactory.d.ts +8 -0
- package/entity/EntityFactory.js +69 -3
- package/entity/EntityLoader.js +31 -15
- package/entity/WrappedEntity.js +6 -1
- package/entity/defineEntity.d.ts +32 -19
- package/entity/defineEntity.js +9 -0
- package/entity/validators.js +4 -1
- package/errors.d.ts +7 -0
- package/errors.js +15 -0
- package/hydration/ObjectHydrator.js +6 -2
- package/metadata/MetadataDiscovery.d.ts +10 -0
- package/metadata/MetadataDiscovery.js +178 -30
- package/metadata/MetadataStorage.d.ts +8 -0
- package/metadata/MetadataStorage.js +47 -3
- package/metadata/MetadataValidator.js +2 -1
- package/metadata/types.d.ts +12 -0
- package/package.json +1 -1
- package/platforms/Platform.d.ts +6 -0
- package/platforms/Platform.js +26 -0
- package/serialization/EntitySerializer.js +52 -12
- package/types/DecimalType.js +34 -1
- package/types/DoubleType.d.ts +7 -3
- package/types/DoubleType.js +8 -3
- package/typings.d.ts +26 -6
- package/typings.js +5 -2
- package/unit-of-work/ChangeSetComputer.js +22 -7
- package/unit-of-work/ChangeSetPersister.js +24 -5
- package/unit-of-work/CommitOrderCalculator.js +2 -1
- package/unit-of-work/IdentityMap.d.ts +3 -2
- package/unit-of-work/IdentityMap.js +30 -7
- package/unit-of-work/UnitOfWork.js +45 -7
- package/utils/Configuration.d.ts +12 -1
- package/utils/EntityComparator.d.ts +5 -0
- package/utils/EntityComparator.js +87 -19
- package/utils/QueryHelper.d.ts +14 -0
- package/utils/QueryHelper.js +90 -13
- package/utils/RawQueryFragment.d.ts +5 -1
- package/utils/RawQueryFragment.js +27 -19
- package/utils/TransactionManager.js +4 -0
- package/utils/Utils.d.ts +2 -0
- package/utils/Utils.js +24 -8
- package/utils/clone.js +5 -7
- package/utils/env-vars.js +2 -0
- package/utils/fs-utils.js +19 -15
- package/utils/upsert-utils.js +2 -1
|
@@ -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/types/DecimalType.js
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
import { Type } from './Type.js';
|
|
2
|
+
/** Scales a plain decimal string to an integer rounded half away from zero at `scale`, like the DB does. */
|
|
3
|
+
function toScaledInteger(value, scale) {
|
|
4
|
+
if (typeof value !== 'string') {
|
|
5
|
+
return undefined;
|
|
6
|
+
}
|
|
7
|
+
const negative = value.startsWith('-');
|
|
8
|
+
const [int, fraction = ''] = (negative ? value.slice(1) : value).split('.');
|
|
9
|
+
const digits = int + fraction;
|
|
10
|
+
for (let i = 0; i < digits.length; i++) {
|
|
11
|
+
const code = digits.charCodeAt(i);
|
|
12
|
+
if (code < 48 || code > 57) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
let scaled = BigInt('0' + int + fraction.slice(0, scale).padEnd(scale, '0'));
|
|
17
|
+
if (fraction.charCodeAt(scale) >= 53) {
|
|
18
|
+
scaled++;
|
|
19
|
+
}
|
|
20
|
+
return negative ? -scaled : scaled;
|
|
21
|
+
}
|
|
2
22
|
/**
|
|
3
23
|
* Type that maps an SQL DECIMAL to a JS string or number.
|
|
4
24
|
*/
|
|
@@ -16,7 +36,20 @@ export class DecimalType extends Type {
|
|
|
16
36
|
return String(value);
|
|
17
37
|
}
|
|
18
38
|
compareValues(a, b) {
|
|
19
|
-
|
|
39
|
+
if (a === b) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
const scale = this.prop?.scale;
|
|
43
|
+
if (this.platform.formatDecimal(a, scale) !== this.platform.formatDecimal(b, scale)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
// doubles are exact only up to 15 significant digits, so only higher precision strings can differ past the numeric check
|
|
47
|
+
if ((this.prop?.precision ?? 0) <= 15 || this.compareAsType() !== 'string') {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
const scaledA = toScaledInteger(a, scale ?? 0);
|
|
51
|
+
const scaledB = toScaledInteger(b, scale ?? 0);
|
|
52
|
+
return scaledA == null || scaledB == null || scaledA === scaledB;
|
|
20
53
|
}
|
|
21
54
|
getColumnType(prop, platform) {
|
|
22
55
|
return platform.getDecimalTypeDeclarationSQL(prop);
|
package/types/DoubleType.d.ts
CHANGED
|
@@ -2,10 +2,14 @@ import { Type } from './Type.js';
|
|
|
2
2
|
import type { Platform } from '../platforms/Platform.js';
|
|
3
3
|
import type { EntityProperty } from '../typings.js';
|
|
4
4
|
/**
|
|
5
|
-
* Type that maps an SQL DOUBLE to a JS
|
|
5
|
+
* Type that maps an SQL DOUBLE to a JS number (default) or string.
|
|
6
6
|
*/
|
|
7
|
-
export declare class DoubleType extends
|
|
8
|
-
|
|
7
|
+
export declare class DoubleType<Mode extends 'number' | 'string' = 'number'> extends Type<JSTypeByMode<Mode>, string> {
|
|
8
|
+
mode?: Mode | undefined;
|
|
9
|
+
constructor(mode?: Mode | undefined);
|
|
10
|
+
convertToJSValue(value: string): JSTypeByMode<Mode>;
|
|
9
11
|
getColumnType(prop: EntityProperty, platform: Platform): string;
|
|
10
12
|
compareAsType(): string;
|
|
11
13
|
}
|
|
14
|
+
type JSTypeByMode<Mode extends 'number' | 'string'> = Mode extends 'number' ? number : string;
|
|
15
|
+
export {};
|
package/types/DoubleType.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { Type } from './Type.js';
|
|
2
2
|
/**
|
|
3
|
-
* Type that maps an SQL DOUBLE to a JS
|
|
3
|
+
* Type that maps an SQL DOUBLE to a JS number (default) or string.
|
|
4
4
|
*/
|
|
5
5
|
export class DoubleType extends Type {
|
|
6
|
+
mode;
|
|
7
|
+
constructor(mode) {
|
|
8
|
+
super();
|
|
9
|
+
this.mode = mode;
|
|
10
|
+
}
|
|
6
11
|
/* v8 ignore next */
|
|
7
12
|
convertToJSValue(value) {
|
|
8
|
-
if (this.prop?.runtimeType === 'number') {
|
|
13
|
+
if ((this.mode ?? this.prop?.runtimeType) === 'number') {
|
|
9
14
|
return +value;
|
|
10
15
|
}
|
|
11
16
|
return String(value);
|
|
@@ -14,6 +19,6 @@ export class DoubleType extends Type {
|
|
|
14
19
|
return platform.getDoubleDeclarationSQL();
|
|
15
20
|
}
|
|
16
21
|
compareAsType() {
|
|
17
|
-
return this.prop?.runtimeType ?? 'number';
|
|
22
|
+
return this.mode ?? this.prop?.runtimeType ?? 'number';
|
|
18
23
|
}
|
|
19
24
|
}
|
package/typings.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ import type { SerializeOptions } from './serialization/EntitySerializer.js';
|
|
|
12
12
|
import type { MetadataStorage } from './metadata/MetadataStorage.js';
|
|
13
13
|
import type { Routine } from './metadata/Routine.js';
|
|
14
14
|
import type { EntitySchema } from './metadata/EntitySchema.js';
|
|
15
|
-
import type { EntityPartitionBy, IndexColumnOptions } from './metadata/types.js';
|
|
15
|
+
import type { EntityPartitionBy, IndexColumnOptions, SequenceOptions } from './metadata/types.js';
|
|
16
16
|
import type { Type, types } from './types/index.js';
|
|
17
17
|
import type { Platform } from './platforms/Platform.js';
|
|
18
18
|
import type { Configuration } from './utils/Configuration.js';
|
|
@@ -358,12 +358,15 @@ type ElemMatchCondition<T extends Record<string, any>> = {
|
|
|
358
358
|
$and?: ElemMatchCondition<T>[];
|
|
359
359
|
$not?: ElemMatchCondition<T>;
|
|
360
360
|
};
|
|
361
|
-
type
|
|
361
|
+
type ArrayFilter<T> = T extends readonly (infer E)[] ? [E] extends [Scalar] ? {
|
|
362
|
+
$in?: readonly (readonly ExpandScalar<E>[])[];
|
|
363
|
+
$nin?: readonly (readonly ExpandScalar<E>[])[];
|
|
364
|
+
} : E extends Record<string, any> ? {
|
|
362
365
|
$elemMatch: ElemMatchCondition<E>;
|
|
363
366
|
} : never : never;
|
|
364
367
|
/** Object form of a filter query, mapping entity keys to their filter conditions. */
|
|
365
368
|
export type FilterObject<T> = {
|
|
366
|
-
-readonly [K in EntityKey<T>]?: ExpandQuery<ExpandProperty<FilterObjectProp<T, K>>> | ExpandQueryMerged<ExpandProperty<FilterObjectProp<T, K>>> | FilterValue<ExpandProperty<FilterObjectProp<T, K>>> |
|
|
369
|
+
-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;
|
|
367
370
|
};
|
|
368
371
|
/** Recursively expands a type into its `FilterQuery` form for nested object filtering. */
|
|
369
372
|
export type ExpandQuery<T> = T extends object ? (T extends Scalar ? never : FilterQuery<T>) : FilterValue<T>;
|
|
@@ -383,7 +386,7 @@ export type FilterQuery<T> = ObjectQuery<T> | NonNullable<ExpandScalar<Primary<T
|
|
|
383
386
|
* Used when `using` option is set in `FindOptions` to enforce type-safe index usage.
|
|
384
387
|
*/
|
|
385
388
|
export type IndexFilterQuery<T, Using extends string> = [Using] extends [never] ? FilterQuery<T> : (OperatorMap<T> & {
|
|
386
|
-
-readonly [K in Extract<EntityKey<T>, IndexColumns<T, Using>>]?: ExpandQuery<ExpandProperty<FilterObjectProp<T, K>>> | ExpandQueryMerged<ExpandProperty<FilterObjectProp<T, K>>> | FilterValue<ExpandProperty<FilterObjectProp<T, K>>> |
|
|
389
|
+
-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;
|
|
387
390
|
}) | NonNullable<ExpandScalar<Primary<T>>> | IndexFilterQuery<T, Using>[];
|
|
388
391
|
/** Replaces `where` and `using` on an options type with index-aware variants when `Using` is specified. */
|
|
389
392
|
export type WithUsingOptions<Opts, Entity, Using extends string> = Omit<Opts, 'where' | 'using'> & {
|
|
@@ -564,6 +567,13 @@ export declare namespace LazyRef {
|
|
|
564
567
|
[__lazyRef]?: (arg: T) => T;
|
|
565
568
|
}
|
|
566
569
|
}
|
|
570
|
+
/**
|
|
571
|
+
* The brand's only key is optional, so an object type without named properties (like `Record<string, number>`)
|
|
572
|
+
* extends `LazyRef.Brand<infer U>` too, with `U` inferred as `unknown`. `LazyRef<T>` needs `T extends object`,
|
|
573
|
+
* so an `unknown` target means the type is not a `LazyRef` and resolves to `Fallback`. `any` stays `any`.
|
|
574
|
+
* Only checked once `unknown extends U` holds, so a real `LazyRef` skips it.
|
|
575
|
+
*/
|
|
576
|
+
type NotLazyRef<U, Fallback> = IsAny<U> extends true ? U : Fallback;
|
|
567
577
|
/** Alias for `Reference<T> & { id: number }` (see {@apilink Ref}). */
|
|
568
578
|
export type EntityRef<T extends object> = true extends IsUnknown<PrimaryProperty<T>> ? Reference<T> : IsAny<T> extends true ? Reference<T> : {
|
|
569
579
|
[K in PrimaryProperty<T> & keyof T]: T[K];
|
|
@@ -986,6 +996,7 @@ export interface EntityProperty<Owner = any, Target = any> {
|
|
|
986
996
|
hasConvertToJSValueSQL: boolean;
|
|
987
997
|
hasConvertToDatabaseValueSQL: boolean;
|
|
988
998
|
autoincrement?: boolean;
|
|
999
|
+
sequence?: SequenceOptions;
|
|
989
1000
|
returning?: boolean;
|
|
990
1001
|
primary?: boolean;
|
|
991
1002
|
serializedPrimaryKey: boolean;
|
|
@@ -1175,6 +1186,7 @@ export interface EntityMetadata<Entity = any, Class extends EntityCtor<Entity> =
|
|
|
1175
1186
|
meta: EntityMetadata<Entity>;
|
|
1176
1187
|
prop: EntityProperty<Entity>;
|
|
1177
1188
|
}[];
|
|
1189
|
+
targetKeys?: EntityKey<Entity>[];
|
|
1178
1190
|
comparableProps: EntityProperty<Entity>[];
|
|
1179
1191
|
trackingProps: EntityProperty<Entity>[];
|
|
1180
1192
|
hydrateProps: EntityProperty<Entity>[];
|
|
@@ -1258,6 +1270,8 @@ export interface EntityMetadata<Entity = any, Class extends EntityCtor<Entity> =
|
|
|
1258
1270
|
orderBy?: QueryOrderMap<Entity> | QueryOrderMap<Entity>[];
|
|
1259
1271
|
/** @internal can be used for computed numeric cache keys */
|
|
1260
1272
|
readonly _id: number;
|
|
1273
|
+
/** @internal name that does not depend on module evaluation order (unlike `_id`), used to key `compiledFunctions` */
|
|
1274
|
+
compiledName?: string;
|
|
1261
1275
|
}
|
|
1262
1276
|
/** Options for `ISchemaGenerator.create()`. */
|
|
1263
1277
|
export interface CreateSchemaOptions {
|
|
@@ -1616,7 +1630,8 @@ export type UnboxArray<T> = T extends any[] ? ArrayElement<T> : T;
|
|
|
1616
1630
|
/** Extracts the element type from an array type. */
|
|
1617
1631
|
export type ArrayElement<ArrayType extends unknown[]> = ArrayType extends (infer ElementType)[] ? ElementType : never;
|
|
1618
1632
|
/** Unwraps a property type from its wrapper (Reference, Collection, or array) to the inner entity type. */
|
|
1619
|
-
export type ExpandProperty<T> =
|
|
1633
|
+
export type ExpandProperty<T> = ExpandPropertyOf<T, T>;
|
|
1634
|
+
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>;
|
|
1620
1635
|
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;
|
|
1621
1636
|
type IsTrue<T> = IsNever<T> extends true ? false : T extends boolean ? (T extends true ? true : false) : false;
|
|
1622
1637
|
type StringLiteral<T> = T extends string ? (string extends T ? never : T) : never;
|
|
@@ -1651,7 +1666,12 @@ export type MergeSelected<T, U, F extends string> = IsLoadedType<T> extends true
|
|
|
1651
1666
|
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>;
|
|
1652
1667
|
/** Extracts the nullability modifiers (`null`, `undefined`, or both) from a type `T`. */
|
|
1653
1668
|
export type AddOptional<T> = undefined | null extends T ? null | undefined : null extends T ? null : undefined extends T ? undefined : never;
|
|
1654
|
-
|
|
1669
|
+
/**
|
|
1670
|
+
* Like {@link ExtractTypeWithLazyRef}, but a record type that matched `LazyRef.Brand` resolves to itself (see
|
|
1671
|
+
* {@link NotLazyRef}). `AutoPath` keeps the unguarded version, as it is on its hot path.
|
|
1672
|
+
*/
|
|
1673
|
+
type ExtractLoadedType<T> = T extends LazyRef.Brand<infer U> ? (unknown extends U ? NotLazyRef<U, ExtractType<T>> : U) : ExtractType<T>;
|
|
1674
|
+
type LoadedProp<T, L extends string = never, F extends string = '*', E extends string = never> = LoadedLoadable<T, Loaded<ExtractLoadedType<T>, L, F, E>>;
|
|
1655
1675
|
/** Extracts the eager-loaded property names declared via `[EagerProps]` as a string union. */
|
|
1656
1676
|
export type AddEager<T> = ExtractEagerProps<T> & string;
|
|
1657
1677
|
/** Combines an explicit populate hint `L` with the entity's eagerly loaded properties. */
|
package/typings.js
CHANGED
|
@@ -304,8 +304,11 @@ export class EntityMetadata {
|
|
|
304
304
|
}, { __gettersDefined: { value: true, enumerable: false } });
|
|
305
305
|
}
|
|
306
306
|
initIndexes(prop) {
|
|
307
|
-
|
|
308
|
-
const
|
|
307
|
+
// `prop.index`/`prop.unique` keep only the name, so an index with any other option must stay in `meta.indexes`
|
|
308
|
+
const isSimple = (index) => index.properties === prop.name &&
|
|
309
|
+
Object.entries(index).every(([key, value]) => key === 'name' || key === 'properties' || value == null);
|
|
310
|
+
const simpleIndex = this.indexes.find(isSimple);
|
|
311
|
+
const simpleUnique = this.uniques.find(isSimple);
|
|
309
312
|
const owner = prop.kind === ReferenceKind.MANY_TO_ONE;
|
|
310
313
|
if (!prop.index && simpleIndex) {
|
|
311
314
|
Utils.defaultValue(simpleIndex, 'name', true);
|
|
@@ -7,6 +7,7 @@ import { Reference } from '../entity/Reference.js';
|
|
|
7
7
|
import { PolymorphicRef } from '../entity/PolymorphicRef.js';
|
|
8
8
|
import { ReferenceKind } from '../enums.js';
|
|
9
9
|
import { isRaw } from '../utils/RawQueryFragment.js';
|
|
10
|
+
import { ValidationError } from '../errors.js';
|
|
10
11
|
/** @internal Computes change sets by comparing entity state against original snapshots. */
|
|
11
12
|
export class ChangeSetComputer {
|
|
12
13
|
#comparator;
|
|
@@ -48,6 +49,10 @@ export class ChangeSetComputer {
|
|
|
48
49
|
if (Utils.equals(data, wrapped.__originalEntityData)) {
|
|
49
50
|
return null;
|
|
50
51
|
}
|
|
52
|
+
// a reference known only by its `targetKey` has no PK, so the update would have no condition
|
|
53
|
+
if (!wrapped.hasPrimaryKey()) {
|
|
54
|
+
throw ValidationError.referenceWithoutPK(entity);
|
|
55
|
+
}
|
|
51
56
|
}
|
|
52
57
|
const changeSet = new ChangeSet(entity, type, this.computePayload(entity), meta);
|
|
53
58
|
changeSet.originalEntity = wrapped.__originalEntityData;
|
|
@@ -160,23 +165,33 @@ export class ChangeSetComputer {
|
|
|
160
165
|
const targets = Utils.unwrapProperty(changeSet.entity, changeSet.meta, prop);
|
|
161
166
|
targets.forEach(([rawTarget, idx]) => {
|
|
162
167
|
const target = Reference.unwrapReference(rawTarget);
|
|
163
|
-
|
|
168
|
+
// a same-named scalar of another polymorphic embeddable variant is not a reference
|
|
169
|
+
const needsProcessing = Utils.isEntity(target) && (prop.targetKey != null || !target.__helper.hasPrimaryKey());
|
|
164
170
|
if (needsProcessing) {
|
|
171
|
+
// a reference known only by its `targetKey` has no PK, and is never inserted to generate one
|
|
172
|
+
if (!prop.targetKey && !target.__helper.__initialized) {
|
|
173
|
+
throw ValidationError.referenceWithoutPK(target);
|
|
174
|
+
}
|
|
165
175
|
let value = prop.targetKey ? target[prop.targetKey] : target.__helper.__identifier;
|
|
166
|
-
|
|
167
|
-
if (prop.targetKey
|
|
168
|
-
const targetProp =
|
|
176
|
+
// use the actual target's meta, a polymorphic relation has several
|
|
177
|
+
if (prop.targetKey) {
|
|
178
|
+
const targetProp = target.__meta.properties[prop.targetKey];
|
|
169
179
|
if (targetProp?.customType) {
|
|
170
180
|
value = targetProp.customType.convertToDatabaseValue(value, this.#platform, { mode: 'serialization' });
|
|
171
181
|
}
|
|
172
182
|
}
|
|
173
183
|
if (prop.polymorphic) {
|
|
174
184
|
const discriminator = QueryHelper.findDiscriminatorValue(prop.discriminatorMap, target.constructor);
|
|
175
|
-
|
|
185
|
+
value = new PolymorphicRef(discriminator, value);
|
|
176
186
|
}
|
|
177
|
-
|
|
178
|
-
|
|
187
|
+
// an unchanged `targetKey` FK is not part of the diff
|
|
188
|
+
if (prop.targetKey &&
|
|
189
|
+
changeSet.type === ChangeSetType.UPDATE &&
|
|
190
|
+
!(prop.name in changeSet.payload) &&
|
|
191
|
+
Utils.equals(changeSet.originalEntity?.[prop.name], value)) {
|
|
192
|
+
return;
|
|
179
193
|
}
|
|
194
|
+
Utils.setPayloadProperty(changeSet.payload, changeSet.meta, prop, value, idx);
|
|
180
195
|
}
|
|
181
196
|
});
|
|
182
197
|
}
|
|
@@ -196,10 +196,17 @@ export class ChangeSetPersister {
|
|
|
196
196
|
}
|
|
197
197
|
async persistManagedEntities(meta, changeSets, options) {
|
|
198
198
|
const size = this.#config.get('batchSize');
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
199
|
+
// a batch checks the version in all rows or none, so references without a loaded version go separately
|
|
200
|
+
const hasVersion = (cs) => cs.entity[meta.versionProperty] != null;
|
|
201
|
+
const groups = meta.ownsVersionProperty()
|
|
202
|
+
? [changeSets.filter(hasVersion), changeSets.filter(cs => !hasVersion(cs))]
|
|
203
|
+
: [changeSets];
|
|
204
|
+
for (const group of groups) {
|
|
205
|
+
for (let i = 0; i < group.length; i += size) {
|
|
206
|
+
const chunk = group.slice(i, i + size);
|
|
207
|
+
await this.persistManagedEntitiesBatch(meta, chunk, options);
|
|
208
|
+
await this.reloadVersionValues(meta, chunk, options);
|
|
209
|
+
}
|
|
203
210
|
}
|
|
204
211
|
}
|
|
205
212
|
checkConcurrencyKeys(meta, changeSet, cond) {
|
|
@@ -223,9 +230,14 @@ export class ChangeSetPersister {
|
|
|
223
230
|
});
|
|
224
231
|
const cond = [];
|
|
225
232
|
const payload = [];
|
|
233
|
+
// like in `updateEntity()`, but the batch needs the version in all rows or none
|
|
234
|
+
const versioned = meta.ownsVersionProperty() && changeSets.every(cs => cs.entity[meta.versionProperty] != null);
|
|
226
235
|
for (const changeSet of changeSets) {
|
|
227
236
|
const where = changeSet.getPrimaryKey(true);
|
|
228
237
|
this.checkConcurrencyKeys(meta, changeSet, where);
|
|
238
|
+
if (versioned) {
|
|
239
|
+
where[meta.versionProperty] = this.#platform.convertVersionValue(changeSet.entity[meta.versionProperty], meta.properties[meta.versionProperty]);
|
|
240
|
+
}
|
|
229
241
|
cond.push(where);
|
|
230
242
|
payload.push(changeSet.payload);
|
|
231
243
|
}
|
|
@@ -235,6 +247,12 @@ export class ChangeSetPersister {
|
|
|
235
247
|
// from those to be able to match them with `getSerializedPrimaryKey()` of the entity
|
|
236
248
|
const pkFields = meta.getPrimaryProps().flatMap(prop => prop.fieldNames);
|
|
237
249
|
res.rows?.forEach(item => map.set(Utils.getPrimaryKeyHash(pkFields.map(field => item[field])), item));
|
|
250
|
+
if ((meta.ownsVersionProperty() || meta.getOwnConcurrencyCheckKeys().length > 0) &&
|
|
251
|
+
res.affectedRows < changeSets.length) {
|
|
252
|
+
// without `returning`, the stale entity can't be identified, so we report the first one
|
|
253
|
+
const failed = changeSets.find(cs => !map.has(helper(cs.entity).getSerializedPrimaryKey())) ?? changeSets[0];
|
|
254
|
+
throw OptimisticLockError.lockFailed(failed.entity);
|
|
255
|
+
}
|
|
238
256
|
for (const changeSet of changeSets) {
|
|
239
257
|
if (res.rows) {
|
|
240
258
|
const row = map.get(helper(changeSet.entity).getSerializedPrimaryKey());
|
|
@@ -484,7 +502,8 @@ export class ChangeSetPersister {
|
|
|
484
502
|
}
|
|
485
503
|
if (upsert) {
|
|
486
504
|
for (const prop of meta.props) {
|
|
487
|
-
|
|
505
|
+
// null is not converted, the same way the hydrator handles it
|
|
506
|
+
if (prop.customType && mapped[prop.name] != null) {
|
|
488
507
|
mapped[prop.name] = prop.customType.convertToJSValue(mapped[prop.name], this.#platform);
|
|
489
508
|
}
|
|
490
509
|
}
|
|
@@ -44,7 +44,8 @@ export class CommitOrderCalculator {
|
|
|
44
44
|
if (!toOneOwner && !toManyOwner) {
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
// TPT tables are separate nodes and the FK references the target's own table, not the root
|
|
48
|
+
const propertyType = (prop.targetMeta?.inheritanceType === 'tpt' ? prop.targetMeta : prop.targetMeta?.root)?._id;
|
|
48
49
|
if (propertyType == null || !this.hasNode(propertyType)) {
|
|
49
50
|
return;
|
|
50
51
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { AnyEntity, EntityMetadata } from '../typings.js';
|
|
2
|
-
/** @internal Stores managed entity instances keyed by their primary key hash, ensuring each row is loaded once. */
|
|
2
|
+
/** @internal Stores managed entity instances keyed by their primary key hash (and `targetKey` values), ensuring each row is loaded once. */
|
|
3
3
|
export declare class IdentityMap {
|
|
4
4
|
#private;
|
|
5
5
|
constructor(defaultSchema?: string);
|
|
6
|
-
/** Stores an entity in the identity map under its primary key hash. */
|
|
6
|
+
/** Stores an entity in the identity map under its primary key hash and the `targetKey` values referencing it. */
|
|
7
7
|
store<T>(item: T): void;
|
|
8
8
|
/**
|
|
9
9
|
* Stores an entity under an alternate key (non-PK property).
|
|
@@ -25,6 +25,7 @@ export declare class IdentityMap {
|
|
|
25
25
|
/** Returns all entities currently in the identity map. */
|
|
26
26
|
values(): AnyEntity[];
|
|
27
27
|
[Symbol.iterator](): IterableIterator<AnyEntity>;
|
|
28
|
+
private getEntities;
|
|
28
29
|
/** Returns all hash keys currently in the identity map. */
|
|
29
30
|
keys(): string[];
|
|
30
31
|
/**
|
|
@@ -1,15 +1,29 @@
|
|
|
1
|
-
/** @internal Stores managed entity instances keyed by their primary key hash, ensuring each row is loaded once. */
|
|
1
|
+
/** @internal Stores managed entity instances keyed by their primary key hash (and `targetKey` values), ensuring each row is loaded once. */
|
|
2
2
|
export class IdentityMap {
|
|
3
3
|
#defaultSchema;
|
|
4
4
|
#registry = new Map();
|
|
5
5
|
/** Tracks alternate key hashes for each entity so we can clean them up on delete */
|
|
6
6
|
#alternateKeys = new WeakMap();
|
|
7
|
+
#storesWithAlternateKeys = new WeakSet();
|
|
7
8
|
constructor(defaultSchema) {
|
|
8
9
|
this.#defaultSchema = defaultSchema;
|
|
9
10
|
}
|
|
10
|
-
/** Stores an entity in the identity map under its primary key hash. */
|
|
11
|
+
/** Stores an entity in the identity map under its primary key hash and the `targetKey` values referencing it. */
|
|
11
12
|
store(item) {
|
|
12
|
-
|
|
13
|
+
const meta = item.__meta.root;
|
|
14
|
+
const wrapped = item.__helper;
|
|
15
|
+
// PK-less references (known only by a `targetKey`) would share one slot that outlives their later removal
|
|
16
|
+
if (!meta.targetKeys || wrapped.hasPrimaryKey()) {
|
|
17
|
+
this.getStore(meta).set(this.getPkHash(item), item);
|
|
18
|
+
}
|
|
19
|
+
// references resolved via `targetKey` look the entity up by that key instead of the PK
|
|
20
|
+
for (const key of meta.targetKeys ?? []) {
|
|
21
|
+
const value = item[key];
|
|
22
|
+
if (value != null) {
|
|
23
|
+
const schema = wrapped.__schema ?? meta.schema ?? this.#defaultSchema;
|
|
24
|
+
this.storeByKey(item, key, '' + value, schema);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
13
27
|
}
|
|
14
28
|
/**
|
|
15
29
|
* Stores an entity under an alternate key (non-PK property).
|
|
@@ -17,7 +31,9 @@ export class IdentityMap {
|
|
|
17
31
|
*/
|
|
18
32
|
storeByKey(item, key, value, schema) {
|
|
19
33
|
const hash = this.getKeyHash(key, value, schema);
|
|
20
|
-
this.getStore(item.__meta.root)
|
|
34
|
+
const store = this.getStore(item.__meta.root);
|
|
35
|
+
store.set(hash, item);
|
|
36
|
+
this.#storesWithAlternateKeys.add(store);
|
|
21
37
|
// Track this alternate key so we can clean it up when the entity is deleted
|
|
22
38
|
let keys = this.#alternateKeys.get(item);
|
|
23
39
|
if (!keys) {
|
|
@@ -35,7 +51,10 @@ export class IdentityMap {
|
|
|
35
51
|
const altKeys = this.#alternateKeys.get(item);
|
|
36
52
|
if (altKeys) {
|
|
37
53
|
for (const hash of altKeys) {
|
|
38
|
-
|
|
54
|
+
// the hash might be owned by another entity by now, e.g. after swapping unique values
|
|
55
|
+
if (store.get(hash) === item) {
|
|
56
|
+
store.delete(hash);
|
|
57
|
+
}
|
|
39
58
|
}
|
|
40
59
|
this.#alternateKeys.delete(item);
|
|
41
60
|
}
|
|
@@ -69,7 +88,7 @@ export class IdentityMap {
|
|
|
69
88
|
values() {
|
|
70
89
|
const ret = [];
|
|
71
90
|
for (const store of this.#registry.values()) {
|
|
72
|
-
for (const item of
|
|
91
|
+
for (const item of this.getEntities(store)) {
|
|
73
92
|
ret.push(item);
|
|
74
93
|
}
|
|
75
94
|
}
|
|
@@ -77,11 +96,15 @@ export class IdentityMap {
|
|
|
77
96
|
}
|
|
78
97
|
*[Symbol.iterator]() {
|
|
79
98
|
for (const store of this.#registry.values()) {
|
|
80
|
-
for (const item of
|
|
99
|
+
for (const item of this.getEntities(store)) {
|
|
81
100
|
yield item;
|
|
82
101
|
}
|
|
83
102
|
}
|
|
84
103
|
}
|
|
104
|
+
getEntities(store) {
|
|
105
|
+
// an entity indexed by alternate keys occupies several slots of its store
|
|
106
|
+
return this.#storesWithAlternateKeys.has(store) ? new Set(store.values()) : store.values();
|
|
107
|
+
}
|
|
85
108
|
/** Returns all hash keys currently in the identity map. */
|
|
86
109
|
keys() {
|
|
87
110
|
const ret = [];
|