@mikro-orm/core 7.2.0-dev.18 → 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 +1 -0
- package/drivers/DatabaseDriver.d.ts +3 -3
- package/drivers/DatabaseDriver.js +38 -31
- 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/platforms/Platform.d.ts +3 -1
- package/platforms/Platform.js +5 -1
- package/typings.d.ts +1 -0
- package/utils/Utils.js +6 -2
package/EntityManager.js
CHANGED
|
@@ -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.
|
|
70
|
-
*
|
|
71
|
-
*
|
|
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
|
|
185
|
+
const createOrderBy = (prop, direction, properties = meta.properties) => {
|
|
186
186
|
const propMeta = properties[prop];
|
|
187
|
-
// nullable relations and embeddables null out their joined columns, and a formula can yield null unannounced
|
|
188
|
-
nullable ||= !!propMeta?.nullable || !!propMeta?.formula;
|
|
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 ?? {}
|
|
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
|
-
//
|
|
200
|
-
|
|
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.
|
|
223
|
-
*
|
|
224
|
-
*
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
return { desc, nullsFirst: true };
|
|
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 };
|
|
234
232
|
}
|
|
235
|
-
|
|
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
|
|
@@ -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
|
|
328
|
-
|
|
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],
|
|
331
|
-
|
|
332
|
-
//
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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 `$
|
|
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
|
}
|
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.2.0-dev.
|
|
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",
|
package/platforms/Platform.d.ts
CHANGED
|
@@ -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`.
|
|
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. */
|
package/platforms/Platform.js
CHANGED
|
@@ -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`.
|
|
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.
|
|
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
|
-
|
|
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
|
}
|