@mikro-orm/core 7.1.16-dev.1 → 7.1.16-dev.11

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.
Files changed (61) hide show
  1. package/EntityManager.d.ts +41 -7
  2. package/EntityManager.js +203 -42
  3. package/MikroORM.d.ts +4 -0
  4. package/MikroORM.js +9 -0
  5. package/README.md +1 -0
  6. package/cache/FileCacheAdapter.js +1 -1
  7. package/connections/Connection.d.ts +10 -1
  8. package/connections/Connection.js +9 -0
  9. package/drivers/DatabaseDriver.d.ts +14 -5
  10. package/drivers/DatabaseDriver.js +151 -52
  11. package/entity/Collection.js +4 -2
  12. package/entity/EntityLoader.d.ts +7 -1
  13. package/entity/EntityLoader.js +35 -5
  14. package/entity/EntityRepository.d.ts +4 -5
  15. package/entity/EntityRepository.js +2 -1
  16. package/entity/defineEntity.d.ts +23 -5
  17. package/entity/defineEntity.js +31 -0
  18. package/enums.d.ts +5 -1
  19. package/enums.js +2 -0
  20. package/errors.d.ts +35 -0
  21. package/errors.js +87 -0
  22. package/exceptions.d.ts +5 -0
  23. package/exceptions.js +5 -0
  24. package/index.d.ts +1 -1
  25. package/metadata/MetadataDiscovery.d.ts +3 -0
  26. package/metadata/MetadataDiscovery.js +94 -7
  27. package/metadata/types.d.ts +19 -3
  28. package/package.json +1 -1
  29. package/platforms/Platform.d.ts +19 -1
  30. package/platforms/Platform.js +56 -0
  31. package/types/BigIntType.d.ts +1 -0
  32. package/types/BigIntType.js +23 -0
  33. package/types/DateTimeType.d.ts +1 -0
  34. package/types/DateTimeType.js +8 -0
  35. package/types/StringType.d.ts +14 -3
  36. package/types/StringType.js +34 -4
  37. package/types/TextType.d.ts +2 -4
  38. package/types/TextType.js +2 -8
  39. package/types/Type.d.ts +11 -0
  40. package/types/index.d.ts +2 -2
  41. package/typings.d.ts +48 -0
  42. package/typings.js +1 -0
  43. package/unit-of-work/UnitOfWork.js +1 -0
  44. package/utils/Configuration.d.ts +21 -1
  45. package/utils/Configuration.js +12 -1
  46. package/utils/Cursor.d.ts +2 -0
  47. package/utils/Cursor.js +43 -33
  48. package/utils/QueryHelper.d.ts +12 -0
  49. package/utils/QueryHelper.js +63 -0
  50. package/utils/RawQueryFragment.d.ts +6 -0
  51. package/utils/RawQueryFragment.js +15 -6
  52. package/utils/RequestContext.d.ts +2 -2
  53. package/utils/RequestContext.js +11 -2
  54. package/utils/TransactionManager.js +1 -1
  55. package/utils/Utils.d.ts +2 -0
  56. package/utils/Utils.js +12 -3
  57. package/utils/env-vars.js +2 -0
  58. package/utils/index.d.ts +1 -0
  59. package/utils/index.js +1 -0
  60. package/utils/rls-utils.d.ts +35 -0
  61. package/utils/rls-utils.js +97 -0
@@ -65,13 +65,22 @@ export declare abstract class DatabaseDriver<C extends Connection> implements ID
65
65
  where: FilterQuery<T>;
66
66
  };
67
67
  /**
68
- * Restores the JS value of a single cursor offset: ISO strings become `Date` instances based on the
69
- * property type (never based on the string shape alone), and custom types are restored via
70
- * `convertToJSValue`. Values compared against a JSON document keep their serialized form instead,
71
- * unless the platform preserves native date types inside JSON documents (mongo).
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>): FilterQuery<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
- /* v8 ignore next */
162
- offsets = def ? Cursor.decode(def) : [];
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 = direction === QueryOrderNumeric.DESC || direction.toString().toLowerCase() === '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
- * Restores the JS value of a single cursor offset: ISO strings become `Date` instances based on the
207
- * property type (never based on the string shape alone), and custom types are restored via
208
- * `convertToJSValue`. Values compared against a JSON document keep their serialized form instead,
209
- * unless the platform preserves native date types inside JSON documents (mongo).
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
- mapCursorOffset(prop, value, insideJson) {
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 serialized form
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
- // restore the JS value from the serialized form, `processWhere` then converts it to
228
- // the database form, which is what the JSON document holds for custom typed props
229
- return prop?.customType ? prop.customType.convertToJSValue(value, this.platform) : value;
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
- (prop?.runtimeType === 'Date' ||
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 prop?.customType ? prop.customType.convertToJSValue(value, this.platform) : value;
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 value = Utils.keys(direction).reduce((o, key) => {
251
- Object.assign(o, createCondition(key, direction[key], offset?.[key], eq, `${path}.${key}`, childProps ?? {}, insideJson));
252
- return o;
253
- }, {});
254
- return { [prop]: value };
255
- }
256
- const isDesc = direction === QueryOrderNumeric.DESC || direction.toString().toLowerCase() === 'desc';
257
- const dirStr = direction.toString().toLowerCase();
258
- let nullsFirst;
259
- if (dirStr.includes('nulls first')) {
260
- nullsFirst = true;
261
- }
262
- else if (dirStr.includes('nulls last')) {
263
- nullsFirst = false;
264
- }
265
- else {
266
- // Default: NULLS LAST for ASC, NULLS FIRST for DESC (matches most databases)
267
- nullsFirst = isDesc;
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
- offset = this.mapCursorOffset(propMeta, offset, insideJson);
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
- // Equal to null
279
- return { [prop]: null };
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
- return { [prop]: { [operator + (eq ? 'e' : '')]: offset } };
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
- return {
300
- ...createCondition(prop, direction, offset, true),
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 `$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));
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
  }
@@ -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
- const map = await em
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, ctx, options);
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
  }
@@ -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 ?? [];
@@ -624,7 +635,7 @@ export class EntityLoader {
624
635
  if (!Utils.isEmpty(prop.where)) {
625
636
  where = { $and: [where, prop.where] };
626
637
  }
627
- const map = await this.#driver.loadFromPivotTable(prop, ids, where, orderBy, this.#em.getTransactionContext(), options2, pivotJoin);
638
+ const map = await this.#em.withSessionContext(this.#em.getTransactionContext(), ctx => this.#driver.loadFromPivotTable(prop, ids, where, orderBy, ctx, options2, pivotJoin));
628
639
  const children = [];
629
640
  const isUnionTargetMN = QueryHelper.isUnionTargetPolymorphic(prop);
630
641
  for (let i = 0; i < filtered.length; i++) {
@@ -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}`);
@@ -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);
@@ -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
  };
@@ -148,6 +148,8 @@ export interface PropertyChain<Value, Options> {
148
148
  orphanRemoval(orphanRemoval?: boolean): HasKind<Options, '1:m' | '1:1'> extends true ? PropertyChain<Value, Options> : never;
149
149
  discriminator(discriminator: string): HasKind<Options, 'm:1' | '1:1' | 'm:n'> extends true ? PropertyChain<Value, Options> : never;
150
150
  discriminatorMap(discriminatorMap: Dictionary<string>): HasKind<Options, 'm:1' | '1:1' | 'm:n'> extends true ? PropertyChain<Value, Options> : never;
151
+ /** Resolve this read-only to-one relation via a subquery on another entity (see {@doclink relationships#to-one-relations-through-another-entity | To-one relations through another entity}). */
152
+ through(through: () => EntityName): HasKind<Options, 'm:1' | '1:1'> extends true ? PropertyChain<Value, Options> : never;
151
153
  pivotTable(pivotTable: string): HasKind<Options, 'm:n'> extends true ? PropertyChain<Value, Options> : never;
152
154
  pivotEntity(pivotEntity: () => EntityName): HasKind<Options, 'm:n'> extends true ? PropertyChain<Value, Options> : never;
153
155
  fixedOrder(fixedOrder?: boolean): HasKind<Options, 'm:n'> extends true ? PropertyChain<Value, Options> : never;
@@ -180,7 +182,7 @@ export interface PropertyChain<Value, Options> {
180
182
  foreignKeyName(foreignKeyName: string): HasKind<Options, 'm:1' | '1:m' | '1:1' | 'm:n'> extends true ? PropertyChain<Value, Options> : never;
181
183
  }
182
184
  /** @internal */
183
- export declare class UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys extends BuilderKeys> implements Record<Exclude<UniversalPropertyKeys, ExcludeKeys>, any> {
185
+ export declare class UniversalPropertyOptionsBuilder<in out Value, in out Options, in out IncludeKeys extends BuilderKeys> implements Record<Exclude<UniversalPropertyKeys, ExcludeKeys>, any> {
184
186
  '~options': Options;
185
187
  '~type'?: {
186
188
  value: Value;
@@ -528,6 +530,8 @@ export declare class UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys
528
530
  fixedOrderColumn(fixedOrderColumn: string): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
529
531
  /** Override default name for pivot table (see {@doclink naming-strategy | Naming Strategy}). */
530
532
  pivotTable(pivotTable: string): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
533
+ /** Resolve this read-only to-one relation via a subquery on another entity (see {@doclink relationships#to-one-relations-through-another-entity | To-one relations through another entity}). */
534
+ through(through: () => EntityName): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
531
535
  /** Set pivot entity for this relation (see {@doclink collections#custom-pivot-table-entity | Custom pivot table entity}). */
532
536
  pivotEntity(pivotEntity: () => EntityName): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
533
537
  /** Override the default database column name on the owning side (see {@doclink naming-strategy | Naming Strategy}). This option is only for simple properties represented by a single column. */
@@ -570,6 +574,13 @@ export declare class UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys
570
574
  export interface EmptyOptions extends Partial<Record<UniversalPropertyKeys, unknown>> {
571
575
  }
572
576
  /** @internal */
577
+ export declare class StringPropertyOptionsBuilder<Value, Options> extends UniversalPropertyOptionsBuilder<Value, Options, IncludeKeysForProperty> {
578
+ trim(): StringPropertyOptionsBuilder<Value, Options>;
579
+ lowercase(): StringPropertyOptionsBuilder<Value, Options>;
580
+ uppercase(): StringPropertyOptionsBuilder<Value, Options>;
581
+ private withOptions;
582
+ }
583
+ /** @internal */
573
584
  export declare class OneToManyOptionsBuilderOnlyMappedBy<Value extends object> extends UniversalPropertyOptionsBuilder<Value, EmptyOptions & {
574
585
  kind: '1:m';
575
586
  }, IncludeKeysForOneToManyOptions> {
@@ -582,7 +593,7 @@ type EntityTarget = {
582
593
  '~entity': any;
583
594
  } | EntityClass;
584
595
  declare const propertyBuilders: PropertyBuilders;
585
- type PropertyBuildersOverrideKeys = 'bigint' | 'array' | 'decimal' | 'json' | 'datetime' | 'time' | 'enum';
596
+ type PropertyBuildersOverrideKeys = 'bigint' | 'array' | 'decimal' | 'json' | 'string' | 'text' | 'datetime' | 'time' | 'enum';
586
597
  /** Map of factory functions for creating type-safe property builders (scalars, enums, embeddables, and relations). */
587
598
  export type PropertyBuilders = {
588
599
  [K in Exclude<keyof typeof types, PropertyBuildersOverrideKeys>]: () => UniversalPropertyOptionsBuilder<InferPropertyValueType<(typeof types)[K]>, EmptyOptions, IncludeKeysForProperty>;
@@ -591,6 +602,8 @@ export type PropertyBuilders = {
591
602
  array: <T = string>(toJsValue?: (i: string) => T, toDbValue?: (i: T) => string) => UniversalPropertyOptionsBuilder<InferPropertyValueType<typeof types.array<T>>, EmptyOptions, IncludeKeysForProperty>;
592
603
  decimal: <Mode extends 'number' | 'string' = 'string'>(mode?: Mode) => UniversalPropertyOptionsBuilder<InferPropertyValueType<typeof types.decimal<Mode>>, EmptyOptions, IncludeKeysForProperty>;
593
604
  json: <T>() => UniversalPropertyOptionsBuilder<T, EmptyOptions, IncludeKeysForProperty>;
605
+ string: () => StringPropertyOptionsBuilder<InferPropertyValueType<typeof types.string>, EmptyOptions>;
606
+ text: () => StringPropertyOptionsBuilder<InferPropertyValueType<typeof types.text>, EmptyOptions>;
594
607
  formula: <T>(formula: string | FormulaCallback<any>) => UniversalPropertyOptionsBuilder<T, EmptyOptions, IncludeKeysForProperty>;
595
608
  datetime: (length?: number) => UniversalPropertyOptionsBuilder<InferPropertyValueType<typeof types.datetime>, EmptyOptions, IncludeKeysForProperty>;
596
609
  time: (length?: number) => UniversalPropertyOptionsBuilder<InferPropertyValueType<typeof types.time>, EmptyOptions, IncludeKeysForProperty>;
@@ -638,6 +651,9 @@ export interface EntityMetadataWithProperties<TName extends string, TTableName e
638
651
  entity?: EntityName<any> | EntityName<any>[];
639
652
  args?: boolean;
640
653
  strict?: boolean;
654
+ rls?: boolean | {
655
+ setting?: string;
656
+ };
641
657
  }>;
642
658
  forceObject?: TForceObject;
643
659
  embeddable?: TEmbeddable;
@@ -726,16 +742,18 @@ type InferTypeByString<T extends string> = T extends keyof typeof types ? InferJ
726
742
  type InferJSType<T> = T extends typeof Type<infer TValue, any> ? NonNullable<TValue> : never;
727
743
  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
744
  type BaseEntityMethodKeys = 'toObject' | 'toPOJO' | 'serialize' | 'assign' | 'populate' | 'init' | 'toReference';
745
+ interface BaseEntityMethods<in out Entity extends object> extends Pick<IWrappedEntity<Entity>, BaseEntityMethodKeys> {
746
+ }
729
747
  /** Infers the entity type from a `defineEntity()` properties map, resolving builders, base classes, and primary keys. */
730
748
  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
749
  toObject(...args: any[]): any;
732
- } ? Pick<IWrappedEntity<{
750
+ } ? BaseEntityMethods<{
733
751
  -readonly [K in keyof Properties]: InferBuilderValue<MaybeReturnType<Properties[K]>>;
734
752
  } & {
735
753
  [PrimaryKeyProp]?: InferCombinedPrimaryKey<Properties, PK, Base>;
736
754
  } & (IsNever<Repository> extends true ? {} : {
737
755
  [EntityRepositoryType]?: Repository extends Constructor<infer R> ? R : Repository;
738
- }) & NarrowDiscriminator<Omit<Base, typeof PrimaryKeyProp>, BaseDiscriminatorColumn, DiscriminatorValue, Embeddable>>, BaseEntityMethodKeys> : {}) & {
756
+ }) & NarrowDiscriminator<Omit<Base, typeof PrimaryKeyProp>, BaseDiscriminatorColumn, DiscriminatorValue, Embeddable>> : {}) & {
739
757
  -readonly [K in keyof Properties]: InferBuilderValue<MaybeReturnType<Properties[K]>>;
740
758
  } & {
741
759
  [PrimaryKeyProp]?: InferCombinedPrimaryKey<Properties, PK, Base>;