From acd8ff341ec4e88b39a2084c4e9194af07efb4e0 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Mon, 20 Apr 2026 12:18:43 +0000 Subject: [PATCH] feat!: unify grants[] and policies[] in node type registry and export config Replace old grant_roles/grant_privileges/policy_* fields with unified: - grants[]: array of { roles: string[], privileges: unknown[] } objects - policies[]: array of { $type, data, privileges, policy_role, permissive } objects Updated files: - generate-types.ts: BlueprintEntityTableProvision + BlueprintTable - relation-many-to-many.ts: parameter schema for junction table - export-utils.ts: secure_table_provision column types - export.test.ts: test data + snapshots Companion to constructive-db PR #929 (grants[]) and PR #924 (policies[]). --- .../src/blueprint-types.generated.ts | 50 ++++++++--------- .../src/codegen/generate-types.ts | 32 ++++++----- .../src/relation/relation-many-to-many.ts | 53 +++++++------------ .../__snapshots__/export.test.ts.snap | 5 +- packages/csv-to-pg/__tests__/export.test.ts | 6 +-- pgpm/export/src/export-utils.ts | 9 +--- 6 files changed, 72 insertions(+), 83 deletions(-) diff --git a/graphql/node-type-registry/src/blueprint-types.generated.ts b/graphql/node-type-registry/src/blueprint-types.generated.ts index 9c76ad327..7d288ab5a 100644 --- a/graphql/node-type-registry/src/blueprint-types.generated.ts +++ b/graphql/node-type-registry/src/blueprint-types.generated.ts @@ -610,22 +610,22 @@ export interface RelationManyToManyParams { nodes?: { [key: string]: unknown; }[]; - /* Database roles to grant privileges to. Forwarded to secure_table_provision as-is. Default: [authenticated] */ - grant_roles?: string[]; - /* Privilege grants for the junction table as [verb, columns] tuples (e.g. [['select','*'],['insert','*']]). Forwarded to secure_table_provision as-is. Default: select/insert/delete for all columns */ - grant_privileges?: string[][]; - /* RLS policy type for the junction table. Forwarded to secure_table_provision as-is. NULL means no policy. */ - policy_type?: string; - /* Privileges the policy applies to. Forwarded to secure_table_provision as-is. NULL means derived from grant_privileges verbs. */ - policy_privileges?: string[]; - /* Database role the policy targets. Forwarded to secure_table_provision as-is. NULL means falls back to first grant_role. */ - policy_role?: string; - /* Whether the policy is PERMISSIVE (true) or RESTRICTIVE (false). Forwarded to secure_table_provision as-is. */ - policy_permissive?: boolean; - /* Policy configuration forwarded to secure_table_provision as-is. Structure varies by policy_type. */ - policy_data?: { - [key: string]: unknown; - }; + /* Unified grant objects for the junction table. Each entry is { roles: string[], privileges: string[][] }. Forwarded to secure_table_provision as-is. Default: [] */ + grants?: { + roles: string[]; + privileges: string[][]; + }[]; + /* RLS policy objects for the junction table. Each entry has $type (Authz* generator), optional data, privileges, policy_role, permissive, policy_name. Forwarded to secure_table_provision as-is. Default: [] */ + policies?: { + $type: string; + data?: { + [key: string]: unknown; + }; + privileges?: string[]; + policy_role?: string; + permissive?: boolean; + policy_name?: string; + }[]; } /** Declares a spatial predicate between two existing geometry/geography columns. Inserts a metaschema_public.spatial_relation row; the sync_spatial_relation_tags trigger then projects a @spatialRelation smart tag onto the owner column so graphile-postgis' PostgisSpatialRelationsPlugin can expose it as a cross-table filter in GraphQL. Metadata-only: both source_field and target_field must already exist on their tables. Idempotent on (source_table_id, name). One direction per tag — author two RelationSpatial entries if symmetry is desired. */ export interface RelationSpatialParams { @@ -838,10 +838,11 @@ export interface BlueprintEntityTableProvision { nodes?: BlueprintNode[]; /** Custom fields (columns) to add to the entity table. Forwarded to secure_table_provision as-is. */ fields?: BlueprintField[]; - /** Privilege grants for the entity table as [verb, columns] tuples (e.g. [["select","*"],["insert","*"]]). Forwarded to secure_table_provision as-is. */ - grant_privileges?: unknown[]; - /** Database roles to grant privileges to. Forwarded to secure_table_provision as-is. Defaults to ["authenticated"]. */ - grant_roles?: string[]; + /** Unified grant objects for the entity table. Each entry is { roles: string[], privileges: unknown[] } where privileges are [verb, columns] tuples. Forwarded to secure_table_provision as-is. Defaults to []. */ + grants?: { + roles: string[]; + privileges: unknown[]; + }[]; /** RLS policies for the entity table. When present, these policies fully replace the five default entity-table policies (is_visible becomes a no-op). */ policies?: BlueprintPolicy[]; } @@ -1075,10 +1076,11 @@ export interface BlueprintTable { fields?: BlueprintField[]; /** RLS policies for this table. */ policies?: BlueprintPolicy[]; - /** Database roles to grant privileges to. Defaults to ["authenticated"]. */ - grant_roles?: string[]; - /** Privilege grants as [verb, column] tuples or objects. Defaults to empty (no grants — callers must explicitly specify). */ - grants?: unknown[]; + /** Unified grant objects. Each entry is { roles: string[], privileges: unknown[] } where privileges are [verb, columns] tuples (e.g. [["select","*"]]). Enables per-role targeting. Defaults to []. */ + grants?: { + roles: string[]; + privileges: unknown[]; + }[]; /** Whether to enable RLS on this table. Defaults to true. */ use_rls?: boolean; /** Table-level indexes (table_name inherited from parent). */ diff --git a/graphql/node-type-registry/src/codegen/generate-types.ts b/graphql/node-type-registry/src/codegen/generate-types.ts index b07e40cf4..890c71bd4 100644 --- a/graphql/node-type-registry/src/codegen/generate-types.ts +++ b/graphql/node-type-registry/src/codegen/generate-types.ts @@ -642,12 +642,16 @@ function buildBlueprintEntityTableProvision(): t.ExportNamedDeclaration { 'Custom fields (columns) to add to the entity table. Forwarded to secure_table_provision as-is.' ), addJSDoc( - optionalProp('grant_privileges', t.tsArrayType(t.tsUnknownKeyword())), - 'Privilege grants for the entity table as [verb, columns] tuples (e.g. [["select","*"],["insert","*"]]). Forwarded to secure_table_provision as-is.' - ), - addJSDoc( - optionalProp('grant_roles', t.tsArrayType(t.tsStringKeyword())), - 'Database roles to grant privileges to. Forwarded to secure_table_provision as-is. Defaults to ["authenticated"].' + optionalProp( + 'grants', + t.tsArrayType( + t.tsTypeLiteral([ + requiredProp('roles', t.tsArrayType(t.tsStringKeyword())), + requiredProp('privileges', t.tsArrayType(t.tsUnknownKeyword())), + ]) + ) + ), + 'Unified grant objects for the entity table. Each entry is { roles: string[], privileges: unknown[] } where privileges are [verb, columns] tuples. Forwarded to secure_table_provision as-is. Defaults to [].' ), addJSDoc( optionalProp( @@ -749,12 +753,16 @@ function buildBlueprintTable(): t.ExportNamedDeclaration { 'RLS policies for this table.' ), addJSDoc( - optionalProp('grant_roles', t.tsArrayType(t.tsStringKeyword())), - 'Database roles to grant privileges to. Defaults to ["authenticated"].' - ), - addJSDoc( - optionalProp('grants', t.tsArrayType(t.tsUnknownKeyword())), - 'Privilege grants as [verb, column] tuples or objects. Defaults to empty (no grants — callers must explicitly specify).' + optionalProp( + 'grants', + t.tsArrayType( + t.tsTypeLiteral([ + requiredProp('roles', t.tsArrayType(t.tsStringKeyword())), + requiredProp('privileges', t.tsArrayType(t.tsUnknownKeyword())), + ]) + ) + ), + 'Unified grant objects. Each entry is { roles: string[], privileges: unknown[] } where privileges are [verb, columns] tuples (e.g. [["select","*"]]). Enables per-role targeting. Defaults to [].' ), addJSDoc( optionalProp('use_rls', t.tsBooleanKeyword()), diff --git a/graphql/node-type-registry/src/relation/relation-many-to-many.ts b/graphql/node-type-registry/src/relation/relation-many-to-many.ts index a084e7618..5f4af8cd0 100644 --- a/graphql/node-type-registry/src/relation/relation-many-to-many.ts +++ b/graphql/node-type-registry/src/relation/relation-many-to-many.ts @@ -48,46 +48,33 @@ export const RelationManyToMany: NodeTypeDefinition = { }, "description": "Array of node objects for field creation on junction table. Each object has a $type key (e.g. DataId, DataEntityMembership) and optional data keys. Forwarded to secure_table_provision as-is. Empty array means no additional fields." }, - "grant_roles": { + "grants": { "type": "array", "items": { - "type": "string" + "type": "object", + "properties": { + "roles": { "type": "array", "items": { "type": "string" } }, + "privileges": { "type": "array", "items": { "type": "array", "items": { "type": "string" } } } + }, + "required": ["roles", "privileges"] }, - "description": "Database roles to grant privileges to. Forwarded to secure_table_provision as-is. Default: [authenticated]" + "description": "Unified grant objects for the junction table. Each entry is { roles: string[], privileges: string[][] }. Forwarded to secure_table_provision as-is. Default: []" }, - "grant_privileges": { + "policies": { "type": "array", "items": { - "type": "array", - "items": { - "type": "string" - } + "type": "object", + "properties": { + "$type": { "type": "string" }, + "data": { "type": "object" }, + "privileges": { "type": "array", "items": { "type": "string" } }, + "policy_role": { "type": "string" }, + "permissive": { "type": "boolean" }, + "policy_name": { "type": "string" } + }, + "required": ["$type"] }, - "description": "Privilege grants for the junction table as [verb, columns] tuples (e.g. [['select','*'],['insert','*']]). Forwarded to secure_table_provision as-is. Default: select/insert/delete for all columns" - }, - "policy_type": { - "type": "string", - "description": "RLS policy type for the junction table. Forwarded to secure_table_provision as-is. NULL means no policy." - }, - "policy_privileges": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Privileges the policy applies to. Forwarded to secure_table_provision as-is. NULL means derived from grant_privileges verbs." - }, - "policy_role": { - "type": "string", - "description": "Database role the policy targets. Forwarded to secure_table_provision as-is. NULL means falls back to first grant_role." - }, - "policy_permissive": { - "type": "boolean", - "description": "Whether the policy is PERMISSIVE (true) or RESTRICTIVE (false). Forwarded to secure_table_provision as-is.", - "default": true - }, - "policy_data": { - "type": "object", - "description": "Policy configuration forwarded to secure_table_provision as-is. Structure varies by policy_type." + "description": "RLS policy objects for the junction table. Each entry has $type (Authz* generator), optional data, privileges, policy_role, permissive, policy_name. Forwarded to secure_table_provision as-is. Default: []" } }, "required": [ diff --git a/packages/csv-to-pg/__tests__/__snapshots__/export.test.ts.snap b/packages/csv-to-pg/__tests__/__snapshots__/export.test.ts.snap index 125b4cdf7..715ddb30a 100644 --- a/packages/csv-to-pg/__tests__/__snapshots__/export.test.ts.snap +++ b/packages/csv-to-pg/__tests__/__snapshots__/export.test.ts.snap @@ -13,7 +13,7 @@ exports[`test case empty array fields emit empty array literal 1`] = ` id, node_type, fields, - grant_roles + out_fields ) VALUES ('450e3b3b-b68d-4abc-990c-65cb8a1dcdb4', 'DataTimestamps', '{}', '{}');" `; @@ -76,10 +76,9 @@ exports[`test case null array fields emit empty array literal instead of NULL 1` id, node_type, fields, - grant_privileges, out_fields ) VALUES - ('450e3b3b-b68d-4abc-990c-65cb8a1dcdb4', 'DataTimestamps', '{}', '{}', '{}');" + ('450e3b3b-b68d-4abc-990c-65cb8a1dcdb4', 'DataTimestamps', '{}', '{}');" `; exports[`test case test case 1`] = `Promise {}`; diff --git a/packages/csv-to-pg/__tests__/export.test.ts b/packages/csv-to-pg/__tests__/export.test.ts index 4c36e6ffb..6366b59ac 100644 --- a/packages/csv-to-pg/__tests__/export.test.ts +++ b/packages/csv-to-pg/__tests__/export.test.ts @@ -229,7 +229,6 @@ it('null array fields emit empty array literal instead of NULL', async () => { id: 'uuid', node_type: 'text', fields: 'jsonb[]', - grant_privileges: 'jsonb[]', out_fields: 'uuid[]' } }); @@ -239,7 +238,6 @@ it('null array fields emit empty array literal instead of NULL', async () => { id: '450e3b3b-b68d-4abc-990c-65cb8a1dcdb4', node_type: 'DataTimestamps', fields: null, - grant_privileges: null, out_fields: null } ]); @@ -259,7 +257,7 @@ it('empty array fields emit empty array literal', async () => { id: 'uuid', node_type: 'text', fields: 'jsonb[]', - grant_roles: 'text[]' + out_fields: 'uuid[]' } }); @@ -268,7 +266,7 @@ it('empty array fields emit empty array literal', async () => { id: '450e3b3b-b68d-4abc-990c-65cb8a1dcdb4', node_type: 'DataTimestamps', fields: [], - grant_roles: [] + out_fields: [] } ]); diff --git a/pgpm/export/src/export-utils.ts b/pgpm/export/src/export-utils.ts index 644a4f343..870d99882 100644 --- a/pgpm/export/src/export-utils.ts +++ b/pgpm/export/src/export-utils.ts @@ -985,14 +985,9 @@ export const META_TABLE_CONFIG: Record = { node_type: 'text', use_rls: 'boolean', node_data: 'jsonb', - grant_roles: 'text[]', fields: 'jsonb[]', - grant_privileges: 'jsonb[]', - policy_type: 'text', - policy_privileges: 'text[]', - policy_role: 'text', - policy_permissive: 'boolean', - policy_data: 'jsonb', + grants: 'jsonb', + policies: 'jsonb', out_fields: 'uuid[]' } },