From 0695197b97ec5709ba589f151fbd7522b985b2e4 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 17 Jun 2025 15:25:55 -0700 Subject: [PATCH 01/12] new tables to track workflow state --- .../migrations/0043_silent_the_anarchist.sql | 55 + .../sim/db/migrations/meta/0043_snapshot.json | 3742 +++++++++++++++++ apps/sim/db/migrations/meta/_journal.json | 9 +- apps/sim/db/schema.ts | 128 + apps/sim/stores/workflows/workflow/types.ts | 41 + 5 files changed, 3974 insertions(+), 1 deletion(-) create mode 100644 apps/sim/db/migrations/0043_silent_the_anarchist.sql create mode 100644 apps/sim/db/migrations/meta/0043_snapshot.json 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..8960ef2edaf --- /dev/null +++ b/apps/sim/db/migrations/0043_silent_the_anarchist.sql @@ -0,0 +1,55 @@ +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_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_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..edac424091b --- /dev/null +++ b/apps/sim/db/migrations/meta/0043_snapshot.json @@ -0,0 +1,3742 @@ +{ + "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": {} + } +} \ No newline at end of file diff --git a/apps/sim/db/migrations/meta/_journal.json b/apps/sim/db/migrations/meta/_journal.json index ff79c9ee703..b36b4118638 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 } ] -} +} \ No newline at end of file diff --git a/apps/sim/db/schema.ts b/apps/sim/db/schema.ts index c0094992c6b..e69b7fc12d5 100644 --- a/apps/sim/db/schema.ts +++ b/apps/sim/db/schema.ts @@ -131,6 +131,134 @@ 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'), // References another block's ID (self-referential) + 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(), // Source block ID + targetBlockId: text('target_block_id').notNull(), // 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 + ), + + // Foreign key constraints (need to be added manually since source/target reference blocks) + sourceBlockFk: index('workflow_edges_source_block_fk_idx').on(table.sourceBlockId), + targetBlockFk: index('workflow_edges_target_block_fk_idx').on(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/stores/workflows/workflow/types.ts b/apps/sim/stores/workflows/workflow/types.ts index 555d40ae067..bd3410e1d88 100644 --- a/apps/sim/stores/workflows/workflow/types.ts +++ b/apps/sim/stores/workflows/workflow/types.ts @@ -2,6 +2,47 @@ 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 From 1faea917b0451554926916252b1ae5e04fed2997 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 17 Jun 2025 15:26:05 -0700 Subject: [PATCH 02/12] fix lint --- .../sim/db/migrations/meta/0043_snapshot.json | 342 +++++------------- apps/sim/db/migrations/meta/_journal.json | 2 +- apps/sim/db/schema.ts | 41 +-- apps/sim/stores/workflows/workflow/types.ts | 2 +- 4 files changed, 107 insertions(+), 280 deletions(-) diff --git a/apps/sim/db/migrations/meta/0043_snapshot.json b/apps/sim/db/migrations/meta/0043_snapshot.json index edac424091b..d5b4c51c700 100644 --- a/apps/sim/db/migrations/meta/0043_snapshot.json +++ b/apps/sim/db/migrations/meta/0043_snapshot.json @@ -93,12 +93,8 @@ "name": "account_user_id_user_id_fk", "tableFrom": "account", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -170,12 +166,8 @@ "name": "api_key_user_id_user_id_fk", "tableFrom": "api_key", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -185,9 +177,7 @@ "api_key_key_unique": { "name": "api_key_key_unique", "nullsNotDistinct": false, - "columns": [ - "key" - ] + "columns": ["key"] } }, "policies": {}, @@ -312,12 +302,8 @@ "name": "chat_workflow_id_workflow_id_fk", "tableFrom": "chat", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -325,12 +311,8 @@ "name": "chat_user_id_user_id_fk", "tableFrom": "chat", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -396,12 +378,8 @@ "name": "custom_tools_user_id_user_id_fk", "tableFrom": "custom_tools", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -619,12 +597,8 @@ "name": "document_knowledge_base_id_knowledge_base_id_fk", "tableFrom": "document", "tableTo": "knowledge_base", - "columnsFrom": [ - "knowledge_base_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1011,12 +985,8 @@ "name": "embedding_knowledge_base_id_knowledge_base_id_fk", "tableFrom": "embedding", "tableTo": "knowledge_base", - "columnsFrom": [ - "knowledge_base_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1024,12 +994,8 @@ "name": "embedding_document_id_document_id_fk", "tableFrom": "embedding", "tableTo": "document", - "columnsFrom": [ - "document_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["document_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1081,12 +1047,8 @@ "name": "environment_user_id_user_id_fk", "tableFrom": "environment", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1096,9 +1058,7 @@ "environment_user_id_unique": { "name": "environment_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } }, "policies": {}, @@ -1165,12 +1125,8 @@ "name": "invitation_inviter_id_user_id_fk", "tableFrom": "invitation", "tableTo": "user", - "columnsFrom": [ - "inviter_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1178,12 +1134,8 @@ "name": "invitation_organization_id_organization_id_fk", "tableFrom": "invitation", "tableTo": "organization", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1350,12 +1302,8 @@ "name": "knowledge_base_user_id_user_id_fk", "tableFrom": "knowledge_base", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1363,12 +1311,8 @@ "name": "knowledge_base_workspace_id_workspace_id_fk", "tableFrom": "knowledge_base", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1459,12 +1403,8 @@ "name": "marketplace_workflow_id_workflow_id_fk", "tableFrom": "marketplace", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1472,12 +1412,8 @@ "name": "marketplace_author_id_user_id_fk", "tableFrom": "marketplace", "tableTo": "user", - "columnsFrom": [ - "author_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["author_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1530,12 +1466,8 @@ "name": "member_user_id_user_id_fk", "tableFrom": "member", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1543,12 +1475,8 @@ "name": "member_organization_id_organization_id_fk", "tableFrom": "member", "tableTo": "organization", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1672,12 +1600,8 @@ "name": "memory_workflow_id_workflow_id_fk", "tableFrom": "memory", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1810,12 +1734,8 @@ "name": "session_user_id_user_id_fk", "tableFrom": "session", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1823,12 +1743,8 @@ "name": "session_active_organization_id_organization_id_fk", "tableFrom": "session", "tableTo": "organization", - "columnsFrom": [ - "active_organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -1838,9 +1754,7 @@ "session_token_unique": { "name": "session_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -1933,12 +1847,8 @@ "name": "settings_user_id_user_id_fk", "tableFrom": "settings", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1948,9 +1858,7 @@ "settings_user_id_unique": { "name": "settings_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } }, "policies": {}, @@ -2108,9 +2016,7 @@ "user_email_unique": { "name": "user_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] } }, "policies": {}, @@ -2196,12 +2102,8 @@ "name": "user_stats_user_id_user_id_fk", "tableFrom": "user_stats", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2211,9 +2113,7 @@ "user_stats_user_id_unique": { "name": "user_stats_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } }, "policies": {}, @@ -2314,9 +2214,7 @@ "waitlist_email_unique": { "name": "waitlist_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] } }, "policies": {}, @@ -2401,12 +2299,8 @@ "name": "webhook_workflow_id_workflow_id_fk", "tableFrom": "webhook", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2554,12 +2448,8 @@ "name": "workflow_user_id_user_id_fk", "tableFrom": "workflow", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2567,12 +2457,8 @@ "name": "workflow_workspace_id_workspace_id_fk", "tableFrom": "workflow", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2580,12 +2466,8 @@ "name": "workflow_folder_id_workflow_folder_id_fk", "tableFrom": "workflow", "tableTo": "workflow_folder", - "columnsFrom": [ - "folder_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -2791,12 +2673,8 @@ "name": "workflow_blocks_workflow_id_workflow_id_fk", "tableFrom": "workflow_blocks", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2979,12 +2857,8 @@ "name": "workflow_edges_workflow_id_workflow_id_fk", "tableFrom": "workflow_edges", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3129,12 +3003,8 @@ "name": "workflow_folder_user_id_user_id_fk", "tableFrom": "workflow_folder", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3142,12 +3012,8 @@ "name": "workflow_folder_workspace_id_workspace_id_fk", "tableFrom": "workflow_folder", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3224,12 +3090,8 @@ "name": "workflow_logs_workflow_id_workflow_id_fk", "tableFrom": "workflow_logs", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3328,12 +3190,8 @@ "name": "workflow_schedule_workflow_id_workflow_id_fk", "tableFrom": "workflow_schedule", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3343,9 +3201,7 @@ "workflow_schedule_workflow_id_unique": { "name": "workflow_schedule_workflow_id_unique", "nullsNotDistinct": false, - "columns": [ - "workflow_id" - ] + "columns": ["workflow_id"] } }, "policies": {}, @@ -3439,12 +3295,8 @@ "name": "workflow_subflows_workflow_id_workflow_id_fk", "tableFrom": "workflow_subflows", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3498,12 +3350,8 @@ "name": "workspace_owner_id_user_id_fk", "tableFrom": "workspace", "tableTo": "user", - "columnsFrom": [ - "owner_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3589,12 +3437,8 @@ "name": "workspace_invitation_workspace_id_workspace_id_fk", "tableFrom": "workspace_invitation", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3602,12 +3446,8 @@ "name": "workspace_invitation_inviter_id_user_id_fk", "tableFrom": "workspace_invitation", "tableTo": "user", - "columnsFrom": [ - "inviter_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3617,9 +3457,7 @@ "workspace_invitation_token_unique": { "name": "workspace_invitation_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -3698,12 +3536,8 @@ "name": "workspace_member_workspace_id_workspace_id_fk", "tableFrom": "workspace_member", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3711,12 +3545,8 @@ "name": "workspace_member_user_id_user_id_fk", "tableFrom": "workspace_member", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3739,4 +3569,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/apps/sim/db/migrations/meta/_journal.json b/apps/sim/db/migrations/meta/_journal.json index b36b4118638..6bf0d597bd7 100644 --- a/apps/sim/db/migrations/meta/_journal.json +++ b/apps/sim/db/migrations/meta/_journal.json @@ -304,4 +304,4 @@ "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/apps/sim/db/schema.ts b/apps/sim/db/schema.ts index e69b7fc12d5..d48ae00ee69 100644 --- a/apps/sim/db/schema.ts +++ b/apps/sim/db/schema.ts @@ -140,30 +140,30 @@ export const workflowBlocks = pgTable( 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'), // References another block's ID (self-referential) extent: text('extent'), // 'parent' or null - for ReactFlow parent constraint - + // Timestamps createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), @@ -171,16 +171,16 @@ export const workflowBlocks = pgTable( (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), }) @@ -194,26 +194,26 @@ export const workflowEdges = pgTable( 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(), // Source block ID targetBlockId: text('target_block_id').notNull(), // 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, @@ -223,7 +223,7 @@ export const workflowEdges = pgTable( table.workflowId, table.targetBlockId ), - + // Foreign key constraints (need to be added manually since source/target reference blocks) sourceBlockFk: index('workflow_edges_source_block_fk_idx').on(table.sourceBlockId), targetBlockFk: index('workflow_edges_target_block_fk_idx').on(table.targetBlockId), @@ -238,11 +238,11 @@ export const workflowSubflows = pgTable( 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(), @@ -250,12 +250,9 @@ export const workflowSubflows = pgTable( (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 - ), + workflowTypeIdx: index('workflow_subflows_workflow_type_idx').on(table.workflowId, table.type), }) ) diff --git a/apps/sim/stores/workflows/workflow/types.ts b/apps/sim/stores/workflows/workflow/types.ts index bd3410e1d88..d1620a98acf 100644 --- a/apps/sim/stores/workflows/workflow/types.ts +++ b/apps/sim/stores/workflows/workflow/types.ts @@ -12,7 +12,7 @@ export const SUBFLOW_TYPES = { // BATCH: 'batch', } as const -export type SubflowType = typeof SUBFLOW_TYPES[keyof typeof SUBFLOW_TYPES] +export type SubflowType = (typeof SUBFLOW_TYPES)[keyof typeof SUBFLOW_TYPES] // Type guard for runtime validation export function isValidSubflowType(type: string): type is SubflowType { From 5a424e5e2df52abb9a118cc6f3ea42e535949039 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 17 Jun 2025 15:58:49 -0700 Subject: [PATCH 03/12] refactor into separate tables --- apps/sim/app/api/workflows/[id]/route.ts | 37 ++- apps/sim/app/api/workflows/sync/route.ts | 35 ++- apps/sim/lib/workflows/db-helpers.ts | 287 ++++++++++++++++++++ apps/sim/stores/workflows/workflow/types.ts | 8 +- 4 files changed, 362 insertions(+), 5 deletions(-) create mode 100644 apps/sim/lib/workflows/db-helpers.ts 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/lib/workflows/db-helpers.ts b/apps/sim/lib/workflows/db-helpers.ts new file mode 100644 index 00000000000..f3e3fa97f0e --- /dev/null +++ b/apps/sim/lib/workflows/db-helpers.ts @@ -0,0 +1,287 @@ +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 { isValidSubflowType, 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) => { + if (!isValidSubflowType(SUBFLOW_TYPES.LOOP)) { + logger.warn(`Invalid loop type for loop ${loop.id}`) + return + } + + subflowInserts.push({ + id: loop.id, + workflowId: workflowId, + type: SUBFLOW_TYPES.LOOP, + config: loop, + }) + }) + + // Add parallels + Object.values(state.parallels || {}).forEach((parallel) => { + if (!isValidSubflowType(SUBFLOW_TYPES.PARALLEL)) { + logger.warn(`Invalid parallel type for parallel ${parallel.id}`) + return + } + + 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 d1620a98acf..3ee98e124f1 100644 --- a/apps/sim/stores/workflows/workflow/types.ts +++ b/apps/sim/stores/workflows/workflow/types.ts @@ -48,6 +48,12 @@ export interface Position { y: number } +export interface BlockData { + parentId?: string + extent?: 'parent' + [key: string]: any // Allow other arbitrary properties while still typing the known ones +} + export interface BlockState { id: string type: string @@ -60,7 +66,7 @@ export interface BlockState { isWide?: boolean height?: number advancedMode?: boolean - data?: Record + data?: BlockData } export interface SubBlockState { From 4b49ac1f1554987735503b8629dbfb922e23b81f Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 17 Jun 2025 16:02:58 -0700 Subject: [PATCH 04/12] fix typing --- apps/sim/stores/workflows/workflow/types.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/sim/stores/workflows/workflow/types.ts b/apps/sim/stores/workflows/workflow/types.ts index 3ee98e124f1..c6ae6f05281 100644 --- a/apps/sim/stores/workflows/workflow/types.ts +++ b/apps/sim/stores/workflows/workflow/types.ts @@ -49,9 +49,21 @@ export interface Position { } export interface BlockData { + // Parent-child relationships for container nodes parentId?: string extent?: 'parent' - [key: string]: any // Allow other arbitrary properties while still typing the known ones + + // 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?: string // Type of loop ('for', 'forEach', etc.) + + // Container node type (for ReactFlow node type determination) + type?: string } export interface BlockState { From 5411a52aafcf19baa4d017b2cfd77903ab335bf9 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 17 Jun 2025 16:03:11 -0700 Subject: [PATCH 05/12] fix lint --- apps/sim/stores/workflows/workflow/types.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/sim/stores/workflows/workflow/types.ts b/apps/sim/stores/workflows/workflow/types.ts index c6ae6f05281..2283eec8117 100644 --- a/apps/sim/stores/workflows/workflow/types.ts +++ b/apps/sim/stores/workflows/workflow/types.ts @@ -52,16 +52,16 @@ 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?: string // Type of loop ('for', 'forEach', etc.) - + // Container node type (for ReactFlow node type determination) type?: string } From 10410f923c9624b5d3d3bac32989f4d7fbeaa6e3 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 17 Jun 2025 16:23:26 -0700 Subject: [PATCH 06/12] add tests --- apps/sim/lib/workflows/db-helpers.test.ts | 751 ++++++++++++++++++++++ 1 file changed, 751 insertions(+) create mode 100644 apps/sim/lib/workflows/db-helpers.test.ts 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..a79bc2a94b6 --- /dev/null +++ b/apps/sim/lib/workflows/db-helpers.test.ts @@ -0,0 +1,751 @@ +/** + * @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) + }) + }) +}) \ No newline at end of file From 7b51755e85e08d97ecc4e9260b0b3a55459d99ac Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 17 Jun 2025 16:23:35 -0700 Subject: [PATCH 07/12] fix lint --- apps/sim/lib/workflows/db-helpers.test.ts | 42 ++++++++++++++++++----- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/apps/sim/lib/workflows/db-helpers.test.ts b/apps/sim/lib/workflows/db-helpers.test.ts index a79bc2a94b6..a092435c73c 100644 --- a/apps/sim/lib/workflows/db-helpers.test.ts +++ b/apps/sim/lib/workflows/db-helpers.test.ts @@ -429,7 +429,10 @@ describe('Database Helpers', () => { mockDb.transaction = mockTransaction - const result = await dbHelpers.saveWorkflowToNormalizedTables(mockWorkflowId, mockWorkflowState) + const result = await dbHelpers.saveWorkflowToNormalizedTables( + mockWorkflowId, + mockWorkflowState + ) expect(result.success).toBe(true) expect(result.jsonBlob).toBeDefined() @@ -469,7 +472,10 @@ describe('Database Helpers', () => { mockDb.transaction = mockTransaction - const result = await dbHelpers.saveWorkflowToNormalizedTables(mockWorkflowId, emptyWorkflowState) + const result = await dbHelpers.saveWorkflowToNormalizedTables( + mockWorkflowId, + emptyWorkflowState + ) expect(result.success).toBe(true) expect(result.jsonBlob.blocks).toEqual({}) @@ -482,7 +488,10 @@ describe('Database Helpers', () => { const mockTransaction = vi.fn().mockRejectedValue(new Error('Transaction failed')) mockDb.transaction = mockTransaction - const result = await dbHelpers.saveWorkflowToNormalizedTables(mockWorkflowId, mockWorkflowState) + const result = await dbHelpers.saveWorkflowToNormalizedTables( + mockWorkflowId, + mockWorkflowState + ) expect(result.success).toBe(false) expect(result.error).toBe('Transaction failed') @@ -495,7 +504,10 @@ describe('Database Helpers', () => { const mockTransaction = vi.fn().mockRejectedValue(constraintError) mockDb.transaction = mockTransaction - const result = await dbHelpers.saveWorkflowToNormalizedTables(mockWorkflowId, mockWorkflowState) + const result = await dbHelpers.saveWorkflowToNormalizedTables( + mockWorkflowId, + mockWorkflowState + ) expect(result.success).toBe(false) expect(result.error).toBe('Unique constraint violation') @@ -641,7 +653,10 @@ describe('Database Helpers', () => { mockDb.transaction = mockTransaction - const result = await dbHelpers.migrateWorkflowToNormalizedTables(mockWorkflowId, mockJsonState) + const result = await dbHelpers.migrateWorkflowToNormalizedTables( + mockWorkflowId, + mockJsonState + ) expect(result.success).toBe(true) expect(result.error).toBeUndefined() @@ -651,7 +666,10 @@ describe('Database Helpers', () => { const mockTransaction = vi.fn().mockRejectedValue(new Error('Migration failed')) mockDb.transaction = mockTransaction - const result = await dbHelpers.migrateWorkflowToNormalizedTables(mockWorkflowId, mockJsonState) + const result = await dbHelpers.migrateWorkflowToNormalizedTables( + mockWorkflowId, + mockJsonState + ) expect(result.success).toBe(false) expect(result.error).toBe('Migration failed') @@ -678,7 +696,10 @@ describe('Database Helpers', () => { mockDb.transaction = mockTransaction - const result = await dbHelpers.migrateWorkflowToNormalizedTables(mockWorkflowId, incompleteJsonState) + const result = await dbHelpers.migrateWorkflowToNormalizedTables( + mockWorkflowId, + incompleteJsonState + ) expect(result.success).toBe(true) }) @@ -741,11 +762,14 @@ describe('Database Helpers', () => { mockDb.transaction = mockTransaction - const result = await dbHelpers.saveWorkflowToNormalizedTables(mockWorkflowId, largeWorkflowState) + 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) }) }) -}) \ No newline at end of file +}) From cd6c4b00ab6dec73de2dbdaf234c937930365fc5 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 17 Jun 2025 16:35:10 -0700 Subject: [PATCH 08/12] add correct foreign key constraint --- apps/sim/db/migrations/0043_silent_the_anarchist.sql | 2 ++ apps/sim/db/schema.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/sim/db/migrations/0043_silent_the_anarchist.sql b/apps/sim/db/migrations/0043_silent_the_anarchist.sql index 8960ef2edaf..c4846ce9837 100644 --- a/apps/sim/db/migrations/0043_silent_the_anarchist.sql +++ b/apps/sim/db/migrations/0043_silent_the_anarchist.sql @@ -39,6 +39,8 @@ CREATE TABLE "workflow_subflows" ( --> 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_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 diff --git a/apps/sim/db/schema.ts b/apps/sim/db/schema.ts index d48ae00ee69..d5f59e61469 100644 --- a/apps/sim/db/schema.ts +++ b/apps/sim/db/schema.ts @@ -196,8 +196,12 @@ export const workflowEdges = pgTable( .references(() => workflow.id, { onDelete: 'cascade' }), // Link to parent workflow // Connection definition (from ReactFlow Edge interface) - sourceBlockId: text('source_block_id').notNull(), // Source block ID - targetBlockId: text('target_block_id').notNull(), // Target block ID + 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) @@ -223,10 +227,6 @@ export const workflowEdges = pgTable( table.workflowId, table.targetBlockId ), - - // Foreign key constraints (need to be added manually since source/target reference blocks) - sourceBlockFk: index('workflow_edges_source_block_fk_idx').on(table.sourceBlockId), - targetBlockFk: index('workflow_edges_target_block_fk_idx').on(table.targetBlockId), }) ) From 931152a285a3798e10bfa419ee57f28a44cea62a Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 17 Jun 2025 16:40:26 -0700 Subject: [PATCH 09/12] add self ref --- apps/sim/db/migrations/0043_silent_the_anarchist.sql | 1 + apps/sim/db/schema.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/sim/db/migrations/0043_silent_the_anarchist.sql b/apps/sim/db/migrations/0043_silent_the_anarchist.sql index c4846ce9837..7efb7998296 100644 --- a/apps/sim/db/migrations/0043_silent_the_anarchist.sql +++ b/apps/sim/db/migrations/0043_silent_the_anarchist.sql @@ -38,6 +38,7 @@ CREATE TABLE "workflow_subflows" ( ); --> 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 diff --git a/apps/sim/db/schema.ts b/apps/sim/db/schema.ts index d5f59e61469..9657777fb02 100644 --- a/apps/sim/db/schema.ts +++ b/apps/sim/db/schema.ts @@ -161,7 +161,7 @@ export const workflowBlocks = pgTable( data: jsonb('data').default('{}'), // Additional block-specific data // Hierarchy support (for loop/parallel child blocks) - parentId: text('parent_id'), // References another block's ID (self-referential) + parentId: text('parent_id').references(() => workflowBlocks.id, { onDelete: 'cascade' }), // Self-referential FK extent: text('extent'), // 'parent' or null - for ReactFlow parent constraint // Timestamps From d45dc354a7075b0b8b965c12d9573156e8b83709 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 17 Jun 2025 16:45:14 -0700 Subject: [PATCH 10/12] remove unused checks --- apps/sim/db/schema.ts | 2 +- apps/sim/lib/workflows/db-helpers.ts | 12 +----------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/apps/sim/db/schema.ts b/apps/sim/db/schema.ts index 9657777fb02..030de3cc93f 100644 --- a/apps/sim/db/schema.ts +++ b/apps/sim/db/schema.ts @@ -161,7 +161,7 @@ export const workflowBlocks = pgTable( data: jsonb('data').default('{}'), // Additional block-specific data // Hierarchy support (for loop/parallel child blocks) - parentId: text('parent_id').references(() => workflowBlocks.id, { onDelete: 'cascade' }), // Self-referential FK + parentId: text('parent_id'), // Self-reference handled by foreign key constraint in migration extent: text('extent'), // 'parent' or null - for ReactFlow parent constraint // Timestamps diff --git a/apps/sim/lib/workflows/db-helpers.ts b/apps/sim/lib/workflows/db-helpers.ts index f3e3fa97f0e..fb4d7a59044 100644 --- a/apps/sim/lib/workflows/db-helpers.ts +++ b/apps/sim/lib/workflows/db-helpers.ts @@ -3,7 +3,7 @@ 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 { isValidSubflowType, SUBFLOW_TYPES } from '@/stores/workflows/workflow/types' +import { SUBFLOW_TYPES } from '@/stores/workflows/workflow/types' const logger = createLogger('WorkflowDBHelpers') @@ -166,11 +166,6 @@ export async function saveWorkflowToNormalizedTables( // Add loops Object.values(state.loops || {}).forEach((loop) => { - if (!isValidSubflowType(SUBFLOW_TYPES.LOOP)) { - logger.warn(`Invalid loop type for loop ${loop.id}`) - return - } - subflowInserts.push({ id: loop.id, workflowId: workflowId, @@ -181,11 +176,6 @@ export async function saveWorkflowToNormalizedTables( // Add parallels Object.values(state.parallels || {}).forEach((parallel) => { - if (!isValidSubflowType(SUBFLOW_TYPES.PARALLEL)) { - logger.warn(`Invalid parallel type for parallel ${parallel.id}`) - return - } - subflowInserts.push({ id: parallel.id, workflowId: workflowId, From ceddd3de6c69106f160e1eee93ecdf20acd6984f Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 17 Jun 2025 16:48:00 -0700 Subject: [PATCH 11/12] fix types --- apps/sim/stores/workflows/workflow/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/stores/workflows/workflow/types.ts b/apps/sim/stores/workflows/workflow/types.ts index 2283eec8117..cde2c6b1e14 100644 --- a/apps/sim/stores/workflows/workflow/types.ts +++ b/apps/sim/stores/workflows/workflow/types.ts @@ -60,7 +60,7 @@ export interface BlockData { // Loop-specific properties collection?: any // The items to iterate over in a loop count?: number // Number of iterations for numeric loops - loopType?: string // Type of loop ('for', 'forEach', etc.) + loopType?: 'for' | 'forEach' // Type of loop - must match Loop interface // Container node type (for ReactFlow node type determination) type?: string From df5207fe17b83fd2c2ecac81c4b4875247a16a6e Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 17 Jun 2025 16:55:51 -0700 Subject: [PATCH 12/12] fix type --- apps/sim/stores/workflows/workflow/types.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/sim/stores/workflows/workflow/types.ts b/apps/sim/stores/workflows/workflow/types.ts index cde2c6b1e14..c59b1b16a01 100644 --- a/apps/sim/stores/workflows/workflow/types.ts +++ b/apps/sim/stores/workflows/workflow/types.ts @@ -62,6 +62,9 @@ export interface BlockData { 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 }