From 0ed370b05b7e43b4702f8a8b5278adb388a59dd0 Mon Sep 17 00:00:00 2001 From: Ajit Kadaveru Date: Sat, 21 Jun 2025 14:50:42 -0700 Subject: [PATCH 1/2] PR: changes for migration --- .../sim/db/migrations/0046_loose_blizzard.sql | 19 + .../sim/db/migrations/meta/0046_snapshot.json | 3670 +++++++++++++++++ apps/sim/db/migrations/meta/_journal.json | 7 + apps/sim/db/schema.ts | 51 + 4 files changed, 3747 insertions(+) create mode 100644 apps/sim/db/migrations/0046_loose_blizzard.sql create mode 100644 apps/sim/db/migrations/meta/0046_snapshot.json diff --git a/apps/sim/db/migrations/0046_loose_blizzard.sql b/apps/sim/db/migrations/0046_loose_blizzard.sql new file mode 100644 index 00000000000..4569a0b41fb --- /dev/null +++ b/apps/sim/db/migrations/0046_loose_blizzard.sql @@ -0,0 +1,19 @@ +CREATE TYPE "public"."permission_type" AS ENUM('admin', 'write', 'read');--> statement-breakpoint +CREATE TABLE "permissions" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "entity_type" text NOT NULL, + "entity_id" text NOT NULL, + "permission_type" "permission_type" NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "workspace_invitation" ADD COLUMN "permissions" "permission_type" DEFAULT 'admin' NOT NULL;--> statement-breakpoint +ALTER TABLE "permissions" ADD CONSTRAINT "permissions_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "permissions_user_id_idx" ON "permissions" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "permissions_entity_idx" ON "permissions" USING btree ("entity_type","entity_id");--> statement-breakpoint +CREATE INDEX "permissions_user_entity_type_idx" ON "permissions" USING btree ("user_id","entity_type");--> statement-breakpoint +CREATE INDEX "permissions_user_entity_permission_idx" ON "permissions" USING btree ("user_id","entity_type","permission_type");--> statement-breakpoint +CREATE INDEX "permissions_user_entity_idx" ON "permissions" USING btree ("user_id","entity_type","entity_id");--> statement-breakpoint +CREATE UNIQUE INDEX "permissions_unique_constraint" ON "permissions" USING btree ("user_id","entity_type","entity_id"); \ No newline at end of file diff --git a/apps/sim/db/migrations/meta/0046_snapshot.json b/apps/sim/db/migrations/meta/0046_snapshot.json new file mode 100644 index 00000000000..4995d873e7c --- /dev/null +++ b/apps/sim/db/migrations/meta/0046_snapshot.json @@ -0,0 +1,3670 @@ +{ + "id": "cc643ea0-33d4-410a-9c53-82faa9d2c352", + "prevId": "9e6a8ffc-fb16-466b-b09f-20441e4a1ccc", + "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 + }, + "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_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 + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "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_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.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "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": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "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": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "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": "numeric", + "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": {} + } + }, + "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" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_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 + }, + "permissions": { + "name": "permissions", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + }, + "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": { + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/apps/sim/db/migrations/meta/_journal.json b/apps/sim/db/migrations/meta/_journal.json index 44a06c19251..1f03e0039d2 100644 --- a/apps/sim/db/migrations/meta/_journal.json +++ b/apps/sim/db/migrations/meta/_journal.json @@ -316,6 +316,13 @@ "when": 1750379637336, "tag": "0045_sour_chameleon", "breakpoints": true + }, + { + "idx": 46, + "version": "7", + "when": 1750527995274, + "tag": "0046_loose_blizzard", + "breakpoints": true } ] } diff --git a/apps/sim/db/schema.ts b/apps/sim/db/schema.ts index 08fd8779f55..8a5d7404219 100644 --- a/apps/sim/db/schema.ts +++ b/apps/sim/db/schema.ts @@ -8,6 +8,7 @@ import { integer, json, jsonb, + pgEnum, pgTable, text, timestamp, @@ -533,6 +534,9 @@ export const workspaceMember = pgTable( } ) +// Define the permission enum +export const permissionTypeEnum = pgEnum('permission_type', ['admin', 'write', 'read']) + export const workspaceInvitation = pgTable('workspace_invitation', { id: text('id').primaryKey(), workspaceId: text('workspace_id') @@ -545,11 +549,58 @@ export const workspaceInvitation = pgTable('workspace_invitation', { role: text('role').notNull().default('member'), status: text('status').notNull().default('pending'), token: text('token').notNull().unique(), + permissions: permissionTypeEnum('permissions').notNull().default('admin'), expiresAt: timestamp('expires_at').notNull(), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }) +export const permissions = pgTable( + 'permissions', + { + id: text('id').primaryKey(), + userId: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + entityType: text('entity_type').notNull(), // 'workspace', 'workflow', 'organization', etc. + entityId: text('entity_id').notNull(), // ID of the workspace, workflow, etc. + permissionType: permissionTypeEnum('permission_type').notNull(), // Use enum instead of text + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => ({ + // Primary access pattern - get all permissions for a user + userIdIdx: index('permissions_user_id_idx').on(table.userId), + + // Entity-based queries - get all users with permissions on an entity + entityIdx: index('permissions_entity_idx').on(table.entityType, table.entityId), + + // User + entity type queries - get user's permissions for all workspaces + userEntityTypeIdx: index('permissions_user_entity_type_idx').on(table.userId, table.entityType), + + // Specific permission checks - does user have specific permission on entity + userEntityPermissionIdx: index('permissions_user_entity_permission_idx').on( + table.userId, + table.entityType, + table.permissionType + ), + + // User + specific entity queries - get user's permissions for specific entity + userEntityIdx: index('permissions_user_entity_idx').on( + table.userId, + table.entityType, + table.entityId + ), + + // Uniqueness constraint - prevent duplicate permission rows (one permission per user/entity) + uniquePermissionConstraint: uniqueIndex('permissions_unique_constraint').on( + table.userId, + table.entityType, + table.entityId + ), + }) +) + export const memory = pgTable( 'memory', { From 8cf0b76e7b70f1d1644ebd244c47ae8ac40ec9d7 Mon Sep 17 00:00:00 2001 From: Ajit Kadaveru Date: Sat, 21 Jun 2025 15:08:12 -0700 Subject: [PATCH 2/2] add changes on top of db migration changes --- .../api/workspaces/[id]/permissions/route.ts | 178 ++++++ apps/sim/app/api/workspaces/[id]/route.ts | 63 +- .../workspaces/invitations/accept/route.ts | 54 +- .../app/api/workspaces/invitations/route.ts | 56 +- .../app/api/workspaces/members/[id]/route.ts | 74 --- apps/sim/app/api/workspaces/members/route.ts | 120 +++- apps/sim/app/api/workspaces/route.ts | 13 +- .../deployment-controls.tsx | 50 +- .../components/control-bar/control-bar.tsx | 593 ++++++++++-------- .../toolbar-block/toolbar-block.tsx | 41 +- .../toolbar-loop-block/toolbar-loop-block.tsx | 68 +- .../toolbar-parallel-block.tsx | 70 ++- .../app/w/[id]/components/toolbar/toolbar.tsx | 21 +- .../components/action-bar/action-bar.tsx | 56 +- .../connection-blocks/connection-blocks.tsx | 22 +- .../sub-block/components/checkbox-list.tsx | 8 +- .../components/sub-block/components/code.tsx | 8 +- .../sub-block/components/condition-input.tsx | 24 +- .../sub-block/components/date-input.tsx | 6 +- .../sub-block/components/dropdown.tsx | 8 +- .../sub-block/components/eval-input.tsx | 22 +- .../sub-block/components/file-upload.tsx | 6 +- .../sub-block/components/long-input.tsx | 7 +- .../components/schedule/schedule-config.tsx | 12 +- .../sub-block/components/short-input.tsx | 10 +- .../sub-block/components/slider-input.tsx | 8 +- .../components/starter/input-format.tsx | 18 +- .../sub-block/components/switch.tsx | 8 +- .../components/sub-block/components/table.tsx | 35 +- .../sub-block/components/time-input.tsx | 6 +- .../components/tool-input/tool-input.tsx | 19 + .../sub-block/components/webhook/webhook.tsx | 18 +- .../components/sub-block/sub-block.tsx | 39 +- .../workflow-block/workflow-block.tsx | 37 +- apps/sim/app/w/[id]/workflow.tsx | 83 ++- .../components/invite-modal/invite-modal.tsx | 432 ++++++++++++- .../workspace-header/workspace-header.tsx | 53 +- .../workflow-preview/workflow-preview.tsx | 4 +- apps/sim/hooks/use-user-permissions.ts | 83 +++ apps/sim/hooks/use-workspace-permissions.ts | 100 +++ apps/sim/lib/env.ts | 1 + apps/sim/lib/permissions/utils.test.ts | 146 +++++ apps/sim/lib/permissions/utils.ts | 45 ++ apps/sim/stores/constants.ts | 1 + 44 files changed, 2066 insertions(+), 660 deletions(-) create mode 100644 apps/sim/app/api/workspaces/[id]/permissions/route.ts create mode 100644 apps/sim/hooks/use-user-permissions.ts create mode 100644 apps/sim/hooks/use-workspace-permissions.ts create mode 100644 apps/sim/lib/permissions/utils.test.ts create mode 100644 apps/sim/lib/permissions/utils.ts diff --git a/apps/sim/app/api/workspaces/[id]/permissions/route.ts b/apps/sim/app/api/workspaces/[id]/permissions/route.ts new file mode 100644 index 00000000000..84d3d7c618f --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/permissions/route.ts @@ -0,0 +1,178 @@ +import { and, eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { db } from '@/db' +import { permissions, type permissionTypeEnum, user, workspaceMember } from '@/db/schema' + +// Extract the enum type from Drizzle schema +type PermissionType = (typeof permissionTypeEnum.enumValues)[number] + +interface UpdatePermissionsRequest { + updates: Array<{ + userId: string + permissions: PermissionType // Single permission type instead of object with booleans + }> +} + +// Helper function to fetch users with permissions for a workspace +async function getUsersWithPermissions(workspaceId: string) { + const usersWithPermissions = await db + .select({ + userId: user.id, + email: user.email, + name: user.name, + image: user.image, + permissionType: permissions.permissionType, + }) + .from(permissions) + .innerJoin(user, eq(permissions.userId, user.id)) + .where(and(eq(permissions.entityType, 'workspace'), eq(permissions.entityId, workspaceId))) + .orderBy(user.email) + + // Since each user has only one permission, we can use the results directly + return usersWithPermissions.map((row) => ({ + userId: row.userId, + email: row.email, + name: row.name, + image: row.image, + permissionType: row.permissionType, + })) +} + +/** + * GET /api/workspaces/[id]/permissions + * + * Retrieves all users who have permissions for the specified workspace. + * Returns user details along with their specific permissions. + * + * @param workspaceId - The workspace ID from the URL parameters + * @returns Array of users with their permissions for the workspace + */ +export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + try { + const { id: workspaceId } = await params + const session = await getSession() + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }) + } + + // Verify the current user has access to this workspace + const userMembership = await db + .select() + .from(workspaceMember) + .where( + and( + eq(workspaceMember.workspaceId, workspaceId), + eq(workspaceMember.userId, session.user.id) + ) + ) + .limit(1) + + if (userMembership.length === 0) { + return NextResponse.json({ error: 'Workspace not found or access denied' }, { status: 404 }) + } + + const result = await getUsersWithPermissions(workspaceId) + + return NextResponse.json({ + users: result, + total: result.length, + }) + } catch (error) { + console.error('Error fetching workspace permissions:', error) + return NextResponse.json({ error: 'Failed to fetch workspace permissions' }, { status: 500 }) + } +} + +/** + * PATCH /api/workspaces/[id]/permissions + * + * Updates permissions for existing workspace members. + * Only admin users can update permissions. + * + * @param workspaceId - The workspace ID from the URL parameters + * @param updates - Array of permission updates for users + * @returns Success message or error + */ +export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + try { + const { id: workspaceId } = await params + const session = await getSession() + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }) + } + + // Verify the current user has admin access to this workspace + const userPermissions = await db + .select() + .from(permissions) + .where( + and( + eq(permissions.userId, session.user.id), + eq(permissions.entityType, 'workspace'), + eq(permissions.entityId, workspaceId), + eq(permissions.permissionType, 'admin') + ) + ) + .limit(1) + + if (userPermissions.length === 0) { + return NextResponse.json( + { error: 'Admin access required to update permissions' }, + { status: 403 } + ) + } + + // Parse and validate request body + const body: UpdatePermissionsRequest = await request.json() + + // Prevent users from modifying their own admin permissions + const selfUpdate = body.updates.find((update) => update.userId === session.user.id) + if (selfUpdate && selfUpdate.permissions !== 'admin') { + return NextResponse.json( + { error: 'Cannot remove your own admin permissions' }, + { status: 400 } + ) + } + + // Process updates in a transaction + await db.transaction(async (tx) => { + for (const update of body.updates) { + // Delete existing permissions for this user and workspace + await tx + .delete(permissions) + .where( + and( + eq(permissions.userId, update.userId), + eq(permissions.entityType, 'workspace'), + eq(permissions.entityId, workspaceId) + ) + ) + + // Insert the single new permission + await tx.insert(permissions).values({ + id: crypto.randomUUID(), + userId: update.userId, + entityType: 'workspace' as const, + entityId: workspaceId, + permissionType: update.permissions, + createdAt: new Date(), + updatedAt: new Date(), + }) + } + }) + + const updatedUsers = await getUsersWithPermissions(workspaceId) + + return NextResponse.json({ + message: 'Permissions updated successfully', + users: updatedUsers, + total: updatedUsers.length, + }) + } catch (error) { + console.error('Error updating workspace permissions:', error) + return NextResponse.json({ error: 'Failed to update workspace permissions' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/workspaces/[id]/route.ts b/apps/sim/app/api/workspaces/[id]/route.ts index ab6c0cadc44..7c2ba26f247 100644 --- a/apps/sim/app/api/workspaces/[id]/route.ts +++ b/apps/sim/app/api/workspaces/[id]/route.ts @@ -1,8 +1,9 @@ import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' +import { getUserEntityPermissions } from '@/lib/permissions/utils' import { db } from '@/db' -import { workspace, workspaceMember } from '@/db/schema' +import { permissions, workspace } from '@/db/schema' export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { const { id } = await params @@ -14,16 +15,9 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ const workspaceId = id - // Check if user is a member of this workspace - const membership = await db - .select() - .from(workspaceMember) - .where( - and(eq(workspaceMember.workspaceId, workspaceId), eq(workspaceMember.userId, session.user.id)) - ) - .then((rows) => rows[0]) - - if (!membership) { + // Check if user has read access to this workspace + const userPermission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId) + if (userPermission !== 'read') { return NextResponse.json({ error: 'Workspace not found or access denied' }, { status: 404 }) } @@ -41,7 +35,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ workspace: { ...workspaceDetails, - role: membership.role, + permissions: userPermission, }, }) } @@ -56,21 +50,9 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< const workspaceId = id - // Check if user is a member with appropriate permissions - const membership = await db - .select() - .from(workspaceMember) - .where( - and(eq(workspaceMember.workspaceId, workspaceId), eq(workspaceMember.userId, session.user.id)) - ) - .then((rows) => rows[0]) - - if (!membership) { - return NextResponse.json({ error: 'Workspace not found or access denied' }, { status: 404 }) - } - - // For now, only allow owners to update workspace - if (membership.role !== 'owner') { + // Check if user has admin permissions to update workspace + const userPermission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId) + if (userPermission !== 'admin') { return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) } @@ -100,7 +82,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< return NextResponse.json({ workspace: { ...updatedWorkspace, - role: membership.role, + permissions: userPermission, }, }) } catch (error) { @@ -122,22 +104,23 @@ export async function DELETE( const workspaceId = id - // Check if user is the owner - const membership = await db - .select() - .from(workspaceMember) - .where( - and(eq(workspaceMember.workspaceId, workspaceId), eq(workspaceMember.userId, session.user.id)) - ) - .then((rows) => rows[0]) - - if (!membership || membership.role !== 'owner') { + // Check if user has admin permissions to delete workspace + const userPermission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId) + if (userPermission !== 'admin') { return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) } try { - // Delete workspace (cascade will handle members) - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + // Use a transaction to ensure data consistency + await db.transaction(async (tx) => { + // 1. Delete all permissions associated with this workspace + await tx + .delete(permissions) + .where(and(eq(permissions.entityType, 'workspace'), eq(permissions.entityId, workspaceId))) + + // 2. Delete workspace (cascade will handle members, workflows, etc.) + await tx.delete(workspace).where(eq(workspace.id, workspaceId)) + }) return NextResponse.json({ success: true }) } catch (error) { diff --git a/apps/sim/app/api/workspaces/invitations/accept/route.ts b/apps/sim/app/api/workspaces/invitations/accept/route.ts index 030e4ad1d4e..2b30618005a 100644 --- a/apps/sim/app/api/workspaces/invitations/accept/route.ts +++ b/apps/sim/app/api/workspaces/invitations/accept/route.ts @@ -4,7 +4,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { env } from '@/lib/env' import { db } from '@/db' -import { user, workspace, workspaceInvitation, workspaceMember } from '@/db/schema' +import { permissions, user, workspace, workspaceInvitation, workspaceMember } from '@/db/schema' // Accept an invitation via token export async function GET(req: NextRequest) { @@ -153,24 +153,44 @@ export async function GET(req: NextRequest) { ) } - // Add user to workspace - await db.insert(workspaceMember).values({ - id: randomUUID(), - workspaceId: invitation.workspaceId, - userId: session.user.id, - role: invitation.role, - joinedAt: new Date(), - updatedAt: new Date(), - }) - - // Mark invitation as accepted - await db - .update(workspaceInvitation) - .set({ - status: 'accepted', + // Add user to workspace, permissions, and mark invitation as accepted in a transaction + await db.transaction(async (tx) => { + // Add user to workspace + await tx.insert(workspaceMember).values({ + id: randomUUID(), + workspaceId: invitation.workspaceId, + userId: session.user.id, + role: invitation.role, + joinedAt: new Date(), updatedAt: new Date(), }) - .where(eq(workspaceInvitation.id, invitation.id)) + + // Create permissions for the user + const permissionsToInsert = [ + { + id: randomUUID(), + entityType: 'workspace' as const, + entityId: invitation.workspaceId, + userId: session.user.id, + permissionType: invitation.permissions || 'read', + createdAt: new Date(), + updatedAt: new Date(), + }, + ] + + if (permissionsToInsert.length > 0) { + await tx.insert(permissions).values(permissionsToInsert) + } + + // Mark invitation as accepted + await tx + .update(workspaceInvitation) + .set({ + status: 'accepted', + updatedAt: new Date(), + }) + .where(eq(workspaceInvitation.id, invitation.id)) + }) // Redirect to the workspace return NextResponse.redirect( diff --git a/apps/sim/app/api/workspaces/invitations/route.ts b/apps/sim/app/api/workspaces/invitations/route.ts index 1e9ab928e8a..6c7795f3549 100644 --- a/apps/sim/app/api/workspaces/invitations/route.ts +++ b/apps/sim/app/api/workspaces/invitations/route.ts @@ -9,13 +9,22 @@ import { env } from '@/lib/env' import { createLogger } from '@/lib/logs/console-logger' import { getEmailDomain } from '@/lib/urls/utils' import { db } from '@/db' -import { user, workspace, workspaceInvitation, workspaceMember } from '@/db/schema' +import { + type permissionTypeEnum, + user, + workspace, + workspaceInvitation, + workspaceMember, +} from '@/db/schema' export const dynamic = 'force-dynamic' const logger = createLogger('WorkspaceInvitationsAPI') const resend = env.RESEND_API_KEY ? new Resend(env.RESEND_API_KEY) : null +// Define the permission type +type PermissionType = (typeof permissionTypeEnum.enumValues)[number] + // Get all invitations for the user's workspaces export async function GET(req: NextRequest) { const session = await getSession() @@ -66,12 +75,21 @@ export async function POST(req: NextRequest) { } try { - const { workspaceId, email, role = 'member' } = await req.json() + const { workspaceId, email, role = 'member', permission = 'read' } = await req.json() if (!workspaceId || !email) { return NextResponse.json({ error: 'Workspace ID and email are required' }, { status: 400 }) } + // Validate permission type + const validPermissions: PermissionType[] = ['admin', 'write', 'read'] + if (!validPermissions.includes(permission)) { + return NextResponse.json( + { error: `Invalid permission: must be one of ${validPermissions.join(', ')}` }, + { status: 400 } + ) + } + // Check if user is authorized to invite to this workspace (must be owner) const membership = await db .select() @@ -160,22 +178,22 @@ export async function POST(req: NextRequest) { expiresAt.setDate(expiresAt.getDate() + 7) // 7 days expiry // Create the invitation - const invitation = await db - .insert(workspaceInvitation) - .values({ - id: randomUUID(), - workspaceId, - email, - inviterId: session.user.id, - role, - status: 'pending', - token, - expiresAt, - createdAt: new Date(), - updatedAt: new Date(), - }) - .returning() - .then((rows) => rows[0]) + const invitationData = { + id: randomUUID(), + workspaceId, + email, + inviterId: session.user.id, + role, + status: 'pending', + token, + permissions: permission, + expiresAt, + createdAt: new Date(), + updatedAt: new Date(), + } + + // Create invitation + await db.insert(workspaceInvitation).values(invitationData) // Send the invitation email await sendInvitationEmail({ @@ -185,7 +203,7 @@ export async function POST(req: NextRequest) { token: token, }) - return NextResponse.json({ success: true, invitation }) + return NextResponse.json({ success: true, invitation: invitationData }) } catch (error) { console.error('Error creating workspace invitation:', error) return NextResponse.json({ error: 'Failed to create invitation' }, { status: 500 }) diff --git a/apps/sim/app/api/workspaces/members/[id]/route.ts b/apps/sim/app/api/workspaces/members/[id]/route.ts index c3056e8db0b..57febd2cdae 100644 --- a/apps/sim/app/api/workspaces/members/[id]/route.ts +++ b/apps/sim/app/api/workspaces/members/[id]/route.ts @@ -4,80 +4,6 @@ import { getSession } from '@/lib/auth' import { db } from '@/db' import { workspaceMember } from '@/db/schema' -// Update a member's role -export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { - const { id } = await params - const session = await getSession() - - if (!session?.user?.id) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const membershipId = id - - try { - const { role } = await req.json() - - if (!role) { - return NextResponse.json({ error: 'Role is required' }, { status: 400 }) - } - - // Get the membership to update - const membership = await db - .select({ - id: workspaceMember.id, - workspaceId: workspaceMember.workspaceId, - userId: workspaceMember.userId, - role: workspaceMember.role, - }) - .from(workspaceMember) - .where(eq(workspaceMember.id, membershipId)) - .then((rows) => rows[0]) - - if (!membership) { - return NextResponse.json({ error: 'Membership not found' }, { status: 404 }) - } - - // Check if current user is an owner of the workspace - const currentUserMembership = await db - .select() - .from(workspaceMember) - .where( - and( - eq(workspaceMember.workspaceId, membership.workspaceId), - eq(workspaceMember.userId, session.user.id) - ) - ) - .then((rows) => rows[0]) - - if (!currentUserMembership || currentUserMembership.role !== 'owner') { - return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) - } - - // Prevent changing your own role if you're the owner - if (membership.userId === session.user.id && membership.role === 'owner') { - return NextResponse.json( - { error: 'Cannot change the role of the workspace owner' }, - { status: 400 } - ) - } - - // Update the role - await db - .update(workspaceMember) - .set({ - role, - updatedAt: new Date(), - }) - .where(eq(workspaceMember.id, membershipId)) - - return NextResponse.json({ success: true }) - } catch (error) { - console.error('Error updating workspace member:', error) - return NextResponse.json({ error: 'Failed to update workspace member' }, { status: 500 }) - } -} - // DELETE /api/workspaces/members/[id] - Remove a member from a workspace export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { const { id } = await params diff --git a/apps/sim/app/api/workspaces/members/route.ts b/apps/sim/app/api/workspaces/members/route.ts index 82e0cfe0483..2820fa1dd98 100644 --- a/apps/sim/app/api/workspaces/members/route.ts +++ b/apps/sim/app/api/workspaces/members/route.ts @@ -2,7 +2,49 @@ import { and, eq } from 'drizzle-orm' import { NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { db } from '@/db' -import { user, workspaceMember } from '@/db/schema' +import { permissions, type permissionTypeEnum, user, workspaceMember } from '@/db/schema' + +// Extract the enum type from Drizzle schema +type PermissionType = (typeof permissionTypeEnum.enumValues)[number] + +/** + * Helper function to check if a user has admin permission for a workspace + */ +async function hasAdminPermission(userId: string, workspaceId: string): Promise { + const result = await db + .select() + .from(permissions) + .where( + and( + eq(permissions.userId, userId), + eq(permissions.entityType, 'workspace'), + eq(permissions.entityId, workspaceId), + eq(permissions.permissionType, 'admin') + ) + ) + .limit(1) + + return result.length > 0 +} + +/** + * Helper function to create default permissions for a new member + */ +async function createMemberPermissions( + userId: string, + workspaceId: string, + memberPermission: PermissionType = 'read' +): Promise { + await db.insert(permissions).values({ + id: crypto.randomUUID(), + userId, + entityType: 'workspace' as const, + entityId: workspaceId, + permissionType: memberPermission, + createdAt: new Date(), + updatedAt: new Date(), + }) +} // Add a member to a workspace export async function POST(req: Request) { @@ -13,7 +55,7 @@ export async function POST(req: Request) { } try { - const { workspaceId, userEmail, role = 'member' } = await req.json() + const { workspaceId, userEmail, permission = 'read' } = await req.json() if (!workspaceId || !userEmail) { return NextResponse.json( @@ -22,19 +64,19 @@ export async function POST(req: Request) { ) } - // Check if current user is an owner or admin of the workspace - const currentUserMembership = await db - .select() - .from(workspaceMember) - .where( - and( - eq(workspaceMember.workspaceId, workspaceId), - eq(workspaceMember.userId, session.user.id) - ) + // Validate permission type + const validPermissions: PermissionType[] = ['admin', 'write', 'read'] + if (!validPermissions.includes(permission)) { + return NextResponse.json( + { error: `Invalid permission: must be one of ${validPermissions.join(', ')}` }, + { status: 400 } ) - .then((rows) => rows[0]) + } + + // Check if current user has admin permission for the workspace + const hasAdmin = await hasAdminPermission(session.user.id, workspaceId) - if (!currentUserMembership || currentUserMembership.role !== 'owner') { + if (!hasAdmin) { return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) } @@ -49,33 +91,53 @@ export async function POST(req: Request) { return NextResponse.json({ error: 'User not found' }, { status: 404 }) } - // Check if user is already a member - const existingMembership = await db + // Check if user already has permissions for this workspace + const existingPermissions = await db .select() - .from(workspaceMember) + .from(permissions) .where( - and(eq(workspaceMember.workspaceId, workspaceId), eq(workspaceMember.userId, targetUser.id)) + and( + eq(permissions.userId, targetUser.id), + eq(permissions.entityType, 'workspace'), + eq(permissions.entityId, workspaceId) + ) ) - .then((rows) => rows[0]) - if (existingMembership) { + if (existingPermissions.length > 0) { return NextResponse.json( - { error: 'User is already a member of this workspace' }, + { error: 'User already has permissions for this workspace' }, { status: 400 } ) } - // Add user to workspace - await db.insert(workspaceMember).values({ - id: crypto.randomUUID(), - workspaceId, - userId: targetUser.id, - role, - joinedAt: new Date(), - updatedAt: new Date(), + // Use a transaction to ensure data consistency + await db.transaction(async (tx) => { + // Add user to workspace members table (keeping for compatibility) + await tx.insert(workspaceMember).values({ + id: crypto.randomUUID(), + workspaceId, + userId: targetUser.id, + role: 'member', // Default role for compatibility + joinedAt: new Date(), + updatedAt: new Date(), + }) + + // Create single permission for the new member + await tx.insert(permissions).values({ + id: crypto.randomUUID(), + userId: targetUser.id, + entityType: 'workspace' as const, + entityId: workspaceId, + permissionType: permission, + createdAt: new Date(), + updatedAt: new Date(), + }) }) - return NextResponse.json({ success: true }) + return NextResponse.json({ + success: true, + message: `User added to workspace with ${permission} permission`, + }) } catch (error) { console.error('Error adding workspace member:', error) return NextResponse.json({ error: 'Failed to add workspace member' }, { status: 500 }) diff --git a/apps/sim/app/api/workspaces/route.ts b/apps/sim/app/api/workspaces/route.ts index 50236cafc5f..310d9d309e4 100644 --- a/apps/sim/app/api/workspaces/route.ts +++ b/apps/sim/app/api/workspaces/route.ts @@ -2,7 +2,7 @@ import { and, desc, eq, isNull } from 'drizzle-orm' import { NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { db } from '@/db' -import { workflow, workspace, workspaceMember } from '@/db/schema' +import { permissions, workflow, workspace, workspaceMember } from '@/db/schema' // Get all workspaces for the current user export async function GET() { @@ -98,6 +98,17 @@ async function createWorkspace(userId: string, name: string) { updatedAt: new Date(), }) + // Create default permissions for the workspace owner + await db.insert(permissions).values({ + id: crypto.randomUUID(), + entityType: 'workspace' as const, + entityId: workspaceId, + userId: userId, + permissionType: 'admin' as const, + createdAt: new Date(), + updatedAt: new Date(), + }) + // Return the workspace data directly instead of querying again return { id: workspaceId, diff --git a/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/deployment-controls.tsx b/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/deployment-controls.tsx index 1dc67e4df0b..cf1571a1194 100644 --- a/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/deployment-controls.tsx +++ b/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/deployment-controls.tsx @@ -1,10 +1,11 @@ 'use client' -import { useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { Loader2, Rocket } from 'lucide-react' import { Button } from '@/components/ui/button' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { cn } from '@/lib/utils' +import type { WorkspaceUserPermissions } from '@/hooks/use-user-permissions' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import type { WorkflowState } from '@/stores/workflows/workflow/types' import { DeployModal } from '../deploy-modal/deploy-modal' @@ -16,6 +17,7 @@ interface DeploymentControlsProps { deployedState: WorkflowState | null isLoadingDeployedState: boolean refetchDeployedState: () => Promise + userPermissions: WorkspaceUserPermissions } export function DeploymentControls({ @@ -25,6 +27,7 @@ export function DeploymentControls({ deployedState, isLoadingDeployedState, refetchDeployedState, + userPermissions, }: DeploymentControlsProps) { const deploymentStatus = useWorkflowRegistry((state) => state.getWorkflowDeploymentStatus(activeWorkflowId) @@ -52,6 +55,31 @@ export function DeploymentControls({ } catch (error) {} } + const canDeploy = userPermissions.canAdmin + const isDisabled = isDeploying || !canDeploy + + const handleDeployClick = useCallback(() => { + if (canDeploy) { + setIsModalOpen(true) + } + }, [canDeploy, setIsModalOpen]) + + const getTooltipText = () => { + if (!canDeploy) { + return 'Admin permissions required to deploy workflows as API' + } + if (isDeploying) { + return 'Deploying...' + } + if (isDeployed && workflowNeedsRedeployment) { + return 'Workflow changes detected' + } + if (isDeployed) { + return 'Deployment Settings' + } + return 'Deploy as API' + } + return ( <> @@ -60,9 +88,13 @@ export function DeploymentControls({ - - - Delete Workflow - + const renderDeleteButton = () => { + const canEdit = userPermissions.canEdit + const hasMultipleWorkflows = Object.keys(workflows).length > 1 + const isDisabled = !canEdit || !hasMultipleWorkflows + + const getTooltipText = () => { + if (!canEdit) return 'Edit permissions required to delete workflows' + if (!hasMultipleWorkflows) return 'Cannot delete the last workflow' + return 'Delete Workflow' + } - - - Delete Workflow - - Are you sure you want to delete this workflow? This action cannot be undone. - - - - Cancel - - Delete - - - - - ) + return ( + + + + + + + + {getTooltipText()} + + + + + Delete Workflow + + Are you sure you want to delete this workflow? This action cannot be undone. + + + + Cancel + + Delete + + + + + ) + } /** * Render deploy button with tooltip @@ -655,6 +707,7 @@ export function ControlBar() { deployedState={deployedState} isLoadingDeployedState={isLoadingDeployedState} refetchDeployedState={fetchDeployedState} + userPermissions={userPermissions} /> ) @@ -805,35 +858,44 @@ export function ControlBar() { /** * Render workflow duplicate button */ - const renderDuplicateButton = () => ( - - - - - Duplicate Workflow - - ) + const renderDuplicateButton = () => { + const canEdit = userPermissions.canEdit + + return ( + + + + + + {canEdit ? 'Duplicate Workflow' : 'Edit permissions required to duplicate workflows'} + + + ) + } /** * Render auto-layout button */ const renderAutoLayoutButton = () => { const handleAutoLayoutClick = () => { - if (isExecuting || isMultiRunning || isDebugging) { + if (isExecuting || isMultiRunning || isDebugging || !userPermissions.canEdit) { return } window.dispatchEvent(new CustomEvent('trigger-auto-layout')) } + const isDisabled = isExecuting || isMultiRunning || isDebugging || !userPermissions.canEdit + return ( @@ -841,14 +903,18 @@ export function ControlBar() { variant='ghost' size='icon' onClick={handleAutoLayoutClick} - className='hover:text-primary' - disabled={isExecuting || isMultiRunning || isDebugging} + className={cn('hover:text-primary', isDisabled && 'cursor-not-allowed opacity-50')} + disabled={isDisabled} > Auto Layout - Auto Layout + + {!userPermissions.canEdit + ? 'Edit permissions required to use auto-layout' + : 'Auto Layout'} + ) } @@ -918,7 +984,11 @@ export function ControlBar() { * Render debug mode toggle button */ const renderDebugModeToggle = () => { + const canDebug = userPermissions.canRead // Debug mode now requires only read permissions + const handleToggleDebugMode = () => { + if (!canDebug) return + if (isDebugModeEnabled) { if (!isExecuting) { useExecutionStore.getState().setIsDebugging(false) @@ -935,133 +1005,65 @@ export function ControlBar() { variant='ghost' size='icon' onClick={handleToggleDebugMode} - disabled={isExecuting || isMultiRunning} - className={cn(isDebugModeEnabled && 'text-amber-500')} + disabled={isExecuting || isMultiRunning || !canDebug} + className={cn( + isDebugModeEnabled && 'text-amber-500', + !canDebug && 'cursor-not-allowed opacity-50' + )} > Toggle Debug Mode - {isDebugModeEnabled ? 'Disable Debug Mode' : 'Enable Debug Mode'} + {!canDebug + ? 'Read permissions required to use debug mode' + : isDebugModeEnabled + ? 'Disable Debug Mode' + : 'Enable Debug Mode'} ) } - // Helper function to open subscription settings - const openSubscriptionSettings = () => { - if (typeof window !== 'undefined') { - window.dispatchEvent( - new CustomEvent('open-settings', { - detail: { tab: 'subscription' }, - }) - ) - } - } - /** * Render run workflow button with multi-run dropdown and cancel button */ - const renderRunButton = () => ( -
- {showRunProgress && isMultiRunning && ( -
- -

- {completedRuns}/{runCount} runs -

-
- )} + const renderRunButton = () => { + const canRun = userPermissions.canRead // Running only requires read permissions + const isLoadingPermissions = userPermissions.isLoading + const isButtonDisabled = + isExecuting || isMultiRunning || isCancelling || (!canRun && !isLoadingPermissions) - {/* Show how many blocks have been executed in debug mode if debugging */} - {isDebugging && ( -
-
- Debugging Mode + return ( +
+ {showRunProgress && isMultiRunning && ( +
+ +

+ {completedRuns}/{runCount} runs +

-
- )} + )} - {renderDebugControls()} + {/* Show how many blocks have been executed in debug mode if debugging */} + {isDebugging && ( +
+
+ Debugging Mode +
+
+ )} -
- {/* Main Run/Debug Button */} - - - - - - {usageExceeded ? ( -
-

Usage Limit Exceeded

-

- You've used {usageData?.currentUsage.toFixed(2)}$ of {usageData?.limit}$. Upgrade - your plan to continue. -

-
- ) : ( - <> - {isDebugModeEnabled - ? 'Debug Workflow' - : runCount === 1 - ? 'Run Workflow' - : `Run Workflow ${runCount} times`} - - )} -
-
+ {renderDebugControls()} - {/* Dropdown Trigger - Only show when not in debug mode and not multi-running */} - {!isDebugModeEnabled && !isMultiRunning && ( - - +
+ {/* Main Run/Debug Button */} + + - - - {RUN_COUNT_OPTIONS.map((count) => ( - setRunCount(count)} - className={cn('justify-center', runCount === count && 'bg-muted')} - > - {count} - - ))} - - - )} - - {/* Cancel Button - Only show when multi-running */} - {isMultiRunning && ( - - - - {runCount > 1 ? 'Cancel Runs' : 'Cancel Run'} + + {!canRun && !isLoadingPermissions ? ( + 'Read permissions required to run workflows' + ) : usageExceeded ? ( +
+

Usage Limit Exceeded

+

+ You've used {usageData?.currentUsage.toFixed(2)}$ of {usageData?.limit}$. + Upgrade your plan to continue. +

+
+ ) : ( + <> + {isDebugModeEnabled + ? 'Debug Workflow' + : runCount === 1 + ? 'Run Workflow' + : `Run Workflow ${runCount} times`} + + )} +
- )} + + {/* Dropdown Trigger - Only show when not in debug mode and not multi-running */} + {!isDebugModeEnabled && !isMultiRunning && ( + + + + + + {RUN_COUNT_OPTIONS.map((count) => ( + setRunCount(count)} + className={cn('justify-center', runCount === count && 'bg-muted')} + > + {count} + + ))} + + + )} + + {/* Cancel Button - Only show when multi-running */} + {isMultiRunning && ( + + + + + Cancel Runs + + )} +
-
- ) + ) + } return (
diff --git a/apps/sim/app/w/[id]/components/toolbar/components/toolbar-block/toolbar-block.tsx b/apps/sim/app/w/[id]/components/toolbar/components/toolbar-block/toolbar-block.tsx index 2220ea7c966..b625fa40403 100644 --- a/apps/sim/app/w/[id]/components/toolbar/components/toolbar-block/toolbar-block.tsx +++ b/apps/sim/app/w/[id]/components/toolbar/components/toolbar-block/toolbar-block.tsx @@ -1,19 +1,26 @@ import { useCallback } from 'react' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { cn } from '@/lib/utils' import type { BlockConfig } from '@/blocks/types' export type ToolbarBlockProps = { config: BlockConfig + disabled?: boolean } -export function ToolbarBlock({ config }: ToolbarBlockProps) { +export function ToolbarBlock({ config, disabled = false }: ToolbarBlockProps) { const handleDragStart = (e: React.DragEvent) => { + if (disabled) { + e.preventDefault() + return + } e.dataTransfer.setData('application/json', JSON.stringify({ type: config.type })) e.dataTransfer.effectAllowed = 'move' } // Handle click to add block const handleClick = useCallback(() => { - if (config.type === 'connectionBlock') return + if (config.type === 'connectionBlock' || disabled) return // Dispatch a custom event to be caught by the workflow component const event = new CustomEvent('add-block-from-toolbar', { @@ -22,23 +29,30 @@ export function ToolbarBlock({ config }: ToolbarBlockProps) { }, }) window.dispatchEvent(event) - }, [config.type]) + }, [config.type, disabled]) - return ( + const blockContent = (
@@ -47,4 +61,15 @@ export function ToolbarBlock({ config }: ToolbarBlockProps) {
) + + if (disabled) { + return ( + + {blockContent} + Edit permissions required to add blocks + + ) + } + + return blockContent } diff --git a/apps/sim/app/w/[id]/components/toolbar/components/toolbar-loop-block/toolbar-loop-block.tsx b/apps/sim/app/w/[id]/components/toolbar/components/toolbar-loop-block/toolbar-loop-block.tsx index d07ca5e5713..6097e644273 100644 --- a/apps/sim/app/w/[id]/components/toolbar/components/toolbar-loop-block/toolbar-loop-block.tsx +++ b/apps/sim/app/w/[id]/components/toolbar/components/toolbar-loop-block/toolbar-loop-block.tsx @@ -1,9 +1,19 @@ import { useCallback } from 'react' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { cn } from '@/lib/utils' import { LoopTool } from '../../../loop-node/loop-config' +type LoopToolbarItemProps = { + disabled?: boolean +} + // Custom component for the Loop Tool -export default function LoopToolbarItem() { +export default function LoopToolbarItem({ disabled = false }: LoopToolbarItemProps) { const handleDragStart = (e: React.DragEvent) => { + if (disabled) { + e.preventDefault() + return + } // Only send the essential data for the loop node const simplifiedData = { type: 'loop', @@ -13,30 +23,45 @@ export default function LoopToolbarItem() { } // Handle click to add loop block - const handleClick = useCallback((e: React.MouseEvent) => { - // Dispatch a custom event to be caught by the workflow component - const event = new CustomEvent('add-block-from-toolbar', { - detail: { - type: 'loop', - clientX: e.clientX, - clientY: e.clientY, - }, - }) - window.dispatchEvent(event) - }, []) + const handleClick = useCallback( + (e: React.MouseEvent) => { + if (disabled) return + + // Dispatch a custom event to be caught by the workflow component + const event = new CustomEvent('add-block-from-toolbar', { + detail: { + type: 'loop', + clientX: e.clientX, + clientY: e.clientY, + }, + }) + window.dispatchEvent(event) + }, + [disabled] + ) - return ( + const blockContent = (
- +

{LoopTool.name}

@@ -44,4 +69,15 @@ export default function LoopToolbarItem() {
) + + if (disabled) { + return ( + + {blockContent} + Edit permissions required to add blocks + + ) + } + + return blockContent } diff --git a/apps/sim/app/w/[id]/components/toolbar/components/toolbar-parallel-block/toolbar-parallel-block.tsx b/apps/sim/app/w/[id]/components/toolbar/components/toolbar-parallel-block/toolbar-parallel-block.tsx index 9f277ba67ba..08c732dacbb 100644 --- a/apps/sim/app/w/[id]/components/toolbar/components/toolbar-parallel-block/toolbar-parallel-block.tsx +++ b/apps/sim/app/w/[id]/components/toolbar/components/toolbar-parallel-block/toolbar-parallel-block.tsx @@ -1,9 +1,19 @@ import { useCallback } from 'react' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { cn } from '@/lib/utils' import { ParallelTool } from '../../../parallel-node/parallel-config' +type ParallelToolbarItemProps = { + disabled?: boolean +} + // Custom component for the Parallel Tool -export default function ParallelToolbarItem() { +export default function ParallelToolbarItem({ disabled = false }: ParallelToolbarItemProps) { const handleDragStart = (e: React.DragEvent) => { + if (disabled) { + e.preventDefault() + return + } // Only send the essential data for the parallel node const simplifiedData = { type: 'parallel', @@ -13,31 +23,46 @@ export default function ParallelToolbarItem() { } // Handle click to add parallel block - const handleClick = useCallback((e: React.MouseEvent) => { - // Dispatch a custom event to be caught by the workflow component - const event = new CustomEvent('add-block-from-toolbar', { - detail: { - type: 'parallel', - clientX: e.clientX, - clientY: e.clientY, - }, - bubbles: true, - }) - window.dispatchEvent(event) - }, []) + const handleClick = useCallback( + (e: React.MouseEvent) => { + if (disabled) return + + // Dispatch a custom event to be caught by the workflow component + const event = new CustomEvent('add-block-from-toolbar', { + detail: { + type: 'parallel', + clientX: e.clientX, + clientY: e.clientY, + }, + bubbles: true, + }) + window.dispatchEvent(event) + }, + [disabled] + ) - return ( + const blockContent = (
- +

{ParallelTool.name}

@@ -45,4 +70,15 @@ export default function ParallelToolbarItem() {
) + + if (disabled) { + return ( + + {blockContent} + Edit permissions required to add blocks + + ) + } + + return blockContent } diff --git a/apps/sim/app/w/[id]/components/toolbar/toolbar.tsx b/apps/sim/app/w/[id]/components/toolbar/toolbar.tsx index a5b6d6a1746..5a257b81b48 100644 --- a/apps/sim/app/w/[id]/components/toolbar/toolbar.tsx +++ b/apps/sim/app/w/[id]/components/toolbar/toolbar.tsx @@ -2,18 +2,33 @@ import { useMemo, useState } from 'react' import { PanelLeftClose, PanelRight, Search } from 'lucide-react' +import { useParams } from 'next/navigation' import { Input } from '@/components/ui/input' import { ScrollArea } from '@/components/ui/scroll-area' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { getAllBlocks, getBlocksByCategory } from '@/blocks' import type { BlockCategory } from '@/blocks/types' +import { useUserPermissions } from '@/hooks/use-user-permissions' import { useSidebarStore } from '@/stores/sidebar/store' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { ToolbarBlock } from './components/toolbar-block/toolbar-block' import LoopToolbarItem from './components/toolbar-loop-block/toolbar-loop-block' import ParallelToolbarItem from './components/toolbar-parallel-block/toolbar-parallel-block' import { ToolbarTabs } from './components/toolbar-tabs/toolbar-tabs' export function Toolbar() { + const params = useParams() + const workflowId = params?.id as string + + // Get the workspace ID from the workflow registry + const activeWorkspaceId = useWorkflowRegistry((state) => state.activeWorkspaceId) + const currentWorkflow = useWorkflowRegistry((state) => + workflowId ? state.workflows[workflowId] : null + ) + const workspaceId = currentWorkflow?.workspaceId || activeWorkspaceId + + const userPermissions = useUserPermissions(workspaceId) + const [activeTab, setActiveTab] = useState('blocks') const [searchQuery, setSearchQuery] = useState('') const { mode, isExpanded } = useSidebarStore() @@ -87,12 +102,12 @@ export function Toolbar() {
{blocks.map((block) => ( - + ))} {activeTab === 'blocks' && !searchQuery && ( <> - - + + )}
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/action-bar/action-bar.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/action-bar/action-bar.tsx index 7a626eab205..aa0d6ac2e34 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/action-bar/action-bar.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/action-bar/action-bar.tsx @@ -7,9 +7,10 @@ import { useWorkflowStore } from '@/stores/workflows/workflow/store' interface ActionBarProps { blockId: string blockType: string + disabled?: boolean } -export function ActionBar({ blockId, blockType }: ActionBarProps) { +export function ActionBar({ blockId, blockType, disabled = false }: ActionBarProps) { const removeBlock = useWorkflowStore((state) => state.removeBlock) const toggleBlockEnabled = useWorkflowStore((state) => state.toggleBlockEnabled) const toggleBlockHandles = useWorkflowStore((state) => state.toggleBlockHandles) @@ -52,13 +53,20 @@ export function ActionBar({ blockId, blockType }: ActionBarProps) { - {isEnabled ? 'Disable Block' : 'Enable Block'} + + {disabled ? 'Read-only mode' : isEnabled ? 'Disable Block' : 'Enable Block'} + {!isStarterBlock && ( @@ -67,13 +75,20 @@ export function ActionBar({ blockId, blockType }: ActionBarProps) { - Duplicate Block + + {disabled ? 'Read-only mode' : 'Duplicate Block'} + )} @@ -82,8 +97,13 @@ export function ActionBar({ blockId, blockType }: ActionBarProps) { - {horizontalHandles ? 'Vertical Ports' : 'Horizontal Ports'} + {disabled ? 'Read-only mode' : horizontalHandles ? 'Vertical Ports' : 'Horizontal Ports'} @@ -103,13 +123,23 @@ export function ActionBar({ blockId, blockType }: ActionBarProps) { - Delete Block + + {disabled ? 'Read-only mode' : 'Delete Block'} + )}
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/connection-blocks/connection-blocks.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/connection-blocks/connection-blocks.tsx index 10accd8d66d..baf322f53e7 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/connection-blocks/connection-blocks.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/connection-blocks/connection-blocks.tsx @@ -1,10 +1,12 @@ import { Card } from '@/components/ui/card' +import { cn } from '@/lib/utils' import { type ConnectedBlock, useBlockConnections } from '@/app/w/[id]/hooks/use-block-connections' import { useSubBlockStore } from '@/stores/workflows/subblock/store' interface ConnectionBlocksProps { blockId: string setIsConnecting: (isConnecting: boolean) => void + isDisabled?: boolean } interface ResponseField { @@ -13,7 +15,11 @@ interface ResponseField { description?: string } -export function ConnectionBlocks({ blockId, setIsConnecting }: ConnectionBlocksProps) { +export function ConnectionBlocks({ + blockId, + setIsConnecting, + isDisabled = false, +}: ConnectionBlocksProps) { const { incomingConnections, hasIncomingConnections } = useBlockConnections(blockId) if (!hasIncomingConnections) return null @@ -23,6 +29,11 @@ export function ConnectionBlocks({ blockId, setIsConnecting }: ConnectionBlocksP connection: ConnectedBlock, field?: ResponseField ) => { + if (isDisabled) { + e.preventDefault() + return + } + e.stopPropagation() // Prevent parent drag handlers from firing setIsConnecting(true) e.dataTransfer.setData( @@ -127,10 +138,15 @@ export function ConnectionBlocks({ blockId, setIsConnecting }: ConnectionBlocksP return ( handleDragStart(e, connection, field)} onDragEnd={handleDragEnd} - className='group flex w-max cursor-grab items-center rounded-lg border bg-card p-2 shadow-sm transition-colors hover:bg-accent/50 active:cursor-grabbing' + className={cn( + 'group flex w-max items-center rounded-lg border bg-card p-2 shadow-sm transition-colors', + !isDisabled + ? 'cursor-grab hover:bg-accent/50 active:cursor-grabbing' + : 'cursor-not-allowed opacity-60' + )} >
{displayName} diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/checkbox-list.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/checkbox-list.tsx index 842462abfb5..72f0deb46b7 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/checkbox-list.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/checkbox-list.tsx @@ -11,6 +11,7 @@ interface CheckboxListProps { layout?: 'full' | 'half' isPreview?: boolean subBlockValues?: Record + disabled?: boolean } export function CheckboxList({ @@ -21,6 +22,7 @@ export function CheckboxList({ layout, isPreview = false, subBlockValues, + disabled = false, }: CheckboxListProps) { return (
@@ -35,8 +37,8 @@ export function CheckboxList({ const value = isPreview ? previewValue : storeValue const handleChange = (checked: boolean) => { - // Only update store when not in preview mode - if (!isPreview) { + // Only update store when not in preview mode or disabled + if (!isPreview && !disabled) { setStoreValue(checked) } } @@ -47,7 +49,7 @@ export function CheckboxList({ id={`${blockId}-${option.id}`} checked={Boolean(value)} onCheckedChange={handleChange} - disabled={isPreview} + disabled={isPreview || disabled} />

-
+ + +
+ {hasPendingChanges && userPerms.canAdmin && ( +
+ + +
+ )} +
diff --git a/apps/sim/app/w/components/sidebar/components/workspace-header/workspace-header.tsx b/apps/sim/app/w/components/sidebar/components/workspace-header/workspace-header.tsx index 473a3494c04..aac282e0dbd 100644 --- a/apps/sim/app/w/components/sidebar/components/workspace-header/workspace-header.tsx +++ b/apps/sim/app/w/components/sidebar/components/workspace-header/workspace-header.tsx @@ -29,6 +29,7 @@ import { Input } from '@/components/ui/input' import { Skeleton } from '@/components/ui/skeleton' import { useSession } from '@/lib/auth-client' import { cn } from '@/lib/utils' +import { useUserPermissions } from '@/hooks/use-user-permissions' import { useSidebarStore } from '@/stores/sidebar/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' @@ -243,6 +244,9 @@ export function WorkspaceHeader({ // Get workflowRegistry state and actions const { activeWorkspaceId, switchToWorkspace, setActiveWorkspaceId } = useWorkflowRegistry() + // Get user permissions for the active workspace + const userPermissions = useUserPermissions(activeWorkspace?.id || '') + const userName = sessionData?.user?.name || sessionData?.user?.email || 'User' // Set isClientLoading to false after hydration @@ -348,18 +352,14 @@ export function WorkspaceHeader({ } const handleUpdateWorkspace = async (id: string, name: string) => { - // Check if user has permission to update the workspace - const workspace = workspaces.find((w) => w.id === id) - if (!workspace || workspace.role !== 'owner') { - console.error('Permission denied: Only workspace owners can update workspaces') - return - } - + // For update operations, we need to check permissions for the specific workspace + // Since we can only use hooks at the component level, we'll make the API call + // and let the backend handle the permission check setIsWorkspacesLoading(true) try { const response = await fetch(`/api/workspaces/${id}`, { - method: 'PUT', + method: 'PATCH', headers: { 'Content-Type': 'application/json', }, @@ -367,19 +367,26 @@ export function WorkspaceHeader({ }) if (!response.ok) { + if (response.status === 403) { + console.error( + 'Permission denied: Only users with admin permissions can update workspaces' + ) + } throw new Error('Failed to update workspace') } - const { workspace } = await response.json() + const { workspace: updatedWorkspace } = await response.json() // Update workspaces list setWorkspaces((prevWorkspaces) => - prevWorkspaces.map((w) => (w.id === workspace.id ? { ...w, name: workspace.name } : w)) + prevWorkspaces.map((w) => + w.id === updatedWorkspace.id ? { ...w, name: updatedWorkspace.name } : w + ) ) // If active workspace was updated, update it too - if (activeWorkspace?.id === workspace.id) { - setActiveWorkspace({ ...activeWorkspace, name: workspace.name } as Workspace) + if (activeWorkspace?.id === updatedWorkspace.id) { + setActiveWorkspace({ ...activeWorkspace, name: updatedWorkspace.name } as Workspace) } } catch (err) { console.error('Error updating workspace:', err) @@ -389,13 +396,9 @@ export function WorkspaceHeader({ } const handleDeleteWorkspace = async (id: string) => { - // Check if user has permission to delete the workspace - const workspace = workspaces.find((w) => w.id === id) - if (!workspace || workspace.role !== 'owner') { - console.error('Permission denied: Only workspace owners can delete workspaces') - return - } - + // For delete operations, we need to check permissions for the specific workspace + // Since we can only use hooks at the component level, we'll make the API call + // and let the backend handle the permission check setIsDeleting(true) try { @@ -404,6 +407,11 @@ export function WorkspaceHeader({ }) if (!response.ok) { + if (response.status === 403) { + console.error( + 'Permission denied: Only users with admin permissions can delete workspaces' + ) + } throw new Error('Failed to delete workspace') } @@ -429,9 +437,8 @@ export function WorkspaceHeader({ const openEditModal = (workspace: Workspace, e: React.MouseEvent) => { e.stopPropagation() - // Check if user has permission to edit the workspace - if (workspace.role !== 'owner') { - console.error('Permission denied: Only workspace owners can edit workspaces') + // Only show edit/delete options for the active workspace if user has admin permissions + if (activeWorkspace?.id !== workspace.id || !userPermissions.canAdmin) { return } setEditingWorkspace(workspace) @@ -584,7 +591,7 @@ export function WorkspaceHeader({ onClick={() => switchWorkspace(workspace)} > {workspace.name} - {workspace.role === 'owner' && ( + {userPermissions.canAdmin && activeWorkspace?.id === workspace.id && (