@mikro-orm/core 7.1.16-dev.1 → 7.1.16-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.js CHANGED
@@ -383,6 +383,7 @@ export class EntityManager {
383
383
  filters,
384
384
  populate: hint.children,
385
385
  populateWhere: PopulateHint.ALL,
386
+ populateFilter: undefined,
386
387
  });
387
388
  if (Utils.hasObjectKeys(where)) {
388
389
  ret[field] = ret[field] ? { $and: [where, ret[field]] } : where;
@@ -184,8 +184,14 @@ export class DatabaseDriver {
184
184
  }, {});
185
185
  return { [prop]: value };
186
186
  }
187
- const desc = direction === QueryOrderNumeric.DESC || direction.toString().toLowerCase() === 'desc';
188
- const dir = Utils.xor(desc, isLast) ? 'desc' : 'asc';
187
+ const dirStr = direction.toString().toLowerCase();
188
+ const desc = direction === QueryOrderNumeric.DESC || dirStr.startsWith('desc');
189
+ let dir = Utils.xor(desc, isLast) ? 'desc' : 'asc';
190
+ const nullsFirst = dirStr.includes('nulls first');
191
+ // backward pagination reverses the whole ordering, so the nulls placement flips with the direction
192
+ if (nullsFirst || dirStr.includes('nulls last')) {
193
+ dir += Utils.xor(nullsFirst, isLast) ? ' nulls first' : ' nulls last';
194
+ }
189
195
  return { [prop]: dir };
190
196
  };
191
197
  // the cursor condition is created at the driver level, after the EM already converted custom types
@@ -251,10 +257,12 @@ export class DatabaseDriver {
251
257
  Object.assign(o, createCondition(key, direction[key], offset?.[key], eq, `${path}.${key}`, childProps ?? {}, insideJson));
252
258
  return o;
253
259
  }, {});
254
- return { [prop]: value };
260
+ // an unconstrained group must stay unconstrained, an empty object condition would instead
261
+ // match only rows where every child value is null (e.g. object embeddables)
262
+ return Utils.hasObjectKeys(value) ? { [prop]: value } : {};
255
263
  }
256
- const isDesc = direction === QueryOrderNumeric.DESC || direction.toString().toLowerCase() === 'desc';
257
264
  const dirStr = direction.toString().toLowerCase();
265
+ const isDesc = direction === QueryOrderNumeric.DESC || dirStr.startsWith('desc');
258
266
  let nullsFirst;
259
267
  if (dirStr.includes('nulls first')) {
260
268
  nullsFirst = true;
@@ -274,13 +282,14 @@ export class DatabaseDriver {
274
282
  offset = this.mapCursorOffset(propMeta, offset, insideJson);
275
283
  // Handle null offset (intentional null cursor value)
276
284
  if (offset === null) {
285
+ // hasItemsAfterNull: forward + nullsFirst, or backward + nullsLast
286
+ const hasItemsAfterNull = Utils.xor(nullsFirst, inverse);
277
287
  if (eq) {
278
- // Equal to null
279
- return { [prop]: null };
288
+ // the `>=` half of the keyset condition: every row when the nulls come first in this
289
+ // direction, only the null ones when they come last
290
+ return hasItemsAfterNull ? {} : { [prop]: null };
280
291
  }
281
292
  // Strict comparison with null cursor value
282
- // hasItemsAfterNull: forward + nullsFirst, or backward + nullsLast
283
- const hasItemsAfterNull = Utils.xor(nullsFirst, inverse);
284
293
  if (hasItemsAfterNull) {
285
294
  return { [prop]: { $ne: null } };
286
295
  }
@@ -422,8 +431,8 @@ export class DatabaseDriver {
422
431
  const props = prop.embeddedProps;
423
432
  let unknownProp = false;
424
433
  Object.keys(data[prop.name]).forEach(kk => {
425
- // explicitly allow `$exists`, `$eq`, `$ne` and `$elemMatch` operators here as they can't be misused this way
426
- const operator = Object.keys(data[prop.name]).some(f => Utils.isOperator(f) && !['$exists', '$ne', '$eq', '$elemMatch'].includes(f));
434
+ // explicitly allow `$exists`, `$eq`, `$ne`, `$elemMatch` and `$all` operators here as they can't be misused this way
435
+ const operator = Object.keys(data[prop.name]).some(f => Utils.isOperator(f) && !['$exists', '$ne', '$eq', '$elemMatch', '$all'].includes(f));
427
436
  if (operator) {
428
437
  throw ValidationError.cannotUseOperatorsInsideEmbeddables(meta.class, prop.name, data);
429
438
  }
@@ -1,4 +1,4 @@
1
- import type { AnyEntity, AutoPath, ConnectionType, EntityName, EntityProperty, FilterQuery, PopulateHintOptions, PopulateOptions } from '../typings.js';
1
+ import type { AnyEntity, AutoPath, ConnectionType, EntityName, EntityProperty, FilterQuery, ObjectQuery, PopulateHintOptions, PopulateOptions } from '../typings.js';
2
2
  import type { EntityManager } from '../EntityManager.js';
3
3
  import { LoadStrategy, type LockMode, type PopulateHint, PopulatePath, type QueryOrderMap } from '../enums.js';
4
4
  import type { InflightQueryAbortStrategy } from '../connections/Connection.js';
@@ -14,6 +14,8 @@ export interface EntityLoaderOptions<Entity, Fields extends string = never, Excl
14
14
  where?: FilterQuery<Entity>;
15
15
  /** Controls how `where` conditions are applied to populated relations. */
16
16
  populateWhere?: PopulateHint | `${PopulateHint}`;
17
+ /** @see FindOptions.populateFilter */
18
+ populateFilter?: ObjectQuery<Entity>;
17
19
  /** Ordering for populated relations. */
18
20
  orderBy?: QueryOrderMap<Entity> | QueryOrderMap<Entity>[];
19
21
  /** Whether to reload already loaded entities. */
@@ -77,6 +79,10 @@ export declare class EntityLoader {
77
79
  /** @internal */
78
80
  findChildrenFromPivotTable<Entity extends object>(filtered: Entity[], prop: EntityProperty<Entity>, options: Required<EntityLoaderOptions<Entity>>, orderBy?: QueryOrderMap<Entity>[], populate?: PopulateOptions<Entity>, pivotJoin?: boolean): Promise<AnyEntity[][]>;
79
81
  private extractChildCondition;
82
+ /** Extracts the part of `options.populateFilter` that applies to the given relation. */
83
+ private extractChildPopulateFilter;
84
+ /** Splits an extracted populate filter into the conditions on the populated entity and those on its own relations. */
85
+ private splitPopulateFilter;
80
86
  private buildFields;
81
87
  private getChildReferences;
82
88
  private filterCollections;
@@ -334,7 +334,7 @@ export class EntityLoader {
334
334
  // When targetKey is set, use it for FK lookup instead of the PK
335
335
  let fk = prop.targetKey ?? Utils.getPrimaryKeyHash(meta.primaryKeys);
336
336
  let schema = options.schema;
337
- const partial = !Utils.isEmpty(prop.where) || !Utils.isEmpty(options.where);
337
+ const partial = !Utils.isEmpty(prop.where) || !Utils.isEmpty(options.where) || !Utils.isEmpty(options.populateFilter);
338
338
  let polymorphicOwnerProp;
339
339
  const ownerProp = prop.kind === ReferenceKind.ONE_TO_MANY || (prop.kind === ReferenceKind.MANY_TO_MANY && !prop.owner)
340
340
  ? meta.properties[prop.mappedBy]
@@ -394,12 +394,20 @@ export class EntityLoader {
394
394
  if (!Utils.isEmpty(prop.where) || Raw.hasObjectFragments(prop.where)) {
395
395
  where = { $and: [where, prop.where] };
396
396
  }
397
+ const childFilter = options.populateFilter ? await this.extractChildPopulateFilter(options, prop) : undefined;
398
+ // conditions on the populated entity itself have no sink in the child query (the joined strategy puts
399
+ // them on the join), only the nested relation ones can be forwarded as its own `populateFilter`
400
+ const [ownFilter, nestedFilter] = this.splitPopulateFilter(childFilter, meta);
401
+ if (ownFilter) {
402
+ where = { $and: [where, ownFilter] };
403
+ }
397
404
  const orderBy = QueryHelper.mergeOrderBy(options.orderBy, prop.orderBy);
398
405
  const findOptions = {
399
406
  filters,
400
407
  convertCustomTypes,
401
408
  lockMode,
402
409
  populateWhere,
410
+ populateFilter: nestedFilter,
403
411
  logging,
404
412
  orderBy,
405
413
  populate: populate.children ?? populate.all ?? [],
@@ -575,6 +583,9 @@ export class EntityLoader {
575
583
  filters,
576
584
  ignoreLazyScalarProperties,
577
585
  populateWhere,
586
+ populateFilter: options.populateFilter
587
+ ? (await this.extractChildPopulateFilter(options, prop))
588
+ : undefined,
578
589
  connectionType,
579
590
  logging,
580
591
  schema,
@@ -611,7 +622,7 @@ export class EntityLoader {
611
622
  const fields = this.buildFields(options.fields, prop);
612
623
  // oxfmt-ignore
613
624
  const exclude = Array.isArray(options.exclude) ? Utils.extractChildElements(options.exclude, prop.name) : options.exclude;
614
- const populateFilter = options.populateFilter?.[prop.name];
625
+ const populateFilter = options.populateFilter ? await this.extractChildPopulateFilter(options, prop) : undefined;
615
626
  const options2 = { ...options, fields, exclude, populateFilter };
616
627
  ['limit', 'offset', 'first', 'last', 'before', 'after', 'overfetch'].forEach(prop => delete options2[prop]);
617
628
  options2.populate = populate?.children ?? [];
@@ -656,7 +667,8 @@ export class EntityLoader {
656
667
  }
657
668
  async extractChildCondition(options, prop, filters = false) {
658
669
  const where = options.where;
659
- const subCond = Utils.isPlainObject(where[prop.name]) ? where[prop.name] : {};
670
+ // shallow copy, the operator normalization below must not mutate the caller's condition
671
+ const subCond = Utils.isPlainObject(where[prop.name]) ? { ...where[prop.name] } : {};
660
672
  const meta2 = prop.targetMeta;
661
673
  const pk = Utils.getPrimaryKeyHash(meta2.primaryKeys);
662
674
  ['$and', '$or'].forEach(op => {
@@ -679,8 +691,8 @@ export class EntityLoader {
679
691
  });
680
692
  const operators = Object.keys(subCond).filter(key => Utils.isOperator(key, false));
681
693
  if (operators.length > 0) {
694
+ subCond[pk] = Utils.isPlainObject(subCond[pk]) ? { ...subCond[pk] } : (subCond[pk] ?? {});
682
695
  operators.forEach(op => {
683
- subCond[pk] ??= {};
684
696
  subCond[pk][op] = subCond[op];
685
697
  delete subCond[op];
686
698
  });
@@ -690,6 +702,24 @@ export class EntityLoader {
690
702
  }
691
703
  return subCond;
692
704
  }
705
+ /** Extracts the part of `options.populateFilter` that applies to the given relation. */
706
+ async extractChildPopulateFilter(options, prop) {
707
+ const filter = await this.extractChildCondition({ ...options, where: options.populateFilter }, prop);
708
+ return Utils.isEmpty(filter) ? undefined : filter;
709
+ }
710
+ /** Splits an extracted populate filter into the conditions on the populated entity and those on its own relations. */
711
+ splitPopulateFilter(filter, meta) {
712
+ if (!filter) {
713
+ return [undefined, undefined];
714
+ }
715
+ const own = {};
716
+ const nested = {};
717
+ for (const key of Object.keys(filter)) {
718
+ const target = meta.relations.some(rel => rel.name === key) ? nested : own;
719
+ target[key] = filter[key];
720
+ }
721
+ return [Utils.isEmpty(own) ? undefined : own, Utils.isEmpty(nested) ? undefined : nested];
722
+ }
693
723
  buildFields(fields = [], prop, ref) {
694
724
  if (ref) {
695
725
  fields = prop.targetMeta.primaryKeys.map(targetPkName => `${prop.name}.${targetPkName}`);
@@ -22,7 +22,7 @@ type HasKind<Options, K extends string> = Options extends {
22
22
  kind: infer X extends string;
23
23
  } ? X extends K ? true : false : false;
24
24
  /** Lightweight chain result type for property builders - reduces type instantiation cost by avoiding full class resolution. */
25
- export interface PropertyChain<Value, Options> {
25
+ export interface PropertyChain<in out Value, in out Options> {
26
26
  '~type'?: {
27
27
  value: Value;
28
28
  };
@@ -180,7 +180,7 @@ export interface PropertyChain<Value, Options> {
180
180
  foreignKeyName(foreignKeyName: string): HasKind<Options, 'm:1' | '1:m' | '1:1' | 'm:n'> extends true ? PropertyChain<Value, Options> : never;
181
181
  }
182
182
  /** @internal */
183
- export declare class UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys extends BuilderKeys> implements Record<Exclude<UniversalPropertyKeys, ExcludeKeys>, any> {
183
+ export declare class UniversalPropertyOptionsBuilder<in out Value, in out Options, in out IncludeKeys extends BuilderKeys> implements Record<Exclude<UniversalPropertyKeys, ExcludeKeys>, any> {
184
184
  '~options': Options;
185
185
  '~type'?: {
186
186
  value: Value;
@@ -726,16 +726,18 @@ type InferTypeByString<T extends string> = T extends keyof typeof types ? InferJ
726
726
  type InferJSType<T> = T extends typeof Type<infer TValue, any> ? NonNullable<TValue> : never;
727
727
  type InferColumnType<T extends string> = T extends 'int' | 'int4' | 'integer' | 'bigint' | 'int8' | 'int2' | 'tinyint' | 'smallint' | 'mediumint' ? number : T extends 'double' | 'double precision' | 'real' | 'float8' | 'decimal' | 'numeric' | 'float' | 'float4' ? number : T extends 'datetime' | 'time' | 'time with time zone' | 'timestamp' | 'timestamp with time zone' | 'timetz' | 'timestamptz' | 'date' | 'interval' ? Date : T extends 'ObjectId' | 'objectId' | 'character varying' | 'varchar' | 'char' | 'character' | 'uuid' | 'text' | 'tinytext' | 'mediumtext' | 'longtext' | 'enum' ? string : T extends 'boolean' | 'bool' | 'bit' ? boolean : T extends 'blob' | 'tinyblob' | 'mediumblob' | 'longblob' | 'bytea' ? Buffer : T extends 'point' | 'line' | 'lseg' | 'box' | 'circle' | 'path' | 'polygon' | 'geometry' ? number[] : T extends 'tsvector' | 'tsquery' ? string[] : T extends 'json' | 'jsonb' ? any : any;
728
728
  type BaseEntityMethodKeys = 'toObject' | 'toPOJO' | 'serialize' | 'assign' | 'populate' | 'init' | 'toReference';
729
+ interface BaseEntityMethods<in out Entity extends object> extends Pick<IWrappedEntity<Entity>, BaseEntityMethodKeys> {
730
+ }
729
731
  /** Infers the entity type from a `defineEntity()` properties map, resolving builders, base classes, and primary keys. */
730
732
  export type InferEntityFromProperties<Properties extends Record<string, any>, PK extends (keyof Properties)[] | undefined = undefined, Base = never, Repository = never, ForceObject extends boolean = false, BaseDiscriminatorColumn extends string | undefined = undefined, DiscriminatorValue extends string | number | undefined = undefined, Embeddable extends boolean = false> = (IsNever<Base> extends true ? {} : Base extends {
731
733
  toObject(...args: any[]): any;
732
- } ? Pick<IWrappedEntity<{
734
+ } ? BaseEntityMethods<{
733
735
  -readonly [K in keyof Properties]: InferBuilderValue<MaybeReturnType<Properties[K]>>;
734
736
  } & {
735
737
  [PrimaryKeyProp]?: InferCombinedPrimaryKey<Properties, PK, Base>;
736
738
  } & (IsNever<Repository> extends true ? {} : {
737
739
  [EntityRepositoryType]?: Repository extends Constructor<infer R> ? R : Repository;
738
- }) & NarrowDiscriminator<Omit<Base, typeof PrimaryKeyProp>, BaseDiscriminatorColumn, DiscriminatorValue, Embeddable>>, BaseEntityMethodKeys> : {}) & {
740
+ }) & NarrowDiscriminator<Omit<Base, typeof PrimaryKeyProp>, BaseDiscriminatorColumn, DiscriminatorValue, Embeddable>> : {}) & {
739
741
  -readonly [K in keyof Properties]: InferBuilderValue<MaybeReturnType<Properties[K]>>;
740
742
  } & {
741
743
  [PrimaryKeyProp]?: InferCombinedPrimaryKey<Properties, PK, Base>;
package/enums.d.ts CHANGED
@@ -39,6 +39,8 @@ export declare enum QueryOperator {
39
39
  $in = "in",
40
40
  /** Not included in the given list. */
41
41
  $nin = "not in",
42
+ /** Contains all of the given values, supported on collection properties and on mongo arrays. */
43
+ $all = "all",
42
44
  /** Greater than. */
43
45
  $gt = ">",
44
46
  /** Greater than or equal to. */
package/enums.js CHANGED
@@ -41,6 +41,8 @@ export var QueryOperator;
41
41
  QueryOperator["$in"] = "in";
42
42
  /** Not included in the given list. */
43
43
  QueryOperator["$nin"] = "not in";
44
+ /** Contains all of the given values, supported on collection properties and on mongo arrays. */
45
+ QueryOperator["$all"] = "all";
44
46
  /** Greater than. */
45
47
  QueryOperator["$gt"] = ">";
46
48
  /** Greater than or equal to. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/core",
3
- "version": "7.1.16-dev.1",
3
+ "version": "7.1.16-dev.10",
4
4
  "description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
5
5
  "keywords": [
6
6
  "data-mapper",
package/typings.d.ts CHANGED
@@ -314,6 +314,7 @@ export type OperatorMap<T> = {
314
314
  $ne?: ExpandScalar<T> | readonly ExpandScalar<T>[] | Subquery;
315
315
  $in?: readonly ExpandScalar<T>[] | readonly Primary<T>[] | Raw | Subquery;
316
316
  $nin?: readonly ExpandScalar<T>[] | readonly Primary<T>[] | Raw | Subquery;
317
+ $all?: readonly ExpandQuery<T>[];
317
318
  $not?: ExpandQuery<T>;
318
319
  $none?: ExpandQuery<T>;
319
320
  $some?: ExpandQuery<T>;
package/utils/Utils.js CHANGED
@@ -153,7 +153,7 @@ export function parseJsonSafe(value) {
153
153
  /** Collection of general-purpose utility methods used throughout the ORM. */
154
154
  export class Utils {
155
155
  static PK_SEPARATOR = '~~~';
156
- static #ORM_VERSION = '7.1.16-dev.1';
156
+ static #ORM_VERSION = '7.1.16-dev.10';
157
157
  /**
158
158
  * Checks if the argument is instance of `Object`. Returns false for arrays.
159
159
  */
@@ -854,7 +854,11 @@ export class Utils {
854
854
  return await import(module);
855
855
  }
856
856
  catch (err) {
857
- if (warning && err.code === 'ERR_MODULE_NOT_FOUND') {
857
+ // only a missing module is expected here, anything else is a real failure inside the module
858
+ if (!['ERR_MODULE_NOT_FOUND', 'MODULE_NOT_FOUND'].includes(err.code)) {
859
+ throw err;
860
+ }
861
+ if (warning) {
858
862
  // eslint-disable-next-line no-console
859
863
  console.warn(warning);
860
864
  }