@mikro-orm/core 7.2.0-dev.8 → 7.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/EntityManager.d.ts +41 -7
  2. package/EntityManager.js +225 -45
  3. package/MikroORM.d.ts +4 -0
  4. package/MikroORM.js +9 -0
  5. package/README.md +1 -0
  6. package/connections/Connection.d.ts +3 -1
  7. package/drivers/DatabaseDriver.d.ts +14 -5
  8. package/drivers/DatabaseDriver.js +151 -52
  9. package/drivers/IDatabaseDriver.d.ts +1 -0
  10. package/entity/Collection.js +4 -2
  11. package/entity/EntityFactory.js +6 -0
  12. package/entity/EntityLoader.d.ts +7 -1
  13. package/entity/EntityLoader.js +46 -11
  14. package/entity/EntityRepository.d.ts +4 -5
  15. package/entity/EntityRepository.js +7 -2
  16. package/entity/defineEntity.d.ts +48 -14
  17. package/entity/defineEntity.js +32 -1
  18. package/enums.d.ts +5 -1
  19. package/enums.js +2 -0
  20. package/errors.d.ts +36 -0
  21. package/errors.js +90 -0
  22. package/events/EventManager.js +6 -3
  23. package/exceptions.d.ts +5 -0
  24. package/exceptions.js +5 -0
  25. package/hydration/ObjectHydrator.d.ts +2 -0
  26. package/hydration/ObjectHydrator.js +12 -8
  27. package/index.d.ts +1 -1
  28. package/metadata/MetadataDiscovery.d.ts +3 -0
  29. package/metadata/MetadataDiscovery.js +133 -18
  30. package/metadata/MetadataStorage.js +17 -1
  31. package/metadata/types.d.ts +19 -3
  32. package/package.json +1 -1
  33. package/platforms/Platform.d.ts +22 -3
  34. package/platforms/Platform.js +59 -1
  35. package/types/BigIntType.d.ts +1 -0
  36. package/types/BigIntType.js +23 -0
  37. package/types/DateTimeType.d.ts +1 -0
  38. package/types/DateTimeType.js +8 -0
  39. package/types/StringType.d.ts +14 -3
  40. package/types/StringType.js +34 -4
  41. package/types/TextType.d.ts +2 -4
  42. package/types/TextType.js +2 -8
  43. package/types/Type.d.ts +11 -0
  44. package/types/Type.js +4 -4
  45. package/types/index.d.ts +2 -2
  46. package/typings.d.ts +56 -2
  47. package/typings.js +24 -1
  48. package/unit-of-work/ChangeSetPersister.js +17 -13
  49. package/unit-of-work/UnitOfWork.js +11 -4
  50. package/utils/Configuration.d.ts +15 -1
  51. package/utils/Configuration.js +11 -1
  52. package/utils/Cursor.d.ts +2 -0
  53. package/utils/Cursor.js +43 -33
  54. package/utils/DataloaderUtils.js +2 -1
  55. package/utils/EntityComparator.d.ts +2 -0
  56. package/utils/EntityComparator.js +7 -3
  57. package/utils/QueryHelper.d.ts +12 -0
  58. package/utils/QueryHelper.js +75 -4
  59. package/utils/RawQueryFragment.d.ts +6 -0
  60. package/utils/RawQueryFragment.js +15 -6
  61. package/utils/TransactionManager.js +1 -1
  62. package/utils/Utils.d.ts +14 -2
  63. package/utils/Utils.js +31 -4
  64. package/utils/env-vars.js +1 -0
  65. package/utils/index.d.ts +1 -0
  66. package/utils/index.js +1 -0
  67. package/utils/rls-utils.d.ts +35 -0
  68. package/utils/rls-utils.js +97 -0
  69. package/utils/upsert-utils.d.ts +9 -1
  70. package/utils/upsert-utils.js +26 -3
@@ -1,8 +1,17 @@
1
1
  import { Type } from './Type.js';
2
- /** Maps a database VARCHAR column to a JS `string`. */
3
- export class StringType extends Type {
4
- getColumnType(prop, platform) {
5
- return platform.getVarcharTypeDeclarationSQL(prop);
2
+ /** @internal */
3
+ export class BaseStringType extends Type {
4
+ options;
5
+ constructor(options = {}) {
6
+ super();
7
+ this.options = options;
8
+ // a defined `compareValues` replaces the inline `!==` comparator, so only provide it when normalization is configured
9
+ if (options.trim || options.case) {
10
+ this.compareValues = (a, b) => this.normalize(a) === this.normalize(b);
11
+ }
12
+ }
13
+ convertToDatabaseValue(value) {
14
+ return this.normalize(value);
6
15
  }
7
16
  compareAsType() {
8
17
  return 'string';
@@ -10,6 +19,27 @@ export class StringType extends Type {
10
19
  ensureComparable() {
11
20
  return false;
12
21
  }
22
+ normalize(value) {
23
+ if (value == null) {
24
+ return value;
25
+ }
26
+ if (this.options.trim) {
27
+ value = value.trim();
28
+ }
29
+ if (this.options.case === 'upper') {
30
+ return value.toUpperCase();
31
+ }
32
+ if (this.options.case === 'lower') {
33
+ return value.toLowerCase();
34
+ }
35
+ return value;
36
+ }
37
+ }
38
+ /** Maps a database VARCHAR column to a JS `string`. */
39
+ export class StringType extends BaseStringType {
40
+ getColumnType(prop, platform) {
41
+ return platform.getVarcharTypeDeclarationSQL(prop);
42
+ }
13
43
  getDefaultLength(platform) {
14
44
  return platform.getDefaultVarcharLength();
15
45
  }
@@ -1,9 +1,7 @@
1
- import { Type } from './Type.js';
1
+ import { BaseStringType } from './StringType.js';
2
2
  import type { Platform } from '../platforms/Platform.js';
3
3
  import type { EntityProperty } from '../typings.js';
4
4
  /** Maps a database TEXT column (unbounded length) to a JS `string`. */
5
- export declare class TextType extends Type<string | null | undefined, string | null | undefined> {
5
+ export declare class TextType extends BaseStringType {
6
6
  getColumnType(prop: EntityProperty, platform: Platform): string;
7
- compareAsType(): string;
8
- ensureComparable(): boolean;
9
7
  }
package/types/TextType.js CHANGED
@@ -1,13 +1,7 @@
1
- import { Type } from './Type.js';
1
+ import { BaseStringType } from './StringType.js';
2
2
  /** Maps a database TEXT column (unbounded length) to a JS `string`. */
3
- export class TextType extends Type {
3
+ export class TextType extends BaseStringType {
4
4
  getColumnType(prop, platform) {
5
5
  return platform.getTextTypeDeclarationSQL(prop);
6
6
  }
7
- compareAsType() {
8
- return 'string';
9
- }
10
- ensureComparable() {
11
- return false;
12
- }
13
7
  }
package/types/Type.d.ts CHANGED
@@ -59,6 +59,17 @@ export declare abstract class Type<JSType = string, DBType = JSType> {
59
59
  * By default uses the runtime value.
60
60
  */
61
61
  toJSON(value: JSType, platform: Platform): JSType | DBType;
62
+ /**
63
+ * Converts a value from its serialized JSON form back to its JS representation. Used when
64
+ * decoding cursor values. The input is what `toJSON` produced, after a `JSON.parse` round
65
+ * trip, and never an already restored JS value. Cursors are client supplied, so the value
66
+ * can be any JSON shape: validate it and throw for values the type cannot restore, and
67
+ * `findByCursor` surfaces the failure as a `CursorError`.
68
+ * Implementing this method also makes cursor encoding use `toJSON`. Without it, cursors
69
+ * carry the raw JS value, and decoding falls back to `convertToJSValue`, with type-based
70
+ * `Date` restoration for date-like columns.
71
+ */
72
+ fromJSON?(value: unknown, platform: Platform): JSType;
62
73
  /**
63
74
  * Gets the SQL declaration snippet for a field of this type.
64
75
  */
package/types/Type.js CHANGED
@@ -55,11 +55,11 @@ export class Type {
55
55
  return prop.columnTypes?.[0] ?? platform.getTextTypeDeclarationSQL(prop);
56
56
  }
57
57
  static getType(cls) {
58
- const key = cls.name;
59
- if (!Type.types.has(key)) {
60
- Type.types.set(key, new cls());
58
+ // keyed by class reference, as minifiers can mangle two classes to the same name
59
+ if (!Type.types.has(cls)) {
60
+ Type.types.set(cls, new cls());
61
61
  }
62
- return Type.types.get(key);
62
+ return Type.types.get(cls);
63
63
  }
64
64
  /**
65
65
  * Checks whether the argument is instance of `Type`.
package/types/index.d.ts CHANGED
@@ -15,7 +15,7 @@ import { IntervalType } from './IntervalType.js';
15
15
  import { JsonType } from './JsonType.js';
16
16
  import { MediumIntType } from './MediumIntType.js';
17
17
  import { SmallIntType } from './SmallIntType.js';
18
- import { StringType } from './StringType.js';
18
+ import { type StringTypeOptions, StringType } from './StringType.js';
19
19
  import { TextType } from './TextType.js';
20
20
  import { TimeType } from './TimeType.js';
21
21
  import { TinyIntType } from './TinyIntType.js';
@@ -23,7 +23,7 @@ import { type IType, type TransformContext, Type } from './Type.js';
23
23
  import { Uint8ArrayType } from './Uint8ArrayType.js';
24
24
  import { UnknownType } from './UnknownType.js';
25
25
  import { UuidType } from './UuidType.js';
26
- export type { TransformContext, IType };
26
+ export type { TransformContext, IType, StringTypeOptions };
27
27
  export { Type, DateType, TimeType, DateTimeType, BigIntType, BlobType, Uint8ArrayType, ArrayType, EnumArrayType, EnumType, JsonType, IntegerType, SmallIntType, TinyIntType, MediumIntType, FloatType, DoubleType, BooleanType, DecimalType, StringType, UuidType, TextType, UnknownType, IntervalType, CharacterType, };
28
28
  /** Registry of all built-in type constructors, keyed by their short name (e.g., `types.integer`, `types.uuid`). */
29
29
  export declare const types: {
package/typings.d.ts CHANGED
@@ -314,6 +314,7 @@ export type OperatorMap<T> = {
314
314
  $ne?: ExpandScalar<T> | readonly ExpandScalar<T>[] | Subquery;
315
315
  $in?: readonly ExpandScalar<T>[] | readonly Primary<T>[] | Raw | Subquery;
316
316
  $nin?: readonly ExpandScalar<T>[] | readonly Primary<T>[] | Raw | Subquery;
317
+ $all?: readonly ExpandQuery<T>[];
317
318
  $not?: ExpandQuery<T>;
318
319
  $none?: ExpandQuery<T>;
319
320
  $some?: ExpandQuery<T>;
@@ -478,12 +479,12 @@ type NonArrayObject = object & {
478
479
  export type EntityDataProp<T, C extends boolean> = T extends Date ? string | Date : T extends Scalar ? T : T extends ScalarReference<infer U> ? EntityDataProp<U, C> : T extends {
479
480
  __runtime?: infer Runtime;
480
481
  __raw?: infer Raw;
481
- } ? C extends true ? Raw : Runtime : T extends LazyRef.Brand<infer U> ? EntityDataNested<U, C> : T extends ReferenceShape<infer U> ? EntityDataNested<U, C> : T extends CollectionShape<infer U> ? U | U[] | EntityDataNested<U & object, C> | EntityDataNested<U & object, C>[] : T extends readonly (infer U)[] ? U extends NonArrayObject ? U | U[] | EntityDataNested<U, C> | EntityDataNested<U, C>[] : U[] | EntityDataNested<U, C>[] : EntityDataNested<T, C>;
482
+ } ? C extends true ? Raw : Runtime : T extends LazyRef.Brand<infer U> ? EntityDataNested<U, C> : T extends ReferenceShape<infer U> ? EntityDataNested<U, C> : T extends CollectionShape<infer U> ? U | U[] | EntityDataNested<U & object, C> | EntityDataNested<U & object, C>[] : T extends readonly (infer U)[] ? [U] extends [NonArrayObject] ? U | U[] | EntityDataNested<U, C> | EntityDataNested<U, C>[] : U[] | EntityDataNested<U, C>[] : EntityDataNested<T, C>;
482
483
  /** Like `EntityDataProp` but used in `RequiredEntityData` context with required/optional key distinction. */
483
484
  export type RequiredEntityDataProp<T, O, C extends boolean> = T extends Date ? string | Date : Exclude<T, null> extends RequiredNullable.Brand ? T | null : T extends Scalar ? T : T extends ScalarReference<infer U> ? RequiredEntityDataProp<U, O, C> : T extends {
484
485
  __runtime?: infer Runtime;
485
486
  __raw?: infer Raw;
486
- } ? C extends true ? Raw : Runtime : T extends LazyRef.Brand<infer U> ? RequiredEntityDataNested<U, O, C> : T extends ReferenceShape<infer U> ? RequiredEntityDataNested<U, O, C> : T extends CollectionShape<infer U> ? U | U[] | RequiredEntityDataNested<U & object, O, C> | RequiredEntityDataNested<U & object, O, C>[] : T extends readonly (infer U)[] ? U extends NonArrayObject ? U | U[] | RequiredEntityDataNested<U, O, C> | RequiredEntityDataNested<U, O, C>[] : U[] | RequiredEntityDataNested<U, O, C>[] : RequiredEntityDataNested<T, O, C>;
487
+ } ? C extends true ? Raw : Runtime : T extends LazyRef.Brand<infer U> ? RequiredEntityDataNested<U, O, C> : T extends ReferenceShape<infer U> ? RequiredEntityDataNested<U, O, C> : T extends CollectionShape<infer U> ? U | U[] | RequiredEntityDataNested<U & object, O, C> | RequiredEntityDataNested<U & object, O, C>[] : T extends readonly (infer U)[] ? [U] extends [NonArrayObject] ? U | U[] | RequiredEntityDataNested<U, O, C> | RequiredEntityDataNested<U, O, C>[] : U[] | RequiredEntityDataNested<U, O, C>[] : RequiredEntityDataNested<T, O, C>;
487
488
  /** Nested entity data shape for embedded or related entities within `EntityData`. */
488
489
  export type EntityDataNested<T, C extends boolean = false> = T extends undefined ? never : T extends any[] ? Readonly<T> : EntityData<T, C> | ExpandEntityProp<T, C>;
489
490
  type UnwrapScalarRef<T> = T extends ScalarReference<infer U> ? U : T;
@@ -653,6 +654,16 @@ export type SerializeDTO<T, H extends string = never, E extends string = never,
653
654
  };
654
655
  type TargetKeys<T> = T extends EntityClass<infer P> ? keyof P : keyof T;
655
656
  type PropertyName<T> = IsUnknown<T> extends false ? TargetKeys<T> : string;
657
+ /** Resolved `through` option of a virtual to-one relation, populated during discovery. */
658
+ export interface ThroughRelation {
659
+ entity: EntityClass;
660
+ where?: FilterQuery<any>;
661
+ orderBy?: QueryOrderMap<any>[];
662
+ /** M:1 property on the `through` entity pointing back to the owner. */
663
+ ownerProperty: string;
664
+ /** M:1 property on the `through` entity pointing to the target, undefined when the target is selected directly. */
665
+ targetProperty?: string;
666
+ }
656
667
  /** Table reference object passed to formula callbacks, including alias and schema information. */
657
668
  export type FormulaTable = {
658
669
  alias: string;
@@ -710,6 +721,8 @@ export type IndexCallback<T> = (columns: Record<PropertyName<T>, string>, table:
710
721
  export type FormulaCallback<T> = (columns: FormulaColumns<T>, table: FormulaTable) => string | Raw;
711
722
  /** Callback for CHECK constraint expressions. Receives column mappings and table info. */
712
723
  export type CheckCallback<T> = (columns: SchemaColumns<T>, table: SchemaTable) => string | Raw;
724
+ /** Callback for row level security policy expressions. Receives column mappings and table info. */
725
+ export type PolicyCallback<T> = (columns: SchemaColumns<T>, table: SchemaTable) => string | Raw;
713
726
  /** Callback for trigger body expressions. Receives column mappings and table info. */
714
727
  export type TriggerCallback<T> = (columns: Record<PropertyName<T>, string>, table: SchemaTable) => string | Raw;
715
728
  /**
@@ -724,6 +737,28 @@ export interface CheckConstraint<T = any> {
724
737
  property?: string;
725
738
  expression: string | Raw | CheckCallback<T>;
726
739
  }
740
+ /** Definition of a PostgreSQL row level security policy on a table. */
741
+ export interface PolicyDef<T = any> {
742
+ /** Policy name. Auto-generated if omitted. */
743
+ name?: string;
744
+ /** DML command the policy applies to. Defaults to `'all'`. */
745
+ command?: 'select' | 'insert' | 'update' | 'delete' | 'all';
746
+ /** Whether the policy is permissive (OR-combined) or restrictive (AND-combined). Defaults to `'permissive'`. */
747
+ type?: 'permissive' | 'restrictive';
748
+ /** Database roles the policy applies to. Defaults to `PUBLIC`. */
749
+ roles?: string[];
750
+ /** `USING` expression filtering visible rows. Can be a string, Raw query, or callback receiving column name mappings. */
751
+ using?: string | Raw | PolicyCallback<T>;
752
+ /** `WITH CHECK` expression validating written rows. Can be a string, Raw query, or callback receiving column name mappings. */
753
+ check?: string | Raw | PolicyCallback<T>;
754
+ }
755
+ /** Per-context database session state applied for row level security (session variables and role). */
756
+ export interface SessionContext {
757
+ /** Session variables set via `set_config`, typically referenced by RLS policies through `current_setting()`. `Date` values are serialized to ISO 8601. */
758
+ variables?: Dictionary<string | number | boolean | Date>;
759
+ /** Database role to switch to for the duration of the context (`set local role` / `set role`). */
760
+ role?: string;
761
+ }
727
762
  /** Definition of a database trigger on a table. */
728
763
  export interface TriggerDef<T = any> {
729
764
  /** Trigger name. Auto-generated if omitted. */
@@ -1019,6 +1054,7 @@ export interface EntityProperty<Owner = any, Target = any> {
1019
1054
  fixedOrderColumn?: string;
1020
1055
  pivotTable: string;
1021
1056
  pivotEntity: EntityClass<Target>;
1057
+ through?: ThroughRelation;
1022
1058
  joinColumns: string[];
1023
1059
  ownColumns: string[];
1024
1060
  inverseJoinColumns: string[];
@@ -1050,6 +1086,12 @@ export declare class EntityMetadata<Entity = any, Class extends EntityCtor<Entit
1050
1086
  constructor(meta?: Partial<EntityMetadata>);
1051
1087
  addProperty(prop: Partial<EntityProperty<Entity>>): void;
1052
1088
  removeProperty(name: string, sync?: boolean): void;
1089
+ /** For TPT entities, the version column exists only on the table of the entity that declares it. */
1090
+ ownsVersionProperty(): boolean;
1091
+ /** For TPT entities, concurrency check columns exist only on the table of the entity that declares them. */
1092
+ getOwnConcurrencyCheckKeys(): EntityKey<Entity>[];
1093
+ /** Whether updates of this table are guarded by a version property or concurrency check columns it owns. */
1094
+ hasOptimisticLock(): boolean;
1053
1095
  getPrimaryProps(flatten?: boolean): EntityProperty<Entity>[];
1054
1096
  getPrimaryProp(): EntityProperty<Entity>;
1055
1097
  /**
@@ -1165,6 +1207,9 @@ export interface EntityMetadata<Entity = any, Class extends EntityCtor<Entity> =
1165
1207
  }[];
1166
1208
  checks: CheckConstraint<Entity>[];
1167
1209
  triggers: TriggerDef<Entity>[];
1210
+ policies: PolicyDef<Entity>[];
1211
+ /** Enables row level security on the table. `'force'` also enables it for the table owner. Implied by non-empty `policies`, unless set to `false`, which keeps the policies staged but RLS disabled. */
1212
+ rowLevelSecurity?: boolean | 'force';
1168
1213
  repositoryClass?: string;
1169
1214
  repository: () => EntityClass<EntityRepository<any>>;
1170
1215
  hooks: {
@@ -1492,6 +1537,15 @@ type FilterDefResolved<T extends object = any> = {
1492
1537
  entity?: EntityName<T> | EntityName<T>[];
1493
1538
  args?: boolean;
1494
1539
  strict?: boolean;
1540
+ /**
1541
+ * Also materializes this filter as a PostgreSQL row level security policy on the entity's table, and stages the
1542
+ * matching session variables when its params are enabled via `em.setFilterParams()`. The `cond` must be compilable
1543
+ * to a static expression (no access to `em`/`type`/`options`, not async). Each referenced argument maps to a session
1544
+ * variable named `mikro.<filterName>.<argName>`; pass `{ setting }` to override that name for a single-argument filter.
1545
+ */
1546
+ rls?: boolean | {
1547
+ setting?: string;
1548
+ };
1495
1549
  };
1496
1550
  /** Definition of a query filter that can be registered globally or per-entity via `@Filter()`. */
1497
1551
  export type FilterDef<T extends EntityName | readonly EntityName[] = any> = FilterDefResolved<EntityFromInput<T>> & {
package/typings.js CHANGED
@@ -49,6 +49,7 @@ export class EntityMetadata {
49
49
  this.uniques = [];
50
50
  this.checks = [];
51
51
  this.triggers = [];
52
+ this.policies = [];
52
53
  this.referencingProperties = [];
53
54
  this.concurrencyCheckKeys = new Set();
54
55
  Object.assign(this, meta);
@@ -79,6 +80,25 @@ export class EntityMetadata {
79
80
  this.sync();
80
81
  }
81
82
  }
83
+ /** For TPT entities, the version column exists only on the table of the entity that declares it. */
84
+ ownsVersionProperty() {
85
+ if (!this.versionProperty) {
86
+ return false;
87
+ }
88
+ return this.inheritanceType !== 'tpt' || !this.ownProps || this.ownProps.some(p => p.name === this.versionProperty);
89
+ }
90
+ /** For TPT entities, concurrency check columns exist only on the table of the entity that declares them. */
91
+ getOwnConcurrencyCheckKeys() {
92
+ const keys = [...this.concurrencyCheckKeys];
93
+ if (this.inheritanceType !== 'tpt' || !this.ownProps) {
94
+ return keys;
95
+ }
96
+ return keys.filter(key => this.ownProps.some(p => p.name === key));
97
+ }
98
+ /** Whether updates of this table are guarded by a version property or concurrency check columns it owns. */
99
+ hasOptimisticLock() {
100
+ return this.ownsVersionProperty() || this.getOwnConcurrencyCheckKeys().length > 0;
101
+ }
82
102
  getPrimaryProps(flatten = false) {
83
103
  const pks = this.primaryKeys.map(pk => this.properties[pk]);
84
104
  if (flatten) {
@@ -216,7 +236,10 @@ export class EntityMetadata {
216
236
  if (config) {
217
237
  const platform = config.getPlatform();
218
238
  for (const prop of this.props) {
219
- if (prop.enum && !prop.nativeEnumName && prop.items?.every(item => typeof item === 'string')) {
239
+ if (prop.enum &&
240
+ !prop.nativeEnumName &&
241
+ prop.items?.every(item => typeof item === 'string') &&
242
+ !['json', 'jsonb'].includes(prop.columnTypes?.[0])) {
220
243
  const name = platform.getIndexName(this.tableName, prop.fieldNames, 'check');
221
244
  const exists = this.checks.findIndex(check => check.name === name);
222
245
  if (exists !== -1) {
@@ -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
  }
@@ -303,32 +304,33 @@ export class ChangeSetPersister {
303
304
  options = this.prepareOptions(meta, options, {
304
305
  convertCustomTypes: false,
305
306
  });
306
- if (meta.concurrencyCheckKeys.size === 0 &&
307
- (!meta.versionProperty || changeSet.entity[meta.versionProperty] == null)) {
307
+ if (meta.getOwnConcurrencyCheckKeys().length === 0 &&
308
+ (!meta.ownsVersionProperty() || changeSet.entity[meta.versionProperty] == null)) {
308
309
  return this.#driver.nativeUpdate(changeSet.meta.class, cond, changeSet.payload, options);
309
310
  }
310
- if (meta.versionProperty) {
311
+ if (meta.ownsVersionProperty()) {
311
312
  cond[meta.versionProperty] = this.#platform.convertVersionValue(changeSet.entity[meta.versionProperty], meta.properties[meta.versionProperty]);
312
313
  }
313
314
  this.checkConcurrencyKeys(meta, changeSet, cond);
314
315
  return this.#driver.nativeUpdate(changeSet.meta.class, cond, changeSet.payload, options);
315
316
  }
316
317
  async checkOptimisticLocks(meta, changeSets, options) {
317
- if (meta.concurrencyCheckKeys.size === 0 &&
318
- (!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))) {
319
321
  return;
320
322
  }
321
323
  // skip entity references as they don't have version values loaded
322
324
  changeSets = changeSets.filter(cs => helper(cs.entity).__initialized);
325
+ const primaryKeys = meta.primaryKeys.concat(...concurrencyCheckKeys);
323
326
  const $or = changeSets.map(cs => {
324
- const cond = Utils.getPrimaryKeyCond(cs.originalEntity, meta.primaryKeys.concat(...meta.concurrencyCheckKeys));
325
- if (meta.versionProperty) {
327
+ const cond = Utils.getPrimaryKeyCond(cs.originalEntity, primaryKeys);
328
+ if (meta.ownsVersionProperty()) {
326
329
  // @ts-ignore
327
330
  cond[meta.versionProperty] = this.#platform.convertVersionValue(cs.entity[meta.versionProperty], meta.properties[meta.versionProperty]);
328
331
  }
329
332
  return cond;
330
333
  });
331
- const primaryKeys = meta.primaryKeys.concat(...meta.concurrencyCheckKeys);
332
334
  options = this.prepareOptions(meta, options, {
333
335
  fields: primaryKeys,
334
336
  orderBy: meta.primaryKeys.reduce((o, pk) => {
@@ -336,7 +338,9 @@ export class ChangeSetPersister {
336
338
  return o;
337
339
  }, {}),
338
340
  });
339
- 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);
340
344
  if (res.length !== changeSets.length) {
341
345
  // a FK pointing to a composite PK is an array, so the values need to be compared deeply
342
346
  const compare = (a, b, keys) => keys.every(k => equals(a[k], b[k]));
@@ -347,7 +351,7 @@ export class ChangeSetPersister {
347
351
  }
348
352
  }
349
353
  checkOptimisticLock(meta, changeSet, res) {
350
- if ((meta.versionProperty || meta.concurrencyCheckKeys.size > 0) && res && !res.affectedRows) {
354
+ if ((meta.ownsVersionProperty() || meta.getOwnConcurrencyCheckKeys().length > 0) && res && !res.affectedRows) {
351
355
  throw OptimisticLockError.lockFailed(changeSet.entity);
352
356
  }
353
357
  }
@@ -356,7 +360,7 @@ export class ChangeSetPersister {
356
360
  * so we use a single query in case of both versioning and default values is used.
357
361
  */
358
362
  async reloadVersionValues(meta, changeSets, options) {
359
- const reloadProps = meta.versionProperty && !this.#usesReturningStatement ? [meta.properties[meta.versionProperty]] : [];
363
+ const reloadProps = meta.ownsVersionProperty() && !this.#usesReturningStatement ? [meta.properties[meta.versionProperty]] : [];
360
364
  if (changeSets[0].type === ChangeSetType.CREATE) {
361
365
  for (const prop of meta.props) {
362
366
  if (prop.persist === false) {
@@ -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];
@@ -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,
@@ -94,6 +95,7 @@ const DEFAULTS = {
94
95
  ignoreSchema: [],
95
96
  ignoreTriggers: false,
96
97
  ignoreRoutines: false,
98
+ ignorePolicies: false,
97
99
  skipTables: [],
98
100
  skipViews: [],
99
101
  skipColumns: {},
@@ -393,7 +395,15 @@ export class Configuration {
393
395
  }
394
396
  this.#options.schema ??= this.#platform.getDefaultSchemaName();
395
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
+ }
396
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
+ }
397
407
  this.#options.filters[key].default ??= true;
398
408
  });
399
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
  /**