diff --git a/graphql/node-type-registry/src/blueprint-types.generated.ts b/graphql/node-type-registry/src/blueprint-types.generated.ts index 7d288ab5a..990694c11 100644 --- a/graphql/node-type-registry/src/blueprint-types.generated.ts +++ b/graphql/node-type-registry/src/blueprint-types.generated.ts @@ -1,4 +1,5 @@ // GENERATED FILE — DO NOT EDIT +/* eslint-disable @typescript-eslint/no-empty-object-type */ // // Regenerate with: // cd graphql/node-type-registry && pnpm generate:types @@ -69,7 +70,7 @@ export interface DataJobTriggerParams { /* Job task identifier passed to add_job (e.g., process_invoice, sync_to_stripe) */ task_identifier: string; /* How to build the job payload: row (full NEW/OLD), row_id (just id), fields (selected columns), custom (mapped columns) */ - payload_strategy?: "row" | "row_id" | "fields" | "custom"; + payload_strategy?: 'row' | 'row_id' | 'fields' | 'custom'; /* Column names to include in payload (only for fields strategy) */ payload_fields?: string[]; /* Key-to-column mapping for custom payload (e.g., {"invoice_id": "id", "total": "amount"}) */ @@ -77,7 +78,7 @@ export interface DataJobTriggerParams { [key: string]: unknown; }; /* Trigger events to create */ - events?: ("INSERT" | "UPDATE" | "DELETE")[]; + events?: ('INSERT' | 'UPDATE' | 'DELETE')[]; /* Include OLD row in payload (for UPDATE triggers) */ include_old?: boolean; /* Include table/schema metadata in payload */ @@ -144,7 +145,7 @@ export interface DataInflectionParams { /* Name of the field to transform */ field_name: string; /* Inflection operations to apply in order */ - ops: ("plural" | "singular" | "camel" | "pascal" | "dashed" | "slugify" | "underscore" | "lower" | "upper")[]; + ops: ('plural' | 'singular' | 'camel' | 'pascal' | 'dashed' | 'slugify' | 'underscore' | 'lower' | 'upper')[]; } /** Restricts which user can modify specific columns in shared objects. Creates an AFTER UPDATE trigger that throws OWNED_PROPS when a non-owner tries to change protected fields. References fields by name in data jsonb. */ export interface DataOwnedFieldsParams { @@ -181,7 +182,7 @@ export interface DataCompositeFieldParams { /* Array of source field names to concatenate into the target field */ source_fields: string[]; /* Output format: 'labeled' (field_name: value) or 'plain' (values only). Default: 'labeled' */ - format?: "labeled" | "plain"; + format?: 'labeled' | 'plain'; } /** Creates a user profiles table with standard profile fields (profile_picture, bio, first_name, last_name, tags, desired). Uses AuthzDirectOwner for edit access and AuthzAllowAll for select. */ export type TableUserProfilesParams = {}; @@ -202,9 +203,9 @@ export interface SearchVectorParams { /* Vector dimensions (e.g. 384, 768, 1536, 3072) */ dimensions?: number; /* Index type for similarity search */ - index_method?: "hnsw" | "ivfflat"; + index_method?: 'hnsw' | 'ivfflat'; /* Distance metric (cosine, l2, ip) */ - metric?: "cosine" | "l2" | "ip"; + metric?: 'cosine' | 'l2' | 'ip'; /* Index-specific options. HNSW: {m, ef_construction}. IVFFlat: {lists}. */ index_options?: { [key: string]: unknown; @@ -218,13 +219,13 @@ export interface SearchVectorParams { /* Task identifier for the job queue */ job_task_name?: string; /* Strategy for tracking embedding staleness. column: embedding_stale boolean. null: set embedding to NULL. hash: md5 hash of source fields. */ - stale_strategy?: "column" | "null" | "hash"; + stale_strategy?: 'column' | 'null' | 'hash'; /* Chunking configuration for long-text embedding. Creates an embedding_chunks record that drives automatic text splitting and per-chunk embedding. Omit to skip chunking. */ chunks?: { /* Name of the text content column in the chunks table */content_field_name?: string; /* Maximum number of characters per chunk */chunk_size?: number; /* Number of overlapping characters between consecutive chunks */chunk_overlap?: number; - /* Strategy for splitting text into chunks */chunk_strategy?: "fixed" | "sentence" | "paragraph" | "semantic"; + /* Strategy for splitting text into chunks */chunk_strategy?: 'fixed' | 'sentence' | 'paragraph' | 'semantic'; /* Metadata fields from parent to copy into chunks */metadata_fields?: { [key: string]: unknown; }; @@ -239,7 +240,7 @@ export interface SearchFullTextParams { /* Source columns that feed the tsvector. Each has a field name, weight (A-D), and language config. */ source_fields: { /* Name of the source column */field: string; - /* tsvector weight class (A=highest, D=lowest) */weight?: "A" | "B" | "C" | "D"; + /* tsvector weight class (A=highest, D=lowest) */weight?: 'A' | 'B' | 'C' | 'D'; /* PostgreSQL text search configuration */lang?: string; }[]; /* Weight for this algorithm in composite searchScore */ @@ -265,7 +266,7 @@ export interface SearchUnifiedParams { field_name?: string; source_fields?: { field: string; - weight?: "A" | "B" | "C" | "D"; + weight?: 'A' | 'B' | 'C' | 'D'; lang?: string; }[]; search_score_weight?: number; @@ -282,15 +283,15 @@ export interface SearchUnifiedParams { embedding?: { field_name?: string; dimensions?: number; - index_method?: "hnsw" | "ivfflat"; - metric?: "cosine" | "l2" | "ip"; + index_method?: 'hnsw' | 'ivfflat'; + metric?: 'cosine' | 'l2' | 'ip'; source_fields?: string[]; search_score_weight?: number; /* Chunking configuration for long-text embedding. Creates an embedding_chunks record that drives automatic text splitting and per-chunk embedding. Omit to skip chunking. */chunks?: { /* Name of the text content column in the chunks table */content_field_name?: string; /* Maximum number of characters per chunk */chunk_size?: number; /* Number of overlapping characters between consecutive chunks */chunk_overlap?: number; - /* Strategy for splitting text into chunks */chunk_strategy?: "fixed" | "sentence" | "paragraph" | "semantic"; + /* Strategy for splitting text into chunks */chunk_strategy?: 'fixed' | 'sentence' | 'paragraph' | 'semantic'; /* Metadata fields from parent to copy into chunks */metadata_fields?: { [key: string]: unknown; }; @@ -305,7 +306,7 @@ export interface SearchUnifiedParams { /* Per-algorithm weights: {tsv: 1.5, bm25: 1.0, pgvector: 0.8, trgm: 0.3} */weights?: { [key: string]: unknown; }; - /* Score normalization strategy */normalization?: "linear" | "sigmoid"; + /* Score normalization strategy */normalization?: 'linear' | 'sigmoid'; /* Enable recency boost for search results */boost_recent?: boolean; /* Timestamp field for recency boost (e.g. created_at, updated_at) */boost_recency_field?: string; /* Decay rate for recency boost (0-1, lower = faster decay) */boost_recency_decay?: number; @@ -316,7 +317,7 @@ export interface SearchSpatialParams { /* Name of the geometry/geography column */ field_name?: string; /* PostGIS geometry type constraint */ - geometry_type?: "Point" | "LineString" | "Polygon" | "MultiPoint" | "MultiLineString" | "MultiPolygon" | "GeometryCollection" | "Geometry"; + geometry_type?: 'Point' | 'LineString' | 'Polygon' | 'MultiPoint' | 'MultiLineString' | 'MultiPolygon' | 'GeometryCollection' | 'Geometry'; /* Spatial Reference System Identifier (e.g. 4326 for WGS84) */ srid?: number; /* Coordinate dimension (2=XY, 3=XYZ, 4=XYZM) */ @@ -324,7 +325,7 @@ export interface SearchSpatialParams { /* Use geography type instead of geometry (for geodetic calculations on the sphere) */ use_geography?: boolean; /* Spatial index method */ - index_method?: "gist" | "spgist"; + index_method?: 'gist' | 'spgist'; } /** Creates a derived/materialized geometry field on the parent table that automatically aggregates geometries from a source (child) table via triggers. When child rows are inserted/updated/deleted, the parent aggregate field is recalculated using the specified PostGIS aggregation function (ST_Union, ST_Collect, ST_ConvexHull, ST_ConcaveHull). Useful for materializing spatial boundaries from collections of points or polygons. */ export interface SearchSpatialAggregateParams { @@ -337,9 +338,9 @@ export interface SearchSpatialAggregateParams { /* Name of the foreign key column on the source table pointing to the parent */ source_fk_field: string; /* PostGIS aggregation function: union (ST_Union, merges overlapping), collect (ST_Collect, groups without merging), convex_hull (smallest convex polygon), concave_hull (tighter boundary) */ - aggregate_function?: "union" | "collect" | "convex_hull" | "concave_hull"; + aggregate_function?: 'union' | 'collect' | 'convex_hull' | 'concave_hull'; /* Output geometry type constraint for the aggregate field */ - geometry_type?: "Point" | "LineString" | "Polygon" | "MultiPoint" | "MultiLineString" | "MultiPolygon" | "GeometryCollection" | "Geometry"; + geometry_type?: 'Point' | 'LineString' | 'Polygon' | 'MultiPoint' | 'MultiLineString' | 'MultiPolygon' | 'GeometryCollection' | 'Geometry'; /* Spatial Reference System Identifier (e.g. 4326 for WGS84) */ srid?: number; /* Coordinate dimension (2=XY, 3=XYZ, 4=XYZM) */ @@ -347,7 +348,7 @@ export interface SearchSpatialAggregateParams { /* Use geography type instead of geometry */ use_geography?: boolean; /* Spatial index method for the aggregate field */ - index_method?: "gist" | "spgist"; + index_method?: 'gist' | 'spgist'; } /** Creates GIN trigram indexes (gin_trgm_ops) on specified text/citext fields for fuzzy LIKE/ILIKE/similarity search. Adds @trgmSearch smart tag for PostGraphile integration. Fields must already exist on the table. */ export interface SearchTrgmParams { @@ -432,7 +433,7 @@ export interface AuthzRelatedEntityMembershipParams { /** Organizational hierarchy visibility using closure table. Managers can see subordinate data or subordinates can see manager data. */ export interface AuthzOrgHierarchyParams { /* down=manager sees subordinates, up=subordinate sees managers */ - direction: "up" | "down"; + direction: 'up' | 'down'; /* Field referencing the org entity */ entity_field?: string; /* Field referencing the user (e.g., owner_id) */ @@ -486,7 +487,7 @@ export type AuthzDenyAllParams = {}; export interface AuthzCompositeParams { /* Boolean expression combining multiple authorization nodes */ BoolExpr?: { - /* Boolean operator: AND_EXPR, OR_EXPR, or NOT_EXPR */boolop?: "AND_EXPR" | "OR_EXPR" | "NOT_EXPR"; + /* Boolean operator: AND_EXPR, OR_EXPR, or NOT_EXPR */boolop?: 'AND_EXPR' | 'OR_EXPR' | 'NOT_EXPR'; /* Array of authorization nodes to combine */args?: { [key: string]: unknown; }[]; @@ -560,7 +561,7 @@ export interface RelationBelongsToParams { /* FK field name on the source table. Auto-derived from target table name if omitted (e.g., projects → project_id) */ field_name?: string; /* FK delete action: c=CASCADE, r=RESTRICT, n=SET NULL, d=SET DEFAULT, a=NO ACTION. Required. */ - delete_action: "c" | "r" | "n" | "d" | "a"; + delete_action: 'c' | 'r' | 'n' | 'd' | 'a'; /* Whether the FK field is NOT NULL */ is_required?: boolean; } @@ -573,7 +574,7 @@ export interface RelationHasOneParams { /* FK field name on the source table. Auto-derived from target table name if omitted (e.g., users → user_id) */ field_name?: string; /* FK delete action: c=CASCADE, r=RESTRICT, n=SET NULL, d=SET DEFAULT, a=NO ACTION. Required. */ - delete_action: "c" | "r" | "n" | "d" | "a"; + delete_action: 'c' | 'r' | 'n' | 'd' | 'a'; /* Whether the FK field is NOT NULL */ is_required?: boolean; } @@ -586,7 +587,7 @@ export interface RelationHasManyParams { /* FK field name on the target table. Auto-derived from source table name if omitted (e.g., projects derives project_id) */ field_name?: string; /* FK delete action: c=CASCADE, r=RESTRICT, n=SET NULL, d=SET DEFAULT, a=NO ACTION. Required. */ - delete_action: "c" | "r" | "n" | "d" | "a"; + delete_action: 'c' | 'r' | 'n' | 'd' | 'a'; /* Whether the FK field is NOT NULL */ is_required?: boolean; } @@ -640,7 +641,7 @@ export interface RelationSpatialParams { /* Relation name (stable, snake_case). Becomes the generated filter field name in GraphQL (e.g. nearby_clinic). Unique per (source_table_id, name) — idempotency key. */ name: string; /* PostGIS spatial predicate. One of the 8 whitelisted operators. st_dwithin requires param_name. */ - operator: "st_contains" | "st_within" | "st_intersects" | "st_covers" | "st_coveredby" | "st_overlaps" | "st_touches" | "st_dwithin"; + operator: 'st_contains' | 'st_within' | 'st_intersects' | 'st_covers' | 'st_coveredby' | 'st_overlaps' | 'st_touches' | 'st_dwithin'; /* Parameter name for parametric operators (currently only st_dwithin, which needs a distance argument). Must be NULL for all other operators. Enforced by table CHECK. */ param_name?: string; } @@ -683,7 +684,7 @@ export interface ViewJoinedTablesParams { /* Array of join specifications */ joins: { /* UUID of the joined table */table_id: string; - join_type?: "INNER" | "LEFT" | "RIGHT" | "FULL"; + join_type?: 'INNER' | 'LEFT' | 'RIGHT' | 'FULL'; /* Field on primary table */primary_field: string; /* Field on joined table */join_field: string; /* Optional column names to include from this joined table */columns?: string[]; @@ -699,7 +700,7 @@ export interface ViewAggregatedParams { group_by_fields: string[]; /* Array of aggregate specifications */ aggregates: { - function: "COUNT" | "SUM" | "AVG" | "MIN" | "MAX"; + function: 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX'; /* Field to aggregate (or * for COUNT) */field?: string; /* Output column name */alias: string; }[]; @@ -733,7 +734,7 @@ export interface BlueprintField { /** An RLS policy entry for a blueprint table. Uses $type to match the blueprint JSON convention. */ export interface BlueprintPolicy { /** Authz* policy type name (e.g., "AuthzDirectOwner", "AuthzAllowAll"). */ - $type: "AuthzDirectOwner" | "AuthzDirectOwnerAny" | "AuthzMembership" | "AuthzEntityMembership" | "AuthzRelatedEntityMembership" | "AuthzOrgHierarchy" | "AuthzTemporal" | "AuthzPublishable" | "AuthzMemberList" | "AuthzRelatedMemberList" | "AuthzAllowAll" | "AuthzDenyAll" | "AuthzComposite" | "AuthzNotReadOnly" | "AuthzPeerOwnership" | "AuthzRelatedPeerOwnership"; + $type: 'AuthzDirectOwner' | 'AuthzDirectOwnerAny' | 'AuthzMembership' | 'AuthzEntityMembership' | 'AuthzRelatedEntityMembership' | 'AuthzOrgHierarchy' | 'AuthzTemporal' | 'AuthzPublishable' | 'AuthzMemberList' | 'AuthzRelatedMemberList' | 'AuthzAllowAll' | 'AuthzDenyAll' | 'AuthzComposite' | 'AuthzNotReadOnly' | 'AuthzPeerOwnership' | 'AuthzRelatedPeerOwnership'; /** Privileges this policy applies to (e.g., ["select"], ["insert", "update", "delete"]). */ privileges?: string[]; /** Whether this policy is permissive (true) or restrictive (false). Defaults to true. */ @@ -830,6 +831,49 @@ export interface BlueprintTableUniqueConstraint { /** Optional schema name override. */ schema_name?: string; } +/** A storage-specific RLS policy object for apply_storage_security(). Each entry defines an Authz* policy with explicit privileges, scoped to specific storage tables. */ +export interface BlueprintStoragePolicy { + /** Authz* policy generator type (e.g., "AuthzPublishable", "AuthzDirectOwner", "AuthzEntityMembership"). */ + $type: string; + /** Privilege array (e.g., ["select", "insert", "update", "delete"]). Intersected with each storage table's supported operations. */ + privileges: string[]; + /** Policy data config. Auto-derived from $type when omitted (e.g., AuthzPublishable defaults to {"is_published_field": "is_public", "require_published_at": false}). */ + data?: Record; + /** Which storage tables to apply this policy to. Defaults to all three when omitted. Uses logical names (not prefixed). */ + tables?: ('buckets' | 'files' | 'upload_requests')[]; + /** Custom RLS policy name suffix. Auto-derived from $type when omitted (pub/own/mem). */ + policy_name?: string; +} +/** A bucket seed entry for storage_config.buckets[]. Creates an initial bucket row in the {prefix}_buckets table during entity type provisioning. Only used for app-level storage (not entity-scoped). */ +export interface BlueprintBucketSeed { + /** Bucket key name (e.g., "avatars", "documents"). Becomes the key column value. */ + name: string; + /** Human-readable description of this bucket. */ + description?: string; + /** Whether the bucket is publicly readable. Defaults to false. */ + is_public?: boolean; + /** MIME type allowlist (e.g., ["image/png", "image/jpeg"]). NULL means all types allowed. */ + allowed_mime_types?: string[]; + /** Maximum file size in bytes for this bucket. NULL means no limit. */ + max_file_size?: number; + /** CORS allowed origins for this bucket. */ + allowed_origins?: string[]; +} +/** Storage configuration for an entity type. Controls RLS policies on storage tables, seeds initial buckets, and overrides module-level settings (expiry times, file size limits, CORS). */ +export interface BlueprintStorageConfig { + /** Custom RLS policies for storage tables. When provided, replaces the default policy set (AuthzPublishable + membership + AuthzDirectOwner). Each entry is a policy object with $type, privileges, and optional data/tables/policy_name. */ + policies?: BlueprintStoragePolicy[]; + /** Initial bucket seed entries. Each creates a row in {prefix}_buckets during provisioning. Only used for app-level storage (not entity-scoped). */ + buckets?: BlueprintBucketSeed[]; + /** Override for presigned upload URL expiry time in seconds. */ + upload_url_expiry_seconds?: number; + /** Override for presigned download URL expiry time in seconds. */ + download_url_expiry_seconds?: number; + /** Default maximum file size in bytes for the storage module. */ + default_max_file_size?: number; + /** CORS allowed origins for the storage module. */ + allowed_origins?: string[]; +} /** Override object for the entity table created by a BlueprintMembershipType. Shape mirrors BlueprintTable / secure_table_provision vocabulary. When supplied, policies[] replaces the default entity-table policies entirely. */ export interface BlueprintEntityTableProvision { /** Whether to enable RLS on the entity table. Forwarded to secure_table_provision. Defaults to true. */ @@ -866,10 +910,14 @@ export interface BlueprintMembershipType { has_profiles?: boolean; /** Whether to provision a levels module for this entity type. Defaults to false. */ has_levels?: boolean; + /** Whether to provision a storage module (buckets, files, upload_requests tables) for this entity type. Defaults to false. */ + has_storage?: boolean; /** Escape hatch: when true AND table_provision is NULL, zero policies are provisioned on the entity table. Defaults to false. */ skip_entity_policies?: boolean; /** Override for the entity table. Shape mirrors BlueprintTable / secure_table_provision vocabulary. When supplied, its policies[] replaces the five default entity-table policies; is_visible becomes a no-op. When NULL (default), the five default policies are applied (gated by is_visible). */ table_provision?: BlueprintEntityTableProvision; + /** Storage configuration. Only used when has_storage is true. Controls RLS policies on storage tables, seeds initial buckets, and overrides module-level settings (expiry times, file size limits, CORS). */ + storage?: BlueprintStorageConfig; } /** * =========================================================================== @@ -878,142 +926,142 @@ export interface BlueprintMembershipType { */ ; /** String shorthand -- just the node type name. */ -export type BlueprintNodeShorthand = "AuthzDirectOwner" | "AuthzDirectOwnerAny" | "AuthzMembership" | "AuthzEntityMembership" | "AuthzRelatedEntityMembership" | "AuthzOrgHierarchy" | "AuthzTemporal" | "AuthzPublishable" | "AuthzMemberList" | "AuthzRelatedMemberList" | "AuthzAllowAll" | "AuthzDenyAll" | "AuthzComposite" | "AuthzNotReadOnly" | "AuthzPeerOwnership" | "AuthzRelatedPeerOwnership" | "DataId" | "DataDirectOwner" | "DataEntityMembership" | "DataOwnershipInEntity" | "DataTimestamps" | "DataPeoplestamps" | "DataPublishable" | "DataSoftDelete" | "SearchVector" | "SearchFullText" | "SearchBm25" | "SearchUnified" | "SearchSpatial" | "SearchSpatialAggregate" | "DataJobTrigger" | "DataTags" | "DataStatusField" | "DataJsonb" | "SearchTrgm" | "DataSlug" | "DataInflection" | "DataOwnedFields" | "DataInheritFromParent" | "DataForceCurrentUser" | "DataImmutableFields" | "DataCompositeField" | "TableUserProfiles" | "TableOrganizationSettings" | "TableUserSettings"; +export type BlueprintNodeShorthand = 'AuthzDirectOwner' | 'AuthzDirectOwnerAny' | 'AuthzMembership' | 'AuthzEntityMembership' | 'AuthzRelatedEntityMembership' | 'AuthzOrgHierarchy' | 'AuthzTemporal' | 'AuthzPublishable' | 'AuthzMemberList' | 'AuthzRelatedMemberList' | 'AuthzAllowAll' | 'AuthzDenyAll' | 'AuthzComposite' | 'AuthzNotReadOnly' | 'AuthzPeerOwnership' | 'AuthzRelatedPeerOwnership' | 'DataId' | 'DataDirectOwner' | 'DataEntityMembership' | 'DataOwnershipInEntity' | 'DataTimestamps' | 'DataPeoplestamps' | 'DataPublishable' | 'DataSoftDelete' | 'SearchVector' | 'SearchFullText' | 'SearchBm25' | 'SearchUnified' | 'SearchSpatial' | 'SearchSpatialAggregate' | 'DataJobTrigger' | 'DataTags' | 'DataStatusField' | 'DataJsonb' | 'SearchTrgm' | 'DataSlug' | 'DataInflection' | 'DataOwnedFields' | 'DataInheritFromParent' | 'DataForceCurrentUser' | 'DataImmutableFields' | 'DataCompositeField' | 'TableUserProfiles' | 'TableOrganizationSettings' | 'TableUserSettings'; /** Object form -- { $type, data } with typed parameters. */ export type BlueprintNodeObject = { - $type: "AuthzDirectOwner"; + $type: 'AuthzDirectOwner'; data: AuthzDirectOwnerParams; } | { - $type: "AuthzDirectOwnerAny"; + $type: 'AuthzDirectOwnerAny'; data: AuthzDirectOwnerAnyParams; } | { - $type: "AuthzMembership"; + $type: 'AuthzMembership'; data: AuthzMembershipParams; } | { - $type: "AuthzEntityMembership"; + $type: 'AuthzEntityMembership'; data: AuthzEntityMembershipParams; } | { - $type: "AuthzRelatedEntityMembership"; + $type: 'AuthzRelatedEntityMembership'; data: AuthzRelatedEntityMembershipParams; } | { - $type: "AuthzOrgHierarchy"; + $type: 'AuthzOrgHierarchy'; data: AuthzOrgHierarchyParams; } | { - $type: "AuthzTemporal"; + $type: 'AuthzTemporal'; data: AuthzTemporalParams; } | { - $type: "AuthzPublishable"; + $type: 'AuthzPublishable'; data: AuthzPublishableParams; } | { - $type: "AuthzMemberList"; + $type: 'AuthzMemberList'; data: AuthzMemberListParams; } | { - $type: "AuthzRelatedMemberList"; + $type: 'AuthzRelatedMemberList'; data: AuthzRelatedMemberListParams; } | { - $type: "AuthzAllowAll"; + $type: 'AuthzAllowAll'; data?: Record; } | { - $type: "AuthzDenyAll"; + $type: 'AuthzDenyAll'; data?: Record; } | { - $type: "AuthzComposite"; + $type: 'AuthzComposite'; data: AuthzCompositeParams; } | { - $type: "AuthzNotReadOnly"; + $type: 'AuthzNotReadOnly'; data: AuthzNotReadOnlyParams; } | { - $type: "AuthzPeerOwnership"; + $type: 'AuthzPeerOwnership'; data: AuthzPeerOwnershipParams; } | { - $type: "AuthzRelatedPeerOwnership"; + $type: 'AuthzRelatedPeerOwnership'; data: AuthzRelatedPeerOwnershipParams; } | { - $type: "DataId"; + $type: 'DataId'; data: DataIdParams; } | { - $type: "DataDirectOwner"; + $type: 'DataDirectOwner'; data: DataDirectOwnerParams; } | { - $type: "DataEntityMembership"; + $type: 'DataEntityMembership'; data: DataEntityMembershipParams; } | { - $type: "DataOwnershipInEntity"; + $type: 'DataOwnershipInEntity'; data: DataOwnershipInEntityParams; } | { - $type: "DataTimestamps"; + $type: 'DataTimestamps'; data: DataTimestampsParams; } | { - $type: "DataPeoplestamps"; + $type: 'DataPeoplestamps'; data: DataPeoplestampsParams; } | { - $type: "DataPublishable"; + $type: 'DataPublishable'; data: DataPublishableParams; } | { - $type: "DataSoftDelete"; + $type: 'DataSoftDelete'; data: DataSoftDeleteParams; } | { - $type: "SearchVector"; + $type: 'SearchVector'; data: SearchVectorParams; } | { - $type: "SearchFullText"; + $type: 'SearchFullText'; data: SearchFullTextParams; } | { - $type: "SearchBm25"; + $type: 'SearchBm25'; data: SearchBm25Params; } | { - $type: "SearchUnified"; + $type: 'SearchUnified'; data: SearchUnifiedParams; } | { - $type: "SearchSpatial"; + $type: 'SearchSpatial'; data: SearchSpatialParams; } | { - $type: "SearchSpatialAggregate"; + $type: 'SearchSpatialAggregate'; data: SearchSpatialAggregateParams; } | { - $type: "DataJobTrigger"; + $type: 'DataJobTrigger'; data: DataJobTriggerParams; } | { - $type: "DataTags"; + $type: 'DataTags'; data: DataTagsParams; } | { - $type: "DataStatusField"; + $type: 'DataStatusField'; data: DataStatusFieldParams; } | { - $type: "DataJsonb"; + $type: 'DataJsonb'; data: DataJsonbParams; } | { - $type: "SearchTrgm"; + $type: 'SearchTrgm'; data: SearchTrgmParams; } | { - $type: "DataSlug"; + $type: 'DataSlug'; data: DataSlugParams; } | { - $type: "DataInflection"; + $type: 'DataInflection'; data: DataInflectionParams; } | { - $type: "DataOwnedFields"; + $type: 'DataOwnedFields'; data: DataOwnedFieldsParams; } | { - $type: "DataInheritFromParent"; + $type: 'DataInheritFromParent'; data: DataInheritFromParentParams; } | { - $type: "DataForceCurrentUser"; + $type: 'DataForceCurrentUser'; data: DataForceCurrentUserParams; } | { - $type: "DataImmutableFields"; + $type: 'DataImmutableFields'; data: DataImmutableFieldsParams; } | { - $type: "DataCompositeField"; + $type: 'DataCompositeField'; data: DataCompositeFieldParams; } | { - $type: "TableUserProfiles"; + $type: 'TableUserProfiles'; data?: Record; } | { - $type: "TableOrganizationSettings"; + $type: 'TableOrganizationSettings'; data?: Record; } | { - $type: "TableUserSettings"; + $type: 'TableUserSettings'; data?: Record; }; /** A node entry in a blueprint table. Either a string shorthand or a typed object. */ @@ -1026,31 +1074,31 @@ export type BlueprintNode = BlueprintNodeShorthand | BlueprintNodeObject; ; /** A relation entry in a blueprint definition. */ export type BlueprintRelation = { - $type: "RelationBelongsTo"; + $type: 'RelationBelongsTo'; source_table: string; target_table: string; source_schema_name?: string; target_schema_name?: string; } & Partial | { - $type: "RelationHasOne"; + $type: 'RelationHasOne'; source_table: string; target_table: string; source_schema_name?: string; target_schema_name?: string; } & Partial | { - $type: "RelationHasMany"; + $type: 'RelationHasMany'; source_table: string; target_table: string; source_schema_name?: string; target_schema_name?: string; } & Partial | { - $type: "RelationManyToMany"; + $type: 'RelationManyToMany'; source_table: string; target_table: string; source_schema_name?: string; target_schema_name?: string; } & Partial | { - $type: "RelationSpatial"; + $type: 'RelationSpatial'; source_table: string; target_table: string; source_schema_name?: string; diff --git a/graphql/node-type-registry/src/codegen/generate-types.ts b/graphql/node-type-registry/src/codegen/generate-types.ts index 890c71bd4..88add2c32 100644 --- a/graphql/node-type-registry/src/codegen/generate-types.ts +++ b/graphql/node-type-registry/src/codegen/generate-types.ts @@ -20,10 +20,10 @@ // eslint-disable-next-line @typescript-eslint/no-var-requires const generate = require('@babel/generator').default ?? require('@babel/generator'); import * as t from '@babel/types'; -import { writeFileSync, readFileSync, mkdirSync, existsSync } from 'fs'; +import { existsSync,mkdirSync, readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; -import { generateTypeScriptTypes } from 'schema-typescript'; import type { JSONSchema as SchemaTS_JSONSchema } from 'schema-typescript'; +import { generateTypeScriptTypes } from 'schema-typescript'; import { allNodeTypes } from '../index'; import type { NodeTypeDefinition } from '../types'; @@ -55,7 +55,7 @@ interface MetaTableInfo { /** Attach a JSDoc-style leading comment to an AST node. */ function addJSDoc(node: T, description: string): T { node.leadingComments = [ - { type: 'CommentBlock', value: `* ${description} ` } as t.CommentBlock, + { type: 'CommentBlock', value: `* ${description} ` } as t.CommentBlock ]; return node; } @@ -193,7 +193,7 @@ function generateParamsInterfaces( const astNodes = generateTypeScriptTypes(sanitized as SchemaTS_JSONSchema, { includePropertyComments: true, includeTypeComments: false, - strictTypeSafety: true, + strictTypeSafety: true }); if (astNodes.length > 0) { @@ -221,31 +221,31 @@ function generateParamsInterfaces( function pgTypeToTSType(pgType: string, isArray: boolean): t.TSType { let base: t.TSType; switch (pgType) { - case 'bool': - case 'boolean': - base = t.tsBooleanKeyword(); - break; - case 'int2': - case 'int4': - case 'int8': - case 'integer': - case 'smallint': - case 'bigint': - case 'float4': - case 'float8': - case 'float': - case 'double precision': - case 'numeric': - case 'real': - base = t.tsNumberKeyword(); - break; - case 'jsonb': - case 'json': - base = recordType(t.tsStringKeyword(), t.tsUnknownKeyword()); - break; - default: - base = t.tsStringKeyword(); - break; + case 'bool': + case 'boolean': + base = t.tsBooleanKeyword(); + break; + case 'int2': + case 'int4': + case 'int8': + case 'integer': + case 'smallint': + case 'bigint': + case 'float4': + case 'float8': + case 'float': + case 'double precision': + case 'numeric': + case 'real': + base = t.tsNumberKeyword(); + break; + case 'jsonb': + case 'json': + base = recordType(t.tsStringKeyword(), t.tsUnknownKeyword()); + break; + default: + base = t.tsStringKeyword(); + break; } return isArray ? t.tsArrayType(base) : base; } @@ -306,7 +306,7 @@ function buildBlueprintField( return deriveInterfaceFromTable( table, 'BlueprintField', - 'A custom field (column) to add to a blueprint table. Derived from _meta.', + 'A custom field (column) to add to a blueprint table. Derived from _meta.' ); } // Static fallback @@ -316,7 +316,7 @@ function buildBlueprintField( addJSDoc(requiredProp('type', t.tsStringKeyword()), 'The PostgreSQL type (e.g., "text", "integer", "boolean", "uuid").'), addJSDoc(optionalProp('is_required', t.tsBooleanKeyword()), 'Whether the column has a NOT NULL constraint.'), addJSDoc(optionalProp('default_value', t.tsStringKeyword()), 'SQL default value expression (e.g., "true", "now()").'), - addJSDoc(optionalProp('description', t.tsStringKeyword()), 'Comment/description for this field.'), + addJSDoc(optionalProp('description', t.tsStringKeyword()), 'Comment/description for this field.') ]), 'A custom field (column) to add to a blueprint table.' ); @@ -342,7 +342,7 @@ function buildBlueprintPolicy( addJSDoc(optionalProp('permissive', t.tsBooleanKeyword()), 'Whether this policy is permissive (true) or restrictive (false). Defaults to true.'), addJSDoc(optionalProp('policy_role', t.tsStringKeyword()), 'Role for this policy. Defaults to "authenticated".'), addJSDoc(optionalProp('policy_name', t.tsStringKeyword()), 'Optional custom name for this policy.'), - addJSDoc(optionalProp('data', recordType(t.tsStringKeyword(), t.tsUnknownKeyword())), 'Policy-specific data (structure varies by policy type).'), + addJSDoc(optionalProp('data', recordType(t.tsStringKeyword(), t.tsUnknownKeyword())), 'Policy-specific data (structure varies by policy type).') ]), 'An RLS policy entry for a blueprint table. Uses $type to match the blueprint JSON convention.' ); @@ -362,7 +362,7 @@ function buildBlueprintFtsSource(): t.ExportNamedDeclaration { addJSDoc( optionalProp('lang', t.tsStringKeyword()), 'Language for text search. Defaults to "english".' - ), + ) ]), 'A source field contributing to a full-text search tsvector column.' ); @@ -389,7 +389,7 @@ function buildBlueprintFullTextSearch(): t.ExportNamedDeclaration { t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintFtsSource'))) ), 'Source fields that feed into this tsvector.' - ), + ) ]), 'A full-text search configuration for a blueprint table (top-level, requires table_name).' ); @@ -412,7 +412,7 @@ function buildBlueprintTableFullTextSearch(): t.ExportNamedDeclaration { addJSDoc( optionalProp('schema_name', t.tsStringKeyword()), 'Optional schema name override.' - ), + ) ]), 'A full-text search configuration nested inside a table definition (table_name not required).' ); @@ -431,8 +431,8 @@ function buildBlueprintIndex( // JSONB columns get Record instead of the default index_params: recordType(t.tsStringKeyword(), t.tsUnknownKeyword()), where_clause: recordType(t.tsStringKeyword(), t.tsUnknownKeyword()), - options: recordType(t.tsStringKeyword(), t.tsUnknownKeyword()), - }, + options: recordType(t.tsStringKeyword(), t.tsUnknownKeyword()) + } ); } // Static fallback @@ -446,7 +446,7 @@ function buildBlueprintIndex( addJSDoc(optionalProp('is_unique', t.tsBooleanKeyword()), 'Whether this is a unique index.'), addJSDoc(optionalProp('name', t.tsStringKeyword()), 'Optional custom name for the index.'), addJSDoc(optionalProp('op_classes', t.tsArrayType(t.tsStringKeyword())), 'Operator classes for the index columns.'), - addJSDoc(optionalProp('options', recordType(t.tsStringKeyword(), t.tsUnknownKeyword())), 'Additional index-specific options.'), + addJSDoc(optionalProp('options', recordType(t.tsStringKeyword(), t.tsUnknownKeyword())), 'Additional index-specific options.') ]), 'An index definition within a blueprint (top-level, requires table_name).' ); @@ -462,7 +462,7 @@ function buildBlueprintTableIndex(): t.ExportNamedDeclaration { addJSDoc(optionalProp('name', t.tsStringKeyword()), 'Optional custom name for the index.'), addJSDoc(optionalProp('op_classes', t.tsArrayType(t.tsStringKeyword())), 'Operator classes for the index columns.'), addJSDoc(optionalProp('options', recordType(t.tsStringKeyword(), t.tsUnknownKeyword())), 'Additional index-specific options.'), - addJSDoc(optionalProp('schema_name', t.tsStringKeyword()), 'Optional schema name override.'), + addJSDoc(optionalProp('schema_name', t.tsStringKeyword()), 'Optional schema name override.') ]), 'An index definition nested inside a table definition (table_name not required).' ); @@ -519,7 +519,7 @@ function buildNodeTypes( 'BlueprintNode', t.tsUnionType([ t.tsTypeReference(t.identifier('BlueprintNodeShorthand')), - t.tsTypeReference(t.identifier('BlueprintNodeObject')), + t.tsTypeReference(t.identifier('BlueprintNodeObject')) ]) ), 'A node entry in a blueprint table. Either a string shorthand or a typed object.' @@ -542,7 +542,7 @@ function buildRelationTypes( requiredProp('source_table', t.tsStringKeyword()), requiredProp('target_table', t.tsStringKeyword()), optionalProp('source_schema_name', t.tsStringKeyword()), - optionalProp('target_schema_name', t.tsStringKeyword()), + optionalProp('target_schema_name', t.tsStringKeyword()) ]; // RelationSpatial is the only relation type that references *existing* @@ -568,7 +568,7 @@ function buildRelationTypes( return t.tsIntersectionType([ baseType, - partialOf(t.tsTypeReference(t.identifier(`${nt.name}Params`))), + partialOf(t.tsTypeReference(t.identifier(`${nt.name}Params`))) ]); }); @@ -576,7 +576,7 @@ function buildRelationTypes( addJSDoc( exportTypeAlias('BlueprintRelation', t.tsUnionType(relationMembers)), 'A relation entry in a blueprint definition.' - ), + ) ]; } @@ -598,7 +598,7 @@ function buildBlueprintUniqueConstraint(): t.ExportNamedDeclaration { addJSDoc( requiredProp('columns', t.tsArrayType(t.tsStringKeyword())), 'Column names that form the unique constraint.' - ), + ) ]), 'A unique constraint definition within a blueprint (top-level, requires table_name).' ); @@ -614,12 +614,147 @@ function buildBlueprintTableUniqueConstraint(): t.ExportNamedDeclaration { addJSDoc( optionalProp('schema_name', t.tsStringKeyword()), 'Optional schema name override.' - ), + ) ]), 'A unique constraint nested inside a table definition (table_name not required).' ); } +/** + * Build the BlueprintStoragePolicy interface. + * + * Matches the jsonb policy objects accepted by apply_storage_security(): + * { "$type": "AuthzPublishable", "privileges": ["select"], "data": {...}, "tables": [...], "policy_name": "pub" } + */ +function buildBlueprintStoragePolicy(): t.ExportNamedDeclaration { + return addJSDoc( + exportInterface('BlueprintStoragePolicy', [ + addJSDoc( + requiredProp('$type', t.tsStringKeyword()), + 'Authz* policy generator type (e.g., "AuthzPublishable", "AuthzDirectOwner", "AuthzEntityMembership").' + ), + addJSDoc( + requiredProp('privileges', t.tsArrayType(t.tsStringKeyword())), + 'Privilege array (e.g., ["select", "insert", "update", "delete"]). Intersected with each storage table\'s supported operations.' + ), + addJSDoc( + optionalProp( + 'data', + recordType(t.tsStringKeyword(), t.tsUnknownKeyword()) + ), + 'Policy data config. Auto-derived from $type when omitted (e.g., AuthzPublishable defaults to {"is_published_field": "is_public", "require_published_at": false}).' + ), + addJSDoc( + optionalProp( + 'tables', + t.tsArrayType( + strUnion(['buckets', 'files', 'upload_requests']) + ) + ), + 'Which storage tables to apply this policy to. Defaults to all three when omitted. Uses logical names (not prefixed).' + ), + addJSDoc( + optionalProp('policy_name', t.tsStringKeyword()), + 'Custom RLS policy name suffix. Auto-derived from $type when omitted (pub/own/mem).' + ) + ]), + 'A storage-specific RLS policy object for apply_storage_security(). Each entry defines an Authz* policy with explicit privileges, scoped to specific storage tables.' + ); +} + +/** + * Build the BlueprintBucketSeed interface. + * + * Matches the bucket entries in storage_config.buckets[]. + */ +function buildBlueprintBucketSeed(): t.ExportNamedDeclaration { + return addJSDoc( + exportInterface('BlueprintBucketSeed', [ + addJSDoc( + requiredProp('name', t.tsStringKeyword()), + 'Bucket key name (e.g., "avatars", "documents"). Becomes the key column value.' + ), + addJSDoc( + optionalProp('description', t.tsStringKeyword()), + 'Human-readable description of this bucket.' + ), + addJSDoc( + optionalProp('is_public', t.tsBooleanKeyword()), + 'Whether the bucket is publicly readable. Defaults to false.' + ), + addJSDoc( + optionalProp( + 'allowed_mime_types', + t.tsArrayType(t.tsStringKeyword()) + ), + 'MIME type allowlist (e.g., ["image/png", "image/jpeg"]). NULL means all types allowed.' + ), + addJSDoc( + optionalProp('max_file_size', t.tsNumberKeyword()), + 'Maximum file size in bytes for this bucket. NULL means no limit.' + ), + addJSDoc( + optionalProp( + 'allowed_origins', + t.tsArrayType(t.tsStringKeyword()) + ), + 'CORS allowed origins for this bucket.' + ) + ]), + 'A bucket seed entry for storage_config.buckets[]. Creates an initial bucket row in the {prefix}_buckets table during entity type provisioning. Only used for app-level storage (not entity-scoped).' + ); +} + +/** + * Build the BlueprintStorageConfig interface. + * + * Matches the jsonb shape accepted by storage_config on entity_type_provision. + */ +function buildBlueprintStorageConfig(): t.ExportNamedDeclaration { + return addJSDoc( + exportInterface('BlueprintStorageConfig', [ + addJSDoc( + optionalProp( + 'policies', + t.tsArrayType( + t.tsTypeReference(t.identifier('BlueprintStoragePolicy')) + ) + ), + 'Custom RLS policies for storage tables. When provided, replaces the default policy set (AuthzPublishable + membership + AuthzDirectOwner). Each entry is a policy object with $type, privileges, and optional data/tables/policy_name.' + ), + addJSDoc( + optionalProp( + 'buckets', + t.tsArrayType( + t.tsTypeReference(t.identifier('BlueprintBucketSeed')) + ) + ), + 'Initial bucket seed entries. Each creates a row in {prefix}_buckets during provisioning. Only used for app-level storage (not entity-scoped).' + ), + addJSDoc( + optionalProp('upload_url_expiry_seconds', t.tsNumberKeyword()), + 'Override for presigned upload URL expiry time in seconds.' + ), + addJSDoc( + optionalProp('download_url_expiry_seconds', t.tsNumberKeyword()), + 'Override for presigned download URL expiry time in seconds.' + ), + addJSDoc( + optionalProp('default_max_file_size', t.tsNumberKeyword()), + 'Default maximum file size in bytes for the storage module.' + ), + addJSDoc( + optionalProp( + 'allowed_origins', + t.tsArrayType(t.tsStringKeyword()) + ), + 'CORS allowed origins for the storage module.' + ) + ]), + 'Storage configuration for an entity type. Controls RLS policies on storage tables, seeds initial buckets, and overrides module-level settings (expiry times, file size limits, CORS).' + ); +} + function buildBlueprintEntityTableProvision(): t.ExportNamedDeclaration { return addJSDoc( exportInterface('BlueprintEntityTableProvision', [ @@ -647,7 +782,7 @@ function buildBlueprintEntityTableProvision(): t.ExportNamedDeclaration { t.tsArrayType( t.tsTypeLiteral([ requiredProp('roles', t.tsArrayType(t.tsStringKeyword())), - requiredProp('privileges', t.tsArrayType(t.tsUnknownKeyword())), + requiredProp('privileges', t.tsArrayType(t.tsUnknownKeyword())) ]) ) ), @@ -659,7 +794,7 @@ function buildBlueprintEntityTableProvision(): t.ExportNamedDeclaration { t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintPolicy'))) ), 'RLS policies for the entity table. When present, these policies fully replace the five default entity-table policies (is_visible becomes a no-op).' - ), + ) ]), 'Override object for the entity table created by a BlueprintMembershipType. Shape mirrors BlueprintTable / secure_table_provision vocabulary. When supplied, policies[] replaces the default entity-table policies entirely.' ); @@ -704,6 +839,10 @@ function buildBlueprintMembershipType(): t.ExportNamedDeclaration { optionalProp('has_levels', t.tsBooleanKeyword()), 'Whether to provision a levels module for this entity type. Defaults to false.' ), + addJSDoc( + optionalProp('has_storage', t.tsBooleanKeyword()), + 'Whether to provision a storage module (buckets, files, upload_requests tables) for this entity type. Defaults to false.' + ), addJSDoc( optionalProp('skip_entity_policies', t.tsBooleanKeyword()), 'Escape hatch: when true AND table_provision is NULL, zero policies are provisioned on the entity table. Defaults to false.' @@ -715,6 +854,13 @@ function buildBlueprintMembershipType(): t.ExportNamedDeclaration { ), 'Override for the entity table. Shape mirrors BlueprintTable / secure_table_provision vocabulary. When supplied, its policies[] replaces the five default entity-table policies; is_visible becomes a no-op. When NULL (default), the five default policies are applied (gated by is_visible).' ), + addJSDoc( + optionalProp( + 'storage', + t.tsTypeReference(t.identifier('BlueprintStorageConfig')) + ), + 'Storage configuration. Only used when has_storage is true. Controls RLS policies on storage tables, seeds initial buckets, and overrides module-level settings (expiry times, file size limits, CORS).' + ) ]), '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.' ); @@ -758,7 +904,7 @@ function buildBlueprintTable(): t.ExportNamedDeclaration { t.tsArrayType( t.tsTypeLiteral([ requiredProp('roles', t.tsArrayType(t.tsStringKeyword())), - requiredProp('privileges', t.tsArrayType(t.tsUnknownKeyword())), + requiredProp('privileges', t.tsArrayType(t.tsUnknownKeyword())) ]) ) ), @@ -788,7 +934,7 @@ function buildBlueprintTable(): t.ExportNamedDeclaration { t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintTableUniqueConstraint'))) ), 'Table-level unique constraints (table_name inherited from parent).' - ), + ) ]), 'A table definition within a blueprint.' ); @@ -844,7 +990,7 @@ function buildBlueprintDefinition(): t.ExportNamedDeclaration { ) ), '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().' ); @@ -859,8 +1005,8 @@ function sectionComment(title: string): t.Statement { empty.leadingComments = [ { type: 'CommentBlock', - value: `*\n * ===========================================================================\n * ${title}\n * ===========================================================================\n `, - } as t.CommentBlock, + value: `*\n * ===========================================================================\n * ${title}\n * ===========================================================================\n ` + } as t.CommentBlock ]; return empty; } @@ -916,6 +1062,9 @@ function buildProgram(meta?: MetaTableInfo[]): string { statements.push(buildBlueprintTableIndex()); statements.push(buildBlueprintUniqueConstraint()); statements.push(buildBlueprintTableUniqueConstraint()); + statements.push(buildBlueprintStoragePolicy()); + statements.push(buildBlueprintBucketSeed()); + statements.push(buildBlueprintStorageConfig()); statements.push(buildBlueprintEntityTableProvision()); statements.push(buildBlueprintMembershipType()); @@ -942,6 +1091,7 @@ function buildProgram(meta?: MetaTableInfo[]): string { const header = [ '// GENERATED FILE \u2014 DO NOT EDIT', + '/* eslint-disable @typescript-eslint/no-empty-object-type */', '//', '// Regenerate with:', '// cd graphql/node-type-registry && pnpm generate:types', @@ -949,10 +1099,10 @@ function buildProgram(meta?: MetaTableInfo[]): string { '// These types match the JSONB shape expected by construct_blueprint().', '// All field names are snake_case to match the SQL convention.', '', - '', + '' ].join('\n'); - const output = generate(file, { comments: true }); + const output = generate(file, { comments: true, jsescOption: { quotes: 'single' } }); return header + output.code + '\n'; }