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

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 +34 -5
  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
@@ -3,7 +3,7 @@ import { PolymorphicRef } from '../entity/PolymorphicRef.js';
3
3
  import { helper } from '../entity/wrap.js';
4
4
  import { ChangeSetType } from './ChangeSet.js';
5
5
  import { isRaw } from '../utils/RawQueryFragment.js';
6
- import { Utils } from '../utils/Utils.js';
6
+ import { equals, Utils } from '../utils/Utils.js';
7
7
  import { OptimisticLockError, ValidationError } from '../errors.js';
8
8
  import { ReferenceKind } from '../enums.js';
9
9
  /** @internal Executes change sets against the database, handling inserts, updates, and deletes. */
@@ -204,13 +204,14 @@ export class ChangeSetPersister {
204
204
  }
205
205
  checkConcurrencyKeys(meta, changeSet, cond) {
206
206
  const tmp = [];
207
- for (const key of meta.concurrencyCheckKeys) {
207
+ const keys = meta.getOwnConcurrencyCheckKeys();
208
+ for (const key of keys) {
208
209
  cond[key] = changeSet.originalEntity[key];
209
210
  if (changeSet.payload[key]) {
210
211
  tmp.push(key);
211
212
  }
212
213
  }
213
- if (tmp.length === 0 && meta.concurrencyCheckKeys.size > 0) {
214
+ if (tmp.length === 0 && keys.length > 0) {
214
215
  throw OptimisticLockError.lockFailed(changeSet.entity);
215
216
  }
216
217
  }
@@ -230,11 +231,15 @@ export class ChangeSetPersister {
230
231
  }
231
232
  const res = await this.#driver.nativeUpdateMany(meta.class, cond, payload, options);
232
233
  const map = new Map();
233
- res.rows?.forEach(item => map.set(Utils.getCompositeKeyHash(item, meta, true, this.#platform, true), item));
234
+ // returning rows are not mapped yet, so they are keyed by field names - we need to build the hash
235
+ // from those to be able to match them with `getSerializedPrimaryKey()` of the entity
236
+ const pkFields = meta.getPrimaryProps().flatMap(prop => prop.fieldNames);
237
+ res.rows?.forEach(item => map.set(Utils.getPrimaryKeyHash(pkFields.map(field => item[field])), item));
234
238
  for (const changeSet of changeSets) {
235
239
  if (res.rows) {
236
240
  const row = map.get(helper(changeSet.entity).getSerializedPrimaryKey());
237
- this.mapReturnedValues(changeSet.entity, changeSet.payload, row, meta);
241
+ // STI batches can mix child types, so map through the change set's own metadata
242
+ this.mapReturnedValues(changeSet.entity, changeSet.payload, row, changeSet.meta);
238
243
  }
239
244
  changeSet.persisted = true;
240
245
  }
@@ -299,32 +304,33 @@ export class ChangeSetPersister {
299
304
  options = this.prepareOptions(meta, options, {
300
305
  convertCustomTypes: false,
301
306
  });
302
- if (meta.concurrencyCheckKeys.size === 0 &&
303
- (!meta.versionProperty || changeSet.entity[meta.versionProperty] == null)) {
307
+ if (meta.getOwnConcurrencyCheckKeys().length === 0 &&
308
+ (!meta.ownsVersionProperty() || changeSet.entity[meta.versionProperty] == null)) {
304
309
  return this.#driver.nativeUpdate(changeSet.meta.class, cond, changeSet.payload, options);
305
310
  }
306
- if (meta.versionProperty) {
311
+ if (meta.ownsVersionProperty()) {
307
312
  cond[meta.versionProperty] = this.#platform.convertVersionValue(changeSet.entity[meta.versionProperty], meta.properties[meta.versionProperty]);
308
313
  }
309
314
  this.checkConcurrencyKeys(meta, changeSet, cond);
310
315
  return this.#driver.nativeUpdate(changeSet.meta.class, cond, changeSet.payload, options);
311
316
  }
312
317
  async checkOptimisticLocks(meta, changeSets, options) {
313
- if (meta.concurrencyCheckKeys.size === 0 &&
314
- (!meta.versionProperty || changeSets.every(cs => cs.entity[meta.versionProperty] == null))) {
318
+ const concurrencyCheckKeys = meta.getOwnConcurrencyCheckKeys();
319
+ if (concurrencyCheckKeys.length === 0 &&
320
+ (!meta.ownsVersionProperty() || changeSets.every(cs => cs.entity[meta.versionProperty] == null))) {
315
321
  return;
316
322
  }
317
323
  // skip entity references as they don't have version values loaded
318
324
  changeSets = changeSets.filter(cs => helper(cs.entity).__initialized);
325
+ const primaryKeys = meta.primaryKeys.concat(...concurrencyCheckKeys);
319
326
  const $or = changeSets.map(cs => {
320
- const cond = Utils.getPrimaryKeyCond(cs.originalEntity, meta.primaryKeys.concat(...meta.concurrencyCheckKeys));
321
- if (meta.versionProperty) {
327
+ const cond = Utils.getPrimaryKeyCond(cs.originalEntity, primaryKeys);
328
+ if (meta.ownsVersionProperty()) {
322
329
  // @ts-ignore
323
330
  cond[meta.versionProperty] = this.#platform.convertVersionValue(cs.entity[meta.versionProperty], meta.properties[meta.versionProperty]);
324
331
  }
325
332
  return cond;
326
333
  });
327
- const primaryKeys = meta.primaryKeys.concat(...meta.concurrencyCheckKeys);
328
334
  options = this.prepareOptions(meta, options, {
329
335
  fields: primaryKeys,
330
336
  orderBy: meta.primaryKeys.reduce((o, pk) => {
@@ -332,9 +338,12 @@ export class ChangeSetPersister {
332
338
  return o;
333
339
  }, {}),
334
340
  });
335
- const res = await this.#driver.find(meta.root.class, { $or }, options);
341
+ // TPT tables query their own metadata, as the version column might not live on the root table
342
+ const target = meta.inheritanceType === 'tpt' ? meta : meta.root;
343
+ const res = await this.#driver.find(target.class, { $or }, options);
336
344
  if (res.length !== changeSets.length) {
337
- const compare = (a, b, keys) => keys.every(k => a[k] === b[k]);
345
+ // a FK pointing to a composite PK is an array, so the values need to be compared deeply
346
+ const compare = (a, b, keys) => keys.every(k => equals(a[k], b[k]));
338
347
  const entity = changeSets.find(cs => {
339
348
  return !res.some(row => compare(Utils.getPrimaryKeyCond(cs.entity, primaryKeys), row, primaryKeys));
340
349
  }).entity;
@@ -342,7 +351,7 @@ export class ChangeSetPersister {
342
351
  }
343
352
  }
344
353
  checkOptimisticLock(meta, changeSet, res) {
345
- if ((meta.versionProperty || meta.concurrencyCheckKeys.size > 0) && res && !res.affectedRows) {
354
+ if ((meta.ownsVersionProperty() || meta.getOwnConcurrencyCheckKeys().length > 0) && res && !res.affectedRows) {
346
355
  throw OptimisticLockError.lockFailed(changeSet.entity);
347
356
  }
348
357
  }
@@ -351,7 +360,7 @@ export class ChangeSetPersister {
351
360
  * so we use a single query in case of both versioning and default values is used.
352
361
  */
353
362
  async reloadVersionValues(meta, changeSets, options) {
354
- const reloadProps = meta.versionProperty && !this.#usesReturningStatement ? [meta.properties[meta.versionProperty]] : [];
363
+ const reloadProps = meta.ownsVersionProperty() && !this.#usesReturningStatement ? [meta.properties[meta.versionProperty]] : [];
355
364
  if (changeSets[0].type === ChangeSetType.CREATE) {
356
365
  for (const prop of meta.props) {
357
366
  if (prop.persist === false) {
@@ -375,7 +384,8 @@ export class ChangeSetPersister {
375
384
  changeSets.forEach(cs => {
376
385
  Utils.keys(cs.payload).forEach(k => {
377
386
  if (isRaw(cs.payload[k]) && isRaw(cs.entity[k])) {
378
- returning.add(meta.properties[k]);
387
+ // STI batches can mix child types, so the property might not exist on `meta`
388
+ returning.add(cs.meta.properties[k]);
379
389
  }
380
390
  });
381
391
  });
@@ -400,12 +410,14 @@ export class ChangeSetPersister {
400
410
  options = this.prepareOptions(meta, options, {
401
411
  fields: Utils.unique(reloadProps.map(prop => prop.name)),
402
412
  });
403
- const data = await this.#driver.find(meta.class, { [pk]: { $in: pks } }, options);
413
+ // a mixed STI batch shares one table but the child discriminator would filter out the siblings
414
+ const target = changeSets.some(cs => cs.meta !== meta) && meta.root.discriminatorColumn ? meta.root : meta;
415
+ const data = await this.#driver.find(target.class, { [pk]: { $in: pks } }, options);
404
416
  const map = new Map();
405
417
  data.forEach(item => map.set(Utils.getCompositeKeyHash(item, meta, false, this.#platform, true), item));
406
418
  for (const changeSet of changeSets) {
407
419
  const data = map.get(helper(changeSet.entity).getSerializedPrimaryKey());
408
- this.#hydrator.hydrate(changeSet.entity, meta, data, this.#factory, 'full', false, true);
420
+ this.#hydrator.hydrate(changeSet.entity, changeSet.meta, data, this.#factory, 'full', false, true);
409
421
  Object.assign(changeSet.payload, data); // merge to the changeset payload, so it gets saved to the entity snapshot
410
422
  }
411
423
  }
@@ -124,7 +124,6 @@ export class UnitOfWork {
124
124
  if (options?.newEntity) {
125
125
  return entity;
126
126
  }
127
- const forceUndefined = this.#em.config.get('forceUndefined');
128
127
  const wrapped = helper(entity);
129
128
  if (options?.loaded && wrapped.__initialized && !wrapped.__onLoadFired) {
130
129
  this.#loadedEntities.add(entity);
@@ -332,6 +331,7 @@ export class UnitOfWork {
332
331
  let parentCs = changeSet.tptChangeSets.find(pc => pc.meta === current);
333
332
  if (!parentCs) {
334
333
  parentCs = new ChangeSet(entity, changeSet.type, {}, current);
334
+ parentCs.originalEntity = changeSet.originalEntity;
335
335
  changeSet.tptChangeSets.splice(idx, 0, parentCs);
336
336
  }
337
337
  idx++;
@@ -472,6 +472,7 @@ export class UnitOfWork {
472
472
  const loggerContext = Utils.merge({ id: this.#em._id }, this.#em.getLoggerContext({ disableContextResolution: true }));
473
473
  await this.#em.getConnection('write').transactional(trx => this.persistToDatabase(groups, trx), {
474
474
  ctx: oldTx,
475
+ sessionContext: this.#em.getTransactionSessionContext(),
475
476
  eventBroadcaster: new TransactionEventBroadcaster(this.#em),
476
477
  loggerContext,
477
478
  });
@@ -527,6 +528,10 @@ export class UnitOfWork {
527
528
  if (prop.formula) {
528
529
  delete referrer[prop.name];
529
530
  }
531
+ else if (prop.primary) {
532
+ // the referrer's primary key contains the removed entity, so its identity cannot survive the removal
533
+ this.unsetIdentity(referrer);
534
+ }
530
535
  else {
531
536
  delete helper(referrer).__data[prop.name];
532
537
  }
@@ -702,13 +707,14 @@ export class UnitOfWork {
702
707
  payload[pk] = identifiers[i] ?? originalChangeSet.payload[pk];
703
708
  }
704
709
  }
705
- if (!isCreate && Object.keys(payload).length === 0) {
710
+ // the table declaring the version property or a concurrency check still needs its bump and lock check
711
+ if (!isCreate && Object.keys(payload).length === 0 && !current.hasOptimisticLock()) {
706
712
  current = current.tptParent;
707
713
  continue;
708
714
  }
709
715
  const cs = new ChangeSet(entity, originalChangeSet.type, payload, current);
716
+ cs.originalEntity = originalChangeSet.originalEntity;
710
717
  if (current === meta) {
711
- cs.originalEntity = originalChangeSet.originalEntity;
712
718
  leafCs = cs;
713
719
  }
714
720
  else {
@@ -1205,7 +1211,8 @@ export class UnitOfWork {
1205
1211
  const addToGroup = (cs) => {
1206
1212
  // Skip stub TPT changesets with empty payload (e.g. leaf with no own-property changes on UPDATE)
1207
1213
  if ((cs.type === ChangeSetType.UPDATE || cs.type === ChangeSetType.UPDATE_EARLY) &&
1208
- !Utils.hasObjectKeys(cs.payload)) {
1214
+ !Utils.hasObjectKeys(cs.payload) &&
1215
+ !cs.meta.hasOptimisticLock()) {
1209
1216
  return;
1210
1217
  }
1211
1218
  const group = groups[cs.type];
@@ -103,7 +103,7 @@ export declare abstract class AbstractMigrator<D extends IDatabaseDriver> implem
103
103
  name: string;
104
104
  path: string;
105
105
  }): RunnableMigration;
106
- protected initialize(MigrationClass: Constructor<Migration>, name: string): RunnableMigration;
106
+ protected initialize(MigrationClass: Constructor<Migration>, name?: string): RunnableMigration;
107
107
  /**
108
108
  * Checks if `src` folder exists, it so, tries to adjust the migrations and seeders paths automatically to use it.
109
109
  * If there is a `dist` or `build` folder, it will be used for the JS variant (`path` option), while the `src` folder will be
@@ -358,7 +358,8 @@ export class AbstractMigrator {
358
358
  initialize(MigrationClass, name) {
359
359
  const instance = new MigrationClass(this.driver, this.config);
360
360
  return {
361
- name: this.storage.getMigrationName(name),
361
+ // the constructor name is the last resort, minifiers can mangle it (e.g. when bundling)
362
+ name: this.storage.getMigrationName(name ?? instance.name ?? MigrationClass.name),
362
363
  up: afterRun => this.runner.run(instance, 'up', afterRun),
363
364
  down: afterRun => this.runner.run(instance, 'down', afterRun),
364
365
  };
@@ -409,7 +410,7 @@ export class AbstractMigrator {
409
410
  if (this.options.migrationsList) {
410
411
  return this.options.migrationsList.map(migration => {
411
412
  if (typeof migration === 'function') {
412
- return this.initialize(migration, migration.name);
413
+ return this.initialize(migration);
413
414
  }
414
415
  return this.initialize(migration.class, migration.name);
415
416
  });
@@ -439,7 +439,7 @@ export interface Options<Driver extends IDatabaseDriver = IDatabaseDriver, EM ex
439
439
  */
440
440
  filters: Dictionary<{
441
441
  name?: string;
442
- } & Omit<FilterDef, 'name'>>;
442
+ } & Omit<FilterDef, 'name' | 'rls'>>;
443
443
  /**
444
444
  * Metadata discovery configuration options.
445
445
  * Controls how entities are discovered and validated.
@@ -480,6 +480,12 @@ export interface Options<Driver extends IDatabaseDriver = IDatabaseDriver, EM ex
480
480
  * @default false
481
481
  */
482
482
  disableTransactions?: boolean;
483
+ /**
484
+ * How `em.setSessionContext()` session variables/role are applied for row level security.
485
+ * `'transaction'` (default) emits `set_config(..., true)` inside each transaction; `'connection'` applies them on every pooled connection acquire (PostgreSQL only).
486
+ * @default 'transaction'
487
+ */
488
+ sessionContext?: 'transaction' | 'connection';
483
489
  /**
484
490
  * Enable verbose logging of internal operations.
485
491
  * @default false
@@ -840,6 +846,14 @@ export interface Options<Driver extends IDatabaseDriver = IDatabaseDriver, EM ex
840
846
  * @default false
841
847
  */
842
848
  ignoreRoutines?: boolean;
849
+ /**
850
+ * Leave row level security policies unmanaged. Declared policies are still created and RLS is still enabled or
851
+ * forced based on the entity metadata, but existing policies are never dropped or altered and RLS is never
852
+ * disabled or unforced — use this to protect hand-written policies from being removed when they are not
853
+ * mirrored in the entity definitions.
854
+ * @default false
855
+ */
856
+ ignorePolicies?: boolean;
843
857
  /**
844
858
  * Table names or patterns to skip during schema generation.
845
859
  * @default []
@@ -7,7 +7,7 @@ import { Utils } from '../utils/Utils.js';
7
7
  import { Routine } from '../metadata/Routine.js';
8
8
  import { MetadataValidator } from '../metadata/MetadataValidator.js';
9
9
  import { MetadataProvider } from '../metadata/MetadataProvider.js';
10
- import { NotFoundError } from '../errors.js';
10
+ import { MetadataError, NotFoundError, ValidationError } from '../errors.js';
11
11
  import { RequestContext } from './RequestContext.js';
12
12
  import { DataloaderType, FlushMode, LoadStrategy, PopulateHint } from '../enums.js';
13
13
  import { MemoryCacheAdapter } from '../cache/MemoryCacheAdapter.js';
@@ -71,6 +71,7 @@ const DEFAULTS = {
71
71
  ensureDatabase: true,
72
72
  ensureIndexes: false,
73
73
  batchSize: 300,
74
+ sessionContext: 'transaction',
74
75
  debug: false,
75
76
  ignoreDeprecations: false,
76
77
  verbose: false,
@@ -86,13 +87,15 @@ const DEFAULTS = {
86
87
  snapshot: true,
87
88
  snapshotOnMigrate: true,
88
89
  emit: 'ts',
89
- fileName: (timestamp, name) => `Migration${timestamp}${name ? '_' + name : ''}`,
90
+ // mirrors `NamingStrategy.classToMigrationName`, so the file name matches the class it declares
91
+ fileName: (timestamp, name) => `Migration${timestamp}${name ? '_' + name.replace(/[^$\p{ID_Continue}]+/gu, '_') : ''}`,
90
92
  },
91
93
  schemaGenerator: {
92
94
  createForeignKeyConstraints: true,
93
95
  ignoreSchema: [],
94
96
  ignoreTriggers: false,
95
97
  ignoreRoutines: false,
98
+ ignorePolicies: false,
96
99
  skipTables: [],
97
100
  skipViews: [],
98
101
  skipColumns: {},
@@ -392,7 +395,15 @@ export class Configuration {
392
395
  }
393
396
  this.#options.schema ??= this.#platform.getDefaultSchemaName();
394
397
  this.#options.charset ??= this.#platform.getDefaultCharset();
398
+ // fail closed instead of silently applying no session state on drivers without the reserve hook (e.g. pglite)
399
+ if (this.#options.sessionContext === 'connection' && !this.#platform.supportsConnectionSessionContext()) {
400
+ throw ValidationError.connectionSessionContextNotSupported();
401
+ }
395
402
  Object.keys(this.#options.filters).forEach(key => {
403
+ // global filters have no entity to attach a policy to, so `rls` is only valid on entity-scoped filters
404
+ if (this.#options.filters[key].rls) {
405
+ throw MetadataError.rlsFilterMustBeEntityScoped(key);
406
+ }
396
407
  this.#options.filters[key].default ??= true;
397
408
  });
398
409
  if (!this.#options.filtersOnRelations) {
package/utils/Cursor.d.ts CHANGED
@@ -61,6 +61,8 @@ export declare class Cursor<Entity extends object, Hint extends string = never,
61
61
  * Computes the cursor value for a given entity.
62
62
  */
63
63
  from(entity: Entity | Loaded<Entity, Hint, Fields, Excludes>): string;
64
+ /** Serializes a single cursor value, walking nested directions and reading the owner's properties. */
65
+ private static serialize;
64
66
  [Symbol.iterator](): IterableIterator<Loaded<Entity, Hint, Fields, Excludes>>;
65
67
  get length(): number;
66
68
  /**
package/utils/Cursor.js CHANGED
@@ -58,6 +58,7 @@ export class Cursor {
58
58
  hasPrevPage;
59
59
  hasNextPage;
60
60
  #definition;
61
+ #meta;
61
62
  constructor(items, totalCount, options, meta) {
62
63
  this.items = items;
63
64
  this.totalCount = totalCount;
@@ -76,6 +77,7 @@ export class Cursor {
76
77
  }
77
78
  }
78
79
  this.#definition = Cursor.getDefinition(meta, orderBy);
80
+ this.#meta = meta;
79
81
  }
80
82
  get startCursor() {
81
83
  if (this.items.length === 0) {
@@ -93,37 +95,46 @@ export class Cursor {
93
95
  * Computes the cursor value for a given entity.
94
96
  */
95
97
  from(entity) {
96
- const processEntity = (entity, prop, direction, object = false) => {
97
- if (Utils.isPlainObject(direction)) {
98
- const unwrapped = Reference.unwrapReference(entity[prop]);
99
- // Check if the relation is loaded - for nested properties, undefined means not populated
100
- if (Utils.isEntity(unwrapped) && !helper(unwrapped).isInitialized()) {
101
- throw CursorError.entityNotPopulated(entity, prop);
102
- }
103
- return Utils.keys(direction).reduce((o, key) => {
104
- Object.assign(o, processEntity(unwrapped, key, direction[key], true));
105
- return o;
106
- }, {});
107
- }
108
- let value = entity[prop];
109
- // Allow null/undefined values in cursor - they will be handled in createCursorCondition
110
- // undefined can occur with forceUndefined config option which converts null to undefined
111
- if (value == null) {
112
- return object ? { [prop]: null } : null;
113
- }
114
- if (Utils.isEntity(value, true)) {
115
- value = helper(value).getPrimaryKey();
116
- }
117
- if (Utils.isScalarReference(value)) {
118
- value = value.unwrap();
98
+ const value = this.#definition.map(([key, direction]) => Cursor.serialize(this.#meta.properties, entity, key, direction));
99
+ return Cursor.encode(value);
100
+ }
101
+ /** Serializes a single cursor value, walking nested directions and reading the owner's properties. */
102
+ static serialize(properties, owner, key, direction) {
103
+ const prop = properties[key];
104
+ let value = owner[key];
105
+ if (Utils.isPlainObject(direction)) {
106
+ const unwrapped = Reference.unwrapReference(value);
107
+ // for nested properties, an uninitialized relation means not populated
108
+ if (Utils.isEntity(unwrapped) && !helper(unwrapped).isInitialized()) {
109
+ throw CursorError.entityNotPopulated(owner, key);
119
110
  }
120
- if (object) {
121
- return { [prop]: value };
111
+ if (unwrapped == null || typeof unwrapped !== 'object') {
112
+ return unwrapped;
122
113
  }
123
- return value;
124
- };
125
- const value = this.#definition.map(([key, direction]) => processEntity(entity, key, direction));
126
- return Cursor.encode(value);
114
+ const childProps = prop?.kind === ReferenceKind.EMBEDDED ? prop.embeddedProps : prop?.targetMeta?.properties;
115
+ return Utils.keys(direction).reduce((o, childKey) => {
116
+ o[childKey] = Cursor.serialize(childProps ?? {}, unwrapped, childKey, direction[childKey]);
117
+ return o;
118
+ }, {});
119
+ }
120
+ // allow null/undefined values in cursor - they will be handled in createCursorCondition
121
+ // undefined can occur with forceUndefined config option which converts null to undefined
122
+ if (value == null) {
123
+ return null;
124
+ }
125
+ if (Utils.isEntity(value, true)) {
126
+ value = helper(value).getPrimaryKey();
127
+ }
128
+ if (Utils.isScalarReference(value)) {
129
+ value = value.unwrap();
130
+ }
131
+ // only types implementing `fromJSON` own their wire format, others keep the raw JS value,
132
+ // so their cursors stay decodable by the `convertToJSValue` fallback
133
+ if (prop?.customType?.fromJSON) {
134
+ // the platform is assigned to the type instance during discovery
135
+ return prop.customType.toJSON(value, prop.customType.platform);
136
+ }
137
+ return value;
127
138
  }
128
139
  *[Symbol.iterator]() {
129
140
  for (const item of this.items) {
@@ -138,24 +149,18 @@ export class Cursor {
138
149
  */
139
150
  static for(meta, entity, orderBy) {
140
151
  const definition = this.getDefinition(meta, orderBy);
141
- return Cursor.encode(definition.map(([key]) => {
142
- const value = entity[key];
143
- if (value === undefined) {
152
+ return Cursor.encode(definition.map(([key, direction]) => {
153
+ if (entity[key] === undefined) {
144
154
  throw CursorError.missingValue(meta.className, key);
145
155
  }
146
- return value;
156
+ return this.serialize(meta.properties, entity, key, direction);
147
157
  }));
148
158
  }
149
159
  static encode(value) {
150
160
  return Buffer.from(JSON.stringify(value)).toString('base64url');
151
161
  }
152
162
  static decode(value) {
153
- return JSON.parse(Buffer.from(value, 'base64url').toString('utf8')).map((value) => {
154
- if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}/.exec(value)) {
155
- return new Date(value);
156
- }
157
- return value;
158
- });
163
+ return JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
159
164
  }
160
165
  static getDefinition(meta, orderBy) {
161
166
  return Utils.asArray(orderBy).flatMap(order => {
@@ -197,7 +197,8 @@ export class DataloaderUtils {
197
197
  const prop = group[0][0].property;
198
198
  const options = {};
199
199
  const wrap = (cond) => ({ [prop.name]: cond });
200
- const orderBy = Utils.asArray(group[0][1]?.orderBy).map(o => wrap(o));
200
+ // `findChildrenFromPivotTable` expects the `orderBy` relative to the target entity, so no wrapping here
201
+ const orderBy = Utils.asArray(group[0][1]?.orderBy);
201
202
  const populate = wrap(group[0][1]?.populate);
202
203
  const owners = group.map(c => c[0].owner);
203
204
  const $or = [];
@@ -80,6 +80,8 @@ export declare class EntityComparator {
80
80
  private getGenericComparator;
81
81
  private getPropertyComparator;
82
82
  private wrap;
83
+ /** Renders a key as a single-quoted JS string literal, safe to embed in generated code. */
84
+ private quote;
83
85
  private safeKey;
84
86
  /**
85
87
  * Sets the toArray helper in the context if not already set.
@@ -164,7 +164,9 @@ export class EntityComparator {
164
164
  const lines = [];
165
165
  const context = new Map();
166
166
  context.set('isEntityOrRef', (val) => Utils.isEntity(val, true));
167
- context.set('getCompositeKeyValue', (val) => Utils.flatten(Utils.getCompositeKeyValue(val, meta, 'convertToDatabaseValue', this.#platform)));
167
+ context.set('getCompositeKeyValue', (val) =>
168
+ // deep flatten, nested composite PKs produce nested arrays that would be comma-joined by the hash
169
+ Utils.flatten(Utils.getCompositeKeyValue(val, meta, 'convertToDatabaseValue', this.#platform), true));
168
170
  context.set('getPrimaryKeyHash', (val) => Utils.getPrimaryKeyHash(Utils.asArray(val)));
169
171
  if (meta.primaryKeys.length > 1) {
170
172
  lines.push(` const pks = entity.__helper.__pk ? getCompositeKeyValue(entity.__helper.__pk) : [`);
@@ -447,6 +449,7 @@ export class EntityComparator {
447
449
  const ret = [];
448
450
  const padding = ' '.repeat(level * 2);
449
451
  const idx = this.#tmpIndex++;
452
+ ret.push(`${padding}if (entity${entityKey} === null) ret${dataKey} = null;`);
450
453
  ret.push(`${padding}if (Array.isArray(entity${entityKey})) {`);
451
454
  ret.push(`${padding} ret${dataKey} = [];`);
452
455
  ret.push(`${padding} entity${entityKey}.forEach((_, idx_${idx}) => {`);
@@ -584,7 +587,7 @@ export class EntityComparator {
584
587
  }
585
588
  }
586
589
  else if (prop.polymorphic) {
587
- const discriminatorMapKey = `discriminatorMapReverse_${prop.name}`;
590
+ const discriminatorMapKey = `discriminatorMapReverse_${this.safeKey(prop.name)}`;
588
591
  const reverseMap = new Map();
589
592
  for (const [key, value] of Object.entries(prop.discriminatorMap)) {
590
593
  reverseMap.set(value, key);
@@ -755,10 +758,14 @@ export class EntityComparator {
755
758
  return this.getGenericComparator(this.wrap(prop.name), `!equals(last${this.wrap(prop.name)}, current${this.wrap(prop.name)})`);
756
759
  }
757
760
  wrap(key) {
758
- if (/^\[.*]$/.exec(key)) {
761
+ if (/^\[idx_\d+]$/.exec(key)) {
759
762
  return key;
760
763
  }
761
- return /^\w+$/.exec(key) ? `.${key}` : `['${key}']`;
764
+ return /^\w+$/.exec(key) ? `.${key}` : `[${this.quote(key)}]`;
765
+ }
766
+ /** Renders a key as a single-quoted JS string literal, safe to embed in generated code. */
767
+ quote(key) {
768
+ return `'${key.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
762
769
  }
763
770
  safeKey(key) {
764
771
  return key.replace(/\W/g, '_');
@@ -37,9 +37,26 @@ export declare class QueryHelper {
37
37
  static inlinePrimaryKeyObjects<T extends object>(where: Dictionary, meta: EntityMetadata<T>, metadata: MetadataStorage, key?: string): boolean;
38
38
  static processWhere<T extends object>(options: ProcessWhereOptions<T>): FilterQuery<T>;
39
39
  static getActiveFilters<T>(meta: EntityMetadata<T>, options: FilterOptions | undefined, filters: Dictionary<FilterDef>): FilterDef[];
40
+ /** @internal Sentinel wrapping for arguments accessed while statically resolving an `rls` filter condition. */
41
+ static readonly RLS_SENTINEL_PREFIX = "__mikro_rls_arg__";
42
+ /** @internal */
43
+ static readonly RLS_SENTINEL_SUFFIX = "__";
44
+ /**
45
+ * Resolves an `rls` filter's condition to a static `FilterQuery`. Function conditions are called with a proxy `args`
46
+ * that yields a unique sentinel per accessed argument, real `type`/`entityName` strings (validated to not affect the
47
+ * result), and a poison proxy or `undefined` for the remaining runtime-only parameters.
48
+ *
49
+ * @internal
50
+ */
51
+ static resolveRlsFilterCond(filter: FilterDef, accessed: Set<string>, entityName?: string): Dictionary;
40
52
  static mergePropertyFilters(propFilters: FilterOptions | undefined, options: FilterOptions | undefined): FilterOptions | undefined;
41
53
  static isFilterActive<T>(meta: EntityMetadata<T>, filterName: string, filter: FilterDef, options: Dictionary<boolean | Dictionary>): boolean;
42
54
  static processCustomType<T extends object>(prop: EntityProperty<T>, cond: FilterQuery<T>, platform: Platform, key?: string, fromQuery?: boolean): FilterQuery<T>;
55
+ /**
56
+ * Composite PK conditions are keyed by a hash of all the PK names, which `findProperty` cannot
57
+ * resolve, so the custom types have to be applied positionally instead.
58
+ */
59
+ private static processCompositeCustomTypes;
43
60
  private static isSupportedOperator;
44
61
  private static processJsonCondition;
45
62
  static findProperty<T>(fieldName: string, options: ProcessWhereOptions<T>): EntityProperty<T> | undefined;