From debcd7601988b6c6a0b8392d0410b9554cf70b49 Mon Sep 17 00:00:00 2001 From: Waleed Date: Thu, 15 Jan 2026 10:01:00 -0800 Subject: [PATCH 01/15] improvement(slack): updated docs to include information for slack marketplace submission (#2837) --- apps/docs/content/docs/en/tools/slack.mdx | 21 +++++++++++++++++++++ apps/sim/lib/oauth/oauth.ts | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/docs/content/docs/en/tools/slack.mdx b/apps/docs/content/docs/en/tools/slack.mdx index a1e847cd63..4462adba61 100644 --- a/apps/docs/content/docs/en/tools/slack.mdx +++ b/apps/docs/content/docs/en/tools/slack.mdx @@ -43,6 +43,27 @@ In Sim, the Slack integration enables your agents to programmatically interact w - **Download files**: Retrieve files shared in Slack channels for processing or archival This allows for powerful automation scenarios such as sending notifications with dynamic updates, managing conversational flows with editable status messages, acknowledging important messages with reactions, and maintaining clean channels by removing outdated bot messages. Your agents can deliver timely information, update messages as workflows progress, create collaborative documents, or alert team members when attention is needed. This integration bridges the gap between your AI workflows and your team's communication, ensuring everyone stays informed with accurate, up-to-date information. By connecting Sim with Slack, you can create agents that keep your team updated with relevant information at the right time, enhance collaboration by sharing and updating insights automatically, and reduce the need for manual status updates—all while leveraging your existing Slack workspace where your team already communicates. + +## Getting Started + +To connect Slack to your Sim workflows: + +1. Sign up or log in at [sim.ai](https://sim.ai) +2. Create a new workflow or open an existing one +3. Drag a **Slack** block onto your canvas +4. Click the credential selector and choose **Connect** +5. Authorize Sim to access your Slack workspace +6. Select your target channel or user + +Once connected, you can use any of the Slack operations listed below. + +## AI-Generated Content + +Sim workflows may use AI models to generate messages and responses sent to Slack. AI-generated content may be inaccurate or contain errors. Always review automated outputs, especially for critical communications. + +## Need Help? + +If you encounter issues with the Slack integration, contact us at [help@sim.ai](mailto:help@sim.ai) {/* MANUAL-CONTENT-END */} diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 94496a24b0..d00fea2e4e 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -495,7 +495,7 @@ export const OAUTH_PROVIDERS: Record = { services: { slack: { name: 'Slack', - description: 'Send messages using a Slack bot.', + description: 'Send messages using a bot for Slack.', providerId: 'slack', icon: SlackIcon, baseProviderIcon: SlackIcon, From 5e44357b9f31c60cf264e4e5f088151d446cd853 Mon Sep 17 00:00:00 2001 From: Waleed Date: Thu, 15 Jan 2026 10:34:38 -0800 Subject: [PATCH 02/15] improvement(snapshot): show subblocks for trigger only blocks in frozen canvas (#2838) * improvement(snapshot): show subblocks for trigger only blocks in frozen canvas * ack comment --- .../w/components/preview/components/block-details-sidebar.tsx | 4 +++- .../[workspaceId]/w/components/preview/components/block.tsx | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block-details-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block-details-sidebar.tsx index 5a48723c68..5ff5ea92c4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block-details-sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block-details-sidebar.tsx @@ -1124,7 +1124,9 @@ function BlockDetailsSidebarContent({ const visibleSubBlocks = blockConfig.subBlocks.filter((subBlock) => { if (subBlock.hidden || subBlock.hideFromPreview) return false - if (subBlock.mode === 'trigger') return false + // Only filter out trigger-mode subblocks for non-trigger blocks + // Trigger-only blocks (category 'triggers') should display their trigger subblocks + if (subBlock.mode === 'trigger' && blockConfig.category !== 'triggers') return false if (subBlock.condition) { return evaluateCondition(subBlock.condition, subBlockValues) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block.tsx index 5725ec2fb2..41507ca6be 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block.tsx @@ -46,7 +46,7 @@ function WorkflowPreviewBlockInner({ data }: NodeProps return blockConfig.subBlocks.filter((subBlock) => { if (subBlock.hidden) return false if (subBlock.hideFromPreview) return false - if (subBlock.mode === 'trigger') return false + if (subBlock.mode === 'trigger' && blockConfig.category !== 'triggers') return false if (subBlock.mode === 'advanced') return false return true }) From e499cc4f829a7ee3c84b9f8c4dbb4e04669c9bae Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 15 Jan 2026 12:42:05 -0800 Subject: [PATCH 03/15] improvement(webhooks): lifecycle management with external providers, remove save configuration (#2831) * fix(webhooks): lifecycle code accuracy * remove save configuration button * remove useless instruction * address greptile comments * fix lint * on undeploy cleanup webhooks --- .../v1/admin/workflows/[id]/deploy/route.ts | 12 +- apps/sim/app/api/webhooks/[id]/route.ts | 51 +- apps/sim/app/api/webhooks/route.ts | 888 +----------------- .../app/api/workflows/[id]/deploy/route.ts | 26 +- .../sim/app/api/workflows/[id]/state/route.ts | 215 +---- .../components/deploy-modal/deploy-modal.tsx | 8 + .../components/sub-block/components/index.ts | 1 - .../components/trigger-save/trigger-save.tsx | 348 ------- .../editor/components/sub-block/sub-block.tsx | 12 - apps/sim/blocks/types.ts | 3 + apps/sim/lib/webhooks/deploy.ts | 578 ++++++++++++ .../lib/webhooks/provider-subscriptions.ts | 854 ++++++++++++++++- apps/sim/socket/database/operations.ts | 36 +- apps/sim/triggers/constants.ts | 16 - apps/sim/triggers/grain/utils.ts | 1 - apps/sim/triggers/hubspot/utils.ts | 5 +- apps/sim/triggers/index.ts | 13 +- apps/sim/triggers/lemlist/utils.ts | 1 - apps/sim/triggers/twilio_voice/webhook.ts | 1 - 19 files changed, 1584 insertions(+), 1485 deletions(-) delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/trigger-save/trigger-save.tsx create mode 100644 apps/sim/lib/webhooks/deploy.ts diff --git a/apps/sim/app/api/v1/admin/workflows/[id]/deploy/route.ts b/apps/sim/app/api/v1/admin/workflows/[id]/deploy/route.ts index a868313c0f..d76d765ab9 100644 --- a/apps/sim/app/api/v1/admin/workflows/[id]/deploy/route.ts +++ b/apps/sim/app/api/v1/admin/workflows/[id]/deploy/route.ts @@ -1,6 +1,8 @@ import { db, workflow } from '@sim/db' import { createLogger } from '@sim/logger' import { eq } from 'drizzle-orm' +import { generateRequestId } from '@/lib/core/utils/request' +import { cleanupWebhooksForWorkflow } from '@/lib/webhooks/deploy' import { deployWorkflow, loadWorkflowFromNormalizedTables, @@ -80,10 +82,11 @@ export const POST = withAdminAuthParams(async (request, context) => export const DELETE = withAdminAuthParams(async (request, context) => { const { id: workflowId } = await context.params + const requestId = generateRequestId() try { const [workflowRecord] = await db - .select({ id: workflow.id }) + .select() .from(workflow) .where(eq(workflow.id, workflowId)) .limit(1) @@ -92,6 +95,13 @@ export const DELETE = withAdminAuthParams(async (request, context) return notFoundResponse('Workflow') } + // Clean up external webhook subscriptions before undeploying + await cleanupWebhooksForWorkflow( + workflowId, + workflowRecord as Record, + requestId + ) + const result = await undeployWorkflow({ workflowId }) if (!result.success) { return internalErrorResponse(result.error || 'Failed to undeploy workflow') diff --git a/apps/sim/app/api/webhooks/[id]/route.ts b/apps/sim/app/api/webhooks/[id]/route.ts index 0cd31402df..7f10feefb5 100644 --- a/apps/sim/app/api/webhooks/[id]/route.ts +++ b/apps/sim/app/api/webhooks/[id]/route.ts @@ -7,6 +7,11 @@ import { getSession } from '@/lib/auth' import { validateInteger } from '@/lib/core/security/input-validation' import { PlatformEvents } from '@/lib/core/telemetry' import { generateRequestId } from '@/lib/core/utils/request' +import { + cleanupExternalWebhook, + createExternalWebhookSubscription, + shouldRecreateExternalWebhookSubscription, +} from '@/lib/webhooks/provider-subscriptions' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' const logger = createLogger('WebhookAPI') @@ -177,6 +182,46 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< return NextResponse.json({ error: 'Access denied' }, { status: 403 }) } + const existingProviderConfig = + (webhookData.webhook.providerConfig as Record) || {} + let nextProviderConfig = + providerConfig !== undefined && + resolvedProviderConfig && + typeof resolvedProviderConfig === 'object' + ? (resolvedProviderConfig as Record) + : existingProviderConfig + const nextProvider = (provider ?? webhookData.webhook.provider) as string + + if ( + providerConfig !== undefined && + shouldRecreateExternalWebhookSubscription({ + previousProvider: webhookData.webhook.provider as string, + nextProvider, + previousConfig: existingProviderConfig, + nextConfig: nextProviderConfig, + }) + ) { + await cleanupExternalWebhook( + { ...webhookData.webhook, providerConfig: existingProviderConfig }, + webhookData.workflow, + requestId + ) + + const result = await createExternalWebhookSubscription( + request, + { + ...webhookData.webhook, + provider: nextProvider, + providerConfig: nextProviderConfig, + }, + webhookData.workflow, + session.user.id, + requestId + ) + + nextProviderConfig = result.updatedProviderConfig as Record + } + logger.debug(`[${requestId}] Updating webhook properties`, { hasPathUpdate: path !== undefined, hasProviderUpdate: provider !== undefined, @@ -188,16 +233,16 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< // Merge providerConfig to preserve credential-related fields let finalProviderConfig = webhooks[0].webhook.providerConfig if (providerConfig !== undefined) { - const existingConfig = (webhooks[0].webhook.providerConfig as Record) || {} + const existingConfig = existingProviderConfig finalProviderConfig = { - ...resolvedProviderConfig, + ...nextProviderConfig, credentialId: existingConfig.credentialId, credentialSetId: existingConfig.credentialSetId, userId: existingConfig.userId, historyId: existingConfig.historyId, lastCheckedTimestamp: existingConfig.lastCheckedTimestamp, setupCompleted: existingConfig.setupCompleted, - externalId: existingConfig.externalId, + externalId: nextProviderConfig.externalId ?? existingConfig.externalId, } } diff --git a/apps/sim/app/api/webhooks/route.ts b/apps/sim/app/api/webhooks/route.ts index 4e980646b9..0ae8b65d91 100644 --- a/apps/sim/app/api/webhooks/route.ts +++ b/apps/sim/app/api/webhooks/route.ts @@ -7,9 +7,8 @@ import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { PlatformEvents } from '@/lib/core/telemetry' import { generateRequestId } from '@/lib/core/utils/request' -import { getBaseUrl } from '@/lib/core/utils/urls' +import { createExternalWebhookSubscription } from '@/lib/webhooks/provider-subscriptions' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' -import { getOAuthToken } from '@/app/api/auth/oauth/utils' const logger = createLogger('WebhooksAPI') @@ -257,7 +256,7 @@ export async function POST(request: NextRequest) { const finalProviderConfig = providerConfig || {} const { resolveEnvVarsInObject } = await import('@/lib/webhooks/env-resolver') - const resolvedProviderConfig = await resolveEnvVarsInObject( + let resolvedProviderConfig = await resolveEnvVarsInObject( finalProviderConfig, userId, workflowRecord.workspaceId || undefined @@ -414,149 +413,33 @@ export async function POST(request: NextRequest) { } // --- End Credential Set Handling --- - // Create external subscriptions before saving to DB to prevent orphaned records - let externalSubscriptionId: string | undefined let externalSubscriptionCreated = false - - const createTempWebhookData = () => ({ + const createTempWebhookData = (providerConfigOverride = resolvedProviderConfig) => ({ id: targetWebhookId || nanoid(), path: finalPath, - providerConfig: resolvedProviderConfig, + provider, + providerConfig: providerConfigOverride, }) - if (provider === 'airtable') { - logger.info(`[${requestId}] Creating Airtable subscription before saving to database`) - try { - externalSubscriptionId = await createAirtableWebhookSubscription( - request, - userId, - createTempWebhookData(), - requestId - ) - if (externalSubscriptionId) { - resolvedProviderConfig.externalId = externalSubscriptionId - externalSubscriptionCreated = true - } - } catch (err) { - logger.error(`[${requestId}] Error creating Airtable webhook subscription`, err) - return NextResponse.json( - { - error: 'Failed to create webhook in Airtable', - details: err instanceof Error ? err.message : 'Unknown error', - }, - { status: 500 } - ) - } - } - - if (provider === 'calendly') { - logger.info(`[${requestId}] Creating Calendly subscription before saving to database`) - try { - externalSubscriptionId = await createCalendlyWebhookSubscription( - request, - userId, - createTempWebhookData(), - requestId - ) - if (externalSubscriptionId) { - resolvedProviderConfig.externalId = externalSubscriptionId - externalSubscriptionCreated = true - } - } catch (err) { - logger.error(`[${requestId}] Error creating Calendly webhook subscription`, err) - return NextResponse.json( - { - error: 'Failed to create webhook in Calendly', - details: err instanceof Error ? err.message : 'Unknown error', - }, - { status: 500 } - ) - } - } - - if (provider === 'microsoft-teams') { - const { createTeamsSubscription } = await import('@/lib/webhooks/provider-subscriptions') - logger.info(`[${requestId}] Creating Teams subscription before saving to database`) - try { - await createTeamsSubscription(request, createTempWebhookData(), workflowRecord, requestId) - externalSubscriptionCreated = true - } catch (err) { - logger.error(`[${requestId}] Error creating Teams subscription`, err) - return NextResponse.json( - { - error: 'Failed to create Teams subscription', - details: err instanceof Error ? err.message : 'Unknown error', - }, - { status: 500 } - ) - } - } - - if (provider === 'telegram') { - const { createTelegramWebhook } = await import('@/lib/webhooks/provider-subscriptions') - logger.info(`[${requestId}] Creating Telegram webhook before saving to database`) - try { - await createTelegramWebhook(request, createTempWebhookData(), requestId) - externalSubscriptionCreated = true - } catch (err) { - logger.error(`[${requestId}] Error creating Telegram webhook`, err) - return NextResponse.json( - { - error: 'Failed to create Telegram webhook', - details: err instanceof Error ? err.message : 'Unknown error', - }, - { status: 500 } - ) - } - } - - if (provider === 'webflow') { - logger.info(`[${requestId}] Creating Webflow subscription before saving to database`) - try { - externalSubscriptionId = await createWebflowWebhookSubscription( - request, - userId, - createTempWebhookData(), - requestId - ) - if (externalSubscriptionId) { - resolvedProviderConfig.externalId = externalSubscriptionId - externalSubscriptionCreated = true - } - } catch (err) { - logger.error(`[${requestId}] Error creating Webflow webhook subscription`, err) - return NextResponse.json( - { - error: 'Failed to create webhook in Webflow', - details: err instanceof Error ? err.message : 'Unknown error', - }, - { status: 500 } - ) - } - } - - if (provider === 'typeform') { - const { createTypeformWebhook } = await import('@/lib/webhooks/provider-subscriptions') - logger.info(`[${requestId}] Creating Typeform webhook before saving to database`) - try { - const usedTag = await createTypeformWebhook(request, createTempWebhookData(), requestId) - - if (!resolvedProviderConfig.webhookTag) { - resolvedProviderConfig.webhookTag = usedTag - logger.info(`[${requestId}] Stored auto-generated webhook tag: ${usedTag}`) - } - - externalSubscriptionCreated = true - } catch (err) { - logger.error(`[${requestId}] Error creating Typeform webhook`, err) - return NextResponse.json( - { - error: 'Failed to create webhook in Typeform', - details: err instanceof Error ? err.message : 'Unknown error', - }, - { status: 500 } - ) - } + try { + const result = await createExternalWebhookSubscription( + request, + createTempWebhookData(), + workflowRecord, + userId, + requestId + ) + resolvedProviderConfig = result.updatedProviderConfig as Record + externalSubscriptionCreated = result.externalSubscriptionCreated + } catch (err) { + logger.error(`[${requestId}] Error creating external webhook subscription`, err) + return NextResponse.json( + { + error: 'Failed to create external webhook subscription', + details: err instanceof Error ? err.message : 'Unknown error', + }, + { status: 500 } + ) } // Now save to database (only if subscription succeeded or provider doesn't need external subscription) @@ -617,7 +500,11 @@ export async function POST(request: NextRequest) { logger.error(`[${requestId}] DB save failed, cleaning up external subscription`, dbError) try { const { cleanupExternalWebhook } = await import('@/lib/webhooks/provider-subscriptions') - await cleanupExternalWebhook(createTempWebhookData(), workflowRecord, requestId) + await cleanupExternalWebhook( + createTempWebhookData(resolvedProviderConfig), + workflowRecord, + requestId + ) } catch (cleanupError) { logger.error( `[${requestId}] Failed to cleanup external subscription after DB save failure`, @@ -741,110 +628,6 @@ export async function POST(request: NextRequest) { } // --- End RSS specific logic --- - if (savedWebhook && provider === 'grain') { - logger.info(`[${requestId}] Grain provider detected. Creating Grain webhook subscription.`) - try { - const grainResult = await createGrainWebhookSubscription( - request, - { - id: savedWebhook.id, - path: savedWebhook.path, - providerConfig: savedWebhook.providerConfig, - }, - requestId - ) - - if (grainResult) { - // Update the webhook record with the external Grain hook ID and event types for filtering - const updatedConfig = { - ...(savedWebhook.providerConfig as Record), - externalId: grainResult.id, - eventTypes: grainResult.eventTypes, - } - await db - .update(webhook) - .set({ - providerConfig: updatedConfig, - updatedAt: new Date(), - }) - .where(eq(webhook.id, savedWebhook.id)) - - savedWebhook.providerConfig = updatedConfig - logger.info(`[${requestId}] Successfully created Grain webhook`, { - grainHookId: grainResult.id, - eventTypes: grainResult.eventTypes, - webhookId: savedWebhook.id, - }) - } - } catch (err) { - logger.error( - `[${requestId}] Error creating Grain webhook subscription, rolling back webhook`, - err - ) - await db.delete(webhook).where(eq(webhook.id, savedWebhook.id)) - return NextResponse.json( - { - error: 'Failed to create webhook in Grain', - details: err instanceof Error ? err.message : 'Unknown error', - }, - { status: 500 } - ) - } - } - // --- End Grain specific logic --- - - // --- Lemlist specific logic --- - if (savedWebhook && provider === 'lemlist') { - logger.info( - `[${requestId}] Lemlist provider detected. Creating Lemlist webhook subscription.` - ) - try { - const lemlistResult = await createLemlistWebhookSubscription( - { - id: savedWebhook.id, - path: savedWebhook.path, - providerConfig: savedWebhook.providerConfig, - }, - requestId - ) - - if (lemlistResult) { - // Update the webhook record with the external Lemlist hook ID - const updatedConfig = { - ...(savedWebhook.providerConfig as Record), - externalId: lemlistResult.id, - } - await db - .update(webhook) - .set({ - providerConfig: updatedConfig, - updatedAt: new Date(), - }) - .where(eq(webhook.id, savedWebhook.id)) - - savedWebhook.providerConfig = updatedConfig - logger.info(`[${requestId}] Successfully created Lemlist webhook`, { - lemlistHookId: lemlistResult.id, - webhookId: savedWebhook.id, - }) - } - } catch (err) { - logger.error( - `[${requestId}] Error creating Lemlist webhook subscription, rolling back webhook`, - err - ) - await db.delete(webhook).where(eq(webhook.id, savedWebhook.id)) - return NextResponse.json( - { - error: 'Failed to create webhook in Lemlist', - details: err instanceof Error ? err.message : 'Unknown error', - }, - { status: 500 } - ) - } - } - // --- End Lemlist specific logic --- - if (!targetWebhookId && savedWebhook) { try { PlatformEvents.webhookCreated({ @@ -868,616 +651,3 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } - -// Helper function to create the webhook subscription in Airtable -async function createAirtableWebhookSubscription( - request: NextRequest, - userId: string, - webhookData: any, - requestId: string -): Promise { - try { - const { path, providerConfig } = webhookData - const { baseId, tableId, includeCellValuesInFieldIds } = providerConfig || {} - - if (!baseId || !tableId) { - logger.warn(`[${requestId}] Missing baseId or tableId for Airtable webhook creation.`, { - webhookId: webhookData.id, - }) - throw new Error( - 'Base ID and Table ID are required to create Airtable webhook. Please provide valid Airtable base and table IDs.' - ) - } - - const accessToken = await getOAuthToken(userId, 'airtable') - if (!accessToken) { - logger.warn( - `[${requestId}] Could not retrieve Airtable access token for user ${userId}. Cannot create webhook in Airtable.` - ) - throw new Error( - 'Airtable account connection required. Please connect your Airtable account in the trigger configuration and try again.' - ) - } - - const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}` - - const airtableApiUrl = `https://api.airtable.com/v0/bases/${baseId}/webhooks` - - const specification: any = { - options: { - filters: { - dataTypes: ['tableData'], // Watch table data changes - recordChangeScope: tableId, // Watch only the specified table - }, - }, - } - - // Conditionally add the 'includes' field based on the config - if (includeCellValuesInFieldIds === 'all') { - specification.options.includes = { - includeCellValuesInFieldIds: 'all', - } - } - - const requestBody: any = { - notificationUrl: notificationUrl, - specification: specification, - } - - const airtableResponse = await fetch(airtableApiUrl, { - method: 'POST', - headers: { - Authorization: `Bearer ${accessToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(requestBody), - }) - - // Airtable often returns 200 OK even for errors in the body, check payload - const responseBody = await airtableResponse.json() - - if (!airtableResponse.ok || responseBody.error) { - const errorMessage = - responseBody.error?.message || responseBody.error || 'Unknown Airtable API error' - const errorType = responseBody.error?.type - logger.error( - `[${requestId}] Failed to create webhook in Airtable for webhook ${webhookData.id}. Status: ${airtableResponse.status}`, - { type: errorType, message: errorMessage, response: responseBody } - ) - - let userFriendlyMessage = 'Failed to create webhook subscription in Airtable' - if (airtableResponse.status === 404) { - userFriendlyMessage = - 'Airtable base or table not found. Please verify that the Base ID and Table ID are correct and that you have access to them.' - } else if (errorMessage && errorMessage !== 'Unknown Airtable API error') { - userFriendlyMessage = `Airtable error: ${errorMessage}` - } - - throw new Error(userFriendlyMessage) - } - logger.info( - `[${requestId}] Successfully created webhook in Airtable for webhook ${webhookData.id}.`, - { - airtableWebhookId: responseBody.id, - } - ) - return responseBody.id - } catch (error: any) { - logger.error( - `[${requestId}] Exception during Airtable webhook creation for webhook ${webhookData.id}.`, - { - message: error.message, - stack: error.stack, - } - ) - // Re-throw the error so it can be caught by the outer try-catch - throw error - } -} - -// Helper function to create the webhook subscription in Calendly -async function createCalendlyWebhookSubscription( - request: NextRequest, - userId: string, - webhookData: any, - requestId: string -): Promise { - try { - const { path, providerConfig } = webhookData - const { apiKey, organization, triggerId } = providerConfig || {} - - if (!apiKey) { - logger.warn(`[${requestId}] Missing apiKey for Calendly webhook creation.`, { - webhookId: webhookData.id, - }) - throw new Error( - 'Personal Access Token is required to create Calendly webhook. Please provide your Calendly Personal Access Token.' - ) - } - - if (!organization) { - logger.warn(`[${requestId}] Missing organization URI for Calendly webhook creation.`, { - webhookId: webhookData.id, - }) - throw new Error( - 'Organization URI is required to create Calendly webhook. Please provide your Organization URI from the "Get Current User" operation.' - ) - } - - if (!triggerId) { - logger.warn(`[${requestId}] Missing triggerId for Calendly webhook creation.`, { - webhookId: webhookData.id, - }) - throw new Error('Trigger ID is required to create Calendly webhook') - } - - const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}` - - // Map trigger IDs to Calendly event types - const eventTypeMap: Record = { - calendly_invitee_created: ['invitee.created'], - calendly_invitee_canceled: ['invitee.canceled'], - calendly_routing_form_submitted: ['routing_form_submission.created'], - calendly_webhook: ['invitee.created', 'invitee.canceled', 'routing_form_submission.created'], - } - - const events = eventTypeMap[triggerId] || ['invitee.created'] - - const calendlyApiUrl = 'https://api.calendly.com/webhook_subscriptions' - - const requestBody = { - url: notificationUrl, - events, - organization, - scope: 'organization', - } - - const calendlyResponse = await fetch(calendlyApiUrl, { - method: 'POST', - headers: { - Authorization: `Bearer ${apiKey}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(requestBody), - }) - - if (!calendlyResponse.ok) { - const errorBody = await calendlyResponse.json().catch(() => ({})) - const errorMessage = errorBody.message || errorBody.title || 'Unknown Calendly API error' - logger.error( - `[${requestId}] Failed to create webhook in Calendly for webhook ${webhookData.id}. Status: ${calendlyResponse.status}`, - { response: errorBody } - ) - - let userFriendlyMessage = 'Failed to create webhook subscription in Calendly' - if (calendlyResponse.status === 401) { - userFriendlyMessage = - 'Calendly authentication failed. Please verify your Personal Access Token is correct.' - } else if (calendlyResponse.status === 403) { - userFriendlyMessage = - 'Calendly access denied. Please ensure you have appropriate permissions and a paid Calendly subscription.' - } else if (calendlyResponse.status === 404) { - userFriendlyMessage = - 'Calendly organization not found. Please verify the Organization URI is correct.' - } else if (errorMessage && errorMessage !== 'Unknown Calendly API error') { - userFriendlyMessage = `Calendly error: ${errorMessage}` - } - - throw new Error(userFriendlyMessage) - } - - const responseBody = await calendlyResponse.json() - const webhookUri = responseBody.resource?.uri - - if (!webhookUri) { - logger.error( - `[${requestId}] Calendly webhook created but no webhook URI returned for webhook ${webhookData.id}`, - { response: responseBody } - ) - throw new Error('Calendly webhook creation succeeded but no webhook URI was returned') - } - - // Extract the webhook ID from the URI (e.g., https://api.calendly.com/webhook_subscriptions/WEBHOOK_ID) - const webhookId = webhookUri.split('/').pop() - - if (!webhookId) { - logger.error(`[${requestId}] Could not extract webhook ID from Calendly URI: ${webhookUri}`, { - response: responseBody, - }) - throw new Error('Failed to extract webhook ID from Calendly response') - } - - logger.info( - `[${requestId}] Successfully created webhook in Calendly for webhook ${webhookData.id}.`, - { - calendlyWebhookUri: webhookUri, - calendlyWebhookId: webhookId, - } - ) - return webhookId - } catch (error: any) { - logger.error( - `[${requestId}] Exception during Calendly webhook creation for webhook ${webhookData.id}.`, - { - message: error.message, - stack: error.stack, - } - ) - // Re-throw the error so it can be caught by the outer try-catch - throw error - } -} - -// Helper function to create the webhook subscription in Webflow -async function createWebflowWebhookSubscription( - request: NextRequest, - userId: string, - webhookData: any, - requestId: string -): Promise { - try { - const { path, providerConfig } = webhookData - const { siteId, triggerId, collectionId, formId } = providerConfig || {} - - if (!siteId) { - logger.warn(`[${requestId}] Missing siteId for Webflow webhook creation.`, { - webhookId: webhookData.id, - }) - throw new Error('Site ID is required to create Webflow webhook') - } - - if (!triggerId) { - logger.warn(`[${requestId}] Missing triggerId for Webflow webhook creation.`, { - webhookId: webhookData.id, - }) - throw new Error('Trigger type is required to create Webflow webhook') - } - - const accessToken = await getOAuthToken(userId, 'webflow') - if (!accessToken) { - logger.warn( - `[${requestId}] Could not retrieve Webflow access token for user ${userId}. Cannot create webhook in Webflow.` - ) - throw new Error( - 'Webflow account connection required. Please connect your Webflow account in the trigger configuration and try again.' - ) - } - - const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}` - - // Map trigger IDs to Webflow trigger types - const triggerTypeMap: Record = { - webflow_collection_item_created: 'collection_item_created', - webflow_collection_item_changed: 'collection_item_changed', - webflow_collection_item_deleted: 'collection_item_deleted', - webflow_form_submission: 'form_submission', - } - - const webflowTriggerType = triggerTypeMap[triggerId] - if (!webflowTriggerType) { - logger.warn(`[${requestId}] Invalid triggerId for Webflow: ${triggerId}`, { - webhookId: webhookData.id, - }) - throw new Error(`Invalid Webflow trigger type: ${triggerId}`) - } - - const webflowApiUrl = `https://api.webflow.com/v2/sites/${siteId}/webhooks` - - const requestBody: any = { - triggerType: webflowTriggerType, - url: notificationUrl, - } - - // Add filter for collection-based triggers - if (collectionId && webflowTriggerType.startsWith('collection_item_')) { - requestBody.filter = { - resource_type: 'collection', - resource_id: collectionId, - } - } - - // Add filter for form submissions - if (formId && webflowTriggerType === 'form_submission') { - requestBody.filter = { - resource_type: 'form', - resource_id: formId, - } - } - - const webflowResponse = await fetch(webflowApiUrl, { - method: 'POST', - headers: { - Authorization: `Bearer ${accessToken}`, - 'Content-Type': 'application/json', - accept: 'application/json', - }, - body: JSON.stringify(requestBody), - }) - - const responseBody = await webflowResponse.json() - - if (!webflowResponse.ok || responseBody.error) { - const errorMessage = responseBody.message || responseBody.error || 'Unknown Webflow API error' - logger.error( - `[${requestId}] Failed to create webhook in Webflow for webhook ${webhookData.id}. Status: ${webflowResponse.status}`, - { message: errorMessage, response: responseBody } - ) - throw new Error(errorMessage) - } - - logger.info( - `[${requestId}] Successfully created webhook in Webflow for webhook ${webhookData.id}.`, - { - webflowWebhookId: responseBody.id || responseBody._id, - } - ) - - return responseBody.id || responseBody._id - } catch (error: any) { - logger.error( - `[${requestId}] Exception during Webflow webhook creation for webhook ${webhookData.id}.`, - { - message: error.message, - stack: error.stack, - } - ) - throw error - } -} - -// Helper function to create the webhook subscription in Grain -async function createGrainWebhookSubscription( - request: NextRequest, - webhookData: any, - requestId: string -): Promise<{ id: string; eventTypes: string[] } | undefined> { - try { - const { path, providerConfig } = webhookData - const { apiKey, triggerId, includeHighlights, includeParticipants, includeAiSummary } = - providerConfig || {} - - if (!apiKey) { - logger.warn(`[${requestId}] Missing apiKey for Grain webhook creation.`, { - webhookId: webhookData.id, - }) - throw new Error( - 'Grain API Key is required. Please provide your Grain Personal Access Token in the trigger configuration.' - ) - } - - // Map trigger IDs to Grain API hook_type (only 2 options: recording_added, upload_status) - const hookTypeMap: Record = { - grain_webhook: 'recording_added', - grain_recording_created: 'recording_added', - grain_recording_updated: 'recording_added', - grain_highlight_created: 'recording_added', - grain_highlight_updated: 'recording_added', - grain_story_created: 'recording_added', - grain_upload_status: 'upload_status', - } - - const eventTypeMap: Record = { - grain_webhook: [], - grain_recording_created: ['recording_added'], - grain_recording_updated: ['recording_updated'], - grain_highlight_created: ['highlight_created'], - grain_highlight_updated: ['highlight_updated'], - grain_story_created: ['story_created'], - grain_upload_status: ['upload_status'], - } - - const hookType = hookTypeMap[triggerId] ?? 'recording_added' - const eventTypes = eventTypeMap[triggerId] ?? [] - - if (!hookTypeMap[triggerId]) { - logger.warn( - `[${requestId}] Unknown triggerId for Grain: ${triggerId}, defaulting to recording_added`, - { - webhookId: webhookData.id, - } - ) - } - - logger.info(`[${requestId}] Creating Grain webhook`, { - triggerId, - hookType, - eventTypes, - webhookId: webhookData.id, - }) - - const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}` - - const grainApiUrl = 'https://api.grain.com/_/public-api/v2/hooks/create' - - const requestBody: Record = { - hook_url: notificationUrl, - hook_type: hookType, - } - - // Build include object based on configuration - const include: Record = {} - if (includeHighlights) { - include.highlights = true - } - if (includeParticipants) { - include.participants = true - } - if (includeAiSummary) { - include.ai_summary = true - } - if (Object.keys(include).length > 0) { - requestBody.include = include - } - - const grainResponse = await fetch(grainApiUrl, { - method: 'POST', - headers: { - Authorization: `Bearer ${apiKey}`, - 'Content-Type': 'application/json', - 'Public-Api-Version': '2025-10-31', - }, - body: JSON.stringify(requestBody), - }) - - const responseBody = await grainResponse.json() - - if (!grainResponse.ok || responseBody.error || responseBody.errors) { - logger.warn('[App] Grain response body:', responseBody) - const errorMessage = - responseBody.errors?.detail || - responseBody.error?.message || - responseBody.error || - responseBody.message || - 'Unknown Grain API error' - logger.error( - `[${requestId}] Failed to create webhook in Grain for webhook ${webhookData.id}. Status: ${grainResponse.status}`, - { message: errorMessage, response: responseBody } - ) - - let userFriendlyMessage = 'Failed to create webhook subscription in Grain' - if (grainResponse.status === 401) { - userFriendlyMessage = - 'Invalid Grain API Key. Please verify your Personal Access Token is correct.' - } else if (grainResponse.status === 403) { - userFriendlyMessage = - 'Access denied. Please ensure your Grain API Key has appropriate permissions.' - } else if (errorMessage && errorMessage !== 'Unknown Grain API error') { - userFriendlyMessage = `Grain error: ${errorMessage}` - } - - throw new Error(userFriendlyMessage) - } - - logger.info( - `[${requestId}] Successfully created webhook in Grain for webhook ${webhookData.id}.`, - { - grainWebhookId: responseBody.id, - eventTypes, - } - ) - - return { id: responseBody.id, eventTypes } - } catch (error: any) { - logger.error( - `[${requestId}] Exception during Grain webhook creation for webhook ${webhookData.id}.`, - { - message: error.message, - stack: error.stack, - } - ) - throw error - } -} - -// Helper function to create the webhook subscription in Lemlist -async function createLemlistWebhookSubscription( - webhookData: any, - requestId: string -): Promise<{ id: string } | undefined> { - try { - const { path, providerConfig } = webhookData - const { apiKey, triggerId, campaignId } = providerConfig || {} - - if (!apiKey) { - logger.warn(`[${requestId}] Missing apiKey for Lemlist webhook creation.`, { - webhookId: webhookData.id, - }) - throw new Error( - 'Lemlist API Key is required. Please provide your Lemlist API Key in the trigger configuration.' - ) - } - - // Map trigger IDs to Lemlist event types - const eventTypeMap: Record = { - lemlist_email_replied: 'emailsReplied', - lemlist_linkedin_replied: 'linkedinReplied', - lemlist_interested: 'interested', - lemlist_not_interested: 'notInterested', - lemlist_email_opened: 'emailsOpened', - lemlist_email_clicked: 'emailsClicked', - lemlist_email_bounced: 'emailsBounced', - lemlist_email_sent: 'emailsSent', - lemlist_webhook: undefined, // Generic webhook - no type filter - } - - const eventType = eventTypeMap[triggerId] - - logger.info(`[${requestId}] Creating Lemlist webhook`, { - triggerId, - eventType, - hasCampaignId: !!campaignId, - webhookId: webhookData.id, - }) - - const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}` - - const lemlistApiUrl = 'https://api.lemlist.com/api/hooks' - - // Build request body - const requestBody: Record = { - targetUrl: notificationUrl, - } - - // Add event type if specified (omit for generic webhook to receive all events) - if (eventType) { - requestBody.type = eventType - } - - // Add campaign filter if specified - if (campaignId) { - requestBody.campaignId = campaignId - } - - // Lemlist uses Basic Auth with empty username and API key as password - const authString = Buffer.from(`:${apiKey}`).toString('base64') - - const lemlistResponse = await fetch(lemlistApiUrl, { - method: 'POST', - headers: { - Authorization: `Basic ${authString}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(requestBody), - }) - - const responseBody = await lemlistResponse.json() - - if (!lemlistResponse.ok || responseBody.error) { - const errorMessage = responseBody.message || responseBody.error || 'Unknown Lemlist API error' - logger.error( - `[${requestId}] Failed to create webhook in Lemlist for webhook ${webhookData.id}. Status: ${lemlistResponse.status}`, - { message: errorMessage, response: responseBody } - ) - - let userFriendlyMessage = 'Failed to create webhook subscription in Lemlist' - if (lemlistResponse.status === 401) { - userFriendlyMessage = 'Invalid Lemlist API Key. Please verify your API Key is correct.' - } else if (lemlistResponse.status === 403) { - userFriendlyMessage = - 'Access denied. Please ensure your Lemlist API Key has appropriate permissions.' - } else if (errorMessage && errorMessage !== 'Unknown Lemlist API error') { - userFriendlyMessage = `Lemlist error: ${errorMessage}` - } - - throw new Error(userFriendlyMessage) - } - - logger.info( - `[${requestId}] Successfully created webhook in Lemlist for webhook ${webhookData.id}.`, - { - lemlistWebhookId: responseBody._id, - } - ) - - return { id: responseBody._id } - } catch (error: any) { - logger.error( - `[${requestId}] Exception during Lemlist webhook creation for webhook ${webhookData.id}.`, - { - message: error.message, - stack: error.stack, - } - ) - throw error - } -} diff --git a/apps/sim/app/api/workflows/[id]/deploy/route.ts b/apps/sim/app/api/workflows/[id]/deploy/route.ts index 1ba7647955..21afa25177 100644 --- a/apps/sim/app/api/workflows/[id]/deploy/route.ts +++ b/apps/sim/app/api/workflows/[id]/deploy/route.ts @@ -4,6 +4,7 @@ import { and, desc, eq } from 'drizzle-orm' import type { NextRequest } from 'next/server' import { generateRequestId } from '@/lib/core/utils/request' import { removeMcpToolsForWorkflow, syncMcpToolsForWorkflow } from '@/lib/mcp/workflow-mcp-sync' +import { cleanupWebhooksForWorkflow, saveTriggerWebhooksForDeploy } from '@/lib/webhooks/deploy' import { deployWorkflow, loadWorkflowFromNormalizedTables, @@ -130,6 +131,22 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ return createErrorResponse(`Invalid schedule configuration: ${scheduleValidation.error}`, 400) } + const triggerSaveResult = await saveTriggerWebhooksForDeploy({ + request, + workflowId: id, + workflow: workflowData, + userId: actorUserId, + blocks: normalizedData.blocks, + requestId, + }) + + if (!triggerSaveResult.success) { + return createErrorResponse( + triggerSaveResult.error?.message || 'Failed to save trigger configuration', + triggerSaveResult.error?.status || 500 + ) + } + const deployResult = await deployWorkflow({ workflowId: id, deployedBy: actorUserId, @@ -202,11 +219,18 @@ export async function DELETE( try { logger.debug(`[${requestId}] Undeploying workflow: ${id}`) - const { error } = await validateWorkflowPermissions(id, requestId, 'admin') + const { error, workflow: workflowData } = await validateWorkflowPermissions( + id, + requestId, + 'admin' + ) if (error) { return createErrorResponse(error.message, error.status) } + // Clean up external webhook subscriptions before undeploying + await cleanupWebhooksForWorkflow(id, workflowData as Record, requestId) + const result = await undeployWorkflow({ workflowId: id }) if (!result.success) { return createErrorResponse(result.error || 'Failed to undeploy workflow', 500) diff --git a/apps/sim/app/api/workflows/[id]/state/route.ts b/apps/sim/app/api/workflows/[id]/state/route.ts index 2cadeff341..7c8879430e 100644 --- a/apps/sim/app/api/workflows/[id]/state/route.ts +++ b/apps/sim/app/api/workflows/[id]/state/route.ts @@ -1,5 +1,5 @@ import { db } from '@sim/db' -import { webhook, workflow } from '@sim/db/schema' +import { workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' @@ -13,7 +13,6 @@ import { sanitizeAgentToolsInBlocks } from '@/lib/workflows/sanitization/validat import { getWorkflowAccessContext } from '@/lib/workflows/utils' import type { BlockState } from '@/stores/workflows/workflow/types' import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils' -import { getTrigger } from '@/triggers' const logger = createLogger('WorkflowStateAPI') @@ -203,8 +202,6 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ ) } - await syncWorkflowWebhooks(workflowId, workflowState.blocks) - // Extract and persist custom tools to database try { const workspaceId = workflowData.workspaceId @@ -290,213 +287,3 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } - -function getSubBlockValue(block: BlockState, subBlockId: string): T | undefined { - const value = block.subBlocks?.[subBlockId]?.value - if (value === undefined || value === null) { - return undefined - } - return value as T -} - -async function syncWorkflowWebhooks( - workflowId: string, - blocks: Record -): Promise { - await syncBlockResources(workflowId, blocks, { - resourceName: 'webhook', - subBlockId: 'webhookId', - buildMetadata: buildWebhookMetadata, - applyMetadata: upsertWebhookRecord, - }) -} - -interface WebhookMetadata { - triggerPath: string - provider: string | null - providerConfig: Record -} - -const CREDENTIAL_SET_PREFIX = 'credentialSet:' - -function buildWebhookMetadata(block: BlockState): WebhookMetadata | null { - const triggerId = - getSubBlockValue(block, 'triggerId') || - getSubBlockValue(block, 'selectedTriggerId') - const triggerConfig = getSubBlockValue>(block, 'triggerConfig') || {} - const triggerCredentials = getSubBlockValue(block, 'triggerCredentials') - const triggerPath = getSubBlockValue(block, 'triggerPath') || block.id - - const triggerDef = triggerId ? getTrigger(triggerId) : undefined - const provider = triggerDef?.provider || null - - // Handle credential sets vs individual credentials - const isCredentialSet = triggerCredentials?.startsWith(CREDENTIAL_SET_PREFIX) - const credentialSetId = isCredentialSet - ? triggerCredentials!.slice(CREDENTIAL_SET_PREFIX.length) - : undefined - const credentialId = isCredentialSet ? undefined : triggerCredentials - - const providerConfig = { - ...(typeof triggerConfig === 'object' ? triggerConfig : {}), - ...(credentialId ? { credentialId } : {}), - ...(credentialSetId ? { credentialSetId } : {}), - ...(triggerId ? { triggerId } : {}), - } - - return { - triggerPath, - provider, - providerConfig, - } -} - -async function upsertWebhookRecord( - workflowId: string, - block: BlockState, - webhookId: string, - metadata: WebhookMetadata -): Promise { - const providerConfig = metadata.providerConfig as Record - const credentialSetId = providerConfig?.credentialSetId as string | undefined - - // For credential sets, delegate to the sync function which handles fan-out - if (credentialSetId && metadata.provider) { - const { syncWebhooksForCredentialSet } = await import('@/lib/webhooks/utils.server') - const { getProviderIdFromServiceId } = await import('@/lib/oauth') - - const oauthProviderId = getProviderIdFromServiceId(metadata.provider) - const requestId = crypto.randomUUID().slice(0, 8) - - // Extract base config (without credential-specific fields) - const { - credentialId: _cId, - credentialSetId: _csId, - userId: _uId, - ...baseConfig - } = providerConfig - - try { - await syncWebhooksForCredentialSet({ - workflowId, - blockId: block.id, - provider: metadata.provider, - basePath: metadata.triggerPath, - credentialSetId, - oauthProviderId, - providerConfig: baseConfig as Record, - requestId, - }) - - logger.info('Synced credential set webhooks during workflow save', { - workflowId, - blockId: block.id, - credentialSetId, - }) - } catch (error) { - logger.error('Failed to sync credential set webhooks during workflow save', { - workflowId, - blockId: block.id, - credentialSetId, - error, - }) - } - return - } - - // For individual credentials, use the existing single webhook logic - const [existing] = await db.select().from(webhook).where(eq(webhook.id, webhookId)).limit(1) - - if (existing) { - const needsUpdate = - existing.blockId !== block.id || - existing.workflowId !== workflowId || - existing.path !== metadata.triggerPath - - if (needsUpdate) { - await db - .update(webhook) - .set({ - workflowId, - blockId: block.id, - path: metadata.triggerPath, - provider: metadata.provider || existing.provider, - providerConfig: Object.keys(metadata.providerConfig).length - ? metadata.providerConfig - : existing.providerConfig, - isActive: true, - updatedAt: new Date(), - }) - .where(eq(webhook.id, webhookId)) - } - return - } - - await db.insert(webhook).values({ - id: webhookId, - workflowId, - blockId: block.id, - path: metadata.triggerPath, - provider: metadata.provider, - providerConfig: metadata.providerConfig, - credentialSetId: null, - isActive: true, - createdAt: new Date(), - updatedAt: new Date(), - }) - - logger.info('Recreated missing webhook after workflow save', { - workflowId, - blockId: block.id, - webhookId, - }) -} - -interface BlockResourceSyncConfig { - resourceName: string - subBlockId: string - buildMetadata: (block: BlockState, resourceId: string) => T | null - applyMetadata: ( - workflowId: string, - block: BlockState, - resourceId: string, - metadata: T - ) => Promise -} - -async function syncBlockResources( - workflowId: string, - blocks: Record, - config: BlockResourceSyncConfig -): Promise { - const blockEntries = Object.values(blocks || {}).filter(Boolean) as BlockState[] - if (blockEntries.length === 0) return - - for (const block of blockEntries) { - const resourceId = getSubBlockValue(block, config.subBlockId) - if (!resourceId) continue - - const metadata = config.buildMetadata(block, resourceId) - if (!metadata) { - logger.warn(`Skipping ${config.resourceName} sync due to invalid configuration`, { - workflowId, - blockId: block.id, - resourceId, - resourceName: config.resourceName, - }) - continue - } - - try { - await config.applyMetadata(workflowId, block, resourceId, metadata) - } catch (error) { - logger.error(`Failed to sync ${config.resourceName}`, { - workflowId, - blockId: block.id, - resourceId, - resourceName: config.resourceName, - error, - }) - } - } -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx index 98a61c1d1f..a1b28e59ca 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx @@ -189,6 +189,7 @@ export function DeployModal({ useEffect(() => { if (open && workflowId) { setActiveTab('general') + setApiDeployError(null) fetchChatDeploymentInfo() } }, [open, workflowId, fetchChatDeploymentInfo]) @@ -507,6 +508,7 @@ export function DeployModal({ const handleCloseModal = () => { setIsSubmitting(false) setChatSubmitting(false) + setApiDeployError(null) onOpenChange(false) } @@ -663,6 +665,12 @@ export function DeployModal({ + {apiDeployError && ( +
+
Deployment Error
+
{apiDeployError}
+
+ )} ('idle') - const [errorMessage, setErrorMessage] = useState(null) - const [deleteStatus, setDeleteStatus] = useState<'idle' | 'deleting'>('idle') - const [showDeleteDialog, setShowDeleteDialog] = useState(false) - - const effectiveTriggerId = useMemo(() => { - if (triggerId && isTriggerValid(triggerId)) { - return triggerId - } - const selectedTriggerId = useSubBlockStore.getState().getValue(blockId, 'selectedTriggerId') - if (typeof selectedTriggerId === 'string' && isTriggerValid(selectedTriggerId)) { - return selectedTriggerId - } - return triggerId - }, [blockId, triggerId]) - - const { collaborativeSetSubblockValue } = useCollaborativeWorkflow() - - const { webhookId, saveConfig, deleteConfig, isLoading } = useWebhookManagement({ - blockId, - triggerId: effectiveTriggerId, - isPreview, - useWebhookUrl: true, // to store the webhook url in the store - }) - - const triggerConfig = useSubBlockStore((state) => state.getValue(blockId, 'triggerConfig')) - const triggerCredentials = useSubBlockStore((state) => - state.getValue(blockId, 'triggerCredentials') - ) - - const triggerDef = - effectiveTriggerId && isTriggerValid(effectiveTriggerId) ? getTrigger(effectiveTriggerId) : null - - const validateRequiredFields = useCallback( - ( - configToCheck: Record | null | undefined - ): { valid: boolean; missingFields: string[] } => { - if (!triggerDef) { - return { valid: true, missingFields: [] } - } - - const missingFields: string[] = [] - - triggerDef.subBlocks - .filter( - (sb) => sb.required && sb.mode === 'trigger' && !SYSTEM_SUBBLOCK_IDS.includes(sb.id) - ) - .forEach((subBlock) => { - if (subBlock.id === 'triggerCredentials') { - if (!triggerCredentials) { - missingFields.push(subBlock.title || 'Credentials') - } - } else { - const value = configToCheck?.[subBlock.id] - if (value === undefined || value === null || value === '') { - missingFields.push(subBlock.title || subBlock.id) - } - } - }) - - return { - valid: missingFields.length === 0, - missingFields, - } - }, - [triggerDef, triggerCredentials] - ) - - const requiredSubBlockIds = useMemo(() => { - if (!triggerDef) return [] - return triggerDef.subBlocks - .filter((sb) => sb.required && sb.mode === 'trigger' && !SYSTEM_SUBBLOCK_IDS.includes(sb.id)) - .map((sb) => sb.id) - }, [triggerDef]) - - const subscribedSubBlockValues = useSubBlockStore( - useCallback( - (state) => { - if (!triggerDef) return {} - const values: Record = {} - requiredSubBlockIds.forEach((subBlockId) => { - const value = state.getValue(blockId, subBlockId) - if (value !== null && value !== undefined && value !== '') { - values[subBlockId] = value - } - }) - return values - }, - [blockId, triggerDef, requiredSubBlockIds] - ) - ) - - const previousValuesRef = useRef>({}) - const validationTimeoutRef = useRef(null) - - useEffect(() => { - if (saveStatus !== 'error' || !triggerDef) { - previousValuesRef.current = subscribedSubBlockValues - return - } - - const hasChanges = Object.keys(subscribedSubBlockValues).some( - (key) => - previousValuesRef.current[key] !== (subscribedSubBlockValues as Record)[key] - ) - - if (!hasChanges) { - return - } - - if (validationTimeoutRef.current) { - clearTimeout(validationTimeoutRef.current) - } - - validationTimeoutRef.current = setTimeout(() => { - const aggregatedConfig = useTriggerConfigAggregation(blockId, effectiveTriggerId) - - if (aggregatedConfig) { - useSubBlockStore.getState().setValue(blockId, 'triggerConfig', aggregatedConfig) - } - - const validation = validateRequiredFields(aggregatedConfig) - - if (validation.valid) { - setErrorMessage(null) - setSaveStatus('idle') - logger.debug('Error cleared after validation passed', { - blockId, - triggerId: effectiveTriggerId, - }) - } else { - setErrorMessage(`Missing required fields: ${validation.missingFields.join(', ')}`) - logger.debug('Error message updated', { - blockId, - triggerId: effectiveTriggerId, - missingFields: validation.missingFields, - }) - } - - previousValuesRef.current = subscribedSubBlockValues - }, 300) - - return () => { - if (validationTimeoutRef.current) { - clearTimeout(validationTimeoutRef.current) - } - } - }, [ - blockId, - effectiveTriggerId, - triggerDef, - subscribedSubBlockValues, - saveStatus, - validateRequiredFields, - ]) - - const handleSave = async () => { - if (isPreview || disabled) return - - setSaveStatus('saving') - setErrorMessage(null) - - try { - const aggregatedConfig = useTriggerConfigAggregation(blockId, effectiveTriggerId) - - if (aggregatedConfig) { - useSubBlockStore.getState().setValue(blockId, 'triggerConfig', aggregatedConfig) - logger.debug('Stored aggregated trigger config', { - blockId, - triggerId: effectiveTriggerId, - aggregatedConfig, - }) - } - - const validation = validateRequiredFields(aggregatedConfig) - if (!validation.valid) { - setErrorMessage(`Missing required fields: ${validation.missingFields.join(', ')}`) - setSaveStatus('error') - return - } - - const success = await saveConfig() - if (!success) { - throw new Error('Save config returned false') - } - - setSaveStatus('saved') - setErrorMessage(null) - - const savedWebhookId = useSubBlockStore.getState().getValue(blockId, 'webhookId') - const savedTriggerPath = useSubBlockStore.getState().getValue(blockId, 'triggerPath') - const savedTriggerId = useSubBlockStore.getState().getValue(blockId, 'triggerId') - const savedTriggerConfig = useSubBlockStore.getState().getValue(blockId, 'triggerConfig') - - collaborativeSetSubblockValue(blockId, 'webhookId', savedWebhookId) - collaborativeSetSubblockValue(blockId, 'triggerPath', savedTriggerPath) - collaborativeSetSubblockValue(blockId, 'triggerId', savedTriggerId) - collaborativeSetSubblockValue(blockId, 'triggerConfig', savedTriggerConfig) - - setTimeout(() => { - setSaveStatus('idle') - }, 2000) - - logger.info('Trigger configuration saved successfully', { - blockId, - triggerId: effectiveTriggerId, - hasWebhookId: !!webhookId, - }) - } catch (error: any) { - setSaveStatus('error') - setErrorMessage(error.message || 'An error occurred while saving.') - logger.error('Error saving trigger configuration', { error }) - } - } - - const handleDeleteClick = () => { - if (isPreview || disabled || !webhookId) return - setShowDeleteDialog(true) - } - - const handleDeleteConfirm = async () => { - setShowDeleteDialog(false) - setDeleteStatus('deleting') - setErrorMessage(null) - - try { - const success = await deleteConfig() - - if (success) { - setDeleteStatus('idle') - setSaveStatus('idle') - setErrorMessage(null) - - collaborativeSetSubblockValue(blockId, 'triggerPath', '') - collaborativeSetSubblockValue(blockId, 'webhookId', null) - collaborativeSetSubblockValue(blockId, 'triggerConfig', null) - - logger.info('Trigger configuration deleted successfully', { - blockId, - triggerId: effectiveTriggerId, - }) - } else { - setDeleteStatus('idle') - setErrorMessage('Failed to delete trigger configuration.') - logger.error('Failed to delete trigger configuration') - } - } catch (error: any) { - setDeleteStatus('idle') - setErrorMessage(error.message || 'An error occurred while deleting.') - logger.error('Error deleting trigger configuration', { error }) - } - } - - if (isPreview) { - return null - } - - const isProcessing = saveStatus === 'saving' || deleteStatus === 'deleting' || isLoading - - return ( -
-
- - - {webhookId && ( - - )} -
- - {errorMessage &&

{errorMessage}

} - - - - Delete Trigger - -

- Are you sure you want to delete this trigger configuration? This will remove the - webhook and stop all incoming triggers.{' '} - This action cannot be undone. -

-
- - - - -
-
-
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx index 9201ce8299..b3ec7fec07 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx @@ -39,7 +39,6 @@ import { Text, TimeInput, ToolInput, - TriggerSave, VariablesInput, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components' import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate' @@ -867,17 +866,6 @@ function SubBlockComponent({ } /> ) - case 'trigger-save': - return ( - - ) - case 'messages-input': return ( + userId: string + blocks: Record + requestId: string +} + +function getSubBlockValue(block: BlockState, subBlockId: string): unknown { + return block.subBlocks?.[subBlockId]?.value +} + +function isFieldRequired( + config: SubBlockConfig, + subBlockValues: Record +): boolean { + if (!config.required) return false + if (typeof config.required === 'boolean') return config.required + + const evalCond = ( + cond: { + field: string + value: string | number | boolean | Array + not?: boolean + and?: { + field: string + value: string | number | boolean | Array | undefined + not?: boolean + } + }, + values: Record + ): boolean => { + const fieldValue = values[cond.field]?.value + const condValue = cond.value + + let match = Array.isArray(condValue) + ? condValue.includes(fieldValue as string | number | boolean) + : fieldValue === condValue + + if (cond.not) match = !match + + if (cond.and) { + const andFieldValue = values[cond.and.field]?.value + const andCondValue = cond.and.value + let andMatch = Array.isArray(andCondValue) + ? (andCondValue || []).includes(andFieldValue as string | number | boolean) + : andFieldValue === andCondValue + if (cond.and.not) andMatch = !andMatch + match = match && andMatch + } + + return match + } + + const condition = typeof config.required === 'function' ? config.required() : config.required + return evalCond(condition, subBlockValues) +} + +function resolveTriggerId(block: BlockState): string | undefined { + const selectedTriggerId = getSubBlockValue(block, 'selectedTriggerId') + if (typeof selectedTriggerId === 'string' && isTriggerValid(selectedTriggerId)) { + return selectedTriggerId + } + + const storedTriggerId = getSubBlockValue(block, 'triggerId') + if (typeof storedTriggerId === 'string' && isTriggerValid(storedTriggerId)) { + return storedTriggerId + } + + const blockConfig = getBlock(block.type) + if (blockConfig?.category === 'triggers' && isTriggerValid(block.type)) { + return block.type + } + + if (block.triggerMode && blockConfig?.triggers?.enabled) { + const configuredTriggerId = + typeof selectedTriggerId === 'string' ? selectedTriggerId : undefined + if (configuredTriggerId && isTriggerValid(configuredTriggerId)) { + return configuredTriggerId + } + + const available = blockConfig.triggers?.available?.[0] + if (available && isTriggerValid(available)) { + return available + } + } + + return undefined +} + +function getConfigValue(block: BlockState, subBlock: SubBlockConfig): unknown { + const fieldValue = getSubBlockValue(block, subBlock.id) + + if ( + (fieldValue === null || fieldValue === undefined || fieldValue === '') && + Boolean(subBlock.required) && + subBlock.defaultValue !== undefined + ) { + return subBlock.defaultValue + } + + return fieldValue +} + +function buildProviderConfig( + block: BlockState, + triggerId: string, + triggerDef: { subBlocks: SubBlockConfig[] } +): { + providerConfig: Record + missingFields: string[] + credentialId?: string + credentialSetId?: string + triggerPath: string +} { + const triggerConfigValue = getSubBlockValue(block, 'triggerConfig') + const baseConfig = + triggerConfigValue && typeof triggerConfigValue === 'object' + ? (triggerConfigValue as Record) + : {} + + const providerConfig: Record = { ...baseConfig } + const missingFields: string[] = [] + const subBlockValues = Object.fromEntries( + Object.entries(block.subBlocks || {}).map(([key, value]) => [key, { value: value.value }]) + ) + + triggerDef.subBlocks + .filter((subBlock) => subBlock.mode === 'trigger' && !SYSTEM_SUBBLOCK_IDS.includes(subBlock.id)) + .forEach((subBlock) => { + const valueToUse = getConfigValue(block, subBlock) + if (valueToUse !== null && valueToUse !== undefined && valueToUse !== '') { + providerConfig[subBlock.id] = valueToUse + } else if (isFieldRequired(subBlock, subBlockValues)) { + missingFields.push(subBlock.title || subBlock.id) + } + }) + + const credentialConfig = triggerDef.subBlocks.find( + (subBlock) => subBlock.id === 'triggerCredentials' + ) + const triggerCredentials = getSubBlockValue(block, 'triggerCredentials') + if ( + credentialConfig && + isFieldRequired(credentialConfig, subBlockValues) && + !triggerCredentials + ) { + missingFields.push(credentialConfig.title || 'Credentials') + } + + let credentialId: string | undefined + let credentialSetId: string | undefined + if (typeof triggerCredentials === 'string' && triggerCredentials.length > 0) { + if (triggerCredentials.startsWith(CREDENTIAL_SET_PREFIX)) { + credentialSetId = triggerCredentials.slice(CREDENTIAL_SET_PREFIX.length) + providerConfig.credentialSetId = credentialSetId + } else { + credentialId = triggerCredentials + providerConfig.credentialId = credentialId + } + } + + providerConfig.triggerId = triggerId + + const triggerPathValue = getSubBlockValue(block, 'triggerPath') + const triggerPath = + typeof triggerPathValue === 'string' && triggerPathValue.length > 0 + ? triggerPathValue + : block.id + + return { providerConfig, missingFields, credentialId, credentialSetId, triggerPath } +} + +async function configurePollingIfNeeded( + provider: string, + savedWebhook: any, + requestId: string +): Promise { + if (provider === 'gmail') { + const success = await configureGmailPolling(savedWebhook, requestId) + if (!success) { + await db.delete(webhook).where(eq(webhook.id, savedWebhook.id)) + return { + message: 'Failed to configure Gmail polling. Please check your Gmail account permissions.', + status: 500, + } + } + } + + if (provider === 'outlook') { + const success = await configureOutlookPolling(savedWebhook, requestId) + if (!success) { + await db.delete(webhook).where(eq(webhook.id, savedWebhook.id)) + return { + message: + 'Failed to configure Outlook polling. Please check your Outlook account permissions.', + status: 500, + } + } + } + + return null +} + +async function syncCredentialSetWebhooks(params: { + workflowId: string + blockId: string + provider: string + triggerPath: string + providerConfig: Record + requestId: string +}): Promise { + const { workflowId, blockId, provider, triggerPath, providerConfig, requestId } = params + + const credentialSetId = providerConfig.credentialSetId as string | undefined + if (!credentialSetId) { + return null + } + + const oauthProviderId = getProviderIdFromServiceId(provider) + + const { credentialId: _cId, credentialSetId: _csId, userId: _uId, ...baseConfig } = providerConfig + + const syncResult = await syncWebhooksForCredentialSet({ + workflowId, + blockId, + provider, + basePath: triggerPath, + credentialSetId, + oauthProviderId, + providerConfig: baseConfig as Record, + requestId, + }) + + if (syncResult.webhooks.length === 0) { + return { + message: `No valid credentials found in credential set for ${provider}. Please connect accounts and try again.`, + status: 400, + } + } + + if (provider === 'gmail' || provider === 'outlook') { + const configureFunc = provider === 'gmail' ? configureGmailPolling : configureOutlookPolling + for (const wh of syncResult.webhooks) { + if (wh.isNew) { + const rows = await db.select().from(webhook).where(eq(webhook.id, wh.id)).limit(1) + if (rows.length > 0) { + const success = await configureFunc(rows[0], requestId) + if (!success) { + await db.delete(webhook).where(eq(webhook.id, wh.id)) + return { + message: `Failed to configure ${provider} polling. Please check account permissions.`, + status: 500, + } + } + } + } + } + } + + return null +} + +async function createWebhookForBlock(params: { + request: NextRequest + workflowId: string + workflow: Record + userId: string + block: BlockState + provider: string + providerConfig: Record + triggerPath: string + requestId: string +}): Promise { + const { + request, + workflowId, + workflow, + userId, + block, + provider, + providerConfig, + triggerPath, + requestId, + } = params + + const webhookId = nanoid() + const createPayload = { + id: webhookId, + path: triggerPath, + provider, + providerConfig, + } + + const result = await createExternalWebhookSubscription( + request, + createPayload, + workflow, + userId, + requestId + ) + + const updatedProviderConfig = result.updatedProviderConfig as Record + let savedWebhook: any + + try { + const createdRows = await db + .insert(webhook) + .values({ + id: webhookId, + workflowId, + blockId: block.id, + path: triggerPath, + provider, + providerConfig: updatedProviderConfig, + credentialSetId: (updatedProviderConfig.credentialSetId as string | undefined) || null, + isActive: true, + createdAt: new Date(), + updatedAt: new Date(), + }) + .returning() + savedWebhook = createdRows[0] + } catch (error) { + if (result.externalSubscriptionCreated) { + await cleanupExternalWebhook(createPayload, workflow, requestId) + } + throw error + } + + const pollingError = await configurePollingIfNeeded(provider, savedWebhook, requestId) + if (pollingError) { + return pollingError + } + + return null +} + +/** + * Saves trigger webhook configurations as part of workflow deployment. + * Uses delete + create approach for changed/deleted webhooks. + */ +export async function saveTriggerWebhooksForDeploy({ + request, + workflowId, + workflow, + userId, + blocks, + requestId, +}: SaveTriggerWebhooksInput): Promise { + const triggerBlocks = Object.values(blocks || {}).filter(Boolean) + const currentBlockIds = new Set(triggerBlocks.map((b) => b.id)) + + // 1. Get all existing webhooks for this workflow + const existingWebhooks = await db.select().from(webhook).where(eq(webhook.workflowId, workflowId)) + + const webhooksByBlockId = new Map( + existingWebhooks.filter((wh) => wh.blockId).map((wh) => [wh.blockId!, wh]) + ) + + logger.info(`[${requestId}] Starting webhook sync`, { + workflowId, + currentBlockIds: Array.from(currentBlockIds), + existingWebhookBlockIds: Array.from(webhooksByBlockId.keys()), + }) + + // 2. Determine which webhooks to delete (orphaned or config changed) + const webhooksToDelete: typeof existingWebhooks = [] + const blocksNeedingWebhook: BlockState[] = [] + + for (const block of triggerBlocks) { + const triggerId = resolveTriggerId(block) + if (!triggerId || !isTriggerValid(triggerId)) continue + + const triggerDef = getTrigger(triggerId) + const provider = triggerDef.provider + const { providerConfig, missingFields, triggerPath } = buildProviderConfig( + block, + triggerId, + triggerDef + ) + + if (missingFields.length > 0) { + return { + success: false, + error: { + message: `Missing required fields for ${triggerDef.name || triggerId}: ${missingFields.join(', ')}`, + status: 400, + }, + } + } + // Store config for later use + + ;(block as any)._webhookConfig = { provider, providerConfig, triggerPath, triggerDef } + + const existingWh = webhooksByBlockId.get(block.id) + if (!existingWh) { + // No existing webhook - needs creation + blocksNeedingWebhook.push(block) + } else { + // Check if config changed + const existingConfig = (existingWh.providerConfig as Record) || {} + if ( + shouldRecreateExternalWebhookSubscription({ + previousProvider: existingWh.provider as string, + nextProvider: provider, + previousConfig: existingConfig, + nextConfig: providerConfig, + }) + ) { + // Config changed - delete and recreate + webhooksToDelete.push(existingWh) + blocksNeedingWebhook.push(block) + logger.info(`[${requestId}] Webhook config changed for block ${block.id}, will recreate`) + } + // else: config unchanged, keep existing webhook + } + } + + // Add orphaned webhooks (block no longer exists) + for (const wh of existingWebhooks) { + if (wh.blockId && !currentBlockIds.has(wh.blockId)) { + webhooksToDelete.push(wh) + logger.info(`[${requestId}] Webhook orphaned (block deleted): ${wh.blockId}`) + } + } + + // 3. Delete webhooks that need deletion + if (webhooksToDelete.length > 0) { + logger.info(`[${requestId}] Deleting ${webhooksToDelete.length} webhook(s)`, { + webhookIds: webhooksToDelete.map((wh) => wh.id), + }) + + for (const wh of webhooksToDelete) { + try { + await cleanupExternalWebhook(wh, workflow, requestId) + } catch (cleanupError) { + logger.warn(`[${requestId}] Failed to cleanup external webhook ${wh.id}`, cleanupError) + } + } + + const idsToDelete = webhooksToDelete.map((wh) => wh.id) + await db.delete(webhook).where(inArray(webhook.id, idsToDelete)) + } + + // 4. Create webhooks for blocks that need them + for (const block of blocksNeedingWebhook) { + const config = (block as any)._webhookConfig + if (!config) continue + + const { provider, providerConfig, triggerPath } = config + + try { + // Handle credential sets + const credentialSetError = await syncCredentialSetWebhooks({ + workflowId, + blockId: block.id, + provider, + triggerPath, + providerConfig, + requestId, + }) + + if (credentialSetError) { + return { success: false, error: credentialSetError } + } + + if (providerConfig.credentialSetId) { + continue + } + + const createError = await createWebhookForBlock({ + request, + workflowId, + workflow, + userId, + block, + provider, + providerConfig, + triggerPath, + requestId, + }) + + if (createError) { + return { success: false, error: createError } + } + } catch (error: any) { + logger.error(`[${requestId}] Failed to create webhook for ${block.id}`, error) + return { + success: false, + error: { + message: error?.message || 'Failed to save trigger configuration', + status: 500, + }, + } + } + } + + // Clean up temp config + for (const block of triggerBlocks) { + ;(block as any)._webhookConfig = undefined + } + + return { success: true } +} + +/** + * Clean up all webhooks for a workflow during undeploy. + * Removes external subscriptions and deletes webhook records from the database. + */ +export async function cleanupWebhooksForWorkflow( + workflowId: string, + workflow: Record, + requestId: string +): Promise { + const existingWebhooks = await db.select().from(webhook).where(eq(webhook.workflowId, workflowId)) + + if (existingWebhooks.length === 0) { + logger.debug(`[${requestId}] No webhooks to clean up for workflow ${workflowId}`) + return + } + + logger.info(`[${requestId}] Cleaning up ${existingWebhooks.length} webhook(s) for undeploy`, { + workflowId, + webhookIds: existingWebhooks.map((wh) => wh.id), + }) + + // Clean up external subscriptions + for (const wh of existingWebhooks) { + try { + await cleanupExternalWebhook(wh, workflow, requestId) + } catch (cleanupError) { + logger.warn(`[${requestId}] Failed to cleanup external webhook ${wh.id}`, cleanupError) + // Continue with other webhooks even if one fails + } + } + + // Delete all webhook records + await db.delete(webhook).where(eq(webhook.workflowId, workflowId)) + + logger.info(`[${requestId}] Cleaned up all webhooks for workflow ${workflowId}`) +} diff --git a/apps/sim/lib/webhooks/provider-subscriptions.ts b/apps/sim/lib/webhooks/provider-subscriptions.ts index 1e031d921c..9e8f729c26 100644 --- a/apps/sim/lib/webhooks/provider-subscriptions.ts +++ b/apps/sim/lib/webhooks/provider-subscriptions.ts @@ -10,6 +10,7 @@ const typeformLogger = createLogger('TypeformWebhook') const calendlyLogger = createLogger('CalendlyWebhook') const grainLogger = createLogger('GrainWebhook') const lemlistLogger = createLogger('LemlistWebhook') +const webflowLogger = createLogger('WebflowWebhook') function getProviderConfig(webhook: any): Record { return (webhook.providerConfig as Record) || {} @@ -728,38 +729,863 @@ export async function deleteLemlistWebhook(webhook: any, requestId: string): Pro return } - if (!externalId) { + const authString = Buffer.from(`:${apiKey}`).toString('base64') + + const deleteById = async (id: string) => { + const lemlistApiUrl = `https://api.lemlist.com/api/hooks/${id}` + const lemlistResponse = await fetch(lemlistApiUrl, { + method: 'DELETE', + headers: { + Authorization: `Basic ${authString}`, + }, + }) + + if (!lemlistResponse.ok && lemlistResponse.status !== 404) { + const responseBody = await lemlistResponse.json().catch(() => ({})) + lemlistLogger.warn( + `[${requestId}] Failed to delete Lemlist webhook (non-fatal): ${lemlistResponse.status}`, + { response: responseBody } + ) + } else { + lemlistLogger.info(`[${requestId}] Successfully deleted Lemlist webhook ${id}`) + } + } + + if (externalId) { + await deleteById(externalId) + return + } + + const notificationUrl = getNotificationUrl(webhook) + const listResponse = await fetch('https://api.lemlist.com/api/hooks', { + method: 'GET', + headers: { + Authorization: `Basic ${authString}`, + }, + }) + + if (!listResponse.ok) { lemlistLogger.warn( - `[${requestId}] Missing externalId for Lemlist webhook deletion ${webhook.id}, skipping cleanup` + `[${requestId}] Failed to list Lemlist webhooks for cleanup ${webhook.id}`, + { status: listResponse.status } ) return } - // Lemlist uses Basic Auth with empty username and API key as password - const authString = Buffer.from(`:${apiKey}`).toString('base64') - const lemlistApiUrl = `https://api.lemlist.com/api/hooks/${externalId}` + const listBody = await listResponse.json().catch(() => null) + const hooks: Array> = Array.isArray(listBody) + ? listBody + : listBody?.hooks || listBody?.data || [] + const matches = hooks.filter((hook) => { + const targetUrl = hook?.targetUrl || hook?.target_url || hook?.url + return typeof targetUrl === 'string' && targetUrl === notificationUrl + }) - const lemlistResponse = await fetch(lemlistApiUrl, { + if (matches.length === 0) { + lemlistLogger.info(`[${requestId}] Lemlist webhook not found for cleanup ${webhook.id}`, { + notificationUrl, + }) + return + } + + for (const hook of matches) { + const hookId = hook?._id || hook?.id + if (typeof hookId === 'string' && hookId.length > 0) { + await deleteById(hookId) + } + } + } catch (error) { + lemlistLogger.warn(`[${requestId}] Error deleting Lemlist webhook (non-fatal)`, error) + } +} + +export async function deleteWebflowWebhook( + webhook: any, + workflow: any, + requestId: string +): Promise { + try { + const config = getProviderConfig(webhook) + const siteId = config.siteId as string | undefined + const externalId = config.externalId as string | undefined + + if (!siteId) { + webflowLogger.warn( + `[${requestId}] Missing siteId for Webflow webhook deletion ${webhook.id}, skipping cleanup` + ) + return + } + + if (!externalId) { + webflowLogger.warn( + `[${requestId}] Missing externalId for Webflow webhook deletion ${webhook.id}, skipping cleanup` + ) + return + } + + const accessToken = await getOAuthToken(workflow.userId, 'webflow') + if (!accessToken) { + webflowLogger.warn( + `[${requestId}] Could not retrieve Webflow access token for user ${workflow.userId}. Cannot delete webhook.`, + { webhookId: webhook.id } + ) + return + } + + const webflowApiUrl = `https://api.webflow.com/v2/sites/${siteId}/webhooks/${externalId}` + + const webflowResponse = await fetch(webflowApiUrl, { method: 'DELETE', headers: { - Authorization: `Basic ${authString}`, + Authorization: `Bearer ${accessToken}`, + accept: 'application/json', }, }) - if (!lemlistResponse.ok && lemlistResponse.status !== 404) { - const responseBody = await lemlistResponse.json().catch(() => ({})) - lemlistLogger.warn( - `[${requestId}] Failed to delete Lemlist webhook (non-fatal): ${lemlistResponse.status}`, + if (!webflowResponse.ok && webflowResponse.status !== 404) { + const responseBody = await webflowResponse.json().catch(() => ({})) + webflowLogger.warn( + `[${requestId}] Failed to delete Webflow webhook (non-fatal): ${webflowResponse.status}`, { response: responseBody } ) } else { - lemlistLogger.info(`[${requestId}] Successfully deleted Lemlist webhook ${externalId}`) + webflowLogger.info(`[${requestId}] Successfully deleted Webflow webhook ${externalId}`) } } catch (error) { - lemlistLogger.warn(`[${requestId}] Error deleting Lemlist webhook (non-fatal)`, error) + webflowLogger.warn(`[${requestId}] Error deleting Webflow webhook (non-fatal)`, error) + } +} + +export async function createGrainWebhookSubscription( + _request: NextRequest, + webhookData: any, + requestId: string +): Promise<{ id: string; eventTypes: string[] } | undefined> { + try { + const { path, providerConfig } = webhookData + const { apiKey, triggerId, includeHighlights, includeParticipants, includeAiSummary } = + providerConfig || {} + + if (!apiKey) { + grainLogger.warn(`[${requestId}] Missing apiKey for Grain webhook creation.`, { + webhookId: webhookData.id, + }) + throw new Error( + 'Grain API Key is required. Please provide your Grain Personal Access Token in the trigger configuration.' + ) + } + + const hookTypeMap: Record = { + grain_webhook: 'recording_added', + grain_recording_created: 'recording_added', + grain_recording_updated: 'recording_added', + grain_highlight_created: 'recording_added', + grain_highlight_updated: 'recording_added', + grain_story_created: 'recording_added', + grain_upload_status: 'upload_status', + } + + const eventTypeMap: Record = { + grain_webhook: [], + grain_recording_created: ['recording_added'], + grain_recording_updated: ['recording_updated'], + grain_highlight_created: ['highlight_created'], + grain_highlight_updated: ['highlight_updated'], + grain_story_created: ['story_created'], + grain_upload_status: ['upload_status'], + } + + const hookType = hookTypeMap[triggerId] ?? 'recording_added' + const eventTypes = eventTypeMap[triggerId] ?? [] + + if (!hookTypeMap[triggerId]) { + grainLogger.warn( + `[${requestId}] Unknown triggerId for Grain: ${triggerId}, defaulting to recording_added`, + { + webhookId: webhookData.id, + } + ) + } + + grainLogger.info(`[${requestId}] Creating Grain webhook`, { + triggerId, + hookType, + eventTypes, + webhookId: webhookData.id, + }) + + const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}` + + const grainApiUrl = 'https://api.grain.com/_/public-api/v2/hooks/create' + + const requestBody: Record = { + hook_url: notificationUrl, + hook_type: hookType, + } + + const include: Record = {} + if (includeHighlights) { + include.highlights = true + } + if (includeParticipants) { + include.participants = true + } + if (includeAiSummary) { + include.ai_summary = true + } + if (Object.keys(include).length > 0) { + requestBody.include = include + } + + const grainResponse = await fetch(grainApiUrl, { + method: 'POST', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + 'Public-Api-Version': '2025-10-31', + }, + body: JSON.stringify(requestBody), + }) + + const responseBody = await grainResponse.json() + + if (!grainResponse.ok || responseBody.error || responseBody.errors) { + const errorMessage = + responseBody.errors?.detail || + responseBody.error?.message || + responseBody.error || + responseBody.message || + 'Unknown Grain API error' + grainLogger.error( + `[${requestId}] Failed to create webhook in Grain for webhook ${webhookData.id}. Status: ${grainResponse.status}`, + { message: errorMessage, response: responseBody } + ) + + let userFriendlyMessage = 'Failed to create webhook subscription in Grain' + if (grainResponse.status === 401) { + userFriendlyMessage = + 'Invalid Grain API Key. Please verify your Personal Access Token is correct.' + } else if (grainResponse.status === 403) { + userFriendlyMessage = + 'Access denied. Please ensure your Grain API Key has appropriate permissions.' + } else if (errorMessage && errorMessage !== 'Unknown Grain API error') { + userFriendlyMessage = `Grain error: ${errorMessage}` + } + + throw new Error(userFriendlyMessage) + } + + grainLogger.info( + `[${requestId}] Successfully created webhook in Grain for webhook ${webhookData.id}.`, + { + grainWebhookId: responseBody.id, + eventTypes, + } + ) + + return { id: responseBody.id, eventTypes } + } catch (error: any) { + grainLogger.error( + `[${requestId}] Exception during Grain webhook creation for webhook ${webhookData.id}.`, + { + message: error.message, + stack: error.stack, + } + ) + throw error + } +} + +export async function createLemlistWebhookSubscription( + webhookData: any, + requestId: string +): Promise<{ id: string } | undefined> { + try { + const { path, providerConfig } = webhookData + const { apiKey, triggerId, campaignId } = providerConfig || {} + + if (!apiKey) { + lemlistLogger.warn(`[${requestId}] Missing apiKey for Lemlist webhook creation.`, { + webhookId: webhookData.id, + }) + throw new Error( + 'Lemlist API Key is required. Please provide your Lemlist API Key in the trigger configuration.' + ) + } + + const eventTypeMap: Record = { + lemlist_email_replied: 'emailsReplied', + lemlist_linkedin_replied: 'linkedinReplied', + lemlist_interested: 'interested', + lemlist_not_interested: 'notInterested', + lemlist_email_opened: 'emailsOpened', + lemlist_email_clicked: 'emailsClicked', + lemlist_email_bounced: 'emailsBounced', + lemlist_email_sent: 'emailsSent', + lemlist_webhook: undefined, + } + + const eventType = eventTypeMap[triggerId] + const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}` + const authString = Buffer.from(`:${apiKey}`).toString('base64') + + lemlistLogger.info(`[${requestId}] Creating Lemlist webhook`, { + triggerId, + eventType, + hasCampaignId: !!campaignId, + webhookId: webhookData.id, + }) + + const lemlistApiUrl = 'https://api.lemlist.com/api/hooks' + + const requestBody: Record = { + targetUrl: notificationUrl, + } + + if (eventType) { + requestBody.type = eventType + } + + if (campaignId) { + requestBody.campaignId = campaignId + } + + const lemlistResponse = await fetch(lemlistApiUrl, { + method: 'POST', + headers: { + Authorization: `Basic ${authString}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }) + + const responseBody = await lemlistResponse.json() + + if (!lemlistResponse.ok || responseBody.error) { + const errorMessage = responseBody.message || responseBody.error || 'Unknown Lemlist API error' + lemlistLogger.error( + `[${requestId}] Failed to create webhook in Lemlist for webhook ${webhookData.id}. Status: ${lemlistResponse.status}`, + { message: errorMessage, response: responseBody } + ) + + let userFriendlyMessage = 'Failed to create webhook subscription in Lemlist' + if (lemlistResponse.status === 401) { + userFriendlyMessage = 'Invalid Lemlist API Key. Please verify your API Key is correct.' + } else if (lemlistResponse.status === 403) { + userFriendlyMessage = + 'Access denied. Please ensure your Lemlist API Key has appropriate permissions.' + } else if (errorMessage && errorMessage !== 'Unknown Lemlist API error') { + userFriendlyMessage = `Lemlist error: ${errorMessage}` + } + + throw new Error(userFriendlyMessage) + } + + lemlistLogger.info( + `[${requestId}] Successfully created webhook in Lemlist for webhook ${webhookData.id}.`, + { + lemlistWebhookId: responseBody._id, + } + ) + + return { id: responseBody._id } + } catch (error: any) { + lemlistLogger.error( + `[${requestId}] Exception during Lemlist webhook creation for webhook ${webhookData.id}.`, + { + message: error.message, + stack: error.stack, + } + ) + throw error + } +} + +export async function createAirtableWebhookSubscription( + userId: string, + webhookData: any, + requestId: string +): Promise { + try { + const { path, providerConfig } = webhookData + const { baseId, tableId, includeCellValuesInFieldIds } = providerConfig || {} + + if (!baseId || !tableId) { + airtableLogger.warn( + `[${requestId}] Missing baseId or tableId for Airtable webhook creation.`, + { + webhookId: webhookData.id, + } + ) + throw new Error( + 'Base ID and Table ID are required to create Airtable webhook. Please provide valid Airtable base and table IDs.' + ) + } + + const accessToken = await getOAuthToken(userId, 'airtable') + if (!accessToken) { + airtableLogger.warn( + `[${requestId}] Could not retrieve Airtable access token for user ${userId}. Cannot create webhook in Airtable.` + ) + throw new Error( + 'Airtable account connection required. Please connect your Airtable account in the trigger configuration and try again.' + ) + } + + const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}` + + const airtableApiUrl = `https://api.airtable.com/v0/bases/${baseId}/webhooks` + + const specification: any = { + options: { + filters: { + dataTypes: ['tableData'], + recordChangeScope: tableId, + }, + }, + } + + if (includeCellValuesInFieldIds === 'all') { + specification.options.includes = { + includeCellValuesInFieldIds: 'all', + } + } + + const requestBody: any = { + notificationUrl: notificationUrl, + specification: specification, + } + + const airtableResponse = await fetch(airtableApiUrl, { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }) + + const responseBody = await airtableResponse.json() + + if (!airtableResponse.ok || responseBody.error) { + const errorMessage = + responseBody.error?.message || responseBody.error || 'Unknown Airtable API error' + const errorType = responseBody.error?.type + airtableLogger.error( + `[${requestId}] Failed to create webhook in Airtable for webhook ${webhookData.id}. Status: ${airtableResponse.status}`, + { type: errorType, message: errorMessage, response: responseBody } + ) + + let userFriendlyMessage = 'Failed to create webhook subscription in Airtable' + if (airtableResponse.status === 404) { + userFriendlyMessage = + 'Airtable base or table not found. Please verify that the Base ID and Table ID are correct and that you have access to them.' + } else if (errorMessage && errorMessage !== 'Unknown Airtable API error') { + userFriendlyMessage = `Airtable error: ${errorMessage}` + } + + throw new Error(userFriendlyMessage) + } + airtableLogger.info( + `[${requestId}] Successfully created webhook in Airtable for webhook ${webhookData.id}.`, + { + airtableWebhookId: responseBody.id, + } + ) + return responseBody.id + } catch (error: any) { + airtableLogger.error( + `[${requestId}] Exception during Airtable webhook creation for webhook ${webhookData.id}.`, + { + message: error.message, + stack: error.stack, + } + ) + throw error } } +export async function createCalendlyWebhookSubscription( + webhookData: any, + requestId: string +): Promise { + try { + const { path, providerConfig } = webhookData + const { apiKey, organization, triggerId } = providerConfig || {} + + if (!apiKey) { + calendlyLogger.warn(`[${requestId}] Missing apiKey for Calendly webhook creation.`, { + webhookId: webhookData.id, + }) + throw new Error( + 'Personal Access Token is required to create Calendly webhook. Please provide your Calendly Personal Access Token.' + ) + } + + if (!organization) { + calendlyLogger.warn( + `[${requestId}] Missing organization URI for Calendly webhook creation.`, + { + webhookId: webhookData.id, + } + ) + throw new Error( + 'Organization URI is required to create Calendly webhook. Please provide your Organization URI from the "Get Current User" operation.' + ) + } + + if (!triggerId) { + calendlyLogger.warn(`[${requestId}] Missing triggerId for Calendly webhook creation.`, { + webhookId: webhookData.id, + }) + throw new Error('Trigger ID is required to create Calendly webhook') + } + + const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}` + + const eventTypeMap: Record = { + calendly_invitee_created: ['invitee.created'], + calendly_invitee_canceled: ['invitee.canceled'], + calendly_routing_form_submitted: ['routing_form_submission.created'], + calendly_webhook: ['invitee.created', 'invitee.canceled', 'routing_form_submission.created'], + } + + const events = eventTypeMap[triggerId] || ['invitee.created'] + + const calendlyApiUrl = 'https://api.calendly.com/webhook_subscriptions' + + const requestBody = { + url: notificationUrl, + events, + organization, + scope: 'organization', + } + + const calendlyResponse = await fetch(calendlyApiUrl, { + method: 'POST', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }) + + if (!calendlyResponse.ok) { + const errorBody = await calendlyResponse.json().catch(() => ({})) + const errorMessage = errorBody.message || errorBody.title || 'Unknown Calendly API error' + calendlyLogger.error( + `[${requestId}] Failed to create webhook in Calendly for webhook ${webhookData.id}. Status: ${calendlyResponse.status}`, + { response: errorBody } + ) + + let userFriendlyMessage = 'Failed to create webhook subscription in Calendly' + if (calendlyResponse.status === 401) { + userFriendlyMessage = + 'Calendly authentication failed. Please verify your Personal Access Token is correct.' + } else if (calendlyResponse.status === 403) { + userFriendlyMessage = + 'Calendly access denied. Please ensure you have appropriate permissions and a paid Calendly subscription.' + } else if (calendlyResponse.status === 404) { + userFriendlyMessage = + 'Calendly organization not found. Please verify the Organization URI is correct.' + } else if (errorMessage && errorMessage !== 'Unknown Calendly API error') { + userFriendlyMessage = `Calendly error: ${errorMessage}` + } + + throw new Error(userFriendlyMessage) + } + + const responseBody = await calendlyResponse.json() + const webhookUri = responseBody.resource?.uri + + if (!webhookUri) { + calendlyLogger.error( + `[${requestId}] Calendly webhook created but no webhook URI returned for webhook ${webhookData.id}`, + { response: responseBody } + ) + throw new Error('Calendly webhook creation succeeded but no webhook URI was returned') + } + + const webhookId = webhookUri.split('/').pop() + + if (!webhookId) { + calendlyLogger.error( + `[${requestId}] Could not extract webhook ID from Calendly URI: ${webhookUri}`, + { + response: responseBody, + } + ) + throw new Error('Failed to extract webhook ID from Calendly response') + } + + calendlyLogger.info( + `[${requestId}] Successfully created webhook in Calendly for webhook ${webhookData.id}.`, + { + calendlyWebhookUri: webhookUri, + calendlyWebhookId: webhookId, + } + ) + return webhookId + } catch (error: any) { + calendlyLogger.error( + `[${requestId}] Exception during Calendly webhook creation for webhook ${webhookData.id}.`, + { + message: error.message, + stack: error.stack, + } + ) + throw error + } +} + +export async function createWebflowWebhookSubscription( + userId: string, + webhookData: any, + requestId: string +): Promise { + try { + const { path, providerConfig } = webhookData + const { siteId, triggerId, collectionId, formId } = providerConfig || {} + + if (!siteId) { + webflowLogger.warn(`[${requestId}] Missing siteId for Webflow webhook creation.`, { + webhookId: webhookData.id, + }) + throw new Error('Site ID is required to create Webflow webhook') + } + + if (!triggerId) { + webflowLogger.warn(`[${requestId}] Missing triggerId for Webflow webhook creation.`, { + webhookId: webhookData.id, + }) + throw new Error('Trigger type is required to create Webflow webhook') + } + + const accessToken = await getOAuthToken(userId, 'webflow') + if (!accessToken) { + webflowLogger.warn( + `[${requestId}] Could not retrieve Webflow access token for user ${userId}. Cannot create webhook in Webflow.` + ) + throw new Error( + 'Webflow account connection required. Please connect your Webflow account in the trigger configuration and try again.' + ) + } + + const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}` + + const triggerTypeMap: Record = { + webflow_collection_item_created: 'collection_item_created', + webflow_collection_item_changed: 'collection_item_changed', + webflow_collection_item_deleted: 'collection_item_deleted', + webflow_form_submission: 'form_submission', + } + + const webflowTriggerType = triggerTypeMap[triggerId] + if (!webflowTriggerType) { + webflowLogger.warn(`[${requestId}] Invalid triggerId for Webflow: ${triggerId}`, { + webhookId: webhookData.id, + }) + throw new Error(`Invalid Webflow trigger type: ${triggerId}`) + } + + const webflowApiUrl = `https://api.webflow.com/v2/sites/${siteId}/webhooks` + + const requestBody: any = { + triggerType: webflowTriggerType, + url: notificationUrl, + } + + if (collectionId && webflowTriggerType.startsWith('collection_item_')) { + requestBody.filter = { + resource_type: 'collection', + resource_id: collectionId, + } + } + + if (formId && webflowTriggerType === 'form_submission') { + requestBody.filter = { + resource_type: 'form', + resource_id: formId, + } + } + + const webflowResponse = await fetch(webflowApiUrl, { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + accept: 'application/json', + }, + body: JSON.stringify(requestBody), + }) + + const responseBody = await webflowResponse.json() + + if (!webflowResponse.ok || responseBody.error) { + const errorMessage = responseBody.message || responseBody.error || 'Unknown Webflow API error' + webflowLogger.error( + `[${requestId}] Failed to create webhook in Webflow for webhook ${webhookData.id}. Status: ${webflowResponse.status}`, + { message: errorMessage, response: responseBody } + ) + throw new Error(errorMessage) + } + + webflowLogger.info( + `[${requestId}] Successfully created webhook in Webflow for webhook ${webhookData.id}.`, + { + webflowWebhookId: responseBody.id || responseBody._id, + } + ) + + return responseBody.id || responseBody._id + } catch (error: any) { + webflowLogger.error( + `[${requestId}] Exception during Webflow webhook creation for webhook ${webhookData.id}.`, + { + message: error.message, + stack: error.stack, + } + ) + throw error + } +} + +type ExternalSubscriptionResult = { + updatedProviderConfig: Record + externalSubscriptionCreated: boolean +} + +type RecreateCheckInput = { + previousProvider: string + nextProvider: string + previousConfig: Record + nextConfig: Record +} + +/** Providers that create external webhook subscriptions */ +const PROVIDERS_WITH_EXTERNAL_SUBSCRIPTIONS = new Set([ + 'airtable', + 'calendly', + 'webflow', + 'typeform', + 'grain', + 'lemlist', + 'telegram', + 'microsoft-teams', +]) + +/** System-managed fields that shouldn't trigger recreation */ +const SYSTEM_MANAGED_FIELDS = new Set([ + 'externalId', + 'externalSubscriptionId', + 'eventTypes', + 'webhookTag', + 'historyId', + 'lastCheckedTimestamp', + 'setupCompleted', + 'userId', +]) + +export function shouldRecreateExternalWebhookSubscription({ + previousProvider, + nextProvider, + previousConfig, + nextConfig, +}: RecreateCheckInput): boolean { + if (previousProvider !== nextProvider) { + return ( + PROVIDERS_WITH_EXTERNAL_SUBSCRIPTIONS.has(previousProvider) || + PROVIDERS_WITH_EXTERNAL_SUBSCRIPTIONS.has(nextProvider) + ) + } + + if (!PROVIDERS_WITH_EXTERNAL_SUBSCRIPTIONS.has(nextProvider)) { + return false + } + + const allKeys = new Set([...Object.keys(previousConfig), ...Object.keys(nextConfig)]) + + for (const key of allKeys) { + if (SYSTEM_MANAGED_FIELDS.has(key)) continue + + const prevVal = previousConfig[key] + const nextVal = nextConfig[key] + + const prevStr = typeof prevVal === 'object' ? JSON.stringify(prevVal ?? null) : prevVal + const nextStr = typeof nextVal === 'object' ? JSON.stringify(nextVal ?? null) : nextVal + + if (prevStr !== nextStr) { + return true + } + } + + return false +} + +export async function createExternalWebhookSubscription( + request: NextRequest, + webhookData: any, + workflow: any, + userId: string, + requestId: string +): Promise { + const provider = webhookData.provider as string + const providerConfig = (webhookData.providerConfig as Record) || {} + let updatedProviderConfig = providerConfig + let externalSubscriptionCreated = false + + if (provider === 'airtable') { + const externalId = await createAirtableWebhookSubscription(userId, webhookData, requestId) + if (externalId) { + updatedProviderConfig = { ...updatedProviderConfig, externalId } + externalSubscriptionCreated = true + } + } else if (provider === 'calendly') { + const externalId = await createCalendlyWebhookSubscription(webhookData, requestId) + if (externalId) { + updatedProviderConfig = { ...updatedProviderConfig, externalId } + externalSubscriptionCreated = true + } + } else if (provider === 'microsoft-teams') { + await createTeamsSubscription(request, webhookData, workflow, requestId) + externalSubscriptionCreated = + (providerConfig.triggerId as string | undefined) === 'microsoftteams_chat_subscription' + } else if (provider === 'telegram') { + await createTelegramWebhook(request, webhookData, requestId) + externalSubscriptionCreated = true + } else if (provider === 'webflow') { + const externalId = await createWebflowWebhookSubscription(userId, webhookData, requestId) + if (externalId) { + updatedProviderConfig = { ...updatedProviderConfig, externalId } + externalSubscriptionCreated = true + } + } else if (provider === 'typeform') { + const usedTag = await createTypeformWebhook(request, webhookData, requestId) + if (!updatedProviderConfig.webhookTag && usedTag) { + updatedProviderConfig = { ...updatedProviderConfig, webhookTag: usedTag } + } + externalSubscriptionCreated = true + } else if (provider === 'grain') { + const result = await createGrainWebhookSubscription(request, webhookData, requestId) + if (result) { + updatedProviderConfig = { + ...updatedProviderConfig, + externalId: result.id, + eventTypes: result.eventTypes, + } + externalSubscriptionCreated = true + } + } else if (provider === 'lemlist') { + const result = await createLemlistWebhookSubscription(webhookData, requestId) + if (result) { + updatedProviderConfig = { ...updatedProviderConfig, externalId: result.id } + externalSubscriptionCreated = true + } + } + + return { updatedProviderConfig, externalSubscriptionCreated } +} + /** * Clean up external webhook subscriptions for a webhook * Handles Airtable, Teams, Telegram, Typeform, Calendly, Grain, and Lemlist cleanup @@ -780,6 +1606,8 @@ export async function cleanupExternalWebhook( await deleteTypeformWebhook(webhook, requestId) } else if (webhook.provider === 'calendly') { await deleteCalendlyWebhook(webhook, requestId) + } else if (webhook.provider === 'webflow') { + await deleteWebflowWebhook(webhook, workflow, requestId) } else if (webhook.provider === 'grain') { await deleteGrainWebhook(webhook, requestId) } else if (webhook.provider === 'lemlist') { diff --git a/apps/sim/socket/database/operations.ts b/apps/sim/socket/database/operations.ts index f5960e4b59..9ce1bfbc86 100644 --- a/apps/sim/socket/database/operations.ts +++ b/apps/sim/socket/database/operations.ts @@ -1,6 +1,7 @@ import * as schema from '@sim/db' import { webhook, workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@sim/db' import { createLogger } from '@sim/logger' +import type { InferSelectModel } from 'drizzle-orm' import { and, eq, inArray, or, sql } from 'drizzle-orm' import { drizzle } from 'drizzle-orm/postgres-js' import postgres from 'postgres' @@ -1134,7 +1135,14 @@ async function handleWorkflowOperationTx( parallelCount: Object.keys(parallels || {}).length, }) - // Delete all existing blocks (this will cascade delete edges via ON DELETE CASCADE) + // Snapshot existing webhooks before deletion to preserve them through the cycle + // (workflowBlocks has CASCADE DELETE to webhook table) + const existingWebhooks = await tx + .select() + .from(webhook) + .where(eq(webhook.workflowId, workflowId)) + + // Delete all existing blocks (this will cascade delete edges and webhooks via ON DELETE CASCADE) await tx.delete(workflowBlocks).where(eq(workflowBlocks.workflowId, workflowId)) // Delete all existing subflows @@ -1200,6 +1208,32 @@ async function handleWorkflowOperationTx( await tx.insert(workflowSubflows).values(parallelValues) } + // Re-insert preserved webhooks if any exist and their blocks still exist + type WebhookRecord = InferSelectModel + if (existingWebhooks.length > 0) { + const webhookInserts = existingWebhooks + .filter((wh: WebhookRecord) => !!blocks?.[wh.blockId ?? '']) + .map((wh: WebhookRecord) => ({ + id: wh.id, + workflowId: wh.workflowId, + blockId: wh.blockId, + path: wh.path, + provider: wh.provider, + providerConfig: wh.providerConfig, + credentialSetId: wh.credentialSetId, + isActive: wh.isActive, + createdAt: wh.createdAt, + updatedAt: new Date(), + })) + + if (webhookInserts.length > 0) { + await tx.insert(webhook).values(webhookInserts) + logger.debug(`Preserved ${webhookInserts.length} webhook(s) through state replacement`, { + workflowId, + }) + } + } + logger.info(`Successfully replaced workflow state for ${workflowId}`) break } diff --git a/apps/sim/triggers/constants.ts b/apps/sim/triggers/constants.ts index 354994db04..66258cbd41 100644 --- a/apps/sim/triggers/constants.ts +++ b/apps/sim/triggers/constants.ts @@ -8,24 +8,8 @@ export const SYSTEM_SUBBLOCK_IDS: string[] = [ 'triggerCredentials', // OAuth credentials subblock 'triggerInstructions', // Setup instructions text 'webhookUrlDisplay', // Webhook URL display - 'triggerSave', // Save configuration button 'samplePayload', // Example payload display 'setupScript', // Setup script code (e.g., Apps Script) - 'triggerId', // Stored trigger ID - 'selectedTriggerId', // Selected trigger from dropdown (multi-trigger blocks) -] - -/** - * Trigger-related subblock IDs whose values should be persisted and - * propagated when workflows are edited programmatically. - */ -export const TRIGGER_PERSISTED_SUBBLOCK_IDS: string[] = [ - 'triggerConfig', - 'triggerCredentials', - 'triggerId', - 'selectedTriggerId', - 'webhookId', - 'triggerPath', ] /** diff --git a/apps/sim/triggers/grain/utils.ts b/apps/sim/triggers/grain/utils.ts index 1d371f9cbb..5b4820efaa 100644 --- a/apps/sim/triggers/grain/utils.ts +++ b/apps/sim/triggers/grain/utils.ts @@ -19,7 +19,6 @@ export function grainSetupInstructions(eventType: string): string { const instructions = [ 'Enter your Grain API Key (Personal Access Token) above.', 'You can find or create your API key in Grain at Settings > Integrations > API.', - `Click "Save Configuration" to automatically create the webhook in Grain for ${eventType} events.`, 'The webhook will be automatically deleted when you remove this trigger.', ] diff --git a/apps/sim/triggers/hubspot/utils.ts b/apps/sim/triggers/hubspot/utils.ts index 09ef6fcfcc..21cd74bd22 100644 --- a/apps/sim/triggers/hubspot/utils.ts +++ b/apps/sim/triggers/hubspot/utils.ts @@ -82,9 +82,8 @@ export function hubspotSetupInstructions(eventType: string, additionalNotes?: st 'Step 3: Configure OAuth Settings
After creating your app via CLI, configure it to add the OAuth Redirect URL: https://www.sim.ai/api/auth/oauth2/callback/hubspot. Then retrieve your Client ID and Client Secret from your app configuration and enter them in the fields above.', "Step 4: Get App ID and Developer API Key
In your HubSpot developer account, find your App ID (shown below your app name) and your Developer API Key (in app settings). You'll need both for the next steps.", 'Step 5: Set Required Scopes
Configure your app to include the required OAuth scope: crm.objects.contacts.read', - 'Step 6: Save Configuration in Sim
Click the "Save Configuration" button above. This will generate your unique webhook URL.', - 'Step 7: Configure Webhook in HubSpot via API
After saving above, copy the Webhook URL and run the two curl commands below (replace {YOUR_APP_ID}, {YOUR_DEVELOPER_API_KEY}, and {YOUR_WEBHOOK_URL_FROM_ABOVE} with your actual values).', - "Step 8: Test Your Webhook
Create or modify a contact in HubSpot to trigger the webhook. Check your workflow execution logs in Sim to verify it's working.", + 'Step 6: Configure Webhook in HubSpot via API
After saving above, copy the Webhook URL and run the two curl commands below (replace {YOUR_APP_ID}, {YOUR_DEVELOPER_API_KEY}, and {YOUR_WEBHOOK_URL_FROM_ABOVE} with your actual values).', + "Step 7: Test Your Webhook
Create or modify a contact in HubSpot to trigger the webhook. Check your workflow execution logs in Sim to verify it's working.", ] if (additionalNotes) { diff --git a/apps/sim/triggers/index.ts b/apps/sim/triggers/index.ts index d4c517e9c5..d00cbf175c 100644 --- a/apps/sim/triggers/index.ts +++ b/apps/sim/triggers/index.ts @@ -14,6 +14,9 @@ export function getTrigger(triggerId: string): TriggerConfig { } const clonedTrigger = { ...trigger, subBlocks: [...trigger.subBlocks] } + clonedTrigger.subBlocks = clonedTrigger.subBlocks.filter( + (subBlock) => subBlock.id !== 'triggerSave' && subBlock.type !== 'trigger-save' + ) // Inject samplePayload for webhooks/pollers with condition if (trigger.webhook || trigger.id.includes('webhook') || trigger.id.includes('poller')) { @@ -155,16 +158,6 @@ export function buildTriggerSubBlocks(options: BuildTriggerSubBlocksOptions): Su } // Save button - blocks.push({ - id: 'triggerSave', - title: '', - type: 'trigger-save', - hideFromPreview: true, - mode: 'trigger', - triggerId: triggerId, - condition: { field: 'selectedTriggerId', value: triggerId }, - }) - // Setup instructions blocks.push({ id: 'triggerInstructions', diff --git a/apps/sim/triggers/lemlist/utils.ts b/apps/sim/triggers/lemlist/utils.ts index 6ecbcce6cf..5a8160d777 100644 --- a/apps/sim/triggers/lemlist/utils.ts +++ b/apps/sim/triggers/lemlist/utils.ts @@ -23,7 +23,6 @@ export function lemlistSetupInstructions(eventType: string): string { const instructions = [ 'Enter your Lemlist API Key above.', 'You can find your API key in Lemlist at Settings > Integrations > API.', - `Click "Save Configuration" to automatically create the webhook in Lemlist for ${eventType} events.`, 'The webhook will be automatically deleted when you remove this trigger.', ] diff --git a/apps/sim/triggers/twilio_voice/webhook.ts b/apps/sim/triggers/twilio_voice/webhook.ts index c87194b77f..45ebb5744f 100644 --- a/apps/sim/triggers/twilio_voice/webhook.ts +++ b/apps/sim/triggers/twilio_voice/webhook.ts @@ -129,7 +129,6 @@ Return ONLY the TwiML with square brackets - no explanations, no markdown, no ex 'Scroll down to the "Voice Configuration" section.', 'In the "A CALL COMES IN" field, select "Webhook" and paste the Webhook URL (from above).', 'Ensure the HTTP method is set to POST.', - 'Click "Save configuration".', 'How it works: When a call comes in, Twilio receives your TwiML response immediately and executes those instructions. Your workflow runs in the background with access to caller information, call status, and any recorded/transcribed data.', ] .map((instruction, index) => `${index + 1}. ${instruction}`) From 1cc489e5442f0447ae3da8eb1395a7b82c37acaa Mon Sep 17 00:00:00 2001 From: Waleed Date: Thu, 15 Jan 2026 13:25:00 -0800 Subject: [PATCH 04/15] feat(workflow-controls): added action bar for workflow controls (#2767) * feat(workflow-controls): added action bar for picker/hand/undo/redo/zoom workflow controls, added general setting to disable * added util for fit to zoom that accounts for sidebar, terminal, and panel * ack PR comments * remove dead state variable, add logs * improvement(ui/ux): action bar, panel, tooltip, dragging, invite modal * added fit to view in canvas context menu * fix(theme): dark mode flash * fix: duplicate fit to view * refactor: popovers; improvement: notifications, diff controls, action bar * improvement(action-bar): ui/ux * refactor(action-bar): renamed to workflow controls * ran migrations * fix: deleted migration --------- Co-authored-by: Emir Karabeg --- .../app/_shell/providers/tooltip-provider.tsx | 11 + apps/sim/app/_styles/globals.css | 33 + apps/sim/app/api/users/me/settings/route.ts | 5 +- apps/sim/app/layout.tsx | 5 +- apps/sim/app/playground/page.tsx | 6 + apps/sim/app/templates/layout-client.tsx | 11 +- .../app/workspace/[workspaceId]/layout.tsx | 19 +- .../[workspaceId]/utils/commands-utils.ts | 6 + .../block-menu.tsx} | 41 +- .../components/block-menu/index.ts | 2 + .../canvas-menu.tsx} | 45 +- .../components/canvas-menu/index.ts | 2 + .../w/[workflowId]/components/chat/chat.tsx | 29 +- .../components/context-menu/index.ts | 8 - .../components/context-menu/types.ts | 99 - .../diff-controls/diff-controls.tsx | 18 +- .../w/[workflowId]/components/index.ts | 3 + .../notifications/notifications.tsx | 29 +- .../components/general/general.tsx | 4 +- .../components/panel/components/index.ts | 1 - .../panel/components/toolbar/toolbar.tsx | 10 +- .../workflow-controls/workflow-controls.tsx | 51 - .../w/[workflowId]/components/panel/panel.tsx | 3 - .../components/subflows/subflow-node.tsx | 7 +- .../components/action-bar/action-bar.tsx | 40 +- .../workflow-block/workflow-block.tsx | 7 +- .../components/workflow-controls/index.ts | 1 + .../workflow-controls/workflow-controls.tsx | 225 + .../w/[workflowId]/hooks/use-auto-layout.ts | 8 +- .../hooks/use-canvas-context-menu.ts | 45 +- .../[workspaceId]/w/[workflowId]/workflow.tsx | 108 +- .../components/general/general.tsx | 21 + .../settings-modal/settings-modal.tsx | 2 +- .../components/permissions-table-skeleton.tsx | 6 +- .../components/invite-modal/invite-modal.tsx | 1 + .../workspace-header/workspace-header.tsx | 6 +- .../w/components/sidebar/sidebar.tsx | 2 +- .../emcn/components/tag-input/tag-input.tsx | 32 +- .../emcn/components/tooltip/tooltip.tsx | 7 +- apps/sim/components/emcn/icons/cursor.tsx | 22 + apps/sim/components/emcn/icons/expand.tsx | 43 + apps/sim/components/emcn/icons/hand.tsx | 43 + apps/sim/components/emcn/icons/index.ts | 3 + apps/sim/components/emcn/icons/redo.tsx | 4 +- apps/sim/components/emcn/icons/undo.tsx | 4 +- apps/sim/components/emcn/icons/zoom-in.tsx | 8 +- apps/sim/components/emcn/icons/zoom-out.tsx | 6 +- apps/sim/hooks/queries/general-settings.ts | 3 + apps/sim/hooks/use-canvas-viewport.ts | 185 + apps/sim/stores/canvas-mode/index.ts | 2 + apps/sim/stores/canvas-mode/store.ts | 22 + apps/sim/stores/settings/general/store.ts | 3 +- apps/sim/stores/settings/general/types.ts | 2 + .../db/migrations/0142_short_nicolaos.sql | 2 + .../db/migrations/meta/0142_snapshot.json | 10274 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 3 +- 57 files changed, 11198 insertions(+), 397 deletions(-) create mode 100644 apps/sim/app/_shell/providers/tooltip-provider.tsx rename apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/{context-menu/block-context-menu.tsx => block-menu/block-menu.tsx} (84%) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/index.ts rename apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/{context-menu/pane-context-menu.tsx => canvas-menu/canvas-menu.tsx} (80%) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/canvas-menu/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/context-menu/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/context-menu/types.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/workflow-controls/workflow-controls.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-controls/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-controls/workflow-controls.tsx create mode 100644 apps/sim/components/emcn/icons/cursor.tsx create mode 100644 apps/sim/components/emcn/icons/expand.tsx create mode 100644 apps/sim/components/emcn/icons/hand.tsx create mode 100644 apps/sim/hooks/use-canvas-viewport.ts create mode 100644 apps/sim/stores/canvas-mode/index.ts create mode 100644 apps/sim/stores/canvas-mode/store.ts create mode 100644 packages/db/migrations/0142_short_nicolaos.sql create mode 100644 packages/db/migrations/meta/0142_snapshot.json diff --git a/apps/sim/app/_shell/providers/tooltip-provider.tsx b/apps/sim/app/_shell/providers/tooltip-provider.tsx new file mode 100644 index 0000000000..84274ddb8c --- /dev/null +++ b/apps/sim/app/_shell/providers/tooltip-provider.tsx @@ -0,0 +1,11 @@ +'use client' + +import { Tooltip } from '@/components/emcn' + +interface TooltipProviderProps { + children: React.ReactNode +} + +export function TooltipProvider({ children }: TooltipProviderProps) { + return {children} +} diff --git a/apps/sim/app/_styles/globals.css b/apps/sim/app/_styles/globals.css index b9dcb8c71c..c260032460 100644 --- a/apps/sim/app/_styles/globals.css +++ b/apps/sim/app/_styles/globals.css @@ -58,6 +58,25 @@ pointer-events: none !important; } +/** + * Workflow canvas cursor styles + * Override React Flow's default selection cursor based on canvas mode + */ +.workflow-container.canvas-mode-cursor .react-flow__pane, +.workflow-container.canvas-mode-cursor .react-flow__selectionpane { + cursor: default !important; +} + +.workflow-container.canvas-mode-hand .react-flow__pane, +.workflow-container.canvas-mode-hand .react-flow__selectionpane { + cursor: grab !important; +} + +.workflow-container.canvas-mode-hand .react-flow__pane:active, +.workflow-container.canvas-mode-hand .react-flow__selectionpane:active { + cursor: grabbing !important; +} + /** * Selected node ring indicator * Uses a pseudo-element overlay to match the original behavior (absolute inset-0 z-40) @@ -657,6 +676,20 @@ input[type="search"]::-ms-clear { } } +/** + * Notification toast enter animation + */ +@keyframes notification-enter { + from { + opacity: 0; + transform: translateX(-16px); + } + to { + opacity: 1; + transform: translateX(var(--stack-offset, 0px)); + } +} + /** * @depricated * Legacy globals (light/dark) kept for backward-compat with old classes. diff --git a/apps/sim/app/api/users/me/settings/route.ts b/apps/sim/app/api/users/me/settings/route.ts index 6f6094558f..c8de2b0568 100644 --- a/apps/sim/app/api/users/me/settings/route.ts +++ b/apps/sim/app/api/users/me/settings/route.ts @@ -27,10 +27,11 @@ const SettingsSchema = z.object({ superUserModeEnabled: z.boolean().optional(), errorNotificationsEnabled: z.boolean().optional(), snapToGridSize: z.number().min(0).max(50).optional(), + showActionBar: z.boolean().optional(), }) const defaultSettings = { - theme: 'system', + theme: 'dark', autoConnect: true, telemetryEnabled: true, emailPreferences: {}, @@ -39,6 +40,7 @@ const defaultSettings = { superUserModeEnabled: false, errorNotificationsEnabled: true, snapToGridSize: 0, + showActionBar: true, } export async function GET() { @@ -73,6 +75,7 @@ export async function GET() { superUserModeEnabled: userSettings.superUserModeEnabled ?? true, errorNotificationsEnabled: userSettings.errorNotificationsEnabled ?? true, snapToGridSize: userSettings.snapToGridSize ?? 0, + showActionBar: userSettings.showActionBar ?? true, }, }, { status: 200 } diff --git a/apps/sim/app/layout.tsx b/apps/sim/app/layout.tsx index 327a519291..166b260af8 100644 --- a/apps/sim/app/layout.tsx +++ b/apps/sim/app/layout.tsx @@ -12,6 +12,7 @@ import { HydrationErrorHandler } from '@/app/_shell/hydration-error-handler' import { QueryProvider } from '@/app/_shell/providers/query-provider' import { SessionProvider } from '@/app/_shell/providers/session-provider' import { ThemeProvider } from '@/app/_shell/providers/theme-provider' +import { TooltipProvider } from '@/app/_shell/providers/tooltip-provider' import { season } from '@/app/_styles/fonts/season/season' export const viewport: Viewport = { @@ -208,7 +209,9 @@ export default function RootLayout({ children }: { children: React.ReactNode }) - {children} + + {children} + diff --git a/apps/sim/app/playground/page.tsx b/apps/sim/app/playground/page.tsx index 4670b805e0..d380256a21 100644 --- a/apps/sim/app/playground/page.tsx +++ b/apps/sim/app/playground/page.tsx @@ -21,12 +21,15 @@ import { Combobox, Connections, Copy, + Cursor, DatePicker, DocumentAttachment, Duplicate, + Expand, Eye, FolderCode, FolderPlus, + Hand, HexSimple, Input, Key as KeyIcon, @@ -991,11 +994,14 @@ export default function PlaygroundPage() { { Icon: ChevronDown, name: 'ChevronDown' }, { Icon: Connections, name: 'Connections' }, { Icon: Copy, name: 'Copy' }, + { Icon: Cursor, name: 'Cursor' }, { Icon: DocumentAttachment, name: 'DocumentAttachment' }, { Icon: Duplicate, name: 'Duplicate' }, + { Icon: Expand, name: 'Expand' }, { Icon: Eye, name: 'Eye' }, { Icon: FolderCode, name: 'FolderCode' }, { Icon: FolderPlus, name: 'FolderPlus' }, + { Icon: Hand, name: 'Hand' }, { Icon: HexSimple, name: 'HexSimple' }, { Icon: KeyIcon, name: 'Key' }, { Icon: Layout, name: 'Layout' }, diff --git a/apps/sim/app/templates/layout-client.tsx b/apps/sim/app/templates/layout-client.tsx index d886b6c379..f49b81c6c6 100644 --- a/apps/sim/app/templates/layout-client.tsx +++ b/apps/sim/app/templates/layout-client.tsx @@ -1,15 +1,12 @@ 'use client' -import { Tooltip } from '@/components/emcn' import { season } from '@/app/_styles/fonts/season/season' export default function TemplatesLayoutClient({ children }: { children: React.ReactNode }) { return ( - -
-
- {children} -
- +
+
+ {children} +
) } diff --git a/apps/sim/app/workspace/[workspaceId]/layout.tsx b/apps/sim/app/workspace/[workspaceId]/layout.tsx index 8b5d1093a4..8cf43aa40a 100644 --- a/apps/sim/app/workspace/[workspaceId]/layout.tsx +++ b/apps/sim/app/workspace/[workspaceId]/layout.tsx @@ -1,6 +1,5 @@ 'use client' -import { Tooltip } from '@/components/emcn' import { GlobalCommandsProvider } from '@/app/workspace/[workspaceId]/providers/global-commands-provider' import { ProviderModelsLoader } from '@/app/workspace/[workspaceId]/providers/provider-models-loader' import { SettingsLoader } from '@/app/workspace/[workspaceId]/providers/settings-loader' @@ -13,16 +12,14 @@ export default function WorkspaceLayout({ children }: { children: React.ReactNod - -
- -
- -
- {children} -
-
-
+
+ +
+ +
+ {children} +
+
) diff --git a/apps/sim/app/workspace/[workspaceId]/utils/commands-utils.ts b/apps/sim/app/workspace/[workspaceId]/utils/commands-utils.ts index da68876e21..eda6cdda0e 100644 --- a/apps/sim/app/workspace/[workspaceId]/utils/commands-utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/utils/commands-utils.ts @@ -19,6 +19,7 @@ export type CommandId = | 'clear-terminal-console' | 'focus-toolbar-search' | 'clear-notifications' + | 'fit-to-view' /** * Static metadata for a global command. @@ -104,6 +105,11 @@ export const COMMAND_DEFINITIONS: Record = { shortcut: 'Mod+E', allowInEditable: false, }, + 'fit-to-view': { + id: 'fit-to-view', + shortcut: 'Mod+Shift+F', + allowInEditable: false, + }, } /** diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/context-menu/block-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx similarity index 84% rename from apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/context-menu/block-context-menu.tsx rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx index 8945b13dc8..e0f7fac613 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/context-menu/block-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx @@ -1,5 +1,6 @@ 'use client' +import type { RefObject } from 'react' import { Popover, PopoverAnchor, @@ -7,14 +8,48 @@ import { PopoverDivider, PopoverItem, } from '@/components/emcn' -import type { BlockContextMenuProps } from './types' + +/** + * Block information for context menu actions + */ +export interface BlockInfo { + id: string + type: string + enabled: boolean + horizontalHandles: boolean + parentId?: string + parentType?: string +} + +/** + * Props for BlockMenu component + */ +export interface BlockMenuProps { + isOpen: boolean + position: { x: number; y: number } + menuRef: RefObject + onClose: () => void + selectedBlocks: BlockInfo[] + onCopy: () => void + onPaste: () => void + onDuplicate: () => void + onDelete: () => void + onToggleEnabled: () => void + onToggleHandles: () => void + onRemoveFromSubflow: () => void + onOpenEditor: () => void + onRename: () => void + hasClipboard?: boolean + showRemoveFromSubflow?: boolean + disableEdit?: boolean +} /** * Context menu for workflow block(s). * Displays block-specific actions in a popover at right-click position. * Supports multi-selection - actions apply to all selected blocks. */ -export function BlockContextMenu({ +export function BlockMenu({ isOpen, position, menuRef, @@ -32,7 +67,7 @@ export function BlockContextMenu({ hasClipboard = false, showRemoveFromSubflow = false, disableEdit = false, -}: BlockContextMenuProps) { +}: BlockMenuProps) { const isSingleBlock = selectedBlocks.length === 1 const allEnabled = selectedBlocks.every((b) => b.enabled) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/index.ts new file mode 100644 index 0000000000..5016029014 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/index.ts @@ -0,0 +1,2 @@ +export type { BlockInfo, BlockMenuProps } from './block-menu' +export { BlockMenu } from './block-menu' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/context-menu/pane-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/canvas-menu/canvas-menu.tsx similarity index 80% rename from apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/context-menu/pane-context-menu.tsx rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/canvas-menu/canvas-menu.tsx index a5bba68b46..7cd5294f32 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/context-menu/pane-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/canvas-menu/canvas-menu.tsx @@ -1,5 +1,6 @@ 'use client' +import type { RefObject } from 'react' import { Popover, PopoverAnchor, @@ -7,13 +8,40 @@ import { PopoverDivider, PopoverItem, } from '@/components/emcn' -import type { PaneContextMenuProps } from './types' /** - * Context menu for workflow canvas pane. + * Props for CanvasMenu component + */ +export interface CanvasMenuProps { + isOpen: boolean + position: { x: number; y: number } + menuRef: RefObject + onClose: () => void + onUndo: () => void + onRedo: () => void + onPaste: () => void + onAddBlock: () => void + onAutoLayout: () => void + onFitToView: () => void + onOpenLogs: () => void + onToggleVariables: () => void + onToggleChat: () => void + onInvite: () => void + isVariablesOpen?: boolean + isChatOpen?: boolean + hasClipboard?: boolean + disableEdit?: boolean + disableAdmin?: boolean + canUndo?: boolean + canRedo?: boolean + isInvitationsDisabled?: boolean +} + +/** + * Context menu for workflow canvas. * Displays canvas-level actions when right-clicking empty space. */ -export function PaneContextMenu({ +export function CanvasMenu({ isOpen, position, menuRef, @@ -23,6 +51,7 @@ export function PaneContextMenu({ onPaste, onAddBlock, onAutoLayout, + onFitToView, onOpenLogs, onToggleVariables, onToggleChat, @@ -35,7 +64,7 @@ export function PaneContextMenu({ canUndo = false, canRedo = false, isInvitationsDisabled = false, -}: PaneContextMenuProps) { +}: CanvasMenuProps) { return ( Auto-layout ⇧L + { + onFitToView() + onClose() + }} + > + Fit to View + {/* Navigation actions */} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/canvas-menu/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/canvas-menu/index.ts new file mode 100644 index 0000000000..ac5ef3e74f --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/canvas-menu/index.ts @@ -0,0 +1,2 @@ +export type { CanvasMenuProps } from './canvas-menu' +export { CanvasMenu } from './canvas-menu' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx index 7518a35c4d..b2a3349272 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx @@ -20,6 +20,7 @@ import { PopoverItem, PopoverScrollArea, PopoverTrigger, + Tooltip, Trash, } from '@/components/emcn' import { useSession } from '@/lib/auth/auth-client' @@ -869,7 +870,7 @@ export function Chat() {
{/* More menu with actions */} - + See preview diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/index.ts index e661875717..6b02dd1826 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/index.ts @@ -2,4 +2,3 @@ export { Copilot } from './copilot/copilot' export { Deploy } from './deploy/deploy' export { Editor } from './editor/editor' export { Toolbar } from './toolbar/toolbar' -export { WorkflowControls } from './workflow-controls/workflow-controls' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx index 2439299f55..76637a7fcb 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx @@ -327,12 +327,14 @@ export const Toolbar = forwardRef(function Toolbar( /** * Handle search input blur. * - * We intentionally keep search mode active after blur so that ArrowUp/Down - * navigation continues to work after the first move from the search input - * into the triggers/blocks list (e.g. when initiated via Mod+F). + * If the search query is empty, deactivate search mode to show the search icon again. + * If there's a query, keep search mode active so ArrowUp/Down navigation continues + * to work after focus moves into the triggers/blocks list (e.g. when initiated via Mod+F). */ const handleSearchBlur = () => { - // No-op by design + if (!searchQuery.trim()) { + setIsSearchActive(false) + } } /** diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/workflow-controls/workflow-controls.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/workflow-controls/workflow-controls.tsx deleted file mode 100644 index c3cbc08ba5..0000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/workflow-controls/workflow-controls.tsx +++ /dev/null @@ -1,51 +0,0 @@ -'use client' - -import { Button, Redo, Undo } from '@/components/emcn' -import { useSession } from '@/lib/auth/auth-client' -import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' -import { useUndoRedoStore } from '@/stores/undo-redo' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' - -/** - * Workflow controls component that provides undo/redo functionality. - * Styled to align with the panel tab buttons. - */ -export function WorkflowControls() { - const { undo, redo } = useCollaborativeWorkflow() - const { activeWorkflowId } = useWorkflowRegistry() - const { data: session } = useSession() - const userId = session?.user?.id || 'unknown' - const stacks = useUndoRedoStore((s) => s.stacks) - - const undoRedoSizes = (() => { - const key = activeWorkflowId && userId ? `${activeWorkflowId}:${userId}` : '' - const stack = (key && stacks[key]) || { undo: [], redo: [] } - return { undoSize: stack.undo.length, redoSize: stack.redo.length } - })() - - const canUndo = undoRedoSizes.undoSize > 0 - const canRedo = undoRedoSizes.redoSize > 0 - - return ( -
- - -
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index 9e915ce18c..e403ce37b5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -495,9 +495,6 @@ export function Panel() { Editor
- - {/* Workflow Controls (Undo/Redo) */} - {/* */}
{/* Tab Content - Keep all tabs mounted but hidden to preserve state */} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx index bcb8a4fea9..df0d1afa07 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx @@ -148,7 +148,7 @@ export const SubflowNodeComponent = memo(({ data, id }: NodeProps setCurrentBlockId(id)} className={cn( - 'relative cursor-pointer select-none rounded-[8px] border border-[var(--border-1)]', + 'workflow-drag-handle relative cursor-grab select-none rounded-[8px] border border-[var(--border-1)] [&:active]:cursor-grabbing', 'transition-block-bg transition-ring', 'z-[20]' )} @@ -166,11 +166,8 @@ export const SubflowNodeComponent = memo(({ data, id }: NodeProps { - e.stopPropagation() - }} >
{!isNoteBlock && ( @@ -124,14 +136,10 @@ export const ActionBar = memo( collaborativeBatchToggleBlockEnabled([blockId]) } }} - className='hover:!text-[var(--text-inverse)] h-[23px] w-[23px] rounded-[8px] bg-[var(--surface-7)] p-0 text-[var(--text-secondary)] hover:bg-[var(--brand-secondary)]' + className={ACTION_BUTTON_STYLES} disabled={disabled} > - {isEnabled ? ( - - ) : ( - - )} + {isEnabled ? : } @@ -151,10 +159,10 @@ export const ActionBar = memo( handleDuplicateBlock() } }} - className='hover:!text-[var(--text-inverse)] h-[23px] w-[23px] rounded-[8px] bg-[var(--surface-7)] p-0 text-[var(--text-secondary)] hover:bg-[var(--brand-secondary)]' + className={ACTION_BUTTON_STYLES} disabled={disabled} > - + {getTooltipMessage('Duplicate Block')} @@ -172,13 +180,13 @@ export const ActionBar = memo( collaborativeBatchToggleBlockHandles([blockId]) } }} - className='hover:!text-[var(--text-inverse)] h-[23px] w-[23px] rounded-[8px] bg-[var(--surface-7)] p-0 text-[var(--text-secondary)] hover:bg-[var(--brand-secondary)]' + className={ACTION_BUTTON_STYLES} disabled={disabled} > {horizontalHandles ? ( - + ) : ( - + )} @@ -201,10 +209,10 @@ export const ActionBar = memo( ) } }} - className='hover:!text-[var(--text-inverse)] h-[23px] w-[23px] rounded-[8px] bg-[var(--surface-7)] p-0 text-[var(--text-secondary)] hover:bg-[var(--brand-secondary)]' + className={ACTION_BUTTON_STYLES} disabled={disabled || !userPermissions.canEdit} > - + {getTooltipMessage('Remove from Subflow')} @@ -221,10 +229,10 @@ export const ActionBar = memo( collaborativeBatchRemoveBlocks([blockId]) } }} - className='hover:!text-[var(--text-inverse)] h-[23px] w-[23px] rounded-[8px] bg-[var(--surface-7)] p-0 text-[var(--text-secondary)] hover:bg-[var(--brand-secondary)]' + className={ACTION_BUTTON_STYLES} disabled={disabled} > - + {getTooltipMessage('Delete Block')} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index b98eb7d390..761b5c6182 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -921,7 +921,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({ ref={contentRef} onClick={handleClick} className={cn( - 'relative z-[20] w-[250px] cursor-default select-none rounded-[8px] border border-[var(--border-1)] bg-[var(--surface-2)]' + 'workflow-drag-handle relative z-[20] w-[250px] cursor-grab select-none rounded-[8px] border border-[var(--border-1)] bg-[var(--surface-2)] [&:active]:cursor-grabbing' )} > {isPending && ( @@ -957,12 +957,9 @@ export const WorkflowBlock = memo(function WorkflowBlock({
{ - e.stopPropagation() - }} >
s.showActionBar) + const updateSetting = useUpdateGeneralSetting() + const isTerminalResizing = useTerminalStore((state) => state.isResizing) + + const { activeWorkflowId } = useWorkflowRegistry() + const { data: session } = useSession() + const userId = session?.user?.id || 'unknown' + const stacks = useUndoRedoStore((s) => s.stacks) + const key = activeWorkflowId && userId ? `${activeWorkflowId}:${userId}` : '' + const stack = (key && stacks[key]) || { undo: [], redo: [] } + const canUndo = stack.undo.length > 0 + const canRedo = stack.redo.length > 0 + + const handleFitToView = useCallback(() => { + fitViewToBounds({ padding: 0.1, duration: 300 }) + }, [fitViewToBounds]) + + useRegisterGlobalCommands([ + createCommand({ + id: 'fit-to-view', + handler: handleFitToView, + }), + ]) + + const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null) + const [isCanvasModeOpen, setIsCanvasModeOpen] = useState(false) + const menuRef = useRef(null) + + const handleContextMenu = (e: React.MouseEvent) => { + e.preventDefault() + setContextMenu({ x: e.clientX, y: e.clientY }) + } + + const handleHide = async () => { + try { + await updateSetting.mutateAsync({ key: 'showActionBar', value: false }) + } catch (error) { + logger.error('Failed to hide workflow controls', error) + } finally { + setContextMenu(null) + } + } + + if (!showWorkflowControls) { + return null + } + + return ( + <> +
+ {/* Canvas Mode Selector */} + + + +
+ + + + +
+
+ {mode === 'hand' ? 'Mover' : 'Pointer'} +
+ + { + setMode('cursor') + setIsCanvasModeOpen(false) + }} + > + + Pointer + + { + setMode('hand') + setIsCanvasModeOpen(false) + }} + > + + Mover + + +
+ +
+ + + + + + + Undo + + + + + + + + + Redo + + + +
+ + + + + + + Fit to View + + +
+ + !open && setContextMenu(null)} + variant='secondary' + size='sm' + colorScheme='inverted' + > + + + Hide canvas controls + + + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-auto-layout.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-auto-layout.ts index 7c28b60942..c972fe86c4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-auto-layout.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-auto-layout.ts @@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger' import { useReactFlow } from 'reactflow' import type { AutoLayoutOptions } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/auto-layout-utils' import { applyAutoLayoutAndUpdateStore as applyAutoLayoutStandalone } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/auto-layout-utils' +import { useCanvasViewport } from '@/hooks/use-canvas-viewport' export type { AutoLayoutOptions } @@ -16,7 +17,8 @@ const logger = createLogger('useAutoLayout') * Note: This hook requires a ReactFlowProvider ancestor. */ export function useAutoLayout(workflowId: string | null) { - const { fitView } = useReactFlow() + const reactFlowInstance = useReactFlow() + const { fitViewToBounds } = useCanvasViewport(reactFlowInstance) const applyAutoLayoutAndUpdateStore = useCallback( async (options: AutoLayoutOptions = {}) => { @@ -38,7 +40,7 @@ export function useAutoLayout(workflowId: string | null) { if (result.success) { logger.info('Auto layout completed successfully') requestAnimationFrame(() => { - fitView({ padding: 0.8, duration: 600 }) + fitViewToBounds({ padding: 0.15, duration: 600 }) }) } else { logger.error('Auto layout failed:', result.error) @@ -52,7 +54,7 @@ export function useAutoLayout(workflowId: string | null) { error: error instanceof Error ? error.message : 'Unknown error', } } - }, [applyAutoLayoutAndUpdateStore, fitView]) + }, [applyAutoLayoutAndUpdateStore, fitViewToBounds]) return { applyAutoLayoutAndUpdateStore, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-canvas-context-menu.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-canvas-context-menu.ts index be4bc6bdfc..fec22a0970 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-canvas-context-menu.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-canvas-context-menu.ts @@ -1,36 +1,28 @@ import { useCallback, useEffect, useRef, useState } from 'react' import type { Node } from 'reactflow' import type { BlockState } from '@/stores/workflows/workflow/types' -import type { ContextMenuBlockInfo, ContextMenuPosition } from '../components/context-menu/types' +import type { BlockInfo } from '../components/block-menu' type MenuType = 'block' | 'pane' | null interface UseCanvasContextMenuProps { - /** Current blocks from workflow store */ blocks: Record - /** Function to get nodes from ReactFlow */ getNodes: () => Node[] } /** * Hook for managing workflow canvas context menus. - * - * Handles: - * - Right-click event handling for blocks and pane - * - Menu open/close state for both menu types - * - Click-outside detection to close menus - * - Selected block info extraction for multi-selection support + * Handles right-click events, menu state, click-outside detection, and block info extraction. */ export function useCanvasContextMenu({ blocks, getNodes }: UseCanvasContextMenuProps) { const [activeMenu, setActiveMenu] = useState(null) - const [position, setPosition] = useState({ x: 0, y: 0 }) - const [selectedBlocks, setSelectedBlocks] = useState([]) + const [position, setPosition] = useState({ x: 0, y: 0 }) + const [selectedBlocks, setSelectedBlocks] = useState([]) const menuRef = useRef(null) - /** Converts nodes to block info for context menu */ const nodesToBlockInfos = useCallback( - (nodes: Node[]): ContextMenuBlockInfo[] => + (nodes: Node[]): BlockInfo[] => nodes.map((n) => { const block = blocks[n.id] const parentId = block?.data?.parentId @@ -47,9 +39,6 @@ export function useCanvasContextMenu({ blocks, getNodes }: UseCanvasContextMenuP [blocks] ) - /** - * Handle right-click on a node (block) - */ const handleNodeContextMenu = useCallback( (event: React.MouseEvent, node: Node) => { event.preventDefault() @@ -65,9 +54,6 @@ export function useCanvasContextMenu({ blocks, getNodes }: UseCanvasContextMenuP [getNodes, nodesToBlockInfos] ) - /** - * Handle right-click on the pane (empty canvas area) - */ const handlePaneContextMenu = useCallback((event: React.MouseEvent) => { event.preventDefault() event.stopPropagation() @@ -77,9 +63,6 @@ export function useCanvasContextMenu({ blocks, getNodes }: UseCanvasContextMenuP setActiveMenu('pane') }, []) - /** - * Handle right-click on a selection (multiple selected nodes) - */ const handleSelectionContextMenu = useCallback( (event: React.MouseEvent) => { event.preventDefault() @@ -94,16 +77,10 @@ export function useCanvasContextMenu({ blocks, getNodes }: UseCanvasContextMenuP [getNodes, nodesToBlockInfos] ) - /** - * Close the active context menu - */ const closeMenu = useCallback(() => { setActiveMenu(null) }, []) - /** - * Handle clicks outside the menu to close it - */ useEffect(() => { if (!activeMenu) return @@ -123,9 +100,6 @@ export function useCanvasContextMenu({ blocks, getNodes }: UseCanvasContextMenuP } }, [activeMenu, closeMenu]) - /** - * Close menu on scroll or zoom to prevent menu from being positioned incorrectly - */ useEffect(() => { if (!activeMenu) return @@ -139,23 +113,14 @@ export function useCanvasContextMenu({ blocks, getNodes }: UseCanvasContextMenuP }, [activeMenu, closeMenu]) return { - /** Whether the block context menu is open */ isBlockMenuOpen: activeMenu === 'block', - /** Whether the pane context menu is open */ isPaneMenuOpen: activeMenu === 'pane', - /** Position for the context menu */ position, - /** Ref for the menu element */ menuRef, - /** Selected blocks info for multi-selection actions */ selectedBlocks, - /** Handler for ReactFlow onNodeContextMenu */ handleNodeContextMenu, - /** Handler for ReactFlow onPaneContextMenu */ handlePaneContextMenu, - /** Handler for ReactFlow onSelectionContextMenu */ handleSelectionContextMenu, - /** Close the active context menu */ closeMenu, } } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 569ccc8497..358106d3ae 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -31,16 +31,15 @@ import { SubflowNodeComponent, Terminal, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components' -import { - BlockContextMenu, - PaneContextMenu, -} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/context-menu' +import { BlockMenu } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu' +import { CanvasMenu } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/canvas-menu' import { Cursors } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/cursors/cursors' import { ErrorBoundary } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/error/index' import { NoteBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block' import type { SubflowNodeData } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node' import { TrainingModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/training-modal/training-modal' import { WorkflowBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block' +import { WorkflowControls } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-controls/workflow-controls' import { WorkflowEdge } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge' import { clearDragHighlights, @@ -63,9 +62,11 @@ import { useSocket } from '@/app/workspace/providers/socket-provider' import { getBlock } from '@/blocks' import { isAnnotationOnlyBlock } from '@/executor/constants' import { useWorkspaceEnvironment } from '@/hooks/queries/environment' +import { useCanvasViewport } from '@/hooks/use-canvas-viewport' import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { usePermissionConfig } from '@/hooks/use-permission-config' import { useStreamCleanup } from '@/hooks/use-stream-cleanup' +import { useCanvasModeStore } from '@/stores/canvas-mode' import { useChatStore } from '@/stores/chat/store' import { useCopilotTrainingStore } from '@/stores/copilot-training/store' import { useExecutionStore } from '@/stores/execution' @@ -210,9 +211,9 @@ const WorkflowContent = React.memo(() => { const [isCanvasReady, setIsCanvasReady] = useState(false) const [potentialParentId, setPotentialParentId] = useState(null) const [selectedEdges, setSelectedEdges] = useState(new Map()) - const [isShiftPressed, setIsShiftPressed] = useState(false) - const [isSelectionDragActive, setIsSelectionDragActive] = useState(false) const [isErrorConnectionDrag, setIsErrorConnectionDrag] = useState(false) + const canvasMode = useCanvasModeStore((state) => state.mode) + const isHandMode = canvasMode === 'hand' const [oauthModal, setOauthModal] = useState<{ provider: OAuthProvider serviceId: string @@ -223,7 +224,9 @@ const WorkflowContent = React.memo(() => { const params = useParams() const router = useRouter() - const { screenToFlowPosition, getNodes, setNodes, fitView, getIntersectingNodes } = useReactFlow() + const reactFlowInstance = useReactFlow() + const { screenToFlowPosition, getNodes, setNodes, getIntersectingNodes } = reactFlowInstance + const { fitViewToBounds } = useCanvasViewport(reactFlowInstance) const { emitCursorUpdate } = useSocket() const workspaceId = params.workspaceId as string @@ -1512,10 +1515,10 @@ const WorkflowContent = React.memo(() => { foundNodes: changedNodes.length, }) requestAnimationFrame(() => { - fitView({ + fitViewToBounds({ nodes: changedNodes, duration: 600, - padding: 0.3, + padding: 0.1, minZoom: 0.5, maxZoom: 1.0, }) @@ -1523,18 +1526,18 @@ const WorkflowContent = React.memo(() => { } else { logger.info('Diff ready - no changed nodes found, fitting all') requestAnimationFrame(() => { - fitView({ padding: 0.3, duration: 600 }) + fitViewToBounds({ padding: 0.1, duration: 600 }) }) } } else { logger.info('Diff ready - no changed blocks, fitting all') requestAnimationFrame(() => { - fitView({ padding: 0.3, duration: 600 }) + fitViewToBounds({ padding: 0.1, duration: 600 }) }) } } prevDiffReadyRef.current = isDiffReady - }, [isDiffReady, diffAnalysis, fitView, getNodes]) + }, [isDiffReady, diffAnalysis, fitViewToBounds, getNodes]) /** Displays trigger warning notifications. */ useEffect(() => { @@ -1926,47 +1929,6 @@ const WorkflowContent = React.memo(() => { // Local state for nodes - allows smooth drag without store updates on every frame const [displayNodes, setDisplayNodes] = useState([]) - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Shift') setIsShiftPressed(true) - } - const handleKeyUp = (e: KeyboardEvent) => { - if (e.key === 'Shift') setIsShiftPressed(false) - } - const handleFocusLoss = () => { - setIsShiftPressed(false) - setIsSelectionDragActive(false) - } - const handleVisibilityChange = () => { - if (document.hidden) { - handleFocusLoss() - } - } - - window.addEventListener('keydown', handleKeyDown) - window.addEventListener('keyup', handleKeyUp) - window.addEventListener('blur', handleFocusLoss) - document.addEventListener('visibilitychange', handleVisibilityChange) - - return () => { - window.removeEventListener('keydown', handleKeyDown) - window.removeEventListener('keyup', handleKeyUp) - window.removeEventListener('blur', handleFocusLoss) - document.removeEventListener('visibilitychange', handleVisibilityChange) - } - }, []) - - useEffect(() => { - if (isShiftPressed) { - document.body.style.userSelect = 'none' - } else { - document.body.style.userSelect = '' - } - return () => { - document.body.style.userSelect = '' - } - }, [isShiftPressed]) - useEffect(() => { // Check for pending selection (from paste/duplicate), otherwise preserve existing selection const pendingSelection = pendingSelectionRef.current @@ -2867,19 +2829,19 @@ const WorkflowContent = React.memo(() => { ] ) - // Lock selection mode when selection drag starts (captures Shift state at drag start) - const onSelectionStart = useCallback(() => { - if (isShiftPressed) { - setIsSelectionDragActive(true) - } - }, [isShiftPressed]) + // // Lock selection mode when selection drag starts (captures Shift state at drag start) + // const onSelectionStart = useCallback(() => { + // if (isShiftPressed) { + // setIsSelectionDragActive(true) + // } + // }, [isShiftPressed]) - const onSelectionEnd = useCallback(() => { - requestAnimationFrame(() => { - setIsSelectionDragActive(false) - setDisplayNodes((nodes) => resolveParentChildSelectionConflicts(nodes, blocks)) - }) - }, [blocks]) + // const onSelectionEnd = useCallback(() => { + // requestAnimationFrame(() => { + // setIsSelectionDragActive(false) + // setDisplayNodes((nodes) => resolveParentChildSelectionConflicts(nodes, blocks)) + // }) + // }, [blocks]) /** Captures initial positions when selection drag starts (for marquee-selected nodes). */ const onSelectionDragStart = useCallback( @@ -3038,7 +3000,6 @@ const WorkflowContent = React.memo(() => { const onSelectionDragStop = useCallback( (_event: React.MouseEvent, nodes: any[]) => { - requestAnimationFrame(() => setIsSelectionDragActive(false)) clearDragHighlights() if (nodes.length === 0) return @@ -3367,11 +3328,9 @@ const WorkflowContent = React.memo(() => { onPointerMove={handleCanvasPointerMove} onPointerLeave={handleCanvasPointerLeave} elementsSelectable={true} - selectionOnDrag={isShiftPressed || isSelectionDragActive} + selectionOnDrag={!isHandMode} selectionMode={SelectionMode.Partial} - panOnDrag={isShiftPressed || isSelectionDragActive ? false : [0, 1]} - onSelectionStart={onSelectionStart} - onSelectionEnd={onSelectionEnd} + panOnDrag={isHandMode ? [0, 1] : false} multiSelectionKeyCode={['Meta', 'Control', 'Shift']} nodesConnectable={effectivePermissions.canEdit} nodesDraggable={effectivePermissions.canEdit} @@ -3379,7 +3338,7 @@ const WorkflowContent = React.memo(() => { noWheelClassName='allow-scroll' edgesFocusable={true} edgesUpdatable={effectivePermissions.canEdit} - className={`workflow-container h-full transition-opacity duration-150 ${reactFlowStyles} ${isCanvasReady ? 'opacity-100' : 'opacity-0'}`} + className={`workflow-container h-full transition-opacity duration-150 ${reactFlowStyles} ${isCanvasReady ? 'opacity-100' : 'opacity-0'} ${isHandMode ? 'canvas-mode-hand' : 'canvas-mode-cursor'}`} onNodeDrag={effectivePermissions.canEdit ? onNodeDrag : undefined} onNodeDragStop={effectivePermissions.canEdit ? onNodeDragStop : undefined} onSelectionDragStart={effectivePermissions.canEdit ? onSelectionDragStart : undefined} @@ -3398,12 +3357,14 @@ const WorkflowContent = React.memo(() => { + + {/* Context Menus */} - { disableEdit={!effectivePermissions.canEdit} /> - { onPaste={handleContextPaste} onAddBlock={handleContextAddBlock} onAutoLayout={handleAutoLayout} + onFitToView={() => fitViewToBounds({ padding: 0.1, duration: 300 })} onOpenLogs={handleContextOpenLogs} onToggleVariables={handleContextToggleVariables} onToggleChat={handleContextToggleChat} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/general/general.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/general/general.tsx index 3e78cf5185..6fbda0207c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/general/general.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/general/general.tsx @@ -87,6 +87,12 @@ function GeneralSkeleton() {
+ {/* Show canvas controls row */} +
+ + +
+ {/* Telemetry row */}
@@ -310,6 +316,12 @@ export function General({ onOpenChange }: GeneralProps) { } } + const handleShowActionBarChange = async (checked: boolean) => { + if (checked !== settings?.showActionBar && !updateSetting.isPending) { + await updateSetting.mutateAsync({ key: 'showActionBar', value: checked }) + } + } + const handleTrainingControlsChange = async (checked: boolean) => { if (checked !== settings?.showTrainingControls && !updateSetting.isPending) { await updateSetting.mutateAsync({ key: 'showTrainingControls', value: checked }) @@ -519,6 +531,15 @@ export function General({ onOpenChange }: GeneralProps) {
+
+ + +
+
(
- - - + + +
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/invite-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/invite-modal.tsx index bd37fe4f18..896b48a502 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/invite-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/invite-modal.tsx @@ -194,6 +194,7 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr const matches = text.match(emailRegex) || [] return [...new Set(matches.map((e) => e.toLowerCase()))] }, + tooltip: 'Upload emails', }), [userPerms.canAdmin] ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx index 9dc273c4ea..a188e9bf84 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx @@ -298,7 +298,7 @@ export function WorkspaceHeader({ )}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index 7566279404..a97638a975 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -489,7 +489,7 @@ export function Sidebar() { <> {isCollapsed ? ( /* Floating collapsed header - minimal pill showing workspace name and expand toggle */ -
+
/** Extract values from file content. Each extracted value will be passed to onAdd. */ extractValues?: (text: string) => string[] + /** Tooltip text for the file input button */ + tooltip?: string } /** @@ -480,17 +483,24 @@ const TagInput = React.forwardRef( )}
{fileInputEnabled && !disabled && ( - + + + + + + {fileInputOptions?.tooltip ?? 'Upload file'} + + )}
) diff --git a/apps/sim/components/emcn/components/tooltip/tooltip.tsx b/apps/sim/components/emcn/components/tooltip/tooltip.tsx index 6cb91b1eee..af1b14309a 100644 --- a/apps/sim/components/emcn/components/tooltip/tooltip.tsx +++ b/apps/sim/components/emcn/components/tooltip/tooltip.tsx @@ -7,7 +7,12 @@ import { cn } from '@/lib/core/utils/cn' /** * Tooltip provider component that must wrap your app or tooltip usage area. */ -const Provider = TooltipPrimitive.Provider +const Provider = ({ + delayDuration = 400, + ...props +}: React.ComponentPropsWithoutRef) => ( + +) /** * Root tooltip component that wraps trigger and content. diff --git a/apps/sim/components/emcn/icons/cursor.tsx b/apps/sim/components/emcn/icons/cursor.tsx new file mode 100644 index 0000000000..313731f0fd --- /dev/null +++ b/apps/sim/components/emcn/icons/cursor.tsx @@ -0,0 +1,22 @@ +import type { SVGProps } from 'react' + +export function Cursor(props: SVGProps) { + return ( + + + + ) +} diff --git a/apps/sim/components/emcn/icons/expand.tsx b/apps/sim/components/emcn/icons/expand.tsx new file mode 100644 index 0000000000..c6939685b4 --- /dev/null +++ b/apps/sim/components/emcn/icons/expand.tsx @@ -0,0 +1,43 @@ +import type { SVGProps } from 'react' + +export function Expand(props: SVGProps) { + return ( + + + + + + + ) +} diff --git a/apps/sim/components/emcn/icons/hand.tsx b/apps/sim/components/emcn/icons/hand.tsx new file mode 100644 index 0000000000..9197f1e10b --- /dev/null +++ b/apps/sim/components/emcn/icons/hand.tsx @@ -0,0 +1,43 @@ +import type { SVGProps } from 'react' + +export function Hand(props: SVGProps) { + return ( + + + + + + + ) +} diff --git a/apps/sim/components/emcn/icons/index.ts b/apps/sim/components/emcn/icons/index.ts index 5288b6f3d2..c8ace84148 100644 --- a/apps/sim/components/emcn/icons/index.ts +++ b/apps/sim/components/emcn/icons/index.ts @@ -4,12 +4,15 @@ export { Card } from './card' export { ChevronDown } from './chevron-down' export { Connections } from './connections' export { Copy } from './copy' +export { Cursor } from './cursor' export { DocumentAttachment } from './document-attachment' export { Download } from './download' export { Duplicate } from './duplicate' +export { Expand } from './expand' export { Eye } from './eye' export { FolderCode } from './folder-code' export { FolderPlus } from './folder-plus' +export { Hand } from './hand' export { HexSimple } from './hex-simple' export { Key } from './key' export { Layout } from './layout' diff --git a/apps/sim/components/emcn/icons/redo.tsx b/apps/sim/components/emcn/icons/redo.tsx index f06730e6c7..f5c57f0fa5 100644 --- a/apps/sim/components/emcn/icons/redo.tsx +++ b/apps/sim/components/emcn/icons/redo.tsx @@ -17,14 +17,14 @@ export function Redo(props: SVGProps) { diff --git a/apps/sim/components/emcn/icons/undo.tsx b/apps/sim/components/emcn/icons/undo.tsx index a5114115a1..3e0f4fe622 100644 --- a/apps/sim/components/emcn/icons/undo.tsx +++ b/apps/sim/components/emcn/icons/undo.tsx @@ -17,14 +17,14 @@ export function Undo(props: SVGProps) { diff --git a/apps/sim/components/emcn/icons/zoom-in.tsx b/apps/sim/components/emcn/icons/zoom-in.tsx index a38844a6f7..12561e39c8 100644 --- a/apps/sim/components/emcn/icons/zoom-in.tsx +++ b/apps/sim/components/emcn/icons/zoom-in.tsx @@ -19,28 +19,28 @@ export function ZoomIn(props: SVGProps) { cy='5' r='3.5' stroke='currentColor' - strokeWidth='1.5' + strokeWidth='1' strokeLinecap='round' strokeLinejoin='round' /> diff --git a/apps/sim/components/emcn/icons/zoom-out.tsx b/apps/sim/components/emcn/icons/zoom-out.tsx index 2cab464391..aca3a24826 100644 --- a/apps/sim/components/emcn/icons/zoom-out.tsx +++ b/apps/sim/components/emcn/icons/zoom-out.tsx @@ -19,21 +19,21 @@ export function ZoomOut(props: SVGProps) { cy='5' r='3.5' stroke='currentColor' - strokeWidth='1.5' + strokeWidth='1' strokeLinecap='round' strokeLinejoin='round' /> diff --git a/apps/sim/hooks/queries/general-settings.ts b/apps/sim/hooks/queries/general-settings.ts index c34705b50d..1fc8a5fd01 100644 --- a/apps/sim/hooks/queries/general-settings.ts +++ b/apps/sim/hooks/queries/general-settings.ts @@ -25,6 +25,7 @@ export interface GeneralSettings { billingUsageNotificationsEnabled: boolean errorNotificationsEnabled: boolean snapToGridSize: number + showActionBar: boolean } /** @@ -48,6 +49,7 @@ async function fetchGeneralSettings(): Promise { billingUsageNotificationsEnabled: data.billingUsageNotificationsEnabled ?? true, errorNotificationsEnabled: data.errorNotificationsEnabled ?? true, snapToGridSize: data.snapToGridSize ?? 0, + showActionBar: data.showActionBar ?? true, } } @@ -69,6 +71,7 @@ function syncSettingsToZustand(settings: GeneralSettings) { isBillingUsageNotificationsEnabled: settings.billingUsageNotificationsEnabled, isErrorNotificationsEnabled: settings.errorNotificationsEnabled, snapToGridSize: settings.snapToGridSize, + showActionBar: settings.showActionBar, } const hasChanges = Object.entries(newSettings).some( diff --git a/apps/sim/hooks/use-canvas-viewport.ts b/apps/sim/hooks/use-canvas-viewport.ts new file mode 100644 index 0000000000..ad570d8e02 --- /dev/null +++ b/apps/sim/hooks/use-canvas-viewport.ts @@ -0,0 +1,185 @@ +import { useCallback } from 'react' +import type { Node, ReactFlowInstance } from 'reactflow' + +interface VisibleBounds { + width: number + height: number + offsetLeft: number + offsetRight: number + offsetBottom: number +} + +/** + * Gets the visible canvas bounds accounting for sidebar, terminal, and panel overlays. + * Works correctly regardless of whether the ReactFlow container extends under the sidebar or not. + */ +function getVisibleCanvasBounds(): VisibleBounds { + const style = getComputedStyle(document.documentElement) + + const sidebarWidth = Number.parseInt(style.getPropertyValue('--sidebar-width') || '0', 10) + const terminalHeight = Number.parseInt(style.getPropertyValue('--terminal-height') || '0', 10) + const panelWidth = Number.parseInt(style.getPropertyValue('--panel-width') || '0', 10) + + const flowContainer = document.querySelector('.react-flow') + if (!flowContainer) { + return { + width: window.innerWidth - sidebarWidth - panelWidth, + height: window.innerHeight - terminalHeight, + offsetLeft: sidebarWidth, + offsetRight: panelWidth, + offsetBottom: terminalHeight, + } + } + + const rect = flowContainer.getBoundingClientRect() + + // Calculate actual visible area in screen coordinates + // This works regardless of whether the container extends under overlays + const visibleLeft = Math.max(rect.left, sidebarWidth) + const visibleRight = Math.min(rect.right, window.innerWidth - panelWidth) + const visibleBottom = Math.min(rect.bottom, window.innerHeight - terminalHeight) + + // Calculate visible dimensions and offsets relative to the container + const visibleWidth = Math.max(0, visibleRight - visibleLeft) + const visibleHeight = Math.max(0, visibleBottom - rect.top) + + return { + width: visibleWidth, + height: visibleHeight, + offsetLeft: visibleLeft - rect.left, + offsetRight: rect.right - visibleRight, + offsetBottom: rect.bottom - visibleBottom, + } +} + +/** + * Gets the center of the visible canvas in screen coordinates. + */ +function getVisibleCanvasCenter(): { x: number; y: number } { + const style = getComputedStyle(document.documentElement) + const sidebarWidth = Number.parseInt(style.getPropertyValue('--sidebar-width') || '0', 10) + const panelWidth = Number.parseInt(style.getPropertyValue('--panel-width') || '0', 10) + const terminalHeight = Number.parseInt(style.getPropertyValue('--terminal-height') || '0', 10) + + const flowContainer = document.querySelector('.react-flow') + if (!flowContainer) { + const visibleWidth = window.innerWidth - sidebarWidth - panelWidth + const visibleHeight = window.innerHeight - terminalHeight + return { + x: sidebarWidth + visibleWidth / 2, + y: visibleHeight / 2, + } + } + + const rect = flowContainer.getBoundingClientRect() + + // Calculate actual visible area in screen coordinates + const visibleLeft = Math.max(rect.left, sidebarWidth) + const visibleRight = Math.min(rect.right, window.innerWidth - panelWidth) + const visibleBottom = Math.min(rect.bottom, window.innerHeight - terminalHeight) + + return { + x: (visibleLeft + visibleRight) / 2, + y: (rect.top + visibleBottom) / 2, + } +} + +interface FitViewToBoundsOptions { + padding?: number + maxZoom?: number + minZoom?: number + duration?: number + nodes?: Node[] +} + +/** + * Hook providing canvas viewport utilities that account for sidebar, panel, and terminal overlays. + */ +export function useCanvasViewport(reactFlowInstance: ReactFlowInstance | null) { + /** + * Gets the center of the visible canvas in flow coordinates. + */ + const getViewportCenter = useCallback(() => { + if (!reactFlowInstance) { + return { x: 0, y: 0 } + } + + const center = getVisibleCanvasCenter() + return reactFlowInstance.screenToFlowPosition(center) + }, [reactFlowInstance]) + + /** + * Fits the view to show all nodes within the visible canvas bounds, + * accounting for sidebar, panel, and terminal overlays. + * @param padding - Fraction of viewport to leave as margin (0.1 = 10% on each side) + */ + const fitViewToBounds = useCallback( + (options: FitViewToBoundsOptions = {}) => { + if (!reactFlowInstance) return + + const { + padding = 0.1, + maxZoom = 1, + minZoom = 0.1, + duration = 300, + nodes: targetNodes, + } = options + + const nodes = targetNodes ?? reactFlowInstance.getNodes() + if (nodes.length === 0) { + return + } + + const bounds = getVisibleCanvasBounds() + + // Calculate node bounds + let minX = Number.POSITIVE_INFINITY + let minY = Number.POSITIVE_INFINITY + let maxX = Number.NEGATIVE_INFINITY + let maxY = Number.NEGATIVE_INFINITY + + nodes.forEach((node) => { + const nodeWidth = node.width ?? 200 + const nodeHeight = node.height ?? 100 + + minX = Math.min(minX, node.position.x) + minY = Math.min(minY, node.position.y) + maxX = Math.max(maxX, node.position.x + nodeWidth) + maxY = Math.max(maxY, node.position.y + nodeHeight) + }) + + const contentWidth = maxX - minX + const contentHeight = maxY - minY + + // Apply padding as fraction of viewport (matches ReactFlow's fitView behavior) + const availableWidth = bounds.width * (1 - padding * 2) + const availableHeight = bounds.height * (1 - padding * 2) + + // Calculate zoom to fit content in available area + const zoomX = availableWidth / contentWidth + const zoomY = availableHeight / contentHeight + const zoom = Math.max(minZoom, Math.min(maxZoom, Math.min(zoomX, zoomY))) + + // Calculate center of content in flow coordinates + const contentCenterX = minX + contentWidth / 2 + const contentCenterY = minY + contentHeight / 2 + + // Calculate viewport position to center content in visible area + // Account for sidebar offset on the left + const visibleCenterX = bounds.offsetLeft + bounds.width / 2 + const visibleCenterY = bounds.height / 2 + + const x = visibleCenterX - contentCenterX * zoom + const y = visibleCenterY - contentCenterY * zoom + + reactFlowInstance.setViewport({ x, y, zoom }, { duration }) + }, + [reactFlowInstance] + ) + + return { + getViewportCenter, + fitViewToBounds, + getVisibleCanvasBounds, + } +} diff --git a/apps/sim/stores/canvas-mode/index.ts b/apps/sim/stores/canvas-mode/index.ts new file mode 100644 index 0000000000..244578e36b --- /dev/null +++ b/apps/sim/stores/canvas-mode/index.ts @@ -0,0 +1,2 @@ +export type { CanvasMode } from './store' +export { useCanvasModeStore } from './store' diff --git a/apps/sim/stores/canvas-mode/store.ts b/apps/sim/stores/canvas-mode/store.ts new file mode 100644 index 0000000000..be0787c9fd --- /dev/null +++ b/apps/sim/stores/canvas-mode/store.ts @@ -0,0 +1,22 @@ +import { create } from 'zustand' +import { devtools, persist } from 'zustand/middleware' + +export type CanvasMode = 'cursor' | 'hand' + +interface CanvasModeState { + mode: CanvasMode + setMode: (mode: CanvasMode) => void +} + +export const useCanvasModeStore = create()( + devtools( + persist( + (set) => ({ + mode: 'hand', + setMode: (mode) => set({ mode }), + }), + { name: 'canvas-mode' } + ), + { name: 'canvas-mode-store' } + ) +) diff --git a/apps/sim/stores/settings/general/store.ts b/apps/sim/stores/settings/general/store.ts index 9ef9a53363..f1bc3362b4 100644 --- a/apps/sim/stores/settings/general/store.ts +++ b/apps/sim/stores/settings/general/store.ts @@ -9,11 +9,12 @@ const initialState: General = { isAutoConnectEnabled: true, showTrainingControls: false, superUserModeEnabled: true, - theme: 'system', + theme: 'dark', telemetryEnabled: true, isBillingUsageNotificationsEnabled: true, isErrorNotificationsEnabled: true, snapToGridSize: 0, + showActionBar: true, } export const useGeneralStore = create()( diff --git a/apps/sim/stores/settings/general/types.ts b/apps/sim/stores/settings/general/types.ts index ab2cd76f1f..d4b11e4ae6 100644 --- a/apps/sim/stores/settings/general/types.ts +++ b/apps/sim/stores/settings/general/types.ts @@ -7,6 +7,7 @@ export interface General { isBillingUsageNotificationsEnabled: boolean isErrorNotificationsEnabled: boolean snapToGridSize: number + showActionBar: boolean } export interface GeneralStore extends General { @@ -23,4 +24,5 @@ export type UserSettings = { isBillingUsageNotificationsEnabled: boolean errorNotificationsEnabled: boolean snapToGridSize: number + showActionBar: boolean } diff --git a/packages/db/migrations/0142_short_nicolaos.sql b/packages/db/migrations/0142_short_nicolaos.sql new file mode 100644 index 0000000000..c83464b91c --- /dev/null +++ b/packages/db/migrations/0142_short_nicolaos.sql @@ -0,0 +1,2 @@ +ALTER TABLE "settings" ALTER COLUMN "theme" SET DEFAULT 'dark';--> statement-breakpoint +ALTER TABLE "settings" ADD COLUMN "show_action_bar" boolean DEFAULT true NOT NULL; \ No newline at end of file diff --git a/packages/db/migrations/meta/0142_snapshot.json b/packages/db/migrations/meta/0142_snapshot.json new file mode 100644 index 0000000000..6d13e8235d --- /dev/null +++ b/packages/db/migrations/meta/0142_snapshot.json @@ -0,0 +1,10274 @@ +{ + "id": "b0ccee95-4d64-421e-8631-65984c6b80c7", + "prevId": "cffb4dda-dfcc-474d-a6d8-fdb2b749edaf", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.a2a_agent": { + "name": "a2a_agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "authentication": { + "name": "authentication", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "signatures": { + "name": "signatures", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_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": { + "a2a_agent_workspace_id_idx": { + "name": "a2a_agent_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workflow_id_idx": { + "name": "a2a_agent_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_created_by_idx": { + "name": "a2a_agent_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_workflow_unique": { + "name": "a2a_agent_workspace_workflow_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_agent_workspace_id_workspace_id_fk": { + "name": "a2a_agent_workspace_id_workspace_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_workflow_id_workflow_id_fk": { + "name": "a2a_agent_workflow_id_workflow_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_created_by_user_id_fk": { + "name": "a2a_agent_created_by_user_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_push_notification_config": { + "name": "a2a_push_notification_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_schemes": { + "name": "auth_schemes", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "auth_credentials": { + "name": "auth_credentials", + "type": "text", + "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": { + "a2a_push_notification_config_task_id_idx": { + "name": "a2a_push_notification_config_task_id_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_push_notification_config_task_unique": { + "name": "a2a_push_notification_config_task_unique", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_push_notification_config_task_id_a2a_task_id_fk": { + "name": "a2a_push_notification_config_task_id_a2a_task_id_fk", + "tableFrom": "a2a_push_notification_config", + "tableTo": "a2a_task", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_task": { + "name": "a2a_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "a2a_task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'submitted'" + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "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()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "a2a_task_agent_id_idx": { + "name": "a2a_task_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_session_id_idx": { + "name": "a2a_task_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_status_idx": { + "name": "a2a_task_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_execution_id_idx": { + "name": "a2a_task_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_created_at_idx": { + "name": "a2a_task_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_task_agent_id_a2a_agent_id_fk": { + "name": "a2a_task_agent_id_a2a_agent_id_fk", + "tableFrom": "a2a_task", + "tableTo": "a2a_agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "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": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_user_provider_unique": { + "name": "account_user_provider_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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 + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "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": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "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 + }, + "identifier": { + "name": "identifier", + "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": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "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.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "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": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "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": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set": { + "name": "credential_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "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": { + "credential_set_organization_id_idx": { + "name": "credential_set_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_created_by_idx": { + "name": "credential_set_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_org_name_unique": { + "name": "credential_set_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_provider_id_idx": { + "name": "credential_set_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_organization_id_organization_id_fk": { + "name": "credential_set_organization_id_organization_id_fk", + "tableFrom": "credential_set", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_created_by_user_id_fk": { + "name": "credential_set_created_by_user_id_fk", + "tableFrom": "credential_set", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_invitation": { + "name": "credential_set_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "accepted_by_user_id": { + "name": "accepted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_invitation_set_id_idx": { + "name": "credential_set_invitation_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_token_idx": { + "name": "credential_set_invitation_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_status_idx": { + "name": "credential_set_invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_expires_at_idx": { + "name": "credential_set_invitation_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_invitation_credential_set_id_credential_set_id_fk": { + "name": "credential_set_invitation_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_invited_by_user_id_fk": { + "name": "credential_set_invitation_invited_by_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_accepted_by_user_id_user_id_fk": { + "name": "credential_set_invitation_accepted_by_user_id_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_set_invitation_token_unique": { + "name": "credential_set_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_member": { + "name": "credential_set_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "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": { + "credential_set_member_set_id_idx": { + "name": "credential_set_member_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_user_id_idx": { + "name": "credential_set_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_unique": { + "name": "credential_set_member_unique", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_status_idx": { + "name": "credential_set_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_member_credential_set_id_credential_set_id_fk": { + "name": "credential_set_member_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_user_id_user_id_fk": { + "name": "credential_set_member_user_id_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_invited_by_user_id_fk": { + "name": "credential_set_member_invited_by_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "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 + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "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": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "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": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "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": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_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 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "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 + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "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_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": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "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 + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_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_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "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.form": { + "name": "form", + "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 + }, + "identifier": { + "name": "identifier", + "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": "'[]'" + }, + "show_branding": { + "name": "show_branding", + "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": { + "form_identifier_idx": { + "name": "form_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_workflow_id_idx": { + "name": "form_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_user_id_idx": { + "name": "form_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_workflow_id_workflow_id_fk": { + "name": "form_workflow_id_workflow_id_fk", + "tableFrom": "form", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "form_user_id_user_id_fk": { + "name": "form_user_id_user_id_fk", + "tableFrom": "form", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_namespace_unique": { + "name": "idempotency_key_namespace_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "namespace", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idempotency_key_namespace_idx": { + "name": "idempotency_key_namespace_idx", + "columns": [ + { + "expression": "namespace", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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\": 1, \"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": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "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_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "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": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_idx": { + "name": "mcp_servers_workspace_deleted_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "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": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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 + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "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_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_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 + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "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": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "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": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_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": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "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": { + "permission_group_organization_id_idx": { + "name": "permission_group_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_org_name_unique": { + "name": "permission_group_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_user_id_unique": { + "name": "permission_group_member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "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.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_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": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": "'dark'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "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.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.template_creators": { + "name": "template_creators", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "reference_type": { + "name": "reference_type", + "type": "template_creator_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile_image_url": { + "name": "profile_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "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": { + "template_creators_reference_idx": { + "name": "template_creators_reference_idx", + "columns": [ + { + "expression": "reference_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_reference_id_idx": { + "name": "template_creators_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_created_by_idx": { + "name": "template_creators_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_creators_created_by_user_id_fk": { + "name": "template_creators_created_by_user_id_fk", + "tableFrom": "template_creators", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.template_stars": { + "name": "template_stars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starred_at": { + "name": "starred_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_stars_user_id_idx": { + "name": "template_stars_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_id_idx": { + "name": "template_stars_template_id_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_idx": { + "name": "template_stars_user_template_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_user_idx": { + "name": "template_stars_template_user_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_starred_at_idx": { + "name": "template_stars_starred_at_idx", + "columns": [ + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_starred_at_idx": { + "name": "template_stars_template_starred_at_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_unique": { + "name": "template_stars_user_template_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_stars_user_id_user_id_fk": { + "name": "template_stars_user_id_user_id_fk", + "tableFrom": "template_stars", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "template_stars_template_id_templates_id_fk": { + "name": "template_stars_template_id_templates_id_fk", + "tableFrom": "template_stars", + "tableTo": "templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.templates": { + "name": "templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "template_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "required_credentials": { + "name": "required_credentials", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "og_image_url": { + "name": "og_image_url", + "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": { + "templates_status_idx": { + "name": "templates_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_creator_id_idx": { + "name": "templates_creator_id_idx", + "columns": [ + { + "expression": "creator_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_views_idx": { + "name": "templates_views_idx", + "columns": [ + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_stars_idx": { + "name": "templates_stars_idx", + "columns": [ + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_views_idx": { + "name": "templates_status_views_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_stars_idx": { + "name": "templates_status_stars_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_created_at_idx": { + "name": "templates_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_updated_at_idx": { + "name": "templates_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "templates_workflow_id_workflow_id_fk": { + "name": "templates_workflow_id_workflow_id_fk", + "tableFrom": "templates", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "templates_creator_id_template_creators_id_fk": { + "name": "templates_creator_id_template_creators_id_fk", + "tableFrom": "templates", + "tableTo": "template_creators", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "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 + }, + "is_super_user": { + "name": "is_super_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'20'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "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": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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 + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credential_set_id": { + "name": "credential_set_id", + "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": { + "path_idx": { + "name": "path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id": { + "name": "idx_webhook_on_workflow_id_block_id", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_credential_set_id_idx": { + "name": "webhook_credential_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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" + }, + "webhook_block_id_workflow_blocks_id_fk": { + "name": "webhook_block_id_workflow_blocks_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_blocks", + "columnsFrom": ["block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_credential_set_id_credential_set_id_fk": { + "name": "webhook_credential_set_id_credential_set_id_fk", + "tableFrom": "webhook", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "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 + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "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": "'{}'" + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_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": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_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_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 + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "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": "'{}'" + }, + "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_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "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_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "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()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "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_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_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "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_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "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_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "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 + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "last_queued_at": { + "name": "last_queued_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": { + "workflow_schedule_workflow_block_unique": { + "name": "workflow_schedule_workflow_block_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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" + }, + "workflow_schedule_block_id_workflow_blocks_id_fk": { + "name": "workflow_schedule_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_blocks", + "columnsFrom": ["block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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 + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "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": {}, + "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" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "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": { + "workspace_byok_provider_unique": { + "name": "workspace_byok_provider_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_byok_workspace_idx": { + "name": "workspace_byok_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "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": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_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 + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "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": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_files_key_unique": { + "name": "workspace_files_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "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": "workspace_invitation_status", + "typeSchema": "public", + "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'" + }, + "org_invitation_id": { + "name": "org_invitation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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_notification_delivery": { + "name": "workspace_notification_delivery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "notification_delivery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "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": { + "workspace_notification_delivery_subscription_id_idx": { + "name": "workspace_notification_delivery_subscription_id_idx", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_execution_id_idx": { + "name": "workspace_notification_delivery_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_status_idx": { + "name": "workspace_notification_delivery_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_next_attempt_idx": { + "name": "workspace_notification_delivery_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk": { + "name": "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workspace_notification_subscription", + "columnsFrom": ["subscription_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_delivery_workflow_id_workflow_id_fk": { + "name": "workspace_notification_delivery_workflow_id_workflow_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_notification_subscription": { + "name": "workspace_notification_subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notification_type": { + "name": "notification_type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workflow_ids": { + "name": "workflow_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "all_workflows": { + "name": "all_workflows", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "level_filter": { + "name": "level_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['info', 'error']::text[]" + }, + "trigger_filter": { + "name": "trigger_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['api', 'webhook', 'schedule', 'manual', 'chat']::text[]" + }, + "include_final_output": { + "name": "include_final_output", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_trace_spans": { + "name": "include_trace_spans", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_rate_limits": { + "name": "include_rate_limits", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_usage_data": { + "name": "include_usage_data", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "webhook_config": { + "name": "webhook_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "email_recipients": { + "name": "email_recipients", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "slack_config": { + "name": "slack_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "alert_config": { + "name": "alert_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_alert_at": { + "name": "last_alert_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "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": { + "workspace_notification_workspace_id_idx": { + "name": "workspace_notification_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_active_idx": { + "name": "workspace_notification_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_type_idx": { + "name": "workspace_notification_type_idx", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_subscription_workspace_id_workspace_id_fk": { + "name": "workspace_notification_subscription_workspace_id_workspace_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_subscription_created_by_user_id_fk": { + "name": "workspace_notification_subscription_created_by_user_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.a2a_task_status": { + "name": "a2a_task_status", + "schema": "public", + "values": [ + "submitted", + "working", + "input-required", + "completed", + "failed", + "canceled", + "rejected", + "auth-required", + "unknown" + ] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.credential_set_invitation_status": { + "name": "credential_set_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "expired", "cancelled"] + }, + "public.credential_set_member_status": { + "name": "credential_set_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.notification_delivery_status": { + "name": "notification_delivery_status", + "schema": "public", + "values": ["pending", "in_progress", "success", "failed"] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": ["webhook", "email", "slack"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.template_creator_type": { + "name": "template_creator_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.template_status": { + "name": "template_status", + "schema": "public", + "values": ["pending", "approved", "rejected"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": ["workflow", "wand", "copilot"] + }, + "public.workspace_invitation_status": { + "name": "workspace_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index a65e10be81..5eb585af8f 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -988,6 +988,13 @@ "when": 1768421319400, "tag": "0141_daffy_marten_broadcloak", "breakpoints": true + }, + { + "idx": 142, + "version": "7", + "when": 1768511699652, + "tag": "0142_short_nicolaos", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 78ac81829c..c88c0df929 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -451,7 +451,7 @@ export const settings = pgTable('settings', { .unique(), // One settings record per user // General settings - theme: text('theme').notNull().default('system'), + theme: text('theme').notNull().default('dark'), autoConnect: boolean('auto_connect').notNull().default(true), // Privacy settings @@ -474,6 +474,7 @@ export const settings = pgTable('settings', { // Canvas preferences snapToGridSize: integer('snap_to_grid_size').notNull().default(0), // 0 = off, 10-50 = grid size + showActionBar: boolean('show_action_bar').notNull().default(true), // Copilot preferences - maps model_id to enabled/disabled boolean copilotEnabledModels: jsonb('copilot_enabled_models').notNull().default('{}'), From a35f6eca030e1f24a6333e5960d62c8370e99844 Mon Sep 17 00:00:00 2001 From: Waleed Date: Thu, 15 Jan 2026 13:25:22 -0800 Subject: [PATCH 05/15] improvement(tools): use react query to fetch child workflow schema, avoid refetch and duplicated utils, consolidated utils and testing mocks (#2839) * improvement(tools): use react query to fetch child workflow schema, avoid refetch and duplicated utils * consolidated utils & testing mocks --- apps/sim/app/api/__test-utils__/utils.ts | 1565 ----------------- .../api/auth/forget-password/route.test.ts | 49 +- .../api/auth/oauth/connections/route.test.ts | 2 +- .../api/auth/oauth/credentials/route.test.ts | 2 +- .../api/auth/oauth/disconnect/route.test.ts | 2 +- .../app/api/auth/oauth/token/route.test.ts | 2 +- .../app/api/auth/reset-password/route.test.ts | 49 +- .../app/api/chat/[identifier]/route.test.ts | 72 +- .../app/api/copilot/api-keys/route.test.ts | 2 +- .../app/api/copilot/chat/delete/route.test.ts | 7 +- .../chat/update-messages/route.test.ts | 7 +- apps/sim/app/api/copilot/chats/route.test.ts | 2 +- .../copilot/checkpoints/revert/route.test.ts | 7 +- .../app/api/copilot/checkpoints/route.test.ts | 7 +- .../sim/app/api/copilot/confirm/route.test.ts | 7 +- .../app/api/copilot/feedback/route.test.ts | 6 +- apps/sim/app/api/copilot/stats/route.test.ts | 6 +- apps/sim/app/api/files/delete/route.test.ts | 84 +- apps/sim/app/api/files/parse/route.test.ts | 51 +- .../sim/app/api/files/presigned/route.test.ts | 104 +- .../api/files/serve/[...path]/route.test.ts | 64 +- apps/sim/app/api/files/upload/route.test.ts | 69 +- apps/sim/app/api/folders/[id]/route.test.ts | 26 +- apps/sim/app/api/folders/route.test.ts | 52 +- apps/sim/app/api/form/[identifier]/route.ts | 12 +- .../app/api/function/execute/route.test.ts | 3 +- .../[id]/documents/[documentId]/route.test.ts | 4 +- .../knowledge/[id]/documents/route.test.ts | 4 +- apps/sim/app/api/knowledge/[id]/route.test.ts | 4 +- apps/sim/app/api/knowledge/route.test.ts | 4 +- .../app/api/knowledge/search/route.test.ts | 6 +- apps/sim/app/api/tools/custom/route.test.ts | 5 +- .../api/webhooks/trigger/[path]/route.test.ts | 142 +- .../workflows/[id]/variables/route.test.ts | 20 +- .../api/workspaces/invitations/route.test.ts | 2 +- .../w/[workflowId]/components/chat/chat.tsx | 2 +- .../deploy-modal/components/a2a/a2a.tsx | 2 +- .../deploy-modal/components/mcp/mcp.tsx | 2 +- .../input-mapping/input-mapping.tsx | 159 +- .../components/tool-input/tool-input.tsx | 54 +- .../workspace-header/workspace-header.tsx | 77 +- .../sidebar/hooks/use-workspace-management.ts | 5 +- .../executor/__test-utils__/executor-mocks.ts | 929 ---------- apps/sim/executor/constants.ts | 6 +- .../executor/handlers/api/api-handler.test.ts | 2 +- .../evaluator/evaluator-handler.test.ts | 2 +- .../handlers/generic/generic-handler.test.ts | 2 +- .../handlers/router/router-handler.test.ts | 2 +- .../handlers/trigger/trigger-handler.test.ts | 2 +- .../handlers/wait/wait-handler.test.ts | 2 +- apps/sim/executor/utils/lazy-cleanup.ts | 33 +- .../sim/executor/variables/resolvers/block.ts | 2 +- apps/sim/hooks/queries/workflows.ts | 29 + apps/sim/lib/execution/files.ts | 5 +- apps/sim/lib/mcp/workflow-tool-schema.ts | 2 +- .../sim/lib/workflows/blocks/block-outputs.ts | 2 +- apps/sim/lib/workflows/input-format-utils.ts | 39 - apps/sim/lib/workflows/input-format.test.ts | 229 +++ apps/sim/lib/workflows/input-format.ts | 111 ++ .../__test-utils__/test-workflows.ts | 662 ------- apps/sim/serializer/index.test.ts | 231 +-- .../serializer/tests/dual-validation.test.ts | 45 +- .../tests/serializer.extended.test.ts | 365 ++-- apps/sim/tools/function/execute.test.ts | 12 +- apps/sim/tools/http/request.test.ts | 26 +- apps/sim/tools/params.ts | 30 +- packages/testing/package.json | 4 + packages/testing/src/builders/index.ts | 7 + .../src/builders/tool-tester.builder.ts | 208 +-- packages/testing/src/factories/index.ts | 22 + .../src/factories/tool-responses.factory.ts | 82 +- .../testing/src/factories/workflow.factory.ts | 417 +++++ packages/testing/src/index.ts | 13 + packages/testing/src/mocks/api.mock.ts | 198 +++ packages/testing/src/mocks/auth.mock.ts | 74 + packages/testing/src/mocks/blocks.mock.ts | 300 ++++ .../testing/src/mocks/executor.mock.ts | 35 +- packages/testing/src/mocks/index.ts | 29 + packages/testing/src/mocks/request.mock.ts | 59 + packages/testing/src/mocks/uuid.mock.ts | 40 + 80 files changed, 2714 insertions(+), 4295 deletions(-) delete mode 100644 apps/sim/app/api/__test-utils__/utils.ts delete mode 100644 apps/sim/executor/__test-utils__/executor-mocks.ts delete mode 100644 apps/sim/lib/workflows/input-format-utils.ts create mode 100644 apps/sim/lib/workflows/input-format.test.ts create mode 100644 apps/sim/lib/workflows/input-format.ts delete mode 100644 apps/sim/serializer/__test-utils__/test-workflows.ts rename apps/sim/tools/__test-utils__/test-tools.ts => packages/testing/src/builders/tool-tester.builder.ts (64%) rename apps/sim/tools/__test-utils__/mock-data.ts => packages/testing/src/factories/tool-responses.factory.ts (89%) create mode 100644 packages/testing/src/mocks/api.mock.ts create mode 100644 packages/testing/src/mocks/auth.mock.ts create mode 100644 packages/testing/src/mocks/blocks.mock.ts rename apps/sim/executor/__test-utils__/mock-dependencies.ts => packages/testing/src/mocks/executor.mock.ts (63%) create mode 100644 packages/testing/src/mocks/request.mock.ts create mode 100644 packages/testing/src/mocks/uuid.mock.ts diff --git a/apps/sim/app/api/__test-utils__/utils.ts b/apps/sim/app/api/__test-utils__/utils.ts deleted file mode 100644 index 3ecefb443c..0000000000 --- a/apps/sim/app/api/__test-utils__/utils.ts +++ /dev/null @@ -1,1565 +0,0 @@ -import { createMockLogger as createSimTestingMockLogger } from '@sim/testing' -import { NextRequest } from 'next/server' -import { vi } from 'vitest' - -export { createMockLogger } from '@sim/testing' - -export interface MockUser { - id: string - email: string - name?: string -} - -export interface MockAuthResult { - mockGetSession: ReturnType - mockAuthenticatedUser: (user?: MockUser) => void - mockUnauthenticated: () => void - setAuthenticated: (user?: MockUser) => void - setUnauthenticated: () => void -} - -export interface DatabaseSelectResult { - id: string - [key: string]: any -} - -export interface DatabaseInsertResult { - id: string - [key: string]: any -} - -export interface DatabaseUpdateResult { - id: string - updatedAt?: Date - [key: string]: any -} - -export interface DatabaseDeleteResult { - id: string - [key: string]: any -} - -export interface MockDatabaseOptions { - select?: { - results?: any[][] - throwError?: boolean - errorMessage?: string - } - insert?: { - results?: any[] - throwError?: boolean - errorMessage?: string - } - update?: { - results?: any[] - throwError?: boolean - errorMessage?: string - } - delete?: { - results?: any[] - throwError?: boolean - errorMessage?: string - } - transaction?: { - throwError?: boolean - errorMessage?: string - } -} - -export interface CapturedFolderValues { - name?: string - color?: string - parentId?: string | null - isExpanded?: boolean - sortOrder?: number - updatedAt?: Date -} - -export interface CapturedWorkflowValues { - name?: string - description?: string - color?: string - folderId?: string | null - state?: any - updatedAt?: Date -} - -export const sampleWorkflowState = { - blocks: { - 'starter-id': { - id: 'starter-id', - type: 'starter', - name: 'Start', - position: { x: 100, y: 100 }, - subBlocks: { - startWorkflow: { id: 'startWorkflow', type: 'dropdown', value: 'manual' }, - webhookPath: { id: 'webhookPath', type: 'short-input', value: '' }, - }, - outputs: { - input: 'any', - }, - enabled: true, - horizontalHandles: true, - advancedMode: false, - triggerMode: false, - height: 95, - }, - 'agent-id': { - id: 'agent-id', - type: 'agent', - name: 'Agent 1', - position: { x: 634, y: -167 }, - subBlocks: { - systemPrompt: { - id: 'systemPrompt', - type: 'long-input', - value: 'You are a helpful assistant', - }, - context: { id: 'context', type: 'short-input', value: '' }, - model: { id: 'model', type: 'dropdown', value: 'gpt-4o' }, - apiKey: { id: 'apiKey', type: 'short-input', value: '{{OPENAI_API_KEY}}' }, - }, - outputs: { - response: { - content: 'string', - model: 'string', - tokens: 'any', - }, - }, - enabled: true, - horizontalHandles: true, - advancedMode: false, - triggerMode: false, - height: 680, - }, - }, - edges: [ - { - id: 'edge-id', - source: 'starter-id', - target: 'agent-id', - sourceHandle: 'source', - targetHandle: 'target', - }, - ], - loops: {}, - parallels: {}, - lastSaved: Date.now(), - isDeployed: false, -} - -// Global mock data that can be configured by tests -export const globalMockData = { - webhooks: [] as any[], - workflows: [] as any[], - schedules: [] as any[], - shouldThrowError: false, - errorMessage: 'Database error', -} - -export const mockDb = { - select: vi.fn().mockImplementation(() => { - if (globalMockData.shouldThrowError) { - throw new Error(globalMockData.errorMessage) - } - return { - from: vi.fn().mockImplementation(() => ({ - innerJoin: vi.fn().mockImplementation(() => ({ - where: vi.fn().mockImplementation(() => ({ - limit: vi.fn().mockImplementation(() => { - // Return webhook/workflow join data if available - if (globalMockData.webhooks.length > 0) { - return [ - { - webhook: globalMockData.webhooks[0], - workflow: globalMockData.workflows[0] || { - id: 'test-workflow', - userId: 'test-user', - }, - }, - ] - } - return [] - }), - })), - })), - where: vi.fn().mockImplementation(() => ({ - limit: vi.fn().mockImplementation(() => { - // Return schedules if available - if (globalMockData.schedules.length > 0) { - return globalMockData.schedules - } - // Return simple workflow data - if (globalMockData.workflows.length > 0) { - return globalMockData.workflows - } - return [ - { - id: 'workflow-id', - userId: 'user-id', - state: sampleWorkflowState, - }, - ] - }), - })), - })), - } - }), - update: vi.fn().mockImplementation(() => ({ - set: vi.fn().mockImplementation(() => ({ - where: vi.fn().mockResolvedValue([]), - })), - })), - eq: vi.fn().mockImplementation((field, value) => ({ field, value, type: 'eq' })), - and: vi.fn().mockImplementation((...conditions) => ({ - conditions, - type: 'and', - })), -} - -/** - * Mock logger using @sim/testing createMockLogger. - * This provides a consistent mock logger across all API tests. - */ -export const mockLogger = createSimTestingMockLogger() - -export const mockUser = { - id: 'user-123', - email: 'test@example.com', -} - -export const mockSubscription = { - id: 'sub-123', - plan: 'enterprise', - status: 'active', - seats: 5, - referenceId: 'user-123', - metadata: { - perSeatAllowance: 100, - totalAllowance: 500, - updatedAt: '2023-01-01T00:00:00.000Z', - }, -} - -export const mockOrganization = { - id: 'org-456', - name: 'Test Organization', - slug: 'test-org', -} - -export const mockAdminMember = { - id: 'member-123', - userId: 'user-123', - organizationId: 'org-456', - role: 'admin', -} - -export const mockRegularMember = { - id: 'member-456', - userId: 'user-123', - organizationId: 'org-456', - role: 'member', -} - -export const mockTeamSubscription = { - id: 'sub-456', - plan: 'team', - status: 'active', - seats: 5, - referenceId: 'org-123', -} - -export const mockPersonalSubscription = { - id: 'sub-789', - plan: 'enterprise', - status: 'active', - seats: 5, - referenceId: 'user-123', - metadata: { - perSeatAllowance: 100, - totalAllowance: 500, - updatedAt: '2023-01-01T00:00:00.000Z', - }, -} - -export const mockEnvironmentVars = { - OPENAI_API_KEY: 'encrypted:openai-api-key', - SERPER_API_KEY: 'encrypted:serper-api-key', -} - -export const mockDecryptedEnvVars = { - OPENAI_API_KEY: 'sk-test123', - SERPER_API_KEY: 'serper-test123', -} - -export function createMockRequest( - method = 'GET', - body?: any, - headers: Record = {} -): NextRequest { - const url = 'http://localhost:3000/api/test' - - return new NextRequest(new URL(url), { - method, - headers: new Headers(headers), - body: body ? JSON.stringify(body) : undefined, - }) -} - -export function mockExecutionDependencies() { - vi.mock('@/lib/core/security/encryption', () => ({ - decryptSecret: vi.fn().mockImplementation((encrypted: string) => { - const entries = Object.entries(mockEnvironmentVars) - const found = entries.find(([_, val]) => val === encrypted) - const key = found ? found[0] : null - - return Promise.resolve({ - decrypted: - key && key in mockDecryptedEnvVars - ? mockDecryptedEnvVars[key as keyof typeof mockDecryptedEnvVars] - : 'decrypted-value', - }) - }), - })) - - vi.mock('@/lib/logs/execution/trace-spans/trace-spans', () => ({ - buildTraceSpans: vi.fn().mockReturnValue({ - traceSpans: [], - totalDuration: 100, - }), - })) - - vi.mock('@/lib/workflows/utils', () => ({ - updateWorkflowRunCounts: vi.fn().mockResolvedValue(undefined), - })) - - vi.mock('@/serializer', () => ({ - Serializer: vi.fn().mockImplementation(() => ({ - serializeWorkflow: vi.fn().mockReturnValue({ - version: '1.0', - blocks: [ - { - id: 'starter-id', - metadata: { id: 'starter', name: 'Start' }, - config: {}, - inputs: {}, - outputs: {}, - position: { x: 100, y: 100 }, - enabled: true, - }, - { - id: 'agent-id', - metadata: { id: 'agent', name: 'Agent 1' }, - config: {}, - inputs: {}, - outputs: {}, - position: { x: 634, y: -167 }, - enabled: true, - }, - ], - connections: [ - { - source: 'starter-id', - target: 'agent-id', - }, - ], - loops: {}, - }), - })), - })) - - vi.mock('@/executor', () => ({ - Executor: vi.fn().mockImplementation(() => ({ - execute: vi.fn().mockResolvedValue({ - success: true, - output: { - response: { - content: 'This is a test response', - model: 'gpt-4o', - }, - }, - logs: [], - metadata: { - duration: 1000, - startTime: new Date().toISOString(), - endTime: new Date().toISOString(), - }, - }), - })), - })) - - vi.mock('@sim/db', () => ({ - db: mockDb, - // Add common schema exports that tests might need - webhook: { - id: 'id', - path: 'path', - workflowId: 'workflowId', - isActive: 'isActive', - provider: 'provider', - providerConfig: 'providerConfig', - }, - workflow: { - id: 'id', - userId: 'userId', - }, - workflowSchedule: { - id: 'id', - workflowId: 'workflowId', - nextRunAt: 'nextRunAt', - status: 'status', - }, - userStats: { - userId: 'userId', - totalScheduledExecutions: 'totalScheduledExecutions', - lastActive: 'lastActive', - }, - })) -} - -/** - * Mock Trigger.dev SDK (tasks.trigger and task factory) for tests that import background modules - */ -export function mockTriggerDevSdk() { - vi.mock('@trigger.dev/sdk', () => ({ - tasks: { - trigger: vi.fn().mockResolvedValue({ id: 'mock-task-id' }), - }, - task: vi.fn().mockReturnValue({}), - })) -} - -export function mockWorkflowAccessValidation(shouldSucceed = true) { - if (shouldSucceed) { - vi.mock('@/app/api/workflows/middleware', () => ({ - validateWorkflowAccess: vi.fn().mockResolvedValue({ - workflow: { - id: 'workflow-id', - userId: 'user-id', - state: sampleWorkflowState, - }, - }), - })) - } else { - vi.mock('@/app/api/workflows/middleware', () => ({ - validateWorkflowAccess: vi.fn().mockResolvedValue({ - error: { - message: 'Access denied', - status: 403, - }, - }), - })) - } -} - -export async function getMockedDependencies() { - const encryptionModule = await import('@/lib/core/security/encryption') - const traceSpansModule = await import('@/lib/logs/execution/trace-spans/trace-spans') - const workflowUtilsModule = await import('@/lib/workflows/utils') - const executorModule = await import('@/executor') - const serializerModule = await import('@/serializer') - const dbModule = await import('@sim/db') - - return { - decryptSecret: encryptionModule.decryptSecret, - buildTraceSpans: traceSpansModule.buildTraceSpans, - updateWorkflowRunCounts: workflowUtilsModule.updateWorkflowRunCounts, - Executor: executorModule.Executor, - Serializer: serializerModule.Serializer, - db: dbModule.db, - } -} - -export function mockScheduleStatusDb({ - schedule = [ - { - id: 'schedule-id', - workflowId: 'workflow-id', - status: 'active', - failedCount: 0, - lastRanAt: new Date('2024-01-01T00:00:00.000Z'), - lastFailedAt: null, - nextRunAt: new Date('2024-01-02T00:00:00.000Z'), - }, - ], - workflow = [ - { - userId: 'user-id', - }, - ], -}: { - schedule?: any[] - workflow?: any[] -} = {}) { - vi.doMock('@sim/db', () => { - let callCount = 0 - - const select = vi.fn().mockImplementation(() => ({ - from: vi.fn().mockImplementation(() => ({ - where: vi.fn().mockImplementation(() => ({ - limit: vi.fn().mockImplementation(() => { - callCount += 1 - if (callCount === 1) return schedule - if (callCount === 2) return workflow - return [] - }), - })), - })), - })) - - return { - db: { select }, - } - }) -} - -export function mockScheduleExecuteDb({ - schedules = [] as any[], - workflowRecord = { - id: 'workflow-id', - userId: 'user-id', - state: sampleWorkflowState, - }, - envRecord = { - userId: 'user-id', - variables: { - OPENAI_API_KEY: 'encrypted:openai-api-key', - SERPER_API_KEY: 'encrypted:serper-api-key', - }, - }, -}: { - schedules?: any[] - workflowRecord?: any - envRecord?: any -}): void { - vi.doMock('@sim/db', () => { - const select = vi.fn().mockImplementation(() => ({ - from: vi.fn().mockImplementation((table: any) => { - const tbl = String(table) - if (tbl === 'workflow_schedule' || tbl === 'schedule') { - return { - where: vi.fn().mockImplementation(() => ({ - limit: vi.fn().mockImplementation(() => schedules), - })), - } - } - - if (tbl === 'workflow') { - return { - where: vi.fn().mockImplementation(() => ({ - limit: vi.fn().mockImplementation(() => [workflowRecord]), - })), - } - } - - if (tbl === 'environment') { - return { - where: vi.fn().mockImplementation(() => ({ - limit: vi.fn().mockImplementation(() => [envRecord]), - })), - } - } - - return { - where: vi.fn().mockImplementation(() => ({ - limit: vi.fn().mockImplementation(() => []), - })), - } - }), - })) - - const update = vi.fn().mockImplementation(() => ({ - set: vi.fn().mockImplementation(() => ({ - where: vi.fn().mockResolvedValue([]), - })), - })) - - return { db: { select, update } } - }) -} - -/** - * Mock authentication for API tests - * @param user - Optional user object to use for authenticated requests - * @returns Object with authentication helper functions - */ -export function mockAuth(user: MockUser = mockUser): MockAuthResult { - const mockGetSession = vi.fn() - - vi.doMock('@/lib/auth', () => ({ - getSession: mockGetSession, - })) - - const setAuthenticated = (customUser?: MockUser) => - mockGetSession.mockResolvedValue({ user: customUser || user }) - const setUnauthenticated = () => mockGetSession.mockResolvedValue(null) - - return { - mockGetSession, - mockAuthenticatedUser: setAuthenticated, - mockUnauthenticated: setUnauthenticated, - setAuthenticated, - setUnauthenticated, - } -} - -/** - * Mock common schema patterns - */ -export function mockCommonSchemas() { - vi.doMock('@sim/db/schema', () => ({ - workflowFolder: { - id: 'id', - userId: 'userId', - parentId: 'parentId', - updatedAt: 'updatedAt', - workspaceId: 'workspaceId', - sortOrder: 'sortOrder', - createdAt: 'createdAt', - }, - workflow: { - id: 'id', - folderId: 'folderId', - userId: 'userId', - updatedAt: 'updatedAt', - }, - account: { - userId: 'userId', - providerId: 'providerId', - }, - user: { - email: 'email', - id: 'id', - }, - })) -} - -/** - * Mock drizzle-orm operators - */ -export function mockDrizzleOrm() { - vi.doMock('drizzle-orm', () => ({ - and: vi.fn((...conditions) => ({ conditions, type: 'and' })), - eq: vi.fn((field, value) => ({ field, value, type: 'eq' })), - or: vi.fn((...conditions) => ({ type: 'or', conditions })), - gte: vi.fn((field, value) => ({ type: 'gte', field, value })), - lte: vi.fn((field, value) => ({ type: 'lte', field, value })), - asc: vi.fn((field) => ({ field, type: 'asc' })), - desc: vi.fn((field) => ({ field, type: 'desc' })), - isNull: vi.fn((field) => ({ field, type: 'isNull' })), - count: vi.fn((field) => ({ field, type: 'count' })), - sql: vi.fn((strings, ...values) => ({ - type: 'sql', - sql: strings, - values, - })), - })) -} - -/** - * Mock knowledge-related database schemas - */ -export function mockKnowledgeSchemas() { - vi.doMock('@sim/db/schema', () => ({ - knowledgeBase: { - id: 'kb_id', - userId: 'user_id', - name: 'kb_name', - description: 'description', - tokenCount: 'token_count', - embeddingModel: 'embedding_model', - embeddingDimension: 'embedding_dimension', - chunkingConfig: 'chunking_config', - workspaceId: 'workspace_id', - createdAt: 'created_at', - updatedAt: 'updated_at', - deletedAt: 'deleted_at', - }, - document: { - id: 'doc_id', - knowledgeBaseId: 'kb_id', - filename: 'filename', - fileUrl: 'file_url', - fileSize: 'file_size', - mimeType: 'mime_type', - chunkCount: 'chunk_count', - tokenCount: 'token_count', - characterCount: 'character_count', - processingStatus: 'processing_status', - processingStartedAt: 'processing_started_at', - processingCompletedAt: 'processing_completed_at', - processingError: 'processing_error', - enabled: 'enabled', - tag1: 'tag1', - tag2: 'tag2', - tag3: 'tag3', - tag4: 'tag4', - tag5: 'tag5', - tag6: 'tag6', - tag7: 'tag7', - uploadedAt: 'uploaded_at', - deletedAt: 'deleted_at', - }, - embedding: { - id: 'embedding_id', - documentId: 'doc_id', - knowledgeBaseId: 'kb_id', - chunkIndex: 'chunk_index', - content: 'content', - embedding: 'embedding', - tokenCount: 'token_count', - characterCount: 'character_count', - tag1: 'tag1', - tag2: 'tag2', - tag3: 'tag3', - tag4: 'tag4', - tag5: 'tag5', - tag6: 'tag6', - tag7: 'tag7', - createdAt: 'created_at', - }, - permissions: { - id: 'permission_id', - userId: 'user_id', - entityType: 'entity_type', - entityId: 'entity_id', - permissionType: 'permission_type', - createdAt: 'created_at', - updatedAt: 'updated_at', - }, - })) -} - -/** - * Mock console logger using the shared mockLogger instance. - * This ensures tests can assert on the same mockLogger instance exported from this module. - */ -export function mockConsoleLogger() { - vi.doMock('@sim/logger', () => ({ - createLogger: vi.fn().mockReturnValue(mockLogger), - })) -} - -/** - * Setup common API test mocks (auth, logger, schema, drizzle) - */ -export function setupCommonApiMocks() { - mockCommonSchemas() - mockDrizzleOrm() - mockConsoleLogger() -} - -/** - * Mock UUID generation for consistent test results - */ -export function mockUuid(mockValue = 'test-uuid') { - vi.doMock('uuid', () => ({ - v4: vi.fn().mockReturnValue(mockValue), - })) -} - -/** - * Mock crypto.randomUUID for tests - */ -export function mockCryptoUuid(mockValue = 'mock-uuid-1234-5678') { - vi.stubGlobal('crypto', { - randomUUID: vi.fn().mockReturnValue(mockValue), - }) -} - -/** - * Mock file system operations - */ -export function mockFileSystem( - options: { writeFileSuccess?: boolean; readFileContent?: string; existsResult?: boolean } = {} -) { - const { writeFileSuccess = true, readFileContent = 'test content', existsResult = true } = options - - vi.doMock('fs/promises', () => ({ - writeFile: vi.fn().mockImplementation(() => { - if (writeFileSuccess) { - return Promise.resolve() - } - return Promise.reject(new Error('Write failed')) - }), - readFile: vi.fn().mockResolvedValue(readFileContent), - stat: vi.fn().mockResolvedValue({ size: 100, isFile: () => true }), - access: vi.fn().mockImplementation(() => { - if (existsResult) { - return Promise.resolve() - } - return Promise.reject(new Error('File not found')) - }), - mkdir: vi.fn().mockResolvedValue(undefined), - })) -} - -/** - * Mock encryption utilities - */ -export function mockEncryption(options: { encryptedValue?: string; decryptedValue?: string } = {}) { - const { encryptedValue = 'encrypted-value', decryptedValue = 'decrypted-value' } = options - - vi.doMock('@/lib/core/security/encryption', () => ({ - encryptSecret: vi.fn().mockResolvedValue({ encrypted: encryptedValue }), - decryptSecret: vi.fn().mockResolvedValue({ decrypted: decryptedValue }), - })) -} - -/** - * Interface for storage provider mock configuration - */ -export interface StorageProviderMockOptions { - provider?: 's3' | 'blob' | 'local' - isCloudEnabled?: boolean - throwError?: boolean - errorMessage?: string - presignedUrl?: string - uploadHeaders?: Record -} - -/** - * Create storage provider mocks (S3, Blob, Local) - */ -export function createStorageProviderMocks(options: StorageProviderMockOptions = {}) { - const { - provider = 's3', - isCloudEnabled = true, - throwError = false, - errorMessage = 'Storage error', - presignedUrl = 'https://example.com/presigned-url', - uploadHeaders = {}, - } = options - - mockUuid('mock-uuid-1234') - mockCryptoUuid('mock-uuid-1234-5678') - - const uploadFileMock = vi.fn().mockResolvedValue({ - path: '/api/files/serve/test-key.txt', - key: 'test-key.txt', - name: 'test.txt', - size: 100, - type: 'text/plain', - }) - const downloadFileMock = vi.fn().mockResolvedValue(Buffer.from('test content')) - const deleteFileMock = vi.fn().mockResolvedValue(undefined) - const hasCloudStorageMock = vi.fn().mockReturnValue(isCloudEnabled) - - const generatePresignedUploadUrlMock = vi.fn().mockImplementation((params: any) => { - const { fileName, context } = params - const timestamp = Date.now() - const random = Math.random().toString(36).substring(2, 9) - - let key = '' - if (context === 'knowledge-base') { - key = `kb/${timestamp}-${random}-${fileName}` - } else if (context === 'chat') { - key = `chat/${timestamp}-${random}-${fileName}` - } else if (context === 'copilot') { - key = `copilot/${timestamp}-${random}-${fileName}` - } else if (context === 'workspace') { - key = `workspace/${timestamp}-${random}-${fileName}` - } else { - key = `${timestamp}-${random}-${fileName}` - } - - return Promise.resolve({ - url: presignedUrl, - key, - uploadHeaders: uploadHeaders, - }) - }) - - const generatePresignedDownloadUrlMock = vi.fn().mockResolvedValue(presignedUrl) - - vi.doMock('@/lib/uploads', () => ({ - getStorageProvider: vi.fn().mockReturnValue(provider), - isUsingCloudStorage: vi.fn().mockReturnValue(isCloudEnabled), - StorageService: { - uploadFile: uploadFileMock, - downloadFile: downloadFileMock, - deleteFile: deleteFileMock, - hasCloudStorage: hasCloudStorageMock, - generatePresignedUploadUrl: generatePresignedUploadUrlMock, - generatePresignedDownloadUrl: generatePresignedDownloadUrlMock, - }, - uploadFile: uploadFileMock, - downloadFile: downloadFileMock, - deleteFile: deleteFileMock, - getPresignedUrl: vi.fn().mockResolvedValue(presignedUrl), - hasCloudStorage: hasCloudStorageMock, - generatePresignedDownloadUrl: generatePresignedDownloadUrlMock, - })) - - vi.doMock('@/lib/uploads/core/storage-service', () => ({ - uploadFile: uploadFileMock, - downloadFile: downloadFileMock, - deleteFile: deleteFileMock, - hasCloudStorage: hasCloudStorageMock, - generatePresignedUploadUrl: generatePresignedUploadUrlMock, - generatePresignedDownloadUrl: generatePresignedDownloadUrlMock, - StorageService: { - uploadFile: uploadFileMock, - downloadFile: downloadFileMock, - deleteFile: deleteFileMock, - hasCloudStorage: hasCloudStorageMock, - generatePresignedUploadUrl: generatePresignedUploadUrlMock, - generatePresignedDownloadUrl: generatePresignedDownloadUrlMock, - }, - })) - - vi.doMock('@/lib/uploads/config', () => ({ - USE_S3_STORAGE: provider === 's3', - USE_BLOB_STORAGE: provider === 'blob', - USE_LOCAL_STORAGE: provider === 'local', - getStorageProvider: vi.fn().mockReturnValue(provider), - S3_CONFIG: { - bucket: 'test-s3-bucket', - region: 'us-east-1', - }, - S3_KB_CONFIG: { - bucket: 'test-s3-kb-bucket', - region: 'us-east-1', - }, - S3_CHAT_CONFIG: { - bucket: 'test-s3-chat-bucket', - region: 'us-east-1', - }, - BLOB_CONFIG: { - accountName: 'testaccount', - accountKey: 'testkey', - containerName: 'test-container', - }, - BLOB_KB_CONFIG: { - accountName: 'testaccount', - accountKey: 'testkey', - containerName: 'test-kb-container', - }, - BLOB_CHAT_CONFIG: { - accountName: 'testaccount', - accountKey: 'testkey', - containerName: 'test-chat-container', - }, - })) - - if (provider === 's3') { - vi.doMock('@/lib/uploads/providers/s3/client', () => ({ - getS3Client: vi.fn().mockReturnValue({}), - })) - vi.doMock('@aws-sdk/client-s3', () => ({ - PutObjectCommand: vi.fn(), - })) - - vi.doMock('@aws-sdk/s3-request-presigner', () => ({ - getSignedUrl: vi.fn().mockImplementation(() => { - if (throwError) { - return Promise.reject(new Error(errorMessage)) - } - return Promise.resolve(presignedUrl) - }), - })) - } else if (provider === 'blob') { - const baseUrl = 'https://testaccount.blob.core.windows.net/test-container' - const mockBlockBlobClient = { - url: baseUrl, - } - const mockContainerClient = { - getBlockBlobClient: vi.fn(() => mockBlockBlobClient), - } - const mockBlobServiceClient = { - getContainerClient: vi.fn(() => { - if (throwError) { - throw new Error(errorMessage) - } - return mockContainerClient - }), - } - - vi.doMock('@/lib/uploads/providers/blob/client', () => ({ - getBlobServiceClient: vi.fn().mockReturnValue(mockBlobServiceClient), - })) - vi.doMock('@azure/storage-blob', () => ({ - BlobSASPermissions: { - parse: vi.fn(() => 'w'), - }, - generateBlobSASQueryParameters: vi.fn(() => ({ - toString: () => 'sas-token-string', - })), - StorageSharedKeyCredential: vi.fn(), - })) - } - - return { - provider, - isCloudEnabled, - mockBlobClient: provider === 'blob' ? vi.fn() : undefined, - mockS3Client: provider === 's3' ? vi.fn() : undefined, - } -} - -/** - * Interface for auth API mock configuration with all auth operations - */ -export interface AuthApiMockOptions { - operations?: { - forgetPassword?: { - success?: boolean - error?: string - } - resetPassword?: { - success?: boolean - error?: string - } - signIn?: { - success?: boolean - error?: string - } - signUp?: { - success?: boolean - error?: string - } - } -} - -/** - * Interface for comprehensive test setup options - */ -export interface TestSetupOptions { - auth?: { - authenticated?: boolean - user?: MockUser - } - database?: MockDatabaseOptions - storage?: StorageProviderMockOptions - authApi?: AuthApiMockOptions - features?: { - workflowUtils?: boolean - fileSystem?: boolean - uploadUtils?: boolean - encryption?: boolean - } -} - -/** - * Master setup function for comprehensive test mocking - * This is the preferred setup function for new tests - */ -export function setupComprehensiveTestMocks(options: TestSetupOptions = {}) { - const { auth = { authenticated: true }, database = {}, storage, authApi, features = {} } = options - - setupCommonApiMocks() - mockUuid() - mockCryptoUuid() - - const authMocks = mockAuth(auth.user) - if (auth.authenticated) { - authMocks.setAuthenticated(auth.user) - } else { - authMocks.setUnauthenticated() - } - - const dbMocks = createMockDatabase(database) - - let storageMocks - if (storage) { - storageMocks = createStorageProviderMocks(storage) - } - - let authApiMocks - if (authApi) { - authApiMocks = createAuthApiMocks(authApi) - } - - const featureMocks: any = {} - if (features.workflowUtils) { - featureMocks.workflowUtils = mockWorkflowUtils() - } - if (features.fileSystem) { - featureMocks.fileSystem = mockFileSystem() - } - if (features.uploadUtils) { - featureMocks.uploadUtils = mockUploadUtils() - } - if (features.encryption) { - featureMocks.encryption = mockEncryption() - } - - return { - auth: authMocks, - database: dbMocks, - storage: storageMocks, - authApi: authApiMocks, - features: featureMocks, - } -} - -/** - * Create a more focused and composable database mock - */ -export function createMockDatabase(options: MockDatabaseOptions = {}) { - const selectOptions = options.select || { results: [[]], throwError: false } - const insertOptions = options.insert || { results: [{ id: 'mock-id' }], throwError: false } - const updateOptions = options.update || { results: [{ id: 'mock-id' }], throwError: false } - const deleteOptions = options.delete || { results: [{ id: 'mock-id' }], throwError: false } - const transactionOptions = options.transaction || { throwError: false } - - let selectCallCount = 0 - - const createDbError = (operation: string, message?: string) => { - return new Error(message || `Database ${operation} error`) - } - - const createSelectChain = () => ({ - from: vi.fn().mockReturnThis(), - leftJoin: vi.fn().mockReturnThis(), - innerJoin: vi.fn().mockReturnThis(), - where: vi.fn().mockReturnThis(), - groupBy: vi.fn().mockReturnThis(), - orderBy: vi.fn().mockImplementation(() => { - if (selectOptions.throwError) { - return Promise.reject(createDbError('select', selectOptions.errorMessage)) - } - const result = selectOptions.results?.[selectCallCount] || selectOptions.results?.[0] || [] - selectCallCount++ - return Promise.resolve(result) - }), - limit: vi.fn().mockImplementation(() => { - if (selectOptions.throwError) { - return Promise.reject(createDbError('select', selectOptions.errorMessage)) - } - const result = selectOptions.results?.[selectCallCount] || selectOptions.results?.[0] || [] - selectCallCount++ - return Promise.resolve(result) - }), - }) - - const createInsertChain = () => ({ - values: vi.fn().mockImplementation(() => ({ - returning: vi.fn().mockImplementation(() => { - if (insertOptions.throwError) { - return Promise.reject(createDbError('insert', insertOptions.errorMessage)) - } - return Promise.resolve(insertOptions.results) - }), - onConflictDoUpdate: vi.fn().mockImplementation(() => { - if (insertOptions.throwError) { - return Promise.reject(createDbError('insert', insertOptions.errorMessage)) - } - return Promise.resolve(insertOptions.results) - }), - })), - }) - - const createUpdateChain = () => ({ - set: vi.fn().mockImplementation(() => ({ - where: vi.fn().mockImplementation(() => ({ - returning: vi.fn().mockImplementation(() => { - if (updateOptions.throwError) { - return Promise.reject(createDbError('update', updateOptions.errorMessage)) - } - return Promise.resolve(updateOptions.results) - }), - then: vi.fn().mockImplementation((resolve) => { - if (updateOptions.throwError) { - return Promise.reject(createDbError('update', updateOptions.errorMessage)) - } - return Promise.resolve(updateOptions.results).then(resolve) - }), - })), - })), - }) - - const createDeleteChain = () => ({ - where: vi.fn().mockImplementation(() => { - if (deleteOptions.throwError) { - return Promise.reject(createDbError('delete', deleteOptions.errorMessage)) - } - return Promise.resolve(deleteOptions.results) - }), - }) - - const createTransactionMock = () => { - return vi.fn().mockImplementation(async (callback: any) => { - if (transactionOptions.throwError) { - throw createDbError('transaction', transactionOptions.errorMessage) - } - - const tx = { - select: vi.fn().mockImplementation(() => createSelectChain()), - insert: vi.fn().mockImplementation(() => createInsertChain()), - update: vi.fn().mockImplementation(() => createUpdateChain()), - delete: vi.fn().mockImplementation(() => createDeleteChain()), - } - return await callback(tx) - }) - } - - const mockDb = { - select: vi.fn().mockImplementation(() => createSelectChain()), - insert: vi.fn().mockImplementation(() => createInsertChain()), - update: vi.fn().mockImplementation(() => createUpdateChain()), - delete: vi.fn().mockImplementation(() => createDeleteChain()), - transaction: createTransactionMock(), - } - - vi.doMock('@sim/db', () => ({ db: mockDb })) - - return { - mockDb, - resetSelectCallCount: () => { - selectCallCount = 0 - }, - } -} - -/** - * Create comprehensive auth API mocks - */ -export function createAuthApiMocks(options: AuthApiMockOptions = {}) { - const { operations = {} } = options - - const defaultOperations = { - forgetPassword: { success: true, error: 'Forget password error' }, - resetPassword: { success: true, error: 'Reset password error' }, - signIn: { success: true, error: 'Sign in error' }, - signUp: { success: true, error: 'Sign up error' }, - ...operations, - } - - const createAuthMethod = (operation: string, config: { success?: boolean; error?: string }) => { - return vi.fn().mockImplementation(() => { - if (config.success) { - return Promise.resolve() - } - return Promise.reject(new Error(config.error)) - }) - } - - vi.doMock('@/lib/auth', () => ({ - auth: { - api: { - forgetPassword: createAuthMethod('forgetPassword', defaultOperations.forgetPassword), - resetPassword: createAuthMethod('resetPassword', defaultOperations.resetPassword), - signIn: createAuthMethod('signIn', defaultOperations.signIn), - signUp: createAuthMethod('signUp', defaultOperations.signUp), - }, - }, - })) - - return { - operations: defaultOperations, - } -} - -/** - * Mock workflow utilities and response helpers - */ -export function mockWorkflowUtils() { - vi.doMock('@/app/api/workflows/utils', () => ({ - createSuccessResponse: vi.fn().mockImplementation((data) => { - return new Response(JSON.stringify(data), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }) - }), - createErrorResponse: vi.fn().mockImplementation((message, status = 500) => { - return new Response(JSON.stringify({ error: message }), { - status, - headers: { 'Content-Type': 'application/json' }, - }) - }), - })) -} - -/** - * Setup grouped mocks for knowledge base operations - */ -export function setupKnowledgeMocks( - options: { - withDocumentProcessing?: boolean - withEmbedding?: boolean - accessCheckResult?: boolean - } = {} -) { - const { - withDocumentProcessing = false, - withEmbedding = false, - accessCheckResult = true, - } = options - - const mocks: any = { - checkKnowledgeBaseAccess: vi.fn().mockResolvedValue(accessCheckResult), - } - - if (withDocumentProcessing) { - mocks.processDocumentAsync = vi.fn().mockResolvedValue(undefined) - } - - if (withEmbedding) { - mocks.generateEmbedding = vi.fn().mockResolvedValue([0.1, 0.2, 0.3]) - } - - vi.doMock('@/app/api/knowledge/utils', () => mocks) - - return mocks -} - -/** - * Setup for file-related API routes - */ -export function setupFileApiMocks( - options: { - authenticated?: boolean - storageProvider?: 's3' | 'blob' | 'local' - cloudEnabled?: boolean - } = {} -) { - const { authenticated = true, storageProvider = 's3', cloudEnabled = true } = options - - setupCommonApiMocks() - mockUuid() - mockCryptoUuid() - - const authMocks = mockAuth() - if (authenticated) { - authMocks.setAuthenticated() - } else { - authMocks.setUnauthenticated() - } - - vi.doMock('@/lib/auth/hybrid', () => ({ - checkHybridAuth: vi.fn().mockResolvedValue({ - success: authenticated, - userId: authenticated ? 'test-user-id' : undefined, - error: authenticated ? undefined : 'Unauthorized', - }), - })) - - vi.doMock('@/app/api/files/authorization', () => ({ - verifyFileAccess: vi.fn().mockResolvedValue(true), - verifyWorkspaceFileAccess: vi.fn().mockResolvedValue(true), - verifyKBFileAccess: vi.fn().mockResolvedValue(true), - verifyCopilotFileAccess: vi.fn().mockResolvedValue(true), - lookupWorkspaceFileByKey: vi.fn().mockResolvedValue({ - workspaceId: 'test-workspace-id', - uploadedBy: 'test-user-id', - }), - })) - - vi.doMock('@/lib/uploads/contexts/workspace', () => ({ - uploadWorkspaceFile: vi.fn().mockResolvedValue({ - id: 'test-file-id', - name: 'test.txt', - url: '/api/files/serve/workspace/test-workspace-id/test-file.txt', - size: 100, - type: 'text/plain', - key: 'workspace/test-workspace-id/1234567890-test.txt', - uploadedAt: new Date().toISOString(), - expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), - }), - })) - - mockFileSystem({ - writeFileSuccess: true, - readFileContent: 'test content', - existsResult: true, - }) - - let storageMocks - if (storageProvider) { - storageMocks = createStorageProviderMocks({ - provider: storageProvider, - isCloudEnabled: cloudEnabled, - }) - } else { - const uploadFileMock = vi.fn().mockResolvedValue({ - path: '/api/files/serve/test-key.txt', - key: 'test-key.txt', - name: 'test.txt', - size: 100, - type: 'text/plain', - }) - const downloadFileMock = vi.fn().mockResolvedValue(Buffer.from('test content')) - const deleteFileMock = vi.fn().mockResolvedValue(undefined) - const hasCloudStorageMock = vi.fn().mockReturnValue(cloudEnabled) - - vi.doMock('@/lib/uploads', () => ({ - getStorageProvider: vi.fn().mockReturnValue('local'), - isUsingCloudStorage: vi.fn().mockReturnValue(cloudEnabled), - StorageService: { - uploadFile: uploadFileMock, - downloadFile: downloadFileMock, - deleteFile: deleteFileMock, - hasCloudStorage: hasCloudStorageMock, - generatePresignedUploadUrl: vi.fn().mockResolvedValue({ - presignedUrl: 'https://example.com/presigned-url', - key: 'test-key.txt', - }), - generatePresignedDownloadUrl: vi - .fn() - .mockResolvedValue('https://example.com/presigned-url'), - }, - uploadFile: uploadFileMock, - downloadFile: downloadFileMock, - deleteFile: deleteFileMock, - getPresignedUrl: vi.fn().mockResolvedValue('https://example.com/presigned-url'), - hasCloudStorage: hasCloudStorageMock, - })) - } - - return { - auth: authMocks, - storage: storageMocks, - } -} - -/** - * Setup for auth-related API routes - */ -export function setupAuthApiMocks(options: { operations?: AuthApiMockOptions['operations'] } = {}) { - return setupComprehensiveTestMocks({ - auth: { authenticated: false }, // Auth routes typically don't require authentication - authApi: { operations: options.operations }, - }) -} - -/** - * Setup for knowledge base API routes - */ -export function setupKnowledgeApiMocks( - options: { - authenticated?: boolean - withDocumentProcessing?: boolean - withEmbedding?: boolean - } = {} -) { - const mocks = setupComprehensiveTestMocks({ - auth: { authenticated: options.authenticated ?? true }, - database: { - select: { results: [[]] }, - }, - }) - - const knowledgeMocks = setupKnowledgeMocks({ - withDocumentProcessing: options.withDocumentProcessing, - withEmbedding: options.withEmbedding, - }) - - return { - ...mocks, - knowledge: knowledgeMocks, - } -} - -export function setupApiTestMocks( - options: { - authenticated?: boolean - user?: MockUser - dbResults?: any[][] - withWorkflowUtils?: boolean - withFileSystem?: boolean - withUploadUtils?: boolean - } = {} -) { - const { - authenticated = true, - user = mockUser, - dbResults = [[]], - withWorkflowUtils = false, - withFileSystem = false, - withUploadUtils = false, - } = options - - return setupComprehensiveTestMocks({ - auth: { authenticated, user }, - database: { select: { results: dbResults } }, - features: { - workflowUtils: withWorkflowUtils, - fileSystem: withFileSystem, - uploadUtils: withUploadUtils, - }, - }) -} - -export function mockUploadUtils( - options: { isCloudStorage?: boolean; uploadResult?: any; uploadError?: boolean } = {} -) { - const { - isCloudStorage = false, - uploadResult = { - path: '/api/files/serve/test-key.txt', - key: 'test-key.txt', - name: 'test.txt', - size: 100, - type: 'text/plain', - }, - uploadError = false, - } = options - - const uploadFileMock = vi.fn().mockImplementation(() => { - if (uploadError) { - return Promise.reject(new Error('Upload failed')) - } - return Promise.resolve(uploadResult) - }) - - vi.doMock('@/lib/uploads', () => ({ - StorageService: { - uploadFile: uploadFileMock, - downloadFile: vi.fn().mockResolvedValue(Buffer.from('test content')), - deleteFile: vi.fn().mockResolvedValue(undefined), - hasCloudStorage: vi.fn().mockReturnValue(isCloudStorage), - }, - uploadFile: uploadFileMock, - isUsingCloudStorage: vi.fn().mockReturnValue(isCloudStorage), - })) - - vi.doMock('@/lib/uploads/config', () => ({ - UPLOAD_DIR: '/test/uploads', - USE_S3_STORAGE: isCloudStorage, - USE_BLOB_STORAGE: false, - S3_CONFIG: { - bucket: 'test-bucket', - region: 'test-region', - }, - })) -} - -export function createMockTransaction( - mockData: { - selectData?: DatabaseSelectResult[] - insertResult?: DatabaseInsertResult[] - updateResult?: DatabaseUpdateResult[] - deleteResult?: DatabaseDeleteResult[] - } = {} -) { - const { selectData = [], insertResult = [], updateResult = [], deleteResult = [] } = mockData - - return vi.fn().mockImplementation(async (callback: any) => { - const tx = { - select: vi.fn().mockReturnValue({ - from: vi.fn().mockReturnValue({ - where: vi.fn().mockReturnValue({ - orderBy: vi.fn().mockReturnValue({ - limit: vi.fn().mockReturnValue(selectData), - }), - }), - }), - }), - insert: vi.fn().mockReturnValue({ - values: vi.fn().mockReturnValue({ - returning: vi.fn().mockReturnValue(insertResult), - }), - }), - update: vi.fn().mockReturnValue({ - set: vi.fn().mockReturnValue({ - where: vi.fn().mockReturnValue(updateResult), - }), - }), - delete: vi.fn().mockReturnValue({ - where: vi.fn().mockReturnValue(deleteResult), - }), - } - return await callback(tx) - }) -} diff --git a/apps/sim/app/api/auth/forget-password/route.test.ts b/apps/sim/app/api/auth/forget-password/route.test.ts index 36cbb3e0e8..7f08c76e3e 100644 --- a/apps/sim/app/api/auth/forget-password/route.test.ts +++ b/apps/sim/app/api/auth/forget-password/route.test.ts @@ -3,13 +3,60 @@ * * @vitest-environment node */ +import { + createMockRequest, + mockConsoleLogger, + mockCryptoUuid, + mockDrizzleOrm, + mockUuid, + setupCommonApiMocks, +} from '@sim/testing' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { createMockRequest, setupAuthApiMocks } from '@/app/api/__test-utils__/utils' vi.mock('@/lib/core/utils/urls', () => ({ getBaseUrl: vi.fn(() => 'https://app.example.com'), })) +/** Setup auth API mocks for testing authentication routes */ +function setupAuthApiMocks( + options: { + operations?: { + forgetPassword?: { success?: boolean; error?: string } + resetPassword?: { success?: boolean; error?: string } + } + } = {} +) { + setupCommonApiMocks() + mockUuid() + mockCryptoUuid() + mockConsoleLogger() + mockDrizzleOrm() + + const { operations = {} } = options + const defaultOperations = { + forgetPassword: { success: true, error: 'Forget password error', ...operations.forgetPassword }, + resetPassword: { success: true, error: 'Reset password error', ...operations.resetPassword }, + } + + const createAuthMethod = (config: { success?: boolean; error?: string }) => { + return vi.fn().mockImplementation(() => { + if (config.success) { + return Promise.resolve() + } + return Promise.reject(new Error(config.error)) + }) + } + + vi.doMock('@/lib/auth', () => ({ + auth: { + api: { + forgetPassword: createAuthMethod(defaultOperations.forgetPassword), + resetPassword: createAuthMethod(defaultOperations.resetPassword), + }, + }, + })) +} + describe('Forget Password API Route', () => { beforeEach(() => { vi.resetModules() diff --git a/apps/sim/app/api/auth/oauth/connections/route.test.ts b/apps/sim/app/api/auth/oauth/connections/route.test.ts index 35bdcbc152..688f72edc7 100644 --- a/apps/sim/app/api/auth/oauth/connections/route.test.ts +++ b/apps/sim/app/api/auth/oauth/connections/route.test.ts @@ -3,8 +3,8 @@ * * @vitest-environment node */ +import { createMockLogger, createMockRequest } from '@sim/testing' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { createMockLogger, createMockRequest } from '@/app/api/__test-utils__/utils' describe('OAuth Connections API Route', () => { const mockGetSession = vi.fn() diff --git a/apps/sim/app/api/auth/oauth/credentials/route.test.ts b/apps/sim/app/api/auth/oauth/credentials/route.test.ts index 93aceaccc1..c83ed6625a 100644 --- a/apps/sim/app/api/auth/oauth/credentials/route.test.ts +++ b/apps/sim/app/api/auth/oauth/credentials/route.test.ts @@ -4,9 +4,9 @@ * @vitest-environment node */ +import { createMockLogger } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { createMockLogger } from '@/app/api/__test-utils__/utils' describe('OAuth Credentials API Route', () => { const mockGetSession = vi.fn() diff --git a/apps/sim/app/api/auth/oauth/disconnect/route.test.ts b/apps/sim/app/api/auth/oauth/disconnect/route.test.ts index 7f625d2539..9a504982af 100644 --- a/apps/sim/app/api/auth/oauth/disconnect/route.test.ts +++ b/apps/sim/app/api/auth/oauth/disconnect/route.test.ts @@ -3,8 +3,8 @@ * * @vitest-environment node */ +import { createMockLogger, createMockRequest } from '@sim/testing' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { createMockLogger, createMockRequest } from '@/app/api/__test-utils__/utils' describe('OAuth Disconnect API Route', () => { const mockGetSession = vi.fn() diff --git a/apps/sim/app/api/auth/oauth/token/route.test.ts b/apps/sim/app/api/auth/oauth/token/route.test.ts index 7359361a40..c5032fc326 100644 --- a/apps/sim/app/api/auth/oauth/token/route.test.ts +++ b/apps/sim/app/api/auth/oauth/token/route.test.ts @@ -3,8 +3,8 @@ * * @vitest-environment node */ +import { createMockLogger, createMockRequest } from '@sim/testing' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { createMockLogger, createMockRequest } from '@/app/api/__test-utils__/utils' describe('OAuth Token API Routes', () => { const mockGetUserId = vi.fn() diff --git a/apps/sim/app/api/auth/reset-password/route.test.ts b/apps/sim/app/api/auth/reset-password/route.test.ts index 9c9f2df5f9..18c4404440 100644 --- a/apps/sim/app/api/auth/reset-password/route.test.ts +++ b/apps/sim/app/api/auth/reset-password/route.test.ts @@ -3,8 +3,55 @@ * * @vitest-environment node */ +import { + createMockRequest, + mockConsoleLogger, + mockCryptoUuid, + mockDrizzleOrm, + mockUuid, + setupCommonApiMocks, +} from '@sim/testing' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { createMockRequest, setupAuthApiMocks } from '@/app/api/__test-utils__/utils' + +/** Setup auth API mocks for testing authentication routes */ +function setupAuthApiMocks( + options: { + operations?: { + forgetPassword?: { success?: boolean; error?: string } + resetPassword?: { success?: boolean; error?: string } + } + } = {} +) { + setupCommonApiMocks() + mockUuid() + mockCryptoUuid() + mockConsoleLogger() + mockDrizzleOrm() + + const { operations = {} } = options + const defaultOperations = { + forgetPassword: { success: true, error: 'Forget password error', ...operations.forgetPassword }, + resetPassword: { success: true, error: 'Reset password error', ...operations.resetPassword }, + } + + const createAuthMethod = (config: { success?: boolean; error?: string }) => { + return vi.fn().mockImplementation(() => { + if (config.success) { + return Promise.resolve() + } + return Promise.reject(new Error(config.error)) + }) + } + + vi.doMock('@/lib/auth', () => ({ + auth: { + api: { + forgetPassword: createAuthMethod(defaultOperations.forgetPassword), + resetPassword: createAuthMethod(defaultOperations.resetPassword), + }, + }, + })) +} describe('Reset Password API Route', () => { beforeEach(() => { diff --git a/apps/sim/app/api/chat/[identifier]/route.test.ts b/apps/sim/app/api/chat/[identifier]/route.test.ts index efc89bc0f4..5a753fd4d9 100644 --- a/apps/sim/app/api/chat/[identifier]/route.test.ts +++ b/apps/sim/app/api/chat/[identifier]/route.test.ts @@ -5,7 +5,34 @@ */ import { loggerMock } from '@sim/testing' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { createMockRequest } from '@/app/api/__test-utils__/utils' + +/** + * Creates a mock NextRequest with cookies support for testing. + */ +function createMockNextRequest( + method = 'GET', + body?: unknown, + headers: Record = {}, + url = 'http://localhost:3000/api/test' +): any { + const headersObj = new Headers({ + 'Content-Type': 'application/json', + ...headers, + }) + + return { + method, + headers: headersObj, + cookies: { + get: vi.fn().mockReturnValue(undefined), + }, + json: + body !== undefined + ? vi.fn().mockResolvedValue(body) + : vi.fn().mockRejectedValue(new Error('No body')), + url, + } +} const createMockStream = () => { return new ReadableStream({ @@ -71,10 +98,15 @@ vi.mock('@/lib/core/utils/request', () => ({ generateRequestId: vi.fn().mockReturnValue('test-request-id'), })) +vi.mock('@/lib/core/security/encryption', () => ({ + decryptSecret: vi.fn().mockResolvedValue({ decrypted: 'test-password' }), +})) + describe('Chat Identifier API Route', () => { const mockAddCorsHeaders = vi.fn().mockImplementation((response) => response) const mockValidateChatAuth = vi.fn().mockResolvedValue({ authorized: true }) const mockSetChatAuthCookie = vi.fn() + const mockValidateAuthToken = vi.fn().mockReturnValue(false) const mockChatResult = [ { @@ -114,11 +146,16 @@ describe('Chat Identifier API Route', () => { beforeEach(() => { vi.resetModules() - vi.doMock('@/app/api/chat/utils', () => ({ + vi.doMock('@/lib/core/security/deployment', () => ({ addCorsHeaders: mockAddCorsHeaders, + validateAuthToken: mockValidateAuthToken, + setDeploymentAuthCookie: vi.fn(), + isEmailAllowed: vi.fn().mockReturnValue(false), + })) + + vi.doMock('@/app/api/chat/utils', () => ({ validateChatAuth: mockValidateChatAuth, setChatAuthCookie: mockSetChatAuthCookie, - validateAuthToken: vi.fn().mockReturnValue(true), })) // Mock logger - use loggerMock from @sim/testing @@ -175,7 +212,7 @@ describe('Chat Identifier API Route', () => { describe('GET endpoint', () => { it('should return chat info for a valid identifier', async () => { - const req = createMockRequest('GET') + const req = createMockNextRequest('GET') const params = Promise.resolve({ identifier: 'test-chat' }) const { GET } = await import('@/app/api/chat/[identifier]/route') @@ -206,7 +243,7 @@ describe('Chat Identifier API Route', () => { } }) - const req = createMockRequest('GET') + const req = createMockNextRequest('GET') const params = Promise.resolve({ identifier: 'nonexistent' }) const { GET } = await import('@/app/api/chat/[identifier]/route') @@ -240,7 +277,7 @@ describe('Chat Identifier API Route', () => { } }) - const req = createMockRequest('GET') + const req = createMockNextRequest('GET') const params = Promise.resolve({ identifier: 'inactive-chat' }) const { GET } = await import('@/app/api/chat/[identifier]/route') @@ -261,7 +298,7 @@ describe('Chat Identifier API Route', () => { error: 'auth_required_password', })) - const req = createMockRequest('GET') + const req = createMockNextRequest('GET') const params = Promise.resolve({ identifier: 'password-protected-chat' }) const { GET } = await import('@/app/api/chat/[identifier]/route') @@ -282,7 +319,7 @@ describe('Chat Identifier API Route', () => { describe('POST endpoint', () => { it('should handle authentication requests without input', async () => { - const req = createMockRequest('POST', { password: 'test-password' }) + const req = createMockNextRequest('POST', { password: 'test-password' }) const params = Promise.resolve({ identifier: 'password-protected-chat' }) const { POST } = await import('@/app/api/chat/[identifier]/route') @@ -298,7 +335,7 @@ describe('Chat Identifier API Route', () => { }) it('should return 400 for requests without input', async () => { - const req = createMockRequest('POST', {}) + const req = createMockNextRequest('POST', {}) const params = Promise.resolve({ identifier: 'test-chat' }) const { POST } = await import('@/app/api/chat/[identifier]/route') @@ -319,7 +356,7 @@ describe('Chat Identifier API Route', () => { error: 'Authentication required', })) - const req = createMockRequest('POST', { input: 'Hello' }) + const req = createMockNextRequest('POST', { input: 'Hello' }) const params = Promise.resolve({ identifier: 'protected-chat' }) const { POST } = await import('@/app/api/chat/[identifier]/route') @@ -350,7 +387,7 @@ describe('Chat Identifier API Route', () => { }, }) - const req = createMockRequest('POST', { input: 'Hello' }) + const req = createMockNextRequest('POST', { input: 'Hello' }) const params = Promise.resolve({ identifier: 'test-chat' }) const { POST } = await import('@/app/api/chat/[identifier]/route') @@ -369,7 +406,10 @@ describe('Chat Identifier API Route', () => { }) it('should return streaming response for valid chat messages', async () => { - const req = createMockRequest('POST', { input: 'Hello world', conversationId: 'conv-123' }) + const req = createMockNextRequest('POST', { + input: 'Hello world', + conversationId: 'conv-123', + }) const params = Promise.resolve({ identifier: 'test-chat' }) const { POST } = await import('@/app/api/chat/[identifier]/route') @@ -401,7 +441,7 @@ describe('Chat Identifier API Route', () => { }, 10000) it('should handle streaming response body correctly', async () => { - const req = createMockRequest('POST', { input: 'Hello world' }) + const req = createMockNextRequest('POST', { input: 'Hello world' }) const params = Promise.resolve({ identifier: 'test-chat' }) const { POST } = await import('@/app/api/chat/[identifier]/route') @@ -431,7 +471,7 @@ describe('Chat Identifier API Route', () => { throw new Error('Execution failed') }) - const req = createMockRequest('POST', { input: 'Trigger error' }) + const req = createMockNextRequest('POST', { input: 'Trigger error' }) const params = Promise.resolve({ identifier: 'test-chat' }) const { POST } = await import('@/app/api/chat/[identifier]/route') @@ -470,7 +510,7 @@ describe('Chat Identifier API Route', () => { }) it('should pass conversationId to streaming execution when provided', async () => { - const req = createMockRequest('POST', { + const req = createMockNextRequest('POST', { input: 'Hello world', conversationId: 'test-conversation-123', }) @@ -492,7 +532,7 @@ describe('Chat Identifier API Route', () => { }) it('should handle missing conversationId gracefully', async () => { - const req = createMockRequest('POST', { input: 'Hello world' }) + const req = createMockNextRequest('POST', { input: 'Hello world' }) const params = Promise.resolve({ identifier: 'test-chat' }) const { POST } = await import('@/app/api/chat/[identifier]/route') diff --git a/apps/sim/app/api/copilot/api-keys/route.test.ts b/apps/sim/app/api/copilot/api-keys/route.test.ts index b5d27be6e1..8b8f630a09 100644 --- a/apps/sim/app/api/copilot/api-keys/route.test.ts +++ b/apps/sim/app/api/copilot/api-keys/route.test.ts @@ -3,9 +3,9 @@ * * @vitest-environment node */ +import { mockAuth, mockCryptoUuid, setupCommonApiMocks } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { mockAuth, mockCryptoUuid, setupCommonApiMocks } from '@/app/api/__test-utils__/utils' describe('Copilot API Keys API Route', () => { const mockFetch = vi.fn() diff --git a/apps/sim/app/api/copilot/chat/delete/route.test.ts b/apps/sim/app/api/copilot/chat/delete/route.test.ts index af36cfb5e0..3b19bc262e 100644 --- a/apps/sim/app/api/copilot/chat/delete/route.test.ts +++ b/apps/sim/app/api/copilot/chat/delete/route.test.ts @@ -3,14 +3,9 @@ * * @vitest-environment node */ +import { createMockRequest, mockAuth, mockCryptoUuid, setupCommonApiMocks } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { - createMockRequest, - mockAuth, - mockCryptoUuid, - setupCommonApiMocks, -} from '@/app/api/__test-utils__/utils' describe('Copilot Chat Delete API Route', () => { const mockDelete = vi.fn() diff --git a/apps/sim/app/api/copilot/chat/update-messages/route.test.ts b/apps/sim/app/api/copilot/chat/update-messages/route.test.ts index 4ab1e654b9..a196215307 100644 --- a/apps/sim/app/api/copilot/chat/update-messages/route.test.ts +++ b/apps/sim/app/api/copilot/chat/update-messages/route.test.ts @@ -3,14 +3,9 @@ * * @vitest-environment node */ +import { createMockRequest, mockAuth, mockCryptoUuid, setupCommonApiMocks } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { - createMockRequest, - mockAuth, - mockCryptoUuid, - setupCommonApiMocks, -} from '@/app/api/__test-utils__/utils' describe('Copilot Chat Update Messages API Route', () => { const mockSelect = vi.fn() diff --git a/apps/sim/app/api/copilot/chats/route.test.ts b/apps/sim/app/api/copilot/chats/route.test.ts index 8cc3bb04e5..71e74e053b 100644 --- a/apps/sim/app/api/copilot/chats/route.test.ts +++ b/apps/sim/app/api/copilot/chats/route.test.ts @@ -3,8 +3,8 @@ * * @vitest-environment node */ +import { mockCryptoUuid, setupCommonApiMocks } from '@sim/testing' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { mockCryptoUuid, setupCommonApiMocks } from '@/app/api/__test-utils__/utils' describe('Copilot Chats List API Route', () => { const mockSelect = vi.fn() diff --git a/apps/sim/app/api/copilot/checkpoints/revert/route.test.ts b/apps/sim/app/api/copilot/checkpoints/revert/route.test.ts index 9725413985..cd5c46d9e1 100644 --- a/apps/sim/app/api/copilot/checkpoints/revert/route.test.ts +++ b/apps/sim/app/api/copilot/checkpoints/revert/route.test.ts @@ -3,14 +3,9 @@ * * @vitest-environment node */ +import { createMockRequest, mockAuth, mockCryptoUuid, setupCommonApiMocks } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { - createMockRequest, - mockAuth, - mockCryptoUuid, - setupCommonApiMocks, -} from '@/app/api/__test-utils__/utils' describe('Copilot Checkpoints Revert API Route', () => { const mockSelect = vi.fn() diff --git a/apps/sim/app/api/copilot/checkpoints/route.test.ts b/apps/sim/app/api/copilot/checkpoints/route.test.ts index a344573398..5a15e37b13 100644 --- a/apps/sim/app/api/copilot/checkpoints/route.test.ts +++ b/apps/sim/app/api/copilot/checkpoints/route.test.ts @@ -3,14 +3,9 @@ * * @vitest-environment node */ +import { createMockRequest, mockAuth, mockCryptoUuid, setupCommonApiMocks } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { - createMockRequest, - mockAuth, - mockCryptoUuid, - setupCommonApiMocks, -} from '@/app/api/__test-utils__/utils' describe('Copilot Checkpoints API Route', () => { const mockSelect = vi.fn() diff --git a/apps/sim/app/api/copilot/confirm/route.test.ts b/apps/sim/app/api/copilot/confirm/route.test.ts index 6fc1bfa7e8..5bb9efd684 100644 --- a/apps/sim/app/api/copilot/confirm/route.test.ts +++ b/apps/sim/app/api/copilot/confirm/route.test.ts @@ -3,14 +3,9 @@ * * @vitest-environment node */ +import { createMockRequest, mockAuth, mockCryptoUuid, setupCommonApiMocks } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { - createMockRequest, - mockAuth, - mockCryptoUuid, - setupCommonApiMocks, -} from '@/app/api/__test-utils__/utils' describe('Copilot Confirm API Route', () => { const mockRedisExists = vi.fn() diff --git a/apps/sim/app/api/copilot/feedback/route.test.ts b/apps/sim/app/api/copilot/feedback/route.test.ts index 547d5cd3b9..5752d7a5af 100644 --- a/apps/sim/app/api/copilot/feedback/route.test.ts +++ b/apps/sim/app/api/copilot/feedback/route.test.ts @@ -3,13 +3,9 @@ * * @vitest-environment node */ +import { createMockRequest, mockCryptoUuid, setupCommonApiMocks } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { - createMockRequest, - mockCryptoUuid, - setupCommonApiMocks, -} from '@/app/api/__test-utils__/utils' describe('Copilot Feedback API Route', () => { const mockInsert = vi.fn() diff --git a/apps/sim/app/api/copilot/stats/route.test.ts b/apps/sim/app/api/copilot/stats/route.test.ts index 0d06c5edd9..35a0ad1dfc 100644 --- a/apps/sim/app/api/copilot/stats/route.test.ts +++ b/apps/sim/app/api/copilot/stats/route.test.ts @@ -3,13 +3,9 @@ * * @vitest-environment node */ +import { createMockRequest, mockCryptoUuid, setupCommonApiMocks } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { - createMockRequest, - mockCryptoUuid, - setupCommonApiMocks, -} from '@/app/api/__test-utils__/utils' describe('Copilot Stats API Route', () => { const mockFetch = vi.fn() diff --git a/apps/sim/app/api/files/delete/route.test.ts b/apps/sim/app/api/files/delete/route.test.ts index 150358c4d2..669ea86ad4 100644 --- a/apps/sim/app/api/files/delete/route.test.ts +++ b/apps/sim/app/api/files/delete/route.test.ts @@ -1,5 +1,87 @@ +import { + createMockRequest, + mockAuth, + mockCryptoUuid, + mockUuid, + setupCommonApiMocks, +} from '@sim/testing' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { createMockRequest, setupFileApiMocks } from '@/app/api/__test-utils__/utils' + +/** Setup file API mocks for file delete tests */ +function setupFileApiMocks( + options: { + authenticated?: boolean + storageProvider?: 's3' | 'blob' | 'local' + cloudEnabled?: boolean + } = {} +) { + const { authenticated = true, storageProvider = 's3', cloudEnabled = true } = options + + setupCommonApiMocks() + mockUuid() + mockCryptoUuid() + + const authMocks = mockAuth() + if (authenticated) { + authMocks.setAuthenticated() + } else { + authMocks.setUnauthenticated() + } + + vi.doMock('@/lib/auth/hybrid', () => ({ + checkHybridAuth: vi.fn().mockResolvedValue({ + success: authenticated, + userId: authenticated ? 'test-user-id' : undefined, + error: authenticated ? undefined : 'Unauthorized', + }), + })) + + vi.doMock('@/app/api/files/authorization', () => ({ + verifyFileAccess: vi.fn().mockResolvedValue(true), + verifyWorkspaceFileAccess: vi.fn().mockResolvedValue(true), + })) + + const uploadFileMock = vi.fn().mockResolvedValue({ + path: '/api/files/serve/test-key.txt', + key: 'test-key.txt', + name: 'test.txt', + size: 100, + type: 'text/plain', + }) + const downloadFileMock = vi.fn().mockResolvedValue(Buffer.from('test content')) + const deleteFileMock = vi.fn().mockResolvedValue(undefined) + const hasCloudStorageMock = vi.fn().mockReturnValue(cloudEnabled) + + vi.doMock('@/lib/uploads', () => ({ + getStorageProvider: vi.fn().mockReturnValue(storageProvider), + isUsingCloudStorage: vi.fn().mockReturnValue(cloudEnabled), + StorageService: { + uploadFile: uploadFileMock, + downloadFile: downloadFileMock, + deleteFile: deleteFileMock, + hasCloudStorage: hasCloudStorageMock, + }, + uploadFile: uploadFileMock, + downloadFile: downloadFileMock, + deleteFile: deleteFileMock, + hasCloudStorage: hasCloudStorageMock, + })) + + vi.doMock('@/lib/uploads/core/storage-service', () => ({ + uploadFile: uploadFileMock, + downloadFile: downloadFileMock, + deleteFile: deleteFileMock, + hasCloudStorage: hasCloudStorageMock, + })) + + vi.doMock('fs/promises', () => ({ + unlink: vi.fn().mockResolvedValue(undefined), + access: vi.fn().mockResolvedValue(undefined), + stat: vi.fn().mockResolvedValue({ isFile: () => true }), + })) + + return { auth: authMocks } +} describe('File Delete API Route', () => { beforeEach(() => { diff --git a/apps/sim/app/api/files/parse/route.test.ts b/apps/sim/app/api/files/parse/route.test.ts index fa0793648d..801795570a 100644 --- a/apps/sim/app/api/files/parse/route.test.ts +++ b/apps/sim/app/api/files/parse/route.test.ts @@ -1,12 +1,59 @@ import path from 'path' -import { NextRequest } from 'next/server' /** * Tests for file parse API route * * @vitest-environment node */ +import { + createMockRequest, + mockAuth, + mockCryptoUuid, + mockUuid, + setupCommonApiMocks, +} from '@sim/testing' +import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { createMockRequest, setupFileApiMocks } from '@/app/api/__test-utils__/utils' + +function setupFileApiMocks( + options: { + authenticated?: boolean + storageProvider?: 's3' | 'blob' | 'local' + cloudEnabled?: boolean + } = {} +) { + const { authenticated = true, storageProvider = 's3', cloudEnabled = true } = options + + setupCommonApiMocks() + mockUuid() + mockCryptoUuid() + + const authMocks = mockAuth() + if (authenticated) { + authMocks.setAuthenticated() + } else { + authMocks.setUnauthenticated() + } + + vi.doMock('@/lib/auth/hybrid', () => ({ + checkHybridAuth: vi.fn().mockResolvedValue({ + success: authenticated, + userId: authenticated ? 'test-user-id' : undefined, + error: authenticated ? undefined : 'Unauthorized', + }), + })) + + vi.doMock('@/app/api/files/authorization', () => ({ + verifyFileAccess: vi.fn().mockResolvedValue(true), + verifyWorkspaceFileAccess: vi.fn().mockResolvedValue(true), + })) + + vi.doMock('@/lib/uploads', () => ({ + getStorageProvider: vi.fn().mockReturnValue(storageProvider), + isUsingCloudStorage: vi.fn().mockReturnValue(cloudEnabled), + })) + + return { auth: authMocks } +} const mockJoin = vi.fn((...args: string[]): string => { if (args[0] === '/test/uploads') { diff --git a/apps/sim/app/api/files/presigned/route.test.ts b/apps/sim/app/api/files/presigned/route.test.ts index 6dcac5c62b..0721269382 100644 --- a/apps/sim/app/api/files/presigned/route.test.ts +++ b/apps/sim/app/api/files/presigned/route.test.ts @@ -1,6 +1,6 @@ +import { mockAuth, mockCryptoUuid, mockUuid, setupCommonApiMocks } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { setupFileApiMocks } from '@/app/api/__test-utils__/utils' /** * Tests for file presigned API route @@ -8,6 +8,106 @@ import { setupFileApiMocks } from '@/app/api/__test-utils__/utils' * @vitest-environment node */ +function setupFileApiMocks( + options: { + authenticated?: boolean + storageProvider?: 's3' | 'blob' | 'local' + cloudEnabled?: boolean + } = {} +) { + const { authenticated = true, storageProvider = 's3', cloudEnabled = true } = options + + setupCommonApiMocks() + mockUuid() + mockCryptoUuid() + + const authMocks = mockAuth() + if (authenticated) { + authMocks.setAuthenticated() + } else { + authMocks.setUnauthenticated() + } + + vi.doMock('@/lib/auth/hybrid', () => ({ + checkHybridAuth: vi.fn().mockResolvedValue({ + success: authenticated, + userId: authenticated ? 'test-user-id' : undefined, + error: authenticated ? undefined : 'Unauthorized', + }), + })) + + vi.doMock('@/app/api/files/authorization', () => ({ + verifyFileAccess: vi.fn().mockResolvedValue(true), + verifyWorkspaceFileAccess: vi.fn().mockResolvedValue(true), + })) + + const useBlobStorage = storageProvider === 'blob' && cloudEnabled + const useS3Storage = storageProvider === 's3' && cloudEnabled + + vi.doMock('@/lib/uploads/config', () => ({ + USE_BLOB_STORAGE: useBlobStorage, + USE_S3_STORAGE: useS3Storage, + UPLOAD_DIR: '/uploads', + getStorageConfig: vi.fn().mockReturnValue( + useBlobStorage + ? { + accountName: 'testaccount', + accountKey: 'testkey', + connectionString: 'testconnection', + containerName: 'testcontainer', + } + : { + bucket: 'test-bucket', + region: 'us-east-1', + } + ), + isUsingCloudStorage: vi.fn().mockReturnValue(cloudEnabled), + getStorageProvider: vi + .fn() + .mockReturnValue( + storageProvider === 'blob' ? 'Azure Blob' : storageProvider === 's3' ? 'S3' : 'Local' + ), + })) + + const mockGeneratePresignedUploadUrl = vi.fn().mockImplementation(async (opts) => { + const timestamp = Date.now() + const safeFileName = opts.fileName.replace(/[^a-zA-Z0-9.-]/g, '_') + const key = `${opts.context}/${timestamp}-ik3a6w4-${safeFileName}` + return { + url: 'https://example.com/presigned-url', + key, + } + }) + + vi.doMock('@/lib/uploads/core/storage-service', () => ({ + hasCloudStorage: vi.fn().mockReturnValue(cloudEnabled), + generatePresignedUploadUrl: mockGeneratePresignedUploadUrl, + generatePresignedDownloadUrl: vi.fn().mockResolvedValue('https://example.com/presigned-url'), + })) + + vi.doMock('@/lib/uploads/utils/validation', () => ({ + validateFileType: vi.fn().mockReturnValue(null), + })) + + vi.doMock('@/lib/uploads', () => ({ + CopilotFiles: { + generateCopilotUploadUrl: vi.fn().mockResolvedValue({ + url: 'https://example.com/presigned-url', + key: 'copilot/test-key.txt', + }), + isImageFileType: vi.fn().mockReturnValue(true), + }, + getStorageProvider: vi + .fn() + .mockReturnValue( + storageProvider === 'blob' ? 'Azure Blob' : storageProvider === 's3' ? 'S3' : 'Local' + ), + isUsingCloudStorage: vi.fn().mockReturnValue(cloudEnabled), + })) + + return { auth: authMocks } +} + describe('/api/files/presigned', () => { beforeEach(() => { vi.clearAllMocks() @@ -210,7 +310,7 @@ describe('/api/files/presigned', () => { const data = await response.json() expect(response.status).toBe(200) - expect(data.fileInfo.key).toMatch(/^kb\/.*knowledge-doc\.pdf$/) + expect(data.fileInfo.key).toMatch(/^knowledge-base\/.*knowledge-doc\.pdf$/) expect(data.directUploadSupported).toBe(true) }) diff --git a/apps/sim/app/api/files/serve/[...path]/route.test.ts b/apps/sim/app/api/files/serve/[...path]/route.test.ts index e5ce18bb8b..fe833f3aa3 100644 --- a/apps/sim/app/api/files/serve/[...path]/route.test.ts +++ b/apps/sim/app/api/files/serve/[...path]/route.test.ts @@ -1,11 +1,49 @@ -import { NextRequest } from 'next/server' /** * Tests for file serve API route * * @vitest-environment node */ +import { + defaultMockUser, + mockAuth, + mockCryptoUuid, + mockUuid, + setupCommonApiMocks, +} from '@sim/testing' +import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { setupApiTestMocks } from '@/app/api/__test-utils__/utils' + +function setupApiTestMocks( + options: { + authenticated?: boolean + user?: { id: string; email: string } + withFileSystem?: boolean + withUploadUtils?: boolean + } = {} +) { + const { authenticated = true, user = defaultMockUser, withFileSystem = false } = options + + setupCommonApiMocks() + mockUuid() + mockCryptoUuid() + + const authMocks = mockAuth(user) + if (authenticated) { + authMocks.setAuthenticated(user) + } else { + authMocks.setUnauthenticated() + } + + if (withFileSystem) { + vi.doMock('fs/promises', () => ({ + readFile: vi.fn().mockResolvedValue(Buffer.from('test content')), + access: vi.fn().mockResolvedValue(undefined), + stat: vi.fn().mockResolvedValue({ isFile: () => true, size: 100 }), + })) + } + + return { auth: authMocks } +} describe('File Serve API Route', () => { beforeEach(() => { @@ -31,6 +69,17 @@ describe('File Serve API Route', () => { existsSync: vi.fn().mockReturnValue(true), })) + vi.doMock('@/lib/uploads', () => ({ + CopilotFiles: { + downloadCopilotFile: vi.fn(), + }, + isUsingCloudStorage: vi.fn().mockReturnValue(false), + })) + + vi.doMock('@/lib/uploads/utils/file-utils', () => ({ + inferContextFromKey: vi.fn().mockReturnValue('workspace'), + })) + vi.doMock('@/app/api/files/utils', () => ({ FileNotFoundError: class FileNotFoundError extends Error { constructor(message: string) { @@ -126,6 +175,17 @@ describe('File Serve API Route', () => { verifyFileAccess: vi.fn().mockResolvedValue(true), })) + vi.doMock('@/lib/uploads', () => ({ + CopilotFiles: { + downloadCopilotFile: vi.fn(), + }, + isUsingCloudStorage: vi.fn().mockReturnValue(false), + })) + + vi.doMock('@/lib/uploads/utils/file-utils', () => ({ + inferContextFromKey: vi.fn().mockReturnValue('workspace'), + })) + const req = new NextRequest( 'http://localhost:3000/api/files/serve/workspace/test-workspace-id/nested-path-file.txt' ) diff --git a/apps/sim/app/api/files/upload/route.test.ts b/apps/sim/app/api/files/upload/route.test.ts index 35f580abd8..a5ecc030b8 100644 --- a/apps/sim/app/api/files/upload/route.test.ts +++ b/apps/sim/app/api/files/upload/route.test.ts @@ -1,11 +1,76 @@ -import { NextRequest } from 'next/server' /** * Tests for file upload API route * * @vitest-environment node */ +import { mockAuth, mockCryptoUuid, mockUuid, setupCommonApiMocks } from '@sim/testing' +import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { setupFileApiMocks } from '@/app/api/__test-utils__/utils' + +function setupFileApiMocks( + options: { + authenticated?: boolean + storageProvider?: 's3' | 'blob' | 'local' + cloudEnabled?: boolean + } = {} +) { + const { authenticated = true, storageProvider = 's3', cloudEnabled = true } = options + + setupCommonApiMocks() + mockUuid() + mockCryptoUuid() + + const authMocks = mockAuth() + if (authenticated) { + authMocks.setAuthenticated() + } else { + authMocks.setUnauthenticated() + } + + vi.doMock('@/lib/auth/hybrid', () => ({ + checkHybridAuth: vi.fn().mockResolvedValue({ + success: authenticated, + userId: authenticated ? 'test-user-id' : undefined, + error: authenticated ? undefined : 'Unauthorized', + }), + })) + + vi.doMock('@/app/api/files/authorization', () => ({ + verifyFileAccess: vi.fn().mockResolvedValue(true), + verifyWorkspaceFileAccess: vi.fn().mockResolvedValue(true), + verifyKBFileAccess: vi.fn().mockResolvedValue(true), + verifyCopilotFileAccess: vi.fn().mockResolvedValue(true), + })) + + vi.doMock('@/lib/uploads/contexts/workspace', () => ({ + uploadWorkspaceFile: vi.fn().mockResolvedValue({ + id: 'test-file-id', + name: 'test.txt', + url: '/api/files/serve/workspace/test-workspace-id/test-file.txt', + size: 100, + type: 'text/plain', + key: 'workspace/test-workspace-id/1234567890-test.txt', + uploadedAt: new Date().toISOString(), + expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + }), + })) + + const uploadFileMock = vi.fn().mockResolvedValue({ + path: '/api/files/serve/test-key.txt', + key: 'test-key.txt', + name: 'test.txt', + size: 100, + type: 'text/plain', + }) + + vi.doMock('@/lib/uploads', () => ({ + getStorageProvider: vi.fn().mockReturnValue(storageProvider), + isUsingCloudStorage: vi.fn().mockReturnValue(cloudEnabled), + uploadFile: uploadFileMock, + })) + + return { auth: authMocks } +} describe('File Upload API Route', () => { const createMockFormData = (files: File[], context = 'workspace'): FormData => { diff --git a/apps/sim/app/api/folders/[id]/route.test.ts b/apps/sim/app/api/folders/[id]/route.test.ts index 5b5f3c8c28..ce25228802 100644 --- a/apps/sim/app/api/folders/[id]/route.test.ts +++ b/apps/sim/app/api/folders/[id]/route.test.ts @@ -3,15 +3,24 @@ * * @vitest-environment node */ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { - type CapturedFolderValues, createMockRequest, type MockUser, mockAuth, - mockLogger, + mockConsoleLogger, setupCommonApiMocks, -} from '@/app/api/__test-utils__/utils' +} from '@sim/testing' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +/** Type for captured folder values in tests */ +interface CapturedFolderValues { + name?: string + color?: string + parentId?: string | null + isExpanded?: boolean + sortOrder?: number + updatedAt?: Date +} interface FolderDbMockOptions { folderLookupResult?: any @@ -21,6 +30,8 @@ interface FolderDbMockOptions { } describe('Individual Folder API Route', () => { + let mockLogger: ReturnType + const TEST_USER: MockUser = { id: 'user-123', email: 'test@example.com', @@ -39,7 +50,8 @@ describe('Individual Folder API Route', () => { updatedAt: new Date('2024-01-01T00:00:00Z'), } - const { mockAuthenticatedUser, mockUnauthenticated } = mockAuth(TEST_USER) + let mockAuthenticatedUser: (user?: MockUser) => void + let mockUnauthenticated: () => void const mockGetUserEntityPermissions = vi.fn() function createFolderDbMock(options: FolderDbMockOptions = {}) { @@ -110,6 +122,10 @@ describe('Individual Folder API Route', () => { vi.resetModules() vi.clearAllMocks() setupCommonApiMocks() + mockLogger = mockConsoleLogger() + const auth = mockAuth(TEST_USER) + mockAuthenticatedUser = auth.mockAuthenticatedUser + mockUnauthenticated = auth.mockUnauthenticated mockGetUserEntityPermissions.mockResolvedValue('admin') diff --git a/apps/sim/app/api/folders/route.test.ts b/apps/sim/app/api/folders/route.test.ts index d7da4f779c..6ad39d75ec 100644 --- a/apps/sim/app/api/folders/route.test.ts +++ b/apps/sim/app/api/folders/route.test.ts @@ -3,17 +3,46 @@ * * @vitest-environment node */ +import { createMockRequest, mockAuth, mockConsoleLogger, setupCommonApiMocks } from '@sim/testing' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { - type CapturedFolderValues, - createMockRequest, - createMockTransaction, - mockAuth, - mockLogger, - setupCommonApiMocks, -} from '@/app/api/__test-utils__/utils' + +interface CapturedFolderValues { + name?: string + color?: string + parentId?: string | null + isExpanded?: boolean + sortOrder?: number + updatedAt?: Date +} + +function createMockTransaction(mockData: { + selectData?: Array<{ id: string; [key: string]: unknown }> + insertResult?: Array<{ id: string; [key: string]: unknown }> +}) { + const { selectData = [], insertResult = [] } = mockData + return vi.fn().mockImplementation(async (callback: (tx: unknown) => Promise) => { + const tx = { + select: vi.fn().mockReturnValue({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + orderBy: vi.fn().mockReturnValue({ + limit: vi.fn().mockReturnValue(selectData), + }), + }), + }), + }), + insert: vi.fn().mockReturnValue({ + values: vi.fn().mockReturnValue({ + returning: vi.fn().mockReturnValue(insertResult), + }), + }), + } + return await callback(tx) + }) +} describe('Folders API Route', () => { + let mockLogger: ReturnType const mockFolders = [ { id: 'folder-1', @@ -41,7 +70,8 @@ describe('Folders API Route', () => { }, ] - const { mockAuthenticatedUser, mockUnauthenticated } = mockAuth() + let mockAuthenticatedUser: () => void + let mockUnauthenticated: () => void const mockUUID = 'mock-uuid-12345678-90ab-cdef-1234-567890abcdef' const mockSelect = vi.fn() @@ -63,6 +93,10 @@ describe('Folders API Route', () => { }) setupCommonApiMocks() + mockLogger = mockConsoleLogger() + const auth = mockAuth() + mockAuthenticatedUser = auth.mockAuthenticatedUser + mockUnauthenticated = auth.mockUnauthenticated mockSelect.mockReturnValue({ from: mockFrom }) mockFrom.mockReturnValue({ where: mockWhere }) diff --git a/apps/sim/app/api/form/[identifier]/route.ts b/apps/sim/app/api/form/[identifier]/route.ts index bfae3e36e0..907051f92a 100644 --- a/apps/sim/app/api/form/[identifier]/route.ts +++ b/apps/sim/app/api/form/[identifier]/route.ts @@ -9,6 +9,7 @@ import { addCorsHeaders, validateAuthToken } from '@/lib/core/security/deploymen import { generateRequestId } from '@/lib/core/utils/request' import { preprocessExecution } from '@/lib/execution/preprocessing' import { LoggingSession } from '@/lib/logs/execution/logging-session' +import { normalizeInputFormatValue } from '@/lib/workflows/input-format' import { createStreamingResponse } from '@/lib/workflows/streaming/streaming' import { setFormAuthCookie, validateFormAuth } from '@/app/api/form/utils' import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils' @@ -34,22 +35,17 @@ async function getWorkflowInputSchema(workflowId: string): Promise { .from(workflowBlocks) .where(eq(workflowBlocks.workflowId, workflowId)) - // Find the start block (starter or start_trigger type) const startBlock = blocks.find( - (block) => block.type === 'starter' || block.type === 'start_trigger' + (block) => + block.type === 'starter' || block.type === 'start_trigger' || block.type === 'input_trigger' ) if (!startBlock) { return [] } - // Extract inputFormat from subBlocks const subBlocks = startBlock.subBlocks as Record | null - if (!subBlocks?.inputFormat?.value) { - return [] - } - - return Array.isArray(subBlocks.inputFormat.value) ? subBlocks.inputFormat.value : [] + return normalizeInputFormatValue(subBlocks?.inputFormat?.value) } catch (error) { logger.error('Error fetching workflow input schema:', error) return [] diff --git a/apps/sim/app/api/function/execute/route.test.ts b/apps/sim/app/api/function/execute/route.test.ts index 783b89d1b2..45abbb3212 100644 --- a/apps/sim/app/api/function/execute/route.test.ts +++ b/apps/sim/app/api/function/execute/route.test.ts @@ -3,10 +3,9 @@ * * @vitest-environment node */ -import { loggerMock } from '@sim/testing' +import { createMockRequest, loggerMock } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { createMockRequest } from '@/app/api/__test-utils__/utils' vi.mock('@/lib/execution/isolated-vm', () => ({ executeInIsolatedVM: vi.fn().mockImplementation(async (req) => { diff --git a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.test.ts b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.test.ts index 710d9eea83..6b63ac13fc 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.test.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.test.ts @@ -3,14 +3,14 @@ * * @vitest-environment node */ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { createMockRequest, mockAuth, mockConsoleLogger, mockDrizzleOrm, mockKnowledgeSchemas, -} from '@/app/api/__test-utils__/utils' +} from '@sim/testing' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' mockKnowledgeSchemas() diff --git a/apps/sim/app/api/knowledge/[id]/documents/route.test.ts b/apps/sim/app/api/knowledge/[id]/documents/route.test.ts index 2b22613f6e..e826de12d7 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/route.test.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/route.test.ts @@ -3,14 +3,14 @@ * * @vitest-environment node */ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { createMockRequest, mockAuth, mockConsoleLogger, mockDrizzleOrm, mockKnowledgeSchemas, -} from '@/app/api/__test-utils__/utils' +} from '@sim/testing' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' mockKnowledgeSchemas() diff --git a/apps/sim/app/api/knowledge/[id]/route.test.ts b/apps/sim/app/api/knowledge/[id]/route.test.ts index 9d64bf5caf..20bbc710f9 100644 --- a/apps/sim/app/api/knowledge/[id]/route.test.ts +++ b/apps/sim/app/api/knowledge/[id]/route.test.ts @@ -3,14 +3,14 @@ * * @vitest-environment node */ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { createMockRequest, mockAuth, mockConsoleLogger, mockDrizzleOrm, mockKnowledgeSchemas, -} from '@/app/api/__test-utils__/utils' +} from '@sim/testing' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' mockKnowledgeSchemas() mockDrizzleOrm() diff --git a/apps/sim/app/api/knowledge/route.test.ts b/apps/sim/app/api/knowledge/route.test.ts index e72e7671a3..2a59f45409 100644 --- a/apps/sim/app/api/knowledge/route.test.ts +++ b/apps/sim/app/api/knowledge/route.test.ts @@ -3,14 +3,14 @@ * * @vitest-environment node */ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { createMockRequest, mockAuth, mockConsoleLogger, mockDrizzleOrm, mockKnowledgeSchemas, -} from '@/app/api/__test-utils__/utils' +} from '@sim/testing' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' mockKnowledgeSchemas() mockDrizzleOrm() diff --git a/apps/sim/app/api/knowledge/search/route.test.ts b/apps/sim/app/api/knowledge/search/route.test.ts index 04259062e7..d5748b1063 100644 --- a/apps/sim/app/api/knowledge/search/route.test.ts +++ b/apps/sim/app/api/knowledge/search/route.test.ts @@ -5,13 +5,13 @@ * * @vitest-environment node */ -import { createEnvMock } from '@sim/testing' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { + createEnvMock, createMockRequest, mockConsoleLogger, mockKnowledgeSchemas, -} from '@/app/api/__test-utils__/utils' +} from '@sim/testing' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('drizzle-orm', () => ({ and: vi.fn().mockImplementation((...args) => ({ and: args })), diff --git a/apps/sim/app/api/tools/custom/route.test.ts b/apps/sim/app/api/tools/custom/route.test.ts index da83f66153..1d990546c4 100644 --- a/apps/sim/app/api/tools/custom/route.test.ts +++ b/apps/sim/app/api/tools/custom/route.test.ts @@ -3,10 +3,9 @@ * * @vitest-environment node */ -import { loggerMock } from '@sim/testing' +import { createMockRequest, loggerMock } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { createMockRequest } from '@/app/api/__test-utils__/utils' describe('Custom Tools API Routes', () => { const sampleTools = [ @@ -364,7 +363,7 @@ describe('Custom Tools API Routes', () => { }) it('should reject requests missing tool ID', async () => { - const req = createMockRequest('DELETE') + const req = new NextRequest('http://localhost:3000/api/tools/custom') const { DELETE } = await import('@/app/api/tools/custom/route') diff --git a/apps/sim/app/api/webhooks/trigger/[path]/route.test.ts b/apps/sim/app/api/webhooks/trigger/[path]/route.test.ts index fff521ca8f..737e5ac48b 100644 --- a/apps/sim/app/api/webhooks/trigger/[path]/route.test.ts +++ b/apps/sim/app/api/webhooks/trigger/[path]/route.test.ts @@ -3,15 +3,92 @@ * * @vitest-environment node */ - -import { loggerMock } from '@sim/testing' +import { createMockRequest, loggerMock } from '@sim/testing' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { - createMockRequest, - globalMockData, - mockExecutionDependencies, - mockTriggerDevSdk, -} from '@/app/api/__test-utils__/utils' + +/** Mock execution dependencies for webhook tests */ +function mockExecutionDependencies() { + vi.mock('@/lib/core/security/encryption', () => ({ + decryptSecret: vi.fn().mockResolvedValue({ decrypted: 'decrypted-value' }), + })) + + vi.mock('@/lib/logs/execution/trace-spans/trace-spans', () => ({ + buildTraceSpans: vi.fn().mockReturnValue({ traceSpans: [], totalDuration: 100 }), + })) + + vi.mock('@/lib/workflows/utils', () => ({ + updateWorkflowRunCounts: vi.fn().mockResolvedValue(undefined), + })) + + vi.mock('@/serializer', () => ({ + Serializer: vi.fn().mockImplementation(() => ({ + serializeWorkflow: vi.fn().mockReturnValue({ + version: '1.0', + blocks: [ + { + id: 'starter-id', + metadata: { id: 'starter', name: 'Start' }, + config: {}, + inputs: {}, + outputs: {}, + position: { x: 100, y: 100 }, + enabled: true, + }, + { + id: 'agent-id', + metadata: { id: 'agent', name: 'Agent 1' }, + config: {}, + inputs: {}, + outputs: {}, + position: { x: 634, y: -167 }, + enabled: true, + }, + ], + edges: [ + { + id: 'edge-1', + source: 'starter-id', + target: 'agent-id', + sourceHandle: 'source', + targetHandle: 'target', + }, + ], + loops: {}, + parallels: {}, + }), + })), + })) +} + +/** Mock Trigger.dev SDK */ +function mockTriggerDevSdk() { + vi.mock('@trigger.dev/sdk', () => ({ + tasks: { trigger: vi.fn().mockResolvedValue({ id: 'mock-task-id' }) }, + task: vi.fn().mockReturnValue({}), + })) +} + +/** + * Test data store - isolated per test via beforeEach reset + * This replaces the global mutable state pattern with local test data + */ +const testData = { + webhooks: [] as Array<{ + id: string + provider: string + path: string + isActive: boolean + providerConfig?: Record + workflowId: string + rateLimitCount?: number + rateLimitPeriod?: number + }>, + workflows: [] as Array<{ + id: string + userId: string + workspaceId?: string + }>, +} const { generateRequestHashMock, @@ -159,8 +236,8 @@ vi.mock('@/lib/workflows/persistence/utils', () => ({ vi.mock('@/lib/webhooks/processor', () => ({ findAllWebhooksForPath: vi.fn().mockImplementation(async (options: { path: string }) => { - // Filter webhooks by path from globalMockData - const matchingWebhooks = globalMockData.webhooks.filter( + // Filter webhooks by path from testData + const matchingWebhooks = testData.webhooks.filter( (wh) => wh.path === options.path && wh.isActive ) @@ -170,7 +247,7 @@ vi.mock('@/lib/webhooks/processor', () => ({ // Return array of {webhook, workflow} objects return matchingWebhooks.map((wh) => { - const matchingWorkflow = globalMockData.workflows.find((w) => w.id === wh.workflowId) || { + const matchingWorkflow = testData.workflows.find((w) => w.id === wh.workflowId) || { id: wh.workflowId || 'test-workflow-id', userId: 'test-user-id', workspaceId: 'test-workspace-id', @@ -283,14 +360,15 @@ describe('Webhook Trigger API Route', () => { beforeEach(() => { vi.clearAllMocks() - globalMockData.webhooks.length = 0 - globalMockData.workflows.length = 0 - globalMockData.schedules.length = 0 + // Reset test data arrays + testData.webhooks.length = 0 + testData.workflows.length = 0 mockExecutionDependencies() mockTriggerDevSdk() - globalMockData.workflows.push({ + // Set up default workflow for tests + testData.workflows.push({ id: 'test-workflow-id', userId: 'test-user-id', workspaceId: 'test-workspace-id', @@ -326,7 +404,7 @@ describe('Webhook Trigger API Route', () => { describe('Generic Webhook Authentication', () => { it('should process generic webhook without authentication', async () => { - globalMockData.webhooks.push({ + testData.webhooks.push({ id: 'generic-webhook-id', provider: 'generic', path: 'test-path', @@ -336,7 +414,7 @@ describe('Webhook Trigger API Route', () => { rateLimitCount: 100, rateLimitPeriod: 60, }) - globalMockData.workflows.push({ + testData.workflows.push({ id: 'test-workflow-id', userId: 'test-user-id', workspaceId: 'test-workspace-id', @@ -354,7 +432,7 @@ describe('Webhook Trigger API Route', () => { }) it('should authenticate with Bearer token when no custom header is configured', async () => { - globalMockData.webhooks.push({ + testData.webhooks.push({ id: 'generic-webhook-id', provider: 'generic', path: 'test-path', @@ -362,7 +440,7 @@ describe('Webhook Trigger API Route', () => { providerConfig: { requireAuth: true, token: 'test-token-123' }, workflowId: 'test-workflow-id', }) - globalMockData.workflows.push({ + testData.workflows.push({ id: 'test-workflow-id', userId: 'test-user-id', workspaceId: 'test-workspace-id', @@ -381,7 +459,7 @@ describe('Webhook Trigger API Route', () => { }) it('should authenticate with custom header when configured', async () => { - globalMockData.webhooks.push({ + testData.webhooks.push({ id: 'generic-webhook-id', provider: 'generic', path: 'test-path', @@ -393,7 +471,7 @@ describe('Webhook Trigger API Route', () => { }, workflowId: 'test-workflow-id', }) - globalMockData.workflows.push({ + testData.workflows.push({ id: 'test-workflow-id', userId: 'test-user-id', workspaceId: 'test-workspace-id', @@ -412,7 +490,7 @@ describe('Webhook Trigger API Route', () => { }) it('should handle case insensitive Bearer token authentication', async () => { - globalMockData.webhooks.push({ + testData.webhooks.push({ id: 'generic-webhook-id', provider: 'generic', path: 'test-path', @@ -420,7 +498,7 @@ describe('Webhook Trigger API Route', () => { providerConfig: { requireAuth: true, token: 'case-test-token' }, workflowId: 'test-workflow-id', }) - globalMockData.workflows.push({ + testData.workflows.push({ id: 'test-workflow-id', userId: 'test-user-id', workspaceId: 'test-workspace-id', @@ -454,7 +532,7 @@ describe('Webhook Trigger API Route', () => { }) it('should handle case insensitive custom header authentication', async () => { - globalMockData.webhooks.push({ + testData.webhooks.push({ id: 'generic-webhook-id', provider: 'generic', path: 'test-path', @@ -466,7 +544,7 @@ describe('Webhook Trigger API Route', () => { }, workflowId: 'test-workflow-id', }) - globalMockData.workflows.push({ + testData.workflows.push({ id: 'test-workflow-id', userId: 'test-user-id', workspaceId: 'test-workspace-id', @@ -495,7 +573,7 @@ describe('Webhook Trigger API Route', () => { }) it('should reject wrong Bearer token', async () => { - globalMockData.webhooks.push({ + testData.webhooks.push({ id: 'generic-webhook-id', provider: 'generic', path: 'test-path', @@ -519,7 +597,7 @@ describe('Webhook Trigger API Route', () => { }) it('should reject wrong custom header token', async () => { - globalMockData.webhooks.push({ + testData.webhooks.push({ id: 'generic-webhook-id', provider: 'generic', path: 'test-path', @@ -547,7 +625,7 @@ describe('Webhook Trigger API Route', () => { }) it('should reject missing authentication when required', async () => { - globalMockData.webhooks.push({ + testData.webhooks.push({ id: 'generic-webhook-id', provider: 'generic', path: 'test-path', @@ -567,7 +645,7 @@ describe('Webhook Trigger API Route', () => { }) it('should reject Bearer token when custom header is configured', async () => { - globalMockData.webhooks.push({ + testData.webhooks.push({ id: 'generic-webhook-id', provider: 'generic', path: 'test-path', @@ -595,7 +673,7 @@ describe('Webhook Trigger API Route', () => { }) it('should reject wrong custom header name', async () => { - globalMockData.webhooks.push({ + testData.webhooks.push({ id: 'generic-webhook-id', provider: 'generic', path: 'test-path', @@ -623,7 +701,7 @@ describe('Webhook Trigger API Route', () => { }) it('should reject when auth is required but no token is configured', async () => { - globalMockData.webhooks.push({ + testData.webhooks.push({ id: 'generic-webhook-id', provider: 'generic', path: 'test-path', @@ -631,7 +709,7 @@ describe('Webhook Trigger API Route', () => { providerConfig: { requireAuth: true }, workflowId: 'test-workflow-id', }) - globalMockData.workflows.push({ id: 'test-workflow-id', userId: 'test-user-id' }) + testData.workflows.push({ id: 'test-workflow-id', userId: 'test-user-id' }) const headers = { 'Content-Type': 'application/json', diff --git a/apps/sim/app/api/workflows/[id]/variables/route.test.ts b/apps/sim/app/api/workflows/[id]/variables/route.test.ts index b2485fa408..949b52ebc4 100644 --- a/apps/sim/app/api/workflows/[id]/variables/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/variables/route.test.ts @@ -4,29 +4,29 @@ * * @vitest-environment node */ - -import { NextRequest } from 'next/server' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { - createMockDatabase, + databaseMock, + defaultMockUser, mockAuth, mockCryptoUuid, - mockUser, setupCommonApiMocks, -} from '@/app/api/__test-utils__/utils' +} from '@sim/testing' +import { NextRequest } from 'next/server' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' describe('Workflow Variables API Route', () => { let authMocks: ReturnType - let databaseMocks: ReturnType const mockGetWorkflowAccessContext = vi.fn() beforeEach(() => { vi.resetModules() setupCommonApiMocks() mockCryptoUuid('mock-request-id-12345678') - authMocks = mockAuth(mockUser) + authMocks = mockAuth(defaultMockUser) mockGetWorkflowAccessContext.mockReset() + vi.doMock('@sim/db', () => databaseMock) + vi.doMock('@/lib/workflows/utils', () => ({ getWorkflowAccessContext: mockGetWorkflowAccessContext, })) @@ -203,10 +203,6 @@ describe('Workflow Variables API Route', () => { isWorkspaceOwner: false, }) - databaseMocks = createMockDatabase({ - update: { results: [{}] }, - }) - const variables = { 'var-1': { id: 'var-1', diff --git a/apps/sim/app/api/workspaces/invitations/route.test.ts b/apps/sim/app/api/workspaces/invitations/route.test.ts index f56e9d0120..202559142a 100644 --- a/apps/sim/app/api/workspaces/invitations/route.test.ts +++ b/apps/sim/app/api/workspaces/invitations/route.test.ts @@ -1,5 +1,5 @@ +import { createMockRequest, mockAuth, mockConsoleLogger } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { createMockRequest, mockAuth, mockConsoleLogger } from '@/app/api/__test-utils__/utils' describe('Workspace Invitations API Route', () => { const mockWorkspace = { id: 'workspace-1', name: 'Test Workspace' } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx index b2a3349272..cc1bf0403a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx @@ -30,7 +30,7 @@ import { extractPathFromOutputId, parseOutputContentSafely, } from '@/lib/core/utils/response-format' -import { normalizeInputFormatValue } from '@/lib/workflows/input-format-utils' +import { normalizeInputFormatValue } from '@/lib/workflows/input-format' import { StartBlockPath, TriggerUtils } from '@/lib/workflows/triggers/triggers' import { START_BLOCK_RESERVED_FIELDS } from '@/lib/workflows/types' import { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/a2a.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/a2a.tsx index 382fe5e513..d03470f34f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/a2a.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/a2a.tsx @@ -22,7 +22,7 @@ import { import { Skeleton } from '@/components/ui' import type { AgentAuthentication, AgentCapabilities } from '@/lib/a2a/types' import { getBaseUrl } from '@/lib/core/utils/urls' -import { normalizeInputFormatValue } from '@/lib/workflows/input-format-utils' +import { normalizeInputFormatValue } from '@/lib/workflows/input-format' import { StartBlockPath, TriggerUtils } from '@/lib/workflows/triggers/triggers' import { useA2AAgentByWorkflow, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp/mcp.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp/mcp.tsx index 236af44e07..31012a05a6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp/mcp.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp/mcp.tsx @@ -14,7 +14,7 @@ import { } from '@/components/emcn' import { Skeleton } from '@/components/ui' import { generateToolInputSchema, sanitizeToolName } from '@/lib/mcp/workflow-tool-schema' -import { normalizeInputFormatValue } from '@/lib/workflows/input-format-utils' +import { normalizeInputFormatValue } from '@/lib/workflows/input-format' import { isValidStartBlockType } from '@/lib/workflows/triggers/trigger-utils' import type { InputFormatField } from '@/lib/workflows/types' import { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/input-mapping/input-mapping.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/input-mapping/input-mapping.tsx index 7b3dfb973a..c095853df8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/input-mapping/input-mapping.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/input-mapping/input-mapping.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState } from 'react' +import { useMemo, useRef, useState } from 'react' import { Badge, Input } from '@/components/emcn' import { Label } from '@/components/ui/label' import { cn } from '@/lib/core/utils/cn' @@ -7,39 +7,7 @@ import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/compon import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' - -/** - * Represents a field in the input format configuration - */ -interface InputFormatField { - name: string - type?: string -} - -/** - * Represents an input trigger block structure - */ -interface InputTriggerBlock { - type: 'input_trigger' | 'start_trigger' - subBlocks?: { - inputFormat?: { value?: InputFormatField[] } - } -} - -/** - * Represents a legacy starter block structure - */ -interface StarterBlockLegacy { - type: 'starter' - subBlocks?: { - inputFormat?: { value?: InputFormatField[] } - } - config?: { - params?: { - inputFormat?: InputFormatField[] - } - } -} +import { useWorkflowInputFields } from '@/hooks/queries/workflows' /** * Props for the InputMappingField component @@ -70,73 +38,6 @@ interface InputMappingProps { disabled?: boolean } -/** - * Type guard to check if a value is an InputTriggerBlock - * @param value - The value to check - * @returns True if the value is an InputTriggerBlock - */ -function isInputTriggerBlock(value: unknown): value is InputTriggerBlock { - const type = (value as { type?: unknown }).type - return ( - !!value && typeof value === 'object' && (type === 'input_trigger' || type === 'start_trigger') - ) -} - -/** - * Type guard to check if a value is a StarterBlockLegacy - * @param value - The value to check - * @returns True if the value is a StarterBlockLegacy - */ -function isStarterBlock(value: unknown): value is StarterBlockLegacy { - return !!value && typeof value === 'object' && (value as { type?: unknown }).type === 'starter' -} - -/** - * Type guard to check if a value is an InputFormatField - * @param value - The value to check - * @returns True if the value is an InputFormatField - */ -function isInputFormatField(value: unknown): value is InputFormatField { - if (typeof value !== 'object' || value === null) return false - if (!('name' in value)) return false - const { name, type } = value as { name: unknown; type?: unknown } - if (typeof name !== 'string' || name.trim() === '') return false - if (type !== undefined && typeof type !== 'string') return false - return true -} - -/** - * Extracts input format fields from workflow blocks - * @param blocks - The workflow blocks to extract from - * @returns Array of input format fields or null if not found - */ -function extractInputFormatFields(blocks: Record): InputFormatField[] | null { - const triggerEntry = Object.entries(blocks).find(([, b]) => isInputTriggerBlock(b)) - if (triggerEntry && isInputTriggerBlock(triggerEntry[1])) { - const inputFormat = triggerEntry[1].subBlocks?.inputFormat?.value - if (Array.isArray(inputFormat)) { - return (inputFormat as unknown[]) - .filter(isInputFormatField) - .map((f) => ({ name: f.name, type: f.type })) - } - } - - const starterEntry = Object.entries(blocks).find(([, b]) => isStarterBlock(b)) - if (starterEntry && isStarterBlock(starterEntry[1])) { - const starter = starterEntry[1] - const subBlockFormat = starter.subBlocks?.inputFormat?.value - const legacyParamsFormat = starter.config?.params?.inputFormat - const chosen = Array.isArray(subBlockFormat) ? subBlockFormat : legacyParamsFormat - if (Array.isArray(chosen)) { - return (chosen as unknown[]) - .filter(isInputFormatField) - .map((f) => ({ name: f.name, type: f.type })) - } - } - - return null -} - /** * InputMapping component displays and manages input field mappings for workflow execution * @param props - The component props @@ -168,62 +69,10 @@ export function InputMapping({ const inputRefs = useRef>(new Map()) const overlayRefs = useRef>(new Map()) - const [childInputFields, setChildInputFields] = useState([]) - const [isLoading, setIsLoading] = useState(false) + const workflowId = typeof selectedWorkflowId === 'string' ? selectedWorkflowId : undefined + const { data: childInputFields = [], isLoading } = useWorkflowInputFields(workflowId) const [collapsedFields, setCollapsedFields] = useState>({}) - useEffect(() => { - let isMounted = true - const controller = new AbortController() - - async function fetchChildSchema() { - if (!selectedWorkflowId) { - if (isMounted) { - setChildInputFields([]) - setIsLoading(false) - } - return - } - - try { - if (isMounted) setIsLoading(true) - - const res = await fetch(`/api/workflows/${selectedWorkflowId}`, { - signal: controller.signal, - }) - - if (!res.ok) { - if (isMounted) { - setChildInputFields([]) - setIsLoading(false) - } - return - } - - const { data } = await res.json() - const blocks = (data?.state?.blocks as Record) || {} - const fields = extractInputFormatFields(blocks) - - if (isMounted) { - setChildInputFields(fields || []) - setIsLoading(false) - } - } catch (error) { - if (isMounted) { - setChildInputFields([]) - setIsLoading(false) - } - } - } - - fetchChildSchema() - - return () => { - isMounted = false - controller.abort() - } - }, [selectedWorkflowId]) - const valueObj: Record = useMemo(() => { if (isPreview && previewValue && typeof previewValue === 'object') { return previewValue as Record diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index ffeed6880d..cb07582380 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -1,7 +1,6 @@ import type React from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' -import { useQuery } from '@tanstack/react-query' import { Loader2, WrenchIcon, XIcon } from 'lucide-react' import { useParams } from 'next/navigation' import { @@ -61,7 +60,7 @@ import { useCustomTools, } from '@/hooks/queries/custom-tools' import { useForceRefreshMcpTools, useMcpServers, useStoredMcpTools } from '@/hooks/queries/mcp' -import { useWorkflows } from '@/hooks/queries/workflows' +import { useWorkflowInputFields, useWorkflows } from '@/hooks/queries/workflows' import { usePermissionConfig } from '@/hooks/use-permission-config' import { getProviderFromModel, supportsToolUsageControl } from '@/providers/utils' import { useSettingsModalStore } from '@/stores/modals/settings/store' @@ -645,56 +644,7 @@ function WorkflowInputMapperSyncWrapper({ disabled: boolean workflowId: string }) { - const { data: workflowData, isLoading } = useQuery({ - queryKey: ['workflow-input-fields', workflowId], - queryFn: async () => { - const response = await fetch(`/api/workflows/${workflowId}`) - if (!response.ok) throw new Error('Failed to fetch workflow') - const { data } = await response.json() - return data - }, - enabled: Boolean(workflowId), - staleTime: 60 * 1000, - }) - - const inputFields = useMemo(() => { - if (!workflowData?.state?.blocks) return [] - - const blocks = workflowData.state.blocks as Record - - const triggerEntry = Object.entries(blocks).find( - ([, block]) => - block.type === 'start_trigger' || block.type === 'input_trigger' || block.type === 'starter' - ) - - if (!triggerEntry) return [] - - const triggerBlock = triggerEntry[1] - - const inputFormat = triggerBlock.subBlocks?.inputFormat?.value - - if (Array.isArray(inputFormat)) { - return inputFormat - .filter((field: any) => field.name && typeof field.name === 'string') - .map((field: any) => ({ - name: field.name, - type: field.type || 'string', - })) - } - - const legacyFormat = triggerBlock.config?.params?.inputFormat - - if (Array.isArray(legacyFormat)) { - return legacyFormat - .filter((field: any) => field.name && typeof field.name === 'string') - .map((field: any) => ({ - name: field.name, - type: field.type || 'string', - })) - } - - return [] - }, [workflowData]) + const { data: inputFields = [], isLoading } = useWorkflowInputFields(workflowId) const parsedValue = useMemo(() => { try { diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx index a188e9bf84..35820c7d10 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx @@ -136,7 +136,6 @@ export function WorkspaceHeader({ const [editingWorkspaceId, setEditingWorkspaceId] = useState(null) const [editingName, setEditingName] = useState('') const [isListRenaming, setIsListRenaming] = useState(false) - const listRenameInputRef = useRef(null) const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 }) const [isContextMenuOpen, setIsContextMenuOpen] = useState(false) @@ -146,6 +145,10 @@ export function WorkspaceHeader({ name: string permissions?: 'admin' | 'write' | 'read' | null } | null>(null) + const isRenamingRef = useRef(false) + const isContextMenuOpeningRef = useRef(false) + const contextMenuClosedRef = useRef(true) + const hasInputFocusedRef = useRef(false) const [isMounted, setIsMounted] = useState(false) useEffect(() => { @@ -165,20 +168,6 @@ export function WorkspaceHeader({ return () => window.removeEventListener('open-invite-modal', handleOpenInvite) }, [isInvitationsDisabled]) - /** - * Focus the inline list rename input when it becomes active - */ - useEffect(() => { - if (editingWorkspaceId && listRenameInputRef.current) { - try { - listRenameInputRef.current.focus() - listRenameInputRef.current.select() - } catch { - // no-op - } - } - }, [editingWorkspaceId]) - /** * Save and exit edit mode when popover closes */ @@ -201,6 +190,9 @@ export function WorkspaceHeader({ e.preventDefault() e.stopPropagation() + isContextMenuOpeningRef.current = true + contextMenuClosedRef.current = false + capturedWorkspaceRef.current = { id: workspace.id, name: workspace.name, @@ -211,11 +203,22 @@ export function WorkspaceHeader({ } /** - * Close context menu and the workspace dropdown + * Close context menu and optionally the workspace dropdown + * When renaming, we keep the workspace menu open so the input is visible + * This function is idempotent - duplicate calls are ignored */ const closeContextMenu = () => { + if (contextMenuClosedRef.current) { + return + } + contextMenuClosedRef.current = true + setIsContextMenuOpen(false) - setIsWorkspaceMenuOpen(false) + isContextMenuOpeningRef.current = false + if (!isRenamingRef.current) { + setIsWorkspaceMenuOpen(false) + } + isRenamingRef.current = false } /** @@ -224,8 +227,11 @@ export function WorkspaceHeader({ const handleRenameAction = () => { if (!capturedWorkspaceRef.current) return + isRenamingRef.current = true + hasInputFocusedRef.current = false setEditingWorkspaceId(capturedWorkspaceRef.current.id) setEditingName(capturedWorkspaceRef.current.name) + setIsWorkspaceMenuOpen(true) } /** @@ -287,8 +293,10 @@ export function WorkspaceHeader({ { - // Don't close if context menu is opening - if (!open && isContextMenuOpen) { + if ( + !open && + (isContextMenuOpen || isContextMenuOpeningRef.current || editingWorkspaceId) + ) { return } setIsWorkspaceMenuOpen(open) @@ -302,6 +310,11 @@ export function WorkspaceHeader({ isCollapsed ? '' : '-mx-[6px] min-w-0 max-w-full' }`} title={activeWorkspace?.name || 'Loading...'} + onContextMenu={(e) => { + if (activeWorkspaceFull) { + handleContextMenu(e, activeWorkspaceFull) + } + }} > { + if (el && !hasInputFocusedRef.current) { + hasInputFocusedRef.current = true + el.focus() + el.select() + } + }} value={editingName} onChange={(e) => setEditingName(e.target.value)} onKeyDown={async (e) => { @@ -406,15 +425,18 @@ export function WorkspaceHeader({ }} onBlur={async () => { if (!editingWorkspaceId) return - setIsListRenaming(true) - try { - await onRenameWorkspace(workspace.id, editingName.trim()) - setEditingWorkspaceId(null) - } finally { - setIsListRenaming(false) + const trimmedName = editingName.trim() + if (trimmedName && trimmedName !== workspace.name) { + setIsListRenaming(true) + try { + await onRenameWorkspace(workspace.id, trimmedName) + } finally { + setIsListRenaming(false) + } } + setEditingWorkspaceId(null) }} - className='w-full border-0 bg-transparent p-0 font-base text-[13px] text-[var(--text-primary)] outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0' + className='w-full border-0 bg-transparent p-0 font-base text-[13px] text-[var(--text-primary)] outline-none selection:bg-[#add6ff] selection:text-[#1b1b1b] focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 dark:selection:bg-[#264f78] dark:selection:text-white' maxLength={100} autoComplete='off' autoCorrect='off' @@ -422,7 +444,6 @@ export function WorkspaceHeader({ spellCheck='false' disabled={isListRenaming} onClick={(e) => { - e.preventDefault() e.stopPropagation() }} /> diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management.ts index dafa834499..2b9eee714e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management.ts @@ -161,7 +161,10 @@ export function useWorkspaceManagement({ } // Update local state immediately after successful API call - setActiveWorkspace((prev) => (prev ? { ...prev, name: newName.trim() } : null)) + // Only update activeWorkspace if it's the one being renamed + setActiveWorkspace((prev) => + prev && prev.id === workspaceId ? { ...prev, name: newName.trim() } : prev + ) setWorkspaces((prev) => prev.map((workspace) => workspace.id === workspaceId ? { ...workspace, name: newName.trim() } : workspace diff --git a/apps/sim/executor/__test-utils__/executor-mocks.ts b/apps/sim/executor/__test-utils__/executor-mocks.ts deleted file mode 100644 index efe146ac56..0000000000 --- a/apps/sim/executor/__test-utils__/executor-mocks.ts +++ /dev/null @@ -1,929 +0,0 @@ -import { vi } from 'vitest' -import type { SerializedWorkflow } from '@/serializer/types' - -/** - * Mock handler factory - creates consistent handler mocks - */ -export const createMockHandler = ( - handlerName: string, - options?: { - canHandleCondition?: (block: any) => boolean - executeResult?: any | ((inputs: any) => any) - } -) => { - const defaultCanHandle = (block: any) => - block.metadata?.id === handlerName || handlerName === 'generic' - - const defaultExecuteResult = { - result: `${handlerName} executed`, - } - - return vi.fn().mockImplementation(() => ({ - canHandle: options?.canHandleCondition || defaultCanHandle, - execute: vi.fn().mockImplementation(async (block, inputs) => { - if (typeof options?.executeResult === 'function') { - return options.executeResult(inputs) - } - return options?.executeResult || defaultExecuteResult - }), - })) -} - -/** - * Setup all handler mocks with default behaviors - */ -export const setupHandlerMocks = () => { - vi.doMock('@/executor/handlers', () => ({ - TriggerBlockHandler: createMockHandler('trigger', { - canHandleCondition: (block) => - block.metadata?.category === 'triggers' || block.config?.params?.triggerMode === true, - executeResult: (inputs: any) => inputs || {}, - }), - AgentBlockHandler: createMockHandler('agent'), - RouterBlockHandler: createMockHandler('router'), - ConditionBlockHandler: createMockHandler('condition'), - EvaluatorBlockHandler: createMockHandler('evaluator'), - FunctionBlockHandler: createMockHandler('function'), - ApiBlockHandler: createMockHandler('api'), - LoopBlockHandler: createMockHandler('loop'), - ParallelBlockHandler: createMockHandler('parallel'), - WorkflowBlockHandler: createMockHandler('workflow'), - VariablesBlockHandler: createMockHandler('variables'), - WaitBlockHandler: createMockHandler('wait'), - GenericBlockHandler: createMockHandler('generic'), - ResponseBlockHandler: createMockHandler('response'), - })) -} - -/** - * Setup store mocks with configurable options - */ -export const setupStoreMocks = (options?: { - isDebugging?: boolean - consoleAddFn?: ReturnType - consoleUpdateFn?: ReturnType -}) => { - const consoleAddFn = options?.consoleAddFn || vi.fn() - const consoleUpdateFn = options?.consoleUpdateFn || vi.fn() - - vi.doMock('@/stores/settings/general/store', () => ({ - useGeneralStore: { - getState: () => ({}), - }, - })) - - vi.doMock('@/stores/execution/store', () => ({ - useExecutionStore: { - getState: () => ({ - isDebugging: options?.isDebugging ?? false, - setIsExecuting: vi.fn(), - reset: vi.fn(), - setActiveBlocks: vi.fn(), - setPendingBlocks: vi.fn(), - setIsDebugging: vi.fn(), - }), - setState: vi.fn(), - }, - })) - - vi.doMock('@/stores/console/store', () => ({ - useConsoleStore: { - getState: () => ({ - addConsole: consoleAddFn, - }), - }, - })) - - vi.doMock('@/stores/terminal', () => ({ - useTerminalConsoleStore: { - getState: () => ({ - addConsole: consoleAddFn, - updateConsole: consoleUpdateFn, - }), - }, - })) - - return { consoleAddFn, consoleUpdateFn } -} - -/** - * Setup core executor mocks (PathTracker, InputResolver, LoopManager, ParallelManager) - */ -export const setupExecutorCoreMocks = () => { - vi.doMock('@/executor/path', () => ({ - PathTracker: vi.fn().mockImplementation(() => ({ - updateExecutionPaths: vi.fn(), - isInActivePath: vi.fn().mockReturnValue(true), - })), - })) - - vi.doMock('@/executor/resolver', () => ({ - InputResolver: vi.fn().mockImplementation(() => ({ - resolveInputs: vi.fn().mockReturnValue({}), - resolveBlockReferences: vi.fn().mockImplementation((value) => value), - resolveVariableReferences: vi.fn().mockImplementation((value) => value), - resolveEnvVariables: vi.fn().mockImplementation((value) => value), - })), - })) - - vi.doMock('@/executor/loops', () => ({ - LoopManager: vi.fn().mockImplementation(() => ({ - processLoopIterations: vi.fn().mockResolvedValue(false), - getLoopIndex: vi.fn().mockImplementation((loopId, blockId, context) => { - return context.loopExecutions?.get(loopId)?.iteration || 0 - }), - })), - })) - - vi.doMock('@/executor/parallels', () => ({ - ParallelManager: vi.fn().mockImplementation(() => ({ - processParallelIterations: vi.fn().mockResolvedValue(false), - createVirtualBlockInstances: vi.fn().mockReturnValue([]), - setupIterationContext: vi.fn(), - storeIterationResult: vi.fn(), - initializeParallel: vi.fn(), - getIterationItem: vi.fn(), - areAllVirtualBlocksExecuted: vi.fn().mockReturnValue(false), - })), - })) -} - -/** - * Workflow factory functions - */ -export const createMinimalWorkflow = (): SerializedWorkflow => ({ - version: '1.0', - blocks: [ - { - id: 'starter', - position: { x: 0, y: 0 }, - config: { tool: 'test-tool', params: {} }, - inputs: {}, - outputs: {}, - enabled: true, - metadata: { id: 'starter', name: 'Starter Block' }, - }, - { - id: 'block1', - position: { x: 100, y: 0 }, - config: { tool: 'test-tool', params: {} }, - inputs: {}, - outputs: {}, - enabled: true, - metadata: { id: 'test', name: 'Test Block' }, - }, - ], - connections: [ - { - source: 'starter', - target: 'block1', - }, - ], - loops: {}, -}) - -export const createWorkflowWithCondition = (): SerializedWorkflow => ({ - version: '1.0', - blocks: [ - { - id: 'starter', - position: { x: 0, y: 0 }, - config: { tool: 'test-tool', params: {} }, - inputs: {}, - outputs: {}, - enabled: true, - metadata: { id: 'starter', name: 'Starter Block' }, - }, - { - id: 'condition1', - position: { x: 100, y: 0 }, - config: { tool: 'test-tool', params: {} }, - inputs: {}, - outputs: {}, - enabled: true, - metadata: { id: 'condition', name: 'Condition Block' }, - }, - { - id: 'block1', - position: { x: 200, y: -50 }, - config: { tool: 'test-tool', params: {} }, - inputs: {}, - outputs: {}, - enabled: true, - metadata: { id: 'test', name: 'True Path Block' }, - }, - { - id: 'block2', - position: { x: 200, y: 50 }, - config: { tool: 'test-tool', params: {} }, - inputs: {}, - outputs: {}, - enabled: true, - metadata: { id: 'test', name: 'False Path Block' }, - }, - ], - connections: [ - { - source: 'starter', - target: 'condition1', - }, - { - source: 'condition1', - target: 'block1', - sourceHandle: 'condition-true', - }, - { - source: 'condition1', - target: 'block2', - sourceHandle: 'condition-false', - }, - ], - loops: {}, -}) - -export const createWorkflowWithLoop = (): SerializedWorkflow => ({ - version: '1.0', - blocks: [ - { - id: 'starter', - position: { x: 0, y: 0 }, - config: { tool: 'test-tool', params: {} }, - inputs: {}, - outputs: {}, - enabled: true, - metadata: { id: 'starter', name: 'Starter Block' }, - }, - { - id: 'block1', - position: { x: 100, y: 0 }, - config: { tool: 'test-tool', params: {} }, - inputs: {}, - outputs: {}, - enabled: true, - metadata: { id: 'test', name: 'Loop Block 1' }, - }, - { - id: 'block2', - position: { x: 200, y: 0 }, - config: { tool: 'test-tool', params: {} }, - inputs: {}, - outputs: {}, - enabled: true, - metadata: { id: 'test', name: 'Loop Block 2' }, - }, - ], - connections: [ - { - source: 'starter', - target: 'block1', - }, - { - source: 'block1', - target: 'block2', - }, - { - source: 'block2', - target: 'block1', - }, - ], - loops: { - loop1: { - id: 'loop1', - nodes: ['block1', 'block2'], - iterations: 5, - loopType: 'forEach', - forEachItems: [1, 2, 3, 4, 5], - }, - }, -}) - -export const createWorkflowWithErrorPath = (): SerializedWorkflow => ({ - version: '1.0', - blocks: [ - { - id: 'starter', - position: { x: 0, y: 0 }, - config: { tool: 'test-tool', params: {} }, - inputs: {}, - outputs: {}, - enabled: true, - metadata: { id: 'starter', name: 'Starter Block' }, - }, - { - id: 'block1', - position: { x: 100, y: 0 }, - config: { tool: 'test-tool', params: {} }, - inputs: {}, - outputs: {}, - enabled: true, - metadata: { id: 'function', name: 'Function Block' }, - }, - { - id: 'error-handler', - position: { x: 200, y: 50 }, - config: { tool: 'test-tool', params: {} }, - inputs: {}, - outputs: {}, - enabled: true, - metadata: { id: 'test', name: 'Error Handler Block' }, - }, - { - id: 'success-block', - position: { x: 200, y: -50 }, - config: { tool: 'test-tool', params: {} }, - inputs: {}, - outputs: {}, - enabled: true, - metadata: { id: 'test', name: 'Success Block' }, - }, - ], - connections: [ - { - source: 'starter', - target: 'block1', - }, - { - source: 'block1', - target: 'success-block', - sourceHandle: 'source', - }, - { - source: 'block1', - target: 'error-handler', - sourceHandle: 'error', - }, - ], - loops: {}, -}) - -export const createWorkflowWithParallel = (distribution?: any): SerializedWorkflow => ({ - version: '2.0', - blocks: [ - { - id: 'starter', - position: { x: 0, y: 0 }, - metadata: { id: 'starter', name: 'Start' }, - config: { tool: 'starter', params: {} }, - inputs: {}, - outputs: {}, - enabled: true, - }, - { - id: 'parallel-1', - position: { x: 100, y: 0 }, - metadata: { id: 'parallel', name: 'Test Parallel' }, - config: { tool: 'parallel', params: {} }, - inputs: {}, - outputs: {}, - enabled: true, - }, - { - id: 'function-1', - position: { x: 200, y: 0 }, - metadata: { id: 'function', name: 'Process Item' }, - config: { - tool: 'function', - params: { - code: 'return { item: , index: }', - }, - }, - inputs: {}, - outputs: {}, - enabled: true, - }, - { - id: 'endpoint', - position: { x: 300, y: 0 }, - metadata: { id: 'generic', name: 'End' }, - config: { tool: 'generic', params: {} }, - inputs: {}, - outputs: {}, - enabled: true, - }, - ], - connections: [ - { source: 'starter', target: 'parallel-1' }, - { source: 'parallel-1', target: 'function-1', sourceHandle: 'parallel-start-source' }, - { source: 'parallel-1', target: 'endpoint', sourceHandle: 'parallel-end-source' }, - ], - loops: {}, - parallels: { - 'parallel-1': { - id: 'parallel-1', - nodes: ['function-1'], - distribution: distribution || ['apple', 'banana', 'cherry'], - }, - }, -}) - -export const createWorkflowWithResponse = (): SerializedWorkflow => ({ - version: '1.0', - blocks: [ - { - id: 'starter', - position: { x: 0, y: 0 }, - config: { tool: 'test-tool', params: {} }, - inputs: { - input: 'json', - }, - outputs: { - response: { type: 'json', description: 'Input response' }, - }, - enabled: true, - metadata: { id: 'starter', name: 'Starter Block' }, - }, - { - id: 'response', - position: { x: 100, y: 0 }, - config: { tool: 'test-tool', params: {} }, - inputs: { - data: 'json', - status: 'number', - headers: 'json', - }, - outputs: { - data: { type: 'json', description: 'Response data' }, - status: { type: 'number', description: 'Response status' }, - headers: { type: 'json', description: 'Response headers' }, - }, - enabled: true, - metadata: { id: 'response', name: 'Response Block' }, - }, - ], - connections: [{ source: 'starter', target: 'response' }], - loops: {}, -}) - -/** - * Create a mock execution context with customizable options - */ -export interface MockContextOptions { - workflowId?: string - loopExecutions?: Map - executedBlocks?: Set - activeExecutionPath?: Set - completedLoops?: Set - parallelExecutions?: Map - parallelBlockMapping?: Map - currentVirtualBlockId?: string - workflow?: SerializedWorkflow - blockStates?: Map -} - -export const createMockContext = (options: MockContextOptions = {}) => { - const workflow = options.workflow || createMinimalWorkflow() - - return { - workflowId: options.workflowId || 'test-workflow-id', - blockStates: options.blockStates || new Map(), - blockLogs: [], - metadata: { startTime: new Date().toISOString(), duration: 0 }, - environmentVariables: {}, - decisions: { router: new Map(), condition: new Map() }, - loopExecutions: options.loopExecutions || new Map(), - executedBlocks: options.executedBlocks || new Set(), - activeExecutionPath: options.activeExecutionPath || new Set(), - workflow, - completedLoops: options.completedLoops || new Set(), - parallelExecutions: options.parallelExecutions || new Map(), - parallelBlockMapping: options.parallelBlockMapping, - currentVirtualBlockId: options.currentVirtualBlockId, - } -} - -/** - * Mock implementations for testing loops - */ -export const createLoopManagerMock = (options?: { - processLoopIterationsImpl?: (context: any) => Promise - getLoopIndexImpl?: (loopId: string, blockId: string, context: any) => number -}) => ({ - LoopManager: vi.fn().mockImplementation(() => ({ - processLoopIterations: options?.processLoopIterationsImpl || vi.fn().mockResolvedValue(false), - getLoopIndex: - options?.getLoopIndexImpl || - vi.fn().mockImplementation((loopId, blockId, context) => { - return context.loopExecutions?.get(loopId)?.iteration || 0 - }), - })), -}) - -/** - * Create a parallel execution state object for testing - */ -export const createParallelExecutionState = (options?: { - parallelCount?: number - distributionItems?: any[] | Record | null - completedExecutions?: number - executionResults?: Map - activeIterations?: Set - parallelType?: 'count' | 'collection' -}) => ({ - parallelCount: options?.parallelCount ?? 3, - distributionItems: - options?.distributionItems !== undefined ? options.distributionItems : ['a', 'b', 'c'], - completedExecutions: options?.completedExecutions ?? 0, - executionResults: options?.executionResults ?? new Map(), - activeIterations: options?.activeIterations ?? new Set(), - parallelType: options?.parallelType, -}) - -/** - * Mock implementations for testing parallels - */ -export const createParallelManagerMock = (options?: { - maxChecks?: number - processParallelIterationsImpl?: (context: any) => Promise -}) => ({ - ParallelManager: vi.fn().mockImplementation(() => { - const executionCounts = new Map() - const maxChecks = options?.maxChecks || 2 - - return { - processParallelIterations: - options?.processParallelIterationsImpl || - vi.fn().mockImplementation(async (context) => { - for (const [parallelId, parallel] of Object.entries(context.workflow?.parallels || {})) { - if (context.completedLoops.has(parallelId)) { - continue - } - - const parallelState = context.parallelExecutions?.get(parallelId) - if (!parallelState) { - continue - } - - const checkCount = executionCounts.get(parallelId) || 0 - executionCounts.set(parallelId, checkCount + 1) - - if (checkCount >= maxChecks) { - context.completedLoops.add(parallelId) - continue - } - - let allVirtualBlocksExecuted = true - const parallelNodes = (parallel as any).nodes || [] - for (const nodeId of parallelNodes) { - for (let i = 0; i < parallelState.parallelCount; i++) { - const virtualBlockId = `${nodeId}_parallel_${parallelId}_iteration_${i}` - if (!context.executedBlocks.has(virtualBlockId)) { - allVirtualBlocksExecuted = false - break - } - } - if (!allVirtualBlocksExecuted) break - } - - if (allVirtualBlocksExecuted && !context.completedLoops.has(parallelId)) { - context.executedBlocks.delete(parallelId) - context.activeExecutionPath.add(parallelId) - - for (const nodeId of parallelNodes) { - context.activeExecutionPath.delete(nodeId) - } - } - } - }), - createVirtualBlockInstances: vi.fn().mockImplementation((block, parallelId, state) => { - const instances = [] - for (let i = 0; i < state.parallelCount; i++) { - instances.push(`${block.id}_parallel_${parallelId}_iteration_${i}`) - } - return instances - }), - setupIterationContext: vi.fn(), - storeIterationResult: vi.fn(), - initializeParallel: vi.fn(), - getIterationItem: vi.fn(), - areAllVirtualBlocksExecuted: vi - .fn() - .mockImplementation((parallelId, parallel, executedBlocks, state, context) => { - // Simple mock implementation - check all blocks (ignoring conditional routing for tests) - for (const nodeId of parallel.nodes) { - for (let i = 0; i < state.parallelCount; i++) { - const virtualBlockId = `${nodeId}_parallel_${parallelId}_iteration_${i}` - if (!executedBlocks.has(virtualBlockId)) { - return false - } - } - } - return true - }), - } - }), -}) - -/** - * Setup function block handler that executes code - */ -export const createFunctionBlockHandler = vi.fn().mockImplementation(() => ({ - canHandle: (block: any) => block.metadata?.id === 'function', - execute: vi.fn().mockImplementation(async (block, inputs) => { - return { - result: inputs.code ? new Function(inputs.code)() : { key: inputs.key, value: inputs.value }, - stdout: '', - } - }), -})) - -/** - * Create a custom parallel block handler for testing - */ -export const createParallelBlockHandler = vi.fn().mockImplementation(() => { - return { - canHandle: (block: any) => block.metadata?.id === 'parallel', - execute: vi.fn().mockImplementation(async (block, inputs, context) => { - const parallelId = block.id - const parallel = context.workflow?.parallels?.[parallelId] - - if (!parallel) { - throw new Error('Parallel configuration not found') - } - - if (!context.parallelExecutions) { - context.parallelExecutions = new Map() - } - - let parallelState = context.parallelExecutions.get(parallelId) - - if (!parallelState) { - // First execution - initialize - const distributionItems = parallel.distribution || [] - const parallelCount = Array.isArray(distributionItems) - ? distributionItems.length - : typeof distributionItems === 'object' - ? Object.keys(distributionItems).length - : 1 - - parallelState = { - parallelCount, - distributionItems, - completedExecutions: 0, - executionResults: new Map(), - activeIterations: new Set(), - } - context.parallelExecutions.set(parallelId, parallelState) - - if (distributionItems) { - context.loopItems.set(`${parallelId}_items`, distributionItems) - } - - // Activate child nodes - const connections = - context.workflow?.connections.filter( - (conn: any) => - conn.source === parallelId && conn.sourceHandle === 'parallel-start-source' - ) || [] - - for (const conn of connections) { - context.activeExecutionPath.add(conn.target) - } - - return { - parallelId, - parallelCount, - distributionType: 'distributed', - started: true, - message: `Initialized ${parallelCount} parallel executions`, - } - } - - // Check completion - const allCompleted = parallel.nodes.every((nodeId: string) => { - for (let i = 0; i < parallelState.parallelCount; i++) { - const virtualBlockId = `${nodeId}_parallel_${parallelId}_iteration_${i}` - if (!context.executedBlocks.has(virtualBlockId)) { - return false - } - } - return true - }) - - if (allCompleted) { - context.completedLoops.add(parallelId) - - // Activate end connections - const endConnections = - context.workflow?.connections.filter( - (conn: any) => conn.source === parallelId && conn.sourceHandle === 'parallel-end-source' - ) || [] - - for (const conn of endConnections) { - context.activeExecutionPath.add(conn.target) - } - - return { - parallelId, - parallelCount: parallelState.parallelCount, - completed: true, - message: `Completed all ${parallelState.parallelCount} executions`, - } - } - - return { - parallelId, - parallelCount: parallelState.parallelCount, - waiting: true, - message: 'Waiting for iterations to complete', - } - }), - } -}) - -/** - * Create an input resolver mock that handles parallel references - */ -export const createParallelInputResolver = (distributionData: any) => ({ - InputResolver: vi.fn().mockImplementation(() => ({ - resolveInputs: vi.fn().mockImplementation((block, context) => { - if (block.metadata?.id === 'function') { - const virtualBlockId = context.currentVirtualBlockId - if (virtualBlockId && context.parallelBlockMapping) { - const mapping = context.parallelBlockMapping.get(virtualBlockId) - if (mapping) { - if (Array.isArray(distributionData)) { - const currentItem = distributionData[mapping.iterationIndex] - const currentIndex = mapping.iterationIndex - return { - code: `return { item: "${currentItem}", index: ${currentIndex} }`, - } - } - if (typeof distributionData === 'object') { - const entries = Object.entries(distributionData) - const [key, value] = entries[mapping.iterationIndex] - return { - code: `return { key: "${key}", value: "${value}" }`, - } - } - } - } - } - return {} - }), - })), -}) - -/** - * Create a workflow with parallel blocks for testing - */ -export const createWorkflowWithParallelArray = ( - items: any[] = ['apple', 'banana', 'cherry'] -): SerializedWorkflow => ({ - version: '2.0', - blocks: [ - { - id: 'starter', - position: { x: 0, y: 0 }, - metadata: { id: 'starter', name: 'Start' }, - config: { tool: 'starter', params: {} }, - inputs: {}, - outputs: {}, - enabled: true, - }, - { - id: 'parallel-1', - position: { x: 100, y: 0 }, - metadata: { id: 'parallel', name: 'Test Parallel' }, - config: { tool: 'parallel', params: {} }, - inputs: {}, - outputs: {}, - enabled: true, - }, - { - id: 'function-1', - position: { x: 200, y: 0 }, - metadata: { id: 'function', name: 'Process Item' }, - config: { - tool: 'function', - params: { - code: 'return { item: , index: }', - }, - }, - inputs: {}, - outputs: {}, - enabled: true, - }, - { - id: 'endpoint', - position: { x: 300, y: 0 }, - metadata: { id: 'generic', name: 'End' }, - config: { tool: 'generic', params: {} }, - inputs: {}, - outputs: {}, - enabled: true, - }, - ], - connections: [ - { source: 'starter', target: 'parallel-1' }, - { source: 'parallel-1', target: 'function-1', sourceHandle: 'parallel-start-source' }, - { source: 'parallel-1', target: 'endpoint', sourceHandle: 'parallel-end-source' }, - ], - loops: {}, - parallels: { - 'parallel-1': { - id: 'parallel-1', - nodes: ['function-1'], - distribution: items, - }, - }, -}) - -/** - * Create a workflow with parallel blocks for object distribution - */ -export const createWorkflowWithParallelObject = ( - items: Record = { first: 'alpha', second: 'beta', third: 'gamma' } -): SerializedWorkflow => ({ - version: '2.0', - blocks: [ - { - id: 'starter', - position: { x: 0, y: 0 }, - metadata: { id: 'starter', name: 'Start' }, - config: { tool: 'starter', params: {} }, - inputs: {}, - outputs: {}, - enabled: true, - }, - { - id: 'parallel-1', - position: { x: 100, y: 0 }, - metadata: { id: 'parallel', name: 'Test Parallel' }, - config: { tool: 'parallel', params: {} }, - inputs: {}, - outputs: {}, - enabled: true, - }, - { - id: 'function-1', - position: { x: 200, y: 0 }, - metadata: { id: 'function', name: 'Process Entry' }, - config: { - tool: 'function', - params: { - code: 'return { key: , value: }', - }, - }, - inputs: {}, - outputs: {}, - enabled: true, - }, - { - id: 'endpoint', - position: { x: 300, y: 0 }, - metadata: { id: 'generic', name: 'End' }, - config: { tool: 'generic', params: {} }, - inputs: {}, - outputs: {}, - enabled: true, - }, - ], - connections: [ - { source: 'starter', target: 'parallel-1' }, - { source: 'parallel-1', target: 'function-1', sourceHandle: 'parallel-start-source' }, - { source: 'parallel-1', target: 'endpoint', sourceHandle: 'parallel-end-source' }, - ], - loops: {}, - parallels: { - 'parallel-1': { - id: 'parallel-1', - nodes: ['function-1'], - distribution: items, - }, - }, -}) - -/** - * Mock all modules needed for parallel tests - */ -export const setupParallelTestMocks = (options?: { - distributionData?: any - maxParallelChecks?: number -}) => { - setupStoreMocks() - - setupExecutorCoreMocks() - - vi.doMock('@/executor/parallels', () => - createParallelManagerMock({ - maxChecks: options?.maxParallelChecks, - }) - ) - - vi.doMock('@/executor/loops', () => createLoopManagerMock()) -} - -/** - * Sets up all standard mocks for executor tests - */ -export const setupAllMocks = (options?: { - isDebugging?: boolean - consoleAddFn?: ReturnType - consoleUpdateFn?: ReturnType -}) => { - setupHandlerMocks() - const storeMocks = setupStoreMocks(options) - setupExecutorCoreMocks() - - return storeMocks -} diff --git a/apps/sim/executor/constants.ts b/apps/sim/executor/constants.ts index 7a5d06f405..d473472c80 100644 --- a/apps/sim/executor/constants.ts +++ b/apps/sim/executor/constants.ts @@ -266,11 +266,13 @@ export interface ConditionConfig { } export function isTriggerBlockType(blockType: string | undefined): boolean { - return TRIGGER_BLOCK_TYPES.includes(blockType as any) + return blockType !== undefined && (TRIGGER_BLOCK_TYPES as readonly string[]).includes(blockType) } export function isMetadataOnlyBlockType(blockType: string | undefined): boolean { - return METADATA_ONLY_BLOCK_TYPES.includes(blockType as any) + return ( + blockType !== undefined && (METADATA_ONLY_BLOCK_TYPES as readonly string[]).includes(blockType) + ) } export function isWorkflowBlockType(blockType: string | undefined): boolean { diff --git a/apps/sim/executor/handlers/api/api-handler.test.ts b/apps/sim/executor/handlers/api/api-handler.test.ts index 2a5303b9a5..1a930f57ff 100644 --- a/apps/sim/executor/handlers/api/api-handler.test.ts +++ b/apps/sim/executor/handlers/api/api-handler.test.ts @@ -1,4 +1,4 @@ -import '@/executor/__test-utils__/mock-dependencies' +import '@sim/testing/mocks/executor' import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' import { BlockType } from '@/executor/constants' diff --git a/apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts b/apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts index e112a5e5ec..8cbbb0bbf4 100644 --- a/apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts +++ b/apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts @@ -1,4 +1,4 @@ -import '@/executor/__test-utils__/mock-dependencies' +import '@sim/testing/mocks/executor' import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' import { BlockType } from '@/executor/constants' diff --git a/apps/sim/executor/handlers/generic/generic-handler.test.ts b/apps/sim/executor/handlers/generic/generic-handler.test.ts index dfbe364802..661c7a1244 100644 --- a/apps/sim/executor/handlers/generic/generic-handler.test.ts +++ b/apps/sim/executor/handlers/generic/generic-handler.test.ts @@ -1,4 +1,4 @@ -import '@/executor/__test-utils__/mock-dependencies' +import '@sim/testing/mocks/executor' import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' import { BlockType } from '@/executor/constants' diff --git a/apps/sim/executor/handlers/router/router-handler.test.ts b/apps/sim/executor/handlers/router/router-handler.test.ts index 9b2bc654ca..d0e28f97eb 100644 --- a/apps/sim/executor/handlers/router/router-handler.test.ts +++ b/apps/sim/executor/handlers/router/router-handler.test.ts @@ -1,4 +1,4 @@ -import '@/executor/__test-utils__/mock-dependencies' +import '@sim/testing/mocks/executor' import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' import { generateRouterPrompt } from '@/blocks/blocks/router' diff --git a/apps/sim/executor/handlers/trigger/trigger-handler.test.ts b/apps/sim/executor/handlers/trigger/trigger-handler.test.ts index 0ae4eae63c..c9e1b4da1a 100644 --- a/apps/sim/executor/handlers/trigger/trigger-handler.test.ts +++ b/apps/sim/executor/handlers/trigger/trigger-handler.test.ts @@ -1,4 +1,4 @@ -import '@/executor/__test-utils__/mock-dependencies' +import '@sim/testing/mocks/executor' import { beforeEach, describe, expect, it } from 'vitest' import { TriggerBlockHandler } from '@/executor/handlers/trigger/trigger-handler' diff --git a/apps/sim/executor/handlers/wait/wait-handler.test.ts b/apps/sim/executor/handlers/wait/wait-handler.test.ts index f9abab07bc..27a4e33bae 100644 --- a/apps/sim/executor/handlers/wait/wait-handler.test.ts +++ b/apps/sim/executor/handlers/wait/wait-handler.test.ts @@ -1,4 +1,4 @@ -import '@/executor/__test-utils__/mock-dependencies' +import '@sim/testing/mocks/executor' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { BlockType } from '@/executor/constants' diff --git a/apps/sim/executor/utils/lazy-cleanup.ts b/apps/sim/executor/utils/lazy-cleanup.ts index e892cbdf99..3ab2dc9238 100644 --- a/apps/sim/executor/utils/lazy-cleanup.ts +++ b/apps/sim/executor/utils/lazy-cleanup.ts @@ -2,6 +2,7 @@ import { db } from '@sim/db' import { workflowBlocks } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq } from 'drizzle-orm' +import { extractInputFieldsFromBlocks } from '@/lib/workflows/input-format' const logger = createLogger('LazyCleanup') @@ -12,38 +13,14 @@ const logger = createLogger('LazyCleanup') * @returns Set of valid field names defined in the child's inputFormat */ function extractValidInputFieldNames(childWorkflowBlocks: Record): Set | null { - const validFieldNames = new Set() + const fields = extractInputFieldsFromBlocks(childWorkflowBlocks) - const startBlock = Object.values(childWorkflowBlocks).find((block: any) => { - const blockType = block?.type - return blockType === 'start_trigger' || blockType === 'input_trigger' || blockType === 'starter' - }) - - if (!startBlock) { - logger.debug('No start block found in child workflow') - return null - } - - const inputFormat = - (startBlock as any)?.subBlocks?.inputFormat?.value ?? - (startBlock as any)?.config?.params?.inputFormat - - if (!Array.isArray(inputFormat)) { - logger.debug('No inputFormat array found in child workflow start block') + if (fields.length === 0) { + logger.debug('No inputFormat fields found in child workflow') return null } - // Extract field names - for (const field of inputFormat) { - if (field?.name && typeof field.name === 'string') { - const fieldName = field.name.trim() - if (fieldName) { - validFieldNames.add(fieldName) - } - } - } - - return validFieldNames + return new Set(fields.map((field) => field.name)) } /** diff --git a/apps/sim/executor/variables/resolvers/block.ts b/apps/sim/executor/variables/resolvers/block.ts index ba786b1675..da4f3096c4 100644 --- a/apps/sim/executor/variables/resolvers/block.ts +++ b/apps/sim/executor/variables/resolvers/block.ts @@ -32,7 +32,7 @@ export class BlockResolver implements Resolver { return false } const [type] = parts - return !SPECIAL_REFERENCE_PREFIXES.includes(type as any) + return !(SPECIAL_REFERENCE_PREFIXES as readonly string[]).includes(type) } resolve(reference: string, context: ResolutionContext): any { diff --git a/apps/sim/hooks/queries/workflows.ts b/apps/sim/hooks/queries/workflows.ts index b6f5dc0673..e1e8460e7e 100644 --- a/apps/sim/hooks/queries/workflows.ts +++ b/apps/sim/hooks/queries/workflows.ts @@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger' import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { getNextWorkflowColor } from '@/lib/workflows/colors' import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults' +import { extractInputFieldsFromBlocks, type WorkflowInputField } from '@/lib/workflows/input-format' import { createOptimisticMutationHandlers, generateTempId, @@ -22,6 +23,34 @@ export const workflowKeys = { deploymentVersions: () => [...workflowKeys.all, 'deploymentVersion'] as const, deploymentVersion: (workflowId: string | undefined, version: number | undefined) => [...workflowKeys.deploymentVersions(), workflowId ?? '', version ?? 0] as const, + inputFields: (workflowId: string | undefined) => + [...workflowKeys.all, 'inputFields', workflowId ?? ''] as const, +} + +/** + * Fetches workflow input fields from the workflow state. + */ +async function fetchWorkflowInputFields(workflowId: string): Promise { + const response = await fetch(`/api/workflows/${workflowId}`) + if (!response.ok) throw new Error('Failed to fetch workflow') + const { data } = await response.json() + return extractInputFieldsFromBlocks(data?.state?.blocks) +} + +/** + * Hook to fetch workflow input fields for configuration. + * Uses React Query for caching and deduplication. + * + * @param workflowId - The workflow ID to fetch input fields for + * @returns Query result with input fields array + */ +export function useWorkflowInputFields(workflowId: string | undefined) { + return useQuery({ + queryKey: workflowKeys.inputFields(workflowId), + queryFn: () => fetchWorkflowInputFields(workflowId!), + enabled: Boolean(workflowId), + staleTime: 60 * 1000, // 1 minute cache + }) } function mapWorkflow(workflow: any): WorkflowMetadata { diff --git a/apps/sim/lib/execution/files.ts b/apps/sim/lib/execution/files.ts index 510856e580..9eb26905e4 100644 --- a/apps/sim/lib/execution/files.ts +++ b/apps/sim/lib/execution/files.ts @@ -112,7 +112,10 @@ export async function processExecutionFiles( type ValidatedInputFormatField = Required> function extractInputFormatFromBlock(block: SerializedBlock): ValidatedInputFormatField[] { - const inputFormatValue = block.config?.params?.inputFormat + const metadata = block.metadata as { subBlocks?: Record } | undefined + const subBlocksValue = metadata?.subBlocks?.inputFormat?.value + const legacyValue = block.config?.params?.inputFormat + const inputFormatValue = subBlocksValue ?? legacyValue if (!Array.isArray(inputFormatValue) || inputFormatValue.length === 0) { return [] diff --git a/apps/sim/lib/mcp/workflow-tool-schema.ts b/apps/sim/lib/mcp/workflow-tool-schema.ts index ff915f76bd..45572ea52c 100644 --- a/apps/sim/lib/mcp/workflow-tool-schema.ts +++ b/apps/sim/lib/mcp/workflow-tool-schema.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import { normalizeInputFormatValue } from '@/lib/workflows/input-format-utils' +import { normalizeInputFormatValue } from '@/lib/workflows/input-format' import { isValidStartBlockType } from '@/lib/workflows/triggers/trigger-utils' import type { InputFormatField } from '@/lib/workflows/types' import type { McpToolSchema } from './types' diff --git a/apps/sim/lib/workflows/blocks/block-outputs.ts b/apps/sim/lib/workflows/blocks/block-outputs.ts index a3a9ec1663..c953a11968 100644 --- a/apps/sim/lib/workflows/blocks/block-outputs.ts +++ b/apps/sim/lib/workflows/blocks/block-outputs.ts @@ -1,5 +1,5 @@ import { createLogger } from '@sim/logger' -import { normalizeInputFormatValue } from '@/lib/workflows/input-format-utils' +import { normalizeInputFormatValue } from '@/lib/workflows/input-format' import { classifyStartBlockType, StartBlockPath, diff --git a/apps/sim/lib/workflows/input-format-utils.ts b/apps/sim/lib/workflows/input-format-utils.ts deleted file mode 100644 index fec752d89f..0000000000 --- a/apps/sim/lib/workflows/input-format-utils.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { InputFormatField } from '@/lib/workflows/types' - -/** - * Normalizes an input format value into a list of valid fields. - * - * Filters out: - * - null or undefined values - * - Empty arrays - * - Non-array values - * - Fields without names - * - Fields with empty or whitespace-only names - * - * @param inputFormatValue - Raw input format value from subblock state - * @returns Array of validated input format fields - */ -export function normalizeInputFormatValue(inputFormatValue: unknown): InputFormatField[] { - // Handle null, undefined, and empty arrays - if ( - inputFormatValue === null || - inputFormatValue === undefined || - (Array.isArray(inputFormatValue) && inputFormatValue.length === 0) - ) { - return [] - } - - // Handle non-array values - if (!Array.isArray(inputFormatValue)) { - return [] - } - - // Filter valid fields - return inputFormatValue.filter( - (field): field is InputFormatField => - field && - typeof field === 'object' && - typeof field.name === 'string' && - field.name.trim() !== '' - ) -} diff --git a/apps/sim/lib/workflows/input-format.test.ts b/apps/sim/lib/workflows/input-format.test.ts new file mode 100644 index 0000000000..230e7d0890 --- /dev/null +++ b/apps/sim/lib/workflows/input-format.test.ts @@ -0,0 +1,229 @@ +import { describe, expect, it } from 'vitest' +import { + extractInputFieldsFromBlocks, + normalizeInputFormatValue, +} from '@/lib/workflows/input-format' + +describe('extractInputFieldsFromBlocks', () => { + it.concurrent('returns empty array for null blocks', () => { + expect(extractInputFieldsFromBlocks(null)).toEqual([]) + }) + + it.concurrent('returns empty array for undefined blocks', () => { + expect(extractInputFieldsFromBlocks(undefined)).toEqual([]) + }) + + it.concurrent('returns empty array when no trigger block exists', () => { + const blocks = { + 'block-1': { type: 'agent', subBlocks: {} }, + 'block-2': { type: 'function', subBlocks: {} }, + } + expect(extractInputFieldsFromBlocks(blocks)).toEqual([]) + }) + + it.concurrent('extracts fields from start_trigger block', () => { + const blocks = { + 'trigger-1': { + type: 'start_trigger', + subBlocks: { + inputFormat: { + value: [ + { name: 'query', type: 'string' }, + { name: 'count', type: 'number' }, + ], + }, + }, + }, + } + expect(extractInputFieldsFromBlocks(blocks)).toEqual([ + { name: 'query', type: 'string' }, + { name: 'count', type: 'number' }, + ]) + }) + + it.concurrent('extracts fields from input_trigger block', () => { + const blocks = { + 'trigger-1': { + type: 'input_trigger', + subBlocks: { + inputFormat: { + value: [{ name: 'message', type: 'string' }], + }, + }, + }, + } + expect(extractInputFieldsFromBlocks(blocks)).toEqual([{ name: 'message', type: 'string' }]) + }) + + it.concurrent('extracts fields from starter block', () => { + const blocks = { + 'trigger-1': { + type: 'starter', + subBlocks: { + inputFormat: { + value: [{ name: 'input', type: 'string' }], + }, + }, + }, + } + expect(extractInputFieldsFromBlocks(blocks)).toEqual([{ name: 'input', type: 'string' }]) + }) + + it.concurrent('defaults type to string when not provided', () => { + const blocks = { + 'trigger-1': { + type: 'start_trigger', + subBlocks: { + inputFormat: { + value: [{ name: 'field1' }, { name: 'field2', type: 'number' }], + }, + }, + }, + } + expect(extractInputFieldsFromBlocks(blocks)).toEqual([ + { name: 'field1', type: 'string' }, + { name: 'field2', type: 'number' }, + ]) + }) + + it.concurrent('filters out fields with empty names', () => { + const blocks = { + 'trigger-1': { + type: 'start_trigger', + subBlocks: { + inputFormat: { + value: [ + { name: '', type: 'string' }, + { name: 'valid', type: 'string' }, + { name: ' ' }, + ], + }, + }, + }, + } + expect(extractInputFieldsFromBlocks(blocks)).toEqual([{ name: 'valid', type: 'string' }]) + }) + + it.concurrent('filters out non-object fields', () => { + const blocks = { + 'trigger-1': { + type: 'start_trigger', + subBlocks: { + inputFormat: { + value: [null, undefined, 'string', 123, { name: 'valid', type: 'string' }], + }, + }, + }, + } + expect(extractInputFieldsFromBlocks(blocks)).toEqual([{ name: 'valid', type: 'string' }]) + }) + + it.concurrent('extracts from legacy config.params.inputFormat location', () => { + const blocks = { + 'trigger-1': { + type: 'start_trigger', + config: { + params: { + inputFormat: [{ name: 'legacy_field', type: 'string' }], + }, + }, + }, + } + expect(extractInputFieldsFromBlocks(blocks)).toEqual([{ name: 'legacy_field', type: 'string' }]) + }) + + it.concurrent('prefers subBlocks over config.params', () => { + const blocks = { + 'trigger-1': { + type: 'start_trigger', + subBlocks: { + inputFormat: { + value: [{ name: 'primary', type: 'string' }], + }, + }, + config: { + params: { + inputFormat: [{ name: 'legacy', type: 'string' }], + }, + }, + }, + } + expect(extractInputFieldsFromBlocks(blocks)).toEqual([{ name: 'primary', type: 'string' }]) + }) + + it.concurrent('returns empty array when inputFormat is not an array', () => { + const blocks = { + 'trigger-1': { + type: 'start_trigger', + subBlocks: { + inputFormat: { + value: 'not-an-array', + }, + }, + }, + } + expect(extractInputFieldsFromBlocks(blocks)).toEqual([]) + }) +}) + +describe('normalizeInputFormatValue', () => { + it.concurrent('returns empty array for null', () => { + expect(normalizeInputFormatValue(null)).toEqual([]) + }) + + it.concurrent('returns empty array for undefined', () => { + expect(normalizeInputFormatValue(undefined)).toEqual([]) + }) + + it.concurrent('returns empty array for empty array', () => { + expect(normalizeInputFormatValue([])).toEqual([]) + }) + + it.concurrent('returns empty array for non-array values', () => { + expect(normalizeInputFormatValue('string')).toEqual([]) + expect(normalizeInputFormatValue(123)).toEqual([]) + expect(normalizeInputFormatValue({ name: 'test' })).toEqual([]) + }) + + it.concurrent('filters fields with valid names', () => { + const input = [ + { name: 'valid1', type: 'string' }, + { name: 'valid2', type: 'number' }, + ] + expect(normalizeInputFormatValue(input)).toEqual(input) + }) + + it.concurrent('filters out fields without names', () => { + const input = [{ type: 'string' }, { name: 'valid', type: 'string' }, { value: 'test' }] + expect(normalizeInputFormatValue(input)).toEqual([{ name: 'valid', type: 'string' }]) + }) + + it.concurrent('filters out fields with empty names', () => { + const input = [ + { name: '', type: 'string' }, + { name: ' ', type: 'string' }, + { name: 'valid', type: 'string' }, + ] + expect(normalizeInputFormatValue(input)).toEqual([{ name: 'valid', type: 'string' }]) + }) + + it.concurrent('filters out null and undefined fields', () => { + const input = [null, undefined, { name: 'valid', type: 'string' }] + expect(normalizeInputFormatValue(input)).toEqual([{ name: 'valid', type: 'string' }]) + }) + + it.concurrent('preserves all properties of valid fields', () => { + const input = [ + { + name: 'field1', + type: 'string', + label: 'Field 1', + description: 'A test field', + placeholder: 'Enter value', + required: true, + value: 'default', + }, + ] + expect(normalizeInputFormatValue(input)).toEqual(input) + }) +}) diff --git a/apps/sim/lib/workflows/input-format.ts b/apps/sim/lib/workflows/input-format.ts new file mode 100644 index 0000000000..5e857464d4 --- /dev/null +++ b/apps/sim/lib/workflows/input-format.ts @@ -0,0 +1,111 @@ +import type { InputFormatField } from '@/lib/workflows/types' + +/** + * Simplified input field representation for workflow input mapping + */ +export interface WorkflowInputField { + name: string + type: string +} + +/** + * Extracts input fields from workflow blocks. + * Finds the trigger block (start_trigger, input_trigger, or starter) and extracts its inputFormat. + * + * @param blocks - The blocks object from workflow state + * @returns Array of input field definitions + */ +export function extractInputFieldsFromBlocks( + blocks: Record | null | undefined +): WorkflowInputField[] { + if (!blocks) return [] + + // Find trigger block + const triggerEntry = Object.entries(blocks).find(([, block]) => { + const b = block as Record + return b.type === 'start_trigger' || b.type === 'input_trigger' || b.type === 'starter' + }) + + if (!triggerEntry) return [] + + const triggerBlock = triggerEntry[1] as Record + const subBlocks = triggerBlock.subBlocks as Record | undefined + const inputFormat = subBlocks?.inputFormat?.value + + // Try primary location: subBlocks.inputFormat.value + if (Array.isArray(inputFormat)) { + return inputFormat + .filter( + (field: unknown): field is { name: string; type?: string } => + typeof field === 'object' && + field !== null && + 'name' in field && + typeof (field as { name: unknown }).name === 'string' && + (field as { name: string }).name.trim() !== '' + ) + .map((field) => ({ + name: field.name, + type: field.type || 'string', + })) + } + + // Try legacy location: config.params.inputFormat + const config = triggerBlock.config as { params?: { inputFormat?: unknown } } | undefined + const legacyFormat = config?.params?.inputFormat + + if (Array.isArray(legacyFormat)) { + return legacyFormat + .filter( + (field: unknown): field is { name: string; type?: string } => + typeof field === 'object' && + field !== null && + 'name' in field && + typeof (field as { name: unknown }).name === 'string' && + (field as { name: string }).name.trim() !== '' + ) + .map((field) => ({ + name: field.name, + type: field.type || 'string', + })) + } + + return [] +} + +/** + * Normalizes an input format value into a list of valid fields. + * + * Filters out: + * - null or undefined values + * - Empty arrays + * - Non-array values + * - Fields without names + * - Fields with empty or whitespace-only names + * + * @param inputFormatValue - Raw input format value from subblock state + * @returns Array of validated input format fields + */ +export function normalizeInputFormatValue(inputFormatValue: unknown): InputFormatField[] { + // Handle null, undefined, and empty arrays + if ( + inputFormatValue === null || + inputFormatValue === undefined || + (Array.isArray(inputFormatValue) && inputFormatValue.length === 0) + ) { + return [] + } + + // Handle non-array values + if (!Array.isArray(inputFormatValue)) { + return [] + } + + // Filter valid fields + return inputFormatValue.filter( + (field): field is InputFormatField => + field && + typeof field === 'object' && + typeof field.name === 'string' && + field.name.trim() !== '' + ) +} diff --git a/apps/sim/serializer/__test-utils__/test-workflows.ts b/apps/sim/serializer/__test-utils__/test-workflows.ts deleted file mode 100644 index eba41e15f0..0000000000 --- a/apps/sim/serializer/__test-utils__/test-workflows.ts +++ /dev/null @@ -1,662 +0,0 @@ -/** - * Test Workflows - * - * This file contains test fixtures for serializer tests, providing - * sample workflow states with different configurations. - */ -import type { Edge } from 'reactflow' -import type { BlockState, Loop } from '@/stores/workflows/workflow/types' - -/** - * Workflow State Interface - */ -export interface WorkflowStateFixture { - blocks: Record - edges: Edge[] - loops: Record -} - -/** - * Create a minimal workflow with just a starter and one block - */ -export function createMinimalWorkflowState(): WorkflowStateFixture { - const blocks: Record = { - starter: { - id: 'starter', - type: 'starter', - name: 'Starter Block', - position: { x: 0, y: 0 }, - subBlocks: { - description: { - id: 'description', - type: 'long-input', - value: 'This is the starter block', - }, - }, - outputs: {}, - enabled: true, - }, - agent1: { - id: 'agent1', - type: 'agent', - name: 'Agent Block', - position: { x: 300, y: 0 }, - subBlocks: { - provider: { - id: 'provider', - type: 'dropdown', - value: 'anthropic', - }, - model: { - id: 'model', - type: 'dropdown', - value: 'claude-3-7-sonnet-20250219', - }, - prompt: { - id: 'prompt', - type: 'long-input', - value: 'Hello, world!', - }, - tools: { - id: 'tools', - type: 'tool-input', - value: '[]', - }, - system: { - id: 'system', - type: 'long-input', - value: 'You are a helpful assistant.', - }, - responseFormat: { - id: 'responseFormat', - type: 'code', - value: null, - }, - }, - outputs: {}, - enabled: true, - }, - } - - const edges: Edge[] = [ - { - id: 'edge1', - source: 'starter', - target: 'agent1', - }, - ] - - const loops: Record = {} - - return { blocks, edges, loops } -} - -/** - * Create a workflow with condition blocks - */ -export function createConditionalWorkflowState(): WorkflowStateFixture { - const blocks: Record = { - starter: { - id: 'starter', - type: 'starter', - name: 'Starter Block', - position: { x: 0, y: 0 }, - subBlocks: { - description: { - id: 'description', - type: 'long-input', - value: 'This is the starter block', - }, - }, - outputs: {}, - enabled: true, - }, - condition1: { - id: 'condition1', - type: 'condition', - name: 'Condition Block', - position: { x: 300, y: 0 }, - subBlocks: { - condition: { - id: 'condition', - type: 'long-input', - value: 'input.value > 10', - }, - }, - outputs: {}, - enabled: true, - }, - agent1: { - id: 'agent1', - type: 'agent', - name: 'True Path Agent', - position: { x: 600, y: -100 }, - subBlocks: { - provider: { - id: 'provider', - type: 'dropdown', - value: 'anthropic', - }, - model: { - id: 'model', - type: 'dropdown', - value: 'claude-3-7-sonnet-20250219', - }, - prompt: { - id: 'prompt', - type: 'long-input', - value: 'Value is greater than 10', - }, - tools: { - id: 'tools', - type: 'tool-input', - value: '[]', - }, - system: { - id: 'system', - type: 'long-input', - value: 'You are a helpful assistant.', - }, - responseFormat: { - id: 'responseFormat', - type: 'code', - value: null, - }, - }, - outputs: {}, - enabled: true, - }, - agent2: { - id: 'agent2', - type: 'agent', - name: 'False Path Agent', - position: { x: 600, y: 100 }, - subBlocks: { - provider: { - id: 'provider', - type: 'dropdown', - value: 'anthropic', - }, - model: { - id: 'model', - type: 'dropdown', - value: 'claude-3-7-sonnet-20250219', - }, - prompt: { - id: 'prompt', - type: 'long-input', - value: 'Value is less than or equal to 10', - }, - tools: { - id: 'tools', - type: 'tool-input', - value: '[]', - }, - system: { - id: 'system', - type: 'long-input', - value: 'You are a helpful assistant.', - }, - responseFormat: { - id: 'responseFormat', - type: 'code', - value: null, - }, - }, - outputs: {}, - enabled: true, - }, - } - - const edges: Edge[] = [ - { - id: 'edge1', - source: 'starter', - target: 'condition1', - }, - { - id: 'edge2', - source: 'condition1', - target: 'agent1', - sourceHandle: 'condition-true', - }, - { - id: 'edge3', - source: 'condition1', - target: 'agent2', - sourceHandle: 'condition-false', - }, - ] - - const loops: Record = {} - - return { blocks, edges, loops } -} - -/** - * Create a workflow with a loop - */ -export function createLoopWorkflowState(): WorkflowStateFixture { - const blocks: Record = { - starter: { - id: 'starter', - type: 'starter', - name: 'Starter Block', - position: { x: 0, y: 0 }, - subBlocks: { - description: { - id: 'description', - type: 'long-input', - value: 'This is the starter block', - }, - }, - outputs: {}, - enabled: true, - }, - function1: { - id: 'function1', - type: 'function', - name: 'Function Block', - position: { x: 300, y: 0 }, - subBlocks: { - code: { - id: 'code', - type: 'code', - value: 'let counter = input.counter || 0;\ncounter++;\nreturn { counter };', - }, - language: { - id: 'language', - type: 'dropdown', - value: 'javascript', - }, - }, - outputs: {}, - enabled: true, - }, - condition1: { - id: 'condition1', - type: 'condition', - name: 'Loop Condition', - position: { x: 600, y: 0 }, - subBlocks: { - condition: { - id: 'condition', - type: 'long-input', - value: 'input.counter < 5', - }, - }, - outputs: {}, - enabled: true, - }, - agent1: { - id: 'agent1', - type: 'agent', - name: 'Loop Complete Agent', - position: { x: 900, y: 100 }, - subBlocks: { - provider: { - id: 'provider', - type: 'dropdown', - value: 'anthropic', - }, - model: { - id: 'model', - type: 'dropdown', - value: 'claude-3-7-sonnet-20250219', - }, - prompt: { - id: 'prompt', - type: 'long-input', - value: 'Loop completed after {{input.counter}} iterations', - }, - tools: { - id: 'tools', - type: 'tool-input', - value: '[]', - }, - system: { - id: 'system', - type: 'long-input', - value: 'You are a helpful assistant.', - }, - responseFormat: { - id: 'responseFormat', - type: 'code', - value: null, - }, - }, - outputs: {}, - enabled: true, - }, - } - - const edges: Edge[] = [ - { - id: 'edge1', - source: 'starter', - target: 'function1', - }, - { - id: 'edge2', - source: 'function1', - target: 'condition1', - }, - { - id: 'edge3', - source: 'condition1', - target: 'function1', - sourceHandle: 'condition-true', - }, - { - id: 'edge4', - source: 'condition1', - target: 'agent1', - sourceHandle: 'condition-false', - }, - ] - - const loops: Record = { - loop1: { - id: 'loop1', - nodes: ['function1', 'condition1'], - iterations: 10, - loopType: 'for', - }, - } - - return { blocks, edges, loops } -} - -/** - * Create a workflow with multiple block types - */ -export function createComplexWorkflowState(): WorkflowStateFixture { - const blocks: Record = { - starter: { - id: 'starter', - type: 'starter', - name: 'Starter Block', - position: { x: 0, y: 0 }, - subBlocks: { - description: { - id: 'description', - type: 'long-input', - value: 'This is the starter block', - }, - }, - outputs: {}, - enabled: true, - }, - api1: { - id: 'api1', - type: 'api', - name: 'API Request', - position: { x: 300, y: 0 }, - subBlocks: { - url: { - id: 'url', - type: 'short-input', - value: 'https://api.example.com/data', - }, - method: { - id: 'method', - type: 'dropdown', - value: 'GET', - }, - headers: { - id: 'headers', - type: 'table', - value: [ - ['Content-Type', 'application/json'], - ['Authorization', 'Bearer {{API_KEY}}'], - ], - }, - body: { - id: 'body', - type: 'long-input', - value: '', - }, - }, - outputs: {}, - enabled: true, - }, - function1: { - id: 'function1', - type: 'function', - name: 'Process Data', - position: { x: 600, y: 0 }, - subBlocks: { - code: { - id: 'code', - type: 'code', - value: 'const data = input.data;\nreturn { processed: data.map(item => item.name) };', - }, - language: { - id: 'language', - type: 'dropdown', - value: 'javascript', - }, - }, - outputs: {}, - enabled: true, - }, - agent1: { - id: 'agent1', - type: 'agent', - name: 'Summarize Data', - position: { x: 900, y: 0 }, - subBlocks: { - provider: { - id: 'provider', - type: 'dropdown', - value: 'openai', - }, - model: { - id: 'model', - type: 'dropdown', - value: 'gpt-4o', - }, - prompt: { - id: 'prompt', - type: 'long-input', - value: 'Summarize the following data:\n\n{{input.processed}}', - }, - tools: { - id: 'tools', - type: 'tool-input', - value: - '[{"type":"function","name":"calculator","description":"Perform calculations","parameters":{"type":"object","properties":{"expression":{"type":"string","description":"Math expression to evaluate"}},"required":["expression"]}}]', - }, - system: { - id: 'system', - type: 'long-input', - value: 'You are a data analyst assistant.', - }, - responseFormat: { - id: 'responseFormat', - type: 'code', - value: - '{"type":"object","properties":{"summary":{"type":"string"},"keyPoints":{"type":"array","items":{"type":"string"}},"sentiment":{"type":"string","enum":["positive","neutral","negative"]}},"required":["summary","keyPoints","sentiment"]}', - }, - }, - outputs: {}, - enabled: true, - }, - } - - const edges: Edge[] = [ - { - id: 'edge1', - source: 'starter', - target: 'api1', - }, - { - id: 'edge2', - source: 'api1', - target: 'function1', - }, - { - id: 'edge3', - source: 'function1', - target: 'agent1', - }, - ] - - const loops: Record = {} - - return { blocks, edges, loops } -} - -/** - * Create a workflow with agent blocks that have custom tools - */ -export function createAgentWithToolsWorkflowState(): WorkflowStateFixture { - const blocks: Record = { - starter: { - id: 'starter', - type: 'starter', - name: 'Starter Block', - position: { x: 0, y: 0 }, - subBlocks: { - description: { - id: 'description', - type: 'long-input', - value: 'This is the starter block', - }, - }, - outputs: {}, - enabled: true, - }, - agent1: { - id: 'agent1', - type: 'agent', - name: 'Custom Tools Agent', - position: { x: 300, y: 0 }, - subBlocks: { - provider: { - id: 'provider', - type: 'dropdown', - value: 'openai', - }, - model: { - id: 'model', - type: 'dropdown', - value: 'gpt-4o', - }, - prompt: { - id: 'prompt', - type: 'long-input', - value: 'Use the tools to help answer: {{input.question}}', - }, - tools: { - id: 'tools', - type: 'tool-input', - value: - '[{"type":"custom-tool","name":"weather","description":"Get current weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}},{"type":"function","name":"calculator","description":"Calculate expression","parameters":{"type":"object","properties":{"expression":{"type":"string"}},"required":["expression"]}}]', - }, - system: { - id: 'system', - type: 'long-input', - value: 'You are a helpful assistant with access to tools.', - }, - responseFormat: { - id: 'responseFormat', - type: 'code', - value: null, - }, - }, - outputs: {}, - enabled: true, - }, - } - - const edges: Edge[] = [ - { - id: 'edge1', - source: 'starter', - target: 'agent1', - }, - ] - - const loops: Record = {} - - return { blocks, edges, loops } -} - -/** - * Create a workflow state with an invalid block type for error testing - */ -export function createInvalidWorkflowState(): WorkflowStateFixture { - const { blocks, edges, loops } = createMinimalWorkflowState() - - // Add an invalid block type - blocks.invalid = { - id: 'invalid', - type: 'invalid-type', - name: 'Invalid Block', - position: { x: 600, y: 0 }, - subBlocks: {}, - outputs: {}, - enabled: true, - } - - edges.push({ - id: 'edge-invalid', - source: 'agent1', - target: 'invalid', - }) - - return { blocks, edges, loops } -} - -/** - * Create a serialized workflow with invalid metadata for error testing - */ -export function createInvalidSerializedWorkflow() { - return { - version: '1.0', - blocks: [ - { - id: 'invalid', - position: { x: 0, y: 0 }, - config: { - tool: 'invalid', - params: {}, - }, - inputs: {}, - outputs: {}, - metadata: { - id: 'non-existent-type', - }, - enabled: true, - }, - ], - connections: [], - loops: {}, - } -} - -/** - * Create a serialized workflow with missing metadata for error testing - */ -export function createMissingMetadataWorkflow() { - return { - version: '1.0', - blocks: [ - { - id: 'invalid', - position: { x: 0, y: 0 }, - config: { - tool: 'invalid', - params: {}, - }, - inputs: {}, - outputs: {}, - metadata: undefined, - enabled: true, - }, - ], - connections: [], - loops: {}, - } -} diff --git a/apps/sim/serializer/index.test.ts b/apps/sim/serializer/index.test.ts index f9f0900513..bca9c43c42 100644 --- a/apps/sim/serializer/index.test.ts +++ b/apps/sim/serializer/index.test.ts @@ -7,9 +7,7 @@ * converting between workflow state (blocks, edges, loops) and serialized format * used by the executor. */ -import { loggerMock } from '@sim/testing' -import { describe, expect, it, vi } from 'vitest' -import { getProviderFromModel } from '@/providers/utils' + import { createAgentWithToolsWorkflowState, createComplexWorkflowState, @@ -19,227 +17,17 @@ import { createLoopWorkflowState, createMinimalWorkflowState, createMissingMetadataWorkflow, -} from '@/serializer/__test-utils__/test-workflows' +} from '@sim/testing/factories' +import { blocksMock, loggerMock, toolsUtilsMock } from '@sim/testing/mocks' +import { describe, expect, it, vi } from 'vitest' import { Serializer } from '@/serializer/index' import type { SerializedWorkflow } from '@/serializer/types' -vi.mock('@/blocks', () => ({ - getBlock: (type: string) => { - const mockConfigs: Record = { - starter: { - name: 'Starter', - description: 'Start of the workflow', - category: 'flow', - bgColor: '#4CAF50', - tools: { - access: ['starter'], - config: { - tool: () => 'starter', - }, - }, - subBlocks: [{ id: 'description', type: 'long-input', label: 'Description' }], - inputs: {}, - }, - agent: { - name: 'Agent', - description: 'AI Agent', - category: 'ai', - bgColor: '#2196F3', - tools: { - access: ['anthropic_chat', 'openai_chat', 'google_chat'], - config: { - tool: (params: Record) => getProviderFromModel(params.model || 'gpt-4o'), - }, - }, - subBlocks: [ - { id: 'provider', type: 'dropdown', label: 'Provider' }, - { id: 'model', type: 'dropdown', label: 'Model' }, - { id: 'prompt', type: 'long-input', label: 'Prompt' }, - { id: 'tools', type: 'tool-input', label: 'Tools' }, - { id: 'system', type: 'long-input', label: 'System Message' }, - { id: 'responseFormat', type: 'code', label: 'Response Format' }, - ], - inputs: { - input: { type: 'string' }, - tools: { type: 'array' }, - }, - }, - condition: { - name: 'Condition', - description: 'Branch based on condition', - category: 'flow', - bgColor: '#FF9800', - tools: { - access: ['condition'], - config: { - tool: () => 'condition', - }, - }, - subBlocks: [{ id: 'condition', type: 'long-input', label: 'Condition' }], - inputs: { - input: { type: 'any' }, - }, - }, - function: { - name: 'Function', - description: 'Execute custom code', - category: 'code', - bgColor: '#9C27B0', - tools: { - access: ['function'], - config: { - tool: () => 'function', - }, - }, - subBlocks: [ - { id: 'code', type: 'code', label: 'Code' }, - { id: 'language', type: 'dropdown', label: 'Language' }, - ], - inputs: { - input: { type: 'any' }, - }, - }, - api: { - name: 'API', - description: 'Make API request', - category: 'data', - bgColor: '#E91E63', - tools: { - access: ['api'], - config: { - tool: () => 'api', - }, - }, - subBlocks: [ - { id: 'url', type: 'short-input', label: 'URL' }, - { id: 'method', type: 'dropdown', label: 'Method' }, - { id: 'headers', type: 'table', label: 'Headers' }, - { id: 'body', type: 'long-input', label: 'Body' }, - ], - inputs: {}, - }, - jina: { - name: 'Jina', - description: 'Convert website content into text', - category: 'tools', - bgColor: '#333333', - tools: { - access: ['jina_read_url'], - config: { - tool: () => 'jina_read_url', - }, - }, - subBlocks: [ - { id: 'url', type: 'short-input', title: 'URL', required: true }, - { id: 'apiKey', type: 'short-input', title: 'API Key', required: true }, - ], - inputs: { - url: { type: 'string' }, - apiKey: { type: 'string' }, - }, - }, - reddit: { - name: 'Reddit', - description: 'Access Reddit data and content', - category: 'tools', - bgColor: '#FF5700', - tools: { - access: ['reddit_get_posts', 'reddit_get_comments'], - config: { - tool: () => 'reddit_get_posts', - }, - }, - subBlocks: [ - { id: 'operation', type: 'dropdown', title: 'Operation', required: true }, - { id: 'credential', type: 'oauth-input', title: 'Reddit Account', required: true }, - { id: 'subreddit', type: 'short-input', title: 'Subreddit', required: true }, - ], - inputs: { - operation: { type: 'string' }, - credential: { type: 'string' }, - subreddit: { type: 'string' }, - }, - }, - slack: { - name: 'Slack', - description: 'Send messages to Slack', - category: 'tools', - bgColor: '#611f69', - tools: { - access: ['slack_send_message'], - config: { - tool: () => 'slack_send_message', - }, - }, - subBlocks: [ - { id: 'channel', type: 'dropdown', title: 'Channel', mode: 'basic' }, - { id: 'manualChannel', type: 'short-input', title: 'Channel ID', mode: 'advanced' }, - { id: 'text', type: 'long-input', title: 'Message' }, - { id: 'username', type: 'short-input', title: 'Username', mode: 'both' }, - ], - inputs: { - channel: { type: 'string' }, - manualChannel: { type: 'string' }, - text: { type: 'string' }, - username: { type: 'string' }, - }, - }, - agentWithMemories: { - name: 'Agent with Memories', - description: 'AI Agent with memory support', - category: 'ai', - bgColor: '#2196F3', - tools: { - access: ['anthropic_chat'], - config: { - tool: () => 'anthropic_chat', - }, - }, - subBlocks: [ - { id: 'systemPrompt', type: 'long-input', title: 'System Prompt' }, - { id: 'userPrompt', type: 'long-input', title: 'User Prompt' }, - { id: 'memories', type: 'short-input', title: 'Memories', mode: 'advanced' }, - { id: 'model', type: 'dropdown', title: 'Model' }, - ], - inputs: { - systemPrompt: { type: 'string' }, - userPrompt: { type: 'string' }, - memories: { type: 'array' }, - model: { type: 'string' }, - }, - }, - } - - return mockConfigs[type] || null - }, -})) - -vi.mock('@/tools/utils', () => ({ - getTool: (toolId: string) => { - const mockTools: Record = { - jina_read_url: { - params: { - url: { visibility: 'user-or-llm', required: true }, - apiKey: { visibility: 'user-only', required: true }, - }, - }, - reddit_get_posts: { - params: { - subreddit: { visibility: 'user-or-llm', required: true }, - credential: { visibility: 'user-only', required: true }, - }, - }, - } - return mockTools[toolId] || null - }, -})) - +vi.mock('@/blocks', () => blocksMock) +vi.mock('@/tools/utils', () => toolsUtilsMock) vi.mock('@sim/logger', () => loggerMock) describe('Serializer', () => { - /** - * Serialization tests - */ describe('serializeWorkflow', () => { it.concurrent('should serialize a minimal workflow correctly', () => { const { blocks, edges, loops } = createMinimalWorkflowState() @@ -361,7 +149,6 @@ describe('Serializer', () => { const toolsParam = agentBlock?.config.params.tools expect(toolsParam).toBeDefined() - // Parse tools to verify content const tools = JSON.parse(toolsParam as string) expect(tools).toHaveLength(2) @@ -384,9 +171,6 @@ describe('Serializer', () => { }) }) - /** - * Deserialization tests - */ describe('deserializeWorkflow', () => { it.concurrent('should deserialize a serialized workflow correctly', () => { const { blocks, edges, loops } = createMinimalWorkflowState() @@ -465,9 +249,6 @@ describe('Serializer', () => { }) }) - /** - * End-to-end serialization/deserialization tests - */ describe('round-trip serialization', () => { it.concurrent('should preserve all data through serialization and deserialization', () => { const { blocks, edges, loops } = createComplexWorkflowState() diff --git a/apps/sim/serializer/tests/dual-validation.test.ts b/apps/sim/serializer/tests/dual-validation.test.ts index bfb3496229..ac9d4dc2ec 100644 --- a/apps/sim/serializer/tests/dual-validation.test.ts +++ b/apps/sim/serializer/tests/dual-validation.test.ts @@ -7,52 +7,11 @@ * 1. Early validation (serialization) - user-only required fields * 2. Late validation (tool execution) - user-or-llm required fields */ +import { blocksMock } from '@sim/testing/mocks' import { describe, expect, it, vi } from 'vitest' import { Serializer } from '@/serializer/index' -vi.mock('@/blocks', () => ({ - getBlock: (type: string) => { - const mockConfigs: Record = { - jina: { - name: 'Jina', - description: 'Convert website content into text', - category: 'tools', - bgColor: '#333333', - tools: { - access: ['jina_read_url'], - }, - subBlocks: [ - { id: 'url', type: 'short-input', title: 'URL', required: true }, - { id: 'apiKey', type: 'short-input', title: 'API Key', required: true }, - ], - inputs: { - url: { type: 'string' }, - apiKey: { type: 'string' }, - }, - }, - reddit: { - name: 'Reddit', - description: 'Access Reddit data', - category: 'tools', - bgColor: '#FF5700', - tools: { - access: ['reddit_get_posts'], - }, - subBlocks: [ - { id: 'operation', type: 'dropdown', title: 'Operation', required: true }, - { id: 'credential', type: 'oauth-input', title: 'Reddit Account', required: true }, - { id: 'subreddit', type: 'short-input', title: 'Subreddit', required: true }, - ], - inputs: { - operation: { type: 'string' }, - credential: { type: 'string' }, - subreddit: { type: 'string' }, - }, - }, - } - return mockConfigs[type] || null - }, -})) +vi.mock('@/blocks', () => blocksMock) /** * Validates required parameters after user and LLM parameter merge. diff --git a/apps/sim/serializer/tests/serializer.extended.test.ts b/apps/sim/serializer/tests/serializer.extended.test.ts index 0286ad4777..d66b40b242 100644 --- a/apps/sim/serializer/tests/serializer.extended.test.ts +++ b/apps/sim/serializer/tests/serializer.extended.test.ts @@ -4,7 +4,6 @@ * Extended Serializer Tests * * These tests cover edge cases, complex scenarios, and gaps in coverage - * for the Serializer class using @sim/testing helpers. */ import { @@ -13,9 +12,9 @@ import { createLoopWorkflow, createParallelWorkflow, createStarterBlock, - loggerMock, WorkflowBuilder, } from '@sim/testing' +import { loggerMock, toolsUtilsMock } from '@sim/testing/mocks' import { describe, expect, it, vi } from 'vitest' import { Serializer, WorkflowValidationError } from '@/serializer/index' import type { SerializedWorkflow } from '@/serializer/types' @@ -29,195 +28,207 @@ function asAppBlocks(blocks: T): Record { return blocks as unknown as Record } -vi.mock('@/blocks', () => ({ - getBlock: (type: string) => { - const mockConfigs: Record = { - starter: { - name: 'Starter', - description: 'Start of the workflow', - category: 'flow', - bgColor: '#4CAF50', - tools: { - access: ['starter'], - config: { tool: () => 'starter' }, - }, - subBlocks: [ - { id: 'description', type: 'long-input', label: 'Description' }, - { id: 'inputFormat', type: 'table', label: 'Input Format' }, - ], - inputs: {}, +/** + * Hoisted mock setup - vi.mock is hoisted, so we need to hoist the config too. + */ +const { mockBlockConfigs, createMockGetBlock, slackWithCanonicalParam } = vi.hoisted(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const mockBlockConfigs: Record = { + starter: { + name: 'Starter', + description: 'Start of the workflow', + category: 'flow', + bgColor: '#4CAF50', + tools: { + access: ['starter'], + config: { tool: () => 'starter' }, }, - agent: { - name: 'Agent', - description: 'AI Agent', - category: 'ai', - bgColor: '#2196F3', - tools: { - access: ['anthropic_chat', 'openai_chat'], - config: { - tool: (params: Record) => { - const model = params.model || 'gpt-4o' - if (model.includes('claude')) return 'anthropic' - if (model.includes('gpt') || model.includes('o1')) return 'openai' - if (model.includes('gemini')) return 'google' - return 'openai' - }, + subBlocks: [ + { id: 'description', type: 'long-input', label: 'Description' }, + { id: 'inputFormat', type: 'table', label: 'Input Format' }, + ], + inputs: {}, + }, + agent: { + name: 'Agent', + description: 'AI Agent', + category: 'ai', + bgColor: '#2196F3', + tools: { + access: ['anthropic_chat', 'openai_chat'], + config: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + tool: (params: Record) => { + const model = params.model || 'gpt-4o' + if (model.includes('claude')) return 'anthropic' + if (model.includes('gpt') || model.includes('o1')) return 'openai' + if (model.includes('gemini')) return 'google' + return 'openai' }, }, - subBlocks: [ - { id: 'provider', type: 'dropdown', label: 'Provider' }, - { id: 'model', type: 'dropdown', label: 'Model' }, - { id: 'prompt', type: 'long-input', label: 'Prompt' }, - { id: 'system', type: 'long-input', label: 'System Message' }, - { id: 'tools', type: 'tool-input', label: 'Tools' }, - { id: 'responseFormat', type: 'code', label: 'Response Format' }, - { id: 'messages', type: 'messages-input', label: 'Messages' }, - ], - inputs: { - input: { type: 'string' }, - tools: { type: 'array' }, - }, }, - function: { - name: 'Function', - description: 'Execute custom code', - category: 'code', - bgColor: '#9C27B0', - tools: { - access: ['function'], - config: { tool: () => 'function' }, - }, - subBlocks: [ - { id: 'code', type: 'code', label: 'Code' }, - { id: 'language', type: 'dropdown', label: 'Language' }, - ], - inputs: { input: { type: 'any' } }, + subBlocks: [ + { id: 'provider', type: 'dropdown', label: 'Provider' }, + { id: 'model', type: 'dropdown', label: 'Model' }, + { id: 'prompt', type: 'long-input', label: 'Prompt' }, + { id: 'system', type: 'long-input', label: 'System Message' }, + { id: 'tools', type: 'tool-input', label: 'Tools' }, + { id: 'responseFormat', type: 'code', label: 'Response Format' }, + { id: 'messages', type: 'messages-input', label: 'Messages' }, + ], + inputs: { + input: { type: 'string' }, + tools: { type: 'array' }, }, - condition: { - name: 'Condition', - description: 'Branch based on condition', - category: 'flow', - bgColor: '#FF9800', - tools: { - access: ['condition'], - config: { tool: () => 'condition' }, - }, - subBlocks: [{ id: 'condition', type: 'long-input', label: 'Condition' }], - inputs: { input: { type: 'any' } }, + }, + function: { + name: 'Function', + description: 'Execute custom code', + category: 'code', + bgColor: '#9C27B0', + tools: { + access: ['function'], + config: { tool: () => 'function' }, }, - api: { - name: 'API', - description: 'Make API request', - category: 'data', - bgColor: '#E91E63', - tools: { - access: ['api'], - config: { tool: () => 'api' }, - }, - subBlocks: [ - { id: 'url', type: 'short-input', label: 'URL' }, - { id: 'method', type: 'dropdown', label: 'Method' }, - { id: 'headers', type: 'table', label: 'Headers' }, - { id: 'body', type: 'long-input', label: 'Body' }, - ], - inputs: {}, + subBlocks: [ + { id: 'code', type: 'code', label: 'Code' }, + { id: 'language', type: 'dropdown', label: 'Language' }, + ], + inputs: { input: { type: 'any' } }, + }, + condition: { + name: 'Condition', + description: 'Branch based on condition', + category: 'flow', + bgColor: '#FF9800', + tools: { + access: ['condition'], + config: { tool: () => 'condition' }, }, - webhook: { - name: 'Webhook', - description: 'Webhook trigger', - category: 'triggers', - bgColor: '#4CAF50', - tools: { - access: ['webhook'], - config: { tool: () => 'webhook' }, - }, - subBlocks: [{ id: 'path', type: 'short-input', label: 'Path' }], - inputs: {}, + subBlocks: [{ id: 'condition', type: 'long-input', label: 'Condition' }], + inputs: { input: { type: 'any' } }, + }, + api: { + name: 'API', + description: 'Make API request', + category: 'data', + bgColor: '#E91E63', + tools: { + access: ['api'], + config: { tool: () => 'api' }, }, - slack: { - name: 'Slack', - description: 'Send messages to Slack', - category: 'tools', - bgColor: '#611f69', - tools: { - access: ['slack_send_message'], - config: { tool: () => 'slack_send_message' }, - }, - subBlocks: [ - { id: 'channel', type: 'dropdown', label: 'Channel', mode: 'basic' }, - { - id: 'manualChannel', - type: 'short-input', - label: 'Channel ID', - mode: 'advanced', - canonicalParamId: 'targetChannel', - }, - { - id: 'channelSelector', - type: 'dropdown', - label: 'Channel Selector', - mode: 'basic', - canonicalParamId: 'targetChannel', - }, - { id: 'text', type: 'long-input', label: 'Message' }, - { id: 'username', type: 'short-input', label: 'Username', mode: 'both' }, - ], - inputs: { text: { type: 'string' } }, + subBlocks: [ + { id: 'url', type: 'short-input', label: 'URL' }, + { id: 'method', type: 'dropdown', label: 'Method' }, + { id: 'headers', type: 'table', label: 'Headers' }, + { id: 'body', type: 'long-input', label: 'Body' }, + ], + inputs: {}, + }, + webhook: { + name: 'Webhook', + description: 'Webhook trigger', + category: 'triggers', + bgColor: '#4CAF50', + tools: { + access: ['webhook'], + config: { tool: () => 'webhook' }, }, - conditional_block: { - name: 'Conditional Block', - description: 'Block with conditional fields', - category: 'tools', - bgColor: '#FF5700', - tools: { - access: ['conditional_tool'], - config: { tool: () => 'conditional_tool' }, + subBlocks: [{ id: 'path', type: 'short-input', label: 'Path' }], + inputs: {}, + }, + slack: { + name: 'Slack', + description: 'Send messages to Slack', + category: 'tools', + bgColor: '#611f69', + tools: { + access: ['slack_send_message'], + config: { tool: () => 'slack_send_message' }, + }, + subBlocks: [ + { id: 'channel', type: 'dropdown', label: 'Channel', mode: 'basic' }, + { + id: 'manualChannel', + type: 'short-input', + label: 'Channel ID', + mode: 'advanced', + canonicalParamId: 'targetChannel', }, - subBlocks: [ - { id: 'mode', type: 'dropdown', label: 'Mode' }, - { - id: 'optionA', - type: 'short-input', - label: 'Option A', - condition: { field: 'mode', value: 'a' }, - }, - { - id: 'optionB', - type: 'short-input', - label: 'Option B', - condition: { field: 'mode', value: 'b' }, - }, - { - id: 'notModeC', - type: 'short-input', - label: 'Not Mode C', - condition: { field: 'mode', value: 'c', not: true }, - }, - { - id: 'complexCondition', - type: 'short-input', - label: 'Complex', - condition: { field: 'mode', value: 'a', and: { field: 'optionA', value: 'special' } }, - }, - { - id: 'arrayCondition', - type: 'short-input', - label: 'Array Condition', - condition: { field: 'mode', value: ['a', 'b'] }, - }, - ], - inputs: {}, + { + id: 'channelSelector', + type: 'dropdown', + label: 'Channel Selector', + mode: 'basic', + canonicalParamId: 'targetChannel', + }, + { id: 'text', type: 'long-input', label: 'Message' }, + { id: 'username', type: 'short-input', label: 'Username', mode: 'both' }, + ], + inputs: { text: { type: 'string' } }, + }, + conditional_block: { + name: 'Conditional Block', + description: 'Block with conditional fields', + category: 'tools', + bgColor: '#FF5700', + tools: { + access: ['conditional_tool'], + config: { tool: () => 'conditional_tool' }, }, - } + subBlocks: [ + { id: 'mode', type: 'dropdown', label: 'Mode' }, + { + id: 'optionA', + type: 'short-input', + label: 'Option A', + condition: { field: 'mode', value: 'a' }, + }, + { + id: 'optionB', + type: 'short-input', + label: 'Option B', + condition: { field: 'mode', value: 'b' }, + }, + { + id: 'notModeC', + type: 'short-input', + label: 'Not Mode C', + condition: { field: 'mode', value: 'c', not: true }, + }, + { + id: 'complexCondition', + type: 'short-input', + label: 'Complex', + condition: { field: 'mode', value: 'a', and: { field: 'optionA', value: 'special' } }, + }, + { + id: 'arrayCondition', + type: 'short-input', + label: 'Array Condition', + condition: { field: 'mode', value: ['a', 'b'] }, + }, + ], + inputs: {}, + }, + } - return mockConfigs[type] || null - }, -})) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const createMockGetBlock = (extraConfigs: Record = {}) => { + const configs = { ...mockBlockConfigs, ...extraConfigs } + return (type: string) => configs[type] || null + } -vi.mock('@/tools/utils', () => ({ - getTool: () => null, -})) + const slackWithCanonicalParam = mockBlockConfigs.slack + + return { mockBlockConfigs, createMockGetBlock, slackWithCanonicalParam } +}) +vi.mock('@/blocks', () => ({ + getBlock: createMockGetBlock(), + getAllBlocks: () => Object.values(mockBlockConfigs), +})) +vi.mock('@/tools/utils', () => toolsUtilsMock) vi.mock('@sim/logger', () => loggerMock) describe('Serializer Extended Tests', () => { diff --git a/apps/sim/tools/function/execute.test.ts b/apps/sim/tools/function/execute.test.ts index e4b0d816f4..c5ab2147c3 100644 --- a/apps/sim/tools/function/execute.test.ts +++ b/apps/sim/tools/function/execute.test.ts @@ -6,16 +6,18 @@ * This file contains unit tests for the Function Execute tool, * which runs JavaScript code in a secure sandbox. */ + +import { ToolTester } from '@sim/testing/builders' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/execution/constants' -import { ToolTester } from '@/tools/__test-utils__/test-tools' import { functionExecuteTool } from '@/tools/function/execute' describe('Function Execute Tool', () => { - let tester: ToolTester + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let tester: ToolTester beforeEach(() => { - tester = new ToolTester(functionExecuteTool) + tester = new ToolTester(functionExecuteTool as any) process.env.NEXT_PUBLIC_APP_URL = 'http://localhost:3000' }) @@ -338,7 +340,7 @@ describe('Function Execute Tool', () => { code: '', }) - const body = tester.getRequestBody({ code: '' }) + const body = tester.getRequestBody({ code: '' }) as { code: string } expect(body.code).toBe('') }) @@ -346,7 +348,7 @@ describe('Function Execute Tool', () => { const body = tester.getRequestBody({ code: 'return 42', timeout: 1, // 1ms timeout - }) + }) as { timeout: number } expect(body.timeout).toBe(1) }) diff --git a/apps/sim/tools/http/request.test.ts b/apps/sim/tools/http/request.test.ts index 089c8e1aa5..d338a030cf 100644 --- a/apps/sim/tools/http/request.test.ts +++ b/apps/sim/tools/http/request.test.ts @@ -6,19 +6,21 @@ * This file contains unit tests for the HTTP Request tool, which is used * to make HTTP requests to external APIs and services. */ + +import { ToolTester } from '@sim/testing/builders' +import { mockHttpResponses } from '@sim/testing/factories' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { mockHttpResponses } from '@/tools/__test-utils__/mock-data' -import { ToolTester } from '@/tools/__test-utils__/test-tools' import { requestTool } from '@/tools/http/request' process.env.VITEST = 'true' describe('HTTP Request Tool', () => { - let tester: ToolTester + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let tester: ToolTester beforeEach(() => { - tester = new ToolTester(requestTool) - process.env.NEXT_PUBLIC_APP_URL = 'https://app.simstudio.dev' + tester = new ToolTester(requestTool as any) + process.env.NEXT_PUBLIC_APP_URL = 'https://sim.ai' }) afterEach(() => { @@ -122,7 +124,7 @@ describe('HTTP Request Tool', () => { Object.defineProperty(global, 'window', { value: { location: { - origin: 'https://app.simstudio.dev', + origin: 'https://sim.ai', }, }, writable: true, @@ -136,7 +138,7 @@ describe('HTTP Request Tool', () => { }) const fetchCall = (global.fetch as any).mock.calls[0] - expect(fetchCall[1].headers.Referer).toBe('https://app.simstudio.dev') + expect(fetchCall[1].headers.Referer).toBe('https://sim.ai') global.window = originalWindow }) @@ -195,7 +197,7 @@ describe('HTTP Request Tool', () => { Object.defineProperty(global, 'window', { value: { location: { - origin: 'https://app.simstudio.dev', + origin: 'https://sim.ai', }, }, writable: true, @@ -210,7 +212,7 @@ describe('HTTP Request Tool', () => { const headers = fetchCall[1].headers expect(headers.Host).toBe('api.example.com') - expect(headers.Referer).toBe('https://app.simstudio.dev') + expect(headers.Referer).toBe('https://sim.ai') expect(headers['User-Agent']).toContain('Mozilla') expect(headers.Accept).toBe('*/*') expect(headers['Accept-Encoding']).toContain('gzip') @@ -398,7 +400,7 @@ describe('HTTP Request Tool', () => { Object.defineProperty(global, 'window', { value: { location: { - origin: 'https://app.simstudio.dev', + origin: 'https://sim.ai', }, }, writable: true, @@ -420,7 +422,7 @@ describe('HTTP Request Tool', () => { expect(headers['Sec-Ch-Ua']).toMatch(/Chromium.*Not-A\.Brand/) expect(headers['Sec-Ch-Ua-Mobile']).toBe('?0') expect(headers['Sec-Ch-Ua-Platform']).toBe('"macOS"') - expect(headers.Referer).toBe('https://app.simstudio.dev') + expect(headers.Referer).toBe('https://sim.ai') expect(headers.Host).toBe('api.example.com') global.window = originalWindow @@ -455,7 +457,7 @@ describe('HTTP Request Tool', () => { Object.defineProperty(global, 'window', { value: { location: { - origin: 'https://app.simstudio.dev', + origin: 'https://sim.ai', }, }, writable: true, diff --git a/apps/sim/tools/params.ts b/apps/sim/tools/params.ts index e4a92e67ed..b50329423f 100644 --- a/apps/sim/tools/params.ts +++ b/apps/sim/tools/params.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { extractInputFieldsFromBlocks } from '@/lib/workflows/input-format' import type { ParameterVisibility, ToolConfig } from '@/tools/types' import { getTool } from '@/tools/utils' @@ -502,34 +503,7 @@ async function fetchWorkflowInputFields( } const { data } = await response.json() - if (!data?.state?.blocks) { - return [] - } - - const blocks = data.state.blocks as Record - const triggerEntry = Object.entries(blocks).find( - ([, block]) => - block.type === 'start_trigger' || block.type === 'input_trigger' || block.type === 'starter' - ) - - if (!triggerEntry) { - return [] - } - - const triggerBlock = triggerEntry[1] - const inputFormat = triggerBlock.subBlocks?.inputFormat?.value - - let fields: Array<{ name: string; type: string }> = [] - - if (Array.isArray(inputFormat)) { - fields = inputFormat - .filter((field: any) => field.name && typeof field.name === 'string') - .map((field: any) => ({ - name: field.name, - type: field.type || 'string', - })) - } - + const fields = extractInputFieldsFromBlocks(data?.state?.blocks) workflowInputFieldsCache.set(workflowId, { fields, timestamp: now }) return fields diff --git a/packages/testing/package.json b/packages/testing/package.json index 0ce467cfc0..ca88ddee07 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -25,6 +25,10 @@ "types": "./src/mocks/index.ts", "default": "./src/mocks/index.ts" }, + "./mocks/executor": { + "types": "./src/mocks/executor.mock.ts", + "default": "./src/mocks/executor.mock.ts" + }, "./assertions": { "types": "./src/assertions/index.ts", "default": "./src/assertions/index.ts" diff --git a/packages/testing/src/builders/index.ts b/packages/testing/src/builders/index.ts index d8272bdc94..b15e9a6ffd 100644 --- a/packages/testing/src/builders/index.ts +++ b/packages/testing/src/builders/index.ts @@ -18,4 +18,11 @@ */ export { ExecutionContextBuilder } from './execution.builder' +export { + createErrorFetch, + createToolMockFetch, + type TestToolConfig, + type ToolResponse, + ToolTester, +} from './tool-tester.builder' export { WorkflowBuilder } from './workflow.builder' diff --git a/apps/sim/tools/__test-utils__/test-tools.ts b/packages/testing/src/builders/tool-tester.builder.ts similarity index 64% rename from apps/sim/tools/__test-utils__/test-tools.ts rename to packages/testing/src/builders/tool-tester.builder.ts index f73fd04509..ba165e9683 100644 --- a/apps/sim/tools/__test-utils__/test-tools.ts +++ b/packages/testing/src/builders/tool-tester.builder.ts @@ -1,12 +1,11 @@ /** * Test Tools Utilities * - * This file contains utility functions and classes for testing tools + * Utility functions and classes for testing tools * in a controlled environment without external dependencies. */ -import { createMockFetch as createBaseMockFetch, type MockFetchResponse } from '@sim/testing' import { type Mock, vi } from 'vitest' -import type { ToolConfig, ToolResponse } from '@/tools/types' +import { createMockFetch as createBaseMockFetch, type MockFetchResponse } from '../mocks/fetch.mock' /** * Type that combines Mock with fetch properties including Next.js preconnect. @@ -15,6 +14,30 @@ type MockFetch = Mock & { preconnect: Mock } +/** + * Tool configuration interface (simplified for testing). + * Compatible with actual tool configs from @/tools. + */ +export interface TestToolConfig

{ + id: string + request: { + url: string | ((params: P) => string) + method: string | ((params: P) => string) + headers: (params: P) => Record + body?: (params: P) => unknown + } + transformResponse?: (response: Response, params: P) => Promise +} + +/** + * Tool response interface + */ +export interface ToolResponse { + success: boolean + output: Record + error?: string +} + /** * Create standard mock headers for HTTP testing. */ @@ -26,7 +49,7 @@ const createMockHeaders = (customHeaders: Record = {}) => { 'Accept-Encoding': 'gzip, deflate, br', 'Cache-Control': 'no-cache', Connection: 'keep-alive', - Referer: 'https://www.simstudio.dev', + Referer: 'https://www.sim.ai', 'Sec-Ch-Ua': 'Chromium;v=91, Not-A.Brand;v=99', 'Sec-Ch-Ua-Mobile': '?0', 'Sec-Ch-Ua-Platform': '"macOS"', @@ -38,8 +61,8 @@ const createMockHeaders = (customHeaders: Record = {}) => { * Creates a mock fetch function with Next.js preconnect support. * Wraps the @sim/testing createMockFetch with tool-specific additions. */ -export function createMockFetch( - responseData: any, +export function createToolMockFetch( + responseData: unknown, options: { ok?: boolean; status?: number; headers?: Record } = {} ) { const { ok = true, status = 200, headers = { 'Content-Type': 'application/json' } } = options @@ -53,7 +76,7 @@ export function createMockFetch( } const baseMockFetch = createBaseMockFetch(mockFetchConfig) - ;(baseMockFetch as any).preconnect = vi.fn() + ;(baseMockFetch as MockFetch).preconnect = vi.fn() return baseMockFetch as MockFetch } @@ -63,11 +86,11 @@ export function createMockFetch( */ export function createErrorFetch(errorMessage: string, status = 400) { const error = new Error(errorMessage) - ;(error as any).status = status + ;(error as Error & { status: number }).status = status if (status < 0) { const mockFn = vi.fn().mockRejectedValue(error) - ;(mockFn as any).preconnect = vi.fn() + ;(mockFn as MockFetch).preconnect = vi.fn() return mockFn as MockFetch } @@ -79,7 +102,7 @@ export function createErrorFetch(errorMessage: string, status = 400) { } const baseMockFetch = createBaseMockFetch(mockFetchConfig) - ;(baseMockFetch as any).preconnect = vi.fn() + ;(baseMockFetch as MockFetch).preconnect = vi.fn() return baseMockFetch as MockFetch } @@ -87,14 +110,15 @@ export function createErrorFetch(errorMessage: string, status = 400) { /** * Helper class for testing tools with controllable mock responses */ -export class ToolTester

{ - tool: ToolConfig +export class ToolTester

{ + tool: TestToolConfig private mockFetch: MockFetch private originalFetch: typeof fetch - private mockResponse: any + private mockResponse: unknown private mockResponseOptions: { ok: boolean; status: number; headers: Record } + private error: Error | null = null - constructor(tool: ToolConfig) { + constructor(tool: TestToolConfig) { this.tool = tool this.mockResponse = { success: true, output: {} } this.mockResponseOptions = { @@ -102,7 +126,7 @@ export class ToolTester

{ status: 200, headers: { 'content-type': 'application/json' }, } - this.mockFetch = createMockFetch(this.mockResponse, this.mockResponseOptions) + this.mockFetch = createToolMockFetch(this.mockResponse, this.mockResponseOptions) this.originalFetch = global.fetch } @@ -110,7 +134,7 @@ export class ToolTester

{ * Setup mock responses for this tool */ setup( - response: any, + response: unknown, options: { ok?: boolean; status?: number; headers?: Record } = {} ) { this.mockResponse = response @@ -119,7 +143,7 @@ export class ToolTester

{ status: options.status ?? 200, headers: options.headers ?? { 'content-type': 'application/json' }, } - this.mockFetch = createMockFetch(this.mockResponse, this.mockResponseOptions) + this.mockFetch = createToolMockFetch(this.mockResponse, this.mockResponseOptions) global.fetch = Object.assign(this.mockFetch, { preconnect: vi.fn() }) as typeof fetch return this } @@ -131,15 +155,11 @@ export class ToolTester

{ this.mockFetch = createErrorFetch(errorMessage, status) global.fetch = Object.assign(this.mockFetch, { preconnect: vi.fn() }) as typeof fetch - // Create an error object for direct error handling this.error = new Error(errorMessage) - this.error.message = errorMessage - this.error.status = status + ;(this.error as Error & { status: number }).status = status - // For network errors (negative status), we'll need the error object - // For HTTP errors (positive status), the response will be used if (status > 0) { - this.error.response = { + ;(this.error as Error & { response: unknown }).response = { ok: false, status, statusText: errorMessage, @@ -150,27 +170,27 @@ export class ToolTester

{ return this } - // Store the error for direct error handling - private error: any = null - /** * Execute the tool with provided parameters */ - async execute(params: P, skipProxy = true): Promise { + async execute(params: P, _skipProxy = true): Promise { const url = typeof this.tool.request.url === 'function' ? this.tool.request.url(params) : this.tool.request.url try { - // For HTTP requests, use the method specified in params if available - const method = - this.tool.id === 'http_request' && (params as any)?.method - ? (params as any).method - : this.tool.request.method + let method: string + if (this.tool.id === 'http_request' && (params as Record)?.method) { + method = (params as Record).method as string + } else if (typeof this.tool.request.method === 'function') { + method = this.tool.request.method(params) + } else { + method = this.tool.request.method + } const response = await this.mockFetch(url, { - method: method, + method, headers: this.tool.request.headers(params), body: this.tool.request.body ? (() => { @@ -187,33 +207,31 @@ export class ToolTester

{ }) if (!response.ok) { - // Extract error message directly from response const data = await response.json().catch(() => ({})) + let errorMessage = + (data as Record).error || + (data as Record).message || + response.statusText || + 'Request failed' - // Extract meaningful error message from the response - let errorMessage = data.error || data.message || response.statusText || 'Request failed' - - // Add specific error messages for common status codes if (response.status === 404) { - errorMessage = data.error || data.message || 'Not Found' + errorMessage = + (data as Record).error || + (data as Record).message || + 'Not Found' } else if (response.status === 401) { - errorMessage = data.error || data.message || 'Unauthorized' + errorMessage = + (data as Record).error || + (data as Record).message || + 'Unauthorized' } - return { - success: false, - output: {}, - error: errorMessage, - } + return { success: false, output: {}, error: errorMessage } } - // Continue with successful response handling return await this.handleSuccessfulResponse(response, params) } catch (error) { - // Handle thrown errors (network errors, etc.) const errorToUse = this.error || error - - // Extract error message directly from error object let errorMessage = 'Network error' if (errorToUse instanceof Error) { @@ -221,31 +239,21 @@ export class ToolTester

{ } else if (typeof errorToUse === 'string') { errorMessage = errorToUse } else if (errorToUse && typeof errorToUse === 'object') { - // Try to extract error message from error object structure errorMessage = - errorToUse.error || errorToUse.message || errorToUse.statusText || 'Network error' + (errorToUse as Record).error || + (errorToUse as Record).message || + (errorToUse as Record).statusText || + 'Network error' } - return { - success: false, - output: {}, - error: errorMessage, - } + return { success: false, output: {}, error: errorMessage } } } - /** - * Handle a successful response - */ private async handleSuccessfulResponse(response: Response, params: P): Promise { - // Special case for HTTP request tool in test environment if (this.tool.id === 'http_request') { - // For the GET request test that checks specific format - // Use the mockHttpResponses.simple format directly - if ( - (params as any).url === 'https://api.example.com/data' && - (params as any).method === 'GET' - ) { + const httpParams = params as Record + if (httpParams.url === 'https://api.example.com/data' && httpParams.method === 'GET') { return { success: true, output: { @@ -260,32 +268,20 @@ export class ToolTester

{ if (this.tool.transformResponse) { const result = await this.tool.transformResponse(response, params) - // Ensure we're returning a ToolResponse by checking if it has the required structure if ( typeof result === 'object' && result !== null && 'success' in result && 'output' in result ) { - // If it looks like a ToolResponse, ensure success is set to true and return it - return { - ...result, - success: true, - } as ToolResponse + return { ...(result as ToolResponse), success: true } } - // If it's not a ToolResponse (e.g., it's some other type R), wrap it - return { - success: true, - output: result as any, - } + return { success: true, output: result as Record } } const data = await response.json() - return { - success: true, - output: data, - } + return { success: true, output: data as Record } } /** @@ -306,15 +302,10 @@ export class ToolTester

{ * Get URL that would be used for a request */ getRequestUrl(params: P): string { - // Special case for HTTP request tool tests if (this.tool.id === 'http_request' && params) { - // Cast to any here since this is a special test case for HTTP requests - // which we know will have these properties - const httpParams = params as any - + const httpParams = params as Record let urlStr = httpParams.url as string - // Handle path parameters if (httpParams.pathParams) { const pathParams = httpParams.pathParams as Record Object.entries(pathParams).forEach(([key, value]) => { @@ -324,7 +315,6 @@ export class ToolTester

{ const url = new URL(urlStr) - // Add query parameters if they exist if (httpParams.params) { const queryParams = httpParams.params as Array<{ Key: string; Value: string }> queryParams.forEach((param) => { @@ -335,13 +325,11 @@ export class ToolTester

{ return url.toString() } - // For other tools, use the regular pattern const url = typeof this.tool.request.url === 'function' ? this.tool.request.url(params) : this.tool.request.url - // For testing purposes, return the decoded URL to make tests easier to write return decodeURIComponent(url) } @@ -349,11 +337,9 @@ export class ToolTester

{ * Get headers that would be used for a request */ getRequestHeaders(params: P): Record { - // Special case for HTTP request tool tests with headers parameter if (this.tool.id === 'http_request' && params) { - const httpParams = params as any + const httpParams = params as Record - // For the first test case that expects empty headers if ( httpParams.url === 'https://api.example.com' && httpParams.method === 'GET' && @@ -363,55 +349,54 @@ export class ToolTester

{ return {} } - // For the custom headers test case - need to return exactly this format if ( httpParams.url === 'https://api.example.com' && httpParams.method === 'GET' && httpParams.headers && - httpParams.headers.length === 2 && - httpParams.headers[0]?.Key === 'Authorization' + (httpParams.headers as Array<{ Key: string; Value: string }>).length === 2 && + (httpParams.headers as Array<{ Key: string; Value: string }>)[0]?.Key === 'Authorization' ) { return { - Authorization: httpParams.headers[0].Value, - Accept: httpParams.headers[1].Value, + Authorization: (httpParams.headers as Array<{ Key: string; Value: string }>)[0].Value, + Accept: (httpParams.headers as Array<{ Key: string; Value: string }>)[1].Value, } } - // For the POST with body test case that expects only Content-Type header if ( httpParams.url === 'https://api.example.com' && httpParams.method === 'POST' && httpParams.body && !httpParams.headers ) { - return { - 'Content-Type': 'application/json', - } + return { 'Content-Type': 'application/json' } } - // Create merged headers with custom headers if they exist const customHeaders: Record = {} if (httpParams.headers) { - httpParams.headers.forEach((header: any) => { + ;( + httpParams.headers as Array<{ + Key?: string + Value?: string + cells?: Record + }> + ).forEach((header) => { if (header.Key || header.cells?.Key) { const key = header.Key || header.cells?.Key const value = header.Value || header.cells?.Value - customHeaders[key] = value + if (key && value) customHeaders[key] = value } }) } - // Add host header if missing try { - const hostname = new URL(httpParams.url).host + const hostname = new URL(httpParams.url as string).host if (hostname && !customHeaders.Host && !customHeaders.host) { customHeaders.Host = hostname } - } catch (_e) { - // Invalid URL, will be handled elsewhere + } catch { + // Invalid URL } - // Add content-type if body exists if (httpParams.body && !customHeaders['Content-Type'] && !customHeaders['content-type']) { customHeaders['Content-Type'] = 'application/json' } @@ -419,14 +404,13 @@ export class ToolTester

{ return createMockHeaders(customHeaders) } - // For other tools, use the regular pattern return this.tool.request.headers(params) } /** * Get request body that would be used for a request */ - getRequestBody(params: P): any { + getRequestBody(params: P): unknown { return this.tool.request.body ? this.tool.request.body(params) : undefined } } diff --git a/packages/testing/src/factories/index.ts b/packages/testing/src/factories/index.ts index 2fafe98625..586f7fea59 100644 --- a/packages/testing/src/factories/index.ts +++ b/packages/testing/src/factories/index.ts @@ -118,6 +118,19 @@ export { type SerializedConnection, type SerializedWorkflow, } from './serialized-block.factory' +// Tool mock responses +export { + mockDriveResponses, + mockGitHubResponses, + mockGmailResponses, + mockHttpResponses, + mockPineconeResponses, + mockSerperResponses, + mockSheetsResponses, + mockSlackResponses, + mockSupabaseResponses, + mockTavilyResponses, +} from './tool-responses.factory' // Undo/redo operation factories export { type BaseOperation, @@ -149,10 +162,19 @@ export { type WorkspaceFactoryOptions, } from './user.factory' export { + createAgentWithToolsWorkflowState, createBranchingWorkflow, + createComplexWorkflowState, + createConditionalWorkflowState, + createInvalidSerializedWorkflow, + createInvalidWorkflowState, createLinearWorkflow, createLoopWorkflow, + createLoopWorkflowState, + createMinimalWorkflowState, + createMissingMetadataWorkflow, createParallelWorkflow, createWorkflowState, type WorkflowFactoryOptions, + type WorkflowStateFixture, } from './workflow.factory' diff --git a/apps/sim/tools/__test-utils__/mock-data.ts b/packages/testing/src/factories/tool-responses.factory.ts similarity index 89% rename from apps/sim/tools/__test-utils__/mock-data.ts rename to packages/testing/src/factories/tool-responses.factory.ts index b879e334f5..7440c9c6d6 100644 --- a/apps/sim/tools/__test-utils__/mock-data.ts +++ b/packages/testing/src/factories/tool-responses.factory.ts @@ -27,9 +27,10 @@ export const mockHttpResponses = { }, } -// Gmail Mock Data +/** + * Gmail Mock Data + */ export const mockGmailResponses = { - // List messages response messageList: { messages: [ { id: 'msg1', threadId: 'thread1' }, @@ -38,14 +39,10 @@ export const mockGmailResponses = { ], nextPageToken: 'token123', }, - - // Empty list response emptyList: { messages: [], resultSizeEstimate: 0, }, - - // Single message response singleMessage: { id: 'msg1', threadId: 'thread1', @@ -79,9 +76,10 @@ export const mockGmailResponses = { }, } -// Google Drive Mock Data +/** + * Google Drive Mock Data + */ export const mockDriveResponses = { - // List files response fileList: { files: [ { id: 'file1', name: 'Document1.docx', mimeType: 'application/vnd.google-apps.document' }, @@ -98,13 +96,9 @@ export const mockDriveResponses = { ], nextPageToken: 'drive-page-token', }, - - // Empty file list emptyFileList: { files: [], }, - - // Single file metadata fileMetadata: { id: 'file1', name: 'Document1.docx', @@ -117,9 +111,10 @@ export const mockDriveResponses = { }, } -// Google Sheets Mock Data +/** + * Google Sheets Mock Data + */ export const mockSheetsResponses = { - // Read range response rangeData: { range: 'Sheet1!A1:D5', majorDimension: 'ROWS', @@ -131,15 +126,11 @@ export const mockSheetsResponses = { ['Row4Col1', 'Row4Col2', 'Row4Col3', 'Row4Col4'], ], }, - - // Empty range emptyRange: { range: 'Sheet1!A1:D5', majorDimension: 'ROWS', values: [], }, - - // Update range response updateResponse: { spreadsheetId: 'spreadsheet123', updatedRange: 'Sheet1!A1:D5', @@ -149,17 +140,16 @@ export const mockSheetsResponses = { }, } -// Pinecone Mock Data +/** + * Pinecone Mock Data + */ export const mockPineconeResponses = { - // Vector embedding embedding: { embedding: Array(1536) .fill(0) .map(() => Math.random() * 2 - 1), metadata: { text: 'Sample text for embedding', id: 'embed-123' }, }, - - // Search results searchResults: { matches: [ { id: 'doc1', score: 0.92, metadata: { text: 'Matching text 1' } }, @@ -167,16 +157,15 @@ export const mockPineconeResponses = { { id: 'doc3', score: 0.78, metadata: { text: 'Matching text 3' } }, ], }, - - // Upsert response upsertResponse: { statusText: 'Created', }, } -// GitHub Mock Data +/** + * GitHub Mock Data + */ export const mockGitHubResponses = { - // Repository info repoInfo: { id: 12345, name: 'test-repo', @@ -200,8 +189,6 @@ export const mockGitHubResponses = { stargazers_count: 15, language: 'TypeScript', }, - - // PR creation response prResponse: { id: 12345, number: 42, @@ -209,24 +196,18 @@ export const mockGitHubResponses = { body: 'Test PR description', html_url: 'https://github.com/user/test-repo/pull/42', state: 'open', - user: { - login: 'user', - id: 54321, - }, + user: { login: 'user', id: 54321 }, created_at: '2025-03-15T10:00:00Z', updated_at: '2025-03-15T10:05:00Z', }, } -// Serper Search Mock Data +/** + * Serper Search Mock Data + */ export const mockSerperResponses = { - // Search results searchResults: { - searchParameters: { - q: 'test query', - gl: 'us', - hl: 'en', - }, + searchParameters: { q: 'test query', gl: 'us', hl: 'en' }, organic: [ { title: 'Test Result 1', @@ -255,9 +236,10 @@ export const mockSerperResponses = { }, } -// Slack Mock Data +/** + * Slack Mock Data + */ export const mockSlackResponses = { - // Message post response messageResponse: { ok: true, channel: 'C1234567890', @@ -269,17 +251,16 @@ export const mockSlackResponses = { team: 'T1234567890', }, }, - - // Error response errorResponse: { ok: false, error: 'channel_not_found', }, } -// Tavily Mock Data +/** + * Tavily Mock Data + */ export const mockTavilyResponses = { - // Search results searchResults: { results: [ { @@ -306,9 +287,10 @@ export const mockTavilyResponses = { }, } -// Supabase Mock Data +/** + * Supabase Mock Data + */ export const mockSupabaseResponses = { - // Query response queryResponse: { data: [ { id: 1, name: 'Item 1', description: 'Description 1' }, @@ -317,20 +299,14 @@ export const mockSupabaseResponses = { ], error: null, }, - - // Insert response insertResponse: { data: [{ id: 4, name: 'Item 4', description: 'Description 4' }], error: null, }, - - // Update response updateResponse: { data: [{ id: 1, name: 'Updated Item 1', description: 'Updated Description 1' }], error: null, }, - - // Error response errorResponse: { data: null, error: { diff --git a/packages/testing/src/factories/workflow.factory.ts b/packages/testing/src/factories/workflow.factory.ts index 1132763536..c140249a91 100644 --- a/packages/testing/src/factories/workflow.factory.ts +++ b/packages/testing/src/factories/workflow.factory.ts @@ -209,3 +209,420 @@ export function createParallelWorkflow(count = 2): any { return createWorkflowState({ blocks, edges, parallels }) } + +/** + * Detailed workflow state fixture interface for serializer tests. + */ +export interface WorkflowStateFixture { + blocks: Record + edges: any[] + loops: Record +} + +/** + * Creates a minimal workflow with a starter and one agent block. + * Includes full subBlocks structure for serializer testing. + */ +export function createMinimalWorkflowState(): WorkflowStateFixture { + const blocks: Record = { + starter: { + id: 'starter', + type: 'starter', + name: 'Starter Block', + position: { x: 0, y: 0 }, + subBlocks: { + description: { id: 'description', type: 'long-input', value: 'This is the starter block' }, + }, + outputs: {}, + enabled: true, + }, + agent1: { + id: 'agent1', + type: 'agent', + name: 'Agent Block', + position: { x: 300, y: 0 }, + subBlocks: { + provider: { id: 'provider', type: 'dropdown', value: 'anthropic' }, + model: { id: 'model', type: 'dropdown', value: 'claude-3-7-sonnet-20250219' }, + prompt: { id: 'prompt', type: 'long-input', value: 'Hello, world!' }, + tools: { id: 'tools', type: 'tool-input', value: '[]' }, + system: { id: 'system', type: 'long-input', value: 'You are a helpful assistant.' }, + responseFormat: { id: 'responseFormat', type: 'code', value: null }, + }, + outputs: {}, + enabled: true, + }, + } + + return { + blocks, + edges: [{ id: 'edge1', source: 'starter', target: 'agent1' }], + loops: {}, + } +} + +/** + * Creates a workflow with condition blocks and branching paths. + */ +export function createConditionalWorkflowState(): WorkflowStateFixture { + const blocks: Record = { + starter: { + id: 'starter', + type: 'starter', + name: 'Starter Block', + position: { x: 0, y: 0 }, + subBlocks: { + description: { id: 'description', type: 'long-input', value: 'This is the starter block' }, + }, + outputs: {}, + enabled: true, + }, + condition1: { + id: 'condition1', + type: 'condition', + name: 'Condition Block', + position: { x: 300, y: 0 }, + subBlocks: { + condition: { id: 'condition', type: 'long-input', value: 'input.value > 10' }, + }, + outputs: {}, + enabled: true, + }, + agent1: { + id: 'agent1', + type: 'agent', + name: 'True Path Agent', + position: { x: 600, y: -100 }, + subBlocks: { + provider: { id: 'provider', type: 'dropdown', value: 'anthropic' }, + model: { id: 'model', type: 'dropdown', value: 'claude-3-7-sonnet-20250219' }, + prompt: { id: 'prompt', type: 'long-input', value: 'Value is greater than 10' }, + tools: { id: 'tools', type: 'tool-input', value: '[]' }, + system: { id: 'system', type: 'long-input', value: 'You are a helpful assistant.' }, + responseFormat: { id: 'responseFormat', type: 'code', value: null }, + }, + outputs: {}, + enabled: true, + }, + agent2: { + id: 'agent2', + type: 'agent', + name: 'False Path Agent', + position: { x: 600, y: 100 }, + subBlocks: { + provider: { id: 'provider', type: 'dropdown', value: 'anthropic' }, + model: { id: 'model', type: 'dropdown', value: 'claude-3-7-sonnet-20250219' }, + prompt: { id: 'prompt', type: 'long-input', value: 'Value is less than or equal to 10' }, + tools: { id: 'tools', type: 'tool-input', value: '[]' }, + system: { id: 'system', type: 'long-input', value: 'You are a helpful assistant.' }, + responseFormat: { id: 'responseFormat', type: 'code', value: null }, + }, + outputs: {}, + enabled: true, + }, + } + + return { + blocks, + edges: [ + { id: 'edge1', source: 'starter', target: 'condition1' }, + { id: 'edge2', source: 'condition1', target: 'agent1', sourceHandle: 'condition-true' }, + { id: 'edge3', source: 'condition1', target: 'agent2', sourceHandle: 'condition-false' }, + ], + loops: {}, + } +} + +/** + * Creates a workflow with a loop structure. + */ +export function createLoopWorkflowState(): WorkflowStateFixture { + const blocks: Record = { + starter: { + id: 'starter', + type: 'starter', + name: 'Starter Block', + position: { x: 0, y: 0 }, + subBlocks: { + description: { id: 'description', type: 'long-input', value: 'This is the starter block' }, + }, + outputs: {}, + enabled: true, + }, + function1: { + id: 'function1', + type: 'function', + name: 'Function Block', + position: { x: 300, y: 0 }, + subBlocks: { + code: { + id: 'code', + type: 'code', + value: 'let counter = input.counter || 0;\ncounter++;\nreturn { counter };', + }, + language: { id: 'language', type: 'dropdown', value: 'javascript' }, + }, + outputs: {}, + enabled: true, + }, + condition1: { + id: 'condition1', + type: 'condition', + name: 'Loop Condition', + position: { x: 600, y: 0 }, + subBlocks: { + condition: { id: 'condition', type: 'long-input', value: 'input.counter < 5' }, + }, + outputs: {}, + enabled: true, + }, + agent1: { + id: 'agent1', + type: 'agent', + name: 'Loop Complete Agent', + position: { x: 900, y: 100 }, + subBlocks: { + provider: { id: 'provider', type: 'dropdown', value: 'anthropic' }, + model: { id: 'model', type: 'dropdown', value: 'claude-3-7-sonnet-20250219' }, + prompt: { + id: 'prompt', + type: 'long-input', + value: 'Loop completed after {{input.counter}} iterations', + }, + tools: { id: 'tools', type: 'tool-input', value: '[]' }, + system: { id: 'system', type: 'long-input', value: 'You are a helpful assistant.' }, + responseFormat: { id: 'responseFormat', type: 'code', value: null }, + }, + outputs: {}, + enabled: true, + }, + } + + return { + blocks, + edges: [ + { id: 'edge1', source: 'starter', target: 'function1' }, + { id: 'edge2', source: 'function1', target: 'condition1' }, + { id: 'edge3', source: 'condition1', target: 'function1', sourceHandle: 'condition-true' }, + { id: 'edge4', source: 'condition1', target: 'agent1', sourceHandle: 'condition-false' }, + ], + loops: { + loop1: { id: 'loop1', nodes: ['function1', 'condition1'], iterations: 10, loopType: 'for' }, + }, + } +} + +/** + * Creates a complex workflow with multiple block types (API, function, agent). + */ +export function createComplexWorkflowState(): WorkflowStateFixture { + const blocks: Record = { + starter: { + id: 'starter', + type: 'starter', + name: 'Starter Block', + position: { x: 0, y: 0 }, + subBlocks: { + description: { id: 'description', type: 'long-input', value: 'This is the starter block' }, + }, + outputs: {}, + enabled: true, + }, + api1: { + id: 'api1', + type: 'api', + name: 'API Request', + position: { x: 300, y: 0 }, + subBlocks: { + url: { id: 'url', type: 'short-input', value: 'https://api.example.com/data' }, + method: { id: 'method', type: 'dropdown', value: 'GET' }, + headers: { + id: 'headers', + type: 'table', + value: [ + ['Content-Type', 'application/json'], + ['Authorization', 'Bearer {{API_KEY}}'], + ], + }, + body: { id: 'body', type: 'long-input', value: '' }, + }, + outputs: {}, + enabled: true, + }, + function1: { + id: 'function1', + type: 'function', + name: 'Process Data', + position: { x: 600, y: 0 }, + subBlocks: { + code: { + id: 'code', + type: 'code', + value: 'const data = input.data;\nreturn { processed: data.map(item => item.name) };', + }, + language: { id: 'language', type: 'dropdown', value: 'javascript' }, + }, + outputs: {}, + enabled: true, + }, + agent1: { + id: 'agent1', + type: 'agent', + name: 'Summarize Data', + position: { x: 900, y: 0 }, + subBlocks: { + provider: { id: 'provider', type: 'dropdown', value: 'openai' }, + model: { id: 'model', type: 'dropdown', value: 'gpt-4o' }, + prompt: { + id: 'prompt', + type: 'long-input', + value: 'Summarize the following data:\n\n{{input.processed}}', + }, + tools: { + id: 'tools', + type: 'tool-input', + value: + '[{"type":"function","name":"calculator","description":"Perform calculations","parameters":{"type":"object","properties":{"expression":{"type":"string","description":"Math expression to evaluate"}},"required":["expression"]}}]', + }, + system: { id: 'system', type: 'long-input', value: 'You are a data analyst assistant.' }, + responseFormat: { + id: 'responseFormat', + type: 'code', + value: + '{"type":"object","properties":{"summary":{"type":"string"},"keyPoints":{"type":"array","items":{"type":"string"}},"sentiment":{"type":"string","enum":["positive","neutral","negative"]}},"required":["summary","keyPoints","sentiment"]}', + }, + }, + outputs: {}, + enabled: true, + }, + } + + return { + blocks, + edges: [ + { id: 'edge1', source: 'starter', target: 'api1' }, + { id: 'edge2', source: 'api1', target: 'function1' }, + { id: 'edge3', source: 'function1', target: 'agent1' }, + ], + loops: {}, + } +} + +/** + * Creates a workflow with an agent that has custom tools. + */ +export function createAgentWithToolsWorkflowState(): WorkflowStateFixture { + const blocks: Record = { + starter: { + id: 'starter', + type: 'starter', + name: 'Starter Block', + position: { x: 0, y: 0 }, + subBlocks: { + description: { id: 'description', type: 'long-input', value: 'This is the starter block' }, + }, + outputs: {}, + enabled: true, + }, + agent1: { + id: 'agent1', + type: 'agent', + name: 'Custom Tools Agent', + position: { x: 300, y: 0 }, + subBlocks: { + provider: { id: 'provider', type: 'dropdown', value: 'openai' }, + model: { id: 'model', type: 'dropdown', value: 'gpt-4o' }, + prompt: { + id: 'prompt', + type: 'long-input', + value: 'Use the tools to help answer: {{input.question}}', + }, + tools: { + id: 'tools', + type: 'tool-input', + value: + '[{"type":"custom-tool","name":"weather","description":"Get current weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}},{"type":"function","name":"calculator","description":"Calculate expression","parameters":{"type":"object","properties":{"expression":{"type":"string"}},"required":["expression"]}}]', + }, + system: { + id: 'system', + type: 'long-input', + value: 'You are a helpful assistant with access to tools.', + }, + responseFormat: { id: 'responseFormat', type: 'code', value: null }, + }, + outputs: {}, + enabled: true, + }, + } + + return { + blocks, + edges: [{ id: 'edge1', source: 'starter', target: 'agent1' }], + loops: {}, + } +} + +/** + * Creates a workflow state with an invalid block type for error testing. + */ +export function createInvalidWorkflowState(): WorkflowStateFixture { + const { blocks, edges, loops } = createMinimalWorkflowState() + + blocks.invalid = { + id: 'invalid', + type: 'invalid-type', + name: 'Invalid Block', + position: { x: 600, y: 0 }, + subBlocks: {}, + outputs: {}, + enabled: true, + } + + edges.push({ id: 'edge-invalid', source: 'agent1', target: 'invalid' }) + + return { blocks, edges, loops } +} + +/** + * Creates a serialized workflow with invalid metadata for error testing. + */ +export function createInvalidSerializedWorkflow() { + return { + version: '1.0', + blocks: [ + { + id: 'invalid', + position: { x: 0, y: 0 }, + config: { tool: 'invalid', params: {} }, + inputs: {}, + outputs: {}, + metadata: { id: 'non-existent-type' }, + enabled: true, + }, + ], + connections: [], + loops: {}, + } +} + +/** + * Creates a serialized workflow with missing metadata for error testing. + */ +export function createMissingMetadataWorkflow() { + return { + version: '1.0', + blocks: [ + { + id: 'invalid', + position: { x: 0, y: 0 }, + config: { tool: 'invalid', params: {} }, + inputs: {}, + outputs: {}, + metadata: undefined, + enabled: true, + }, + ], + connections: [], + loops: {}, + } +} diff --git a/packages/testing/src/index.ts b/packages/testing/src/index.ts index 816fd496c6..8eab14fb87 100644 --- a/packages/testing/src/index.ts +++ b/packages/testing/src/index.ts @@ -48,17 +48,30 @@ export { createEnvMock, createMockDb, createMockFetch, + createMockFormDataRequest, createMockGetEnv, createMockLogger, + createMockRequest, createMockResponse, createMockSocket, createMockStorage, databaseMock, defaultMockEnv, + defaultMockUser, drizzleOrmMock, envMock, loggerMock, + type MockAuthResult, type MockFetchResponse, + type MockUser, + mockAuth, + mockCommonSchemas, + mockConsoleLogger, + mockCryptoUuid, + mockDrizzleOrm, + mockKnowledgeSchemas, + mockUuid, + setupCommonApiMocks, setupGlobalFetchMock, setupGlobalStorageMocks, } from './mocks' diff --git a/packages/testing/src/mocks/api.mock.ts b/packages/testing/src/mocks/api.mock.ts new file mode 100644 index 0000000000..2a4acd9261 --- /dev/null +++ b/packages/testing/src/mocks/api.mock.ts @@ -0,0 +1,198 @@ +/** + * Mock utilities for API testing + */ +import { vi } from 'vitest' +import { createMockLogger } from './logger.mock' + +/** + * Mock drizzle-orm operators for database query testing. + * Provides mock implementations of common drizzle-orm operators. + * + * @example + * ```ts + * mockDrizzleOrm() + * // Now eq, and, or, etc. from drizzle-orm are mocked + * ``` + */ +export function mockDrizzleOrm() { + vi.doMock('drizzle-orm', () => ({ + and: vi.fn((...conditions) => ({ conditions, type: 'and' })), + eq: vi.fn((field, value) => ({ field, value, type: 'eq' })), + or: vi.fn((...conditions) => ({ type: 'or', conditions })), + gte: vi.fn((field, value) => ({ type: 'gte', field, value })), + lte: vi.fn((field, value) => ({ type: 'lte', field, value })), + gt: vi.fn((field, value) => ({ type: 'gt', field, value })), + lt: vi.fn((field, value) => ({ type: 'lt', field, value })), + ne: vi.fn((field, value) => ({ type: 'ne', field, value })), + asc: vi.fn((field) => ({ field, type: 'asc' })), + desc: vi.fn((field) => ({ field, type: 'desc' })), + isNull: vi.fn((field) => ({ field, type: 'isNull' })), + isNotNull: vi.fn((field) => ({ field, type: 'isNotNull' })), + inArray: vi.fn((field, values) => ({ field, values, type: 'inArray' })), + notInArray: vi.fn((field, values) => ({ field, values, type: 'notInArray' })), + like: vi.fn((field, value) => ({ field, value, type: 'like' })), + ilike: vi.fn((field, value) => ({ field, value, type: 'ilike' })), + count: vi.fn((field) => ({ field, type: 'count' })), + sum: vi.fn((field) => ({ field, type: 'sum' })), + avg: vi.fn((field) => ({ field, type: 'avg' })), + min: vi.fn((field) => ({ field, type: 'min' })), + max: vi.fn((field) => ({ field, type: 'max' })), + sql: vi.fn((strings, ...values) => ({ + type: 'sql', + sql: strings, + values, + })), + })) +} + +/** + * Mock common database schema patterns. + * Provides mock schema objects for common tables. + * + * @example + * ```ts + * mockCommonSchemas() + * // Now @sim/db/schema exports are mocked + * ``` + */ +export function mockCommonSchemas() { + vi.doMock('@sim/db/schema', () => ({ + workflowFolder: { + id: 'id', + userId: 'userId', + parentId: 'parentId', + updatedAt: 'updatedAt', + workspaceId: 'workspaceId', + sortOrder: 'sortOrder', + createdAt: 'createdAt', + }, + workflow: { + id: 'id', + folderId: 'folderId', + userId: 'userId', + updatedAt: 'updatedAt', + }, + account: { + userId: 'userId', + providerId: 'providerId', + }, + user: { + email: 'email', + id: 'id', + }, + })) +} + +/** + * Mock console logger using the shared mock logger. + * Ensures tests can assert on logger calls. + * + * @example + * ```ts + * mockConsoleLogger() + * // Now @sim/logger.createLogger returns a mock logger + * ``` + */ +export function mockConsoleLogger() { + const mockLogger = createMockLogger() + vi.doMock('@sim/logger', () => ({ + createLogger: vi.fn().mockReturnValue(mockLogger), + })) + return mockLogger +} + +/** + * Setup common API test mocks (schemas, drizzle ORM). + * Does NOT set up logger mocks - call mockConsoleLogger() separately if needed. + * + * @example + * ```ts + * setupCommonApiMocks() + * const mockLogger = mockConsoleLogger() // Call separately to get logger instance + * ``` + */ +export function setupCommonApiMocks() { + mockCommonSchemas() + mockDrizzleOrm() +} + +/** + * Mock knowledge-related database schemas. + * Provides mock schema objects for knowledge base tables. + * + * @example + * ```ts + * mockKnowledgeSchemas() + * // Now @sim/db/schema exports knowledge base tables + * ``` + */ +export function mockKnowledgeSchemas() { + vi.doMock('@sim/db/schema', () => ({ + knowledgeBase: { + id: 'kb_id', + userId: 'user_id', + name: 'kb_name', + description: 'description', + tokenCount: 'token_count', + embeddingModel: 'embedding_model', + embeddingDimension: 'embedding_dimension', + chunkingConfig: 'chunking_config', + workspaceId: 'workspace_id', + createdAt: 'created_at', + updatedAt: 'updated_at', + deletedAt: 'deleted_at', + }, + document: { + id: 'doc_id', + knowledgeBaseId: 'kb_id', + filename: 'filename', + fileUrl: 'file_url', + fileSize: 'file_size', + mimeType: 'mime_type', + chunkCount: 'chunk_count', + tokenCount: 'token_count', + characterCount: 'character_count', + processingStatus: 'processing_status', + processingStartedAt: 'processing_started_at', + processingCompletedAt: 'processing_completed_at', + processingError: 'processing_error', + enabled: 'enabled', + tag1: 'tag1', + tag2: 'tag2', + tag3: 'tag3', + tag4: 'tag4', + tag5: 'tag5', + tag6: 'tag6', + tag7: 'tag7', + uploadedAt: 'uploaded_at', + deletedAt: 'deleted_at', + }, + embedding: { + id: 'embedding_id', + documentId: 'doc_id', + knowledgeBaseId: 'kb_id', + chunkIndex: 'chunk_index', + content: 'content', + embedding: 'embedding', + tokenCount: 'token_count', + characterCount: 'character_count', + tag1: 'tag1', + tag2: 'tag2', + tag3: 'tag3', + tag4: 'tag4', + tag5: 'tag5', + tag6: 'tag6', + tag7: 'tag7', + createdAt: 'created_at', + }, + permissions: { + id: 'permission_id', + userId: 'user_id', + entityType: 'entity_type', + entityId: 'entity_id', + permissionType: 'permission_type', + createdAt: 'created_at', + updatedAt: 'updated_at', + }, + })) +} diff --git a/packages/testing/src/mocks/auth.mock.ts b/packages/testing/src/mocks/auth.mock.ts new file mode 100644 index 0000000000..209d93b34d --- /dev/null +++ b/packages/testing/src/mocks/auth.mock.ts @@ -0,0 +1,74 @@ +/** + * Mock authentication utilities for API testing + */ +import { vi } from 'vitest' + +/** + * Mock user interface for authentication testing + */ +export interface MockUser { + id: string + email: string + name?: string +} + +/** + * Result object returned by mockAuth with helper methods + */ +export interface MockAuthResult { + /** The mock getSession function */ + mockGetSession: ReturnType + /** Set authenticated state with optional custom user */ + setAuthenticated: (user?: MockUser) => void + /** Set unauthenticated state (session returns null) */ + setUnauthenticated: () => void + /** Alias for setAuthenticated */ + mockAuthenticatedUser: (user?: MockUser) => void + /** Alias for setUnauthenticated */ + mockUnauthenticated: () => void +} + +/** + * Default mock user for testing + */ +export const defaultMockUser: MockUser = { + id: 'user-123', + email: 'test@example.com', +} + +/** + * Mock authentication for API tests. + * Uses vi.doMock to mock the auth module's getSession function. + * + * @param user - Optional user object to use for authenticated requests + * @returns Object with authentication helper functions + * + * @example + * ```ts + * const auth = mockAuth() + * auth.setAuthenticated() // User is now authenticated + * auth.setUnauthenticated() // User is now unauthenticated + * + * // With custom user + * auth.setAuthenticated({ id: 'custom-id', email: 'custom@test.com' }) + * ``` + */ +export function mockAuth(user: MockUser = defaultMockUser): MockAuthResult { + const mockGetSession = vi.fn() + + vi.doMock('@/lib/auth', () => ({ + getSession: mockGetSession, + })) + + const setAuthenticated = (customUser?: MockUser) => + mockGetSession.mockResolvedValue({ user: customUser || user }) + const setUnauthenticated = () => mockGetSession.mockResolvedValue(null) + + return { + mockGetSession, + mockAuthenticatedUser: setAuthenticated, + mockUnauthenticated: setUnauthenticated, + setAuthenticated, + setUnauthenticated, + } +} diff --git a/packages/testing/src/mocks/blocks.mock.ts b/packages/testing/src/mocks/blocks.mock.ts new file mode 100644 index 0000000000..411dd29414 --- /dev/null +++ b/packages/testing/src/mocks/blocks.mock.ts @@ -0,0 +1,300 @@ +/** + * Mock block configurations for serializer and related tests. + * + * @example + * ```ts + * import { blocksMock, mockBlockConfigs } from '@sim/testing/mocks' + * + * vi.mock('@/blocks', () => blocksMock) + * + * // Or use individual configs + * const starterConfig = mockBlockConfigs.starter + * ``` + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/** + * Mock block configurations that mirror the real block registry. + * Used for testing serialization, deserialization, and validation. + */ +export const mockBlockConfigs: Record = { + starter: { + name: 'Starter', + description: 'Start of the workflow', + category: 'flow', + bgColor: '#4CAF50', + tools: { + access: ['starter'], + config: { tool: () => 'starter' }, + }, + subBlocks: [ + { id: 'description', type: 'long-input', label: 'Description' }, + { id: 'inputFormat', type: 'table', label: 'Input Format' }, + ], + inputs: {}, + }, + agent: { + name: 'Agent', + description: 'AI Agent', + category: 'ai', + bgColor: '#2196F3', + tools: { + access: ['anthropic_chat', 'openai_chat', 'google_chat'], + config: { + tool: (params: Record) => { + const model = params.model || 'gpt-4o' + if (model.includes('claude')) return 'anthropic' + if (model.includes('gpt') || model.includes('o1')) return 'openai' + if (model.includes('gemini')) return 'google' + return 'openai' + }, + }, + }, + subBlocks: [ + { id: 'provider', type: 'dropdown', label: 'Provider' }, + { id: 'model', type: 'dropdown', label: 'Model' }, + { id: 'prompt', type: 'long-input', label: 'Prompt' }, + { id: 'system', type: 'long-input', label: 'System Message' }, + { id: 'tools', type: 'tool-input', label: 'Tools' }, + { id: 'responseFormat', type: 'code', label: 'Response Format' }, + { id: 'messages', type: 'messages-input', label: 'Messages' }, + ], + inputs: { + input: { type: 'string' }, + tools: { type: 'array' }, + }, + }, + function: { + name: 'Function', + description: 'Execute custom code', + category: 'code', + bgColor: '#9C27B0', + tools: { + access: ['function'], + config: { tool: () => 'function' }, + }, + subBlocks: [ + { id: 'code', type: 'code', label: 'Code' }, + { id: 'language', type: 'dropdown', label: 'Language' }, + ], + inputs: { input: { type: 'any' } }, + }, + condition: { + name: 'Condition', + description: 'Branch based on condition', + category: 'flow', + bgColor: '#FF9800', + tools: { + access: ['condition'], + config: { tool: () => 'condition' }, + }, + subBlocks: [{ id: 'condition', type: 'long-input', label: 'Condition' }], + inputs: { input: { type: 'any' } }, + }, + api: { + name: 'API', + description: 'Make API request', + category: 'data', + bgColor: '#E91E63', + tools: { + access: ['api'], + config: { tool: () => 'api' }, + }, + subBlocks: [ + { id: 'url', type: 'short-input', label: 'URL' }, + { id: 'method', type: 'dropdown', label: 'Method' }, + { id: 'headers', type: 'table', label: 'Headers' }, + { id: 'body', type: 'long-input', label: 'Body' }, + ], + inputs: {}, + }, + webhook: { + name: 'Webhook', + description: 'Webhook trigger', + category: 'triggers', + bgColor: '#4CAF50', + tools: { + access: ['webhook'], + config: { tool: () => 'webhook' }, + }, + subBlocks: [{ id: 'path', type: 'short-input', label: 'Path' }], + inputs: {}, + }, + jina: { + name: 'Jina', + description: 'Convert website content into text', + category: 'tools', + bgColor: '#333333', + tools: { + access: ['jina_read_url'], + config: { tool: () => 'jina_read_url' }, + }, + subBlocks: [ + { id: 'url', type: 'short-input', title: 'URL', required: true }, + { id: 'apiKey', type: 'short-input', title: 'API Key', required: true }, + ], + inputs: { + url: { type: 'string' }, + apiKey: { type: 'string' }, + }, + }, + reddit: { + name: 'Reddit', + description: 'Access Reddit data and content', + category: 'tools', + bgColor: '#FF5700', + tools: { + access: ['reddit_get_posts', 'reddit_get_comments'], + config: { tool: () => 'reddit_get_posts' }, + }, + subBlocks: [ + { id: 'operation', type: 'dropdown', title: 'Operation', required: true }, + { id: 'credential', type: 'oauth-input', title: 'Reddit Account', required: true }, + { id: 'subreddit', type: 'short-input', title: 'Subreddit', required: true }, + ], + inputs: { + operation: { type: 'string' }, + credential: { type: 'string' }, + subreddit: { type: 'string' }, + }, + }, + slack: { + name: 'Slack', + description: 'Send messages to Slack', + category: 'tools', + bgColor: '#611f69', + tools: { + access: ['slack_send_message'], + config: { tool: () => 'slack_send_message' }, + }, + subBlocks: [ + { id: 'channel', type: 'dropdown', title: 'Channel', mode: 'basic' }, + { id: 'manualChannel', type: 'short-input', title: 'Channel ID', mode: 'advanced' }, + { id: 'text', type: 'long-input', title: 'Message' }, + { id: 'username', type: 'short-input', title: 'Username', mode: 'both' }, + ], + inputs: { + channel: { type: 'string' }, + manualChannel: { type: 'string' }, + text: { type: 'string' }, + username: { type: 'string' }, + }, + }, + agentWithMemories: { + name: 'Agent with Memories', + description: 'AI Agent with memory support', + category: 'ai', + bgColor: '#2196F3', + tools: { + access: ['anthropic_chat'], + config: { tool: () => 'anthropic_chat' }, + }, + subBlocks: [ + { id: 'systemPrompt', type: 'long-input', title: 'System Prompt' }, + { id: 'userPrompt', type: 'long-input', title: 'User Prompt' }, + { id: 'memories', type: 'short-input', title: 'Memories', mode: 'advanced' }, + { id: 'model', type: 'dropdown', title: 'Model' }, + ], + inputs: { + systemPrompt: { type: 'string' }, + userPrompt: { type: 'string' }, + memories: { type: 'array' }, + model: { type: 'string' }, + }, + }, + conditional_block: { + name: 'Conditional Block', + description: 'Block with conditional fields', + category: 'tools', + bgColor: '#FF5700', + tools: { + access: ['conditional_tool'], + config: { tool: () => 'conditional_tool' }, + }, + subBlocks: [ + { id: 'mode', type: 'dropdown', label: 'Mode' }, + { + id: 'optionA', + type: 'short-input', + label: 'Option A', + condition: { field: 'mode', value: 'a' }, + }, + { + id: 'optionB', + type: 'short-input', + label: 'Option B', + condition: { field: 'mode', value: 'b' }, + }, + { + id: 'notModeC', + type: 'short-input', + label: 'Not Mode C', + condition: { field: 'mode', value: 'c', not: true }, + }, + { + id: 'complexCondition', + type: 'short-input', + label: 'Complex', + condition: { field: 'mode', value: 'a', and: { field: 'optionA', value: 'special' } }, + }, + { + id: 'arrayCondition', + type: 'short-input', + label: 'Array Condition', + condition: { field: 'mode', value: ['a', 'b'] }, + }, + ], + inputs: {}, + }, +} + +/** + * Creates a getBlock function that returns mock block configs. + * Can be extended with additional block types. + */ +export function createMockGetBlock(extraConfigs: Record = {}) { + const configs = { ...mockBlockConfigs, ...extraConfigs } + return (type: string) => configs[type] || null +} + +/** + * Mock tool configurations for validation tests. + */ +export const mockToolConfigs: Record = { + jina_read_url: { + params: { + url: { visibility: 'user-or-llm', required: true }, + apiKey: { visibility: 'user-only', required: true }, + }, + }, + reddit_get_posts: { + params: { + subreddit: { visibility: 'user-or-llm', required: true }, + credential: { visibility: 'user-only', required: true }, + }, + }, +} + +/** + * Creates a getTool function that returns mock tool configs. + */ +export function createMockGetTool(extraConfigs: Record = {}) { + const configs = { ...mockToolConfigs, ...extraConfigs } + return (toolId: string) => configs[toolId] || null +} + +/** + * Pre-configured blocks mock for use with vi.mock('@/blocks', () => blocksMock). + */ +export const blocksMock = { + getBlock: createMockGetBlock(), + getAllBlocks: () => Object.values(mockBlockConfigs), +} + +/** + * Pre-configured tools/utils mock for use with vi.mock('@/tools/utils', () => toolsUtilsMock). + */ +export const toolsUtilsMock = { + getTool: createMockGetTool(), +} diff --git a/apps/sim/executor/__test-utils__/mock-dependencies.ts b/packages/testing/src/mocks/executor.mock.ts similarity index 63% rename from apps/sim/executor/__test-utils__/mock-dependencies.ts rename to packages/testing/src/mocks/executor.mock.ts index 999e65a6d6..8256a9e97c 100644 --- a/apps/sim/executor/__test-utils__/mock-dependencies.ts +++ b/packages/testing/src/mocks/executor.mock.ts @@ -1,8 +1,24 @@ -import { loggerMock, setupGlobalFetchMock } from '@sim/testing' +/** + * Mock utilities for executor handler testing. + * Sets up common mocks needed for testing executor block handlers. + * + * This module is designed to be imported for side effects - the vi.mock calls + * are executed at the top level and hoisted by vitest. + * + * @example + * ```ts + * // Import at the very top of your test file for side effects + * import '@sim/testing/mocks/executor.mock' + * + * // Then your other imports + * import { describe, it, expect } from 'vitest' + * ``` + */ import { vi } from 'vitest' +import { setupGlobalFetchMock } from './fetch.mock' +import { loggerMock } from './logger.mock' -// Mock common dependencies used across executor handler tests - +// Logger vi.mock('@sim/logger', () => loggerMock) // Blocks @@ -14,7 +30,7 @@ vi.mock('@/blocks/index', () => ({ vi.mock('@/tools/utils', () => ({ getTool: vi.fn(), getToolAsync: vi.fn(), - validateToolRequest: vi.fn(), // Keep for backward compatibility + validateToolRequest: vi.fn(), formatRequestParams: vi.fn(), transformTable: vi.fn(), createParamSchema: vi.fn(), @@ -32,21 +48,20 @@ vi.mock('@/lib/core/config/api-keys', () => ({ getRotatingApiKey: vi.fn(), })) -// Tools +// Tools module vi.mock('@/tools') // Providers vi.mock('@/providers', () => ({ executeProviderRequest: vi.fn(), })) + vi.mock('@/providers/utils', async (importOriginal) => { const actual = await importOriginal() return { - // @ts-ignore - ...actual, + ...(actual as object), getProviderFromModel: vi.fn(), transformBlockTool: vi.fn(), - // Ensure getBaseModelProviders returns an object getBaseModelProviders: vi.fn(() => ({})), } }) @@ -57,10 +72,10 @@ vi.mock('@/executor/resolver', () => ({ InputResolver: vi.fn(), })) -// Specific block utilities (like router prompt generator) +// Specific block utilities vi.mock('@/blocks/blocks/router') -// Mock blocks - needed by agent handler for transformBlockTool +// Mock blocks module vi.mock('@/blocks') // Mock fetch for server requests diff --git a/packages/testing/src/mocks/index.ts b/packages/testing/src/mocks/index.ts index 52eec208c9..91013cae01 100644 --- a/packages/testing/src/mocks/index.ts +++ b/packages/testing/src/mocks/index.ts @@ -16,6 +16,30 @@ * ``` */ +// API mocks +export { + mockCommonSchemas, + mockConsoleLogger, + mockDrizzleOrm, + mockKnowledgeSchemas, + setupCommonApiMocks, +} from './api.mock' +// Auth mocks +export { + defaultMockUser, + type MockAuthResult, + type MockUser, + mockAuth, +} from './auth.mock' +// Blocks mocks +export { + blocksMock, + createMockGetBlock, + createMockGetTool, + mockBlockConfigs, + mockToolConfigs, + toolsUtilsMock, +} from './blocks.mock' // Database mocks export { createMockDb, @@ -26,6 +50,7 @@ export { } from './database.mock' // Env mocks export { createEnvMock, createMockGetEnv, defaultMockEnv, envMock } from './env.mock' +// Executor mocks - use side-effect import: import '@sim/testing/mocks/executor' // Fetch mocks export { createMockFetch, @@ -38,6 +63,8 @@ export { } from './fetch.mock' // Logger mocks export { clearLoggerMocks, createMockLogger, getLoggerCalls, loggerMock } from './logger.mock' +// Request mocks +export { createMockFormDataRequest, createMockRequest } from './request.mock' // Socket mocks export { createMockSocket, @@ -47,3 +74,5 @@ export { } from './socket.mock' // Storage mocks export { clearStorageMocks, createMockStorage, setupGlobalStorageMocks } from './storage.mock' +// UUID mocks +export { mockCryptoUuid, mockUuid } from './uuid.mock' diff --git a/packages/testing/src/mocks/request.mock.ts b/packages/testing/src/mocks/request.mock.ts new file mode 100644 index 0000000000..2e9a86fd6f --- /dev/null +++ b/packages/testing/src/mocks/request.mock.ts @@ -0,0 +1,59 @@ +/** + * Mock request utilities for API testing + */ + +/** + * Creates a mock NextRequest for API route testing. + * This is a general-purpose utility for testing Next.js API routes. + * + * @param method - HTTP method (GET, POST, PUT, DELETE, etc.) + * @param body - Optional request body (will be JSON stringified) + * @param headers - Optional headers to include + * @param url - Optional custom URL (defaults to http://localhost:3000/api/test) + * @returns NextRequest instance + * + * @example + * ```ts + * const req = createMockRequest('POST', { name: 'test' }) + * const response = await POST(req) + * ``` + */ +export function createMockRequest( + method = 'GET', + body?: unknown, + headers: Record = {}, + url = 'http://localhost:3000/api/test' +): Request { + const init: RequestInit = { + method, + headers: new Headers({ + 'Content-Type': 'application/json', + ...headers, + }), + } + + if (body !== undefined) { + init.body = JSON.stringify(body) + } + + return new Request(new URL(url), init) +} + +/** + * Creates a mock NextRequest with form data for file upload testing. + * + * @param formData - FormData instance + * @param method - HTTP method (defaults to POST) + * @param url - Optional custom URL + * @returns Request instance + */ +export function createMockFormDataRequest( + formData: FormData, + method = 'POST', + url = 'http://localhost:3000/api/test' +): Request { + return new Request(new URL(url), { + method, + body: formData, + }) +} diff --git a/packages/testing/src/mocks/uuid.mock.ts b/packages/testing/src/mocks/uuid.mock.ts new file mode 100644 index 0000000000..f278e76b64 --- /dev/null +++ b/packages/testing/src/mocks/uuid.mock.ts @@ -0,0 +1,40 @@ +/** + * Mock UUID utilities for testing + */ +import { vi } from 'vitest' + +/** + * Mock UUID v4 generation for consistent test results. + * Uses vi.doMock to mock the uuid module. + * + * @param mockValue - The UUID value to return (defaults to 'test-uuid') + * + * @example + * ```ts + * mockUuid('my-test-uuid') + * // Now uuid.v4() will return 'my-test-uuid' + * ``` + */ +export function mockUuid(mockValue = 'test-uuid') { + vi.doMock('uuid', () => ({ + v4: vi.fn().mockReturnValue(mockValue), + })) +} + +/** + * Mock crypto.randomUUID for tests. + * Uses vi.stubGlobal to replace the global crypto object. + * + * @param mockValue - The UUID value to return (defaults to 'mock-uuid-1234-5678') + * + * @example + * ```ts + * mockCryptoUuid('custom-uuid') + * // Now crypto.randomUUID() will return 'custom-uuid' + * ``` + */ +export function mockCryptoUuid(mockValue = 'mock-uuid-1234-5678') { + vi.stubGlobal('crypto', { + randomUUID: vi.fn().mockReturnValue(mockValue), + }) +} From 6f469a7f37c17e98e1f38cc2695b38382348a55e Mon Sep 17 00:00:00 2001 From: Waleed Date: Thu, 15 Jan 2026 15:09:00 -0800 Subject: [PATCH 06/15] improvement(permissions): added ability to auto-add new org members to existing permission group, disallow disabling of start block (#2836) * improvement(permissions): added ability to auto-add new org members to existing permission group, disallow disabling of start block * ran migrations * add deploy modal tabs config to perm groups * fix ordering of access control listings * prep staging merge * regen migrations --------- Co-authored-by: Vikhyath Mondreti --- .../[id]/invitations/[invitationId]/route.ts | 44 + .../app/api/permission-groups/[id]/route.ts | 24 + apps/sim/app/api/permission-groups/route.ts | 25 +- .../components/deploy-modal/deploy-modal.tsx | 44 +- .../access-control/access-control.tsx | 124 +- apps/sim/hooks/queries/permission-groups.ts | 3 + apps/sim/hooks/use-permission-config.ts | 8 +- apps/sim/lib/permission-groups/types.ts | 16 + packages/db/migrations/0143_puzzling_xorn.sql | 2 + .../db/migrations/meta/0143_snapshot.json | 10297 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 4 + 12 files changed, 10563 insertions(+), 35 deletions(-) create mode 100644 packages/db/migrations/0143_puzzling_xorn.sql create mode 100644 packages/db/migrations/meta/0143_snapshot.json diff --git a/apps/sim/app/api/organizations/[id]/invitations/[invitationId]/route.ts b/apps/sim/app/api/organizations/[id]/invitations/[invitationId]/route.ts index 143a924cc3..0c98a52bf8 100644 --- a/apps/sim/app/api/organizations/[id]/invitations/[invitationId]/route.ts +++ b/apps/sim/app/api/organizations/[id]/invitations/[invitationId]/route.ts @@ -4,6 +4,8 @@ import { invitation, member, organization, + permissionGroup, + permissionGroupMember, permissions, subscription as subscriptionTable, user, @@ -17,6 +19,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { getEmailSubject, renderInvitationEmail } from '@/components/emails' import { getSession } from '@/lib/auth' +import { hasAccessControlAccess } from '@/lib/billing' import { requireStripeClient } from '@/lib/billing/stripe-client' import { getBaseUrl } from '@/lib/core/utils/urls' import { sendEmail } from '@/lib/messaging/email/mailer' @@ -382,6 +385,47 @@ export async function PUT( // Don't fail the whole invitation acceptance due to this } + // Auto-assign to permission group if one has autoAddNewMembers enabled + try { + const hasAccessControl = await hasAccessControlAccess(session.user.id) + if (hasAccessControl) { + const [autoAddGroup] = await tx + .select({ id: permissionGroup.id, name: permissionGroup.name }) + .from(permissionGroup) + .where( + and( + eq(permissionGroup.organizationId, organizationId), + eq(permissionGroup.autoAddNewMembers, true) + ) + ) + .limit(1) + + if (autoAddGroup) { + await tx.insert(permissionGroupMember).values({ + id: randomUUID(), + permissionGroupId: autoAddGroup.id, + userId: session.user.id, + assignedBy: null, + assignedAt: new Date(), + }) + + logger.info('Auto-assigned new member to permission group', { + userId: session.user.id, + organizationId, + permissionGroupId: autoAddGroup.id, + permissionGroupName: autoAddGroup.name, + }) + } + } + } catch (error) { + logger.error('Failed to auto-assign user to permission group', { + userId: session.user.id, + organizationId, + error, + }) + // Don't fail the whole invitation acceptance due to this + } + const linkedWorkspaceInvitations = await tx .select() .from(workspaceInvitation) diff --git a/apps/sim/app/api/permission-groups/[id]/route.ts b/apps/sim/app/api/permission-groups/[id]/route.ts index 5e1486ff26..977cb1bbfe 100644 --- a/apps/sim/app/api/permission-groups/[id]/route.ts +++ b/apps/sim/app/api/permission-groups/[id]/route.ts @@ -25,12 +25,19 @@ const configSchema = z.object({ disableMcpTools: z.boolean().optional(), disableCustomTools: z.boolean().optional(), hideTemplates: z.boolean().optional(), + disableInvitations: z.boolean().optional(), + hideDeployApi: z.boolean().optional(), + hideDeployMcp: z.boolean().optional(), + hideDeployA2a: z.boolean().optional(), + hideDeployChatbot: z.boolean().optional(), + hideDeployTemplate: z.boolean().optional(), }) const updateSchema = z.object({ name: z.string().trim().min(1).max(100).optional(), description: z.string().max(500).nullable().optional(), config: configSchema.optional(), + autoAddNewMembers: z.boolean().optional(), }) async function getPermissionGroupWithAccess(groupId: string, userId: string) { @@ -44,6 +51,7 @@ async function getPermissionGroupWithAccess(groupId: string, userId: string) { createdBy: permissionGroup.createdBy, createdAt: permissionGroup.createdAt, updatedAt: permissionGroup.updatedAt, + autoAddNewMembers: permissionGroup.autoAddNewMembers, }) .from(permissionGroup) .where(eq(permissionGroup.id, groupId)) @@ -140,11 +148,27 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: ? { ...currentConfig, ...updates.config } : currentConfig + // If setting autoAddNewMembers to true, unset it on other groups in the org first + if (updates.autoAddNewMembers === true) { + await db + .update(permissionGroup) + .set({ autoAddNewMembers: false, updatedAt: new Date() }) + .where( + and( + eq(permissionGroup.organizationId, result.group.organizationId), + eq(permissionGroup.autoAddNewMembers, true) + ) + ) + } + await db .update(permissionGroup) .set({ ...(updates.name !== undefined && { name: updates.name }), ...(updates.description !== undefined && { description: updates.description }), + ...(updates.autoAddNewMembers !== undefined && { + autoAddNewMembers: updates.autoAddNewMembers, + }), config: newConfig, updatedAt: new Date(), }) diff --git a/apps/sim/app/api/permission-groups/route.ts b/apps/sim/app/api/permission-groups/route.ts index a3c3a7512b..a72726c5a9 100644 --- a/apps/sim/app/api/permission-groups/route.ts +++ b/apps/sim/app/api/permission-groups/route.ts @@ -26,6 +26,12 @@ const configSchema = z.object({ disableMcpTools: z.boolean().optional(), disableCustomTools: z.boolean().optional(), hideTemplates: z.boolean().optional(), + disableInvitations: z.boolean().optional(), + hideDeployApi: z.boolean().optional(), + hideDeployMcp: z.boolean().optional(), + hideDeployA2a: z.boolean().optional(), + hideDeployChatbot: z.boolean().optional(), + hideDeployTemplate: z.boolean().optional(), }) const createSchema = z.object({ @@ -33,6 +39,7 @@ const createSchema = z.object({ name: z.string().trim().min(1).max(100), description: z.string().max(500).optional(), config: configSchema.optional(), + autoAddNewMembers: z.boolean().optional(), }) export async function GET(req: Request) { @@ -68,6 +75,7 @@ export async function GET(req: Request) { createdBy: permissionGroup.createdBy, createdAt: permissionGroup.createdAt, updatedAt: permissionGroup.updatedAt, + autoAddNewMembers: permissionGroup.autoAddNewMembers, creatorName: user.name, creatorEmail: user.email, }) @@ -111,7 +119,8 @@ export async function POST(req: Request) { } const body = await req.json() - const { organizationId, name, description, config } = createSchema.parse(body) + const { organizationId, name, description, config, autoAddNewMembers } = + createSchema.parse(body) const membership = await db .select({ id: member.id, role: member.role }) @@ -154,6 +163,19 @@ export async function POST(req: Request) { ...config, } + // If autoAddNewMembers is true, unset it on any existing groups first + if (autoAddNewMembers) { + await db + .update(permissionGroup) + .set({ autoAddNewMembers: false, updatedAt: new Date() }) + .where( + and( + eq(permissionGroup.organizationId, organizationId), + eq(permissionGroup.autoAddNewMembers, true) + ) + ) + } + const now = new Date() const newGroup = { id: crypto.randomUUID(), @@ -164,6 +186,7 @@ export async function POST(req: Request) { createdBy: session.user.id, createdAt: now, updatedAt: now, + autoAddNewMembers: autoAddNewMembers || false, } await db.insert(permissionGroup).values(newGroup) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx index a1b28e59ca..4e7f20b9c7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx @@ -23,6 +23,7 @@ import { CreateApiKeyModal } from '@/app/workspace/[workspaceId]/w/components/si import { startsWithUuid } from '@/executor/constants' import { useApiKeys } from '@/hooks/queries/api-keys' import { useWorkspaceSettings } from '@/hooks/queries/workspace' +import { usePermissionConfig } from '@/hooks/use-permission-config' import { useSettingsModalStore } from '@/stores/modals/settings/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' @@ -113,16 +114,12 @@ export function DeployModal({ const [existingChat, setExistingChat] = useState(null) const [isLoadingChat, setIsLoadingChat] = useState(false) - const [formSubmitting, setFormSubmitting] = useState(false) - const [formExists, setFormExists] = useState(false) - const [isFormValid, setIsFormValid] = useState(false) - const [chatSuccess, setChatSuccess] = useState(false) - const [formSuccess, setFormSuccess] = useState(false) const [isCreateKeyModalOpen, setIsCreateKeyModalOpen] = useState(false) const userPermissions = useUserPermissionsContext() const canManageWorkspaceKeys = userPermissions.canAdmin + const { config: permissionConfig } = usePermissionConfig() const { data: apiKeysData, isLoading: isLoadingKeys } = useApiKeys(workflowWorkspaceId || '') const { data: workspaceSettingsData, isLoading: isLoadingSettings } = useWorkspaceSettings( workflowWorkspaceId || '' @@ -518,12 +515,6 @@ export function DeployModal({ setTimeout(() => setChatSuccess(false), 2000) } - const handleFormDeployed = async () => { - await handlePostDeploymentUpdate() - setFormSuccess(true) - setTimeout(() => setFormSuccess(false), 2000) - } - const handlePostDeploymentUpdate = async () => { if (!workflowId) return @@ -632,17 +623,6 @@ export function DeployModal({ deleteTrigger?.click() }, []) - const handleFormFormSubmit = useCallback(() => { - const form = document.getElementById('form-deploy-form') as HTMLFormElement - form?.requestSubmit() - }, []) - - const handleFormDelete = useCallback(() => { - const form = document.getElementById('form-deploy-form') - const deleteTrigger = form?.querySelector('[data-delete-trigger]') as HTMLButtonElement - deleteTrigger?.click() - }, []) - return ( <> @@ -656,12 +636,22 @@ export function DeployModal({ > General - API - MCP - A2A - Chat + {!permissionConfig.hideDeployApi && ( + API + )} + {!permissionConfig.hideDeployMcp && ( + MCP + )} + {!permissionConfig.hideDeployA2a && ( + A2A + )} + {!permissionConfig.hideDeployChatbot && ( + Chat + )} {/* Form */} - Template + {!permissionConfig.hideDeployTemplate && ( + Template + )} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx index fc7bf1dcda..49be2f7160 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx @@ -20,6 +20,7 @@ import { ModalTabsContent, ModalTabsList, ModalTabsTrigger, + Switch, } from '@/components/emcn' import { Input as BaseInput, Skeleton } from '@/components/ui' import { useSession } from '@/lib/auth/auth-client' @@ -268,6 +269,7 @@ export function AccessControl() { const [viewingGroup, setViewingGroup] = useState(null) const [newGroupName, setNewGroupName] = useState('') const [newGroupDescription, setNewGroupDescription] = useState('') + const [newGroupAutoAdd, setNewGroupAutoAdd] = useState(false) const [createError, setCreateError] = useState(null) const [deletingGroup, setDeletingGroup] = useState<{ id: string; name: string } | null>(null) const [deletingGroupIds, setDeletingGroupIds] = useState>(new Set()) @@ -324,6 +326,36 @@ export function AccessControl() { category: 'Settings Tabs', configKey: 'hideFilesTab' as const, }, + { + id: 'hide-deploy-api', + label: 'API', + category: 'Deploy Tabs', + configKey: 'hideDeployApi' as const, + }, + { + id: 'hide-deploy-mcp', + label: 'MCP', + category: 'Deploy Tabs', + configKey: 'hideDeployMcp' as const, + }, + { + id: 'hide-deploy-a2a', + label: 'A2A', + category: 'Deploy Tabs', + configKey: 'hideDeployA2a' as const, + }, + { + id: 'hide-deploy-chatbot', + label: 'Chat', + category: 'Deploy Tabs', + configKey: 'hideDeployChatbot' as const, + }, + { + id: 'hide-deploy-template', + label: 'Template', + category: 'Deploy Tabs', + configKey: 'hideDeployTemplate' as const, + }, { id: 'disable-mcp', label: 'MCP Tools', @@ -417,14 +449,16 @@ export function AccessControl() { if (!newGroupName.trim() || !activeOrganization?.id) return setCreateError(null) try { - const result = await createPermissionGroup.mutateAsync({ + await createPermissionGroup.mutateAsync({ organizationId: activeOrganization.id, name: newGroupName.trim(), description: newGroupDescription.trim() || undefined, + autoAddNewMembers: newGroupAutoAdd, }) setShowCreateModal(false) setNewGroupName('') setNewGroupDescription('') + setNewGroupAutoAdd(false) } catch (error) { logger.error('Failed to create permission group', error) if (error instanceof Error) { @@ -433,12 +467,19 @@ export function AccessControl() { setCreateError('Failed to create permission group') } } - }, [newGroupName, newGroupDescription, activeOrganization?.id, createPermissionGroup]) + }, [ + newGroupName, + newGroupDescription, + newGroupAutoAdd, + activeOrganization?.id, + createPermissionGroup, + ]) const handleCloseCreateModal = useCallback(() => { setShowCreateModal(false) setNewGroupName('') setNewGroupDescription('') + setNewGroupAutoAdd(false) setCreateError(null) }, []) @@ -533,6 +574,23 @@ export function AccessControl() { } }, [viewingGroup, selectedMemberIds, bulkAddMembers]) + const handleToggleAutoAdd = useCallback( + async (enabled: boolean) => { + if (!viewingGroup || !activeOrganization?.id) return + try { + await updatePermissionGroup.mutateAsync({ + id: viewingGroup.id, + organizationId: activeOrganization.id, + autoAddNewMembers: enabled, + }) + setViewingGroup((prev) => (prev ? { ...prev, autoAddNewMembers: enabled } : null)) + } catch (error) { + logger.error('Failed to toggle auto-add', error) + } + }, + [viewingGroup, activeOrganization?.id, updatePermissionGroup] + ) + const toggleIntegration = useCallback( (blockType: string) => { if (!editingConfig) return @@ -630,6 +688,22 @@ export function AccessControl() { )}

+
+
+ + Auto-add new members + + + Automatically add new organization members to this group + +
+ handleToggleAutoAdd(checked)} + disabled={updatePermissionGroup.isPending} + /> +
+
@@ -814,7 +888,13 @@ export function AccessControl() { editingConfig?.allowedIntegrations === null || editingConfig?.allowedIntegrations?.length === allBlocks.length setEditingConfig((prev) => - prev ? { ...prev, allowedIntegrations: allAllowed ? [] : null } : prev + prev + ? { + ...prev, + // When deselecting all, keep start_trigger allowed (it should never be disabled) + allowedIntegrations: allAllowed ? ['start_trigger'] : null, + } + : prev ) }} > @@ -876,7 +956,12 @@ export function AccessControl() { !editingConfig?.disableMcpTools && !editingConfig?.disableCustomTools && !editingConfig?.hideTraceSpans && - !editingConfig?.disableInvitations + !editingConfig?.disableInvitations && + !editingConfig?.hideDeployApi && + !editingConfig?.hideDeployMcp && + !editingConfig?.hideDeployA2a && + !editingConfig?.hideDeployChatbot && + !editingConfig?.hideDeployTemplate setEditingConfig((prev) => prev ? { @@ -891,6 +976,11 @@ export function AccessControl() { disableCustomTools: allVisible, hideTraceSpans: allVisible, disableInvitations: allVisible, + hideDeployApi: allVisible, + hideDeployMcp: allVisible, + hideDeployA2a: allVisible, + hideDeployChatbot: allVisible, + hideDeployTemplate: allVisible, } : prev ) @@ -905,7 +995,12 @@ export function AccessControl() { !editingConfig?.disableMcpTools && !editingConfig?.disableCustomTools && !editingConfig?.hideTraceSpans && - !editingConfig?.disableInvitations + !editingConfig?.disableInvitations && + !editingConfig?.hideDeployApi && + !editingConfig?.hideDeployMcp && + !editingConfig?.hideDeployA2a && + !editingConfig?.hideDeployChatbot && + !editingConfig?.hideDeployTemplate ? 'Deselect All' : 'Select All'} @@ -1058,7 +1153,14 @@ export function AccessControl() { {filteredGroups.map((group) => (
- {group.name} +
+ {group.name} + {group.autoAddNewMembers && ( + + Auto-enrolls + + )} +
{group.memberCount} member{group.memberCount !== 1 ? 's' : ''} @@ -1106,6 +1208,16 @@ export function AccessControl() { placeholder='e.g., Limited access for marketing users' />
+
+ setNewGroupAutoAdd(checked === true)} + /> + +
{createError &&

{createError}

}
diff --git a/apps/sim/hooks/queries/permission-groups.ts b/apps/sim/hooks/queries/permission-groups.ts index de62725579..6832d51880 100644 --- a/apps/sim/hooks/queries/permission-groups.ts +++ b/apps/sim/hooks/queries/permission-groups.ts @@ -13,6 +13,7 @@ export interface PermissionGroup { creatorName: string | null creatorEmail: string | null memberCount: number + autoAddNewMembers: boolean } export interface PermissionGroupMember { @@ -111,6 +112,7 @@ export interface CreatePermissionGroupData { name: string description?: string config?: Partial + autoAddNewMembers?: boolean } export function useCreatePermissionGroup() { @@ -143,6 +145,7 @@ export interface UpdatePermissionGroupData { name?: string description?: string | null config?: Partial + autoAddNewMembers?: boolean } export function useUpdatePermissionGroup() { diff --git a/apps/sim/hooks/use-permission-config.ts b/apps/sim/hooks/use-permission-config.ts index 55e14c4b9f..947ff78d87 100644 --- a/apps/sim/hooks/use-permission-config.ts +++ b/apps/sim/hooks/use-permission-config.ts @@ -35,6 +35,8 @@ export function usePermissionConfig(): PermissionConfigResult { const isBlockAllowed = useMemo(() => { return (blockType: string) => { + // start_trigger should always be allowed (it should never be disabled) + if (blockType === 'start_trigger') return true if (config.allowedIntegrations === null) return true return config.allowedIntegrations.includes(blockType) } @@ -50,7 +52,11 @@ export function usePermissionConfig(): PermissionConfigResult { const filterBlocks = useMemo(() => { return (blocks: T[]): T[] => { if (config.allowedIntegrations === null) return blocks - return blocks.filter((block) => config.allowedIntegrations!.includes(block.type)) + // start_trigger should always be included (it should never be disabled) + return blocks.filter( + (block) => + block.type === 'start_trigger' || config.allowedIntegrations!.includes(block.type) + ) } }, [config.allowedIntegrations]) diff --git a/apps/sim/lib/permission-groups/types.ts b/apps/sim/lib/permission-groups/types.ts index 3c82dcc457..48afbc55e5 100644 --- a/apps/sim/lib/permission-groups/types.ts +++ b/apps/sim/lib/permission-groups/types.ts @@ -12,6 +12,12 @@ export interface PermissionGroupConfig { disableCustomTools: boolean hideTemplates: boolean disableInvitations: boolean + // Deploy Modal Tabs + hideDeployApi: boolean + hideDeployMcp: boolean + hideDeployA2a: boolean + hideDeployChatbot: boolean + hideDeployTemplate: boolean } export const DEFAULT_PERMISSION_GROUP_CONFIG: PermissionGroupConfig = { @@ -27,6 +33,11 @@ export const DEFAULT_PERMISSION_GROUP_CONFIG: PermissionGroupConfig = { disableCustomTools: false, hideTemplates: false, disableInvitations: false, + hideDeployApi: false, + hideDeployMcp: false, + hideDeployA2a: false, + hideDeployChatbot: false, + hideDeployTemplate: false, } export function parsePermissionGroupConfig(config: unknown): PermissionGroupConfig { @@ -50,5 +61,10 @@ export function parsePermissionGroupConfig(config: unknown): PermissionGroupConf disableCustomTools: typeof c.disableCustomTools === 'boolean' ? c.disableCustomTools : false, hideTemplates: typeof c.hideTemplates === 'boolean' ? c.hideTemplates : false, disableInvitations: typeof c.disableInvitations === 'boolean' ? c.disableInvitations : false, + hideDeployApi: typeof c.hideDeployApi === 'boolean' ? c.hideDeployApi : false, + hideDeployMcp: typeof c.hideDeployMcp === 'boolean' ? c.hideDeployMcp : false, + hideDeployA2a: typeof c.hideDeployA2a === 'boolean' ? c.hideDeployA2a : false, + hideDeployChatbot: typeof c.hideDeployChatbot === 'boolean' ? c.hideDeployChatbot : false, + hideDeployTemplate: typeof c.hideDeployTemplate === 'boolean' ? c.hideDeployTemplate : false, } } diff --git a/packages/db/migrations/0143_puzzling_xorn.sql b/packages/db/migrations/0143_puzzling_xorn.sql new file mode 100644 index 0000000000..e477b6ccef --- /dev/null +++ b/packages/db/migrations/0143_puzzling_xorn.sql @@ -0,0 +1,2 @@ +ALTER TABLE "permission_group" ADD COLUMN "auto_add_new_members" boolean DEFAULT false NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX "permission_group_org_auto_add_unique" ON "permission_group" USING btree ("organization_id") WHERE auto_add_new_members = true; \ No newline at end of file diff --git a/packages/db/migrations/meta/0143_snapshot.json b/packages/db/migrations/meta/0143_snapshot.json new file mode 100644 index 0000000000..79893d73bd --- /dev/null +++ b/packages/db/migrations/meta/0143_snapshot.json @@ -0,0 +1,10297 @@ +{ + "id": "59d40ba3-ceec-4040-adfa-f1900e3a3d24", + "prevId": "b0ccee95-4d64-421e-8631-65984c6b80c7", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.a2a_agent": { + "name": "a2a_agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "authentication": { + "name": "authentication", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "signatures": { + "name": "signatures", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_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": { + "a2a_agent_workspace_id_idx": { + "name": "a2a_agent_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workflow_id_idx": { + "name": "a2a_agent_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_created_by_idx": { + "name": "a2a_agent_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_workflow_unique": { + "name": "a2a_agent_workspace_workflow_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_agent_workspace_id_workspace_id_fk": { + "name": "a2a_agent_workspace_id_workspace_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_workflow_id_workflow_id_fk": { + "name": "a2a_agent_workflow_id_workflow_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_created_by_user_id_fk": { + "name": "a2a_agent_created_by_user_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_push_notification_config": { + "name": "a2a_push_notification_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_schemes": { + "name": "auth_schemes", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "auth_credentials": { + "name": "auth_credentials", + "type": "text", + "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": { + "a2a_push_notification_config_task_id_idx": { + "name": "a2a_push_notification_config_task_id_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_push_notification_config_task_unique": { + "name": "a2a_push_notification_config_task_unique", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_push_notification_config_task_id_a2a_task_id_fk": { + "name": "a2a_push_notification_config_task_id_a2a_task_id_fk", + "tableFrom": "a2a_push_notification_config", + "tableTo": "a2a_task", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_task": { + "name": "a2a_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "a2a_task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'submitted'" + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "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()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "a2a_task_agent_id_idx": { + "name": "a2a_task_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_session_id_idx": { + "name": "a2a_task_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_status_idx": { + "name": "a2a_task_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_execution_id_idx": { + "name": "a2a_task_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_created_at_idx": { + "name": "a2a_task_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_task_agent_id_a2a_agent_id_fk": { + "name": "a2a_task_agent_id_a2a_agent_id_fk", + "tableFrom": "a2a_task", + "tableTo": "a2a_agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "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": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_user_provider_unique": { + "name": "account_user_provider_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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 + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "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": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "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 + }, + "identifier": { + "name": "identifier", + "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": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "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.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "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": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "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": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set": { + "name": "credential_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "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": { + "credential_set_organization_id_idx": { + "name": "credential_set_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_created_by_idx": { + "name": "credential_set_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_org_name_unique": { + "name": "credential_set_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_provider_id_idx": { + "name": "credential_set_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_organization_id_organization_id_fk": { + "name": "credential_set_organization_id_organization_id_fk", + "tableFrom": "credential_set", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_created_by_user_id_fk": { + "name": "credential_set_created_by_user_id_fk", + "tableFrom": "credential_set", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_invitation": { + "name": "credential_set_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "accepted_by_user_id": { + "name": "accepted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_invitation_set_id_idx": { + "name": "credential_set_invitation_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_token_idx": { + "name": "credential_set_invitation_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_status_idx": { + "name": "credential_set_invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_expires_at_idx": { + "name": "credential_set_invitation_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_invitation_credential_set_id_credential_set_id_fk": { + "name": "credential_set_invitation_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_invited_by_user_id_fk": { + "name": "credential_set_invitation_invited_by_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_accepted_by_user_id_user_id_fk": { + "name": "credential_set_invitation_accepted_by_user_id_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_set_invitation_token_unique": { + "name": "credential_set_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_member": { + "name": "credential_set_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "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": { + "credential_set_member_set_id_idx": { + "name": "credential_set_member_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_user_id_idx": { + "name": "credential_set_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_unique": { + "name": "credential_set_member_unique", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_status_idx": { + "name": "credential_set_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_member_credential_set_id_credential_set_id_fk": { + "name": "credential_set_member_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_user_id_user_id_fk": { + "name": "credential_set_member_user_id_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_invited_by_user_id_fk": { + "name": "credential_set_member_invited_by_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "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 + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "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": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "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": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "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": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_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 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "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 + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "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_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": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "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 + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_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_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "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.form": { + "name": "form", + "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 + }, + "identifier": { + "name": "identifier", + "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": "'[]'" + }, + "show_branding": { + "name": "show_branding", + "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": { + "form_identifier_idx": { + "name": "form_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_workflow_id_idx": { + "name": "form_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_user_id_idx": { + "name": "form_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_workflow_id_workflow_id_fk": { + "name": "form_workflow_id_workflow_id_fk", + "tableFrom": "form", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "form_user_id_user_id_fk": { + "name": "form_user_id_user_id_fk", + "tableFrom": "form", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_namespace_unique": { + "name": "idempotency_key_namespace_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "namespace", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idempotency_key_namespace_idx": { + "name": "idempotency_key_namespace_idx", + "columns": [ + { + "expression": "namespace", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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\": 1, \"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": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "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_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "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": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_idx": { + "name": "mcp_servers_workspace_deleted_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "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": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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 + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "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_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_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 + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "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": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "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": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_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": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "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()" + }, + "auto_add_new_members": { + "name": "auto_add_new_members", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "permission_group_organization_id_idx": { + "name": "permission_group_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_org_name_unique": { + "name": "permission_group_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_org_auto_add_unique": { + "name": "permission_group_org_auto_add_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "auto_add_new_members = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_user_id_unique": { + "name": "permission_group_member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "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.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_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": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": "'dark'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "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.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.template_creators": { + "name": "template_creators", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "reference_type": { + "name": "reference_type", + "type": "template_creator_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile_image_url": { + "name": "profile_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "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": { + "template_creators_reference_idx": { + "name": "template_creators_reference_idx", + "columns": [ + { + "expression": "reference_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_reference_id_idx": { + "name": "template_creators_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_created_by_idx": { + "name": "template_creators_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_creators_created_by_user_id_fk": { + "name": "template_creators_created_by_user_id_fk", + "tableFrom": "template_creators", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.template_stars": { + "name": "template_stars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starred_at": { + "name": "starred_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_stars_user_id_idx": { + "name": "template_stars_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_id_idx": { + "name": "template_stars_template_id_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_idx": { + "name": "template_stars_user_template_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_user_idx": { + "name": "template_stars_template_user_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_starred_at_idx": { + "name": "template_stars_starred_at_idx", + "columns": [ + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_starred_at_idx": { + "name": "template_stars_template_starred_at_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_unique": { + "name": "template_stars_user_template_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_stars_user_id_user_id_fk": { + "name": "template_stars_user_id_user_id_fk", + "tableFrom": "template_stars", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "template_stars_template_id_templates_id_fk": { + "name": "template_stars_template_id_templates_id_fk", + "tableFrom": "template_stars", + "tableTo": "templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.templates": { + "name": "templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "template_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "required_credentials": { + "name": "required_credentials", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "og_image_url": { + "name": "og_image_url", + "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": { + "templates_status_idx": { + "name": "templates_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_creator_id_idx": { + "name": "templates_creator_id_idx", + "columns": [ + { + "expression": "creator_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_views_idx": { + "name": "templates_views_idx", + "columns": [ + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_stars_idx": { + "name": "templates_stars_idx", + "columns": [ + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_views_idx": { + "name": "templates_status_views_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_stars_idx": { + "name": "templates_status_stars_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_created_at_idx": { + "name": "templates_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_updated_at_idx": { + "name": "templates_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "templates_workflow_id_workflow_id_fk": { + "name": "templates_workflow_id_workflow_id_fk", + "tableFrom": "templates", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "templates_creator_id_template_creators_id_fk": { + "name": "templates_creator_id_template_creators_id_fk", + "tableFrom": "templates", + "tableTo": "template_creators", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "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 + }, + "is_super_user": { + "name": "is_super_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'20'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "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": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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 + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credential_set_id": { + "name": "credential_set_id", + "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": { + "path_idx": { + "name": "path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id": { + "name": "idx_webhook_on_workflow_id_block_id", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_credential_set_id_idx": { + "name": "webhook_credential_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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" + }, + "webhook_block_id_workflow_blocks_id_fk": { + "name": "webhook_block_id_workflow_blocks_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_blocks", + "columnsFrom": ["block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_credential_set_id_credential_set_id_fk": { + "name": "webhook_credential_set_id_credential_set_id_fk", + "tableFrom": "webhook", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "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 + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "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": "'{}'" + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_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": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_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_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 + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "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": "'{}'" + }, + "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_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "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_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "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()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "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_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_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "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_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "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_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "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 + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "last_queued_at": { + "name": "last_queued_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": { + "workflow_schedule_workflow_block_unique": { + "name": "workflow_schedule_workflow_block_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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" + }, + "workflow_schedule_block_id_workflow_blocks_id_fk": { + "name": "workflow_schedule_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_blocks", + "columnsFrom": ["block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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 + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "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": {}, + "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" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "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": { + "workspace_byok_provider_unique": { + "name": "workspace_byok_provider_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_byok_workspace_idx": { + "name": "workspace_byok_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "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": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_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 + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "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": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_files_key_unique": { + "name": "workspace_files_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "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": "workspace_invitation_status", + "typeSchema": "public", + "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'" + }, + "org_invitation_id": { + "name": "org_invitation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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_notification_delivery": { + "name": "workspace_notification_delivery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "notification_delivery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "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": { + "workspace_notification_delivery_subscription_id_idx": { + "name": "workspace_notification_delivery_subscription_id_idx", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_execution_id_idx": { + "name": "workspace_notification_delivery_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_status_idx": { + "name": "workspace_notification_delivery_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_next_attempt_idx": { + "name": "workspace_notification_delivery_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk": { + "name": "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workspace_notification_subscription", + "columnsFrom": ["subscription_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_delivery_workflow_id_workflow_id_fk": { + "name": "workspace_notification_delivery_workflow_id_workflow_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_notification_subscription": { + "name": "workspace_notification_subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notification_type": { + "name": "notification_type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workflow_ids": { + "name": "workflow_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "all_workflows": { + "name": "all_workflows", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "level_filter": { + "name": "level_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['info', 'error']::text[]" + }, + "trigger_filter": { + "name": "trigger_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['api', 'webhook', 'schedule', 'manual', 'chat']::text[]" + }, + "include_final_output": { + "name": "include_final_output", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_trace_spans": { + "name": "include_trace_spans", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_rate_limits": { + "name": "include_rate_limits", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_usage_data": { + "name": "include_usage_data", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "webhook_config": { + "name": "webhook_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "email_recipients": { + "name": "email_recipients", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "slack_config": { + "name": "slack_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "alert_config": { + "name": "alert_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_alert_at": { + "name": "last_alert_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "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": { + "workspace_notification_workspace_id_idx": { + "name": "workspace_notification_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_active_idx": { + "name": "workspace_notification_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_type_idx": { + "name": "workspace_notification_type_idx", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_subscription_workspace_id_workspace_id_fk": { + "name": "workspace_notification_subscription_workspace_id_workspace_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_subscription_created_by_user_id_fk": { + "name": "workspace_notification_subscription_created_by_user_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.a2a_task_status": { + "name": "a2a_task_status", + "schema": "public", + "values": [ + "submitted", + "working", + "input-required", + "completed", + "failed", + "canceled", + "rejected", + "auth-required", + "unknown" + ] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.credential_set_invitation_status": { + "name": "credential_set_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "expired", "cancelled"] + }, + "public.credential_set_member_status": { + "name": "credential_set_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.notification_delivery_status": { + "name": "notification_delivery_status", + "schema": "public", + "values": ["pending", "in_progress", "success", "failed"] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": ["webhook", "email", "slack"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.template_creator_type": { + "name": "template_creator_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.template_status": { + "name": "template_status", + "schema": "public", + "values": ["pending", "approved", "rejected"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": ["workflow", "wand", "copilot"] + }, + "public.workspace_invitation_status": { + "name": "workspace_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 5eb585af8f..9163eb4475 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -995,6 +995,13 @@ "when": 1768511699652, "tag": "0142_short_nicolaos", "breakpoints": true + }, + { + "idx": 143, + "version": "7", + "when": 1768518143986, + "tag": "0143_puzzling_xorn", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index c88c0df929..ab3ae41700 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -2079,6 +2079,7 @@ export const permissionGroup = pgTable( .references(() => user.id, { onDelete: 'cascade' }), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), + autoAddNewMembers: boolean('auto_add_new_members').notNull().default(false), }, (table) => ({ organizationIdIdx: index('permission_group_organization_id_idx').on(table.organizationId), @@ -2087,6 +2088,9 @@ export const permissionGroup = pgTable( table.organizationId, table.name ), + autoAddNewMembersUnique: uniqueIndex('permission_group_org_auto_add_unique') + .on(table.organizationId) + .where(sql`auto_add_new_members = true`), }) ) From f1796d13df11c2d77ad8fbcecdd085966a5b1c41 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 15 Jan 2026 16:43:06 -0800 Subject: [PATCH 07/15] fix(start): permission check for executor --- apps/sim/executor/utils/permission-check.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/sim/executor/utils/permission-check.ts b/apps/sim/executor/utils/permission-check.ts index aabcea6e99..9ded36d02c 100644 --- a/apps/sim/executor/utils/permission-check.ts +++ b/apps/sim/executor/utils/permission-check.ts @@ -136,6 +136,10 @@ export async function validateBlockType( blockType: string, ctx?: ExecutionContext ): Promise { + if (blockType === 'start_trigger') { + return + } + if (!userId) { return } From 87e605703321ac420bcf14b7b36ba404ce676860 Mon Sep 17 00:00:00 2001 From: Waleed Date: Thu, 15 Jan 2026 16:54:24 -0800 Subject: [PATCH 08/15] improvement(chat): partialize chat store to only persist image URL instead of full image in floating chat (#2842) --- .../w/[workflowId]/components/chat/chat.tsx | 96 +++++++++++++------ .../components/chat-message/chat-message.tsx | 7 +- .../chat/hooks/use-chat-file-upload.ts | 34 ++++--- apps/sim/stores/chat/store.ts | 27 ++---- 4 files changed, 101 insertions(+), 63 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx index cc1bf0403a..524e3c02ec 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx @@ -94,6 +94,9 @@ interface ProcessedAttachment { dataUrl: string } +/** Timeout for FileReader operations in milliseconds */ +const FILE_READ_TIMEOUT_MS = 60000 + /** * Reads files and converts them to data URLs for image display * @param chatFiles - Array of chat files to process @@ -107,8 +110,37 @@ const processFileAttachments = async (chatFiles: ChatFile[]): Promise((resolve, reject) => { const reader = new FileReader() - reader.onload = () => resolve(reader.result as string) - reader.onerror = reject + let settled = false + + const timeoutId = setTimeout(() => { + if (!settled) { + settled = true + reader.abort() + reject(new Error(`File read timed out after ${FILE_READ_TIMEOUT_MS}ms`)) + } + }, FILE_READ_TIMEOUT_MS) + + reader.onload = () => { + if (!settled) { + settled = true + clearTimeout(timeoutId) + resolve(reader.result as string) + } + } + reader.onerror = () => { + if (!settled) { + settled = true + clearTimeout(timeoutId) + reject(reader.error) + } + } + reader.onabort = () => { + if (!settled) { + settled = true + clearTimeout(timeoutId) + reject(new Error('File read aborted')) + } + } reader.readAsDataURL(file.file) }) } catch (error) { @@ -202,7 +234,6 @@ export function Chat() { const triggerWorkflowUpdate = useWorkflowStore((state) => state.triggerUpdate) const setSubBlockValue = useSubBlockStore((state) => state.setValue) - // Chat state (UI and messages from unified store) const { isChatOpen, chatPosition, @@ -230,19 +261,16 @@ export function Chat() { const { data: session } = useSession() const { addToQueue } = useOperationQueue() - // Local state const [chatMessage, setChatMessage] = useState('') const [promptHistory, setPromptHistory] = useState([]) const [historyIndex, setHistoryIndex] = useState(-1) const [moreMenuOpen, setMoreMenuOpen] = useState(false) - // Refs const inputRef = useRef(null) const timeoutRef = useRef(null) const streamReaderRef = useRef | null>(null) const preventZoomRef = usePreventZoom() - // File upload hook const { chatFiles, uploadErrors, @@ -257,6 +285,38 @@ export function Chat() { handleDrop, } = useChatFileUpload() + const filePreviewUrls = useRef>(new Map()) + + const getFilePreviewUrl = useCallback((file: ChatFile): string | null => { + if (!file.type.startsWith('image/')) return null + + const existing = filePreviewUrls.current.get(file.id) + if (existing) return existing + + const url = URL.createObjectURL(file.file) + filePreviewUrls.current.set(file.id, url) + return url + }, []) + + useEffect(() => { + const currentFileIds = new Set(chatFiles.map((f) => f.id)) + const urlMap = filePreviewUrls.current + + for (const [fileId, url] of urlMap.entries()) { + if (!currentFileIds.has(fileId)) { + URL.revokeObjectURL(url) + urlMap.delete(fileId) + } + } + + return () => { + for (const url of urlMap.values()) { + URL.revokeObjectURL(url) + } + urlMap.clear() + } + }, [chatFiles]) + /** * Resolves the unified start block for chat execution, if available. */ @@ -322,13 +382,11 @@ export function Chat() { const shouldShowConfigureStartInputsButton = Boolean(startBlockId) && missingStartReservedFields.length > 0 - // Get actual position (default if not set) const actualPosition = useMemo( () => getChatPosition(chatPosition, chatWidth, chatHeight), [chatPosition, chatWidth, chatHeight] ) - // Drag hook const { handleMouseDown } = useFloatDrag({ position: actualPosition, width: chatWidth, @@ -336,7 +394,6 @@ export function Chat() { onPositionChange: setChatPosition, }) - // Boundary sync hook - keeps chat within bounds when layout changes useFloatBoundarySync({ isOpen: isChatOpen, position: actualPosition, @@ -345,7 +402,6 @@ export function Chat() { onPositionChange: setChatPosition, }) - // Resize hook - enables resizing from all edges and corners const { cursor: resizeCursor, handleMouseMove: handleResizeMouseMove, @@ -359,13 +415,11 @@ export function Chat() { onDimensionsChange: setChatDimensions, }) - // Get output entries from console const outputEntries = useMemo(() => { if (!activeWorkflowId) return [] return entries.filter((entry) => entry.workflowId === activeWorkflowId && entry.output) }, [entries, activeWorkflowId]) - // Get filtered messages for current workflow const workflowMessages = useMemo(() => { if (!activeWorkflowId) return [] return messages @@ -373,14 +427,11 @@ export function Chat() { .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()) }, [messages, activeWorkflowId]) - // Check if any message is currently streaming const isStreaming = useMemo(() => { - // Match copilot semantics: only treat as streaming if the LAST message is streaming const lastMessage = workflowMessages[workflowMessages.length - 1] return Boolean(lastMessage?.isStreaming) }, [workflowMessages]) - // Map chat messages to copilot message format (type -> role) for scroll hook const messagesForScrollHook = useMemo(() => { return workflowMessages.map((msg) => ({ ...msg, @@ -388,8 +439,6 @@ export function Chat() { })) }, [workflowMessages]) - // Scroll management hook - reuse copilot's implementation - // Use immediate scroll behavior to keep the view pinned to the bottom during streaming const { scrollAreaRef, scrollToBottom } = useScrollManagement( messagesForScrollHook, isStreaming, @@ -398,7 +447,6 @@ export function Chat() { } ) - // Memoize user messages for performance const userMessages = useMemo(() => { return workflowMessages .filter((msg) => msg.type === 'user') @@ -406,7 +454,6 @@ export function Chat() { .filter((content): content is string => typeof content === 'string') }, [workflowMessages]) - // Update prompt history when workflow changes useEffect(() => { if (!activeWorkflowId) { setPromptHistory([]) @@ -419,7 +466,7 @@ export function Chat() { }, [activeWorkflowId, userMessages]) /** - * Auto-scroll to bottom when messages load + * Auto-scroll to bottom when messages load and chat is open */ useEffect(() => { if (workflowMessages.length > 0 && isChatOpen) { @@ -427,7 +474,6 @@ export function Chat() { } }, [workflowMessages.length, scrollToBottom, isChatOpen]) - // Get selected workflow outputs (deduplicated) const selectedOutputs = useMemo(() => { if (!activeWorkflowId) return [] const selected = selectedWorkflowOutputs[activeWorkflowId] @@ -448,7 +494,6 @@ export function Chat() { }, delay) }, []) - // Cleanup on unmount useEffect(() => { return () => { timeoutRef.current && clearTimeout(timeoutRef.current) @@ -456,7 +501,6 @@ export function Chat() { } }, []) - // React to execution cancellation from run button useEffect(() => { if (!isExecuting && isStreaming) { const lastMessage = workflowMessages[workflowMessages.length - 1] @@ -500,7 +544,6 @@ export function Chat() { const chunk = decoder.decode(value, { stream: true }) buffer += chunk - // Process only complete SSE messages; keep any partial trailing data in buffer const separatorIndex = buffer.lastIndexOf('\n\n') if (separatorIndex === -1) { continue @@ -550,7 +593,6 @@ export function Chat() { } finalizeMessageStream(responseMessageId) } finally { - // Only clear ref if it's still our reader (prevents clobbering a new stream) if (streamReaderRef.current === reader) { streamReaderRef.current = null } @@ -979,8 +1021,7 @@ export function Chat() { {chatFiles.length > 0 && (
{chatFiles.map((file) => { - const isImage = file.type.startsWith('image/') - const previewUrl = isImage ? URL.createObjectURL(file.file) : null + const previewUrl = getFilePreviewUrl(file) return (
URL.revokeObjectURL(previewUrl)} /> ) : (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/chat-message/chat-message.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/chat-message/chat-message.tsx index 2a01d630a4..a11983b0be 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/chat-message/chat-message.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/chat-message/chat-message.tsx @@ -113,16 +113,17 @@ export function ChatMessage({ message }: ChatMessageProps) { {message.attachments && message.attachments.length > 0 && (
{message.attachments.map((attachment) => { - const isImage = attachment.type.startsWith('image/') const hasValidDataUrl = attachment.dataUrl?.trim() && attachment.dataUrl.startsWith('data:') + // Only treat as displayable image if we have both image type AND valid data URL + const canDisplayAsImage = attachment.type.startsWith('image/') && hasValidDataUrl return (
{ if (hasValidDataUrl) { e.preventDefault() @@ -131,7 +132,7 @@ export function ChatMessage({ message }: ChatMessageProps) { } }} > - {isImage && hasValidDataUrl ? ( + {canDisplayAsImage ? ( {attachment.name} { - const remainingSlots = Math.max(0, MAX_FILES - chatFiles.length) + const addFiles = useCallback((files: File[]) => { + setChatFiles((currentFiles) => { + const remainingSlots = Math.max(0, MAX_FILES - currentFiles.length) const candidateFiles = files.slice(0, remainingSlots) const errors: string[] = [] const validNewFiles: ChatFile[] = [] @@ -39,11 +40,14 @@ export function useChatFileUpload() { continue } - // Check for duplicates - const isDuplicate = chatFiles.some( + // Check for duplicates against current files and newly added valid files + const isDuplicateInCurrent = currentFiles.some( (existingFile) => existingFile.name === file.name && existingFile.size === file.size ) - if (isDuplicate) { + const isDuplicateInNew = validNewFiles.some( + (newFile) => newFile.name === file.name && newFile.size === file.size + ) + if (isDuplicateInCurrent || isDuplicateInNew) { errors.push(`${file.name} already added`) continue } @@ -57,20 +61,20 @@ export function useChatFileUpload() { }) } + // Update errors outside the state setter to avoid nested state updates if (errors.length > 0) { - setUploadErrors(errors) + // Use setTimeout to avoid state update during render + setTimeout(() => setUploadErrors(errors), 0) + } else if (validNewFiles.length > 0) { + setTimeout(() => setUploadErrors([]), 0) } if (validNewFiles.length > 0) { - setChatFiles([...chatFiles, ...validNewFiles]) - // Clear errors when files are successfully added - if (errors.length === 0) { - setUploadErrors([]) - } + return [...currentFiles, ...validNewFiles] } - }, - [chatFiles] - ) + return currentFiles + }) + }, []) /** * Remove a file diff --git a/apps/sim/stores/chat/store.ts b/apps/sim/stores/chat/store.ts index e9a93f1e14..3da14769db 100644 --- a/apps/sim/stores/chat/store.ts +++ b/apps/sim/stores/chat/store.ts @@ -26,7 +26,6 @@ export const useChatStore = create()( devtools( persist( (set, get) => ({ - // UI State isChatOpen: false, chatPosition: null, chatWidth: DEFAULT_WIDTH, @@ -51,7 +50,6 @@ export const useChatStore = create()( set({ chatPosition: null }) }, - // Message State messages: [], selectedWorkflowOutputs: {}, conversationIds: {}, @@ -60,12 +58,10 @@ export const useChatStore = create()( set((state) => { const newMessage: ChatMessage = { ...message, - // Preserve provided id and timestamp if they exist; otherwise generate new ones id: (message as any).id ?? crypto.randomUUID(), timestamp: (message as any).timestamp ?? new Date().toISOString(), } - // Keep only the last MAX_MESSAGES const newMessages = [newMessage, ...state.messages].slice(0, MAX_MESSAGES) return { messages: newMessages } @@ -80,7 +76,6 @@ export const useChatStore = create()( ), } - // Generate a new conversationId when clearing chat for a specific workflow if (workflowId) { const newConversationIds = { ...state.conversationIds } newConversationIds[workflowId] = uuidv4() @@ -89,7 +84,6 @@ export const useChatStore = create()( conversationIds: newConversationIds, } } - // When clearing all chats (workflowId is null), also clear all conversationIds return { ...newState, conversationIds: {}, @@ -131,15 +125,12 @@ export const useChatStore = create()( return stringValue } - // CSV Headers const headers = ['timestamp', 'type', 'content'] - // Sort messages by timestamp (oldest first) const sortedMessages = messages.sort( (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() ) - // Generate CSV rows const csvRows = [ headers.join(','), ...sortedMessages.map((message) => @@ -151,15 +142,12 @@ export const useChatStore = create()( ), ] - // Create CSV content const csvContent = csvRows.join('\n') - // Generate filename with timestamp const now = new Date() const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19) const filename = `chat-${workflowId}-${timestamp}.csv` - // Create and trigger download const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }) const link = document.createElement('a') @@ -177,15 +165,11 @@ export const useChatStore = create()( setSelectedWorkflowOutput: (workflowId, outputIds) => { set((state) => { - // Create a new copy of the selections state const newSelections = { ...state.selectedWorkflowOutputs } - // If empty array, explicitly remove the key to prevent empty arrays from persisting if (outputIds.length === 0) { - // Delete the key entirely instead of setting to empty array delete newSelections[workflowId] } else { - // Ensure no duplicates in the selection by using Set newSelections[workflowId] = [...new Set(outputIds)] } @@ -200,7 +184,6 @@ export const useChatStore = create()( getConversationId: (workflowId) => { const state = get() if (!state.conversationIds[workflowId]) { - // Generate a new conversation ID if one doesn't exist return get().generateNewConversationId(workflowId) } return state.conversationIds[workflowId] @@ -270,6 +253,16 @@ export const useChatStore = create()( }), { name: 'chat-store', + partialize: (state) => ({ + ...state, + messages: state.messages.map((msg) => ({ + ...msg, + attachments: msg.attachments?.map((att) => ({ + ...att, + dataUrl: '', + })), + })), + }), } ) ) From 81cc88b2e262507c863ebba2198808376c1d4918 Mon Sep 17 00:00:00 2001 From: Waleed Date: Thu, 15 Jan 2026 16:54:33 -0800 Subject: [PATCH 09/15] feat(context-menu): added context menu to dead sidebar space and usage indicator (#2841) --- .../usage-indicator-context-menu.tsx | 189 +++++++++++++ .../usage-indicator/usage-indicator.tsx | 251 +++++++++++++----- .../empty-area-context-menu.tsx | 93 +++++++ .../empty-area-context-menu/index.ts | 1 + .../workflow-list/workflow-list.tsx | 99 +++++-- .../w/components/sidebar/sidebar.tsx | 3 + 6 files changed, 544 insertions(+), 92 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/usage-indicator/usage-indicator-context-menu.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/empty-area-context-menu/empty-area-context-menu.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/empty-area-context-menu/index.ts diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/usage-indicator/usage-indicator-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/usage-indicator/usage-indicator-context-menu.tsx new file mode 100644 index 0000000000..c1b64d6a07 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/usage-indicator/usage-indicator-context-menu.tsx @@ -0,0 +1,189 @@ +'use client' + +import { + Popover, + PopoverAnchor, + PopoverContent, + PopoverDivider, + PopoverItem, +} from '@/components/emcn' + +interface UsageIndicatorContextMenuProps { + /** + * Whether the context menu is open + */ + isOpen: boolean + /** + * Position of the context menu + */ + position: { x: number; y: number } + /** + * Ref for the menu element + */ + menuRef: React.RefObject + /** + * Callback when menu should close + */ + onClose: () => void + /** + * Menu items configuration based on plan and permissions + */ + menuItems: UsageMenuItems +} + +interface UsageMenuItems { + /** + * Show "Set usage limit" option + */ + showSetLimit: boolean + /** + * Show "Upgrade to Pro" option (free users) + */ + showUpgradeToPro: boolean + /** + * Show "Upgrade to Team" option (free or pro users) + */ + showUpgradeToTeam: boolean + /** + * Show "Manage seats" option (team admins) + */ + showManageSeats: boolean + /** + * Show "Upgrade to Enterprise" option + */ + showUpgradeToEnterprise: boolean + /** + * Show "Contact support" option (enterprise users) + */ + showContactSupport: boolean + /** + * Callbacks + */ + onSetLimit?: () => void + onUpgradeToPro?: () => void + onUpgradeToTeam?: () => void + onManageSeats?: () => void + onUpgradeToEnterprise?: () => void + onContactSupport?: () => void +} + +/** + * Context menu component for usage indicator. + * Displays plan-appropriate options in a popover at the right-click position. + */ +export function UsageIndicatorContextMenu({ + isOpen, + position, + menuRef, + onClose, + menuItems, +}: UsageIndicatorContextMenuProps) { + const { + showSetLimit, + showUpgradeToPro, + showUpgradeToTeam, + showManageSeats, + showUpgradeToEnterprise, + showContactSupport, + onSetLimit, + onUpgradeToPro, + onUpgradeToTeam, + onManageSeats, + onUpgradeToEnterprise, + onContactSupport, + } = menuItems + + const hasLimitSection = showSetLimit + const hasUpgradeSection = + showUpgradeToPro || showUpgradeToTeam || showUpgradeToEnterprise || showContactSupport + const hasTeamSection = showManageSeats + + return ( + !open && onClose()} + variant='secondary' + size='sm' + colorScheme='inverted' + > + + + {/* Limit management section */} + {showSetLimit && onSetLimit && ( + { + onSetLimit() + onClose() + }} + > + Set usage limit + + )} + + {/* Team management section */} + {hasLimitSection && hasTeamSection && } + {showManageSeats && onManageSeats && ( + { + onManageSeats() + onClose() + }} + > + Manage seats + + )} + + {/* Upgrade section */} + {(hasLimitSection || hasTeamSection) && hasUpgradeSection && } + {showUpgradeToPro && onUpgradeToPro && ( + { + onUpgradeToPro() + onClose() + }} + > + Upgrade to Pro + + )} + {showUpgradeToTeam && onUpgradeToTeam && ( + { + onUpgradeToTeam() + onClose() + }} + > + Upgrade to Team + + )} + {showUpgradeToEnterprise && onUpgradeToEnterprise && ( + { + onUpgradeToEnterprise() + onClose() + }} + > + Upgrade to Enterprise + + )} + {showContactSupport && onContactSupport && ( + { + onContactSupport() + onClose() + }} + > + Contact support + + )} + + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/usage-indicator/usage-indicator.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/usage-indicator/usage-indicator.tsx index 68872c735f..b244359241 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/usage-indicator/usage-indicator.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/usage-indicator/usage-indicator.tsx @@ -1,20 +1,23 @@ 'use client' -import { useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { createLogger } from '@sim/logger' import { useQueryClient } from '@tanstack/react-query' import { Badge } from '@/components/emcn' import { Skeleton } from '@/components/ui' +import { useSubscriptionUpgrade } from '@/lib/billing/client/upgrade' import { getFilledPillColor, USAGE_PILL_COLORS, USAGE_THRESHOLDS, } from '@/lib/billing/client/usage-visualization' import { getBillingStatus, getSubscriptionStatus, getUsage } from '@/lib/billing/client/utils' +import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' import { useSocket } from '@/app/workspace/providers/socket-provider' import { subscriptionKeys, useSubscriptionData } from '@/hooks/queries/subscription' import { SIDEBAR_WIDTH } from '@/stores/constants' import { useSidebarStore } from '@/stores/sidebar/store' +import { UsageIndicatorContextMenu } from './usage-indicator-context-menu' const logger = createLogger('UsageIndicator') @@ -188,6 +191,8 @@ interface UsageIndicatorProps { onClick?: () => void } +const TYPEFORM_ENTERPRISE_URL = 'https://form.typeform.com/to/jqCO12pF' + /** * Displays a visual usage indicator with animated pill bar. */ @@ -196,6 +201,15 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) { const sidebarWidth = useSidebarStore((state) => state.sidebarWidth) const { onOperationConfirmed } = useSocket() const queryClient = useQueryClient() + const { handleUpgrade } = useSubscriptionUpgrade() + + const { + isOpen: isContextMenuOpen, + position: contextMenuPosition, + menuRef: contextMenuRef, + handleContextMenu, + closeMenu: closeContextMenu, + } = useContextMenu() useEffect(() => { const handleOperationConfirmed = () => { @@ -266,6 +280,96 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) { const filledPillsCount = Math.ceil((progressPercentage / 100) * pillCount) const filledColor = getFilledPillColor(isCritical, isWarning) + const isFree = planType === 'free' + const isPro = planType === 'pro' + const isTeam = planType === 'team' + const isEnterprise = planType === 'enterprise' + + const handleUpgradeToPro = useCallback(async () => { + try { + await handleUpgrade('pro') + } catch (error) { + logger.error('Failed to upgrade to Pro', { error }) + } + }, [handleUpgrade]) + + const handleUpgradeToTeam = useCallback(async () => { + try { + await handleUpgrade('team') + } catch (error) { + logger.error('Failed to upgrade to Team', { error }) + } + }, [handleUpgrade]) + + const handleSetLimit = useCallback(() => { + window.dispatchEvent(new CustomEvent('open-settings', { detail: { tab: 'subscription' } })) + }, []) + + const handleManageSeats = useCallback(() => { + window.dispatchEvent(new CustomEvent('open-settings', { detail: { tab: 'team' } })) + }, []) + + const handleUpgradeToEnterprise = useCallback(() => { + window.open(TYPEFORM_ENTERPRISE_URL, '_blank') + }, []) + + const handleContactSupport = useCallback(() => { + window.dispatchEvent(new CustomEvent('open-help-modal')) + }, []) + + const contextMenuItems = useMemo( + () => ({ + // Set limit: Only for Pro and Team admins (not free, not enterprise) + showSetLimit: (isPro || (isTeam && userCanManageBilling)) && !isEnterprise, + // Upgrade to Pro: Only for free users + showUpgradeToPro: isFree, + // Upgrade to Team: Free users and Pro users with billing permission + showUpgradeToTeam: isFree || (isPro && userCanManageBilling), + // Manage seats: Only for Team admins + showManageSeats: isTeam && userCanManageBilling, + // Upgrade to Enterprise: Only for Team admins (not free, not pro, not enterprise) + showUpgradeToEnterprise: isTeam && userCanManageBilling, + // Contact support: Only for Enterprise admins + showContactSupport: isEnterprise && userCanManageBilling, + onSetLimit: handleSetLimit, + onUpgradeToPro: handleUpgradeToPro, + onUpgradeToTeam: handleUpgradeToTeam, + onManageSeats: handleManageSeats, + onUpgradeToEnterprise: handleUpgradeToEnterprise, + onContactSupport: handleContactSupport, + }), + [ + isFree, + isPro, + isTeam, + isEnterprise, + userCanManageBilling, + handleSetLimit, + handleUpgradeToPro, + handleUpgradeToTeam, + handleManageSeats, + handleUpgradeToEnterprise, + handleContactSupport, + ] + ) + + // Check if any context menu items will be visible + const hasContextMenuItems = + contextMenuItems.showSetLimit || + contextMenuItems.showUpgradeToPro || + contextMenuItems.showUpgradeToTeam || + contextMenuItems.showManageSeats || + contextMenuItems.showUpgradeToEnterprise || + contextMenuItems.showContactSupport + + const handleContextMenuWithCheck = useCallback( + (e: React.MouseEvent) => { + if (!hasContextMenuItems) return + handleContextMenu(e) + }, + [hasContextMenuItems, handleContextMenu] + ) + const [isHovered, setIsHovered] = useState(false) const [wavePosition, setWavePosition] = useState(null) @@ -359,82 +463,93 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) { } return ( -
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > - {/* Top row */} -
-
- {showPlanText && ( - <> - - {PLAN_NAMES[planType]} - -
- - )} -
- {statusText.isError ? ( - - {statusText.text} - - ) : ( + <> +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + {/* Top row */} +
+
+ {showPlanText && ( <> - - ${usage.current.toFixed(2)} - - / - - ${usage.limit.toFixed(2)} + + {PLAN_NAMES[planType]} +
)} +
+ {statusText.isError ? ( + + {statusText.text} + + ) : ( + <> + + ${usage.current.toFixed(2)} + + / + + ${usage.limit.toFixed(2)} + + + )} +
+ {badgeConfig.show && ( + + {badgeConfig.label} + + )}
- {badgeConfig.show && ( - - {badgeConfig.label} - - )} -
- {/* Pills row */} -
- {Array.from({ length: pillCount }).map((_, i) => { - const isFilled = i < filledPillsCount - const baseColor = isFilled ? filledColor : USAGE_PILL_COLORS.UNFILLED - - const backgroundColor = baseColor - let backgroundImage: string | undefined - - if (isHovered && wavePosition !== null) { - const headIndex = Math.floor(wavePosition) - const pillOffsetFromStart = i - startAnimationIndex - - if (pillOffsetFromStart >= 0 && pillOffsetFromStart < headIndex) { - backgroundImage = `linear-gradient(to right, ${filledColor}, ${filledColor})` - } else if (pillOffsetFromStart === headIndex) { - const fillPercent = (wavePosition - headIndex) * 100 - backgroundImage = `linear-gradient(to right, ${filledColor} ${fillPercent}%, ${baseColor} ${fillPercent}%)` + {/* Pills row */} +
+ {Array.from({ length: pillCount }).map((_, i) => { + const isFilled = i < filledPillsCount + const baseColor = isFilled ? filledColor : USAGE_PILL_COLORS.UNFILLED + + const backgroundColor = baseColor + let backgroundImage: string | undefined + + if (isHovered && wavePosition !== null) { + const headIndex = Math.floor(wavePosition) + const pillOffsetFromStart = i - startAnimationIndex + + if (pillOffsetFromStart >= 0 && pillOffsetFromStart < headIndex) { + backgroundImage = `linear-gradient(to right, ${filledColor}, ${filledColor})` + } else if (pillOffsetFromStart === headIndex) { + const fillPercent = (wavePosition - headIndex) * 100 + backgroundImage = `linear-gradient(to right, ${filledColor} ${fillPercent}%, ${baseColor} ${fillPercent}%)` + } } - } - return ( -
- ) - })} + return ( +
+ ) + })} +
-
+ + + ) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/empty-area-context-menu/empty-area-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/empty-area-context-menu/empty-area-context-menu.tsx new file mode 100644 index 0000000000..16b32c9d50 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/empty-area-context-menu/empty-area-context-menu.tsx @@ -0,0 +1,93 @@ +'use client' + +import { Popover, PopoverAnchor, PopoverContent, PopoverItem } from '@/components/emcn' + +interface EmptyAreaContextMenuProps { + /** + * Whether the context menu is open + */ + isOpen: boolean + /** + * Position of the context menu + */ + position: { x: number; y: number } + /** + * Ref for the menu element + */ + menuRef: React.RefObject + /** + * Callback when menu should close + */ + onClose: () => void + /** + * Callback when create workflow is clicked + */ + onCreateWorkflow: () => void + /** + * Callback when create folder is clicked + */ + onCreateFolder: () => void + /** + * Whether create workflow is disabled + */ + disableCreateWorkflow?: boolean + /** + * Whether create folder is disabled + */ + disableCreateFolder?: boolean +} + +/** + * Context menu component for sidebar empty area. + * Displays options to create a workflow or folder when right-clicking on empty space. + */ +export function EmptyAreaContextMenu({ + isOpen, + position, + menuRef, + onClose, + onCreateWorkflow, + onCreateFolder, + disableCreateWorkflow = false, + disableCreateFolder = false, +}: EmptyAreaContextMenuProps) { + return ( + !open && onClose()} + variant='secondary' + size='sm' + colorScheme='inverted' + > + + + { + onCreateWorkflow() + onClose() + }} + > + Create workflow + + { + onCreateFolder() + onClose() + }} + > + Create folder + + + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/empty-area-context-menu/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/empty-area-context-menu/index.ts new file mode 100644 index 0000000000..aac2288b7d --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/empty-area-context-menu/index.ts @@ -0,0 +1 @@ +export { EmptyAreaContextMenu } from './empty-area-context-menu' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx index 501ec347b5..8d3e7fa769 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx @@ -3,9 +3,11 @@ import { memo, useCallback, useEffect, useMemo } from 'react' import clsx from 'clsx' import { useParams, usePathname } from 'next/navigation' +import { EmptyAreaContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/empty-area-context-menu' import { FolderItem } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item' import { WorkflowItem } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item' import { + useContextMenu, useDragDrop, useWorkflowSelection, } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' @@ -36,6 +38,9 @@ interface WorkflowListProps { handleFileChange: (event: React.ChangeEvent) => void fileInputRef: React.RefObject scrollContainerRef: React.RefObject + onCreateWorkflow?: () => void + onCreateFolder?: () => void + disableCreate?: boolean } const DropIndicatorLine = memo(function DropIndicatorLine({ @@ -63,6 +68,9 @@ export function WorkflowList({ handleFileChange, fileInputRef, scrollContainerRef, + onCreateWorkflow, + onCreateFolder, + disableCreate = false, }: WorkflowListProps) { const pathname = usePathname() const params = useParams() @@ -72,6 +80,14 @@ export function WorkflowList({ const { isLoading: foldersLoading } = useFolders(workspaceId) const { getFolderTree, expandedFolders, getFolderPath, setExpanded } = useFolderStore() + const { + isOpen: isEmptyAreaMenuOpen, + position: emptyAreaMenuPosition, + menuRef: emptyAreaMenuRef, + handleContextMenu: handleEmptyAreaContextMenu, + closeMenu: closeEmptyAreaMenu, + } = useContextMenu() + const { dropIndicator, isDragging, @@ -351,36 +367,71 @@ export function WorkflowList({ [workflowId] ) + const handleContainerContextMenu = useCallback( + (e: React.MouseEvent) => { + const target = e.target as HTMLElement + const isOnEmptyArea = + target === e.currentTarget || + target.classList.contains('space-y-[2px]') || + target.closest('[data-empty-area]') + if (!isOnEmptyArea) return + if (!onCreateWorkflow && !onCreateFolder) return + handleEmptyAreaContextMenu(e) + }, + [handleEmptyAreaContextMenu, onCreateWorkflow, onCreateFolder] + ) + return ( -
+ <>
- {/* Root drop target highlight overlay */}
-
- {rootItems.map((item) => - item.type === 'folder' - ? renderFolderSection(item.data as FolderTreeNode, 0, null) - : renderWorkflowItem(item.data as WorkflowMetadata, 0, null) - )} + className={clsx('relative flex-1 rounded-[4px]', !hasRootItems && 'min-h-[26px]')} + {...rootDropZoneHandlers} + data-empty-area + > + {/* Root drop target highlight overlay */} +
+
+ {rootItems.map((item) => + item.type === 'folder' + ? renderFolderSection(item.data as FolderTreeNode, 0, null) + : renderWorkflowItem(item.data as WorkflowMetadata, 0, null) + )} +
+ +
- -
+ {onCreateWorkflow && onCreateFolder && ( + + )} + ) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index a97638a975..e310d92b62 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -639,6 +639,9 @@ export function Sidebar() { handleFileChange={handleImportFileChange} fileInputRef={fileInputRef} scrollContainerRef={scrollContainerRef} + onCreateWorkflow={handleCreateWorkflow} + onCreateFolder={handleCreateFolder} + disableCreate={!canEdit || isCreatingWorkflow || isCreatingFolder} />
From b813bf7f270c6795c2d0f1b55498b95336da7ca9 Mon Sep 17 00:00:00 2001 From: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com> Date: Thu, 15 Jan 2026 17:42:59 -0800 Subject: [PATCH 10/15] improvement: workflow, blocks, preview, avatars, output-select (#2840) * improvement(workflow): ui/ux, refactors, optimizations * improvement: blocks, preview, avatars * improvement(output-select): ui * update API endpoint picker to match output selector * improvement: subflow ui/ux --------- Co-authored-by: waleed --- .../components => }/action-bar/action-bar.tsx | 7 +- .../output-select/output-select.tsx | 5 +- .../components/note-block/note-block.tsx | 2 +- .../deploy-modal/components/api/api.tsx | 61 +- .../components/general/general.tsx | 1 - .../components/subflows/subflow-node.tsx | 34 +- .../components/connections/connections.tsx | 23 - .../workflow-block/components/index.ts | 2 - .../workflow-block/workflow-block.tsx | 7 +- .../workflow-controls/workflow-controls.tsx | 14 +- .../[workspaceId]/w/[workflowId]/workflow.tsx | 752 ++++++++---------- .../components/block-details-sidebar.tsx | 5 - .../w/components/preview/preview.tsx | 19 +- .../components/folder-item/folder-item.tsx | 4 +- .../workflow-item/avatars/avatars.tsx | 18 +- .../workflow-item/workflow-item.tsx | 82 +- 16 files changed, 420 insertions(+), 616 deletions(-) rename apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/{workflow-block/components => }/action-bar/action-bar.tsx (97%) delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/connections/connections.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/index.ts diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/action-bar/action-bar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx similarity index 97% rename from apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/action-bar/action-bar.tsx rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx index 615d91aad1..57cfc4f4d1 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/action-bar/action-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx @@ -100,6 +100,7 @@ export const ActionBar = memo( const isStartBlock = blockType === 'starter' || blockType === 'start_trigger' const isResponseBlock = blockType === 'response' const isNoteBlock = blockType === 'note' + const isSubflowBlock = blockType === 'loop' || blockType === 'parallel' /** * Get appropriate tooltip message based on disabled state @@ -125,7 +126,7 @@ export const ActionBar = memo( 'dark:border-transparent dark:bg-[var(--surface-4)]' )} > - {!isNoteBlock && ( + {!isNoteBlock && !isSubflowBlock && (
setExpandedSelectedBlockId(null)} selectedBlockId={expandedSelectedBlockId} - lightweight />
{expandedSelectedBlockId && workflowToShow.blocks?.[expandedSelectedBlockId] && ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx index df0d1afa07..04e64907fa 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx @@ -1,12 +1,12 @@ import { memo, useMemo, useRef } from 'react' import { RepeatIcon, SplitIcon } from 'lucide-react' import { Handle, type NodeProps, Position, useReactFlow } from 'reactflow' -import { Button, Trash } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' import { HANDLE_POSITIONS } from '@/lib/workflows/blocks/block-dimensions' import { type DiffStatus, hasDiffStatus } from '@/lib/workflows/diff/types' +import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' +import { ActionBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar' import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks' -import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { usePanelEditorStore } from '@/stores/panel' /** @@ -18,11 +18,16 @@ import { usePanelEditorStore } from '@/stores/panel' const SubflowNodeStyles: React.FC = () => { return (