@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.
Files changed (40) hide show
  1. package/EntityManager.d.ts +12 -9
  2. package/EntityManager.js +77 -52
  3. package/README.md +2 -0
  4. package/decorators/Property.d.ts +53 -3
  5. package/entity/Collection.js +3 -0
  6. package/entity/EntityFactory.d.ts +1 -0
  7. package/entity/EntityFactory.js +7 -3
  8. package/entity/EntityHelper.js +17 -2
  9. package/entity/EntityLoader.d.ts +3 -3
  10. package/entity/EntityLoader.js +20 -2
  11. package/entity/Reference.d.ts +1 -0
  12. package/entity/Reference.js +10 -4
  13. package/entity/defineEntity.d.ts +12 -8
  14. package/entity/defineEntity.js +9 -2
  15. package/hydration/ObjectHydrator.d.ts +4 -4
  16. package/hydration/ObjectHydrator.js +25 -22
  17. package/index.d.ts +1 -1
  18. package/metadata/EntitySchema.d.ts +2 -2
  19. package/metadata/MetadataDiscovery.d.ts +1 -0
  20. package/metadata/MetadataDiscovery.js +37 -3
  21. package/naming-strategy/AbstractNamingStrategy.d.ts +5 -1
  22. package/naming-strategy/AbstractNamingStrategy.js +7 -1
  23. package/naming-strategy/NamingStrategy.d.ts +11 -1
  24. package/package.json +2 -2
  25. package/platforms/Platform.js +1 -1
  26. package/serialization/EntitySerializer.js +1 -1
  27. package/serialization/EntityTransformer.js +1 -1
  28. package/serialization/SerializationContext.js +1 -1
  29. package/typings.d.ts +11 -4
  30. package/unit-of-work/ChangeSetPersister.js +16 -5
  31. package/unit-of-work/UnitOfWork.d.ts +6 -0
  32. package/unit-of-work/UnitOfWork.js +37 -23
  33. package/utils/Configuration.d.ts +4 -0
  34. package/utils/Configuration.js +10 -0
  35. package/utils/EntityComparator.js +11 -1
  36. package/utils/QueryHelper.d.ts +3 -1
  37. package/utils/QueryHelper.js +18 -0
  38. package/utils/RawQueryFragment.d.ts +2 -2
  39. package/utils/TransactionManager.js +0 -2
  40. package/utils/Utils.js +2 -2
@@ -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>], enabled?: boolean): void;
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>], enabled?: boolean): void;
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>], enabled?: boolean): void;
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>[], enabled?: boolean): void;
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>, cond: ObjectQuery<Entity>, options: FindOptions<Entity, any, any, any> | FindOneOptions<Entity, any, any, any>): Promise<ObjectQuery<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>): Promise<void>;
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: Dictionary<boolean | Dictionary> | string[] | boolean, type: 'read' | 'update' | 'delete', findOptions?: FindOptions<any, any, any, any> | FindOneOptions<any, any, any, any>): Promise<FilterQuery<Entity> | undefined>;
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, { ...where }, options);
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, { ...where }, options);
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, enabled = true) {
263
- const options = { name, cond, default: enabled };
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, cond, options) {
342
+ async getJoinedFilters(meta, options) {
343
+ if (!this.config.get('filtersOnRelations') || !options.populate) {
344
+ return undefined;
345
+ }
343
346
  const ret = {};
344
- if (options.populate) {
345
- for (const hint of options.populate) {
346
- const field = hint.field.split(':')[0];
347
- const prop = meta.properties[field];
348
- const strategy = getLoadingStrategy(prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'), prop.kind);
349
- const joined = strategy === LoadStrategy.JOINED && prop.kind !== ReferenceKind.SCALAR;
350
- if (!joined && !hint.filter) {
351
- continue;
352
- }
353
- const where = await this.applyFilters(prop.type, {}, options.filters ?? {}, 'read', { ...options, populate: hint.children });
354
- const where2 = await this.getJoinedFilters(prop.targetMeta, {}, { ...options, populate: hint.children, populateWhere: PopulateHint.ALL });
355
- if (Utils.hasObjectKeys(where)) {
356
- ret[field] = ret[field] ? { $and: [where, ret[field]] } : where;
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
- if (Utils.hasObjectKeys(where2)) {
359
- if (ret[field]) {
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 props) {
383
- const cond = await this.applyFilters(prop.type, {}, options.filters ?? {}, 'read', options);
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
- if (populated.length > 0) {
388
- for (const hint of populated) {
389
- if (!hint.all) {
390
- hint.filter = true;
391
- found = true;
392
- }
393
- else if (hint.field === `${prop.name}:ref`) {
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
- ret.push(QueryHelper.processWhere({
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: helper(entity).__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 = this.comparator.prepareEntity(e);
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, helper(entity).__meta, data, em.entityFactory, 'full', false, true);
579
- helper(entity).__originalEntityData = this.comparator.prepareEntity(reloaded);
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, { ...where }, options);
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, { ...where }, options);
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).
@@ -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
@@ -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);
@@ -15,6 +15,7 @@ export interface FactoryOptions {
15
15
  recomputeSnapshot?: boolean;
16
16
  schema?: string;
17
17
  parentSchema?: string;
18
+ normalizeAccessors?: boolean;
18
19
  }
19
20
  export declare class EntityFactory {
20
21
  private readonly em;
@@ -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
- data[prop.name] = prop.customType.convertToDatabaseValue(data[prop.name], this.platform, { key: prop.name, mode: 'hydration' });
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);
@@ -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 = { ...this };
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
  }
@@ -1,7 +1,7 @@
1
- import type { AnyEntity, ConnectionType, Dictionary, EntityProperty, FilterQuery, PopulateOptions } from '../typings.js';
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?: Dictionary<boolean | Dictionary> | string[] | boolean;
17
+ filters?: FilterOptions;
18
18
  strategy?: LoadStrategy;
19
19
  lockMode?: Exclude<LockMode, LockMode.OPTIMISTIC>;
20
20
  schema?: string;
@@ -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;
@@ -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?: {