diff --git a/graphql/node-type-registry/src/authz/authz-entity-membership.ts b/graphql/node-type-registry/src/authz/authz-entity-membership.ts index ef20235b3..745d4435d 100644 --- a/graphql/node-type-registry/src/authz/authz-entity-membership.ts +++ b/graphql/node-type-registry/src/authz/authz-entity-membership.ts @@ -18,7 +18,7 @@ export const AuthzEntityMembership: NodeTypeDefinition = { "integer", "string" ], - "description": "Scope: 1=app, 2=org, 3=group (or string name resolved via membership_types_module)" + "description": "Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)" }, "permission": { "type": "string", diff --git a/graphql/node-type-registry/src/authz/authz-membership-check.ts b/graphql/node-type-registry/src/authz/authz-membership-check.ts index ca4b9adb2..f9a2c8594 100644 --- a/graphql/node-type-registry/src/authz/authz-membership-check.ts +++ b/graphql/node-type-registry/src/authz/authz-membership-check.ts @@ -14,7 +14,7 @@ export const AuthzMembership: NodeTypeDefinition = { "integer", "string" ], - "description": "Scope: 1=app, 2=org, 3=group (or string name resolved via membership_types_module)" + "description": "Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)" }, "permission": { "type": "string", diff --git a/graphql/node-type-registry/src/authz/authz-peer-ownership.ts b/graphql/node-type-registry/src/authz/authz-peer-ownership.ts index d26e91c6b..8d14d32dc 100644 --- a/graphql/node-type-registry/src/authz/authz-peer-ownership.ts +++ b/graphql/node-type-registry/src/authz/authz-peer-ownership.ts @@ -18,7 +18,7 @@ export const AuthzPeerOwnership: NodeTypeDefinition = { "integer", "string" ], - "description": "Scope: 1=app, 2=org, 3=group (or string name resolved via membership_types_module)" + "description": "Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)" }, "permission": { "type": "string", diff --git a/graphql/node-type-registry/src/authz/authz-related-entity-membership.ts b/graphql/node-type-registry/src/authz/authz-related-entity-membership.ts index f8b37db27..335505f78 100644 --- a/graphql/node-type-registry/src/authz/authz-related-entity-membership.ts +++ b/graphql/node-type-registry/src/authz/authz-related-entity-membership.ts @@ -18,7 +18,7 @@ export const AuthzRelatedEntityMembership: NodeTypeDefinition = { "integer", "string" ], - "description": "Scope: 1=app, 2=org, 3=group (or string name resolved via membership_types_module)" + "description": "Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)" }, "obj_table_id": { "type": "string", diff --git a/graphql/node-type-registry/src/authz/authz-related-peer-ownership.ts b/graphql/node-type-registry/src/authz/authz-related-peer-ownership.ts index f5432ed02..84081140c 100644 --- a/graphql/node-type-registry/src/authz/authz-related-peer-ownership.ts +++ b/graphql/node-type-registry/src/authz/authz-related-peer-ownership.ts @@ -18,7 +18,7 @@ export const AuthzRelatedPeerOwnership: NodeTypeDefinition = { "integer", "string" ], - "description": "Scope: 1=app, 2=org, 3=group (or string name resolved via membership_types_module)" + "description": "Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)" }, "obj_table_id": { "type": "string", diff --git a/graphql/node-type-registry/src/blueprint-types.generated.ts b/graphql/node-type-registry/src/blueprint-types.generated.ts index 9339720c7..284f24c21 100644 --- a/graphql/node-type-registry/src/blueprint-types.generated.ts +++ b/graphql/node-type-registry/src/blueprint-types.generated.ts @@ -372,7 +372,7 @@ export interface AuthzDirectOwnerAnyParams { } /** Membership check that verifies the user has membership (optionally with specific permission) without binding to any entity from the row. Uses EXISTS subquery against SPRT table. */ export interface AuthzMembershipParams { - /* Scope: 1=app, 2=org, 3=group (or string name resolved via membership_types_module) */ + /* Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module) */ membership_type: number | string; /* Single permission name to check (resolved to bitstring mask) */ permission?: string; @@ -387,7 +387,7 @@ export interface AuthzMembershipParams { export interface AuthzEntityMembershipParams { /* Column name referencing the entity (e.g., entity_id, org_id) */ entity_field: string; - /* Scope: 1=app, 2=org, 3=group (or string name resolved via membership_types_module) */ + /* Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module) */ membership_type?: number | string; /* Single permission name to check (resolved to bitstring mask) */ permission?: string; @@ -402,7 +402,7 @@ export interface AuthzEntityMembershipParams { export interface AuthzRelatedEntityMembershipParams { /* Column name on protected table referencing the join table */ entity_field: string; - /* Scope: 1=app, 2=org, 3=group (or string name resolved via membership_types_module) */ + /* Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module) */ membership_type?: number | string; /* UUID of the join table (alternative to obj_schema/obj_table) */ obj_table_id?: string; @@ -490,7 +490,7 @@ export interface AuthzCompositeParams { export interface AuthzPeerOwnershipParams { /* Column name on protected table referencing the owning user (e.g., owner_id) */ owner_field: string; - /* Scope: 1=app, 2=org, 3=group (or string name resolved via membership_types_module) */ + /* Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module) */ membership_type?: number | string; /* Single permission name to check on the current user membership (resolved to bitstring mask) */ permission?: string; @@ -505,7 +505,7 @@ export interface AuthzPeerOwnershipParams { export interface AuthzRelatedPeerOwnershipParams { /* Column name on protected table referencing the related table (e.g., message_id) */ entity_field: string; - /* Scope: 1=app, 2=org, 3=group (or string name resolved via membership_types_module) */ + /* Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module) */ membership_type?: number | string; /* UUID of the related table (alternative to obj_schema/obj_table) */ obj_table_id?: string; @@ -796,6 +796,29 @@ export interface BlueprintTableUniqueConstraint { /** Optional schema name override. */ schema_name?: string; } +/** A membership type entry for Phase 0 of construct_blueprint(). Provisions a full entity type with its own entity table, membership modules, and security policies via entity_type_provision. */ +export interface BlueprintMembershipType { + /** Entity type name (e.g., "data_room", "channel", "department"). Must be unique per database. */ + name: string; + /** Short prefix for generated objects (e.g., "dr", "ch", "dept"). Used in table/trigger naming. */ + prefix: string; + /** Human-readable description of this entity type. */ + description?: string; + /** Parent entity type name. Defaults to "org". */ + parent_entity?: string; + /** Custom table name for the entity table. Defaults to name-derived convention. */ + table_name?: string; + /** Whether this entity type is visible in the API. Defaults to true. */ + is_visible?: boolean; + /** Whether to provision a limits module for this entity type. Defaults to false. */ + has_limits?: boolean; + /** Whether to provision a profiles module for this entity type. Defaults to false. */ + has_profiles?: boolean; + /** Whether to provision a levels module for this entity type. Defaults to false. */ + has_levels?: boolean; + /** Whether to skip creating default RLS policies on the entity table. Defaults to false. */ + skip_entity_policies?: boolean; +} /** * =========================================================================== * Node types -- discriminated union for nodes[] entries @@ -1015,4 +1038,6 @@ export interface BlueprintDefinition { full_text_searches?: BlueprintFullTextSearch[]; /** Unique constraints on table columns. */ unique_constraints?: BlueprintUniqueConstraint[]; + /** Entity types to provision in Phase 0 (before tables). Each entry creates an entity table with membership modules and security. */ + membership_types?: BlueprintMembershipType[]; } diff --git a/graphql/node-type-registry/src/codegen/generate-types.ts b/graphql/node-type-registry/src/codegen/generate-types.ts index 7bff7815f..7b4f9aaa5 100644 --- a/graphql/node-type-registry/src/codegen/generate-types.ts +++ b/graphql/node-type-registry/src/codegen/generate-types.ts @@ -599,6 +599,54 @@ function buildBlueprintTableUniqueConstraint(): t.ExportNamedDeclaration { ); } +function buildBlueprintMembershipType(): t.ExportNamedDeclaration { + return addJSDoc( + exportInterface('BlueprintMembershipType', [ + addJSDoc( + requiredProp('name', t.tsStringKeyword()), + 'Entity type name (e.g., "data_room", "channel", "department"). Must be unique per database.' + ), + addJSDoc( + requiredProp('prefix', t.tsStringKeyword()), + 'Short prefix for generated objects (e.g., "dr", "ch", "dept"). Used in table/trigger naming.' + ), + addJSDoc( + optionalProp('description', t.tsStringKeyword()), + 'Human-readable description of this entity type.' + ), + addJSDoc( + optionalProp('parent_entity', t.tsStringKeyword()), + 'Parent entity type name. Defaults to "org".' + ), + addJSDoc( + optionalProp('table_name', t.tsStringKeyword()), + 'Custom table name for the entity table. Defaults to name-derived convention.' + ), + addJSDoc( + optionalProp('is_visible', t.tsBooleanKeyword()), + 'Whether this entity type is visible in the API. Defaults to true.' + ), + addJSDoc( + optionalProp('has_limits', t.tsBooleanKeyword()), + 'Whether to provision a limits module for this entity type. Defaults to false.' + ), + addJSDoc( + optionalProp('has_profiles', t.tsBooleanKeyword()), + 'Whether to provision a profiles module for this entity type. Defaults to false.' + ), + addJSDoc( + optionalProp('has_levels', t.tsBooleanKeyword()), + 'Whether to provision a levels module for this entity type. Defaults to false.' + ), + addJSDoc( + optionalProp('skip_entity_policies', t.tsBooleanKeyword()), + 'Whether to skip creating default RLS policies on the entity table. Defaults to false.' + ), + ]), + 'A membership type entry for Phase 0 of construct_blueprint(). Provisions a full entity type with its own entity table, membership modules, and security policies via entity_type_provision.' + ); +} + function buildBlueprintTable(): t.ExportNamedDeclaration { return addJSDoc( exportInterface('BlueprintTable', [ @@ -711,6 +759,15 @@ function buildBlueprintDefinition(): t.ExportNamedDeclaration { ), 'Unique constraints on table columns.' ), + addJSDoc( + optionalProp( + 'membership_types', + t.tsArrayType( + t.tsTypeReference(t.identifier('BlueprintMembershipType')) + ) + ), + 'Entity types to provision in Phase 0 (before tables). Each entry creates an entity table with membership modules and security.' + ), ]), 'The complete blueprint definition -- the JSONB shape accepted by construct_blueprint().' ); @@ -782,6 +839,7 @@ function buildProgram(meta?: MetaTableInfo[]): string { statements.push(buildBlueprintTableIndex()); statements.push(buildBlueprintUniqueConstraint()); statements.push(buildBlueprintTableUniqueConstraint()); + statements.push(buildBlueprintMembershipType()); // -- Node types discriminated union -- statements.push(