From f3ded0f41a7c2ba2d70ba08b34e86a3f08bfd38c Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Mon, 18 May 2026 23:42:56 +0000 Subject: [PATCH 1/2] feat: update blueprint types for array-only multi-module storage - Remove has_storage from BlueprintEntityType (now derived from storage[].length > 0) - Change storage from BlueprintStorageConfig to BlueprintStorageConfig[] (array-only) - Add storage_key field to BlueprintStorageConfig for multi-module support - Update b2b-storage preset description to reference array format --- .../src/blueprint-types.generated.ts | 139 ++++++++---------- .../src/codegen/generate-types.ts | 23 +-- .../src/module-presets/b2b-storage.ts | 8 +- 3 files changed, 83 insertions(+), 87 deletions(-) diff --git a/packages/node-type-registry/src/blueprint-types.generated.ts b/packages/node-type-registry/src/blueprint-types.generated.ts index 519765c5a..f6945906c 100644 --- a/packages/node-type-registry/src/blueprint-types.generated.ts +++ b/packages/node-type-registry/src/blueprint-types.generated.ts @@ -35,23 +35,6 @@ export interface TriggerCondition { /** Negated condition. */ NOT?: TriggerCondition; } -/** - * =========================================================================== - * Billing node type parameters - * =========================================================================== - */ -; -/** Declaratively attaches billing usage-recording triggers to a table. On INSERT the named meter is incremented via record_usage; on DELETE it is decremented (reversal). On UPDATE, if the entity_field changes, the old entity is decremented and the new entity is incremented. Requires a provisioned billing_module for the target database. */ -export interface BillingMeterParams { - /* Slug of the billing meter to record usage against (must match a meters table entry, e.g. "databases", "seats") */ - meter_slug: string; - /* Column on the target table that holds the entity id for billing */ - entity_field?: string; - /* Units to record per event (default 1) */ - quantity?: number; - /* Which DML events to attach triggers for */ - events?: ('INSERT' | 'DELETE' | 'UPDATE')[]; -} /** * =========================================================================== * Check node type parameters @@ -216,9 +199,9 @@ export interface DataPeoplestampsParams { /** Adds publish state columns (is_published, published_at) for content visibility. Enables AuthzPublishable and AuthzTemporal authorization. */ export interface DataPublishableParams { /* Column name for the published boolean flag */ - is_published_field?: string; + is_published_field_name?: string; /* Column name for the publish timestamp */ - published_at_field?: string; + published_at_field_name?: string; /* If true, also adds a UUID primary key column with auto-generation */ include_id?: boolean; } @@ -282,41 +265,6 @@ export type TableOrganizationSettingsParams = {}; export type TableUserProfilesParams = {}; /** Creates a user settings table for user-specific configuration. Uses AuthzDirectOwner for access control. */ export type TableUserSettingsParams = {}; -/** - * =========================================================================== - * Limit node type parameters - * =========================================================================== - */ -; -/** Declaratively attaches aggregate limit-tracking triggers to a table. On INSERT the named limit is incremented per entity; on DELETE it is decremented. Uses org_limit_aggregates_inc/dec for per-entity (org-level) aggregate limits rather than per-user limits. Requires a provisioned limits_module for the target database. */ -export interface LimitAggregateParams { - /* Name of the aggregate limit to track (must match a default_limits entry, e.g. "databases", "members") */ - limit_name: string; - /* Column on the target table that holds the entity id for aggregate limit lookup */ - entity_field?: string; - /* Which DML events to attach triggers for */ - events?: ('INSERT' | 'DELETE' | 'UPDATE')[]; -} -/** Gates a table behind a feature flag backed by the cap tables. Attaches a BEFORE INSERT trigger that checks whether the named feature cap value is > 0. Features are modeled as caps with max=0 (disabled) or max=1 (enabled) in limit_caps / limit_caps_defaults tables. Resolution: COALESCE(per-entity cap, scope default, 0). */ -export interface LimitFeatureFlagParams { - /* Cap name representing this feature (must match a limit_caps_defaults entry with max=0 or max=1) */ - feature_name: string; - /* Feature scope: "app" (membership_type=1, app-level caps) or "org" (membership_type=2, per-entity caps) */ - scope?: 'app' | 'org'; - /* Column on the target table that holds the entity id for per-entity cap lookups (only used for org scope) */ - entity_field?: string; -} -/** Declaratively attaches limit-tracking triggers to a table. On INSERT the named limit is incremented; on DELETE it is decremented. Requires a provisioned limits_module for the target scope. */ -export interface LimitCounterParams { - /* Name of the limit to track (must match a default_limits entry, e.g. "projects", "members") */ - limit_name: string; - /* Limit scope: "app" (membership_type=1, user-level) or "org" (membership_type=2, entity-level) */ - scope?: 'app' | 'org'; - /* Column on the target table that holds the actor or entity id used for limit lookup */ - actor_field?: string; - /* Which DML events to attach triggers for */ - events?: ('INSERT' | 'DELETE' | 'UPDATE')[]; -} /** * =========================================================================== * Search node type parameters @@ -419,6 +367,8 @@ export interface SearchUnifiedParams { index_method?: 'hnsw' | 'ivfflat'; metric?: 'cosine' | 'l2' | 'ip'; source_fields?: string[]; + /* Embedding model identifier. When null, the worker falls back to runtime config. */embedding_model?: string; + /* Embedding provider name. When null, the worker falls back to runtime config. */embedding_provider?: 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; @@ -465,6 +415,10 @@ export interface SearchVectorParams { }; /* Column names that feed the embedding. Used by stale trigger to detect content changes. */ source_fields?: string[]; + /* Embedding model identifier (e.g. "nomic-embed-text", "text-embedding-3-small"). Included in the job payload so the worker knows which model to use. When null, the worker falls back to runtime config (llm_module / env vars). */ + embedding_model?: string; + /* Embedding provider name (e.g. "ollama", "openai"). When null, the worker falls back to runtime config. */ + embedding_provider?: string; /* Auto-create trigger that enqueues embedding generation jobs */ enqueue_job?: boolean; /* Task identifier for the job queue */ @@ -545,10 +499,16 @@ export interface ProcessChunksParams { dimensions?: number; /* Distance metric for the HNSW index on chunk embeddings */ metric?: 'cosine' | 'l2' | 'ip'; + /* Embedding model identifier for per-chunk embeddings. When null, the worker falls back to runtime config (llm_module / env vars). */ + embedding_model?: string; + /* Embedding provider name (e.g. "ollama", "openai"). When null, the worker falls back to runtime config. */ + embedding_provider?: string; /* Override the chunks table name. Defaults to {parent_table}_chunks. */ chunks_table_name?: string; /* Field names from the parent table to copy into chunk metadata */ metadata_fields?: string[]; + /* Text search indexes to create on the chunks content column. Omit to mirror the parent table's text search indexes. Set explicitly to override (e.g. ["fulltext", "bm25"]). */ + search_indexes?: ('fulltext' | 'bm25' | 'trigram')[]; /* Whether to create a job trigger that auto-enqueues chunking on parent INSERT/UPDATE */ enqueue_chunking_job?: boolean; /* Task identifier for the chunking job queue */ @@ -568,6 +528,10 @@ export interface ProcessFileEmbeddingParams { index_options?: { [key: string]: unknown; }; + /* Embedding model identifier (e.g. "nomic-embed-text", "text-embedding-3-small", "clip-vit-base-patch32"). Included in the job payload so the worker knows which model to use. When null, the worker falls back to runtime config (llm_module / env vars). */ + embedding_model?: string; + /* Embedding provider name (e.g. "ollama", "openai"). When null, the worker falls back to runtime config. */ + embedding_provider?: string; /* MIME type LIKE patterns to match. Multiple patterns are OR'd together. Examples: ['image/%'], ['application/pdf', 'text/%'], ['audio/%']. */ mime_patterns?: string[]; /* Job task identifier for the worker. In direct mode this is the embedding worker; in extract mode this is the extraction worker. */ @@ -578,7 +542,7 @@ export interface ProcessFileEmbeddingParams { payload_custom?: { [key: string]: unknown; }; - /* Additional compound conditions beyond MIME filtering. Merged with the auto-generated MIME conditions via AND. Use this to add status checks, field guards, etc. */ + /* Additional compound conditions beyond auto-generated filtering. Merged with the auto-generated conditions via AND. */ trigger_conditions?: TriggerCondition | TriggerCondition[]; /* Text extraction configuration. When present, the generator creates extraction output fields on the table and configures SearchVector with source_fields + stale tracking. When absent, the node operates in direct mode (single vector per file, no text extraction). */ extraction?: { @@ -594,6 +558,7 @@ export interface ProcessFileEmbeddingParams { /* Number of overlapping characters between consecutive chunks */chunk_overlap?: number; /* Strategy for splitting text into chunks */chunk_strategy?: 'fixed' | 'sentence' | 'paragraph' | 'semantic'; /* Field names from parent to copy into chunk metadata */metadata_fields?: string[]; + /* Text search indexes to create on the chunks content column. Omit to mirror the parent table's text search indexes. Set explicitly to override. */search_indexes?: ('fulltext' | 'bm25' | 'trigram')[]; /* Whether to auto-enqueue a chunking job on insert/update */enqueue_chunking_job?: boolean; /* Task identifier for the chunking job queue */chunking_task_name?: string; }; @@ -612,6 +577,10 @@ export interface ProcessImageEmbeddingParams { index_options?: { [key: string]: unknown; }; + /* Embedding model identifier (e.g. "clip-vit-base-patch32"). Included in the job payload so the worker knows which model to use. When null, the worker falls back to runtime config (llm_module / env vars). */ + embedding_model?: string; + /* Embedding provider name (e.g. "ollama", "openai"). When null, the worker falls back to runtime config. */ + embedding_provider?: string; /* MIME type LIKE patterns to match. Multiple patterns are OR'd together. */ mime_patterns?: string[]; /* Job task identifier for the image embedding worker */ @@ -622,7 +591,7 @@ export interface ProcessImageEmbeddingParams { payload_custom?: { [key: string]: unknown; }; - /* Additional compound conditions beyond MIME filtering. Merged with the auto-generated MIME conditions via AND. */ + /* Additional compound conditions beyond auto-generated filtering. Merged with the auto-generated conditions via AND. */ trigger_conditions?: TriggerCondition | TriggerCondition[]; /* Text extraction configuration. Forwarded to ProcessFileEmbedding. When present, enables extract mode (e.g., OCR for images). */ extraction?: { @@ -648,6 +617,10 @@ export interface ProcessExtractionParams { text_field?: string; /* JSONB field for extraction metadata (page count, language, etc.) */ metadata_field?: string; + /* Extraction model identifier (e.g. a vision model for OCR, an LLM for structured extraction). Included in the job payload so the worker knows which model to use. When null, the worker falls back to runtime config. */ + extraction_model?: string; + /* Extraction provider name (e.g. "ollama", "openai"). When null, the worker falls back to runtime config. */ + extraction_provider?: string; /* MIME type LIKE patterns to match. Multiple patterns are OR'd together. Examples: ['application/pdf', 'text/%'], ['application/vnd.openxmlformats%']. */ mime_patterns?: string[]; /* Job task identifier for the extraction worker */ @@ -658,7 +631,7 @@ export interface ProcessExtractionParams { payload_custom?: { [key: string]: unknown; }; - /* Additional compound conditions beyond MIME filtering. Merged with the auto-generated MIME conditions via AND. Use this to add status checks (e.g., status = 'uploaded'). */ + /* Additional compound conditions beyond auto-generated filtering. Merged with the auto-generated conditions via AND. */ trigger_conditions?: TriggerCondition | TriggerCondition[]; /* Job queue name for extraction tasks */ queue_name?: string; @@ -688,7 +661,7 @@ export interface ProcessImageVersionsParams { payload_custom?: { [key: string]: unknown; }; - /* Additional compound conditions beyond MIME filtering. Merged with the auto-generated MIME conditions via AND. */ + /* Additional compound conditions beyond auto-generated filtering. Merged with the auto-generated conditions via AND. */ trigger_conditions?: TriggerCondition | TriggerCondition[]; /* Job queue name for image processing tasks */ queue_name?: string; @@ -1209,7 +1182,9 @@ export interface BlueprintBucketSeed { } /** Storage configuration for an entity type. Seeds initial buckets, overrides module-level settings (expiry times, file size limits, CORS), and provides per-table provisioning overrides via provisions. */ export interface BlueprintStorageConfig { - /** Initial bucket seed entries. Each creates a row in {prefix}_buckets during provisioning. Only used for app-level storage (not entity-scoped). */ + /** Discriminator for multi-module storage. Defaults to "default" (omitted from table names). Non-default keys appear as an infix: {prefix}_{storage_key}_buckets. Max 16 chars, lowercase snake_case. */ + storage_key?: string; + /** Initial bucket seed entries. Each creates a row in {prefix}_buckets during provisioning. */ buckets?: BlueprintBucketSeed[]; /** Override for presigned upload URL expiry time in seconds. */ upload_url_expiry_seconds?: number; @@ -1300,8 +1275,6 @@ export interface BlueprintEntityType { 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 tables) for this entity type. Defaults to false. */ - has_storage?: boolean; /** Whether to provision entity-scoped invite tables ({prefix}_invites, {prefix}_claimed_invites) and a submit_{prefix}_invite_code() function. Defaults to false. */ has_invites?: boolean; /** Whether to auto-attach an EventTracker to the claimed_invites table for invite-based achievements. Requires has_invites=true AND has_levels=true. When true, records 'invite_claimed' events credited to the sender (inviter) on each claimed invite. Defaults to false. */ @@ -1310,8 +1283,8 @@ export interface BlueprintEntityType { 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; + /** Storage module configuration array. Each entry provisions a separate storage module with its own tables, RLS, and settings. When non-empty, has_storage is derived as true. Each entry may specify a storage_key for multi-module support (defaults to "default"). */ + storage?: BlueprintStorageConfig[]; } /** * =========================================================================== @@ -1320,7 +1293,7 @@ export interface BlueprintEntityType { */ ; /** String shorthand -- just the node type name. */ -export type BlueprintNodeShorthand = 'AuthzAllowAll' | 'AuthzAppMembership' | 'AuthzComposite' | 'AuthzDenyAll' | 'AuthzFilePath' | 'AuthzDirectOwner' | 'AuthzDirectOwnerAny' | 'AuthzEntityMembership' | 'AuthzMemberList' | 'AuthzNotReadOnly' | 'AuthzOrgHierarchy' | 'AuthzPeerOwnership' | 'AuthzPublishable' | 'AuthzRelatedEntityMembership' | 'AuthzRelatedMemberList' | 'AuthzRelatedPeerOwnership' | 'AuthzTemporal' | 'CheckGreaterThan' | 'CheckLessThan' | 'CheckNotEqual' | 'CheckOneOf' | 'LimitAggregate' | 'BillingMeter' | 'DataBulk' | 'ProcessChunks' | 'DataCompositeField' | 'DataDirectOwner' | 'DataEntityMembership' | 'ProcessFileEmbedding' | 'LimitFeatureFlag' | 'DataForceCurrentUser' | 'DataId' | 'ProcessImageEmbedding' | 'DataImmutableFields' | 'DataInflection' | 'DataInheritFromParent' | 'JobTrigger' | 'LimitCounter' | 'DataJsonb' | 'DataOwnedFields' | 'ProcessExtraction' | 'ProcessImageVersions' | 'DataOwnershipInEntity' | 'DataPeoplestamps' | 'DataPublishable' | 'DataRealtime' | 'DataSlug' | 'DataSoftDelete' | 'DataStatusField' | 'DataTags' | 'DataTimestamps' | 'SearchBm25' | 'SearchFullText' | 'SearchSpatial' | 'SearchSpatialAggregate' | 'SearchTrgm' | 'SearchUnified' | 'SearchVector' | 'TableOrganizationSettings' | 'TableUserProfiles' | 'TableUserSettings'; +export type BlueprintNodeShorthand = 'AuthzAllowAll' | 'AuthzAppMembership' | 'AuthzComposite' | 'AuthzDenyAll' | 'AuthzFilePath' | 'AuthzDirectOwner' | 'AuthzDirectOwnerAny' | 'AuthzEntityMembership' | 'AuthzMemberList' | 'AuthzNotReadOnly' | 'AuthzOrgHierarchy' | 'AuthzPeerOwnership' | 'AuthzPublishable' | 'AuthzRelatedEntityMembership' | 'AuthzRelatedMemberList' | 'AuthzRelatedPeerOwnership' | 'AuthzTemporal' | 'CheckGreaterThan' | 'CheckLessThan' | 'CheckNotEqual' | 'CheckOneOf' | 'LimitEnforceAggregate' | 'LimitTrackUsage' | 'DataBulk' | 'ProcessChunks' | 'DataCompositeField' | 'DataDirectOwner' | 'DataEntityMembership' | 'EventReferral' | 'EventTracker' | 'ProcessFileEmbedding' | 'LimitEnforceFeature' | 'DataForceCurrentUser' | 'DataId' | 'ProcessImageEmbedding' | 'DataImmutableFields' | 'DataInflection' | 'DataInheritFromParent' | 'JobTrigger' | 'LimitEnforceCounter' | 'LimitEnforceRate' | 'LimitWarningCounter' | 'LimitWarningAggregate' | 'LimitWarningRate' | 'DataJsonb' | 'DataOwnedFields' | 'ProcessExtraction' | 'ProcessImageVersions' | 'DataOwnershipInEntity' | 'DataPeoplestamps' | 'DataPublishable' | 'DataRealtime' | 'DataSlug' | 'DataSoftDelete' | 'DataStatusField' | 'DataTags' | 'DataTimestamps' | 'SearchBm25' | 'SearchFullText' | 'SearchSpatial' | 'SearchSpatialAggregate' | 'SearchTrgm' | 'SearchUnified' | 'SearchVector' | 'TableOrganizationSettings' | 'TableUserProfiles' | 'TableUserSettings'; /** Object form -- { $type, data } with typed parameters. */ export type BlueprintNodeObject = { $type: 'AuthzAllowAll'; @@ -1386,11 +1359,11 @@ export type BlueprintNodeObject = { $type: 'CheckOneOf'; data: CheckOneOfParams; } | { - $type: 'LimitAggregate'; - data: LimitAggregateParams; + $type: 'LimitEnforceAggregate'; + data: LimitEnforceAggregateParams; } | { - $type: 'BillingMeter'; - data: BillingMeterParams; + $type: 'LimitTrackUsage'; + data: LimitTrackUsageParams; } | { $type: 'DataBulk'; data: DataBulkParams; @@ -1406,12 +1379,18 @@ export type BlueprintNodeObject = { } | { $type: 'DataEntityMembership'; data: DataEntityMembershipParams; +} | { + $type: 'EventReferral'; + data: EventReferralParams; +} | { + $type: 'EventTracker'; + data: EventTrackerParams; } | { $type: 'ProcessFileEmbedding'; data: ProcessFileEmbeddingParams; } | { - $type: 'LimitFeatureFlag'; - data: LimitFeatureFlagParams; + $type: 'LimitEnforceFeature'; + data: LimitEnforceFeatureParams; } | { $type: 'DataForceCurrentUser'; data: DataForceCurrentUserParams; @@ -1434,8 +1413,20 @@ export type BlueprintNodeObject = { $type: 'JobTrigger'; data: JobTriggerParams; } | { - $type: 'LimitCounter'; - data: LimitCounterParams; + $type: 'LimitEnforceCounter'; + data: LimitEnforceCounterParams; +} | { + $type: 'LimitEnforceRate'; + data: LimitEnforceRateParams; +} | { + $type: 'LimitWarningCounter'; + data: LimitWarningCounterParams; +} | { + $type: 'LimitWarningAggregate'; + data: LimitWarningAggregateParams; +} | { + $type: 'LimitWarningRate'; + data: LimitWarningRateParams; } | { $type: 'DataJsonb'; data: DataJsonbParams; @@ -1594,8 +1585,8 @@ export interface BlueprintDefinition { unique_constraints?: BlueprintUniqueConstraint[]; /** Entity types to provision in Phase 0 (before tables). Each entry creates an entity table with membership modules and security. */ entity_types?: BlueprintEntityType[]; - /** App-level storage configuration. Creates a storage_module (membership_type = NULL), seeds initial buckets, and overrides module-level settings (expiry times, file size limits, CORS). Use provisions for per-table policy overrides. For entity-scoped storage, use entity_types[].has_storage + entity_types[].storage instead. */ - storage?: BlueprintStorageConfig; + /** App-level storage configuration array. Each entry creates a storage_module (membership_type = NULL) with its own tables and settings. For entity-scoped storage, use entity_types[].storage instead. */ + storage?: BlueprintStorageConfig[]; /** Achievement definitions. Each entry creates a level with requirements and optional rewards in the events_module. Requires events_module to be provisioned (e.g., via entity_types[].has_levels = true or modules includes events_module). */ achievements?: BlueprintAchievement[]; } diff --git a/packages/node-type-registry/src/codegen/generate-types.ts b/packages/node-type-registry/src/codegen/generate-types.ts index af6ef0dce..f902de99b 100644 --- a/packages/node-type-registry/src/codegen/generate-types.ts +++ b/packages/node-type-registry/src/codegen/generate-types.ts @@ -787,6 +787,10 @@ function buildBlueprintBucketSeed(): t.ExportNamedDeclaration { function buildBlueprintStorageConfig(): t.ExportNamedDeclaration { return addJSDoc( exportInterface('BlueprintStorageConfig', [ + addJSDoc( + optionalProp('storage_key', t.tsStringKeyword()), + 'Discriminator for multi-module storage. Defaults to "default" (omitted from table names). Non-default keys appear as an infix: {prefix}_{storage_key}_buckets. Max 16 chars, lowercase snake_case.' + ), addJSDoc( optionalProp( 'buckets', @@ -794,7 +798,7 @@ function buildBlueprintStorageConfig(): t.ExportNamedDeclaration { 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).' + 'Initial bucket seed entries. Each creates a row in {prefix}_buckets during provisioning.' ), addJSDoc( optionalProp('upload_url_expiry_seconds', t.tsNumberKeyword()), @@ -1024,10 +1028,7 @@ function buildBlueprintEntityType(): 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 tables) for this entity type. Defaults to false.' - ), + addJSDoc( optionalProp('has_invites', t.tsBooleanKeyword()), 'Whether to provision entity-scoped invite tables ({prefix}_invites, {prefix}_claimed_invites) and a submit_{prefix}_invite_code() function. Defaults to false.' @@ -1050,9 +1051,11 @@ function buildBlueprintEntityType(): t.ExportNamedDeclaration { addJSDoc( optionalProp( 'storage', - t.tsTypeReference(t.identifier('BlueprintStorageConfig')) + t.tsArrayType( + 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).' + 'Storage module configuration array. Each entry provisions a separate storage module with its own tables, RLS, and settings. When non-empty, has_storage is derived as true. Each entry may specify a storage_key for multi-module support (defaults to "default").' ) ]), 'An entity 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.' @@ -1187,9 +1190,11 @@ function buildBlueprintDefinition(): t.ExportNamedDeclaration { addJSDoc( optionalProp( 'storage', - t.tsTypeReference(t.identifier('BlueprintStorageConfig')) + t.tsArrayType( + t.tsTypeReference(t.identifier('BlueprintStorageConfig')) + ) ), - 'App-level storage configuration. Creates a storage_module (membership_type = NULL), seeds initial buckets, and overrides module-level settings (expiry times, file size limits, CORS). Use provisions for per-table policy overrides. For entity-scoped storage, use entity_types[].has_storage + entity_types[].storage instead.' + 'App-level storage configuration array. Each entry creates a storage_module (membership_type = NULL) with its own tables and settings. For entity-scoped storage, use entity_types[].storage instead.' ), addJSDoc( optionalProp( diff --git a/packages/node-type-registry/src/module-presets/b2b-storage.ts b/packages/node-type-registry/src/module-presets/b2b-storage.ts index ac2b26925..8ae108321 100644 --- a/packages/node-type-registry/src/module-presets/b2b-storage.ts +++ b/packages/node-type-registry/src/module-presets/b2b-storage.ts @@ -20,9 +20,9 @@ export const PresetB2bStorage: ModulePreset = { 'hierarchy), plus `storage_module` for file uploads. The storage module creates ' + '`app_buckets` and `app_files` tables with full RLS: AuthzPublishable for public reads, ' + 'AuthzAppMembership for member access, AuthzDirectOwner for uploader-only modify/delete. ' + - 'Entity-type provisioning with `has_storage=true` adds per-scope storage tables ' + - 'automatically. Choose this when your B2B app needs file uploads, avatars, attachments, ' + - 'or any object storage tied to workspaces.', + 'Entity-type provisioning with a non-empty `storage` array adds per-scope storage tables ' + + 'automatically (multiple modules per entity via storage_key). Choose this when your B2B ' + + 'app needs file uploads, avatars, attachments, or any object storage tied to workspaces.', good_for: [ 'B2B SaaS with file uploads (documents, avatars, attachments)', 'Apps where storage is scoped to orgs/workspaces', @@ -65,7 +65,7 @@ export const PresetB2bStorage: ModulePreset = { 'devices_module' ], includes_notes: { - storage_module: 'File upload infrastructure: app_buckets + app_files tables with RLS. Entity-type storage scopes layered on top via `has_storage=true`.', + storage_module: 'File upload infrastructure: app_buckets + app_files tables with RLS. Entity-type storage scopes layered on top via the `storage` array (array-only format, supports multiple modules per entity via storage_key).', devices_module: 'Device tracking and trusted-device MFA bypass.' }, omits_notes: { From bfbcc36a78bfde4938fdbec792af882f6ed82bf5 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Tue, 19 May 2026 00:07:05 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20regenerate=20blueprint=20types=20fro?= =?UTF-8?q?m=20main=20base=20=E2=80=94=20storage=20changes=20only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Start from main's generated file and apply only the storage-related changes to avoid picking up unmerged node type definitions (LimitWarning*, EventReferral, etc.) that don't have params generated. --- .../src/blueprint-types.generated.ts | 130 ++++++++++-------- 1 file changed, 70 insertions(+), 60 deletions(-) diff --git a/packages/node-type-registry/src/blueprint-types.generated.ts b/packages/node-type-registry/src/blueprint-types.generated.ts index f6945906c..d6501424e 100644 --- a/packages/node-type-registry/src/blueprint-types.generated.ts +++ b/packages/node-type-registry/src/blueprint-types.generated.ts @@ -35,6 +35,23 @@ export interface TriggerCondition { /** Negated condition. */ NOT?: TriggerCondition; } +/** + * =========================================================================== + * Billing node type parameters + * =========================================================================== + */ +; +/** Declaratively attaches billing usage-recording triggers to a table. On INSERT the named meter is incremented via record_usage; on DELETE it is decremented (reversal). On UPDATE, if the entity_field changes, the old entity is decremented and the new entity is incremented. Requires a provisioned billing_module for the target database. */ +export interface BillingMeterParams { + /* Slug of the billing meter to record usage against (must match a meters table entry, e.g. "databases", "seats") */ + meter_slug: string; + /* Column on the target table that holds the entity id for billing */ + entity_field?: string; + /* Units to record per event (default 1) */ + quantity?: number; + /* Which DML events to attach triggers for */ + events?: ('INSERT' | 'DELETE' | 'UPDATE')[]; +} /** * =========================================================================== * Check node type parameters @@ -199,9 +216,9 @@ export interface DataPeoplestampsParams { /** Adds publish state columns (is_published, published_at) for content visibility. Enables AuthzPublishable and AuthzTemporal authorization. */ export interface DataPublishableParams { /* Column name for the published boolean flag */ - is_published_field_name?: string; + is_published_field?: string; /* Column name for the publish timestamp */ - published_at_field_name?: string; + published_at_field?: string; /* If true, also adds a UUID primary key column with auto-generation */ include_id?: boolean; } @@ -265,6 +282,41 @@ export type TableOrganizationSettingsParams = {}; export type TableUserProfilesParams = {}; /** Creates a user settings table for user-specific configuration. Uses AuthzDirectOwner for access control. */ export type TableUserSettingsParams = {}; +/** + * =========================================================================== + * Limit node type parameters + * =========================================================================== + */ +; +/** Declaratively attaches aggregate limit-tracking triggers to a table. On INSERT the named limit is incremented per entity; on DELETE it is decremented. Uses org_limit_aggregates_inc/dec for per-entity (org-level) aggregate limits rather than per-user limits. Requires a provisioned limits_module for the target database. */ +export interface LimitAggregateParams { + /* Name of the aggregate limit to track (must match a default_limits entry, e.g. "databases", "members") */ + limit_name: string; + /* Column on the target table that holds the entity id for aggregate limit lookup */ + entity_field?: string; + /* Which DML events to attach triggers for */ + events?: ('INSERT' | 'DELETE' | 'UPDATE')[]; +} +/** Gates a table behind a feature flag backed by the cap tables. Attaches a BEFORE INSERT trigger that checks whether the named feature cap value is > 0. Features are modeled as caps with max=0 (disabled) or max=1 (enabled) in limit_caps / limit_caps_defaults tables. Resolution: COALESCE(per-entity cap, scope default, 0). */ +export interface LimitFeatureFlagParams { + /* Cap name representing this feature (must match a limit_caps_defaults entry with max=0 or max=1) */ + feature_name: string; + /* Feature scope: "app" (membership_type=1, app-level caps) or "org" (membership_type=2, per-entity caps) */ + scope?: 'app' | 'org'; + /* Column on the target table that holds the entity id for per-entity cap lookups (only used for org scope) */ + entity_field?: string; +} +/** Declaratively attaches limit-tracking triggers to a table. On INSERT the named limit is incremented; on DELETE it is decremented. Requires a provisioned limits_module for the target scope. */ +export interface LimitCounterParams { + /* Name of the limit to track (must match a default_limits entry, e.g. "projects", "members") */ + limit_name: string; + /* Limit scope: "app" (membership_type=1, user-level) or "org" (membership_type=2, entity-level) */ + scope?: 'app' | 'org'; + /* Column on the target table that holds the actor or entity id used for limit lookup */ + actor_field?: string; + /* Which DML events to attach triggers for */ + events?: ('INSERT' | 'DELETE' | 'UPDATE')[]; +} /** * =========================================================================== * Search node type parameters @@ -367,8 +419,6 @@ export interface SearchUnifiedParams { index_method?: 'hnsw' | 'ivfflat'; metric?: 'cosine' | 'l2' | 'ip'; source_fields?: string[]; - /* Embedding model identifier. When null, the worker falls back to runtime config. */embedding_model?: string; - /* Embedding provider name. When null, the worker falls back to runtime config. */embedding_provider?: 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; @@ -415,10 +465,6 @@ export interface SearchVectorParams { }; /* Column names that feed the embedding. Used by stale trigger to detect content changes. */ source_fields?: string[]; - /* Embedding model identifier (e.g. "nomic-embed-text", "text-embedding-3-small"). Included in the job payload so the worker knows which model to use. When null, the worker falls back to runtime config (llm_module / env vars). */ - embedding_model?: string; - /* Embedding provider name (e.g. "ollama", "openai"). When null, the worker falls back to runtime config. */ - embedding_provider?: string; /* Auto-create trigger that enqueues embedding generation jobs */ enqueue_job?: boolean; /* Task identifier for the job queue */ @@ -499,16 +545,10 @@ export interface ProcessChunksParams { dimensions?: number; /* Distance metric for the HNSW index on chunk embeddings */ metric?: 'cosine' | 'l2' | 'ip'; - /* Embedding model identifier for per-chunk embeddings. When null, the worker falls back to runtime config (llm_module / env vars). */ - embedding_model?: string; - /* Embedding provider name (e.g. "ollama", "openai"). When null, the worker falls back to runtime config. */ - embedding_provider?: string; /* Override the chunks table name. Defaults to {parent_table}_chunks. */ chunks_table_name?: string; /* Field names from the parent table to copy into chunk metadata */ metadata_fields?: string[]; - /* Text search indexes to create on the chunks content column. Omit to mirror the parent table's text search indexes. Set explicitly to override (e.g. ["fulltext", "bm25"]). */ - search_indexes?: ('fulltext' | 'bm25' | 'trigram')[]; /* Whether to create a job trigger that auto-enqueues chunking on parent INSERT/UPDATE */ enqueue_chunking_job?: boolean; /* Task identifier for the chunking job queue */ @@ -528,10 +568,6 @@ export interface ProcessFileEmbeddingParams { index_options?: { [key: string]: unknown; }; - /* Embedding model identifier (e.g. "nomic-embed-text", "text-embedding-3-small", "clip-vit-base-patch32"). Included in the job payload so the worker knows which model to use. When null, the worker falls back to runtime config (llm_module / env vars). */ - embedding_model?: string; - /* Embedding provider name (e.g. "ollama", "openai"). When null, the worker falls back to runtime config. */ - embedding_provider?: string; /* MIME type LIKE patterns to match. Multiple patterns are OR'd together. Examples: ['image/%'], ['application/pdf', 'text/%'], ['audio/%']. */ mime_patterns?: string[]; /* Job task identifier for the worker. In direct mode this is the embedding worker; in extract mode this is the extraction worker. */ @@ -542,7 +578,7 @@ export interface ProcessFileEmbeddingParams { payload_custom?: { [key: string]: unknown; }; - /* Additional compound conditions beyond auto-generated filtering. Merged with the auto-generated conditions via AND. */ + /* Additional compound conditions beyond MIME filtering. Merged with the auto-generated MIME conditions via AND. Use this to add status checks, field guards, etc. */ trigger_conditions?: TriggerCondition | TriggerCondition[]; /* Text extraction configuration. When present, the generator creates extraction output fields on the table and configures SearchVector with source_fields + stale tracking. When absent, the node operates in direct mode (single vector per file, no text extraction). */ extraction?: { @@ -558,7 +594,6 @@ export interface ProcessFileEmbeddingParams { /* Number of overlapping characters between consecutive chunks */chunk_overlap?: number; /* Strategy for splitting text into chunks */chunk_strategy?: 'fixed' | 'sentence' | 'paragraph' | 'semantic'; /* Field names from parent to copy into chunk metadata */metadata_fields?: string[]; - /* Text search indexes to create on the chunks content column. Omit to mirror the parent table's text search indexes. Set explicitly to override. */search_indexes?: ('fulltext' | 'bm25' | 'trigram')[]; /* Whether to auto-enqueue a chunking job on insert/update */enqueue_chunking_job?: boolean; /* Task identifier for the chunking job queue */chunking_task_name?: string; }; @@ -577,10 +612,6 @@ export interface ProcessImageEmbeddingParams { index_options?: { [key: string]: unknown; }; - /* Embedding model identifier (e.g. "clip-vit-base-patch32"). Included in the job payload so the worker knows which model to use. When null, the worker falls back to runtime config (llm_module / env vars). */ - embedding_model?: string; - /* Embedding provider name (e.g. "ollama", "openai"). When null, the worker falls back to runtime config. */ - embedding_provider?: string; /* MIME type LIKE patterns to match. Multiple patterns are OR'd together. */ mime_patterns?: string[]; /* Job task identifier for the image embedding worker */ @@ -591,7 +622,7 @@ export interface ProcessImageEmbeddingParams { payload_custom?: { [key: string]: unknown; }; - /* Additional compound conditions beyond auto-generated filtering. Merged with the auto-generated conditions via AND. */ + /* Additional compound conditions beyond MIME filtering. Merged with the auto-generated MIME conditions via AND. */ trigger_conditions?: TriggerCondition | TriggerCondition[]; /* Text extraction configuration. Forwarded to ProcessFileEmbedding. When present, enables extract mode (e.g., OCR for images). */ extraction?: { @@ -617,10 +648,6 @@ export interface ProcessExtractionParams { text_field?: string; /* JSONB field for extraction metadata (page count, language, etc.) */ metadata_field?: string; - /* Extraction model identifier (e.g. a vision model for OCR, an LLM for structured extraction). Included in the job payload so the worker knows which model to use. When null, the worker falls back to runtime config. */ - extraction_model?: string; - /* Extraction provider name (e.g. "ollama", "openai"). When null, the worker falls back to runtime config. */ - extraction_provider?: string; /* MIME type LIKE patterns to match. Multiple patterns are OR'd together. Examples: ['application/pdf', 'text/%'], ['application/vnd.openxmlformats%']. */ mime_patterns?: string[]; /* Job task identifier for the extraction worker */ @@ -631,7 +658,7 @@ export interface ProcessExtractionParams { payload_custom?: { [key: string]: unknown; }; - /* Additional compound conditions beyond auto-generated filtering. Merged with the auto-generated conditions via AND. */ + /* Additional compound conditions beyond MIME filtering. Merged with the auto-generated MIME conditions via AND. Use this to add status checks (e.g., status = 'uploaded'). */ trigger_conditions?: TriggerCondition | TriggerCondition[]; /* Job queue name for extraction tasks */ queue_name?: string; @@ -661,7 +688,7 @@ export interface ProcessImageVersionsParams { payload_custom?: { [key: string]: unknown; }; - /* Additional compound conditions beyond auto-generated filtering. Merged with the auto-generated conditions via AND. */ + /* Additional compound conditions beyond MIME filtering. Merged with the auto-generated MIME conditions via AND. */ trigger_conditions?: TriggerCondition | TriggerCondition[]; /* Job queue name for image processing tasks */ queue_name?: string; @@ -1275,6 +1302,7 @@ export interface BlueprintEntityType { has_profiles?: boolean; /** Whether to provision a levels module for this entity type. Defaults to false. */ has_levels?: boolean; + /** Whether to provision entity-scoped invite tables ({prefix}_invites, {prefix}_claimed_invites) and a submit_{prefix}_invite_code() function. Defaults to false. */ has_invites?: boolean; /** Whether to auto-attach an EventTracker to the claimed_invites table for invite-based achievements. Requires has_invites=true AND has_levels=true. When true, records 'invite_claimed' events credited to the sender (inviter) on each claimed invite. Defaults to false. */ @@ -1283,7 +1311,7 @@ export interface BlueprintEntityType { 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 module configuration array. Each entry provisions a separate storage module with its own tables, RLS, and settings. When non-empty, has_storage is derived as true. Each entry may specify a storage_key for multi-module support (defaults to "default"). */ + /** Storage configuration (array-only). A non-empty array enables storage provisioning. Each entry creates a separate storage module with its own tables ({prefix}_{storage_key}_buckets/files). Controls RLS policies, bucket seeding, and module-level settings. */ storage?: BlueprintStorageConfig[]; } /** @@ -1293,7 +1321,7 @@ export interface BlueprintEntityType { */ ; /** String shorthand -- just the node type name. */ -export type BlueprintNodeShorthand = 'AuthzAllowAll' | 'AuthzAppMembership' | 'AuthzComposite' | 'AuthzDenyAll' | 'AuthzFilePath' | 'AuthzDirectOwner' | 'AuthzDirectOwnerAny' | 'AuthzEntityMembership' | 'AuthzMemberList' | 'AuthzNotReadOnly' | 'AuthzOrgHierarchy' | 'AuthzPeerOwnership' | 'AuthzPublishable' | 'AuthzRelatedEntityMembership' | 'AuthzRelatedMemberList' | 'AuthzRelatedPeerOwnership' | 'AuthzTemporal' | 'CheckGreaterThan' | 'CheckLessThan' | 'CheckNotEqual' | 'CheckOneOf' | 'LimitEnforceAggregate' | 'LimitTrackUsage' | 'DataBulk' | 'ProcessChunks' | 'DataCompositeField' | 'DataDirectOwner' | 'DataEntityMembership' | 'EventReferral' | 'EventTracker' | 'ProcessFileEmbedding' | 'LimitEnforceFeature' | 'DataForceCurrentUser' | 'DataId' | 'ProcessImageEmbedding' | 'DataImmutableFields' | 'DataInflection' | 'DataInheritFromParent' | 'JobTrigger' | 'LimitEnforceCounter' | 'LimitEnforceRate' | 'LimitWarningCounter' | 'LimitWarningAggregate' | 'LimitWarningRate' | 'DataJsonb' | 'DataOwnedFields' | 'ProcessExtraction' | 'ProcessImageVersions' | 'DataOwnershipInEntity' | 'DataPeoplestamps' | 'DataPublishable' | 'DataRealtime' | 'DataSlug' | 'DataSoftDelete' | 'DataStatusField' | 'DataTags' | 'DataTimestamps' | 'SearchBm25' | 'SearchFullText' | 'SearchSpatial' | 'SearchSpatialAggregate' | 'SearchTrgm' | 'SearchUnified' | 'SearchVector' | 'TableOrganizationSettings' | 'TableUserProfiles' | 'TableUserSettings'; +export type BlueprintNodeShorthand = 'AuthzAllowAll' | 'AuthzAppMembership' | 'AuthzComposite' | 'AuthzDenyAll' | 'AuthzFilePath' | 'AuthzDirectOwner' | 'AuthzDirectOwnerAny' | 'AuthzEntityMembership' | 'AuthzMemberList' | 'AuthzNotReadOnly' | 'AuthzOrgHierarchy' | 'AuthzPeerOwnership' | 'AuthzPublishable' | 'AuthzRelatedEntityMembership' | 'AuthzRelatedMemberList' | 'AuthzRelatedPeerOwnership' | 'AuthzTemporal' | 'CheckGreaterThan' | 'CheckLessThan' | 'CheckNotEqual' | 'CheckOneOf' | 'LimitAggregate' | 'BillingMeter' | 'DataBulk' | 'ProcessChunks' | 'DataCompositeField' | 'DataDirectOwner' | 'DataEntityMembership' | 'ProcessFileEmbedding' | 'LimitFeatureFlag' | 'DataForceCurrentUser' | 'DataId' | 'ProcessImageEmbedding' | 'DataImmutableFields' | 'DataInflection' | 'DataInheritFromParent' | 'JobTrigger' | 'LimitCounter' | 'DataJsonb' | 'DataOwnedFields' | 'ProcessExtraction' | 'ProcessImageVersions' | 'DataOwnershipInEntity' | 'DataPeoplestamps' | 'DataPublishable' | 'DataRealtime' | 'DataSlug' | 'DataSoftDelete' | 'DataStatusField' | 'DataTags' | 'DataTimestamps' | 'SearchBm25' | 'SearchFullText' | 'SearchSpatial' | 'SearchSpatialAggregate' | 'SearchTrgm' | 'SearchUnified' | 'SearchVector' | 'TableOrganizationSettings' | 'TableUserProfiles' | 'TableUserSettings'; /** Object form -- { $type, data } with typed parameters. */ export type BlueprintNodeObject = { $type: 'AuthzAllowAll'; @@ -1359,11 +1387,11 @@ export type BlueprintNodeObject = { $type: 'CheckOneOf'; data: CheckOneOfParams; } | { - $type: 'LimitEnforceAggregate'; - data: LimitEnforceAggregateParams; + $type: 'LimitAggregate'; + data: LimitAggregateParams; } | { - $type: 'LimitTrackUsage'; - data: LimitTrackUsageParams; + $type: 'BillingMeter'; + data: BillingMeterParams; } | { $type: 'DataBulk'; data: DataBulkParams; @@ -1379,18 +1407,12 @@ export type BlueprintNodeObject = { } | { $type: 'DataEntityMembership'; data: DataEntityMembershipParams; -} | { - $type: 'EventReferral'; - data: EventReferralParams; -} | { - $type: 'EventTracker'; - data: EventTrackerParams; } | { $type: 'ProcessFileEmbedding'; data: ProcessFileEmbeddingParams; } | { - $type: 'LimitEnforceFeature'; - data: LimitEnforceFeatureParams; + $type: 'LimitFeatureFlag'; + data: LimitFeatureFlagParams; } | { $type: 'DataForceCurrentUser'; data: DataForceCurrentUserParams; @@ -1413,20 +1435,8 @@ export type BlueprintNodeObject = { $type: 'JobTrigger'; data: JobTriggerParams; } | { - $type: 'LimitEnforceCounter'; - data: LimitEnforceCounterParams; -} | { - $type: 'LimitEnforceRate'; - data: LimitEnforceRateParams; -} | { - $type: 'LimitWarningCounter'; - data: LimitWarningCounterParams; -} | { - $type: 'LimitWarningAggregate'; - data: LimitWarningAggregateParams; -} | { - $type: 'LimitWarningRate'; - data: LimitWarningRateParams; + $type: 'LimitCounter'; + data: LimitCounterParams; } | { $type: 'DataJsonb'; data: DataJsonbParams; @@ -1585,7 +1595,7 @@ export interface BlueprintDefinition { unique_constraints?: BlueprintUniqueConstraint[]; /** Entity types to provision in Phase 0 (before tables). Each entry creates an entity table with membership modules and security. */ entity_types?: BlueprintEntityType[]; - /** App-level storage configuration array. Each entry creates a storage_module (membership_type = NULL) with its own tables and settings. For entity-scoped storage, use entity_types[].storage instead. */ + /** App-level storage configuration (array-only). Creates storage_module(s) (membership_type = NULL), seeds initial buckets, and overrides module-level settings. Each entry creates a separate storage module. For entity-scoped storage, use entity_types[].storage instead. */ storage?: BlueprintStorageConfig[]; /** Achievement definitions. Each entry creates a level with requirements and optional rewards in the events_module. Requires events_module to be provisioned (e.g., via entity_types[].has_levels = true or modules includes events_module). */ achievements?: BlueprintAchievement[];