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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/EntityManager.d.ts +41 -7
  2. package/EntityManager.js +203 -42
  3. package/MikroORM.d.ts +4 -0
  4. package/MikroORM.js +9 -0
  5. package/README.md +1 -0
  6. package/cache/FileCacheAdapter.js +1 -1
  7. package/connections/Connection.d.ts +10 -1
  8. package/connections/Connection.js +9 -0
  9. package/drivers/DatabaseDriver.d.ts +14 -5
  10. package/drivers/DatabaseDriver.js +151 -52
  11. package/entity/Collection.js +4 -2
  12. package/entity/EntityLoader.d.ts +7 -1
  13. package/entity/EntityLoader.js +35 -5
  14. package/entity/EntityRepository.d.ts +4 -5
  15. package/entity/EntityRepository.js +2 -1
  16. package/entity/defineEntity.d.ts +23 -5
  17. package/entity/defineEntity.js +31 -0
  18. package/enums.d.ts +5 -1
  19. package/enums.js +2 -0
  20. package/errors.d.ts +35 -0
  21. package/errors.js +87 -0
  22. package/exceptions.d.ts +5 -0
  23. package/exceptions.js +5 -0
  24. package/index.d.ts +1 -1
  25. package/metadata/MetadataDiscovery.d.ts +3 -0
  26. package/metadata/MetadataDiscovery.js +94 -7
  27. package/metadata/types.d.ts +19 -3
  28. package/package.json +1 -1
  29. package/platforms/Platform.d.ts +19 -1
  30. package/platforms/Platform.js +56 -0
  31. package/types/BigIntType.d.ts +1 -0
  32. package/types/BigIntType.js +23 -0
  33. package/types/DateTimeType.d.ts +1 -0
  34. package/types/DateTimeType.js +8 -0
  35. package/types/StringType.d.ts +14 -3
  36. package/types/StringType.js +34 -4
  37. package/types/TextType.d.ts +2 -4
  38. package/types/TextType.js +2 -8
  39. package/types/Type.d.ts +11 -0
  40. package/types/index.d.ts +2 -2
  41. package/typings.d.ts +48 -0
  42. package/typings.js +1 -0
  43. package/unit-of-work/UnitOfWork.js +1 -0
  44. package/utils/Configuration.d.ts +21 -1
  45. package/utils/Configuration.js +12 -1
  46. package/utils/Cursor.d.ts +2 -0
  47. package/utils/Cursor.js +43 -33
  48. package/utils/QueryHelper.d.ts +12 -0
  49. package/utils/QueryHelper.js +63 -0
  50. package/utils/RawQueryFragment.d.ts +6 -0
  51. package/utils/RawQueryFragment.js +15 -6
  52. package/utils/RequestContext.d.ts +2 -2
  53. package/utils/RequestContext.js +11 -2
  54. package/utils/TransactionManager.js +1 -1
  55. package/utils/Utils.d.ts +2 -0
  56. package/utils/Utils.js +12 -3
  57. package/utils/env-vars.js +2 -0
  58. package/utils/index.d.ts +1 -0
  59. package/utils/index.js +1 -0
  60. package/utils/rls-utils.d.ts +35 -0
  61. package/utils/rls-utils.js +97 -0
@@ -471,6 +471,14 @@ export class Platform {
471
471
  preservesDatesInsideJson() {
472
472
  return false;
473
473
  }
474
+ /** Whether `nulls first`/`nulls last` can be requested in an `orderBy`. */
475
+ supportsNullsOrdering() {
476
+ return true;
477
+ }
478
+ /** Where nulls land when an `orderBy` requests no explicit placement: lowest (`asc` puts them first) or highest. */
479
+ sortsNullsLowest() {
480
+ return false;
481
+ }
474
482
  /** Converts a JS value to its JSON database representation (typically JSON.stringify). */
475
483
  convertJsonToDatabaseValue(value, context) {
476
484
  return JSON.stringify(value);
@@ -616,6 +624,14 @@ export class Platform {
616
624
  Object.defineProperty(copy, JsonProperty, { enumerable: false, value: true });
617
625
  return copy;
618
626
  }
627
+ /**
628
+ * Builds the correlated subquery used as the formula of a virtual to-one relation defined via `through`.
629
+ * @internal
630
+ */
631
+ /* v8 ignore next 3 */
632
+ getThroughRelationFormula(prop, columns) {
633
+ throw new Error(`${this.constructor.name} does not support the 'through' option of ${prop.name}`);
634
+ }
619
635
  /** Initializes the platform with the ORM configuration. */
620
636
  setConfig(config) {
621
637
  this.config = config;
@@ -722,11 +738,51 @@ export class Platform {
722
738
  supportsDeferredUniqueConstraints() {
723
739
  return true;
724
740
  }
741
+ /** Whether the platform supports row level security (PostgreSQL). */
742
+ supportsRowLevelSecurity() {
743
+ return false;
744
+ }
745
+ /** Whether the driver can apply the session context on every pooled connection acquire (`sessionContext: 'connection'`). */
746
+ supportsConnectionSessionContext() {
747
+ return false;
748
+ }
749
+ /**
750
+ * SQL cast suffix (e.g. `'::uuid'`, or `''` when none is needed) applied when an RLS filter reads a session
751
+ * variable via `current_setting()` as the given column type, or `null` if the type has no automatic cast.
752
+ */
753
+ getCurrentSettingCast(mappedType) {
754
+ return null;
755
+ }
725
756
  /** Platform-specific validation of entity metadata. */
726
757
  validateMetadata(meta) {
727
758
  if (meta.partitionBy && !this.supportsPartitionedTables()) {
728
759
  throw new MetadataError(`Entity ${meta.className} uses partitionBy, but ${this.constructor.name} does not support partitioned tables`);
729
760
  }
761
+ const declaresRls = meta.policies.length > 0 || !!meta.rowLevelSecurity;
762
+ if (declaresRls && !this.supportsRowLevelSecurity()) {
763
+ throw MetadataError.rowLevelSecurityNotSupportedByDriver(meta);
764
+ }
765
+ // STI hierarchies share a single table, so only the root may declare policies; `root` is optional-chained
766
+ // as `validateMetadata` is public API and tolerates partially populated metadata
767
+ if (declaresRls && meta.root?.inheritanceType === 'sti' && meta.root !== meta) {
768
+ throw MetadataError.rowLevelSecurityOnNonRootStiEntity(meta);
769
+ }
770
+ if (meta.root?.inheritanceType === 'sti' && meta.root !== meta) {
771
+ for (const filter of Object.values(meta.filters)) {
772
+ // inherited root filters share the def object; only defs declared on the child itself are a problem,
773
+ // as non-root STI metas never reach the schema generator and the policy would silently not exist
774
+ if (filter.rls && meta.root.filters[filter.name] !== filter) {
775
+ throw MetadataError.rlsFilterOnNonRootStiEntity(meta, filter.name);
776
+ }
777
+ }
778
+ }
779
+ if (!this.supportsRowLevelSecurity()) {
780
+ for (const filter of Object.values(meta.filters)) {
781
+ if (filter.rls) {
782
+ throw MetadataError.rlsFilterNotSupportedByDriver(meta, filter.name);
783
+ }
784
+ }
785
+ }
730
786
  }
731
787
  /**
732
788
  * Generates a custom order by statement given a set of in order values, eg.
@@ -11,6 +11,7 @@ export declare class BigIntType<Mode extends 'bigint' | 'number' | 'string' = 'b
11
11
  convertToDatabaseValue(value: JSTypeByMode<Mode> | null | undefined): string | null | undefined;
12
12
  convertToJSValue(value: string | bigint | null | undefined): JSTypeByMode<Mode> | null | undefined;
13
13
  toJSON(value: JSTypeByMode<Mode> | null | undefined): JSTypeByMode<Mode> | null | undefined;
14
+ fromJSON(value: unknown): JSTypeByMode<Mode> | null | undefined;
14
15
  getColumnType(prop: EntityProperty, platform: Platform): string;
15
16
  compareAsType(): string;
16
17
  compareValues(a: string, b: string): boolean;
@@ -1,4 +1,5 @@
1
1
  import { Type } from './Type.js';
2
+ import { ValidationError } from '../errors.js';
2
3
  /**
3
4
  * This type will automatically convert string values returned from the database to native JS bigints (default)
4
5
  * or numbers (safe only for values up to `Number.MAX_SAFE_INTEGER`), or strings, depending on the `mode`.
@@ -36,6 +37,28 @@ export class BigIntType extends Type {
36
37
  }
37
38
  return this.convertToDatabaseValue(value);
38
39
  }
40
+ fromJSON(value) {
41
+ // the serialized form is a decimal string, or a plain number in `number` mode
42
+ const valid = (typeof value === 'string' && /^-?\d+$/.test(value)) || (typeof value === 'number' && Number.isInteger(value));
43
+ if (!valid) {
44
+ throw ValidationError.invalidType(BigIntType, value, 'JSON');
45
+ }
46
+ switch (this.mode) {
47
+ case 'number': {
48
+ // `Number` silently rounds past `MAX_SAFE_INTEGER`, tampered cursors must fail loudly
49
+ const num = Number(value);
50
+ if (!Number.isSafeInteger(num)) {
51
+ throw ValidationError.invalidType(BigIntType, value, 'JSON');
52
+ }
53
+ return num;
54
+ }
55
+ case 'string':
56
+ return String(value);
57
+ case 'bigint':
58
+ default:
59
+ return BigInt(value);
60
+ }
61
+ }
39
62
  getColumnType(prop, platform) {
40
63
  return platform.getBigIntTypeDeclarationSQL(prop);
41
64
  }
@@ -5,6 +5,7 @@ import type { EntityProperty } from '../typings.js';
5
5
  export declare class DateTimeType extends Type<Date, string> {
6
6
  getColumnType(prop: EntityProperty, platform: Platform): string;
7
7
  compareAsType(): string;
8
+ fromJSON(value: unknown): Date;
8
9
  get runtimeType(): string;
9
10
  ensureComparable(): boolean;
10
11
  getDefaultLength(platform: Platform): number;
@@ -1,4 +1,5 @@
1
1
  import { Type } from './Type.js';
2
+ import { ValidationError } from '../errors.js';
2
3
  /** Maps a database DATETIME/TIMESTAMP column to a JS `Date` object. */
3
4
  export class DateTimeType extends Type {
4
5
  getColumnType(prop, platform) {
@@ -7,6 +8,13 @@ export class DateTimeType extends Type {
7
8
  compareAsType() {
8
9
  return 'Date';
9
10
  }
11
+ fromJSON(value) {
12
+ const date = new Date(value);
13
+ if (typeof value !== 'string' || Number.isNaN(date.getTime())) {
14
+ throw ValidationError.invalidType(DateTimeType, value, 'JSON');
15
+ }
16
+ return date;
17
+ }
10
18
  get runtimeType() {
11
19
  return 'Date';
12
20
  }
@@ -1,10 +1,21 @@
1
1
  import { Type } from './Type.js';
2
2
  import type { Platform } from '../platforms/Platform.js';
3
3
  import type { EntityProperty } from '../typings.js';
4
- /** Maps a database VARCHAR column to a JS `string`. */
5
- export declare class StringType extends Type<string | null | undefined, string | null | undefined> {
6
- getColumnType(prop: EntityProperty, platform: Platform): string;
4
+ export interface StringTypeOptions {
5
+ trim?: boolean;
6
+ case?: 'upper' | 'lower';
7
+ }
8
+ /** @internal */
9
+ export declare abstract class BaseStringType extends Type<string | null | undefined, string | null | undefined> {
10
+ readonly options: StringTypeOptions;
11
+ constructor(options?: StringTypeOptions);
12
+ convertToDatabaseValue(value: string | null | undefined): string | null | undefined;
7
13
  compareAsType(): string;
8
14
  ensureComparable(): boolean;
15
+ private normalize;
16
+ }
17
+ /** Maps a database VARCHAR column to a JS `string`. */
18
+ export declare class StringType extends BaseStringType {
19
+ getColumnType(prop: EntityProperty, platform: Platform): string;
9
20
  getDefaultLength(platform: Platform): number;
10
21
  }
@@ -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/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>;
@@ -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[];
@@ -1171,6 +1207,9 @@ export interface EntityMetadata<Entity = any, Class extends EntityCtor<Entity> =
1171
1207
  }[];
1172
1208
  checks: CheckConstraint<Entity>[];
1173
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';
1174
1213
  repositoryClass?: string;
1175
1214
  repository: () => EntityClass<EntityRepository<any>>;
1176
1215
  hooks: {
@@ -1498,6 +1537,15 @@ type FilterDefResolved<T extends object = any> = {
1498
1537
  entity?: EntityName<T> | EntityName<T>[];
1499
1538
  args?: boolean;
1500
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
+ };
1501
1549
  };
1502
1550
  /** Definition of a query filter that can be registered globally or per-entity via `@Filter()`. */
1503
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);
@@ -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
  });
@@ -233,6 +233,12 @@ export type MigrationsOptions = {
233
233
  * @default true
234
234
  */
235
235
  snapshot?: boolean;
236
+ /**
237
+ * Update the snapshot from the database schema when running migrations up or down.
238
+ * Disable to keep the snapshot managed solely by `migration:create`.
239
+ * @default true
240
+ */
241
+ snapshotOnMigrate?: boolean;
236
242
  /** Custom name for the snapshot file. */
237
243
  snapshotName?: string;
238
244
  /**
@@ -433,7 +439,7 @@ export interface Options<Driver extends IDatabaseDriver = IDatabaseDriver, EM ex
433
439
  */
434
440
  filters: Dictionary<{
435
441
  name?: string;
436
- } & Omit<FilterDef, 'name'>>;
442
+ } & Omit<FilterDef, 'name' | 'rls'>>;
437
443
  /**
438
444
  * Metadata discovery configuration options.
439
445
  * Controls how entities are discovered and validated.
@@ -474,6 +480,12 @@ export interface Options<Driver extends IDatabaseDriver = IDatabaseDriver, EM ex
474
480
  * @default false
475
481
  */
476
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';
477
489
  /**
478
490
  * Enable verbose logging of internal operations.
479
491
  * @default false
@@ -834,6 +846,14 @@ export interface Options<Driver extends IDatabaseDriver = IDatabaseDriver, EM ex
834
846
  * @default false
835
847
  */
836
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;
837
857
  /**
838
858
  * Table names or patterns to skip during schema generation.
839
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,
@@ -84,6 +85,7 @@ const DEFAULTS = {
84
85
  dropTables: true,
85
86
  safe: false,
86
87
  snapshot: true,
88
+ snapshotOnMigrate: true,
87
89
  emit: 'ts',
88
90
  // mirrors `NamingStrategy.classToMigrationName`, so the file name matches the class it declares
89
91
  fileName: (timestamp, name) => `Migration${timestamp}${name ? '_' + name.replace(/[^$\p{ID_Continue}]+/gu, '_') : ''}`,
@@ -93,6 +95,7 @@ const DEFAULTS = {
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,12 +149,11 @@ 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) {
@@ -37,6 +37,18 @@ 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>;