@mikro-orm/core 7.1.16-dev.6 → 7.1.16-dev.8
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 +1 -0
- package/drivers/DatabaseDriver.js +19 -10
- package/entity/EntityLoader.d.ts +7 -1
- package/entity/EntityLoader.js +34 -4
- package/enums.d.ts +2 -0
- package/enums.js +2 -0
- package/package.json +1 -1
- package/typings.d.ts +1 -0
- package/utils/Utils.js +1 -1
package/EntityManager.js
CHANGED
|
@@ -184,8 +184,14 @@ export class DatabaseDriver {
|
|
|
184
184
|
}, {});
|
|
185
185
|
return { [prop]: value };
|
|
186
186
|
}
|
|
187
|
-
const
|
|
188
|
-
const
|
|
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
|
-
|
|
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
|
-
//
|
|
279
|
-
|
|
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 `$
|
|
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
|
}
|
package/entity/EntityLoader.d.ts
CHANGED
|
@@ -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;
|
package/entity/EntityLoader.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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.1.16-dev.
|
|
3
|
+
"version": "7.1.16-dev.8",
|
|
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.
|
|
156
|
+
static #ORM_VERSION = '7.1.16-dev.8';
|
|
157
157
|
/**
|
|
158
158
|
* Checks if the argument is instance of `Object`. Returns false for arrays.
|
|
159
159
|
*/
|