@prisma-next/target-postgres 0.14.0-dev.55 → 0.14.0-dev.56
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.
- package/dist/{authoring-BBIGcFZM.mjs → authoring-WkqG_MkU.mjs} +38 -6
- package/dist/authoring-WkqG_MkU.mjs.map +1 -0
- package/dist/contract-free.d.mts +27 -2
- package/dist/contract-free.d.mts.map +1 -1
- package/dist/contract-free.mjs +3 -3
- package/dist/{control-policy-DievMCOV.mjs → control-policy-BtOZBVxL.mjs} +38 -13
- package/dist/control-policy-BtOZBVxL.mjs.map +1 -0
- package/dist/control.mjs +6 -6
- package/dist/{ddl-D5j4JiAB.mjs → ddl-B-krEXY1.mjs} +18 -4
- package/dist/{ddl-D5j4JiAB.mjs.map → ddl-B-krEXY1.mjs.map} +1 -1
- package/dist/ddl.d.mts +2 -2
- package/dist/ddl.mjs +2 -2
- package/dist/{descriptor-meta-C-tyS25s.mjs → descriptor-meta-BOcKPaLE.mjs} +4 -3
- package/dist/descriptor-meta-BOcKPaLE.mjs.map +1 -0
- package/dist/{diff-database-schema-ff3ioNtK.mjs → diff-database-schema-CWb-jeLy.mjs} +19 -11
- package/dist/diff-database-schema-CWb-jeLy.mjs.map +1 -0
- package/dist/diff-database-schema.d.mts +1 -1
- package/dist/diff-database-schema.d.mts.map +1 -1
- package/dist/diff-database-schema.mjs +1 -1
- package/dist/issue-planner.d.mts +1 -1
- package/dist/issue-planner.d.mts.map +1 -1
- package/dist/issue-planner.mjs +1 -1
- package/dist/migration.d.mts +2 -2
- package/dist/migration.mjs +2 -2
- package/dist/{nodes-Dh__NS58.mjs → nodes-B815pj_X.mjs} +34 -2
- package/dist/{nodes-Dh__NS58.mjs.map → nodes-B815pj_X.mjs.map} +1 -1
- package/dist/{nodes-CSkPX3rI.d.mts → nodes-BtQyZha6.d.mts} +29 -3
- package/dist/nodes-BtQyZha6.d.mts.map +1 -0
- package/dist/{op-factory-call-Dt-Jx5JS.d.mts → op-factory-call-D6_7-7Vc.d.mts} +24 -2
- package/dist/{op-factory-call-Dt-Jx5JS.d.mts.map → op-factory-call-D6_7-7Vc.d.mts.map} +1 -1
- package/dist/{op-factory-call-DZWTTjKM.mjs → op-factory-call-DxNNKUeZ.mjs} +99 -4
- package/dist/op-factory-call-DxNNKUeZ.mjs.map +1 -0
- package/dist/op-factory-call.d.mts +1 -1
- package/dist/op-factory-call.mjs +1 -1
- package/dist/pack.d.mts +29 -2
- package/dist/pack.d.mts.map +1 -1
- package/dist/pack.mjs +1 -1
- package/dist/{planner-DWQYPxP8.mjs → planner-BryLk0Ie.mjs} +80 -24
- package/dist/planner-BryLk0Ie.mjs.map +1 -0
- package/dist/{planner-produced-postgres-migration-Ch1L8RiV.mjs → planner-produced-postgres-migration-BLJH1zIc.mjs} +2 -2
- package/dist/{planner-produced-postgres-migration-Ch1L8RiV.mjs.map → planner-produced-postgres-migration-BLJH1zIc.mjs.map} +1 -1
- package/dist/{planner-produced-postgres-migration-C0a7I1-e.d.mts → planner-produced-postgres-migration-CGifB6VH.d.mts} +2 -2
- package/dist/{planner-produced-postgres-migration-C0a7I1-e.d.mts.map → planner-produced-postgres-migration-CGifB6VH.d.mts.map} +1 -1
- package/dist/planner-produced-postgres-migration.d.mts +1 -1
- package/dist/planner-produced-postgres-migration.mjs +1 -1
- package/dist/{planner-sql-checks-D8Q5ck4o.mjs → planner-sql-checks-AXoxB1Jl.mjs} +2 -2
- package/dist/{planner-sql-checks-D8Q5ck4o.mjs.map → planner-sql-checks-AXoxB1Jl.mjs.map} +1 -1
- package/dist/planner-sql-checks.mjs +1 -1
- package/dist/planner.d.mts +23 -6
- package/dist/planner.d.mts.map +1 -1
- package/dist/planner.mjs +2 -2
- package/dist/{postgres-contract-view-C-By7rOB.mjs → postgres-contract-view-Dg-N7KA7.mjs} +4 -3
- package/dist/postgres-contract-view-Dg-N7KA7.mjs.map +1 -0
- package/dist/{postgres-database-schema-node-UpzA0Ug5.d.mts → postgres-database-schema-node-CfdTY-Qs.d.mts} +18 -3
- package/dist/postgres-database-schema-node-CfdTY-Qs.d.mts.map +1 -0
- package/dist/{postgres-migration-D3XACWKM.mjs → postgres-migration-BahMHTbj.mjs} +3 -3
- package/dist/{postgres-migration-D3XACWKM.mjs.map → postgres-migration-BahMHTbj.mjs.map} +1 -1
- package/dist/{postgres-migration-DFaxuRyI.d.mts → postgres-migration-cyBe2Ee0.d.mts} +2 -2
- package/dist/{postgres-migration-DFaxuRyI.d.mts.map → postgres-migration-cyBe2Ee0.d.mts.map} +1 -1
- package/dist/{postgres-table-schema-node-BZWWEo7B.mjs → postgres-role-schema-node-BiSTCTNg.mjs} +4 -69
- package/dist/postgres-role-schema-node-BiSTCTNg.mjs.map +1 -0
- package/dist/{postgres-schema-9vFZLwGo.d.mts → postgres-schema-CU4gF6QZ.d.mts} +30 -2
- package/dist/postgres-schema-CU4gF6QZ.d.mts.map +1 -0
- package/dist/{postgres-schema-DcG72fcT.mjs → postgres-schema-DTLA_aRi.mjs} +41 -2
- package/dist/postgres-schema-DTLA_aRi.mjs.map +1 -0
- package/dist/postgres-table-schema-node-9FCtCUz2.mjs +153 -0
- package/dist/postgres-table-schema-node-9FCtCUz2.mjs.map +1 -0
- package/dist/{resolve-ddl-schema-BOtCoeKV.mjs → resolve-ddl-schema-Ca88Tg6W.mjs} +2 -2
- package/dist/{resolve-ddl-schema-BOtCoeKV.mjs.map → resolve-ddl-schema-Ca88Tg6W.mjs.map} +1 -1
- package/dist/rls-canonicalize.d.mts +19 -1
- package/dist/rls-canonicalize.d.mts.map +1 -0
- package/dist/rls-canonicalize.mjs +2 -2
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +1 -1
- package/dist/schema-ir-annotations.mjs +1 -1
- package/dist/types.d.mts +3 -3
- package/dist/types.mjs +4 -3
- package/dist/{canonicalize-Bn3BM1Cn.mjs → wire-name-rHHtr1h0.mjs} +31 -2
- package/dist/wire-name-rHHtr1h0.mjs.map +1 -0
- package/package.json +20 -20
- package/src/contract-free/ddl.ts +26 -0
- package/src/core/authoring.ts +44 -1
- package/src/core/ddl/nodes.ts +48 -1
- package/src/core/descriptor-meta.ts +2 -0
- package/src/core/entity-kinds.ts +11 -0
- package/src/core/migrations/contract-to-postgres-database-schema-node.ts +9 -0
- package/src/core/migrations/control-policy.ts +31 -1
- package/src/core/migrations/diff-database-schema.ts +1 -0
- package/src/core/migrations/issue-planner.ts +49 -4
- package/src/core/migrations/op-factory-call.ts +81 -1
- package/src/core/migrations/operations/rls.ts +82 -1
- package/src/core/migrations/planner.ts +110 -26
- package/src/core/postgres-contract-serializer.ts +7 -1
- package/src/core/postgres-rls-enablement.ts +34 -0
- package/src/core/postgres-schema.ts +19 -3
- package/src/core/postgres-validators.ts +6 -0
- package/src/core/rls/wire-name.ts +33 -0
- package/src/core/schema-ir/postgres-table-schema-node.ts +25 -3
- package/src/exports/contract-free.ts +4 -0
- package/src/exports/ddl.ts +2 -0
- package/src/exports/rls-canonicalize.ts +1 -0
- package/src/exports/types.ts +4 -0
- package/dist/authoring-BBIGcFZM.mjs.map +0 -1
- package/dist/canonicalize-Bn3BM1Cn.mjs.map +0 -1
- package/dist/control-policy-DievMCOV.mjs.map +0 -1
- package/dist/descriptor-meta-C-tyS25s.mjs.map +0 -1
- package/dist/diff-database-schema-ff3ioNtK.mjs.map +0 -1
- package/dist/nodes-CSkPX3rI.d.mts.map +0 -1
- package/dist/op-factory-call-DZWTTjKM.mjs.map +0 -1
- package/dist/planner-DWQYPxP8.mjs.map +0 -1
- package/dist/postgres-contract-view-C-By7rOB.mjs.map +0 -1
- package/dist/postgres-database-schema-node-UpzA0Ug5.d.mts.map +0 -1
- package/dist/postgres-schema-9vFZLwGo.d.mts.map +0 -1
- package/dist/postgres-schema-DcG72fcT.mjs.map +0 -1
- package/dist/postgres-table-schema-node-BZWWEo7B.mjs.map +0 -1
- package/dist/schema-node-kinds-ClScchhi.mjs +0 -76
- package/dist/schema-node-kinds-ClScchhi.mjs.map +0 -1
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
CreateIndexCall,
|
|
64
64
|
CreateSchemaCall,
|
|
65
65
|
CreateTableCall,
|
|
66
|
+
DisableRowLevelSecurityCall,
|
|
66
67
|
DropCheckConstraintCall,
|
|
67
68
|
DropColumnCall,
|
|
68
69
|
DropConstraintCall,
|
|
@@ -70,6 +71,7 @@ import {
|
|
|
70
71
|
DropIndexCall,
|
|
71
72
|
DropNotNullCall,
|
|
72
73
|
DropTableCall,
|
|
74
|
+
EnableRowLevelSecurityCall,
|
|
73
75
|
type PostgresOpFactoryCall,
|
|
74
76
|
SetDefaultCall,
|
|
75
77
|
SetNotNullCall,
|
|
@@ -163,6 +165,7 @@ function classifyCall(call: PostgresOpFactoryCall): CallCategory {
|
|
|
163
165
|
case 'createTable':
|
|
164
166
|
return 'table';
|
|
165
167
|
case 'enableRowLevelSecurity':
|
|
168
|
+
case 'disableRowLevelSecurity':
|
|
166
169
|
return 'rlsEnable';
|
|
167
170
|
case 'createRlsPolicy':
|
|
168
171
|
return 'rlsPolicy';
|
|
@@ -465,6 +468,7 @@ function fkSpecFromNode(fk: SqlForeignKeyIR, tableName: string): ForeignKeySpec
|
|
|
465
468
|
*/
|
|
466
469
|
function buildCreateTableCallsFromNode(
|
|
467
470
|
schemaName: string,
|
|
471
|
+
ddlSchemaName: string,
|
|
468
472
|
table: PostgresTableSchemaNode,
|
|
469
473
|
codecHooks: ReadonlyMap<string, CodecControlHooks>,
|
|
470
474
|
): PostgresOpFactoryCall[] {
|
|
@@ -509,6 +513,13 @@ function buildCreateTableCallsFromNode(
|
|
|
509
513
|
const constraintName = unique.name ?? `${table.name}_${unique.columns.join('_')}_key`;
|
|
510
514
|
calls.push(new AddUniqueCall(schemaName, table.name, constraintName, [...unique.columns]));
|
|
511
515
|
}
|
|
516
|
+
// Marker-driven: a newly-created table that is RLS-controlled enables RLS
|
|
517
|
+
// as part of its creation bundle. The policy set never decides this. The
|
|
518
|
+
// resolved schema (not the emission sentinel) binds into the op's
|
|
519
|
+
// relrowsecurity checks.
|
|
520
|
+
if (table.rlsEnabled) {
|
|
521
|
+
calls.push(new EnableRowLevelSecurityCall(ddlSchemaName, table.name));
|
|
522
|
+
}
|
|
512
523
|
return calls;
|
|
513
524
|
}
|
|
514
525
|
|
|
@@ -519,6 +530,11 @@ function nodeConflict(kind: SqlPlannerConflict['kind'], message: string): SqlPla
|
|
|
519
530
|
function mapTableNodeIssue(
|
|
520
531
|
issue: SchemaDiffIssue,
|
|
521
532
|
schemaName: string,
|
|
533
|
+
// The diff tree's RESOLVED physical schema. Enablement ops bind it into
|
|
534
|
+
// their `pg_class`/`pg_namespace` checks (the emission sentinel would
|
|
535
|
+
// never match a live nspname), mirroring the schema the retired
|
|
536
|
+
// policy-half enable resolved via `resolveDdlSchemaForNamespaceStorage`.
|
|
537
|
+
ddlSchemaName: string,
|
|
522
538
|
codecHooks: ReadonlyMap<string, CodecControlHooks>,
|
|
523
539
|
): Result<readonly PostgresOpFactoryCall[], SqlPlannerConflict> {
|
|
524
540
|
if (issue.reason === 'not-found') {
|
|
@@ -526,7 +542,7 @@ function mapTableNodeIssue(
|
|
|
526
542
|
PostgresTableSchemaNode,
|
|
527
543
|
'a not-found table issue always carries the expected PostgresTableSchemaNode'
|
|
528
544
|
>(issue.expected);
|
|
529
|
-
return ok(buildCreateTableCallsFromNode(schemaName, table, codecHooks));
|
|
545
|
+
return ok(buildCreateTableCallsFromNode(schemaName, ddlSchemaName, table, codecHooks));
|
|
530
546
|
}
|
|
531
547
|
if (issue.reason === 'not-expected') {
|
|
532
548
|
const table = blindCast<
|
|
@@ -535,8 +551,33 @@ function mapTableNodeIssue(
|
|
|
535
551
|
>(issue.actual);
|
|
536
552
|
return ok([new DropTableCall(schemaName, table.name)]);
|
|
537
553
|
}
|
|
538
|
-
//
|
|
539
|
-
|
|
554
|
+
// A paired table `not-equal` means enablement drift TODAY, because
|
|
555
|
+
// `isEqualTo` compares only name + `rlsEnabled`. Key on the actual
|
|
556
|
+
// expected-vs-actual `rlsEnabled` delta rather than assuming it: emit ENABLE
|
|
557
|
+
// when it flipped on, DISABLE when it flipped off, and fail loud when
|
|
558
|
+
// `rlsEnabled` matches on both sides — that means a second table attribute
|
|
559
|
+
// drifted into `isEqualTo` and this mapper does not yet handle it. The
|
|
560
|
+
// expected side is authoritative (marker-driven, never the policy set).
|
|
561
|
+
const expected = blindCast<
|
|
562
|
+
PostgresTableSchemaNode,
|
|
563
|
+
'a not-equal table issue always carries the expected PostgresTableSchemaNode'
|
|
564
|
+
>(issue.expected);
|
|
565
|
+
const actual = blindCast<
|
|
566
|
+
PostgresTableSchemaNode,
|
|
567
|
+
'a not-equal table issue always carries the actual PostgresTableSchemaNode'
|
|
568
|
+
>(issue.actual);
|
|
569
|
+
if (expected.rlsEnabled && !actual.rlsEnabled) {
|
|
570
|
+
return ok([new EnableRowLevelSecurityCall(ddlSchemaName, expected.name)]);
|
|
571
|
+
}
|
|
572
|
+
if (!expected.rlsEnabled && actual.rlsEnabled) {
|
|
573
|
+
return ok([new DisableRowLevelSecurityCall(ddlSchemaName, expected.name)]);
|
|
574
|
+
}
|
|
575
|
+
return notOk(
|
|
576
|
+
nodeConflict(
|
|
577
|
+
'unsupportedOperation',
|
|
578
|
+
`unhandled table-attribute drift on "${expected.name}": table not-equal with no rlsEnabled delta (a second table attribute drifted)`,
|
|
579
|
+
),
|
|
580
|
+
);
|
|
540
581
|
}
|
|
541
582
|
|
|
542
583
|
function mapColumnNodeIssue(
|
|
@@ -787,7 +828,7 @@ export function mapNodeIssueToCall(
|
|
|
787
828
|
|
|
788
829
|
switch (node.nodeKind) {
|
|
789
830
|
case PostgresSchemaNodeKind.table:
|
|
790
|
-
return mapTableNodeIssue(issue, schemaName, ctx.codecHooks);
|
|
831
|
+
return mapTableNodeIssue(issue, schemaName, ddlSchemaName, ctx.codecHooks);
|
|
791
832
|
case RelationalSchemaNodeKind.column:
|
|
792
833
|
return mapColumnNodeIssue(issue, schemaName, tableName, ctx.codecHooks);
|
|
793
834
|
case RelationalSchemaNodeKind.columnDefault: {
|
|
@@ -952,6 +993,10 @@ export function planIssues(
|
|
|
952
993
|
...byCategory('unique'),
|
|
953
994
|
...byCategory('index'),
|
|
954
995
|
...byCategory('foreignKey'),
|
|
996
|
+
// Enablement changes run after all relational DDL (the table must exist)
|
|
997
|
+
// and before the policy calls the planner appends after `planIssues` —
|
|
998
|
+
// the same position the retired imperative enable-on-first-policy used.
|
|
999
|
+
...byCategory('rlsEnable'),
|
|
955
1000
|
];
|
|
956
1001
|
|
|
957
1002
|
return ok({ calls });
|
|
@@ -63,7 +63,13 @@ import {
|
|
|
63
63
|
} from './operations/constraints';
|
|
64
64
|
import { createExtension } from './operations/dependencies';
|
|
65
65
|
import { createIndex, dropIndex } from './operations/indexes';
|
|
66
|
-
import {
|
|
66
|
+
import {
|
|
67
|
+
createRlsPolicy,
|
|
68
|
+
disableRowLevelSecurity,
|
|
69
|
+
dropRlsPolicy,
|
|
70
|
+
enableRowLevelSecurity,
|
|
71
|
+
renameRlsPolicy,
|
|
72
|
+
} from './operations/rls';
|
|
67
73
|
import type { ForeignKeySpec } from './operations/shared';
|
|
68
74
|
import { step, targetDetails } from './operations/shared';
|
|
69
75
|
import { dropTable } from './operations/tables';
|
|
@@ -1461,6 +1467,78 @@ export class EnableRowLevelSecurityCall extends PostgresOpFactoryCallNode {
|
|
|
1461
1467
|
}
|
|
1462
1468
|
}
|
|
1463
1469
|
|
|
1470
|
+
export class DisableRowLevelSecurityCall extends PostgresOpFactoryCallNode {
|
|
1471
|
+
readonly factoryName = 'disableRowLevelSecurity' as const;
|
|
1472
|
+
readonly operationClass = 'destructive' as const;
|
|
1473
|
+
readonly schemaName: string;
|
|
1474
|
+
readonly tableName: string;
|
|
1475
|
+
readonly label: string;
|
|
1476
|
+
|
|
1477
|
+
constructor(schemaName: string, tableName: string) {
|
|
1478
|
+
super();
|
|
1479
|
+
this.schemaName = schemaName;
|
|
1480
|
+
this.tableName = tableName;
|
|
1481
|
+
this.label = `Disable row-level security on "${tableName}"`;
|
|
1482
|
+
this.freeze();
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
async toOp(lowerer?: ExecuteRequestLowerer): Promise<Op> {
|
|
1486
|
+
if (lowerer === undefined) {
|
|
1487
|
+
throw new Error(
|
|
1488
|
+
`DisableRowLevelSecurityCall.toOp: a lowerer is required on the Postgres planner path (table "${this.tableName}"). Pass the control adapter to createPostgresMigrationPlanner.`,
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
return disableRowLevelSecurity(this.schemaName, this.tableName, lowerer);
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
renderTypeScript(): string {
|
|
1495
|
+
return `disableRowLevelSecurity(${jsonToTsSource(this.schemaName)}, ${jsonToTsSource(this.tableName)})`;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
export class RenamePostgresRlsPolicyCall extends PostgresOpFactoryCallNode {
|
|
1500
|
+
readonly factoryName = 'renameRlsPolicy' as const;
|
|
1501
|
+
// `widening` is chosen so the rename plans under every allowance set except
|
|
1502
|
+
// additive-only init — a rename is neither additive-creation nor
|
|
1503
|
+
// destructive, and the class vocabulary has no neutral middle class. It is
|
|
1504
|
+
// NOT that a rename widens anything; this is the accepted typology tradeoff.
|
|
1505
|
+
readonly operationClass = 'widening' as const;
|
|
1506
|
+
readonly schemaName: string;
|
|
1507
|
+
readonly tableName: string;
|
|
1508
|
+
readonly oldPolicyName: string;
|
|
1509
|
+
readonly newPolicyName: string;
|
|
1510
|
+
readonly label: string;
|
|
1511
|
+
|
|
1512
|
+
constructor(schemaName: string, tableName: string, oldPolicyName: string, newPolicyName: string) {
|
|
1513
|
+
super();
|
|
1514
|
+
this.schemaName = schemaName;
|
|
1515
|
+
this.tableName = tableName;
|
|
1516
|
+
this.oldPolicyName = oldPolicyName;
|
|
1517
|
+
this.newPolicyName = newPolicyName;
|
|
1518
|
+
this.label = `Rename RLS policy "${oldPolicyName}" to "${newPolicyName}" on "${tableName}"`;
|
|
1519
|
+
this.freeze();
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
async toOp(lowerer?: ExecuteRequestLowerer): Promise<Op> {
|
|
1523
|
+
if (lowerer === undefined) {
|
|
1524
|
+
throw new Error(
|
|
1525
|
+
`RenamePostgresRlsPolicyCall.toOp: a lowerer is required on the Postgres planner path (policy "${this.oldPolicyName}" on table "${this.tableName}"). Pass the control adapter to createPostgresMigrationPlanner.`,
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
return renameRlsPolicy(
|
|
1529
|
+
this.schemaName,
|
|
1530
|
+
this.tableName,
|
|
1531
|
+
this.oldPolicyName,
|
|
1532
|
+
this.newPolicyName,
|
|
1533
|
+
lowerer,
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
renderTypeScript(): string {
|
|
1538
|
+
return `renameRlsPolicy(${jsonToTsSource(this.schemaName)}, ${jsonToTsSource(this.tableName)}, ${jsonToTsSource(this.oldPolicyName)}, ${jsonToTsSource(this.newPolicyName)})`;
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1464
1542
|
export type PostgresOpFactoryCall =
|
|
1465
1543
|
| CreateTableCall
|
|
1466
1544
|
| DropTableCall
|
|
@@ -1487,4 +1565,6 @@ export type PostgresOpFactoryCall =
|
|
|
1487
1565
|
| CreatePostgresRlsPolicyCall
|
|
1488
1566
|
| DropPostgresRlsPolicyCall
|
|
1489
1567
|
| EnableRowLevelSecurityCall
|
|
1568
|
+
| DisableRowLevelSecurityCall
|
|
1569
|
+
| RenamePostgresRlsPolicyCall
|
|
1490
1570
|
| DataTransformCall;
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { ExecuteRequestLowerer } from '@prisma-next/family-sql/control-adapter';
|
|
2
2
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
3
3
|
import { rlsEnabledAst, rlsPolicyExistsAst } from '../../../contract-free/checks';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
alterPolicyRename,
|
|
6
|
+
createPolicy,
|
|
7
|
+
disableRowLevelSecurity as disableRowLevelSecurityDdl,
|
|
8
|
+
dropPolicy,
|
|
9
|
+
} from '../../../contract-free/ddl';
|
|
5
10
|
import type { PostgresRlsPolicy } from '../../postgres-rls-policy';
|
|
6
11
|
import { qualifyTableName } from '../planner-sql-checks';
|
|
7
12
|
import { type Op, step, targetDetails } from './shared';
|
|
@@ -104,3 +109,79 @@ export async function enableRowLevelSecurity(
|
|
|
104
109
|
],
|
|
105
110
|
};
|
|
106
111
|
}
|
|
112
|
+
|
|
113
|
+
export async function disableRowLevelSecurity(
|
|
114
|
+
schemaName: string,
|
|
115
|
+
tableName: string,
|
|
116
|
+
lowerer: ExecuteRequestLowerer,
|
|
117
|
+
): Promise<Op> {
|
|
118
|
+
const ddlNode = disableRowLevelSecurityDdl({ schema: schemaName, table: tableName });
|
|
119
|
+
const checks = rlsEnabledAst(schemaName, tableName);
|
|
120
|
+
const enabled = await lowerer.lowerToExecuteRequest(checks.rlsEnabled());
|
|
121
|
+
const execute = await lowerer.lowerToExecuteRequest(ddlNode);
|
|
122
|
+
const disabled = await lowerer.lowerToExecuteRequest(checks.rlsDisabled());
|
|
123
|
+
return {
|
|
124
|
+
id: `rowLevelSecurity.${schemaName}.${tableName}.disable`,
|
|
125
|
+
label: `Disable row-level security on "${tableName}"`,
|
|
126
|
+
operationClass: 'destructive',
|
|
127
|
+
target: targetDetails('rowLevelSecurity', tableName, schemaName),
|
|
128
|
+
precheck: [
|
|
129
|
+
step(`check RLS is currently enabled on "${tableName}"`, enabled.sql, enabled.params),
|
|
130
|
+
],
|
|
131
|
+
execute: [step(`disable row-level security on "${tableName}"`, execute.sql, execute.params)],
|
|
132
|
+
postcheck: [
|
|
133
|
+
step(
|
|
134
|
+
`verify row-level security is disabled on "${tableName}"`,
|
|
135
|
+
disabled.sql,
|
|
136
|
+
disabled.params,
|
|
137
|
+
),
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function renameRlsPolicy(
|
|
143
|
+
schemaName: string,
|
|
144
|
+
tableName: string,
|
|
145
|
+
oldPolicyName: string,
|
|
146
|
+
newPolicyName: string,
|
|
147
|
+
lowerer: ExecuteRequestLowerer,
|
|
148
|
+
): Promise<Op> {
|
|
149
|
+
const ddlNode = alterPolicyRename({
|
|
150
|
+
schema: schemaName,
|
|
151
|
+
table: tableName,
|
|
152
|
+
name: oldPolicyName,
|
|
153
|
+
newName: newPolicyName,
|
|
154
|
+
});
|
|
155
|
+
const oldChecks = rlsPolicyExistsAst({
|
|
156
|
+
schema: schemaName,
|
|
157
|
+
table: tableName,
|
|
158
|
+
policyName: oldPolicyName,
|
|
159
|
+
});
|
|
160
|
+
const newChecks = rlsPolicyExistsAst({
|
|
161
|
+
schema: schemaName,
|
|
162
|
+
table: tableName,
|
|
163
|
+
policyName: newPolicyName,
|
|
164
|
+
});
|
|
165
|
+
const oldPresent = await lowerer.lowerToExecuteRequest(oldChecks.policyPresent());
|
|
166
|
+
const execute = await lowerer.lowerToExecuteRequest(ddlNode);
|
|
167
|
+
const newPresent = await lowerer.lowerToExecuteRequest(newChecks.policyPresent());
|
|
168
|
+
return {
|
|
169
|
+
id: `rlsPolicy.${schemaName}.${tableName}.${oldPolicyName}.rename`,
|
|
170
|
+
label: `Rename RLS policy "${oldPolicyName}" to "${newPolicyName}" on "${tableName}"`,
|
|
171
|
+
operationClass: 'widening',
|
|
172
|
+
target: targetDetails('rlsPolicy', newPolicyName, schemaName, tableName),
|
|
173
|
+
precheck: [
|
|
174
|
+
step(`ensure RLS policy "${oldPolicyName}" exists`, oldPresent.sql, oldPresent.params),
|
|
175
|
+
],
|
|
176
|
+
execute: [
|
|
177
|
+
step(
|
|
178
|
+
`rename RLS policy "${oldPolicyName}" to "${newPolicyName}"`,
|
|
179
|
+
execute.sql,
|
|
180
|
+
execute.params,
|
|
181
|
+
),
|
|
182
|
+
],
|
|
183
|
+
postcheck: [
|
|
184
|
+
step(`verify RLS policy "${newPolicyName}" exists`, newPresent.sql, newPresent.params),
|
|
185
|
+
],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
@@ -27,6 +27,7 @@ import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
|
|
|
27
27
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
28
28
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
29
29
|
import { PostgresRlsPolicy } from '../postgres-rls-policy';
|
|
30
|
+
import { parseRlsPolicyWireName } from '../rls/wire-name';
|
|
30
31
|
import { PostgresDatabaseSchemaNode } from '../schema-ir/postgres-database-schema-node';
|
|
31
32
|
import { PostgresPolicySchemaNode } from '../schema-ir/postgres-policy-schema-node';
|
|
32
33
|
import { PostgresSchemaNodeKind, type SqlSchemaDiffNode } from '../schema-ir/schema-node-kinds';
|
|
@@ -49,7 +50,7 @@ import type { PostgresOpFactoryCall } from './op-factory-call';
|
|
|
49
50
|
import {
|
|
50
51
|
CreatePostgresRlsPolicyCall,
|
|
51
52
|
DropPostgresRlsPolicyCall,
|
|
52
|
-
|
|
53
|
+
RenamePostgresRlsPolicyCall,
|
|
53
54
|
} from './op-factory-call';
|
|
54
55
|
import { TypeScriptRenderablePostgresMigration } from './planner-produced-postgres-migration';
|
|
55
56
|
import { postgresPlannerStrategies } from './planner-strategies';
|
|
@@ -335,17 +336,40 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
|
|
|
335
336
|
|
|
336
337
|
/**
|
|
337
338
|
* Maps the RLS policy presence findings of the one combined tree diff
|
|
338
|
-
* (`buildPostgresPlanDiff`, already ownership-filtered) into
|
|
339
|
-
*
|
|
340
|
-
* exactly the policy-node subset of the
|
|
339
|
+
* (`buildPostgresPlanDiff`, already ownership-filtered) into
|
|
340
|
+
* `CREATE POLICY` / `DROP POLICY` / `ALTER POLICY … RENAME TO` ops. It
|
|
341
|
+
* does not re-diff — it consumes exactly the policy-node subset of the
|
|
342
|
+
* shared diff's issues. Enablement is NOT decided here: `ENABLE`/`DISABLE
|
|
343
|
+
* ROW LEVEL SECURITY` derive from the table's marker-driven `rlsEnabled`
|
|
344
|
+
* attribute diff on the relational side.
|
|
345
|
+
*
|
|
346
|
+
* Rename post-pass: a `not-found` and a `not-expected` policy on the SAME
|
|
347
|
+
* table whose wire-name content hashes match but prefixes differ are one
|
|
348
|
+
* prefix-only rename, collapsed into a single non-destructive
|
|
349
|
+
* `RenamePostgresRlsPolicyCall`. Multi-candidate hash groups pair
|
|
350
|
+
* deterministically by sorted wire name; leftovers proceed as
|
|
351
|
+
* create/drop. Unparseable wire names never pair.
|
|
352
|
+
*
|
|
353
|
+
* The pairing runs only when the policy allows `widening` (rename's
|
|
354
|
+
* class). Without it (db-init's additive-only set), pairing degrades
|
|
355
|
+
* deliberately to the additive half: the new name is CREATEd and the old
|
|
356
|
+
* policy survives live until a widening/destructive-allowed plan runs —
|
|
357
|
+
* emitting an ungated widening rename would only fail at the runner's
|
|
358
|
+
* class re-enforcement.
|
|
341
359
|
*/
|
|
342
360
|
private planPostgresSchemaDiff(
|
|
343
361
|
options: PlannerOptionsWithComponents,
|
|
344
362
|
filteredDiffIssues: readonly SchemaDiffIssue<SqlSchemaDiffNode>[],
|
|
345
363
|
): readonly PostgresOpFactoryCall[] {
|
|
346
364
|
const allowsDestructive = options.policy.allowedOperationClasses.includes('destructive');
|
|
347
|
-
const
|
|
348
|
-
|
|
365
|
+
const allowsWidening = options.policy.allowedOperationClasses.includes('widening');
|
|
366
|
+
|
|
367
|
+
interface PolicyFinding {
|
|
368
|
+
readonly node: PostgresPolicySchemaNode;
|
|
369
|
+
readonly schemaForTable: string;
|
|
370
|
+
}
|
|
371
|
+
const missing: PolicyFinding[] = [];
|
|
372
|
+
const extra: PolicyFinding[] = [];
|
|
349
373
|
|
|
350
374
|
for (const issue of filteredDiffIssues) {
|
|
351
375
|
// 'not-equal' is unreachable for content-addressed policies: the wire name
|
|
@@ -356,30 +380,90 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
|
|
|
356
380
|
PostgresPolicySchemaNode.assert(expected);
|
|
357
381
|
// expected.namespaceId is the DDL schema name (resolved during projection);
|
|
358
382
|
// this re-resolution is a no-op as long as PostgresSchema.ddlSchemaName() returns this.id.
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
if (!seenEnableTables.has(tableKey)) {
|
|
365
|
-
seenEnableTables.add(tableKey);
|
|
366
|
-
calls.push(new EnableRowLevelSecurityCall(schemaForTable, expected.tableName));
|
|
367
|
-
}
|
|
368
|
-
calls.push(
|
|
369
|
-
new CreatePostgresRlsPolicyCall(
|
|
370
|
-
schemaForTable,
|
|
371
|
-
expected.tableName,
|
|
372
|
-
policyNodeToContractPolicy(expected),
|
|
383
|
+
missing.push({
|
|
384
|
+
node: expected,
|
|
385
|
+
schemaForTable: resolveDdlSchemaForNamespaceStorage(
|
|
386
|
+
options.contract.storage,
|
|
387
|
+
expected.namespaceId,
|
|
373
388
|
),
|
|
374
|
-
);
|
|
375
|
-
} else if (issue.reason === 'not-expected'
|
|
389
|
+
});
|
|
390
|
+
} else if (issue.reason === 'not-expected') {
|
|
376
391
|
const actual = issue.actual;
|
|
377
392
|
PostgresPolicySchemaNode.assert(actual);
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
393
|
+
extra.push({
|
|
394
|
+
node: actual,
|
|
395
|
+
schemaForTable: resolveDdlSchemaForNamespaceStorage(
|
|
396
|
+
options.contract.storage,
|
|
397
|
+
actual.namespaceId,
|
|
398
|
+
),
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const calls: PostgresOpFactoryCall[] = [];
|
|
404
|
+
const renamedExtras = new Set<PolicyFinding>();
|
|
405
|
+
const renamedMissing = new Set<PolicyFinding>();
|
|
406
|
+
const pairingKey = (finding: PolicyFinding, hash: string): string =>
|
|
407
|
+
JSON.stringify([finding.schemaForTable, finding.node.tableName, hash]);
|
|
408
|
+
|
|
409
|
+
if (allowsWidening) {
|
|
410
|
+
const extrasByGroup = new Map<string, PolicyFinding[]>();
|
|
411
|
+
for (const finding of extra) {
|
|
412
|
+
const parsed = parseRlsPolicyWireName(finding.node.name);
|
|
413
|
+
if (parsed === undefined) continue;
|
|
414
|
+
const key = pairingKey(finding, parsed.hash);
|
|
415
|
+
const group = extrasByGroup.get(key) ?? [];
|
|
416
|
+
group.push(finding);
|
|
417
|
+
extrasByGroup.set(key, group);
|
|
418
|
+
}
|
|
419
|
+
for (const group of extrasByGroup.values()) {
|
|
420
|
+
group.sort((a, b) => (a.node.name < b.node.name ? -1 : a.node.name > b.node.name ? 1 : 0));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const sortedMissing = [...missing].sort((a, b) =>
|
|
424
|
+
a.node.name < b.node.name ? -1 : a.node.name > b.node.name ? 1 : 0,
|
|
425
|
+
);
|
|
426
|
+
for (const missingFinding of sortedMissing) {
|
|
427
|
+
const parsed = parseRlsPolicyWireName(missingFinding.node.name);
|
|
428
|
+
if (parsed === undefined) continue;
|
|
429
|
+
const candidates = extrasByGroup.get(pairingKey(missingFinding, parsed.hash));
|
|
430
|
+
const candidate = candidates?.shift();
|
|
431
|
+
if (candidate === undefined) continue;
|
|
432
|
+
// Same name would never surface as missing+extra (the differ pairs by
|
|
433
|
+
// name), so a matched candidate always differs in prefix only.
|
|
434
|
+
renamedExtras.add(candidate);
|
|
435
|
+
renamedMissing.add(missingFinding);
|
|
436
|
+
calls.push(
|
|
437
|
+
new RenamePostgresRlsPolicyCall(
|
|
438
|
+
missingFinding.schemaForTable,
|
|
439
|
+
missingFinding.node.tableName,
|
|
440
|
+
candidate.node.name,
|
|
441
|
+
missingFinding.node.name,
|
|
442
|
+
),
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
for (const finding of missing) {
|
|
448
|
+
if (renamedMissing.has(finding)) continue;
|
|
449
|
+
calls.push(
|
|
450
|
+
new CreatePostgresRlsPolicyCall(
|
|
451
|
+
finding.schemaForTable,
|
|
452
|
+
finding.node.tableName,
|
|
453
|
+
policyNodeToContractPolicy(finding.node),
|
|
454
|
+
),
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
if (allowsDestructive) {
|
|
458
|
+
for (const finding of extra) {
|
|
459
|
+
if (renamedExtras.has(finding)) continue;
|
|
460
|
+
calls.push(
|
|
461
|
+
new DropPostgresRlsPolicyCall(
|
|
462
|
+
finding.schemaForTable,
|
|
463
|
+
finding.node.tableName,
|
|
464
|
+
finding.node.name,
|
|
465
|
+
),
|
|
381
466
|
);
|
|
382
|
-
calls.push(new DropPostgresRlsPolicyCall(schemaForTable, actual.tableName, actual.name));
|
|
383
467
|
}
|
|
384
468
|
}
|
|
385
469
|
|
|
@@ -19,7 +19,12 @@ import { blindCast } from '@prisma-next/utils/casts';
|
|
|
19
19
|
import type { JsonObject } from '@prisma-next/utils/json';
|
|
20
20
|
import { postgresAuthoringEntityTypes } from './authoring';
|
|
21
21
|
import { PG_INT_CODEC_ID, PG_TEXT_CODEC_ID } from './codec-ids';
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
nativeEnumEntityKind,
|
|
24
|
+
policyEntityKind,
|
|
25
|
+
rlsEnablementEntityKind,
|
|
26
|
+
roleEntityKind,
|
|
27
|
+
} from './entity-kinds';
|
|
23
28
|
import { PostgresSchema } from './postgres-schema';
|
|
24
29
|
|
|
25
30
|
const POSTGRES_AUTHORING_CTX: AuthoringEntityContext = {
|
|
@@ -73,6 +78,7 @@ export class PostgresContractSerializer extends SqlContractSerializerBase<Contra
|
|
|
73
78
|
super(storageTypesHydrators, [
|
|
74
79
|
policyEntityKind,
|
|
75
80
|
roleEntityKind,
|
|
81
|
+
rlsEnablementEntityKind,
|
|
76
82
|
nativeEnumEntityKind,
|
|
77
83
|
...extraPackEntityKinds,
|
|
78
84
|
]);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { freezeNode } from '@prisma-next/framework-components/ir';
|
|
2
|
+
import { SqlNode } from '@prisma-next/sql-contract/types';
|
|
3
|
+
|
|
4
|
+
export interface PostgresRlsEnablementInput {
|
|
5
|
+
/** Name of the table this marker declares RLS-controlled. */
|
|
6
|
+
readonly tableName: string;
|
|
7
|
+
/** Namespace coordinate (schema name). Markers are schema-scoped like policies. */
|
|
8
|
+
readonly namespaceId: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Postgres contract-IR marker for `@@rls` on a model: the table named here is
|
|
13
|
+
* RLS-controlled (`ENABLE ROW LEVEL SECURITY` is driven by this marker, never
|
|
14
|
+
* by the policy set).
|
|
15
|
+
*
|
|
16
|
+
* This is an authored, serialized Contract-IR entity — it is registered as an
|
|
17
|
+
* entity kind, extends `SqlNode`, and is stored in `contract.json` under
|
|
18
|
+
* `entries.rls[tableName]`. Target-only concept — no SQL-family abstract.
|
|
19
|
+
* Frozen at construction via `freezeNode(this)`. The `kind: 'rls'`
|
|
20
|
+
* discriminant is enumerable so it survives JSON serialization; the literal
|
|
21
|
+
* matches the entries key (one-string rule).
|
|
22
|
+
*/
|
|
23
|
+
export class PostgresRlsEnablement extends SqlNode {
|
|
24
|
+
override readonly kind = 'rls' as const;
|
|
25
|
+
readonly tableName: string;
|
|
26
|
+
readonly namespaceId: string;
|
|
27
|
+
|
|
28
|
+
constructor(input: PostgresRlsEnablementInput) {
|
|
29
|
+
super();
|
|
30
|
+
this.tableName = input.tableName;
|
|
31
|
+
this.namespaceId = input.namespaceId;
|
|
32
|
+
freezeNode(this);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -19,8 +19,14 @@ import { blindCast } from '@prisma-next/utils/casts';
|
|
|
19
19
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
20
20
|
import { PostgresTableSource } from './ast/table-source';
|
|
21
21
|
import { PG_TEXT_CODEC_ID } from './codec-ids';
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
nativeEnumEntityKind,
|
|
24
|
+
policyEntityKind,
|
|
25
|
+
rlsEnablementEntityKind,
|
|
26
|
+
roleEntityKind,
|
|
27
|
+
} from './entity-kinds';
|
|
23
28
|
import type { PostgresNativeEnum } from './postgres-native-enum';
|
|
29
|
+
import type { PostgresRlsEnablement } from './postgres-rls-enablement';
|
|
24
30
|
import type { PostgresRlsPolicy } from './postgres-rls-policy';
|
|
25
31
|
import type { PostgresRole } from './postgres-role';
|
|
26
32
|
import { escapeLiteral } from './sql-utils';
|
|
@@ -30,6 +36,7 @@ export type PostgresContract = Contract<SqlStorage> & { readonly target: 'postgr
|
|
|
30
36
|
export type PostgresNamespaceEntries = SqlNamespaceEntries & {
|
|
31
37
|
readonly policy?: Readonly<Record<string, PostgresRlsPolicy>>;
|
|
32
38
|
readonly role?: Readonly<Record<string, PostgresRole>>;
|
|
39
|
+
readonly rls?: Readonly<Record<string, PostgresRlsEnablement>>;
|
|
33
40
|
readonly native_enum?: Readonly<Record<string, PostgresNativeEnum>>;
|
|
34
41
|
};
|
|
35
42
|
|
|
@@ -72,7 +79,12 @@ export class PostgresSchema extends SqlNamespaceBase {
|
|
|
72
79
|
|
|
73
80
|
const dispatched = hydrateNamespaceEntities(
|
|
74
81
|
input.entries,
|
|
75
|
-
composeSqlEntityKinds([
|
|
82
|
+
composeSqlEntityKinds([
|
|
83
|
+
policyEntityKind,
|
|
84
|
+
roleEntityKind,
|
|
85
|
+
rlsEnablementEntityKind,
|
|
86
|
+
nativeEnumEntityKind,
|
|
87
|
+
]),
|
|
76
88
|
'carry',
|
|
77
89
|
);
|
|
78
90
|
|
|
@@ -90,7 +102,7 @@ export class PostgresSchema extends SqlNamespaceBase {
|
|
|
90
102
|
this.entries = Object.freeze(
|
|
91
103
|
blindCast<
|
|
92
104
|
PostgresNamespaceEntries,
|
|
93
|
-
'composeSqlEntityKinds([policyEntityKind, roleEntityKind, nativeEnumEntityKind]) supplies table→StorageTable, valueSet→StorageValueSet, policy→PostgresRlsPolicy, role→PostgresRole, native_enum→PostgresNativeEnum descriptors'
|
|
105
|
+
'composeSqlEntityKinds([policyEntityKind, roleEntityKind, rlsEnablementEntityKind, nativeEnumEntityKind]) supplies table→StorageTable, valueSet→StorageValueSet, policy→PostgresRlsPolicy, role→PostgresRole, rls→PostgresRlsEnablement, native_enum→PostgresNativeEnum descriptors'
|
|
94
106
|
>(entriesInput),
|
|
95
107
|
);
|
|
96
108
|
Object.defineProperty(this, 'kind', {
|
|
@@ -118,6 +130,10 @@ export class PostgresSchema extends SqlNamespaceBase {
|
|
|
118
130
|
return this.entries.role ?? Object.freeze({});
|
|
119
131
|
}
|
|
120
132
|
|
|
133
|
+
get rls(): Readonly<Record<string, PostgresRlsEnablement>> {
|
|
134
|
+
return this.entries.rls ?? Object.freeze({});
|
|
135
|
+
}
|
|
136
|
+
|
|
121
137
|
/**
|
|
122
138
|
* The bare schema qualifier as it would appear in a rendered SQL
|
|
123
139
|
* fragment (already quoted). The unbound-schema singleton overrides
|
|
@@ -19,6 +19,12 @@ export const PostgresRlsPolicySchema = type({
|
|
|
19
19
|
permissive: 'boolean',
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
+
export const PostgresRlsEnablementSchema = type({
|
|
23
|
+
kind: "'rls'",
|
|
24
|
+
tableName: 'string',
|
|
25
|
+
namespaceId: 'string',
|
|
26
|
+
});
|
|
27
|
+
|
|
22
28
|
export const PostgresNativeEnumSchema = type({
|
|
23
29
|
kind: "'postgres-enum'",
|
|
24
30
|
typeName: 'string',
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface RlsPolicyWireName {
|
|
2
|
+
/** The user-supplied part before the `_<8hex>` suffix. */
|
|
3
|
+
readonly prefix: string;
|
|
4
|
+
/** The 8-lowercase-hex content-hash suffix. */
|
|
5
|
+
readonly hash: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const WIRE_NAME_PATTERN = /^(.+)_([0-9a-f]{8})$/;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Assembles an RLS policy wire name from its user-supplied prefix and its
|
|
12
|
+
* 8-hex content-hash suffix. This module owns the `<prefix>_<hash>` format
|
|
13
|
+
* on both sides — construction here and parsing in
|
|
14
|
+
* {@link parseRlsPolicyWireName} — so the two never drift.
|
|
15
|
+
*/
|
|
16
|
+
export function formatRlsPolicyWireName(prefix: string, hash: string): string {
|
|
17
|
+
return `${prefix}_${hash}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Splits an RLS policy wire name (`<prefix>_<8hex>`) into its prefix and
|
|
22
|
+
* content-hash suffix. Returns `undefined` when the name does not follow the
|
|
23
|
+
* wire-name shape (e.g. a policy created outside the toolchain) — callers
|
|
24
|
+
* treat such names as all-prefix. Consumed by introspection (prefix
|
|
25
|
+
* extraction) and by rename pairing (same hash, different prefix).
|
|
26
|
+
*/
|
|
27
|
+
export function parseRlsPolicyWireName(name: string): RlsPolicyWireName | undefined {
|
|
28
|
+
const match = WIRE_NAME_PATTERN.exec(name);
|
|
29
|
+
const prefix = match?.[1];
|
|
30
|
+
const hash = match?.[2];
|
|
31
|
+
if (prefix === undefined || hash === undefined) return undefined;
|
|
32
|
+
return { prefix, hash };
|
|
33
|
+
}
|