@mikro-orm/core 7.2.0-dev.17 → 7.2.0-dev.19

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
@@ -512,6 +512,7 @@ export class EntityManager {
512
512
  filters,
513
513
  populate: hint.children,
514
514
  populateWhere: PopulateHint.ALL,
515
+ populateFilter: undefined,
515
516
  });
516
517
  if (Utils.hasObjectKeys(where)) {
517
518
  ret[field] = ret[field] ? { $and: [where, ret[field]] } : where;
@@ -66,9 +66,9 @@ export declare abstract class DatabaseDriver<C extends Connection> implements ID
66
66
  };
67
67
  /**
68
68
  * Resolves a leaf `orderBy` direction into the two flags the rewritten `orderBy` and the cursor
69
- * condition have to agree on, or pagination skips rows at the null boundary. Platforms that cannot
70
- * order nulls explicitly sort them as the lowest value, elsewhere the requested placement wins,
71
- * defaulting to nulls last for `asc` and nulls first for `desc`.
69
+ * condition have to agree on, or pagination skips rows at the null boundary. A placement the caller
70
+ * asked for wins where the platform can honor it, anything else follows the platform's own default,
71
+ * which is what the untouched `orderBy` will get.
72
72
  */
73
73
  private parseCursorDirection;
74
74
  /**
@@ -182,22 +182,21 @@ export class DatabaseDriver {
182
182
  if (limit != null) {
183
183
  options.limit = limit + (overfetch ? 1 : 0);
184
184
  }
185
- const createOrderBy = (prop, direction, properties = meta.properties, nullable = false) => {
185
+ const createOrderBy = (prop, direction, properties = meta.properties) => {
186
186
  const propMeta = properties[prop];
187
- // a nullable relation or embeddable makes its joined columns null too
188
- nullable ||= !!propMeta?.nullable;
189
187
  if (Utils.isPlainObject(direction)) {
190
188
  const childProps = propMeta?.kind === ReferenceKind.EMBEDDED ? propMeta.embeddedProps : propMeta?.targetMeta?.properties;
191
189
  const value = Utils.getObjectQueryKeys(direction).reduce((o, key) => {
192
- Object.assign(o, createOrderBy(key, direction[key], childProps ?? {}, nullable));
190
+ Object.assign(o, createOrderBy(key, direction[key], childProps ?? {}));
193
191
  return o;
194
192
  }, {});
195
193
  return { [prop]: value };
196
194
  }
197
- const { desc, nullsFirst } = this.parseCursorDirection(direction);
195
+ const { desc, nullsFirst, explicit } = this.parseCursorDirection(direction);
198
196
  const dir = Utils.xor(desc, isLast) ? 'desc' : 'asc';
199
- // the condition assumes a placement, spell it out instead of taking the database default
200
- if (nullable && this.platform.supportsNullsOrdering()) {
197
+ // only a requested placement is spelled out, an unqualified direction already lands where the
198
+ // condition expects it; backward pagination reverses the ordering, so the placement flips too
199
+ if (explicit) {
201
200
  const nulls = Utils.xor(nullsFirst, isLast) ? 'first' : 'last';
202
201
  return { [prop]: `${dir} nulls ${nulls}` };
203
202
  }
@@ -219,23 +218,19 @@ export class DatabaseDriver {
219
218
  }
220
219
  /**
221
220
  * Resolves a leaf `orderBy` direction into the two flags the rewritten `orderBy` and the cursor
222
- * condition have to agree on, or pagination skips rows at the null boundary. Platforms that cannot
223
- * order nulls explicitly sort them as the lowest value, elsewhere the requested placement wins,
224
- * defaulting to nulls last for `asc` and nulls first for `desc`.
221
+ * condition have to agree on, or pagination skips rows at the null boundary. A placement the caller
222
+ * asked for wins where the platform can honor it, anything else follows the platform's own default,
223
+ * which is what the untouched `orderBy` will get.
225
224
  */
226
225
  parseCursorDirection(direction) {
227
226
  const dir = ('' + direction).toLowerCase();
228
227
  const desc = direction === QueryOrderNumeric.DESC || dir.startsWith('desc');
229
- if (!this.platform.supportsNullsOrdering()) {
230
- return { desc, nullsFirst: !desc };
228
+ const nullsFirst = dir.includes('nulls first');
229
+ const explicit = (nullsFirst || dir.includes('nulls last')) && this.platform.supportsNullsOrdering();
230
+ if (!explicit) {
231
+ return { desc, nullsFirst: this.platform.sortsNullsLowest() ? !desc : desc, explicit };
231
232
  }
232
- if (dir.includes('nulls first')) {
233
- return { desc, nullsFirst: true };
234
- }
235
- if (dir.includes('nulls last')) {
236
- return { desc, nullsFirst: false };
237
- }
238
- return { desc, nullsFirst: desc };
233
+ return { desc, nullsFirst, explicit };
239
234
  }
240
235
  /**
241
236
  * Restores the JS value of a single cursor offset. String-cursor values (`fromJson`) are decoded
@@ -313,8 +308,8 @@ export class DatabaseDriver {
313
308
  createCursorCondition(definition, offsets, inverse, meta, fromJson = false) {
314
309
  const createCondition = (prop, direction, offset, eq = false, path = prop, properties = meta.properties, insideJson = false, nullable = false) => {
315
310
  const propMeta = properties[prop];
316
- // a nullable relation or embeddable makes its joined columns null too
317
- nullable ||= !!propMeta?.nullable;
311
+ // nullable relations and embeddables null out their joined columns, and a formula can yield null unannounced
312
+ nullable ||= !!propMeta?.nullable || !!propMeta?.formula;
318
313
  if (Utils.isPlainObject(direction)) {
319
314
  if (offset === undefined) {
320
315
  throw CursorError.missingValue(meta.className, path);
@@ -324,17 +319,29 @@ export class DatabaseDriver {
324
319
  const childProps = propMeta?.kind === ReferenceKind.EMBEDDED ? propMeta.embeddedProps : propMeta?.targetMeta?.properties;
325
320
  insideJson ||=
326
321
  (propMeta?.kind === ReferenceKind.EMBEDDED && !!propMeta.object) || propMeta?.customType instanceof JsonType;
327
- const children = Utils.keys(direction)
328
- .map(key => createCondition(key, direction[key],
322
+ const keys = Utils.keys(direction);
323
+ const child = (key, childEq) => createCondition(key, direction[key],
329
324
  // a null relation offset means the whole sort key is null, propagate it to the leaves
330
- offset === null ? null : offset[key], eq, `${path}.${key}`, childProps ?? {}, insideJson, nullable))
331
- .filter(child => Utils.hasObjectKeys(child));
332
- // children comparing against nulls are group conditions, those cannot be merged into one object
333
- const value = children.length > 1 && children.some(child => '$or' in child)
334
- ? { $and: children }
335
- : children.reduce((o, child) => Object.assign(o, child), {});
336
- // an unconstrained child condition must not degrade to `{ [prop]: {} }`
337
- return children.length > 0 ? { [prop]: value } : {};
325
+ offset === null ? null : offset[key], childEq, `${path}.${key}`, childProps ?? {}, insideJson, nullable);
326
+ // the group's own keys are a keyset in their own right, so they decompose the same way the
327
+ // top level does; merging them into one object would compare every key independently instead
328
+ const lex = (index) => {
329
+ const key = keys[index];
330
+ if (index === keys.length - 1) {
331
+ return child(key, eq);
332
+ }
333
+ const atOrPast = child(key, true);
334
+ const tail = lex(index + 1);
335
+ // an unconstrained tail matches anything, leaving only the `at or past` prefix to constrain
336
+ const past = Utils.hasObjectKeys(tail) ? { $or: [child(key, false), tail] } : {};
337
+ if (!Utils.hasObjectKeys(past)) {
338
+ return atOrPast;
339
+ }
340
+ return Utils.hasObjectKeys(atOrPast) ? { $and: [atOrPast, past] } : past;
341
+ };
342
+ const value = keys.length > 0 ? lex(0) : {};
343
+ // an unconstrained group condition must not degrade to `{ [prop]: {} }`
344
+ return Utils.hasObjectKeys(value) ? { [prop]: value } : {};
338
345
  }
339
346
  const { desc: isDesc, nullsFirst } = this.parseCursorDirection(direction);
340
347
  const operator = Utils.xor(isDesc, inverse) ? '$lt' : '$gt';
@@ -514,8 +521,8 @@ export class DatabaseDriver {
514
521
  const props = prop.embeddedProps;
515
522
  let unknownProp = false;
516
523
  Object.keys(data[prop.name]).forEach(kk => {
517
- // explicitly allow `$exists`, `$eq`, `$ne` and `$elemMatch` operators here as they can't be misused this way
518
- const operator = Object.keys(data[prop.name]).some(f => Utils.isOperator(f) && !['$exists', '$ne', '$eq', '$elemMatch'].includes(f));
524
+ // explicitly allow `$exists`, `$eq`, `$ne`, `$elemMatch` and `$all` operators here as they can't be misused this way
525
+ const operator = Object.keys(data[prop.name]).some(f => Utils.isOperator(f) && !['$exists', '$ne', '$eq', '$elemMatch', '$all'].includes(f));
519
526
  if (operator) {
520
527
  throw ValidationError.cannotUseOperatorsInsideEmbeddables(meta.class, prop.name, data);
521
528
  }
@@ -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}`);
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.2.0-dev.17",
3
+ "version": "7.2.0-dev.19",
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",
@@ -230,8 +230,10 @@ export declare abstract class Platform {
230
230
  convertsJsonAutomatically(): boolean;
231
231
  /** Whether date values inside JSON documents keep their native type (e.g. BSON dates), instead of being serialized to ISO strings. */
232
232
  preservesDatesInsideJson(): boolean;
233
- /** Whether `nulls first`/`nulls last` can be requested in an `orderBy`. Platforms returning `false` always sort nulls as the lowest value. */
233
+ /** Whether `nulls first`/`nulls last` can be requested in an `orderBy`. */
234
234
  supportsNullsOrdering(): boolean;
235
+ /** Where nulls land when an `orderBy` requests no explicit placement: lowest (`asc` puts them first) or highest. */
236
+ sortsNullsLowest(): boolean;
235
237
  /** Converts a JS value to its JSON database representation (typically JSON.stringify). */
236
238
  convertJsonToDatabaseValue(value: unknown, context?: TransformContext): unknown;
237
239
  /** Converts a database JSON value to its JS representation. */
@@ -471,10 +471,14 @@ export class Platform {
471
471
  preservesDatesInsideJson() {
472
472
  return false;
473
473
  }
474
- /** Whether `nulls first`/`nulls last` can be requested in an `orderBy`. Platforms returning `false` always sort nulls as the lowest value. */
474
+ /** Whether `nulls first`/`nulls last` can be requested in an `orderBy`. */
475
475
  supportsNullsOrdering() {
476
476
  return true;
477
477
  }
478
+ /** Where nulls land when an `orderBy` requests no explicit placement: lowest (`asc` puts them first) or highest. */
479
+ sortsNullsLowest() {
480
+ return false;
481
+ }
478
482
  /** Converts a JS value to its JSON database representation (typically JSON.stringify). */
479
483
  convertJsonToDatabaseValue(value, context) {
480
484
  return JSON.stringify(value);
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.2.0-dev.17';
156
+ static #ORM_VERSION = '7.2.0-dev.19';
157
157
  /** Default session variable name backing an RLS filter argument (`current_setting('mikro.<filter>.<arg>')`). */
158
158
  static getRlsSettingName(filterName, argName) {
159
159
  return `mikro.${filterName}.${argName}`;
@@ -858,7 +858,11 @@ export class Utils {
858
858
  return await import(module);
859
859
  }
860
860
  catch (err) {
861
- if (warning && err.code === 'ERR_MODULE_NOT_FOUND') {
861
+ // only a missing module is expected here, anything else is a real failure inside the module
862
+ if (!['ERR_MODULE_NOT_FOUND', 'MODULE_NOT_FOUND'].includes(err.code)) {
863
+ throw err;
864
+ }
865
+ if (warning) {
862
866
  // eslint-disable-next-line no-console
863
867
  console.warn(warning);
864
868
  }