@mikro-orm/core 7.0.0-dev.39 → 7.0.0-dev.40
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 +12 -9
- package/EntityManager.js +77 -52
- package/README.md +2 -0
- package/decorators/Property.d.ts +53 -3
- package/entity/Collection.js +3 -0
- package/entity/EntityFactory.d.ts +1 -0
- package/entity/EntityFactory.js +7 -3
- package/entity/EntityHelper.js +17 -2
- package/entity/EntityLoader.d.ts +3 -3
- package/entity/EntityLoader.js +20 -2
- package/entity/Reference.d.ts +1 -0
- package/entity/Reference.js +10 -4
- package/entity/defineEntity.d.ts +12 -8
- package/entity/defineEntity.js +9 -2
- package/hydration/ObjectHydrator.d.ts +4 -4
- package/hydration/ObjectHydrator.js +25 -22
- package/index.d.ts +1 -1
- package/metadata/EntitySchema.d.ts +2 -2
- package/metadata/MetadataDiscovery.d.ts +1 -0
- package/metadata/MetadataDiscovery.js +37 -3
- package/naming-strategy/AbstractNamingStrategy.d.ts +5 -1
- package/naming-strategy/AbstractNamingStrategy.js +7 -1
- package/naming-strategy/NamingStrategy.d.ts +11 -1
- package/package.json +2 -2
- package/platforms/Platform.js +1 -1
- package/serialization/EntitySerializer.js +1 -1
- package/serialization/EntityTransformer.js +1 -1
- package/serialization/SerializationContext.js +1 -1
- package/typings.d.ts +11 -4
- package/unit-of-work/ChangeSetPersister.js +16 -5
- package/unit-of-work/UnitOfWork.d.ts +6 -0
- package/unit-of-work/UnitOfWork.js +37 -23
- package/utils/Configuration.d.ts +4 -0
- package/utils/Configuration.js +10 -0
- package/utils/EntityComparator.js +11 -1
- package/utils/QueryHelper.d.ts +3 -1
- package/utils/QueryHelper.js +18 -0
- package/utils/RawQueryFragment.d.ts +2 -2
- package/utils/TransactionManager.js +0 -2
- package/utils/Utils.js +2 -2
package/EntityManager.d.ts
CHANGED
|
@@ -9,8 +9,8 @@ import { type EntityRepository } from './entity/EntityRepository.js';
|
|
|
9
9
|
import { EntityLoader, type EntityLoaderOptions } from './entity/EntityLoader.js';
|
|
10
10
|
import { Reference } from './entity/Reference.js';
|
|
11
11
|
import { UnitOfWork } from './unit-of-work/UnitOfWork.js';
|
|
12
|
-
import type { CountOptions, DeleteOptions, FindAllOptions, FindByCursorOptions, FindOneOptions, FindOneOrFailOptions, FindOptions, GetReferenceOptions, IDatabaseDriver, LockOptions, NativeInsertUpdateOptions, StreamOptions, UpdateOptions, UpsertManyOptions, UpsertOptions } from './drivers/IDatabaseDriver.js';
|
|
13
|
-
import type { AnyEntity, AnyString, ArrayElement, AutoPath, ConnectionType, Dictionary, EntityData, EntityDictionary, EntityDTO, EntityMetadata, EntityName, FilterQuery, FromEntityType, GetRepository, IHydrator, IsSubset, Loaded, MaybePromise, MergeLoaded, MergeSelected, NoInfer, ObjectQuery, Primary, Ref, RequiredEntityData, UnboxArray } from './typings.js';
|
|
12
|
+
import type { CountOptions, DeleteOptions, FilterOptions, FindAllOptions, FindByCursorOptions, FindOneOptions, FindOneOrFailOptions, FindOptions, GetReferenceOptions, IDatabaseDriver, LockOptions, NativeInsertUpdateOptions, StreamOptions, UpdateOptions, UpsertManyOptions, UpsertOptions } from './drivers/IDatabaseDriver.js';
|
|
13
|
+
import type { AnyEntity, AnyString, ArrayElement, AutoPath, ConnectionType, Dictionary, EntityData, EntityDictionary, EntityDTO, EntityMetadata, EntityName, FilterDef, FilterQuery, FromEntityType, GetRepository, IHydrator, IsSubset, Loaded, MaybePromise, MergeLoaded, MergeSelected, NoInfer, ObjectQuery, Primary, Ref, RequiredEntityData, UnboxArray } from './typings.js';
|
|
14
14
|
import { FlushMode, LockMode, PopulatePath, type TransactionOptions } from './enums.js';
|
|
15
15
|
import type { MetadataStorage } from './metadata/MetadataStorage.js';
|
|
16
16
|
import type { Transaction } from './connections/Connection.js';
|
|
@@ -109,19 +109,19 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
|
|
|
109
109
|
/**
|
|
110
110
|
* Registers global filter to this entity manager. Global filters are enabled by default (unless disabled via last parameter).
|
|
111
111
|
*/
|
|
112
|
-
addFilter<T1>(name: string, cond: FilterQuery<T1> | ((args: Dictionary) => MaybePromise<FilterQuery<T1>>), entityName?: EntityName<T1> | [EntityName<T1>],
|
|
112
|
+
addFilter<T1>(name: string, cond: FilterQuery<T1> | ((args: Dictionary) => MaybePromise<FilterQuery<T1>>), entityName?: EntityName<T1> | [EntityName<T1>], options?: boolean | Partial<FilterDef>): void;
|
|
113
113
|
/**
|
|
114
114
|
* Registers global filter to this entity manager. Global filters are enabled by default (unless disabled via last parameter).
|
|
115
115
|
*/
|
|
116
|
-
addFilter<T1, T2>(name: string, cond: FilterQuery<T1 | T2> | ((args: Dictionary) => MaybePromise<FilterQuery<T1 | T2>>), entityName?: [EntityName<T1>, EntityName<T2>],
|
|
116
|
+
addFilter<T1, T2>(name: string, cond: FilterQuery<T1 | T2> | ((args: Dictionary) => MaybePromise<FilterQuery<T1 | T2>>), entityName?: [EntityName<T1>, EntityName<T2>], options?: boolean | Partial<FilterDef>): void;
|
|
117
117
|
/**
|
|
118
118
|
* Registers global filter to this entity manager. Global filters are enabled by default (unless disabled via last parameter).
|
|
119
119
|
*/
|
|
120
|
-
addFilter<T1, T2, T3>(name: string, cond: FilterQuery<T1 | T2 | T3> | ((args: Dictionary) => MaybePromise<FilterQuery<T1 | T2 | T3>>), entityName?: [EntityName<T1>, EntityName<T2>, EntityName<T3>],
|
|
120
|
+
addFilter<T1, T2, T3>(name: string, cond: FilterQuery<T1 | T2 | T3> | ((args: Dictionary) => MaybePromise<FilterQuery<T1 | T2 | T3>>), entityName?: [EntityName<T1>, EntityName<T2>, EntityName<T3>], options?: boolean | Partial<FilterDef>): void;
|
|
121
121
|
/**
|
|
122
122
|
* Registers global filter to this entity manager. Global filters are enabled by default (unless disabled via last parameter).
|
|
123
123
|
*/
|
|
124
|
-
addFilter(name: string, cond: Dictionary | ((args: Dictionary) => MaybePromise<FilterQuery<AnyEntity>>), entityName?: EntityName<AnyEntity> | EntityName<AnyEntity>[],
|
|
124
|
+
addFilter(name: string, cond: Dictionary | ((args: Dictionary) => MaybePromise<FilterQuery<AnyEntity>>), entityName?: EntityName<AnyEntity> | EntityName<AnyEntity>[], options?: boolean | Partial<FilterDef>): void;
|
|
125
125
|
/**
|
|
126
126
|
* Sets filter parameter values globally inside context defined by this entity manager.
|
|
127
127
|
* If you want to set shared value for all contexts, be sure to use the root entity manager.
|
|
@@ -145,15 +145,18 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
|
|
|
145
145
|
protected processWhere<Entity extends object, Hint extends string = never, Fields extends string = '*', Excludes extends string = never>(entityName: string, where: FilterQuery<Entity>, options: FindOptions<Entity, Hint, Fields, Excludes> | FindOneOptions<Entity, Hint, Fields, Excludes>, type: 'read' | 'update' | 'delete'): Promise<FilterQuery<Entity>>;
|
|
146
146
|
protected applyDiscriminatorCondition<Entity extends object>(entityName: string, where: FilterQuery<Entity>): FilterQuery<Entity>;
|
|
147
147
|
protected createPopulateWhere<Entity extends object>(cond: ObjectQuery<Entity>, options: FindOptions<Entity, any, any, any> | FindOneOptions<Entity, any, any, any> | CountOptions<Entity, any>): ObjectQuery<Entity>;
|
|
148
|
-
protected getJoinedFilters<Entity extends object>(meta: EntityMetadata<Entity>,
|
|
148
|
+
protected getJoinedFilters<Entity extends object>(meta: EntityMetadata<Entity>, options: FindOptions<Entity, any, any, any> | FindOneOptions<Entity, any, any, any>): Promise<ObjectQuery<Entity> | undefined>;
|
|
149
149
|
/**
|
|
150
150
|
* When filters are active on M:1 or 1:1 relations, we need to ref join them eagerly as they might affect the FK value.
|
|
151
151
|
*/
|
|
152
|
-
protected autoJoinRefsForFilters<T extends object>(meta: EntityMetadata<T>, options: FindOptions<T, any, any, any> | FindOneOptions<T, any, any, any
|
|
152
|
+
protected autoJoinRefsForFilters<T extends object>(meta: EntityMetadata<T>, options: FindOptions<T, any, any, any> | FindOneOptions<T, any, any, any>, parent?: {
|
|
153
|
+
className: string;
|
|
154
|
+
propName: string;
|
|
155
|
+
}): Promise<void>;
|
|
153
156
|
/**
|
|
154
157
|
* @internal
|
|
155
158
|
*/
|
|
156
|
-
applyFilters<Entity extends object>(entityName: string, where: FilterQuery<Entity> | undefined, options:
|
|
159
|
+
applyFilters<Entity extends object>(entityName: string, where: FilterQuery<Entity> | undefined, options: FilterOptions | undefined, type: 'read' | 'update' | 'delete', findOptions?: FindOptions<any, any, any, any> | FindOneOptions<any, any, any, any>): Promise<FilterQuery<Entity> | undefined>;
|
|
157
160
|
/**
|
|
158
161
|
* Calls `em.find()` and `em.count()` with the same arguments (where applicable) and returns the results as tuple
|
|
159
162
|
* where the first element is the array of entities, and the second is the count.
|
package/EntityManager.js
CHANGED
|
@@ -150,7 +150,7 @@ export class EntityManager {
|
|
|
150
150
|
// save the original hint value so we know it was infer/all
|
|
151
151
|
options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
|
|
152
152
|
options.populateWhere = this.createPopulateWhere({ ...where }, options);
|
|
153
|
-
options.populateFilter = await this.getJoinedFilters(meta,
|
|
153
|
+
options.populateFilter = await this.getJoinedFilters(meta, options);
|
|
154
154
|
const results = await em.driver.find(entityName, where, { ctx: em.transactionContext, em, ...options });
|
|
155
155
|
if (results.length === 0) {
|
|
156
156
|
await em.storeCache(options.cache, cached, []);
|
|
@@ -214,7 +214,7 @@ export class EntityManager {
|
|
|
214
214
|
// save the original hint value so we know it was infer/all
|
|
215
215
|
options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
|
|
216
216
|
options.populateWhere = this.createPopulateWhere({ ...where }, options);
|
|
217
|
-
options.populateFilter = await this.getJoinedFilters(meta,
|
|
217
|
+
options.populateFilter = await this.getJoinedFilters(meta, options);
|
|
218
218
|
const stream = em.driver.stream(entityName, where, {
|
|
219
219
|
ctx: em.transactionContext,
|
|
220
220
|
mapResults: false,
|
|
@@ -259,8 +259,8 @@ export class EntityManager {
|
|
|
259
259
|
/**
|
|
260
260
|
* Registers global filter to this entity manager. Global filters are enabled by default (unless disabled via last parameter).
|
|
261
261
|
*/
|
|
262
|
-
addFilter(name, cond, entityName,
|
|
263
|
-
|
|
262
|
+
addFilter(name, cond, entityName, options = true) {
|
|
263
|
+
options = typeof options === 'object' ? { name, cond, default: true, ...options } : { name, cond, default: options };
|
|
264
264
|
if (entityName) {
|
|
265
265
|
options.entity = Utils.asArray(entityName).map(n => Utils.className(n));
|
|
266
266
|
}
|
|
@@ -339,29 +339,39 @@ export class EntityManager {
|
|
|
339
339
|
}
|
|
340
340
|
return ret;
|
|
341
341
|
}
|
|
342
|
-
async getJoinedFilters(meta,
|
|
342
|
+
async getJoinedFilters(meta, options) {
|
|
343
|
+
if (!this.config.get('filtersOnRelations') || !options.populate) {
|
|
344
|
+
return undefined;
|
|
345
|
+
}
|
|
343
346
|
const ret = {};
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
347
|
+
for (const hint of options.populate) {
|
|
348
|
+
const field = hint.field.split(':')[0];
|
|
349
|
+
const prop = meta.properties[field];
|
|
350
|
+
const strategy = getLoadingStrategy(prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'), prop.kind);
|
|
351
|
+
const joined = strategy === LoadStrategy.JOINED && prop.kind !== ReferenceKind.SCALAR;
|
|
352
|
+
if (!joined && !hint.filter) {
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
const filters = QueryHelper.mergePropertyFilters(prop.filters, options.filters);
|
|
356
|
+
const where = await this.applyFilters(prop.type, {}, filters, 'read', {
|
|
357
|
+
...options,
|
|
358
|
+
populate: hint.children,
|
|
359
|
+
});
|
|
360
|
+
const where2 = await this.getJoinedFilters(prop.targetMeta, {
|
|
361
|
+
...options,
|
|
362
|
+
filters,
|
|
363
|
+
populate: hint.children,
|
|
364
|
+
populateWhere: PopulateHint.ALL,
|
|
365
|
+
});
|
|
366
|
+
if (Utils.hasObjectKeys(where)) {
|
|
367
|
+
ret[field] = ret[field] ? { $and: [where, ret[field]] } : where;
|
|
368
|
+
}
|
|
369
|
+
if (where2 && Utils.hasObjectKeys(where2)) {
|
|
370
|
+
if (ret[field]) {
|
|
371
|
+
Utils.merge(ret[field], where2);
|
|
357
372
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
Utils.merge(ret[field], where2);
|
|
361
|
-
}
|
|
362
|
-
else {
|
|
363
|
-
ret[field] = where2;
|
|
364
|
-
}
|
|
373
|
+
else {
|
|
374
|
+
ret[field] = where2;
|
|
365
375
|
}
|
|
366
376
|
}
|
|
367
377
|
}
|
|
@@ -370,29 +380,30 @@ export class EntityManager {
|
|
|
370
380
|
/**
|
|
371
381
|
* When filters are active on M:1 or 1:1 relations, we need to ref join them eagerly as they might affect the FK value.
|
|
372
382
|
*/
|
|
373
|
-
async autoJoinRefsForFilters(meta, options) {
|
|
374
|
-
if (!meta || !this.config.get('autoJoinRefsForFilters')) {
|
|
383
|
+
async autoJoinRefsForFilters(meta, options, parent) {
|
|
384
|
+
if (!meta || !this.config.get('autoJoinRefsForFilters') || options.filters === false) {
|
|
375
385
|
return;
|
|
376
386
|
}
|
|
377
|
-
const props = meta.relations.filter(prop => {
|
|
378
|
-
return !prop.object && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)
|
|
379
|
-
&& ((options.fields?.length ?? 0) === 0 || options.fields?.some(f => prop.name === f || prop.name.startsWith(`${String(f)}.`)));
|
|
380
|
-
});
|
|
381
387
|
const ret = options.populate;
|
|
382
|
-
for (const prop of
|
|
383
|
-
|
|
388
|
+
for (const prop of meta.relations) {
|
|
389
|
+
if (prop.object
|
|
390
|
+
|| ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)
|
|
391
|
+
|| !((options.fields?.length ?? 0) === 0 || options.fields?.some(f => prop.name === f || prop.name.startsWith(`${String(f)}.`)))
|
|
392
|
+
|| (parent?.className === prop.targetMeta.root.className && parent.propName === prop.inversedBy)) {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
options = { ...options, filters: QueryHelper.mergePropertyFilters(prop.filters, options.filters) };
|
|
396
|
+
const cond = await this.applyFilters(prop.type, {}, options.filters, 'read', options);
|
|
384
397
|
if (!Utils.isEmpty(cond)) {
|
|
385
398
|
const populated = options.populate.filter(({ field }) => field.split(':')[0] === prop.name);
|
|
386
399
|
let found = false;
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
found = true;
|
|
395
|
-
}
|
|
400
|
+
for (const hint of populated) {
|
|
401
|
+
if (!hint.all) {
|
|
402
|
+
hint.filter = true;
|
|
403
|
+
}
|
|
404
|
+
const strategy = getLoadingStrategy(prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'), prop.kind);
|
|
405
|
+
if (hint.field === `${prop.name}:ref` || (hint.filter && strategy === LoadStrategy.JOINED)) {
|
|
406
|
+
found = true;
|
|
396
407
|
}
|
|
397
408
|
}
|
|
398
409
|
if (!found) {
|
|
@@ -400,6 +411,14 @@ export class EntityManager {
|
|
|
400
411
|
}
|
|
401
412
|
}
|
|
402
413
|
}
|
|
414
|
+
for (const hint of ret) {
|
|
415
|
+
const [field, ref] = hint.field.split(':');
|
|
416
|
+
const prop = meta?.properties[field];
|
|
417
|
+
if (prop && !ref) {
|
|
418
|
+
hint.children ??= [];
|
|
419
|
+
await this.autoJoinRefsForFilters(prop.targetMeta, { ...options, populate: hint.children }, { className: meta.root.className, propName: prop.name });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
403
422
|
}
|
|
404
423
|
/**
|
|
405
424
|
* @internal
|
|
@@ -429,7 +448,7 @@ export class EntityManager {
|
|
|
429
448
|
let cond;
|
|
430
449
|
if (filter.cond instanceof Function) {
|
|
431
450
|
// @ts-ignore
|
|
432
|
-
const args = Utils.isPlainObject(options[filter.name]) ? options[filter.name] : this.getContext().filterParams[filter.name];
|
|
451
|
+
const args = Utils.isPlainObject(options?.[filter.name]) ? options[filter.name] : this.getContext().filterParams[filter.name];
|
|
433
452
|
if (!args && filter.cond.length > 0 && filter.args !== false) {
|
|
434
453
|
throw new Error(`No arguments provided for filter '${filter.name}'`);
|
|
435
454
|
}
|
|
@@ -438,13 +457,17 @@ export class EntityManager {
|
|
|
438
457
|
else {
|
|
439
458
|
cond = filter.cond;
|
|
440
459
|
}
|
|
441
|
-
|
|
460
|
+
cond = QueryHelper.processWhere({
|
|
442
461
|
where: cond,
|
|
443
462
|
entityName,
|
|
444
463
|
metadata: this.metadata,
|
|
445
464
|
platform: this.driver.getPlatform(),
|
|
446
465
|
aliased: type === 'read',
|
|
447
|
-
})
|
|
466
|
+
});
|
|
467
|
+
if (filter.strict) {
|
|
468
|
+
Object.defineProperty(cond, '__strict', { value: filter.strict, enumerable: false });
|
|
469
|
+
}
|
|
470
|
+
ret.push(cond);
|
|
448
471
|
}
|
|
449
472
|
const conds = [...ret, where].filter(c => Utils.hasObjectKeys(c));
|
|
450
473
|
return conds.length > 1 ? { $and: conds } : conds[0];
|
|
@@ -555,8 +578,9 @@ export class EntityManager {
|
|
|
555
578
|
async refresh(entity, options = {}) {
|
|
556
579
|
const fork = this.fork({ keepTransactionContext: true });
|
|
557
580
|
const entityName = entity.constructor.name;
|
|
581
|
+
const wrapped = helper(entity);
|
|
558
582
|
const reloaded = await fork.findOne(entityName, entity, {
|
|
559
|
-
schema:
|
|
583
|
+
schema: wrapped.__schema,
|
|
560
584
|
...options,
|
|
561
585
|
flushMode: FlushMode.COMMIT,
|
|
562
586
|
});
|
|
@@ -570,13 +594,13 @@ export class EntityManager {
|
|
|
570
594
|
const ref = em.getReference(e.constructor.name, helper(e).getPrimaryKey());
|
|
571
595
|
const data = helper(e).serialize({ ignoreSerializers: true, includeHidden: true });
|
|
572
596
|
em.config.getHydrator(this.metadata).hydrate(ref, helper(ref).__meta, data, em.entityFactory, 'full', false, true);
|
|
573
|
-
helper(ref).__originalEntityData
|
|
597
|
+
Utils.merge(helper(ref).__originalEntityData, this.comparator.prepareEntity(e));
|
|
574
598
|
found ||= ref === entity;
|
|
575
599
|
}
|
|
576
600
|
if (!found) {
|
|
577
601
|
const data = helper(reloaded).serialize({ ignoreSerializers: true, includeHidden: true });
|
|
578
|
-
em.config.getHydrator(this.metadata).hydrate(entity,
|
|
579
|
-
|
|
602
|
+
em.config.getHydrator(this.metadata).hydrate(entity, wrapped.__meta, data, em.entityFactory, 'full', false, true);
|
|
603
|
+
Utils.merge(wrapped.__originalEntityData, this.comparator.prepareEntity(reloaded));
|
|
580
604
|
}
|
|
581
605
|
return entity;
|
|
582
606
|
}
|
|
@@ -629,7 +653,7 @@ export class EntityManager {
|
|
|
629
653
|
// save the original hint value so we know it was infer/all
|
|
630
654
|
options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
|
|
631
655
|
options.populateWhere = this.createPopulateWhere({ ...where }, options);
|
|
632
|
-
options.populateFilter = await this.getJoinedFilters(meta,
|
|
656
|
+
options.populateFilter = await this.getJoinedFilters(meta, options);
|
|
633
657
|
const data = await em.driver.findOne(entityName, where, {
|
|
634
658
|
ctx: em.transactionContext,
|
|
635
659
|
em,
|
|
@@ -1272,6 +1296,7 @@ export class EntityManager {
|
|
|
1272
1296
|
...options,
|
|
1273
1297
|
newEntity: !options.managed,
|
|
1274
1298
|
merge: options.managed,
|
|
1299
|
+
normalizeAccessors: true,
|
|
1275
1300
|
});
|
|
1276
1301
|
options.persist ??= em.config.get('persistOnCreate');
|
|
1277
1302
|
if (options.persist && !this.getMetadata(entityName).embeddable) {
|
|
@@ -1321,7 +1346,7 @@ export class EntityManager {
|
|
|
1321
1346
|
const meta = em.metadata.find(entityName);
|
|
1322
1347
|
options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
|
|
1323
1348
|
options.populateWhere = this.createPopulateWhere({ ...where }, options);
|
|
1324
|
-
options.populateFilter = await this.getJoinedFilters(meta,
|
|
1349
|
+
options.populateFilter = await this.getJoinedFilters(meta, options);
|
|
1325
1350
|
em.validator.validateParams(where);
|
|
1326
1351
|
delete options.orderBy;
|
|
1327
1352
|
const cacheKey = em.cacheKey(entityName, options, 'em.count', where);
|
|
@@ -1455,7 +1480,7 @@ export class EntityManager {
|
|
|
1455
1480
|
const em = this.getContext();
|
|
1456
1481
|
em.prepareOptions(options);
|
|
1457
1482
|
const entityName = arr[0].constructor.name;
|
|
1458
|
-
const preparedPopulate = await em.preparePopulate(entityName, { populate: populate }, options.validate);
|
|
1483
|
+
const preparedPopulate = await em.preparePopulate(entityName, { populate: populate, filters: options.filters }, options.validate);
|
|
1459
1484
|
await em.entityLoader.populate(entityName, arr, preparedPopulate, options);
|
|
1460
1485
|
return entities;
|
|
1461
1486
|
}
|
package/README.md
CHANGED
|
@@ -381,6 +381,8 @@ See also the list of contributors who [participated](https://github.com/mikro-or
|
|
|
381
381
|
|
|
382
382
|
Please ⭐️ this repository if this project helped you!
|
|
383
383
|
|
|
384
|
+
> If you'd like to support my open-source work, consider sponsoring me directly at [github.com/sponsors/b4nan](https://github.com/sponsors/b4nan).
|
|
385
|
+
|
|
384
386
|
## 📝 License
|
|
385
387
|
|
|
386
388
|
Copyright © 2018 [Martin Adámek](https://github.com/b4nan).
|
package/decorators/Property.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { EntityName, Constructor, CheckCallback, GeneratedColumnCallback, A
|
|
|
3
3
|
import type { Type, types } from '../types/index.js';
|
|
4
4
|
import type { EntityManager } from '../EntityManager.js';
|
|
5
5
|
import type { SerializeOptions } from '../serialization/EntitySerializer.js';
|
|
6
|
+
import type { FilterOptions } from '../drivers/IDatabaseDriver.js';
|
|
6
7
|
export declare function Property<T extends object>(options?: PropertyOptions<T>): (target: T, propertyName: string) => any;
|
|
7
8
|
export interface PropertyOptions<Owner> {
|
|
8
9
|
/**
|
|
@@ -161,7 +162,7 @@ export interface PropertyOptions<Owner> {
|
|
|
161
162
|
* Set true to define the properties as setter. (virtual)
|
|
162
163
|
*
|
|
163
164
|
* @example
|
|
164
|
-
* ```
|
|
165
|
+
* ```ts
|
|
165
166
|
* @Property({ setter: true })
|
|
166
167
|
* set address(value: string) {
|
|
167
168
|
* this._address = value.toLocaleLowerCase();
|
|
@@ -173,7 +174,7 @@ export interface PropertyOptions<Owner> {
|
|
|
173
174
|
* Set true to define the properties as getter. (virtual)
|
|
174
175
|
*
|
|
175
176
|
* @example
|
|
176
|
-
* ```
|
|
177
|
+
* ```ts
|
|
177
178
|
* @Property({ getter: true })
|
|
178
179
|
* get fullName() {
|
|
179
180
|
* return this.firstName + this.lastName;
|
|
@@ -186,7 +187,7 @@ export interface PropertyOptions<Owner> {
|
|
|
186
187
|
* to the method name.
|
|
187
188
|
*
|
|
188
189
|
* @example
|
|
189
|
-
* ```
|
|
190
|
+
* ```ts
|
|
190
191
|
* @Property({ getter: true })
|
|
191
192
|
* getFullName() {
|
|
192
193
|
* return this.firstName + this.lastName;
|
|
@@ -194,6 +195,53 @@ export interface PropertyOptions<Owner> {
|
|
|
194
195
|
* ```
|
|
195
196
|
*/
|
|
196
197
|
getterName?: keyof Owner;
|
|
198
|
+
/**
|
|
199
|
+
* When using a private property backed by a public get/set pair, use the `accessor` option to point to the other side.
|
|
200
|
+
*
|
|
201
|
+
* > The `fieldName` will be inferred based on the accessor name unless specified explicitly.
|
|
202
|
+
*
|
|
203
|
+
* If the `accessor` option points to something, the ORM will use the backing property directly.
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* ```ts
|
|
207
|
+
* @Entity()
|
|
208
|
+
* export class User {
|
|
209
|
+
* // the ORM will use the backing field directly
|
|
210
|
+
* @Property({ accessor: 'email' })
|
|
211
|
+
* private _email: string;
|
|
212
|
+
*
|
|
213
|
+
* get email() {
|
|
214
|
+
* return this._email;
|
|
215
|
+
* }
|
|
216
|
+
*
|
|
217
|
+
* set email() {
|
|
218
|
+
* return this._email;
|
|
219
|
+
* }
|
|
220
|
+
* }
|
|
221
|
+
*```
|
|
222
|
+
*
|
|
223
|
+
* If you want to the ORM to use your accessor internally too, use `accessor: true` on the get/set property instead.
|
|
224
|
+
* This is handy if you want to use a native private property for the backing field.
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* ```ts
|
|
228
|
+
* @Entity({ forceConstructor: true })
|
|
229
|
+
* export class User {
|
|
230
|
+
* #email: string;
|
|
231
|
+
*
|
|
232
|
+
* // the ORM will use the accessor internally
|
|
233
|
+
* @Property({ accessor: true })
|
|
234
|
+
* get email() {
|
|
235
|
+
* return this.#email;
|
|
236
|
+
* }
|
|
237
|
+
*
|
|
238
|
+
* set email() {
|
|
239
|
+
* return this.#email;
|
|
240
|
+
* }
|
|
241
|
+
* }
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
244
|
+
accessor?: keyof Owner | AnyString | boolean;
|
|
197
245
|
/**
|
|
198
246
|
* Set to define serialized primary key for MongoDB. (virtual)
|
|
199
247
|
* Alias for `@SerializedPrimaryKey()` decorator.
|
|
@@ -242,6 +290,8 @@ export interface ReferenceOptions<Owner, Target> extends PropertyOptions<Owner>
|
|
|
242
290
|
eager?: boolean;
|
|
243
291
|
/** Override the default loading strategy for this property. This option has precedence over the global `loadStrategy`, but can be overridden by `FindOptions.strategy`. */
|
|
244
292
|
strategy?: LoadStrategy | `${LoadStrategy}`;
|
|
293
|
+
/** Control filter parameters for the relation. This will serve as a default value when processing filters on this relation. It's value can be overridden via `em.fork()` or `FindOptions`. */
|
|
294
|
+
filters?: FilterOptions;
|
|
245
295
|
}
|
|
246
296
|
/**
|
|
247
297
|
* Inspired by https://github.com/typeorm/typeorm/blob/941b584ba135617e55d6685caef671172ec1dc03/src/driver/types/ColumnTypes.ts
|
package/entity/Collection.js
CHANGED
|
@@ -4,6 +4,7 @@ import { ValidationError } from '../errors.js';
|
|
|
4
4
|
import { ReferenceKind, DataloaderType } from '../enums.js';
|
|
5
5
|
import { Reference } from './Reference.js';
|
|
6
6
|
import { helper } from './wrap.js';
|
|
7
|
+
import { QueryHelper } from '../utils/QueryHelper.js';
|
|
7
8
|
export class Collection extends ArrayCollection {
|
|
8
9
|
readonly;
|
|
9
10
|
_populated;
|
|
@@ -32,6 +33,7 @@ export class Collection extends ArrayCollection {
|
|
|
32
33
|
async load(options = {}) {
|
|
33
34
|
if (this.isInitialized(true) && !options.refresh) {
|
|
34
35
|
const em = this.getEntityManager(this.items, false);
|
|
36
|
+
options = { ...options, filters: QueryHelper.mergePropertyFilters(this.property.filters, options.filters) };
|
|
35
37
|
await em?.populate(this.items, options.populate, options);
|
|
36
38
|
this.setSerializationContext(options);
|
|
37
39
|
}
|
|
@@ -217,6 +219,7 @@ export class Collection extends ArrayCollection {
|
|
|
217
219
|
return this;
|
|
218
220
|
}
|
|
219
221
|
const em = this.getEntityManager();
|
|
222
|
+
options = { ...options, filters: QueryHelper.mergePropertyFilters(this.property.filters, options.filters) };
|
|
220
223
|
if (options.dataloader ?? [DataloaderType.ALL, DataloaderType.COLLECTION].includes(em.config.getDataloaderType())) {
|
|
221
224
|
const order = [...this.items]; // copy order of references
|
|
222
225
|
const orderBy = this.createOrderBy(options.orderBy);
|
package/entity/EntityFactory.js
CHANGED
|
@@ -4,6 +4,7 @@ import { EventType, ReferenceKind } from '../enums.js';
|
|
|
4
4
|
import { Reference } from './Reference.js';
|
|
5
5
|
import { helper } from './wrap.js';
|
|
6
6
|
import { EntityHelper } from './EntityHelper.js';
|
|
7
|
+
import { JsonType } from '../types/JsonType.js';
|
|
7
8
|
export class EntityFactory {
|
|
8
9
|
em;
|
|
9
10
|
driver;
|
|
@@ -73,7 +74,9 @@ export class EntityFactory {
|
|
|
73
74
|
if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && Utils.isPlainObject(data[prop.name])) {
|
|
74
75
|
data[prop.name] = Utils.getPrimaryKeyValues(data[prop.name], prop.targetMeta, true);
|
|
75
76
|
}
|
|
76
|
-
|
|
77
|
+
if (prop.customType instanceof JsonType && this.platform.convertsJsonAutomatically()) {
|
|
78
|
+
data[prop.name] = prop.customType.convertToDatabaseValue(data[prop.name], this.platform, { key: prop.name, mode: 'hydration' });
|
|
79
|
+
}
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
82
|
}
|
|
@@ -157,6 +160,7 @@ export class EntityFactory {
|
|
|
157
160
|
this.create(prop.type, data[prop.name], options); // we can ignore the value, we just care about the `mergeData` call
|
|
158
161
|
}
|
|
159
162
|
});
|
|
163
|
+
this.unitOfWork.normalizeEntityData(meta, originalEntityData);
|
|
160
164
|
helper(entity).__touched = false;
|
|
161
165
|
}
|
|
162
166
|
createReference(entityName, id, options = {}) {
|
|
@@ -244,10 +248,10 @@ export class EntityFactory {
|
|
|
244
248
|
}
|
|
245
249
|
hydrate(entity, meta, data, options) {
|
|
246
250
|
if (options.initialized) {
|
|
247
|
-
this.hydrator.hydrate(entity, meta, data, this, 'full', options.newEntity, options.convertCustomTypes, options.schema, this.driver.getSchemaName(meta, options));
|
|
251
|
+
this.hydrator.hydrate(entity, meta, data, this, 'full', options.newEntity, options.convertCustomTypes, options.schema, this.driver.getSchemaName(meta, options), options.normalizeAccessors);
|
|
248
252
|
}
|
|
249
253
|
else {
|
|
250
|
-
this.hydrator.hydrateReference(entity, meta, data, this, options.convertCustomTypes, options.schema, this.driver.getSchemaName(meta, options));
|
|
254
|
+
this.hydrator.hydrateReference(entity, meta, data, this, options.convertCustomTypes, options.schema, this.driver.getSchemaName(meta, options), options.normalizeAccessors);
|
|
251
255
|
}
|
|
252
256
|
Utils.keys(data).forEach(key => {
|
|
253
257
|
helper(entity)?.__loadedProperties.add(key);
|
package/entity/EntityHelper.js
CHANGED
|
@@ -87,7 +87,7 @@ export class EntityHelper {
|
|
|
87
87
|
});
|
|
88
88
|
return;
|
|
89
89
|
}
|
|
90
|
-
if (prop.inherited || prop.primary || prop.persist === false || prop.trackChanges === false || prop.embedded || isCollection) {
|
|
90
|
+
if (prop.inherited || prop.primary || prop.accessor || prop.persist === false || prop.trackChanges === false || prop.embedded || isCollection) {
|
|
91
91
|
return;
|
|
92
92
|
}
|
|
93
93
|
Object.defineProperty(meta.prototype, prop.name, {
|
|
@@ -113,7 +113,18 @@ export class EntityHelper {
|
|
|
113
113
|
static defineCustomInspect(meta) {
|
|
114
114
|
// @ts-ignore
|
|
115
115
|
meta.prototype[inspect.custom] ??= function (depth = 2) {
|
|
116
|
-
const object = {
|
|
116
|
+
const object = {};
|
|
117
|
+
const keys = new Set(Utils.keys(this)); // .sort((a, b) => (meta.propertyOrder.get(a) ?? 0) - (meta.propertyOrder.get(b) ?? 0));
|
|
118
|
+
for (const prop of meta.props) {
|
|
119
|
+
if (keys.has(prop.name) || (prop.getter && prop.accessor === prop.name)) {
|
|
120
|
+
object[prop.name] = this[prop.name];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
for (const key of keys) {
|
|
124
|
+
if (!meta.properties[key]) {
|
|
125
|
+
object[key] = this[key];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
117
128
|
// ensure we dont have internal symbols in the POJO
|
|
118
129
|
[OptionalProps, EntityRepositoryType, PrimaryKeyProp, EagerProps, HiddenProps].forEach(sym => delete object[sym]);
|
|
119
130
|
meta.props
|
|
@@ -172,6 +183,10 @@ export class EntityHelper {
|
|
|
172
183
|
continue;
|
|
173
184
|
}
|
|
174
185
|
const inverse = value?.[prop2.name];
|
|
186
|
+
if (prop.ref && owner[prop.name]) {
|
|
187
|
+
// eslint-disable-next-line dot-notation
|
|
188
|
+
owner[prop.name]['property'] = prop;
|
|
189
|
+
}
|
|
175
190
|
if (Utils.isCollection(inverse) && inverse.isPartial()) {
|
|
176
191
|
continue;
|
|
177
192
|
}
|
package/entity/EntityLoader.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { AnyEntity, ConnectionType,
|
|
1
|
+
import type { AnyEntity, ConnectionType, EntityProperty, FilterQuery, 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 { EntityField } from '../drivers/IDatabaseDriver.js';
|
|
4
|
+
import type { EntityField, FilterOptions } from '../drivers/IDatabaseDriver.js';
|
|
5
5
|
import type { LoggingOptions } from '../logging/Logger.js';
|
|
6
6
|
export type EntityLoaderOptions<Entity, Fields extends string = PopulatePath.ALL, Excludes extends string = never> = {
|
|
7
7
|
where?: FilterQuery<Entity>;
|
|
@@ -14,7 +14,7 @@ export type EntityLoaderOptions<Entity, Fields extends string = PopulatePath.ALL
|
|
|
14
14
|
lookup?: boolean;
|
|
15
15
|
convertCustomTypes?: boolean;
|
|
16
16
|
ignoreLazyScalarProperties?: boolean;
|
|
17
|
-
filters?:
|
|
17
|
+
filters?: FilterOptions;
|
|
18
18
|
strategy?: LoadStrategy;
|
|
19
19
|
lockMode?: Exclude<LockMode, LockMode.OPTIMISTIC>;
|
|
20
20
|
schema?: string;
|
package/entity/EntityLoader.js
CHANGED
|
@@ -32,7 +32,6 @@ export class EntityLoader {
|
|
|
32
32
|
const visited = options.visited ??= new Set();
|
|
33
33
|
options.where ??= {};
|
|
34
34
|
options.orderBy ??= {};
|
|
35
|
-
options.filters ??= {};
|
|
36
35
|
options.lookup ??= true;
|
|
37
36
|
options.validate ??= true;
|
|
38
37
|
options.refresh ??= false;
|
|
@@ -210,7 +209,7 @@ export class EntityLoader {
|
|
|
210
209
|
}
|
|
211
210
|
}
|
|
212
211
|
async findChildren(entities, prop, populate, options, ref) {
|
|
213
|
-
const children = this.getChildReferences(entities, prop, options, ref);
|
|
212
|
+
const children = Utils.unique(this.getChildReferences(entities, prop, options, ref));
|
|
214
213
|
const meta = prop.targetMeta;
|
|
215
214
|
let fk = Utils.getPrimaryKeyHash(meta.primaryKeys);
|
|
216
215
|
let schema = options.schema;
|
|
@@ -270,6 +269,24 @@ export class EntityLoader {
|
|
|
270
269
|
// @ts-ignore not a public option, will be propagated to the populate call
|
|
271
270
|
visited: options.visited,
|
|
272
271
|
});
|
|
272
|
+
if ([ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind) && items.length !== children.length) {
|
|
273
|
+
const nullVal = this.em.config.get('forceUndefined') ? undefined : null;
|
|
274
|
+
const itemsMap = new Set();
|
|
275
|
+
const childrenMap = new Set();
|
|
276
|
+
for (const item of items) {
|
|
277
|
+
itemsMap.add(helper(item).getSerializedPrimaryKey());
|
|
278
|
+
}
|
|
279
|
+
for (const child of children) {
|
|
280
|
+
childrenMap.add(helper(child).getSerializedPrimaryKey());
|
|
281
|
+
}
|
|
282
|
+
for (const entity of entities) {
|
|
283
|
+
const key = helper(entity[prop.name] ?? {})?.getSerializedPrimaryKey();
|
|
284
|
+
if (childrenMap.has(key) && !itemsMap.has(key)) {
|
|
285
|
+
entity[prop.name] = nullVal;
|
|
286
|
+
helper(entity).__originalEntityData[prop.name] = null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
273
290
|
for (const item of items) {
|
|
274
291
|
if (ref && !helper(item).__onLoadFired) {
|
|
275
292
|
helper(item).__initialized = false;
|
|
@@ -293,6 +310,7 @@ export class EntityLoader {
|
|
|
293
310
|
if (prop.kind === ReferenceKind.SCALAR && !prop.lazy) {
|
|
294
311
|
return;
|
|
295
312
|
}
|
|
313
|
+
options = { ...options, filters: QueryHelper.mergePropertyFilters(prop.filters, options.filters) };
|
|
296
314
|
const populated = await this.populateMany(entityName, entities, populate, options);
|
|
297
315
|
if (!populate.children && !populate.all) {
|
|
298
316
|
return;
|
package/entity/Reference.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { AddEager, AddOptional, Dictionary, EntityClass, EntityKey, EntityP
|
|
|
3
3
|
import type { FindOneOptions, FindOneOrFailOptions } from '../drivers/IDatabaseDriver.js';
|
|
4
4
|
export declare class Reference<T extends object> {
|
|
5
5
|
private entity;
|
|
6
|
+
private property?;
|
|
6
7
|
constructor(entity: T);
|
|
7
8
|
static create<T extends object>(entity: T | Ref<T>): Ref<T>;
|
|
8
9
|
static createFromPK<T extends object>(entityType: EntityClass<T>, pk: Primary<T>, options?: {
|