@mikro-orm/core 7.2.0-dev.9 → 7.2.1-dev.0
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.d.ts +41 -7
- package/EntityManager.js +225 -45
- package/MikroORM.js +3 -0
- package/README.md +1 -0
- package/connections/Connection.d.ts +3 -1
- package/drivers/DatabaseDriver.d.ts +14 -5
- package/drivers/DatabaseDriver.js +151 -52
- package/drivers/IDatabaseDriver.d.ts +1 -0
- package/entity/Collection.js +4 -2
- package/entity/EntityFactory.js +6 -0
- package/entity/EntityLoader.d.ts +7 -1
- package/entity/EntityLoader.js +46 -11
- package/entity/EntityRepository.d.ts +4 -5
- package/entity/EntityRepository.js +7 -2
- package/entity/defineEntity.d.ts +48 -14
- package/entity/defineEntity.js +32 -1
- package/enums.d.ts +5 -1
- package/enums.js +2 -0
- package/errors.d.ts +36 -0
- package/errors.js +90 -0
- package/events/EventManager.js +6 -3
- package/exceptions.d.ts +5 -0
- package/exceptions.js +5 -0
- package/hydration/ObjectHydrator.d.ts +2 -0
- package/hydration/ObjectHydrator.js +12 -8
- package/index.d.ts +1 -1
- package/metadata/MetadataDiscovery.d.ts +3 -0
- package/metadata/MetadataDiscovery.js +127 -17
- package/metadata/MetadataStorage.js +17 -1
- package/metadata/types.d.ts +19 -3
- package/package.json +1 -1
- package/platforms/Platform.d.ts +22 -3
- package/platforms/Platform.js +59 -1
- package/types/BigIntType.d.ts +1 -0
- package/types/BigIntType.js +23 -0
- package/types/DateTimeType.d.ts +1 -0
- package/types/DateTimeType.js +8 -0
- package/types/StringType.d.ts +14 -3
- package/types/StringType.js +34 -4
- package/types/TextType.d.ts +2 -4
- package/types/TextType.js +2 -8
- package/types/Type.d.ts +11 -0
- package/types/Type.js +4 -4
- package/types/index.d.ts +2 -2
- package/typings.d.ts +56 -2
- package/typings.js +24 -1
- package/unit-of-work/ChangeSetPersister.js +17 -13
- package/unit-of-work/UnitOfWork.js +11 -4
- package/utils/Configuration.d.ts +15 -1
- package/utils/Configuration.js +11 -1
- package/utils/Cursor.d.ts +2 -0
- package/utils/Cursor.js +43 -33
- package/utils/DataloaderUtils.js +2 -1
- package/utils/EntityComparator.d.ts +2 -0
- package/utils/EntityComparator.js +7 -3
- package/utils/QueryHelper.d.ts +12 -0
- package/utils/QueryHelper.js +75 -4
- package/utils/RawQueryFragment.d.ts +6 -0
- package/utils/RawQueryFragment.js +15 -6
- package/utils/TransactionManager.js +1 -1
- package/utils/Utils.d.ts +14 -2
- package/utils/Utils.js +31 -4
- package/utils/env-vars.js +1 -0
- package/utils/index.d.ts +1 -0
- package/utils/index.js +1 -0
- package/utils/rls-utils.d.ts +35 -0
- package/utils/rls-utils.js +97 -0
- package/utils/upsert-utils.d.ts +9 -1
- package/utils/upsert-utils.js +26 -3
|
@@ -65,13 +65,22 @@ export declare abstract class DatabaseDriver<C extends Connection> implements ID
|
|
|
65
65
|
where: FilterQuery<T>;
|
|
66
66
|
};
|
|
67
67
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
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. 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
|
+
*/
|
|
73
|
+
private parseCursorDirection;
|
|
74
|
+
/**
|
|
75
|
+
* Restores the JS value of a single cursor offset. String-cursor values (`fromJson`) are decoded
|
|
76
|
+
* JSON: types implementing `fromJSON` own the round trip, other custom types restore via
|
|
77
|
+
* `convertToJSValue`, with `Date` healing for date-like columns based on the property type (never
|
|
78
|
+
* based on the string shape alone). POJO values are already JS values and only date-like strings
|
|
79
|
+
* are healed. Values compared against a JSON document keep their serialized form instead, unless
|
|
80
|
+
* the platform preserves native date types inside JSON documents (mongo).
|
|
72
81
|
*/
|
|
73
82
|
private mapCursorOffset;
|
|
74
|
-
protected createCursorCondition<T extends object>(definition: (readonly [keyof T & string, QueryOrder])[], offsets: Dictionary[], inverse: boolean, meta: EntityMetadata<T
|
|
83
|
+
protected createCursorCondition<T extends object>(definition: (readonly [keyof T & string, QueryOrder])[], offsets: Dictionary[], inverse: boolean, meta: EntityMetadata<T>, fromJson?: boolean): FilterQuery<T>;
|
|
75
84
|
/** @internal */
|
|
76
85
|
mapDataToFieldNames(data: Dictionary, stringifyJsonArrays: boolean, properties?: Record<string, EntityProperty>, convertCustomTypes?: boolean, object?: boolean): Dictionary;
|
|
77
86
|
protected inlineEmbeddables<T extends object>(meta: EntityMetadata<T>, data: T, where?: boolean): void;
|
|
@@ -10,7 +10,6 @@ import { helper } from '../entity/wrap.js';
|
|
|
10
10
|
import { Reference } from '../entity/Reference.js';
|
|
11
11
|
import { PolymorphicRef } from '../entity/PolymorphicRef.js';
|
|
12
12
|
import { JsonType } from '../types/JsonType.js';
|
|
13
|
-
import { DateTimeType } from '../types/DateTimeType.js';
|
|
14
13
|
import { QueryHelper } from '../utils/QueryHelper.js';
|
|
15
14
|
import { MikroORM } from '../MikroORM.js';
|
|
16
15
|
/** Abstract base class for all database drivers, implementing common driver logic. */
|
|
@@ -146,6 +145,7 @@ export class DatabaseDriver {
|
|
|
146
145
|
const createCursor = (val, key, inverse = false) => {
|
|
147
146
|
const def = Reference.unwrapReference((isCursor(val, key) ? val[key] : val));
|
|
148
147
|
let offsets;
|
|
148
|
+
let fromJson = false;
|
|
149
149
|
// entity (and reference) instances are supported as cursors too, their properties are read the same way
|
|
150
150
|
if (Utils.isPlainObject(def) || Utils.isEntity(def)) {
|
|
151
151
|
// POJO values are already JS values, extract them ordered per the definition,
|
|
@@ -158,11 +158,17 @@ export class DatabaseDriver {
|
|
|
158
158
|
});
|
|
159
159
|
}
|
|
160
160
|
else {
|
|
161
|
-
|
|
162
|
-
|
|
161
|
+
try {
|
|
162
|
+
/* v8 ignore next */
|
|
163
|
+
offsets = def ? Cursor.decode(def) : [];
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
throw CursorError.invalidCursor(meta.className, error);
|
|
167
|
+
}
|
|
168
|
+
fromJson = true;
|
|
163
169
|
}
|
|
164
170
|
if (definition.length > 0 && definition.length === offsets.length) {
|
|
165
|
-
return this.createCursorCondition(definition, offsets, inverse, meta);
|
|
171
|
+
return this.createCursorCondition(definition, offsets, inverse, meta, fromJson);
|
|
166
172
|
}
|
|
167
173
|
/* v8 ignore next */
|
|
168
174
|
return {};
|
|
@@ -176,16 +182,24 @@ export class DatabaseDriver {
|
|
|
176
182
|
if (limit != null) {
|
|
177
183
|
options.limit = limit + (overfetch ? 1 : 0);
|
|
178
184
|
}
|
|
179
|
-
const createOrderBy = (prop, direction) => {
|
|
185
|
+
const createOrderBy = (prop, direction, properties = meta.properties) => {
|
|
186
|
+
const propMeta = properties[prop];
|
|
180
187
|
if (Utils.isPlainObject(direction)) {
|
|
188
|
+
const childProps = propMeta?.kind === ReferenceKind.EMBEDDED ? propMeta.embeddedProps : propMeta?.targetMeta?.properties;
|
|
181
189
|
const value = Utils.getObjectQueryKeys(direction).reduce((o, key) => {
|
|
182
|
-
Object.assign(o, createOrderBy(key, direction[key]));
|
|
190
|
+
Object.assign(o, createOrderBy(key, direction[key], childProps ?? {}));
|
|
183
191
|
return o;
|
|
184
192
|
}, {});
|
|
185
193
|
return { [prop]: value };
|
|
186
194
|
}
|
|
187
|
-
const desc
|
|
195
|
+
const { desc, nullsFirst, explicit } = this.parseCursorDirection(direction);
|
|
188
196
|
const dir = Utils.xor(desc, isLast) ? 'desc' : 'asc';
|
|
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) {
|
|
200
|
+
const nulls = Utils.xor(nullsFirst, isLast) ? 'first' : 'last';
|
|
201
|
+
return { [prop]: `${dir} nulls ${nulls}` };
|
|
202
|
+
}
|
|
189
203
|
return { [prop]: dir };
|
|
190
204
|
};
|
|
191
205
|
// the cursor condition is created at the driver level, after the EM already converted custom types
|
|
@@ -203,12 +217,30 @@ export class DatabaseDriver {
|
|
|
203
217
|
};
|
|
204
218
|
}
|
|
205
219
|
/**
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
220
|
+
* Resolves a leaf `orderBy` direction into the two flags the rewritten `orderBy` and the cursor
|
|
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.
|
|
210
224
|
*/
|
|
211
|
-
|
|
225
|
+
parseCursorDirection(direction) {
|
|
226
|
+
const dir = ('' + direction).toLowerCase();
|
|
227
|
+
const desc = direction === QueryOrderNumeric.DESC || dir.startsWith('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 };
|
|
232
|
+
}
|
|
233
|
+
return { desc, nullsFirst, explicit };
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Restores the JS value of a single cursor offset. String-cursor values (`fromJson`) are decoded
|
|
237
|
+
* JSON: types implementing `fromJSON` own the round trip, other custom types restore via
|
|
238
|
+
* `convertToJSValue`, with `Date` healing for date-like columns based on the property type (never
|
|
239
|
+
* based on the string shape alone). POJO values are already JS values and only date-like strings
|
|
240
|
+
* are healed. Values compared against a JSON document keep their serialized form instead, unless
|
|
241
|
+
* the platform preserves native date types inside JSON documents (mongo).
|
|
242
|
+
*/
|
|
243
|
+
mapCursorOffset(prop, value, insideJson, fromJson) {
|
|
212
244
|
if (Utils.isScalarReference(value)) {
|
|
213
245
|
value = value.unwrap();
|
|
214
246
|
}
|
|
@@ -219,25 +251,65 @@ export class DatabaseDriver {
|
|
|
219
251
|
if (value == null) {
|
|
220
252
|
return value;
|
|
221
253
|
}
|
|
254
|
+
// mongo preserves native date types inside JSON documents, restored JS values compare directly
|
|
222
255
|
if (insideJson && !this.platform.preservesDatesInsideJson()) {
|
|
223
|
-
// compared against the JSON document, which holds the
|
|
256
|
+
// compared against the JSON document, which holds the JSON form of the database value
|
|
224
257
|
if (value instanceof Date) {
|
|
225
258
|
return value.toISOString();
|
|
226
259
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
260
|
+
if (prop?.customType && fromJson) {
|
|
261
|
+
const restored = prop.customType.fromJSON
|
|
262
|
+
? prop.customType.fromJSON(value, this.platform)
|
|
263
|
+
: prop.customType.convertToJSValue(value, this.platform);
|
|
264
|
+
// a restored `Date` compares against its ISO string in the document, everything else
|
|
265
|
+
// keeps the JS value and gets its single `convertToDatabaseValue` in `processWhere`
|
|
266
|
+
if (restored instanceof Date) {
|
|
267
|
+
const converted = prop.customType.convertToDatabaseValue(restored, this.platform, {
|
|
268
|
+
fromQuery: true,
|
|
269
|
+
key: prop.name,
|
|
270
|
+
mode: 'query',
|
|
271
|
+
});
|
|
272
|
+
if (converted instanceof Date) {
|
|
273
|
+
return converted.toISOString();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return restored;
|
|
277
|
+
}
|
|
278
|
+
return value;
|
|
279
|
+
}
|
|
280
|
+
if (prop?.customType) {
|
|
281
|
+
if (fromJson && prop.customType.fromJSON) {
|
|
282
|
+
// the type owns its JSON round trip
|
|
283
|
+
return prop.customType.fromJSON(value, this.platform);
|
|
284
|
+
}
|
|
285
|
+
// A string is either a serialized form (string cursor) or a hand-written one (POJO).
|
|
286
|
+
// Types whose JS value is a `Date` must receive one. The rest get the string first,
|
|
287
|
+
// so any sub-millisecond precision survives.
|
|
288
|
+
if (typeof value === 'string' &&
|
|
289
|
+
(prop.runtimeType === 'Date' || this.platform.getMappedType(prop.columnTypes?.[0] ?? '').runtimeType === 'Date')) {
|
|
290
|
+
if (prop.runtimeType !== 'Date') {
|
|
291
|
+
try {
|
|
292
|
+
return prop.customType.convertToJSValue(value, this.platform);
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
// the type cannot read the serialized string, fall back to the `Date` form
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return prop.customType.convertToJSValue(new Date(value), this.platform);
|
|
299
|
+
}
|
|
300
|
+
// serialized non-string values still need restoring; POJO values are already JS values
|
|
301
|
+
return fromJson ? prop.customType.convertToJSValue(value, this.platform) : value;
|
|
230
302
|
}
|
|
231
|
-
if (typeof value === 'string' &&
|
|
232
|
-
|
|
233
|
-
(prop?.customType && this.platform.getMappedType(prop.columnTypes?.[0] ?? '') instanceof DateTimeType))) {
|
|
234
|
-
value = new Date(value);
|
|
303
|
+
if (typeof value === 'string' && prop?.runtimeType === 'Date') {
|
|
304
|
+
return new Date(value);
|
|
235
305
|
}
|
|
236
|
-
return
|
|
306
|
+
return value;
|
|
237
307
|
}
|
|
238
|
-
createCursorCondition(definition, offsets, inverse, meta) {
|
|
239
|
-
const createCondition = (prop, direction, offset, eq = false, path = prop, properties = meta.properties, insideJson = false) => {
|
|
308
|
+
createCursorCondition(definition, offsets, inverse, meta, fromJson = false) {
|
|
309
|
+
const createCondition = (prop, direction, offset, eq = false, path = prop, properties = meta.properties, insideJson = false, nullable = false) => {
|
|
240
310
|
const propMeta = properties[prop];
|
|
311
|
+
// nullable relations and embeddables null out their joined columns, and a formula can yield null unannounced
|
|
312
|
+
nullable ||= !!propMeta?.nullable || !!propMeta?.formula;
|
|
241
313
|
if (Utils.isPlainObject(direction)) {
|
|
242
314
|
if (offset === undefined) {
|
|
243
315
|
throw CursorError.missingValue(meta.className, path);
|
|
@@ -247,40 +319,57 @@ export class DatabaseDriver {
|
|
|
247
319
|
const childProps = propMeta?.kind === ReferenceKind.EMBEDDED ? propMeta.embeddedProps : propMeta?.targetMeta?.properties;
|
|
248
320
|
insideJson ||=
|
|
249
321
|
(propMeta?.kind === ReferenceKind.EMBEDDED && !!propMeta.object) || propMeta?.customType instanceof JsonType;
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
322
|
+
const keys = Utils.keys(direction);
|
|
323
|
+
const child = (key, childEq) => createCondition(key, direction[key],
|
|
324
|
+
// a null relation offset means the whole sort key is null, propagate it to the leaves
|
|
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 } : {};
|
|
268
345
|
}
|
|
346
|
+
const { desc: isDesc, nullsFirst } = this.parseCursorDirection(direction);
|
|
269
347
|
const operator = Utils.xor(isDesc, inverse) ? '$lt' : '$gt';
|
|
270
348
|
// For leaf-level properties, undefined means missing value
|
|
271
349
|
if (offset === undefined) {
|
|
272
350
|
throw CursorError.missingValue(meta.className, path);
|
|
273
351
|
}
|
|
274
|
-
|
|
352
|
+
// string-cursor values are client supplied, so a value the type cannot restore is an
|
|
353
|
+
// invalid cursor, letting callers map `CursorError` to a client error response
|
|
354
|
+
try {
|
|
355
|
+
offset = this.mapCursorOffset(propMeta, offset, insideJson, fromJson);
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
if (!fromJson || error instanceof CursorError) {
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
throw CursorError.invalidCursor(meta.className, error);
|
|
362
|
+
}
|
|
275
363
|
// Handle null offset (intentional null cursor value)
|
|
276
364
|
if (offset === null) {
|
|
365
|
+
// hasItemsAfterNull: forward + nullsFirst, or backward + nullsLast
|
|
366
|
+
const hasItemsAfterNull = Utils.xor(nullsFirst, inverse);
|
|
277
367
|
if (eq) {
|
|
278
|
-
//
|
|
279
|
-
|
|
368
|
+
// at-or-after a null sort key means every row when non-null rows follow the null block,
|
|
369
|
+
// so the tie-breaker in the `$or` sibling can reach past it
|
|
370
|
+
return hasItemsAfterNull ? {} : { [prop]: null };
|
|
280
371
|
}
|
|
281
372
|
// Strict comparison with null cursor value
|
|
282
|
-
// hasItemsAfterNull: forward + nullsFirst, or backward + nullsLast
|
|
283
|
-
const hasItemsAfterNull = Utils.xor(nullsFirst, inverse);
|
|
284
373
|
if (hasItemsAfterNull) {
|
|
285
374
|
return { [prop]: { $ne: null } };
|
|
286
375
|
}
|
|
@@ -288,7 +377,12 @@ export class DatabaseDriver {
|
|
|
288
377
|
return { [prop]: [] };
|
|
289
378
|
}
|
|
290
379
|
// Non-null offset
|
|
291
|
-
|
|
380
|
+
const condition = { [prop]: { [operator + (eq ? 'e' : '')]: offset } };
|
|
381
|
+
// null sort keys lie past the offset here, and no comparison ever matches them
|
|
382
|
+
if (nullable && !Utils.xor(nullsFirst, inverse)) {
|
|
383
|
+
return { $or: [condition, { [prop]: null }] };
|
|
384
|
+
}
|
|
385
|
+
return condition;
|
|
292
386
|
};
|
|
293
387
|
const [order, ...otherOrders] = definition;
|
|
294
388
|
const [offset, ...otherOffsets] = offsets;
|
|
@@ -296,13 +390,18 @@ export class DatabaseDriver {
|
|
|
296
390
|
if (!otherOrders.length) {
|
|
297
391
|
return createCondition(prop, direction, offset);
|
|
298
392
|
}
|
|
299
|
-
|
|
300
|
-
|
|
393
|
+
const atOrPast = createCondition(prop, direction, offset, true);
|
|
394
|
+
const past = {
|
|
301
395
|
$or: [
|
|
302
396
|
createCondition(prop, direction, offset),
|
|
303
|
-
this.createCursorCondition(otherOrders, otherOffsets, inverse, meta),
|
|
397
|
+
this.createCursorCondition(otherOrders, otherOffsets, inverse, meta, fromJson),
|
|
304
398
|
],
|
|
305
399
|
};
|
|
400
|
+
// the `at or past` prefix is itself a group condition when it compares against nulls
|
|
401
|
+
if ('$or' in atOrPast) {
|
|
402
|
+
return { $and: [atOrPast, past] };
|
|
403
|
+
}
|
|
404
|
+
return { ...atOrPast, ...past };
|
|
306
405
|
}
|
|
307
406
|
/** @internal */
|
|
308
407
|
mapDataToFieldNames(data, stringifyJsonArrays, properties, convertCustomTypes, object) {
|
|
@@ -422,8 +521,8 @@ export class DatabaseDriver {
|
|
|
422
521
|
const props = prop.embeddedProps;
|
|
423
522
|
let unknownProp = false;
|
|
424
523
|
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));
|
|
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));
|
|
427
526
|
if (operator) {
|
|
428
527
|
throw ValidationError.cannotUseOperatorsInsideEmbeddables(meta.class, prop.name, data);
|
|
429
528
|
}
|
|
@@ -345,6 +345,7 @@ export interface CountByOptions<T extends object> {
|
|
|
345
345
|
filters?: FilterOptions;
|
|
346
346
|
having?: FilterQuery<T>;
|
|
347
347
|
schema?: string;
|
|
348
|
+
connectionType?: ConnectionType;
|
|
348
349
|
flushMode?: FlushMode | `${FlushMode}`;
|
|
349
350
|
loggerContext?: LogContext;
|
|
350
351
|
logging?: LoggingOptions;
|
package/entity/Collection.js
CHANGED
|
@@ -117,9 +117,11 @@ export class Collection {
|
|
|
117
117
|
opts.orderBy = QueryHelper.mergeOrderBy(opts.orderBy, this.property.orderBy, this.property.targetMeta?.orderBy);
|
|
118
118
|
options.populate = (await em.preparePopulate(this.property.targetMeta.class, options));
|
|
119
119
|
const cond = (await em.applyFilters(this.property.targetMeta.class, where, options.filters ?? {}, 'read'));
|
|
120
|
-
|
|
120
|
+
// fall back to the ambient transaction context, or `withSessionContext` would wrap the pivot load in a
|
|
121
|
+
// second concurrent transaction (a deadlock with a single-connection pool) instead of joining the open one
|
|
122
|
+
const map = await em.withSessionContext(ctx ?? em.getTransactionContext(), trx => em
|
|
121
123
|
.getDriver()
|
|
122
|
-
.loadFromPivotTable(this.property, [helper(this.owner).__primaryKeys], cond, opts.orderBy,
|
|
124
|
+
.loadFromPivotTable(this.property, [helper(this.owner).__primaryKeys], cond, opts.orderBy, trx, options));
|
|
123
125
|
items = map[helper(this.owner).getSerializedPrimaryKey()].map((item) => em.merge(this.property.targetMeta.class, item, { convertCustomTypes: true }));
|
|
124
126
|
await em.populate(items, options.populate, options);
|
|
125
127
|
}
|
package/entity/EntityFactory.js
CHANGED
|
@@ -77,6 +77,12 @@ export class EntityFactory {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
data = { ...data };
|
|
80
|
+
if (options.newEntity && meta2.root.inheritanceType === 'sti' && meta2.discriminatorValue != null) {
|
|
81
|
+
const prop = meta2.properties[meta2.root.discriminatorColumn];
|
|
82
|
+
if (prop && prop.userDefined !== false) {
|
|
83
|
+
data[prop.name] ??= meta2.discriminatorValue;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
80
86
|
const entity = exists ?? this.createEntity(data, meta2, options);
|
|
81
87
|
wrapped = helper(entity);
|
|
82
88
|
wrapped.__processing = true;
|
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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { QueryHelper } from '../utils/QueryHelper.js';
|
|
2
|
-
import { Utils } from '../utils/Utils.js';
|
|
2
|
+
import { DANGEROUS_PROPERTY_NAMES, Utils } from '../utils/Utils.js';
|
|
3
3
|
import { ValidationError } from '../errors.js';
|
|
4
4
|
import { LoadStrategy, PopulatePath, ReferenceKind, } from '../enums.js';
|
|
5
5
|
import { Reference } from './Reference.js';
|
|
@@ -124,10 +124,10 @@ export class EntityLoader {
|
|
|
124
124
|
mergeNestedPopulate(populate) {
|
|
125
125
|
const tmp = populate.reduce((ret, item) => {
|
|
126
126
|
/* v8 ignore next */
|
|
127
|
-
if (item.field === PopulatePath.ALL) {
|
|
127
|
+
if (item.field === PopulatePath.ALL || DANGEROUS_PROPERTY_NAMES.includes(item.field)) {
|
|
128
128
|
return ret;
|
|
129
129
|
}
|
|
130
|
-
if (!ret
|
|
130
|
+
if (!Object.hasOwn(ret, item.field)) {
|
|
131
131
|
ret[item.field] = item;
|
|
132
132
|
return ret;
|
|
133
133
|
}
|
|
@@ -229,9 +229,10 @@ export class EntityLoader {
|
|
|
229
229
|
}
|
|
230
230
|
toPopulate.push(entity);
|
|
231
231
|
}
|
|
232
|
-
else if (refValue == null && !helper(entity).__loadedProperties.has(prop.name)) {
|
|
232
|
+
else if (refValue == null && !prop.object && !helper(entity).__loadedProperties.has(prop.name)) {
|
|
233
233
|
// FK columns weren't loaded (partial loading) — need to re-fetch them.
|
|
234
234
|
// If the property IS in __loadedProperties, the FK was loaded and is genuinely null.
|
|
235
|
+
// Object-embedded virtual props are skipped — they are populated via the embeddable instance.
|
|
235
236
|
needsFkLoad.push(entity);
|
|
236
237
|
}
|
|
237
238
|
}
|
|
@@ -333,7 +334,7 @@ export class EntityLoader {
|
|
|
333
334
|
// When targetKey is set, use it for FK lookup instead of the PK
|
|
334
335
|
let fk = prop.targetKey ?? Utils.getPrimaryKeyHash(meta.primaryKeys);
|
|
335
336
|
let schema = options.schema;
|
|
336
|
-
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);
|
|
337
338
|
let polymorphicOwnerProp;
|
|
338
339
|
const ownerProp = prop.kind === ReferenceKind.ONE_TO_MANY || (prop.kind === ReferenceKind.MANY_TO_MANY && !prop.owner)
|
|
339
340
|
? meta.properties[prop.mappedBy]
|
|
@@ -393,12 +394,20 @@ export class EntityLoader {
|
|
|
393
394
|
if (!Utils.isEmpty(prop.where) || Raw.hasObjectFragments(prop.where)) {
|
|
394
395
|
where = { $and: [where, prop.where] };
|
|
395
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
|
+
}
|
|
396
404
|
const orderBy = QueryHelper.mergeOrderBy(options.orderBy, prop.orderBy);
|
|
397
405
|
const findOptions = {
|
|
398
406
|
filters,
|
|
399
407
|
convertCustomTypes,
|
|
400
408
|
lockMode,
|
|
401
409
|
populateWhere,
|
|
410
|
+
populateFilter: nestedFilter,
|
|
402
411
|
logging,
|
|
403
412
|
orderBy,
|
|
404
413
|
populate: populate.children ?? populate.all ?? [],
|
|
@@ -445,7 +454,10 @@ export class EntityLoader {
|
|
|
445
454
|
}
|
|
446
455
|
}
|
|
447
456
|
}
|
|
448
|
-
|
|
457
|
+
// a missing target row means an orphaned reference, unless the query was narrowed by a populate condition
|
|
458
|
+
if ([ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind) &&
|
|
459
|
+
items.length !== children.length &&
|
|
460
|
+
Utils.isEmpty(options.where)) {
|
|
449
461
|
const nullVal = this.#em.config.get('forceUndefined') ? undefined : null;
|
|
450
462
|
const itemsMap = new Set();
|
|
451
463
|
const childrenMap = new Set();
|
|
@@ -571,6 +583,9 @@ export class EntityLoader {
|
|
|
571
583
|
filters,
|
|
572
584
|
ignoreLazyScalarProperties,
|
|
573
585
|
populateWhere,
|
|
586
|
+
populateFilter: options.populateFilter
|
|
587
|
+
? (await this.extractChildPopulateFilter(options, prop))
|
|
588
|
+
: undefined,
|
|
574
589
|
connectionType,
|
|
575
590
|
logging,
|
|
576
591
|
schema,
|
|
@@ -607,7 +622,7 @@ export class EntityLoader {
|
|
|
607
622
|
const fields = this.buildFields(options.fields, prop);
|
|
608
623
|
// oxfmt-ignore
|
|
609
624
|
const exclude = Array.isArray(options.exclude) ? Utils.extractChildElements(options.exclude, prop.name) : options.exclude;
|
|
610
|
-
const populateFilter = options.populateFilter
|
|
625
|
+
const populateFilter = options.populateFilter ? await this.extractChildPopulateFilter(options, prop) : undefined;
|
|
611
626
|
const options2 = { ...options, fields, exclude, populateFilter };
|
|
612
627
|
['limit', 'offset', 'first', 'last', 'before', 'after', 'overfetch'].forEach(prop => delete options2[prop]);
|
|
613
628
|
options2.populate = populate?.children ?? [];
|
|
@@ -620,7 +635,7 @@ export class EntityLoader {
|
|
|
620
635
|
if (!Utils.isEmpty(prop.where)) {
|
|
621
636
|
where = { $and: [where, prop.where] };
|
|
622
637
|
}
|
|
623
|
-
const map = await this.#driver.loadFromPivotTable(prop, ids, where, orderBy,
|
|
638
|
+
const map = await this.#em.withSessionContext(this.#em.getTransactionContext(), ctx => this.#driver.loadFromPivotTable(prop, ids, where, orderBy, ctx, options2, pivotJoin));
|
|
624
639
|
const children = [];
|
|
625
640
|
const isUnionTargetMN = QueryHelper.isUnionTargetPolymorphic(prop);
|
|
626
641
|
for (let i = 0; i < filtered.length; i++) {
|
|
@@ -652,7 +667,8 @@ export class EntityLoader {
|
|
|
652
667
|
}
|
|
653
668
|
async extractChildCondition(options, prop, filters = false) {
|
|
654
669
|
const where = options.where;
|
|
655
|
-
|
|
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] } : {};
|
|
656
672
|
const meta2 = prop.targetMeta;
|
|
657
673
|
const pk = Utils.getPrimaryKeyHash(meta2.primaryKeys);
|
|
658
674
|
['$and', '$or'].forEach(op => {
|
|
@@ -667,15 +683,16 @@ export class EntityLoader {
|
|
|
667
683
|
}
|
|
668
684
|
return cond;
|
|
669
685
|
});
|
|
670
|
-
|
|
686
|
+
// partial extraction from `$or` is unsound — the parent may have matched via a dropped branch
|
|
687
|
+
if (child.length > 0 && (op === '$and' || child.length === where[op].length)) {
|
|
671
688
|
subCond[op] = child;
|
|
672
689
|
}
|
|
673
690
|
}
|
|
674
691
|
});
|
|
675
692
|
const operators = Object.keys(subCond).filter(key => Utils.isOperator(key, false));
|
|
676
693
|
if (operators.length > 0) {
|
|
694
|
+
subCond[pk] = Utils.isPlainObject(subCond[pk]) ? { ...subCond[pk] } : (subCond[pk] ?? {});
|
|
677
695
|
operators.forEach(op => {
|
|
678
|
-
subCond[pk] ??= {};
|
|
679
696
|
subCond[pk][op] = subCond[op];
|
|
680
697
|
delete subCond[op];
|
|
681
698
|
});
|
|
@@ -685,6 +702,24 @@ export class EntityLoader {
|
|
|
685
702
|
}
|
|
686
703
|
return subCond;
|
|
687
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
|
+
}
|
|
688
723
|
buildFields(fields = [], prop, ref) {
|
|
689
724
|
if (ref) {
|
|
690
725
|
fields = prop.targetMeta.primaryKeys.map(targetPkName => `${prop.name}.${targetPkName}`);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PopulatePath } from '../enums.js';
|
|
2
|
-
import type { CreateOptions, EntityManager, MergeOptions } from '../EntityManager.js';
|
|
2
|
+
import type { CreateOptions, EntityManager, MapOptions, MergeOptions } from '../EntityManager.js';
|
|
3
3
|
import type { AssignOptions } from './EntityAssigner.js';
|
|
4
4
|
import type { Dictionary, EntityData, EntityDictionary, EntityKey, EntityName, FilterQuery, Loaded, Primary, AutoPath, RequiredEntityData, Ref, EntityType, EntityDTO, MergeSelected, FromEntityType, IsSubset, MergeLoaded, ArrayElement, IndexFilterQuery, WithUsingOptions } from '../typings.js';
|
|
5
5
|
import type { CountByOptions, CountOptions, DeleteOptions, FindAllOptions, FindByCursorOptions, FindOneOptions, FindOneOrFailOptions, FindOptions, GetReferenceOptions, NativeInsertUpdateOptions, StreamOptions, UpdateOptions, UpsertManyOptions, UpsertOptions } from '../drivers/IDatabaseDriver.js';
|
|
@@ -115,11 +115,10 @@ export declare class EntityRepository<Entity extends object> {
|
|
|
115
115
|
*/
|
|
116
116
|
nativeDelete(where: FilterQuery<Entity>, options?: DeleteOptions<Entity>): Promise<number>;
|
|
117
117
|
/**
|
|
118
|
-
* Maps raw database result to an entity and merges it to this EntityManager.
|
|
118
|
+
* Maps raw database result to an entity and merges it to this EntityManager by default.
|
|
119
|
+
* Use `disableIdentityMap` to return an isolated entity without affecting the current context.
|
|
119
120
|
*/
|
|
120
|
-
map(result: EntityDictionary<Entity>, options?:
|
|
121
|
-
schema?: string;
|
|
122
|
-
}): Entity;
|
|
121
|
+
map(result: EntityDictionary<Entity>, options?: Omit<MapOptions, 'mapped'>): Entity;
|
|
123
122
|
/**
|
|
124
123
|
* Gets a reference to the entity identified by the given type and alternate key property without actually loading it.
|
|
125
124
|
* The key option specifies which property to use for identity map lookup instead of the primary key.
|
|
@@ -131,7 +131,8 @@ export class EntityRepository {
|
|
|
131
131
|
return this.getEntityManager().nativeDelete(this.entityName, where, options);
|
|
132
132
|
}
|
|
133
133
|
/**
|
|
134
|
-
* Maps raw database result to an entity and merges it to this EntityManager.
|
|
134
|
+
* Maps raw database result to an entity and merges it to this EntityManager by default.
|
|
135
|
+
* Use `disableIdentityMap` to return an isolated entity without affecting the current context.
|
|
135
136
|
*/
|
|
136
137
|
map(result, options) {
|
|
137
138
|
return this.getEntityManager().map(this.entityName, result, options);
|
|
@@ -223,7 +224,11 @@ export class EntityRepository {
|
|
|
223
224
|
}
|
|
224
225
|
const entityName = entities[0].constructor.name;
|
|
225
226
|
const repoType = Utils.className(this.entityName);
|
|
226
|
-
|
|
227
|
+
// compare class identity where possible, as minifiers can mangle two classes to the same name
|
|
228
|
+
const entityMeta = entities[0].__meta;
|
|
229
|
+
const repoMeta = this.getEntityManager().getMetadata?.().find(this.entityName);
|
|
230
|
+
const mismatch = entityMeta && repoMeta ? entityMeta.class !== repoMeta.class : entityName && repoType !== entityName;
|
|
231
|
+
if (mismatch) {
|
|
227
232
|
throw ValidationError.fromWrongRepositoryType(entityName, repoType, method);
|
|
228
233
|
}
|
|
229
234
|
}
|