@mikro-orm/core 7.1.0-dev.1 → 7.1.0-dev.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/EntityManager.d.ts +63 -11
- package/EntityManager.js +163 -18
- package/drivers/DatabaseDriver.d.ts +1 -0
- package/drivers/DatabaseDriver.js +3 -0
- package/drivers/IDatabaseDriver.d.ts +30 -1
- package/entity/BaseEntity.d.ts +2 -2
- package/entity/Collection.d.ts +6 -3
- package/entity/Collection.js +15 -4
- package/entity/EntityFactory.js +20 -1
- package/entity/EntityLoader.d.ts +3 -1
- package/entity/EntityLoader.js +39 -16
- package/entity/EntityRepository.d.ts +27 -9
- package/entity/EntityRepository.js +12 -0
- package/entity/WrappedEntity.d.ts +2 -2
- package/entity/defineEntity.d.ts +40 -9
- package/entity/defineEntity.js +0 -6
- package/entity/utils.js +4 -5
- package/errors.d.ts +2 -0
- package/errors.js +4 -0
- package/index.d.ts +2 -2
- package/index.js +1 -1
- package/metadata/MetadataDiscovery.d.ts +11 -0
- package/metadata/MetadataDiscovery.js +127 -9
- package/metadata/MetadataValidator.js +9 -0
- package/metadata/types.d.ts +10 -2
- package/naming-strategy/AbstractNamingStrategy.d.ts +1 -1
- package/naming-strategy/NamingStrategy.d.ts +1 -1
- package/package.json +1 -1
- package/platforms/Platform.d.ts +5 -0
- package/platforms/Platform.js +7 -0
- package/serialization/EntitySerializer.d.ts +18 -6
- package/serialization/EntitySerializer.js +31 -5
- package/typings.d.ts +108 -6
- package/typings.js +19 -2
- package/utils/AbstractMigrator.d.ts +11 -1
- package/utils/AbstractMigrator.js +203 -0
- package/utils/Configuration.d.ts +7 -0
- package/utils/Configuration.js +1 -0
- package/utils/DataloaderUtils.d.ts +10 -1
- package/utils/DataloaderUtils.js +78 -0
- package/utils/QueryHelper.d.ts +16 -0
- package/utils/QueryHelper.js +15 -0
- package/utils/Utils.d.ts +1 -1
- package/utils/Utils.js +1 -1
- package/utils/fs-utils.d.ts +2 -0
- package/utils/fs-utils.js +7 -1
package/EntityManager.d.ts
CHANGED
|
@@ -6,8 +6,8 @@ import { type EntityRepository } from './entity/EntityRepository.js';
|
|
|
6
6
|
import { EntityLoader, type EntityLoaderOptions } from './entity/EntityLoader.js';
|
|
7
7
|
import { Reference } from './entity/Reference.js';
|
|
8
8
|
import { UnitOfWork } from './unit-of-work/UnitOfWork.js';
|
|
9
|
-
import type { CountOptions, DeleteOptions, FilterOptions, FindAllOptions, FindByCursorOptions, FindOneOptions, FindOneOrFailOptions, FindOptions, GetReferenceOptions, IDatabaseDriver, LockOptions, NativeInsertUpdateOptions, StreamOptions, UpdateOptions, UpsertManyOptions, UpsertOptions } from './drivers/IDatabaseDriver.js';
|
|
10
|
-
import type { AnyString, ArrayElement, AutoPath, ConnectionType, Dictionary, EntityClass, EntityData, EntityDictionary, EntityDTO, EntityMetadata, EntityName, FilterDef, FilterQuery, FromEntityType, GetRepository, IHydrator, IsSubset, Loaded, MergeLoaded, MergeSelected, ObjectQuery, PopulateOptions, Primary, Ref, RequiredEntityData, UnboxArray } from './typings.js';
|
|
9
|
+
import type { CountByOptions, CountOptions, DeleteOptions, FilterOptions, FindAllOptions, FindByCursorOptions, FindOneOptions, FindOneOrFailOptions, FindOptions, GetReferenceOptions, IDatabaseDriver, LockOptions, NativeInsertUpdateOptions, StreamOptions, UpdateOptions, UpsertManyOptions, UpsertOptions } from './drivers/IDatabaseDriver.js';
|
|
10
|
+
import type { AnyString, ArrayElement, AutoPath, ConnectionType, Dictionary, EntityClass, EntityData, EntityDictionary, EntityDTO, EntityKey, EntityMetadata, EntityName, FilterDef, FilterQuery, FromEntityType, GetRepository, IHydrator, IsSubset, Loaded, MergeLoaded, MergeSelected, ObjectQuery, PopulateOptions, Primary, Ref, RequiredEntityData, UnboxArray, IndexFilterQuery, WithUsingOptions } from './typings.js';
|
|
11
11
|
import { FlushMode, LockMode, PopulatePath, type TransactionOptions } from './enums.js';
|
|
12
12
|
import type { MetadataStorage } from './metadata/MetadataStorage.js';
|
|
13
13
|
import type { Transaction } from './connections/Connection.js';
|
|
@@ -61,7 +61,9 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
|
|
|
61
61
|
/**
|
|
62
62
|
* Finds all entities matching your `where` query. You can pass additional options via the `options` parameter.
|
|
63
63
|
*/
|
|
64
|
-
find<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never>(entityName: EntityName<Entity>, where: FilterQuery<NoInfer<Entity
|
|
64
|
+
find<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(entityName: EntityName<Entity>, where: [Using] extends [never] ? FilterQuery<NoInfer<Entity>> : IndexFilterQuery<NoInfer<Entity>, Using>, options?: FindOptions<Entity, Hint, Fields, Excludes> & {
|
|
65
|
+
using?: Using | Using[];
|
|
66
|
+
}): Promise<Loaded<Entity, Hint, Fields, Excludes>[]>;
|
|
65
67
|
/**
|
|
66
68
|
* Finds all entities and returns an async iterable (async generator) that yields results one by one.
|
|
67
69
|
* The results are merged and mapped to entity instances, without adding them to the identity map.
|
|
@@ -79,11 +81,11 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
|
|
|
79
81
|
* }
|
|
80
82
|
* ```
|
|
81
83
|
*/
|
|
82
|
-
stream<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never>(entityName: EntityName<Entity>, options?: StreamOptions<NoInfer<Entity>, Hint, Fields, Excludes>): AsyncIterableIterator<Loaded<Entity, Hint, Fields, Excludes>>;
|
|
84
|
+
stream<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(entityName: EntityName<Entity>, options?: WithUsingOptions<StreamOptions<NoInfer<Entity>, Hint, Fields, Excludes>, NoInfer<Entity>, Using>): AsyncIterableIterator<Loaded<Entity, Hint, Fields, Excludes>>;
|
|
83
85
|
/**
|
|
84
86
|
* Finds all entities of given type, optionally matching the `where` condition provided in the `options` parameter.
|
|
85
87
|
*/
|
|
86
|
-
findAll<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never>(entityName: EntityName<Entity>, options?: FindAllOptions<NoInfer<Entity>, Hint, Fields, Excludes>): Promise<Loaded<Entity, Hint, Fields, Excludes>[]>;
|
|
88
|
+
findAll<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(entityName: EntityName<Entity>, options?: WithUsingOptions<FindAllOptions<NoInfer<Entity>, Hint, Fields, Excludes>, NoInfer<Entity>, Using>): Promise<Loaded<Entity, Hint, Fields, Excludes>[]>;
|
|
87
89
|
private getPopulateWhere;
|
|
88
90
|
/**
|
|
89
91
|
* Registers global filter to this entity manager. Global filters are enabled by default (unless disabled via last parameter).
|
|
@@ -130,7 +132,9 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
|
|
|
130
132
|
* Calls `em.find()` and `em.count()` with the same arguments (where applicable) and returns the results as tuple
|
|
131
133
|
* where the first element is the array of entities, and the second is the count.
|
|
132
134
|
*/
|
|
133
|
-
findAndCount<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never>(entityName: EntityName<Entity>, where: FilterQuery<NoInfer<Entity
|
|
135
|
+
findAndCount<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(entityName: EntityName<Entity>, where: [Using] extends [never] ? FilterQuery<NoInfer<Entity>> : IndexFilterQuery<NoInfer<Entity>, Using>, options?: FindOptions<Entity, Hint, Fields, Excludes> & {
|
|
136
|
+
using?: Using | Using[];
|
|
137
|
+
}): Promise<[Loaded<Entity, Hint, Fields, Excludes>[], number]>;
|
|
134
138
|
/**
|
|
135
139
|
* Calls `em.find()` and `em.count()` with the same arguments (where applicable) and returns the results as {@apilink Cursor} object.
|
|
136
140
|
* Supports `before`, `after`, `first` and `last` options while disallowing `limit` and `offset`. Explicit `orderBy` option
|
|
@@ -187,7 +191,7 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
|
|
|
187
191
|
* }
|
|
188
192
|
* ```
|
|
189
193
|
*/
|
|
190
|
-
findByCursor<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never, IncludeCount extends boolean = true>(entityName: EntityName<Entity>, options: FindByCursorOptions<Entity, Hint, Fields, Excludes, IncludeCount>): Promise<Cursor<Entity, Hint, Fields, Excludes, IncludeCount>>;
|
|
194
|
+
findByCursor<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never, IncludeCount extends boolean = true, Using extends string = never>(entityName: EntityName<Entity>, options: WithUsingOptions<FindByCursorOptions<Entity, Hint, Fields, Excludes, IncludeCount>, Entity, Using>): Promise<Cursor<Entity, Hint, Fields, Excludes, IncludeCount>>;
|
|
191
195
|
/**
|
|
192
196
|
* Refreshes the persistent state of an entity from the database, overriding any local changes that have not yet been
|
|
193
197
|
* persisted. Returns the same entity instance (same object reference), but re-hydrated. If the entity is no longer
|
|
@@ -203,14 +207,18 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
|
|
|
203
207
|
/**
|
|
204
208
|
* Finds first entity matching your `where` query.
|
|
205
209
|
*/
|
|
206
|
-
findOne<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never>(entityName: EntityName<Entity>, where: FilterQuery<NoInfer<Entity
|
|
210
|
+
findOne<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(entityName: EntityName<Entity>, where: [Using] extends [never] ? FilterQuery<NoInfer<Entity>> : IndexFilterQuery<NoInfer<Entity>, Using>, options?: FindOneOptions<Entity, Hint, Fields, Excludes> & {
|
|
211
|
+
using?: Using | Using[];
|
|
212
|
+
}): Promise<Loaded<Entity, Hint, Fields, Excludes> | null>;
|
|
207
213
|
/**
|
|
208
214
|
* Finds first entity matching your `where` query. If nothing found, it will throw an error.
|
|
209
215
|
* If the `strict` option is specified and nothing is found or more than one matching entity is found, it will throw an error.
|
|
210
216
|
* You can override the factory for creating this method via `options.failHandler` locally
|
|
211
217
|
* or via `Configuration.findOneOrFailHandler` (`findExactlyOneOrFailHandler` when specifying `strict`) globally.
|
|
212
218
|
*/
|
|
213
|
-
findOneOrFail<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never>(entityName: EntityName<Entity>, where: FilterQuery<NoInfer<Entity
|
|
219
|
+
findOneOrFail<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(entityName: EntityName<Entity>, where: [Using] extends [never] ? FilterQuery<NoInfer<Entity>> : IndexFilterQuery<NoInfer<Entity>, Using>, options?: FindOneOrFailOptions<Entity, Hint, Fields, Excludes> & {
|
|
220
|
+
using?: Using | Using[];
|
|
221
|
+
}): Promise<Loaded<Entity, Hint, Fields, Excludes>>;
|
|
214
222
|
/**
|
|
215
223
|
* Creates or updates the entity, based on whether it is already present in the database.
|
|
216
224
|
* This method performs an `insert on conflict merge` query ensuring the database is in sync, returning a managed
|
|
@@ -307,6 +315,25 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
|
|
|
307
315
|
* Fires native insert query. Calling this has no side effects on the context (identity map).
|
|
308
316
|
*/
|
|
309
317
|
insert<Entity extends object>(entityNameOrEntity: EntityName<Entity> | Entity, data?: RequiredEntityData<Entity> | Entity, options?: NativeInsertUpdateOptions<Entity>): Promise<Primary<Entity>>;
|
|
318
|
+
/**
|
|
319
|
+
* Clones rows matching the condition at the database level via INSERT...SELECT.
|
|
320
|
+
* Automatically excludes auto-increment PKs, generated columns, and resets version properties.
|
|
321
|
+
* Returns the cloned entity, registered in the identity map.
|
|
322
|
+
*
|
|
323
|
+
* @example
|
|
324
|
+
* ```ts
|
|
325
|
+
* // Clone by entity class + where + overrides
|
|
326
|
+
* const cloned = await em.clone(Author, { id: 1 }, { email: 'new@email.com' });
|
|
327
|
+
*
|
|
328
|
+
* // Pure clone (all non-PK fields copied)
|
|
329
|
+
* const cloned = await em.clone(Author, { id: 1 });
|
|
330
|
+
*
|
|
331
|
+
* // Clone a loaded entity
|
|
332
|
+
* const author = await em.findOneOrFail(Author, 1);
|
|
333
|
+
* const cloned = await em.clone(author, { email: 'new@email.com' });
|
|
334
|
+
* ```
|
|
335
|
+
*/
|
|
336
|
+
clone<Entity extends object>(entityNameOrEntity: EntityName<Entity> | Entity, whereOrOverrides?: FilterQuery<NoInfer<Entity>> | EntityData<Entity>, overridesOrOptions?: EntityData<Entity> | NativeInsertUpdateOptions<Entity>, options?: NativeInsertUpdateOptions<Entity>): Promise<Entity>;
|
|
310
337
|
/**
|
|
311
338
|
* Fires native multi-insert query. Calling this has no side effects on the context (identity map).
|
|
312
339
|
*/
|
|
@@ -414,6 +441,27 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
|
|
|
414
441
|
* Returns total number of entities matching your `where` query.
|
|
415
442
|
*/
|
|
416
443
|
count<Entity extends object, Hint extends string = never>(entityName: EntityName<Entity>, where?: FilterQuery<NoInfer<Entity>>, options?: CountOptions<Entity, Hint>): Promise<number>;
|
|
444
|
+
/**
|
|
445
|
+
* Counts entities grouped by one or more properties. Returns a dictionary keyed by the grouped
|
|
446
|
+
* field value(s), with counts as values. For composite `groupBy`, keys are joined with `~~~`.
|
|
447
|
+
*
|
|
448
|
+
* SQL drivers issue a single `GROUP BY` query; MongoDB uses an aggregation pipeline.
|
|
449
|
+
*
|
|
450
|
+
* @example
|
|
451
|
+
* ```ts
|
|
452
|
+
* // Count books per author
|
|
453
|
+
* const counts = await em.countBy(Book, 'author');
|
|
454
|
+
* // { '1': 2, '2': 1, '3': 3 }
|
|
455
|
+
*
|
|
456
|
+
* // Count with a filter
|
|
457
|
+
* const counts = await em.countBy(Book, 'author', { where: { active: true } });
|
|
458
|
+
*
|
|
459
|
+
* // Composite groupBy — keys joined with ~~~
|
|
460
|
+
* const counts = await em.countBy(Order, ['status', 'country']);
|
|
461
|
+
* // { 'pending~~~US': 5, 'shipped~~~DE': 3 }
|
|
462
|
+
* ```
|
|
463
|
+
*/
|
|
464
|
+
countBy<Entity extends object>(entityName: EntityName<Entity>, groupBy: EntityKey<Entity> | readonly EntityKey<Entity>[], options?: CountByOptions<Entity>): Promise<Dictionary<number>>;
|
|
417
465
|
/**
|
|
418
466
|
* Tells the EntityManager to make an instance managed and persistent.
|
|
419
467
|
* The entity will be entered into the database at or before transaction commit or as a result of the flush operation.
|
|
@@ -505,16 +553,20 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
|
|
|
505
553
|
*/
|
|
506
554
|
getComparator(): EntityComparator;
|
|
507
555
|
private checkLockRequirements;
|
|
556
|
+
private validateIndexUsage;
|
|
557
|
+
private validateWhereKeysForIndex;
|
|
508
558
|
private lockAndPopulate;
|
|
509
559
|
private buildFields;
|
|
510
560
|
/** @internal */
|
|
511
561
|
preparePopulate<Entity extends object>(entityName: EntityName<Entity>, options: Pick<FindOptions<Entity, any, any, any>, 'populate' | 'strategy' | 'fields' | 'flags' | 'filters' | 'exclude' | 'populateHints'>, validate?: boolean): Promise<PopulateOptions<Entity>[]>;
|
|
562
|
+
/** Force SELECT_IN strategy on populate entries with `limit`, since JOINED cannot do per-parent limiting. */
|
|
563
|
+
private forceSelectInForLimitedPopulate;
|
|
512
564
|
/**
|
|
513
565
|
* when the entity is found in identity map, we check if it was partially loaded or we are trying to populate
|
|
514
566
|
* some additional lazy properties, if so, we reload and merge the data from database
|
|
515
567
|
*/
|
|
516
568
|
protected shouldRefresh<T extends object, P extends string = never, F extends string = never, E extends string = never>(meta: EntityMetadata<T>, entity: T, options: FindOneOptions<T, P, F, E>): boolean;
|
|
517
|
-
protected prepareOptions(options: FindOptions<any, any, any, any> | FindOneOptions<any, any, any, any> | CountOptions<any, any>): void;
|
|
569
|
+
protected prepareOptions(options: FindOptions<any, any, any, any> | FindOneOptions<any, any, any, any> | CountOptions<any, any> | CountByOptions<any>): void;
|
|
518
570
|
/**
|
|
519
571
|
* @internal
|
|
520
572
|
*/
|
|
@@ -556,7 +608,7 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
|
|
|
556
608
|
*/
|
|
557
609
|
set schema(schema: string | null | undefined);
|
|
558
610
|
/** @internal */
|
|
559
|
-
getDataLoader(type: 'ref' | '1:m' | 'm:n'): Promise<any>;
|
|
611
|
+
getDataLoader(type: 'ref' | '1:m' | 'm:n' | 'count'): Promise<any>;
|
|
560
612
|
/**
|
|
561
613
|
* Returns the ID of this EntityManager. Respects the context, so global EM will give you the contextual ID
|
|
562
614
|
* if executed inside request context handler.
|
package/EntityManager.js
CHANGED
|
@@ -103,9 +103,6 @@ export class EntityManager {
|
|
|
103
103
|
repo(entityName) {
|
|
104
104
|
return this.getRepository(entityName);
|
|
105
105
|
}
|
|
106
|
-
/**
|
|
107
|
-
* Finds all entities matching your `where` query. You can pass additional options via the `options` parameter.
|
|
108
|
-
*/
|
|
109
106
|
async find(entityName, where, options = {}) {
|
|
110
107
|
if (options.disableIdentityMap ?? this.config.get('disableIdentityMap')) {
|
|
111
108
|
const em = this.getContext(false);
|
|
@@ -116,10 +113,11 @@ export class EntityManager {
|
|
|
116
113
|
}
|
|
117
114
|
const em = this.getContext();
|
|
118
115
|
em.prepareOptions(options);
|
|
116
|
+
const meta = this.metadata.get(entityName);
|
|
117
|
+
em.validateIndexUsage(meta, where, options);
|
|
119
118
|
await em.tryFlush(entityName, options);
|
|
120
119
|
where = await em.processWhere(entityName, where, options, 'read');
|
|
121
120
|
validateParams(where);
|
|
122
|
-
const meta = this.metadata.get(entityName);
|
|
123
121
|
if (meta.orderBy) {
|
|
124
122
|
options.orderBy = QueryHelper.mergeOrderBy(options.orderBy, meta.orderBy);
|
|
125
123
|
}
|
|
@@ -203,6 +201,7 @@ export class EntityManager {
|
|
|
203
201
|
options.orderBy = options.orderBy || {};
|
|
204
202
|
options.populate = (await em.preparePopulate(entityName, options));
|
|
205
203
|
const meta = this.metadata.get(entityName);
|
|
204
|
+
em.validateIndexUsage(meta, options.where ?? {}, options);
|
|
206
205
|
options = { ...options };
|
|
207
206
|
// save the original hint value so we know it was infer/all
|
|
208
207
|
options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
|
|
@@ -494,10 +493,6 @@ export class EntityManager {
|
|
|
494
493
|
const conds = [...ret, where].filter(c => Utils.hasObjectKeys(c) || Raw.hasObjectFragments(c));
|
|
495
494
|
return conds.length > 1 ? { $and: conds } : conds[0];
|
|
496
495
|
}
|
|
497
|
-
/**
|
|
498
|
-
* Calls `em.find()` and `em.count()` with the same arguments (where applicable) and returns the results as tuple
|
|
499
|
-
* where the first element is the array of entities, and the second is the count.
|
|
500
|
-
*/
|
|
501
496
|
async findAndCount(entityName, where, options = {}) {
|
|
502
497
|
const em = this.getContext(false);
|
|
503
498
|
await em.tryFlush(entityName, options);
|
|
@@ -631,9 +626,6 @@ export class EntityManager {
|
|
|
631
626
|
}
|
|
632
627
|
return entity;
|
|
633
628
|
}
|
|
634
|
-
/**
|
|
635
|
-
* Finds first entity matching your `where` query.
|
|
636
|
-
*/
|
|
637
629
|
async findOne(entityName, where, options = {}) {
|
|
638
630
|
if (options.disableIdentityMap ?? this.config.get('disableIdentityMap')) {
|
|
639
631
|
const em = this.getContext(false);
|
|
@@ -653,6 +645,7 @@ export class EntityManager {
|
|
|
653
645
|
}
|
|
654
646
|
await em.tryFlush(entityName, options);
|
|
655
647
|
const meta = em.metadata.get(entityName);
|
|
648
|
+
em.validateIndexUsage(meta, where, options);
|
|
656
649
|
where = await em.processWhere(entityName, where, options, 'read');
|
|
657
650
|
validateEmptyWhere(where);
|
|
658
651
|
em.checkLockRequirements(options.lockMode, meta);
|
|
@@ -701,12 +694,6 @@ export class EntityManager {
|
|
|
701
694
|
await em.storeCache(options.cache, cached, () => helper(entity).toPOJO());
|
|
702
695
|
return entity;
|
|
703
696
|
}
|
|
704
|
-
/**
|
|
705
|
-
* Finds first entity matching your `where` query. If nothing found, it will throw an error.
|
|
706
|
-
* If the `strict` option is specified and nothing is found or more than one matching entity is found, it will throw an error.
|
|
707
|
-
* You can override the factory for creating this method via `options.failHandler` locally
|
|
708
|
-
* or via `Configuration.findOneOrFailHandler` (`findExactlyOneOrFailHandler` when specifying `strict`) globally.
|
|
709
|
-
*/
|
|
710
697
|
async findOneOrFail(entityName, where, options = {}) {
|
|
711
698
|
let entity;
|
|
712
699
|
let isStrictViolation = false;
|
|
@@ -1211,6 +1198,54 @@ export class EntityManager {
|
|
|
1211
1198
|
});
|
|
1212
1199
|
return res.insertId;
|
|
1213
1200
|
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Clones rows matching the condition at the database level via INSERT...SELECT.
|
|
1203
|
+
* Automatically excludes auto-increment PKs, generated columns, and resets version properties.
|
|
1204
|
+
* Returns the cloned entity, registered in the identity map.
|
|
1205
|
+
*
|
|
1206
|
+
* @example
|
|
1207
|
+
* ```ts
|
|
1208
|
+
* // Clone by entity class + where + overrides
|
|
1209
|
+
* const cloned = await em.clone(Author, { id: 1 }, { email: 'new@email.com' });
|
|
1210
|
+
*
|
|
1211
|
+
* // Pure clone (all non-PK fields copied)
|
|
1212
|
+
* const cloned = await em.clone(Author, { id: 1 });
|
|
1213
|
+
*
|
|
1214
|
+
* // Clone a loaded entity
|
|
1215
|
+
* const author = await em.findOneOrFail(Author, 1);
|
|
1216
|
+
* const cloned = await em.clone(author, { email: 'new@email.com' });
|
|
1217
|
+
* ```
|
|
1218
|
+
*/
|
|
1219
|
+
async clone(entityNameOrEntity, whereOrOverrides, overridesOrOptions, options) {
|
|
1220
|
+
const em = this.getContext(false);
|
|
1221
|
+
let entityName;
|
|
1222
|
+
let where;
|
|
1223
|
+
let overrides;
|
|
1224
|
+
if (Utils.isEntity(entityNameOrEntity)) {
|
|
1225
|
+
// clone(entity, overrides?, options?)
|
|
1226
|
+
entityName = entityNameOrEntity.constructor;
|
|
1227
|
+
where = helper(entityNameOrEntity).getPrimaryKey();
|
|
1228
|
+
overrides = whereOrOverrides;
|
|
1229
|
+
options = overridesOrOptions;
|
|
1230
|
+
}
|
|
1231
|
+
else {
|
|
1232
|
+
// clone(EntityClass, where, overrides?, options?)
|
|
1233
|
+
entityName = entityNameOrEntity;
|
|
1234
|
+
where = whereOrOverrides;
|
|
1235
|
+
overrides = overridesOrOptions;
|
|
1236
|
+
}
|
|
1237
|
+
options ??= {};
|
|
1238
|
+
em.prepareOptions(options);
|
|
1239
|
+
const meta = em.metadata.get(entityName);
|
|
1240
|
+
const res = await em.driver.nativeClone(entityName, where, overrides, {
|
|
1241
|
+
ctx: em.#transactionContext,
|
|
1242
|
+
...options,
|
|
1243
|
+
});
|
|
1244
|
+
const pk = res.insertId ?? res.row?.[meta.primaryKeys[0]];
|
|
1245
|
+
return em.findOneOrFail(entityName, pk, {
|
|
1246
|
+
schema: options.schema,
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1214
1249
|
/**
|
|
1215
1250
|
* Fires native multi-insert query. Calling this has no side effects on the context (identity map).
|
|
1216
1251
|
*/
|
|
@@ -1423,6 +1458,29 @@ export class EntityManager {
|
|
|
1423
1458
|
await em.storeCache(options.cache, cached, () => +count);
|
|
1424
1459
|
return +count;
|
|
1425
1460
|
}
|
|
1461
|
+
/**
|
|
1462
|
+
* Counts entities grouped by one or more properties. Returns a dictionary keyed by the grouped
|
|
1463
|
+
* field value(s), with counts as values. For composite `groupBy`, keys are joined with `~~~`.
|
|
1464
|
+
*
|
|
1465
|
+
* SQL drivers issue a single `GROUP BY` query; MongoDB uses an aggregation pipeline.
|
|
1466
|
+
*
|
|
1467
|
+
* @example
|
|
1468
|
+
* ```ts
|
|
1469
|
+
* // Count books per author
|
|
1470
|
+
* const counts = await em.countBy(Book, 'author');
|
|
1471
|
+
* // { '1': 2, '2': 1, '3': 3 }
|
|
1472
|
+
*
|
|
1473
|
+
* // Count with a filter
|
|
1474
|
+
* const counts = await em.countBy(Book, 'author', { where: { active: true } });
|
|
1475
|
+
*
|
|
1476
|
+
* // Composite groupBy — keys joined with ~~~
|
|
1477
|
+
* const counts = await em.countBy(Order, ['status', 'country']);
|
|
1478
|
+
* // { 'pending~~~US': 5, 'shipped~~~DE': 3 }
|
|
1479
|
+
* ```
|
|
1480
|
+
*/
|
|
1481
|
+
async countBy(entityName, groupBy, options) {
|
|
1482
|
+
throw new Error(`${this.constructor.name}.countBy() is not supported by the current driver`);
|
|
1483
|
+
}
|
|
1426
1484
|
/**
|
|
1427
1485
|
* Tells the EntityManager to make an instance managed and persistent.
|
|
1428
1486
|
* The entity will be entered into the database at or before transaction commit or as a result of the flush operation.
|
|
@@ -1528,7 +1586,7 @@ export class EntityManager {
|
|
|
1528
1586
|
const em = this.getContext();
|
|
1529
1587
|
em.prepareOptions(options);
|
|
1530
1588
|
const entityName = arr[0].constructor;
|
|
1531
|
-
const preparedPopulate = await em.preparePopulate(entityName, { populate: populate, filters: options.filters }, options.validate);
|
|
1589
|
+
const preparedPopulate = await em.preparePopulate(entityName, { populate: populate, filters: options.filters, populateHints: options.populateHints }, options.validate);
|
|
1532
1590
|
await em.#entityLoader.populate(entityName, arr, preparedPopulate, options);
|
|
1533
1591
|
return entities;
|
|
1534
1592
|
}
|
|
@@ -1679,6 +1737,79 @@ export class EntityManager {
|
|
|
1679
1737
|
throw ValidationError.transactionRequired();
|
|
1680
1738
|
}
|
|
1681
1739
|
}
|
|
1740
|
+
validateIndexUsage(meta, where, options) {
|
|
1741
|
+
if (!options.using) {
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
const indexNames = Utils.asArray(options.using);
|
|
1745
|
+
const allIndexes = [...meta.indexes, ...meta.uniques];
|
|
1746
|
+
const indexMap = new Map();
|
|
1747
|
+
for (const idx of allIndexes) {
|
|
1748
|
+
if (idx.name) {
|
|
1749
|
+
indexMap.set(idx.name, Utils.asArray(idx.properties ?? []));
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
for (const prop of meta.props) {
|
|
1753
|
+
if (typeof prop.index === 'string') {
|
|
1754
|
+
indexMap.set(prop.index, [prop.name]);
|
|
1755
|
+
}
|
|
1756
|
+
if (typeof prop.unique === 'string') {
|
|
1757
|
+
indexMap.set(prop.unique, [prop.name]);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
const allowedProps = new Set();
|
|
1761
|
+
for (const name of indexNames) {
|
|
1762
|
+
const props = indexMap.get(name);
|
|
1763
|
+
if (!props) {
|
|
1764
|
+
const available = [...indexMap.keys()];
|
|
1765
|
+
throw new Error(`Index '${name}' not found on entity '${meta.className}'. ` +
|
|
1766
|
+
(available.length > 0 ? `Available indexes: ${available.join(', ')}` : 'No named indexes defined.'));
|
|
1767
|
+
}
|
|
1768
|
+
for (const prop of props) {
|
|
1769
|
+
allowedProps.add(prop);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
this.validateWhereKeysForIndex(where, allowedProps, indexNames);
|
|
1773
|
+
if (options.orderBy) {
|
|
1774
|
+
const orderMaps = Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy];
|
|
1775
|
+
for (const orderMap of orderMaps) {
|
|
1776
|
+
for (const key of Object.keys(orderMap)) {
|
|
1777
|
+
if (!allowedProps.has(key)) {
|
|
1778
|
+
throw new Error(`Property '${key}' in orderBy is not covered by index '${indexNames.join("', '")}'. ` +
|
|
1779
|
+
`Allowed properties: ${[...allowedProps].join(', ')}`);
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
validateWhereKeysForIndex(where, allowedProps, indexNames) {
|
|
1786
|
+
if (typeof where !== 'object' || where === null) {
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
if (Array.isArray(where)) {
|
|
1790
|
+
for (const item of where) {
|
|
1791
|
+
this.validateWhereKeysForIndex(item, allowedProps, indexNames);
|
|
1792
|
+
}
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
for (const key of Object.keys(where)) {
|
|
1796
|
+
if (key === '$and' || key === '$or') {
|
|
1797
|
+
for (const item of Utils.asArray(where[key])) {
|
|
1798
|
+
this.validateWhereKeysForIndex(item, allowedProps, indexNames);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
else if (key === '$not') {
|
|
1802
|
+
this.validateWhereKeysForIndex(where[key], allowedProps, indexNames);
|
|
1803
|
+
}
|
|
1804
|
+
else if (key.startsWith('$')) {
|
|
1805
|
+
continue;
|
|
1806
|
+
}
|
|
1807
|
+
else if (!allowedProps.has(key)) {
|
|
1808
|
+
throw new Error(`Property '${key}' in where clause is not covered by index '${indexNames.join("', '")}'. ` +
|
|
1809
|
+
`Allowed properties: ${[...allowedProps].join(', ')}`);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1682
1813
|
async lockAndPopulate(meta, entity, where, options) {
|
|
1683
1814
|
if (!meta.virtual && options.lockMode === LockMode.OPTIMISTIC) {
|
|
1684
1815
|
await this.lock(entity, options.lockMode, {
|
|
@@ -1788,9 +1919,21 @@ export class EntityManager {
|
|
|
1788
1919
|
}
|
|
1789
1920
|
if (options.populateHints) {
|
|
1790
1921
|
applyPopulateHints(populate, options.populateHints);
|
|
1922
|
+
this.forceSelectInForLimitedPopulate(populate);
|
|
1791
1923
|
}
|
|
1792
1924
|
return populate;
|
|
1793
1925
|
}
|
|
1926
|
+
/** Force SELECT_IN strategy on populate entries with `limit`, since JOINED cannot do per-parent limiting. */
|
|
1927
|
+
forceSelectInForLimitedPopulate(populate) {
|
|
1928
|
+
for (const entry of populate) {
|
|
1929
|
+
if (entry.limit != null) {
|
|
1930
|
+
entry.strategy = LoadStrategy.SELECT_IN;
|
|
1931
|
+
}
|
|
1932
|
+
if (entry.children?.length) {
|
|
1933
|
+
this.forceSelectInForLimitedPopulate(entry.children);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1794
1937
|
/**
|
|
1795
1938
|
* when the entity is found in identity map, we check if it was partially loaded or we are trying to populate
|
|
1796
1939
|
* some additional lazy properties, if so, we reload and merge the data from database
|
|
@@ -1923,6 +2066,8 @@ export class EntityManager {
|
|
|
1923
2066
|
return (em.#loaders[type] ??= new DataLoader(DataloaderUtils.getColBatchLoadFn(em)));
|
|
1924
2067
|
case 'm:n':
|
|
1925
2068
|
return (em.#loaders[type] ??= new DataLoader(DataloaderUtils.getManyToManyColBatchLoadFn(em)));
|
|
2069
|
+
case 'count':
|
|
2070
|
+
return (em.#loaders[type] ??= new DataLoader(DataloaderUtils.getCountBatchLoadFn(em)));
|
|
1926
2071
|
}
|
|
1927
2072
|
}
|
|
1928
2073
|
/**
|
|
@@ -28,6 +28,7 @@ export declare abstract class DatabaseDriver<C extends Connection> implements ID
|
|
|
28
28
|
abstract nativeUpdate<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, data: EntityDictionary<T>, options?: NativeInsertUpdateOptions<T>): Promise<QueryResult<T>>;
|
|
29
29
|
nativeUpdateMany<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>[], data: EntityDictionary<T>[], options?: NativeInsertUpdateManyOptions<T>): Promise<QueryResult<T>>;
|
|
30
30
|
abstract nativeDelete<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, options?: DeleteOptions<T>): Promise<QueryResult<T>>;
|
|
31
|
+
nativeClone<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, overrides?: EntityData<T>, options?: NativeInsertUpdateOptions<T>): Promise<QueryResult<T>>;
|
|
31
32
|
abstract count<T extends object, P extends string = never>(entityName: EntityName<T>, where: FilterQuery<T>, options?: CountOptions<T, P>): Promise<number>;
|
|
32
33
|
/** Creates a new EntityManager instance bound to this driver. */
|
|
33
34
|
createEntityManager(useContext?: boolean): this[typeof EntityManagerType];
|
|
@@ -27,6 +27,9 @@ export class DatabaseDriver {
|
|
|
27
27
|
async nativeUpdateMany(entityName, where, data, options) {
|
|
28
28
|
throw new Error(`Batch updates are not supported by ${this.constructor.name} driver`);
|
|
29
29
|
}
|
|
30
|
+
async nativeClone(entityName, where, overrides, options) {
|
|
31
|
+
throw new Error(`${this.constructor.name} does not support nativeClone`);
|
|
32
|
+
}
|
|
30
33
|
/** Creates a new EntityManager instance bound to this driver. */
|
|
31
34
|
createEntityManager(useContext) {
|
|
32
35
|
const EntityManagerClass = this.config.get('entityManager', EntityManager);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ConnectionType, Constructor, EntityData, EntityMetadata, EntityProperty, FilterQuery, Primary, Dictionary, IPrimaryKey, PopulateOptions, EntityDictionary, AutoPath, ObjectQuery, FilterObject, Populate, EntityName, PopulateHintOptions, Prefixes } from '../typings.js';
|
|
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
2
|
import type { 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';
|
|
@@ -52,6 +52,11 @@ export interface IDatabaseDriver<C extends Connection = Connection> {
|
|
|
52
52
|
nativeUpdateMany<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>[], data: EntityDictionary<T>[], options?: NativeInsertUpdateManyOptions<T>): Promise<QueryResult<T>>;
|
|
53
53
|
/** Deletes rows matching the given condition. */
|
|
54
54
|
nativeDelete<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, options?: NativeDeleteOptions<T>): Promise<QueryResult<T>>;
|
|
55
|
+
/**
|
|
56
|
+
* Clones rows matching the condition at the database level via INSERT...SELECT.
|
|
57
|
+
* Automatically excludes auto-increment PKs, generated columns, and resets version properties.
|
|
58
|
+
*/
|
|
59
|
+
nativeClone<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, overrides?: EntityData<T>, options?: NativeInsertUpdateOptions<T>): Promise<QueryResult<T>>;
|
|
55
60
|
/** Persists changes to M:N collections (inserts/deletes pivot table rows). */
|
|
56
61
|
syncCollections<T extends object, O extends object>(collections: Iterable<Collection<T, O>>, options?: DriverMethodOptions): Promise<void>;
|
|
57
62
|
/** Counts entities matching the given condition. */
|
|
@@ -211,6 +216,20 @@ export interface FindOptions<Entity, Hint extends string = never, Fields extends
|
|
|
211
216
|
connectionType?: ConnectionType;
|
|
212
217
|
/** SQL: appended to FROM clause (e.g. `'force index(my_index)'`); MongoDB: index name or spec passed as `hint`. */
|
|
213
218
|
indexHint?: string | Dictionary;
|
|
219
|
+
/**
|
|
220
|
+
* Named index(es) for this query. When provided:
|
|
221
|
+
* - Validates that `where` and `orderBy` only reference columns covered by the specified index(es).
|
|
222
|
+
* - Emits SQL index hints where supported (MySQL/MariaDB: `USE INDEX`, MSSQL: `WITH (INDEX(...))`).
|
|
223
|
+
* - If `indexHint` is also set, `indexHint` takes precedence for SQL generation.
|
|
224
|
+
*
|
|
225
|
+
* Accepts a single index name or an array. For `defineEntity` entities with named indexes
|
|
226
|
+
* and decorator entities with `[IndexHints]`, index names are autocompleted.
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* await em.find(Book, { title: 'foo' }, { using: 'idx_book_title' });
|
|
230
|
+
* await em.find(Book, { title: 'foo', author: 1 }, { using: ['idx_book_title', 'idx_book_author'] });
|
|
231
|
+
*/
|
|
232
|
+
using?: IndexName<Entity> | IndexName<Entity>[];
|
|
214
233
|
/** sql only */
|
|
215
234
|
comments?: string | string[];
|
|
216
235
|
/** sql only */
|
|
@@ -306,6 +325,16 @@ export interface CountOptions<T extends object, P extends string = never> {
|
|
|
306
325
|
/** @internal used to apply filters to the auto-joined relations */
|
|
307
326
|
em?: EntityManager;
|
|
308
327
|
}
|
|
328
|
+
/** Options for `em.countBy()` queries. */
|
|
329
|
+
export interface CountByOptions<T extends object> {
|
|
330
|
+
where?: FilterQuery<T>;
|
|
331
|
+
filters?: FilterOptions;
|
|
332
|
+
having?: FilterQuery<T>;
|
|
333
|
+
schema?: string;
|
|
334
|
+
flushMode?: FlushMode | `${FlushMode}`;
|
|
335
|
+
loggerContext?: LogContext;
|
|
336
|
+
logging?: LoggingOptions;
|
|
337
|
+
}
|
|
309
338
|
/** Options for `em.qb().update()` operations. */
|
|
310
339
|
export interface UpdateOptions<T> {
|
|
311
340
|
filters?: FilterOptions;
|
package/entity/BaseEntity.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Ref } from './Reference.js';
|
|
2
|
-
import type { AutoPath, EntityData, EntityDTO, Loaded, LoadedReference, AddEager, EntityKey, FromEntityType, IsSubset, MergeSelected, SerializeDTO } from '../typings.js';
|
|
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';
|
|
5
5
|
import { type SerializeOptions } from '../serialization/EntitySerializer.js';
|
|
@@ -80,7 +80,7 @@ export declare abstract class BaseEntity {
|
|
|
80
80
|
/** Converts the entity to a plain object, including all properties regardless of serialization rules. */
|
|
81
81
|
toPOJO<Entity extends this = this>(): EntityDTO<Entity>;
|
|
82
82
|
/** Serializes the entity with control over which relations and fields to include or exclude. */
|
|
83
|
-
serialize<Entity extends this = this, Naked extends FromEntityType<Entity> = FromEntityType<Entity>, Hint extends string = never, Exclude extends string = never>(options?: SerializeOptions<Naked, Hint, Exclude>): SerializeDTO<Naked, Hint, Exclude
|
|
83
|
+
serialize<Entity extends this = this, Naked extends FromEntityType<Entity> = FromEntityType<Entity>, Hint extends string = never, Exclude extends string = never, Fields extends string = never>(options?: SerializeOptions<Naked, Hint, Exclude, Fields>): SerializeDTO<Naked, Hint, Exclude, never, ResolveSerializeFields<Fields, ExtractFieldsHint<Entity>>, SerializeFieldsKeepPK<Fields>>;
|
|
84
84
|
/** Assigns the given data to this entity, updating its properties and relations. */
|
|
85
85
|
assign<Entity extends this, Naked extends FromEntityType<Entity> = FromEntityType<Entity>, Convert extends boolean = false, Data extends EntityData<Naked, Convert> | Partial<EntityDTO<Naked>> = EntityData<Naked, Convert> | Partial<EntityDTO<Naked>>>(data: Data & IsSubset<EntityData<Naked>, Data>, options?: AssignOptions<Convert>): MergeSelected<Entity, Naked, keyof Data & string>;
|
|
86
86
|
/** Initializes (refreshes) the entity by reloading it from the database. Returns null if not found. */
|
package/entity/Collection.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export declare class Collection<T extends object, O extends object = object> {
|
|
|
37
37
|
/**
|
|
38
38
|
* Gets the count of collection items from database instead of counting loaded items.
|
|
39
39
|
* The value is cached (unless you use the `where` option), use `refresh: true` to force reload it.
|
|
40
|
+
* When the dataloader is enabled (globally or per-query), multiple calls are batched into a single grouped query.
|
|
40
41
|
*/
|
|
41
42
|
loadCount(options?: LoadCountOptions<T> | boolean): Promise<number>;
|
|
42
43
|
/** Queries a subset of the collection items from the database with custom filtering, ordering, and pagination. */
|
|
@@ -48,14 +49,14 @@ export declare class Collection<T extends object, O extends object = object> {
|
|
|
48
49
|
/** Serializes the collection items to plain JSON objects. Returns an empty array if not initialized. */
|
|
49
50
|
toJSON<TT extends T>(): EntityDTO<TT>[];
|
|
50
51
|
/** Adds one or more items to the collection, propagating the change to the inverse side. Returns the number of items added. */
|
|
51
|
-
add<TT extends T>(entity: TT | Reference<TT> | Iterable<TT | Reference<TT>>, ...entities: (
|
|
52
|
+
add<TT extends T>(entity: TT | Reference<TT> | Iterable<TT | Reference<TT>>, ...entities: (T | Reference<T>)[]): number;
|
|
52
53
|
/**
|
|
53
54
|
* Remove specified item(s) from the collection. Note that removing item from collection does not necessarily imply deleting the target entity,
|
|
54
55
|
* it means we are disconnecting the relation - removing items from collection, not removing entities from database - `Collection.remove()`
|
|
55
56
|
* is not the same as `em.remove()`. If we want to delete the entity by removing it from collection, we need to enable `orphanRemoval: true`,
|
|
56
57
|
* which tells the ORM we don't want orphaned entities to exist, so we know those should be removed.
|
|
57
58
|
*/
|
|
58
|
-
remove<TT extends T>(entity: TT | Reference<TT> | Iterable<TT | Reference<TT>> | ((item:
|
|
59
|
+
remove<TT extends T>(entity: TT | Reference<TT> | Iterable<TT | Reference<TT>> | ((item: T) => boolean), ...entities: (T | Reference<T>)[]): number;
|
|
59
60
|
/** Checks whether the collection contains the given item. */
|
|
60
61
|
contains<TT extends T>(item: TT | Reference<TT>, check?: boolean): boolean;
|
|
61
62
|
/** Returns the number of items in the collection. Throws if the collection is not initialized. */
|
|
@@ -93,7 +94,7 @@ export declare class Collection<T extends object, O extends object = object> {
|
|
|
93
94
|
/**
|
|
94
95
|
* @internal
|
|
95
96
|
*/
|
|
96
|
-
hydrate(items: T[], forcePropagate?: boolean, partial?: boolean): void;
|
|
97
|
+
hydrate(items: T[], forcePropagate?: boolean, partial?: boolean, readonly?: boolean): void;
|
|
97
98
|
/**
|
|
98
99
|
* Remove all items from the collection. Note that removing items from collection does not necessarily imply deleting the target entity,
|
|
99
100
|
* it means we are disconnecting the relation - removing items from collection, not removing entities from database - `Collection.remove()`
|
|
@@ -193,4 +194,6 @@ export interface LoadCountOptions<T extends object> extends CountOptions<T, '*'>
|
|
|
193
194
|
refresh?: boolean;
|
|
194
195
|
/** Additional filtering conditions for the count query. */
|
|
195
196
|
where?: FilterQuery<T>;
|
|
197
|
+
/** Whether to use the dataloader for batching count operations. */
|
|
198
|
+
dataloader?: boolean;
|
|
196
199
|
}
|
package/entity/Collection.js
CHANGED
|
@@ -79,10 +79,11 @@ export class Collection {
|
|
|
79
79
|
/**
|
|
80
80
|
* Gets the count of collection items from database instead of counting loaded items.
|
|
81
81
|
* The value is cached (unless you use the `where` option), use `refresh: true` to force reload it.
|
|
82
|
+
* When the dataloader is enabled (globally or per-query), multiple calls are batched into a single grouped query.
|
|
82
83
|
*/
|
|
83
84
|
async loadCount(options = {}) {
|
|
84
85
|
options = typeof options === 'boolean' ? { refresh: options } : options;
|
|
85
|
-
const { refresh, where, ...countOptions } = options;
|
|
86
|
+
const { refresh, where, dataloader, ...countOptions } = options;
|
|
86
87
|
if (!refresh && !where && this.#count != null) {
|
|
87
88
|
return this.#count;
|
|
88
89
|
}
|
|
@@ -92,8 +93,15 @@ export class Collection {
|
|
|
92
93
|
this.property.owner) {
|
|
93
94
|
return (this.#count = this.length);
|
|
94
95
|
}
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
let count;
|
|
97
|
+
if (dataloader ?? [DataloaderType.ALL, DataloaderType.COLLECTION].includes(em.config.getDataloaderType())) {
|
|
98
|
+
const loader = await em.getDataLoader('count');
|
|
99
|
+
count = await loader.load([this, { where, ...countOptions }]);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
const cond = this.createLoadCountCondition(where ?? {});
|
|
103
|
+
count = await em.count(this.property.targetMeta.class, cond, countOptions);
|
|
104
|
+
}
|
|
97
105
|
if (!where) {
|
|
98
106
|
this.#count = count;
|
|
99
107
|
}
|
|
@@ -462,7 +470,7 @@ export class Collection {
|
|
|
462
470
|
/**
|
|
463
471
|
* @internal
|
|
464
472
|
*/
|
|
465
|
-
hydrate(items, forcePropagate, partial) {
|
|
473
|
+
hydrate(items, forcePropagate, partial, readonly) {
|
|
466
474
|
for (let i = 0; i < this.#items.size; i++) {
|
|
467
475
|
delete this[i];
|
|
468
476
|
}
|
|
@@ -472,6 +480,9 @@ export class Collection {
|
|
|
472
480
|
this.#count = 0;
|
|
473
481
|
this.add(items);
|
|
474
482
|
this.takeSnapshot(forcePropagate);
|
|
483
|
+
if (readonly) {
|
|
484
|
+
this.#readonly = true;
|
|
485
|
+
}
|
|
475
486
|
}
|
|
476
487
|
/**
|
|
477
488
|
* Remove all items from the collection. Note that removing items from collection does not necessarily imply deleting the target entity,
|