@prisma-next/target-postgres 0.14.0-dev.60 → 0.14.0-dev.61

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 (71) hide show
  1. package/dist/{authoring-WkqG_MkU.mjs → authoring-BLkL4fFG.mjs} +147 -38
  2. package/dist/authoring-BLkL4fFG.mjs.map +1 -0
  3. package/dist/contract-free.mjs +2 -2
  4. package/dist/{control-policy-Co_13Hxa.mjs → control-policy-BpmChlAK.mjs} +27 -4
  5. package/dist/{control-policy-Co_13Hxa.mjs.map → control-policy-BpmChlAK.mjs.map} +1 -1
  6. package/dist/control.mjs +6 -5
  7. package/dist/control.mjs.map +1 -1
  8. package/dist/{ddl-B-krEXY1.mjs → ddl-BovUVFVi.mjs} +2 -2
  9. package/dist/{ddl-B-krEXY1.mjs.map → ddl-BovUVFVi.mjs.map} +1 -1
  10. package/dist/{descriptor-meta-BOcKPaLE.mjs → descriptor-meta-C1T4BXJ8.mjs} +3 -3
  11. package/dist/{descriptor-meta-BOcKPaLE.mjs.map → descriptor-meta-C1T4BXJ8.mjs.map} +1 -1
  12. package/dist/{diff-database-schema-BOsDnH64.mjs → diff-database-schema-RMwnSOnz.mjs} +46 -14
  13. package/dist/diff-database-schema-RMwnSOnz.mjs.map +1 -0
  14. package/dist/diff-database-schema.d.mts +4 -4
  15. package/dist/diff-database-schema.d.mts.map +1 -1
  16. package/dist/diff-database-schema.mjs +1 -1
  17. package/dist/issue-planner.mjs +1 -1
  18. package/dist/migration.mjs +2 -2
  19. package/dist/{op-factory-call-DxNNKUeZ.mjs → op-factory-call-CI00nS4z.mjs} +3 -3
  20. package/dist/{op-factory-call-DxNNKUeZ.mjs.map → op-factory-call-CI00nS4z.mjs.map} +1 -1
  21. package/dist/op-factory-call.mjs +1 -1
  22. package/dist/pack.d.mts +146 -3
  23. package/dist/pack.d.mts.map +1 -1
  24. package/dist/pack.mjs +1 -1
  25. package/dist/{planner-CNrc1Odu.mjs → planner-CAHZN1Ez.mjs} +8 -8
  26. package/dist/planner-CAHZN1Ez.mjs.map +1 -0
  27. package/dist/{planner-produced-postgres-migration-NI-oGKcw.mjs → planner-produced-postgres-migration-flkG4Y7n.mjs} +2 -2
  28. package/dist/{planner-produced-postgres-migration-NI-oGKcw.mjs.map → planner-produced-postgres-migration-flkG4Y7n.mjs.map} +1 -1
  29. package/dist/planner-produced-postgres-migration.mjs +1 -1
  30. package/dist/{planner-sql-checks-AXoxB1Jl.mjs → planner-sql-checks-C1-Kk3Eu.mjs} +2 -2
  31. package/dist/{planner-sql-checks-AXoxB1Jl.mjs.map → planner-sql-checks-C1-Kk3Eu.mjs.map} +1 -1
  32. package/dist/planner-sql-checks.mjs +1 -1
  33. package/dist/planner.d.mts +2 -2
  34. package/dist/planner.d.mts.map +1 -1
  35. package/dist/planner.mjs +2 -2
  36. package/dist/{postgres-contract-view-Dg-N7KA7.mjs → postgres-contract-view-DWLNQbdR.mjs} +3 -3
  37. package/dist/{postgres-contract-view-Dg-N7KA7.mjs.map → postgres-contract-view-DWLNQbdR.mjs.map} +1 -1
  38. package/dist/{postgres-database-schema-node-CfdTY-Qs.d.mts → postgres-database-schema-node-BVdOffjC.d.mts} +9 -6
  39. package/dist/{postgres-database-schema-node-CfdTY-Qs.d.mts.map → postgres-database-schema-node-BVdOffjC.d.mts.map} +1 -1
  40. package/dist/{postgres-migration-ChyRsWvE.mjs → postgres-migration-C92swCzd.mjs} +3 -3
  41. package/dist/{postgres-migration-ChyRsWvE.mjs.map → postgres-migration-C92swCzd.mjs.map} +1 -1
  42. package/dist/{postgres-role-schema-node-BiSTCTNg.mjs → postgres-role-schema-node-BmCboEa_.mjs} +10 -7
  43. package/dist/postgres-role-schema-node-BmCboEa_.mjs.map +1 -0
  44. package/dist/{postgres-schema-DTLA_aRi.mjs → postgres-schema-COEca21H.mjs} +20 -3
  45. package/dist/postgres-schema-COEca21H.mjs.map +1 -0
  46. package/dist/{postgres-schema-CU4gF6QZ.d.mts → postgres-schema-DsrtYp2v.d.mts} +13 -1
  47. package/dist/postgres-schema-DsrtYp2v.d.mts.map +1 -0
  48. package/dist/postgres-table-schema-node-9FCtCUz2.mjs.map +1 -1
  49. package/dist/{resolve-ddl-schema-Ca88Tg6W.mjs → resolve-ddl-schema-ZkKxt2Fm.mjs} +2 -2
  50. package/dist/{resolve-ddl-schema-Ca88Tg6W.mjs.map → resolve-ddl-schema-ZkKxt2Fm.mjs.map} +1 -1
  51. package/dist/runtime.mjs +1 -1
  52. package/dist/schema-ir-annotations.mjs +1 -1
  53. package/dist/types.d.mts +2 -2
  54. package/dist/types.mjs +2 -2
  55. package/package.json +20 -20
  56. package/src/core/authoring.ts +136 -25
  57. package/src/core/migrations/control-policy.ts +29 -0
  58. package/src/core/migrations/diff-database-schema.ts +47 -11
  59. package/src/core/migrations/planner.ts +5 -0
  60. package/src/core/postgres-role.ts +22 -0
  61. package/src/core/postgres-validators.ts +1 -0
  62. package/src/core/psl-infer/infer-psl-contract.ts +1 -0
  63. package/src/core/schema-ir/postgres-database-schema-node.ts +5 -4
  64. package/src/core/schema-ir/postgres-role-schema-node.ts +4 -2
  65. package/src/core/schema-ir/schema-node-kinds.ts +7 -0
  66. package/dist/authoring-WkqG_MkU.mjs.map +0 -1
  67. package/dist/diff-database-schema-BOsDnH64.mjs.map +0 -1
  68. package/dist/planner-CNrc1Odu.mjs.map +0 -1
  69. package/dist/postgres-role-schema-node-BiSTCTNg.mjs.map +0 -1
  70. package/dist/postgres-schema-CU4gF6QZ.d.mts.map +0 -1
  71. package/dist/postgres-schema-DTLA_aRi.mjs.map +0 -1
@@ -12,10 +12,11 @@ import type {
12
12
  } from '@prisma-next/framework-components/authoring';
13
13
  import { modelAttribute } from '@prisma-next/psl-parser';
14
14
  import type { SqlValueSetDerivingEntityTypeOutput } from '@prisma-next/sql-contract/value-set-derivation-hook';
15
+ import { ifDefined } from '@prisma-next/utils/defined';
15
16
  import { PG_ENUM_CODEC_ID } from './codec-ids';
16
17
  import { PostgresNativeEnum } from './postgres-native-enum';
17
18
  import { PostgresRlsEnablement, type PostgresRlsEnablementInput } from './postgres-rls-enablement';
18
- import { PostgresRlsPolicy } from './postgres-rls-policy';
19
+ import { PostgresRlsPolicy, type RlsPolicyOperation } from './postgres-rls-policy';
19
20
  import { PostgresRole, type PostgresRoleInput } from './postgres-role';
20
21
  import {
21
22
  PostgresNativeEnumSchema,
@@ -50,6 +51,35 @@ export interface RlsPolicyExtensionBlock extends PslExtensionBlock {
50
51
  readonly namespaceId: string;
51
52
  }
52
53
 
54
+ /**
55
+ * Maps a `policy_<op>` keyword to the RLS operation it authors. The keyword
56
+ * IS the operation (per the project's rejection of a `policy { operation = … }`
57
+ * conditional block).
58
+ */
59
+ const POLICY_KEYWORD_OPERATION: Readonly<Record<string, RlsPolicyOperation>> = {
60
+ policy_select: 'select',
61
+ policy_insert: 'insert',
62
+ policy_update: 'update',
63
+ policy_delete: 'delete',
64
+ policy_all: 'all',
65
+ };
66
+
67
+ /**
68
+ * Which predicate each RLS operation admits, mirroring Postgres: SELECT and
69
+ * DELETE decide row visibility/eligibility with `USING` only; INSERT validates
70
+ * the new row with `WITH CHECK` only; UPDATE and ALL take both. A predicate
71
+ * outside this set is a load-time error (see {@link lowerRlsPolicyFromBlock}).
72
+ */
73
+ const POLICY_OPERATION_PREDICATES: Readonly<
74
+ Record<RlsPolicyOperation, { readonly using: boolean; readonly withCheck: boolean }>
75
+ > = {
76
+ select: { using: true, withCheck: false },
77
+ insert: { using: false, withCheck: true },
78
+ update: { using: true, withCheck: true },
79
+ delete: { using: true, withCheck: false },
80
+ all: { using: true, withCheck: true },
81
+ };
82
+
53
83
  function readRefParam(block: PslExtensionBlock, key: string): string | undefined {
54
84
  const param = block.parameters[key];
55
85
  return param?.kind === 'ref' ? param.identifier : undefined;
@@ -101,18 +131,43 @@ function unwrapQuotedString(raw: string): string {
101
131
 
102
132
  function lowerRlsPolicyFromBlock(
103
133
  block: RlsPolicyExtensionBlock,
104
- _ctx: AuthoringEntityContext,
105
- ): PostgresRlsPolicy {
134
+ ctx: AuthoringEntityContext,
135
+ ): PostgresRlsPolicy | undefined {
106
136
  const prefix = block.name;
137
+ const operation = POLICY_KEYWORD_OPERATION[block.keyword] ?? 'select';
107
138
  const targetModelName = readRefParam(block, 'target') ?? '';
108
139
  const tableName = targetModelName.charAt(0).toLowerCase() + targetModelName.slice(1);
109
140
  const roles = [...readListRefParams(block, 'roles')].sort();
110
- const using = unwrapQuotedString(readValueParam(block, 'using') ?? '');
141
+
142
+ const usingRaw = readValueParam(block, 'using');
143
+ const withCheckRaw = readValueParam(block, 'withCheck');
144
+
145
+ // Reject a predicate the operation does not take (e.g. `using` on INSERT, or
146
+ // `withCheck` on SELECT/DELETE). The descriptor's param set already omits it,
147
+ // but the generic descriptor validator is not wired into the SQL-family
148
+ // interpreter, so the lowering enforces the per-operation predicate matrix
149
+ // directly — a wrong predicate is a load-time diagnostic, not a silent drop.
150
+ const support = POLICY_OPERATION_PREDICATES[operation];
151
+ const rejectPredicate = (predicate: 'using' | 'withCheck'): undefined => {
152
+ ctx.diagnostics?.push({
153
+ code: 'PSL_RLS_PREDICATE_NOT_FOR_OPERATION',
154
+ message: `\`${block.keyword}\` policy "${block.name}" does not take a \`${predicate}\` predicate; the ${operation.toUpperCase()} operation uses ${support.using ? '`using`' : '`withCheck`'}${support.using && support.withCheck ? ' and `withCheck`' : ' only'}.`,
155
+ sourceId: ctx.sourceId ?? 'unknown',
156
+ span: block.parameters[predicate]?.span ?? block.span,
157
+ });
158
+ return undefined;
159
+ };
160
+ if (usingRaw !== undefined && !support.using) return rejectPredicate('using');
161
+ if (withCheckRaw !== undefined && !support.withCheck) return rejectPredicate('withCheck');
162
+
163
+ const using = usingRaw !== undefined ? unwrapQuotedString(usingRaw) : undefined;
164
+ const withCheck = withCheckRaw !== undefined ? unwrapQuotedString(withCheckRaw) : undefined;
111
165
 
112
166
  const wireHash = computeContentHash({
113
- using: normalizePredicate(using),
167
+ ...ifDefined('using', using !== undefined ? normalizePredicate(using) : undefined),
168
+ ...ifDefined('withCheck', withCheck !== undefined ? normalizePredicate(withCheck) : undefined),
114
169
  roles,
115
- operation: 'select',
170
+ operation,
116
171
  permissive: true,
117
172
  });
118
173
  const wireName = formatRlsPolicyWireName(prefix, wireHash);
@@ -122,9 +177,10 @@ function lowerRlsPolicyFromBlock(
122
177
  prefix,
123
178
  tableName,
124
179
  namespaceId: block.namespaceId,
125
- operation: 'select',
180
+ operation,
126
181
  roles,
127
- using,
182
+ ...ifDefined('using', using),
183
+ ...ifDefined('withCheck', withCheck),
128
184
  permissive: true,
129
185
  });
130
186
  }
@@ -298,13 +354,10 @@ export const postgresAuthoringEntityTypes = {
298
354
  * the family pack instead.
299
355
  */
300
356
  /**
301
- * PSL block descriptor for `policy_select`.
302
- *
303
- * The parser learns the block shape from this descriptor; lowering from
304
- * `PslExtensionBlock` to `PostgresRlsPolicy` is wired in the PSL
305
- * interpreter (a later dispatch). The `discriminator` matches
306
- * `PostgresRlsPolicy.kind` so the parsed block node carries the same
307
- * discriminant as the IR class it will lower to.
357
+ * Shared parameter descriptors for the five `policy_<op>` PSL block
358
+ * keywords. All five share the `policy` discriminator — the parser
359
+ * dispatches by keyword (see the framework's PSL-block SPI), and every
360
+ * keyword's factory lowers to `PostgresRlsPolicy` via `lowerRlsPolicyFromBlock`.
308
361
  *
309
362
  * The `roles` list uses `scope:'cross-space'` because same-namespace
310
363
  * role ref resolution requires PSL namespace entries keyed by `refKind`
@@ -313,24 +366,82 @@ export const postgresAuthoringEntityTypes = {
313
366
  * slice 4 (cross-space roles). Until then cross-space passes validation
314
367
  * unconditionally and the authored role names flow through unchanged.
315
368
  */
369
+ const policyTargetParam = {
370
+ kind: 'ref',
371
+ refKind: 'model',
372
+ scope: 'same-namespace',
373
+ required: true,
374
+ } as const;
375
+ const policyRolesParam = {
376
+ kind: 'list',
377
+ of: { kind: 'ref', refKind: 'role', scope: 'cross-space' },
378
+ } as const;
379
+ const policyPredicateParam = { kind: 'value', codecId: 'pg/text@1', required: true } as const;
380
+ // A policy may only target an RLS-controlled model: the model named by
381
+ // `target` must declare `@@rls`, or the load fails with a diagnostic naming
382
+ // the model and the policy prefix.
383
+ const policyRequiresRls = { parameter: 'target', attribute: 'rls' } as const;
384
+
316
385
  export const postgresAuthoringPslBlockDescriptors = {
386
+ // The predicate param set per keyword mirrors Postgres: SELECT/DELETE take
387
+ // USING only; INSERT takes WITH CHECK only; UPDATE/ALL take both. The
388
+ // per-operation predicate matrix is enforced in `lowerRlsPolicyFromBlock`
389
+ // (a wrong predicate for the operation is a load-time diagnostic there),
390
+ // since the generic descriptor validator is not wired into the SQL-family
391
+ // interpreter.
317
392
  policy_select: {
318
393
  kind: 'pslBlock',
319
394
  keyword: 'policy_select',
320
395
  discriminator: 'policy',
321
396
  name: { required: true },
397
+ parameters: { target: policyTargetParam, roles: policyRolesParam, using: policyPredicateParam },
398
+ requiresModelAttribute: policyRequiresRls,
399
+ },
400
+ policy_delete: {
401
+ kind: 'pslBlock',
402
+ keyword: 'policy_delete',
403
+ discriminator: 'policy',
404
+ name: { required: true },
405
+ parameters: { target: policyTargetParam, roles: policyRolesParam, using: policyPredicateParam },
406
+ requiresModelAttribute: policyRequiresRls,
407
+ },
408
+ policy_insert: {
409
+ kind: 'pslBlock',
410
+ keyword: 'policy_insert',
411
+ discriminator: 'policy',
412
+ name: { required: true },
322
413
  parameters: {
323
- target: { kind: 'ref', refKind: 'model', scope: 'same-namespace', required: true },
324
- roles: {
325
- kind: 'list',
326
- of: { kind: 'ref', refKind: 'role', scope: 'cross-space' },
327
- },
328
- using: { kind: 'value', codecId: 'pg/text@1', required: true },
414
+ target: policyTargetParam,
415
+ roles: policyRolesParam,
416
+ withCheck: policyPredicateParam,
417
+ },
418
+ requiresModelAttribute: policyRequiresRls,
419
+ },
420
+ policy_update: {
421
+ kind: 'pslBlock',
422
+ keyword: 'policy_update',
423
+ discriminator: 'policy',
424
+ name: { required: true },
425
+ parameters: {
426
+ target: policyTargetParam,
427
+ roles: policyRolesParam,
428
+ using: policyPredicateParam,
429
+ withCheck: policyPredicateParam,
430
+ },
431
+ requiresModelAttribute: policyRequiresRls,
432
+ },
433
+ policy_all: {
434
+ kind: 'pslBlock',
435
+ keyword: 'policy_all',
436
+ discriminator: 'policy',
437
+ name: { required: true },
438
+ parameters: {
439
+ target: policyTargetParam,
440
+ roles: policyRolesParam,
441
+ using: policyPredicateParam,
442
+ withCheck: policyPredicateParam,
329
443
  },
330
- // A policy may only target an RLS-controlled model: the model named by
331
- // `target` must declare `@@rls`, or the load fails with a diagnostic
332
- // naming the model and the policy prefix.
333
- requiresModelAttribute: { parameter: 'target', attribute: 'rls' },
444
+ requiresModelAttribute: policyRequiresRls,
334
445
  },
335
446
  /**
336
447
  * PSL block descriptor for `native_enum`.
@@ -5,6 +5,7 @@ import { entityAt, UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-component
5
5
  import type { SqlStorage, StorageTable } from '@prisma-next/sql-contract/types';
6
6
  import { blindCast } from '@prisma-next/utils/casts';
7
7
  import { ifDefined } from '@prisma-next/utils/defined';
8
+ import { type PostgresRole, ROLE_DEFAULT_CONTROL_POLICY } from '../postgres-role';
8
9
  import { isPostgresSchema } from '../postgres-schema';
9
10
  import type { PostgresNamespaceSchemaNode } from '../schema-ir/postgres-namespace-schema-node';
10
11
  import { PostgresTableSchemaNode } from '../schema-ir/postgres-table-schema-node';
@@ -215,6 +216,17 @@ export function resolvePostgresNodeIssueCreationFactoryName(
215
216
  * `namespaceId` falls back to {@link UNBOUND_NAMESPACE_ID} via
216
217
  * `resolveNamespaceIdForTable`'s own fallback — the call-side resolver does
217
218
  * the same for the `DropTableCall` it produces.
219
+ *
220
+ * A role node resolves against the `PostgresRole` contract entity at that
221
+ * role's name (roles are root-level diff subjects, so the name is the
222
+ * second path segment after the tree root). A `not-found` role is declared,
223
+ * so the entity is always found and its `control` read directly — which is
224
+ * always {@link ROLE_DEFAULT_CONTROL_POLICY} today, since no authoring
225
+ * surface sets it otherwise. A `not-expected` role has no contract entity
226
+ * (an undeclared live role, by definition), so the subject falls back to
227
+ * the same default — roles are referenced but never owned, so an
228
+ * undeclared role gets exactly the same governance as a declared one.
229
+ * Never creates a new object: role provisioning is a non-goal.
218
230
  */
219
231
  export function resolvePostgresNodeIssueControlPolicySubject(
220
232
  issue: SchemaDiffIssue<SqlSchemaDiffNode>,
@@ -234,6 +246,23 @@ export function resolvePostgresNodeIssueControlPolicySubject(
234
246
  };
235
247
  }
236
248
 
249
+ if (node.nodeKind === PostgresSchemaNodeKind.role) {
250
+ const roleName = issue.path[1];
251
+ const roleEntity =
252
+ roleName === undefined
253
+ ? undefined
254
+ : entityAt<PostgresRole>(contract.storage, {
255
+ namespaceId: UNBOUND_NAMESPACE_ID,
256
+ entityKind: 'role',
257
+ entityName: roleName,
258
+ });
259
+ return {
260
+ namespaceId: UNBOUND_NAMESPACE_ID,
261
+ explicitNodeControlPolicy: roleEntity?.control ?? ROLE_DEFAULT_CONTROL_POLICY,
262
+ createsNewObject: false,
263
+ };
264
+ }
265
+
237
266
  const tableName = issue.path[2];
238
267
  if (tableName === undefined) return undefined;
239
268
  const ddlSchemaName = issue.path[1];
@@ -3,8 +3,9 @@ import type { SqlSchemaDiffResult } from '@prisma-next/family-sql/control';
3
3
  import { buildNativeTypeExpander } from '@prisma-next/family-sql/control';
4
4
  import { classifyDiffSubjectGranularity } from '@prisma-next/family-sql/diff';
5
5
  import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
6
- import type { SchemaDiffIssue } from '@prisma-next/framework-components/control';
6
+ import type { DiffableNode, SchemaDiffIssue } from '@prisma-next/framework-components/control';
7
7
  import { diffSchemas } from '@prisma-next/framework-components/control';
8
+ import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
8
9
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
9
10
  import type { SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';
10
11
  import { blindCast } from '@prisma-next/utils/casts';
@@ -19,6 +20,31 @@ import {
19
20
  import { contractToPostgresDatabaseSchemaNode } from './contract-to-postgres-database-schema-node';
20
21
  import { resolvePostgresNodeIssueControlPolicySubject } from './control-policy';
21
22
 
23
+ /**
24
+ * Whether a diff issue's subject node is cluster-scoped — it carries its own
25
+ * `namespaceId` field (only role and policy diff nodes do) and that
26
+ * coordinate is the unbound sentinel, meaning the node is not owned by any
27
+ * schema/namespace. A role node is always cluster-scoped (roles are
28
+ * cluster-level objects); a policy node's `namespaceId` is always its
29
+ * table's resolved DDL schema, never the sentinel. Namespace-ownership
30
+ * scoping (which compares `issue.path[1]` against the set of DDL schemas
31
+ * the contract owns) makes no sense for a cluster-scoped subject — its path
32
+ * segment at that index is the object's own name, not a schema name — so
33
+ * such an issue bypasses that scoping entirely.
34
+ */
35
+ function isClusterScopedIssue(issue: SchemaDiffIssue): boolean {
36
+ const node = issue.expected ?? issue.actual;
37
+ return node !== undefined && nodeNamespaceId(node) === UNBOUND_NAMESPACE_ID;
38
+ }
39
+
40
+ function nodeNamespaceId(node: DiffableNode): string | undefined {
41
+ if (!Object.hasOwn(node, 'namespaceId')) return undefined;
42
+ return blindCast<
43
+ { namespaceId: string },
44
+ 'presence of an own namespaceId field was just checked via Object.hasOwn'
45
+ >(node).namespaceId;
46
+ }
47
+
22
48
  function ownedSchemaNames(expected: PostgresDatabaseSchemaNode): ReadonlySet<string> {
23
49
  const policyNamespaces = Object.values(expected.namespaces).flatMap((ns) =>
24
50
  Object.values(ns.tables).flatMap((t) => t.policies.map((p) => p.namespaceId)),
@@ -53,10 +79,17 @@ function pruneTableLessNamespaces(
53
79
  }
54
80
 
55
81
  /**
56
- * Resolves a verdict-diff issue's subject table's declared control policy
57
- * directly from the contract, by delegating to the same node-typed resolver
82
+ * Resolves a verdict-diff issue's subject's declared control policy directly
83
+ * from the contract, by delegating to the same node-typed resolver
58
84
  * ({@link resolvePostgresNodeIssueControlPolicySubject}) the planner uses to
59
- * gate DDL calls. `undefined` when the issue resolves to no contract table.
85
+ * gate DDL calls. `undefined` when the issue resolves to no contract subject.
86
+ *
87
+ * A role issue resolves through that same resolver to `external`
88
+ * unconditionally (see its role branch), regardless of the contract's own
89
+ * default policy: a role is referenced by the contract but not owned, and
90
+ * `external`'s existing semantics — a missing declared subject still fails,
91
+ * every extra is suppressed — are exactly the wanted asymmetric grading for
92
+ * a cluster object the framework does not own.
60
93
  */
61
94
  function resolveControlPolicy(
62
95
  issue: SchemaDiffIssue,
@@ -75,8 +108,9 @@ function resolveControlPolicy(
75
108
  * the expected tree (resolved leaf values, expander threaded, FK schemas
76
109
  * resolved, table-less namespaces pruned), run the generic
77
110
  * differ over the trees as derived, and scope out `not-expected` findings under namespaces the
78
- * contract does not own. Ownership is role-aware, mirroring the legacy
79
- * decomposition: relational extras check the PRUNED owned set (the legacy
111
+ * contract does not own. Ownership scoping bypasses cluster-scoped subjects
112
+ * (roles today), mirroring the legacy decomposition: relational extras check
113
+ * the PRUNED owned set (the legacy
80
114
  * per-namespace walk never visited a table-less namespace, so its live
81
115
  * relational contents are invisible), while `structural` extras (RLS
82
116
  * policies) check the FULL owned set (the legacy policy diff governed
@@ -107,9 +141,10 @@ export function diffPostgresSchema(input: {
107
141
  const structuralOwned = ownedSchemaNames(fullExpected);
108
142
  const issues = diffSchemas(expected, actual).filter((issue) => {
109
143
  if (issue.reason !== 'not-expected') return true;
144
+ if (isClusterScopedIssue(issue)) return true;
145
+ const granularity = classifyDiffSubjectGranularity(issue, postgresDiffSubjectGranularity);
110
146
  const namespaceSegment = issue.path[1];
111
147
  if (namespaceSegment === undefined) return true;
112
- const granularity = classifyDiffSubjectGranularity(issue, postgresDiffSubjectGranularity);
113
148
  const owned = granularity === 'structural' ? structuralOwned : relationalOwned;
114
149
  return owned.has(namespaceSegment);
115
150
  });
@@ -165,15 +200,15 @@ export interface PostgresPlanDiff {
165
200
  readonly expected: PostgresDatabaseSchemaNode;
166
201
  /** The live ("start") tree, padded with empty namespaces so a missing schema's tables pair. */
167
202
  readonly actual: PostgresDatabaseSchemaNode;
168
- /** The one node diff over the two trees: relational + policy drift, role-aware ownership filtered. */
203
+ /** The one node diff over the two trees: relational + policy drift, cluster-scope-aware ownership filtered. */
169
204
  readonly issues: readonly SchemaDiffIssue<SqlSchemaDiffNode>[];
170
205
  }
171
206
 
172
207
  /**
173
208
  * The Postgres planner's diff input: the SAME tree-building
174
209
  * `diffPostgresSchema` uses (expander threaded, FK schemas resolved,
175
- * table-less namespaces pruned, role-aware ownership filter) plus actual
176
- * namespace padding (so a missing
210
+ * table-less namespaces pruned, cluster-scope-aware ownership filter) plus
211
+ * actual namespace padding (so a missing
177
212
  * schema's tables surface as `not-found` instead of a swallowed namespace
178
213
  * `not-found`). One differ drives both verify and plan; this is the
179
214
  * plan-side derivation. The single issue list covers tables / columns /
@@ -207,9 +242,10 @@ export function buildPostgresPlanDiff(input: {
207
242
  'both trees are PostgresDatabaseSchemaNodes, so every diff-issue node is a SqlSchemaDiffNode'
208
243
  >(diffSchemas(expected, paddedActual)).filter((issue) => {
209
244
  if (issue.reason !== 'not-expected') return true;
245
+ if (isClusterScopedIssue(issue)) return true;
246
+ const granularity = classifyDiffSubjectGranularity(issue, postgresDiffSubjectGranularity);
210
247
  const namespaceSegment = issue.path[1];
211
248
  if (namespaceSegment === undefined) return true;
212
- const granularity = classifyDiffSubjectGranularity(issue, postgresDiffSubjectGranularity);
213
249
  const owned = granularity === 'structural' ? structuralOwned : relationalOwned;
214
250
  return owned.has(namespaceSegment);
215
251
  });
@@ -186,6 +186,11 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
186
186
  frameworkComponents: options.frameworkComponents,
187
187
  });
188
188
  const policyDiffIssues = rawIssues.filter((issue) => isPolicyDiffIssue(issue));
189
+ // Role diff issues resolve to the `external` control policy (see
190
+ // `resolvePostgresNodeIssueControlPolicySubject`'s role branch), so the
191
+ // control-policy partition below suppresses them to zero ops on its own,
192
+ // before `mapNodeIssueToCall` ever sees them — no separate exclusion
193
+ // needed here.
189
194
  const relationalDiffIssues = rawIssues.filter((issue) => !isPolicyDiffIssue(issue));
190
195
 
191
196
  // The generic differ is total and un-gated: strict-mode extras filtering
@@ -1,6 +1,15 @@
1
1
  import { freezeNode } from '@prisma-next/framework-components/ir';
2
2
  import { SqlNode } from '@prisma-next/sql-contract/types';
3
3
 
4
+ /**
5
+ * Roles are referenced by the contract but never owned — the framework
6
+ * issues no `CREATE`/`DROP ROLE`, so a role's effective governance is always
7
+ * `external`: a missing declared role fails verify, an extra live role is
8
+ * tolerated, and the planner emits zero role DDL. `external` is the only
9
+ * value a role's control can ever carry.
10
+ */
11
+ export const ROLE_DEFAULT_CONTROL_POLICY = 'external';
12
+
4
13
  export interface PostgresRoleInput {
5
14
  readonly name: string;
6
15
  /**
@@ -8,6 +17,11 @@ export interface PostgresRoleInput {
8
17
  * from `@prisma-next/framework-components/ir`.
9
18
  */
10
19
  readonly namespaceId: string;
20
+ /**
21
+ * Defaults to {@link ROLE_DEFAULT_CONTROL_POLICY} when omitted. Roles are
22
+ * referenced but never owned, so `'external'` is the only allowed value.
23
+ */
24
+ readonly control?: 'external';
11
25
  }
12
26
 
13
27
  /**
@@ -22,16 +36,24 @@ export interface PostgresRoleInput {
22
36
  * Extends `SqlNode` directly, frozen at construction via `freezeNode(this)`.
23
37
  * The `kind: 'role'` discriminant is enumerable so it survives JSON.
24
38
  * Matches the entries key (one-string rule).
39
+ *
40
+ * `control` always carries a value (never omitted): it defaults to
41
+ * {@link ROLE_DEFAULT_CONTROL_POLICY} in the constructor, so both a
42
+ * freshly-authored role and one hydrated from an older `contract.json`
43
+ * lacking the field resolve the same way — the control-policy resolver
44
+ * reads it directly off the entity instead of special-casing role issues.
25
45
  */
26
46
  export class PostgresRole extends SqlNode {
27
47
  override readonly kind = 'role' as const;
28
48
  readonly name: string;
29
49
  readonly namespaceId: string;
50
+ readonly control: 'external';
30
51
 
31
52
  constructor(input: PostgresRoleInput) {
32
53
  super();
33
54
  this.name = input.name;
34
55
  this.namespaceId = input.namespaceId;
56
+ this.control = input.control ?? ROLE_DEFAULT_CONTROL_POLICY;
35
57
  freezeNode(this);
36
58
  }
37
59
  }
@@ -4,6 +4,7 @@ export const PostgresRoleSchema = type({
4
4
  kind: "'role'",
5
5
  name: 'string',
6
6
  namespaceId: 'string',
7
+ 'control?': "'external'",
7
8
  });
8
9
 
9
10
  export const PostgresRlsPolicySchema = type({
@@ -663,6 +663,7 @@ function buildNativeEnumBlock(
663
663
 
664
664
  return {
665
665
  kind: 'native_enum',
666
+ keyword: 'native_enum',
666
667
  name,
667
668
  parameters,
668
669
  blockAttributes:
@@ -17,9 +17,10 @@ export interface PostgresDatabaseSchemaNodeInput {
17
17
  *
18
18
  * `id` is the fixed sentinel `'database'` — the root has no siblings and
19
19
  * the value is never emitted into migration paths. `isEqualTo` is identity
20
- * (roots always share the `'database'` id). `children()` returns namespace
21
- * nodes only; roles are held on the root but not yielded (role diffing is a
22
- * later slice).
20
+ * (roots always share the `'database'` id). `children()` yields the namespace
21
+ * nodes followed by the role nodes both are root-level diff subjects. The
22
+ * differ pairs siblings by `(nodeKind, id)`, so a role and a namespace may
23
+ * share a name without colliding.
23
24
  */
24
25
  export class PostgresDatabaseSchemaNode extends SqlSchemaIRNode implements DiffableNode {
25
26
  override readonly nodeKind = PostgresSchemaNodeKind.database;
@@ -47,7 +48,7 @@ export class PostgresDatabaseSchemaNode extends SqlSchemaIRNode implements Diffa
47
48
  }
48
49
 
49
50
  children(): readonly DiffableNode[] {
50
- return Object.values(this.namespaces);
51
+ return [...Object.values(this.namespaces), ...this.roles];
51
52
  }
52
53
 
53
54
  static is(node: SqlSchemaIRNode): node is PostgresDatabaseSchemaNode {
@@ -20,8 +20,10 @@ export interface PostgresRoleSchemaNodeInput {
20
20
  * Built by project-from-contract and project-from-database from their respective
21
21
  * `PostgresRole` contract entities / introspected rows.
22
22
  *
23
- * Roles are cluster-scoped, so `id` is the role name alone. `isEqualTo` compares
24
- * names name-equality is role-equality for cluster-scoped objects.
23
+ * Roles are cluster-scoped, so `id` is the bare role name. The differ pairs
24
+ * siblings by `(nodeKind, id)`, so a role and a namespace may share a name
25
+ * (role `public`, schema `public`) without colliding. `isEqualTo` compares
26
+ * ids — name-equality is role-equality for cluster-scoped objects.
25
27
  */
26
28
  export class PostgresRoleSchemaNode extends SqlSchemaIRNode implements DiffableNode {
27
29
  override readonly nodeKind = PostgresSchemaNodeKind.role;
@@ -45,6 +45,13 @@ const POSTGRES_NODE_GRANULARITY: Readonly<Record<PostgresSchemaNodeKind, DiffSub
45
45
  [PostgresSchemaNodeKind.database]: 'structural',
46
46
  [PostgresSchemaNodeKind.namespace]: 'namespace',
47
47
  [PostgresSchemaNodeKind.table]: 'entity',
48
+ // A policy set is owned by the managed table it attaches to; a role is
49
+ // referenced by the contract but not owned (the framework names it in a
50
+ // policy but never owns the cluster's role list). Both classify as
51
+ // `structural`. The asymmetry for roles — a missing declared one fails, an
52
+ // undeclared live one is tolerated unconditionally — comes from the
53
+ // control policy the diff resolves a role issue against (`external`),
54
+ // not from the granularity.
48
55
  [PostgresSchemaNodeKind.policy]: 'structural',
49
56
  [PostgresSchemaNodeKind.role]: 'structural',
50
57
  };
@@ -1 +0,0 @@
1
- {"version":3,"file":"authoring-WkqG_MkU.mjs","names":[],"sources":["../src/core/authoring.ts"],"sourcesContent":["import { temporalAuthoringPresets } from '@prisma-next/family-sql/control';\nimport type {\n AuthoringEntityContext,\n AuthoringEntityTypeFactoryOutput,\n AuthoringEntityTypeNamespace,\n AuthoringFieldNamespace,\n AuthoringModelAttributeContext,\n AuthoringModelAttributeDescriptorNamespace,\n AuthoringPslBlockDescriptorNamespace,\n AuthoringTypeNamespace,\n PslExtensionBlock,\n} from '@prisma-next/framework-components/authoring';\nimport { modelAttribute } from '@prisma-next/psl-parser';\nimport type { SqlValueSetDerivingEntityTypeOutput } from '@prisma-next/sql-contract/value-set-derivation-hook';\nimport { PG_ENUM_CODEC_ID } from './codec-ids';\nimport { PostgresNativeEnum } from './postgres-native-enum';\nimport { PostgresRlsEnablement, type PostgresRlsEnablementInput } from './postgres-rls-enablement';\nimport { PostgresRlsPolicy } from './postgres-rls-policy';\nimport { PostgresRole, type PostgresRoleInput } from './postgres-role';\nimport {\n PostgresNativeEnumSchema,\n PostgresRlsEnablementSchema,\n PostgresRlsPolicySchema,\n PostgresRoleSchema,\n} from './postgres-validators';\nimport { computeContentHash, normalizePredicate } from './rls/canonicalize';\nimport { formatRlsPolicyWireName } from './rls/wire-name';\n\n/**\n * `pg.enum(<ref>)` registers as an ordinary type constructor whose sole\n * positional argument names a `native_enum` entity instead of carrying a\n * literal value. The interpreter resolves the ref to the `native_enum`\n * entity generically (driven by `entityRefArg`); the `pg/enum@1` codec\n * descriptor's `columnFromEntity` hook (see `codecs.ts`) converts that\n * entity into the column's `typeParams` and native type.\n */\nexport const postgresAuthoringTypes = {\n pg: {\n enum: {\n kind: 'typeConstructor',\n entityRefArg: { index: 0, entityKind: 'native_enum' },\n output: {\n codecId: PG_ENUM_CODEC_ID,\n },\n },\n },\n} as const satisfies AuthoringTypeNamespace;\n\nexport interface RlsPolicyExtensionBlock extends PslExtensionBlock {\n readonly namespaceId: string;\n}\n\nfunction readRefParam(block: PslExtensionBlock, key: string): string | undefined {\n const param = block.parameters[key];\n return param?.kind === 'ref' ? param.identifier : undefined;\n}\n\nfunction readValueParam(block: PslExtensionBlock, key: string): string | undefined {\n const param = block.parameters[key];\n return param?.kind === 'value' ? param.raw : undefined;\n}\n\nfunction readListRefParams(block: PslExtensionBlock, key: string): string[] {\n const param = block.parameters[key];\n if (param?.kind !== 'list') return [];\n return param.items.flatMap((item) => (item.kind === 'ref' ? [item.identifier] : []));\n}\n\n/**\n * Unwraps a quoted PSL string argument, inverting the printer's\n * `escapePslString` escapes (`\\\\`, `\\\"`, `\\n`, `\\r`). An unknown escape\n * sequence is kept verbatim, matching the printer-side `unescapePslString`\n * convention.\n */\nfunction unwrapQuotedString(raw: string): string {\n if (!(raw.startsWith('\"') && raw.endsWith('\"') && raw.length >= 2)) {\n return raw;\n }\n const inner = raw.slice(1, -1);\n let result = '';\n for (let i = 0; i < inner.length; i++) {\n if (inner[i] !== '\\\\' || i + 1 >= inner.length) {\n result += inner[i];\n continue;\n }\n const next = inner[i + 1];\n if (next === '\\\\' || next === '\"') {\n result += next;\n } else if (next === 'n') {\n result += '\\n';\n } else if (next === 'r') {\n result += '\\r';\n } else {\n result += '\\\\';\n result += next;\n }\n i++;\n }\n return result;\n}\n\nfunction lowerRlsPolicyFromBlock(\n block: RlsPolicyExtensionBlock,\n _ctx: AuthoringEntityContext,\n): PostgresRlsPolicy {\n const prefix = block.name;\n const targetModelName = readRefParam(block, 'target') ?? '';\n const tableName = targetModelName.charAt(0).toLowerCase() + targetModelName.slice(1);\n const roles = [...readListRefParams(block, 'roles')].sort();\n const using = unwrapQuotedString(readValueParam(block, 'using') ?? '');\n\n const wireHash = computeContentHash({\n using: normalizePredicate(using),\n roles,\n operation: 'select',\n permissive: true,\n });\n const wireName = formatRlsPolicyWireName(prefix, wireHash);\n\n return new PostgresRlsPolicy({\n name: wireName,\n prefix,\n tableName,\n namespaceId: block.namespaceId,\n operation: 'select',\n roles,\n using,\n permissive: true,\n });\n}\n\n/**\n * Lowers a `native_enum { memberName = \"value\" … @@map(\"type_name\") }` block\n * into a {@link PostgresNativeEnum}. Members must be authored as explicit\n * `key = \"value\"` pairs — a bare (value-less) member is a diagnostic, not\n * accepted (authoring-design.md §2.1). The parsed `memberName` is only used\n * to duplicate-check and report diagnostics; the lowered entity carries just\n * the member values (a native enum is value-only — the member \"name\" isn't\n * a separate authoring concept from the value). `typeName` comes from\n * `@@map` or defaults to the block name verbatim.\n */\nfunction lowerNativeEnumFromBlock(\n block: PslExtensionBlock,\n ctx: AuthoringEntityContext,\n): PostgresNativeEnum | undefined {\n const sourceId = ctx.sourceId ?? 'unknown';\n const diagnostics = ctx.diagnostics;\n\n const mapAttr = block.blockAttributes.find((a) => a.name === 'map');\n let typeName = block.name;\n if (mapAttr) {\n const rawArg = mapAttr.args[0]?.value;\n const mapped = rawArg !== undefined ? unwrapQuotedString(rawArg) : undefined;\n if (mapped === undefined) {\n diagnostics?.push({\n code: 'PSL_NATIVE_ENUM_INVALID_MAP',\n message: `native_enum \"${block.name}\" @@map attribute must have a quoted type-name argument`,\n sourceId,\n span: mapAttr.span,\n });\n return undefined;\n }\n typeName = mapped;\n }\n\n let memberError = false;\n const seenValues = new Set<string>();\n const members: string[] = [];\n for (const [memberName, paramValue] of Object.entries(block.parameters)) {\n if (paramValue.kind === 'bare') {\n diagnostics?.push({\n code: 'PSL_NATIVE_ENUM_BARE_MEMBER',\n message: `native_enum \"${block.name}\" member \"${memberName}\" has no value; members must be authored as \"${memberName} = \\\\\"value\\\\\"\"`,\n sourceId,\n span: paramValue.span,\n });\n memberError = true;\n continue;\n }\n if (paramValue.kind !== 'value') continue;\n\n let jsonValue: unknown;\n try {\n jsonValue = JSON.parse(paramValue.raw);\n } catch {\n diagnostics?.push({\n code: 'PSL_EXTENSION_INVALID_VALUE',\n message: `native_enum \"${block.name}\" member \"${memberName}\" value \"${paramValue.raw}\" is not valid JSON`,\n sourceId,\n span: paramValue.span,\n });\n memberError = true;\n continue;\n }\n if (typeof jsonValue !== 'string') {\n diagnostics?.push({\n code: 'PSL_EXTENSION_INVALID_VALUE',\n message: `native_enum \"${block.name}\" member \"${memberName}\" value must be a string`,\n sourceId,\n span: paramValue.span,\n });\n memberError = true;\n continue;\n }\n if (seenValues.has(jsonValue)) {\n diagnostics?.push({\n code: 'PSL_NATIVE_ENUM_DUPLICATE_MEMBER_VALUE',\n message: `native_enum \"${block.name}\": duplicate member value \"${jsonValue}\"`,\n sourceId,\n span: paramValue.span,\n });\n memberError = true;\n continue;\n }\n seenValues.add(jsonValue);\n members.push(jsonValue);\n }\n\n if (memberError) return undefined;\n\n if (members.length === 0) {\n diagnostics?.push({\n code: 'PSL_NATIVE_ENUM_MISSING_MEMBERS',\n message: `native_enum \"${block.name}\" must have at least one member`,\n sourceId,\n span: block.span,\n });\n return undefined;\n }\n\n // `control` stays unset — the effective grade is resolved at read time via `effectiveControlPolicy`, like `StorageTable`/`StorageColumn`.\n return new PostgresNativeEnum({ typeName, members });\n}\n\n/**\n * `native_enum`'s entity-type factory output, checked separately from the assembled\n * `postgresAuthoringEntityTypes` map below: `deriveValueSet` is SQL-family surface\n * ({@link SqlValueSetDerivingEntityTypeOutput}), not part of the framework\n * `AuthoringEntityTypeFactoryOutput` shape, so folding it directly into the map's single\n * `satisfies AuthoringEntityTypeNamespace` check would trip an excess-property error. Checking it\n * here against the intersection of both shapes keeps it structurally valid against each without\n * widening the map's own check.\n */\nconst nativeEnumEntityTypeOutput = {\n factory: lowerNativeEnumFromBlock,\n deriveValueSet: (entity: PostgresNativeEnum) => ({\n kind: 'valueSet' as const,\n values: [...entity.members],\n }),\n} satisfies AuthoringEntityTypeFactoryOutput<PslExtensionBlock, PostgresNativeEnum | undefined> &\n SqlValueSetDerivingEntityTypeOutput;\n\nexport const postgresAuthoringEntityTypes = {\n role: {\n kind: 'entity',\n discriminator: 'role',\n validatorSchema: PostgresRoleSchema,\n output: {\n factory: (input: PostgresRoleInput): PostgresRole => new PostgresRole(input),\n },\n },\n rls: {\n kind: 'entity',\n discriminator: 'rls',\n validatorSchema: PostgresRlsEnablementSchema,\n output: {\n factory: (input: PostgresRlsEnablementInput): PostgresRlsEnablement =>\n new PostgresRlsEnablement(input),\n },\n },\n policy: {\n kind: 'entity',\n discriminator: 'policy',\n validatorSchema: PostgresRlsPolicySchema,\n output: {\n factory: lowerRlsPolicyFromBlock,\n },\n },\n native_enum: {\n kind: 'entity',\n discriminator: 'native_enum',\n validatorSchema: PostgresNativeEnumSchema,\n output: nativeEnumEntityTypeOutput,\n },\n} as const satisfies AuthoringEntityTypeNamespace;\n\n/**\n * Field presets contributed by the Postgres target pack.\n *\n * These mirror the PSL scalar-to-codec mapping used by the Postgres adapter\n * (see `createPostgresPslScalarTypeDescriptors`), so that authoring a field\n * via the TS callback surface (e.g. `field.int()`) and via the PSL scalar\n * surface (e.g. `Int`) lowers to byte-identical contracts.\n *\n * The `uuidNative` / `id.uuidv4Native` / `id.uuidv7Native` presets use the\n * native Postgres `uuid` type (codecId `pg/uuid@1`). For cross-target\n * portability use `uuidString` / `id.uuidv4String` / `id.uuidv7String` from\n * the family pack instead.\n */\n/**\n * PSL block descriptor for `policy_select`.\n *\n * The parser learns the block shape from this descriptor; lowering from\n * `PslExtensionBlock` to `PostgresRlsPolicy` is wired in the PSL\n * interpreter (a later dispatch). The `discriminator` matches\n * `PostgresRlsPolicy.kind` so the parsed block node carries the same\n * discriminant as the IR class it will lower to.\n *\n * The `roles` list uses `scope:'cross-space'` because same-namespace\n * role ref resolution requires PSL namespace entries keyed by `refKind`\n * (i.e. `'role'`), which in turn requires the role block discriminator to\n * equal `'role'`. Aligning discriminator with refKind is tracked for\n * slice 4 (cross-space roles). Until then cross-space passes validation\n * unconditionally and the authored role names flow through unchanged.\n */\nexport const postgresAuthoringPslBlockDescriptors = {\n policy_select: {\n kind: 'pslBlock',\n keyword: 'policy_select',\n discriminator: 'policy',\n name: { required: true },\n parameters: {\n target: { kind: 'ref', refKind: 'model', scope: 'same-namespace', required: true },\n roles: {\n kind: 'list',\n of: { kind: 'ref', refKind: 'role', scope: 'cross-space' },\n },\n using: { kind: 'value', codecId: 'pg/text@1', required: true },\n },\n // A policy may only target an RLS-controlled model: the model named by\n // `target` must declare `@@rls`, or the load fails with a diagnostic\n // naming the model and the policy prefix.\n requiresModelAttribute: { parameter: 'target', attribute: 'rls' },\n },\n /**\n * PSL block descriptor for `native_enum`.\n *\n * Reuses the existing variadic-block mechanism (the same shape the SQL\n * family's `enum` block ships): the body is an open `memberName = \"value\"`\n * list. `variadicParameters: true` opens the block to arbitrary keys\n * beyond the declared (empty) `parameters` set — the lowering factory\n * (`lowerNativeEnumFromBlock`) turns the variadic entries into ordered\n * members and rejects a bare (value-less) member.\n */\n native_enum: {\n kind: 'pslBlock',\n keyword: 'native_enum',\n discriminator: 'native_enum',\n name: { required: true },\n parameters: {},\n variadicParameters: true,\n },\n} as const satisfies AuthoringPslBlockDescriptorNamespace;\n\n/**\n * `@@` model attributes contributed by the Postgres target pack.\n *\n * `@@rls` is argument-less: presence marks the model's table\n * RLS-controlled. It lowers to a {@link PostgresRlsEnablement} marker in\n * the namespace's `entries.rls`, keyed by the model's table name — the\n * marker (never the policy set) is what drives\n * `ENABLE`/`DISABLE ROW LEVEL SECURITY` planning.\n */\nexport const postgresAuthoringModelAttributes = {\n rls: {\n kind: 'modelAttribute',\n attribute: 'rls',\n spec: modelAttribute('rls', {}),\n lower: (_parsed: Record<never, never>, ctx: AuthoringModelAttributeContext) => ({\n key: ctx.storageName,\n entity: new PostgresRlsEnablement({\n tableName: ctx.storageName,\n namespaceId: ctx.namespaceId,\n }),\n }),\n },\n} as const satisfies AuthoringModelAttributeDescriptorNamespace;\n\nexport const postgresAuthoringFieldPresets = {\n text: {\n kind: 'fieldPreset',\n output: {\n codecId: 'pg/text@1',\n nativeType: 'text',\n },\n },\n int: {\n kind: 'fieldPreset',\n output: {\n codecId: 'pg/int4@1',\n nativeType: 'int4',\n },\n },\n bigint: {\n kind: 'fieldPreset',\n output: {\n codecId: 'pg/int8@1',\n nativeType: 'int8',\n },\n },\n float: {\n kind: 'fieldPreset',\n output: {\n codecId: 'pg/float8@1',\n nativeType: 'float8',\n },\n },\n decimal: {\n kind: 'fieldPreset',\n output: {\n codecId: 'pg/numeric@1',\n nativeType: 'numeric',\n },\n },\n boolean: {\n kind: 'fieldPreset',\n output: {\n codecId: 'pg/bool@1',\n nativeType: 'bool',\n },\n },\n json: {\n kind: 'fieldPreset',\n output: {\n codecId: 'pg/jsonb@1',\n nativeType: 'jsonb',\n },\n },\n bytes: {\n kind: 'fieldPreset',\n output: {\n codecId: 'pg/bytea@1',\n nativeType: 'bytea',\n },\n },\n dateTime: {\n kind: 'fieldPreset',\n output: {\n codecId: 'pg/timestamptz@1',\n nativeType: 'timestamptz',\n },\n },\n temporal: /* @__PURE__ */ temporalAuthoringPresets({\n codecId: 'pg/timestamptz@1',\n nativeType: 'timestamptz',\n }),\n uuidNative: {\n kind: 'fieldPreset',\n output: {\n codecId: 'pg/uuid@1',\n nativeType: 'uuid',\n },\n },\n id: {\n uuidv4Native: {\n kind: 'fieldPreset',\n output: {\n codecId: 'pg/uuid@1',\n nativeType: 'uuid',\n executionDefaults: {\n onCreate: {\n kind: 'generator',\n id: 'uuidv4',\n },\n },\n id: true,\n },\n },\n uuidv7Native: {\n kind: 'fieldPreset',\n output: {\n codecId: 'pg/uuid@1',\n nativeType: 'uuid',\n executionDefaults: {\n onCreate: {\n kind: 'generator',\n id: 'uuidv7',\n },\n },\n id: true,\n },\n },\n },\n} as const satisfies AuthoringFieldNamespace;\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"diff-database-schema-BOsDnH64.mjs","names":[],"sources":["../src/core/migrations/contract-to-postgres-database-schema-node.ts","../src/core/migrations/diff-database-schema.ts"],"sourcesContent":["import type { ContractToSchemaIROptions } from '@prisma-next/family-sql/control';\nimport { contractNamespaceToSchemaIR } from '@prisma-next/family-sql/control';\nimport { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';\nimport { SqlForeignKeyIR } from '@prisma-next/sql-schema-ir/types';\nimport { ifDefined } from '@prisma-next/utils/defined';\nimport type { PostgresRlsPolicy } from '../postgres-rls-policy';\nimport type { PostgresContract } from '../postgres-schema';\nimport { isPostgresSchema } from '../postgres-schema';\nimport { PostgresDatabaseSchemaNode } from '../schema-ir/postgres-database-schema-node';\nimport { PostgresNamespaceSchemaNode } from '../schema-ir/postgres-namespace-schema-node';\nimport { PostgresPolicySchemaNode } from '../schema-ir/postgres-policy-schema-node';\nimport { PostgresRoleSchemaNode } from '../schema-ir/postgres-role-schema-node';\nimport { PostgresTableSchemaNode } from '../schema-ir/postgres-table-schema-node';\nimport { resolveDdlSchemaForNamespaceStorage } from './resolve-ddl-schema';\n\nfunction toPolicyNode(policy: PostgresRlsPolicy, namespaceId: string): PostgresPolicySchemaNode {\n return new PostgresPolicySchemaNode({\n name: policy.name,\n prefix: policy.prefix,\n tableName: policy.tableName,\n namespaceId,\n operation: policy.operation,\n roles: [...policy.roles],\n ...ifDefined('using', policy.using),\n ...ifDefined('withCheck', policy.withCheck),\n permissive: policy.permissive,\n });\n}\n\n/**\n * Projects a Postgres contract into the expected schema-diff tree: a\n * `PostgresDatabaseSchemaNode` root holding one `PostgresNamespaceSchemaNode`\n * per Postgres namespace, each holding its `PostgresTableSchemaNode`s with\n * their `PostgresPolicySchemaNode`s, plus the database roles on the root.\n *\n * Not a duplicate of the family's `contractToSchemaIR`: that builds a flat,\n * single `{ tables }` map (and throws on cross-namespace name collisions, with\n * no RLS/role concept) for SQLite's single-schema world. This is the\n * Postgres-specific *tree* shape — multi-schema, RLS-policy-aware, role-aware.\n * It reuses the family's per-namespace table conversion (`contractNamespaceToSchemaIR`)\n * for column/FK/index building and only adds the Postgres tree/policy/role shape.\n *\n * Tables are grouped by their owning namespace (resolved DDL schema name) so\n * the tree mirrors Postgres's object hierarchy. The DDL schema name is\n * resolved once per namespace.\n *\n * A policy that references a table absent from its namespace is a malformed\n * contract — the loop throws rather than fabricating a stub table.\n */\nexport function contractToPostgresDatabaseSchemaNode(\n contract: PostgresContract | null,\n options: ContractToSchemaIROptions,\n): PostgresDatabaseSchemaNode {\n if (contract === null) {\n return new PostgresDatabaseSchemaNode({\n namespaces: {},\n roles: [],\n existingSchemas: [],\n pgVersion: '',\n });\n }\n\n const namespaces: Record<string, PostgresNamespaceSchemaNode> = {};\n const roles: PostgresRoleSchemaNode[] = [];\n const ownedSchemas: string[] = [];\n\n for (const ns of Object.values(contract.storage.namespaces)) {\n if (!isPostgresSchema(ns)) continue;\n const ddlSchema = resolveDdlSchemaForNamespaceStorage(contract.storage, ns.id);\n ownedSchemas.push(ddlSchema);\n\n // Convert only THIS namespace's tables (passing the full storage for\n // type/value-set/enum resolution that spans namespaces), so the same table\n // name can exist in two schemas without colliding in a bare-keyed record.\n const sqlTables = contractNamespaceToSchemaIR(contract.storage, ns.id, options).tables;\n\n const policiesByTable = new Map<string, PostgresPolicySchemaNode[]>();\n for (const policy of Object.values(ns.policy)) {\n const list = policiesByTable.get(policy.tableName) ?? [];\n list.push(toPolicyNode(policy, ddlSchema));\n policiesByTable.set(policy.tableName, list);\n }\n\n const tables: Record<string, PostgresTableSchemaNode> = {};\n for (const tableName of Object.keys(ns.table)) {\n const sqlTable = sqlTables[tableName];\n if (sqlTable === undefined) continue;\n // The family conversion stamps `referencedSchema` only for bound FK\n // targets; an absent value means the FK targets the unbound namespace.\n // Postgres restores its own coordinate for that slot (the unbound\n // singleton's id) so the raw coordinate keeps qualifying REFERENCES\n // clauses, and resolves the real live DDL schema — introspected FKs\n // already carry the live schema, so this is what lets an expected FK\n // pair (by diff-node id) with its introspected counterpart.\n const foreignKeys = sqlTable.foreignKeys.map(\n (fk) =>\n new SqlForeignKeyIR({\n columns: fk.columns,\n referencedTable: fk.referencedTable,\n referencedColumns: fk.referencedColumns,\n referencedSchema: fk.referencedSchema ?? UNBOUND_NAMESPACE_ID,\n ...ifDefined('name', fk.name),\n ...ifDefined('onDelete', fk.onDelete),\n ...ifDefined('onUpdate', fk.onUpdate),\n ...ifDefined('annotations', fk.annotations),\n resolvedReferencedNamespace: resolveDdlSchemaForNamespaceStorage(\n contract.storage,\n fk.referencedSchema ?? UNBOUND_NAMESPACE_ID,\n ),\n }),\n );\n tables[tableName] = new PostgresTableSchemaNode({\n name: sqlTable.name,\n columns: sqlTable.columns,\n foreignKeys,\n uniques: sqlTable.uniques,\n indexes: sqlTable.indexes,\n ...ifDefined('primaryKey', sqlTable.primaryKey),\n ...ifDefined('annotations', sqlTable.annotations),\n ...ifDefined('checks', sqlTable.checks),\n policies: policiesByTable.get(tableName) ?? [],\n // Marker-driven, never derived from the policy set: the `rls` entry\n // is the single authored source of enablement.\n rlsEnabled: Object.hasOwn(ns.rls, tableName),\n });\n }\n\n for (const [tableName, tablePolicies] of policiesByTable) {\n if (!(tableName in tables)) {\n const policyName = tablePolicies[0]?.name ?? '(unknown)';\n throw new Error(\n `contract-to-postgres-database-schema-node: policy \"${policyName}\" references table \"${tableName}\" not present in namespace \"${ddlSchema}\"`,\n );\n }\n if (!Object.hasOwn(ns.rls, tableName)) {\n const policyPrefix = tablePolicies[0]?.prefix ?? '(unknown)';\n throw new Error(\n `contract-to-postgres-database-schema-node: policy \"${policyPrefix}\" targets table \"${tableName}\" in namespace \"${ddlSchema}\", which is not RLS-controlled. Mark the model with @@rls (entries.rls[\"${tableName}\"]) or remove the policy.`,\n );\n }\n }\n\n namespaces[ddlSchema] = new PostgresNamespaceSchemaNode({\n schemaName: ddlSchema,\n tables,\n nativeEnumTypeNames: [],\n });\n\n for (const role of Object.values(ns.role)) {\n roles.push(new PostgresRoleSchemaNode({ name: role.name, namespaceId: role.namespaceId }));\n }\n }\n\n return new PostgresDatabaseSchemaNode({\n namespaces,\n roles,\n existingSchemas: ownedSchemas,\n pgVersion: '',\n });\n}\n","import type { Contract, ControlPolicy } from '@prisma-next/contract/types';\nimport type { SqlSchemaDiffResult } from '@prisma-next/family-sql/control';\nimport { buildNativeTypeExpander } from '@prisma-next/family-sql/control';\nimport { classifyDiffSubjectGranularity } from '@prisma-next/family-sql/diff';\nimport type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';\nimport type { SchemaDiffIssue } from '@prisma-next/framework-components/control';\nimport { diffSchemas } from '@prisma-next/framework-components/control';\nimport type { SqlStorage } from '@prisma-next/sql-contract/types';\nimport type { SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';\nimport { blindCast } from '@prisma-next/utils/casts';\nimport { ifDefined } from '@prisma-next/utils/defined';\nimport type { PostgresContract } from '../postgres-schema';\nimport { PostgresDatabaseSchemaNode } from '../schema-ir/postgres-database-schema-node';\nimport { PostgresNamespaceSchemaNode } from '../schema-ir/postgres-namespace-schema-node';\nimport {\n postgresDiffSubjectGranularity,\n type SqlSchemaDiffNode,\n} from '../schema-ir/schema-node-kinds';\nimport { contractToPostgresDatabaseSchemaNode } from './contract-to-postgres-database-schema-node';\nimport { resolvePostgresNodeIssueControlPolicySubject } from './control-policy';\n\nfunction ownedSchemaNames(expected: PostgresDatabaseSchemaNode): ReadonlySet<string> {\n const policyNamespaces = Object.values(expected.namespaces).flatMap((ns) =>\n Object.values(ns.tables).flatMap((t) => t.policies.map((p) => p.namespaceId)),\n );\n return new Set([...policyNamespaces, ...expected.existingSchemas]);\n}\n\n/**\n * Drops contract namespaces that declare no tables from the verdict-diff\n * expected tree and the relational owned-schema set. The legacy relational\n * walk skipped a table-less namespace (e.g. an enums-only schema) before\n * pairing, so neither its DDL schema's absence nor that schema's live\n * relational contents ever reached the verdict — the pruned tree\n * reproduces that. The prune loses no expected policies: policies attach\n * to tables, so a table-less namespace carries none (the projection\n * throws on a policy referencing an absent table). Live policies in a\n * pruned schema remain governed via the full owned set (see\n * {@link diffPostgresSchema}).\n */\nfunction pruneTableLessNamespaces(\n expected: PostgresDatabaseSchemaNode,\n): PostgresDatabaseSchemaNode {\n const namespaces = Object.fromEntries(\n Object.entries(expected.namespaces).filter(([, ns]) => Object.keys(ns.tables).length > 0),\n );\n return new PostgresDatabaseSchemaNode({\n namespaces,\n roles: [...expected.roles],\n existingSchemas: expected.existingSchemas.filter((s) => namespaces[s] !== undefined),\n pgVersion: expected.pgVersion,\n });\n}\n\n/**\n * Resolves a verdict-diff issue's subject table's declared control policy\n * directly from the contract, by delegating to the same node-typed resolver\n * ({@link resolvePostgresNodeIssueControlPolicySubject}) the planner uses to\n * gate DDL calls. `undefined` when the issue resolves to no contract table.\n */\nfunction resolveControlPolicy(\n issue: SchemaDiffIssue,\n contract: Contract<SqlStorage>,\n): ControlPolicy | undefined {\n const nodeIssue = blindCast<\n SchemaDiffIssue<SqlSchemaDiffNode>,\n 'every node in a Postgres schema diff tree is a SqlSchemaDiffNode'\n >(issue);\n return resolvePostgresNodeIssueControlPolicySubject(nodeIssue, contract)\n ?.explicitNodeControlPolicy;\n}\n\n/**\n * The Postgres full-tree node diff for the family verify verdict: derive\n * the expected tree (resolved leaf values, expander threaded, FK schemas\n * resolved, table-less namespaces pruned), run the generic\n * differ over the trees as derived, and scope out `not-expected` findings under namespaces the\n * contract does not own. Ownership is role-aware, mirroring the legacy\n * decomposition: relational extras check the PRUNED owned set (the legacy\n * per-namespace walk never visited a table-less namespace, so its live\n * relational contents are invisible), while `structural` extras (RLS\n * policies) check the FULL owned set (the legacy policy diff governed\n * every contract schema regardless of tables — RLS governance does not\n * shrink because a namespace declares no tables). The codec `verifyType`\n * hooks run once per contract namespace with tables against that\n * namespace's paired actual node (the hooks read namespace-scoped state\n * such as `nativeEnumTypeNames`).\n */\nexport function diffPostgresSchema(input: {\n readonly contract: Contract<SqlStorage>;\n readonly schema: SqlSchemaIRNode;\n readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;\n}): SqlSchemaDiffResult {\n const postgresContract = blindCast<\n PostgresContract,\n 'diffPostgresSchema is only called with a postgres contract'\n >(input.contract);\n PostgresDatabaseSchemaNode.assert(input.schema);\n const actual = input.schema;\n const expandNativeType = buildNativeTypeExpander(input.frameworkComponents);\n const fullExpected = contractToPostgresDatabaseSchemaNode(postgresContract, {\n annotationNamespace: 'pg',\n ...ifDefined('expandNativeType', expandNativeType),\n });\n const expected = pruneTableLessNamespaces(fullExpected);\n const relationalOwned = ownedSchemaNames(expected);\n const structuralOwned = ownedSchemaNames(fullExpected);\n const issues = diffSchemas(expected, actual).filter((issue) => {\n if (issue.reason !== 'not-expected') return true;\n const namespaceSegment = issue.path[1];\n if (namespaceSegment === undefined) return true;\n const granularity = classifyDiffSubjectGranularity(issue, postgresDiffSubjectGranularity);\n const owned = granularity === 'structural' ? structuralOwned : relationalOwned;\n return owned.has(namespaceSegment);\n });\n const namespacePairs = Object.values(expected.namespaces).map((ns) => ({\n actual: actual.namespaces[ns.schemaName],\n }));\n return {\n issues,\n resolveControlPolicy: (issue) => resolveControlPolicy(issue, postgresContract),\n namespacePairs,\n };\n}\n\n/**\n * Adds an empty namespace node to the actual tree for every expected namespace\n * absent from it. The relational plan diff pairs on namespace: a contract\n * namespace whose live schema does not exist yet must surface each of its\n * tables as `not-found` (→ `CREATE TABLE`), NOT as a single namespace\n * `not-found` that subtree-coalescing would collapse (leaving `CREATE SCHEMA`\n * with no tables). Padding makes the namespaces pair, so only table/column/\n * policy drift surfaces; `CREATE SCHEMA` comes separately from the synthesized\n * namespace-presence stitch (`verifyPostgresNamespacePresence`), never from the\n * tree diff — matching the retired per-namespace-paired relational walk, which\n * paired a missing schema against an empty namespace node.\n */\nfunction padActualNamespaces(\n expected: PostgresDatabaseSchemaNode,\n actual: PostgresDatabaseSchemaNode,\n): PostgresDatabaseSchemaNode {\n const namespaces: Record<string, PostgresNamespaceSchemaNode> = { ...actual.namespaces };\n let padded = false;\n for (const schemaName of Object.keys(expected.namespaces)) {\n if (namespaces[schemaName] === undefined) {\n namespaces[schemaName] = new PostgresNamespaceSchemaNode({\n schemaName,\n tables: {},\n nativeEnumTypeNames: [],\n });\n padded = true;\n }\n }\n if (!padded) return actual;\n return new PostgresDatabaseSchemaNode({\n namespaces,\n roles: [...actual.roles],\n existingSchemas: [...actual.existingSchemas],\n pgVersion: actual.pgVersion,\n });\n}\n\nexport interface PostgresPlanDiff {\n /** The desired (\"end\") tree — resolved leaf values (incl. `codecRef`) on every column, table-less namespaces pruned. */\n readonly expected: PostgresDatabaseSchemaNode;\n /** The live (\"start\") tree, padded with empty namespaces so a missing schema's tables pair. */\n readonly actual: PostgresDatabaseSchemaNode;\n /** The one node diff over the two trees: relational + policy drift, role-aware ownership filtered. */\n readonly issues: readonly SchemaDiffIssue<SqlSchemaDiffNode>[];\n}\n\n/**\n * The Postgres planner's diff input: the SAME tree-building\n * `diffPostgresSchema` uses (expander threaded, FK schemas resolved,\n * table-less namespaces pruned, role-aware ownership filter) plus actual\n * namespace padding (so a missing\n * schema's tables surface as `not-found` instead of a swallowed namespace\n * `not-found`). One differ drives both verify and plan; this is the\n * plan-side derivation. The single issue list covers tables / columns /\n * constraints / indexes / defaults AND policies — the caller splits it\n * (relational → `mapNodeIssueToCall`; policy → RLS ops) and stitches in\n * `CREATE SCHEMA` separately.\n */\nexport function buildPostgresPlanDiff(input: {\n readonly contract: Contract<SqlStorage>;\n readonly actualSchema: SqlSchemaIRNode;\n readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;\n}): PostgresPlanDiff {\n const postgresContract = blindCast<\n PostgresContract,\n 'buildPostgresPlanDiff is only called with a postgres contract'\n >(input.contract);\n PostgresDatabaseSchemaNode.assert(input.actualSchema);\n const actual = input.actualSchema;\n const expandNativeType = buildNativeTypeExpander(input.frameworkComponents);\n const projectionOptions = {\n annotationNamespace: 'pg',\n ...ifDefined('expandNativeType', expandNativeType),\n };\n const fullExpected = contractToPostgresDatabaseSchemaNode(postgresContract, projectionOptions);\n const expected = pruneTableLessNamespaces(fullExpected);\n const paddedActual = padActualNamespaces(expected, actual);\n const relationalOwned = ownedSchemaNames(expected);\n const structuralOwned = ownedSchemaNames(fullExpected);\n const issues = blindCast<\n readonly SchemaDiffIssue<SqlSchemaDiffNode>[],\n 'both trees are PostgresDatabaseSchemaNodes, so every diff-issue node is a SqlSchemaDiffNode'\n >(diffSchemas(expected, paddedActual)).filter((issue) => {\n if (issue.reason !== 'not-expected') return true;\n const namespaceSegment = issue.path[1];\n if (namespaceSegment === undefined) return true;\n const granularity = classifyDiffSubjectGranularity(issue, postgresDiffSubjectGranularity);\n const owned = granularity === 'structural' ? structuralOwned : relationalOwned;\n return owned.has(namespaceSegment);\n });\n return { expected, actual: paddedActual, issues };\n}\n"],"mappings":";;;;;;;;;;;;;AAeA,SAAS,aAAa,QAA2B,aAA+C;CAC9F,OAAO,IAAI,yBAAyB;EAClC,MAAM,OAAO;EACb,QAAQ,OAAO;EACf,WAAW,OAAO;EAClB;EACA,WAAW,OAAO;EAClB,OAAO,CAAC,GAAG,OAAO,KAAK;EACvB,GAAG,UAAU,SAAS,OAAO,KAAK;EAClC,GAAG,UAAU,aAAa,OAAO,SAAS;EAC1C,YAAY,OAAO;CACrB,CAAC;AACH;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,qCACd,UACA,SAC4B;CAC5B,IAAI,aAAa,MACf,OAAO,IAAI,2BAA2B;EACpC,YAAY,CAAC;EACb,OAAO,CAAC;EACR,iBAAiB,CAAC;EAClB,WAAW;CACb,CAAC;CAGH,MAAM,aAA0D,CAAC;CACjE,MAAM,QAAkC,CAAC;CACzC,MAAM,eAAyB,CAAC;CAEhC,KAAK,MAAM,MAAM,OAAO,OAAO,SAAS,QAAQ,UAAU,GAAG;EAC3D,IAAI,CAAC,iBAAiB,EAAE,GAAG;EAC3B,MAAM,YAAY,oCAAoC,SAAS,SAAS,GAAG,EAAE;EAC7E,aAAa,KAAK,SAAS;EAK3B,MAAM,YAAY,4BAA4B,SAAS,SAAS,GAAG,IAAI,OAAO,CAAC,CAAC;EAEhF,MAAM,kCAAkB,IAAI,IAAwC;EACpE,KAAK,MAAM,UAAU,OAAO,OAAO,GAAG,MAAM,GAAG;GAC7C,MAAM,OAAO,gBAAgB,IAAI,OAAO,SAAS,KAAK,CAAC;GACvD,KAAK,KAAK,aAAa,QAAQ,SAAS,CAAC;GACzC,gBAAgB,IAAI,OAAO,WAAW,IAAI;EAC5C;EAEA,MAAM,SAAkD,CAAC;EACzD,KAAK,MAAM,aAAa,OAAO,KAAK,GAAG,KAAK,GAAG;GAC7C,MAAM,WAAW,UAAU;GAC3B,IAAI,aAAa,KAAA,GAAW;GAQ5B,MAAM,cAAc,SAAS,YAAY,KACtC,OACC,IAAI,gBAAgB;IAClB,SAAS,GAAG;IACZ,iBAAiB,GAAG;IACpB,mBAAmB,GAAG;IACtB,kBAAkB,GAAG,oBAAoB;IACzC,GAAG,UAAU,QAAQ,GAAG,IAAI;IAC5B,GAAG,UAAU,YAAY,GAAG,QAAQ;IACpC,GAAG,UAAU,YAAY,GAAG,QAAQ;IACpC,GAAG,UAAU,eAAe,GAAG,WAAW;IAC1C,6BAA6B,oCAC3B,SAAS,SACT,GAAG,oBAAoB,oBACzB;GACF,CAAC,CACL;GACA,OAAO,aAAa,IAAI,wBAAwB;IAC9C,MAAM,SAAS;IACf,SAAS,SAAS;IAClB;IACA,SAAS,SAAS;IAClB,SAAS,SAAS;IAClB,GAAG,UAAU,cAAc,SAAS,UAAU;IAC9C,GAAG,UAAU,eAAe,SAAS,WAAW;IAChD,GAAG,UAAU,UAAU,SAAS,MAAM;IACtC,UAAU,gBAAgB,IAAI,SAAS,KAAK,CAAC;IAG7C,YAAY,OAAO,OAAO,GAAG,KAAK,SAAS;GAC7C,CAAC;EACH;EAEA,KAAK,MAAM,CAAC,WAAW,kBAAkB,iBAAiB;GACxD,IAAI,EAAE,aAAa,SAAS;IAC1B,MAAM,aAAa,cAAc,EAAE,EAAE,QAAQ;IAC7C,MAAM,IAAI,MACR,sDAAsD,WAAW,sBAAsB,UAAU,8BAA8B,UAAU,EAC3I;GACF;GACA,IAAI,CAAC,OAAO,OAAO,GAAG,KAAK,SAAS,GAAG;IACrC,MAAM,eAAe,cAAc,EAAE,EAAE,UAAU;IACjD,MAAM,IAAI,MACR,sDAAsD,aAAa,mBAAmB,UAAU,kBAAkB,UAAU,0EAA0E,UAAU,0BAClN;GACF;EACF;EAEA,WAAW,aAAa,IAAI,4BAA4B;GACtD,YAAY;GACZ;GACA,qBAAqB,CAAC;EACxB,CAAC;EAED,KAAK,MAAM,QAAQ,OAAO,OAAO,GAAG,IAAI,GACtC,MAAM,KAAK,IAAI,uBAAuB;GAAE,MAAM,KAAK;GAAM,aAAa,KAAK;EAAY,CAAC,CAAC;CAE7F;CAEA,OAAO,IAAI,2BAA2B;EACpC;EACA;EACA,iBAAiB;EACjB,WAAW;CACb,CAAC;AACH;;;AC1IA,SAAS,iBAAiB,UAA2D;CACnF,MAAM,mBAAmB,OAAO,OAAO,SAAS,UAAU,CAAC,CAAC,SAAS,OACnE,OAAO,OAAO,GAAG,MAAM,CAAC,CAAC,SAAS,MAAM,EAAE,SAAS,KAAK,MAAM,EAAE,WAAW,CAAC,CAC9E;CACA,OAAO,IAAI,IAAI,CAAC,GAAG,kBAAkB,GAAG,SAAS,eAAe,CAAC;AACnE;;;;;;;;;;;;;AAcA,SAAS,yBACP,UAC4B;CAC5B,MAAM,aAAa,OAAO,YACxB,OAAO,QAAQ,SAAS,UAAU,CAAC,CAAC,QAAQ,GAAG,QAAQ,OAAO,KAAK,GAAG,MAAM,CAAC,CAAC,SAAS,CAAC,CAC1F;CACA,OAAO,IAAI,2BAA2B;EACpC;EACA,OAAO,CAAC,GAAG,SAAS,KAAK;EACzB,iBAAiB,SAAS,gBAAgB,QAAQ,MAAM,WAAW,OAAO,KAAA,CAAS;EACnF,WAAW,SAAS;CACtB,CAAC;AACH;;;;;;;AAQA,SAAS,qBACP,OACA,UAC2B;CAK3B,OAAO,6CAJW,UAGhB,KAC0D,GAAG,QAAQ,CAAC,EACpE;AACN;;;;;;;;;;;;;;;;;AAkBA,SAAgB,mBAAmB,OAIX;CACtB,MAAM,mBAAmB,UAGvB,MAAM,QAAQ;CAChB,2BAA2B,OAAO,MAAM,MAAM;CAC9C,MAAM,SAAS,MAAM;CAErB,MAAM,eAAe,qCAAqC,kBAAkB;EAC1E,qBAAqB;EACrB,GAAG,UAAU,oBAHU,wBAAwB,MAAM,mBAGL,CAAC;CACnD,CAAC;CACD,MAAM,WAAW,yBAAyB,YAAY;CACtD,MAAM,kBAAkB,iBAAiB,QAAQ;CACjD,MAAM,kBAAkB,iBAAiB,YAAY;CAYrD,OAAO;EACL,QAZa,YAAY,UAAU,MAAM,CAAC,CAAC,QAAQ,UAAU;GAC7D,IAAI,MAAM,WAAW,gBAAgB,OAAO;GAC5C,MAAM,mBAAmB,MAAM,KAAK;GACpC,IAAI,qBAAqB,KAAA,GAAW,OAAO;GAG3C,QAFoB,+BAA+B,OAAO,8BAClC,MAAM,eAAe,kBAAkB,gBAAA,CAClD,IAAI,gBAAgB;EACnC,CAKO;EACL,uBAAuB,UAAU,qBAAqB,OAAO,gBAAgB;EAC7E,gBANqB,OAAO,OAAO,SAAS,UAAU,CAAC,CAAC,KAAK,QAAQ,EACrE,QAAQ,OAAO,WAAW,GAAG,YAC/B,EAIe;CACf;AACF;;;;;;;;;;;;;AAcA,SAAS,oBACP,UACA,QAC4B;CAC5B,MAAM,aAA0D,EAAE,GAAG,OAAO,WAAW;CACvF,IAAI,SAAS;CACb,KAAK,MAAM,cAAc,OAAO,KAAK,SAAS,UAAU,GACtD,IAAI,WAAW,gBAAgB,KAAA,GAAW;EACxC,WAAW,cAAc,IAAI,4BAA4B;GACvD;GACA,QAAQ,CAAC;GACT,qBAAqB,CAAC;EACxB,CAAC;EACD,SAAS;CACX;CAEF,IAAI,CAAC,QAAQ,OAAO;CACpB,OAAO,IAAI,2BAA2B;EACpC;EACA,OAAO,CAAC,GAAG,OAAO,KAAK;EACvB,iBAAiB,CAAC,GAAG,OAAO,eAAe;EAC3C,WAAW,OAAO;CACpB,CAAC;AACH;;;;;;;;;;;;;AAuBA,SAAgB,sBAAsB,OAIjB;CACnB,MAAM,mBAAmB,UAGvB,MAAM,QAAQ;CAChB,2BAA2B,OAAO,MAAM,YAAY;CACpD,MAAM,SAAS,MAAM;CAMrB,MAAM,eAAe,qCAAqC,kBAAkB;EAH1E,qBAAqB;EACrB,GAAG,UAAU,oBAHU,wBAAwB,MAAM,mBAGL,CAAC;CAEyC,CAAC;CAC7F,MAAM,WAAW,yBAAyB,YAAY;CACtD,MAAM,eAAe,oBAAoB,UAAU,MAAM;CACzD,MAAM,kBAAkB,iBAAiB,QAAQ;CACjD,MAAM,kBAAkB,iBAAiB,YAAY;CAYrD,OAAO;EAAE;EAAU,QAAQ;EAAc,QAX1B,UAGb,YAAY,UAAU,YAAY,CAAC,CAAC,CAAC,QAAQ,UAAU;GACvD,IAAI,MAAM,WAAW,gBAAgB,OAAO;GAC5C,MAAM,mBAAmB,MAAM,KAAK;GACpC,IAAI,qBAAqB,KAAA,GAAW,OAAO;GAG3C,QAFoB,+BAA+B,OAAO,8BAClC,MAAM,eAAe,kBAAkB,gBAAA,CAClD,IAAI,gBAAgB;EACnC,CAC8C;CAAE;AAClD"}