@prisma-next/target-postgres 0.14.0-dev.10 → 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.
- package/dist/canonicalize-Bn3BM1Cn.mjs +46 -0
- package/dist/canonicalize-Bn3BM1Cn.mjs.map +1 -0
- package/dist/canonicalize-CAbXtVtB.d.mts +37 -0
- package/dist/canonicalize-CAbXtVtB.d.mts.map +1 -0
- package/dist/contract-free.d.mts +1 -1
- package/dist/contract-free.d.mts.map +1 -1
- package/dist/contract-free.mjs +2 -2
- package/dist/control.d.mts +1 -1
- package/dist/control.mjs +4 -4
- package/dist/{data-transform-DDgWdB5o.d.mts → data-transform-qKy1U5V9.d.mts} +2 -2
- package/dist/{data-transform-DDgWdB5o.d.mts.map → data-transform-qKy1U5V9.d.mts.map} +1 -1
- package/dist/data-transform.d.mts +1 -1
- package/dist/{ddl-QDyOSeLc.mjs → ddl-BlsTIBeP.mjs} +54 -4
- package/dist/ddl-BlsTIBeP.mjs.map +1 -0
- package/dist/ddl.d.mts +3 -2
- package/dist/ddl.mjs +2 -2
- package/dist/descriptor-meta-b_SzXwen.mjs +239 -0
- package/dist/descriptor-meta-b_SzXwen.mjs.map +1 -0
- package/dist/{issue-planner-DL6g3CmE.mjs → issue-planner-CwoeupPM.mjs} +9 -6
- package/dist/{issue-planner-DL6g3CmE.mjs.map → issue-planner-CwoeupPM.mjs.map} +1 -1
- package/dist/issue-planner.d.mts +3 -3
- package/dist/issue-planner.d.mts.map +1 -1
- package/dist/issue-planner.mjs +1 -1
- package/dist/migration.d.mts +3 -3
- package/dist/migration.mjs +2 -2
- package/dist/{nodes-Bbhs2rwj.mjs → nodes-DhDIIhBT.mjs} +44 -2
- package/dist/nodes-DhDIIhBT.mjs.map +1 -0
- package/dist/{nodes-pLeLgdis.d.mts → nodes-YvdohJ2X.d.mts} +40 -3
- package/dist/nodes-YvdohJ2X.d.mts.map +1 -0
- package/dist/{op-factory-call-DmQEc3XV.d.mts → op-factory-call-DXHAIisl.d.mts} +37 -4
- package/dist/op-factory-call-DXHAIisl.d.mts.map +1 -0
- package/dist/{op-factory-call-D_p5vxwt.mjs → op-factory-call-tMksrGIq.mjs} +146 -4
- package/dist/op-factory-call-tMksrGIq.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 +74 -1
- package/dist/pack.d.mts.map +1 -1
- package/dist/pack.mjs +1 -1
- package/dist/{planner-Bs_baQax.mjs → planner-BbSZJVW7.mjs} +114 -22
- package/dist/planner-BbSZJVW7.mjs.map +1 -0
- package/dist/{planner-produced-postgres-migration-Cji5vxUf.mjs → planner-produced-postgres-migration-C0FHnEsV.mjs} +2 -2
- package/dist/{planner-produced-postgres-migration-Cji5vxUf.mjs.map → planner-produced-postgres-migration-C0FHnEsV.mjs.map} +1 -1
- package/dist/{planner-produced-postgres-migration-QqHa2C2l.d.mts → planner-produced-postgres-migration-DfM5lBv1.d.mts} +3 -3
- package/dist/{planner-produced-postgres-migration-QqHa2C2l.d.mts.map → planner-produced-postgres-migration-DfM5lBv1.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-jqUUGyQR.mjs → planner-sql-checks-CI6Z21mm.mjs} +2 -2
- package/dist/{planner-sql-checks-jqUUGyQR.mjs.map → planner-sql-checks-CI6Z21mm.mjs.map} +1 -1
- package/dist/planner-sql-checks.mjs +1 -1
- package/dist/{planner-target-details-CIY6tLeo.d.mts → planner-target-details-HkP4RrRO.d.mts} +2 -2
- package/dist/planner-target-details-HkP4RrRO.d.mts.map +1 -0
- package/dist/planner-target-details.d.mts +1 -1
- package/dist/planner.d.mts +35 -3
- package/dist/planner.d.mts.map +1 -1
- package/dist/planner.mjs +2 -2
- package/dist/{postgres-contract-serializer-k3TAcPMY.mjs → postgres-contract-serializer-k5eg5ZUZ.mjs} +29 -16
- package/dist/postgres-contract-serializer-k5eg5ZUZ.mjs.map +1 -0
- package/dist/{postgres-migration-B5jKrXv3.mjs → postgres-migration-BJWxullf.mjs} +2 -2
- package/dist/{postgres-migration-B5jKrXv3.mjs.map → postgres-migration-BJWxullf.mjs.map} +1 -1
- package/dist/{postgres-migration-Y4YBJqkS.d.mts → postgres-migration-D22a2y0l.d.mts} +4 -4
- package/dist/{postgres-migration-Y4YBJqkS.d.mts.map → postgres-migration-D22a2y0l.d.mts.map} +1 -1
- package/dist/postgres-rls-policy-D_z1osEq.d.mts +60 -0
- package/dist/postgres-rls-policy-D_z1osEq.d.mts.map +1 -0
- package/dist/postgres-role-_CCuyPCQ.d.mts +33 -0
- package/dist/postgres-role-_CCuyPCQ.d.mts.map +1 -0
- package/dist/{postgres-schema-COGZ1ark.mjs → postgres-schema-BVDr_61a.mjs} +139 -3
- package/dist/postgres-schema-BVDr_61a.mjs.map +1 -0
- package/dist/postgres-schema-D0h4lsw2.d.mts +158 -0
- package/dist/postgres-schema-D0h4lsw2.d.mts.map +1 -0
- package/dist/postgres-schema-ir-BF0X8jit.mjs +66 -0
- package/dist/postgres-schema-ir-BF0X8jit.mjs.map +1 -0
- package/dist/postgres-schema-ir-BQ3RpZMz.d.mts +60 -0
- package/dist/postgres-schema-ir-BQ3RpZMz.d.mts.map +1 -0
- package/dist/postgres-schema-ir-annotations-El62Sywa.mjs +14 -0
- package/dist/postgres-schema-ir-annotations-El62Sywa.mjs.map +1 -0
- package/dist/render-ops.d.mts +1 -1
- package/dist/rls-canonicalize.d.mts +2 -0
- package/dist/rls-canonicalize.mjs +2 -0
- package/dist/runtime.d.mts +3 -2
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +2 -2
- package/dist/schema-ir-annotations.d.mts +8 -0
- package/dist/schema-ir-annotations.d.mts.map +1 -0
- package/dist/schema-ir-annotations.mjs +2 -0
- package/dist/types.d.mts +5 -140
- package/dist/types.mjs +3 -2
- package/package.json +21 -17
- package/src/contract-free/checks.ts +73 -0
- package/src/contract-free/ddl.ts +35 -0
- package/src/core/authoring.ts +115 -1
- package/src/core/ddl/nodes.ts +68 -1
- package/src/core/descriptor-meta.ts +4 -0
- package/src/core/entity-kinds.ts +16 -0
- package/src/core/migrations/control-policy.ts +3 -0
- package/src/core/migrations/issue-planner.ts +8 -0
- package/src/core/migrations/op-factory-call.ts +96 -0
- package/src/core/migrations/operations/rls.ts +106 -0
- package/src/core/migrations/planner-target-details.ts +3 -1
- package/src/core/migrations/planner.ts +99 -1
- package/src/core/migrations/verify-postgres-namespaces.ts +12 -15
- package/src/core/migrations/verify-postgres-rls-policies.ts +62 -0
- package/src/core/postgres-contract-serializer.ts +43 -19
- package/src/core/postgres-rls-policy.ts +103 -0
- package/src/core/postgres-role.ts +53 -0
- package/src/core/postgres-schema-ir-annotations.ts +22 -0
- package/src/core/postgres-schema-ir.ts +101 -0
- package/src/core/postgres-schema.ts +24 -4
- package/src/core/postgres-validators.ts +20 -0
- package/src/core/rls/canonicalize.ts +48 -0
- package/src/exports/ddl.ts +3 -0
- package/src/exports/planner.ts +1 -0
- package/src/exports/rls-canonicalize.ts +6 -0
- package/src/exports/schema-ir-annotations.ts +1 -0
- package/src/exports/types.ts +12 -0
- package/dist/ddl-QDyOSeLc.mjs.map +0 -1
- package/dist/descriptor-meta-CpGygXpI.mjs +0 -140
- package/dist/descriptor-meta-CpGygXpI.mjs.map +0 -1
- package/dist/nodes-Bbhs2rwj.mjs.map +0 -1
- package/dist/nodes-pLeLgdis.d.mts.map +0 -1
- package/dist/op-factory-call-D_p5vxwt.mjs.map +0 -1
- package/dist/op-factory-call-DmQEc3XV.d.mts.map +0 -1
- package/dist/planner-Bs_baQax.mjs.map +0 -1
- package/dist/planner-target-details-CIY6tLeo.d.mts.map +0 -1
- package/dist/postgres-contract-serializer-k3TAcPMY.mjs.map +0 -1
- package/dist/postgres-schema-COGZ1ark.mjs.map +0 -1
- package/dist/types.d.mts.map +0 -1
|
@@ -25,6 +25,10 @@ import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
|
|
|
25
25
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
26
26
|
import { parsePostgresDefault } from '../default-normalizer';
|
|
27
27
|
import { normalizeSchemaNativeType } from '../native-type-normalizer';
|
|
28
|
+
import { assertPostgresRlsPolicy } from '../postgres-rls-policy';
|
|
29
|
+
import { isPostgresSchema } from '../postgres-schema';
|
|
30
|
+
import { isPostgresSchemaIR } from '../postgres-schema-ir';
|
|
31
|
+
import { resolveDdlSchemaForNamespaceStorage } from '../postgres-schema-ir-annotations';
|
|
28
32
|
import {
|
|
29
33
|
formatPostgresControlPolicySubjectLabel,
|
|
30
34
|
resolvePostgresCallControlPolicySubject,
|
|
@@ -33,9 +37,15 @@ import {
|
|
|
33
37
|
} from './control-policy';
|
|
34
38
|
import { planIssues } from './issue-planner';
|
|
35
39
|
import type { PostgresOpFactoryCall } from './op-factory-call';
|
|
40
|
+
import {
|
|
41
|
+
CreatePostgresRlsPolicyCall,
|
|
42
|
+
DropPostgresRlsPolicyCall,
|
|
43
|
+
EnableRowLevelSecurityCall,
|
|
44
|
+
} from './op-factory-call';
|
|
36
45
|
import { TypeScriptRenderablePostgresMigration } from './planner-produced-postgres-migration';
|
|
37
46
|
import { postgresPlannerStrategies } from './planner-strategies';
|
|
38
47
|
import { verifyPostgresNamespacePresence } from './verify-postgres-namespaces';
|
|
48
|
+
import { diffPostgresRlsPolicies } from './verify-postgres-rls-policies';
|
|
39
49
|
|
|
40
50
|
type PlannerFrameworkComponents = SqlMigrationPlannerPlanOptions extends {
|
|
41
51
|
readonly frameworkComponents: infer T;
|
|
@@ -194,6 +204,44 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
|
|
|
194
204
|
return plannerFailure(result.failure);
|
|
195
205
|
}
|
|
196
206
|
|
|
207
|
+
// Translate RLS diff issues to DDL calls and run through control-policy
|
|
208
|
+
// partition. This runs AFTER the structural planIssues pass so the RLS
|
|
209
|
+
// calls can refer to tables that may have been created in this plan.
|
|
210
|
+
//
|
|
211
|
+
// Fail loud when the contract declares RLS policies but the schema is
|
|
212
|
+
// contract-derived (migration plan path). Without a live-introspected
|
|
213
|
+
// PostgresSchemaIR there is no baseline to reconcile against, so
|
|
214
|
+
// silently emitting no RLS DDL would be wrong. The user must run
|
|
215
|
+
// `db update` / `db init` against a live database to emit RLS.
|
|
216
|
+
if (!isPostgresSchemaIR(options.schema)) {
|
|
217
|
+
const policyNames: string[] = [];
|
|
218
|
+
for (const ns of Object.values(options.contract.storage.namespaces)) {
|
|
219
|
+
if (isPostgresSchema(ns)) {
|
|
220
|
+
for (const [policyName, policy] of Object.entries(ns.policy)) {
|
|
221
|
+
policyNames.push(`${policy.tableName}.${policyName}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (policyNames.length > 0) {
|
|
226
|
+
return plannerFailure([
|
|
227
|
+
{
|
|
228
|
+
kind: 'unsupportedOperation',
|
|
229
|
+
summary: `migration plan does not yet emit RLS policies (the offline contract→schema derivation cannot carry them). Author RLS via 'db update' / 'db init' against a live database. Tracked for a follow-up slice (extension migration participation seam). Affected policies: ${policyNames.join(', ')}`,
|
|
230
|
+
},
|
|
231
|
+
]);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const rlsCalls = this.planRlsDiff(options);
|
|
235
|
+
const rlsPartition = partitionCallsByControlPolicy({
|
|
236
|
+
calls: rlsCalls,
|
|
237
|
+
contract: options.contract,
|
|
238
|
+
resolveControlPolicySubject: (call) =>
|
|
239
|
+
resolvePostgresCallControlPolicySubject(call, options.contract),
|
|
240
|
+
resolveFactoryName: (call) => call.factoryName,
|
|
241
|
+
formatSubjectLabel: (factoryName, subject) =>
|
|
242
|
+
formatPostgresControlPolicySubjectLabel(factoryName, subject, options.contract),
|
|
243
|
+
});
|
|
244
|
+
|
|
197
245
|
// Inline `onFieldEvent`-emitted ops after structural DDL. The fixed
|
|
198
246
|
// ordering is `structural → added → dropped → altered`, with
|
|
199
247
|
// within-group sorting by `(tableName, fieldName)` so re-emits are
|
|
@@ -219,9 +267,10 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
|
|
|
219
267
|
formatSubjectLabel: (factoryName, subject) =>
|
|
220
268
|
formatPostgresControlPolicySubjectLabel(factoryName, subject, options.contract),
|
|
221
269
|
});
|
|
222
|
-
const calls = [...result.value.calls, ...fieldEventPartition.kept];
|
|
270
|
+
const calls = [...result.value.calls, ...rlsPartition.kept, ...fieldEventPartition.kept];
|
|
223
271
|
const warnings: SqlPlannerConflict[] = [
|
|
224
272
|
...issuePartition.warnings,
|
|
273
|
+
...rlsPartition.warnings,
|
|
225
274
|
...fieldEventPartition.warnings,
|
|
226
275
|
];
|
|
227
276
|
|
|
@@ -240,6 +289,54 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
|
|
|
240
289
|
});
|
|
241
290
|
}
|
|
242
291
|
|
|
292
|
+
private planRlsDiff(options: PlannerOptionsWithComponents): readonly PostgresOpFactoryCall[] {
|
|
293
|
+
if (!isPostgresSchemaIR(options.schema)) {
|
|
294
|
+
// Non-PostgresSchemaIR (migration plan path) reaches here only when the
|
|
295
|
+
// contract declares NO policies — `planSql` already returned a failure
|
|
296
|
+
// for the has-policies case. This [] is the genuine no-RLS case.
|
|
297
|
+
return [];
|
|
298
|
+
}
|
|
299
|
+
const diffIssues = diffPostgresRlsPolicies({
|
|
300
|
+
contract: options.contract,
|
|
301
|
+
schema: options.schema,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const allowsDestructive = options.policy.allowedOperationClasses.includes('destructive');
|
|
305
|
+
const calls: PostgresOpFactoryCall[] = [];
|
|
306
|
+
const seenEnableTables = new Set<string>();
|
|
307
|
+
|
|
308
|
+
for (const issue of diffIssues) {
|
|
309
|
+
const { namespaceId, entityName: policyName } = issue.coordinate;
|
|
310
|
+
const schemaForTable = resolveDdlSchemaForNamespaceStorage(
|
|
311
|
+
options.contract.storage,
|
|
312
|
+
namespaceId,
|
|
313
|
+
options.schema,
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// 'mismatch' is unreachable for content-addressed policies: the wire name
|
|
317
|
+
// encodes the body hash, so two policies sharing a coordinate (same name)
|
|
318
|
+
// are always equal and isEqualTo never returns false.
|
|
319
|
+
if (issue.outcome === 'missing') {
|
|
320
|
+
assertPostgresRlsPolicy(issue.expected);
|
|
321
|
+
const tableKey = `${schemaForTable}.${issue.expected.tableName}`;
|
|
322
|
+
if (!seenEnableTables.has(tableKey)) {
|
|
323
|
+
seenEnableTables.add(tableKey);
|
|
324
|
+
calls.push(new EnableRowLevelSecurityCall(schemaForTable, issue.expected.tableName));
|
|
325
|
+
}
|
|
326
|
+
calls.push(
|
|
327
|
+
new CreatePostgresRlsPolicyCall(schemaForTable, issue.expected.tableName, issue.expected),
|
|
328
|
+
);
|
|
329
|
+
} else if (issue.outcome === 'extra' && allowsDestructive) {
|
|
330
|
+
assertPostgresRlsPolicy(issue.actual);
|
|
331
|
+
calls.push(
|
|
332
|
+
new DropPostgresRlsPolicyCall(schemaForTable, issue.actual.tableName, policyName),
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return calls;
|
|
338
|
+
}
|
|
339
|
+
|
|
243
340
|
private ensureAdditivePolicy(policy: MigrationOperationPolicy) {
|
|
244
341
|
if (!policy.allowedOperationClasses.includes('additive')) {
|
|
245
342
|
return plannerFailure([
|
|
@@ -274,6 +371,7 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
|
|
|
274
371
|
// rather than in the family verifier. Stitch it in here so a single
|
|
275
372
|
// `SchemaIssue[]` flows through `planIssues` and the planner emits
|
|
276
373
|
// CREATE SCHEMA in the dep bucket before any CreateTableCall.
|
|
374
|
+
// RLS policy drift is handled separately via diffPostgresRlsPolicies → planRlsDiff.
|
|
277
375
|
const namespaceIssues = verifyPostgresNamespacePresence({
|
|
278
376
|
contract: options.contract,
|
|
279
377
|
schema: options.schema,
|
|
@@ -4,6 +4,7 @@ import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
|
|
|
4
4
|
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
5
5
|
import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
|
|
6
6
|
import { isPostgresSchema } from '../postgres-schema';
|
|
7
|
+
import { isPostgresSchemaIR } from '../postgres-schema-ir';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Resolves the live-database schema name for a given namespace
|
|
@@ -23,25 +24,21 @@ function resolveDdlSchemaName(storage: SqlStorage, namespaceId: string): string
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
|
-
* Reads the introspected list of schema names from
|
|
27
|
-
*
|
|
28
|
-
* `
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
27
|
+
* Reads the introspected list of schema names from a `PostgresSchemaIR`.
|
|
28
|
+
* Defaults to the always-present `public` schema when the schema IR is not a
|
|
29
|
+
* `PostgresSchemaIR` — a fresh Postgres database always carries `public`
|
|
30
|
+
* (unless an operator dropped it manually), so any verifier path that runs
|
|
31
|
+
* without an enriched introspection still suppresses the redundant
|
|
32
|
+
* `CREATE SCHEMA "public"`.
|
|
32
33
|
*
|
|
33
34
|
* Production introspection (`PostgresControlAdapter.introspect`) is the
|
|
34
|
-
* authoritative source: it queries `pg_namespace` and
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* explicitly via the schema IR.
|
|
35
|
+
* authoritative source: it queries `pg_namespace` and sets `existingSchemas`
|
|
36
|
+
* on the returned `PostgresSchemaIR`. Tests that want to assert against a
|
|
37
|
+
* richer initial state construct a `PostgresSchemaIR` explicitly.
|
|
38
38
|
*/
|
|
39
39
|
function existingSchemasFromSchema(schema: SqlSchemaIR): readonly string[] {
|
|
40
|
-
|
|
41
|
-
.
|
|
42
|
-
const slot = annotations?.pg?.existingSchemas;
|
|
43
|
-
if (Array.isArray(slot)) {
|
|
44
|
-
return slot.filter((s): s is string => typeof s === 'string');
|
|
40
|
+
if (isPostgresSchemaIR(schema)) {
|
|
41
|
+
return schema.existingSchemas;
|
|
45
42
|
}
|
|
46
43
|
return ['public'];
|
|
47
44
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Contract } from '@prisma-next/contract/types';
|
|
2
|
+
import type { SchemaDiffIssue } from '@prisma-next/framework-components/control';
|
|
3
|
+
import { diffNodes } from '@prisma-next/framework-components/control';
|
|
4
|
+
import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
|
|
5
|
+
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
6
|
+
import { isPostgresSchema } from '../postgres-schema';
|
|
7
|
+
import type { PostgresSchemaIR } from '../postgres-schema-ir';
|
|
8
|
+
|
|
9
|
+
// Postgres binds the late-bound (`__unbound__`) namespace to the `public`
|
|
10
|
+
// schema, so an unbound contract owns `public` in the live database. Both the
|
|
11
|
+
// contract slot ids and the introspected policy coordinates can carry either
|
|
12
|
+
// form depending on how the schema was lowered, so normalize both sides to the
|
|
13
|
+
// same id before comparing ownership.
|
|
14
|
+
const POSTGRES_DEFAULT_NAMESPACE_ID = 'public';
|
|
15
|
+
|
|
16
|
+
function resolveNamespaceId(namespaceId: string): string {
|
|
17
|
+
return namespaceId === UNBOUND_NAMESPACE_ID ? POSTGRES_DEFAULT_NAMESPACE_ID : namespaceId;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Computes RLS policy drift between the contract and the live DB schema using
|
|
22
|
+
* the generic {@link diffNodes} differ. Returns `SchemaDiffIssue[]` keyed by
|
|
23
|
+
* the full `EntityCoordinate` (plane + namespaceId + entityKind + entityName).
|
|
24
|
+
*
|
|
25
|
+
* Both sides supply `PostgresRlsPolicy` nodes that implement `DiffableNode`
|
|
26
|
+
* with a concrete namespace coordinate — the contract carries explicit
|
|
27
|
+
* `namespaceId` set during lowering (e.g. `'public'`); the introspected schema
|
|
28
|
+
* carries the resolved DDL schema name. The differ matches purely on coordinate
|
|
29
|
+
* identity; no namespace interpretation happens here.
|
|
30
|
+
*
|
|
31
|
+
* The diff is scoped to the namespaces the verified contract owns. The live
|
|
32
|
+
* schema introspection returns every policy across all DB schemas, but a policy
|
|
33
|
+
* in a namespace this contract does not own belongs to another contract space
|
|
34
|
+
* (or is external) and must not be reported as extra. Without this scope a space
|
|
35
|
+
* that owns only `auth`/`storage` (e.g. the supabase extension space) would flag
|
|
36
|
+
* the application space's `public.*` policies as extra during verify.
|
|
37
|
+
*
|
|
38
|
+
* Ownership is the set of namespaces the contract declares (its
|
|
39
|
+
* `storage.namespaces` slot keys), with the late-bound `__unbound__` slot
|
|
40
|
+
* resolved to the Postgres default `public` so a single-namespace contract still
|
|
41
|
+
* owns — and diffs — its `public` policies.
|
|
42
|
+
*/
|
|
43
|
+
export function diffPostgresRlsPolicies(input: {
|
|
44
|
+
readonly contract: Contract<SqlStorage>;
|
|
45
|
+
readonly schema: PostgresSchemaIR;
|
|
46
|
+
}): readonly SchemaDiffIssue[] {
|
|
47
|
+
const { contract, schema } = input;
|
|
48
|
+
|
|
49
|
+
const ownedNamespaceIds = new Set(
|
|
50
|
+
Object.keys(contract.storage.namespaces).map(resolveNamespaceId),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const expected = Object.values(contract.storage.namespaces).flatMap((ns) =>
|
|
54
|
+
isPostgresSchema(ns) ? Object.values(ns.policy) : [],
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const actual = schema.rlsPolicies.filter((policy) =>
|
|
58
|
+
ownedNamespaceIds.has(resolveNamespaceId(policy.namespaceId)),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return diffNodes(expected, actual);
|
|
62
|
+
}
|
|
@@ -10,15 +10,17 @@ import {
|
|
|
10
10
|
isAuthoringEntityTypeDescriptor,
|
|
11
11
|
} from '@prisma-next/framework-components/authoring';
|
|
12
12
|
import {
|
|
13
|
+
type AnyEntityKindDescriptor,
|
|
13
14
|
type Namespace,
|
|
14
15
|
NamespaceBase,
|
|
15
16
|
UNBOUND_NAMESPACE_ID,
|
|
16
17
|
} from '@prisma-next/framework-components/ir';
|
|
17
18
|
import type { SqlNamespaceTablesInput, SqlStorage } from '@prisma-next/sql-contract/types';
|
|
18
19
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
19
|
-
import type { JsonObject } from '@prisma-next/utils/json';
|
|
20
|
+
import type { JsonObject, JsonValue } from '@prisma-next/utils/json';
|
|
20
21
|
import { postgresAuthoringEntityTypes } from './authoring';
|
|
21
22
|
import { postgresTargetDescriptorMeta } from './descriptor-meta';
|
|
23
|
+
import { policyEntityKind, roleEntityKind } from './entity-kinds';
|
|
22
24
|
import { isPostgresSchema, PostgresSchema } from './postgres-schema';
|
|
23
25
|
|
|
24
26
|
const POSTGRES_AUTHORING_CTX: AuthoringEntityContext = {
|
|
@@ -32,7 +34,8 @@ function isAuthoringEntityTypeFactoryOutput(
|
|
|
32
34
|
return (
|
|
33
35
|
typeof output === 'object' &&
|
|
34
36
|
output !== null &&
|
|
35
|
-
|
|
37
|
+
'factory' in output &&
|
|
38
|
+
typeof output.factory === 'function'
|
|
36
39
|
);
|
|
37
40
|
}
|
|
38
41
|
|
|
@@ -65,9 +68,9 @@ function collectStorageTypesHydrators(
|
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
export class PostgresContractSerializer extends SqlContractSerializerBase<Contract<SqlStorage>> {
|
|
68
|
-
constructor() {
|
|
71
|
+
constructor(extraPackEntityKinds: readonly AnyEntityKindDescriptor[] = []) {
|
|
69
72
|
const storageTypesHydrators = collectStorageTypesHydrators(postgresAuthoringEntityTypes);
|
|
70
|
-
super(storageTypesHydrators);
|
|
73
|
+
super(storageTypesHydrators, [policyEntityKind, roleEntityKind, ...extraPackEntityKinds]);
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
protected override get defaultNamespaceId(): string {
|
|
@@ -87,12 +90,14 @@ export class PostgresContractSerializer extends SqlContractSerializerBase<Contra
|
|
|
87
90
|
>(super.hydrateSqlNamespaceEntry(nsId, raw));
|
|
88
91
|
const { id, entries } = hydrated;
|
|
89
92
|
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (id === UNBOUND_NAMESPACE_ID &&
|
|
93
|
+
const allSlotsEmpty = Object.values(entries).every(
|
|
94
|
+
(slot) => slot === undefined || Object.keys(slot).length === 0,
|
|
95
|
+
);
|
|
96
|
+
if (id === UNBOUND_NAMESPACE_ID && allSlotsEmpty) {
|
|
94
97
|
return PostgresSchema.unbound;
|
|
95
98
|
}
|
|
99
|
+
const valueSetSlot = entries['valueSet'];
|
|
100
|
+
const hasValueSets = valueSetSlot !== undefined && Object.keys(valueSetSlot).length > 0;
|
|
96
101
|
return new PostgresSchema({
|
|
97
102
|
id,
|
|
98
103
|
entries: {
|
|
@@ -118,7 +123,7 @@ export class PostgresContractSerializer extends SqlContractSerializerBase<Contra
|
|
|
118
123
|
table: Object.fromEntries(
|
|
119
124
|
Object.entries(ns.entries.table ?? {}).map(([tableName, table]) => [
|
|
120
125
|
tableName,
|
|
121
|
-
this.
|
|
126
|
+
this.serializeJsonObject(table),
|
|
122
127
|
]),
|
|
123
128
|
),
|
|
124
129
|
},
|
|
@@ -132,42 +137,61 @@ export class PostgresContractSerializer extends SqlContractSerializerBase<Contra
|
|
|
132
137
|
if (storage.types !== undefined) {
|
|
133
138
|
const typesOut: Record<string, JsonObject> = {};
|
|
134
139
|
for (const [name, entry] of Object.entries(storage.types)) {
|
|
135
|
-
typesOut[name] = this.
|
|
140
|
+
typesOut[name] = this.serializeJsonObject(entry);
|
|
136
141
|
}
|
|
137
142
|
storageOut['types'] = typesOut;
|
|
138
143
|
}
|
|
139
|
-
return
|
|
144
|
+
return blindCast<
|
|
145
|
+
JsonObject,
|
|
146
|
+
'contract minus storage plus a JSON-shaped storageOut is a JsonObject'
|
|
147
|
+
>({
|
|
140
148
|
...rest,
|
|
141
149
|
storage: storageOut,
|
|
142
|
-
}
|
|
150
|
+
});
|
|
143
151
|
}
|
|
144
152
|
|
|
145
153
|
private serializePostgresNamespace(ns: PostgresSchema, isUnboundSlot: boolean): JsonObject {
|
|
146
154
|
const tablesOut: Record<string, JsonObject> = {};
|
|
147
155
|
for (const [tableName, table] of Object.entries(ns.table)) {
|
|
148
|
-
tablesOut[tableName] = this.
|
|
156
|
+
tablesOut[tableName] = this.serializeJsonObject(table);
|
|
149
157
|
}
|
|
150
158
|
const valueSetEntries = ns.valueSet;
|
|
151
159
|
const valueSetOut: Record<string, JsonObject> = {};
|
|
152
160
|
if (valueSetEntries !== undefined) {
|
|
153
161
|
for (const [valueSetName, valueSet] of Object.entries(valueSetEntries)) {
|
|
154
|
-
valueSetOut[valueSetName] =
|
|
155
|
-
JsonObject,
|
|
156
|
-
'serializeJsonValue round-trips the value-set node through JSON, yielding a JsonObject'
|
|
157
|
-
>(this.serializeJsonValue(valueSet));
|
|
162
|
+
valueSetOut[valueSetName] = this.serializeJsonObject(valueSet);
|
|
158
163
|
}
|
|
159
164
|
}
|
|
165
|
+
const roleOut: Record<string, JsonObject> = {};
|
|
166
|
+
for (const [roleName, role] of Object.entries(ns.role)) {
|
|
167
|
+
roleOut[roleName] = this.serializeJsonObject(role);
|
|
168
|
+
}
|
|
169
|
+
const policyOut: Record<string, JsonObject> = {};
|
|
170
|
+
for (const [policyName, policy] of Object.entries(ns.policy)) {
|
|
171
|
+
policyOut[policyName] = this.serializeJsonObject(policy);
|
|
172
|
+
}
|
|
160
173
|
return {
|
|
161
174
|
id: ns.id,
|
|
162
175
|
kind: isUnboundSlot ? 'postgres-unbound-schema' : 'postgres-schema',
|
|
163
176
|
entries: {
|
|
164
177
|
table: tablesOut,
|
|
165
178
|
...(Object.keys(valueSetOut).length > 0 ? { valueSet: valueSetOut } : {}),
|
|
179
|
+
...(Object.keys(roleOut).length > 0 ? { role: roleOut } : {}),
|
|
180
|
+
...(Object.keys(policyOut).length > 0 ? { policy: policyOut } : {}),
|
|
166
181
|
},
|
|
167
182
|
};
|
|
168
183
|
}
|
|
169
184
|
|
|
170
|
-
private
|
|
171
|
-
return
|
|
185
|
+
private serializeJsonObject(value: unknown): JsonObject {
|
|
186
|
+
return blindCast<
|
|
187
|
+
JsonObject,
|
|
188
|
+
'serializeJsonValue round-trips an IR node through JSON, yielding a JsonObject'
|
|
189
|
+
>(this.serializeJsonValue(value));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private serializeJsonValue(value: unknown): JsonValue {
|
|
193
|
+
return blindCast<JsonValue, 'JSON.parse(JSON.stringify(x)) yields a JsonValue'>(
|
|
194
|
+
JSON.parse(JSON.stringify(value)),
|
|
195
|
+
);
|
|
172
196
|
}
|
|
173
197
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { DiffableNode } from '@prisma-next/framework-components/control';
|
|
2
|
+
import type { EntityCoordinate } from '@prisma-next/framework-components/ir';
|
|
3
|
+
import { freezeNode } from '@prisma-next/framework-components/ir';
|
|
4
|
+
import { SqlNode } from '@prisma-next/sql-contract/types';
|
|
5
|
+
|
|
6
|
+
export type RlsPolicyOperation = 'select' | 'insert' | 'update' | 'delete' | 'all';
|
|
7
|
+
|
|
8
|
+
export interface PostgresRlsPolicyInput {
|
|
9
|
+
/** Full wire name: `<prefix>_<8hex>`. Stored as-is; hashing is not this class's job. */
|
|
10
|
+
readonly name: string;
|
|
11
|
+
/** User-supplied prefix (the part before the `_<8hex>` suffix). */
|
|
12
|
+
readonly prefix: string;
|
|
13
|
+
/** Name of the table this policy attaches to, by name within the same schema. */
|
|
14
|
+
readonly tableName: string;
|
|
15
|
+
/** Namespace coordinate (schema name). Policies are schema-scoped. */
|
|
16
|
+
readonly namespaceId: string;
|
|
17
|
+
readonly operation: RlsPolicyOperation;
|
|
18
|
+
/** Sorted role names rendered in `TO <roles>`. Plain strings in this slice. */
|
|
19
|
+
readonly roles: readonly string[];
|
|
20
|
+
/** USING predicate SQL string, if present. */
|
|
21
|
+
readonly using?: string;
|
|
22
|
+
/** WITH CHECK predicate SQL string, if present. */
|
|
23
|
+
readonly withCheck?: string;
|
|
24
|
+
/** `true` = `AS PERMISSIVE`, `false` = `AS RESTRICTIVE`. */
|
|
25
|
+
readonly permissive: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Postgres IR class for a row-level security policy (`CREATE POLICY … ON …`).
|
|
30
|
+
*
|
|
31
|
+
* Target-only concept — no SQL-family abstract. Extends `SqlNode` directly.
|
|
32
|
+
* Frozen at construction via `freezeNode(this)`. The `kind: 'policy'`
|
|
33
|
+
* discriminant is enumerable (overrides SqlNode's non-enumerable `'sql'`) so it
|
|
34
|
+
* survives JSON serialization and enables dispatch. The literal matches the
|
|
35
|
+
* entries key (one-string rule: node.kind === entries key === entity kind).
|
|
36
|
+
*/
|
|
37
|
+
export class PostgresRlsPolicy extends SqlNode implements DiffableNode {
|
|
38
|
+
override readonly kind = 'policy' as const;
|
|
39
|
+
readonly name: string;
|
|
40
|
+
readonly prefix: string;
|
|
41
|
+
readonly tableName: string;
|
|
42
|
+
readonly namespaceId: string;
|
|
43
|
+
readonly operation: RlsPolicyOperation;
|
|
44
|
+
readonly roles: readonly string[];
|
|
45
|
+
declare readonly using?: string;
|
|
46
|
+
declare readonly withCheck?: string;
|
|
47
|
+
readonly permissive: boolean;
|
|
48
|
+
|
|
49
|
+
constructor(input: PostgresRlsPolicyInput) {
|
|
50
|
+
super();
|
|
51
|
+
this.name = input.name;
|
|
52
|
+
this.prefix = input.prefix;
|
|
53
|
+
this.tableName = input.tableName;
|
|
54
|
+
this.namespaceId = input.namespaceId;
|
|
55
|
+
this.operation = input.operation;
|
|
56
|
+
this.roles = Object.freeze([...input.roles]);
|
|
57
|
+
if (input.using !== undefined) this.using = input.using;
|
|
58
|
+
if (input.withCheck !== undefined) this.withCheck = input.withCheck;
|
|
59
|
+
this.permissive = input.permissive;
|
|
60
|
+
freezeNode(this);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
identity(): EntityCoordinate {
|
|
64
|
+
return {
|
|
65
|
+
plane: 'storage',
|
|
66
|
+
namespaceId: this.namespaceId,
|
|
67
|
+
entityKind: 'policy',
|
|
68
|
+
entityName: this.name,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Equality by wire name only. The wire name is `<prefix>_<sha256(body)[0..8]>`,
|
|
74
|
+
* so name-equality IS body-equality — two policies with different bodies
|
|
75
|
+
* have different hashes and therefore different names. We deliberately
|
|
76
|
+
* skip comparing bodies directly because Postgres reprints predicate
|
|
77
|
+
* expressions (e.g. strips outer parentheses), so a byte-compare against
|
|
78
|
+
* the authored body would produce false mismatches on a clean re-verify.
|
|
79
|
+
*/
|
|
80
|
+
isEqualTo(other: DiffableNode): boolean {
|
|
81
|
+
if (!isPostgresRlsPolicy(other)) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`PostgresRlsPolicy.isEqualTo: expected a PostgresRlsPolicy, got ${other.identity().entityKind}`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return this.name === other.name;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function isPostgresRlsPolicy(node: DiffableNode | undefined): node is PostgresRlsPolicy {
|
|
91
|
+
return node !== undefined && 'kind' in node && node.kind === 'policy';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function assertPostgresRlsPolicy(
|
|
95
|
+
node: DiffableNode | undefined,
|
|
96
|
+
): asserts node is PostgresRlsPolicy {
|
|
97
|
+
if (!isPostgresRlsPolicy(node)) {
|
|
98
|
+
const kind = node !== undefined && 'kind' in node ? String(node.kind) : typeof node;
|
|
99
|
+
throw new Error(
|
|
100
|
+
`planRlsDiff: expected a PostgresRlsPolicy on the policy-diff path but got ${kind}`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { DiffableNode } from '@prisma-next/framework-components/control';
|
|
2
|
+
import type { EntityCoordinate } from '@prisma-next/framework-components/ir';
|
|
3
|
+
import { freezeNode } from '@prisma-next/framework-components/ir';
|
|
4
|
+
import { SqlNode } from '@prisma-next/sql-contract/types';
|
|
5
|
+
|
|
6
|
+
export interface PostgresRoleInput {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
/**
|
|
9
|
+
* Namespace coordinate. Roles are cluster-scoped; pass `UNBOUND_NAMESPACE_ID`
|
|
10
|
+
* from `@prisma-next/framework-components/ir`.
|
|
11
|
+
*/
|
|
12
|
+
readonly namespaceId: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Postgres IR class for a database role (`CREATE ROLE …`).
|
|
17
|
+
*
|
|
18
|
+
* Roles are cluster-scoped, so their namespace coordinate is always
|
|
19
|
+
* `UNBOUND_NAMESPACE_ID`. Target-only concept — no SQL-family abstract.
|
|
20
|
+
* Extends `SqlNode` directly, frozen at construction via `freezeNode(this)`.
|
|
21
|
+
* The `kind: 'role'` discriminant is enumerable so it survives JSON.
|
|
22
|
+
* Matches the entries key (one-string rule).
|
|
23
|
+
*/
|
|
24
|
+
export class PostgresRole extends SqlNode implements DiffableNode {
|
|
25
|
+
override readonly kind = 'role' as const;
|
|
26
|
+
readonly name: string;
|
|
27
|
+
readonly namespaceId: string;
|
|
28
|
+
|
|
29
|
+
constructor(input: PostgresRoleInput) {
|
|
30
|
+
super();
|
|
31
|
+
this.name = input.name;
|
|
32
|
+
this.namespaceId = input.namespaceId;
|
|
33
|
+
freezeNode(this);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
identity(): EntityCoordinate {
|
|
37
|
+
return {
|
|
38
|
+
plane: 'storage',
|
|
39
|
+
namespaceId: this.namespaceId,
|
|
40
|
+
entityKind: 'role',
|
|
41
|
+
entityName: this.name,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
isEqualTo(other: DiffableNode): boolean {
|
|
46
|
+
if (!(other instanceof PostgresRole)) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`PostgresRole.isEqualTo: expected a PostgresRole, got ${other.identity().entityKind}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return this.name === other.name;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
|
|
2
|
+
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
3
|
+
import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
|
|
4
|
+
import { isPostgresSchema } from './postgres-schema';
|
|
5
|
+
import { isPostgresSchemaIR } from './postgres-schema-ir';
|
|
6
|
+
|
|
7
|
+
export function resolveDdlSchemaForNamespaceStorage(
|
|
8
|
+
storage: SqlStorage,
|
|
9
|
+
namespaceId: string,
|
|
10
|
+
schemaIr?: SqlSchemaIR,
|
|
11
|
+
): string {
|
|
12
|
+
if (namespaceId === UNBOUND_NAMESPACE_ID) {
|
|
13
|
+
return (
|
|
14
|
+
(schemaIr && isPostgresSchemaIR(schemaIr) ? schemaIr.pgSchemaName : undefined) ?? 'public'
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
const namespace = storage.namespaces[namespaceId];
|
|
18
|
+
if (namespace && isPostgresSchema(namespace)) {
|
|
19
|
+
return namespace.ddlSchemaName(storage);
|
|
20
|
+
}
|
|
21
|
+
return namespaceId;
|
|
22
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { freezeNode } from '@prisma-next/framework-components/ir';
|
|
2
|
+
import {
|
|
3
|
+
type SqlAnnotations,
|
|
4
|
+
type SqlSchemaIR,
|
|
5
|
+
SqlSchemaIRNode,
|
|
6
|
+
type SqlSchemaTarget,
|
|
7
|
+
SqlTableIR,
|
|
8
|
+
type SqlTableIRInput,
|
|
9
|
+
} from '@prisma-next/sql-schema-ir/types';
|
|
10
|
+
import type { PostgresRlsPolicy } from './postgres-rls-policy';
|
|
11
|
+
import type { PostgresRole } from './postgres-role';
|
|
12
|
+
|
|
13
|
+
export interface PostgresSchemaIRInput {
|
|
14
|
+
readonly tables: Record<string, SqlTableIR | SqlTableIRInput>;
|
|
15
|
+
readonly pgSchemaName: string;
|
|
16
|
+
readonly pgVersion: string;
|
|
17
|
+
readonly rlsPolicies: readonly PostgresRlsPolicy[];
|
|
18
|
+
readonly roles: readonly PostgresRole[];
|
|
19
|
+
readonly existingSchemas: readonly string[];
|
|
20
|
+
readonly nativeEnumTypeNames: readonly string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Postgres-specific schema IR. Mirrors the structure of `SqlSchemaIR`
|
|
25
|
+
* (same `tables` + optional `annotations` fields) and adds typed fields for
|
|
26
|
+
* data the postgres adapter collects during introspection.
|
|
27
|
+
*
|
|
28
|
+
* Extends `SqlSchemaIRNode` directly rather than `SqlSchemaIR` because
|
|
29
|
+
* `SqlSchemaIR` calls `freezeNode` in its constructor, which prevents
|
|
30
|
+
* subclass field initialisation. `PostgresSchemaIR` replicates the minimal
|
|
31
|
+
* `SqlSchemaIR` structure and freezes itself at the end of its own
|
|
32
|
+
* constructor.
|
|
33
|
+
*
|
|
34
|
+
* RLS-specific fields (`rlsPolicies`, `roles`) are typed top-level fields on
|
|
35
|
+
* this class — no `Reflect.get`, no untyped annotation bag access. The
|
|
36
|
+
* `annotations.pg` bag is populated with only the subset the family layer
|
|
37
|
+
* reads (`nativeEnumTypeNames`, `existingSchemas`, `schema`) so family-layer
|
|
38
|
+
* PSL inference and namespace verification continue to work without knowing
|
|
39
|
+
* about RLS.
|
|
40
|
+
*
|
|
41
|
+
* Nothing RLS-specific leaks into the sql-family layer.
|
|
42
|
+
*/
|
|
43
|
+
export class PostgresSchemaIR extends SqlSchemaIRNode {
|
|
44
|
+
readonly nodeTarget: SqlSchemaTarget = 'postgres';
|
|
45
|
+
readonly tables: Readonly<Record<string, SqlTableIR>>;
|
|
46
|
+
declare readonly annotations?: SqlAnnotations;
|
|
47
|
+
readonly pgSchemaName: string;
|
|
48
|
+
readonly pgVersion: string;
|
|
49
|
+
readonly rlsPolicies: readonly PostgresRlsPolicy[];
|
|
50
|
+
readonly roles: readonly PostgresRole[];
|
|
51
|
+
readonly existingSchemas: readonly string[];
|
|
52
|
+
readonly nativeEnumTypeNames: readonly string[];
|
|
53
|
+
|
|
54
|
+
constructor(input: PostgresSchemaIRInput) {
|
|
55
|
+
super();
|
|
56
|
+
this.tables = Object.freeze(
|
|
57
|
+
Object.fromEntries(
|
|
58
|
+
Object.entries(input.tables).map(([key, t]) => [
|
|
59
|
+
key,
|
|
60
|
+
t instanceof SqlTableIR ? t : new SqlTableIR(t),
|
|
61
|
+
]),
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
this.pgSchemaName = input.pgSchemaName;
|
|
65
|
+
this.pgVersion = input.pgVersion;
|
|
66
|
+
this.rlsPolicies = Object.freeze([...input.rlsPolicies]);
|
|
67
|
+
this.roles = Object.freeze([...input.roles]);
|
|
68
|
+
this.existingSchemas = Object.freeze([...input.existingSchemas]);
|
|
69
|
+
this.nativeEnumTypeNames = Object.freeze([...input.nativeEnumTypeNames]);
|
|
70
|
+
// Populate the annotations.pg bag with only the subset the family layer
|
|
71
|
+
// reads (nativeEnumTypeNames for PSL inference, existingSchemas for
|
|
72
|
+
// namespace presence checks). rlsPolicies and roles are NOT placed here
|
|
73
|
+
// — they are consumed directly via typed fields on this class.
|
|
74
|
+
this.annotations = {
|
|
75
|
+
pg: {
|
|
76
|
+
schema: input.pgSchemaName,
|
|
77
|
+
...(input.nativeEnumTypeNames.length > 0 && {
|
|
78
|
+
nativeEnumTypeNames: input.nativeEnumTypeNames,
|
|
79
|
+
}),
|
|
80
|
+
...(input.existingSchemas.length > 0 && {
|
|
81
|
+
existingSchemas: input.existingSchemas,
|
|
82
|
+
}),
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
freezeNode(this);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Structural guard for `PostgresSchemaIR`, narrowing on the `nodeTarget`
|
|
91
|
+
* discriminant rather than `instanceof`. `nodeTarget` is an enumerable own field
|
|
92
|
+
* (a plain class-field initializer), so it survives the `{ ...schema, tables }`
|
|
93
|
+
* spread the multi-space verify path (`projectSchemaToSpace`) produces — that
|
|
94
|
+
* projected object is not a class instance but retains every enumerable own
|
|
95
|
+
* property. The family-level `kind = 'sql-schema-ir'` discriminator is unusable
|
|
96
|
+
* here: it is shared by every SQL schema-IR node and is non-enumerable (dropped
|
|
97
|
+
* by the spread).
|
|
98
|
+
*/
|
|
99
|
+
export function isPostgresSchemaIR(schema: SqlSchemaIR): schema is PostgresSchemaIR {
|
|
100
|
+
return schema.nodeTarget === 'postgres';
|
|
101
|
+
}
|