diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index f13fc8aa8..dfb95dab2 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -1157,6 +1157,21 @@ export function AirweaveIcon(props: SVGProps) { ) } +export function GoogleBooksIcon(props: SVGProps) { + return ( + + + + + ) +} + export function GoogleDocsIcon(props: SVGProps) { return ( = { github_v2: GithubIcon, gitlab: GitLabIcon, gmail_v2: GmailIcon, + google_books: GoogleBooksIcon, google_calendar_v2: GoogleCalendarIcon, google_docs: GoogleDocsIcon, google_drive: GoogleDriveIcon, diff --git a/apps/docs/content/docs/en/tools/google_books.mdx b/apps/docs/content/docs/en/tools/google_books.mdx new file mode 100644 index 000000000..9baec6846 --- /dev/null +++ b/apps/docs/content/docs/en/tools/google_books.mdx @@ -0,0 +1,96 @@ +--- +title: Google Books +description: Search and retrieve book information +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Usage Instructions + +Search for books using the Google Books API. Find volumes by title, author, ISBN, or keywords, and retrieve detailed information about specific books including descriptions, ratings, and publication details. + + + +## Tools + +### `google_books_volume_search` + +Search for books using the Google Books API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Google Books API key | +| `query` | string | Yes | Search query. Supports special keywords: intitle:, inauthor:, inpublisher:, subject:, isbn: | +| `filter` | string | No | Filter results by availability \(partial, full, free-ebooks, paid-ebooks, ebooks\) | +| `printType` | string | No | Restrict to print type \(all, books, magazines\) | +| `orderBy` | string | No | Sort order \(relevance, newest\) | +| `startIndex` | number | No | Index of the first result to return \(for pagination\) | +| `maxResults` | number | No | Maximum number of results to return \(1-40\) | +| `langRestrict` | string | No | Restrict results to a specific language \(ISO 639-1 code\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `totalItems` | number | Total number of matching results | +| `volumes` | array | List of matching volumes | +| ↳ `id` | string | Volume ID | +| ↳ `title` | string | Book title | +| ↳ `subtitle` | string | Book subtitle | +| ↳ `authors` | array | List of authors | +| ↳ `publisher` | string | Publisher name | +| ↳ `publishedDate` | string | Publication date | +| ↳ `description` | string | Book description | +| ↳ `pageCount` | number | Number of pages | +| ↳ `categories` | array | Book categories | +| ↳ `averageRating` | number | Average rating \(1-5\) | +| ↳ `ratingsCount` | number | Number of ratings | +| ↳ `language` | string | Language code | +| ↳ `previewLink` | string | Link to preview on Google Books | +| ↳ `infoLink` | string | Link to info page | +| ↳ `thumbnailUrl` | string | Book cover thumbnail URL | +| ↳ `isbn10` | string | ISBN-10 identifier | +| ↳ `isbn13` | string | ISBN-13 identifier | + +### `google_books_volume_details` + +Get detailed information about a specific book volume + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Google Books API key | +| `volumeId` | string | Yes | The ID of the volume to retrieve | +| `projection` | string | No | Projection level \(full, lite\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Volume ID | +| `title` | string | Book title | +| `subtitle` | string | Book subtitle | +| `authors` | array | List of authors | +| `publisher` | string | Publisher name | +| `publishedDate` | string | Publication date | +| `description` | string | Book description | +| `pageCount` | number | Number of pages | +| `categories` | array | Book categories | +| `averageRating` | number | Average rating \(1-5\) | +| `ratingsCount` | number | Number of ratings | +| `language` | string | Language code | +| `previewLink` | string | Link to preview on Google Books | +| `infoLink` | string | Link to info page | +| `thumbnailUrl` | string | Book cover thumbnail URL | +| `isbn10` | string | ISBN-10 identifier | +| `isbn13` | string | ISBN-13 identifier | + + diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index f9bd3ca1f..c10640a96 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -33,6 +33,7 @@ "github", "gitlab", "gmail", + "google_books", "google_calendar", "google_docs", "google_drive", diff --git a/apps/sim/.env.example b/apps/sim/.env.example index f8e926f88..6c22b09ee 100644 --- a/apps/sim/.env.example +++ b/apps/sim/.env.example @@ -13,6 +13,7 @@ BETTER_AUTH_URL=http://localhost:3000 # NextJS (Required) NEXT_PUBLIC_APP_URL=http://localhost:3000 +# INTERNAL_API_BASE_URL=http://sim-app.default.svc.cluster.local:3000 # Optional: internal URL for server-side /api self-calls; defaults to NEXT_PUBLIC_APP_URL # Security (Required) ENCRYPTION_KEY=your_encryption_key # Use `openssl rand -hex 32` to generate, used to encrypt environment variables diff --git a/apps/sim/app/api/a2a/serve/[agentId]/utils.ts b/apps/sim/app/api/a2a/serve/[agentId]/utils.ts index 1e8f85588..f46013343 100644 --- a/apps/sim/app/api/a2a/serve/[agentId]/utils.ts +++ b/apps/sim/app/api/a2a/serve/[agentId]/utils.ts @@ -1,7 +1,7 @@ import type { Artifact, Message, PushNotificationConfig, Task, TaskState } from '@a2a-js/sdk' import { v4 as uuidv4 } from 'uuid' import { generateInternalToken } from '@/lib/auth/internal' -import { getBaseUrl } from '@/lib/core/utils/urls' +import { getInternalApiBaseUrl } from '@/lib/core/utils/urls' /** A2A v0.3 JSON-RPC method names */ export const A2A_METHODS = { @@ -118,7 +118,7 @@ export interface ExecuteRequestResult { export async function buildExecuteRequest( config: ExecuteRequestConfig ): Promise { - const url = `${getBaseUrl()}/api/workflows/${config.workflowId}/execute` + const url = `${getInternalApiBaseUrl()}/api/workflows/${config.workflowId}/execute` const headers: Record = { 'Content-Type': 'application/json' } let useInternalAuth = false diff --git a/apps/sim/app/api/attribution/route.ts b/apps/sim/app/api/attribution/route.ts new file mode 100644 index 000000000..5fb352482 --- /dev/null +++ b/apps/sim/app/api/attribution/route.ts @@ -0,0 +1,187 @@ +/** + * POST /api/attribution + * + * Automatic UTM-based referral attribution. + * + * Reads the `sim_utm` cookie (set by proxy on auth pages), matches a campaign + * by UTM specificity, and atomically inserts an attribution record + applies + * bonus credits. + * + * Idempotent — the unique constraint on `userId` prevents double-attribution. + */ + +import { db } from '@sim/db' +import { referralAttribution, referralCampaigns, userStats } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { eq } from 'drizzle-orm' +import { nanoid } from 'nanoid' +import { cookies } from 'next/headers' +import { NextResponse } from 'next/server' +import { z } from 'zod' +import { getSession } from '@/lib/auth' +import { applyBonusCredits } from '@/lib/billing/credits/bonus' + +const logger = createLogger('AttributionAPI') + +const COOKIE_NAME = 'sim_utm' + +const UtmCookieSchema = z.object({ + utm_source: z.string().optional(), + utm_medium: z.string().optional(), + utm_campaign: z.string().optional(), + utm_content: z.string().optional(), + referrer_url: z.string().optional(), + landing_page: z.string().optional(), + created_at: z.string().optional(), +}) + +/** + * Finds the most specific active campaign matching the given UTM params. + * Null fields on a campaign act as wildcards. Ties broken by newest campaign. + */ +async function findMatchingCampaign(utmData: z.infer) { + const campaigns = await db + .select() + .from(referralCampaigns) + .where(eq(referralCampaigns.isActive, true)) + + let bestMatch: (typeof campaigns)[number] | null = null + let bestScore = -1 + + for (const campaign of campaigns) { + let score = 0 + let mismatch = false + + const fields = [ + { campaignVal: campaign.utmSource, utmVal: utmData.utm_source }, + { campaignVal: campaign.utmMedium, utmVal: utmData.utm_medium }, + { campaignVal: campaign.utmCampaign, utmVal: utmData.utm_campaign }, + { campaignVal: campaign.utmContent, utmVal: utmData.utm_content }, + ] as const + + for (const { campaignVal, utmVal } of fields) { + if (campaignVal === null) continue + if (campaignVal === utmVal) { + score++ + } else { + mismatch = true + break + } + } + + if (!mismatch && score > 0) { + if ( + score > bestScore || + (score === bestScore && + bestMatch && + campaign.createdAt.getTime() > bestMatch.createdAt.getTime()) + ) { + bestScore = score + bestMatch = campaign + } + } + } + + return bestMatch +} + +export async function POST() { + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const cookieStore = await cookies() + const utmCookie = cookieStore.get(COOKIE_NAME) + if (!utmCookie?.value) { + return NextResponse.json({ attributed: false, reason: 'no_utm_cookie' }) + } + + let utmData: z.infer + try { + let decoded: string + try { + decoded = decodeURIComponent(utmCookie.value) + } catch { + decoded = utmCookie.value + } + utmData = UtmCookieSchema.parse(JSON.parse(decoded)) + } catch { + logger.warn('Failed to parse UTM cookie', { userId: session.user.id }) + cookieStore.delete(COOKIE_NAME) + return NextResponse.json({ attributed: false, reason: 'invalid_cookie' }) + } + + const matchedCampaign = await findMatchingCampaign(utmData) + if (!matchedCampaign) { + cookieStore.delete(COOKIE_NAME) + return NextResponse.json({ attributed: false, reason: 'no_matching_campaign' }) + } + + const bonusAmount = Number(matchedCampaign.bonusCreditAmount) + + let attributed = false + await db.transaction(async (tx) => { + const [existingStats] = await tx + .select({ id: userStats.id }) + .from(userStats) + .where(eq(userStats.userId, session.user.id)) + .limit(1) + + if (!existingStats) { + await tx.insert(userStats).values({ + id: nanoid(), + userId: session.user.id, + }) + } + + const result = await tx + .insert(referralAttribution) + .values({ + id: nanoid(), + userId: session.user.id, + campaignId: matchedCampaign.id, + utmSource: utmData.utm_source || null, + utmMedium: utmData.utm_medium || null, + utmCampaign: utmData.utm_campaign || null, + utmContent: utmData.utm_content || null, + referrerUrl: utmData.referrer_url || null, + landingPage: utmData.landing_page || null, + bonusCreditAmount: bonusAmount.toString(), + }) + .onConflictDoNothing({ target: referralAttribution.userId }) + .returning({ id: referralAttribution.id }) + + if (result.length > 0) { + await applyBonusCredits(session.user.id, bonusAmount, tx) + attributed = true + } + }) + + if (attributed) { + logger.info('Referral attribution created and bonus credits applied', { + userId: session.user.id, + campaignId: matchedCampaign.id, + campaignName: matchedCampaign.name, + utmSource: utmData.utm_source, + utmCampaign: utmData.utm_campaign, + utmContent: utmData.utm_content, + bonusAmount, + }) + } else { + logger.info('User already attributed, skipping', { userId: session.user.id }) + } + + cookieStore.delete(COOKIE_NAME) + + return NextResponse.json({ + attributed, + bonusAmount: attributed ? bonusAmount : undefined, + reason: attributed ? undefined : 'already_attributed', + }) + } catch (error) { + logger.error('Attribution error', { error }) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/auth/oauth/utils.test.ts b/apps/sim/app/api/auth/oauth/utils.test.ts index ca1d2c8eb..352ba5e78 100644 --- a/apps/sim/app/api/auth/oauth/utils.test.ts +++ b/apps/sim/app/api/auth/oauth/utils.test.ts @@ -4,20 +4,10 @@ * @vitest-environment node */ -import { loggerMock } from '@sim/testing' +import { databaseMock, loggerMock } from '@sim/testing' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -vi.mock('@sim/db', () => ({ - db: { - select: vi.fn().mockReturnThis(), - from: vi.fn().mockReturnThis(), - where: vi.fn().mockReturnThis(), - limit: vi.fn().mockReturnValue([]), - update: vi.fn().mockReturnThis(), - set: vi.fn().mockReturnThis(), - orderBy: vi.fn().mockReturnThis(), - }, -})) +vi.mock('@sim/db', () => databaseMock) vi.mock('@/lib/oauth/oauth', () => ({ refreshOAuthToken: vi.fn(), @@ -34,13 +24,36 @@ import { refreshTokenIfNeeded, } from '@/app/api/auth/oauth/utils' -const mockDbTyped = db as any +const mockDb = db as any const mockRefreshOAuthToken = refreshOAuthToken as any +/** + * Creates a chainable mock for db.select() calls. + * Returns a nested chain: select() -> from() -> where() -> limit() / orderBy() + */ +function mockSelectChain(limitResult: unknown[]) { + const mockLimit = vi.fn().mockReturnValue(limitResult) + const mockOrderBy = vi.fn().mockReturnValue(limitResult) + const mockWhere = vi.fn().mockReturnValue({ limit: mockLimit, orderBy: mockOrderBy }) + const mockFrom = vi.fn().mockReturnValue({ where: mockWhere }) + mockDb.select.mockReturnValueOnce({ from: mockFrom }) + return { mockFrom, mockWhere, mockLimit } +} + +/** + * Creates a chainable mock for db.update() calls. + * Returns a nested chain: update() -> set() -> where() + */ +function mockUpdateChain() { + const mockWhere = vi.fn().mockResolvedValue({}) + const mockSet = vi.fn().mockReturnValue({ where: mockWhere }) + mockDb.update.mockReturnValueOnce({ set: mockSet }) + return { mockSet, mockWhere } +} + describe('OAuth Utils', () => { beforeEach(() => { vi.clearAllMocks() - mockDbTyped.limit.mockReturnValue([]) }) afterEach(() => { @@ -50,20 +63,20 @@ describe('OAuth Utils', () => { describe('getCredential', () => { it('should return credential when found', async () => { const mockCredential = { id: 'credential-id', userId: 'test-user-id' } - mockDbTyped.limit.mockReturnValueOnce([mockCredential]) + const { mockFrom, mockWhere, mockLimit } = mockSelectChain([mockCredential]) const credential = await getCredential('request-id', 'credential-id', 'test-user-id') - expect(mockDbTyped.select).toHaveBeenCalled() - expect(mockDbTyped.from).toHaveBeenCalled() - expect(mockDbTyped.where).toHaveBeenCalled() - expect(mockDbTyped.limit).toHaveBeenCalledWith(1) + expect(mockDb.select).toHaveBeenCalled() + expect(mockFrom).toHaveBeenCalled() + expect(mockWhere).toHaveBeenCalled() + expect(mockLimit).toHaveBeenCalledWith(1) expect(credential).toEqual(mockCredential) }) it('should return undefined when credential is not found', async () => { - mockDbTyped.limit.mockReturnValueOnce([]) + mockSelectChain([]) const credential = await getCredential('request-id', 'nonexistent-id', 'test-user-id') @@ -102,11 +115,12 @@ describe('OAuth Utils', () => { refreshToken: 'new-refresh-token', }) + mockUpdateChain() + const result = await refreshTokenIfNeeded('request-id', mockCredential, 'credential-id') expect(mockRefreshOAuthToken).toHaveBeenCalledWith('google', 'refresh-token') - expect(mockDbTyped.update).toHaveBeenCalled() - expect(mockDbTyped.set).toHaveBeenCalled() + expect(mockDb.update).toHaveBeenCalled() expect(result).toEqual({ accessToken: 'new-token', refreshed: true }) }) @@ -152,7 +166,7 @@ describe('OAuth Utils', () => { providerId: 'google', userId: 'test-user-id', } - mockDbTyped.limit.mockReturnValueOnce([mockCredential]) + mockSelectChain([mockCredential]) const token = await refreshAccessTokenIfNeeded('credential-id', 'test-user-id', 'request-id') @@ -169,7 +183,8 @@ describe('OAuth Utils', () => { providerId: 'google', userId: 'test-user-id', } - mockDbTyped.limit.mockReturnValueOnce([mockCredential]) + mockSelectChain([mockCredential]) + mockUpdateChain() mockRefreshOAuthToken.mockResolvedValueOnce({ accessToken: 'new-token', @@ -180,13 +195,12 @@ describe('OAuth Utils', () => { const token = await refreshAccessTokenIfNeeded('credential-id', 'test-user-id', 'request-id') expect(mockRefreshOAuthToken).toHaveBeenCalledWith('google', 'refresh-token') - expect(mockDbTyped.update).toHaveBeenCalled() - expect(mockDbTyped.set).toHaveBeenCalled() + expect(mockDb.update).toHaveBeenCalled() expect(token).toBe('new-token') }) it('should return null if credential not found', async () => { - mockDbTyped.limit.mockReturnValueOnce([]) + mockSelectChain([]) const token = await refreshAccessTokenIfNeeded('nonexistent-id', 'test-user-id', 'request-id') @@ -202,7 +216,7 @@ describe('OAuth Utils', () => { providerId: 'google', userId: 'test-user-id', } - mockDbTyped.limit.mockReturnValueOnce([mockCredential]) + mockSelectChain([mockCredential]) mockRefreshOAuthToken.mockResolvedValueOnce(null) diff --git a/apps/sim/app/api/copilot/chat/route.ts b/apps/sim/app/api/copilot/chat/route.ts index 25349e914..500b7e1a6 100644 --- a/apps/sim/app/api/copilot/chat/route.ts +++ b/apps/sim/app/api/copilot/chat/route.ts @@ -85,7 +85,7 @@ const ChatMessageSchema = z.object({ chatId: z.string().optional(), workflowId: z.string().optional(), workflowName: z.string().optional(), - model: z.string().optional().default('claude-opus-4-6'), + model: z.string().optional().default('claude-opus-4-5'), mode: z.enum(COPILOT_REQUEST_MODES).optional().default('agent'), prefetch: z.boolean().optional(), createNewChat: z.boolean().optional().default(false), @@ -238,7 +238,7 @@ export async function POST(req: NextRequest) { let currentChat: any = null let conversationHistory: any[] = [] let actualChatId = chatId - const selectedModel = model || 'claude-opus-4-6' + const selectedModel = model || 'claude-opus-4-5' if (chatId || createNewChat) { const chatResult = await resolveOrCreateChat({ 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 7193af66d..aa464170a 100644 --- a/apps/sim/app/api/copilot/checkpoints/revert/route.test.ts +++ b/apps/sim/app/api/copilot/checkpoints/revert/route.test.ts @@ -18,9 +18,9 @@ describe('Copilot Checkpoints Revert API Route', () => { setupCommonApiMocks() mockCryptoUuid() - // Mock getBaseUrl to return localhost for tests vi.doMock('@/lib/core/utils/urls', () => ({ getBaseUrl: vi.fn(() => 'http://localhost:3000'), + getInternalApiBaseUrl: vi.fn(() => 'http://localhost:3000'), getBaseDomain: vi.fn(() => 'localhost:3000'), getEmailDomain: vi.fn(() => 'localhost:3000'), })) diff --git a/apps/sim/app/api/copilot/checkpoints/revert/route.ts b/apps/sim/app/api/copilot/checkpoints/revert/route.ts index 72c79a262..7c58a1435 100644 --- a/apps/sim/app/api/copilot/checkpoints/revert/route.ts +++ b/apps/sim/app/api/copilot/checkpoints/revert/route.ts @@ -11,7 +11,7 @@ import { createRequestTracker, createUnauthorizedResponse, } from '@/lib/copilot/request-helpers' -import { getBaseUrl } from '@/lib/core/utils/urls' +import { getInternalApiBaseUrl } from '@/lib/core/utils/urls' import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' import { isUuidV4 } from '@/executor/constants' @@ -99,7 +99,7 @@ export async function POST(request: NextRequest) { } const stateResponse = await fetch( - `${getBaseUrl()}/api/workflows/${checkpoint.workflowId}/state`, + `${getInternalApiBaseUrl()}/api/workflows/${checkpoint.workflowId}/state`, { method: 'PUT', headers: { diff --git a/apps/sim/app/api/knowledge/search/utils.test.ts b/apps/sim/app/api/knowledge/search/utils.test.ts index 6224e046e..a3d6b3856 100644 --- a/apps/sim/app/api/knowledge/search/utils.test.ts +++ b/apps/sim/app/api/knowledge/search/utils.test.ts @@ -4,16 +4,12 @@ * * @vitest-environment node */ -import { createEnvMock, createMockLogger } from '@sim/testing' +import { createEnvMock, databaseMock, loggerMock } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' -const loggerMock = vi.hoisted(() => ({ - createLogger: () => createMockLogger(), -})) - vi.mock('drizzle-orm') vi.mock('@sim/logger', () => loggerMock) -vi.mock('@sim/db') +vi.mock('@sim/db', () => databaseMock) vi.mock('@/lib/knowledge/documents/utils', () => ({ retryWithExponentialBackoff: (fn: any) => fn(), })) diff --git a/apps/sim/app/api/mcp/copilot/route.ts b/apps/sim/app/api/mcp/copilot/route.ts index 5fcce8563..643b339f9 100644 --- a/apps/sim/app/api/mcp/copilot/route.ts +++ b/apps/sim/app/api/mcp/copilot/route.ts @@ -38,7 +38,7 @@ import { const logger = createLogger('CopilotMcpAPI') const mcpRateLimiter = new RateLimiter() -const DEFAULT_COPILOT_MODEL = 'claude-opus-4-6' +const DEFAULT_COPILOT_MODEL = 'claude-opus-4-5' export const dynamic = 'force-dynamic' export const runtime = 'nodejs' diff --git a/apps/sim/app/api/mcp/serve/[serverId]/route.test.ts b/apps/sim/app/api/mcp/serve/[serverId]/route.test.ts index 976678313..95a3f89ed 100644 --- a/apps/sim/app/api/mcp/serve/[serverId]/route.test.ts +++ b/apps/sim/app/api/mcp/serve/[serverId]/route.test.ts @@ -72,6 +72,7 @@ describe('MCP Serve Route', () => { })) vi.doMock('@/lib/core/utils/urls', () => ({ getBaseUrl: () => 'http://localhost:3000', + getInternalApiBaseUrl: () => 'http://localhost:3000', })) vi.doMock('@/lib/core/execution-limits', () => ({ getMaxExecutionTimeout: () => 10_000, diff --git a/apps/sim/app/api/mcp/serve/[serverId]/route.ts b/apps/sim/app/api/mcp/serve/[serverId]/route.ts index 92a77d8b9..1c694a59a 100644 --- a/apps/sim/app/api/mcp/serve/[serverId]/route.ts +++ b/apps/sim/app/api/mcp/serve/[serverId]/route.ts @@ -22,7 +22,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { type AuthResult, checkHybridAuth } from '@/lib/auth/hybrid' import { generateInternalToken } from '@/lib/auth/internal' import { getMaxExecutionTimeout } from '@/lib/core/execution-limits' -import { getBaseUrl } from '@/lib/core/utils/urls' +import { getInternalApiBaseUrl } from '@/lib/core/utils/urls' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' const logger = createLogger('WorkflowMcpServeAPI') @@ -285,7 +285,7 @@ async function handleToolsCall( ) } - const executeUrl = `${getBaseUrl()}/api/workflows/${tool.workflowId}/execute` + const executeUrl = `${getInternalApiBaseUrl()}/api/workflows/${tool.workflowId}/execute` const headers: Record = { 'Content-Type': 'application/json' } if (publicServerOwnerId) { diff --git a/apps/sim/app/api/referral-code/redeem/route.ts b/apps/sim/app/api/referral-code/redeem/route.ts new file mode 100644 index 000000000..be3cbac90 --- /dev/null +++ b/apps/sim/app/api/referral-code/redeem/route.ts @@ -0,0 +1,170 @@ +/** + * POST /api/referral-code/redeem + * + * Redeem a referral/promo code to receive bonus credits. + * + * Body: + * - code: string — The referral code to redeem + * + * Response: { redeemed: boolean, bonusAmount?: number, error?: string } + * + * Constraints: + * - Enterprise users cannot redeem codes + * - One redemption per user, ever (unique constraint on userId) + * - One redemption per organization for team users (partial unique on organizationId) + */ + +import { db } from '@sim/db' +import { referralAttribution, referralCampaigns, userStats } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, eq } from 'drizzle-orm' +import { nanoid } from 'nanoid' +import { NextResponse } from 'next/server' +import { z } from 'zod' +import { getSession } from '@/lib/auth' +import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription' +import { applyBonusCredits } from '@/lib/billing/credits/bonus' + +const logger = createLogger('ReferralCodeRedemption') + +const RedeemCodeSchema = z.object({ + code: z.string().min(1, 'Code is required'), +}) + +export async function POST(request: Request) { + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const { code } = RedeemCodeSchema.parse(body) + + const subscription = await getHighestPrioritySubscription(session.user.id) + + if (subscription?.plan === 'enterprise') { + return NextResponse.json({ + redeemed: false, + error: 'Enterprise accounts cannot redeem referral codes', + }) + } + + const isTeam = subscription?.plan === 'team' + const orgId = isTeam ? subscription.referenceId : null + + const normalizedCode = code.trim().toUpperCase() + + const [campaign] = await db + .select() + .from(referralCampaigns) + .where(and(eq(referralCampaigns.code, normalizedCode), eq(referralCampaigns.isActive, true))) + .limit(1) + + if (!campaign) { + logger.info('Invalid code redemption attempt', { + userId: session.user.id, + code: normalizedCode, + }) + return NextResponse.json({ error: 'Invalid or expired code' }, { status: 404 }) + } + + const [existingUserAttribution] = await db + .select({ id: referralAttribution.id }) + .from(referralAttribution) + .where(eq(referralAttribution.userId, session.user.id)) + .limit(1) + + if (existingUserAttribution) { + return NextResponse.json({ + redeemed: false, + error: 'You have already redeemed a code', + }) + } + + if (orgId) { + const [existingOrgAttribution] = await db + .select({ id: referralAttribution.id }) + .from(referralAttribution) + .where(eq(referralAttribution.organizationId, orgId)) + .limit(1) + + if (existingOrgAttribution) { + return NextResponse.json({ + redeemed: false, + error: 'A code has already been redeemed for your organization', + }) + } + } + + const bonusAmount = Number(campaign.bonusCreditAmount) + + let redeemed = false + await db.transaction(async (tx) => { + const [existingStats] = await tx + .select({ id: userStats.id }) + .from(userStats) + .where(eq(userStats.userId, session.user.id)) + .limit(1) + + if (!existingStats) { + await tx.insert(userStats).values({ + id: nanoid(), + userId: session.user.id, + }) + } + + const result = await tx + .insert(referralAttribution) + .values({ + id: nanoid(), + userId: session.user.id, + organizationId: orgId, + campaignId: campaign.id, + utmSource: null, + utmMedium: null, + utmCampaign: null, + utmContent: null, + referrerUrl: null, + landingPage: null, + bonusCreditAmount: bonusAmount.toString(), + }) + .onConflictDoNothing() + .returning({ id: referralAttribution.id }) + + if (result.length > 0) { + await applyBonusCredits(session.user.id, bonusAmount, tx) + redeemed = true + } + }) + + if (redeemed) { + logger.info('Referral code redeemed', { + userId: session.user.id, + organizationId: orgId, + code: normalizedCode, + campaignId: campaign.id, + campaignName: campaign.name, + bonusAmount, + }) + } + + if (!redeemed) { + return NextResponse.json({ + redeemed: false, + error: 'You have already redeemed a code', + }) + } + + return NextResponse.json({ + redeemed: true, + bonusAmount, + }) + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json({ error: error.errors[0].message }, { status: 400 }) + } + logger.error('Referral code redemption error', { error }) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/schedules/[id]/route.test.ts b/apps/sim/app/api/schedules/[id]/route.test.ts index 012f327d1..f33ed5a24 100644 --- a/apps/sim/app/api/schedules/[id]/route.test.ts +++ b/apps/sim/app/api/schedules/[id]/route.test.ts @@ -3,17 +3,14 @@ * * @vitest-environment node */ -import { loggerMock } from '@sim/testing' +import { databaseMock, loggerMock } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -const { mockGetSession, mockAuthorizeWorkflowByWorkspacePermission, mockDbSelect, mockDbUpdate } = - vi.hoisted(() => ({ - mockGetSession: vi.fn(), - mockAuthorizeWorkflowByWorkspacePermission: vi.fn(), - mockDbSelect: vi.fn(), - mockDbUpdate: vi.fn(), - })) +const { mockGetSession, mockAuthorizeWorkflowByWorkspacePermission } = vi.hoisted(() => ({ + mockGetSession: vi.fn(), + mockAuthorizeWorkflowByWorkspacePermission: vi.fn(), +})) vi.mock('@/lib/auth', () => ({ getSession: mockGetSession, @@ -23,12 +20,7 @@ vi.mock('@/lib/workflows/utils', () => ({ authorizeWorkflowByWorkspacePermission: mockAuthorizeWorkflowByWorkspacePermission, })) -vi.mock('@sim/db', () => ({ - db: { - select: mockDbSelect, - update: mockDbUpdate, - }, -})) +vi.mock('@sim/db', () => databaseMock) vi.mock('@sim/db/schema', () => ({ workflow: { id: 'id', userId: 'userId', workspaceId: 'workspaceId' }, @@ -59,6 +51,9 @@ function createParams(id: string): { params: Promise<{ id: string }> } { return { params: Promise.resolve({ id }) } } +const mockDbSelect = databaseMock.db.select as ReturnType +const mockDbUpdate = databaseMock.db.update as ReturnType + function mockDbChain(selectResults: unknown[][]) { let selectCallIndex = 0 mockDbSelect.mockImplementation(() => ({ diff --git a/apps/sim/app/api/schedules/route.test.ts b/apps/sim/app/api/schedules/route.test.ts index e6320b2b6..9d1530d50 100644 --- a/apps/sim/app/api/schedules/route.test.ts +++ b/apps/sim/app/api/schedules/route.test.ts @@ -3,17 +3,14 @@ * * @vitest-environment node */ -import { loggerMock } from '@sim/testing' +import { databaseMock, loggerMock } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -const { mockGetSession, mockAuthorizeWorkflowByWorkspacePermission, mockDbSelect } = vi.hoisted( - () => ({ - mockGetSession: vi.fn(), - mockAuthorizeWorkflowByWorkspacePermission: vi.fn(), - mockDbSelect: vi.fn(), - }) -) +const { mockGetSession, mockAuthorizeWorkflowByWorkspacePermission } = vi.hoisted(() => ({ + mockGetSession: vi.fn(), + mockAuthorizeWorkflowByWorkspacePermission: vi.fn(), +})) vi.mock('@/lib/auth', () => ({ getSession: mockGetSession, @@ -23,11 +20,7 @@ vi.mock('@/lib/workflows/utils', () => ({ authorizeWorkflowByWorkspacePermission: mockAuthorizeWorkflowByWorkspacePermission, })) -vi.mock('@sim/db', () => ({ - db: { - select: mockDbSelect, - }, -})) +vi.mock('@sim/db', () => databaseMock) vi.mock('@sim/db/schema', () => ({ workflow: { id: 'id', userId: 'userId', workspaceId: 'workspaceId' }, @@ -62,6 +55,8 @@ function createRequest(url: string): NextRequest { return new NextRequest(new URL(url), { method: 'GET' }) } +const mockDbSelect = databaseMock.db.select as ReturnType + function mockDbChain(results: any[]) { let callIndex = 0 mockDbSelect.mockImplementation(() => ({ diff --git a/apps/sim/app/api/templates/[id]/use/route.ts b/apps/sim/app/api/templates/[id]/use/route.ts index 59c546687..b08d6dfb8 100644 --- a/apps/sim/app/api/templates/[id]/use/route.ts +++ b/apps/sim/app/api/templates/[id]/use/route.ts @@ -6,7 +6,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { v4 as uuidv4 } from 'uuid' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' -import { getBaseUrl } from '@/lib/core/utils/urls' +import { getInternalApiBaseUrl } from '@/lib/core/utils/urls' import { type RegenerateStateInput, regenerateWorkflowStateIds, @@ -115,15 +115,18 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ // Step 3: Save the workflow state using the existing state endpoint (like imports do) // Ensure variables in state are remapped for the new workflow as well const workflowStateWithVariables = { ...workflowState, variables: remappedVariables } - const stateResponse = await fetch(`${getBaseUrl()}/api/workflows/${newWorkflowId}/state`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - // Forward the session cookie for authentication - cookie: request.headers.get('cookie') || '', - }, - body: JSON.stringify(workflowStateWithVariables), - }) + const stateResponse = await fetch( + `${getInternalApiBaseUrl()}/api/workflows/${newWorkflowId}/state`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + // Forward the session cookie for authentication + cookie: request.headers.get('cookie') || '', + }, + body: JSON.stringify(workflowStateWithVariables), + } + ) if (!stateResponse.ok) { logger.error(`[${requestId}] Failed to save workflow state for template use`) diff --git a/apps/sim/app/api/v1/admin/index.ts b/apps/sim/app/api/v1/admin/index.ts index fe6006fad..a22b8e4f3 100644 --- a/apps/sim/app/api/v1/admin/index.ts +++ b/apps/sim/app/api/v1/admin/index.ts @@ -66,6 +66,12 @@ * Credits: * POST /api/v1/admin/credits - Issue credits to user (by userId or email) * + * Referral Campaigns: + * GET /api/v1/admin/referral-campaigns - List campaigns (?active=true/false) + * POST /api/v1/admin/referral-campaigns - Create campaign + * GET /api/v1/admin/referral-campaigns/:id - Get campaign details + * PATCH /api/v1/admin/referral-campaigns/:id - Update campaign fields + * * Access Control (Permission Groups): * GET /api/v1/admin/access-control - List permission groups (?organizationId=X) * DELETE /api/v1/admin/access-control - Delete permission groups for org (?organizationId=X) @@ -97,6 +103,7 @@ export type { AdminOrganization, AdminOrganizationBillingSummary, AdminOrganizationDetail, + AdminReferralCampaign, AdminSeatAnalytics, AdminSingleResponse, AdminSubscription, @@ -111,6 +118,7 @@ export type { AdminWorkspaceMember, DbMember, DbOrganization, + DbReferralCampaign, DbSubscription, DbUser, DbUserStats, @@ -139,6 +147,7 @@ export { parseWorkflowVariables, toAdminFolder, toAdminOrganization, + toAdminReferralCampaign, toAdminSubscription, toAdminUser, toAdminWorkflow, diff --git a/apps/sim/app/api/v1/admin/referral-campaigns/[id]/route.ts b/apps/sim/app/api/v1/admin/referral-campaigns/[id]/route.ts new file mode 100644 index 000000000..45f0a230a --- /dev/null +++ b/apps/sim/app/api/v1/admin/referral-campaigns/[id]/route.ts @@ -0,0 +1,142 @@ +/** + * GET /api/v1/admin/referral-campaigns/:id + * + * Get a single referral campaign by ID. + * + * PATCH /api/v1/admin/referral-campaigns/:id + * + * Update campaign fields. All fields are optional. + * + * Body: + * - name: string (non-empty) - Campaign name + * - bonusCreditAmount: number (> 0) - Bonus credits in dollars + * - isActive: boolean - Enable/disable the campaign + * - code: string | null (min 6 chars, auto-uppercased, null to remove) - Redeemable code + * - utmSource: string | null - UTM source match (null = wildcard) + * - utmMedium: string | null - UTM medium match (null = wildcard) + * - utmCampaign: string | null - UTM campaign match (null = wildcard) + * - utmContent: string | null - UTM content match (null = wildcard) + */ + +import { db } from '@sim/db' +import { referralCampaigns } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { eq } from 'drizzle-orm' +import { getBaseUrl } from '@/lib/core/utils/urls' +import { withAdminAuthParams } from '@/app/api/v1/admin/middleware' +import { + badRequestResponse, + internalErrorResponse, + notFoundResponse, + singleResponse, +} from '@/app/api/v1/admin/responses' +import { toAdminReferralCampaign } from '@/app/api/v1/admin/types' + +const logger = createLogger('AdminReferralCampaignDetailAPI') + +interface RouteParams { + id: string +} + +export const GET = withAdminAuthParams(async (_, context) => { + try { + const { id: campaignId } = await context.params + + const [campaign] = await db + .select() + .from(referralCampaigns) + .where(eq(referralCampaigns.id, campaignId)) + .limit(1) + + if (!campaign) { + return notFoundResponse('Campaign') + } + + logger.info(`Admin API: Retrieved referral campaign ${campaignId}`) + + return singleResponse(toAdminReferralCampaign(campaign, getBaseUrl())) + } catch (error) { + logger.error('Admin API: Failed to get referral campaign', { error }) + return internalErrorResponse('Failed to get referral campaign') + } +}) + +export const PATCH = withAdminAuthParams(async (request, context) => { + try { + const { id: campaignId } = await context.params + const body = await request.json() + + const [existing] = await db + .select() + .from(referralCampaigns) + .where(eq(referralCampaigns.id, campaignId)) + .limit(1) + + if (!existing) { + return notFoundResponse('Campaign') + } + + const updateData: Record = { updatedAt: new Date() } + + if (body.name !== undefined) { + if (typeof body.name !== 'string' || body.name.trim().length === 0) { + return badRequestResponse('name must be a non-empty string') + } + updateData.name = body.name.trim() + } + + if (body.bonusCreditAmount !== undefined) { + if ( + typeof body.bonusCreditAmount !== 'number' || + !Number.isFinite(body.bonusCreditAmount) || + body.bonusCreditAmount <= 0 + ) { + return badRequestResponse('bonusCreditAmount must be a positive number') + } + updateData.bonusCreditAmount = body.bonusCreditAmount.toString() + } + + if (body.isActive !== undefined) { + if (typeof body.isActive !== 'boolean') { + return badRequestResponse('isActive must be a boolean') + } + updateData.isActive = body.isActive + } + + if (body.code !== undefined) { + if (body.code !== null) { + if (typeof body.code !== 'string') { + return badRequestResponse('code must be a string or null') + } + if (body.code.trim().length < 6) { + return badRequestResponse('code must be at least 6 characters') + } + } + updateData.code = body.code ? body.code.trim().toUpperCase() : null + } + + for (const field of ['utmSource', 'utmMedium', 'utmCampaign', 'utmContent'] as const) { + if (body[field] !== undefined) { + if (body[field] !== null && typeof body[field] !== 'string') { + return badRequestResponse(`${field} must be a string or null`) + } + updateData[field] = body[field] || null + } + } + + const [updated] = await db + .update(referralCampaigns) + .set(updateData) + .where(eq(referralCampaigns.id, campaignId)) + .returning() + + logger.info(`Admin API: Updated referral campaign ${campaignId}`, { + fields: Object.keys(updateData).filter((k) => k !== 'updatedAt'), + }) + + return singleResponse(toAdminReferralCampaign(updated, getBaseUrl())) + } catch (error) { + logger.error('Admin API: Failed to update referral campaign', { error }) + return internalErrorResponse('Failed to update referral campaign') + } +}) diff --git a/apps/sim/app/api/v1/admin/referral-campaigns/route.ts b/apps/sim/app/api/v1/admin/referral-campaigns/route.ts new file mode 100644 index 000000000..64b711eeb --- /dev/null +++ b/apps/sim/app/api/v1/admin/referral-campaigns/route.ts @@ -0,0 +1,140 @@ +/** + * GET /api/v1/admin/referral-campaigns + * + * List referral campaigns with optional filtering and pagination. + * + * Query Parameters: + * - active: string (optional) - Filter by active status ('true' or 'false') + * - limit: number (default: 50, max: 250) + * - offset: number (default: 0) + * + * POST /api/v1/admin/referral-campaigns + * + * Create a new referral campaign. + * + * Body: + * - name: string (required) - Campaign name + * - bonusCreditAmount: number (required, > 0) - Bonus credits in dollars + * - code: string | null (optional, min 6 chars, auto-uppercased) - Redeemable code + * - utmSource: string | null (optional) - UTM source match (null = wildcard) + * - utmMedium: string | null (optional) - UTM medium match (null = wildcard) + * - utmCampaign: string | null (optional) - UTM campaign match (null = wildcard) + * - utmContent: string | null (optional) - UTM content match (null = wildcard) + */ + +import { db } from '@sim/db' +import { referralCampaigns } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { count, eq, type SQL } from 'drizzle-orm' +import { nanoid } from 'nanoid' +import { getBaseUrl } from '@/lib/core/utils/urls' +import { withAdminAuth } from '@/app/api/v1/admin/middleware' +import { + badRequestResponse, + internalErrorResponse, + listResponse, + singleResponse, +} from '@/app/api/v1/admin/responses' +import { + type AdminReferralCampaign, + createPaginationMeta, + parsePaginationParams, + toAdminReferralCampaign, +} from '@/app/api/v1/admin/types' + +const logger = createLogger('AdminReferralCampaignsAPI') + +export const GET = withAdminAuth(async (request) => { + const url = new URL(request.url) + const { limit, offset } = parsePaginationParams(url) + const activeFilter = url.searchParams.get('active') + + try { + const conditions: SQL[] = [] + if (activeFilter === 'true') { + conditions.push(eq(referralCampaigns.isActive, true)) + } else if (activeFilter === 'false') { + conditions.push(eq(referralCampaigns.isActive, false)) + } + + const whereClause = conditions.length > 0 ? conditions[0] : undefined + const baseUrl = getBaseUrl() + + const [countResult, campaigns] = await Promise.all([ + db.select({ total: count() }).from(referralCampaigns).where(whereClause), + db + .select() + .from(referralCampaigns) + .where(whereClause) + .orderBy(referralCampaigns.createdAt) + .limit(limit) + .offset(offset), + ]) + + const total = countResult[0].total + const data: AdminReferralCampaign[] = campaigns.map((c) => toAdminReferralCampaign(c, baseUrl)) + const pagination = createPaginationMeta(total, limit, offset) + + logger.info(`Admin API: Listed ${data.length} referral campaigns (total: ${total})`) + + return listResponse(data, pagination) + } catch (error) { + logger.error('Admin API: Failed to list referral campaigns', { error }) + return internalErrorResponse('Failed to list referral campaigns') + } +}) + +export const POST = withAdminAuth(async (request) => { + try { + const body = await request.json() + const { name, code, utmSource, utmMedium, utmCampaign, utmContent, bonusCreditAmount } = body + + if (!name || typeof name !== 'string') { + return badRequestResponse('name is required and must be a string') + } + + if ( + typeof bonusCreditAmount !== 'number' || + !Number.isFinite(bonusCreditAmount) || + bonusCreditAmount <= 0 + ) { + return badRequestResponse('bonusCreditAmount must be a positive number') + } + + if (code !== undefined && code !== null) { + if (typeof code !== 'string') { + return badRequestResponse('code must be a string or null') + } + if (code.trim().length < 6) { + return badRequestResponse('code must be at least 6 characters') + } + } + + const id = nanoid() + + const [campaign] = await db + .insert(referralCampaigns) + .values({ + id, + name, + code: code ? code.trim().toUpperCase() : null, + utmSource: utmSource || null, + utmMedium: utmMedium || null, + utmCampaign: utmCampaign || null, + utmContent: utmContent || null, + bonusCreditAmount: bonusCreditAmount.toString(), + }) + .returning() + + logger.info(`Admin API: Created referral campaign ${id}`, { + name, + code: campaign.code, + bonusCreditAmount, + }) + + return singleResponse(toAdminReferralCampaign(campaign, getBaseUrl())) + } catch (error) { + logger.error('Admin API: Failed to create referral campaign', { error }) + return internalErrorResponse('Failed to create referral campaign') + } +}) diff --git a/apps/sim/app/api/v1/admin/types.ts b/apps/sim/app/api/v1/admin/types.ts index 615e02d78..d7ec4f5c3 100644 --- a/apps/sim/app/api/v1/admin/types.ts +++ b/apps/sim/app/api/v1/admin/types.ts @@ -8,6 +8,7 @@ import type { member, organization, + referralCampaigns, subscription, user, userStats, @@ -31,6 +32,7 @@ export type DbOrganization = InferSelectModel export type DbSubscription = InferSelectModel export type DbMember = InferSelectModel export type DbUserStats = InferSelectModel +export type DbReferralCampaign = InferSelectModel // ============================================================================= // Pagination @@ -646,3 +648,49 @@ export interface AdminDeployResult { export interface AdminUndeployResult { isDeployed: boolean } + +// ============================================================================= +// Referral Campaign Types +// ============================================================================= + +export interface AdminReferralCampaign { + id: string + name: string + code: string | null + utmSource: string | null + utmMedium: string | null + utmCampaign: string | null + utmContent: string | null + bonusCreditAmount: string + isActive: boolean + signupUrl: string | null + createdAt: string + updatedAt: string +} + +export function toAdminReferralCampaign( + dbCampaign: DbReferralCampaign, + baseUrl: string +): AdminReferralCampaign { + const utmParams = new URLSearchParams() + if (dbCampaign.utmSource) utmParams.set('utm_source', dbCampaign.utmSource) + if (dbCampaign.utmMedium) utmParams.set('utm_medium', dbCampaign.utmMedium) + if (dbCampaign.utmCampaign) utmParams.set('utm_campaign', dbCampaign.utmCampaign) + if (dbCampaign.utmContent) utmParams.set('utm_content', dbCampaign.utmContent) + const query = utmParams.toString() + + return { + id: dbCampaign.id, + name: dbCampaign.name, + code: dbCampaign.code, + utmSource: dbCampaign.utmSource, + utmMedium: dbCampaign.utmMedium, + utmCampaign: dbCampaign.utmCampaign, + utmContent: dbCampaign.utmContent, + bonusCreditAmount: dbCampaign.bonusCreditAmount, + isActive: dbCampaign.isActive, + signupUrl: query ? `${baseUrl}/signup?${query}` : null, + createdAt: dbCampaign.createdAt.toISOString(), + updatedAt: dbCampaign.updatedAt.toISOString(), + } +} diff --git a/apps/sim/app/api/v1/copilot/chat/route.ts b/apps/sim/app/api/v1/copilot/chat/route.ts index 9a71ee54b..6a3817385 100644 --- a/apps/sim/app/api/v1/copilot/chat/route.ts +++ b/apps/sim/app/api/v1/copilot/chat/route.ts @@ -8,7 +8,7 @@ import { resolveWorkflowIdForUser } from '@/lib/workflows/utils' import { authenticateV1Request } from '@/app/api/v1/auth' const logger = createLogger('CopilotHeadlessAPI') -const DEFAULT_COPILOT_MODEL = 'claude-opus-4-6' +const DEFAULT_COPILOT_MODEL = 'claude-opus-4-5' const RequestSchema = z.object({ message: z.string().min(1, 'message is required'), diff --git a/apps/sim/app/api/workflows/[id]/deployments/[version]/route.ts b/apps/sim/app/api/workflows/[id]/deployments/[version]/route.ts index 74194eba6..3af21e758 100644 --- a/apps/sim/app/api/workflows/[id]/deployments/[version]/route.ts +++ b/apps/sim/app/api/workflows/[id]/deployments/[version]/route.ts @@ -29,7 +29,7 @@ const patchBodySchema = z description: z .string() .trim() - .max(500, 'Description must be 500 characters or less') + .max(2000, 'Description must be 2000 characters or less') .nullable() .optional(), isActive: z.literal(true).optional(), // Set to true to activate this version diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index 13fc0ff41..b6ed6bd8b 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -12,7 +12,7 @@ import { import { generateRequestId } from '@/lib/core/utils/request' import { SSE_HEADERS } from '@/lib/core/utils/sse' import { getBaseUrl } from '@/lib/core/utils/urls' -import { markExecutionCancelled } from '@/lib/execution/cancellation' +import { createExecutionEventWriter, setExecutionMeta } from '@/lib/execution/event-buffer' import { processInputFileFields } from '@/lib/execution/files' import { preprocessExecution } from '@/lib/execution/preprocessing' import { LoggingSession } from '@/lib/logs/execution/logging-session' @@ -700,15 +700,27 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: const timeoutController = createTimeoutAbortController(preprocessResult.executionTimeout?.sync) let isStreamClosed = false + const eventWriter = createExecutionEventWriter(executionId) + setExecutionMeta(executionId, { + status: 'active', + userId: actorUserId, + workflowId, + }).catch(() => {}) + const stream = new ReadableStream({ async start(controller) { - const sendEvent = (event: ExecutionEvent) => { - if (isStreamClosed) return + let finalMetaStatus: 'complete' | 'error' | 'cancelled' | null = null - try { - controller.enqueue(encodeSSEEvent(event)) - } catch { - isStreamClosed = true + const sendEvent = (event: ExecutionEvent) => { + if (!isStreamClosed) { + try { + controller.enqueue(encodeSSEEvent(event)) + } catch { + isStreamClosed = true + } + } + if (event.type !== 'stream:chunk' && event.type !== 'stream:done') { + eventWriter.write(event).catch(() => {}) } } @@ -829,14 +841,12 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: const reader = streamingExec.stream.getReader() const decoder = new TextDecoder() - let chunkCount = 0 try { while (true) { const { done, value } = await reader.read() if (done) break - chunkCount++ const chunk = decoder.decode(value, { stream: true }) sendEvent({ type: 'stream:chunk', @@ -951,6 +961,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: duration: result.metadata?.duration || 0, }, }) + finalMetaStatus = 'error' } else { logger.info(`[${requestId}] Workflow execution was cancelled`) @@ -963,6 +974,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: duration: result.metadata?.duration || 0, }, }) + finalMetaStatus = 'cancelled' } return } @@ -986,6 +998,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: endTime: result.metadata?.endTime || new Date().toISOString(), }, }) + finalMetaStatus = 'complete' } catch (error: unknown) { const isTimeout = isTimeoutError(error) || timeoutController.isTimedOut() const errorMessage = isTimeout @@ -1017,7 +1030,18 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: duration: executionResult?.metadata?.duration || 0, }, }) + finalMetaStatus = 'error' } finally { + try { + await eventWriter.close() + } catch (closeError) { + logger.warn(`[${requestId}] Failed to close event writer`, { + error: closeError instanceof Error ? closeError.message : String(closeError), + }) + } + if (finalMetaStatus) { + setExecutionMeta(executionId, { status: finalMetaStatus }).catch(() => {}) + } timeoutController.cleanup() if (executionId) { await cleanupExecutionBase64Cache(executionId) @@ -1032,10 +1056,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: }, cancel() { isStreamClosed = true - timeoutController.cleanup() - logger.info(`[${requestId}] Client aborted SSE stream, signalling cancellation`) - timeoutController.abort() - markExecutionCancelled(executionId).catch(() => {}) + logger.info(`[${requestId}] Client disconnected from SSE stream`) }, }) diff --git a/apps/sim/app/api/workflows/[id]/executions/[executionId]/stream/route.ts b/apps/sim/app/api/workflows/[id]/executions/[executionId]/stream/route.ts new file mode 100644 index 000000000..1f77ff391 --- /dev/null +++ b/apps/sim/app/api/workflows/[id]/executions/[executionId]/stream/route.ts @@ -0,0 +1,170 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { checkHybridAuth } from '@/lib/auth/hybrid' +import { SSE_HEADERS } from '@/lib/core/utils/sse' +import { + type ExecutionStreamStatus, + getExecutionMeta, + readExecutionEvents, +} from '@/lib/execution/event-buffer' +import { formatSSEEvent } from '@/lib/workflows/executor/execution-events' +import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' + +const logger = createLogger('ExecutionStreamReconnectAPI') + +const POLL_INTERVAL_MS = 500 +const MAX_POLL_DURATION_MS = 10 * 60 * 1000 // 10 minutes + +function isTerminalStatus(status: ExecutionStreamStatus): boolean { + return status === 'complete' || status === 'error' || status === 'cancelled' +} + +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export async function GET( + req: NextRequest, + { params }: { params: Promise<{ id: string; executionId: string }> } +) { + const { id: workflowId, executionId } = await params + + try { + const auth = await checkHybridAuth(req, { requireWorkflowId: false }) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const workflowAuthorization = await authorizeWorkflowByWorkspacePermission({ + workflowId, + userId: auth.userId, + action: 'read', + }) + if (!workflowAuthorization.allowed) { + return NextResponse.json( + { error: workflowAuthorization.message || 'Access denied' }, + { status: workflowAuthorization.status } + ) + } + + const meta = await getExecutionMeta(executionId) + if (!meta) { + return NextResponse.json({ error: 'Execution buffer not found or expired' }, { status: 404 }) + } + + if (meta.workflowId && meta.workflowId !== workflowId) { + return NextResponse.json( + { error: 'Execution does not belong to this workflow' }, + { status: 403 } + ) + } + + const fromParam = req.nextUrl.searchParams.get('from') + const parsed = fromParam ? Number.parseInt(fromParam, 10) : 0 + const fromEventId = Number.isFinite(parsed) && parsed >= 0 ? parsed : 0 + + logger.info('Reconnection stream requested', { + workflowId, + executionId, + fromEventId, + metaStatus: meta.status, + }) + + const encoder = new TextEncoder() + + let closed = false + + const stream = new ReadableStream({ + async start(controller) { + let lastEventId = fromEventId + const pollDeadline = Date.now() + MAX_POLL_DURATION_MS + + const enqueue = (text: string) => { + if (closed) return + try { + controller.enqueue(encoder.encode(text)) + } catch { + closed = true + } + } + + try { + const events = await readExecutionEvents(executionId, lastEventId) + for (const entry of events) { + if (closed) return + enqueue(formatSSEEvent(entry.event)) + lastEventId = entry.eventId + } + + const currentMeta = await getExecutionMeta(executionId) + if (!currentMeta || isTerminalStatus(currentMeta.status)) { + enqueue('data: [DONE]\n\n') + if (!closed) controller.close() + return + } + + while (!closed && Date.now() < pollDeadline) { + await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)) + if (closed) return + + const newEvents = await readExecutionEvents(executionId, lastEventId) + for (const entry of newEvents) { + if (closed) return + enqueue(formatSSEEvent(entry.event)) + lastEventId = entry.eventId + } + + const polledMeta = await getExecutionMeta(executionId) + if (!polledMeta || isTerminalStatus(polledMeta.status)) { + const finalEvents = await readExecutionEvents(executionId, lastEventId) + for (const entry of finalEvents) { + if (closed) return + enqueue(formatSSEEvent(entry.event)) + lastEventId = entry.eventId + } + enqueue('data: [DONE]\n\n') + if (!closed) controller.close() + return + } + } + + if (!closed) { + logger.warn('Reconnection stream poll deadline reached', { executionId }) + enqueue('data: [DONE]\n\n') + controller.close() + } + } catch (error) { + logger.error('Error in reconnection stream', { + executionId, + error: error instanceof Error ? error.message : String(error), + }) + if (!closed) { + try { + controller.close() + } catch {} + } + } + }, + cancel() { + closed = true + logger.info('Client disconnected from reconnection stream', { executionId }) + }, + }) + + return new NextResponse(stream, { + headers: { + ...SSE_HEADERS, + 'X-Execution-Id': executionId, + }, + }) + } catch (error: any) { + logger.error('Failed to start reconnection stream', { + workflowId, + executionId, + error: error.message, + }) + return NextResponse.json( + { error: error.message || 'Failed to start reconnection stream' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/workflows/[id]/route.test.ts b/apps/sim/app/api/workflows/[id]/route.test.ts index 7012453b1..62b3d0437 100644 --- a/apps/sim/app/api/workflows/[id]/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/route.test.ts @@ -5,7 +5,7 @@ * @vitest-environment node */ -import { loggerMock } from '@sim/testing' +import { loggerMock, setupGlobalFetchMock } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -284,9 +284,7 @@ describe('Workflow By ID API Route', () => { where: vi.fn().mockResolvedValue([{ id: 'workflow-123' }]), }) - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - }) + setupGlobalFetchMock({ ok: true }) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', { method: 'DELETE', @@ -331,9 +329,7 @@ describe('Workflow By ID API Route', () => { where: vi.fn().mockResolvedValue([{ id: 'workflow-123' }]), }) - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - }) + setupGlobalFetchMock({ ok: true }) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', { method: 'DELETE', diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/version-description-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/version-description-modal.tsx index 3cf5106ea..63606c56a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/version-description-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/version-description-modal.tsx @@ -113,7 +113,7 @@ export function VersionDescriptionModal({ className='min-h-[120px] resize-none' value={description} onChange={(e) => setDescription(e.target.value)} - maxLength={500} + maxLength={2000} disabled={isGenerating} />
@@ -123,7 +123,7 @@ export function VersionDescriptionModal({

)} {!updateMutation.error && !generateMutation.error &&
} -

{description.length}/500

+

{description.length}/2000

diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-change-detection.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-change-detection.ts index ed0bb66f6..529a4e2f9 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-change-detection.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-change-detection.ts @@ -57,6 +57,21 @@ export function useChangeDetection({ } } + if (block.triggerMode) { + const triggerConfigValue = blockSubValues?.triggerConfig + if ( + triggerConfigValue && + typeof triggerConfigValue === 'object' && + !subBlocks.triggerConfig + ) { + subBlocks.triggerConfig = { + id: 'triggerConfig', + type: 'short-input', + value: triggerConfigValue, + } + } + } + blocksWithSubBlocks[blockId] = { ...block, subBlocks, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-deployment.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-deployment.ts index 1f2a350d8..b6a5d585e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-deployment.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-deployment.ts @@ -1,7 +1,10 @@ import { useCallback, useState } from 'react' import { createLogger } from '@sim/logger' +import { runPreDeployChecks } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-predeploy-checks' import { useNotificationStore } from '@/stores/notifications' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' +import { mergeSubblockState } from '@/stores/workflows/utils' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' const logger = createLogger('useDeployment') @@ -35,6 +38,24 @@ export function useDeployment({ return { success: true, shouldOpenModal: true } } + const { blocks, edges, loops, parallels } = useWorkflowStore.getState() + const liveBlocks = mergeSubblockState(blocks, workflowId) + const checkResult = runPreDeployChecks({ + blocks: liveBlocks, + edges, + loops, + parallels, + workflowId, + }) + if (!checkResult.passed) { + addNotification({ + level: 'error', + message: checkResult.error || 'Pre-deploy validation failed', + workflowId, + }) + return { success: false, shouldOpenModal: false } + } + setIsDeploying(true) try { const response = await fetch(`/api/workflows/${workflowId}/deploy`, { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-credential-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/credential-selector.tsx similarity index 96% rename from apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-credential-selector.tsx rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/credential-selector.tsx index 0496489d4..255d85907 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-credential-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/credential-selector.tsx @@ -4,6 +4,7 @@ import { Button, Combobox } from '@/components/emcn/components' import { getCanonicalScopesForProvider, getProviderIdFromServiceId, + getServiceConfigByProviderId, OAUTH_PROVIDERS, type OAuthProvider, type OAuthService, @@ -26,6 +27,11 @@ const getProviderIcon = (providerName: OAuthProvider) => { } const getProviderName = (providerName: OAuthProvider) => { + const serviceConfig = getServiceConfigByProviderId(providerName) + if (serviceConfig) { + return serviceConfig.name + } + const { baseProvider } = parseProvider(providerName) const baseProviderConfig = OAUTH_PROVIDERS[baseProvider] @@ -54,7 +60,7 @@ export function ToolCredentialSelector({ onChange, provider, requiredScopes = [], - label = 'Select account', + label, serviceId, disabled = false, }: ToolCredentialSelectorProps) { @@ -64,6 +70,7 @@ export function ToolCredentialSelector({ const { activeWorkflowId } = useWorkflowRegistry() const selectedId = value || '' + const effectiveLabel = label || `Select ${getProviderName(provider)} account` const effectiveProviderId = useMemo(() => getProviderIdFromServiceId(serviceId), [serviceId]) @@ -203,7 +210,7 @@ export function ToolCredentialSelector({ selectedValue={selectedId} onChange={handleComboboxChange} onOpenChange={handleOpenChange} - placeholder={label} + placeholder={effectiveLabel} disabled={disabled} editable={true} filterOptions={!isForeign} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/parameter.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/parameter.tsx new file mode 100644 index 000000000..d69ad776b --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/parameter.tsx @@ -0,0 +1,186 @@ +'use client' + +import type React from 'react' +import { useRef, useState } from 'react' +import { ArrowLeftRight, ArrowUp } from 'lucide-react' +import { Button, Input, Label, Tooltip } from '@/components/emcn' +import { cn } from '@/lib/core/utils/cn' +import type { WandControlHandlers } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block' + +/** + * Props for a generic parameter with label component + */ +export interface ParameterWithLabelProps { + paramId: string + title: string + isRequired: boolean + visibility: string + wandConfig?: { + enabled: boolean + prompt?: string + placeholder?: string + } + canonicalToggle?: { + mode: 'basic' | 'advanced' + disabled?: boolean + onToggle?: () => void + } + disabled: boolean + isPreview: boolean + children: (wandControlRef: React.MutableRefObject) => React.ReactNode +} + +/** + * Generic wrapper component for parameters that manages wand state and renders label + input + */ +export function ParameterWithLabel({ + paramId, + title, + isRequired, + visibility, + wandConfig, + canonicalToggle, + disabled, + isPreview, + children, +}: ParameterWithLabelProps) { + const [isSearchActive, setIsSearchActive] = useState(false) + const [searchQuery, setSearchQuery] = useState('') + const searchInputRef = useRef(null) + const wandControlRef = useRef(null) + + const isWandEnabled = wandConfig?.enabled ?? false + const showWand = isWandEnabled && !isPreview && !disabled + + const handleSearchClick = (): void => { + setIsSearchActive(true) + setTimeout(() => { + searchInputRef.current?.focus() + }, 0) + } + + const handleSearchBlur = (): void => { + if (!searchQuery.trim() && !wandControlRef.current?.isWandStreaming) { + setIsSearchActive(false) + } + } + + const handleSearchChange = (value: string): void => { + setSearchQuery(value) + } + + const handleSearchSubmit = (): void => { + if (searchQuery.trim() && wandControlRef.current) { + wandControlRef.current.onWandTrigger(searchQuery) + setSearchQuery('') + setIsSearchActive(false) + } + } + + const handleSearchCancel = (): void => { + setSearchQuery('') + setIsSearchActive(false) + } + + const isStreaming = wandControlRef.current?.isWandStreaming ?? false + + return ( +
+
+ +
+ {showWand && + (!isSearchActive ? ( + + ) : ( +
+ ) => + handleSearchChange(e.target.value) + } + onBlur={(e: React.FocusEvent) => { + const relatedTarget = e.relatedTarget as HTMLElement | null + if (relatedTarget?.closest('button')) return + handleSearchBlur() + }} + onKeyDown={(e: React.KeyboardEvent) => { + if (e.key === 'Enter' && searchQuery.trim() && !isStreaming) { + handleSearchSubmit() + } else if (e.key === 'Escape') { + handleSearchCancel() + } + }} + disabled={isStreaming} + className={cn( + 'h-5 min-w-[80px] flex-1 text-[11px]', + isStreaming && 'text-muted-foreground' + )} + placeholder='Generate with AI...' + /> + +
+ ))} + {canonicalToggle && !isPreview && ( + + + + + +

+ {canonicalToggle.mode === 'advanced' + ? 'Switch to selector' + : 'Switch to manual ID'} +

+
+
+ )} +
+
+
{children(wandControlRef)}
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx new file mode 100644 index 000000000..81ca1f03c --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx @@ -0,0 +1,114 @@ +'use client' + +import { useEffect, useRef } from 'react' +import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' +import { SubBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block' +import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types' + +interface ToolSubBlockRendererProps { + blockId: string + subBlockId: string + toolIndex: number + subBlock: BlockSubBlockConfig + effectiveParamId: string + toolParams: Record | undefined + onParamChange: (toolIndex: number, paramId: string, value: string) => void + disabled: boolean + canonicalToggle?: { + mode: 'basic' | 'advanced' + disabled?: boolean + onToggle?: () => void + } +} + +/** + * SubBlock types whose store values are objects/arrays/non-strings. + * tool.params stores strings (via JSON.stringify), so when syncing + * back to the store we parse them to restore the native shape. + */ +const OBJECT_SUBBLOCK_TYPES = new Set(['file-upload', 'table', 'grouped-checkbox-list']) + +/** + * Bridges the subblock store with StoredTool.params via a synthetic store key, + * then delegates all rendering to SubBlock for full parity. + */ +export function ToolSubBlockRenderer({ + blockId, + subBlockId, + toolIndex, + subBlock, + effectiveParamId, + toolParams, + onParamChange, + disabled, + canonicalToggle, +}: ToolSubBlockRendererProps) { + const syntheticId = `${subBlockId}-tool-${toolIndex}-${effectiveParamId}` + const [storeValue, setStoreValue] = useSubBlockValue(blockId, syntheticId) + + const toolParamValue = toolParams?.[effectiveParamId] ?? '' + const isObjectType = OBJECT_SUBBLOCK_TYPES.has(subBlock.type) + + const lastPushedToStoreRef = useRef(null) + const lastPushedToParamsRef = useRef(null) + + useEffect(() => { + if (!toolParamValue && lastPushedToStoreRef.current === null) { + lastPushedToStoreRef.current = toolParamValue + lastPushedToParamsRef.current = toolParamValue + return + } + if (toolParamValue !== lastPushedToStoreRef.current) { + lastPushedToStoreRef.current = toolParamValue + lastPushedToParamsRef.current = toolParamValue + + if (isObjectType && typeof toolParamValue === 'string' && toolParamValue) { + try { + const parsed = JSON.parse(toolParamValue) + if (typeof parsed === 'object' && parsed !== null) { + setStoreValue(parsed) + return + } + } catch { + // Not valid JSON — fall through to set as string + } + } + setStoreValue(toolParamValue) + } + }, [toolParamValue, setStoreValue, isObjectType]) + + useEffect(() => { + if (storeValue == null && lastPushedToParamsRef.current === null) return + const stringValue = + storeValue == null + ? '' + : typeof storeValue === 'string' + ? storeValue + : JSON.stringify(storeValue) + if (stringValue !== lastPushedToParamsRef.current) { + lastPushedToParamsRef.current = stringValue + lastPushedToStoreRef.current = stringValue + onParamChange(toolIndex, effectiveParamId, stringValue) + } + }, [storeValue, toolIndex, effectiveParamId, onParamChange]) + + const visibility = subBlock.paramVisibility ?? 'user-or-llm' + const isOptionalForUser = visibility !== 'user-only' + + const config = { + ...subBlock, + id: syntheticId, + ...(isOptionalForUser && { required: false }), + } + + return ( + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.test.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.test.ts index 8d2548c13..44b73e1e4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.test.ts @@ -2,37 +2,12 @@ * @vitest-environment node */ import { describe, expect, it } from 'vitest' - -interface StoredTool { - type: string - title?: string - toolId?: string - params?: Record - customToolId?: string - schema?: any - code?: string - operation?: string - usageControl?: 'auto' | 'force' | 'none' -} - -const isMcpToolAlreadySelected = (selectedTools: StoredTool[], mcpToolId: string): boolean => { - return selectedTools.some((tool) => tool.type === 'mcp' && tool.toolId === mcpToolId) -} - -const isCustomToolAlreadySelected = ( - selectedTools: StoredTool[], - customToolId: string -): boolean => { - return selectedTools.some( - (tool) => tool.type === 'custom-tool' && tool.customToolId === customToolId - ) -} - -const isWorkflowAlreadySelected = (selectedTools: StoredTool[], workflowId: string): boolean => { - return selectedTools.some( - (tool) => tool.type === 'workflow_input' && tool.params?.workflowId === workflowId - ) -} +import type { StoredTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/types' +import { + isCustomToolAlreadySelected, + isMcpToolAlreadySelected, + isWorkflowAlreadySelected, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/utils' describe('isMcpToolAlreadySelected', () => { describe('basic functionality', () => { 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 ff08547ec..f92b8150a 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 @@ -23,6 +23,7 @@ import { isToolUnavailable, getMcpToolIssue as validateMcpTool, } from '@/lib/mcp/tool-validation' +import type { McpToolSchema } from '@/lib/mcp/types' import { getCanonicalScopesForProvider, getProviderIdFromServiceId, @@ -32,31 +33,26 @@ import { import { extractInputFieldsFromBlocks } from '@/lib/workflows/input-format' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { - CheckboxList, - Code, - FileSelectorInput, - FileUpload, - FolderSelectorInput, LongInput, - ProjectSelectorInput, - SheetSelectorInput, ShortInput, - SlackSelectorInput, - SliderInput, - Table, - TimeInput, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components' -import { DocumentSelector } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/document-selector/document-selector' -import { DocumentTagEntry } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/document-tag-entry/document-tag-entry' -import { KnowledgeBaseSelector } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-base-selector/knowledge-base-selector' -import { KnowledgeTagFilters } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters' import { type CustomTool, CustomToolModal, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal' -import { ToolCredentialSelector } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-credential-selector' +import { ToolCredentialSelector } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/credential-selector' +import { ParameterWithLabel } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/parameter' +import { ToolSubBlockRenderer } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer' +import type { StoredTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/types' +import { + isCustomToolAlreadySelected, + isMcpToolAlreadySelected, + isWorkflowAlreadySelected, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/utils' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' +import type { WandControlHandlers } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block' import { getAllBlocks } from '@/blocks' +import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types' import { useMcpTools } from '@/hooks/mcp/use-mcp-tools' import { type CustomTool as CustomToolDefinition, @@ -74,682 +70,59 @@ import { useWorkflowState, useWorkflows, } from '@/hooks/queries/workflows' +import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { usePermissionConfig } from '@/hooks/use-permission-config' import { getProviderFromModel, supportsToolUsageControl } from '@/providers/utils' import { useSettingsModalStore } from '@/stores/modals/settings/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' import { formatParameterLabel, + getSubBlocksForToolInput, getToolParametersConfig, isPasswordParameter, + type SubBlocksForToolInput, type ToolParameterConfig, } from '@/tools/params' import { buildCanonicalIndex, buildPreviewContextValues, type CanonicalIndex, + type CanonicalModeOverrides, evaluateSubBlockCondition, + isCanonicalPair, + resolveCanonicalMode, type SubBlockCondition, } from '@/tools/params-resolver' const logger = createLogger('ToolInput') /** - * Props for the ToolInput component + * Extracts canonical mode overrides scoped to a specific tool type. + * Canonical modes are stored with `{blockType}:{canonicalId}` keys to prevent + * cross-tool collisions when multiple tools share the same canonicalParamId. */ -interface ToolInputProps { - /** Unique identifier for the block */ - blockId: string - /** Unique identifier for the sub-block */ - subBlockId: string - /** Whether component is in preview mode */ - isPreview?: boolean - /** Value to display in preview mode */ - previewValue?: any - /** Whether the input is disabled */ - disabled?: boolean - /** Allow expanding tools in preview mode */ - allowExpandInPreview?: boolean -} - -/** - * Represents a tool selected and configured in the workflow - * - * @remarks - * For custom tools (new format), we only store: type, customToolId, usageControl, isExpanded. - * Everything else (title, schema, code) is loaded dynamically from the database. - * Legacy custom tools with inline schema/code are still supported for backwards compatibility. - */ -interface StoredTool { - /** Block type identifier */ - type: string - /** Display title for the tool (optional for new custom tool format) */ - title?: string - /** Direct tool ID for execution (optional for new custom tool format) */ - toolId?: string - /** Parameter values configured by the user (optional for new custom tool format) */ - params?: Record - /** Whether the tool details are expanded in UI */ - isExpanded?: boolean - /** Database ID for custom tools (new format - reference only) */ - customToolId?: string - /** Tool schema for custom tools (legacy format - inline) */ - schema?: any - /** Implementation code for custom tools (legacy format - inline) */ - code?: string - /** Selected operation for multi-operation tools */ - operation?: string - /** Tool usage control mode for LLM */ - usageControl?: 'auto' | 'force' | 'none' -} - -/** - * Resolves a custom tool reference to its full definition. - * - * @remarks - * Custom tools can be stored in two formats: - * 1. Reference-only (new): `{ customToolId: "...", usageControl: "auto" }` - loads from database - * 2. Inline (legacy): `{ schema: {...}, code: "..." }` - uses embedded definition - * - * @param storedTool - The stored tool reference containing either a customToolId or inline definition - * @param customToolsList - List of custom tools fetched from the database - * @returns The resolved custom tool with schema, code, and title, or `null` if not found - */ -function resolveCustomToolFromReference( - storedTool: StoredTool, - customToolsList: CustomToolDefinition[] -): { schema: any; code: string; title: string } | null { - // If the tool has a customToolId (new reference format), look it up - if (storedTool.customToolId) { - const customTool = customToolsList.find((t) => t.id === storedTool.customToolId) - if (customTool) { - return { - schema: customTool.schema, - code: customTool.code, - title: customTool.title, - } - } - // If not found by ID, fall through to try other methods - logger.warn(`Custom tool not found by ID: ${storedTool.customToolId}`) - } - - // Legacy format: inline schema and code - if (storedTool.schema && storedTool.code !== undefined) { - return { - schema: storedTool.schema, - code: storedTool.code, - title: storedTool.title || '', +function scopeCanonicalOverrides( + overrides: CanonicalModeOverrides | undefined, + blockType: string | undefined +): CanonicalModeOverrides | undefined { + if (!overrides || !blockType) return undefined + const prefix = `${blockType}:` + let scoped: CanonicalModeOverrides | undefined + for (const [key, val] of Object.entries(overrides)) { + if (key.startsWith(prefix) && val) { + if (!scoped) scoped = {} + scoped[key.slice(prefix.length)] = val } } - - return null + return scoped } /** - * Generic sync wrapper that synchronizes store values with local component state. - * - * @remarks - * Used to sync tool parameter values between the workflow store and local controlled inputs. - * Listens for changes in the store and propagates them to the local component via onChange. - * - * @typeParam T - The type of the store value being synchronized - * - * @param blockId - The block identifier for store lookup - * @param paramId - The parameter identifier within the block - * @param value - Current local value - * @param onChange - Callback to update the local value - * @param children - Child components to render - * @param transformer - Optional function to transform store value before comparison - * @returns The children wrapped with synchronization logic + * Renders the input for workflow_executor's inputMapping parameter. + * This is a special case that doesn't map to any SubBlockConfig, so it's kept here. */ -function GenericSyncWrapper({ - blockId, - paramId, - value, - onChange, - children, - transformer, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - children: React.ReactNode - transformer?: (storeValue: T) => string -}) { - const [storeValue] = useSubBlockValue(blockId, paramId) - - useEffect(() => { - if (storeValue != null) { - const transformedValue = transformer ? transformer(storeValue) : String(storeValue) - if (transformedValue !== value) { - onChange(transformedValue) - } - } - }, [storeValue, value, onChange, transformer]) - - return <>{children} -} - -function FileSelectorSyncWrapper({ - blockId, - paramId, - value, - onChange, - uiComponent, - disabled, - previewContextValues, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - uiComponent: any - disabled: boolean - previewContextValues?: Record -}) { - return ( - - - - ) -} - -function SheetSelectorSyncWrapper({ - blockId, - paramId, - value, - onChange, - uiComponent, - disabled, - previewContextValues, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - uiComponent: any - disabled: boolean - previewContextValues?: Record -}) { - return ( - - - - ) -} - -function FolderSelectorSyncWrapper({ - blockId, - paramId, - value, - onChange, - uiComponent, - disabled, - previewContextValues, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - uiComponent: any - disabled: boolean - previewContextValues?: Record -}) { - return ( - - - - ) -} - -function KnowledgeBaseSelectorSyncWrapper({ - blockId, - paramId, - value, - onChange, - uiComponent, - disabled, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - uiComponent: any - disabled: boolean -}) { - return ( - - - - ) -} - -function DocumentSelectorSyncWrapper({ - blockId, - paramId, - value, - onChange, - uiComponent, - disabled, - previewContextValues, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - uiComponent: any - disabled: boolean - previewContextValues?: Record -}) { - return ( - - - - ) -} - -function DocumentTagEntrySyncWrapper({ - blockId, - paramId, - value, - onChange, - disabled, - previewContextValues, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - disabled: boolean - previewContextValues?: Record -}) { - return ( - - - - ) -} - -function KnowledgeTagFiltersSyncWrapper({ - blockId, - paramId, - value, - onChange, - disabled, - previewContextValues, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - disabled: boolean - previewContextValues?: Record -}) { - return ( - - - - ) -} - -function TableSyncWrapper({ - blockId, - paramId, - value, - onChange, - uiComponent, - disabled, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - uiComponent: any - disabled: boolean -}) { - return ( - JSON.stringify(storeValue)} - > - - - ) -} - -function TimeInputSyncWrapper({ - blockId, - paramId, - value, - onChange, - uiComponent, - disabled, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - uiComponent: any - disabled: boolean -}) { - return ( - - - - ) -} - -function SliderInputSyncWrapper({ - blockId, - paramId, - value, - onChange, - uiComponent, - disabled, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - uiComponent: any - disabled: boolean -}) { - return ( - String(storeValue)} - > - - - ) -} - -function CheckboxListSyncWrapper({ - blockId, - paramId, - value, - onChange, - uiComponent, - disabled, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - uiComponent: any - disabled: boolean -}) { - return ( - JSON.stringify(storeValue)} - > - - - ) -} - -function ComboboxSyncWrapper({ - blockId, - paramId, - value, - onChange, - uiComponent, - disabled, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - uiComponent: any - disabled: boolean -}) { - const options = (uiComponent.options || []).map((opt: any) => - typeof opt === 'string' ? { label: opt, value: opt } : { label: opt.label, value: opt.id } - ) - - return ( - - - - ) -} - -function FileUploadSyncWrapper({ - blockId, - paramId, - value, - onChange, - uiComponent, - disabled, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - uiComponent: any - disabled: boolean -}) { - return ( - JSON.stringify(storeValue)} - > - - - ) -} - -function SlackSelectorSyncWrapper({ - blockId, - paramId, - value, - onChange, - uiComponent, - disabled, - previewContextValues, - selectorType, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - uiComponent: any - disabled: boolean - previewContextValues?: Record - selectorType: 'channel-selector' | 'user-selector' -}) { - return ( - - - - ) -} - -function WorkflowSelectorSyncWrapper({ - blockId, - paramId, - value, - onChange, - uiComponent, - disabled, - workspaceId, - currentWorkflowId, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - uiComponent: any - disabled: boolean - workspaceId: string - currentWorkflowId?: string -}) { - const { data: workflows = [], isLoading } = useWorkflows(workspaceId, { syncRegistry: false }) - - const availableWorkflows = workflows.filter( - (w) => !currentWorkflowId || w.id !== currentWorkflowId - ) - - const options = availableWorkflows.map((workflow) => ({ - label: workflow.name, - value: workflow.id, - })) - - return ( - - - - ) -} - -function WorkflowInputMapperSyncWrapper({ +function WorkflowInputMapperInput({ blockId, paramId, value, @@ -779,7 +152,7 @@ function WorkflowInputMapperSyncWrapper({ }, [value]) const handleFieldChange = useCallback( - (fieldName: string, fieldValue: any) => { + (fieldName: string, fieldValue: string) => { const newValue = { ...parsedValue, [fieldName]: fieldValue } onChange(JSON.stringify(newValue)) }, @@ -812,7 +185,7 @@ function WorkflowInputMapperSyncWrapper({ return (
- {inputFields.map((field: any) => ( + {inputFields.map((field: { name: string; type: string }) => ( void - disabled: boolean - uiComponent: any - currentToolParams?: Record -}) { - const language = (currentToolParams?.language as 'javascript' | 'python') || 'javascript' - - return ( - - - - ) -} - /** * Badge component showing deployment status for workflow tools */ @@ -941,6 +276,66 @@ function WorkflowToolDeployBadge({ ) } +/** + * Props for the ToolInput component + */ +interface ToolInputProps { + /** Unique identifier for the block */ + blockId: string + /** Unique identifier for the sub-block */ + subBlockId: string + /** Whether component is in preview mode */ + isPreview?: boolean + /** Value to display in preview mode */ + previewValue?: any + /** Whether the input is disabled */ + disabled?: boolean + /** Allow expanding tools in preview mode */ + allowExpandInPreview?: boolean +} + +/** + * Resolves a custom tool reference to its full definition. + * + * @remarks + * Custom tools can be stored in two formats: + * 1. Reference-only (new): `{ customToolId: "...", usageControl: "auto" }` - loads from database + * 2. Inline (legacy): `{ schema: {...}, code: "..." }` - uses embedded definition + * + * @param storedTool - The stored tool reference containing either a customToolId or inline definition + * @param customToolsList - List of custom tools fetched from the database + * @returns The resolved custom tool with schema, code, and title, or `null` if not found + */ +function resolveCustomToolFromReference( + storedTool: StoredTool, + customToolsList: CustomToolDefinition[] +): { schema: any; code: string; title: string } | null { + // If the tool has a customToolId (new reference format), look it up + if (storedTool.customToolId) { + const customTool = customToolsList.find((t) => t.id === storedTool.customToolId) + if (customTool) { + return { + schema: customTool.schema, + code: customTool.code, + title: customTool.title, + } + } + // If not found by ID, fall through to try other methods + logger.warn(`Custom tool not found by ID: ${storedTool.customToolId}`) + } + + // Legacy format: inline schema and code + if (storedTool.schema && storedTool.code !== undefined) { + return { + schema: storedTool.schema, + code: storedTool.code, + title: storedTool.title || '', + } + } + + return null +} + /** * Set of built-in tool types that are core platform tools. * @@ -966,6 +361,80 @@ const BUILT_IN_TOOL_TYPES = new Set([ 'workflow', ]) +/** + * Checks if a block supports multiple operations. + * + * @param blockType - The block type to check + * @returns `true` if the block has more than one tool operation available + */ +function hasMultipleOperations(blockType: string): boolean { + const block = getAllBlocks().find((b) => b.type === blockType) + return (block?.tools?.access?.length || 0) > 1 +} + +/** + * Gets the available operation options for a multi-operation tool. + * + * @param blockType - The block type to get operations for + * @returns Array of operation options with label and id properties + */ +function getOperationOptions(blockType: string): { label: string; id: string }[] { + const block = getAllBlocks().find((b) => b.type === blockType) + if (!block || !block.tools?.access) return [] + + const operationSubBlock = block.subBlocks.find((sb) => sb.id === 'operation') + if ( + operationSubBlock && + operationSubBlock.type === 'dropdown' && + Array.isArray(operationSubBlock.options) + ) { + return operationSubBlock.options as { label: string; id: string }[] + } + + return block.tools.access.map((toolId) => { + try { + const toolParams = getToolParametersConfig(toolId) + return { + id: toolId, + label: toolParams?.toolConfig?.name || toolId, + } + } catch (error) { + logger.error(`Error getting tool config for ${toolId}:`, error) + return { id: toolId, label: toolId } + } + }) +} + +/** + * Gets the correct tool ID for a given operation. + * + * @param blockType - The block type + * @param operation - The selected operation (for multi-operation tools) + * @returns The tool ID to use for execution, or `undefined` if not found + */ +function getToolIdForOperation(blockType: string, operation?: string): string | undefined { + const block = getAllBlocks().find((b) => b.type === blockType) + if (!block || !block.tools?.access) return undefined + + if (block.tools.access.length === 1) { + return block.tools.access[0] + } + + if (operation && block.tools?.config?.tool) { + try { + return block.tools.config.tool({ operation }) + } catch (error) { + logger.error('Error selecting tool for operation:', error) + } + } + + if (operation && block.tools.access.includes(operation)) { + return operation + } + + return block.tools.access[0] +} + /** * Creates a styled icon element for tool items in the selection dropdown. * @@ -973,7 +442,10 @@ const BUILT_IN_TOOL_TYPES = new Set([ * @param IconComponent - The Lucide icon component to render * @returns A styled div containing the icon with consistent dimensions */ -function createToolIcon(bgColor: string, IconComponent: any) { +function createToolIcon( + bgColor: string, + IconComponent: React.ComponentType<{ className?: string }> +) { return (
(null) const [usageControlPopoverIndex, setUsageControlPopoverIndex] = useState(null) + const canonicalModeOverrides = useWorkflowStore( + useCallback( + (state) => state.blocks[blockId]?.data?.canonicalModes as CanonicalModeOverrides | undefined, + [blockId] + ) + ) + const { collaborativeSetBlockCanonicalMode } = useCollaborativeWorkflow() + const value = isPreview ? previewValue : storeValue const selectedTools: StoredTool[] = @@ -1030,12 +510,7 @@ export const ToolInput = memo(function ToolInput({ const shouldFetchCustomTools = !isPreview || hasReferenceOnlyCustomTools const { data: customTools = [] } = useCustomTools(shouldFetchCustomTools ? workspaceId : '') - const { - mcpTools, - isLoading: mcpLoading, - error: mcpError, - refreshTools, - } = useMcpTools(workspaceId) + const { mcpTools, isLoading: mcpLoading } = useMcpTools(workspaceId) const { data: mcpServers = [], isLoading: mcpServersLoading } = useMcpServers(workspaceId) const { data: storedMcpTools = [] } = useStoredMcpTools(workspaceId) @@ -1044,7 +519,6 @@ export const ToolInput = memo(function ToolInput({ const openSettingsModal = useSettingsModalStore((state) => state.openModal) const mcpDataLoading = mcpLoading || mcpServersLoading - // Fetch workflows for the Workflows section in the dropdown const { data: workflowsList = [] } = useWorkflows(workspaceId, { syncRegistry: false }) const availableWorkflows = useMemo( () => workflowsList.filter((w) => w.id !== workflowId), @@ -1082,7 +556,7 @@ export const ToolInput = memo(function ToolInput({ ) || storedMcpTools.find((st) => st.serverId === serverId && st.toolName === toolName) // Use DB schema if available, otherwise use Zustand schema - const schema = storedTool?.schema ?? tool.schema + const schema = storedTool?.schema ?? (tool.schema as McpToolSchema | undefined) return validateMcpTool( { @@ -1225,159 +699,12 @@ export const ToolInput = memo(function ToolInput({ if (hasMultipleOperations(blockType)) { return false } - // Allow multiple instances for workflow and knowledge blocks - // Each instance can target a different workflow/knowledge base if (blockType === 'workflow' || blockType === 'knowledge') { return false } return selectedTools.some((tool) => tool.toolId === toolId) } - /** - * Checks if an MCP tool is already selected. - * - * @param mcpToolId - The MCP tool identifier to check - * @returns `true` if the MCP tool is already selected - */ - const isMcpToolAlreadySelected = (mcpToolId: string): boolean => { - return selectedTools.some((tool) => tool.type === 'mcp' && tool.toolId === mcpToolId) - } - - /** - * Checks if a custom tool is already selected. - * - * @param customToolId - The custom tool identifier to check - * @returns `true` if the custom tool is already selected - */ - const isCustomToolAlreadySelected = (customToolId: string): boolean => { - return selectedTools.some( - (tool) => tool.type === 'custom-tool' && tool.customToolId === customToolId - ) - } - - /** - * Checks if a workflow is already selected. - * - * @param workflowId - The workflow identifier to check - * @returns `true` if the workflow is already selected - */ - const isWorkflowAlreadySelected = (workflowId: string): boolean => { - return selectedTools.some( - (tool) => tool.type === 'workflow_input' && tool.params?.workflowId === workflowId - ) - } - - /** - * Checks if a block supports multiple operations. - * - * @param blockType - The block type to check - * @returns `true` if the block has more than one tool operation available - */ - const hasMultipleOperations = (blockType: string): boolean => { - const block = getAllBlocks().find((block) => block.type === blockType) - return (block?.tools?.access?.length || 0) > 1 - } - - /** - * Gets the available operation options for a multi-operation tool. - * - * @remarks - * First attempts to find options from the block's operation dropdown subBlock, - * then falls back to creating options from the tools.access array. - * - * @param blockType - The block type to get operations for - * @returns Array of operation options with label and id properties - */ - const getOperationOptions = (blockType: string): { label: string; id: string }[] => { - const block = getAllBlocks().find((block) => block.type === blockType) - if (!block || !block.tools?.access) return [] - - // Look for an operation dropdown in the block's subBlocks - const operationSubBlock = block.subBlocks.find((sb) => sb.id === 'operation') - if ( - operationSubBlock && - operationSubBlock.type === 'dropdown' && - Array.isArray(operationSubBlock.options) - ) { - return operationSubBlock.options as { label: string; id: string }[] - } - - // Fallback: create options from tools.access - return block.tools.access.map((toolId) => { - try { - const toolParams = getToolParametersConfig(toolId) - return { - id: toolId, - label: toolParams?.toolConfig?.name || toolId, - } - } catch (error) { - logger.error(`Error getting tool config for ${toolId}:`, error) - return { - id: toolId, - label: toolId, - } - } - }) - } - - /** - * Gets the correct tool ID for a given operation. - * - * @remarks - * For single-tool blocks, returns the first tool. For multi-operation blocks, - * uses the block's tool selection function or matches the operation to a tool ID. - * - * @param blockType - The block type - * @param operation - The selected operation (for multi-operation tools) - * @returns The tool ID to use for execution, or `undefined` if not found - */ - const getToolIdForOperation = (blockType: string, operation?: string): string | undefined => { - const block = getAllBlocks().find((block) => block.type === blockType) - if (!block || !block.tools?.access) return undefined - - // If there's only one tool, return it - if (block.tools.access.length === 1) { - return block.tools.access[0] - } - - // If there's an operation and a tool selection function, use it - if (operation && block.tools?.config?.tool) { - try { - return block.tools.config.tool({ operation }) - } catch (error) { - logger.error('Error selecting tool for operation:', error) - } - } - - // If there's an operation that matches a tool ID, use it - if (operation && block.tools.access.includes(operation)) { - return operation - } - - // Default to first tool - return block.tools.access[0] - } - - /** - * Initializes tool parameters with empty values. - * - * @remarks - * Returns an empty object as parameters are populated dynamically - * based on user input and default values from the tool configuration. - * - * @param toolId - The tool identifier - * @param params - Array of parameter configurations - * @param instanceId - Optional instance identifier for unique param keys - * @returns Empty parameter object to be populated by the user - */ - const initializeToolParams = ( - toolId: string, - params: ToolParameterConfig[], - instanceId?: string - ): Record => { - return {} - } - const handleSelectTool = useCallback( (toolBlock: (typeof toolBlocks)[0]) => { if (isPreview || disabled) return @@ -1394,7 +721,7 @@ export const ToolInput = memo(function ToolInput({ const toolParams = getToolParametersConfig(toolId, toolBlock.type) if (!toolParams) return - const initialParams = initializeToolParams(toolId, toolParams.userInputParameters, blockId) + const initialParams: Record = {} toolParams.userInputParameters.forEach((param) => { if (param.uiComponent?.value && !initialParams[param.id]) { @@ -1420,18 +747,7 @@ export const ToolInput = memo(function ToolInput({ setOpen(false) }, - [ - isPreview, - disabled, - hasMultipleOperations, - getOperationOptions, - getToolIdForOperation, - isToolAlreadySelected, - initializeToolParams, - blockId, - selectedTools, - setStoreValue, - ] + [isPreview, disabled, isToolAlreadySelected, selectedTools, setStoreValue] ) const handleAddCustomTool = useCallback( @@ -1541,7 +857,7 @@ export const ToolInput = memo(function ToolInput({ customTools.some( (customTool) => customTool.id === toolId && - customTool.schema?.function?.name === tool.schema.function.name + customTool.schema?.function?.name === tool.schema?.function?.name ) ) { return false @@ -1597,10 +913,6 @@ export const ToolInput = memo(function ToolInput({ return } - const initialParams = initializeToolParams(newToolId, toolParams.userInputParameters, blockId) - - const oldToolParams = tool.toolId ? getToolParametersConfig(tool.toolId, tool.type) : null - const oldParamIds = new Set(oldToolParams?.userInputParameters.map((p) => p.id) || []) const newParamIds = new Set(toolParams.userInputParameters.map((p) => p.id)) const preservedParams: Record = {} @@ -1626,21 +938,13 @@ export const ToolInput = memo(function ToolInput({ ...tool, toolId: newToolId, operation, - params: { ...initialParams, ...preservedParams }, // Preserve all compatible existing values + params: preservedParams, } : tool ) ) }, - [ - isPreview, - disabled, - selectedTools, - getToolIdForOperation, - initializeToolParams, - blockId, - setStoreValue, - ] + [isPreview, disabled, selectedTools, getToolIdForOperation, blockId, setStoreValue] ) const handleUsageControlChange = useCallback( @@ -1700,19 +1004,22 @@ export const ToolInput = memo(function ToolInput({ setDragOverIndex(null) } - const handleMcpToolSelect = (newTool: StoredTool, closePopover = true) => { - setStoreValue([ - ...selectedTools.map((tool) => ({ - ...tool, - isExpanded: false, - })), - newTool, - ]) + const handleMcpToolSelect = useCallback( + (newTool: StoredTool, closePopover = true) => { + setStoreValue([ + ...selectedTools.map((tool) => ({ + ...tool, + isExpanded: false, + })), + newTool, + ]) - if (closePopover) { - setOpen(false) - } - } + if (closePopover) { + setOpen(false) + } + }, + [selectedTools, setStoreValue] + ) const handleDrop = (e: React.DragEvent, dropIndex: number) => { if (isPreview || disabled || draggedIndex === null || draggedIndex === dropIndex) return @@ -1735,11 +1042,180 @@ export const ToolInput = memo(function ToolInput({ setDragOverIndex(null) } - const IconComponent = ({ icon: Icon, className }: { icon: any; className?: string }) => { + const IconComponent = ({ + icon: Icon, + className, + }: { + icon?: React.ComponentType<{ className?: string }> + className?: string + }) => { if (!Icon) return null return } + const evaluateParameterCondition = (param: ToolParameterConfig, tool: StoredTool): boolean => { + if (!('uiComponent' in param) || !param.uiComponent?.condition) return true + const currentValues: Record = { operation: tool.operation, ...tool.params } + return evaluateSubBlockCondition( + param.uiComponent.condition as SubBlockCondition, + currentValues + ) + } + + /** + * Renders a parameter input for custom tools, MCP tools, and legacy registry + * tools that don't have SubBlockConfig definitions. + * + * Registry tools with subBlocks use ToolSubBlockRenderer instead. + */ + const renderParameterInput = ( + param: ToolParameterConfig, + value: string, + onChange: (value: string) => void, + toolIndex?: number, + currentToolParams?: Record, + wandControlRef?: React.MutableRefObject + ) => { + const uniqueSubBlockId = + toolIndex !== undefined + ? `${subBlockId}-tool-${toolIndex}-${param.id}` + : `${subBlockId}-${param.id}` + const uiComponent = param.uiComponent + + if (!uiComponent) { + return ( + + ) + } + + switch (uiComponent.type) { + case 'dropdown': + return ( + (option.id ?? option.value) !== '') + .map((option) => ({ + label: option.label, + value: option.id ?? option.value ?? '', + })) || [] + } + value={value} + onChange={onChange} + placeholder={uiComponent.placeholder || 'Select option'} + disabled={disabled} + /> + ) + + case 'switch': + return ( + onChange(checked ? 'true' : 'false')} + /> + ) + + case 'long-input': + return ( + + ) + + case 'short-input': + return ( + + ) + + case 'oauth-input': + return ( + + ) + + case 'workflow-input-mapper': { + const selectedWorkflowId = currentToolParams?.workflowId || '' + return ( + + ) + } + + default: + return ( + + ) + } + } + /** * Generates grouped options for the tool selection combobox. * @@ -1752,7 +1228,6 @@ export const ToolInput = memo(function ToolInput({ const toolGroups = useMemo((): ComboboxOptionGroup[] => { const groups: ComboboxOptionGroup[] = [] - // Actions group (no section header) const actionItems: ComboboxOption[] = [] if (!permissionConfig.disableCustomTools) { actionItems.push({ @@ -1782,12 +1257,11 @@ export const ToolInput = memo(function ToolInput({ groups.push({ items: actionItems }) } - // Custom Tools section if (!permissionConfig.disableCustomTools && customTools.length > 0) { groups.push({ section: 'Custom Tools', items: customTools.map((customTool) => { - const alreadySelected = isCustomToolAlreadySelected(customTool.id) + const alreadySelected = isCustomToolAlreadySelected(selectedTools, customTool.id) return { label: customTool.title, value: `custom-${customTool.id}`, @@ -1812,13 +1286,12 @@ export const ToolInput = memo(function ToolInput({ }) } - // MCP Tools section if (!permissionConfig.disableMcpTools && availableMcpTools.length > 0) { groups.push({ section: 'MCP Tools', items: availableMcpTools.map((mcpTool) => { const server = mcpServers.find((s) => s.id === mcpTool.serverId) - const alreadySelected = isMcpToolAlreadySelected(mcpTool.id) + const alreadySelected = isMcpToolAlreadySelected(selectedTools, mcpTool.id) return { label: mcpTool.name, value: `mcp-${mcpTool.id}`, @@ -1850,11 +1323,9 @@ export const ToolInput = memo(function ToolInput({ }) } - // Split tool blocks into built-in tools and integrations const builtInTools = toolBlocks.filter((block) => BUILT_IN_TOOL_TYPES.has(block.type)) const integrations = toolBlocks.filter((block) => !BUILT_IN_TOOL_TYPES.has(block.type)) - // Built-in Tools section if (builtInTools.length > 0) { groups.push({ section: 'Built-in Tools', @@ -1872,7 +1343,6 @@ export const ToolInput = memo(function ToolInput({ }) } - // Integrations section if (integrations.length > 0) { groups.push({ section: 'Integrations', @@ -1895,7 +1365,7 @@ export const ToolInput = memo(function ToolInput({ groups.push({ section: 'Workflows', items: availableWorkflows.map((workflow) => { - const alreadySelected = isWorkflowAlreadySelected(workflow.id) + const alreadySelected = isWorkflowAlreadySelected(selectedTools, workflow.id) return { label: workflow.name, value: `workflow-${workflow.id}`, @@ -1939,11 +1409,7 @@ export const ToolInput = memo(function ToolInput({ permissionConfig.disableCustomTools, permissionConfig.disableMcpTools, availableWorkflows, - getToolIdForOperation, isToolAlreadySelected, - isMcpToolAlreadySelected, - isCustomToolAlreadySelected, - isWorkflowAlreadySelected, ]) const toolRequiresOAuth = (toolId: string): boolean => { @@ -1956,405 +1422,8 @@ export const ToolInput = memo(function ToolInput({ return toolParams?.toolConfig?.oauth } - const evaluateParameterCondition = (param: any, tool: StoredTool): boolean => { - if (!('uiComponent' in param) || !param.uiComponent?.condition) return true - const currentValues: Record = { operation: tool.operation, ...tool.params } - return evaluateSubBlockCondition( - param.uiComponent.condition as SubBlockCondition, - currentValues - ) - } - - /** - * Renders the appropriate UI component for a tool parameter. - * - * @remarks - * Supports multiple input types including dropdown, switch, long-input, - * short-input, file-selector, table, slider, and more. Falls back to - * ShortInput for unknown types. - * - * @param param - The parameter configuration defining the input type - * @param value - The current parameter value - * @param onChange - Callback to handle value changes - * @param toolIndex - Index of the tool in the selected tools array - * @param currentToolParams - Current values of all tool parameters for dependencies - * @returns JSX element for the parameter input component - */ - const renderParameterInput = ( - param: ToolParameterConfig, - value: string, - onChange: (value: string) => void, - toolIndex?: number, - currentToolParams?: Record - ) => { - const uniqueSubBlockId = - toolIndex !== undefined - ? `${subBlockId}-tool-${toolIndex}-${param.id}` - : `${subBlockId}-${param.id}` - const uiComponent = param.uiComponent - - if (!uiComponent) { - return ( - - ) - } - - switch (uiComponent.type) { - case 'dropdown': - return ( - option.id !== '') - .map((option: any) => ({ - label: option.label, - value: option.id, - })) || [] - } - value={value} - onChange={onChange} - placeholder={uiComponent.placeholder || 'Select option'} - disabled={disabled} - /> - ) - - case 'switch': - return ( - onChange(checked ? 'true' : 'false')} - /> - ) - - case 'long-input': - return ( - - ) - - case 'short-input': - return ( - - ) - - case 'channel-selector': - return ( - - ) - - case 'user-selector': - return ( - - ) - - case 'project-selector': - return ( - - ) - - case 'oauth-input': - return ( - - ) - - case 'file-selector': - return ( - - ) - - case 'sheet-selector': - return ( - - ) - - case 'folder-selector': - return ( - - ) - - case 'table': - return ( - - ) - - case 'combobox': - return ( - - ) - - case 'slider': - return ( - - ) - - case 'checkbox-list': - return ( - - ) - - case 'time-input': - return ( - - ) - - case 'file-upload': - return ( - - ) - - case 'workflow-selector': - return ( - - ) - - case 'workflow-input-mapper': { - const selectedWorkflowId = currentToolParams?.workflowId || '' - return ( - - ) - } - - case 'code': - return ( - - ) - - case 'knowledge-base-selector': - return ( - - ) - - case 'document-selector': - return ( - - ) - - case 'document-tag-entry': - return ( - - ) - - case 'knowledge-tag-filters': - return ( - - ) - - default: - return ( - - ) - } - } - return (
- {/* Add Tool Combobox - always at top */} - {/* Selected Tools List */} {selectedTools.length > 0 && selectedTools.map((tool, toolIndex) => { - // Handle custom tools, MCP tools, and workflow tools differently const isCustomTool = tool.type === 'custom-tool' const isMcpTool = tool.type === 'mcp' const isWorkflowTool = tool.type === 'workflow' @@ -2379,13 +1446,11 @@ export const ToolInput = memo(function ToolInput({ ? toolBlocks.find((block) => block.type === tool.type) : null - // Get the current tool ID (may change based on operation) const currentToolId = !isCustomTool && !isMcpTool ? getToolIdForOperation(tool.type, tool.operation) || tool.toolId || '' : tool.toolId || '' - // Get tool parameters using the new utility with block type for UI components const toolParams = !isCustomTool && !isMcpTool && currentToolId ? getToolParametersConfig(currentToolId, tool.type, { @@ -2394,12 +1459,25 @@ export const ToolInput = memo(function ToolInput({ }) : null - // Build canonical index for proper dependency resolution + const toolScopedOverrides = scopeCanonicalOverrides(canonicalModeOverrides, tool.type) + + const subBlocksResult: SubBlocksForToolInput | null = + !isCustomTool && !isMcpTool && currentToolId + ? getSubBlocksForToolInput( + currentToolId, + tool.type, + { + operation: tool.operation, + ...tool.params, + }, + toolScopedOverrides + ) + : null + const toolCanonicalIndex: CanonicalIndex | null = toolBlock?.subBlocks ? buildCanonicalIndex(toolBlock.subBlocks) : null - // Build preview context with canonical resolution const toolContextValues = toolCanonicalIndex ? buildPreviewContextValues(tool.params || {}, { blockType: tool.type, @@ -2409,12 +1487,10 @@ export const ToolInput = memo(function ToolInput({ }) : tool.params || {} - // For custom tools, resolve from reference (new format) or use inline (legacy) const resolvedCustomTool = isCustomTool ? resolveCustomToolFromReference(tool, customTools) : null - // Derive title and schema from resolved tool or inline data const customToolTitle = isCustomTool ? tool.title || resolvedCustomTool?.title || 'Unknown Tool' : null @@ -2433,8 +1509,6 @@ export const ToolInput = memo(function ToolInput({ ) : [] - // For MCP tools, extract parameters from input schema - // Use cached schema from tool object if available, otherwise fetch from mcpTools const mcpTool = isMcpTool ? mcpTools.find((t) => t.id === tool.toolId) : null const mcpToolSchema = isMcpTool ? tool.schema || mcpTool?.inputSchema : null const mcpToolParams = @@ -2451,28 +1525,27 @@ export const ToolInput = memo(function ToolInput({ ) : [] - // Get all parameters to display - const displayParams = isCustomTool + const useSubBlocks = !isCustomTool && !isMcpTool && subBlocksResult?.subBlocks?.length + const displayParams: ToolParameterConfig[] = isCustomTool ? customToolParams : isMcpTool ? mcpToolParams : toolParams?.userInputParameters || [] + const displaySubBlocks: BlockSubBlockConfig[] = useSubBlocks + ? subBlocksResult!.subBlocks + : [] - // Check if tool requires OAuth const requiresOAuth = !isCustomTool && !isMcpTool && currentToolId && toolRequiresOAuth(currentToolId) const oauthConfig = !isCustomTool && !isMcpTool && currentToolId ? getToolOAuthConfig(currentToolId) : null - // Determine if tool has expandable body content const hasOperations = !isCustomTool && !isMcpTool && hasMultipleOperations(tool.type) - const filteredDisplayParams = displayParams.filter((param) => - evaluateParameterCondition(param, tool) - ) - const hasToolBody = - hasOperations || (requiresOAuth && oauthConfig) || filteredDisplayParams.length > 0 + const hasParams = useSubBlocks + ? displaySubBlocks.length > 0 + : displayParams.filter((param) => evaluateParameterCondition(param, tool)).length > 0 + const hasToolBody = hasOperations || (requiresOAuth && oauthConfig) || hasParams - // Only show expansion if tool has body content const isExpandedForDisplay = hasToolBody ? isPreview ? (previewExpanded[toolIndex] ?? !!tool.isExpanded) @@ -2643,7 +1716,6 @@ export const ToolInput = memo(function ToolInput({ {!isCustomTool && isExpandedForDisplay && (
- {/* Operation dropdown for tools with multiple operations */} {(() => { const hasOperations = hasMultipleOperations(tool.type) const operationOptions = hasOperations ? getOperationOptions(tool.type) : [] @@ -2669,23 +1741,23 @@ export const ToolInput = memo(function ToolInput({ ) : null })()} - {/* OAuth credential selector if required */} {requiresOAuth && oauthConfig && (
- Account + Account *
handleParamChange(toolIndex, 'credential', value)} + onChange={(value: string) => + handleParamChange(toolIndex, 'credential', value) + } provider={oauthConfig.provider as OAuthProvider} requiredScopes={ toolBlock?.subBlocks?.find((sb) => sb.id === 'credential') ?.requiredScopes || getCanonicalScopesForProvider(oauthConfig.provider) } - label={`Select ${oauthConfig.provider} account`} serviceId={oauthConfig.provider} disabled={disabled} /> @@ -2693,119 +1765,141 @@ export const ToolInput = memo(function ToolInput({
)} - {/* Tool parameters */} {(() => { - const filteredParams = displayParams.filter((param) => - evaluateParameterCondition(param, tool) - ) - const groupedParams: { [key: string]: ToolParameterConfig[] } = {} - const standaloneParams: ToolParameterConfig[] = [] - - // Group checkbox-list parameters by their UI component title - filteredParams.forEach((param) => { - const paramConfig = param as ToolParameterConfig - if ( - paramConfig.uiComponent?.type === 'checkbox-list' && - paramConfig.uiComponent?.title - ) { - const groupKey = paramConfig.uiComponent.title - if (!groupedParams[groupKey]) { - groupedParams[groupKey] = [] - } - groupedParams[groupKey].push(paramConfig) - } else { - standaloneParams.push(paramConfig) - } - }) - const renderedElements: React.ReactNode[] = [] - // Render grouped checkbox-lists - Object.entries(groupedParams).forEach(([groupTitle, params]) => { - const firstParam = params[0] as ToolParameterConfig - const groupValue = JSON.stringify( - params.reduce( - (acc, p) => ({ ...acc, [p.id]: tool.params?.[p.id] === 'true' }), - {} + if (useSubBlocks && displaySubBlocks.length > 0) { + const coveredParamIds = new Set( + displaySubBlocks.flatMap((sb) => { + const ids = [sb.id] + if (sb.canonicalParamId) ids.push(sb.canonicalParamId) + const cId = toolCanonicalIndex?.canonicalIdBySubBlockId[sb.id] + if (cId) { + const group = toolCanonicalIndex?.groupsById[cId] + if (group) { + if (group.basicId) ids.push(group.basicId) + ids.push(...group.advancedIds) + } + } + return ids + }) + ) + + displaySubBlocks.forEach((sb) => { + const effectiveParamId = sb.id + const canonicalId = toolCanonicalIndex?.canonicalIdBySubBlockId[sb.id] + const canonicalGroup = canonicalId + ? toolCanonicalIndex?.groupsById[canonicalId] + : undefined + const hasCanonicalPair = isCanonicalPair(canonicalGroup) + const canonicalMode = + canonicalGroup && hasCanonicalPair + ? resolveCanonicalMode( + canonicalGroup, + { operation: tool.operation, ...tool.params }, + toolScopedOverrides + ) + : undefined + + const canonicalToggleProp = + hasCanonicalPair && canonicalMode && canonicalId + ? { + mode: canonicalMode, + onToggle: () => { + const nextMode = + canonicalMode === 'advanced' ? 'basic' : 'advanced' + collaborativeSetBlockCanonicalMode( + blockId, + `${tool.type}:${canonicalId}`, + nextMode + ) + }, + } + : undefined + + const sbWithTitle = sb.title + ? sb + : { ...sb, title: formatParameterLabel(effectiveParamId) } + + renderedElements.push( + ) + }) + + const uncoveredParams = displayParams.filter( + (param) => + !coveredParamIds.has(param.id) && evaluateParameterCondition(param, tool) ) - renderedElements.push( -
-
- {groupTitle} -
-
- { - try { - const parsed = JSON.parse(value) - params.forEach((param) => { - handleParamChange( - toolIndex, - param.id, - parsed[param.id] ? 'true' : 'false' - ) - }) - } catch (e) { - // Handle error - } - }} - uiComponent={firstParam.uiComponent} - disabled={disabled} - /> -
-
- ) - }) - - // Render standalone parameters - standaloneParams.forEach((param) => { - renderedElements.push( -
-
- {param.uiComponent?.title || formatParameterLabel(param.id)} - {param.required && param.visibility === 'user-only' && ( - * - )} - {param.visibility === 'user-or-llm' && ( - - (optional) - - )} -
-
- {param.uiComponent ? ( + uncoveredParams.forEach((param) => { + renderedElements.push( + + {(wandControlRef: React.MutableRefObject) => renderParameterInput( param, tool.params?.[param.id] || '', (value) => handleParamChange(toolIndex, param.id, value), toolIndex, - toolContextValues as Record + toolContextValues as Record, + wandControlRef ) - ) : ( - handleParamChange(toolIndex, param.id, value)} - /> - )} -
-
+ } + + ) + }) + + return ( +
{renderedElements}
+ ) + } + + const filteredParams = displayParams.filter((param) => + evaluateParameterCondition(param, tool) + ) + + filteredParams.forEach((param) => { + renderedElements.push( + + {(wandControlRef: React.MutableRefObject) => + renderParameterInput( + param, + tool.params?.[param.id] || '', + (value) => handleParamChange(toolIndex, param.id, value), + toolIndex, + toolContextValues as Record, + wandControlRef + ) + } + ) }) @@ -2817,7 +1911,6 @@ export const ToolInput = memo(function ToolInput({ ) })} - {/* Custom Tool Modal */} { @@ -2831,11 +1924,9 @@ export const ToolInput = memo(function ToolInput({ editingToolIndex !== null && selectedTools[editingToolIndex]?.type === 'custom-tool' ? (() => { const storedTool = selectedTools[editingToolIndex] - // Resolve the full tool definition from reference or inline const resolved = resolveCustomToolFromReference(storedTool, customTools) if (resolved) { - // Find the database ID const dbTool = storedTool.customToolId ? customTools.find((t) => t.id === storedTool.customToolId) : customTools.find( @@ -2849,7 +1940,6 @@ export const ToolInput = memo(function ToolInput({ } } - // Fallback to inline definition (legacy format) return { id: customTools.find( (tool) => tool.schema?.function?.name === storedTool.schema?.function?.name diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/types.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/types.ts new file mode 100644 index 000000000..138b6a562 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/types.ts @@ -0,0 +1,31 @@ +/** + * Represents a tool selected and configured in the workflow + * + * @remarks + * For custom tools (new format), we only store: type, customToolId, usageControl, isExpanded. + * Everything else (title, schema, code) is loaded dynamically from the database. + * Legacy custom tools with inline schema/code are still supported for backwards compatibility. + */ +export interface StoredTool { + /** Block type identifier */ + type: string + /** Display title for the tool (optional for new custom tool format) */ + title?: string + /** Direct tool ID for execution (optional for new custom tool format) */ + toolId?: string + /** Parameter values configured by the user (optional for new custom tool format) */ + params?: Record + /** Whether the tool details are expanded in UI */ + isExpanded?: boolean + /** Database ID for custom tools (new format - reference only) */ + customToolId?: string + /** Tool schema for custom tools (legacy format - inline JSON schema) */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + schema?: Record + /** Implementation code for custom tools (legacy format - inline) */ + code?: string + /** Selected operation for multi-operation tools */ + operation?: string + /** Tool usage control mode for LLM */ + usageControl?: 'auto' | 'force' | 'none' +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/utils.ts new file mode 100644 index 000000000..1110a5808 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/utils.ts @@ -0,0 +1,32 @@ +import type { StoredTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/types' + +/** + * Checks if an MCP tool is already selected. + */ +export function isMcpToolAlreadySelected(selectedTools: StoredTool[], mcpToolId: string): boolean { + return selectedTools.some((tool) => tool.type === 'mcp' && tool.toolId === mcpToolId) +} + +/** + * Checks if a custom tool is already selected. + */ +export function isCustomToolAlreadySelected( + selectedTools: StoredTool[], + customToolId: string +): boolean { + return selectedTools.some( + (tool) => tool.type === 'custom-tool' && tool.customToolId === customToolId + ) +} + +/** + * Checks if a workflow is already selected. + */ +export function isWorkflowAlreadySelected( + selectedTools: StoredTool[], + workflowId: string +): boolean { + return selectedTools.some( + (tool) => tool.type === 'workflow_input' && tool.params?.workflowId === workflowId + ) +} 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 c8422f0e7..180b8bb12 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 @@ -3,7 +3,6 @@ import { isEqual } from 'lodash' import { AlertTriangle, ArrowLeftRight, ArrowUp, Check, Clipboard } from 'lucide-react' import { Button, Input, Label, Tooltip } from '@/components/emcn/components' import { cn } from '@/lib/core/utils/cn' -import type { FieldDiffStatus } from '@/lib/workflows/diff/types' import { CheckboxList, Code, @@ -69,13 +68,15 @@ interface SubBlockProps { isPreview?: boolean subBlockValues?: Record disabled?: boolean - fieldDiffStatus?: FieldDiffStatus allowExpandInPreview?: boolean canonicalToggle?: { mode: 'basic' | 'advanced' disabled?: boolean onToggle?: () => void } + labelSuffix?: React.ReactNode + /** Provides sibling values for dependency resolution in non-preview contexts (e.g. tool-input) */ + dependencyContext?: Record } /** @@ -162,16 +163,14 @@ const getPreviewValue = ( /** * Renders the label with optional validation and description tooltips. * - * @remarks - * Handles JSON validation indicators for code blocks and required field markers. - * Includes inline AI generate button when wand is enabled. - * * @param config - The sub-block configuration defining the label content * @param isValidJson - Whether the JSON content is valid (for code blocks) * @param subBlockValues - Current values of all subblocks for evaluating conditional requirements - * @param wandState - Optional state and handlers for the AI wand feature - * @param canonicalToggle - Optional canonical toggle metadata and handlers - * @param canonicalToggleIsDisabled - Whether the canonical toggle is disabled + * @param wandState - State and handlers for the inline AI generate feature + * @param canonicalToggle - Metadata and handlers for the basic/advanced mode toggle + * @param canonicalToggleIsDisabled - Whether the canonical toggle is disabled (includes dependsOn gating) + * @param copyState - State and handler for the copy-to-clipboard button + * @param labelSuffix - Additional content rendered after the label text * @returns The label JSX element, or `null` for switch types or when no title is defined */ const renderLabel = ( @@ -202,7 +201,8 @@ const renderLabel = ( showCopyButton: boolean copied: boolean onCopy: () => void - } + }, + labelSuffix?: React.ReactNode ): JSX.Element | null => { if (config.type === 'switch') return null if (!config.title) return null @@ -215,9 +215,10 @@ const renderLabel = ( return (
-
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx index a7a5d7c38..9f1905c83 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx @@ -571,7 +571,6 @@ export function Editor() { isPreview={false} subBlockValues={subBlockState} disabled={!canEditBlock} - fieldDiffStatus={undefined} allowExpandInPreview={false} canonicalToggle={ isCanonicalSwap && canonicalMode && canonicalId @@ -635,7 +634,6 @@ export function Editor() { isPreview={false} subBlockValues={subBlockState} disabled={!canEditBlock} - fieldDiffStatus={undefined} allowExpandInPreview={false} /> {index < advancedOnlySubBlocks.length - 1 && ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index 16c0e81f1..1088f8c87 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -1,4 +1,4 @@ -import { useCallback, useRef, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import { useQueryClient } from '@tanstack/react-query' import { v4 as uuidv4 } from 'uuid' @@ -46,7 +46,13 @@ import { useWorkflowStore } from '@/stores/workflows/workflow/store' const logger = createLogger('useWorkflowExecution') -// Debug state validation result +/** + * Module-level Set tracking which workflows have an active reconnection effect. + * Prevents multiple hook instances (from different components) from starting + * concurrent reconnection streams for the same workflow during the same mount cycle. + */ +const activeReconnections = new Set() + interface DebugValidationResult { isValid: boolean error?: string @@ -54,7 +60,7 @@ interface DebugValidationResult { interface BlockEventHandlerConfig { workflowId?: string - executionId?: string + executionIdRef: { current: string } workflowEdges: Array<{ id: string; target: string; sourceHandle?: string | null }> activeBlocksSet: Set accumulatedBlockLogs: BlockLog[] @@ -108,12 +114,15 @@ export function useWorkflowExecution() { const queryClient = useQueryClient() const currentWorkflow = useCurrentWorkflow() const { activeWorkflowId, workflows } = useWorkflowRegistry() - const { toggleConsole, addConsole, updateConsole, cancelRunningEntries } = + const { toggleConsole, addConsole, updateConsole, cancelRunningEntries, clearExecutionEntries } = useTerminalConsoleStore() + const hasHydrated = useTerminalConsoleStore((s) => s._hasHydrated) const { getAllVariables } = useEnvironmentStore() const { getVariablesByWorkflowId, variables } = useVariablesStore() const { isExecuting, isDebugging, pendingBlocks, executor, debugContext } = useCurrentWorkflowExecution() + const setCurrentExecutionId = useExecutionStore((s) => s.setCurrentExecutionId) + const getCurrentExecutionId = useExecutionStore((s) => s.getCurrentExecutionId) const setIsExecuting = useExecutionStore((s) => s.setIsExecuting) const setIsDebugging = useExecutionStore((s) => s.setIsDebugging) const setPendingBlocks = useExecutionStore((s) => s.setPendingBlocks) @@ -297,7 +306,7 @@ export function useWorkflowExecution() { (config: BlockEventHandlerConfig) => { const { workflowId, - executionId, + executionIdRef, workflowEdges, activeBlocksSet, accumulatedBlockLogs, @@ -308,6 +317,14 @@ export function useWorkflowExecution() { onBlockCompleteCallback, } = config + /** Returns true if this execution was cancelled or superseded by another run. */ + const isStaleExecution = () => + !!( + workflowId && + executionIdRef.current && + useExecutionStore.getState().getCurrentExecutionId(workflowId) !== executionIdRef.current + ) + const updateActiveBlocks = (blockId: string, isActive: boolean) => { if (!workflowId) return if (isActive) { @@ -360,7 +377,7 @@ export function useWorkflowExecution() { endedAt: data.endedAt, workflowId, blockId: data.blockId, - executionId, + executionId: executionIdRef.current, blockName: data.blockName || 'Unknown Block', blockType: data.blockType || 'unknown', iterationCurrent: data.iterationCurrent, @@ -383,7 +400,7 @@ export function useWorkflowExecution() { endedAt: data.endedAt, workflowId, blockId: data.blockId, - executionId, + executionId: executionIdRef.current, blockName: data.blockName || 'Unknown Block', blockType: data.blockType || 'unknown', iterationCurrent: data.iterationCurrent, @@ -410,7 +427,7 @@ export function useWorkflowExecution() { iterationType: data.iterationType, iterationContainerId: data.iterationContainerId, }, - executionId + executionIdRef.current ) } @@ -432,11 +449,12 @@ export function useWorkflowExecution() { iterationType: data.iterationType, iterationContainerId: data.iterationContainerId, }, - executionId + executionIdRef.current ) } const onBlockStarted = (data: BlockStartedData) => { + if (isStaleExecution()) return updateActiveBlocks(data.blockId, true) markIncomingEdges(data.blockId) @@ -453,7 +471,7 @@ export function useWorkflowExecution() { endedAt: undefined, workflowId, blockId: data.blockId, - executionId, + executionId: executionIdRef.current, blockName: data.blockName || 'Unknown Block', blockType: data.blockType || 'unknown', isRunning: true, @@ -465,6 +483,7 @@ export function useWorkflowExecution() { } const onBlockCompleted = (data: BlockCompletedData) => { + if (isStaleExecution()) return updateActiveBlocks(data.blockId, false) if (workflowId) setBlockRunStatus(workflowId, data.blockId, 'success') @@ -495,6 +514,7 @@ export function useWorkflowExecution() { } const onBlockError = (data: BlockErrorData) => { + if (isStaleExecution()) return updateActiveBlocks(data.blockId, false) if (workflowId) setBlockRunStatus(workflowId, data.blockId, 'error') @@ -902,10 +922,6 @@ export function useWorkflowExecution() { // Update block logs with actual stream completion times if (result.logs && streamCompletionTimes.size > 0) { - const streamCompletionEndTime = new Date( - Math.max(...Array.from(streamCompletionTimes.values())) - ).toISOString() - result.logs.forEach((log: BlockLog) => { if (streamCompletionTimes.has(log.blockId)) { const completionTime = streamCompletionTimes.get(log.blockId)! @@ -987,7 +1003,6 @@ export function useWorkflowExecution() { return { success: true, stream } } - // For manual (non-chat) execution const manualExecutionId = uuidv4() try { const result = await executeWorkflow( @@ -1002,29 +1017,10 @@ export function useWorkflowExecution() { if (result.metadata.pendingBlocks) { setPendingBlocks(activeWorkflowId, result.metadata.pendingBlocks) } - } else if (result && 'success' in result) { - setExecutionResult(result) - // Reset execution state after successful non-debug execution - setIsExecuting(activeWorkflowId, false) - setIsDebugging(activeWorkflowId, false) - setActiveBlocks(activeWorkflowId, new Set()) - - if (isChatExecution) { - if (!result.metadata) { - result.metadata = { duration: 0, startTime: new Date().toISOString() } - } - ;(result.metadata as any).source = 'chat' - } - - // Invalidate subscription queries to update usage - setTimeout(() => { - queryClient.invalidateQueries({ queryKey: subscriptionKeys.all }) - }, 1000) } return result } catch (error: any) { const errorResult = handleExecutionError(error, { executionId: manualExecutionId }) - // Note: Error logs are already persisted server-side via execution-core.ts return errorResult } }, @@ -1275,7 +1271,7 @@ export function useWorkflowExecution() { if (activeWorkflowId) { logger.info('Using server-side executor') - const executionId = uuidv4() + const executionIdRef = { current: '' } let executionResult: ExecutionResult = { success: false, @@ -1293,7 +1289,7 @@ export function useWorkflowExecution() { try { const blockHandlers = buildBlockEventHandlers({ workflowId: activeWorkflowId, - executionId, + executionIdRef, workflowEdges, activeBlocksSet, accumulatedBlockLogs, @@ -1326,6 +1322,10 @@ export function useWorkflowExecution() { loops: clientWorkflowState.loops, parallels: clientWorkflowState.parallels, }, + onExecutionId: (id) => { + executionIdRef.current = id + setCurrentExecutionId(activeWorkflowId, id) + }, callbacks: { onExecutionStarted: (data) => { logger.info('Server execution started:', data) @@ -1368,6 +1368,18 @@ export function useWorkflowExecution() { }, onExecutionCompleted: (data) => { + if ( + activeWorkflowId && + executionIdRef.current && + useExecutionStore.getState().getCurrentExecutionId(activeWorkflowId) !== + executionIdRef.current + ) + return + + if (activeWorkflowId) { + setCurrentExecutionId(activeWorkflowId, null) + } + executionResult = { success: data.success, output: data.output, @@ -1425,9 +1437,33 @@ export function useWorkflowExecution() { }) } } + + const workflowExecState = activeWorkflowId + ? useExecutionStore.getState().getWorkflowExecution(activeWorkflowId) + : null + if (activeWorkflowId && !workflowExecState?.isDebugging) { + setExecutionResult(executionResult) + setIsExecuting(activeWorkflowId, false) + setActiveBlocks(activeWorkflowId, new Set()) + setTimeout(() => { + queryClient.invalidateQueries({ queryKey: subscriptionKeys.all }) + }, 1000) + } }, onExecutionError: (data) => { + if ( + activeWorkflowId && + executionIdRef.current && + useExecutionStore.getState().getCurrentExecutionId(activeWorkflowId) !== + executionIdRef.current + ) + return + + if (activeWorkflowId) { + setCurrentExecutionId(activeWorkflowId, null) + } + executionResult = { success: false, output: {}, @@ -1441,43 +1477,53 @@ export function useWorkflowExecution() { const isPreExecutionError = accumulatedBlockLogs.length === 0 handleExecutionErrorConsole({ workflowId: activeWorkflowId, - executionId, + executionId: executionIdRef.current, error: data.error, durationMs: data.duration, blockLogs: accumulatedBlockLogs, isPreExecutionError, }) + + if (activeWorkflowId) { + setIsExecuting(activeWorkflowId, false) + setIsDebugging(activeWorkflowId, false) + setActiveBlocks(activeWorkflowId, new Set()) + } }, onExecutionCancelled: (data) => { + if ( + activeWorkflowId && + executionIdRef.current && + useExecutionStore.getState().getCurrentExecutionId(activeWorkflowId) !== + executionIdRef.current + ) + return + + if (activeWorkflowId) { + setCurrentExecutionId(activeWorkflowId, null) + } + handleExecutionCancelledConsole({ workflowId: activeWorkflowId, - executionId, + executionId: executionIdRef.current, durationMs: data?.duration, }) + + if (activeWorkflowId) { + setIsExecuting(activeWorkflowId, false) + setIsDebugging(activeWorkflowId, false) + setActiveBlocks(activeWorkflowId, new Set()) + } }, }, }) return executionResult } catch (error: any) { - // Don't log abort errors - they're intentional user actions if (error.name === 'AbortError' || error.message?.includes('aborted')) { logger.info('Execution aborted by user') - - // Reset execution state - if (activeWorkflowId) { - setIsExecuting(activeWorkflowId, false) - setActiveBlocks(activeWorkflowId, new Set()) - } - - // Return gracefully without error - return { - success: false, - output: {}, - metadata: { duration: 0 }, - logs: [], - } + return executionResult } logger.error('Server-side execution failed:', error) @@ -1485,7 +1531,6 @@ export function useWorkflowExecution() { } } - // Fallback: should never reach here throw new Error('Server-side execution is required') } @@ -1717,25 +1762,28 @@ export function useWorkflowExecution() { * Handles cancelling the current workflow execution */ const handleCancelExecution = useCallback(() => { + if (!activeWorkflowId) return logger.info('Workflow execution cancellation requested') - // Cancel the execution stream for this workflow (server-side) - executionStream.cancel(activeWorkflowId ?? undefined) + const storedExecutionId = getCurrentExecutionId(activeWorkflowId) - // Mark current chat execution as superseded so its cleanup won't affect new executions - currentChatExecutionIdRef.current = null - - // Mark all running entries as canceled in the terminal - if (activeWorkflowId) { - cancelRunningEntries(activeWorkflowId) - - // Reset execution state - this triggers chat stream cleanup via useEffect in chat.tsx - setIsExecuting(activeWorkflowId, false) - setIsDebugging(activeWorkflowId, false) - setActiveBlocks(activeWorkflowId, new Set()) + if (storedExecutionId) { + setCurrentExecutionId(activeWorkflowId, null) + fetch(`/api/workflows/${activeWorkflowId}/executions/${storedExecutionId}/cancel`, { + method: 'POST', + }).catch(() => {}) + handleExecutionCancelledConsole({ + workflowId: activeWorkflowId, + executionId: storedExecutionId, + }) } - // If in debug mode, also reset debug state + executionStream.cancel(activeWorkflowId) + currentChatExecutionIdRef.current = null + setIsExecuting(activeWorkflowId, false) + setIsDebugging(activeWorkflowId, false) + setActiveBlocks(activeWorkflowId, new Set()) + if (isDebugging) { resetDebugState() } @@ -1747,7 +1795,9 @@ export function useWorkflowExecution() { setIsDebugging, setActiveBlocks, activeWorkflowId, - cancelRunningEntries, + getCurrentExecutionId, + setCurrentExecutionId, + handleExecutionCancelledConsole, ]) /** @@ -1847,7 +1897,7 @@ export function useWorkflowExecution() { } setIsExecuting(workflowId, true) - const executionId = uuidv4() + const executionIdRef = { current: '' } const accumulatedBlockLogs: BlockLog[] = [] const accumulatedBlockStates = new Map() const executedBlockIds = new Set() @@ -1856,7 +1906,7 @@ export function useWorkflowExecution() { try { const blockHandlers = buildBlockEventHandlers({ workflowId, - executionId, + executionIdRef, workflowEdges, activeBlocksSet, accumulatedBlockLogs, @@ -1871,6 +1921,10 @@ export function useWorkflowExecution() { startBlockId: blockId, sourceSnapshot: effectiveSnapshot, input: workflowInput, + onExecutionId: (id) => { + executionIdRef.current = id + setCurrentExecutionId(workflowId, id) + }, callbacks: { onBlockStarted: blockHandlers.onBlockStarted, onBlockCompleted: blockHandlers.onBlockCompleted, @@ -1878,7 +1932,6 @@ export function useWorkflowExecution() { onExecutionCompleted: (data) => { if (data.success) { - // Add the start block (trigger) to executed blocks executedBlockIds.add(blockId) const mergedBlockStates: Record = { @@ -1902,6 +1955,10 @@ export function useWorkflowExecution() { } setLastExecutionSnapshot(workflowId, updatedSnapshot) } + + setCurrentExecutionId(workflowId, null) + setIsExecuting(workflowId, false) + setActiveBlocks(workflowId, new Set()) }, onExecutionError: (data) => { @@ -1921,19 +1978,27 @@ export function useWorkflowExecution() { handleExecutionErrorConsole({ workflowId, - executionId, + executionId: executionIdRef.current, error: data.error, durationMs: data.duration, blockLogs: accumulatedBlockLogs, }) + + setCurrentExecutionId(workflowId, null) + setIsExecuting(workflowId, false) + setActiveBlocks(workflowId, new Set()) }, onExecutionCancelled: (data) => { handleExecutionCancelledConsole({ workflowId, - executionId, + executionId: executionIdRef.current, durationMs: data?.duration, }) + + setCurrentExecutionId(workflowId, null) + setIsExecuting(workflowId, false) + setActiveBlocks(workflowId, new Set()) }, }, }) @@ -1942,14 +2007,20 @@ export function useWorkflowExecution() { logger.error('Run-from-block failed:', error) } } finally { - setIsExecuting(workflowId, false) - setActiveBlocks(workflowId, new Set()) + const currentId = getCurrentExecutionId(workflowId) + if (currentId === null || currentId === executionIdRef.current) { + setCurrentExecutionId(workflowId, null) + setIsExecuting(workflowId, false) + setActiveBlocks(workflowId, new Set()) + } } }, [ getLastExecutionSnapshot, setLastExecutionSnapshot, clearLastExecutionSnapshot, + getCurrentExecutionId, + setCurrentExecutionId, setIsExecuting, setActiveBlocks, setBlockRunStatus, @@ -1979,29 +2050,213 @@ export function useWorkflowExecution() { const executionId = uuidv4() try { - const result = await executeWorkflow( - undefined, - undefined, - executionId, - undefined, - 'manual', - blockId - ) - if (result && 'success' in result) { - setExecutionResult(result) - } + await executeWorkflow(undefined, undefined, executionId, undefined, 'manual', blockId) } catch (error) { const errorResult = handleExecutionError(error, { executionId }) return errorResult } finally { + setCurrentExecutionId(workflowId, null) setIsExecuting(workflowId, false) setIsDebugging(workflowId, false) setActiveBlocks(workflowId, new Set()) } }, - [activeWorkflowId, setExecutionResult, setIsExecuting, setIsDebugging, setActiveBlocks] + [ + activeWorkflowId, + setCurrentExecutionId, + setExecutionResult, + setIsExecuting, + setIsDebugging, + setActiveBlocks, + ] ) + useEffect(() => { + if (!activeWorkflowId || !hasHydrated) return + + const entries = useTerminalConsoleStore.getState().entries + const runningEntries = entries.filter( + (e) => e.isRunning && e.workflowId === activeWorkflowId && e.executionId + ) + if (runningEntries.length === 0) return + + if (activeReconnections.has(activeWorkflowId)) return + activeReconnections.add(activeWorkflowId) + + executionStream.cancel(activeWorkflowId) + + const sorted = [...runningEntries].sort((a, b) => { + const aTime = a.startedAt ? new Date(a.startedAt).getTime() : 0 + const bTime = b.startedAt ? new Date(b.startedAt).getTime() : 0 + return bTime - aTime + }) + const executionId = sorted[0].executionId! + + const otherExecutionIds = new Set( + sorted.filter((e) => e.executionId !== executionId).map((e) => e.executionId!) + ) + if (otherExecutionIds.size > 0) { + cancelRunningEntries(activeWorkflowId) + } + + setCurrentExecutionId(activeWorkflowId, executionId) + setIsExecuting(activeWorkflowId, true) + + const workflowEdges = useWorkflowStore.getState().edges + const activeBlocksSet = new Set() + const accumulatedBlockLogs: BlockLog[] = [] + const accumulatedBlockStates = new Map() + const executedBlockIds = new Set() + + const executionIdRef = { current: executionId } + + const handlers = buildBlockEventHandlers({ + workflowId: activeWorkflowId, + executionIdRef, + workflowEdges, + activeBlocksSet, + accumulatedBlockLogs, + accumulatedBlockStates, + executedBlockIds, + consoleMode: 'update', + includeStartConsoleEntry: true, + }) + + const originalEntries = entries + .filter((e) => e.executionId === executionId) + .map((e) => ({ ...e })) + + let cleared = false + let reconnectionComplete = false + let cleanupRan = false + const clearOnce = () => { + if (!cleared) { + cleared = true + clearExecutionEntries(executionId) + } + } + + const reconnectWorkflowId = activeWorkflowId + + executionStream + .reconnect({ + workflowId: reconnectWorkflowId, + executionId, + callbacks: { + onBlockStarted: (data) => { + clearOnce() + handlers.onBlockStarted(data) + }, + onBlockCompleted: (data) => { + clearOnce() + handlers.onBlockCompleted(data) + }, + onBlockError: (data) => { + clearOnce() + handlers.onBlockError(data) + }, + onExecutionCompleted: () => { + const currentId = useExecutionStore + .getState() + .getCurrentExecutionId(reconnectWorkflowId) + if (currentId !== executionId) { + reconnectionComplete = true + activeReconnections.delete(reconnectWorkflowId) + return + } + clearOnce() + reconnectionComplete = true + activeReconnections.delete(reconnectWorkflowId) + setCurrentExecutionId(reconnectWorkflowId, null) + setIsExecuting(reconnectWorkflowId, false) + setActiveBlocks(reconnectWorkflowId, new Set()) + }, + onExecutionError: (data) => { + const currentId = useExecutionStore + .getState() + .getCurrentExecutionId(reconnectWorkflowId) + if (currentId !== executionId) { + reconnectionComplete = true + activeReconnections.delete(reconnectWorkflowId) + return + } + clearOnce() + reconnectionComplete = true + activeReconnections.delete(reconnectWorkflowId) + setCurrentExecutionId(reconnectWorkflowId, null) + setIsExecuting(reconnectWorkflowId, false) + setActiveBlocks(reconnectWorkflowId, new Set()) + handleExecutionErrorConsole({ + workflowId: reconnectWorkflowId, + executionId, + error: data.error, + blockLogs: accumulatedBlockLogs, + }) + }, + onExecutionCancelled: () => { + const currentId = useExecutionStore + .getState() + .getCurrentExecutionId(reconnectWorkflowId) + if (currentId !== executionId) { + reconnectionComplete = true + activeReconnections.delete(reconnectWorkflowId) + return + } + clearOnce() + reconnectionComplete = true + activeReconnections.delete(reconnectWorkflowId) + setCurrentExecutionId(reconnectWorkflowId, null) + setIsExecuting(reconnectWorkflowId, false) + setActiveBlocks(reconnectWorkflowId, new Set()) + handleExecutionCancelledConsole({ + workflowId: reconnectWorkflowId, + executionId, + }) + }, + }, + }) + .catch((error) => { + logger.warn('Execution reconnection failed', { executionId, error }) + }) + .finally(() => { + if (reconnectionComplete || cleanupRan) return + const currentId = useExecutionStore.getState().getCurrentExecutionId(reconnectWorkflowId) + if (currentId !== executionId) return + reconnectionComplete = true + activeReconnections.delete(reconnectWorkflowId) + clearExecutionEntries(executionId) + for (const entry of originalEntries) { + addConsole({ + workflowId: entry.workflowId, + blockId: entry.blockId, + blockName: entry.blockName, + blockType: entry.blockType, + executionId: entry.executionId, + executionOrder: entry.executionOrder, + isRunning: false, + warning: 'Execution result unavailable — check the logs page', + }) + } + setCurrentExecutionId(reconnectWorkflowId, null) + setIsExecuting(reconnectWorkflowId, false) + setActiveBlocks(reconnectWorkflowId, new Set()) + }) + + return () => { + cleanupRan = true + executionStream.cancel(reconnectWorkflowId) + activeReconnections.delete(reconnectWorkflowId) + + if (cleared && !reconnectionComplete) { + clearExecutionEntries(executionId) + for (const entry of originalEntries) { + addConsole(entry) + } + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activeWorkflowId, hasHydrated]) + return { isExecuting, isDebugging, diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components/index.ts index cdfe8a0f5..24f107504 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components/index.ts @@ -1,3 +1,4 @@ export { CancelSubscription } from './cancel-subscription' export { CreditBalance } from './credit-balance' export { PlanCard, type PlanCardProps, type PlanFeature } from './plan-card' +export { ReferralCode } from './referral-code' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components/referral-code/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components/referral-code/index.ts new file mode 100644 index 000000000..b1aca728a --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components/referral-code/index.ts @@ -0,0 +1 @@ +export { ReferralCode } from './referral-code' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components/referral-code/referral-code.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components/referral-code/referral-code.tsx new file mode 100644 index 000000000..78e8c863d --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components/referral-code/referral-code.tsx @@ -0,0 +1,103 @@ +'use client' + +import { useState } from 'react' +import { createLogger } from '@sim/logger' +import { Button, Input, Label } from '@/components/emcn' + +const logger = createLogger('ReferralCode') + +interface ReferralCodeProps { + onRedeemComplete?: () => void +} + +/** + * Inline referral/promo code entry field with redeem button. + * One-time use per account — shows success or "already redeemed" state. + */ +export function ReferralCode({ onRedeemComplete }: ReferralCodeProps) { + const [code, setCode] = useState('') + const [isRedeeming, setIsRedeeming] = useState(false) + const [error, setError] = useState(null) + const [success, setSuccess] = useState<{ bonusAmount: number } | null>(null) + + const handleRedeem = async () => { + const trimmed = code.trim() + if (!trimmed || isRedeeming) return + + setIsRedeeming(true) + setError(null) + + try { + const response = await fetch('/api/referral-code/redeem', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code: trimmed }), + }) + + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to redeem code') + } + + if (data.redeemed) { + setSuccess({ bonusAmount: data.bonusAmount }) + setCode('') + onRedeemComplete?.() + } else { + setError(data.error || 'Code could not be redeemed') + } + } catch (err) { + logger.error('Referral code redemption failed', { error: err }) + setError(err instanceof Error ? err.message : 'Failed to redeem code') + } finally { + setIsRedeeming(false) + } + } + + if (success) { + return ( +
+ + + +${success.bonusAmount} credits applied + +
+ ) + } + + return ( +
+
+ +
+ { + setCode(e.target.value) + setError(null) + }} + onKeyDown={(e) => { + if (e.key === 'Enter') handleRedeem() + }} + placeholder='Enter code' + className='h-[32px] w-[140px] text-[12px]' + disabled={isRedeeming} + /> + +
+
+
+ {error && {error}} +
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx index 7581e8d4f..5ccdde897 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx @@ -17,6 +17,7 @@ import { CancelSubscription, CreditBalance, PlanCard, + ReferralCode, } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components' import { ENTERPRISE_PLAN_FEATURES, @@ -549,6 +550,10 @@ export function Subscription() { /> )} + {!subscription.isEnterprise && ( + refetchSubscription()} /> + )} + {/* Next Billing Date - hidden from team members */} {subscription.isPaid && subscriptionData?.data?.periodEnd && diff --git a/apps/sim/app/workspace/page.tsx b/apps/sim/app/workspace/page.tsx index 2eba03b70..bd122ec89 100644 --- a/apps/sim/app/workspace/page.tsx +++ b/apps/sim/app/workspace/page.tsx @@ -4,12 +4,14 @@ import { useEffect } from 'react' import { createLogger } from '@sim/logger' import { useRouter } from 'next/navigation' import { useSession } from '@/lib/auth/auth-client' +import { useReferralAttribution } from '@/hooks/use-referral-attribution' const logger = createLogger('WorkspacePage') export default function WorkspacePage() { const router = useRouter() const { data: session, isPending } = useSession() + useReferralAttribution() useEffect(() => { const redirectToFirstWorkspace = async () => { diff --git a/apps/sim/blocks/blocks/google_books.ts b/apps/sim/blocks/blocks/google_books.ts new file mode 100644 index 000000000..764ee0290 --- /dev/null +++ b/apps/sim/blocks/blocks/google_books.ts @@ -0,0 +1,201 @@ +import { GoogleBooksIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' + +export const GoogleBooksBlock: BlockConfig = { + type: 'google_books', + name: 'Google Books', + description: 'Search and retrieve book information', + authMode: AuthMode.ApiKey, + longDescription: + 'Search for books using the Google Books API. Find volumes by title, author, ISBN, or keywords, and retrieve detailed information about specific books including descriptions, ratings, and publication details.', + docsLink: 'https://docs.sim.ai/tools/google_books', + category: 'tools', + bgColor: '#E0E0E0', + icon: GoogleBooksIcon, + + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Search Volumes', id: 'volume_search' }, + { label: 'Get Volume Details', id: 'volume_details' }, + ], + value: () => 'volume_search', + }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + password: true, + placeholder: 'Enter your Google Books API key', + required: true, + }, + { + id: 'query', + title: 'Search Query', + type: 'short-input', + placeholder: 'e.g., intitle:harry potter inauthor:rowling', + condition: { field: 'operation', value: 'volume_search' }, + required: { field: 'operation', value: 'volume_search' }, + }, + { + id: 'filter', + title: 'Filter', + type: 'dropdown', + options: [ + { label: 'None', id: '' }, + { label: 'Partial Preview', id: 'partial' }, + { label: 'Full Preview', id: 'full' }, + { label: 'Free eBooks', id: 'free-ebooks' }, + { label: 'Paid eBooks', id: 'paid-ebooks' }, + { label: 'All eBooks', id: 'ebooks' }, + ], + condition: { field: 'operation', value: 'volume_search' }, + mode: 'advanced', + }, + { + id: 'printType', + title: 'Print Type', + type: 'dropdown', + options: [ + { label: 'All', id: 'all' }, + { label: 'Books', id: 'books' }, + { label: 'Magazines', id: 'magazines' }, + ], + value: () => 'all', + condition: { field: 'operation', value: 'volume_search' }, + mode: 'advanced', + }, + { + id: 'orderBy', + title: 'Order By', + type: 'dropdown', + options: [ + { label: 'Relevance', id: 'relevance' }, + { label: 'Newest', id: 'newest' }, + ], + value: () => 'relevance', + condition: { field: 'operation', value: 'volume_search' }, + mode: 'advanced', + }, + { + id: 'maxResults', + title: 'Max Results', + type: 'short-input', + placeholder: 'Number of results (1-40)', + condition: { field: 'operation', value: 'volume_search' }, + mode: 'advanced', + }, + { + id: 'startIndex', + title: 'Start Index', + type: 'short-input', + placeholder: 'Starting index for pagination', + condition: { field: 'operation', value: 'volume_search' }, + mode: 'advanced', + }, + { + id: 'langRestrict', + title: 'Language', + type: 'short-input', + placeholder: 'ISO 639-1 code (e.g., en, es, fr)', + condition: { field: 'operation', value: 'volume_search' }, + mode: 'advanced', + }, + { + id: 'volumeId', + title: 'Volume ID', + type: 'short-input', + placeholder: 'Google Books volume ID', + condition: { field: 'operation', value: 'volume_details' }, + required: { field: 'operation', value: 'volume_details' }, + }, + { + id: 'projection', + title: 'Projection', + type: 'dropdown', + options: [ + { label: 'Full', id: 'full' }, + { label: 'Lite', id: 'lite' }, + ], + value: () => 'full', + condition: { field: 'operation', value: 'volume_details' }, + mode: 'advanced', + }, + ], + + tools: { + access: ['google_books_volume_search', 'google_books_volume_details'], + config: { + tool: (params) => `google_books_${params.operation}`, + params: (params) => { + const { operation, ...rest } = params + + let maxResults: number | undefined + if (params.maxResults) { + maxResults = Number.parseInt(params.maxResults, 10) + if (Number.isNaN(maxResults)) { + maxResults = undefined + } + } + + let startIndex: number | undefined + if (params.startIndex) { + startIndex = Number.parseInt(params.startIndex, 10) + if (Number.isNaN(startIndex)) { + startIndex = undefined + } + } + + return { + ...rest, + maxResults, + startIndex, + filter: params.filter || undefined, + printType: params.printType || undefined, + orderBy: params.orderBy || undefined, + projection: params.projection || undefined, + } + }, + }, + }, + + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'Google Books API key' }, + query: { type: 'string', description: 'Search query' }, + filter: { type: 'string', description: 'Filter by availability' }, + printType: { type: 'string', description: 'Print type filter' }, + orderBy: { type: 'string', description: 'Sort order' }, + maxResults: { type: 'string', description: 'Maximum number of results' }, + startIndex: { type: 'string', description: 'Starting index for pagination' }, + langRestrict: { type: 'string', description: 'Language restriction' }, + volumeId: { type: 'string', description: 'Volume ID for details' }, + projection: { type: 'string', description: 'Projection level' }, + }, + + outputs: { + totalItems: { type: 'number', description: 'Total number of matching results' }, + volumes: { type: 'json', description: 'List of matching volumes' }, + id: { type: 'string', description: 'Volume ID' }, + title: { type: 'string', description: 'Book title' }, + subtitle: { type: 'string', description: 'Book subtitle' }, + authors: { type: 'json', description: 'List of authors' }, + publisher: { type: 'string', description: 'Publisher name' }, + publishedDate: { type: 'string', description: 'Publication date' }, + description: { type: 'string', description: 'Book description' }, + pageCount: { type: 'number', description: 'Number of pages' }, + categories: { type: 'json', description: 'Book categories' }, + averageRating: { type: 'number', description: 'Average rating (1-5)' }, + ratingsCount: { type: 'number', description: 'Number of ratings' }, + language: { type: 'string', description: 'Language code' }, + previewLink: { type: 'string', description: 'Link to preview on Google Books' }, + infoLink: { type: 'string', description: 'Link to info page' }, + thumbnailUrl: { type: 'string', description: 'Book cover thumbnail URL' }, + isbn10: { type: 'string', description: 'ISBN-10 identifier' }, + isbn13: { type: 'string', description: 'ISBN-13 identifier' }, + }, +} diff --git a/apps/sim/blocks/blocks/s3.ts b/apps/sim/blocks/blocks/s3.ts index 10491a078..30fabd9d3 100644 --- a/apps/sim/blocks/blocks/s3.ts +++ b/apps/sim/blocks/blocks/s3.ts @@ -58,6 +58,16 @@ export const S3Block: BlockConfig = { }, required: true, }, + { + id: 'getObjectRegion', + title: 'AWS Region', + type: 'short-input', + placeholder: 'Used when S3 URL does not include region', + condition: { + field: 'operation', + value: ['get_object'], + }, + }, { id: 'bucketName', title: 'Bucket Name', @@ -291,34 +301,11 @@ export const S3Block: BlockConfig = { if (!params.s3Uri) { throw new Error('S3 Object URL is required') } - - // Parse S3 URI for get_object - try { - const url = new URL(params.s3Uri) - const hostname = url.hostname - const bucketName = hostname.split('.')[0] - const regionMatch = hostname.match(/s3[.-]([^.]+)\.amazonaws\.com/) - const region = regionMatch ? regionMatch[1] : params.region - const objectKey = url.pathname.startsWith('/') - ? url.pathname.substring(1) - : url.pathname - - if (!bucketName || !objectKey) { - throw new Error('Could not parse S3 URL') - } - - return { - accessKeyId: params.accessKeyId, - secretAccessKey: params.secretAccessKey, - region, - bucketName, - objectKey, - s3Uri: params.s3Uri, - } - } catch (_error) { - throw new Error( - 'Invalid S3 Object URL format. Expected: https://bucket-name.s3.region.amazonaws.com/path/to/file' - ) + return { + accessKeyId: params.accessKeyId, + secretAccessKey: params.secretAccessKey, + region: params.getObjectRegion || params.region, + s3Uri: params.s3Uri, } } @@ -401,6 +388,7 @@ export const S3Block: BlockConfig = { acl: { type: 'string', description: 'Access control list' }, // Download inputs s3Uri: { type: 'string', description: 'S3 object URL' }, + getObjectRegion: { type: 'string', description: 'Optional AWS region override for downloads' }, // List inputs prefix: { type: 'string', description: 'Prefix filter' }, maxKeys: { type: 'number', description: 'Maximum results' }, diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 301b7b350..f51019da2 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -39,6 +39,7 @@ import { GitHubBlock, GitHubV2Block } from '@/blocks/blocks/github' import { GitLabBlock } from '@/blocks/blocks/gitlab' import { GmailBlock, GmailV2Block } from '@/blocks/blocks/gmail' import { GoogleSearchBlock } from '@/blocks/blocks/google' +import { GoogleBooksBlock } from '@/blocks/blocks/google_books' import { GoogleCalendarBlock, GoogleCalendarV2Block } from '@/blocks/blocks/google_calendar' import { GoogleDocsBlock } from '@/blocks/blocks/google_docs' import { GoogleDriveBlock } from '@/blocks/blocks/google_drive' @@ -214,6 +215,7 @@ export const registry: Record = { gmail_v2: GmailV2Block, google_calendar: GoogleCalendarBlock, google_calendar_v2: GoogleCalendarV2Block, + google_books: GoogleBooksBlock, google_docs: GoogleDocsBlock, google_drive: GoogleDriveBlock, google_forms: GoogleFormsBlock, diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 08a716925..8ac262bef 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -196,6 +196,8 @@ export interface SubBlockConfig { type: SubBlockType mode?: 'basic' | 'advanced' | 'both' | 'trigger' // Default is 'both' if not specified. 'trigger' means only shown in trigger mode canonicalParamId?: string + /** Controls parameter visibility in agent/tool-input context */ + paramVisibility?: 'user-or-llm' | 'user-only' | 'llm-only' | 'hidden' required?: | boolean | { diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index f13fc8aa8..dfb95dab2 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -1157,6 +1157,21 @@ export function AirweaveIcon(props: SVGProps) { ) } +export function GoogleBooksIcon(props: SVGProps) { + return ( + + + + + ) +} + export function GoogleDocsIcon(props: SVGProps) { return ( ({ }), })) +vi.mock('@/executor/utils/http', () => ({ + buildAuthHeaders: vi.fn().mockResolvedValue({ 'Content-Type': 'application/json' }), + buildAPIUrl: vi.fn((path: string, params?: Record) => { + const url = new URL(path, 'http://localhost:3000') + if (params) { + for (const [key, value] of Object.entries(params)) { + if (value !== undefined && value !== null) { + url.searchParams.set(key, value) + } + } + } + return url + }), + extractAPIErrorMessage: vi.fn(async (response: Response) => { + const defaultMessage = `API request failed with status ${response.status}` + try { + const errorData = await response.json() + return errorData.error || defaultMessage + } catch { + return defaultMessage + } + }), +})) + vi.mock('@sim/db', () => ({ db: { select: vi.fn().mockReturnValue({ @@ -84,7 +109,7 @@ vi.mock('@sim/db/schema', () => ({ }, })) -global.fetch = Object.assign(vi.fn(), { preconnect: vi.fn() }) as typeof fetch +setupGlobalFetchMock() const mockGetAllBlocks = getAllBlocks as Mock const mockExecuteTool = executeTool as Mock @@ -1901,5 +1926,301 @@ describe('AgentBlockHandler', () => { expect(discoveryCalls[0].url).toContain('serverId=mcp-legacy-server') }) + + describe('customToolId resolution - DB as source of truth', () => { + const staleInlineSchema = { + function: { + name: 'formatReport', + description: 'Formats a report', + parameters: { + type: 'object', + properties: { + title: { type: 'string', description: 'Report title' }, + content: { type: 'string', description: 'Report content' }, + }, + required: ['title', 'content'], + }, + }, + } + + const dbSchema = { + function: { + name: 'formatReport', + description: 'Formats a report', + parameters: { + type: 'object', + properties: { + title: { type: 'string', description: 'Report title' }, + content: { type: 'string', description: 'Report content' }, + format: { type: 'string', description: 'Output format' }, + }, + required: ['title', 'content', 'format'], + }, + }, + } + + const staleInlineCode = 'return { title, content };' + const dbCode = 'return { title, content, format };' + + function mockFetchForCustomTool(toolId: string) { + mockFetch.mockImplementation((url: string) => { + if (typeof url === 'string' && url.includes('/api/tools/custom')) { + return Promise.resolve({ + ok: true, + headers: { get: () => null }, + json: () => + Promise.resolve({ + data: [ + { + id: toolId, + title: 'formatReport', + schema: dbSchema, + code: dbCode, + }, + ], + }), + }) + } + return Promise.resolve({ + ok: true, + headers: { get: () => null }, + json: () => Promise.resolve({}), + }) + }) + } + + function mockFetchFailure() { + mockFetch.mockImplementation((url: string) => { + if (typeof url === 'string' && url.includes('/api/tools/custom')) { + return Promise.resolve({ + ok: false, + status: 500, + headers: { get: () => null }, + json: () => Promise.resolve({}), + }) + } + return Promise.resolve({ + ok: true, + headers: { get: () => null }, + json: () => Promise.resolve({}), + }) + }) + } + + beforeEach(() => { + Object.defineProperty(global, 'window', { + value: undefined, + writable: true, + configurable: true, + }) + }) + + it('should always fetch latest schema from DB when customToolId is present', async () => { + const toolId = 'custom-tool-123' + mockFetchForCustomTool(toolId) + + const inputs = { + model: 'gpt-4o', + userPrompt: 'Format a report', + apiKey: 'test-api-key', + tools: [ + { + type: 'custom-tool', + customToolId: toolId, + title: 'formatReport', + schema: staleInlineSchema, + code: staleInlineCode, + usageControl: 'auto' as const, + }, + ], + } + + mockGetProviderFromModel.mockReturnValue('openai') + + await handler.execute(mockContext, mockBlock, inputs) + + expect(mockExecuteProviderRequest).toHaveBeenCalled() + const providerCall = mockExecuteProviderRequest.mock.calls[0] + const tools = providerCall[1].tools + + expect(tools.length).toBe(1) + // DB schema wins over stale inline — includes format param + expect(tools[0].parameters.required).toContain('format') + expect(tools[0].parameters.properties).toHaveProperty('format') + }) + + it('should fetch from DB when customToolId has no inline schema', async () => { + const toolId = 'custom-tool-123' + mockFetchForCustomTool(toolId) + + const inputs = { + model: 'gpt-4o', + userPrompt: 'Format a report', + apiKey: 'test-api-key', + tools: [ + { + type: 'custom-tool', + customToolId: toolId, + usageControl: 'auto' as const, + }, + ], + } + + mockGetProviderFromModel.mockReturnValue('openai') + + await handler.execute(mockContext, mockBlock, inputs) + + expect(mockExecuteProviderRequest).toHaveBeenCalled() + const providerCall = mockExecuteProviderRequest.mock.calls[0] + const tools = providerCall[1].tools + + expect(tools.length).toBe(1) + expect(tools[0].name).toBe('formatReport') + expect(tools[0].parameters.required).toContain('format') + }) + + it('should fall back to inline schema when DB fetch fails and inline exists', async () => { + mockFetchFailure() + + const inputs = { + model: 'gpt-4o', + userPrompt: 'Format a report', + apiKey: 'test-api-key', + tools: [ + { + type: 'custom-tool', + customToolId: 'custom-tool-123', + title: 'formatReport', + schema: staleInlineSchema, + code: staleInlineCode, + usageControl: 'auto' as const, + }, + ], + } + + mockGetProviderFromModel.mockReturnValue('openai') + + await handler.execute(mockContext, mockBlock, inputs) + + expect(mockExecuteProviderRequest).toHaveBeenCalled() + const providerCall = mockExecuteProviderRequest.mock.calls[0] + const tools = providerCall[1].tools + + expect(tools.length).toBe(1) + expect(tools[0].name).toBe('formatReport') + expect(tools[0].parameters.required).not.toContain('format') + }) + + it('should return null when DB fetch fails and no inline schema exists', async () => { + mockFetchFailure() + + const inputs = { + model: 'gpt-4o', + userPrompt: 'Format a report', + apiKey: 'test-api-key', + tools: [ + { + type: 'custom-tool', + customToolId: 'custom-tool-123', + usageControl: 'auto' as const, + }, + ], + } + + mockGetProviderFromModel.mockReturnValue('openai') + + await handler.execute(mockContext, mockBlock, inputs) + + expect(mockExecuteProviderRequest).toHaveBeenCalled() + const providerCall = mockExecuteProviderRequest.mock.calls[0] + const tools = providerCall[1].tools + + expect(tools.length).toBe(0) + }) + + it('should use DB code for executeFunction when customToolId resolves', async () => { + const toolId = 'custom-tool-123' + mockFetchForCustomTool(toolId) + + let capturedTools: any[] = [] + Promise.all = vi.fn().mockImplementation((promises: Promise[]) => { + const result = originalPromiseAll.call(Promise, promises) + result.then((tools: any[]) => { + if (tools?.length) { + capturedTools = tools.filter((t) => t !== null) + } + }) + return result + }) + + const inputs = { + model: 'gpt-4o', + userPrompt: 'Format a report', + apiKey: 'test-api-key', + tools: [ + { + type: 'custom-tool', + customToolId: toolId, + title: 'formatReport', + schema: staleInlineSchema, + code: staleInlineCode, + usageControl: 'auto' as const, + }, + ], + } + + mockGetProviderFromModel.mockReturnValue('openai') + + await handler.execute(mockContext, mockBlock, inputs) + + expect(capturedTools.length).toBe(1) + expect(typeof capturedTools[0].executeFunction).toBe('function') + + await capturedTools[0].executeFunction({ title: 'Q1', format: 'pdf' }) + + expect(mockExecuteTool).toHaveBeenCalledWith( + 'function_execute', + expect.objectContaining({ + code: dbCode, + }), + false, + expect.any(Object) + ) + }) + + it('should not fetch from DB when no customToolId is present', async () => { + const inputs = { + model: 'gpt-4o', + userPrompt: 'Use the tool', + apiKey: 'test-api-key', + tools: [ + { + type: 'custom-tool', + title: 'formatReport', + schema: staleInlineSchema, + code: staleInlineCode, + usageControl: 'auto' as const, + }, + ], + } + + mockGetProviderFromModel.mockReturnValue('openai') + + await handler.execute(mockContext, mockBlock, inputs) + + const customToolFetches = mockFetch.mock.calls.filter( + (call: any[]) => typeof call[0] === 'string' && call[0].includes('/api/tools/custom') + ) + expect(customToolFetches.length).toBe(0) + + expect(mockExecuteProviderRequest).toHaveBeenCalled() + const providerCall = mockExecuteProviderRequest.mock.calls[0] + const tools = providerCall[1].tools + + expect(tools.length).toBe(1) + expect(tools[0].name).toBe('formatReport') + expect(tools[0].parameters.required).not.toContain('format') + }) + }) }) }) diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index 7cba8deb7..f87b3cfde 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -62,9 +62,12 @@ export class AgentBlockHandler implements BlockHandler { await validateModelProvider(ctx.userId, model, ctx) const providerId = getProviderFromModel(model) - const formattedTools = await this.formatTools(ctx, filteredInputs.tools || []) + const formattedTools = await this.formatTools( + ctx, + filteredInputs.tools || [], + block.canonicalModes + ) - // Resolve skill metadata for progressive disclosure const skillInputs = filteredInputs.skills ?? [] let skillMetadata: Array<{ name: string; description: string }> = [] if (skillInputs.length > 0 && ctx.workspaceId) { @@ -221,7 +224,11 @@ export class AgentBlockHandler implements BlockHandler { }) } - private async formatTools(ctx: ExecutionContext, inputTools: ToolInput[]): Promise { + private async formatTools( + ctx: ExecutionContext, + inputTools: ToolInput[], + canonicalModes?: Record + ): Promise { if (!Array.isArray(inputTools)) return [] const filtered = inputTools.filter((tool) => { @@ -249,7 +256,7 @@ export class AgentBlockHandler implements BlockHandler { if (tool.type === 'custom-tool' && (tool.schema || tool.customToolId)) { return await this.createCustomTool(ctx, tool) } - return this.transformBlockTool(ctx, tool) + return this.transformBlockTool(ctx, tool, canonicalModes) } catch (error) { logger.error(`[AgentHandler] Error creating tool:`, { tool, error }) return null @@ -272,15 +279,16 @@ export class AgentBlockHandler implements BlockHandler { let code = tool.code let title = tool.title - if (tool.customToolId && !schema) { + if (tool.customToolId) { const resolved = await this.fetchCustomToolById(ctx, tool.customToolId) - if (!resolved) { + if (resolved) { + schema = resolved.schema + code = resolved.code + title = resolved.title + } else if (!schema) { logger.error(`Custom tool not found: ${tool.customToolId}`) return null } - schema = resolved.schema - code = resolved.code - title = resolved.title } if (!schema?.function) { @@ -719,12 +727,17 @@ export class AgentBlockHandler implements BlockHandler { } } - private async transformBlockTool(ctx: ExecutionContext, tool: ToolInput) { + private async transformBlockTool( + ctx: ExecutionContext, + tool: ToolInput, + canonicalModes?: Record + ) { const transformedTool = await transformBlockTool(tool, { selectedOperation: tool.operation, getAllBlocks, getToolAsync: (toolId: string) => getToolAsync(toolId, ctx.workflowId), getTool, + canonicalModes, }) if (transformedTool) { diff --git a/apps/sim/executor/handlers/router/router-handler.ts b/apps/sim/executor/handlers/router/router-handler.ts index a42956c66..5c22a1c49 100644 --- a/apps/sim/executor/handlers/router/router-handler.ts +++ b/apps/sim/executor/handlers/router/router-handler.ts @@ -2,7 +2,7 @@ import { db } from '@sim/db' import { account } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { eq } from 'drizzle-orm' -import { getBaseUrl } from '@/lib/core/utils/urls' +import { getInternalApiBaseUrl } from '@/lib/core/utils/urls' import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils' import { generateRouterPrompt, generateRouterV2Prompt } from '@/blocks/blocks/router' import type { BlockOutput } from '@/blocks/types' @@ -79,7 +79,7 @@ export class RouterBlockHandler implements BlockHandler { const providerId = getProviderFromModel(routerConfig.model) try { - const url = new URL('/api/providers', getBaseUrl()) + const url = new URL('/api/providers', getInternalApiBaseUrl()) if (ctx.userId) url.searchParams.set('userId', ctx.userId) const messages = [{ role: 'user', content: routerConfig.prompt }] @@ -209,7 +209,7 @@ export class RouterBlockHandler implements BlockHandler { const providerId = getProviderFromModel(routerConfig.model) try { - const url = new URL('/api/providers', getBaseUrl()) + const url = new URL('/api/providers', getInternalApiBaseUrl()) if (ctx.userId) url.searchParams.set('userId', ctx.userId) const messages = [{ role: 'user', content: routerConfig.context }] diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.test.ts b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts index 5218dbc05..661796db9 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.test.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts @@ -1,3 +1,4 @@ +import { setupGlobalFetchMock } from '@sim/testing' import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' import { BlockType } from '@/executor/constants' import { WorkflowBlockHandler } from '@/executor/handlers/workflow/workflow-handler' @@ -9,7 +10,7 @@ vi.mock('@/lib/auth/internal', () => ({ })) // Mock fetch globally -global.fetch = vi.fn() +setupGlobalFetchMock() describe('WorkflowBlockHandler', () => { let handler: WorkflowBlockHandler diff --git a/apps/sim/executor/utils/http.ts b/apps/sim/executor/utils/http.ts index 5562e4567..ac4792dd7 100644 --- a/apps/sim/executor/utils/http.ts +++ b/apps/sim/executor/utils/http.ts @@ -1,5 +1,5 @@ import { generateInternalToken } from '@/lib/auth/internal' -import { getBaseUrl } from '@/lib/core/utils/urls' +import { getBaseUrl, getInternalApiBaseUrl } from '@/lib/core/utils/urls' import { HTTP } from '@/executor/constants' export async function buildAuthHeaders(): Promise> { @@ -16,7 +16,8 @@ export async function buildAuthHeaders(): Promise> { } export function buildAPIUrl(path: string, params?: Record): URL { - const url = new URL(path, getBaseUrl()) + const baseUrl = path.startsWith('/api/') ? getInternalApiBaseUrl() : getBaseUrl() + const url = new URL(path, baseUrl) if (params) { for (const [key, value] of Object.entries(params)) { diff --git a/apps/sim/hooks/queries/deployments.ts b/apps/sim/hooks/queries/deployments.ts index 894e1152c..e2f5b5ffe 100644 --- a/apps/sim/hooks/queries/deployments.ts +++ b/apps/sim/hooks/queries/deployments.ts @@ -423,7 +423,7 @@ interface GenerateVersionDescriptionVariables { const VERSION_DESCRIPTION_SYSTEM_PROMPT = `You are writing deployment version descriptions for a workflow automation platform. -Write a brief, factual description (1-3 sentences, under 400 characters) that states what changed between versions. +Write a brief, factual description (1-3 sentences, under 2000 characters) that states what changed between versions. Guidelines: - Use the specific values provided (credential names, channel names, model names) diff --git a/apps/sim/hooks/queries/workflows.ts b/apps/sim/hooks/queries/workflows.ts index 19effa8bd..5e50194c4 100644 --- a/apps/sim/hooks/queries/workflows.ts +++ b/apps/sim/hooks/queries/workflows.ts @@ -642,6 +642,10 @@ export function useDeployChildWorkflow() { queryClient.invalidateQueries({ queryKey: workflowKeys.deploymentStatus(variables.workflowId), }) + // Invalidate workflow state so tool input mappings refresh + queryClient.invalidateQueries({ + queryKey: workflowKeys.state(variables.workflowId), + }) // Also invalidate deployment queries queryClient.invalidateQueries({ queryKey: deploymentKeys.info(variables.workflowId), diff --git a/apps/sim/hooks/use-execution-stream.ts b/apps/sim/hooks/use-execution-stream.ts index e664788b5..2ab98059f 100644 --- a/apps/sim/hooks/use-execution-stream.ts +++ b/apps/sim/hooks/use-execution-stream.ts @@ -1,4 +1,4 @@ -import { useCallback, useRef } from 'react' +import { useCallback } from 'react' import { createLogger } from '@sim/logger' import type { BlockCompletedData, @@ -16,6 +16,18 @@ import type { SerializableExecutionState } from '@/executor/execution/types' const logger = createLogger('useExecutionStream') +/** + * Detects errors caused by the browser killing a fetch (page refresh, navigation, tab close). + * These should be treated as clean disconnects, not execution errors. + */ +function isClientDisconnectError(error: any): boolean { + if (error.name === 'AbortError') return true + const msg = (error.message ?? '').toLowerCase() + return ( + msg.includes('network error') || msg.includes('failed to fetch') || msg.includes('load failed') + ) +} + /** * Processes SSE events from a response body and invokes appropriate callbacks. */ @@ -121,6 +133,7 @@ export interface ExecuteStreamOptions { parallels?: Record } stopAfterBlockId?: string + onExecutionId?: (executionId: string) => void callbacks?: ExecutionStreamCallbacks } @@ -129,30 +142,40 @@ export interface ExecuteFromBlockOptions { startBlockId: string sourceSnapshot: SerializableExecutionState input?: any + onExecutionId?: (executionId: string) => void callbacks?: ExecutionStreamCallbacks } +export interface ReconnectStreamOptions { + workflowId: string + executionId: string + fromEventId?: number + callbacks?: ExecutionStreamCallbacks +} + +/** + * Module-level map shared across all hook instances. + * Ensures ANY instance can cancel streams started by ANY other instance, + * which is critical for SPA navigation where the original hook instance unmounts + * but the SSE stream must be cancellable from the new instance. + */ +const sharedAbortControllers = new Map() + /** * Hook for executing workflows via server-side SSE streaming. * Supports concurrent executions via per-workflow AbortController maps. */ export function useExecutionStream() { - const abortControllersRef = useRef>(new Map()) - const currentExecutionsRef = useRef>( - new Map() - ) - const execute = useCallback(async (options: ExecuteStreamOptions) => { - const { workflowId, callbacks = {}, ...payload } = options + const { workflowId, callbacks = {}, onExecutionId, ...payload } = options - const existing = abortControllersRef.current.get(workflowId) + const existing = sharedAbortControllers.get(workflowId) if (existing) { existing.abort() } const abortController = new AbortController() - abortControllersRef.current.set(workflowId, abortController) - currentExecutionsRef.current.delete(workflowId) + sharedAbortControllers.set(workflowId, abortController) try { const response = await fetch(`/api/workflows/${workflowId}/execute`, { @@ -177,42 +200,48 @@ export function useExecutionStream() { throw new Error('No response body') } - const executionId = response.headers.get('X-Execution-Id') - if (executionId) { - currentExecutionsRef.current.set(workflowId, { workflowId, executionId }) + const serverExecutionId = response.headers.get('X-Execution-Id') + if (serverExecutionId) { + onExecutionId?.(serverExecutionId) } const reader = response.body.getReader() await processSSEStream(reader, callbacks, 'Execution') } catch (error: any) { - if (error.name === 'AbortError') { - logger.info('Execution stream cancelled') - callbacks.onExecutionCancelled?.({ duration: 0 }) - } else { - logger.error('Execution stream error:', error) - callbacks.onExecutionError?.({ - error: error.message || 'Unknown error', - duration: 0, - }) + if (isClientDisconnectError(error)) { + logger.info('Execution stream disconnected (page unload or abort)') + return } + logger.error('Execution stream error:', error) + callbacks.onExecutionError?.({ + error: error.message || 'Unknown error', + duration: 0, + }) throw error } finally { - abortControllersRef.current.delete(workflowId) - currentExecutionsRef.current.delete(workflowId) + if (sharedAbortControllers.get(workflowId) === abortController) { + sharedAbortControllers.delete(workflowId) + } } }, []) const executeFromBlock = useCallback(async (options: ExecuteFromBlockOptions) => { - const { workflowId, startBlockId, sourceSnapshot, input, callbacks = {} } = options + const { + workflowId, + startBlockId, + sourceSnapshot, + input, + onExecutionId, + callbacks = {}, + } = options - const existing = abortControllersRef.current.get(workflowId) + const existing = sharedAbortControllers.get(workflowId) if (existing) { existing.abort() } const abortController = new AbortController() - abortControllersRef.current.set(workflowId, abortController) - currentExecutionsRef.current.delete(workflowId) + sharedAbortControllers.set(workflowId, abortController) try { const response = await fetch(`/api/workflows/${workflowId}/execute`, { @@ -246,64 +275,80 @@ export function useExecutionStream() { throw new Error('No response body') } - const executionId = response.headers.get('X-Execution-Id') - if (executionId) { - currentExecutionsRef.current.set(workflowId, { workflowId, executionId }) + const serverExecutionId = response.headers.get('X-Execution-Id') + if (serverExecutionId) { + onExecutionId?.(serverExecutionId) } const reader = response.body.getReader() await processSSEStream(reader, callbacks, 'Run-from-block') } catch (error: any) { - if (error.name === 'AbortError') { - logger.info('Run-from-block execution cancelled') - callbacks.onExecutionCancelled?.({ duration: 0 }) - } else { - logger.error('Run-from-block execution error:', error) - callbacks.onExecutionError?.({ - error: error.message || 'Unknown error', - duration: 0, - }) + if (isClientDisconnectError(error)) { + logger.info('Run-from-block stream disconnected (page unload or abort)') + return } + logger.error('Run-from-block execution error:', error) + callbacks.onExecutionError?.({ + error: error.message || 'Unknown error', + duration: 0, + }) throw error } finally { - abortControllersRef.current.delete(workflowId) - currentExecutionsRef.current.delete(workflowId) + if (sharedAbortControllers.get(workflowId) === abortController) { + sharedAbortControllers.delete(workflowId) + } + } + }, []) + + const reconnect = useCallback(async (options: ReconnectStreamOptions) => { + const { workflowId, executionId, fromEventId = 0, callbacks = {} } = options + + const existing = sharedAbortControllers.get(workflowId) + if (existing) { + existing.abort() + } + + const abortController = new AbortController() + sharedAbortControllers.set(workflowId, abortController) + try { + const response = await fetch( + `/api/workflows/${workflowId}/executions/${executionId}/stream?from=${fromEventId}`, + { signal: abortController.signal } + ) + if (!response.ok) throw new Error(`Reconnect failed (${response.status})`) + if (!response.body) throw new Error('No response body') + + await processSSEStream(response.body.getReader(), callbacks, 'Reconnect') + } catch (error: any) { + if (isClientDisconnectError(error)) return + logger.error('Reconnection stream error:', error) + throw error + } finally { + if (sharedAbortControllers.get(workflowId) === abortController) { + sharedAbortControllers.delete(workflowId) + } } }, []) const cancel = useCallback((workflowId?: string) => { if (workflowId) { - const execution = currentExecutionsRef.current.get(workflowId) - if (execution) { - fetch(`/api/workflows/${execution.workflowId}/executions/${execution.executionId}/cancel`, { - method: 'POST', - }).catch(() => {}) - } - - const controller = abortControllersRef.current.get(workflowId) + const controller = sharedAbortControllers.get(workflowId) if (controller) { controller.abort() - abortControllersRef.current.delete(workflowId) + sharedAbortControllers.delete(workflowId) } - currentExecutionsRef.current.delete(workflowId) } else { - for (const [, execution] of currentExecutionsRef.current) { - fetch(`/api/workflows/${execution.workflowId}/executions/${execution.executionId}/cancel`, { - method: 'POST', - }).catch(() => {}) - } - - for (const [, controller] of abortControllersRef.current) { + for (const [, controller] of sharedAbortControllers) { controller.abort() } - abortControllersRef.current.clear() - currentExecutionsRef.current.clear() + sharedAbortControllers.clear() } }, []) return { execute, executeFromBlock, + reconnect, cancel, } } diff --git a/apps/sim/hooks/use-referral-attribution.ts b/apps/sim/hooks/use-referral-attribution.ts new file mode 100644 index 000000000..a42305b6d --- /dev/null +++ b/apps/sim/hooks/use-referral-attribution.ts @@ -0,0 +1,46 @@ +'use client' + +import { useEffect, useRef } from 'react' +import { createLogger } from '@sim/logger' + +const logger = createLogger('ReferralAttribution') + +const COOKIE_NAME = 'sim_utm' + +const TERMINAL_REASONS = new Set([ + 'invalid_cookie', + 'no_utm_cookie', + 'no_matching_campaign', + 'already_attributed', +]) + +/** + * Fires a one-shot `POST /api/attribution` when a `sim_utm` cookie is present. + * Retries on transient failures; stops on terminal outcomes. + */ +export function useReferralAttribution() { + const calledRef = useRef(false) + + useEffect(() => { + if (calledRef.current) return + if (!document.cookie.includes(COOKIE_NAME)) return + + calledRef.current = true + + fetch('/api/attribution', { method: 'POST' }) + .then((res) => res.json()) + .then((data) => { + if (data.attributed) { + logger.info('Referral attribution successful', { bonusAmount: data.bonusAmount }) + } else if (data.error || TERMINAL_REASONS.has(data.reason)) { + logger.info('Referral attribution skipped', { reason: data.reason || data.error }) + } else { + calledRef.current = false + } + }) + .catch((err) => { + logger.warn('Referral attribution failed, will retry', { error: err }) + calledRef.current = false + }) + }, []) +} diff --git a/apps/sim/lib/billing/credits/bonus.ts b/apps/sim/lib/billing/credits/bonus.ts new file mode 100644 index 000000000..cb0da3056 --- /dev/null +++ b/apps/sim/lib/billing/credits/bonus.ts @@ -0,0 +1,64 @@ +import { db } from '@sim/db' +import { organization, userStats } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { eq, sql } from 'drizzle-orm' +import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription' +import type { DbOrTx } from '@/lib/db/types' + +const logger = createLogger('BonusCredits') + +/** + * Apply bonus credits to a user (e.g. referral bonuses, promotional codes). + * + * Detects the user's current plan and routes credits accordingly: + * - Free/Pro: adds to `userStats.creditBalance` and increments `currentUsageLimit` + * - Team/Enterprise: adds to `organization.creditBalance` and increments `orgUsageLimit` + * + * Uses direct increment (not recalculation) so it works correctly for free-tier + * users where `setUsageLimitForCredits` would compute planBase=0 and skip the update. + * + * @param tx - Optional Drizzle transaction context. When provided, all DB writes + * participate in the caller's transaction for atomicity. + */ +export async function applyBonusCredits( + userId: string, + amount: number, + tx?: DbOrTx +): Promise { + const dbCtx = tx ?? db + const subscription = await getHighestPrioritySubscription(userId) + const isTeamOrEnterprise = subscription?.plan === 'team' || subscription?.plan === 'enterprise' + + if (isTeamOrEnterprise && subscription?.referenceId) { + const orgId = subscription.referenceId + + await dbCtx + .update(organization) + .set({ + creditBalance: sql`${organization.creditBalance} + ${amount}`, + orgUsageLimit: sql`COALESCE(${organization.orgUsageLimit}, '0')::decimal + ${amount}`, + }) + .where(eq(organization.id, orgId)) + + logger.info('Applied bonus credits to organization', { + userId, + organizationId: orgId, + plan: subscription.plan, + amount, + }) + } else { + await dbCtx + .update(userStats) + .set({ + creditBalance: sql`${userStats.creditBalance} + ${amount}`, + currentUsageLimit: sql`COALESCE(${userStats.currentUsageLimit}, '0')::decimal + ${amount}`, + }) + .where(eq(userStats.userId, userId)) + + logger.info('Applied bonus credits to user', { + userId, + plan: subscription?.plan || 'free', + amount, + }) + } +} diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 31c9c36ad..b154fbdbb 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -220,6 +220,7 @@ export const env = createEnv({ SOCKET_SERVER_URL: z.string().url().optional(), // WebSocket server URL for real-time features SOCKET_PORT: z.number().optional(), // Port for WebSocket server PORT: z.number().optional(), // Main application port + INTERNAL_API_BASE_URL: z.string().optional(), // Optional internal base URL for server-side self-calls; must include protocol if set (e.g., http://sim-app.namespace.svc.cluster.local:3000) ALLOWED_ORIGINS: z.string().optional(), // CORS allowed origins // OAuth Integration Credentials - All optional, enables third-party integrations diff --git a/apps/sim/lib/core/utils/urls.ts b/apps/sim/lib/core/utils/urls.ts index 5021d4494..5be78eb1d 100644 --- a/apps/sim/lib/core/utils/urls.ts +++ b/apps/sim/lib/core/utils/urls.ts @@ -1,6 +1,19 @@ import { getEnv } from '@/lib/core/config/env' import { isProd } from '@/lib/core/config/feature-flags' +function hasHttpProtocol(url: string): boolean { + return /^https?:\/\//i.test(url) +} + +function normalizeBaseUrl(url: string): string { + if (hasHttpProtocol(url)) { + return url + } + + const protocol = isProd ? 'https://' : 'http://' + return `${protocol}${url}` +} + /** * Returns the base URL of the application from NEXT_PUBLIC_APP_URL * This ensures webhooks, callbacks, and other integrations always use the correct public URL @@ -8,7 +21,7 @@ import { isProd } from '@/lib/core/config/feature-flags' * @throws Error if NEXT_PUBLIC_APP_URL is not configured */ export function getBaseUrl(): string { - const baseUrl = getEnv('NEXT_PUBLIC_APP_URL') + const baseUrl = getEnv('NEXT_PUBLIC_APP_URL')?.trim() if (!baseUrl) { throw new Error( @@ -16,12 +29,26 @@ export function getBaseUrl(): string { ) } - if (baseUrl.startsWith('http://') || baseUrl.startsWith('https://')) { - return baseUrl + return normalizeBaseUrl(baseUrl) +} + +/** + * Returns the base URL used by server-side internal API calls. + * Falls back to NEXT_PUBLIC_APP_URL when INTERNAL_API_BASE_URL is not set. + */ +export function getInternalApiBaseUrl(): string { + const internalBaseUrl = getEnv('INTERNAL_API_BASE_URL')?.trim() + if (!internalBaseUrl) { + return getBaseUrl() } - const protocol = isProd ? 'https://' : 'http://' - return `${protocol}${baseUrl}` + if (!hasHttpProtocol(internalBaseUrl)) { + throw new Error( + 'INTERNAL_API_BASE_URL must include protocol (http:// or https://), e.g. http://sim-app.default.svc.cluster.local:3000' + ) + } + + return internalBaseUrl } /** diff --git a/apps/sim/lib/execution/event-buffer.ts b/apps/sim/lib/execution/event-buffer.ts new file mode 100644 index 000000000..4473a922f --- /dev/null +++ b/apps/sim/lib/execution/event-buffer.ts @@ -0,0 +1,246 @@ +import { createLogger } from '@sim/logger' +import { getRedisClient } from '@/lib/core/config/redis' +import type { ExecutionEvent } from '@/lib/workflows/executor/execution-events' + +const logger = createLogger('ExecutionEventBuffer') + +const REDIS_PREFIX = 'execution:stream:' +const TTL_SECONDS = 60 * 60 // 1 hour +const EVENT_LIMIT = 1000 +const RESERVE_BATCH = 100 +const FLUSH_INTERVAL_MS = 15 +const FLUSH_MAX_BATCH = 200 + +function getEventsKey(executionId: string) { + return `${REDIS_PREFIX}${executionId}:events` +} + +function getSeqKey(executionId: string) { + return `${REDIS_PREFIX}${executionId}:seq` +} + +function getMetaKey(executionId: string) { + return `${REDIS_PREFIX}${executionId}:meta` +} + +export type ExecutionStreamStatus = 'active' | 'complete' | 'error' | 'cancelled' + +export interface ExecutionStreamMeta { + status: ExecutionStreamStatus + userId?: string + workflowId?: string + updatedAt?: string +} + +export interface ExecutionEventEntry { + eventId: number + executionId: string + event: ExecutionEvent +} + +export interface ExecutionEventWriter { + write: (event: ExecutionEvent) => Promise + flush: () => Promise + close: () => Promise +} + +export async function setExecutionMeta( + executionId: string, + meta: Partial +): Promise { + const redis = getRedisClient() + if (!redis) { + logger.warn('setExecutionMeta: Redis client unavailable', { executionId }) + return + } + try { + const key = getMetaKey(executionId) + const payload: Record = { + updatedAt: new Date().toISOString(), + } + if (meta.status) payload.status = meta.status + if (meta.userId) payload.userId = meta.userId + if (meta.workflowId) payload.workflowId = meta.workflowId + await redis.hset(key, payload) + await redis.expire(key, TTL_SECONDS) + } catch (error) { + logger.warn('Failed to update execution meta', { + executionId, + error: error instanceof Error ? error.message : String(error), + }) + } +} + +export async function getExecutionMeta(executionId: string): Promise { + const redis = getRedisClient() + if (!redis) { + logger.warn('getExecutionMeta: Redis client unavailable', { executionId }) + return null + } + try { + const key = getMetaKey(executionId) + const meta = await redis.hgetall(key) + if (!meta || Object.keys(meta).length === 0) return null + return meta as unknown as ExecutionStreamMeta + } catch (error) { + logger.warn('Failed to read execution meta', { + executionId, + error: error instanceof Error ? error.message : String(error), + }) + return null + } +} + +export async function readExecutionEvents( + executionId: string, + afterEventId: number +): Promise { + const redis = getRedisClient() + if (!redis) return [] + try { + const raw = await redis.zrangebyscore(getEventsKey(executionId), afterEventId + 1, '+inf') + return raw + .map((entry) => { + try { + return JSON.parse(entry) as ExecutionEventEntry + } catch { + return null + } + }) + .filter((entry): entry is ExecutionEventEntry => Boolean(entry)) + } catch (error) { + logger.warn('Failed to read execution events', { + executionId, + error: error instanceof Error ? error.message : String(error), + }) + return [] + } +} + +export function createExecutionEventWriter(executionId: string): ExecutionEventWriter { + const redis = getRedisClient() + if (!redis) { + logger.warn( + 'createExecutionEventWriter: Redis client unavailable, events will not be buffered', + { + executionId, + } + ) + return { + write: async (event) => ({ eventId: 0, executionId, event }), + flush: async () => {}, + close: async () => {}, + } + } + + let pending: ExecutionEventEntry[] = [] + let nextEventId = 0 + let maxReservedId = 0 + let flushTimer: ReturnType | null = null + + const scheduleFlush = () => { + if (flushTimer) return + flushTimer = setTimeout(() => { + flushTimer = null + void flush() + }, FLUSH_INTERVAL_MS) + } + + const reserveIds = async (minCount: number) => { + const reserveCount = Math.max(RESERVE_BATCH, minCount) + const newMax = await redis.incrby(getSeqKey(executionId), reserveCount) + const startId = newMax - reserveCount + 1 + if (nextEventId === 0 || nextEventId > maxReservedId) { + nextEventId = startId + maxReservedId = newMax + } + } + + let flushPromise: Promise | null = null + let closed = false + const inflightWrites = new Set>() + + const doFlush = async () => { + if (pending.length === 0) return + const batch = pending + pending = [] + try { + const key = getEventsKey(executionId) + const zaddArgs: (string | number)[] = [] + for (const entry of batch) { + zaddArgs.push(entry.eventId, JSON.stringify(entry)) + } + const pipeline = redis.pipeline() + pipeline.zadd(key, ...zaddArgs) + pipeline.expire(key, TTL_SECONDS) + pipeline.expire(getSeqKey(executionId), TTL_SECONDS) + pipeline.zremrangebyrank(key, 0, -EVENT_LIMIT - 1) + await pipeline.exec() + } catch (error) { + logger.warn('Failed to flush execution events', { + executionId, + batchSize: batch.length, + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }) + pending = batch.concat(pending) + } + } + + const flush = async () => { + if (flushPromise) { + await flushPromise + return + } + flushPromise = doFlush() + try { + await flushPromise + } finally { + flushPromise = null + if (pending.length > 0) scheduleFlush() + } + } + + const writeCore = async (event: ExecutionEvent): Promise => { + if (closed) return { eventId: 0, executionId, event } + if (nextEventId === 0 || nextEventId > maxReservedId) { + await reserveIds(1) + } + const eventId = nextEventId++ + const entry: ExecutionEventEntry = { eventId, executionId, event } + pending.push(entry) + if (pending.length >= FLUSH_MAX_BATCH) { + await flush() + } else { + scheduleFlush() + } + return entry + } + + const write = (event: ExecutionEvent): Promise => { + const p = writeCore(event) + inflightWrites.add(p) + const remove = () => inflightWrites.delete(p) + p.then(remove, remove) + return p + } + + const close = async () => { + closed = true + if (flushTimer) { + clearTimeout(flushTimer) + flushTimer = null + } + if (inflightWrites.size > 0) { + await Promise.allSettled(inflightWrites) + } + if (flushPromise) { + await flushPromise + } + if (pending.length > 0) { + await doFlush() + } + } + + return { write, flush, close } +} diff --git a/apps/sim/lib/guardrails/validate_hallucination.ts b/apps/sim/lib/guardrails/validate_hallucination.ts index 621a7d803..658a528fc 100644 --- a/apps/sim/lib/guardrails/validate_hallucination.ts +++ b/apps/sim/lib/guardrails/validate_hallucination.ts @@ -2,7 +2,7 @@ import { db } from '@sim/db' import { account } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { eq } from 'drizzle-orm' -import { getBaseUrl } from '@/lib/core/utils/urls' +import { getInternalApiBaseUrl } from '@/lib/core/utils/urls' import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils' import { executeProviderRequest } from '@/providers' import { getProviderFromModel } from '@/providers/utils' @@ -61,7 +61,7 @@ async function queryKnowledgeBase( }) // Call the knowledge base search API directly - const searchUrl = `${getBaseUrl()}/api/knowledge/search` + const searchUrl = `${getInternalApiBaseUrl()}/api/knowledge/search` const response = await fetch(searchUrl, { method: 'POST', diff --git a/apps/sim/lib/knowledge/documents/document-processor.ts b/apps/sim/lib/knowledge/documents/document-processor.ts index 80789e81b..0185de495 100644 --- a/apps/sim/lib/knowledge/documents/document-processor.ts +++ b/apps/sim/lib/knowledge/documents/document-processor.ts @@ -539,8 +539,8 @@ async function executeMistralOCRRequest( const isInternalRoute = url.startsWith('/') if (isInternalRoute) { - const { getBaseUrl } = await import('@/lib/core/utils/urls') - url = `${getBaseUrl()}${url}` + const { getInternalApiBaseUrl } = await import('@/lib/core/utils/urls') + url = `${getInternalApiBaseUrl()}${url}` } let headers = diff --git a/apps/sim/lib/messaging/email/mailer.test.ts b/apps/sim/lib/messaging/email/mailer.test.ts index c78855e6e..327c8f496 100644 --- a/apps/sim/lib/messaging/email/mailer.test.ts +++ b/apps/sim/lib/messaging/email/mailer.test.ts @@ -1,4 +1,4 @@ -import { createEnvMock, createMockLogger } from '@sim/testing' +import { createEnvMock, loggerMock } from '@sim/testing' import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' /** @@ -10,10 +10,6 @@ import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' * mock functions can intercept. */ -const loggerMock = vi.hoisted(() => ({ - createLogger: () => createMockLogger(), -})) - const mockSend = vi.fn() const mockBatchSend = vi.fn() const mockAzureBeginSend = vi.fn() diff --git a/apps/sim/lib/messaging/email/unsubscribe.test.ts b/apps/sim/lib/messaging/email/unsubscribe.test.ts index 43f2cd581..5cfdce661 100644 --- a/apps/sim/lib/messaging/email/unsubscribe.test.ts +++ b/apps/sim/lib/messaging/email/unsubscribe.test.ts @@ -1,20 +1,8 @@ -import { createEnvMock, createMockLogger } from '@sim/testing' +import { createEnvMock, databaseMock, loggerMock } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { EmailType } from '@/lib/messaging/email/mailer' -const loggerMock = vi.hoisted(() => ({ - createLogger: () => createMockLogger(), -})) - -const mockDb = vi.hoisted(() => ({ - select: vi.fn(), - insert: vi.fn(), - update: vi.fn(), -})) - -vi.mock('@sim/db', () => ({ - db: mockDb, -})) +vi.mock('@sim/db', () => databaseMock) vi.mock('@sim/db/schema', () => ({ user: { id: 'id', email: 'email' }, @@ -30,6 +18,8 @@ vi.mock('drizzle-orm', () => ({ eq: vi.fn((a, b) => ({ type: 'eq', left: a, right: b })), })) +const mockDb = databaseMock.db as Record> + vi.mock('@/lib/core/config/env', () => createEnvMock({ BETTER_AUTH_SECRET: 'test-secret-key' })) vi.mock('@sim/logger', () => loggerMock) diff --git a/apps/sim/lib/webhooks/gmail-polling-service.ts b/apps/sim/lib/webhooks/gmail-polling-service.ts index 7e3bcca5d..9b391002e 100644 --- a/apps/sim/lib/webhooks/gmail-polling-service.ts +++ b/apps/sim/lib/webhooks/gmail-polling-service.ts @@ -11,7 +11,7 @@ import { and, eq, isNull, or, sql } from 'drizzle-orm' import { nanoid } from 'nanoid' import { isOrganizationOnTeamOrEnterprisePlan } from '@/lib/billing' import { pollingIdempotency } from '@/lib/core/idempotency/service' -import { getBaseUrl } from '@/lib/core/utils/urls' +import { getInternalApiBaseUrl } from '@/lib/core/utils/urls' import { getOAuthToken, refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' import type { GmailAttachment } from '@/tools/gmail/types' import { downloadAttachments, extractAttachmentInfo } from '@/tools/gmail/utils' @@ -691,7 +691,7 @@ async function processEmails( `[${requestId}] Sending ${config.includeRawEmail ? 'simplified + raw' : 'simplified'} email payload for ${email.id}` ) - const webhookUrl = `${getBaseUrl()}/api/webhooks/trigger/${webhookData.path}` + const webhookUrl = `${getInternalApiBaseUrl()}/api/webhooks/trigger/${webhookData.path}` const response = await fetch(webhookUrl, { method: 'POST', diff --git a/apps/sim/lib/webhooks/imap-polling-service.ts b/apps/sim/lib/webhooks/imap-polling-service.ts index 9d664531f..37fc4c621 100644 --- a/apps/sim/lib/webhooks/imap-polling-service.ts +++ b/apps/sim/lib/webhooks/imap-polling-service.ts @@ -7,7 +7,7 @@ import type { FetchMessageObject, MailboxLockObject } from 'imapflow' import { ImapFlow } from 'imapflow' import { nanoid } from 'nanoid' import { pollingIdempotency } from '@/lib/core/idempotency/service' -import { getBaseUrl } from '@/lib/core/utils/urls' +import { getInternalApiBaseUrl } from '@/lib/core/utils/urls' import { MAX_CONSECUTIVE_FAILURES } from '@/triggers/constants' const logger = createLogger('ImapPollingService') @@ -639,7 +639,7 @@ async function processEmails( timestamp: new Date().toISOString(), } - const webhookUrl = `${getBaseUrl()}/api/webhooks/trigger/${webhookData.path}` + const webhookUrl = `${getInternalApiBaseUrl()}/api/webhooks/trigger/${webhookData.path}` const response = await fetch(webhookUrl, { method: 'POST', diff --git a/apps/sim/lib/webhooks/outlook-polling-service.ts b/apps/sim/lib/webhooks/outlook-polling-service.ts index 1f1b48e0c..19a807928 100644 --- a/apps/sim/lib/webhooks/outlook-polling-service.ts +++ b/apps/sim/lib/webhooks/outlook-polling-service.ts @@ -12,7 +12,7 @@ import { htmlToText } from 'html-to-text' import { nanoid } from 'nanoid' import { isOrganizationOnTeamOrEnterprisePlan } from '@/lib/billing' import { pollingIdempotency } from '@/lib/core/idempotency' -import { getBaseUrl } from '@/lib/core/utils/urls' +import { getInternalApiBaseUrl } from '@/lib/core/utils/urls' import { getOAuthToken, refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' import { MAX_CONSECUTIVE_FAILURES } from '@/triggers/constants' @@ -601,7 +601,7 @@ async function processOutlookEmails( `[${requestId}] Processing email: ${email.subject} from ${email.from?.emailAddress?.address}` ) - const webhookUrl = `${getBaseUrl()}/api/webhooks/trigger/${webhookData.path}` + const webhookUrl = `${getInternalApiBaseUrl()}/api/webhooks/trigger/${webhookData.path}` const response = await fetch(webhookUrl, { method: 'POST', diff --git a/apps/sim/lib/webhooks/rss-polling-service.ts b/apps/sim/lib/webhooks/rss-polling-service.ts index 5fbdeaba3..d75daa62f 100644 --- a/apps/sim/lib/webhooks/rss-polling-service.ts +++ b/apps/sim/lib/webhooks/rss-polling-service.ts @@ -9,7 +9,7 @@ import { secureFetchWithPinnedIP, validateUrlWithDNS, } from '@/lib/core/security/input-validation.server' -import { getBaseUrl } from '@/lib/core/utils/urls' +import { getInternalApiBaseUrl } from '@/lib/core/utils/urls' import { MAX_CONSECUTIVE_FAILURES } from '@/triggers/constants' const logger = createLogger('RssPollingService') @@ -376,7 +376,7 @@ async function processRssItems( timestamp: new Date().toISOString(), } - const webhookUrl = `${getBaseUrl()}/api/webhooks/trigger/${webhookData.path}` + const webhookUrl = `${getInternalApiBaseUrl()}/api/webhooks/trigger/${webhookData.path}` const response = await fetch(webhookUrl, { method: 'POST', diff --git a/apps/sim/lib/workflows/comparison/compare.test.ts b/apps/sim/lib/workflows/comparison/compare.test.ts index be7b6e9c5..5fe6e5923 100644 --- a/apps/sim/lib/workflows/comparison/compare.test.ts +++ b/apps/sim/lib/workflows/comparison/compare.test.ts @@ -2364,6 +2364,261 @@ describe('hasWorkflowChanged', () => { }) }) + describe('Trigger Config Normalization (False Positive Prevention)', () => { + it.concurrent( + 'should not detect change when deployed has null fields but current has values from triggerConfig', + () => { + // Core scenario: deployed state has null individual fields, current state has + // values populated from triggerConfig at runtime by populateTriggerFieldsFromConfig + const deployedState = createWorkflowState({ + blocks: { + block1: createBlock('block1', { + type: 'starter', + subBlocks: { + signingSecret: { id: 'signingSecret', type: 'short-input', value: null }, + botToken: { id: 'botToken', type: 'short-input', value: null }, + triggerConfig: { + id: 'triggerConfig', + type: 'short-input', + value: { signingSecret: 'secret123', botToken: 'token456' }, + }, + }, + }), + }, + }) + + const currentState = createWorkflowState({ + blocks: { + block1: createBlock('block1', { + type: 'starter', + subBlocks: { + signingSecret: { id: 'signingSecret', type: 'short-input', value: 'secret123' }, + botToken: { id: 'botToken', type: 'short-input', value: 'token456' }, + triggerConfig: { + id: 'triggerConfig', + type: 'short-input', + value: { signingSecret: 'secret123', botToken: 'token456' }, + }, + }, + }), + }, + }) + + expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) + } + ) + + it.concurrent( + 'should detect change when user edits a trigger field to a different value', + () => { + const deployedState = createWorkflowState({ + blocks: { + block1: createBlock('block1', { + type: 'starter', + subBlocks: { + signingSecret: { id: 'signingSecret', type: 'short-input', value: null }, + triggerConfig: { + id: 'triggerConfig', + type: 'short-input', + value: { signingSecret: 'old-secret' }, + }, + }, + }), + }, + }) + + const currentState = createWorkflowState({ + blocks: { + block1: createBlock('block1', { + type: 'starter', + subBlocks: { + signingSecret: { id: 'signingSecret', type: 'short-input', value: 'new-secret' }, + triggerConfig: { + id: 'triggerConfig', + type: 'short-input', + value: { signingSecret: 'old-secret' }, + }, + }, + }), + }, + }) + + expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) + } + ) + + it.concurrent('should not detect change when both sides have no triggerConfig', () => { + const deployedState = createWorkflowState({ + blocks: { + block1: createBlock('block1', { + type: 'starter', + subBlocks: { + signingSecret: { id: 'signingSecret', type: 'short-input', value: null }, + }, + }), + }, + }) + + const currentState = createWorkflowState({ + blocks: { + block1: createBlock('block1', { + type: 'starter', + subBlocks: { + signingSecret: { id: 'signingSecret', type: 'short-input', value: null }, + }, + }), + }, + }) + + expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) + }) + + it.concurrent( + 'should not detect change when deployed has empty fields and triggerConfig populates them', + () => { + // Empty string is also treated as "empty" by normalizeTriggerConfigValues + const deployedState = createWorkflowState({ + blocks: { + block1: createBlock('block1', { + type: 'starter', + subBlocks: { + signingSecret: { id: 'signingSecret', type: 'short-input', value: '' }, + triggerConfig: { + id: 'triggerConfig', + type: 'short-input', + value: { signingSecret: 'secret123' }, + }, + }, + }), + }, + }) + + const currentState = createWorkflowState({ + blocks: { + block1: createBlock('block1', { + type: 'starter', + subBlocks: { + signingSecret: { id: 'signingSecret', type: 'short-input', value: 'secret123' }, + triggerConfig: { + id: 'triggerConfig', + type: 'short-input', + value: { signingSecret: 'secret123' }, + }, + }, + }), + }, + }) + + expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) + } + ) + + it.concurrent('should not detect change when triggerId differs', () => { + const deployedState = createWorkflowState({ + blocks: { + block1: createBlock('block1', { + type: 'starter', + subBlocks: { + model: { value: 'gpt-4' }, + triggerId: { value: null }, + }, + }), + }, + }) + + const currentState = createWorkflowState({ + blocks: { + block1: createBlock('block1', { + type: 'starter', + subBlocks: { + model: { value: 'gpt-4' }, + triggerId: { value: 'slack_webhook' }, + }, + }), + }, + }) + + expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) + }) + + it.concurrent( + 'should not detect change for namespaced system subBlock IDs like samplePayload_slack_webhook', + () => { + const deployedState = createWorkflowState({ + blocks: { + block1: createBlock('block1', { + type: 'starter', + subBlocks: { + model: { value: 'gpt-4' }, + samplePayload_slack_webhook: { value: 'old payload' }, + triggerInstructions_slack_webhook: { value: 'old instructions' }, + }, + }), + }, + }) + + const currentState = createWorkflowState({ + blocks: { + block1: createBlock('block1', { + type: 'starter', + subBlocks: { + model: { value: 'gpt-4' }, + samplePayload_slack_webhook: { value: 'new payload' }, + triggerInstructions_slack_webhook: { value: 'new instructions' }, + }, + }), + }, + }) + + expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) + } + ) + + it.concurrent( + 'should handle mixed scenario: some fields from triggerConfig, some user-edited', + () => { + const deployedState = createWorkflowState({ + blocks: { + block1: createBlock('block1', { + type: 'starter', + subBlocks: { + signingSecret: { id: 'signingSecret', type: 'short-input', value: null }, + botToken: { id: 'botToken', type: 'short-input', value: null }, + includeFiles: { id: 'includeFiles', type: 'switch', value: false }, + triggerConfig: { + id: 'triggerConfig', + type: 'short-input', + value: { signingSecret: 'secret123', botToken: 'token456' }, + }, + }, + }), + }, + }) + + const currentState = createWorkflowState({ + blocks: { + block1: createBlock('block1', { + type: 'starter', + subBlocks: { + signingSecret: { id: 'signingSecret', type: 'short-input', value: 'secret123' }, + botToken: { id: 'botToken', type: 'short-input', value: 'token456' }, + includeFiles: { id: 'includeFiles', type: 'switch', value: true }, + triggerConfig: { + id: 'triggerConfig', + type: 'short-input', + value: { signingSecret: 'secret123', botToken: 'token456' }, + }, + }, + }), + }, + }) + + // includeFiles changed from false to true — this IS a real change + expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) + } + ) + }) + describe('Trigger Runtime Metadata (Should Not Trigger Change)', () => { it.concurrent('should not detect change when webhookId differs', () => { const deployedState = createWorkflowState({ diff --git a/apps/sim/lib/workflows/comparison/compare.ts b/apps/sim/lib/workflows/comparison/compare.ts index ce37dd86a..f739f3f20 100644 --- a/apps/sim/lib/workflows/comparison/compare.ts +++ b/apps/sim/lib/workflows/comparison/compare.ts @@ -9,6 +9,7 @@ import { normalizeLoop, normalizeParallel, normalizeSubBlockValue, + normalizeTriggerConfigValues, normalizeValue, normalizeVariables, sanitizeVariable, @@ -172,14 +173,18 @@ export function generateWorkflowDiffSummary( } } + // Normalize trigger config values for both states before comparison + const normalizedCurrentSubs = normalizeTriggerConfigValues(currentSubBlocks) + const normalizedPreviousSubs = normalizeTriggerConfigValues(previousSubBlocks) + // Compare subBlocks using shared helper for filtering (single source of truth) const allSubBlockIds = filterSubBlockIds([ - ...new Set([...Object.keys(currentSubBlocks), ...Object.keys(previousSubBlocks)]), + ...new Set([...Object.keys(normalizedCurrentSubs), ...Object.keys(normalizedPreviousSubs)]), ]) for (const subId of allSubBlockIds) { - const currentSub = currentSubBlocks[subId] as Record | undefined - const previousSub = previousSubBlocks[subId] as Record | undefined + const currentSub = normalizedCurrentSubs[subId] as Record | undefined + const previousSub = normalizedPreviousSubs[subId] as Record | undefined if (!currentSub || !previousSub) { changes.push({ diff --git a/apps/sim/lib/workflows/comparison/normalize.test.ts b/apps/sim/lib/workflows/comparison/normalize.test.ts index 6c2cc5eb1..9aa6c9b12 100644 --- a/apps/sim/lib/workflows/comparison/normalize.test.ts +++ b/apps/sim/lib/workflows/comparison/normalize.test.ts @@ -4,10 +4,12 @@ import { describe, expect, it } from 'vitest' import type { Loop, Parallel } from '@/stores/workflows/workflow/types' import { + filterSubBlockIds, normalizedStringify, normalizeEdge, normalizeLoop, normalizeParallel, + normalizeTriggerConfigValues, normalizeValue, sanitizeInputFormat, sanitizeTools, @@ -584,4 +586,226 @@ describe('Workflow Normalization Utilities', () => { expect(result2).toBe(result3) }) }) + + describe('filterSubBlockIds', () => { + it.concurrent('should exclude exact SYSTEM_SUBBLOCK_IDS', () => { + const ids = ['signingSecret', 'samplePayload', 'triggerInstructions', 'botToken'] + const result = filterSubBlockIds(ids) + expect(result).toEqual(['botToken', 'signingSecret']) + }) + + it.concurrent('should exclude namespaced SYSTEM_SUBBLOCK_IDS (prefix matching)', () => { + const ids = [ + 'signingSecret', + 'samplePayload_slack_webhook', + 'triggerInstructions_slack_webhook', + 'webhookUrlDisplay_slack_webhook', + 'botToken', + ] + const result = filterSubBlockIds(ids) + expect(result).toEqual(['botToken', 'signingSecret']) + }) + + it.concurrent('should exclude exact TRIGGER_RUNTIME_SUBBLOCK_IDS', () => { + const ids = ['webhookId', 'triggerPath', 'triggerConfig', 'triggerId', 'signingSecret'] + const result = filterSubBlockIds(ids) + expect(result).toEqual(['signingSecret']) + }) + + it.concurrent('should not exclude IDs that merely contain a system ID substring', () => { + const ids = ['mySamplePayload', 'notSamplePayload'] + const result = filterSubBlockIds(ids) + expect(result).toEqual(['mySamplePayload', 'notSamplePayload']) + }) + + it.concurrent('should return sorted results', () => { + const ids = ['zebra', 'alpha', 'middle'] + const result = filterSubBlockIds(ids) + expect(result).toEqual(['alpha', 'middle', 'zebra']) + }) + + it.concurrent('should handle empty array', () => { + expect(filterSubBlockIds([])).toEqual([]) + }) + + it.concurrent('should handle all IDs being excluded', () => { + const ids = ['webhookId', 'triggerPath', 'samplePayload', 'triggerConfig'] + const result = filterSubBlockIds(ids) + expect(result).toEqual([]) + }) + + it.concurrent('should exclude setupScript and scheduleInfo namespaced variants', () => { + const ids = ['setupScript_google_sheets_row', 'scheduleInfo_cron_trigger', 'realField'] + const result = filterSubBlockIds(ids) + expect(result).toEqual(['realField']) + }) + + it.concurrent('should exclude triggerCredentials namespaced variants', () => { + const ids = ['triggerCredentials_slack_webhook', 'signingSecret'] + const result = filterSubBlockIds(ids) + expect(result).toEqual(['signingSecret']) + }) + + it.concurrent('should exclude synthetic tool-input subBlock IDs', () => { + const ids = [ + 'toolConfig', + 'toolConfig-tool-0-query', + 'toolConfig-tool-0-url', + 'toolConfig-tool-1-status', + 'systemPrompt', + ] + const result = filterSubBlockIds(ids) + expect(result).toEqual(['systemPrompt', 'toolConfig']) + }) + }) + + describe('normalizeTriggerConfigValues', () => { + it.concurrent('should return subBlocks unchanged when no triggerConfig exists', () => { + const subBlocks = { + signingSecret: { id: 'signingSecret', type: 'short-input', value: 'secret123' }, + botToken: { id: 'botToken', type: 'short-input', value: 'token456' }, + } + const result = normalizeTriggerConfigValues(subBlocks) + expect(result).toEqual(subBlocks) + }) + + it.concurrent('should return subBlocks unchanged when triggerConfig value is null', () => { + const subBlocks = { + triggerConfig: { id: 'triggerConfig', type: 'short-input', value: null }, + signingSecret: { id: 'signingSecret', type: 'short-input', value: null }, + } + const result = normalizeTriggerConfigValues(subBlocks) + expect(result).toEqual(subBlocks) + }) + + it.concurrent( + 'should return subBlocks unchanged when triggerConfig value is not an object', + () => { + const subBlocks = { + triggerConfig: { id: 'triggerConfig', type: 'short-input', value: 'string-value' }, + signingSecret: { id: 'signingSecret', type: 'short-input', value: null }, + } + const result = normalizeTriggerConfigValues(subBlocks) + expect(result).toEqual(subBlocks) + } + ) + + it.concurrent('should populate null individual fields from triggerConfig', () => { + const subBlocks = { + triggerConfig: { + id: 'triggerConfig', + type: 'short-input', + value: { signingSecret: 'secret123', botToken: 'token456' }, + }, + signingSecret: { id: 'signingSecret', type: 'short-input', value: null }, + botToken: { id: 'botToken', type: 'short-input', value: null }, + } + const result = normalizeTriggerConfigValues(subBlocks) + expect((result.signingSecret as Record).value).toBe('secret123') + expect((result.botToken as Record).value).toBe('token456') + }) + + it.concurrent('should populate undefined individual fields from triggerConfig', () => { + const subBlocks = { + triggerConfig: { + id: 'triggerConfig', + type: 'short-input', + value: { signingSecret: 'secret123' }, + }, + signingSecret: { id: 'signingSecret', type: 'short-input', value: undefined }, + } + const result = normalizeTriggerConfigValues(subBlocks) + expect((result.signingSecret as Record).value).toBe('secret123') + }) + + it.concurrent('should populate empty string individual fields from triggerConfig', () => { + const subBlocks = { + triggerConfig: { + id: 'triggerConfig', + type: 'short-input', + value: { signingSecret: 'secret123' }, + }, + signingSecret: { id: 'signingSecret', type: 'short-input', value: '' }, + } + const result = normalizeTriggerConfigValues(subBlocks) + expect((result.signingSecret as Record).value).toBe('secret123') + }) + + it.concurrent('should NOT overwrite existing non-empty individual field values', () => { + const subBlocks = { + triggerConfig: { + id: 'triggerConfig', + type: 'short-input', + value: { signingSecret: 'old-secret' }, + }, + signingSecret: { id: 'signingSecret', type: 'short-input', value: 'user-edited-secret' }, + } + const result = normalizeTriggerConfigValues(subBlocks) + expect((result.signingSecret as Record).value).toBe('user-edited-secret') + }) + + it.concurrent('should skip triggerConfig fields that are null/undefined', () => { + const subBlocks = { + triggerConfig: { + id: 'triggerConfig', + type: 'short-input', + value: { signingSecret: null, botToken: undefined }, + }, + signingSecret: { id: 'signingSecret', type: 'short-input', value: null }, + botToken: { id: 'botToken', type: 'short-input', value: null }, + } + const result = normalizeTriggerConfigValues(subBlocks) + expect((result.signingSecret as Record).value).toBe(null) + expect((result.botToken as Record).value).toBe(null) + }) + + it.concurrent('should skip fields from triggerConfig that have no matching subBlock', () => { + const subBlocks = { + triggerConfig: { + id: 'triggerConfig', + type: 'short-input', + value: { nonExistentField: 'value123' }, + }, + signingSecret: { id: 'signingSecret', type: 'short-input', value: null }, + } + const result = normalizeTriggerConfigValues(subBlocks) + expect(result.nonExistentField).toBeUndefined() + expect((result.signingSecret as Record).value).toBe(null) + }) + + it.concurrent('should not mutate the original subBlocks object', () => { + const original = { + triggerConfig: { + id: 'triggerConfig', + type: 'short-input', + value: { signingSecret: 'secret123' }, + }, + signingSecret: { id: 'signingSecret', type: 'short-input', value: null }, + } + normalizeTriggerConfigValues(original) + expect((original.signingSecret as Record).value).toBe(null) + }) + + it.concurrent('should preserve other subBlock properties when populating value', () => { + const subBlocks = { + triggerConfig: { + id: 'triggerConfig', + type: 'short-input', + value: { signingSecret: 'secret123' }, + }, + signingSecret: { + id: 'signingSecret', + type: 'short-input', + value: null, + placeholder: 'Enter signing secret', + }, + } + const result = normalizeTriggerConfigValues(subBlocks) + const normalized = result.signingSecret as Record + expect(normalized.value).toBe('secret123') + expect(normalized.id).toBe('signingSecret') + expect(normalized.type).toBe('short-input') + expect(normalized.placeholder).toBe('Enter signing secret') + }) + }) }) diff --git a/apps/sim/lib/workflows/comparison/normalize.ts b/apps/sim/lib/workflows/comparison/normalize.ts index dc414e25b..70a584141 100644 --- a/apps/sim/lib/workflows/comparison/normalize.ts +++ b/apps/sim/lib/workflows/comparison/normalize.ts @@ -411,17 +411,63 @@ export function extractBlockFieldsForComparison(block: BlockState): ExtractedBlo } /** - * Filters subBlock IDs to exclude system and trigger runtime subBlocks. + * Pattern matching synthetic subBlock IDs created by ToolSubBlockRenderer. + * These IDs follow the format `{subBlockId}-tool-{index}-{paramId}` and are + * mirrors of values already stored in toolConfig.value.tools[N].params. + */ +const SYNTHETIC_TOOL_SUBBLOCK_RE = /-tool-\d+-/ + +/** + * Filters subBlock IDs to exclude system, trigger runtime, and synthetic tool subBlocks. * * @param subBlockIds - Array of subBlock IDs to filter * @returns Filtered and sorted array of subBlock IDs */ export function filterSubBlockIds(subBlockIds: string[]): string[] { return subBlockIds - .filter((id) => !SYSTEM_SUBBLOCK_IDS.includes(id) && !TRIGGER_RUNTIME_SUBBLOCK_IDS.includes(id)) + .filter((id) => { + if (TRIGGER_RUNTIME_SUBBLOCK_IDS.includes(id)) return false + if (SYSTEM_SUBBLOCK_IDS.some((sysId) => id === sysId || id.startsWith(`${sysId}_`))) + return false + if (SYNTHETIC_TOOL_SUBBLOCK_RE.test(id)) return false + return true + }) .sort() } +/** + * Normalizes trigger block subBlocks by populating null/empty individual fields + * from the triggerConfig aggregate subBlock. This compensates for the runtime + * population done by populateTriggerFieldsFromConfig, ensuring consistent + * comparison between client state (with populated values) and deployed state + * (with null values from DB). + */ +export function normalizeTriggerConfigValues( + subBlocks: Record +): Record { + const triggerConfigSub = subBlocks.triggerConfig as Record | undefined + const triggerConfigValue = triggerConfigSub?.value + if (!triggerConfigValue || typeof triggerConfigValue !== 'object') { + return subBlocks + } + + const result = { ...subBlocks } + for (const [fieldId, configValue] of Object.entries( + triggerConfigValue as Record + )) { + if (configValue === null || configValue === undefined) continue + const existingSub = result[fieldId] as Record | undefined + if ( + existingSub && + (existingSub.value === null || existingSub.value === undefined || existingSub.value === '') + ) { + result[fieldId] = { ...existingSub, value: configValue } + } + } + + return result +} + /** * Normalizes a subBlock value with sanitization for specific subBlock types. * Sanitizes: tools (removes isExpanded), inputFormat (removes collapsed) diff --git a/apps/sim/lib/workflows/diff/diff-engine.test.ts b/apps/sim/lib/workflows/diff/diff-engine.test.ts index aecbd801e..0f7103a10 100644 --- a/apps/sim/lib/workflows/diff/diff-engine.test.ts +++ b/apps/sim/lib/workflows/diff/diff-engine.test.ts @@ -1,18 +1,11 @@ /** * @vitest-environment node */ +import { loggerMock } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { BlockState, WorkflowState } from '@/stores/workflows/workflow/types' -// Mock all external dependencies before imports -vi.mock('@sim/logger', () => ({ - createLogger: () => ({ - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - debug: vi.fn(), - }), -})) +vi.mock('@sim/logger', () => loggerMock) vi.mock('@/stores/workflows/workflow/store', () => ({ useWorkflowStore: { diff --git a/apps/sim/lib/workflows/utils.test.ts b/apps/sim/lib/workflows/utils.test.ts index e1787e229..da1dd8b26 100644 --- a/apps/sim/lib/workflows/utils.test.ts +++ b/apps/sim/lib/workflows/utils.test.ts @@ -14,22 +14,15 @@ import { databaseMock, expectWorkflowAccessDenied, expectWorkflowAccessGranted, + mockAuth, } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' -vi.mock('@sim/db', () => databaseMock) - -// Mock the auth module -vi.mock('@/lib/auth', () => ({ - getSession: vi.fn(), -})) - -import { db } from '@sim/db' -import { getSession } from '@/lib/auth' -// Import after mocks are set up -import { validateWorkflowPermissions } from '@/lib/workflows/utils' +const mockDb = databaseMock.db describe('validateWorkflowPermissions', () => { + const auth = mockAuth() + const mockSession = createSession({ userId: 'user-1', email: 'user1@test.com' }) const mockWorkflow = createWorkflowRecord({ id: 'wf-1', @@ -42,13 +35,17 @@ describe('validateWorkflowPermissions', () => { }) beforeEach(() => { + vi.resetModules() vi.clearAllMocks() + + vi.doMock('@sim/db', () => databaseMock) }) describe('authentication', () => { it('should return 401 when no session exists', async () => { - vi.mocked(getSession).mockResolvedValue(null) + auth.setUnauthenticated() + const { validateWorkflowPermissions } = await import('@/lib/workflows/utils') const result = await validateWorkflowPermissions('wf-1', 'req-1', 'read') expectWorkflowAccessDenied(result, 401) @@ -56,8 +53,9 @@ describe('validateWorkflowPermissions', () => { }) it('should return 401 when session has no user id', async () => { - vi.mocked(getSession).mockResolvedValue({ user: {} } as any) + auth.mockGetSession.mockResolvedValue({ user: {} } as any) + const { validateWorkflowPermissions } = await import('@/lib/workflows/utils') const result = await validateWorkflowPermissions('wf-1', 'req-1', 'read') expectWorkflowAccessDenied(result, 401) @@ -66,14 +64,14 @@ describe('validateWorkflowPermissions', () => { describe('workflow not found', () => { it('should return 404 when workflow does not exist', async () => { - vi.mocked(getSession).mockResolvedValue(mockSession as any) + auth.mockGetSession.mockResolvedValue(mockSession as any) - // Mock workflow query to return empty const mockLimit = vi.fn().mockResolvedValue([]) const mockWhere = vi.fn(() => ({ limit: mockLimit })) const mockFrom = vi.fn(() => ({ where: mockWhere })) - vi.mocked(db.select).mockReturnValue({ from: mockFrom } as any) + vi.mocked(mockDb.select).mockReturnValue({ from: mockFrom } as any) + const { validateWorkflowPermissions } = await import('@/lib/workflows/utils') const result = await validateWorkflowPermissions('non-existent', 'req-1', 'read') expectWorkflowAccessDenied(result, 404) @@ -83,43 +81,42 @@ describe('validateWorkflowPermissions', () => { describe('owner access', () => { it('should deny access to workflow owner without workspace permissions for read action', async () => { - const ownerSession = createSession({ userId: 'owner-1' }) - vi.mocked(getSession).mockResolvedValue(ownerSession as any) + auth.setAuthenticated({ id: 'owner-1', email: 'owner-1@test.com' }) - // Mock workflow query const mockLimit = vi.fn().mockResolvedValue([mockWorkflow]) const mockWhere = vi.fn(() => ({ limit: mockLimit })) const mockFrom = vi.fn(() => ({ where: mockWhere })) - vi.mocked(db.select).mockReturnValue({ from: mockFrom } as any) + vi.mocked(mockDb.select).mockReturnValue({ from: mockFrom } as any) + const { validateWorkflowPermissions } = await import('@/lib/workflows/utils') const result = await validateWorkflowPermissions('wf-1', 'req-1', 'read') expectWorkflowAccessDenied(result, 403) }) it('should deny access to workflow owner without workspace permissions for write action', async () => { - const ownerSession = createSession({ userId: 'owner-1' }) - vi.mocked(getSession).mockResolvedValue(ownerSession as any) + auth.setAuthenticated({ id: 'owner-1', email: 'owner-1@test.com' }) const mockLimit = vi.fn().mockResolvedValue([mockWorkflow]) const mockWhere = vi.fn(() => ({ limit: mockLimit })) const mockFrom = vi.fn(() => ({ where: mockWhere })) - vi.mocked(db.select).mockReturnValue({ from: mockFrom } as any) + vi.mocked(mockDb.select).mockReturnValue({ from: mockFrom } as any) + const { validateWorkflowPermissions } = await import('@/lib/workflows/utils') const result = await validateWorkflowPermissions('wf-1', 'req-1', 'write') expectWorkflowAccessDenied(result, 403) }) it('should deny access to workflow owner without workspace permissions for admin action', async () => { - const ownerSession = createSession({ userId: 'owner-1' }) - vi.mocked(getSession).mockResolvedValue(ownerSession as any) + auth.setAuthenticated({ id: 'owner-1', email: 'owner-1@test.com' }) const mockLimit = vi.fn().mockResolvedValue([mockWorkflow]) const mockWhere = vi.fn(() => ({ limit: mockLimit })) const mockFrom = vi.fn(() => ({ where: mockWhere })) - vi.mocked(db.select).mockReturnValue({ from: mockFrom } as any) + vi.mocked(mockDb.select).mockReturnValue({ from: mockFrom } as any) + const { validateWorkflowPermissions } = await import('@/lib/workflows/utils') const result = await validateWorkflowPermissions('wf-1', 'req-1', 'admin') expectWorkflowAccessDenied(result, 403) @@ -128,11 +125,10 @@ describe('validateWorkflowPermissions', () => { describe('workspace member access with permissions', () => { beforeEach(() => { - vi.mocked(getSession).mockResolvedValue(mockSession as any) + auth.mockGetSession.mockResolvedValue(mockSession as any) }) it('should grant read access to user with read permission', async () => { - // First call: workflow query, second call: workspace owner, third call: permission let callCount = 0 const mockLimit = vi.fn().mockImplementation(() => { callCount++ @@ -141,8 +137,9 @@ describe('validateWorkflowPermissions', () => { }) const mockWhere = vi.fn(() => ({ limit: mockLimit })) const mockFrom = vi.fn(() => ({ where: mockWhere })) - vi.mocked(db.select).mockReturnValue({ from: mockFrom } as any) + vi.mocked(mockDb.select).mockReturnValue({ from: mockFrom } as any) + const { validateWorkflowPermissions } = await import('@/lib/workflows/utils') const result = await validateWorkflowPermissions('wf-1', 'req-1', 'read') expectWorkflowAccessGranted(result) @@ -157,8 +154,9 @@ describe('validateWorkflowPermissions', () => { }) const mockWhere = vi.fn(() => ({ limit: mockLimit })) const mockFrom = vi.fn(() => ({ where: mockWhere })) - vi.mocked(db.select).mockReturnValue({ from: mockFrom } as any) + vi.mocked(mockDb.select).mockReturnValue({ from: mockFrom } as any) + const { validateWorkflowPermissions } = await import('@/lib/workflows/utils') const result = await validateWorkflowPermissions('wf-1', 'req-1', 'write') expectWorkflowAccessDenied(result, 403) @@ -174,8 +172,9 @@ describe('validateWorkflowPermissions', () => { }) const mockWhere = vi.fn(() => ({ limit: mockLimit })) const mockFrom = vi.fn(() => ({ where: mockWhere })) - vi.mocked(db.select).mockReturnValue({ from: mockFrom } as any) + vi.mocked(mockDb.select).mockReturnValue({ from: mockFrom } as any) + const { validateWorkflowPermissions } = await import('@/lib/workflows/utils') const result = await validateWorkflowPermissions('wf-1', 'req-1', 'write') expectWorkflowAccessGranted(result) @@ -190,8 +189,9 @@ describe('validateWorkflowPermissions', () => { }) const mockWhere = vi.fn(() => ({ limit: mockLimit })) const mockFrom = vi.fn(() => ({ where: mockWhere })) - vi.mocked(db.select).mockReturnValue({ from: mockFrom } as any) + vi.mocked(mockDb.select).mockReturnValue({ from: mockFrom } as any) + const { validateWorkflowPermissions } = await import('@/lib/workflows/utils') const result = await validateWorkflowPermissions('wf-1', 'req-1', 'write') expectWorkflowAccessGranted(result) @@ -206,8 +206,9 @@ describe('validateWorkflowPermissions', () => { }) const mockWhere = vi.fn(() => ({ limit: mockLimit })) const mockFrom = vi.fn(() => ({ where: mockWhere })) - vi.mocked(db.select).mockReturnValue({ from: mockFrom } as any) + vi.mocked(mockDb.select).mockReturnValue({ from: mockFrom } as any) + const { validateWorkflowPermissions } = await import('@/lib/workflows/utils') const result = await validateWorkflowPermissions('wf-1', 'req-1', 'admin') expectWorkflowAccessDenied(result, 403) @@ -223,8 +224,9 @@ describe('validateWorkflowPermissions', () => { }) const mockWhere = vi.fn(() => ({ limit: mockLimit })) const mockFrom = vi.fn(() => ({ where: mockWhere })) - vi.mocked(db.select).mockReturnValue({ from: mockFrom } as any) + vi.mocked(mockDb.select).mockReturnValue({ from: mockFrom } as any) + const { validateWorkflowPermissions } = await import('@/lib/workflows/utils') const result = await validateWorkflowPermissions('wf-1', 'req-1', 'admin') expectWorkflowAccessGranted(result) @@ -233,18 +235,19 @@ describe('validateWorkflowPermissions', () => { describe('no workspace permission', () => { it('should deny access to user without any workspace permission', async () => { - vi.mocked(getSession).mockResolvedValue(mockSession as any) + auth.mockGetSession.mockResolvedValue(mockSession as any) let callCount = 0 const mockLimit = vi.fn().mockImplementation(() => { callCount++ if (callCount === 1) return Promise.resolve([mockWorkflow]) - return Promise.resolve([]) // No permission record + return Promise.resolve([]) }) const mockWhere = vi.fn(() => ({ limit: mockLimit })) const mockFrom = vi.fn(() => ({ where: mockWhere })) - vi.mocked(db.select).mockReturnValue({ from: mockFrom } as any) + vi.mocked(mockDb.select).mockReturnValue({ from: mockFrom } as any) + const { validateWorkflowPermissions } = await import('@/lib/workflows/utils') const result = await validateWorkflowPermissions('wf-1', 'req-1', 'read') expectWorkflowAccessDenied(result, 403) @@ -259,13 +262,14 @@ describe('validateWorkflowPermissions', () => { workspaceId: null, }) - vi.mocked(getSession).mockResolvedValue(mockSession as any) + auth.mockGetSession.mockResolvedValue(mockSession as any) const mockLimit = vi.fn().mockResolvedValue([workflowWithoutWorkspace]) const mockWhere = vi.fn(() => ({ limit: mockLimit })) const mockFrom = vi.fn(() => ({ where: mockWhere })) - vi.mocked(db.select).mockReturnValue({ from: mockFrom } as any) + vi.mocked(mockDb.select).mockReturnValue({ from: mockFrom } as any) + const { validateWorkflowPermissions } = await import('@/lib/workflows/utils') const result = await validateWorkflowPermissions('wf-2', 'req-1', 'read') expectWorkflowAccessDenied(result, 403) @@ -278,13 +282,14 @@ describe('validateWorkflowPermissions', () => { workspaceId: null, }) - vi.mocked(getSession).mockResolvedValue(mockSession as any) + auth.mockGetSession.mockResolvedValue(mockSession as any) const mockLimit = vi.fn().mockResolvedValue([workflowWithoutWorkspace]) const mockWhere = vi.fn(() => ({ limit: mockLimit })) const mockFrom = vi.fn(() => ({ where: mockWhere })) - vi.mocked(db.select).mockReturnValue({ from: mockFrom } as any) + vi.mocked(mockDb.select).mockReturnValue({ from: mockFrom } as any) + const { validateWorkflowPermissions } = await import('@/lib/workflows/utils') const result = await validateWorkflowPermissions('wf-2', 'req-1', 'read') expectWorkflowAccessDenied(result, 403) @@ -293,7 +298,7 @@ describe('validateWorkflowPermissions', () => { describe('default action', () => { it('should default to read action when not specified', async () => { - vi.mocked(getSession).mockResolvedValue(mockSession as any) + auth.mockGetSession.mockResolvedValue(mockSession as any) let callCount = 0 const mockLimit = vi.fn().mockImplementation(() => { @@ -303,8 +308,9 @@ describe('validateWorkflowPermissions', () => { }) const mockWhere = vi.fn(() => ({ limit: mockLimit })) const mockFrom = vi.fn(() => ({ where: mockWhere })) - vi.mocked(db.select).mockReturnValue({ from: mockFrom } as any) + vi.mocked(mockDb.select).mockReturnValue({ from: mockFrom } as any) + const { validateWorkflowPermissions } = await import('@/lib/workflows/utils') const result = await validateWorkflowPermissions('wf-1', 'req-1') expectWorkflowAccessGranted(result) diff --git a/apps/sim/lib/workspaces/permissions/utils.test.ts b/apps/sim/lib/workspaces/permissions/utils.test.ts index 938937d22..04d863323 100644 --- a/apps/sim/lib/workspaces/permissions/utils.test.ts +++ b/apps/sim/lib/workspaces/permissions/utils.test.ts @@ -1,17 +1,7 @@ -import { drizzleOrmMock } from '@sim/testing/mocks' +import { databaseMock, drizzleOrmMock } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' -vi.mock('@sim/db', () => ({ - db: { - select: vi.fn(), - from: vi.fn(), - where: vi.fn(), - limit: vi.fn(), - innerJoin: vi.fn(), - leftJoin: vi.fn(), - orderBy: vi.fn(), - }, -})) +vi.mock('@sim/db', () => databaseMock) vi.mock('@sim/db/schema', () => ({ permissions: { diff --git a/apps/sim/providers/types.ts b/apps/sim/providers/types.ts index 1f1edfe94..cb75153c5 100644 --- a/apps/sim/providers/types.ts +++ b/apps/sim/providers/types.ts @@ -112,6 +112,8 @@ export interface ProviderToolConfig { required: string[] } usageControl?: ToolUsageControl + /** Block-level params transformer — converts SubBlock values to tool-ready params */ + paramsTransform?: (params: Record) => Record } export interface Message { diff --git a/apps/sim/providers/utils.ts b/apps/sim/providers/utils.ts index fed88f31c..ee1b2bfc7 100644 --- a/apps/sim/providers/utils.ts +++ b/apps/sim/providers/utils.ts @@ -4,6 +4,12 @@ import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' import { env } from '@/lib/core/config/env' import { isHosted } from '@/lib/core/config/feature-flags' +import { + buildCanonicalIndex, + type CanonicalGroup, + getCanonicalValues, + isCanonicalPair, +} from '@/lib/workflows/subblocks/visibility' import { isCustomTool } from '@/executor/constants' import { getComputerUseModels, @@ -437,9 +443,10 @@ export async function transformBlockTool( getAllBlocks: () => any[] getTool: (toolId: string) => any getToolAsync?: (toolId: string) => Promise + canonicalModes?: Record } ): Promise { - const { selectedOperation, getAllBlocks, getTool, getToolAsync } = options + const { selectedOperation, getAllBlocks, getTool, getToolAsync, canonicalModes } = options const blockDef = getAllBlocks().find((b: any) => b.type === block.type) if (!blockDef) { @@ -516,12 +523,66 @@ export async function transformBlockTool( uniqueToolId = `${toolConfig.id}_${userProvidedParams.knowledgeBaseId}` } + const blockParamsFn = blockDef?.tools?.config?.params as + | ((p: Record) => Record) + | undefined + const blockInputDefs = blockDef?.inputs as Record | undefined + + const canonicalGroups: CanonicalGroup[] = blockDef?.subBlocks + ? Object.values(buildCanonicalIndex(blockDef.subBlocks).groupsById).filter(isCanonicalPair) + : [] + + const needsTransform = blockParamsFn || blockInputDefs || canonicalGroups.length > 0 + const paramsTransform = needsTransform + ? (params: Record): Record => { + let result = { ...params } + + for (const group of canonicalGroups) { + const { basicValue, advancedValue } = getCanonicalValues(group, result) + const scopedKey = `${block.type}:${group.canonicalId}` + const pairMode = canonicalModes?.[scopedKey] ?? 'basic' + const chosen = pairMode === 'advanced' ? advancedValue : basicValue + + const sourceIds = [group.basicId, ...group.advancedIds].filter(Boolean) as string[] + sourceIds.forEach((id) => delete result[id]) + + if (chosen !== undefined) { + result[group.canonicalId] = chosen + } + } + + if (blockParamsFn) { + const transformed = blockParamsFn(result) + result = { ...result, ...transformed } + } + + if (blockInputDefs) { + for (const [key, schema] of Object.entries(blockInputDefs)) { + const value = result[key] + if (typeof value === 'string' && value.trim().length > 0) { + const inputType = typeof schema === 'object' ? schema.type : schema + if (inputType === 'json' || inputType === 'array') { + try { + result[key] = JSON.parse(value.trim()) + } catch { + // Not valid JSON — keep as string + } + } + } + } + } + + return result + } + : undefined + return { id: uniqueToolId, name: toolName, description: toolDescription, params: userProvidedParams, parameters: llmSchema, + paramsTransform, } } @@ -1028,7 +1089,11 @@ export function getMaxOutputTokensForModel(model: string): number { * Prepare tool execution parameters, separating tool parameters from system parameters */ export function prepareToolExecution( - tool: { params?: Record; parameters?: Record }, + tool: { + params?: Record + parameters?: Record + paramsTransform?: (params: Record) => Record + }, llmArgs: Record, request: { workflowId?: string @@ -1045,8 +1110,15 @@ export function prepareToolExecution( toolParams: Record executionParams: Record } { - // Use centralized merge logic from tools/params - const toolParams = mergeToolParameters(tool.params || {}, llmArgs) as Record + let toolParams = mergeToolParameters(tool.params || {}, llmArgs) as Record + + if (tool.paramsTransform) { + try { + toolParams = tool.paramsTransform(toolParams) + } catch (err) { + logger.warn('paramsTransform failed, using raw params', { error: err }) + } + } const executionParams = { ...toolParams, diff --git a/apps/sim/proxy.ts b/apps/sim/proxy.ts index 36ada3484..b4994d861 100644 --- a/apps/sim/proxy.ts +++ b/apps/sim/proxy.ts @@ -137,6 +137,36 @@ function handleSecurityFiltering(request: NextRequest): NextResponse | null { return null } +const UTM_KEYS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content'] as const +const UTM_COOKIE_NAME = 'sim_utm' +const UTM_COOKIE_MAX_AGE = 3600 + +/** + * Sets a `sim_utm` cookie when UTM params are present on auth pages. + * Captures UTM values, the HTTP Referer, landing page, and a timestamp. + */ +function setUtmCookie(request: NextRequest, response: NextResponse): void { + const { searchParams, pathname } = request.nextUrl + const hasUtm = UTM_KEYS.some((key) => searchParams.get(key)) + if (!hasUtm) return + + const utmData: Record = {} + for (const key of UTM_KEYS) { + const value = searchParams.get(key) + if (value) utmData[key] = value + } + utmData.referrer_url = request.headers.get('referer') || '' + utmData.landing_page = pathname + utmData.created_at = Date.now().toString() + + response.cookies.set(UTM_COOKIE_NAME, JSON.stringify(utmData), { + path: '/', + maxAge: UTM_COOKIE_MAX_AGE, + sameSite: 'lax', + httpOnly: false, // Client-side hook needs to detect cookie presence + }) +} + export async function proxy(request: NextRequest) { const url = request.nextUrl @@ -148,10 +178,13 @@ export async function proxy(request: NextRequest) { if (url.pathname === '/login' || url.pathname === '/signup') { if (hasActiveSession) { - return NextResponse.redirect(new URL('/workspace', request.url)) + const redirect = NextResponse.redirect(new URL('/workspace', request.url)) + setUtmCookie(request, redirect) + return redirect } const response = NextResponse.next() response.headers.set('Content-Security-Policy', generateRuntimeCSP()) + setUtmCookie(request, response) return response } diff --git a/apps/sim/serializer/index.ts b/apps/sim/serializer/index.ts index 622667d9f..35b675d22 100644 --- a/apps/sim/serializer/index.ts +++ b/apps/sim/serializer/index.ts @@ -280,7 +280,7 @@ export class Serializer { }) } - return { + const serialized: SerializedBlock = { id: block.id, position: block.position, config: { @@ -300,6 +300,12 @@ export class Serializer { }, enabled: block.enabled, } + + if (block.data?.canonicalModes) { + serialized.canonicalModes = block.data.canonicalModes as Record + } + + return serialized } private extractParams(block: BlockState): Record { diff --git a/apps/sim/serializer/types.ts b/apps/sim/serializer/types.ts index 4f89bfb71..8192014a4 100644 --- a/apps/sim/serializer/types.ts +++ b/apps/sim/serializer/types.ts @@ -38,6 +38,8 @@ export interface SerializedBlock { color?: string } enabled: boolean + /** Canonical mode overrides from block.data (used by agent handler for tool param resolution) */ + canonicalModes?: Record } export interface SerializedLoop { diff --git a/apps/sim/stores/execution/store.ts b/apps/sim/stores/execution/store.ts index 6983ddcda..b82d4a3c5 100644 --- a/apps/sim/stores/execution/store.ts +++ b/apps/sim/stores/execution/store.ts @@ -129,6 +129,18 @@ export const useExecutionStore = create()((se }) }, + setCurrentExecutionId: (workflowId, executionId) => { + set({ + workflowExecutions: updatedMap(get().workflowExecutions, workflowId, { + currentExecutionId: executionId, + }), + }) + }, + + getCurrentExecutionId: (workflowId) => { + return getOrCreate(get().workflowExecutions, workflowId).currentExecutionId + }, + clearRunPath: (workflowId) => { set({ workflowExecutions: updatedMap(get().workflowExecutions, workflowId, { diff --git a/apps/sim/stores/execution/types.ts b/apps/sim/stores/execution/types.ts index 55d873b49..b36ea43a1 100644 --- a/apps/sim/stores/execution/types.ts +++ b/apps/sim/stores/execution/types.ts @@ -35,6 +35,8 @@ export interface WorkflowExecutionState { lastRunPath: Map /** Maps edge IDs to their run result from the last execution */ lastRunEdges: Map + /** The execution ID of the currently running execution */ + currentExecutionId: string | null } /** @@ -54,6 +56,7 @@ export const defaultWorkflowExecutionState: WorkflowExecutionState = { debugContext: null, lastRunPath: new Map(), lastRunEdges: new Map(), + currentExecutionId: null, } /** @@ -96,6 +99,10 @@ export interface ExecutionActions { setEdgeRunStatus: (workflowId: string, edgeId: string, status: EdgeRunStatus) => void /** Clears the run path and run edges for a workflow */ clearRunPath: (workflowId: string) => void + /** Stores the current execution ID for a workflow */ + setCurrentExecutionId: (workflowId: string, executionId: string | null) => void + /** Returns the current execution ID for a workflow */ + getCurrentExecutionId: (workflowId: string) => string | null /** Resets the entire store to its initial empty state */ reset: () => void /** Stores a serializable execution snapshot for a workflow */ diff --git a/apps/sim/stores/panel/copilot/store.ts b/apps/sim/stores/panel/copilot/store.ts index 44f17df10..bd4dd76e2 100644 --- a/apps/sim/stores/panel/copilot/store.ts +++ b/apps/sim/stores/panel/copilot/store.ts @@ -1042,7 +1042,7 @@ const cachedAutoAllowedTools = readAutoAllowedToolsFromStorage() // Initial state (subset required for UI/streaming) const initialState = { mode: 'build' as const, - selectedModel: 'anthropic/claude-opus-4-6' as CopilotStore['selectedModel'], + selectedModel: 'anthropic/claude-opus-4-5' as CopilotStore['selectedModel'], agentPrefetch: false, availableModels: [] as AvailableModel[], isLoadingModels: false, @@ -2381,17 +2381,17 @@ export const useCopilotStore = create()( (model) => model.id === normalizedSelectedModel ) - // Pick the best default: prefer claude-opus-4-6 with provider priority: + // Pick the best default: prefer claude-opus-4-5 with provider priority: // direct anthropic > bedrock > azure-anthropic > any other. let nextSelectedModel = normalizedSelectedModel if (!selectedModelExists && normalizedModels.length > 0) { - let opus46: AvailableModel | undefined + let opus45: AvailableModel | undefined for (const prov of MODEL_PROVIDER_PRIORITY) { - opus46 = normalizedModels.find((m) => m.id === `${prov}/claude-opus-4-6`) - if (opus46) break + opus45 = normalizedModels.find((m) => m.id === `${prov}/claude-opus-4-5`) + if (opus45) break } - if (!opus46) opus46 = normalizedModels.find((m) => m.id.endsWith('/claude-opus-4-6')) - nextSelectedModel = opus46 ? opus46.id : normalizedModels[0].id + if (!opus45) opus45 = normalizedModels.find((m) => m.id.endsWith('/claude-opus-4-5')) + nextSelectedModel = opus45 ? opus45.id : normalizedModels[0].id } set({ diff --git a/apps/sim/stores/terminal/console/store.ts b/apps/sim/stores/terminal/console/store.ts index 55b59b135..9fddbf3ef 100644 --- a/apps/sim/stores/terminal/console/store.ts +++ b/apps/sim/stores/terminal/console/store.ts @@ -224,7 +224,7 @@ export const useTerminalConsoleStore = create()( const newEntry = get().entries[0] - if (newEntry?.error) { + if (newEntry?.error && newEntry.blockType !== 'cancelled') { notifyBlockError({ error: newEntry.error, blockName: newEntry.blockName || 'Unknown Block', @@ -243,6 +243,11 @@ export const useTerminalConsoleStore = create()( useExecutionStore.getState().clearRunPath(workflowId) }, + clearExecutionEntries: (executionId: string) => + set((state) => ({ + entries: state.entries.filter((e) => e.executionId !== executionId), + })), + exportConsoleCSV: (workflowId: string) => { const entries = get().entries.filter((entry) => entry.workflowId === workflowId) @@ -470,12 +475,24 @@ export const useTerminalConsoleStore = create()( }, merge: (persistedState, currentState) => { const persisted = persistedState as Partial | undefined - const entries = (persisted?.entries ?? currentState.entries).map((entry, index) => { + const rawEntries = persisted?.entries ?? currentState.entries + const oneHourAgo = Date.now() - 60 * 60 * 1000 + + const entries = rawEntries.map((entry, index) => { + let updated = entry if (entry.executionOrder === undefined) { - return { ...entry, executionOrder: index + 1 } + updated = { ...updated, executionOrder: index + 1 } } - return entry + if ( + entry.isRunning && + entry.startedAt && + new Date(entry.startedAt).getTime() < oneHourAgo + ) { + updated = { ...updated, isRunning: false } + } + return updated }) + return { ...currentState, entries, diff --git a/apps/sim/stores/terminal/console/types.ts b/apps/sim/stores/terminal/console/types.ts index f15f36377..e057854d8 100644 --- a/apps/sim/stores/terminal/console/types.ts +++ b/apps/sim/stores/terminal/console/types.ts @@ -51,6 +51,7 @@ export interface ConsoleStore { isOpen: boolean addConsole: (entry: Omit) => ConsoleEntry clearWorkflowConsole: (workflowId: string) => void + clearExecutionEntries: (executionId: string) => void exportConsoleCSV: (workflowId: string) => void getWorkflowEntries: (workflowId: string) => ConsoleEntry[] toggleConsole: () => void diff --git a/apps/sim/tools/file/parser.ts b/apps/sim/tools/file/parser.ts index bcd8826d2..e7740bfa5 100644 --- a/apps/sim/tools/file/parser.ts +++ b/apps/sim/tools/file/parser.ts @@ -95,7 +95,7 @@ export const fileParserTool: ToolConfig = { filePath: { type: 'string', required: false, - visibility: 'user-only', + visibility: 'hidden', description: 'Path to the file(s). Can be a single path, URL, or an array of paths.', }, file: { diff --git a/apps/sim/tools/google_books/index.ts b/apps/sim/tools/google_books/index.ts new file mode 100644 index 000000000..be18ce982 --- /dev/null +++ b/apps/sim/tools/google_books/index.ts @@ -0,0 +1,3 @@ +export * from './types' +export { googleBooksVolumeDetailsTool } from './volume_details' +export { googleBooksVolumeSearchTool } from './volume_search' diff --git a/apps/sim/tools/google_books/types.ts b/apps/sim/tools/google_books/types.ts new file mode 100644 index 000000000..363038de5 --- /dev/null +++ b/apps/sim/tools/google_books/types.ts @@ -0,0 +1,124 @@ +import type { ToolResponse } from '@/tools/types' + +/** + * Raw volume item from Google Books API search response + */ +export interface GoogleBooksVolumeItem { + id: string + volumeInfo: { + title?: string + subtitle?: string + authors?: string[] + publisher?: string + publishedDate?: string + description?: string + pageCount?: number + categories?: string[] + averageRating?: number + ratingsCount?: number + language?: string + previewLink?: string + infoLink?: string + imageLinks?: { + thumbnail?: string + smallThumbnail?: string + } + industryIdentifiers?: Array<{ + type: string + identifier: string + }> + } +} + +/** + * Raw volume response from Google Books API details endpoint + */ +export interface GoogleBooksVolumeResponse { + id: string + volumeInfo: { + title?: string + subtitle?: string + authors?: string[] + publisher?: string + publishedDate?: string + description?: string + pageCount?: number + categories?: string[] + averageRating?: number + ratingsCount?: number + language?: string + previewLink?: string + infoLink?: string + imageLinks?: { + thumbnail?: string + smallThumbnail?: string + } + industryIdentifiers?: Array<{ + type: string + identifier: string + }> + } +} + +/** + * Volume information structure shared between search and details responses + */ +export interface VolumeInfo { + id: string + title: string + subtitle: string | null + authors: string[] + publisher: string | null + publishedDate: string | null + description: string | null + pageCount: number | null + categories: string[] + averageRating: number | null + ratingsCount: number | null + language: string | null + previewLink: string | null + infoLink: string | null + thumbnailUrl: string | null + isbn10: string | null + isbn13: string | null +} + +/** + * Parameters for searching volumes + */ +export interface GoogleBooksVolumeSearchParams { + apiKey: string + query: string + filter?: 'partial' | 'full' | 'free-ebooks' | 'paid-ebooks' | 'ebooks' + printType?: 'all' | 'books' | 'magazines' + orderBy?: 'relevance' | 'newest' + startIndex?: number + maxResults?: number + langRestrict?: string +} + +/** + * Response from volume search + */ +export interface GoogleBooksVolumeSearchResponse extends ToolResponse { + output: { + totalItems: number + volumes: VolumeInfo[] + } +} + +/** + * Parameters for getting volume details + */ +export interface GoogleBooksVolumeDetailsParams { + apiKey: string + volumeId: string + projection?: 'full' | 'lite' +} + +/** + * Response from volume details + */ +export interface GoogleBooksVolumeDetailsResponse extends ToolResponse { + output: VolumeInfo +} diff --git a/apps/sim/tools/google_books/volume_details.ts b/apps/sim/tools/google_books/volume_details.ts new file mode 100644 index 000000000..23fe7cd4a --- /dev/null +++ b/apps/sim/tools/google_books/volume_details.ts @@ -0,0 +1,172 @@ +import type { + GoogleBooksVolumeDetailsParams, + GoogleBooksVolumeDetailsResponse, + GoogleBooksVolumeResponse, +} from '@/tools/google_books/types' +import type { ToolConfig } from '@/tools/types' + +export const googleBooksVolumeDetailsTool: ToolConfig< + GoogleBooksVolumeDetailsParams, + GoogleBooksVolumeDetailsResponse +> = { + id: 'google_books_volume_details', + name: 'Google Books Volume Details', + description: 'Get detailed information about a specific book volume', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Google Books API key', + }, + volumeId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the volume to retrieve', + }, + projection: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Projection level (full, lite)', + }, + }, + + request: { + url: (params) => { + const url = new URL(`https://www.googleapis.com/books/v1/volumes/${params.volumeId.trim()}`) + url.searchParams.set('key', params.apiKey.trim()) + + if (params.projection) { + url.searchParams.set('projection', params.projection) + } + + return url.toString() + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data: GoogleBooksVolumeResponse = await response.json() + + if (!data.volumeInfo) { + throw new Error('Volume not found') + } + + const info = data.volumeInfo + const identifiers = info.industryIdentifiers ?? [] + + return { + success: true, + output: { + id: data.id, + title: info.title ?? '', + subtitle: info.subtitle ?? null, + authors: info.authors ?? [], + publisher: info.publisher ?? null, + publishedDate: info.publishedDate ?? null, + description: info.description ?? null, + pageCount: info.pageCount ?? null, + categories: info.categories ?? [], + averageRating: info.averageRating ?? null, + ratingsCount: info.ratingsCount ?? null, + language: info.language ?? null, + previewLink: info.previewLink ?? null, + infoLink: info.infoLink ?? null, + thumbnailUrl: info.imageLinks?.thumbnail ?? info.imageLinks?.smallThumbnail ?? null, + isbn10: identifiers.find((id) => id.type === 'ISBN_10')?.identifier ?? null, + isbn13: identifiers.find((id) => id.type === 'ISBN_13')?.identifier ?? null, + }, + } + }, + + outputs: { + id: { + type: 'string', + description: 'Volume ID', + }, + title: { + type: 'string', + description: 'Book title', + }, + subtitle: { + type: 'string', + description: 'Book subtitle', + optional: true, + }, + authors: { + type: 'array', + description: 'List of authors', + }, + publisher: { + type: 'string', + description: 'Publisher name', + optional: true, + }, + publishedDate: { + type: 'string', + description: 'Publication date', + optional: true, + }, + description: { + type: 'string', + description: 'Book description', + optional: true, + }, + pageCount: { + type: 'number', + description: 'Number of pages', + optional: true, + }, + categories: { + type: 'array', + description: 'Book categories', + }, + averageRating: { + type: 'number', + description: 'Average rating (1-5)', + optional: true, + }, + ratingsCount: { + type: 'number', + description: 'Number of ratings', + optional: true, + }, + language: { + type: 'string', + description: 'Language code', + optional: true, + }, + previewLink: { + type: 'string', + description: 'Link to preview on Google Books', + optional: true, + }, + infoLink: { + type: 'string', + description: 'Link to info page', + optional: true, + }, + thumbnailUrl: { + type: 'string', + description: 'Book cover thumbnail URL', + optional: true, + }, + isbn10: { + type: 'string', + description: 'ISBN-10 identifier', + optional: true, + }, + isbn13: { + type: 'string', + description: 'ISBN-13 identifier', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/google_books/volume_search.ts b/apps/sim/tools/google_books/volume_search.ts new file mode 100644 index 000000000..55d90cf9a --- /dev/null +++ b/apps/sim/tools/google_books/volume_search.ts @@ -0,0 +1,176 @@ +import type { + GoogleBooksVolumeItem, + GoogleBooksVolumeSearchParams, + GoogleBooksVolumeSearchResponse, + VolumeInfo, +} from '@/tools/google_books/types' +import type { ToolConfig } from '@/tools/types' + +function extractVolumeInfo(item: GoogleBooksVolumeItem): VolumeInfo { + const info = item.volumeInfo + const identifiers = info.industryIdentifiers ?? [] + + return { + id: item.id, + title: info.title ?? '', + subtitle: info.subtitle ?? null, + authors: info.authors ?? [], + publisher: info.publisher ?? null, + publishedDate: info.publishedDate ?? null, + description: info.description ?? null, + pageCount: info.pageCount ?? null, + categories: info.categories ?? [], + averageRating: info.averageRating ?? null, + ratingsCount: info.ratingsCount ?? null, + language: info.language ?? null, + previewLink: info.previewLink ?? null, + infoLink: info.infoLink ?? null, + thumbnailUrl: info.imageLinks?.thumbnail ?? info.imageLinks?.smallThumbnail ?? null, + isbn10: identifiers.find((id) => id.type === 'ISBN_10')?.identifier ?? null, + isbn13: identifiers.find((id) => id.type === 'ISBN_13')?.identifier ?? null, + } +} + +export const googleBooksVolumeSearchTool: ToolConfig< + GoogleBooksVolumeSearchParams, + GoogleBooksVolumeSearchResponse +> = { + id: 'google_books_volume_search', + name: 'Google Books Volume Search', + description: 'Search for books using the Google Books API', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Google Books API key', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Search query. Supports special keywords: intitle:, inauthor:, inpublisher:, subject:, isbn:', + }, + filter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Filter results by availability (partial, full, free-ebooks, paid-ebooks, ebooks)', + }, + printType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Restrict to print type (all, books, magazines)', + }, + orderBy: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort order (relevance, newest)', + }, + startIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Index of the first result to return (for pagination)', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results to return (1-40)', + }, + langRestrict: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Restrict results to a specific language (ISO 639-1 code)', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://www.googleapis.com/books/v1/volumes') + url.searchParams.set('q', params.query.trim()) + url.searchParams.set('key', params.apiKey.trim()) + + if (params.filter) { + url.searchParams.set('filter', params.filter) + } + if (params.printType) { + url.searchParams.set('printType', params.printType) + } + if (params.orderBy) { + url.searchParams.set('orderBy', params.orderBy) + } + if (params.startIndex !== undefined) { + url.searchParams.set('startIndex', String(params.startIndex)) + } + if (params.maxResults !== undefined) { + url.searchParams.set('maxResults', String(params.maxResults)) + } + if (params.langRestrict) { + url.searchParams.set('langRestrict', params.langRestrict) + } + + return url.toString() + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + const items: GoogleBooksVolumeItem[] = data.items ?? [] + const volumes = items.map(extractVolumeInfo) + + return { + success: true, + output: { + totalItems: data.totalItems ?? 0, + volumes, + }, + } + }, + + outputs: { + totalItems: { + type: 'number', + description: 'Total number of matching results', + }, + volumes: { + type: 'array', + description: 'List of matching volumes', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Volume ID' }, + title: { type: 'string', description: 'Book title' }, + subtitle: { type: 'string', description: 'Book subtitle' }, + authors: { type: 'array', description: 'List of authors' }, + publisher: { type: 'string', description: 'Publisher name' }, + publishedDate: { type: 'string', description: 'Publication date' }, + description: { type: 'string', description: 'Book description' }, + pageCount: { type: 'number', description: 'Number of pages' }, + categories: { type: 'array', description: 'Book categories' }, + averageRating: { type: 'number', description: 'Average rating (1-5)' }, + ratingsCount: { type: 'number', description: 'Number of ratings' }, + language: { type: 'string', description: 'Language code' }, + previewLink: { type: 'string', description: 'Link to preview on Google Books' }, + infoLink: { type: 'string', description: 'Link to info page' }, + thumbnailUrl: { type: 'string', description: 'Book cover thumbnail URL' }, + isbn10: { type: 'string', description: 'ISBN-10 identifier' }, + isbn13: { type: 'string', description: 'ISBN-13 identifier' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts index 040a40a27..9af514aeb 100644 --- a/apps/sim/tools/index.ts +++ b/apps/sim/tools/index.ts @@ -6,7 +6,7 @@ import { validateUrlWithDNS, } from '@/lib/core/security/input-validation.server' import { generateRequestId } from '@/lib/core/utils/request' -import { getBaseUrl } from '@/lib/core/utils/urls' +import { getBaseUrl, getInternalApiBaseUrl } from '@/lib/core/utils/urls' import { parseMcpToolId } from '@/lib/mcp/utils' import { isCustomTool, isMcpTool } from '@/executor/constants' import { resolveSkillContent } from '@/executor/handlers/agent/skills-resolver' @@ -285,7 +285,7 @@ export async function executeTool( `[${requestId}] Tool ${toolId} needs access token for credential: ${contextParams.credential}` ) try { - const baseUrl = getBaseUrl() + const baseUrl = getInternalApiBaseUrl() const workflowId = contextParams._context?.workflowId const userId = contextParams._context?.userId @@ -597,12 +597,12 @@ async function executeToolRequest( const requestParams = formatRequestParams(tool, params) try { - const baseUrl = getBaseUrl() const endpointUrl = typeof tool.request.url === 'function' ? tool.request.url(params) : tool.request.url + const isInternalRoute = endpointUrl.startsWith('/api/') + const baseUrl = isInternalRoute ? getInternalApiBaseUrl() : getBaseUrl() const fullUrlObj = new URL(endpointUrl, baseUrl) - const isInternalRoute = endpointUrl.startsWith('/api/') if (isInternalRoute) { const workflowId = params._context?.workflowId @@ -922,7 +922,7 @@ async function executeMcpTool( const { serverId, toolName } = parseMcpToolId(toolId) - const baseUrl = getBaseUrl() + const baseUrl = getInternalApiBaseUrl() const headers: Record = { 'Content-Type': 'application/json' } diff --git a/apps/sim/tools/jira/add_attachment.ts b/apps/sim/tools/jira/add_attachment.ts index 260bcc029..bd890e509 100644 --- a/apps/sim/tools/jira/add_attachment.ts +++ b/apps/sim/tools/jira/add_attachment.ts @@ -36,7 +36,7 @@ export const jiraAddAttachmentTool: ToolConfig, + canonicalModeOverrides?: CanonicalModeOverrides +): SubBlocksForToolInput | null { + try { + const toolConfig = getTool(toolId) + if (!toolConfig) { + logger.warn(`Tool not found: ${toolId}`) + return null + } + + const blockConfigs = getBlockConfigurations() + const blockConfig = blockConfigs[blockType] + if (!blockConfig?.subBlocks?.length) { + return null + } + + const allSubBlocks = blockConfig.subBlocks as BlockSubBlockConfig[] + const canonicalIndex = buildCanonicalIndex(allSubBlocks) + + // Build values for condition evaluation + const values = currentValues || {} + const valuesWithOperation = { ...values } + if (valuesWithOperation.operation === undefined) { + const parts = toolId.split('_') + valuesWithOperation.operation = + parts.length >= 3 ? parts.slice(2).join('_') : parts[parts.length - 1] + } + + // Build a map of tool param IDs to their resolved visibility + const toolParamVisibility: Record = {} + for (const [paramId, param] of Object.entries(toolConfig.params || {})) { + toolParamVisibility[paramId] = + param.visibility ?? (param.required ? 'user-or-llm' : 'user-only') + } + + // Track which canonical groups we've already included (to avoid duplicates) + const includedCanonicalIds = new Set() + + const filtered: BlockSubBlockConfig[] = [] + + for (const sb of allSubBlocks) { + // Skip excluded types + if (EXCLUDED_SUBBLOCK_TYPES.has(sb.type)) continue + + // Skip trigger-mode-only subblocks + if (sb.mode === 'trigger') continue + + // Determine the effective param ID (canonical or subblock id) + const effectiveParamId = sb.canonicalParamId || sb.id + + // Resolve paramVisibility: explicit > inferred from tool params > skip + let visibility = sb.paramVisibility + if (!visibility) { + // Infer from structural checks + if (STRUCTURAL_SUBBLOCK_IDS.has(sb.id)) { + visibility = 'hidden' + } else if (AUTH_SUBBLOCK_TYPES.has(sb.type)) { + visibility = 'hidden' + } else if ( + sb.password && + (sb.id === 'botToken' || sb.id === 'accessToken' || sb.id === 'apiKey') + ) { + // Auth tokens without explicit paramVisibility are hidden + // (they're handled by the OAuth credential selector or structurally) + // But only if they don't have a matching tool param + if (!(sb.id in toolParamVisibility)) { + visibility = 'hidden' + } else { + visibility = toolParamVisibility[sb.id] || 'user-or-llm' + } + } else if (effectiveParamId in toolParamVisibility) { + // Fallback: infer from tool param visibility + visibility = toolParamVisibility[effectiveParamId] + } else if (sb.id in toolParamVisibility) { + visibility = toolParamVisibility[sb.id] + } else if (sb.canonicalParamId) { + // SubBlock has a canonicalParamId that doesn't directly match a tool param. + // This means the block's params() function transforms it before sending to the tool + // (e.g. listFolderId → folderId). These are user-facing inputs, default to user-or-llm. + visibility = 'user-or-llm' + } else { + // SubBlock has no corresponding tool param — skip it + continue + } + } + + // Filter by visibility: exclude hidden and llm-only + if (visibility === 'hidden' || visibility === 'llm-only') continue + + // Evaluate condition against current values + if (sb.condition) { + const conditionMet = evaluateSubBlockCondition( + sb.condition as SubBlockCondition, + valuesWithOperation + ) + if (!conditionMet) continue + } + + // Handle canonical pairs: only include the active mode variant + const canonicalId = canonicalIndex.canonicalIdBySubBlockId[sb.id] + if (canonicalId) { + const group = canonicalIndex.groupsById[canonicalId] + if (group && isCanonicalPair(group)) { + if (includedCanonicalIds.has(canonicalId)) continue + includedCanonicalIds.add(canonicalId) + + // Determine active mode + const mode = resolveCanonicalMode(group, valuesWithOperation, canonicalModeOverrides) + if (mode === 'advanced') { + // Find the advanced variant + const advancedSb = allSubBlocks.find((s) => group.advancedIds.includes(s.id)) + if (advancedSb) { + filtered.push({ ...advancedSb, paramVisibility: visibility }) + } + } else { + // Include basic variant (current sb if it's the basic one) + if (group.basicId === sb.id) { + filtered.push({ ...sb, paramVisibility: visibility }) + } else { + const basicSb = allSubBlocks.find((s) => s.id === group.basicId) + if (basicSb) { + filtered.push({ ...basicSb, paramVisibility: visibility }) + } + } + } + continue + } + } + + // Non-canonical, non-hidden, condition-passing subblock + filtered.push({ ...sb, paramVisibility: visibility }) + } + + return { + toolConfig, + subBlocks: filtered, + oauthConfig: toolConfig.oauth, + } + } catch (error) { + logger.error('Error getting subblocks for tool input:', error) + return null + } +} diff --git a/apps/sim/tools/pulse/parser.ts b/apps/sim/tools/pulse/parser.ts index 805d998ec..182801963 100644 --- a/apps/sim/tools/pulse/parser.ts +++ b/apps/sim/tools/pulse/parser.ts @@ -18,7 +18,7 @@ export const pulseParserTool: ToolConfig = file: { type: 'file', required: false, - visibility: 'hidden', + visibility: 'user-only', description: 'Document file to be processed', }, fileUpload: { @@ -268,7 +268,7 @@ export const pulseParserV2Tool: ToolConfig = { google_docs_read: googleDocsReadTool, google_docs_write: googleDocsWriteTool, google_docs_create: googleDocsCreateTool, + google_books_volume_search: googleBooksVolumeSearchTool, + google_books_volume_details: googleBooksVolumeDetailsTool, google_maps_air_quality: googleMapsAirQualityTool, google_maps_directions: googleMapsDirectionsTool, google_maps_distance_matrix: googleMapsDistanceMatrixTool, diff --git a/apps/sim/tools/s3/get_object.ts b/apps/sim/tools/s3/get_object.ts index 585604265..1e83ecc8b 100644 --- a/apps/sim/tools/s3/get_object.ts +++ b/apps/sim/tools/s3/get_object.ts @@ -26,6 +26,13 @@ export const s3GetObjectTool: ToolConfig = { visibility: 'user-only', description: 'Your AWS Secret Access Key', }, + region: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Optional region override when URL does not include region (e.g., us-east-1, eu-west-1)', + }, s3Uri: { type: 'string', required: true, @@ -37,7 +44,7 @@ export const s3GetObjectTool: ToolConfig = { request: { url: (params) => { try { - const { bucketName, region, objectKey } = parseS3Uri(params.s3Uri) + const { bucketName, region, objectKey } = parseS3Uri(params.s3Uri, params.region) params.bucketName = bucketName params.region = region @@ -46,7 +53,7 @@ export const s3GetObjectTool: ToolConfig = { return `https://${bucketName}.s3.${region}.amazonaws.com/${encodeS3PathComponent(objectKey)}` } catch (_error) { throw new Error( - 'Invalid S3 Object URL format. Expected format: https://bucket-name.s3.region.amazonaws.com/path/to/file' + 'Invalid S3 Object URL. Use a valid S3 URL and optionally provide region if the URL omits it.' ) } }, @@ -55,7 +62,7 @@ export const s3GetObjectTool: ToolConfig = { try { // Parse S3 URI if not already parsed if (!params.bucketName || !params.region || !params.objectKey) { - const { bucketName, region, objectKey } = parseS3Uri(params.s3Uri) + const { bucketName, region, objectKey } = parseS3Uri(params.s3Uri, params.region) params.bucketName = bucketName params.region = region params.objectKey = objectKey @@ -102,7 +109,7 @@ export const s3GetObjectTool: ToolConfig = { transformResponse: async (response: Response, params) => { // Parse S3 URI if not already parsed if (!params.bucketName || !params.region || !params.objectKey) { - const { bucketName, region, objectKey } = parseS3Uri(params.s3Uri) + const { bucketName, region, objectKey } = parseS3Uri(params.s3Uri, params.region) params.bucketName = bucketName params.region = region params.objectKey = objectKey diff --git a/apps/sim/tools/s3/utils.ts b/apps/sim/tools/s3/utils.ts index a0815a878..8d5f5ad65 100644 --- a/apps/sim/tools/s3/utils.ts +++ b/apps/sim/tools/s3/utils.ts @@ -20,7 +20,10 @@ export function getSignatureKey( return kSigning } -export function parseS3Uri(s3Uri: string): { +export function parseS3Uri( + s3Uri: string, + fallbackRegion?: string +): { bucketName: string region: string objectKey: string @@ -28,10 +31,55 @@ export function parseS3Uri(s3Uri: string): { try { const url = new URL(s3Uri) const hostname = url.hostname - const bucketName = hostname.split('.')[0] - const regionMatch = hostname.match(/s3[.-]([^.]+)\.amazonaws\.com/) - const region = regionMatch ? regionMatch[1] : 'us-east-1' - const objectKey = url.pathname.startsWith('/') ? url.pathname.substring(1) : url.pathname + const normalizedPath = url.pathname.startsWith('/') ? url.pathname.slice(1) : url.pathname + + const virtualHostedDualstackMatch = hostname.match( + /^(.+)\.s3\.dualstack\.([^.]+)\.amazonaws\.com(?:\.cn)?$/ + ) + const virtualHostedRegionalMatch = hostname.match( + /^(.+)\.s3[.-]([^.]+)\.amazonaws\.com(?:\.cn)?$/ + ) + const virtualHostedGlobalMatch = hostname.match(/^(.+)\.s3\.amazonaws\.com(?:\.cn)?$/) + + const pathStyleDualstackMatch = hostname.match( + /^s3\.dualstack\.([^.]+)\.amazonaws\.com(?:\.cn)?$/ + ) + const pathStyleRegionalMatch = hostname.match(/^s3[.-]([^.]+)\.amazonaws\.com(?:\.cn)?$/) + const pathStyleGlobalMatch = hostname.match(/^s3\.amazonaws\.com(?:\.cn)?$/) + + const isPathStyleHost = Boolean( + pathStyleDualstackMatch || pathStyleRegionalMatch || pathStyleGlobalMatch + ) + + const firstSlashIndex = normalizedPath.indexOf('/') + const pathStyleBucketName = + firstSlashIndex === -1 ? normalizedPath : normalizedPath.slice(0, firstSlashIndex) + const pathStyleObjectKey = + firstSlashIndex === -1 ? '' : normalizedPath.slice(firstSlashIndex + 1) + + const bucketName = isPathStyleHost + ? pathStyleBucketName + : (virtualHostedDualstackMatch?.[1] ?? + virtualHostedRegionalMatch?.[1] ?? + virtualHostedGlobalMatch?.[1] ?? + '') + + const rawObjectKey = isPathStyleHost ? pathStyleObjectKey : normalizedPath + const objectKey = (() => { + try { + return decodeURIComponent(rawObjectKey) + } catch { + return rawObjectKey + } + })() + + const normalizedFallbackRegion = fallbackRegion?.trim() + const regionFromHost = + virtualHostedDualstackMatch?.[2] ?? + virtualHostedRegionalMatch?.[2] ?? + pathStyleDualstackMatch?.[1] ?? + pathStyleRegionalMatch?.[1] + const region = regionFromHost || normalizedFallbackRegion || 'us-east-1' if (!bucketName || !objectKey) { throw new Error('Invalid S3 URI format') @@ -40,7 +88,7 @@ export function parseS3Uri(s3Uri: string): { return { bucketName, region, objectKey } } catch (_error) { throw new Error( - 'Invalid S3 Object URL format. Expected format: https://bucket-name.s3.region.amazonaws.com/path/to/file' + 'Invalid S3 Object URL format. Expected S3 virtual-hosted or path-style URL with object key.' ) } } diff --git a/apps/sim/tools/sftp/upload.ts b/apps/sim/tools/sftp/upload.ts index 6c8677080..eb1c40531 100644 --- a/apps/sim/tools/sftp/upload.ts +++ b/apps/sim/tools/sftp/upload.ts @@ -53,7 +53,7 @@ export const sftpUploadTool: ToolConfig = { files: { type: 'file[]', required: false, - visibility: 'hidden', + visibility: 'user-only', description: 'Files to upload', }, fileContent: { diff --git a/apps/sim/tools/utils.ts b/apps/sim/tools/utils.ts index 0a7b635fa..e783217a6 100644 --- a/apps/sim/tools/utils.ts +++ b/apps/sim/tools/utils.ts @@ -1,6 +1,6 @@ import { createLogger } from '@sim/logger' import { getMaxExecutionTimeout } from '@/lib/core/execution-limits' -import { getBaseUrl } from '@/lib/core/utils/urls' +import { getInternalApiBaseUrl } from '@/lib/core/utils/urls' import { AGENT, isCustomTool } from '@/executor/constants' import { getCustomTool } from '@/hooks/queries/custom-tools' import { useEnvironmentStore } from '@/stores/settings/environment' @@ -373,7 +373,7 @@ async function fetchCustomToolFromAPI( const identifier = customToolId.replace('custom_', '') try { - const baseUrl = getBaseUrl() + const baseUrl = getInternalApiBaseUrl() const url = new URL('/api/tools/custom', baseUrl) if (workflowId) { diff --git a/apps/sim/tools/vision/tool.ts b/apps/sim/tools/vision/tool.ts index 02dba60f2..01d0b9399 100644 --- a/apps/sim/tools/vision/tool.ts +++ b/apps/sim/tools/vision/tool.ts @@ -106,7 +106,7 @@ export const visionToolV2: ToolConfig = { imageFile: { type: 'file', required: true, - visibility: 'hidden', + visibility: 'user-only', description: 'Image file to analyze', }, model: visionTool.params.model, diff --git a/apps/sim/tools/wordpress/upload_media.ts b/apps/sim/tools/wordpress/upload_media.ts index 50bc57eef..7115346aa 100644 --- a/apps/sim/tools/wordpress/upload_media.ts +++ b/apps/sim/tools/wordpress/upload_media.ts @@ -27,7 +27,7 @@ export const uploadMediaTool: ToolConfig statement-breakpoint +CREATE TABLE "referral_campaigns" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "code" text, + "utm_source" text, + "utm_medium" text, + "utm_campaign" text, + "utm_content" text, + "bonus_credit_amount" numeric NOT NULL, + "is_active" boolean DEFAULT true NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "referral_campaigns_code_unique" UNIQUE("code") +); +--> statement-breakpoint +ALTER TABLE "referral_attribution" ADD CONSTRAINT "referral_attribution_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "referral_attribution" ADD CONSTRAINT "referral_attribution_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "referral_attribution" ADD CONSTRAINT "referral_attribution_campaign_id_referral_campaigns_id_fk" FOREIGN KEY ("campaign_id") REFERENCES "public"."referral_campaigns"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "referral_attribution_user_id_idx" ON "referral_attribution" USING btree ("user_id");--> statement-breakpoint +CREATE UNIQUE INDEX "referral_attribution_org_unique_idx" ON "referral_attribution" USING btree ("organization_id") WHERE "referral_attribution"."organization_id" IS NOT NULL;--> statement-breakpoint +CREATE INDEX "referral_attribution_campaign_id_idx" ON "referral_attribution" USING btree ("campaign_id");--> statement-breakpoint +CREATE INDEX "referral_attribution_utm_campaign_idx" ON "referral_attribution" USING btree ("utm_campaign");--> statement-breakpoint +CREATE INDEX "referral_attribution_utm_content_idx" ON "referral_attribution" USING btree ("utm_content");--> statement-breakpoint +CREATE INDEX "referral_attribution_created_at_idx" ON "referral_attribution" USING btree ("created_at");--> statement-breakpoint +CREATE INDEX "referral_campaigns_active_idx" ON "referral_campaigns" USING btree ("is_active"); \ No newline at end of file diff --git a/packages/db/migrations/meta/0154_snapshot.json b/packages/db/migrations/meta/0154_snapshot.json new file mode 100644 index 000000000..39b2be845 --- /dev/null +++ b/packages/db/migrations/meta/0154_snapshot.json @@ -0,0 +1,10957 @@ +{ + "id": "49f580f7-7eba-4431-bdf4-61db0e606546", + "prevId": "2652353e-bc06-43fe-a8c6-4d03fe4dac93", + "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.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "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()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": true, + "notNull": true + }, + "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_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": {} + } + }, + "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.referral_attribution": { + "name": "referral_attribution", + "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": false + }, + "campaign_id": { + "name": "campaign_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_source": { + "name": "utm_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_medium": { + "name": "utm_medium", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_campaign": { + "name": "utm_campaign", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_content": { + "name": "utm_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "referrer_url": { + "name": "referrer_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "landing_page": { + "name": "landing_page", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bonus_credit_amount": { + "name": "bonus_credit_amount", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "referral_attribution_user_id_idx": { + "name": "referral_attribution_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "referral_attribution_org_unique_idx": { + "name": "referral_attribution_org_unique_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"referral_attribution\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "referral_attribution_campaign_id_idx": { + "name": "referral_attribution_campaign_id_idx", + "columns": [ + { + "expression": "campaign_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "referral_attribution_utm_campaign_idx": { + "name": "referral_attribution_utm_campaign_idx", + "columns": [ + { + "expression": "utm_campaign", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "referral_attribution_utm_content_idx": { + "name": "referral_attribution_utm_content_idx", + "columns": [ + { + "expression": "utm_content", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "referral_attribution_created_at_idx": { + "name": "referral_attribution_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "referral_attribution_user_id_user_id_fk": { + "name": "referral_attribution_user_id_user_id_fk", + "tableFrom": "referral_attribution", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "referral_attribution_organization_id_organization_id_fk": { + "name": "referral_attribution_organization_id_organization_id_fk", + "tableFrom": "referral_attribution", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "referral_attribution_campaign_id_referral_campaigns_id_fk": { + "name": "referral_attribution_campaign_id_referral_campaigns_id_fk", + "tableFrom": "referral_attribution", + "tableTo": "referral_campaigns", + "columnsFrom": ["campaign_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "referral_attribution_user_id_unique": { + "name": "referral_attribution_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.referral_campaigns": { + "name": "referral_campaigns", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_source": { + "name": "utm_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_medium": { + "name": "utm_medium", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_campaign": { + "name": "utm_campaign", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_content": { + "name": "utm_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bonus_credit_amount": { + "name": "bonus_credit_amount", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "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": { + "referral_campaigns_active_idx": { + "name": "referral_campaigns_active_idx", + "columns": [ + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "referral_campaigns_code_unique": { + "name": "referral_campaigns_code_unique", + "nullsNotDistinct": false, + "columns": ["code"] + } + }, + "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.skill": { + "name": "skill", + "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 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "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": { + "skill_workspace_id_idx": { + "name": "skill_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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_mcp_executions": { + "name": "total_mcp_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_a2a_executions": { + "name": "total_a2a_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 + }, + "total_mcp_copilot_calls": { + "name": "total_mcp_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_cost": { + "name": "total_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_mcp_copilot_cost": { + "name": "current_period_mcp_copilot_cost", + "type": "numeric", + "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 + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "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_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_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_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_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 + }, + "locked": { + "name": "locked", + "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 + }, + "description": { + "name": "description", + "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": false + }, + "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": "set null", + "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": false + }, + "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": "set null", + "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 + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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 + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_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", "mcp_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 2fa880f2a..2b83d1c90 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1072,6 +1072,13 @@ "when": 1770410282842, "tag": "0153_complete_arclight", "breakpoints": true + }, + { + "idx": 154, + "version": "7", + "when": 1770869658697, + "tag": "0154_bumpy_living_mummy", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index d145c5796..090ab0855 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -726,6 +726,61 @@ export const userStats = pgTable('user_stats', { billingBlockedReason: billingBlockedReasonEnum('billing_blocked_reason'), }) +export const referralCampaigns = pgTable( + 'referral_campaigns', + { + id: text('id').primaryKey(), + name: text('name').notNull(), + code: text('code').unique(), + utmSource: text('utm_source'), + utmMedium: text('utm_medium'), + utmCampaign: text('utm_campaign'), + utmContent: text('utm_content'), + bonusCreditAmount: decimal('bonus_credit_amount').notNull(), + isActive: boolean('is_active').notNull().default(true), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => ({ + activeIdx: index('referral_campaigns_active_idx').on(table.isActive), + }) +) + +export const referralAttribution = pgTable( + 'referral_attribution', + { + id: text('id').primaryKey(), + userId: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }) + .unique(), + organizationId: text('organization_id').references(() => organization.id, { + onDelete: 'set null', + }), + campaignId: text('campaign_id').references(() => referralCampaigns.id, { + onDelete: 'set null', + }), + utmSource: text('utm_source'), + utmMedium: text('utm_medium'), + utmCampaign: text('utm_campaign'), + utmContent: text('utm_content'), + referrerUrl: text('referrer_url'), + landingPage: text('landing_page'), + bonusCreditAmount: decimal('bonus_credit_amount').notNull().default('0'), + createdAt: timestamp('created_at').notNull().defaultNow(), + }, + (table) => ({ + userIdIdx: index('referral_attribution_user_id_idx').on(table.userId), + orgUniqueIdx: uniqueIndex('referral_attribution_org_unique_idx') + .on(table.organizationId) + .where(sql`${table.organizationId} IS NOT NULL`), + campaignIdIdx: index('referral_attribution_campaign_id_idx').on(table.campaignId), + utmCampaignIdx: index('referral_attribution_utm_campaign_idx').on(table.utmCampaign), + utmContentIdx: index('referral_attribution_utm_content_idx').on(table.utmContent), + createdAtIdx: index('referral_attribution_created_at_idx').on(table.createdAt), + }) +) + export const customTools = pgTable( 'custom_tools', {