@prisma-next/target-postgres 0.14.0-dev.11 → 0.14.0-dev.12

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 (126) hide show
  1. package/dist/canonicalize-Bn3BM1Cn.mjs +46 -0
  2. package/dist/canonicalize-Bn3BM1Cn.mjs.map +1 -0
  3. package/dist/canonicalize-CAbXtVtB.d.mts +37 -0
  4. package/dist/canonicalize-CAbXtVtB.d.mts.map +1 -0
  5. package/dist/contract-free.d.mts +1 -1
  6. package/dist/contract-free.d.mts.map +1 -1
  7. package/dist/contract-free.mjs +2 -2
  8. package/dist/control.d.mts +1 -1
  9. package/dist/control.mjs +4 -4
  10. package/dist/{data-transform-DDgWdB5o.d.mts → data-transform-qKy1U5V9.d.mts} +2 -2
  11. package/dist/{data-transform-DDgWdB5o.d.mts.map → data-transform-qKy1U5V9.d.mts.map} +1 -1
  12. package/dist/data-transform.d.mts +1 -1
  13. package/dist/{ddl-QDyOSeLc.mjs → ddl-BlsTIBeP.mjs} +54 -4
  14. package/dist/ddl-BlsTIBeP.mjs.map +1 -0
  15. package/dist/ddl.d.mts +3 -2
  16. package/dist/ddl.mjs +2 -2
  17. package/dist/descriptor-meta-b_SzXwen.mjs +239 -0
  18. package/dist/descriptor-meta-b_SzXwen.mjs.map +1 -0
  19. package/dist/{issue-planner-DL6g3CmE.mjs → issue-planner-CwoeupPM.mjs} +9 -6
  20. package/dist/{issue-planner-DL6g3CmE.mjs.map → issue-planner-CwoeupPM.mjs.map} +1 -1
  21. package/dist/issue-planner.d.mts +3 -3
  22. package/dist/issue-planner.d.mts.map +1 -1
  23. package/dist/issue-planner.mjs +1 -1
  24. package/dist/migration.d.mts +3 -3
  25. package/dist/migration.mjs +2 -2
  26. package/dist/{nodes-Bbhs2rwj.mjs → nodes-DhDIIhBT.mjs} +44 -2
  27. package/dist/nodes-DhDIIhBT.mjs.map +1 -0
  28. package/dist/{nodes-pLeLgdis.d.mts → nodes-YvdohJ2X.d.mts} +40 -3
  29. package/dist/nodes-YvdohJ2X.d.mts.map +1 -0
  30. package/dist/{op-factory-call-DmQEc3XV.d.mts → op-factory-call-DXHAIisl.d.mts} +37 -4
  31. package/dist/op-factory-call-DXHAIisl.d.mts.map +1 -0
  32. package/dist/{op-factory-call-D_p5vxwt.mjs → op-factory-call-tMksrGIq.mjs} +146 -4
  33. package/dist/op-factory-call-tMksrGIq.mjs.map +1 -0
  34. package/dist/op-factory-call.d.mts +1 -1
  35. package/dist/op-factory-call.mjs +1 -1
  36. package/dist/pack.d.mts +74 -1
  37. package/dist/pack.d.mts.map +1 -1
  38. package/dist/pack.mjs +1 -1
  39. package/dist/{planner-Bs_baQax.mjs → planner-BbSZJVW7.mjs} +114 -22
  40. package/dist/planner-BbSZJVW7.mjs.map +1 -0
  41. package/dist/{planner-produced-postgres-migration-Cji5vxUf.mjs → planner-produced-postgres-migration-C0FHnEsV.mjs} +2 -2
  42. package/dist/{planner-produced-postgres-migration-Cji5vxUf.mjs.map → planner-produced-postgres-migration-C0FHnEsV.mjs.map} +1 -1
  43. package/dist/{planner-produced-postgres-migration-QqHa2C2l.d.mts → planner-produced-postgres-migration-DfM5lBv1.d.mts} +3 -3
  44. package/dist/{planner-produced-postgres-migration-QqHa2C2l.d.mts.map → planner-produced-postgres-migration-DfM5lBv1.d.mts.map} +1 -1
  45. package/dist/planner-produced-postgres-migration.d.mts +1 -1
  46. package/dist/planner-produced-postgres-migration.mjs +1 -1
  47. package/dist/{planner-sql-checks-jqUUGyQR.mjs → planner-sql-checks-CI6Z21mm.mjs} +2 -2
  48. package/dist/{planner-sql-checks-jqUUGyQR.mjs.map → planner-sql-checks-CI6Z21mm.mjs.map} +1 -1
  49. package/dist/planner-sql-checks.mjs +1 -1
  50. package/dist/{planner-target-details-CIY6tLeo.d.mts → planner-target-details-HkP4RrRO.d.mts} +2 -2
  51. package/dist/planner-target-details-HkP4RrRO.d.mts.map +1 -0
  52. package/dist/planner-target-details.d.mts +1 -1
  53. package/dist/planner.d.mts +35 -3
  54. package/dist/planner.d.mts.map +1 -1
  55. package/dist/planner.mjs +2 -2
  56. package/dist/{postgres-contract-serializer-k3TAcPMY.mjs → postgres-contract-serializer-k5eg5ZUZ.mjs} +29 -16
  57. package/dist/postgres-contract-serializer-k5eg5ZUZ.mjs.map +1 -0
  58. package/dist/{postgres-migration-B5jKrXv3.mjs → postgres-migration-BJWxullf.mjs} +2 -2
  59. package/dist/{postgres-migration-B5jKrXv3.mjs.map → postgres-migration-BJWxullf.mjs.map} +1 -1
  60. package/dist/{postgres-migration-Y4YBJqkS.d.mts → postgres-migration-D22a2y0l.d.mts} +4 -4
  61. package/dist/{postgres-migration-Y4YBJqkS.d.mts.map → postgres-migration-D22a2y0l.d.mts.map} +1 -1
  62. package/dist/postgres-rls-policy-D_z1osEq.d.mts +60 -0
  63. package/dist/postgres-rls-policy-D_z1osEq.d.mts.map +1 -0
  64. package/dist/postgres-role-_CCuyPCQ.d.mts +33 -0
  65. package/dist/postgres-role-_CCuyPCQ.d.mts.map +1 -0
  66. package/dist/{postgres-schema-COGZ1ark.mjs → postgres-schema-BVDr_61a.mjs} +139 -3
  67. package/dist/postgres-schema-BVDr_61a.mjs.map +1 -0
  68. package/dist/postgres-schema-D0h4lsw2.d.mts +158 -0
  69. package/dist/postgres-schema-D0h4lsw2.d.mts.map +1 -0
  70. package/dist/postgres-schema-ir-BF0X8jit.mjs +66 -0
  71. package/dist/postgres-schema-ir-BF0X8jit.mjs.map +1 -0
  72. package/dist/postgres-schema-ir-BQ3RpZMz.d.mts +60 -0
  73. package/dist/postgres-schema-ir-BQ3RpZMz.d.mts.map +1 -0
  74. package/dist/postgres-schema-ir-annotations-El62Sywa.mjs +14 -0
  75. package/dist/postgres-schema-ir-annotations-El62Sywa.mjs.map +1 -0
  76. package/dist/render-ops.d.mts +1 -1
  77. package/dist/rls-canonicalize.d.mts +2 -0
  78. package/dist/rls-canonicalize.mjs +2 -0
  79. package/dist/runtime.d.mts +3 -2
  80. package/dist/runtime.d.mts.map +1 -1
  81. package/dist/runtime.mjs +2 -2
  82. package/dist/schema-ir-annotations.d.mts +8 -0
  83. package/dist/schema-ir-annotations.d.mts.map +1 -0
  84. package/dist/schema-ir-annotations.mjs +2 -0
  85. package/dist/types.d.mts +5 -140
  86. package/dist/types.mjs +3 -2
  87. package/package.json +21 -17
  88. package/src/contract-free/checks.ts +73 -0
  89. package/src/contract-free/ddl.ts +35 -0
  90. package/src/core/authoring.ts +115 -1
  91. package/src/core/ddl/nodes.ts +68 -1
  92. package/src/core/descriptor-meta.ts +4 -0
  93. package/src/core/entity-kinds.ts +16 -0
  94. package/src/core/migrations/control-policy.ts +3 -0
  95. package/src/core/migrations/issue-planner.ts +8 -0
  96. package/src/core/migrations/op-factory-call.ts +96 -0
  97. package/src/core/migrations/operations/rls.ts +106 -0
  98. package/src/core/migrations/planner-target-details.ts +3 -1
  99. package/src/core/migrations/planner.ts +99 -1
  100. package/src/core/migrations/verify-postgres-namespaces.ts +12 -15
  101. package/src/core/migrations/verify-postgres-rls-policies.ts +62 -0
  102. package/src/core/postgres-contract-serializer.ts +43 -19
  103. package/src/core/postgres-rls-policy.ts +103 -0
  104. package/src/core/postgres-role.ts +53 -0
  105. package/src/core/postgres-schema-ir-annotations.ts +22 -0
  106. package/src/core/postgres-schema-ir.ts +101 -0
  107. package/src/core/postgres-schema.ts +24 -4
  108. package/src/core/postgres-validators.ts +20 -0
  109. package/src/core/rls/canonicalize.ts +48 -0
  110. package/src/exports/ddl.ts +3 -0
  111. package/src/exports/planner.ts +1 -0
  112. package/src/exports/rls-canonicalize.ts +6 -0
  113. package/src/exports/schema-ir-annotations.ts +1 -0
  114. package/src/exports/types.ts +12 -0
  115. package/dist/ddl-QDyOSeLc.mjs.map +0 -1
  116. package/dist/descriptor-meta-CpGygXpI.mjs +0 -140
  117. package/dist/descriptor-meta-CpGygXpI.mjs.map +0 -1
  118. package/dist/nodes-Bbhs2rwj.mjs.map +0 -1
  119. package/dist/nodes-pLeLgdis.d.mts.map +0 -1
  120. package/dist/op-factory-call-D_p5vxwt.mjs.map +0 -1
  121. package/dist/op-factory-call-DmQEc3XV.d.mts.map +0 -1
  122. package/dist/planner-Bs_baQax.mjs.map +0 -1
  123. package/dist/planner-target-details-CIY6tLeo.d.mts.map +0 -1
  124. package/dist/postgres-contract-serializer-k3TAcPMY.mjs.map +0 -1
  125. package/dist/postgres-schema-COGZ1ark.mjs.map +0 -1
  126. package/dist/types.d.mts.map +0 -1
@@ -345,6 +345,79 @@ export function extensionExistsAst(extensionName: string): ExtensionExistsCheckB
345
345
  };
346
346
  }
347
347
 
348
+ export interface RlsPolicyExistsCheckBuilder {
349
+ policyPresent(): SelectAst;
350
+ policyAbsent(): SelectAst;
351
+ }
352
+
353
+ /**
354
+ * Typed RLS-policy existence check over `pg_policies`, with schema, table,
355
+ * and policy names bound as text parameters (never inlined). The `schema`
356
+ * coordinate is the resolved live-database schema name (e.g. `'public'`),
357
+ * compared against `pg_policies.schemaname` as a bound parameter.
358
+ */
359
+ export function rlsPolicyExistsAst(options: {
360
+ readonly schema: string;
361
+ readonly table: string;
362
+ readonly policyName: string;
363
+ }): RlsPolicyExistsCheckBuilder {
364
+ const inner = () =>
365
+ exprSelect()
366
+ .from(cfTable('pg_policies'))
367
+ .project('one', cfExpr.lit(1))
368
+ .where(
369
+ cfExpr.allOf([
370
+ cfExpr.identifierRef('schemaname').eqParam(options.schema, PG_TEXT_CODEC_ID),
371
+ cfExpr.identifierRef('tablename').eqParam(options.table, PG_TEXT_CODEC_ID),
372
+ cfExpr.identifierRef('policyname').eqParam(options.policyName, PG_TEXT_CODEC_ID),
373
+ ]),
374
+ );
375
+ return {
376
+ policyPresent: () => exprSelect().project('result', cfExpr.exists(inner())).build(),
377
+ policyAbsent: () => exprSelect().project('result', cfExpr.notExists(inner())).build(),
378
+ };
379
+ }
380
+
381
+ export interface RlsEnabledCheckBuilder {
382
+ rlsEnabled(): SelectAst;
383
+ rlsDisabled(): SelectAst;
384
+ }
385
+
386
+ /**
387
+ * Typed row-level-security enabled check over `pg_class` joined to
388
+ * `pg_namespace`, comparing `pg_class.relrowsecurity` against the expected
389
+ * boolean. Schema and table names are bound as text parameters.
390
+ */
391
+ export function rlsEnabledAst(schema: string, table: string): RlsEnabledCheckBuilder {
392
+ const inner = (enabled: boolean) =>
393
+ exprSelect()
394
+ .from(cfTable('pg_class', 'c'))
395
+ .join(
396
+ cfTable('pg_namespace', 'n'),
397
+ cfExpr.columnRef('n', 'oid').eqExpr(cfExpr.columnRef('c', 'relnamespace')),
398
+ )
399
+ .project('one', cfExpr.lit(1))
400
+ .where(
401
+ cfExpr.allOf([
402
+ cfExpr.columnRef('n', 'nspname').eqParam(schema, PG_TEXT_CODEC_ID),
403
+ cfExpr.columnRef('c', 'relname').eqParam(table, PG_TEXT_CODEC_ID),
404
+ enabled
405
+ ? cfExpr.columnRef('c', 'relrowsecurity')
406
+ : cfExpr.columnRef('c', 'relrowsecurity').not(),
407
+ ]),
408
+ );
409
+ return {
410
+ rlsEnabled: () =>
411
+ exprSelect()
412
+ .project('result', cfExpr.exists(inner(true)))
413
+ .build(),
414
+ rlsDisabled: () =>
415
+ exprSelect()
416
+ .project('result', cfExpr.exists(inner(false)))
417
+ .build(),
418
+ };
419
+ }
420
+
348
421
  export interface IndexExistsCheckBuilder {
349
422
  indexPresent(): SelectAst;
350
423
  indexAbsent(): SelectAst;
@@ -3,8 +3,11 @@ import {
3
3
  AddColumnAction,
4
4
  type AnyAlterTableAction,
5
5
  PostgresAlterTable,
6
+ PostgresCreatePolicy,
6
7
  PostgresCreateSchema,
7
8
  PostgresCreateTable,
9
+ PostgresDropPolicy,
10
+ type RlsPolicyOperation,
8
11
  } from '../core/ddl/nodes';
9
12
 
10
13
  /**
@@ -62,3 +65,35 @@ export function alterTable(options: {
62
65
  }): PostgresAlterTable {
63
66
  return new PostgresAlterTable(options);
64
67
  }
68
+
69
+ /**
70
+ * Build a Postgres `CREATE POLICY` DDL node.
71
+ *
72
+ * Identifiers (`schema`, `table`, `name`) are quoted by the renderer.
73
+ * `using` and `withCheck` are emitted verbatim as predicate SQL — callers
74
+ * must supply safe, pre-validated SQL expressions.
75
+ */
76
+ export function createPolicy(options: {
77
+ readonly schema: string;
78
+ readonly table: string;
79
+ readonly name: string;
80
+ readonly permissive: boolean;
81
+ readonly operation: RlsPolicyOperation;
82
+ readonly roles: readonly string[];
83
+ readonly using?: string;
84
+ readonly withCheck?: string;
85
+ }): PostgresCreatePolicy {
86
+ return new PostgresCreatePolicy(options);
87
+ }
88
+
89
+ /**
90
+ * Build a Postgres `DROP POLICY` DDL node.
91
+ * Identifiers (`schema`, `table`, `name`) are quoted by the renderer.
92
+ */
93
+ export function dropPolicy(options: {
94
+ readonly schema: string;
95
+ readonly table: string;
96
+ readonly name: string;
97
+ }): PostgresDropPolicy {
98
+ return new PostgresDropPolicy(options);
99
+ }
@@ -1,13 +1,94 @@
1
1
  import { temporalAuthoringPresets } from '@prisma-next/family-sql/control';
2
2
  import type {
3
+ AuthoringEntityContext,
3
4
  AuthoringEntityTypeNamespace,
4
5
  AuthoringFieldNamespace,
6
+ AuthoringPslBlockDescriptorNamespace,
5
7
  AuthoringTypeNamespace,
8
+ PslExtensionBlock,
6
9
  } from '@prisma-next/framework-components/authoring';
10
+ import { PostgresRlsPolicy } from './postgres-rls-policy';
11
+ import { PostgresRole, type PostgresRoleInput } from './postgres-role';
12
+ import { PostgresRlsPolicySchema, PostgresRoleSchema } from './postgres-validators';
13
+ import { computeContentHash, normalizePredicate } from './rls/canonicalize';
7
14
 
8
15
  export const postgresAuthoringTypes = {} as const satisfies AuthoringTypeNamespace;
9
16
 
10
- export const postgresAuthoringEntityTypes = {} as const satisfies AuthoringEntityTypeNamespace;
17
+ export interface RlsPolicyExtensionBlock extends PslExtensionBlock {
18
+ readonly namespaceId: string;
19
+ }
20
+
21
+ function readRefParam(block: PslExtensionBlock, key: string): string | undefined {
22
+ const param = block.parameters[key];
23
+ return param?.kind === 'ref' ? param.identifier : undefined;
24
+ }
25
+
26
+ function readValueParam(block: PslExtensionBlock, key: string): string | undefined {
27
+ const param = block.parameters[key];
28
+ return param?.kind === 'value' ? param.raw : undefined;
29
+ }
30
+
31
+ function readListRefParams(block: PslExtensionBlock, key: string): string[] {
32
+ const param = block.parameters[key];
33
+ if (param?.kind !== 'list') return [];
34
+ return param.items.flatMap((item) => (item.kind === 'ref' ? [item.identifier] : []));
35
+ }
36
+
37
+ function unwrapQuotedString(raw: string): string {
38
+ if (raw.startsWith('"') && raw.endsWith('"') && raw.length >= 2) {
39
+ return raw.slice(1, -1).replace(/\\"/g, '"');
40
+ }
41
+ return raw;
42
+ }
43
+
44
+ function lowerRlsPolicyFromBlock(
45
+ block: RlsPolicyExtensionBlock,
46
+ _ctx: AuthoringEntityContext,
47
+ ): PostgresRlsPolicy {
48
+ const prefix = block.name;
49
+ const targetModelName = readRefParam(block, 'target') ?? '';
50
+ const tableName = targetModelName.charAt(0).toLowerCase() + targetModelName.slice(1);
51
+ const roles = [...readListRefParams(block, 'roles')].sort();
52
+ const using = unwrapQuotedString(readValueParam(block, 'using') ?? '');
53
+
54
+ const wireHash = computeContentHash({
55
+ using: normalizePredicate(using),
56
+ roles,
57
+ operation: 'select',
58
+ permissive: true,
59
+ });
60
+ const wireName = `${prefix}_${wireHash}`;
61
+
62
+ return new PostgresRlsPolicy({
63
+ name: wireName,
64
+ prefix,
65
+ tableName,
66
+ namespaceId: block.namespaceId,
67
+ operation: 'select',
68
+ roles,
69
+ using,
70
+ permissive: true,
71
+ });
72
+ }
73
+
74
+ export const postgresAuthoringEntityTypes = {
75
+ role: {
76
+ kind: 'entity',
77
+ discriminator: 'role',
78
+ validatorSchema: PostgresRoleSchema,
79
+ output: {
80
+ factory: (input: PostgresRoleInput): PostgresRole => new PostgresRole(input),
81
+ },
82
+ },
83
+ policy: {
84
+ kind: 'entity',
85
+ discriminator: 'policy',
86
+ validatorSchema: PostgresRlsPolicySchema,
87
+ output: {
88
+ factory: lowerRlsPolicyFromBlock,
89
+ },
90
+ },
91
+ } as const satisfies AuthoringEntityTypeNamespace;
11
92
 
12
93
  /**
13
94
  * Field presets contributed by the Postgres target pack.
@@ -22,6 +103,39 @@ export const postgresAuthoringEntityTypes = {} as const satisfies AuthoringEntit
22
103
  * portability use `uuidString` / `id.uuidv4String` / `id.uuidv7String` from
23
104
  * the family pack instead.
24
105
  */
106
+ /**
107
+ * PSL block descriptor for `policy_select`.
108
+ *
109
+ * The parser learns the block shape from this descriptor; lowering from
110
+ * `PslExtensionBlock` to `PostgresRlsPolicy` is wired in the PSL
111
+ * interpreter (a later dispatch). The `discriminator` matches
112
+ * `PostgresRlsPolicy.kind` so the parsed block node carries the same
113
+ * discriminant as the IR class it will lower to.
114
+ *
115
+ * The `roles` list uses `scope:'cross-space'` because same-namespace
116
+ * role ref resolution requires PSL namespace entries keyed by `refKind`
117
+ * (i.e. `'role'`), which in turn requires the role block discriminator to
118
+ * equal `'role'`. Aligning discriminator with refKind is tracked for
119
+ * slice 4 (cross-space roles). Until then cross-space passes validation
120
+ * unconditionally and the authored role names flow through unchanged.
121
+ */
122
+ export const postgresAuthoringPslBlockDescriptors = {
123
+ policy_select: {
124
+ kind: 'pslBlock',
125
+ keyword: 'policy_select',
126
+ discriminator: 'policy',
127
+ name: { required: true },
128
+ parameters: {
129
+ target: { kind: 'ref', refKind: 'model', scope: 'same-namespace', required: true },
130
+ roles: {
131
+ kind: 'list',
132
+ of: { kind: 'ref', refKind: 'role', scope: 'cross-space' },
133
+ },
134
+ using: { kind: 'value', codecId: 'pg/text@1', required: true },
135
+ },
136
+ },
137
+ } as const satisfies AuthoringPslBlockDescriptorNamespace;
138
+
25
139
  export const postgresAuthoringFieldPresets = {
26
140
  text: {
27
141
  kind: 'fieldPreset',
@@ -3,6 +3,7 @@ import {
3
3
  DdlNode,
4
4
  type DdlTableConstraint,
5
5
  } from '@prisma-next/sql-relational-core/ast';
6
+ import type { RlsPolicyOperation } from '../rls/canonicalize';
6
7
 
7
8
  // ---------------------------------------------------------------------------
8
9
  // AlterTableAction — nested polymorphic hierarchy
@@ -42,6 +43,8 @@ export interface PostgresDdlVisitor<R> {
42
43
  createTable(node: PostgresCreateTable): R;
43
44
  createSchema(node: PostgresCreateSchema): R;
44
45
  alterTable(node: PostgresAlterTable): R;
46
+ createPolicy(node: PostgresCreatePolicy): R;
47
+ dropPolicy(node: PostgresDropPolicy): R;
45
48
  }
46
49
 
47
50
  export abstract class PostgresDdlNode extends DdlNode {
@@ -127,4 +130,68 @@ export class PostgresAlterTable extends PostgresDdlNode {
127
130
  }
128
131
  }
129
132
 
130
- export type AnyPostgresDdlNode = PostgresCreateTable | PostgresCreateSchema | PostgresAlterTable;
133
+ export type { RlsPolicyOperation };
134
+
135
+ export class PostgresCreatePolicy extends PostgresDdlNode {
136
+ readonly kind = 'create-policy' as const;
137
+ readonly schema: string;
138
+ readonly table: string;
139
+ readonly name: string;
140
+ readonly permissive: boolean;
141
+ readonly operation: RlsPolicyOperation;
142
+ readonly roles: ReadonlyArray<string>;
143
+ readonly using: string | undefined;
144
+ readonly withCheck: string | undefined;
145
+
146
+ constructor(options: {
147
+ readonly schema: string;
148
+ readonly table: string;
149
+ readonly name: string;
150
+ readonly permissive: boolean;
151
+ readonly operation: RlsPolicyOperation;
152
+ readonly roles: readonly string[];
153
+ readonly using?: string;
154
+ readonly withCheck?: string;
155
+ }) {
156
+ super();
157
+ this.schema = options.schema;
158
+ this.table = options.table;
159
+ this.name = options.name;
160
+ this.permissive = options.permissive;
161
+ this.operation = options.operation;
162
+ this.roles = Object.freeze([...options.roles]);
163
+ this.using = options.using;
164
+ this.withCheck = options.withCheck;
165
+ this.freeze();
166
+ }
167
+
168
+ override accept<R>(visitor: PostgresDdlVisitor<R>): R {
169
+ return visitor.createPolicy(this);
170
+ }
171
+ }
172
+
173
+ export class PostgresDropPolicy extends PostgresDdlNode {
174
+ readonly kind = 'drop-policy' as const;
175
+ readonly schema: string;
176
+ readonly table: string;
177
+ readonly name: string;
178
+
179
+ constructor(options: { readonly schema: string; readonly table: string; readonly name: string }) {
180
+ super();
181
+ this.schema = options.schema;
182
+ this.table = options.table;
183
+ this.name = options.name;
184
+ this.freeze();
185
+ }
186
+
187
+ override accept<R>(visitor: PostgresDdlVisitor<R>): R {
188
+ return visitor.dropPolicy(this);
189
+ }
190
+ }
191
+
192
+ export type AnyPostgresDdlNode =
193
+ | PostgresCreateTable
194
+ | PostgresCreateSchema
195
+ | PostgresAlterTable
196
+ | PostgresCreatePolicy
197
+ | PostgresDropPolicy;
@@ -2,9 +2,11 @@ import type { CodecTypes } from '../exports/codec-types';
2
2
  import {
3
3
  postgresAuthoringEntityTypes,
4
4
  postgresAuthoringFieldPresets,
5
+ postgresAuthoringPslBlockDescriptors,
5
6
  postgresAuthoringTypes,
6
7
  } from './authoring';
7
8
  import { postgresTargetDescriptorMetaRuntime } from './descriptor-meta-runtime';
9
+ import { postgresCreateNamespace } from './postgres-schema';
8
10
 
9
11
  const postgresTargetDescriptorMetaBase = {
10
12
  ...postgresTargetDescriptorMetaRuntime,
@@ -13,6 +15,8 @@ const postgresTargetDescriptorMetaBase = {
13
15
  type: postgresAuthoringTypes,
14
16
  field: postgresAuthoringFieldPresets,
15
17
  entityTypes: postgresAuthoringEntityTypes,
18
+ pslBlockDescriptors: postgresAuthoringPslBlockDescriptors,
19
+ createNamespace: postgresCreateNamespace,
16
20
  },
17
21
  } as const;
18
22
 
@@ -0,0 +1,16 @@
1
+ import type { EntityKindDescriptor } from '@prisma-next/framework-components/ir';
2
+ import { PostgresRlsPolicy, type PostgresRlsPolicyInput } from './postgres-rls-policy';
3
+ import { PostgresRole, type PostgresRoleInput } from './postgres-role';
4
+ import { PostgresRlsPolicySchema, PostgresRoleSchema } from './postgres-validators';
5
+
6
+ export const policyEntityKind: EntityKindDescriptor<PostgresRlsPolicyInput, PostgresRlsPolicy> = {
7
+ kind: 'policy',
8
+ schema: PostgresRlsPolicySchema,
9
+ construct: (input) => new PostgresRlsPolicy(input),
10
+ };
11
+
12
+ export const roleEntityKind: EntityKindDescriptor<PostgresRoleInput, PostgresRole> = {
13
+ kind: 'role',
14
+ schema: PostgresRoleSchema,
15
+ construct: (input) => new PostgresRole(input),
16
+ };
@@ -21,6 +21,8 @@ import type { PostgresOpFactoryCall } from './op-factory-call';
21
21
  const OBJECT_CREATION_FACTORIES: ReadonlySet<string> = new Set<string>([
22
22
  'createTable',
23
23
  'createSchema',
24
+ 'createRlsPolicy',
25
+ 'enableRowLevelSecurity',
24
26
  ]);
25
27
 
26
28
  function createsNewTopLevelObject(call: PostgresOpFactoryCall): boolean {
@@ -151,6 +153,7 @@ export function resolvePostgresCallControlPolicySubject(
151
153
  const POSTGRES_ISSUE_CREATION_FACTORY: Readonly<Record<string, string>> = Object.freeze({
152
154
  missing_schema: 'createSchema',
153
155
  missing_table: 'createTable',
156
+ missing_rls_policy: 'createRlsPolicy',
154
157
  });
155
158
 
156
159
  export function resolvePostgresIssueCreationFactoryName(issue: SchemaIssue): string | undefined {
@@ -695,6 +695,8 @@ type CallCategory =
695
695
  | 'dep'
696
696
  | 'drop'
697
697
  | 'table'
698
+ | 'rlsEnable'
699
+ | 'rlsPolicy'
698
700
  | 'column'
699
701
  | 'alter'
700
702
  | 'primaryKey'
@@ -724,6 +726,12 @@ function classifyCall(call: PostgresOpFactoryCall): CallCategory {
724
726
  return 'unique'; // after uniques, before indexes
725
727
  case 'createTable':
726
728
  return 'table';
729
+ case 'enableRowLevelSecurity':
730
+ return 'rlsEnable';
731
+ case 'createRlsPolicy':
732
+ return 'rlsPolicy';
733
+ case 'dropRlsPolicy':
734
+ return 'drop';
727
735
  case 'addColumn':
728
736
  return 'column';
729
737
  case 'alterColumnType':
@@ -40,6 +40,7 @@ import { blindCast } from '@prisma-next/utils/casts';
40
40
  import { ifDefined } from '@prisma-next/utils/defined';
41
41
  import { columnExistsAst, tableExistsAst } from '../../contract-free/checks';
42
42
  import * as contractFreeDdl from '../../contract-free/ddl';
43
+ import type { PostgresRlsPolicy } from '../postgres-rls-policy';
43
44
  import { escapeLiteral, quoteIdentifier } from '../sql-utils';
44
45
  import type { PostgresColumnDefault } from '../types';
45
46
  import {
@@ -61,6 +62,7 @@ import {
61
62
  } from './operations/constraints';
62
63
  import { createExtension } from './operations/dependencies';
63
64
  import { createIndex, dropIndex } from './operations/indexes';
65
+ import { createRlsPolicy, dropRlsPolicy, enableRowLevelSecurity } from './operations/rls';
64
66
  import type { ForeignKeySpec } from './operations/shared';
65
67
  import { step, targetDetails } from './operations/shared';
66
68
  import { dropTable } from './operations/tables';
@@ -1376,6 +1378,97 @@ export class DataTransformCall extends PostgresOpFactoryCallNode {
1376
1378
  }
1377
1379
  }
1378
1380
 
1381
+ export class CreatePostgresRlsPolicyCall extends PostgresOpFactoryCallNode {
1382
+ readonly factoryName = 'createRlsPolicy' as const;
1383
+ readonly operationClass = 'additive' as const;
1384
+ readonly schemaName: string;
1385
+ readonly tableName: string;
1386
+ readonly policy: PostgresRlsPolicy;
1387
+ readonly label: string;
1388
+
1389
+ constructor(schemaName: string, tableName: string, policy: PostgresRlsPolicy) {
1390
+ super();
1391
+ this.schemaName = schemaName;
1392
+ this.tableName = tableName;
1393
+ this.policy = policy;
1394
+ this.label = `Create RLS policy "${policy.name}" on "${tableName}"`;
1395
+ this.freeze();
1396
+ }
1397
+
1398
+ async toOp(lowerer?: ExecuteRequestLowerer): Promise<Op> {
1399
+ if (lowerer === undefined) {
1400
+ throw new Error(
1401
+ `CreatePostgresRlsPolicyCall.toOp: a lowerer is required on the Postgres planner path (policy "${this.policy.name}" on table "${this.tableName}"). Pass the control adapter to createPostgresMigrationPlanner.`,
1402
+ );
1403
+ }
1404
+ return createRlsPolicy(this.schemaName, this.tableName, this.policy, lowerer);
1405
+ }
1406
+
1407
+ renderTypeScript(): string {
1408
+ return `createRlsPolicy(${jsonToTsSource(this.schemaName)}, ${jsonToTsSource(this.tableName)}, ${jsonToTsSource(this.policy)})`;
1409
+ }
1410
+ }
1411
+
1412
+ export class DropPostgresRlsPolicyCall extends PostgresOpFactoryCallNode {
1413
+ readonly factoryName = 'dropRlsPolicy' as const;
1414
+ readonly operationClass = 'destructive' as const;
1415
+ readonly schemaName: string;
1416
+ readonly tableName: string;
1417
+ readonly policyName: string;
1418
+ readonly label: string;
1419
+
1420
+ constructor(schemaName: string, tableName: string, policyName: string) {
1421
+ super();
1422
+ this.schemaName = schemaName;
1423
+ this.tableName = tableName;
1424
+ this.policyName = policyName;
1425
+ this.label = `Drop RLS policy "${policyName}" on "${tableName}"`;
1426
+ this.freeze();
1427
+ }
1428
+
1429
+ async toOp(lowerer?: ExecuteRequestLowerer): Promise<Op> {
1430
+ if (lowerer === undefined) {
1431
+ throw new Error(
1432
+ `DropPostgresRlsPolicyCall.toOp: a lowerer is required on the Postgres planner path (policy "${this.policyName}" on table "${this.tableName}"). Pass the control adapter to createPostgresMigrationPlanner.`,
1433
+ );
1434
+ }
1435
+ return dropRlsPolicy(this.schemaName, this.tableName, this.policyName, lowerer);
1436
+ }
1437
+
1438
+ renderTypeScript(): string {
1439
+ return `dropRlsPolicy(${jsonToTsSource(this.schemaName)}, ${jsonToTsSource(this.tableName)}, ${jsonToTsSource(this.policyName)})`;
1440
+ }
1441
+ }
1442
+
1443
+ export class EnableRowLevelSecurityCall extends PostgresOpFactoryCallNode {
1444
+ readonly factoryName = 'enableRowLevelSecurity' as const;
1445
+ readonly operationClass = 'additive' as const;
1446
+ readonly schemaName: string;
1447
+ readonly tableName: string;
1448
+ readonly label: string;
1449
+
1450
+ constructor(schemaName: string, tableName: string) {
1451
+ super();
1452
+ this.schemaName = schemaName;
1453
+ this.tableName = tableName;
1454
+ this.label = `Enable row-level security on "${tableName}"`;
1455
+ this.freeze();
1456
+ }
1457
+
1458
+ async toOp(lowerer?: ExecuteRequestLowerer): Promise<Op> {
1459
+ if (lowerer === undefined) {
1460
+ throw new Error(
1461
+ `EnableRowLevelSecurityCall.toOp: a lowerer is required on the Postgres planner path (table "${this.tableName}"). Pass the control adapter to createPostgresMigrationPlanner.`,
1462
+ );
1463
+ }
1464
+ return enableRowLevelSecurity(this.schemaName, this.tableName, lowerer);
1465
+ }
1466
+
1467
+ renderTypeScript(): string {
1468
+ return `enableRowLevelSecurity(${jsonToTsSource(this.schemaName)}, ${jsonToTsSource(this.tableName)})`;
1469
+ }
1470
+ }
1471
+
1379
1472
  export type PostgresOpFactoryCall =
1380
1473
  | CreateTableCall
1381
1474
  | DropTableCall
@@ -1399,4 +1492,7 @@ export type PostgresOpFactoryCall =
1399
1492
  | RawSqlCall
1400
1493
  | CreateExtensionCall
1401
1494
  | CreateSchemaCall
1495
+ | CreatePostgresRlsPolicyCall
1496
+ | DropPostgresRlsPolicyCall
1497
+ | EnableRowLevelSecurityCall
1402
1498
  | DataTransformCall;
@@ -0,0 +1,106 @@
1
+ import type { ExecuteRequestLowerer } from '@prisma-next/family-sql/control-adapter';
2
+ import { ifDefined } from '@prisma-next/utils/defined';
3
+ import { rlsEnabledAst, rlsPolicyExistsAst } from '../../../contract-free/checks';
4
+ import { createPolicy, dropPolicy } from '../../../contract-free/ddl';
5
+ import type { PostgresRlsPolicy } from '../../postgres-rls-policy';
6
+ import { qualifyTableName } from '../planner-sql-checks';
7
+ import { type Op, step, targetDetails } from './shared';
8
+
9
+ const PLAIN_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;
10
+
11
+ function validateRoleName(role: string): string {
12
+ if (!PLAIN_IDENTIFIER.test(role)) {
13
+ throw new Error(
14
+ `Invalid role name ${JSON.stringify(role)}: role names must be plain SQL identifiers matching ^[A-Za-z_][A-Za-z0-9_$]*$`,
15
+ );
16
+ }
17
+ return role;
18
+ }
19
+
20
+ export async function createRlsPolicy(
21
+ schemaName: string,
22
+ tableName: string,
23
+ policy: PostgresRlsPolicy,
24
+ lowerer: ExecuteRequestLowerer,
25
+ ): Promise<Op> {
26
+ const validatedRoles = policy.roles.map(validateRoleName);
27
+ const ddlNode = createPolicy({
28
+ schema: schemaName,
29
+ table: tableName,
30
+ name: policy.name,
31
+ permissive: policy.permissive,
32
+ operation: policy.operation,
33
+ roles: validatedRoles,
34
+ ...ifDefined('using', policy.using),
35
+ ...ifDefined('withCheck', policy.withCheck),
36
+ });
37
+ const checks = rlsPolicyExistsAst({
38
+ schema: schemaName,
39
+ table: tableName,
40
+ policyName: policy.name,
41
+ });
42
+ const absent = await lowerer.lowerToExecuteRequest(checks.policyAbsent());
43
+ const execute = await lowerer.lowerToExecuteRequest(ddlNode);
44
+ const present = await lowerer.lowerToExecuteRequest(checks.policyPresent());
45
+ return {
46
+ id: `rlsPolicy.${schemaName}.${tableName}.${policy.name}`,
47
+ label: `Create RLS policy "${policy.name}" on "${tableName}"`,
48
+ operationClass: 'additive',
49
+ target: targetDetails('rlsPolicy', policy.name, schemaName, tableName),
50
+ precheck: [
51
+ step(`ensure RLS policy "${policy.name}" does not exist`, absent.sql, absent.params),
52
+ ],
53
+ execute: [step(`create RLS policy "${policy.name}"`, execute.sql, execute.params)],
54
+ postcheck: [step(`verify RLS policy "${policy.name}" exists`, present.sql, present.params)],
55
+ };
56
+ }
57
+
58
+ export async function dropRlsPolicy(
59
+ schemaName: string,
60
+ tableName: string,
61
+ policyName: string,
62
+ lowerer: ExecuteRequestLowerer,
63
+ ): Promise<Op> {
64
+ const ddlNode = dropPolicy({ schema: schemaName, table: tableName, name: policyName });
65
+ const checks = rlsPolicyExistsAst({ schema: schemaName, table: tableName, policyName });
66
+ const present = await lowerer.lowerToExecuteRequest(checks.policyPresent());
67
+ const execute = await lowerer.lowerToExecuteRequest(ddlNode);
68
+ const absent = await lowerer.lowerToExecuteRequest(checks.policyAbsent());
69
+ return {
70
+ id: `rlsPolicy.${schemaName}.${tableName}.${policyName}.drop`,
71
+ label: `Drop RLS policy "${policyName}" on "${tableName}"`,
72
+ operationClass: 'destructive',
73
+ target: targetDetails('rlsPolicy', policyName, schemaName, tableName),
74
+ precheck: [step(`ensure RLS policy "${policyName}" exists`, present.sql, present.params)],
75
+ execute: [step(`drop RLS policy "${policyName}"`, execute.sql, execute.params)],
76
+ postcheck: [step(`verify RLS policy "${policyName}" is absent`, absent.sql, absent.params)],
77
+ };
78
+ }
79
+
80
+ export async function enableRowLevelSecurity(
81
+ schemaName: string,
82
+ tableName: string,
83
+ lowerer: ExecuteRequestLowerer,
84
+ ): Promise<Op> {
85
+ const checks = rlsEnabledAst(schemaName, tableName);
86
+ const disabled = await lowerer.lowerToExecuteRequest(checks.rlsDisabled());
87
+ const enabled = await lowerer.lowerToExecuteRequest(checks.rlsEnabled());
88
+ return {
89
+ id: `rowLevelSecurity.${schemaName}.${tableName}`,
90
+ label: `Enable row-level security on "${tableName}"`,
91
+ operationClass: 'additive',
92
+ target: targetDetails('rowLevelSecurity', tableName, schemaName),
93
+ precheck: [
94
+ step(`check RLS is not already enabled on "${tableName}"`, disabled.sql, disabled.params),
95
+ ],
96
+ execute: [
97
+ step(
98
+ `enable row-level security on "${tableName}"`,
99
+ `ALTER TABLE ${qualifyTableName(schemaName, tableName)} ENABLE ROW LEVEL SECURITY`,
100
+ ),
101
+ ],
102
+ postcheck: [
103
+ step(`verify row-level security is enabled on "${tableName}"`, enabled.sql, enabled.params),
104
+ ],
105
+ };
106
+ }
@@ -9,7 +9,9 @@ export type OperationClass =
9
9
  | 'unique'
10
10
  | 'index'
11
11
  | 'foreignKey'
12
- | 'checkConstraint';
12
+ | 'checkConstraint'
13
+ | 'rlsPolicy'
14
+ | 'rowLevelSecurity';
13
15
 
14
16
  export interface PostgresPlanTargetDetails {
15
17
  readonly schema: string;