@mikro-orm/core 7.1.16-dev.10 → 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 (59) hide show
  1. package/EntityManager.d.ts +41 -7
  2. package/EntityManager.js +202 -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 +145 -55
  11. package/entity/Collection.js +4 -2
  12. package/entity/EntityLoader.js +1 -1
  13. package/entity/EntityRepository.d.ts +4 -5
  14. package/entity/EntityRepository.js +2 -1
  15. package/entity/defineEntity.d.ts +17 -1
  16. package/entity/defineEntity.js +31 -0
  17. package/enums.d.ts +3 -1
  18. package/errors.d.ts +35 -0
  19. package/errors.js +87 -0
  20. package/exceptions.d.ts +5 -0
  21. package/exceptions.js +5 -0
  22. package/index.d.ts +1 -1
  23. package/metadata/MetadataDiscovery.d.ts +3 -0
  24. package/metadata/MetadataDiscovery.js +94 -7
  25. package/metadata/types.d.ts +19 -3
  26. package/package.json +1 -1
  27. package/platforms/Platform.d.ts +19 -1
  28. package/platforms/Platform.js +56 -0
  29. package/types/BigIntType.d.ts +1 -0
  30. package/types/BigIntType.js +23 -0
  31. package/types/DateTimeType.d.ts +1 -0
  32. package/types/DateTimeType.js +8 -0
  33. package/types/StringType.d.ts +14 -3
  34. package/types/StringType.js +34 -4
  35. package/types/TextType.d.ts +2 -4
  36. package/types/TextType.js +2 -8
  37. package/types/Type.d.ts +11 -0
  38. package/types/index.d.ts +2 -2
  39. package/typings.d.ts +47 -0
  40. package/typings.js +1 -0
  41. package/unit-of-work/UnitOfWork.js +1 -0
  42. package/utils/Configuration.d.ts +21 -1
  43. package/utils/Configuration.js +12 -1
  44. package/utils/Cursor.d.ts +2 -0
  45. package/utils/Cursor.js +43 -33
  46. package/utils/QueryHelper.d.ts +12 -0
  47. package/utils/QueryHelper.js +63 -0
  48. package/utils/RawQueryFragment.d.ts +6 -0
  49. package/utils/RawQueryFragment.js +15 -6
  50. package/utils/RequestContext.d.ts +2 -2
  51. package/utils/RequestContext.js +11 -2
  52. package/utils/TransactionManager.js +1 -1
  53. package/utils/Utils.d.ts +2 -0
  54. package/utils/Utils.js +7 -2
  55. package/utils/env-vars.js +2 -0
  56. package/utils/index.d.ts +1 -0
  57. package/utils/index.js +1 -0
  58. package/utils/rls-utils.d.ts +35 -0
  59. 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,21 +182,23 @@ 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 dirStr = direction.toString().toLowerCase();
188
- const desc = direction === QueryOrderNumeric.DESC || dirStr.startsWith('desc');
189
- let dir = Utils.xor(desc, isLast) ? 'desc' : 'asc';
190
- const nullsFirst = dirStr.includes('nulls first');
191
- // backward pagination reverses the whole ordering, so the nulls placement flips with the direction
192
- if (nullsFirst || dirStr.includes('nulls last')) {
193
- dir += Utils.xor(nullsFirst, isLast) ? ' nulls first' : ' nulls last';
195
+ const { desc, nullsFirst, explicit } = this.parseCursorDirection(direction);
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}` };
194
202
  }
195
203
  return { [prop]: dir };
196
204
  };
@@ -209,12 +217,30 @@ export class DatabaseDriver {
209
217
  };
210
218
  }
211
219
  /**
212
- * Restores the JS value of a single cursor offset: ISO strings become `Date` instances based on the
213
- * property type (never based on the string shape alone), and custom types are restored via
214
- * `convertToJSValue`. Values compared against a JSON document keep their serialized form instead,
215
- * 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.
216
224
  */
217
- 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) {
218
244
  if (Utils.isScalarReference(value)) {
219
245
  value = value.unwrap();
220
246
  }
@@ -225,25 +251,65 @@ export class DatabaseDriver {
225
251
  if (value == null) {
226
252
  return value;
227
253
  }
254
+ // mongo preserves native date types inside JSON documents, restored JS values compare directly
228
255
  if (insideJson && !this.platform.preservesDatesInsideJson()) {
229
- // 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
230
257
  if (value instanceof Date) {
231
258
  return value.toISOString();
232
259
  }
233
- // restore the JS value from the serialized form, `processWhere` then converts it to
234
- // the database form, which is what the JSON document holds for custom typed props
235
- 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;
236
279
  }
237
- if (typeof value === 'string' &&
238
- (prop?.runtimeType === 'Date' ||
239
- (prop?.customType && this.platform.getMappedType(prop.columnTypes?.[0] ?? '') instanceof DateTimeType))) {
240
- value = new Date(value);
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;
302
+ }
303
+ if (typeof value === 'string' && prop?.runtimeType === 'Date') {
304
+ return new Date(value);
241
305
  }
242
- return prop?.customType ? prop.customType.convertToJSValue(value, this.platform) : value;
306
+ return value;
243
307
  }
244
- createCursorCondition(definition, offsets, inverse, meta) {
245
- 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) => {
246
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;
247
313
  if (Utils.isPlainObject(direction)) {
248
314
  if (offset === undefined) {
249
315
  throw CursorError.missingValue(meta.className, path);
@@ -253,40 +319,54 @@ export class DatabaseDriver {
253
319
  const childProps = propMeta?.kind === ReferenceKind.EMBEDDED ? propMeta.embeddedProps : propMeta?.targetMeta?.properties;
254
320
  insideJson ||=
255
321
  (propMeta?.kind === ReferenceKind.EMBEDDED && !!propMeta.object) || propMeta?.customType instanceof JsonType;
256
- const value = Utils.keys(direction).reduce((o, key) => {
257
- Object.assign(o, createCondition(key, direction[key], offset?.[key], eq, `${path}.${key}`, childProps ?? {}, insideJson));
258
- return o;
259
- }, {});
260
- // an unconstrained group must stay unconstrained, an empty object condition would instead
261
- // match only rows where every child value is null (e.g. object embeddables)
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]: {} }`
262
344
  return Utils.hasObjectKeys(value) ? { [prop]: value } : {};
263
345
  }
264
- const dirStr = direction.toString().toLowerCase();
265
- const isDesc = direction === QueryOrderNumeric.DESC || dirStr.startsWith('desc');
266
- let nullsFirst;
267
- if (dirStr.includes('nulls first')) {
268
- nullsFirst = true;
269
- }
270
- else if (dirStr.includes('nulls last')) {
271
- nullsFirst = false;
272
- }
273
- else {
274
- // Default: NULLS LAST for ASC, NULLS FIRST for DESC (matches most databases)
275
- nullsFirst = isDesc;
276
- }
346
+ const { desc: isDesc, nullsFirst } = this.parseCursorDirection(direction);
277
347
  const operator = Utils.xor(isDesc, inverse) ? '$lt' : '$gt';
278
348
  // For leaf-level properties, undefined means missing value
279
349
  if (offset === undefined) {
280
350
  throw CursorError.missingValue(meta.className, path);
281
351
  }
282
- 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
+ }
283
363
  // Handle null offset (intentional null cursor value)
284
364
  if (offset === null) {
285
365
  // hasItemsAfterNull: forward + nullsFirst, or backward + nullsLast
286
366
  const hasItemsAfterNull = Utils.xor(nullsFirst, inverse);
287
367
  if (eq) {
288
- // the `>=` half of the keyset condition: every row when the nulls come first in this
289
- // direction, only the null ones when they come last
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
290
370
  return hasItemsAfterNull ? {} : { [prop]: null };
291
371
  }
292
372
  // Strict comparison with null cursor value
@@ -297,7 +377,12 @@ export class DatabaseDriver {
297
377
  return { [prop]: [] };
298
378
  }
299
379
  // Non-null offset
300
- 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;
301
386
  };
302
387
  const [order, ...otherOrders] = definition;
303
388
  const [offset, ...otherOffsets] = offsets;
@@ -305,13 +390,18 @@ export class DatabaseDriver {
305
390
  if (!otherOrders.length) {
306
391
  return createCondition(prop, direction, offset);
307
392
  }
308
- return {
309
- ...createCondition(prop, direction, offset, true),
393
+ const atOrPast = createCondition(prop, direction, offset, true);
394
+ const past = {
310
395
  $or: [
311
396
  createCondition(prop, direction, offset),
312
- this.createCursorCondition(otherOrders, otherOffsets, inverse, meta),
397
+ this.createCursorCondition(otherOrders, otherOffsets, inverse, meta, fromJson),
313
398
  ],
314
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 };
315
405
  }
316
406
  /** @internal */
317
407
  mapDataToFieldNames(data, stringifyJsonArrays, properties, convertCustomTypes, object) {
@@ -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
  }
@@ -635,7 +635,7 @@ export class EntityLoader {
635
635
  if (!Utils.isEmpty(prop.where)) {
636
636
  where = { $and: [where, prop.where] };
637
637
  }
638
- 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));
639
639
  const children = [];
640
640
  const isUnionTargetMN = QueryHelper.isUnionTargetPolymorphic(prop);
641
641
  for (let i = 0; i < filtered.length; i++) {
@@ -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);
@@ -148,6 +148,8 @@ export interface PropertyChain<in out Value, in out 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;
@@ -528,6 +530,8 @@ export declare class UniversalPropertyOptionsBuilder<in out Value, in out Option
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<in out Value, in out Option
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;
@@ -403,6 +403,10 @@ export class UniversalPropertyOptionsBuilder {
403
403
  pivotTable(pivotTable) {
404
404
  return this.assignOptions({ pivotTable });
405
405
  }
406
+ /** 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}). */
407
+ through(through) {
408
+ return this.assignOptions({ through });
409
+ }
406
410
  /** Set pivot entity for this relation (see {@doclink collections#custom-pivot-table-entity | Custom pivot table entity}). */
407
411
  pivotEntity(pivotEntity) {
408
412
  return this.assignOptions({ pivotEntity });
@@ -472,6 +476,27 @@ export class UniversalPropertyOptionsBuilder {
472
476
  }
473
477
  }
474
478
  /** @internal */
479
+ export class StringPropertyOptionsBuilder extends UniversalPropertyOptionsBuilder {
480
+ trim() {
481
+ return this.withOptions({ trim: true });
482
+ }
483
+ lowercase() {
484
+ return this.withOptions({ case: 'lower' });
485
+ }
486
+ uppercase() {
487
+ return this.withOptions({ case: 'upper' });
488
+ }
489
+ withOptions(options) {
490
+ const type = this['~options'].type;
491
+ const TypeClass = typeof type === 'function' ? type : type.constructor;
492
+ const currentOptions = typeof type === 'function' ? {} : type.options;
493
+ return new StringPropertyOptionsBuilder({
494
+ ...this['~options'],
495
+ type: new TypeClass({ ...currentOptions, ...options }),
496
+ });
497
+ }
498
+ }
499
+ /** @internal */
475
500
  export class OneToManyOptionsBuilderOnlyMappedBy extends UniversalPropertyOptionsBuilder {
476
501
  /** Point to the owning side property name. */
477
502
  mappedBy(mappedBy) {
@@ -487,6 +512,12 @@ const propertyBuilders = {
487
512
  array: (toJsValue = i => i, toDbValue = i => i) => new UniversalPropertyOptionsBuilder({ type: new types.array(toJsValue, toDbValue) }),
488
513
  decimal: (mode) => new UniversalPropertyOptionsBuilder({ type: new types.decimal(mode) }),
489
514
  json: () => new UniversalPropertyOptionsBuilder({ type: types.json }),
515
+ string: () => new StringPropertyOptionsBuilder({
516
+ type: types.string,
517
+ }),
518
+ text: () => new StringPropertyOptionsBuilder({
519
+ type: types.text,
520
+ }),
490
521
  formula: (formula) => new UniversalPropertyOptionsBuilder({ formula }),
491
522
  datetime: (length) => new UniversalPropertyOptionsBuilder({ type: types.datetime, length }),
492
523
  time: (length) => new UniversalPropertyOptionsBuilder({ type: types.time, length }),
package/enums.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { EntityKey, ExpandProperty } from './typings.js';
1
+ import type { EntityKey, ExpandProperty, SessionContext } from './typings.js';
2
2
  import type { InflightQueryAbortStrategy, Transaction } from './connections/Connection.js';
3
3
  import type { LogContext } from './logging/Logger.js';
4
4
  /** Controls when the `EntityManager` flushes pending changes to the database. */
@@ -309,6 +309,8 @@ export interface TransactionOptions {
309
309
  flushMode?: FlushMode | `${FlushMode}`;
310
310
  ignoreNestedTransactions?: boolean;
311
311
  loggerContext?: LogContext;
312
+ /** @internal database session context applied on `begin()` (set via `em.setSessionContext()`). */
313
+ sessionContext?: SessionContext;
312
314
  /**
313
315
  * `AbortSignal` cancelling every query within the transaction (including the implicit flush).
314
316
  * Cancelling mid-transaction triggers a rollback once the in-flight query settles.
package/errors.d.ts CHANGED
@@ -25,6 +25,13 @@ export declare class ValidationError<T extends AnyEntity = AnyEntity> extends Er
25
25
  static invalidCompositeIdentifier(meta: EntityMetadata): ValidationError;
26
26
  static cannotCommit(): ValidationError;
27
27
  static cannotUseGlobalContext(): ValidationError;
28
+ static sessionContextNotSupported(): ValidationError;
29
+ static sessionContextRequiresImplicitTransactions(): ValidationError;
30
+ static sessionContextWithDisabledTransactions(): ValidationError;
31
+ static sessionContextInsideTransaction(action?: 'set' | 'clear'): ValidationError;
32
+ static cannotStageNonScalarSessionVariable(filterName: string, argName: string): ValidationError;
33
+ static sessionContextStreamRequiresTransaction(): ValidationError;
34
+ static connectionSessionContextNotSupported(): ValidationError;
28
35
  static cannotUseOperatorsInsideEmbeddables(entityName: EntityName, propName: string, payload: unknown): ValidationError;
29
36
  static cannotUseGroupOperatorsInsideScalars(entityName: EntityName, propName: string, payload: unknown): ValidationError;
30
37
  static invalidEmbeddableQuery(entityName: EntityName, propName: string, embeddableType: string): ValidationError;
@@ -34,6 +41,7 @@ export declare class ValidationError<T extends AnyEntity = AnyEntity> extends Er
34
41
  export declare class CursorError<T extends AnyEntity = AnyEntity> extends ValidationError<T> {
35
42
  static entityNotPopulated(entity: AnyEntity, prop: string): ValidationError;
36
43
  static missingValue(entityName: string, prop: string): ValidationError;
44
+ static invalidCursor(entityName: string, cause: Error): CursorError;
37
45
  }
38
46
  /** Error thrown when an optimistic lock conflict is detected during entity persistence. */
39
47
  export declare class OptimisticLockError<T extends AnyEntity = AnyEntity> extends ValidationError<T> {
@@ -66,6 +74,9 @@ export declare class MetadataError<T extends AnyEntity = AnyEntity> extends Vali
66
74
  static targetIsAbstract(meta: EntityMetadata, prop: EntityProperty): MetadataError;
67
75
  static nonPersistentCompositeProp(meta: EntityMetadata, prop: EntityProperty): MetadataError;
68
76
  static propertyTargetsEntityType(meta: EntityMetadata, prop: EntityProperty, target: EntityMetadata): MetadataError;
77
+ static throughRelationMissingProperty(meta: EntityMetadata, prop: EntityProperty, through: EntityMetadata, side: 'owner' | 'target'): MetadataError;
78
+ static throughRelationCompositeTarget(meta: EntityMetadata, prop: EntityProperty): MetadataError;
79
+ static throughRelationInvalidKind(meta: EntityMetadata, prop: EntityProperty): MetadataError;
69
80
  static fromMissingOption(meta: EntityMetadata, prop: EntityProperty, option: string): MetadataError;
70
81
  static targetKeyOnManyToMany(meta: EntityMetadata, prop: EntityProperty): MetadataError;
71
82
  static targetKeyNotUnique(meta: EntityMetadata, prop: EntityProperty, target?: EntityMetadata): MetadataError;
@@ -77,6 +88,30 @@ export declare class MetadataError<T extends AnyEntity = AnyEntity> extends Vali
77
88
  static tptNotSupportedByDriver(meta: EntityMetadata): MetadataError;
78
89
  /** Thrown when database triggers are defined on an entity using a driver that does not support them. */
79
90
  static triggersNotSupportedByDriver(meta: EntityMetadata): MetadataError;
91
+ /** Thrown when row level security is declared on an entity using a driver that does not support it. */
92
+ static rowLevelSecurityNotSupportedByDriver(meta: EntityMetadata): MetadataError;
93
+ /** Thrown when row level security is declared on a non-root entity of an STI hierarchy. */
94
+ static rowLevelSecurityOnNonRootStiEntity(meta: EntityMetadata): MetadataError;
95
+ /** Thrown when two policies on the same entity are given the same explicit name. */
96
+ static duplicatePolicyName(meta: EntityMetadata, name: string): MetadataError;
97
+ /** Thrown when a filter flagged with `rls` is declared on a driver that does not support row level security. */
98
+ static rlsFilterNotSupportedByDriver(meta: EntityMetadata, filterName: string): MetadataError;
99
+ /** Thrown when a filter flagged with `rls` is declared on a non-root entity of an STI hierarchy. */
100
+ static rlsFilterOnNonRootStiEntity(meta: EntityMetadata, filterName: string): MetadataError;
101
+ /** Thrown when a global (config or EM registered) filter is flagged with `rls`; RLS filters must be entity scoped. */
102
+ static rlsFilterMustBeEntityScoped(filterName: string): MetadataError;
103
+ /** Thrown when an entity-scoped `rls` filter is registered at runtime via `em.addFilter()` instead of in metadata. */
104
+ static rlsFilterCannotBeRegisteredAtRuntime(filterName: string): MetadataError;
105
+ /** Thrown when a filter's custom `setting` is used with more than one argument. */
106
+ static rlsFilterMultiArgSetting(filterName: string, args: string[]): MetadataError;
107
+ /** Thrown when an `rls` filter's condition depends on runtime state and cannot be compiled to a static policy. */
108
+ static rlsFilterDependsOnRuntimeState(filterName: string): MetadataError;
109
+ /** Thrown when an `rls` filter compares against a column whose type has no automatic session-variable cast. */
110
+ static rlsFilterUncastableType(filterName: string, columnType: string): MetadataError;
111
+ /** Thrown when an `rls` filter references an argument outside of a direct comparison, which cannot be compiled. */
112
+ static rlsFilterUnsupportedCond(filterName: string): MetadataError;
113
+ /** Thrown when an `rls` filter compares against a column the schema generator does not manage. */
114
+ static rlsFilterUnmanagedColumn(filterName: string, column: string): MetadataError;
80
115
  private static fromMessage;
81
116
  }
82
117
  /** Error thrown when an entity lookup fails to find the expected result. */