diff --git a/apps/sim/app/api/workflows/[id]/route.ts b/apps/sim/app/api/workflows/[id]/route.ts index 7221f4c5b50..062a0e95e52 100644 --- a/apps/sim/app/api/workflows/[id]/route.ts +++ b/apps/sim/app/api/workflows/[id]/route.ts @@ -2,6 +2,7 @@ import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { createLogger } from '@/lib/logs/console-logger' +import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers' import { db } from '@/db' import { workflow, workspaceMember } from '@/db/schema' @@ -10,6 +11,7 @@ const logger = createLogger('WorkflowByIdAPI') /** * GET /api/workflows/[id] * Fetch a single workflow by ID + * Uses hybrid approach: try normalized tables first, fallback to JSON blob */ export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { const requestId = crypto.randomUUID().slice(0, 8) @@ -69,10 +71,43 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ error: 'Access denied' }, { status: 403 }) } + // Try to load from normalized tables first + const normalizedData = await loadWorkflowFromNormalizedTables(workflowId) + + const finalWorkflowData = { ...workflowData } + + if (normalizedData) { + // Use normalized table data - reconstruct complete state object + // First get any existing state properties, then override with normalized data + const existingState = + workflowData.state && typeof workflowData.state === 'object' ? workflowData.state : {} + + finalWorkflowData.state = { + // Default values for expected properties + deploymentStatuses: {}, + hasActiveSchedule: false, + hasActiveWebhook: false, + // Preserve any existing state properties + ...existingState, + // Override with normalized data (this takes precedence) + blocks: normalizedData.blocks, + edges: normalizedData.edges, + loops: normalizedData.loops, + parallels: normalizedData.parallels, + lastSaved: Date.now(), + isDeployed: workflowData.isDeployed || false, + deployedAt: workflowData.deployedAt, + } + logger.info(`[${requestId}] Loaded workflow ${workflowId} from normalized tables`) + } else { + // Fallback to JSON blob + logger.info(`[${requestId}] Using JSON blob for workflow ${workflowId}`) + } + const elapsed = Date.now() - startTime logger.info(`[${requestId}] Successfully fetched workflow ${workflowId} in ${elapsed}ms`) - return NextResponse.json({ data: workflowData }, { status: 200 }) + return NextResponse.json({ data: finalWorkflowData }, { status: 200 }) } catch (error: any) { const elapsed = Date.now() - startTime logger.error(`[${requestId}] Error fetching workflow ${workflowId} after ${elapsed}ms`, error) diff --git a/apps/sim/app/api/workflows/sync/route.ts b/apps/sim/app/api/workflows/sync/route.ts index de6c8fad76d..afa0a43365f 100644 --- a/apps/sim/app/api/workflows/sync/route.ts +++ b/apps/sim/app/api/workflows/sync/route.ts @@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { getSession } from '@/lib/auth' import { createLogger } from '@/lib/logs/console-logger' +import { saveWorkflowToNormalizedTables } from '@/lib/workflows/db-helpers' import { db } from '@/db' import { workflow, workspace, workspaceMember } from '@/db/schema' @@ -386,6 +387,34 @@ export async function POST(req: NextRequest) { // Ensure the workflow has the correct workspaceId const effectiveWorkspaceId = clientWorkflow.workspaceId || workspaceId + // Save to normalized tables for all workflows (hybrid approach) + const normalizedResult = await saveWorkflowToNormalizedTables(id, { + blocks: clientWorkflow.state.blocks || {}, + edges: clientWorkflow.state.edges || [], + loops: clientWorkflow.state.loops || {}, + parallels: clientWorkflow.state.parallels || {}, + lastSaved: clientWorkflow.state.lastSaved, + isDeployed: clientWorkflow.state.isDeployed, + deployedAt: clientWorkflow.state.deployedAt, + deploymentStatuses: (clientWorkflow.state as any).deploymentStatuses || {}, + hasActiveSchedule: (clientWorkflow.state as any).hasActiveSchedule, + hasActiveWebhook: (clientWorkflow.state as any).hasActiveWebhook, + }) + + // Use the JSON blob from normalized save for compatibility, or fallback to original state + const stateToSave = + normalizedResult.success && normalizedResult.jsonBlob + ? normalizedResult.jsonBlob + : clientWorkflow.state + + if (normalizedResult.success) { + logger.info(`[${requestId}] Saved workflow ${id} to normalized tables`) + } else { + logger.warn( + `[${requestId}] Failed to save workflow ${id} to normalized tables: ${normalizedResult.error}` + ) + } + if (!dbWorkflow) { // New workflow - create (state is required by schema) operations.push( @@ -397,7 +426,7 @@ export async function POST(req: NextRequest) { name: clientWorkflow.name, description: clientWorkflow.description, color: clientWorkflow.color, - state: clientWorkflow.state, + state: stateToSave, marketplaceData: clientWorkflow.marketplaceData || null, lastSynced: now, createdAt: now, @@ -451,8 +480,8 @@ export async function POST(req: NextRequest) { } // Always update state since we only sync the active workflow with valid state - if (JSON.stringify(dbWorkflow.state) !== JSON.stringify(clientWorkflow.state)) { - updateData.state = clientWorkflow.state + if (JSON.stringify(dbWorkflow.state) !== JSON.stringify(stateToSave)) { + updateData.state = stateToSave needsUpdate = true } diff --git a/apps/sim/db/migrations/0043_silent_the_anarchist.sql b/apps/sim/db/migrations/0043_silent_the_anarchist.sql new file mode 100644 index 00000000000..7efb7998296 --- /dev/null +++ b/apps/sim/db/migrations/0043_silent_the_anarchist.sql @@ -0,0 +1,58 @@ +CREATE TABLE "workflow_blocks" ( + "id" text PRIMARY KEY NOT NULL, + "workflow_id" text NOT NULL, + "type" text NOT NULL, + "name" text NOT NULL, + "position_x" integer NOT NULL, + "position_y" integer NOT NULL, + "enabled" boolean DEFAULT true NOT NULL, + "horizontal_handles" boolean DEFAULT true NOT NULL, + "is_wide" boolean DEFAULT false NOT NULL, + "height" integer DEFAULT 0 NOT NULL, + "sub_blocks" jsonb DEFAULT '{}' NOT NULL, + "outputs" jsonb DEFAULT '{}' NOT NULL, + "data" jsonb DEFAULT '{}', + "parent_id" text, + "extent" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "workflow_edges" ( + "id" text PRIMARY KEY NOT NULL, + "workflow_id" text NOT NULL, + "source_block_id" text NOT NULL, + "target_block_id" text NOT NULL, + "source_handle" text, + "target_handle" text, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "workflow_subflows" ( + "id" text PRIMARY KEY NOT NULL, + "workflow_id" text NOT NULL, + "type" text NOT NULL, + "config" jsonb DEFAULT '{}' NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "workflow_blocks" ADD CONSTRAINT "workflow_blocks_workflow_id_workflow_id_fk" FOREIGN KEY ("workflow_id") REFERENCES "public"."workflow"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "workflow_blocks" ADD CONSTRAINT "workflow_blocks_parent_id_workflow_blocks_id_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."workflow_blocks"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "workflow_edges" ADD CONSTRAINT "workflow_edges_workflow_id_workflow_id_fk" FOREIGN KEY ("workflow_id") REFERENCES "public"."workflow"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "workflow_edges" ADD CONSTRAINT "workflow_edges_source_block_id_workflow_blocks_id_fk" FOREIGN KEY ("source_block_id") REFERENCES "public"."workflow_blocks"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "workflow_edges" ADD CONSTRAINT "workflow_edges_target_block_id_workflow_blocks_id_fk" FOREIGN KEY ("target_block_id") REFERENCES "public"."workflow_blocks"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "workflow_subflows" ADD CONSTRAINT "workflow_subflows_workflow_id_workflow_id_fk" FOREIGN KEY ("workflow_id") REFERENCES "public"."workflow"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "workflow_blocks_workflow_id_idx" ON "workflow_blocks" USING btree ("workflow_id");--> statement-breakpoint +CREATE INDEX "workflow_blocks_parent_id_idx" ON "workflow_blocks" USING btree ("parent_id");--> statement-breakpoint +CREATE INDEX "workflow_blocks_workflow_parent_idx" ON "workflow_blocks" USING btree ("workflow_id","parent_id");--> statement-breakpoint +CREATE INDEX "workflow_blocks_workflow_type_idx" ON "workflow_blocks" USING btree ("workflow_id","type");--> statement-breakpoint +CREATE INDEX "workflow_edges_workflow_id_idx" ON "workflow_edges" USING btree ("workflow_id");--> statement-breakpoint +CREATE INDEX "workflow_edges_source_block_idx" ON "workflow_edges" USING btree ("source_block_id");--> statement-breakpoint +CREATE INDEX "workflow_edges_target_block_idx" ON "workflow_edges" USING btree ("target_block_id");--> statement-breakpoint +CREATE INDEX "workflow_edges_workflow_source_idx" ON "workflow_edges" USING btree ("workflow_id","source_block_id");--> statement-breakpoint +CREATE INDEX "workflow_edges_workflow_target_idx" ON "workflow_edges" USING btree ("workflow_id","target_block_id");--> statement-breakpoint +CREATE INDEX "workflow_edges_source_block_fk_idx" ON "workflow_edges" USING btree ("source_block_id");--> statement-breakpoint +CREATE INDEX "workflow_edges_target_block_fk_idx" ON "workflow_edges" USING btree ("target_block_id");--> statement-breakpoint +CREATE INDEX "workflow_subflows_workflow_id_idx" ON "workflow_subflows" USING btree ("workflow_id");--> statement-breakpoint +CREATE INDEX "workflow_subflows_workflow_type_idx" ON "workflow_subflows" USING btree ("workflow_id","type"); \ No newline at end of file diff --git a/apps/sim/db/migrations/meta/0043_snapshot.json b/apps/sim/db/migrations/meta/0043_snapshot.json new file mode 100644 index 00000000000..d5b4c51c700 --- /dev/null +++ b/apps/sim/db/migrations/meta/0043_snapshot.json @@ -0,0 +1,3572 @@ +{ + "id": "05e26e13-3a24-4e29-b48b-e70b156e7434", + "prevId": "5a104de1-5afa-46be-bbe8-5a8759024b15", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subdomain": { + "name": "subdomain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "subdomain_idx": { + "name": "subdomain_idx", + "columns": [ + { + "expression": "subdomain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_hash": { + "name": "file_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_file_hash_idx": { + "name": "doc_file_hash_idx", + "columns": [ + { + "expression": "file_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_uploaded_at_idx": { + "name": "doc_kb_uploaded_at_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "uploaded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "overlap_tokens": { + "name": "overlap_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "search_rank": { + "name": "search_rank", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'1.0'" + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "quality_score": { + "name": "quality_score", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_chunk_hash_idx": { + "name": "emb_chunk_hash_idx", + "columns": [ + { + "expression": "chunk_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_access_idx": { + "name": "emb_kb_access_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_accessed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_rank_idx": { + "name": "emb_kb_rank_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "search_rank", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_metadata_gin_idx": { + "name": "emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 100, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.marketplace": { + "name": "marketplace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "marketplace_workflow_id_workflow_id_fk": { + "name": "marketplace_workflow_id_workflow_id_fk", + "tableFrom": "marketplace", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "marketplace_author_id_user_id_fk": { + "name": "marketplace_author_id_user_id_fk", + "tableFrom": "marketplace", + "tableTo": "user", + "columnsFrom": ["author_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workflow_idx": { + "name": "memory_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workflow_key_idx": { + "name": "memory_workflow_key_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workflow_id_workflow_id_fk": { + "name": "memory_workflow_id_workflow_id_fk", + "tableFrom": "memory", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "debug_mode": { + "name": "debug_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auto_fill_env_vars": { + "name": "auto_fill_env_vars", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_notified_user": { + "name": "telemetry_notified_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "general": { + "name": "general", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_idx": { + "name": "path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_state": { + "name": "deployed_state", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "collaborators": { + "name": "collaborators", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "marketplace_data": { + "name": "marketplace_data", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "extent": { + "name": "extent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_parent_id_idx": { + "name": "workflow_blocks_parent_id_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_workflow_parent_idx": { + "name": "workflow_blocks_workflow_parent_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_workflow_type_idx": { + "name": "workflow_blocks_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_source_block_idx": { + "name": "workflow_edges_source_block_idx", + "columns": [ + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_target_block_idx": { + "name": "workflow_edges_target_block_idx", + "columns": [ + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_source_block_fk_idx": { + "name": "workflow_edges_source_block_fk_idx", + "columns": [ + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_target_block_fk_idx": { + "name": "workflow_edges_target_block_fk_idx", + "columns": [ + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_logs": { + "name": "workflow_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "duration": { + "name": "duration", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_logs_workflow_id_workflow_id_fk": { + "name": "workflow_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workflow_schedule_workflow_id_unique": { + "name": "workflow_schedule_workflow_id_unique", + "nullsNotDistinct": false, + "columns": ["workflow_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_invitation": { + "name": "workspace_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_invitation_workspace_id_workspace_id_fk": { + "name": "workspace_invitation_workspace_id_workspace_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invitation_inviter_id_user_id_fk": { + "name": "workspace_invitation_inviter_id_user_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_invitation_token_unique": { + "name": "workspace_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_member": { + "name": "workspace_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_workspace_idx": { + "name": "user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_member_workspace_id_workspace_id_fk": { + "name": "workspace_member_workspace_id_workspace_id_fk", + "tableFrom": "workspace_member", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_member_user_id_user_id_fk": { + "name": "workspace_member_user_id_user_id_fk", + "tableFrom": "workspace_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/apps/sim/db/migrations/meta/_journal.json b/apps/sim/db/migrations/meta/_journal.json index ff79c9ee703..6bf0d597bd7 100644 --- a/apps/sim/db/migrations/meta/_journal.json +++ b/apps/sim/db/migrations/meta/_journal.json @@ -295,6 +295,13 @@ "when": 1749784177503, "tag": "0042_breezy_miracleman", "breakpoints": true + }, + { + "idx": 43, + "version": "7", + "when": 1750193663357, + "tag": "0043_silent_the_anarchist", + "breakpoints": true } ] } diff --git a/apps/sim/db/schema.ts b/apps/sim/db/schema.ts index c0094992c6b..030de3cc93f 100644 --- a/apps/sim/db/schema.ts +++ b/apps/sim/db/schema.ts @@ -131,6 +131,131 @@ export const workflow = pgTable('workflow', { marketplaceData: json('marketplace_data'), }) +// New normalized workflow tables +export const workflowBlocks = pgTable( + 'workflow_blocks', + { + // Primary identification + id: text('id').primaryKey(), // Block UUID from the current JSON structure + workflowId: text('workflow_id') + .notNull() + .references(() => workflow.id, { onDelete: 'cascade' }), // Link to parent workflow + + // Block properties (from current BlockState interface) + type: text('type').notNull(), // e.g., 'starter', 'agent', 'api', 'function' + name: text('name').notNull(), // Display name of the block + + // Position coordinates (from position.x, position.y) + positionX: integer('position_x').notNull(), // X coordinate on canvas + positionY: integer('position_y').notNull(), // Y coordinate on canvas + + // Block behavior flags (from current BlockState) + enabled: boolean('enabled').notNull().default(true), // Whether block is active + horizontalHandles: boolean('horizontal_handles').notNull().default(true), // UI layout preference + isWide: boolean('is_wide').notNull().default(false), // Whether block uses wide layout + height: integer('height').notNull().default(0), // Custom height override + + // Block data (keeping JSON for flexibility as current system does) + subBlocks: jsonb('sub_blocks').notNull().default('{}'), // All subblock configurations + outputs: jsonb('outputs').notNull().default('{}'), // Output type definitions + data: jsonb('data').default('{}'), // Additional block-specific data + + // Hierarchy support (for loop/parallel child blocks) + parentId: text('parent_id'), // Self-reference handled by foreign key constraint in migration + extent: text('extent'), // 'parent' or null - for ReactFlow parent constraint + + // Timestamps + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => ({ + // Primary access pattern: get all blocks for a workflow + workflowIdIdx: index('workflow_blocks_workflow_id_idx').on(table.workflowId), + + // For finding child blocks of a parent (loop/parallel containers) + parentIdIdx: index('workflow_blocks_parent_id_idx').on(table.parentId), + + // Composite index for efficient parent-child queries + workflowParentIdx: index('workflow_blocks_workflow_parent_idx').on( + table.workflowId, + table.parentId + ), + + // For block type filtering/analytics + workflowTypeIdx: index('workflow_blocks_workflow_type_idx').on(table.workflowId, table.type), + }) +) + +export const workflowEdges = pgTable( + 'workflow_edges', + { + // Primary identification + id: text('id').primaryKey(), // Edge UUID from ReactFlow + workflowId: text('workflow_id') + .notNull() + .references(() => workflow.id, { onDelete: 'cascade' }), // Link to parent workflow + + // Connection definition (from ReactFlow Edge interface) + sourceBlockId: text('source_block_id') + .notNull() + .references(() => workflowBlocks.id, { onDelete: 'cascade' }), // Source block ID + targetBlockId: text('target_block_id') + .notNull() + .references(() => workflowBlocks.id, { onDelete: 'cascade' }), // Target block ID + sourceHandle: text('source_handle'), // Specific output handle (optional) + targetHandle: text('target_handle'), // Specific input handle (optional) + + // Timestamps + createdAt: timestamp('created_at').notNull().defaultNow(), + }, + (table) => ({ + // Primary access pattern: get all edges for a workflow + workflowIdIdx: index('workflow_edges_workflow_id_idx').on(table.workflowId), + + // For finding outgoing connections from a block + sourceBlockIdx: index('workflow_edges_source_block_idx').on(table.sourceBlockId), + + // For finding incoming connections to a block + targetBlockIdx: index('workflow_edges_target_block_idx').on(table.targetBlockId), + + // For comprehensive workflow topology queries + workflowSourceIdx: index('workflow_edges_workflow_source_idx').on( + table.workflowId, + table.sourceBlockId + ), + workflowTargetIdx: index('workflow_edges_workflow_target_idx').on( + table.workflowId, + table.targetBlockId + ), + }) +) + +export const workflowSubflows = pgTable( + 'workflow_subflows', + { + // Primary identification + id: text('id').primaryKey(), // Subflow UUID (currently loop/parallel ID) + workflowId: text('workflow_id') + .notNull() + .references(() => workflow.id, { onDelete: 'cascade' }), // Link to parent workflow + + // Subflow type and configuration + type: text('type').notNull(), // 'loop' or 'parallel' (extensible for future types) + config: jsonb('config').notNull().default('{}'), // Type-specific configuration + + // Timestamps + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => ({ + // Primary access pattern: get all subflows for a workflow + workflowIdIdx: index('workflow_subflows_workflow_id_idx').on(table.workflowId), + + // For filtering by subflow type + workflowTypeIdx: index('workflow_subflows_workflow_type_idx').on(table.workflowId, table.type), + }) +) + export const waitlist = pgTable('waitlist', { id: text('id').primaryKey(), email: text('email').notNull().unique(), diff --git a/apps/sim/lib/workflows/db-helpers.test.ts b/apps/sim/lib/workflows/db-helpers.test.ts new file mode 100644 index 00000000000..a092435c73c --- /dev/null +++ b/apps/sim/lib/workflows/db-helpers.test.ts @@ -0,0 +1,775 @@ +/** + * @vitest-environment node + * + * Database Helpers Unit Tests + * + * Tests for normalized table operations including loading, saving, and migrating + * workflow data between JSON blob format and normalized database tables. + */ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { WorkflowState } from '@/stores/workflows/workflow/types' + +// Mock database operations +const mockDb = { + select: vi.fn(), + insert: vi.fn(), + delete: vi.fn(), + transaction: vi.fn(), +} + +// Mock schema objects +const mockWorkflowBlocks = { + workflowId: 'workflowId', + id: 'id', + type: 'type', + name: 'name', + positionX: 'positionX', + positionY: 'positionY', + enabled: 'enabled', + horizontalHandles: 'horizontalHandles', + isWide: 'isWide', + height: 'height', + subBlocks: 'subBlocks', + outputs: 'outputs', + data: 'data', + parentId: 'parentId', + extent: 'extent', +} + +const mockWorkflowEdges = { + workflowId: 'workflowId', + id: 'id', + sourceBlockId: 'sourceBlockId', + targetBlockId: 'targetBlockId', + sourceHandle: 'sourceHandle', + targetHandle: 'targetHandle', +} + +const mockWorkflowSubflows = { + workflowId: 'workflowId', + id: 'id', + type: 'type', + config: 'config', +} + +// Setup mocks before running tests +vi.doMock('@/db', () => ({ + db: mockDb, +})) + +vi.doMock('@/db/schema', () => ({ + workflowBlocks: mockWorkflowBlocks, + workflowEdges: mockWorkflowEdges, + workflowSubflows: mockWorkflowSubflows, +})) + +vi.doMock('drizzle-orm', () => ({ + eq: vi.fn((field, value) => ({ field, value, type: 'eq' })), +})) + +vi.doMock('@/lib/logs/console-logger', () => ({ + createLogger: vi.fn(() => ({ + info: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + })), +})) + +// Test data +const mockWorkflowId = 'test-workflow-123' + +const mockBlocksFromDb = [ + { + id: 'block-1', + workflowId: mockWorkflowId, + type: 'starter', + name: 'Start Block', + positionX: 100, + positionY: 100, + enabled: true, + horizontalHandles: true, + isWide: false, + height: 150, + subBlocks: { input: { id: 'input', type: 'short-input', value: 'test' } }, + outputs: { response: { type: 'string' } }, + data: { parentId: null, extent: null, width: 350 }, + parentId: null, + extent: null, + }, + { + id: 'block-2', + workflowId: mockWorkflowId, + type: 'api', + name: 'API Block', + positionX: 300, + positionY: 100, + enabled: true, + horizontalHandles: true, + isWide: true, + height: 200, + subBlocks: {}, + outputs: {}, + data: { parentId: 'loop-1', extent: 'parent' }, + parentId: 'loop-1', + extent: 'parent', + }, +] + +const mockEdgesFromDb = [ + { + id: 'edge-1', + workflowId: mockWorkflowId, + sourceBlockId: 'block-1', + targetBlockId: 'block-2', + sourceHandle: 'output', + targetHandle: 'input', + }, +] + +const mockSubflowsFromDb = [ + { + id: 'loop-1', + workflowId: mockWorkflowId, + type: 'loop', + config: { + id: 'loop-1', + nodes: ['block-2'], + iterations: 5, + loopType: 'for', + }, + }, + { + id: 'parallel-1', + workflowId: mockWorkflowId, + type: 'parallel', + config: { + id: 'parallel-1', + nodes: ['block-3'], + distribution: ['item1', 'item2'], + }, + }, +] + +const mockWorkflowState: WorkflowState = { + blocks: { + 'block-1': { + id: 'block-1', + type: 'starter', + name: 'Start Block', + position: { x: 100, y: 100 }, + subBlocks: { input: { id: 'input', type: 'short-input', value: 'test' } }, + outputs: { response: { type: 'string' } }, + enabled: true, + horizontalHandles: true, + isWide: false, + height: 150, + data: { width: 350 }, + }, + 'block-2': { + id: 'block-2', + type: 'api', + name: 'API Block', + position: { x: 300, y: 100 }, + subBlocks: {}, + outputs: {}, + enabled: true, + horizontalHandles: true, + isWide: true, + height: 200, + data: { parentId: 'loop-1', extent: 'parent' }, + }, + }, + edges: [ + { + id: 'edge-1', + source: 'block-1', + target: 'block-2', + sourceHandle: 'output', + targetHandle: 'input', + }, + ], + loops: { + 'loop-1': { + id: 'loop-1', + nodes: ['block-2'], + iterations: 5, + loopType: 'for', + }, + }, + parallels: { + 'parallel-1': { + id: 'parallel-1', + nodes: ['block-3'], + distribution: ['item1', 'item2'], + }, + }, + lastSaved: Date.now(), + isDeployed: false, + deploymentStatuses: {}, + hasActiveSchedule: false, + hasActiveWebhook: false, +} + +describe('Database Helpers', () => { + let dbHelpers: typeof import('./db-helpers') + + beforeEach(async () => { + vi.clearAllMocks() + // Import the module after mocks are set up + dbHelpers = await import('./db-helpers') + }) + + afterEach(() => { + vi.resetAllMocks() + }) + + describe('loadWorkflowFromNormalizedTables', () => { + it('should successfully load workflow data from normalized tables', async () => { + // Mock the database queries properly + let callCount = 0 + mockDb.select.mockReturnValue({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockImplementation(() => { + callCount++ + if (callCount === 1) return mockBlocksFromDb // blocks query + if (callCount === 2) return mockEdgesFromDb // edges query + if (callCount === 3) return mockSubflowsFromDb // subflows query + return [] + }), + }), + }) + + const result = await dbHelpers.loadWorkflowFromNormalizedTables(mockWorkflowId) + + expect(result).toBeDefined() + expect(result?.isFromNormalizedTables).toBe(true) + expect(result?.blocks).toBeDefined() + expect(result?.edges).toBeDefined() + expect(result?.loops).toBeDefined() + expect(result?.parallels).toBeDefined() + + // Verify blocks are transformed correctly + expect(result?.blocks['block-1']).toEqual({ + id: 'block-1', + type: 'starter', + name: 'Start Block', + position: { x: 100, y: 100 }, + enabled: true, + horizontalHandles: true, + isWide: false, + height: 150, + subBlocks: { input: { id: 'input', type: 'short-input', value: 'test' } }, + outputs: { response: { type: 'string' } }, + data: { parentId: null, extent: null, width: 350 }, + parentId: null, + extent: null, + }) + + // Verify edges are transformed correctly + expect(result?.edges[0]).toEqual({ + id: 'edge-1', + source: 'block-1', + target: 'block-2', + sourceHandle: 'output', + targetHandle: 'input', + }) + + // Verify loops are transformed correctly + expect(result?.loops['loop-1']).toEqual({ + id: 'loop-1', + nodes: ['block-2'], + iterations: 5, + loopType: 'for', + }) + + // Verify parallels are transformed correctly + expect(result?.parallels['parallel-1']).toEqual({ + id: 'parallel-1', + nodes: ['block-3'], + distribution: ['item1', 'item2'], + }) + }) + + it('should return null when no blocks are found', async () => { + // Mock empty results from all queries + mockDb.select.mockReturnValue({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockResolvedValue([]), + }), + }) + + const result = await dbHelpers.loadWorkflowFromNormalizedTables(mockWorkflowId) + + expect(result).toBeNull() + }) + + it('should return null when database query fails', async () => { + // Mock database error + mockDb.select.mockReturnValue({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockRejectedValue(new Error('Database connection failed')), + }), + }) + + const result = await dbHelpers.loadWorkflowFromNormalizedTables(mockWorkflowId) + + expect(result).toBeNull() + }) + + it('should handle unknown subflow types gracefully', async () => { + const subflowsWithUnknownType = [ + { + id: 'unknown-1', + workflowId: mockWorkflowId, + type: 'unknown-type', + config: { id: 'unknown-1' }, + }, + ] + + // Mock the database queries properly + let callCount = 0 + mockDb.select.mockReturnValue({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockImplementation(() => { + callCount++ + if (callCount === 1) return mockBlocksFromDb // blocks query + if (callCount === 2) return mockEdgesFromDb // edges query + if (callCount === 3) return subflowsWithUnknownType // subflows query + return [] + }), + }), + }) + + const result = await dbHelpers.loadWorkflowFromNormalizedTables(mockWorkflowId) + + expect(result).toBeDefined() + // The function should still return a result but with empty loops and parallels + expect(result?.loops).toEqual({}) + expect(result?.parallels).toEqual({}) + // Verify blocks and edges are still processed correctly + expect(result?.blocks).toBeDefined() + expect(result?.edges).toBeDefined() + }) + + it('should handle malformed database responses', async () => { + const malformedBlocks = [ + { + id: 'block-1', + workflowId: mockWorkflowId, + // Missing required fields + type: null, + name: null, + positionX: 0, + positionY: 0, + enabled: true, + horizontalHandles: true, + isWide: false, + height: 0, + subBlocks: {}, + outputs: {}, + data: {}, + parentId: null, + extent: null, + }, + ] + + // Mock the database queries properly + let callCount = 0 + mockDb.select.mockReturnValue({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockImplementation(() => { + callCount++ + if (callCount === 1) return malformedBlocks // blocks query + if (callCount === 2) return [] // edges query + if (callCount === 3) return [] // subflows query + return [] + }), + }), + }) + + const result = await dbHelpers.loadWorkflowFromNormalizedTables(mockWorkflowId) + + expect(result).toBeDefined() + expect(result?.blocks['block-1']).toBeDefined() + // The function should handle null type and name gracefully + expect(result?.blocks['block-1'].type).toBeNull() + expect(result?.blocks['block-1'].name).toBeNull() + }) + + it('should handle database connection errors gracefully', async () => { + const connectionError = new Error('Connection refused') + ;(connectionError as any).code = 'ECONNREFUSED' + + // Mock database connection error + mockDb.select.mockReturnValue({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockRejectedValue(connectionError), + }), + }) + + const result = await dbHelpers.loadWorkflowFromNormalizedTables(mockWorkflowId) + + expect(result).toBeNull() + }) + }) + + describe('saveWorkflowToNormalizedTables', () => { + it('should successfully save workflow data to normalized tables', async () => { + const mockTransaction = vi.fn().mockImplementation(async (callback) => { + const tx = { + delete: vi.fn().mockReturnValue({ + where: vi.fn().mockResolvedValue([]), + }), + insert: vi.fn().mockReturnValue({ + values: vi.fn().mockResolvedValue([]), + }), + } + return await callback(tx) + }) + + mockDb.transaction = mockTransaction + + const result = await dbHelpers.saveWorkflowToNormalizedTables( + mockWorkflowId, + mockWorkflowState + ) + + expect(result.success).toBe(true) + expect(result.jsonBlob).toBeDefined() + expect(result.jsonBlob.blocks).toEqual(mockWorkflowState.blocks) + expect(result.jsonBlob.edges).toEqual(mockWorkflowState.edges) + expect(result.jsonBlob.loops).toEqual(mockWorkflowState.loops) + expect(result.jsonBlob.parallels).toEqual(mockWorkflowState.parallels) + + // Verify transaction was called + expect(mockTransaction).toHaveBeenCalledTimes(1) + }) + + it('should handle empty workflow state gracefully', async () => { + const emptyWorkflowState: WorkflowState = { + blocks: {}, + edges: [], + loops: {}, + parallels: {}, + lastSaved: Date.now(), + isDeployed: false, + deploymentStatuses: {}, + hasActiveSchedule: false, + hasActiveWebhook: false, + } + + const mockTransaction = vi.fn().mockImplementation(async (callback) => { + const tx = { + delete: vi.fn().mockReturnValue({ + where: vi.fn().mockResolvedValue([]), + }), + insert: vi.fn().mockReturnValue({ + values: vi.fn().mockResolvedValue([]), + }), + } + return await callback(tx) + }) + + mockDb.transaction = mockTransaction + + const result = await dbHelpers.saveWorkflowToNormalizedTables( + mockWorkflowId, + emptyWorkflowState + ) + + expect(result.success).toBe(true) + expect(result.jsonBlob.blocks).toEqual({}) + expect(result.jsonBlob.edges).toEqual([]) + expect(result.jsonBlob.loops).toEqual({}) + expect(result.jsonBlob.parallels).toEqual({}) + }) + + it('should return error when transaction fails', async () => { + const mockTransaction = vi.fn().mockRejectedValue(new Error('Transaction failed')) + mockDb.transaction = mockTransaction + + const result = await dbHelpers.saveWorkflowToNormalizedTables( + mockWorkflowId, + mockWorkflowState + ) + + expect(result.success).toBe(false) + expect(result.error).toBe('Transaction failed') + }) + + it('should handle database constraint errors', async () => { + const constraintError = new Error('Unique constraint violation') + ;(constraintError as any).code = '23505' + + const mockTransaction = vi.fn().mockRejectedValue(constraintError) + mockDb.transaction = mockTransaction + + const result = await dbHelpers.saveWorkflowToNormalizedTables( + mockWorkflowId, + mockWorkflowState + ) + + expect(result.success).toBe(false) + expect(result.error).toBe('Unique constraint violation') + }) + + it('should properly format block data for database insertion', async () => { + let capturedBlockInserts: any[] = [] + let capturedEdgeInserts: any[] = [] + let capturedSubflowInserts: any[] = [] + + const mockTransaction = vi.fn().mockImplementation(async (callback) => { + const tx = { + delete: vi.fn().mockReturnValue({ + where: vi.fn().mockResolvedValue([]), + }), + insert: vi.fn().mockReturnValue({ + values: vi.fn().mockImplementation((data) => { + // Capture the data based on which insert call it is + if (data.length > 0) { + if (data[0].positionX !== undefined) { + capturedBlockInserts = data + } else if (data[0].sourceBlockId !== undefined) { + capturedEdgeInserts = data + } else if (data[0].type === 'loop' || data[0].type === 'parallel') { + capturedSubflowInserts = data + } + } + return Promise.resolve([]) + }), + }), + } + return await callback(tx) + }) + + mockDb.transaction = mockTransaction + + await dbHelpers.saveWorkflowToNormalizedTables(mockWorkflowId, mockWorkflowState) + + expect(capturedBlockInserts).toHaveLength(2) + expect(capturedBlockInserts[0]).toMatchObject({ + id: 'block-1', + workflowId: mockWorkflowId, + type: 'starter', + name: 'Start Block', + positionX: 100, + positionY: 100, + enabled: true, + horizontalHandles: true, + isWide: false, + height: 150, + parentId: null, + extent: null, + }) + + expect(capturedEdgeInserts).toHaveLength(1) + expect(capturedEdgeInserts[0]).toMatchObject({ + id: 'edge-1', + workflowId: mockWorkflowId, + sourceBlockId: 'block-1', + targetBlockId: 'block-2', + sourceHandle: 'output', + targetHandle: 'input', + }) + + expect(capturedSubflowInserts).toHaveLength(2) + expect(capturedSubflowInserts[0]).toMatchObject({ + id: 'loop-1', + workflowId: mockWorkflowId, + type: 'loop', + }) + }) + }) + + describe('workflowExistsInNormalizedTables', () => { + it('should return true when workflow exists in normalized tables', async () => { + mockDb.select.mockReturnValue({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + limit: vi.fn().mockResolvedValue([{ id: 'block-1' }]), + }), + }), + }) + + const result = await dbHelpers.workflowExistsInNormalizedTables(mockWorkflowId) + + expect(result).toBe(true) + }) + + it('should return false when workflow does not exist in normalized tables', async () => { + mockDb.select.mockReturnValue({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + limit: vi.fn().mockResolvedValue([]), + }), + }), + }) + + const result = await dbHelpers.workflowExistsInNormalizedTables(mockWorkflowId) + + expect(result).toBe(false) + }) + + it('should return false when database query fails', async () => { + mockDb.select.mockReturnValue({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + limit: vi.fn().mockRejectedValue(new Error('Database error')), + }), + }), + }) + + const result = await dbHelpers.workflowExistsInNormalizedTables(mockWorkflowId) + + expect(result).toBe(false) + }) + }) + + describe('migrateWorkflowToNormalizedTables', () => { + const mockJsonState = { + blocks: mockWorkflowState.blocks, + edges: mockWorkflowState.edges, + loops: mockWorkflowState.loops, + parallels: mockWorkflowState.parallels, + lastSaved: Date.now(), + isDeployed: false, + deploymentStatuses: {}, + hasActiveSchedule: false, + hasActiveWebhook: false, + } + + it('should successfully migrate workflow from JSON to normalized tables', async () => { + const mockTransaction = vi.fn().mockImplementation(async (callback) => { + const tx = { + delete: vi.fn().mockReturnValue({ + where: vi.fn().mockResolvedValue([]), + }), + insert: vi.fn().mockReturnValue({ + values: vi.fn().mockResolvedValue([]), + }), + } + return await callback(tx) + }) + + mockDb.transaction = mockTransaction + + const result = await dbHelpers.migrateWorkflowToNormalizedTables( + mockWorkflowId, + mockJsonState + ) + + expect(result.success).toBe(true) + expect(result.error).toBeUndefined() + }) + + it('should return error when migration fails', async () => { + const mockTransaction = vi.fn().mockRejectedValue(new Error('Migration failed')) + mockDb.transaction = mockTransaction + + const result = await dbHelpers.migrateWorkflowToNormalizedTables( + mockWorkflowId, + mockJsonState + ) + + expect(result.success).toBe(false) + expect(result.error).toBe('Migration failed') + }) + + it('should handle missing properties in JSON state gracefully', async () => { + const incompleteJsonState = { + blocks: mockWorkflowState.blocks, + edges: mockWorkflowState.edges, + // Missing loops, parallels, and other properties + } + + const mockTransaction = vi.fn().mockImplementation(async (callback) => { + const tx = { + delete: vi.fn().mockReturnValue({ + where: vi.fn().mockResolvedValue([]), + }), + insert: vi.fn().mockReturnValue({ + values: vi.fn().mockResolvedValue([]), + }), + } + return await callback(tx) + }) + + mockDb.transaction = mockTransaction + + const result = await dbHelpers.migrateWorkflowToNormalizedTables( + mockWorkflowId, + incompleteJsonState + ) + + expect(result.success).toBe(true) + }) + + it('should handle null/undefined JSON state', async () => { + const result = await dbHelpers.migrateWorkflowToNormalizedTables(mockWorkflowId, null) + + expect(result.success).toBe(false) + expect(result.error).toContain('Cannot read properties') + }) + }) + + describe('error handling and edge cases', () => { + it('should handle very large workflow data', async () => { + const largeWorkflowState: WorkflowState = { + blocks: {}, + edges: [], + loops: {}, + parallels: {}, + lastSaved: Date.now(), + isDeployed: false, + deploymentStatuses: {}, + hasActiveSchedule: false, + hasActiveWebhook: false, + } + + // Create 1000 blocks + for (let i = 0; i < 1000; i++) { + largeWorkflowState.blocks[`block-${i}`] = { + id: `block-${i}`, + type: 'api', + name: `Block ${i}`, + position: { x: i * 100, y: i * 100 }, + subBlocks: {}, + outputs: {}, + enabled: true, + } + } + + // Create 999 edges to connect them + for (let i = 0; i < 999; i++) { + largeWorkflowState.edges.push({ + id: `edge-${i}`, + source: `block-${i}`, + target: `block-${i + 1}`, + }) + } + + const mockTransaction = vi.fn().mockImplementation(async (callback) => { + const tx = { + delete: vi.fn().mockReturnValue({ + where: vi.fn().mockResolvedValue([]), + }), + insert: vi.fn().mockReturnValue({ + values: vi.fn().mockResolvedValue([]), + }), + } + return await callback(tx) + }) + + mockDb.transaction = mockTransaction + + const result = await dbHelpers.saveWorkflowToNormalizedTables( + mockWorkflowId, + largeWorkflowState + ) + + expect(result.success).toBe(true) + expect(Object.keys(result.jsonBlob.blocks)).toHaveLength(1000) + expect(result.jsonBlob.edges).toHaveLength(999) + }) + }) +}) diff --git a/apps/sim/lib/workflows/db-helpers.ts b/apps/sim/lib/workflows/db-helpers.ts new file mode 100644 index 00000000000..fb4d7a59044 --- /dev/null +++ b/apps/sim/lib/workflows/db-helpers.ts @@ -0,0 +1,277 @@ +import { eq } from 'drizzle-orm' +import { createLogger } from '@/lib/logs/console-logger' +import { db } from '@/db' +import { workflowBlocks, workflowEdges, workflowSubflows } from '@/db/schema' +import type { WorkflowState } from '@/stores/workflows/workflow/types' +import { SUBFLOW_TYPES } from '@/stores/workflows/workflow/types' + +const logger = createLogger('WorkflowDBHelpers') + +export interface NormalizedWorkflowData { + blocks: Record + edges: any[] + loops: Record + parallels: Record + isFromNormalizedTables: true // Flag to indicate this came from new tables +} + +/** + * Load workflow state from normalized tables + * Returns null if no data found (fallback to JSON blob) + */ +export async function loadWorkflowFromNormalizedTables( + workflowId: string +): Promise { + try { + // Load all components in parallel + const [blocks, edges, subflows] = await Promise.all([ + db.select().from(workflowBlocks).where(eq(workflowBlocks.workflowId, workflowId)), + db.select().from(workflowEdges).where(eq(workflowEdges.workflowId, workflowId)), + db.select().from(workflowSubflows).where(eq(workflowSubflows.workflowId, workflowId)), + ]) + + // If no blocks found, assume this workflow hasn't been migrated yet + if (blocks.length === 0) { + return null + } + + // Convert blocks to the expected format + const blocksMap: Record = {} + blocks.forEach((block) => { + blocksMap[block.id] = { + id: block.id, + type: block.type, + name: block.name, + position: { + x: block.positionX, + y: block.positionY, + }, + enabled: block.enabled, + horizontalHandles: block.horizontalHandles, + isWide: block.isWide, + height: block.height, + subBlocks: block.subBlocks || {}, + outputs: block.outputs || {}, + data: block.data || {}, + parentId: (block.data as any)?.parentId || null, + extent: (block.data as any)?.extent || null, + } + }) + + // Convert edges to the expected format + const edgesArray = edges.map((edge) => ({ + id: edge.id, + source: edge.sourceBlockId, + target: edge.targetBlockId, + sourceHandle: edge.sourceHandle, + targetHandle: edge.targetHandle, + })) + + // Convert subflows to loops and parallels + const loops: Record = {} + const parallels: Record = {} + + subflows.forEach((subflow) => { + const config = subflow.config || {} + + if (subflow.type === SUBFLOW_TYPES.LOOP) { + loops[subflow.id] = { + id: subflow.id, + ...config, + } + } else if (subflow.type === SUBFLOW_TYPES.PARALLEL) { + parallels[subflow.id] = { + id: subflow.id, + ...config, + } + } else { + logger.warn(`Unknown subflow type: ${subflow.type} for subflow ${subflow.id}`) + } + }) + + logger.info( + `Loaded workflow ${workflowId} from normalized tables: ${blocks.length} blocks, ${edges.length} edges, ${subflows.length} subflows` + ) + + return { + blocks: blocksMap, + edges: edgesArray, + loops, + parallels, + isFromNormalizedTables: true, + } + } catch (error) { + logger.error(`Error loading workflow ${workflowId} from normalized tables:`, error) + return null + } +} + +/** + * Save workflow state to normalized tables + * Also returns the JSON blob for backward compatibility + */ +export async function saveWorkflowToNormalizedTables( + workflowId: string, + state: WorkflowState +): Promise<{ success: boolean; jsonBlob?: any; error?: string }> { + try { + // Start a transaction + const result = await db.transaction(async (tx) => { + // Clear existing data for this workflow + await Promise.all([ + tx.delete(workflowBlocks).where(eq(workflowBlocks.workflowId, workflowId)), + tx.delete(workflowEdges).where(eq(workflowEdges.workflowId, workflowId)), + tx.delete(workflowSubflows).where(eq(workflowSubflows.workflowId, workflowId)), + ]) + + // Insert blocks + if (Object.keys(state.blocks).length > 0) { + const blockInserts = Object.values(state.blocks).map((block) => ({ + id: block.id, + workflowId: workflowId, + type: block.type, + name: block.name || '', + positionX: Math.round(block.position?.x || 0), + positionY: Math.round(block.position?.y || 0), + enabled: block.enabled ?? true, + horizontalHandles: block.horizontalHandles ?? true, + isWide: block.isWide ?? false, + height: block.height || 0, + subBlocks: block.subBlocks || {}, + outputs: block.outputs || {}, + data: block.data || {}, + parentId: block.data?.parentId || null, + extent: block.data?.extent || null, + })) + + await tx.insert(workflowBlocks).values(blockInserts) + } + + // Insert edges + if (state.edges.length > 0) { + const edgeInserts = state.edges.map((edge) => ({ + id: edge.id, + workflowId: workflowId, + sourceBlockId: edge.source, + targetBlockId: edge.target, + sourceHandle: edge.sourceHandle || null, + targetHandle: edge.targetHandle || null, + })) + + await tx.insert(workflowEdges).values(edgeInserts) + } + + // Insert subflows (loops and parallels) + const subflowInserts: any[] = [] + + // Add loops + Object.values(state.loops || {}).forEach((loop) => { + subflowInserts.push({ + id: loop.id, + workflowId: workflowId, + type: SUBFLOW_TYPES.LOOP, + config: loop, + }) + }) + + // Add parallels + Object.values(state.parallels || {}).forEach((parallel) => { + subflowInserts.push({ + id: parallel.id, + workflowId: workflowId, + type: SUBFLOW_TYPES.PARALLEL, + config: parallel, + }) + }) + + if (subflowInserts.length > 0) { + await tx.insert(workflowSubflows).values(subflowInserts) + } + + return { success: true } + }) + + // Create JSON blob for backward compatibility + const jsonBlob = { + blocks: state.blocks, + edges: state.edges, + loops: state.loops || {}, + parallels: state.parallels || {}, + lastSaved: Date.now(), + isDeployed: state.isDeployed, + deployedAt: state.deployedAt, + deploymentStatuses: state.deploymentStatuses, + hasActiveSchedule: state.hasActiveSchedule, + hasActiveWebhook: state.hasActiveWebhook, + } + + logger.info(`Successfully saved workflow ${workflowId} to normalized tables`) + + return { + success: true, + jsonBlob, + } + } catch (error) { + logger.error(`Error saving workflow ${workflowId} to normalized tables:`, error) + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + } + } +} + +/** + * Check if a workflow exists in normalized tables + */ +export async function workflowExistsInNormalizedTables(workflowId: string): Promise { + try { + const blocks = await db + .select({ id: workflowBlocks.id }) + .from(workflowBlocks) + .where(eq(workflowBlocks.workflowId, workflowId)) + .limit(1) + + return blocks.length > 0 + } catch (error) { + logger.error(`Error checking if workflow ${workflowId} exists in normalized tables:`, error) + return false + } +} + +/** + * Migrate a workflow from JSON blob to normalized tables + */ +export async function migrateWorkflowToNormalizedTables( + workflowId: string, + jsonState: any +): Promise<{ success: boolean; error?: string }> { + try { + // Convert JSON state to WorkflowState format + const workflowState: WorkflowState = { + blocks: jsonState.blocks || {}, + edges: jsonState.edges || [], + loops: jsonState.loops || {}, + parallels: jsonState.parallels || {}, + lastSaved: jsonState.lastSaved, + isDeployed: jsonState.isDeployed, + deployedAt: jsonState.deployedAt, + deploymentStatuses: jsonState.deploymentStatuses || {}, + hasActiveSchedule: jsonState.hasActiveSchedule, + hasActiveWebhook: jsonState.hasActiveWebhook, + } + + const result = await saveWorkflowToNormalizedTables(workflowId, workflowState) + + if (result.success) { + logger.info(`Successfully migrated workflow ${workflowId} to normalized tables`) + return { success: true } + } + return { success: false, error: result.error } + } catch (error) { + logger.error(`Error migrating workflow ${workflowId} to normalized tables:`, error) + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + } + } +} diff --git a/apps/sim/stores/workflows/workflow/types.ts b/apps/sim/stores/workflows/workflow/types.ts index 555d40ae067..c59b1b16a01 100644 --- a/apps/sim/stores/workflows/workflow/types.ts +++ b/apps/sim/stores/workflows/workflow/types.ts @@ -2,11 +2,73 @@ import type { Edge } from 'reactflow' import type { BlockOutput, SubBlockType } from '@/blocks/types' import type { DeploymentStatus } from '../registry/types' +// Centralized subflow type system - easy to extend without database changes +export const SUBFLOW_TYPES = { + LOOP: 'loop', + PARALLEL: 'parallel', + // Future types can be added here: + // CONDITIONAL: 'conditional', + // RETRY: 'retry', + // BATCH: 'batch', +} as const + +export type SubflowType = (typeof SUBFLOW_TYPES)[keyof typeof SUBFLOW_TYPES] + +// Type guard for runtime validation +export function isValidSubflowType(type: string): type is SubflowType { + return Object.values(SUBFLOW_TYPES).includes(type as SubflowType) +} + +// Subflow configuration interfaces +export interface LoopConfig { + nodes: string[] + iterations: number + loopType: 'for' | 'forEach' + forEachItems?: any[] | Record | string +} + +export interface ParallelConfig { + nodes: string[] + distribution?: any[] | Record | string + parallelType?: 'count' | 'collection' +} + +// Generic subflow interface +export interface Subflow { + id: string + workflowId: string + type: SubflowType + config: LoopConfig | ParallelConfig + createdAt: Date + updatedAt: Date +} + export interface Position { x: number y: number } +export interface BlockData { + // Parent-child relationships for container nodes + parentId?: string + extent?: 'parent' + + // Container dimensions + width?: number + height?: number + + // Loop-specific properties + collection?: any // The items to iterate over in a loop + count?: number // Number of iterations for numeric loops + loopType?: 'for' | 'forEach' // Type of loop - must match Loop interface + + // Parallel-specific properties + parallelType?: 'collection' | 'count' // Type of parallel execution + + // Container node type (for ReactFlow node type determination) + type?: string +} + export interface BlockState { id: string type: string @@ -19,7 +81,7 @@ export interface BlockState { isWide?: boolean height?: number advancedMode?: boolean - data?: Record + data?: BlockData } export interface SubBlockState {