@mikro-orm/core 7.1.0-dev.9 → 7.1.0
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.d.ts +47 -3
- package/EntityManager.js +150 -23
- package/README.md +2 -1
- package/connections/Connection.d.ts +67 -1
- package/connections/Connection.js +64 -0
- package/drivers/IDatabaseDriver.d.ts +20 -6
- package/entity/BaseEntity.d.ts +68 -1
- package/entity/BaseEntity.js +18 -0
- package/entity/Collection.d.ts +1 -1
- package/entity/Collection.js +4 -1
- package/entity/EntityAssigner.js +8 -0
- package/entity/EntityFactory.js +5 -3
- package/entity/EntityLoader.d.ts +8 -1
- package/entity/EntityLoader.js +97 -29
- package/entity/Reference.d.ts +42 -1
- package/entity/Reference.js +9 -0
- package/entity/defineEntity.d.ts +69 -16
- package/entity/defineEntity.js +17 -0
- package/entity/utils.js +4 -5
- package/enums.d.ts +8 -1
- package/index.d.ts +1 -1
- package/metadata/EntitySchema.js +18 -0
- package/metadata/MetadataDiscovery.d.ts +1 -0
- package/metadata/MetadataDiscovery.js +79 -15
- package/metadata/MetadataValidator.d.ts +26 -0
- package/metadata/MetadataValidator.js +228 -1
- package/metadata/Routine.d.ts +74 -0
- package/metadata/Routine.js +140 -0
- package/metadata/index.d.ts +1 -0
- package/metadata/index.js +1 -0
- package/metadata/types.d.ts +75 -8
- package/package.json +1 -1
- package/platforms/Platform.d.ts +13 -3
- package/platforms/Platform.js +51 -6
- package/serialization/EntitySerializer.js +4 -3
- package/typings.d.ts +323 -21
- package/typings.js +32 -12
- package/unit-of-work/UnitOfWork.js +43 -5
- package/utils/AbstractMigrator.d.ts +9 -4
- package/utils/AbstractMigrator.js +60 -28
- package/utils/AbstractSchemaGenerator.d.ts +1 -1
- package/utils/AbstractSchemaGenerator.js +4 -1
- package/utils/Configuration.d.ts +30 -0
- package/utils/Configuration.js +45 -0
- package/utils/EntityComparator.js +1 -1
- package/utils/TransactionManager.js +2 -0
- package/utils/Utils.js +1 -1
- package/utils/index.d.ts +1 -0
- package/utils/index.js +1 -0
- package/utils/partition-utils.d.ts +17 -0
- package/utils/partition-utils.js +79 -0
- package/utils/upsert-utils.d.ts +2 -0
- package/utils/upsert-utils.js +26 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ConnectionType, Constructor, EntityData, EntityMetadata, EntityProperty, FilterQuery, Primary, Dictionary, IPrimaryKey, PopulateOptions, EntityDictionary, AutoPath, ObjectQuery, FilterObject, Populate, EntityName, PopulateHintOptions, Prefixes, IndexName } from '../typings.js';
|
|
2
|
-
import type { Connection, QueryResult, Transaction } from '../connections/Connection.js';
|
|
2
|
+
import type { AbortQueryOptions, Connection, QueryResult, Transaction } from '../connections/Connection.js';
|
|
3
3
|
import type { FlushMode, LockMode, QueryOrderMap, QueryFlag, LoadStrategy, PopulateHint, PopulatePath } from '../enums.js';
|
|
4
4
|
import type { Platform } from '../platforms/Platform.js';
|
|
5
5
|
import type { MetadataStorage } from '../metadata/MetadataStorage.js';
|
|
@@ -110,6 +110,20 @@ export interface FindAllOptions<T, P extends string = never, F extends string =
|
|
|
110
110
|
}
|
|
111
111
|
/** Options for streaming query results via `em.stream()`. */
|
|
112
112
|
export interface StreamOptions<Entity, Populate extends string = never, Fields extends string = never, Exclude extends string = never> extends Omit<FindAllOptions<Entity, Populate, Fields, Exclude>, 'cache' | 'before' | 'after' | 'first' | 'last' | 'overfetch' | 'strategy'> {
|
|
113
|
+
/**
|
|
114
|
+
* How many rows to fetch in one round-trip.
|
|
115
|
+
* Lower values will result in more queries and network bandwidth, but less memory usage.
|
|
116
|
+
* Higher values will result in fewer queries and network bandwidth, but higher memory usage.
|
|
117
|
+
* Note that the results are iterated one row at a time regardless of this value.
|
|
118
|
+
*
|
|
119
|
+
* Honored on PostgreSQL (cursor-based fetch), MSSQL (tedious stream chunk size),
|
|
120
|
+
* Oracle (mapped to `fetchArraySize`) and MongoDB (mapped to `batchSize`). Ignored
|
|
121
|
+
* on MySQL, MariaDB, SQLite and libSQL, where the underlying driver already streams
|
|
122
|
+
* row-by-row with no batching knob.
|
|
123
|
+
*
|
|
124
|
+
* @default 100 (on dialects that honor it)
|
|
125
|
+
*/
|
|
126
|
+
chunkSize?: number;
|
|
113
127
|
/**
|
|
114
128
|
* When populating to-many relations, the ORM streams fully merged entities instead of yielding every row.
|
|
115
129
|
* You can opt out of this behavior by specifying `mergeResults: false`. This will yield every row from
|
|
@@ -130,7 +144,7 @@ export interface LoadHint<Entity, Hint extends string = never, Fields extends st
|
|
|
130
144
|
exclude?: readonly AutoPath<Entity, Excludes>[];
|
|
131
145
|
}
|
|
132
146
|
/** Options for `em.find()` queries, including population, ordering, pagination, and locking. */
|
|
133
|
-
export interface FindOptions<Entity, Hint extends string = never, Fields extends string = never, Excludes extends string = never> extends LoadHint<Entity, Hint, Fields, Excludes
|
|
147
|
+
export interface FindOptions<Entity, Hint extends string = never, Fields extends string = never, Excludes extends string = never> extends LoadHint<Entity, Hint, Fields, Excludes>, AbortQueryOptions {
|
|
134
148
|
/**
|
|
135
149
|
* Where condition for populated relations. This will have no effect on the root entity.
|
|
136
150
|
* With `select-in` strategy, this is applied only to the populate queries.
|
|
@@ -260,7 +274,7 @@ export interface FindOneOrFailOptions<T extends object, P extends string = never
|
|
|
260
274
|
strict?: boolean;
|
|
261
275
|
}
|
|
262
276
|
/** Options for native insert and update operations. */
|
|
263
|
-
export interface NativeInsertUpdateOptions<T> {
|
|
277
|
+
export interface NativeInsertUpdateOptions<T> extends AbortQueryOptions {
|
|
264
278
|
convertCustomTypes?: boolean;
|
|
265
279
|
ctx?: Transaction;
|
|
266
280
|
schema?: string;
|
|
@@ -294,7 +308,7 @@ export interface UpsertManyOptions<Entity, Fields extends string = never> extend
|
|
|
294
308
|
batchSize?: number;
|
|
295
309
|
}
|
|
296
310
|
/** Options for `em.count()` queries. */
|
|
297
|
-
export interface CountOptions<T extends object, P extends string = never> {
|
|
311
|
+
export interface CountOptions<T extends object, P extends string = never> extends AbortQueryOptions {
|
|
298
312
|
filters?: FilterOptions;
|
|
299
313
|
schema?: string;
|
|
300
314
|
groupBy?: string | readonly string[];
|
|
@@ -336,7 +350,7 @@ export interface CountByOptions<T extends object> {
|
|
|
336
350
|
logging?: LoggingOptions;
|
|
337
351
|
}
|
|
338
352
|
/** Options for `em.qb().update()` operations. */
|
|
339
|
-
export interface UpdateOptions<T> {
|
|
353
|
+
export interface UpdateOptions<T> extends AbortQueryOptions {
|
|
340
354
|
filters?: FilterOptions;
|
|
341
355
|
schema?: string;
|
|
342
356
|
ctx?: Transaction;
|
|
@@ -375,7 +389,7 @@ export interface LockOptions extends DriverMethodOptions {
|
|
|
375
389
|
logging?: LoggingOptions;
|
|
376
390
|
}
|
|
377
391
|
/** Base options shared by all driver methods (transaction context, schema, logging). */
|
|
378
|
-
export interface DriverMethodOptions {
|
|
392
|
+
export interface DriverMethodOptions extends AbortQueryOptions {
|
|
379
393
|
ctx?: Transaction;
|
|
380
394
|
schema?: string;
|
|
381
395
|
loggerContext?: LogContext;
|
package/entity/BaseEntity.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Ref } from './Reference.js';
|
|
1
|
+
import { type LoadReferenceOptions, type LoadReferenceOrFailOptions, type Ref } from './Reference.js';
|
|
2
2
|
import type { AutoPath, EntityData, EntityDTO, ExtractFieldsHint, Loaded, LoadedReference, AddEager, EntityKey, FromEntityType, IsSubset, MergeSelected, ResolveSerializeFields, SerializeDTO, SerializeFieldsKeepPK } from '../typings.js';
|
|
3
3
|
import { type AssignOptions } from './EntityAssigner.js';
|
|
4
4
|
import type { EntityLoaderOptions } from './EntityLoader.js';
|
|
@@ -90,3 +90,70 @@ export declare abstract class BaseEntity {
|
|
|
90
90
|
/** Sets the database schema for this entity. */
|
|
91
91
|
setSchema(schema?: string): void;
|
|
92
92
|
}
|
|
93
|
+
type EntityConstructor<T extends object = object> = abstract new (...args: any[]) => T;
|
|
94
|
+
/**
|
|
95
|
+
* The `load()` / `loadOrFail()` methods added by the {@link Loadable} mixin. Declared as an interface so the
|
|
96
|
+
* mixin function can have an explicit return type (required by JSR fast-check).
|
|
97
|
+
*/
|
|
98
|
+
export interface LoadableEntity {
|
|
99
|
+
/**
|
|
100
|
+
* Ensures this entity is loaded (without reloading it if it already is). Returns the entity, or `null` if it
|
|
101
|
+
* was not found in the database (e.g. it was deleted in the meantime, or active filters disallow loading it).
|
|
102
|
+
* Use `loadOrFail()` if you want an error to be thrown in such a case.
|
|
103
|
+
*/
|
|
104
|
+
load<Entity extends this = this, Hint extends string = never, Fields extends string = never, Excludes extends string = never>(options?: LoadReferenceOptions<Entity, Hint, Fields, Excludes>): Promise<Loaded<Entity, Hint, Fields, Excludes> | null>;
|
|
105
|
+
/**
|
|
106
|
+
* Ensures this entity is loaded (without reloading it if it already is). Returns the entity, or throws an error
|
|
107
|
+
* just like `em.findOneOrFail()` (and respects the same config options) if it was not found.
|
|
108
|
+
*/
|
|
109
|
+
loadOrFail<Entity extends this = this, Hint extends string = never, Fields extends string = never, Excludes extends string = never>(options?: LoadReferenceOrFailOptions<Entity, Hint, Fields, Excludes>): Promise<Loaded<Entity, Hint, Fields, Excludes>>;
|
|
110
|
+
}
|
|
111
|
+
/** Return-type shape of {@link Loadable} — a constructor that produces instances of `TBase` enriched with {@link LoadableEntity}. */
|
|
112
|
+
export type LoadableConstructor<TBase extends EntityConstructor> = abstract new (...args: any[]) => InstanceType<TBase> & LoadableEntity;
|
|
113
|
+
/** Internal: rejects a base class that already defines `load` or `loadOrFail` to prevent silent override. */
|
|
114
|
+
type EnsureNoLoadConflict<TBase extends EntityConstructor> = InstanceType<TBase> extends {
|
|
115
|
+
load: any;
|
|
116
|
+
} ? 'Loadable: base class already defines `load` — remove it or do not apply the mixin' : InstanceType<TBase> extends {
|
|
117
|
+
loadOrFail: any;
|
|
118
|
+
} ? 'Loadable: base class already defines `loadOrFail` — remove it or do not apply the mixin' : TBase;
|
|
119
|
+
/** Empty base for {@link Loadable} when called without arguments — standalone mixin, no inherited base. */
|
|
120
|
+
declare abstract class EmptyBase {
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Mixin that adds `load()` / `loadOrFail()` methods to an entity class. These methods ensure the entity is loaded
|
|
124
|
+
* from the database without reloading it if it already is — unlike `init()`, which always refreshes.
|
|
125
|
+
*
|
|
126
|
+
* Useful when migrating from a non-`Ref`-based codebase where lazy loading support is desired without the
|
|
127
|
+
* `.$` / `.get()` indirection that the `Reference` wrapper requires. Opt-in so it does not conflict with entities
|
|
128
|
+
* that already define a `load` or `loadOrFail` property — applying the mixin to a base class that already has
|
|
129
|
+
* either method is a compile error to prevent silent override.
|
|
130
|
+
*
|
|
131
|
+
* Call without arguments (`Loadable()`) for a standalone base with no other inheritance, or pass a base class
|
|
132
|
+
* (`Loadable(BaseEntity)`) to compose. The convenience alias {@link LoadableBaseEntity} is shorthand for the
|
|
133
|
+
* latter.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```ts
|
|
137
|
+
* // compose with BaseEntity
|
|
138
|
+
* class User extends Loadable(BaseEntity) {
|
|
139
|
+
* @PrimaryKey()
|
|
140
|
+
* id!: number;
|
|
141
|
+
* }
|
|
142
|
+
*
|
|
143
|
+
* // standalone — no inherited base
|
|
144
|
+
* class Product extends Loadable() {
|
|
145
|
+
* @PrimaryKey()
|
|
146
|
+
* id!: number;
|
|
147
|
+
* }
|
|
148
|
+
*
|
|
149
|
+
* const user = orm.em.getReference(User, 1);
|
|
150
|
+
* await user.load();
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
export declare function Loadable(): LoadableConstructor<typeof EmptyBase> & typeof EmptyBase;
|
|
154
|
+
export declare function Loadable<TBase extends EntityConstructor>(Base: EnsureNoLoadConflict<TBase> extends TBase ? TBase : never): LoadableConstructor<TBase> & TBase;
|
|
155
|
+
declare const LoadableBaseEntityBase: LoadableConstructor<typeof BaseEntity> & typeof BaseEntity;
|
|
156
|
+
/** Convenience: `BaseEntity` pre-composed with the `Loadable` mixin. */
|
|
157
|
+
export declare abstract class LoadableBaseEntity extends LoadableBaseEntityBase {
|
|
158
|
+
}
|
|
159
|
+
export {};
|
package/entity/BaseEntity.js
CHANGED
|
@@ -49,3 +49,21 @@ export class BaseEntity {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
Object.defineProperty(BaseEntity.prototype, '__baseEntity', { value: true, writable: false, enumerable: false });
|
|
52
|
+
/** Empty base for {@link Loadable} when called without arguments — standalone mixin, no inherited base. */
|
|
53
|
+
class EmptyBase {
|
|
54
|
+
}
|
|
55
|
+
export function Loadable(Base = EmptyBase) {
|
|
56
|
+
class LoadableMixin extends Base {
|
|
57
|
+
async load(options) {
|
|
58
|
+
return Reference.create(this).load(options);
|
|
59
|
+
}
|
|
60
|
+
async loadOrFail(options) {
|
|
61
|
+
return Reference.create(this).loadOrFail(options);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return LoadableMixin;
|
|
65
|
+
}
|
|
66
|
+
const LoadableBaseEntityBase = Loadable(BaseEntity);
|
|
67
|
+
/** Convenience: `BaseEntity` pre-composed with the `Loadable` mixin. */
|
|
68
|
+
export class LoadableBaseEntity extends LoadableBaseEntityBase {
|
|
69
|
+
}
|
package/entity/Collection.d.ts
CHANGED
|
@@ -94,7 +94,7 @@ export declare class Collection<T extends object, O extends object = object> {
|
|
|
94
94
|
/**
|
|
95
95
|
* @internal
|
|
96
96
|
*/
|
|
97
|
-
hydrate(items: T[], forcePropagate?: boolean, partial?: boolean): void;
|
|
97
|
+
hydrate(items: T[], forcePropagate?: boolean, partial?: boolean, readonly?: boolean): void;
|
|
98
98
|
/**
|
|
99
99
|
* Remove all items from the collection. Note that removing items from collection does not necessarily imply deleting the target entity,
|
|
100
100
|
* it means we are disconnecting the relation - removing items from collection, not removing entities from database - `Collection.remove()`
|
package/entity/Collection.js
CHANGED
|
@@ -470,7 +470,7 @@ export class Collection {
|
|
|
470
470
|
/**
|
|
471
471
|
* @internal
|
|
472
472
|
*/
|
|
473
|
-
hydrate(items, forcePropagate, partial) {
|
|
473
|
+
hydrate(items, forcePropagate, partial, readonly) {
|
|
474
474
|
for (let i = 0; i < this.#items.size; i++) {
|
|
475
475
|
delete this[i];
|
|
476
476
|
}
|
|
@@ -480,6 +480,9 @@ export class Collection {
|
|
|
480
480
|
this.#count = 0;
|
|
481
481
|
this.add(items);
|
|
482
482
|
this.takeSnapshot(forcePropagate);
|
|
483
|
+
if (readonly) {
|
|
484
|
+
this.#readonly = true;
|
|
485
|
+
}
|
|
483
486
|
}
|
|
484
487
|
/**
|
|
485
488
|
* Remove all items from the collection. Note that removing items from collection does not necessarily imply deleting the target entity,
|
package/entity/EntityAssigner.js
CHANGED
|
@@ -99,6 +99,14 @@ export class EntityAssigner {
|
|
|
99
99
|
return EntityAssigner.assignReference(entity, value, prop, options.em, options);
|
|
100
100
|
}
|
|
101
101
|
if (prop.kind === ReferenceKind.SCALAR && SCALAR_TYPES.has(prop.runtimeType) && (prop.setter || !prop.getter)) {
|
|
102
|
+
// mirror the hydrator (used by `em.create`) and coerce string/number inputs to `Date` instances,
|
|
103
|
+
// since `EntityData` already permits `string | Date` for `Date`-typed properties at the type level
|
|
104
|
+
if (prop.runtimeType === 'Date' &&
|
|
105
|
+
value != null &&
|
|
106
|
+
!(value instanceof Date) &&
|
|
107
|
+
(typeof value === 'string' || typeof value === 'number')) {
|
|
108
|
+
value = new Date(value);
|
|
109
|
+
}
|
|
102
110
|
validateProperty(prop, value, entity);
|
|
103
111
|
return (entity[prop.name] = value);
|
|
104
112
|
}
|
package/entity/EntityFactory.js
CHANGED
|
@@ -161,13 +161,15 @@ export class EntityFactory {
|
|
|
161
161
|
return diff2[key] === undefined;
|
|
162
162
|
})
|
|
163
163
|
.forEach(key => delete diff2[key]);
|
|
164
|
-
// but always add collection properties, formulas, generated columns,
|
|
165
|
-
// are part of the `data`, as these are excluded from
|
|
164
|
+
// but always add collection properties, formulas, generated columns, version properties, and the
|
|
165
|
+
// user-declared STI discriminator if they are part of the `data`, as these are excluded from
|
|
166
|
+
// `comparableProps` and won't appear in the diff
|
|
166
167
|
Utils.keys(data)
|
|
167
168
|
.filter(key => meta.properties[key]?.formula ||
|
|
168
169
|
meta.properties[key]?.version ||
|
|
169
170
|
(meta.properties[key]?.generated && !meta.properties[key]?.primary) ||
|
|
170
|
-
[ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(meta.properties[key]?.kind)
|
|
171
|
+
[ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(meta.properties[key]?.kind) ||
|
|
172
|
+
(key === meta.root.discriminatorColumn && meta.properties[key]?.userDefined !== false))
|
|
171
173
|
.forEach(key => (diff2[key] = data[key]));
|
|
172
174
|
// rehydrated with the new values, skip those changed by user
|
|
173
175
|
// use full hydration if the entity is already initialized, even if the caller used `initialized: false`
|
package/entity/EntityLoader.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { AnyEntity, AutoPath, ConnectionType, EntityName, EntityProperty, FilterQuery, PopulateOptions } from '../typings.js';
|
|
1
|
+
import type { AnyEntity, AutoPath, ConnectionType, EntityName, EntityProperty, FilterQuery, PopulateHintOptions, PopulateOptions } from '../typings.js';
|
|
2
2
|
import type { EntityManager } from '../EntityManager.js';
|
|
3
3
|
import { LoadStrategy, type LockMode, type PopulateHint, PopulatePath, type QueryOrderMap } from '../enums.js';
|
|
4
|
+
import type { InflightQueryAbortStrategy } from '../connections/Connection.js';
|
|
4
5
|
import type { FilterOptions } from '../drivers/IDatabaseDriver.js';
|
|
5
6
|
import type { LoggingOptions } from '../logging/Logger.js';
|
|
6
7
|
/** Options for controlling how relations are loaded by the EntityLoader. */
|
|
@@ -37,6 +38,12 @@ export interface EntityLoaderOptions<Entity, Fields extends string = never, Excl
|
|
|
37
38
|
connectionType?: ConnectionType;
|
|
38
39
|
/** Logging options for the query. */
|
|
39
40
|
logging?: LoggingOptions;
|
|
41
|
+
/** Per-relation populate overrides (limit, offset, orderBy). */
|
|
42
|
+
populateHints?: Record<string, PopulateHintOptions>;
|
|
43
|
+
/** AbortSignal forwarded to populated relation queries. */
|
|
44
|
+
signal?: AbortSignal;
|
|
45
|
+
/** Cancellation strategy paired with {@link signal}. */
|
|
46
|
+
inflightQueryAbortStrategy?: InflightQueryAbortStrategy;
|
|
40
47
|
}
|
|
41
48
|
/** Responsible for batch-loading entity relations using either select-in or joined loading strategies. */
|
|
42
49
|
export declare class EntityLoader {
|
package/entity/EntityLoader.js
CHANGED
|
@@ -177,8 +177,9 @@ export class EntityLoader {
|
|
|
177
177
|
Utils.isObject(orderBy[prop.name]))
|
|
178
178
|
.flatMap(orderBy => orderBy[prop.name]);
|
|
179
179
|
const where = await this.extractChildCondition(options, prop);
|
|
180
|
+
const mergedOrderBy = populate.orderBy ? Utils.asArray(populate.orderBy) : innerOrderBy;
|
|
180
181
|
if (prop.kind === ReferenceKind.MANY_TO_MANY && this.#driver.getPlatform().usesPivotTable()) {
|
|
181
|
-
const pivotOrderBy = QueryHelper.mergeOrderBy(
|
|
182
|
+
const pivotOrderBy = QueryHelper.mergeOrderBy(mergedOrderBy, prop.orderBy, prop.targetMeta?.orderBy);
|
|
182
183
|
const res = await this.findChildrenFromPivotTable(filtered, prop, options, pivotOrderBy, populate, !!ref);
|
|
183
184
|
return Utils.flatten(res);
|
|
184
185
|
}
|
|
@@ -188,10 +189,12 @@ export class EntityLoader {
|
|
|
188
189
|
const { items, partial } = await this.findChildren(options.filtered ?? entities, prop, populate, {
|
|
189
190
|
...options,
|
|
190
191
|
where,
|
|
191
|
-
orderBy:
|
|
192
|
+
orderBy: mergedOrderBy,
|
|
192
193
|
}, !!(ref || prop.mapToPk));
|
|
193
|
-
const
|
|
194
|
-
|
|
194
|
+
const hasLimit = populate.limit != null;
|
|
195
|
+
const isPartial = partial || hasLimit;
|
|
196
|
+
const customOrder = mergedOrderBy.length > 0 || !!prop.orderBy || !!prop.targetMeta?.orderBy;
|
|
197
|
+
this.initializeCollections(filtered, prop, field, items, customOrder, isPartial, hasLimit);
|
|
195
198
|
return items;
|
|
196
199
|
}
|
|
197
200
|
async populateScalar(meta, filtered, options) {
|
|
@@ -269,15 +272,15 @@ export class EntityLoader {
|
|
|
269
272
|
}));
|
|
270
273
|
return allItems;
|
|
271
274
|
}
|
|
272
|
-
initializeCollections(filtered, prop, field, children, customOrder, partial) {
|
|
275
|
+
initializeCollections(filtered, prop, field, children, customOrder, partial, readonly) {
|
|
273
276
|
if (prop.kind === ReferenceKind.ONE_TO_MANY) {
|
|
274
|
-
this.initializeOneToMany(filtered, children, prop, field, partial);
|
|
277
|
+
this.initializeOneToMany(filtered, children, prop, field, partial, readonly);
|
|
275
278
|
}
|
|
276
279
|
if (prop.kind === ReferenceKind.MANY_TO_MANY && !this.#driver.getPlatform().usesPivotTable()) {
|
|
277
|
-
this.initializeManyToMany(filtered, children, prop, field, customOrder, partial);
|
|
280
|
+
this.initializeManyToMany(filtered, children, prop, field, customOrder, partial, readonly);
|
|
278
281
|
}
|
|
279
282
|
}
|
|
280
|
-
initializeOneToMany(filtered, children, prop, field, partial) {
|
|
283
|
+
initializeOneToMany(filtered, children, prop, field, partial, readonly) {
|
|
281
284
|
const mapToPk = prop.targetMeta.properties[prop.mappedBy].mapToPk;
|
|
282
285
|
const map = {};
|
|
283
286
|
for (const entity of filtered) {
|
|
@@ -293,14 +296,14 @@ export class EntityLoader {
|
|
|
293
296
|
}
|
|
294
297
|
for (const entity of filtered) {
|
|
295
298
|
const key = helper(entity).getSerializedPrimaryKey();
|
|
296
|
-
entity[field].hydrate(map[key], undefined, partial);
|
|
299
|
+
entity[field].hydrate(map[key], undefined, partial, readonly);
|
|
297
300
|
}
|
|
298
301
|
}
|
|
299
|
-
initializeManyToMany(filtered, children, prop, field, customOrder, partial) {
|
|
302
|
+
initializeManyToMany(filtered, children, prop, field, customOrder, partial, readonly) {
|
|
300
303
|
if (prop.mappedBy) {
|
|
301
304
|
for (const entity of filtered) {
|
|
302
305
|
const items = children.filter(child => child[prop.mappedBy].contains(entity, false));
|
|
303
|
-
entity[field].hydrate(items, true, partial);
|
|
306
|
+
entity[field].hydrate(items, true, partial, readonly);
|
|
304
307
|
}
|
|
305
308
|
}
|
|
306
309
|
else {
|
|
@@ -311,12 +314,12 @@ export class EntityLoader {
|
|
|
311
314
|
if (!customOrder) {
|
|
312
315
|
items.sort((a, b) => order.indexOf(a) - order.indexOf(b));
|
|
313
316
|
}
|
|
314
|
-
entity[field].hydrate(items, true, partial);
|
|
317
|
+
entity[field].hydrate(items, true, partial, readonly);
|
|
315
318
|
}
|
|
316
319
|
}
|
|
317
320
|
}
|
|
318
321
|
async findChildren(entities, prop, populate, options, ref) {
|
|
319
|
-
const children = Utils.unique(this.getChildReferences(entities, prop, options, ref));
|
|
322
|
+
const children = Utils.unique(this.getChildReferences(entities, prop, options, ref, populate));
|
|
320
323
|
const meta = prop.targetMeta;
|
|
321
324
|
// When targetKey is set, use it for FK lookup instead of the PK
|
|
322
325
|
let fk = prop.targetKey ?? Utils.getPrimaryKeyHash(meta.primaryKeys);
|
|
@@ -334,10 +337,10 @@ export class EntityLoader {
|
|
|
334
337
|
fk = ownerProp.name;
|
|
335
338
|
}
|
|
336
339
|
}
|
|
337
|
-
if (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner && !ref) {
|
|
340
|
+
if (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner && (!ref || (!prop.mapToPk && !populate.filter))) {
|
|
338
341
|
children.length = 0;
|
|
339
342
|
fk = meta.properties[prop.mappedBy].name;
|
|
340
|
-
children.push(...this.filterByReferences(entities, prop
|
|
343
|
+
children.push(...this.filterByReferences(entities, prop, options, ref));
|
|
341
344
|
}
|
|
342
345
|
if (children.length === 0) {
|
|
343
346
|
return { items: [], partial };
|
|
@@ -366,8 +369,11 @@ export class EntityLoader {
|
|
|
366
369
|
}
|
|
367
370
|
const fields = this.buildFields(options.fields, prop, ref);
|
|
368
371
|
/* eslint-disable prefer-const */
|
|
369
|
-
let { refresh, filters, convertCustomTypes, lockMode, strategy, populateWhere
|
|
372
|
+
let { refresh, filters, convertCustomTypes, lockMode, strategy, populateWhere, connectionType, logging } = options;
|
|
370
373
|
/* eslint-enable prefer-const */
|
|
374
|
+
// `populateWhere` is typed as required via `Required<EntityLoaderOptions>`, but `em.populate(...)`
|
|
375
|
+
// forwards user-supplied options without filling it in — keep the runtime default.
|
|
376
|
+
populateWhere ??= 'infer';
|
|
371
377
|
if (typeof populateWhere === 'object') {
|
|
372
378
|
populateWhere = await this.extractChildCondition({ where: populateWhere }, prop);
|
|
373
379
|
}
|
|
@@ -375,7 +381,7 @@ export class EntityLoader {
|
|
|
375
381
|
where = { $and: [where, prop.where] };
|
|
376
382
|
}
|
|
377
383
|
const orderBy = QueryHelper.mergeOrderBy(options.orderBy, prop.orderBy);
|
|
378
|
-
const
|
|
384
|
+
const findOptions = {
|
|
379
385
|
filters,
|
|
380
386
|
convertCustomTypes,
|
|
381
387
|
lockMode,
|
|
@@ -390,11 +396,21 @@ export class EntityLoader {
|
|
|
390
396
|
fields,
|
|
391
397
|
schema,
|
|
392
398
|
connectionType,
|
|
399
|
+
signal: options.signal,
|
|
400
|
+
inflightQueryAbortStrategy: options.inflightQueryAbortStrategy,
|
|
393
401
|
// @ts-ignore not a public option, will be propagated to the populate call
|
|
394
402
|
refresh: refresh && !children.every(item => options.visited.has(item)),
|
|
395
403
|
// @ts-ignore not a public option, will be propagated to the populate call
|
|
396
404
|
visited: options.visited,
|
|
397
|
-
}
|
|
405
|
+
};
|
|
406
|
+
if (populate.limit != null) {
|
|
407
|
+
findOptions._partitionLimit = {
|
|
408
|
+
partitionBy: fk,
|
|
409
|
+
limit: populate.limit,
|
|
410
|
+
offset: populate.offset,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
const items = await this.#em.find(meta.class, where, findOptions);
|
|
398
414
|
// For targetKey relations, wire up loaded entities to parent references
|
|
399
415
|
// This is needed because the references were created under alternate key,
|
|
400
416
|
// but loaded entities are stored under PK, so they don't automatically merge
|
|
@@ -582,6 +598,12 @@ export class EntityLoader {
|
|
|
582
598
|
const options2 = { ...options, fields, exclude, populateFilter };
|
|
583
599
|
['limit', 'offset', 'first', 'last', 'before', 'after', 'overfetch'].forEach(prop => delete options2[prop]);
|
|
584
600
|
options2.populate = populate?.children ?? [];
|
|
601
|
+
if (populate?.limit != null) {
|
|
602
|
+
options2._partitionLimit = {
|
|
603
|
+
limit: populate.limit,
|
|
604
|
+
offset: populate.offset,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
585
607
|
if (!Utils.isEmpty(prop.where)) {
|
|
586
608
|
where = { $and: [where, prop.where] };
|
|
587
609
|
}
|
|
@@ -609,7 +631,8 @@ export class EntityLoader {
|
|
|
609
631
|
});
|
|
610
632
|
return this.#em.getUnitOfWork().register(entity, item, { refresh, loaded: true });
|
|
611
633
|
});
|
|
612
|
-
|
|
634
|
+
const hasLimit = populate?.limit != null;
|
|
635
|
+
entity[prop.name].hydrate(items, true, hasLimit, hasLimit);
|
|
613
636
|
children.push(items);
|
|
614
637
|
}
|
|
615
638
|
return children;
|
|
@@ -670,8 +693,11 @@ export class EntityLoader {
|
|
|
670
693
|
}
|
|
671
694
|
return ret;
|
|
672
695
|
}, []);
|
|
673
|
-
// we need to automatically select the FKs too, e.g. for 1:m
|
|
674
|
-
|
|
696
|
+
// we need to automatically select the FKs too, e.g. for 1:m and inverse 1:1 relations
|
|
697
|
+
// to be able to wire them with the items
|
|
698
|
+
if (prop.kind === ReferenceKind.ONE_TO_MANY ||
|
|
699
|
+
prop.kind === ReferenceKind.MANY_TO_MANY ||
|
|
700
|
+
(prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner)) {
|
|
675
701
|
const owner = prop.targetMeta.properties[prop.mappedBy];
|
|
676
702
|
// when the owning FK is lazy, we need to explicitly select it even without user-provided fields,
|
|
677
703
|
// otherwise the driver will exclude it and we won't be able to map children to their parent collections
|
|
@@ -684,7 +710,7 @@ export class EntityLoader {
|
|
|
684
710
|
}
|
|
685
711
|
return ret;
|
|
686
712
|
}
|
|
687
|
-
getChildReferences(entities, prop, options, ref) {
|
|
713
|
+
getChildReferences(entities, prop, options, ref, populate) {
|
|
688
714
|
const filtered = this.filterCollections(entities, prop.name, options, ref);
|
|
689
715
|
if (prop.kind === ReferenceKind.ONE_TO_MANY) {
|
|
690
716
|
return filtered.map(e => e[prop.name].owner);
|
|
@@ -700,7 +726,7 @@ export class EntityLoader {
|
|
|
700
726
|
return filtered;
|
|
701
727
|
}
|
|
702
728
|
// MANY_TO_ONE or ONE_TO_ONE
|
|
703
|
-
return this.filterReferences(entities, prop.name, options, ref);
|
|
729
|
+
return this.filterReferences(entities, prop.name, options, ref, populate);
|
|
704
730
|
}
|
|
705
731
|
filterCollections(entities, field, options, ref) {
|
|
706
732
|
if (options.refresh) {
|
|
@@ -714,7 +740,16 @@ export class EntityLoader {
|
|
|
714
740
|
}
|
|
715
741
|
const wrapped = helper(entity);
|
|
716
742
|
if (!field.includes('.')) {
|
|
717
|
-
|
|
743
|
+
if (wrapped.__loadedProperties.has(field)) {
|
|
744
|
+
return true;
|
|
745
|
+
}
|
|
746
|
+
// Inlined embeddables track each leaf child separately, not the parent property,
|
|
747
|
+
// so we need to recurse for nested inline embeddables.
|
|
748
|
+
const prop = wrapped.__meta.properties[field];
|
|
749
|
+
if (prop?.kind === ReferenceKind.EMBEDDED && !prop.object) {
|
|
750
|
+
return Object.values(prop.embeddedProps).every(child => this.isPropertyLoaded(entity, child.name));
|
|
751
|
+
}
|
|
752
|
+
return false;
|
|
718
753
|
}
|
|
719
754
|
const [f, ...r] = field.split('.');
|
|
720
755
|
/* v8 ignore next */
|
|
@@ -726,7 +761,7 @@ export class EntityLoader {
|
|
|
726
761
|
}
|
|
727
762
|
return this.isPropertyLoaded(entity[f], r.join('.'));
|
|
728
763
|
}
|
|
729
|
-
filterReferences(entities, field, options, ref) {
|
|
764
|
+
filterReferences(entities, field, options, ref, populate) {
|
|
730
765
|
if (ref) {
|
|
731
766
|
return [];
|
|
732
767
|
}
|
|
@@ -735,13 +770,19 @@ export class EntityLoader {
|
|
|
735
770
|
return children.map(e => Reference.unwrapReference(e[field]));
|
|
736
771
|
}
|
|
737
772
|
if (options.fields) {
|
|
773
|
+
// `:ref` populate children are satisfied by FK/PK and will be loaded lazily — they don't force a parent reload
|
|
774
|
+
const refChildren = new Set((populate?.children ?? [])
|
|
775
|
+
.map(c => c.field.split(':'))
|
|
776
|
+
.filter(parts => parts[1] === 'ref')
|
|
777
|
+
.map(parts => parts[0]));
|
|
738
778
|
return children
|
|
739
779
|
.map(e => Reference.unwrapReference(e[field]))
|
|
740
780
|
.filter(target => {
|
|
741
781
|
const wrapped = helper(target);
|
|
742
782
|
const childFields = options.fields
|
|
743
783
|
.filter(f => f.startsWith(`${field}.`))
|
|
744
|
-
.map(f => f.substring(field.length + 1))
|
|
784
|
+
.map(f => f.substring(field.length + 1))
|
|
785
|
+
.filter(cf => !refChildren.has(cf.split('.')[0]));
|
|
745
786
|
return !wrapped.__initialized || !childFields.every(cf => this.isPropertyLoaded(target, cf));
|
|
746
787
|
});
|
|
747
788
|
}
|
|
@@ -749,12 +790,39 @@ export class EntityLoader {
|
|
|
749
790
|
.filter(e => !e[field].__helper.__initialized)
|
|
750
791
|
.map(e => Reference.unwrapReference(e[field]));
|
|
751
792
|
}
|
|
752
|
-
filterByReferences(entities,
|
|
793
|
+
filterByReferences(entities, prop, options, ref) {
|
|
753
794
|
/* v8 ignore next */
|
|
754
|
-
if (refresh) {
|
|
795
|
+
if (options.refresh) {
|
|
755
796
|
return entities;
|
|
756
797
|
}
|
|
757
|
-
|
|
798
|
+
const field = prop.name;
|
|
799
|
+
return entities.filter(e => {
|
|
800
|
+
const value = e[field];
|
|
801
|
+
if (value === null) {
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
if (value === undefined || !Utils.isEntity(value, true)) {
|
|
805
|
+
return true;
|
|
806
|
+
}
|
|
807
|
+
const target = Reference.unwrapReference(value);
|
|
808
|
+
const wrapped = helper(target);
|
|
809
|
+
if (!wrapped.__initialized) {
|
|
810
|
+
return true;
|
|
811
|
+
}
|
|
812
|
+
if (ref) {
|
|
813
|
+
return false;
|
|
814
|
+
}
|
|
815
|
+
if (options.fields) {
|
|
816
|
+
const childFields = options.fields
|
|
817
|
+
.filter(f => f.startsWith(`${prop.name}.`))
|
|
818
|
+
.map(f => f.substring(prop.name.length + 1));
|
|
819
|
+
return childFields.length > 0 && !childFields.every(f => this.isPropertyLoaded(target, f));
|
|
820
|
+
}
|
|
821
|
+
return prop.targetMeta.comparableProps.some(targetProp => {
|
|
822
|
+
const inlineEmbedded = targetProp.kind === ReferenceKind.EMBEDDED && !targetProp.object;
|
|
823
|
+
return !inlineEmbedded && !targetProp.lazy && !wrapped.__loadedProperties.has(targetProp.name);
|
|
824
|
+
});
|
|
825
|
+
});
|
|
758
826
|
}
|
|
759
827
|
lookupAllRelationships(entityName) {
|
|
760
828
|
const ret = [];
|
package/entity/Reference.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AddEager, AddOptional, Dictionary, EntityClass, EntityKey, EntityProperty, Loaded, LoadedReference, Primary, Ref } from '../typings.js';
|
|
1
|
+
import type { AddEager, AddOptional, Dictionary, EntityClass, EntityKey, EntityProperty, LazyRef, Loaded, LoadedReference, Primary, Ref } from '../typings.js';
|
|
2
2
|
import type { FindOneOptions, FindOneOrFailOptions } from '../drivers/IDatabaseDriver.js';
|
|
3
3
|
/** Wrapper around an entity that provides lazy loading capabilities and identity-preserving reference semantics. */
|
|
4
4
|
export declare class Reference<T extends object> {
|
|
@@ -98,6 +98,47 @@ export declare function ref<I extends unknown | Ref<unknown> | undefined | null,
|
|
|
98
98
|
* shortcut for `Reference.createFromPK(entityType, pk)`
|
|
99
99
|
*/
|
|
100
100
|
export declare function ref<I extends unknown | undefined | null, T, PKV extends Primary<T> = Primary<T>>(entityType: EntityClass<T>, pk: I): Ref<T> | AddOptional<typeof pk>;
|
|
101
|
+
/**
|
|
102
|
+
* Unwraps a reference to its underlying value, returning the entity or scalar. Works on any of:
|
|
103
|
+
*
|
|
104
|
+
* - `Ref<T>` / `Reference<T>` (entity) — calls `.unwrap()` to extract the wrapped entity, returns `T`.
|
|
105
|
+
* - `LazyRef<T>` — type-only marker; runtime value is already `T`, so returned as-is.
|
|
106
|
+
* - plain entity `T` — returned as-is.
|
|
107
|
+
* - `ScalarReference<V>` / `ScalarRef<V>` — calls `.unwrap()` to extract the scalar value, returns `V | undefined`
|
|
108
|
+
* (a scalar reference may be bound without an initial value).
|
|
109
|
+
*
|
|
110
|
+
* Inverse of the {@link ref} helper. Use as a typed escape hatch when you know a relation is populated
|
|
111
|
+
* but don't want to (or can't) thread `Loaded<T, 'relation'>` through a function signature.
|
|
112
|
+
*
|
|
113
|
+
* ```ts
|
|
114
|
+
* function logAuthor(book: Book) {
|
|
115
|
+
* // `book.author` is typed as `LazyRef<Author>`, so `.name` is not directly accessible
|
|
116
|
+
* console.log(unref(book.author).name);
|
|
117
|
+
* }
|
|
118
|
+
*
|
|
119
|
+
* // Scalar reference:
|
|
120
|
+
* const scalar = ref('hello');
|
|
121
|
+
* unref(scalar); // 'hello' (type: string | undefined)
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* Note: `unref` is a compile-time narrowing only for entity refs. On a `LazyRef<T>` or plain entity
|
|
125
|
+
* relation that is a stub (PK only, never populated), reads of non-PK properties will still return
|
|
126
|
+
* `undefined` at runtime — same footgun as a plain non-`Ref` relation.
|
|
127
|
+
*/
|
|
128
|
+
export declare function unref<V>(value: ScalarReference<V>): V | undefined;
|
|
129
|
+
export declare function unref<T extends object>(value: Ref<T> | LazyRef<T> | T): T;
|
|
130
|
+
/**
|
|
131
|
+
* Unwraps a reference to its underlying value. Nullable variant — returns `null` unchanged.
|
|
132
|
+
*/
|
|
133
|
+
export declare function unref<T extends object>(value: Ref<T> | LazyRef<T> | T | null): T | null;
|
|
134
|
+
/**
|
|
135
|
+
* Unwraps a reference to its underlying value. Optional variant — returns `undefined` unchanged.
|
|
136
|
+
*/
|
|
137
|
+
export declare function unref<T extends object>(value: Ref<T> | LazyRef<T> | T | undefined): T | undefined;
|
|
138
|
+
/**
|
|
139
|
+
* Unwraps a reference to its underlying value. Nullable variant — returns `null`/`undefined` unchanged.
|
|
140
|
+
*/
|
|
141
|
+
export declare function unref<T extends object>(value: Ref<T> | LazyRef<T> | T | null | undefined): T | null | undefined;
|
|
101
142
|
/**
|
|
102
143
|
* shortcut for `Reference.createNakedFromPK(entityType, pk)`
|
|
103
144
|
*/
|
package/entity/Reference.js
CHANGED
|
@@ -303,6 +303,15 @@ export function ref(entityOrType, pk) {
|
|
|
303
303
|
}
|
|
304
304
|
return Reference.createFromPK(entityOrType, pk);
|
|
305
305
|
}
|
|
306
|
+
export function unref(value) {
|
|
307
|
+
if (value == null) {
|
|
308
|
+
return value;
|
|
309
|
+
}
|
|
310
|
+
if (ScalarReference.isScalarReference(value)) {
|
|
311
|
+
return value.unwrap();
|
|
312
|
+
}
|
|
313
|
+
return Reference.unwrapReference(value);
|
|
314
|
+
}
|
|
306
315
|
/**
|
|
307
316
|
* shortcut for `Reference.createNakedFromPK(entityType, pk)`
|
|
308
317
|
*/
|