@mikro-orm/core 7.2.0-dev.2 → 7.2.0-dev.21

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 (86) hide show
  1. package/EntityManager.d.ts +42 -8
  2. package/EntityManager.js +256 -70
  3. package/MikroORM.d.ts +4 -0
  4. package/MikroORM.js +9 -0
  5. package/cache/CacheAdapter.d.ts +6 -4
  6. package/cache/FileCacheAdapter.d.ts +1 -1
  7. package/cache/FileCacheAdapter.js +7 -2
  8. package/connections/Connection.d.ts +10 -1
  9. package/connections/Connection.js +9 -0
  10. package/drivers/DatabaseDriver.d.ts +17 -1
  11. package/drivers/DatabaseDriver.js +203 -41
  12. package/drivers/IDatabaseDriver.d.ts +1 -0
  13. package/entity/Collection.js +4 -2
  14. package/entity/EntityFactory.js +6 -0
  15. package/entity/EntityLoader.d.ts +7 -1
  16. package/entity/EntityLoader.js +46 -11
  17. package/entity/EntityRepository.d.ts +4 -5
  18. package/entity/EntityRepository.js +7 -2
  19. package/entity/defineEntity.d.ts +48 -14
  20. package/entity/defineEntity.js +32 -1
  21. package/enums.d.ts +5 -1
  22. package/enums.js +2 -0
  23. package/errors.d.ts +36 -0
  24. package/errors.js +90 -0
  25. package/events/EventManager.js +6 -3
  26. package/exceptions.d.ts +5 -0
  27. package/exceptions.js +5 -0
  28. package/hydration/ObjectHydrator.d.ts +2 -0
  29. package/hydration/ObjectHydrator.js +15 -8
  30. package/index.d.ts +1 -1
  31. package/metadata/EntitySchema.js +5 -2
  32. package/metadata/MetadataDiscovery.d.ts +3 -0
  33. package/metadata/MetadataDiscovery.js +161 -28
  34. package/metadata/MetadataProvider.js +1 -1
  35. package/metadata/MetadataStorage.d.ts +4 -3
  36. package/metadata/MetadataStorage.js +32 -2
  37. package/metadata/Routine.js +4 -1
  38. package/metadata/types.d.ts +19 -3
  39. package/naming-strategy/AbstractNamingStrategy.js +2 -1
  40. package/naming-strategy/NamingStrategy.d.ts +2 -1
  41. package/package.json +1 -1
  42. package/platforms/Platform.d.ts +24 -3
  43. package/platforms/Platform.js +63 -1
  44. package/types/BigIntType.d.ts +1 -0
  45. package/types/BigIntType.js +23 -0
  46. package/types/DateTimeType.d.ts +1 -0
  47. package/types/DateTimeType.js +8 -0
  48. package/types/StringType.d.ts +14 -3
  49. package/types/StringType.js +34 -4
  50. package/types/TextType.d.ts +2 -4
  51. package/types/TextType.js +2 -8
  52. package/types/Type.d.ts +11 -0
  53. package/types/Type.js +4 -4
  54. package/types/index.d.ts +2 -2
  55. package/typings.d.ts +58 -2
  56. package/typings.js +24 -1
  57. package/unit-of-work/ChangeSet.js +10 -7
  58. package/unit-of-work/ChangeSetPersister.js +32 -20
  59. package/unit-of-work/UnitOfWork.js +11 -4
  60. package/utils/AbstractMigrator.d.ts +1 -1
  61. package/utils/AbstractMigrator.js +3 -2
  62. package/utils/Configuration.d.ts +15 -1
  63. package/utils/Configuration.js +13 -2
  64. package/utils/Cursor.d.ts +2 -0
  65. package/utils/Cursor.js +44 -39
  66. package/utils/DataloaderUtils.js +2 -1
  67. package/utils/EntityComparator.d.ts +2 -0
  68. package/utils/EntityComparator.js +11 -4
  69. package/utils/QueryHelper.d.ts +17 -0
  70. package/utils/QueryHelper.js +100 -4
  71. package/utils/RawQueryFragment.d.ts +6 -0
  72. package/utils/RawQueryFragment.js +15 -6
  73. package/utils/RequestContext.d.ts +2 -2
  74. package/utils/RequestContext.js +11 -2
  75. package/utils/TransactionManager.d.ts +6 -0
  76. package/utils/TransactionManager.js +36 -3
  77. package/utils/Utils.d.ts +14 -2
  78. package/utils/Utils.js +36 -6
  79. package/utils/clone.js +6 -0
  80. package/utils/env-vars.js +1 -0
  81. package/utils/index.d.ts +1 -0
  82. package/utils/index.js +1 -0
  83. package/utils/rls-utils.d.ts +35 -0
  84. package/utils/rls-utils.js +97 -0
  85. package/utils/upsert-utils.d.ts +9 -1
  86. package/utils/upsert-utils.js +26 -3
package/MikroORM.d.ts CHANGED
@@ -82,6 +82,10 @@ export declare class MikroORM<Driver extends IDatabaseDriver = IDatabaseDriver,
82
82
  * Closes the database connection.
83
83
  */
84
84
  close(force?: boolean): Promise<void>;
85
+ /**
86
+ * Closes the database connection, allows using the ORM instance with `await using`.
87
+ */
88
+ [Symbol.asyncDispose](): Promise<void>;
85
89
  /**
86
90
  * Gets the `MetadataStorage`.
87
91
  */
package/MikroORM.js CHANGED
@@ -2,6 +2,7 @@ import { MetadataDiscovery } from './metadata/MetadataDiscovery.js';
2
2
  import { MetadataStorage } from './metadata/MetadataStorage.js';
3
3
  import { Configuration } from './utils/Configuration.js';
4
4
  import { loadEnvironmentVars } from './utils/env-vars.js';
5
+ import { clearRlsFilterDefsCache } from './utils/rls-utils.js';
5
6
  import { Utils } from './utils/Utils.js';
6
7
  import { colors } from './logging/colors.js';
7
8
  async function tryRegisterExtension(name, pkg, extensions) {
@@ -159,6 +160,12 @@ export class MikroORM {
159
160
  await this.config.getMetadataCacheAdapter()?.close?.();
160
161
  await this.config.getResultCacheAdapter()?.close?.();
161
162
  }
163
+ /**
164
+ * Closes the database connection, allows using the ORM instance with `await using`.
165
+ */
166
+ async [Symbol.asyncDispose]() {
167
+ await this.close();
168
+ }
162
169
  /**
163
170
  * Gets the `MetadataStorage` (without parameters) or `EntityMetadata` instance when provided with the `entityName` parameter.
164
171
  */
@@ -189,6 +196,8 @@ export class MikroORM {
189
196
  meta.root = this.#metadata.get(meta.root.class);
190
197
  }
191
198
  this.#metadata.decorate(this.em);
199
+ // the newly discovered entities may declare `rls` filters the cached lookup was built without
200
+ clearRlsFilterDefsCache(this.#metadata);
192
201
  }
193
202
  /**
194
203
  * Gets the SchemaGenerator.
@@ -1,9 +1,10 @@
1
1
  /** Interface for async-capable cache storage used by result cache and metadata cache. */
2
2
  export interface CacheAdapter {
3
3
  /**
4
- * Gets the items under `name` key from the cache.
4
+ * Gets the items under `name` key from the cache. When `origin` is provided, adapters that
5
+ * track the entry origin should ignore entries cached from a different source file.
5
6
  */
6
- get<T = any>(name: string): T | Promise<T | undefined> | undefined;
7
+ get<T = any>(name: string, origin?: string): T | Promise<T | undefined> | undefined;
7
8
  /**
8
9
  * Sets the item to the cache. `origin` is used for cache invalidation and should reflect the change in data.
9
10
  */
@@ -24,9 +25,10 @@ export interface CacheAdapter {
24
25
  /** Synchronous variant of CacheAdapter, used for metadata cache where async access is not needed. */
25
26
  export interface SyncCacheAdapter extends CacheAdapter {
26
27
  /**
27
- * Gets the items under `name` key from the cache.
28
+ * Gets the items under `name` key from the cache. When `origin` is provided, adapters that
29
+ * track the entry origin should ignore entries cached from a different source file.
28
30
  */
29
- get<T = any>(name: string): T | undefined;
31
+ get<T = any>(name: string, origin?: string): T | undefined;
30
32
  /**
31
33
  * Sets the item to the cache. `origin` is used for cache invalidation and should reflect the change in data.
32
34
  */
@@ -8,7 +8,7 @@ export declare class FileCacheAdapter implements SyncCacheAdapter {
8
8
  /**
9
9
  * @inheritDoc
10
10
  */
11
- get(name: string): any;
11
+ get(name: string, origin?: string): any;
12
12
  /**
13
13
  * @inheritDoc
14
14
  */
@@ -16,12 +16,17 @@ export class FileCacheAdapter {
16
16
  /**
17
17
  * @inheritDoc
18
18
  */
19
- get(name) {
19
+ get(name, origin) {
20
20
  const path = this.path(name);
21
21
  if (!existsSync(path)) {
22
22
  return null;
23
23
  }
24
24
  const payload = fs.readJSONSync(path);
25
+ // Two classes with the same name share the cache key, ignore entries cached
26
+ // from a different source file.
27
+ if (origin && fs.absolutePath(payload.origin, this.#baseDir) !== fs.absolutePath(origin, this.#baseDir)) {
28
+ return null;
29
+ }
25
30
  const hash = this.getHash(payload.origin);
26
31
  if (!hash || payload.hash !== hash) {
27
32
  return null;
@@ -71,7 +76,7 @@ export class FileCacheAdapter {
71
76
  let path = typeof this.#options.combined === 'string' ? this.#options.combined : './metadata.json';
72
77
  path = fs.normalizePath(this.#options.cacheDir, path);
73
78
  this.#options.combined = path; // override in the options, so we can log it from the CLI in `cache:generate` command
74
- writeFileSync(path, JSON.stringify(this.#cache, null, this.#pretty ? 2 : undefined));
79
+ writeFileSync(path, JSON.stringify(this.#cache, null, this.#pretty ? 2 : undefined), { flush: true });
75
80
  return path;
76
81
  }
77
82
  path(name) {
@@ -1,7 +1,7 @@
1
1
  import { type Configuration, type ConnectionOptions } from '../utils/Configuration.js';
2
2
  import type { LogContext, Logger } from '../logging/Logger.js';
3
3
  import type { MetadataStorage } from '../metadata/MetadataStorage.js';
4
- import type { ConnectionType, Dictionary, MaybePromise, Primary, RoutineProperty } from '../typings.js';
4
+ import type { ConnectionType, Dictionary, MaybePromise, Primary, RoutineProperty, SessionContext } from '../typings.js';
5
5
  import type { Routine } from '../metadata/Routine.js';
6
6
  import type { Platform } from '../platforms/Platform.js';
7
7
  import type { Type } from '../types/Type.js';
@@ -51,6 +51,13 @@ export declare abstract class Connection {
51
51
  * This method doesn't support transactions, as opposed to `orm.schema.execute()`, which is used internally.
52
52
  */
53
53
  executeDump(dump: string): Promise<void>;
54
+ /**
55
+ * Returns the underlying database client the connection drives — e.g. the `pg` pool, the
56
+ * `better-sqlite3` database, or the `PGlite` instance — for vendor APIs MikroORM does not wrap.
57
+ * Each driver narrows the return type to its own client. Its lifecycle belongs to the ORM, so
58
+ * leave closing it to `orm.close()` unless you supplied the client yourself via `driverOptions`.
59
+ */
60
+ getNativeClient(): Promise<unknown>;
54
61
  protected onConnect(): Promise<void>;
55
62
  /** Executes a callback inside a transaction, committing on success and rolling back on failure. */
56
63
  transactional<T>(cb: (trx: Transaction) => Promise<T>, options?: {
@@ -59,6 +66,7 @@ export declare abstract class Connection {
59
66
  ctx?: Transaction;
60
67
  eventBroadcaster?: TransactionEventBroadcaster;
61
68
  loggerContext?: LogContext;
69
+ sessionContext?: SessionContext;
62
70
  }): Promise<T>;
63
71
  /** Begins a new database transaction and returns the transaction context. */
64
72
  begin(options?: {
@@ -67,6 +75,7 @@ export declare abstract class Connection {
67
75
  ctx?: Transaction;
68
76
  eventBroadcaster?: TransactionEventBroadcaster;
69
77
  loggerContext?: LogContext;
78
+ sessionContext?: SessionContext;
70
79
  }): Promise<Transaction>;
71
80
  /** Commits the given transaction. */
72
81
  commit(ctx: Transaction, eventBroadcaster?: TransactionEventBroadcaster, loggerContext?: LogContext): Promise<void>;
@@ -65,6 +65,15 @@ export class Connection {
65
65
  async executeDump(dump) {
66
66
  throw new Error(`Executing SQL dumps is not supported by current driver`);
67
67
  }
68
+ /**
69
+ * Returns the underlying database client the connection drives — e.g. the `pg` pool, the
70
+ * `better-sqlite3` database, or the `PGlite` instance — for vendor APIs MikroORM does not wrap.
71
+ * Each driver narrows the return type to its own client. Its lifecycle belongs to the ORM, so
72
+ * leave closing it to `orm.close()` unless you supplied the client yourself via `driverOptions`.
73
+ */
74
+ async getNativeClient() {
75
+ throw new Error(`Accessing the native client is not supported by current driver`);
76
+ }
68
77
  async onConnect() {
69
78
  const schemaGenerator = this.config.getExtension('@mikro-orm/schema-generator');
70
79
  if (this.type === 'write' && schemaGenerator) {
@@ -64,7 +64,23 @@ export declare abstract class DatabaseDriver<C extends Connection> implements ID
64
64
  orderBy: OrderDefinition<T>[];
65
65
  where: FilterQuery<T>;
66
66
  };
67
- protected createCursorCondition<T extends object>(definition: (readonly [keyof T & string, QueryOrder])[], offsets: Dictionary[], inverse: boolean, meta: EntityMetadata<T>): FilterQuery<T>;
67
+ /**
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).
81
+ */
82
+ private mapCursorOffset;
83
+ protected createCursorCondition<T extends object>(definition: (readonly [keyof T & string, QueryOrder])[], offsets: Dictionary[], inverse: boolean, meta: EntityMetadata<T>, fromJson?: boolean): FilterQuery<T>;
68
84
  /** @internal */
69
85
  mapDataToFieldNames(data: Dictionary, stringifyJsonArrays: boolean, properties?: Record<string, EntityProperty>, convertCustomTypes?: boolean, object?: boolean): Dictionary;
70
86
  protected inlineEmbeddables<T extends object>(meta: EntityMetadata<T>, data: T, where?: boolean): void;
@@ -7,8 +7,10 @@ import { EntityManager } from '../EntityManager.js';
7
7
  import { CursorError, ValidationError } from '../errors.js';
8
8
  import { DriverException } from '../exceptions.js';
9
9
  import { helper } from '../entity/wrap.js';
10
+ import { Reference } from '../entity/Reference.js';
10
11
  import { PolymorphicRef } from '../entity/PolymorphicRef.js';
11
12
  import { JsonType } from '../types/JsonType.js';
13
+ import { QueryHelper } from '../utils/QueryHelper.js';
12
14
  import { MikroORM } from '../MikroORM.js';
13
15
  /** Abstract base class for all database drivers, implementing common driver logic. */
14
16
  export class DatabaseDriver {
@@ -141,14 +143,32 @@ export class DatabaseDriver {
141
143
  return !!val && typeof val === 'object' && key in val;
142
144
  };
143
145
  const createCursor = (val, key, inverse = false) => {
144
- let def = isCursor(val, key) ? val[key] : val;
145
- if (Utils.isPlainObject(def)) {
146
- def = Cursor.for(meta, def, orderBy);
146
+ const def = Reference.unwrapReference((isCursor(val, key) ? val[key] : val));
147
+ let offsets;
148
+ let fromJson = false;
149
+ // entity (and reference) instances are supported as cursors too, their properties are read the same way
150
+ if (Utils.isPlainObject(def) || Utils.isEntity(def)) {
151
+ // POJO values are already JS values, extract them ordered per the definition,
152
+ // without the JSON round trip `Cursor.for` + `Cursor.decode` would impose
153
+ offsets = definition.map(([key]) => {
154
+ if (def[key] === undefined) {
155
+ throw CursorError.missingValue(meta.className, key);
156
+ }
157
+ return def[key];
158
+ });
147
159
  }
148
- /* v8 ignore next */
149
- const offsets = def ? Cursor.decode(def) : [];
150
- if (definition.length === offsets.length) {
151
- return this.createCursorCondition(definition, offsets, inverse, meta);
160
+ else {
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;
169
+ }
170
+ if (definition.length > 0 && definition.length === offsets.length) {
171
+ return this.createCursorCondition(definition, offsets, inverse, meta, fromJson);
152
172
  }
153
173
  /* v8 ignore next */
154
174
  return {};
@@ -162,62 +182,194 @@ export class DatabaseDriver {
162
182
  if (limit != null) {
163
183
  options.limit = limit + (overfetch ? 1 : 0);
164
184
  }
165
- const createOrderBy = (prop, direction) => {
185
+ const createOrderBy = (prop, direction, properties = meta.properties) => {
186
+ const propMeta = properties[prop];
166
187
  if (Utils.isPlainObject(direction)) {
188
+ const childProps = propMeta?.kind === ReferenceKind.EMBEDDED ? propMeta.embeddedProps : propMeta?.targetMeta?.properties;
167
189
  const value = Utils.getObjectQueryKeys(direction).reduce((o, key) => {
168
- Object.assign(o, createOrderBy(key, direction[key]));
190
+ Object.assign(o, createOrderBy(key, direction[key], childProps ?? {}));
169
191
  return o;
170
192
  }, {});
171
193
  return { [prop]: value };
172
194
  }
173
- const desc = direction === QueryOrderNumeric.DESC || direction.toString().toLowerCase() === 'desc';
195
+ const { desc, nullsFirst, explicit } = this.parseCursorDirection(direction);
174
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
+ }
175
203
  return { [prop]: dir };
176
204
  };
205
+ // the cursor condition is created at the driver level, after the EM already converted custom types
206
+ // in the user `where`, so we need to run the same conversion over it explicitly
207
+ const where = QueryHelper.processWhere({
208
+ where: ($and.length > 1 ? { $and } : { ...$and[0] }),
209
+ entityName: meta.class,
210
+ metadata: this.metadata,
211
+ platform: this.platform,
212
+ convertCustomTypes: options.convertCustomTypes,
213
+ });
177
214
  return {
178
215
  orderBy: definition.map(([prop, direction]) => createOrderBy(prop, direction)),
179
- where: ($and.length > 1 ? { $and } : { ...$and[0] }),
216
+ where,
180
217
  };
181
218
  }
182
- createCursorCondition(definition, offsets, inverse, meta) {
183
- const createCondition = (prop, direction, offset, eq = false, path = prop) => {
184
- if (Utils.isPlainObject(direction)) {
185
- if (offset === undefined) {
186
- throw CursorError.missingValue(meta.className, path);
219
+ /**
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.
224
+ */
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) {
244
+ if (Utils.isScalarReference(value)) {
245
+ value = value.unwrap();
246
+ }
247
+ // scalar direction on a relation orders by its primary key
248
+ if (Utils.isEntity(value, true)) {
249
+ value = helper(value).getPrimaryKey();
250
+ }
251
+ if (value == null) {
252
+ return value;
253
+ }
254
+ // mongo preserves native date types inside JSON documents, restored JS values compare directly
255
+ if (insideJson && !this.platform.preservesDatesInsideJson()) {
256
+ // compared against the JSON document, which holds the JSON form of the database value
257
+ if (value instanceof Date) {
258
+ return value.toISOString();
259
+ }
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
+ }
187
275
  }
188
- const value = Utils.keys(direction).reduce((o, key) => {
189
- Object.assign(o, createCondition(key, direction[key], offset?.[key], eq, `${path}.${key}`));
190
- return o;
191
- }, {});
192
- return { [prop]: value };
276
+ return restored;
193
277
  }
194
- const isDesc = direction === QueryOrderNumeric.DESC || direction.toString().toLowerCase() === 'desc';
195
- const dirStr = direction.toString().toLowerCase();
196
- let nullsFirst;
197
- if (dirStr.includes('nulls first')) {
198
- nullsFirst = true;
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);
199
284
  }
200
- else if (dirStr.includes('nulls last')) {
201
- nullsFirst = false;
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);
202
299
  }
203
- else {
204
- // Default: NULLS LAST for ASC, NULLS FIRST for DESC (matches most databases)
205
- nullsFirst = isDesc;
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);
305
+ }
306
+ return value;
307
+ }
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) => {
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;
313
+ if (Utils.isPlainObject(direction)) {
314
+ if (offset === undefined) {
315
+ throw CursorError.missingValue(meta.className, path);
316
+ }
317
+ // POJO cursors can carry entity, reference or embeddable class instances, read their properties directly
318
+ offset = Reference.unwrapReference(offset);
319
+ const childProps = propMeta?.kind === ReferenceKind.EMBEDDED ? propMeta.embeddedProps : propMeta?.targetMeta?.properties;
320
+ insideJson ||=
321
+ (propMeta?.kind === ReferenceKind.EMBEDDED && !!propMeta.object) || propMeta?.customType instanceof JsonType;
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 } : {};
206
345
  }
346
+ const { desc: isDesc, nullsFirst } = this.parseCursorDirection(direction);
207
347
  const operator = Utils.xor(isDesc, inverse) ? '$lt' : '$gt';
208
348
  // For leaf-level properties, undefined means missing value
209
349
  if (offset === undefined) {
210
350
  throw CursorError.missingValue(meta.className, path);
211
351
  }
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
+ }
212
363
  // Handle null offset (intentional null cursor value)
213
364
  if (offset === null) {
365
+ // hasItemsAfterNull: forward + nullsFirst, or backward + nullsLast
366
+ const hasItemsAfterNull = Utils.xor(nullsFirst, inverse);
214
367
  if (eq) {
215
- // Equal to null
216
- 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 };
217
371
  }
218
372
  // Strict comparison with null cursor value
219
- // hasItemsAfterNull: forward + nullsFirst, or backward + nullsLast
220
- const hasItemsAfterNull = Utils.xor(nullsFirst, inverse);
221
373
  if (hasItemsAfterNull) {
222
374
  return { [prop]: { $ne: null } };
223
375
  }
@@ -225,7 +377,12 @@ export class DatabaseDriver {
225
377
  return { [prop]: [] };
226
378
  }
227
379
  // Non-null offset
228
- 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;
229
386
  };
230
387
  const [order, ...otherOrders] = definition;
231
388
  const [offset, ...otherOffsets] = offsets;
@@ -233,13 +390,18 @@ export class DatabaseDriver {
233
390
  if (!otherOrders.length) {
234
391
  return createCondition(prop, direction, offset);
235
392
  }
236
- return {
237
- ...createCondition(prop, direction, offset, true),
393
+ const atOrPast = createCondition(prop, direction, offset, true);
394
+ const past = {
238
395
  $or: [
239
396
  createCondition(prop, direction, offset),
240
- this.createCursorCondition(otherOrders, otherOffsets, inverse, meta),
397
+ this.createCursorCondition(otherOrders, otherOffsets, inverse, meta, fromJson),
241
398
  ],
242
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 };
243
405
  }
244
406
  /** @internal */
245
407
  mapDataToFieldNames(data, stringifyJsonArrays, properties, convertCustomTypes, object) {
@@ -359,8 +521,8 @@ export class DatabaseDriver {
359
521
  const props = prop.embeddedProps;
360
522
  let unknownProp = false;
361
523
  Object.keys(data[prop.name]).forEach(kk => {
362
- // explicitly allow `$exists`, `$eq`, `$ne` and `$elemMatch` operators here as they can't be misused this way
363
- 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));
364
526
  if (operator) {
365
527
  throw ValidationError.cannotUseOperatorsInsideEmbeddables(meta.class, prop.name, data);
366
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;
@@ -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
  }
@@ -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;
@@ -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;