From 172d51e06101d1ea9185275ed9a9f9a6789a40b6 Mon Sep 17 00:00:00 2001 From: Waleed Date: Sat, 13 Sep 2025 11:32:10 -0700 Subject: [PATCH 01/10] fix(security): fix ssrf vuln and path validation for files route (#1325) * update infra and remove railway * fix(security): fix ssrf vuln * fix path validation for file serve * Revert "update infra and remove railway" This reverts commit abfa2f8d51901247acc6397960210569e84d72b1. * lint * ack PR comments --- apps/sim/app/api/files/utils.test.ts | 90 +++++++++- apps/sim/app/api/files/utils.ts | 72 +++++++- .../app/api/function/execute/route.test.ts | 170 +++++++++++------- apps/sim/app/api/function/execute/route.ts | 26 ++- 4 files changed, 288 insertions(+), 70 deletions(-) diff --git a/apps/sim/app/api/files/utils.test.ts b/apps/sim/app/api/files/utils.test.ts index d0ad4567ac..b3deae47bd 100644 --- a/apps/sim/app/api/files/utils.test.ts +++ b/apps/sim/app/api/files/utils.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { createFileResponse, extractFilename } from './utils' +import { createFileResponse, extractFilename, findLocalFile } from './utils' describe('extractFilename', () => { describe('legitimate file paths', () => { @@ -325,3 +325,91 @@ describe('extractFilename', () => { }) }) }) + +describe('findLocalFile - Path Traversal Security Tests', () => { + describe('path traversal attack prevention', () => { + it.concurrent('should reject classic path traversal attacks', () => { + const maliciousInputs = [ + '../../../etc/passwd', + '..\\..\\..\\windows\\system32\\config\\sam', + '../../../../etc/shadow', + '../config.json', + '..\\config.ini', + ] + + maliciousInputs.forEach((input) => { + const result = findLocalFile(input) + expect(result).toBeNull() + }) + }) + + it.concurrent('should reject encoded path traversal attempts', () => { + const encodedInputs = [ + '%2e%2e%2f%2e%2e%2f%65%74%63%2f%70%61%73%73%77%64', // ../../../etc/passwd + '..%2f..%2fetc%2fpasswd', + '..%5c..%5cconfig.ini', + ] + + encodedInputs.forEach((input) => { + const result = findLocalFile(input) + expect(result).toBeNull() + }) + }) + + it.concurrent('should reject mixed path separators', () => { + const mixedInputs = ['../..\\config.txt', '..\\../secret.ini', '/..\\..\\system32'] + + mixedInputs.forEach((input) => { + const result = findLocalFile(input) + expect(result).toBeNull() + }) + }) + + it.concurrent('should reject filenames with dangerous characters', () => { + const dangerousInputs = [ + 'file:with:colons.txt', + 'file|with|pipes.txt', + 'file?with?questions.txt', + 'file*with*asterisks.txt', + ] + + dangerousInputs.forEach((input) => { + const result = findLocalFile(input) + expect(result).toBeNull() + }) + }) + + it.concurrent('should reject null and empty inputs', () => { + expect(findLocalFile('')).toBeNull() + expect(findLocalFile(' ')).toBeNull() + expect(findLocalFile('\t\n')).toBeNull() + }) + + it.concurrent('should reject filenames that become empty after sanitization', () => { + const emptyAfterSanitization = ['../..', '..\\..\\', '////', '....', '..'] + + emptyAfterSanitization.forEach((input) => { + const result = findLocalFile(input) + expect(result).toBeNull() + }) + }) + }) + + describe('security validation passes for legitimate files', () => { + it.concurrent('should accept properly formatted filenames without throwing errors', () => { + const legitimateInputs = [ + 'document.pdf', + 'image.png', + 'data.csv', + 'report-2024.doc', + 'file_with_underscores.txt', + 'file-with-dashes.json', + ] + + legitimateInputs.forEach((input) => { + // Should not throw security errors for legitimate filenames + expect(() => findLocalFile(input)).not.toThrow() + }) + }) + }) +}) diff --git a/apps/sim/app/api/files/utils.ts b/apps/sim/app/api/files/utils.ts index 4e427bb77c..2e88f0c9c4 100644 --- a/apps/sim/app/api/files/utils.ts +++ b/apps/sim/app/api/files/utils.ts @@ -1,8 +1,11 @@ import { existsSync } from 'fs' -import { join } from 'path' +import { join, resolve, sep } from 'path' import { NextResponse } from 'next/server' +import { createLogger } from '@/lib/logs/console/logger' import { UPLOAD_DIR } from '@/lib/uploads/setup' +const logger = createLogger('FilesUtils') + /** * Response type definitions */ @@ -192,18 +195,71 @@ export function extractFilename(path: string): string { } /** - * Find a file in possible local storage locations + * Sanitize filename to prevent path traversal attacks + */ +function sanitizeFilename(filename: string): string { + if (!filename || typeof filename !== 'string') { + throw new Error('Invalid filename provided') + } + + const sanitized = filename + .replace(/\.\./g, '') // Remove .. sequences + .replace(/[/\\]/g, '') // Remove path separators + .replace(/^\./g, '') // Remove leading dots + .trim() + + if (!sanitized || sanitized.length === 0) { + throw new Error('Invalid or empty filename after sanitization') + } + + if ( + sanitized.includes(':') || + sanitized.includes('|') || + sanitized.includes('?') || + sanitized.includes('*') || + sanitized.includes('\x00') || // Null bytes + /[\x00-\x1F\x7F]/.test(sanitized) // Control characters + ) { + throw new Error('Filename contains invalid characters') + } + + return sanitized +} + +/** + * Find a file in possible local storage locations with proper path validation */ export function findLocalFile(filename: string): string | null { - const possiblePaths = [join(UPLOAD_DIR, filename), join(process.cwd(), 'uploads', filename)] + try { + const sanitizedFilename = sanitizeFilename(filename) + + const possiblePaths = [ + join(UPLOAD_DIR, sanitizedFilename), + join(process.cwd(), 'uploads', sanitizedFilename), + ] + + for (const path of possiblePaths) { + const resolvedPath = resolve(path) + const allowedDirs = [resolve(UPLOAD_DIR), resolve(process.cwd(), 'uploads')] - for (const path of possiblePaths) { - if (existsSync(path)) { - return path + const isWithinAllowedDir = allowedDirs.some( + (allowedDir) => resolvedPath.startsWith(allowedDir + sep) || resolvedPath === allowedDir + ) + + if (!isWithinAllowedDir) { + continue // Skip this path as it's outside allowed directories + } + + if (existsSync(resolvedPath)) { + return resolvedPath + } } - } - return null + return null + } catch (error) { + logger.error('Error in findLocalFile:', error) + return null + } } const SAFE_INLINE_TYPES = new Set([ diff --git a/apps/sim/app/api/function/execute/route.test.ts b/apps/sim/app/api/function/execute/route.test.ts index 5ca4eeb36a..8e32ae9cc0 100644 --- a/apps/sim/app/api/function/execute/route.test.ts +++ b/apps/sim/app/api/function/execute/route.test.ts @@ -48,8 +48,52 @@ describe('Function Execute API Route', () => { vi.clearAllMocks() }) + describe('Security Tests', () => { + it.concurrent('should create secure fetch in VM context', async () => { + const req = createMockRequest('POST', { + code: 'return "test"', + useLocalVM: true, + }) + + const { POST } = await import('@/app/api/function/execute/route') + await POST(req) + + expect(mockCreateContext).toHaveBeenCalled() + const contextArgs = mockCreateContext.mock.calls[0][0] + expect(contextArgs).toHaveProperty('fetch') + expect(typeof contextArgs.fetch).toBe('function') + + expect(contextArgs.fetch.name).toBe('secureFetch') + }) + + it.concurrent('should block SSRF attacks through secure fetch wrapper', async () => { + const { validateProxyUrl } = await import('@/lib/security/url-validation') + + expect(validateProxyUrl('http://169.254.169.254/latest/meta-data/').isValid).toBe(false) + expect(validateProxyUrl('http://127.0.0.1:8080/admin').isValid).toBe(false) + expect(validateProxyUrl('http://192.168.1.1/config').isValid).toBe(false) + expect(validateProxyUrl('http://10.0.0.1/internal').isValid).toBe(false) + }) + + it.concurrent('should allow legitimate external URLs', async () => { + const { validateProxyUrl } = await import('@/lib/security/url-validation') + + expect(validateProxyUrl('https://api.github.com/user').isValid).toBe(true) + expect(validateProxyUrl('https://httpbin.org/get').isValid).toBe(true) + expect(validateProxyUrl('http://example.com/api').isValid).toBe(true) + }) + + it.concurrent('should block dangerous protocols', async () => { + const { validateProxyUrl } = await import('@/lib/security/url-validation') + + expect(validateProxyUrl('file:///etc/passwd').isValid).toBe(false) + expect(validateProxyUrl('ftp://internal.server/files').isValid).toBe(false) + expect(validateProxyUrl('gopher://old.server/menu').isValid).toBe(false) + }) + }) + describe('Basic Function Execution', () => { - it('should execute simple JavaScript code successfully', async () => { + it.concurrent('should execute simple JavaScript code successfully', async () => { const req = createMockRequest('POST', { code: 'return "Hello World"', timeout: 5000, @@ -66,7 +110,7 @@ describe('Function Execute API Route', () => { expect(data.output).toHaveProperty('executionTime') }) - it('should handle missing code parameter', async () => { + it.concurrent('should handle missing code parameter', async () => { const req = createMockRequest('POST', { timeout: 5000, }) @@ -80,7 +124,7 @@ describe('Function Execute API Route', () => { expect(data).toHaveProperty('error') }) - it('should use default timeout when not provided', async () => { + it.concurrent('should use default timeout when not provided', async () => { const req = createMockRequest('POST', { code: 'return "test"', useLocalVM: true, @@ -100,7 +144,7 @@ describe('Function Execute API Route', () => { }) describe('Template Variable Resolution', () => { - it('should resolve environment variables with {{var_name}} syntax', async () => { + it.concurrent('should resolve environment variables with {{var_name}} syntax', async () => { const req = createMockRequest('POST', { code: 'return {{API_KEY}}', useLocalVM: true, @@ -116,7 +160,7 @@ describe('Function Execute API Route', () => { // The code should be resolved to: return "secret-key-123" }) - it('should resolve tag variables with syntax', async () => { + it.concurrent('should resolve tag variables with syntax', async () => { const req = createMockRequest('POST', { code: 'return ', useLocalVM: true, @@ -132,7 +176,7 @@ describe('Function Execute API Route', () => { // The code should be resolved with the email object }) - it('should NOT treat email addresses as template variables', async () => { + it.concurrent('should NOT treat email addresses as template variables', async () => { const req = createMockRequest('POST', { code: 'return "Email sent to user"', useLocalVM: true, @@ -151,7 +195,7 @@ describe('Function Execute API Route', () => { // Should not try to replace as a template variable }) - it('should only match valid variable names in angle brackets', async () => { + it.concurrent('should only match valid variable names in angle brackets', async () => { const req = createMockRequest('POST', { code: 'return + "" + ', useLocalVM: true, @@ -170,64 +214,70 @@ describe('Function Execute API Route', () => { }) describe('Gmail Email Data Handling', () => { - it('should handle Gmail webhook data with email addresses containing angle brackets', async () => { - const gmailData = { - email: { - id: '123', - from: 'Waleed Latif ', - to: 'User ', - subject: 'Test Email', - bodyText: 'Hello world', - }, - rawEmail: { - id: '123', - payload: { - headers: [ - { name: 'From', value: 'Waleed Latif ' }, - { name: 'To', value: 'User ' }, - ], + it.concurrent( + 'should handle Gmail webhook data with email addresses containing angle brackets', + async () => { + const gmailData = { + email: { + id: '123', + from: 'Waleed Latif ', + to: 'User ', + subject: 'Test Email', + bodyText: 'Hello world', }, - }, - } - - const req = createMockRequest('POST', { - code: 'return ', - useLocalVM: true, - params: gmailData, - }) + rawEmail: { + id: '123', + payload: { + headers: [ + { name: 'From', value: 'Waleed Latif ' }, + { name: 'To', value: 'User ' }, + ], + }, + }, + } - const { POST } = await import('@/app/api/function/execute/route') - const response = await POST(req) + const req = createMockRequest('POST', { + code: 'return ', + useLocalVM: true, + params: gmailData, + }) - expect(response.status).toBe(200) - const data = await response.json() - expect(data.success).toBe(true) - }) + const { POST } = await import('@/app/api/function/execute/route') + const response = await POST(req) - it('should properly serialize complex email objects with special characters', async () => { - const complexEmailData = { - email: { - from: 'Test User ', - bodyHtml: '
HTML content with "quotes" and \'apostrophes\'
', - bodyText: 'Text with\nnewlines\tand\ttabs', - }, + expect(response.status).toBe(200) + const data = await response.json() + expect(data.success).toBe(true) } + ) - const req = createMockRequest('POST', { - code: 'return ', - useLocalVM: true, - params: complexEmailData, - }) + it.concurrent( + 'should properly serialize complex email objects with special characters', + async () => { + const complexEmailData = { + email: { + from: 'Test User ', + bodyHtml: '
HTML content with "quotes" and \'apostrophes\'
', + bodyText: 'Text with\nnewlines\tand\ttabs', + }, + } - const { POST } = await import('@/app/api/function/execute/route') - const response = await POST(req) + const req = createMockRequest('POST', { + code: 'return ', + useLocalVM: true, + params: complexEmailData, + }) - expect(response.status).toBe(200) - }) + const { POST } = await import('@/app/api/function/execute/route') + const response = await POST(req) + + expect(response.status).toBe(200) + } + ) }) describe('Custom Tools', () => { - it('should handle custom tool execution with direct parameter access', async () => { + it.concurrent('should handle custom tool execution with direct parameter access', async () => { const req = createMockRequest('POST', { code: 'return location + " weather is sunny"', useLocalVM: true, @@ -246,7 +296,7 @@ describe('Function Execute API Route', () => { }) describe('Security and Edge Cases', () => { - it('should handle malformed JSON in request body', async () => { + it.concurrent('should handle malformed JSON in request body', async () => { const req = new NextRequest('http://localhost:3000/api/function/execute', { method: 'POST', body: 'invalid json{', @@ -259,7 +309,7 @@ describe('Function Execute API Route', () => { expect(response.status).toBe(500) }) - it('should handle timeout parameter', async () => { + it.concurrent('should handle timeout parameter', async () => { const req = createMockRequest('POST', { code: 'return "test"', useLocalVM: true, @@ -277,7 +327,7 @@ describe('Function Execute API Route', () => { ) }) - it('should handle empty parameters object', async () => { + it.concurrent('should handle empty parameters object', async () => { const req = createMockRequest('POST', { code: 'return "no params"', useLocalVM: true, @@ -485,7 +535,7 @@ SyntaxError: Invalid or unexpected token expect(data.debug.lineContent).toBe('return a + b + c + d;') }) - it('should provide helpful suggestions for common syntax errors', async () => { + it.concurrent('should provide helpful suggestions for common syntax errors', async () => { const mockScript = vi.fn().mockImplementation(() => { const error = new Error('Unexpected end of input') error.name = 'SyntaxError' @@ -517,7 +567,7 @@ SyntaxError: Invalid or unexpected token }) describe('Utility Functions', () => { - it('should properly escape regex special characters', async () => { + it.concurrent('should properly escape regex special characters', async () => { // This tests the escapeRegExp function indirectly const req = createMockRequest('POST', { code: 'return {{special.chars+*?}}', @@ -534,7 +584,7 @@ SyntaxError: Invalid or unexpected token // Should handle special regex characters in variable names }) - it('should handle JSON serialization edge cases', async () => { + it.concurrent('should handle JSON serialization edge cases', async () => { // Test with complex but not circular data first const req = createMockRequest('POST', { code: 'return ', diff --git a/apps/sim/app/api/function/execute/route.ts b/apps/sim/app/api/function/execute/route.ts index 2182078b15..a046fa3c39 100644 --- a/apps/sim/app/api/function/execute/route.ts +++ b/apps/sim/app/api/function/execute/route.ts @@ -4,6 +4,7 @@ import { env, isTruthy } from '@/lib/env' import { executeInE2B } from '@/lib/execution/e2b' import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages' import { createLogger } from '@/lib/logs/console/logger' +import { validateProxyUrl } from '@/lib/security/url-validation' import { generateRequestId } from '@/lib/utils' export const dynamic = 'force-dynamic' export const runtime = 'nodejs' @@ -11,6 +12,29 @@ export const maxDuration = 60 const logger = createLogger('FunctionExecuteAPI') +function createSecureFetch(requestId: string) { + const originalFetch = (globalThis as any).fetch || require('node-fetch').default + + return async function secureFetch(input: any, init?: any) { + const url = typeof input === 'string' ? input : input?.url || input + + if (!url || typeof url !== 'string') { + throw new Error('Invalid URL provided to fetch') + } + + const validation = validateProxyUrl(url) + if (!validation.isValid) { + logger.warn(`[${requestId}] Blocked fetch request due to SSRF validation`, { + url: url.substring(0, 100), + error: validation.error, + }) + throw new Error(`Security Error: ${validation.error}`) + } + + return originalFetch(input, init) + } +} + // Constants for E2B code wrapping line counts const E2B_JS_WRAPPER_LINES = 3 // Lines before user code: ';(async () => {', ' try {', ' const __sim_result = await (async () => {' const E2B_PYTHON_WRAPPER_LINES = 1 // Lines before user code: 'def __sim_main__():' @@ -737,7 +761,7 @@ export async function POST(req: NextRequest) { params: executionParams, environmentVariables: envVars, ...contextVariables, - fetch: (globalThis as any).fetch || require('node-fetch').default, + fetch: createSecureFetch(requestId), console: { log: (...args: any[]) => { const logMessage = `${args From 3e5d3735dc9e8be750f0537dea8210bc0ca0634f Mon Sep 17 00:00:00 2001 From: Adam Gough <77861281+aadamgough@users.noreply.github.com> Date: Sat, 13 Sep 2025 12:38:50 -0700 Subject: [PATCH 02/10] changed search for folders and workflows in logs (#1327) Co-authored-by: Adam Gough --- .../components/filters/components/folder.tsx | 105 +++++++++------- .../filters/components/workflow.tsx | 115 ++++++++++-------- 2 files changed, 120 insertions(+), 100 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/folder.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/folder.tsx index df73416e6a..02d943c47b 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/folder.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/folder.tsx @@ -1,14 +1,21 @@ -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { Check, ChevronDown } from 'lucide-react' import { useParams } from 'next/navigation' import { Button } from '@/components/ui/button' +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/components/ui/command' import { DropdownMenu, DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' +import { createLogger } from '@/lib/logs/console/logger' import { useFolderStore } from '@/stores/folders/store' import { useFilterStore } from '@/stores/logs/filters/store' @@ -26,6 +33,8 @@ export default function FolderFilter() { const workspaceId = params.workspaceId as string const [folders, setFolders] = useState([]) const [loading, setLoading] = useState(true) + const [search, setSearch] = useState('') + const logger = useMemo(() => createLogger('LogsFolderFilter'), []) // Fetch all available folders from the API useEffect(() => { @@ -62,7 +71,7 @@ export default function FolderFilter() { setFolders(folderOptions) } } catch (error) { - console.error('Failed to fetch folders:', error) + logger.error('Failed to fetch folders', { error }) } finally { setLoading(false) } @@ -105,49 +114,53 @@ export default function FolderFilter() { - { - e.preventDefault() - clearSelections() - }} - className='flex cursor-pointer items-center justify-between rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50' - > - All folders - {folderIds.length === 0 && } - - - {!loading && folders.length > 0 && } - - {!loading && - folders.map((folder) => ( - { - e.preventDefault() - toggleFolderId(folder.id) - }} - className='flex cursor-pointer items-center justify-between rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50' - > -
- - {folder.path} - -
- {isFolderSelected(folder.id) && } -
- ))} - - {loading && ( - - Loading folders... - - )} + + setSearch(v)} /> + + {loading ? 'Loading folders...' : 'No folders found.'} + + { + clearSelections() + }} + className='cursor-pointer' + > + All folders + {folderIds.length === 0 && ( + + )} + + {useMemo(() => { + const q = search.trim().toLowerCase() + const filtered = q + ? folders.filter((f) => (f.path || f.name).toLowerCase().includes(q)) + : folders + return filtered.map((folder) => ( + { + toggleFolderId(folder.id) + }} + className='cursor-pointer' + > +
+ + {folder.path} + +
+ {isFolderSelected(folder.id) && ( + + )} +
+ )) + }, [folders, search, folderIds])} +
+
+
) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/workflow.tsx index 8d4c2b6935..289ca6e593 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/workflow.tsx @@ -1,13 +1,20 @@ -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { Check, ChevronDown } from 'lucide-react' import { Button } from '@/components/ui/button' +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/components/ui/command' import { DropdownMenu, DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' +import { createLogger } from '@/lib/logs/console/logger' import { useFilterStore } from '@/stores/logs/filters/store' interface WorkflowOption { @@ -20,6 +27,8 @@ export default function Workflow() { const { workflowIds, toggleWorkflowId, setWorkflowIds } = useFilterStore() const [workflows, setWorkflows] = useState([]) const [loading, setLoading] = useState(true) + const [search, setSearch] = useState('') + const logger = useMemo(() => createLogger('LogsWorkflowFilter'), []) // Fetch all available workflows from the API useEffect(() => { @@ -37,7 +46,7 @@ export default function Workflow() { setWorkflows(workflowOptions) } } catch (error) { - console.error('Failed to fetch workflows:', error) + logger.error('Failed to fetch workflows', { error }) } finally { setLoading(false) } @@ -80,57 +89,55 @@ export default function Workflow() { - { - e.preventDefault() - clearSelections() - }} - className='flex cursor-pointer items-center justify-between rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50' - > - All workflows - {workflowIds.length === 0 && } - - - {!loading && workflows.length > 0 && } - - {!loading && - workflows.map((workflow) => ( - { - e.preventDefault() - toggleWorkflowId(workflow.id) - }} - className='flex cursor-pointer items-center justify-between rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50' - > -
-
- {workflow.name} -
- {isWorkflowSelected(workflow.id) && ( - - )} - - ))} - - {loading && ( - - Loading workflows... - - )} + + setSearch(v)} /> + + {loading ? 'Loading workflows...' : 'No workflows found.'} + + { + clearSelections() + }} + className='cursor-pointer' + > + All workflows + {workflowIds.length === 0 && ( + + )} + + {useMemo(() => { + const q = search.trim().toLowerCase() + const filtered = q + ? workflows.filter((w) => w.name.toLowerCase().includes(q)) + : workflows + return filtered.map((workflow) => ( + { + toggleWorkflowId(workflow.id) + }} + className='cursor-pointer' + > +
+
+ {workflow.name} +
+ {isWorkflowSelected(workflow.id) && ( + + )} + + )) + }, [workflows, search, workflowIds])} + + + ) From f2ec43e4f98cfc2350c5d7f726a02583414f6464 Mon Sep 17 00:00:00 2001 From: Waleed Date: Sat, 13 Sep 2025 17:32:41 -0700 Subject: [PATCH 03/10] feat(logs): added intelligent search with suggestions to logs (#1329) * update infra and remove railway * feat(logs): added intelligent search to logs * Revert "update infra and remove railway" This reverts commit abfa2f8d51901247acc6397960210569e84d72b1. * cleanup * cleanup --- .../components/filters/components/folder.tsx | 3 +- .../filters/components/workflow.tsx | 6 +- .../logs/components/search/search.tsx | 248 +++++++++++ .../logs/hooks/use-autocomplete.ts | 356 +++++++++++++++ .../app/workspace/[workspaceId]/logs/logs.tsx | 86 +++- apps/sim/lib/logs/query-parser.ts | 208 +++++++++ apps/sim/lib/logs/search-suggestions.test.ts | 157 +++++++ apps/sim/lib/logs/search-suggestions.ts | 420 ++++++++++++++++++ 8 files changed, 1463 insertions(+), 21 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/search/search.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/hooks/use-autocomplete.ts create mode 100644 apps/sim/lib/logs/query-parser.ts create mode 100644 apps/sim/lib/logs/search-suggestions.test.ts create mode 100644 apps/sim/lib/logs/search-suggestions.ts diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/folder.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/folder.tsx index 02d943c47b..e5cb8d7fbc 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/folder.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/folder.tsx @@ -19,6 +19,8 @@ import { createLogger } from '@/lib/logs/console/logger' import { useFolderStore } from '@/stores/folders/store' import { useFilterStore } from '@/stores/logs/filters/store' +const logger = createLogger('LogsFolderFilter') + interface FolderOption { id: string name: string @@ -34,7 +36,6 @@ export default function FolderFilter() { const [folders, setFolders] = useState([]) const [loading, setLoading] = useState(true) const [search, setSearch] = useState('') - const logger = useMemo(() => createLogger('LogsFolderFilter'), []) // Fetch all available folders from the API useEffect(() => { diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/workflow.tsx index 289ca6e593..22ced167b1 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/workflow.tsx @@ -17,6 +17,8 @@ import { import { createLogger } from '@/lib/logs/console/logger' import { useFilterStore } from '@/stores/logs/filters/store' +const logger = createLogger('LogsWorkflowFilter') + interface WorkflowOption { id: string name: string @@ -28,7 +30,6 @@ export default function Workflow() { const [workflows, setWorkflows] = useState([]) const [loading, setLoading] = useState(true) const [search, setSearch] = useState('') - const logger = useMemo(() => createLogger('LogsWorkflowFilter'), []) // Fetch all available workflows from the API useEffect(() => { @@ -55,7 +56,6 @@ export default function Workflow() { fetchWorkflows() }, []) - // Get display text for the dropdown button const getSelectedWorkflowsText = () => { if (workflowIds.length === 0) return 'All workflows' if (workflowIds.length === 1) { @@ -65,12 +65,10 @@ export default function Workflow() { return `${workflowIds.length} workflows selected` } - // Check if a workflow is selected const isWorkflowSelected = (workflowId: string) => { return workflowIds.includes(workflowId) } - // Clear all selections const clearSelections = () => { setWorkflowIds([]) } diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/search/search.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/search/search.tsx new file mode 100644 index 0000000000..051aa2db18 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/search/search.tsx @@ -0,0 +1,248 @@ +'use client' + +import { useMemo } from 'react' +import { Search, X } from 'lucide-react' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { parseQuery } from '@/lib/logs/query-parser' +import { SearchSuggestions } from '@/lib/logs/search-suggestions' +import { cn } from '@/lib/utils' +import { useAutocomplete } from '@/app/workspace/[workspaceId]/logs/hooks/use-autocomplete' + +interface AutocompleteSearchProps { + value: string + onChange: (value: string) => void + placeholder?: string + availableWorkflows?: string[] + availableFolders?: string[] + className?: string +} + +export function AutocompleteSearch({ + value, + onChange, + placeholder = 'Search logs...', + availableWorkflows = [], + availableFolders = [], + className, +}: AutocompleteSearchProps) { + const suggestionEngine = useMemo(() => { + return new SearchSuggestions(availableWorkflows, availableFolders) + }, [availableWorkflows, availableFolders]) + + const { + state, + inputRef, + dropdownRef, + handleInputChange, + handleCursorChange, + handleSuggestionHover, + handleSuggestionSelect, + handleKeyDown, + handleFocus, + handleBlur, + } = useAutocomplete({ + getSuggestions: (inputValue, cursorPos) => + suggestionEngine.getSuggestions(inputValue, cursorPos), + generatePreview: (suggestion, inputValue, cursorPos) => + suggestionEngine.generatePreview(suggestion, inputValue, cursorPos), + onQueryChange: onChange, + validateQuery: (query) => suggestionEngine.validateQuery(query), + debounceMs: 100, + }) + + const parsedQuery = parseQuery(value) + const hasFilters = parsedQuery.filters.length > 0 + const hasTextSearch = parsedQuery.textSearch.length > 0 + + const onInputChange = (e: React.ChangeEvent) => { + const newValue = e.target.value + const cursorPos = e.target.selectionStart || 0 + handleInputChange(newValue, cursorPos) + } + + const updateCursorPosition = (element: HTMLInputElement) => { + const cursorPos = element.selectionStart || 0 + handleCursorChange(cursorPos) + } + + const removeFilter = (filterToRemove: (typeof parsedQuery.filters)[0]) => { + const remainingFilters = parsedQuery.filters.filter( + (f) => !(f.field === filterToRemove.field && f.value === filterToRemove.value) + ) + + const filterStrings = remainingFilters.map( + (f) => `${f.field}:${f.operator !== '=' ? f.operator : ''}${f.originalValue}` + ) + + const newQuery = [...filterStrings, parsedQuery.textSearch].filter(Boolean).join(' ') + + onChange(newQuery) + } + + return ( +
+ {/* Search Input */} +
+ + + {/* Text display with ghost text */} +
+ {/* Invisible input for cursor and interactions */} + updateCursorPosition(e.currentTarget)} + onKeyUp={(e) => updateCursorPosition(e.currentTarget)} + onKeyDown={handleKeyDown} + onSelect={(e) => updateCursorPosition(e.currentTarget)} + className='relative z-10 w-full border-0 bg-transparent p-0 font-[380] font-sans text-base text-transparent leading-none placeholder:text-muted-foreground focus-visible:ring-0 focus-visible:ring-offset-0' + style={{ background: 'transparent' }} + /> + + {/* Always-visible text overlay */} +
+ + {state.inputValue} + {state.showPreview && + state.previewValue && + state.previewValue !== state.inputValue && + state.inputValue && ( + + {state.previewValue.slice(state.inputValue.length)} + + )} + +
+
+ + {/* Clear all button */} + {(hasFilters || hasTextSearch) && ( + + )} +
+ + {/* Suggestions Dropdown */} + {state.isOpen && state.suggestions.length > 0 && ( +
+
+ {state.suggestionType === 'filter-keys' && ( +
+ SUGGESTED FILTERS +
+ )} + {state.suggestionType === 'filter-values' && ( +
+ {state.suggestions[0]?.category?.toUpperCase() || 'VALUES'} +
+ )} + + {state.suggestions.map((suggestion, index) => ( + + ))} +
+
+ )} + + {/* Active filters as chips */} + {hasFilters && ( +
+ ACTIVE FILTERS: + {parsedQuery.filters.map((filter, index) => ( + + {filter.field}: + + {filter.operator !== '=' && filter.operator} + {filter.originalValue} + + + + ))} + {parsedQuery.filters.length > 1 && ( + + )} +
+ )} + + {/* Text search indicator */} + {hasTextSearch && ( +
+ TEXT SEARCH: + + "{parsedQuery.textSearch}" + +
+ )} +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/logs/hooks/use-autocomplete.ts b/apps/sim/app/workspace/[workspaceId]/logs/hooks/use-autocomplete.ts new file mode 100644 index 0000000000..1e821208f5 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/logs/hooks/use-autocomplete.ts @@ -0,0 +1,356 @@ +import { useCallback, useMemo, useReducer, useRef } from 'react' + +export interface Suggestion { + id: string + value: string + label: string + description?: string + category?: string +} + +export interface SuggestionGroup { + type: 'filter-keys' | 'filter-values' + filterKey?: string + suggestions: Suggestion[] +} + +interface AutocompleteState { + // Input state + inputValue: string + cursorPosition: number + + // Dropdown state + isOpen: boolean + suggestions: Suggestion[] + suggestionType: 'filter-keys' | 'filter-values' | null + highlightedIndex: number + + // Preview state + previewValue: string + showPreview: boolean + + // Query state + isValidQuery: boolean + pendingQuery: string | null +} + +type AutocompleteAction = + | { type: 'SET_INPUT_VALUE'; payload: { value: string; cursorPosition: number } } + | { type: 'SET_CURSOR_POSITION'; payload: number } + | { type: 'OPEN_DROPDOWN'; payload: SuggestionGroup } + | { type: 'CLOSE_DROPDOWN' } + | { type: 'HIGHLIGHT_SUGGESTION'; payload: { index: number; preview?: string } } + | { type: 'SET_PREVIEW'; payload: { value: string; show: boolean } } + | { type: 'CLEAR_PREVIEW' } + | { type: 'SET_QUERY_VALIDITY'; payload: boolean } + | { type: 'RESET' } + +const initialState: AutocompleteState = { + inputValue: '', + cursorPosition: 0, + isOpen: false, + suggestions: [], + suggestionType: null, + highlightedIndex: -1, + previewValue: '', + showPreview: false, + isValidQuery: true, + pendingQuery: null, +} + +function autocompleteReducer( + state: AutocompleteState, + action: AutocompleteAction +): AutocompleteState { + switch (action.type) { + case 'SET_INPUT_VALUE': + return { + ...state, + inputValue: action.payload.value, + cursorPosition: action.payload.cursorPosition, + previewValue: '', + showPreview: false, + } + + case 'SET_CURSOR_POSITION': + return { + ...state, + cursorPosition: action.payload, + } + + case 'OPEN_DROPDOWN': + return { + ...state, + isOpen: true, + suggestions: action.payload.suggestions, + suggestionType: action.payload.type, + highlightedIndex: action.payload.suggestions.length > 0 ? 0 : -1, + } + + case 'CLOSE_DROPDOWN': + return { + ...state, + isOpen: false, + suggestions: [], + suggestionType: null, + highlightedIndex: -1, + previewValue: '', + showPreview: false, + } + + case 'HIGHLIGHT_SUGGESTION': + return { + ...state, + highlightedIndex: action.payload.index, + previewValue: action.payload.preview || '', + showPreview: !!action.payload.preview, + } + + case 'SET_PREVIEW': + return { + ...state, + previewValue: action.payload.value, + showPreview: action.payload.show, + } + + case 'CLEAR_PREVIEW': + return { + ...state, + previewValue: '', + showPreview: false, + } + + case 'SET_QUERY_VALIDITY': + return { + ...state, + isValidQuery: action.payload, + } + + case 'RESET': + return initialState + + default: + return state + } +} + +export interface AutocompleteOptions { + getSuggestions: (value: string, cursorPosition: number) => SuggestionGroup | null + generatePreview: (suggestion: Suggestion, currentValue: string, cursorPosition: number) => string + onQueryChange: (query: string) => void + validateQuery?: (query: string) => boolean + debounceMs?: number +} + +export function useAutocomplete({ + getSuggestions, + generatePreview, + onQueryChange, + validateQuery, + debounceMs = 150, +}: AutocompleteOptions) { + const [state, dispatch] = useReducer(autocompleteReducer, initialState) + const inputRef = useRef(null) + const dropdownRef = useRef(null) + const debounceRef = useRef(null) + + const currentSuggestion = useMemo(() => { + if (state.highlightedIndex >= 0 && state.suggestions[state.highlightedIndex]) { + return state.suggestions[state.highlightedIndex] + } + return null + }, [state.highlightedIndex, state.suggestions]) + + const updateSuggestions = useCallback(() => { + const suggestionGroup = getSuggestions(state.inputValue, state.cursorPosition) + + if (suggestionGroup && suggestionGroup.suggestions.length > 0) { + dispatch({ type: 'OPEN_DROPDOWN', payload: suggestionGroup }) + + const firstSuggestion = suggestionGroup.suggestions[0] + const preview = generatePreview(firstSuggestion, state.inputValue, state.cursorPosition) + dispatch({ + type: 'HIGHLIGHT_SUGGESTION', + payload: { index: 0, preview }, + }) + } else { + dispatch({ type: 'CLOSE_DROPDOWN' }) + } + }, [state.inputValue, state.cursorPosition, getSuggestions, generatePreview]) + + const handleInputChange = useCallback( + (value: string, cursorPosition: number) => { + dispatch({ type: 'SET_INPUT_VALUE', payload: { value, cursorPosition } }) + + const isValid = validateQuery ? validateQuery(value) : true + dispatch({ type: 'SET_QUERY_VALIDITY', payload: isValid }) + + if (isValid) { + onQueryChange(value) + } + + if (debounceRef.current) { + clearTimeout(debounceRef.current) + } + + debounceRef.current = setTimeout(updateSuggestions, debounceMs) + }, + [updateSuggestions, onQueryChange, validateQuery, debounceMs] + ) + + const handleCursorChange = useCallback( + (position: number) => { + dispatch({ type: 'SET_CURSOR_POSITION', payload: position }) + updateSuggestions() + }, + [updateSuggestions] + ) + + const handleSuggestionHover = useCallback( + (index: number) => { + if (index >= 0 && index < state.suggestions.length) { + const suggestion = state.suggestions[index] + const preview = generatePreview(suggestion, state.inputValue, state.cursorPosition) + dispatch({ + type: 'HIGHLIGHT_SUGGESTION', + payload: { index, preview }, + }) + } + }, + [state.suggestions, state.inputValue, state.cursorPosition, generatePreview] + ) + + const handleSuggestionSelect = useCallback( + (suggestion?: Suggestion) => { + const selectedSuggestion = suggestion || currentSuggestion + if (!selectedSuggestion) return + + let newValue = generatePreview(selectedSuggestion, state.inputValue, state.cursorPosition) + + let newCursorPosition = newValue.length + + if (state.suggestionType === 'filter-keys' && selectedSuggestion.value.endsWith(':')) { + newCursorPosition = newValue.lastIndexOf(':') + 1 + } else if (state.suggestionType === 'filter-values') { + newValue = `${newValue} ` + newCursorPosition = newValue.length + } + + dispatch({ + type: 'SET_INPUT_VALUE', + payload: { value: newValue, cursorPosition: newCursorPosition }, + }) + + const isValid = validateQuery ? validateQuery(newValue.trim()) : true + dispatch({ type: 'SET_QUERY_VALIDITY', payload: isValid }) + + if (isValid) { + onQueryChange(newValue.trim()) + } + + if (inputRef.current) { + inputRef.current.focus() + requestAnimationFrame(() => { + if (inputRef.current) { + inputRef.current.setSelectionRange(newCursorPosition, newCursorPosition) + } + }) + } + + setTimeout(updateSuggestions, 0) + }, + [ + currentSuggestion, + state.inputValue, + state.cursorPosition, + state.suggestionType, + generatePreview, + onQueryChange, + validateQuery, + updateSuggestions, + ] + ) + + const handleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + if (!state.isOpen) return + + switch (event.key) { + case 'ArrowDown': { + event.preventDefault() + const nextIndex = Math.min(state.highlightedIndex + 1, state.suggestions.length - 1) + handleSuggestionHover(nextIndex) + break + } + + case 'ArrowUp': { + event.preventDefault() + const prevIndex = Math.max(state.highlightedIndex - 1, 0) + handleSuggestionHover(prevIndex) + break + } + + case 'Enter': + event.preventDefault() + handleSuggestionSelect() + break + + case 'Escape': + event.preventDefault() + dispatch({ type: 'CLOSE_DROPDOWN' }) + break + + case 'Tab': + if (currentSuggestion) { + event.preventDefault() + handleSuggestionSelect() + } else { + dispatch({ type: 'CLOSE_DROPDOWN' }) + } + break + } + }, + [ + state.isOpen, + state.highlightedIndex, + state.suggestions.length, + handleSuggestionHover, + handleSuggestionSelect, + currentSuggestion, + ] + ) + + const handleFocus = useCallback(() => { + updateSuggestions() + }, [updateSuggestions]) + + const handleBlur = useCallback(() => { + setTimeout(() => { + dispatch({ type: 'CLOSE_DROPDOWN' }) + }, 150) + }, []) + + return { + // State + state, + currentSuggestion, + + // Refs + inputRef, + dropdownRef, + + // Handlers + handleInputChange, + handleCursorChange, + handleSuggestionHover, + handleSuggestionSelect, + handleKeyDown, + handleFocus, + handleBlur, + + // Actions + closeDropdown: () => dispatch({ type: 'CLOSE_DROPDOWN' }), + clearPreview: () => dispatch({ type: 'CLEAR_PREVIEW' }), + reset: () => dispatch({ type: 'RESET' }), + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx index aa51ac7ef0..595171743a 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx @@ -1,13 +1,14 @@ 'use client' import { useCallback, useEffect, useRef, useState } from 'react' -import { AlertCircle, Info, Loader2, Play, RefreshCw, Search, Square } from 'lucide-react' +import { AlertCircle, Info, Loader2, Play, RefreshCw, Square } from 'lucide-react' import { useParams } from 'next/navigation' import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { createLogger } from '@/lib/logs/console/logger' +import { parseQuery, queryToApiParams } from '@/lib/logs/query-parser' import { cn } from '@/lib/utils' +import { AutocompleteSearch } from '@/app/workspace/[workspaceId]/logs/components/search/search' import { Sidebar } from '@/app/workspace/[workspaceId]/logs/components/sidebar/sidebar' import { formatDate } from '@/app/workspace/[workspaceId]/logs/utils/format-date' import { useDebounce } from '@/hooks/use-debounce' @@ -17,7 +18,6 @@ import type { LogsResponse, WorkflowLog } from '@/stores/logs/filters/types' const logger = createLogger('Logs') const LOGS_PER_PAGE = 50 -// Get color for different trigger types using app's color scheme const getTriggerColor = (trigger: string | null | undefined): string => { if (!trigger) return '#9ca3af' @@ -98,6 +98,10 @@ export default function Logs() { const [searchQuery, setSearchQuery] = useState(storeSearchQuery) const debouncedSearchQuery = useDebounce(searchQuery, 300) + // Available data for suggestions + const [availableWorkflows, setAvailableWorkflows] = useState([]) + const [availableFolders, setAvailableFolders] = useState([]) + // Live and refresh state const [isLive, setIsLive] = useState(false) const [isRefreshing, setIsRefreshing] = useState(false) @@ -108,6 +112,22 @@ export default function Logs() { setSearchQuery(storeSearchQuery) }, [storeSearchQuery]) + useEffect(() => { + const workflowNames = new Set() + const folderNames = new Set() + + logs.forEach((log) => { + if (log.workflow?.name) { + workflowNames.add(log.workflow.name) + } + // Note: folder info would need to be added to the logs response + // For now, we'll leave folders empty + }) + + setAvailableWorkflows(Array.from(workflowNames).slice(0, 10)) // Limit to top 10 + setAvailableFolders([]) // TODO: Add folder data to logs response + }, [logs]) + // Update store when debounced search query changes useEffect(() => { if (isInitialized.current && debouncedSearchQuery !== storeSearchQuery) { @@ -323,7 +343,27 @@ export default function Logs() { // Get fresh query params by calling buildQueryParams from store const { buildQueryParams: getCurrentQueryParams } = useFilterStore.getState() const queryParams = getCurrentQueryParams(pageNum, LOGS_PER_PAGE) - const response = await fetch(`/api/logs?${queryParams}&details=basic`) + + // Parse the current search query for enhanced filtering + const parsedQuery = parseQuery(searchQuery) + const enhancedParams = queryToApiParams(parsedQuery) + + // Add enhanced search parameters to the query string + const allParams = new URLSearchParams(queryParams) + Object.entries(enhancedParams).forEach(([key, value]) => { + if (key === 'triggers' && allParams.has('triggers')) { + // Combine triggers from both sources + const existingTriggers = allParams.get('triggers')?.split(',') || [] + const searchTriggers = value.split(',') + const combined = [...new Set([...existingTriggers, ...searchTriggers])] + allParams.set('triggers', combined.join(',')) + } else { + allParams.set(key, value) + } + }) + + allParams.set('details', 'basic') + const response = await fetch(`/api/logs?${allParams.toString()}`) if (!response.ok) { throw new Error(`Error fetching logs: ${response.statusText}`) @@ -430,12 +470,28 @@ export default function Logs() { params.set('offset', '0') // Always start from page 1 params.set('workspaceId', workspaceId) - // Add filters + // Parse the search query for enhanced filtering + const parsedQuery = parseQuery(searchQuery) + const enhancedParams = queryToApiParams(parsedQuery) + + // Add filters from store if (level !== 'all') params.set('level', level) if (triggers.length > 0) params.set('triggers', triggers.join(',')) if (workflowIds.length > 0) params.set('workflowIds', workflowIds.join(',')) if (folderIds.length > 0) params.set('folderIds', folderIds.join(',')) - if (searchQuery.trim()) params.set('search', searchQuery.trim()) + + // Add enhanced search parameters (these may override some store filters) + Object.entries(enhancedParams).forEach(([key, value]) => { + if (key === 'triggers' && params.has('triggers')) { + // Combine triggers from both sources + const storeTriggers = params.get('triggers')?.split(',') || [] + const searchTriggers = value.split(',') + const combined = [...new Set([...storeTriggers, ...searchTriggers])] + params.set('triggers', combined.join(',')) + } else { + params.set(key, value) + } + }) // Add time range filter if (timeRange !== 'All time') { @@ -588,16 +644,14 @@ export default function Logs() {
{/* Search and Controls */} -
-
- - setSearchQuery(e.target.value)} - className='flex-1 border-0 bg-transparent px-0 font-[380] font-sans text-base text-foreground leading-none placeholder:text-muted-foreground focus-visible:ring-0 focus-visible:ring-offset-0' - /> -
+
+
diff --git a/apps/sim/lib/logs/query-parser.ts b/apps/sim/lib/logs/query-parser.ts new file mode 100644 index 0000000000..9f809b8e05 --- /dev/null +++ b/apps/sim/lib/logs/query-parser.ts @@ -0,0 +1,208 @@ +/** + * Query language parser for logs search + * + * Supports syntax like: + * level:error workflow:"my-workflow" trigger:api cost:>0.005 date:today + */ + +export interface ParsedFilter { + field: string + operator: '=' | '>' | '<' | '>=' | '<=' | '!=' + value: string | number | boolean + originalValue: string +} + +export interface ParsedQuery { + filters: ParsedFilter[] + textSearch: string // Any remaining text not in field:value format +} + +const FILTER_FIELDS = { + level: 'string', + status: 'string', // alias for level + workflow: 'string', + trigger: 'string', + execution: 'string', + id: 'string', + cost: 'number', + duration: 'number', + date: 'date', + folder: 'string', +} as const + +type FilterField = keyof typeof FILTER_FIELDS + +/** + * Parse a search query string into structured filters and text search + */ +export function parseQuery(query: string): ParsedQuery { + const filters: ParsedFilter[] = [] + const tokens: string[] = [] + + const filterRegex = /(\w+):((?:[>=')) { + operator = '>=' + value = value.slice(2) + } else if (value.startsWith('<=')) { + operator = '<=' + value = value.slice(2) + } else if (value.startsWith('!=')) { + operator = '!=' + value = value.slice(2) + } else if (value.startsWith('>')) { + operator = '>' + value = value.slice(1) + } else if (value.startsWith('<')) { + operator = '<' + value = value.slice(1) + } else if (value.startsWith('=')) { + operator = '=' + value = value.slice(1) + } + + const originalValue = value + if (value.startsWith('"') && value.endsWith('"')) { + value = value.slice(1, -1) + } + + let parsedValue: string | number | boolean = value + + if (fieldType === 'number') { + if (field === 'duration' && value.endsWith('ms')) { + parsedValue = Number.parseFloat(value.slice(0, -2)) + } else if (field === 'duration' && value.endsWith('s')) { + parsedValue = Number.parseFloat(value.slice(0, -1)) * 1000 // Convert to ms + } else { + parsedValue = Number.parseFloat(value) + } + + if (Number.isNaN(parsedValue)) { + return null + } + } + + return { + field: filterField, + operator, + value: parsedValue, + originalValue, + } +} + +/** + * Convert parsed query back to URL parameters for the logs API + */ +export function queryToApiParams(parsedQuery: ParsedQuery): Record { + const params: Record = {} + + if (parsedQuery.textSearch) { + params.search = parsedQuery.textSearch + } + + for (const filter of parsedQuery.filters) { + switch (filter.field) { + case 'level': + case 'status': + if (filter.operator === '=') { + params.level = filter.value as string + } + break + + case 'trigger': + if (filter.operator === '=') { + const existing = params.triggers ? params.triggers.split(',') : [] + existing.push(filter.value as string) + params.triggers = existing.join(',') + } + break + + case 'workflow': + if (filter.operator === '=') { + params.workflowName = filter.value as string + } + break + + case 'execution': + if (filter.operator === '=' && parsedQuery.textSearch) { + params.search = `${parsedQuery.textSearch} ${filter.value}`.trim() + } else if (filter.operator === '=') { + params.search = filter.value as string + } + break + + case 'date': + if (filter.operator === '=' && filter.value === 'today') { + const today = new Date() + today.setHours(0, 0, 0, 0) + params.startDate = today.toISOString() + } else if (filter.operator === '=' && filter.value === 'yesterday') { + const yesterday = new Date() + yesterday.setDate(yesterday.getDate() - 1) + yesterday.setHours(0, 0, 0, 0) + params.startDate = yesterday.toISOString() + + const endOfYesterday = new Date(yesterday) + endOfYesterday.setHours(23, 59, 59, 999) + params.endDate = endOfYesterday.toISOString() + } + break + + case 'cost': + params[`cost_${filter.operator}_${filter.value}`] = 'true' + break + + case 'duration': + params[`duration_${filter.operator}_${filter.value}`] = 'true' + break + } + } + + return params +} diff --git a/apps/sim/lib/logs/search-suggestions.test.ts b/apps/sim/lib/logs/search-suggestions.test.ts new file mode 100644 index 0000000000..2bafdd7a25 --- /dev/null +++ b/apps/sim/lib/logs/search-suggestions.test.ts @@ -0,0 +1,157 @@ +import { describe, expect, it } from 'vitest' +import { SearchSuggestions } from './search-suggestions' + +describe('SearchSuggestions', () => { + const engine = new SearchSuggestions(['workflow1', 'workflow2'], ['folder1', 'folder2']) + + describe('validateQuery', () => { + it.concurrent('should return false for incomplete filter expressions', () => { + expect(engine.validateQuery('level:')).toBe(false) + expect(engine.validateQuery('trigger:')).toBe(false) + expect(engine.validateQuery('cost:')).toBe(false) + expect(engine.validateQuery('some text level:')).toBe(false) + }) + + it.concurrent('should return false for incomplete quoted strings', () => { + expect(engine.validateQuery('workflow:"incomplete')).toBe(false) + expect(engine.validateQuery('level:error workflow:"incomplete')).toBe(false) + expect(engine.validateQuery('"incomplete string')).toBe(false) + }) + + it.concurrent('should return true for complete queries', () => { + expect(engine.validateQuery('level:error')).toBe(true) + expect(engine.validateQuery('trigger:api')).toBe(true) + expect(engine.validateQuery('cost:>0.01')).toBe(true) + expect(engine.validateQuery('workflow:"test workflow"')).toBe(true) + expect(engine.validateQuery('level:error trigger:api')).toBe(true) + expect(engine.validateQuery('some search text')).toBe(true) + expect(engine.validateQuery('')).toBe(true) + }) + + it.concurrent('should return true for mixed complete queries', () => { + expect(engine.validateQuery('search text level:error')).toBe(true) + expect(engine.validateQuery('level:error some search')).toBe(true) + expect(engine.validateQuery('workflow:"test" level:error search')).toBe(true) + }) + }) + + describe('getSuggestions', () => { + it.concurrent('should return filter key suggestions at the beginning', () => { + const result = engine.getSuggestions('', 0) + expect(result?.type).toBe('filter-keys') + expect(result?.suggestions.length).toBeGreaterThan(0) + expect(result?.suggestions.some((s) => s.value === 'level:')).toBe(true) + }) + + it.concurrent('should return filter key suggestions for partial matches', () => { + const result = engine.getSuggestions('lev', 3) + expect(result?.type).toBe('filter-keys') + expect(result?.suggestions.some((s) => s.value === 'level:')).toBe(true) + }) + + it.concurrent('should return filter value suggestions after colon', () => { + const result = engine.getSuggestions('level:', 6) + expect(result?.type).toBe('filter-values') + expect(result?.suggestions.length).toBeGreaterThan(0) + expect(result?.suggestions.some((s) => s.value === 'error')).toBe(true) + }) + + it.concurrent('should return filtered value suggestions for partial values', () => { + const result = engine.getSuggestions('level:err', 9) + expect(result?.type).toBe('filter-values') + expect(result?.suggestions.some((s) => s.value === 'error')).toBe(true) + }) + + it.concurrent('should handle workflow suggestions', () => { + const result = engine.getSuggestions('workflow:', 9) + expect(result?.type).toBe('filter-values') + expect(result?.suggestions.some((s) => s.label === 'workflow1')).toBe(true) + }) + + it.concurrent('should return null for text search context', () => { + const result = engine.getSuggestions('some random text', 10) + expect(result).toBe(null) + }) + + it.concurrent('should show filter key suggestions after completing a filter', () => { + const result = engine.getSuggestions('level:error ', 12) + expect(result?.type).toBe('filter-keys') + expect(result?.suggestions.length).toBeGreaterThan(0) + expect(result?.suggestions.some((s) => s.value === 'level:')).toBe(true) + expect(result?.suggestions.some((s) => s.value === 'trigger:')).toBe(true) + }) + + it.concurrent('should show filter key suggestions after multiple completed filters', () => { + const result = engine.getSuggestions('level:error trigger:api ', 24) + expect(result?.type).toBe('filter-keys') + expect(result?.suggestions.length).toBeGreaterThan(0) + }) + + it.concurrent('should handle partial filter keys after existing filters', () => { + const result = engine.getSuggestions('level:error lev', 15) + expect(result?.type).toBe('filter-keys') + expect(result?.suggestions.some((s) => s.value === 'level:')).toBe(true) + }) + + it.concurrent('should handle filter values after existing filters', () => { + const result = engine.getSuggestions('level:error level:', 18) + expect(result?.type).toBe('filter-values') + expect(result?.suggestions.some((s) => s.value === 'info')).toBe(true) + }) + }) + + describe('generatePreview', () => { + it.concurrent('should generate correct preview for filter keys', () => { + const suggestion = { id: 'test', value: 'level:', label: 'Status', category: 'filters' } + const preview = engine.generatePreview(suggestion, '', 0) + expect(preview).toBe('level:') + }) + + it.concurrent('should generate correct preview for filter values', () => { + const suggestion = { id: 'test', value: 'error', label: 'Error', category: 'level' } + const preview = engine.generatePreview(suggestion, 'level:', 6) + expect(preview).toBe('level:error') + }) + + it.concurrent('should handle partial replacements correctly', () => { + const suggestion = { id: 'test', value: 'level:', label: 'Status', category: 'filters' } + const preview = engine.generatePreview(suggestion, 'lev', 3) + expect(preview).toBe('level:') + }) + + it.concurrent('should handle quoted workflow values', () => { + const suggestion = { + id: 'test', + value: '"workflow1"', + label: 'workflow1', + category: 'workflow', + } + const preview = engine.generatePreview(suggestion, 'workflow:', 9) + expect(preview).toBe('workflow:"workflow1"') + }) + + it.concurrent('should add space when adding filter after completed filter', () => { + const suggestion = { id: 'test', value: 'trigger:', label: 'Trigger', category: 'filters' } + const preview = engine.generatePreview(suggestion, 'level:error ', 12) + expect(preview).toBe('level:error trigger:') + }) + + it.concurrent('should handle multiple completed filters', () => { + const suggestion = { id: 'test', value: 'cost:', label: 'Cost', category: 'filters' } + const preview = engine.generatePreview(suggestion, 'level:error trigger:api ', 24) + expect(preview).toBe('level:error trigger:api cost:') + }) + + it.concurrent('should handle adding same filter type multiple times', () => { + const suggestion = { id: 'test', value: 'level:', label: 'Status', category: 'filters' } + const preview = engine.generatePreview(suggestion, 'level:error ', 12) + expect(preview).toBe('level:error level:') + }) + + it.concurrent('should handle filter value after existing filters', () => { + const suggestion = { id: 'test', value: 'info', label: 'Info', category: 'level' } + const preview = engine.generatePreview(suggestion, 'level:error level:', 19) + expect(preview).toBe('level:error level:info') + }) + }) +}) diff --git a/apps/sim/lib/logs/search-suggestions.ts b/apps/sim/lib/logs/search-suggestions.ts new file mode 100644 index 0000000000..20d8bec2a0 --- /dev/null +++ b/apps/sim/lib/logs/search-suggestions.ts @@ -0,0 +1,420 @@ +import type { + Suggestion, + SuggestionGroup, +} from '@/app/workspace/[workspaceId]/logs/hooks/use-autocomplete' + +export interface FilterDefinition { + key: string + label: string + description: string + options: Array<{ + value: string + label: string + description?: string + }> +} + +export const FILTER_DEFINITIONS: FilterDefinition[] = [ + { + key: 'level', + label: 'Status', + description: 'Filter by log level', + options: [ + { value: 'error', label: 'Error', description: 'Error logs only' }, + { value: 'info', label: 'Info', description: 'Info logs only' }, + ], + }, + { + key: 'trigger', + label: 'Trigger', + description: 'Filter by trigger type', + options: [ + { value: 'api', label: 'API', description: 'API-triggered executions' }, + { value: 'manual', label: 'Manual', description: 'Manually triggered executions' }, + { value: 'webhook', label: 'Webhook', description: 'Webhook-triggered executions' }, + { value: 'chat', label: 'Chat', description: 'Chat-triggered executions' }, + { value: 'schedule', label: 'Schedule', description: 'Scheduled executions' }, + ], + }, + { + key: 'cost', + label: 'Cost', + description: 'Filter by execution cost', + options: [ + { value: '>0.01', label: 'Over $0.01', description: 'Executions costing more than $0.01' }, + { + value: '<0.005', + label: 'Under $0.005', + description: 'Executions costing less than $0.005', + }, + { value: '>0.05', label: 'Over $0.05', description: 'Executions costing more than $0.05' }, + { value: '=0', label: 'Free', description: 'Free executions' }, + { value: '>0', label: 'Paid', description: 'Executions with cost' }, + ], + }, + { + key: 'date', + label: 'Date', + description: 'Filter by date range', + options: [ + { value: 'today', label: 'Today', description: "Today's logs" }, + { value: 'yesterday', label: 'Yesterday', description: "Yesterday's logs" }, + { value: 'this-week', label: 'This week', description: "This week's logs" }, + { value: 'last-week', label: 'Last week', description: "Last week's logs" }, + { value: 'this-month', label: 'This month', description: "This month's logs" }, + ], + }, + { + key: 'duration', + label: 'Duration', + description: 'Filter by execution duration', + options: [ + { value: '>5s', label: 'Over 5s', description: 'Executions longer than 5 seconds' }, + { value: '<1s', label: 'Under 1s', description: 'Executions shorter than 1 second' }, + { value: '>10s', label: 'Over 10s', description: 'Executions longer than 10 seconds' }, + { value: '>30s', label: 'Over 30s', description: 'Executions longer than 30 seconds' }, + { value: '<500ms', label: 'Under 0.5s', description: 'Very fast executions' }, + ], + }, +] + +interface QueryContext { + type: 'initial' | 'filter-key-partial' | 'filter-value-context' | 'text-search' + filterKey?: string + partialInput?: string + startPosition?: number + endPosition?: number +} + +export class SearchSuggestions { + private availableWorkflows: string[] + private availableFolders: string[] + + constructor(availableWorkflows: string[] = [], availableFolders: string[] = []) { + this.availableWorkflows = availableWorkflows + this.availableFolders = availableFolders + } + + updateAvailableData(workflows: string[] = [], folders: string[] = []) { + this.availableWorkflows = workflows + this.availableFolders = folders + } + + /** + * Check if a filter value is complete (matches a valid option) + */ + private isCompleteFilterValue(filterKey: string, value: string): boolean { + const filterDef = FILTER_DEFINITIONS.find((f) => f.key === filterKey) + if (filterDef) { + return filterDef.options.some((option) => option.value === value) + } + + // For workflow and folder filters, any quoted value is considered complete + if (filterKey === 'workflow' || filterKey === 'folder') { + return value.startsWith('"') && value.endsWith('"') && value.length > 2 + } + + return false + } + + /** + * Analyze the current input context to determine what suggestions to show. + */ + private analyzeContext(input: string, cursorPosition: number): QueryContext { + const textBeforeCursor = input.slice(0, cursorPosition) + + if (textBeforeCursor === '' || textBeforeCursor.endsWith(' ')) { + return { type: 'initial' } + } + + // Check for filter value context (must be after a space or at start, and not empty value) + const filterValueMatch = textBeforeCursor.match(/(?:^|\s)(\w+):([\w"<>=!]*)$/) + if (filterValueMatch && filterValueMatch[2].length > 0 && !filterValueMatch[2].includes(' ')) { + const filterKey = filterValueMatch[1] + const filterValue = filterValueMatch[2] + + // If the filter value is complete, treat as ready for next filter + if (this.isCompleteFilterValue(filterKey, filterValue)) { + return { type: 'initial' } + } + + // Otherwise, treat as partial value needing completion + return { + type: 'filter-value-context', + filterKey, + partialInput: filterValue, + startPosition: + filterValueMatch.index! + + (filterValueMatch[0].startsWith(' ') ? 1 : 0) + + filterKey.length + + 1, + endPosition: cursorPosition, + } + } + + // Check for empty filter key (just "key:" with no value) + const emptyFilterMatch = textBeforeCursor.match(/(?:^|\s)(\w+):$/) + if (emptyFilterMatch) { + return { type: 'initial' } // Treat as initial to show filter value suggestions + } + + const filterKeyMatch = textBeforeCursor.match(/(?:^|\s)(\w+):?$/) + if (filterKeyMatch && !filterKeyMatch[0].includes(':')) { + return { + type: 'filter-key-partial', + partialInput: filterKeyMatch[1], + startPosition: filterKeyMatch.index! + (filterKeyMatch[0].startsWith(' ') ? 1 : 0), + endPosition: cursorPosition, + } + } + + return { type: 'text-search' } + } + + /** + * Get filter key suggestions + */ + private getFilterKeySuggestions(partialInput?: string): Suggestion[] { + const suggestions: Suggestion[] = [] + + for (const filter of FILTER_DEFINITIONS) { + const matchesPartial = + !partialInput || + filter.key.toLowerCase().startsWith(partialInput.toLowerCase()) || + filter.label.toLowerCase().startsWith(partialInput.toLowerCase()) + + if (matchesPartial) { + suggestions.push({ + id: `filter-key-${filter.key}`, + value: `${filter.key}:`, + label: filter.label, + description: filter.description, + category: 'filters', + }) + } + } + + if (this.availableWorkflows.length > 0) { + const matchesWorkflow = + !partialInput || + 'workflow'.startsWith(partialInput.toLowerCase()) || + 'workflows'.startsWith(partialInput.toLowerCase()) + + if (matchesWorkflow) { + suggestions.push({ + id: 'filter-key-workflow', + value: 'workflow:', + label: 'Workflow', + description: 'Filter by workflow name', + category: 'filters', + }) + } + } + + if (this.availableFolders.length > 0) { + const matchesFolder = + !partialInput || + 'folder'.startsWith(partialInput.toLowerCase()) || + 'folders'.startsWith(partialInput.toLowerCase()) + + if (matchesFolder) { + suggestions.push({ + id: 'filter-key-folder', + value: 'folder:', + label: 'Folder', + description: 'Filter by folder name', + category: 'filters', + }) + } + } + + return suggestions + } + + /** + * Get filter value suggestions for a specific filter key + */ + private getFilterValueSuggestions(filterKey: string, partialInput = ''): Suggestion[] { + const suggestions: Suggestion[] = [] + + const filterDef = FILTER_DEFINITIONS.find((f) => f.key === filterKey) + if (filterDef) { + for (const option of filterDef.options) { + const matchesPartial = + !partialInput || + option.value.toLowerCase().includes(partialInput.toLowerCase()) || + option.label.toLowerCase().includes(partialInput.toLowerCase()) + + if (matchesPartial) { + suggestions.push({ + id: `filter-value-${filterKey}-${option.value}`, + value: option.value, + label: option.label, + description: option.description, + category: filterKey, + }) + } + } + return suggestions + } + + if (filterKey === 'workflow') { + for (const workflow of this.availableWorkflows) { + const matchesPartial = + !partialInput || workflow.toLowerCase().includes(partialInput.toLowerCase()) + + if (matchesPartial) { + suggestions.push({ + id: `filter-value-workflow-${workflow}`, + value: `"${workflow}"`, + label: workflow, + description: 'Workflow name', + category: 'workflow', + }) + } + } + return suggestions.slice(0, 8) + } + + if (filterKey === 'folder') { + for (const folder of this.availableFolders) { + const matchesPartial = + !partialInput || folder.toLowerCase().includes(partialInput.toLowerCase()) + + if (matchesPartial) { + suggestions.push({ + id: `filter-value-folder-${folder}`, + value: `"${folder}"`, + label: folder, + description: 'Folder name', + category: 'folder', + }) + } + } + return suggestions.slice(0, 8) + } + + return suggestions + } + + /** + * Get suggestions based on current input and cursor position + */ + getSuggestions(input: string, cursorPosition: number): SuggestionGroup | null { + const context = this.analyzeContext(input, cursorPosition) + + // Special case: check if we're at "key:" position for filter values + const textBeforeCursor = input.slice(0, cursorPosition) + const emptyFilterMatch = textBeforeCursor.match(/(?:^|\s)(\w+):$/) + if (emptyFilterMatch) { + const filterKey = emptyFilterMatch[1] + const filterValueSuggestions = this.getFilterValueSuggestions(filterKey, '') + return filterValueSuggestions.length > 0 + ? { + type: 'filter-values', + filterKey, + suggestions: filterValueSuggestions, + } + : null + } + + switch (context.type) { + case 'initial': + case 'filter-key-partial': { + const filterKeySuggestions = this.getFilterKeySuggestions(context.partialInput) + return filterKeySuggestions.length > 0 + ? { + type: 'filter-keys', + suggestions: filterKeySuggestions, + } + : null + } + + case 'filter-value-context': { + if (!context.filterKey) return null + const filterValueSuggestions = this.getFilterValueSuggestions( + context.filterKey, + context.partialInput + ) + return filterValueSuggestions.length > 0 + ? { + type: 'filter-values', + filterKey: context.filterKey, + suggestions: filterValueSuggestions, + } + : null + } + default: + return null + } + } + + /** + * Generate preview text for a suggestion - SIMPLE APPROACH + * Show suggestion at the end of input, with proper spacing logic + */ + generatePreview(suggestion: Suggestion, currentValue: string, cursorPosition: number): string { + // If input is empty, just show the suggestion + if (!currentValue.trim()) { + return suggestion.value + } + + // Check if we're doing a partial replacement (like "lev" -> "level:") + const context = this.analyzeContext(currentValue, cursorPosition) + + if ( + context.type === 'filter-key-partial' && + context.startPosition !== undefined && + context.endPosition !== undefined + ) { + // Replace partial text: "lev" -> "level:" + const before = currentValue.slice(0, context.startPosition) + const after = currentValue.slice(context.endPosition) + return `${before}${suggestion.value}${after}` + } + + if ( + context.type === 'filter-value-context' && + context.startPosition !== undefined && + context.endPosition !== undefined + ) { + // Replace partial filter value: "level:err" -> "level:error" + const before = currentValue.slice(0, context.startPosition) + const after = currentValue.slice(context.endPosition) + return `${before}${suggestion.value}${after}` + } + + // For all other cases, append at the end with smart spacing: + let result = currentValue + + if (currentValue.endsWith(':')) { + // Direct append for filter values: "level:" + "error" = "level:error" + result += suggestion.value + } else if (currentValue.endsWith(' ')) { + // Already has space, direct append: "level:error " + "trigger:" = "level:error trigger:" + result += suggestion.value + } else { + // Need space: "level:error" + " " + "trigger:" = "level:error trigger:" + result += ` ${suggestion.value}` + } + + return result + } + + /** + * Validate if a query is complete and should trigger backend calls + */ + validateQuery(query: string): boolean { + const incompleteFilterMatch = query.match(/(\w+):$/) + if (incompleteFilterMatch) { + return false + } + + const openQuotes = (query.match(/"/g) || []).length + if (openQuotes % 2 !== 0) { + return false + } + + return true + } +} From d73a97ffa236e3b4eafdac562365d37d4a76ca14 Mon Sep 17 00:00:00 2001 From: Waleed Date: Mon, 15 Sep 2025 14:50:18 -0700 Subject: [PATCH 04/10] feat(idempotency): added generalized idempotency service for all triggers/webhooks (#1330) * update infra and remove railway * feat(webhooks): add idempotency service for all triggers/webhooks * Revert "update infra and remove railway" This reverts commit abfa2f8d51901247acc6397960210569e84d72b1. * cleanup * ack PR comments --- .../api/webhooks/cleanup/idempotency/route.ts | 64 + .../app/api/webhooks/trigger/[path]/route.ts | 55 +- apps/sim/background/webhook-execution.ts | 25 +- .../db/migrations/0090_fearless_zaladane.sql | 10 + .../sim/db/migrations/meta/0090_snapshot.json | 6845 +++++++++++++++++ apps/sim/db/migrations/meta/_journal.json | 7 + apps/sim/db/schema.ts | 21 + apps/sim/lib/idempotency/cleanup.ts | 175 + apps/sim/lib/idempotency/index.ts | 7 + apps/sim/lib/idempotency/service.ts | 406 + .../sim/lib/webhooks/gmail-polling-service.ts | 309 +- .../lib/webhooks/outlook-polling-service.ts | 218 +- apps/sim/vercel.json | 4 + 13 files changed, 7818 insertions(+), 328 deletions(-) create mode 100644 apps/sim/app/api/webhooks/cleanup/idempotency/route.ts create mode 100644 apps/sim/db/migrations/0090_fearless_zaladane.sql create mode 100644 apps/sim/db/migrations/meta/0090_snapshot.json create mode 100644 apps/sim/lib/idempotency/cleanup.ts create mode 100644 apps/sim/lib/idempotency/index.ts create mode 100644 apps/sim/lib/idempotency/service.ts diff --git a/apps/sim/app/api/webhooks/cleanup/idempotency/route.ts b/apps/sim/app/api/webhooks/cleanup/idempotency/route.ts new file mode 100644 index 0000000000..5f9c1a65c5 --- /dev/null +++ b/apps/sim/app/api/webhooks/cleanup/idempotency/route.ts @@ -0,0 +1,64 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { verifyCronAuth } from '@/lib/auth/internal' +import { cleanupExpiredIdempotencyKeys, getIdempotencyKeyStats } from '@/lib/idempotency/cleanup' +import { createLogger } from '@/lib/logs/console/logger' +import { generateRequestId } from '@/lib/utils' + +const logger = createLogger('IdempotencyCleanupAPI') + +export const dynamic = 'force-dynamic' +export const maxDuration = 300 // Allow up to 5 minutes for cleanup + +export async function GET(request: NextRequest) { + const requestId = generateRequestId() + logger.info(`Idempotency cleanup triggered (${requestId})`) + + try { + const authError = verifyCronAuth(request, 'Idempotency key cleanup') + if (authError) { + return authError + } + + const statsBefore = await getIdempotencyKeyStats() + logger.info( + `Pre-cleanup stats: ${statsBefore.totalKeys} keys across ${Object.keys(statsBefore.keysByNamespace).length} namespaces` + ) + + const result = await cleanupExpiredIdempotencyKeys({ + maxAgeSeconds: 7 * 24 * 60 * 60, // 7 days + batchSize: 1000, + }) + + const statsAfter = await getIdempotencyKeyStats() + logger.info(`Post-cleanup stats: ${statsAfter.totalKeys} keys remaining`) + + return NextResponse.json({ + success: true, + message: 'Idempotency key cleanup completed', + requestId, + result: { + deleted: result.deleted, + errors: result.errors, + statsBefore: { + totalKeys: statsBefore.totalKeys, + keysByNamespace: statsBefore.keysByNamespace, + }, + statsAfter: { + totalKeys: statsAfter.totalKeys, + keysByNamespace: statsAfter.keysByNamespace, + }, + }, + }) + } catch (error) { + logger.error(`Error during idempotency cleanup (${requestId}):`, error) + return NextResponse.json( + { + success: false, + message: 'Idempotency cleanup failed', + error: error instanceof Error ? error.message : 'Unknown error', + requestId, + }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/webhooks/trigger/[path]/route.ts b/apps/sim/app/api/webhooks/trigger/[path]/route.ts index a49c0ed3d4..06482be20d 100644 --- a/apps/sim/app/api/webhooks/trigger/[path]/route.ts +++ b/apps/sim/app/api/webhooks/trigger/[path]/route.ts @@ -4,6 +4,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { checkServerSideUsageLimits } from '@/lib/billing' import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription' import { env, isTruthy } from '@/lib/env' +import { IdempotencyService, webhookIdempotency } from '@/lib/idempotency/service' import { createLogger } from '@/lib/logs/console/logger' import { generateRequestId } from '@/lib/utils' import { @@ -328,7 +329,7 @@ export async function POST( // Continue processing - better to risk usage limit bypass than fail webhook } - // --- PHASE 5: Queue webhook execution (trigger.dev or direct based on env) --- + // --- PHASE 5: Idempotent webhook execution --- try { const payload = { webhookId: foundWebhook.id, @@ -341,22 +342,44 @@ export async function POST( blockId: foundWebhook.blockId, } - const useTrigger = isTruthy(env.TRIGGER_DEV_ENABLED) + const idempotencyKey = IdempotencyService.createWebhookIdempotencyKey( + foundWebhook.id, + body, + Object.fromEntries(request.headers.entries()) + ) - if (useTrigger) { - const handle = await tasks.trigger('webhook-execution', payload) - logger.info( - `[${requestId}] Queued webhook execution task ${handle.id} for ${foundWebhook.provider} webhook` - ) - } else { - // Fire-and-forget direct execution to avoid blocking webhook response - void executeWebhookJob(payload).catch((error) => { - logger.error(`[${requestId}] Direct webhook execution failed`, error) - }) - logger.info( - `[${requestId}] Queued direct webhook execution for ${foundWebhook.provider} webhook (Trigger.dev disabled)` - ) - } + const result = await webhookIdempotency.executeWithIdempotency( + foundWebhook.provider, + idempotencyKey, + async () => { + const useTrigger = isTruthy(env.TRIGGER_DEV_ENABLED) + + if (useTrigger) { + const handle = await tasks.trigger('webhook-execution', payload) + logger.info( + `[${requestId}] Queued webhook execution task ${handle.id} for ${foundWebhook.provider} webhook` + ) + return { + method: 'trigger.dev', + taskId: handle.id, + status: 'queued', + } + } + // Fire-and-forget direct execution to avoid blocking webhook response + void executeWebhookJob(payload).catch((error) => { + logger.error(`[${requestId}] Direct webhook execution failed`, error) + }) + logger.info( + `[${requestId}] Queued direct webhook execution for ${foundWebhook.provider} webhook (Trigger.dev disabled)` + ) + return { + method: 'direct', + status: 'queued', + } + } + ) + + logger.debug(`[${requestId}] Webhook execution result:`, result) // Return immediate acknowledgment with provider-specific format if (foundWebhook.provider === 'microsoftteams') { diff --git a/apps/sim/background/webhook-execution.ts b/apps/sim/background/webhook-execution.ts index f25b0be68b..99fde95728 100644 --- a/apps/sim/background/webhook-execution.ts +++ b/apps/sim/background/webhook-execution.ts @@ -3,6 +3,7 @@ import { eq, sql } from 'drizzle-orm' import { v4 as uuidv4 } from 'uuid' import { checkServerSideUsageLimits } from '@/lib/billing' import { getPersonalAndWorkspaceEnv } from '@/lib/environment/utils' +import { IdempotencyService, webhookIdempotency } from '@/lib/idempotency' import { createLogger } from '@/lib/logs/console/logger' import { LoggingSession } from '@/lib/logs/execution/logging-session' import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans' @@ -41,11 +42,29 @@ export async function executeWebhookJob(payload: WebhookExecutionPayload) { executionId, }) - // Initialize logging session outside try block so it's available in catch + const idempotencyKey = IdempotencyService.createWebhookIdempotencyKey( + payload.webhookId, + payload.body, + payload.headers + ) + + return await webhookIdempotency.executeWithIdempotency( + payload.provider, + idempotencyKey, + async () => { + return await executeWebhookJobInternal(payload, executionId, requestId) + } + ) +} + +async function executeWebhookJobInternal( + payload: WebhookExecutionPayload, + executionId: string, + requestId: string +) { const loggingSession = new LoggingSession(payload.workflowId, executionId, 'webhook', requestId) try { - // Check usage limits first const usageCheck = await checkServerSideUsageLimits(payload.userId) if (usageCheck.isExceeded) { logger.warn( @@ -62,7 +81,6 @@ export async function executeWebhookJob(payload: WebhookExecutionPayload) { ) } - // Load workflow from normalized tables const workflowData = await loadWorkflowFromNormalizedTables(payload.workflowId) if (!workflowData) { throw new Error(`Workflow not found: ${payload.workflowId}`) @@ -70,7 +88,6 @@ export async function executeWebhookJob(payload: WebhookExecutionPayload) { const { blocks, edges, loops, parallels } = workflowData - // Get environment variables with workspace precedence const wfRows = await db .select({ workspaceId: workflowTable.workspaceId }) .from(workflowTable) diff --git a/apps/sim/db/migrations/0090_fearless_zaladane.sql b/apps/sim/db/migrations/0090_fearless_zaladane.sql new file mode 100644 index 0000000000..1394150d19 --- /dev/null +++ b/apps/sim/db/migrations/0090_fearless_zaladane.sql @@ -0,0 +1,10 @@ +CREATE TABLE "idempotency_key" ( + "key" text NOT NULL, + "namespace" text DEFAULT 'default' NOT NULL, + "result" json NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX "idempotency_key_namespace_unique" ON "idempotency_key" USING btree ("key","namespace");--> statement-breakpoint +CREATE INDEX "idempotency_key_created_at_idx" ON "idempotency_key" USING btree ("created_at");--> statement-breakpoint +CREATE INDEX "idempotency_key_namespace_idx" ON "idempotency_key" USING btree ("namespace"); \ No newline at end of file diff --git a/apps/sim/db/migrations/meta/0090_snapshot.json b/apps/sim/db/migrations/meta/0090_snapshot.json new file mode 100644 index 0000000000..129eb65056 --- /dev/null +++ b/apps/sim/db/migrations/meta/0090_snapshot.json @@ -0,0 +1,6845 @@ +{ + "id": "199e3203-483a-476e-9556-1bfa9811667e", + "prevId": "687bfe13-ff4a-4c5a-a3ef-b2674b31485d", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "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": {} + } + }, + "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": {}, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subdomain": { + "name": "subdomain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "subdomain_idx": { + "name": "subdomain_idx", + "columns": [ + { + "expression": "subdomain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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 + }, + "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.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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 + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_uploaded_at_idx": { + "name": "doc_kb_uploaded_at_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "uploaded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "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": {} + } + }, + "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 + }, + "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_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.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_namespace_unique": { + "name": "idempotency_key_namespace_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "namespace", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idempotency_key_namespace_idx": { + "name": "idempotency_key_namespace_idx", + "columns": [ + { + "expression": "namespace", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.marketplace": { + "name": "marketplace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "marketplace_workflow_id_workflow_id_fk": { + "name": "marketplace_workflow_id_workflow_id_fk", + "tableFrom": "marketplace", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "marketplace_author_id_user_id_fk": { + "name": "marketplace_author_id_user_id_fk", + "tableFrom": "marketplace", + "tableTo": "user", + "columnsFrom": ["author_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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 + }, + "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_idx": { + "name": "member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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 + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workflow_idx": { + "name": "memory_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workflow_key_idx": { + "name": "memory_workflow_key_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workflow_id_workflow_id_fk": { + "name": "memory_workflow_id_workflow_id_fk", + "tableFrom": "memory", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "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": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auto_fill_env_vars": { + "name": "auto_fill_env_vars", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auto_pan": { + "name": "auto_pan", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "console_expanded_by_default": { + "name": "console_expanded_by_default", + "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 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "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_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 + }, + "user_id": { + "name": "user_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 + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'FileText'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "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()" + } + }, + "indexes": { + "templates_workflow_id_idx": { + "name": "templates_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_user_id_idx": { + "name": "templates_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_category_idx": { + "name": "templates_category_idx", + "columns": [ + { + "expression": "category", + "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_category_views_idx": { + "name": "templates_category_views_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_category_stars_idx": { + "name": "templates_category_stars_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_user_category_idx": { + "name": "templates_user_category_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "category", + "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": "no action", + "onUpdate": "no action" + }, + "templates_user_id_user_id_fk": { + "name": "templates_user_id_user_id_fk", + "tableFrom": "templates", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "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 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_rate_limits": { + "name": "user_rate_limits", + "schema": "", + "columns": { + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sync_api_requests": { + "name": "sync_api_requests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "async_api_requests": { + "name": "async_api_requests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "api_endpoint_requests": { + "name": "api_endpoint_requests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "window_start": { + "name": "window_start", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_request_at": { + "name": "last_request_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_rate_limited": { + "name": "is_rate_limited", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "rate_limit_reset_at": { + "name": "rate_limit_reset_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'10'" + }, + "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'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "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 + }, + "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 + } + }, + "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": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_idx": { + "name": "path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_block_id_workflow_blocks_id_fk": { + "name": "webhook_block_id_workflow_blocks_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_blocks", + "columnsFrom": ["block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_state": { + "name": "deployed_state", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned_api_key_id": { + "name": "pinned_api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "collaborators": { + "name": "collaborators", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "marketplace_data": { + "name": "marketplace_data", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "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": {} + } + }, + "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" + }, + "workflow_pinned_api_key_id_api_key_id_fk": { + "name": "workflow_pinned_api_key_id_api_key_id_fk", + "tableFrom": "workflow", + "tableTo": "api_key", + "columnsFrom": ["pinned_api_key_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "extent": { + "name": "extent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_parent_id_idx": { + "name": "workflow_blocks_parent_id_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_workflow_parent_idx": { + "name": "workflow_blocks_workflow_parent_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_workflow_type_idx": { + "name": "workflow_blocks_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_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 + }, + "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_id_idx": { + "name": "workflow_deployment_version_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "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_source_block_idx": { + "name": "workflow_edges_source_block_idx", + "columns": [ + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_target_block_idx": { + "name": "workflow_edges_target_block_idx", + "columns": [ + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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_execution_id_idx": { + "name": "workflow_execution_logs_execution_id_idx", + "columns": [ + { + "expression": "execution_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": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_log_webhook": { + "name": "workflow_log_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "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[]" + }, + "active": { + "name": "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": { + "workflow_log_webhook_workflow_id_idx": { + "name": "workflow_log_webhook_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_log_webhook_active_idx": { + "name": "workflow_log_webhook_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_log_webhook_workflow_id_workflow_id_fk": { + "name": "workflow_log_webhook_workflow_id_workflow_id_fk", + "tableFrom": "workflow_log_webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_log_webhook_delivery": { + "name": "workflow_log_webhook_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": "webhook_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": { + "workflow_log_webhook_delivery_subscription_id_idx": { + "name": "workflow_log_webhook_delivery_subscription_id_idx", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_log_webhook_delivery_execution_id_idx": { + "name": "workflow_log_webhook_delivery_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_log_webhook_delivery_status_idx": { + "name": "workflow_log_webhook_delivery_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_log_webhook_delivery_next_attempt_idx": { + "name": "workflow_log_webhook_delivery_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_log_webhook_delivery_subscription_id_workflow_log_webhook_id_fk": { + "name": "workflow_log_webhook_delivery_subscription_id_workflow_log_webhook_id_fk", + "tableFrom": "workflow_log_webhook_delivery", + "tableTo": "workflow_log_webhook", + "columnsFrom": ["subscription_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_log_webhook_delivery_workflow_id_workflow_id_fk": { + "name": "workflow_log_webhook_delivery_workflow_id_workflow_id_fk", + "tableFrom": "workflow_log_webhook_delivery", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_unique": { + "name": "workflow_schedule_workflow_block_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_block_id_workflow_blocks_id_fk": { + "name": "workflow_schedule_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_blocks", + "columnsFrom": ["block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_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_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 + } + }, + "enums": { + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.webhook_delivery_status": { + "name": "webhook_delivery_status", + "schema": "public", + "values": ["pending", "in_progress", "success", "failed"] + }, + "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/apps/sim/db/migrations/meta/_journal.json b/apps/sim/db/migrations/meta/_journal.json index 19a75a7dbb..69e1a98aeb 100644 --- a/apps/sim/db/migrations/meta/_journal.json +++ b/apps/sim/db/migrations/meta/_journal.json @@ -624,6 +624,13 @@ "when": 1757628623657, "tag": "0089_amused_pete_wisdom", "breakpoints": true + }, + { + "idx": 90, + "version": "7", + "when": 1757805452908, + "tag": "0090_fearless_zaladane", + "breakpoints": true } ] } diff --git a/apps/sim/db/schema.ts b/apps/sim/db/schema.ts index 5d66f05df9..e1cb849c05 100644 --- a/apps/sim/db/schema.ts +++ b/apps/sim/db/schema.ts @@ -1333,6 +1333,27 @@ export const workflowDeploymentVersion = pgTable( }) ) +// Idempotency keys for preventing duplicate processing across all webhooks and triggers +export const idempotencyKey = pgTable( + 'idempotency_key', + { + key: text('key').notNull(), + namespace: text('namespace').notNull().default('default'), + result: json('result').notNull(), + createdAt: timestamp('created_at').notNull().defaultNow(), + }, + (table) => ({ + // Primary key is combination of key and namespace + keyNamespacePk: uniqueIndex('idempotency_key_namespace_unique').on(table.key, table.namespace), + + // Index for cleanup operations by creation time + createdAtIdx: index('idempotency_key_created_at_idx').on(table.createdAt), + + // Index for namespace-based queries + namespaceIdx: index('idempotency_key_namespace_idx').on(table.namespace), + }) +) + export const mcpServers = pgTable( 'mcp_servers', { diff --git a/apps/sim/lib/idempotency/cleanup.ts b/apps/sim/lib/idempotency/cleanup.ts new file mode 100644 index 0000000000..097c30693e --- /dev/null +++ b/apps/sim/lib/idempotency/cleanup.ts @@ -0,0 +1,175 @@ +import { and, eq, lt } from 'drizzle-orm' +import { createLogger } from '@/lib/logs/console/logger' +import { db } from '@/db' +import { idempotencyKey } from '@/db/schema' + +const logger = createLogger('IdempotencyCleanup') + +export interface CleanupOptions { + /** + * Maximum age of idempotency keys in seconds before they're considered expired + * Default: 7 days (604800 seconds) + */ + maxAgeSeconds?: number + + /** + * Maximum number of keys to delete in a single batch + * Default: 1000 + */ + batchSize?: number + + /** + * Specific namespace to clean up, or undefined to clean all namespaces + */ + namespace?: string +} + +/** + * Clean up expired idempotency keys from the database + */ +export async function cleanupExpiredIdempotencyKeys( + options: CleanupOptions = {} +): Promise<{ deleted: number; errors: string[] }> { + const { + maxAgeSeconds = 7 * 24 * 60 * 60, // 7 days + batchSize = 1000, + namespace, + } = options + + const errors: string[] = [] + let totalDeleted = 0 + + try { + const cutoffDate = new Date(Date.now() - maxAgeSeconds * 1000) + + logger.info('Starting idempotency key cleanup', { + cutoffDate: cutoffDate.toISOString(), + namespace: namespace || 'all', + batchSize, + }) + + let hasMore = true + let batchCount = 0 + + while (hasMore) { + try { + const whereCondition = namespace + ? and(lt(idempotencyKey.createdAt, cutoffDate), eq(idempotencyKey.namespace, namespace)) + : lt(idempotencyKey.createdAt, cutoffDate) + + // First, find IDs to delete with limit + const toDelete = await db + .select({ key: idempotencyKey.key, namespace: idempotencyKey.namespace }) + .from(idempotencyKey) + .where(whereCondition) + .limit(batchSize) + + if (toDelete.length === 0) { + break + } + + // Delete the found records + const deleteResult = await db + .delete(idempotencyKey) + .where( + and( + ...toDelete.map((item) => + and(eq(idempotencyKey.key, item.key), eq(idempotencyKey.namespace, item.namespace)) + ) + ) + ) + .returning({ key: idempotencyKey.key }) + + const deletedCount = deleteResult.length + totalDeleted += deletedCount + batchCount++ + + if (deletedCount === 0) { + hasMore = false + logger.info('No more expired idempotency keys found') + } else if (deletedCount < batchSize) { + hasMore = false + logger.info(`Deleted final batch of ${deletedCount} expired idempotency keys`) + } else { + logger.info(`Deleted batch ${batchCount}: ${deletedCount} expired idempotency keys`) + + await new Promise((resolve) => setTimeout(resolve, 100)) + } + } catch (batchError) { + const errorMessage = + batchError instanceof Error ? batchError.message : 'Unknown batch error' + logger.error(`Error deleting batch ${batchCount + 1}:`, batchError) + errors.push(`Batch ${batchCount + 1}: ${errorMessage}`) + + batchCount++ + + if (errors.length > 5) { + logger.error('Too many batch errors, stopping cleanup') + break + } + } + } + + logger.info('Idempotency key cleanup completed', { + totalDeleted, + batchCount, + errors: errors.length, + }) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error' + logger.error('Failed to cleanup expired idempotency keys:', error) + errors.push(`General error: ${errorMessage}`) + } + + return { deleted: totalDeleted, errors } +} + +/** + * Get statistics about idempotency key usage + */ +export async function getIdempotencyKeyStats(): Promise<{ + totalKeys: number + keysByNamespace: Record + oldestKey: Date | null + newestKey: Date | null +}> { + try { + const allKeys = await db + .select({ + namespace: idempotencyKey.namespace, + createdAt: idempotencyKey.createdAt, + }) + .from(idempotencyKey) + + const totalKeys = allKeys.length + const keysByNamespace: Record = {} + let oldestKey: Date | null = null + let newestKey: Date | null = null + + for (const key of allKeys) { + keysByNamespace[key.namespace] = (keysByNamespace[key.namespace] || 0) + 1 + + if (!oldestKey || key.createdAt < oldestKey) { + oldestKey = key.createdAt + } + if (!newestKey || key.createdAt > newestKey) { + newestKey = key.createdAt + } + } + + return { + totalKeys, + keysByNamespace, + oldestKey, + newestKey, + } + } catch (error) { + logger.error('Failed to get idempotency key stats:', error) + return { + totalKeys: 0, + keysByNamespace: {}, + oldestKey: null, + newestKey: null, + } + } +} diff --git a/apps/sim/lib/idempotency/index.ts b/apps/sim/lib/idempotency/index.ts new file mode 100644 index 0000000000..7be3bb72de --- /dev/null +++ b/apps/sim/lib/idempotency/index.ts @@ -0,0 +1,7 @@ +export * from './cleanup' +export * from './service' +export { + pollingIdempotency, + triggerIdempotency, + webhookIdempotency, +} from './service' diff --git a/apps/sim/lib/idempotency/service.ts b/apps/sim/lib/idempotency/service.ts new file mode 100644 index 0000000000..d46a04d6a4 --- /dev/null +++ b/apps/sim/lib/idempotency/service.ts @@ -0,0 +1,406 @@ +import * as crypto from 'crypto' +import { and, eq } from 'drizzle-orm' +import { createLogger } from '@/lib/logs/console/logger' +import { getRedisClient } from '@/lib/redis' +import { db } from '@/db' +import { idempotencyKey } from '@/db/schema' + +const logger = createLogger('IdempotencyService') + +export interface IdempotencyConfig { + /** + * Time-to-live for the idempotency key in seconds + * Default: 7 days (604800 seconds) + */ + ttlSeconds?: number + + /** + * Namespace for the idempotency key (e.g., 'gmail', 'webhook', 'trigger') + * Default: 'default' + */ + namespace?: string + + /** + * Enable database fallback when Redis is not available + * Default: true + */ + enableDatabaseFallback?: boolean +} + +export interface IdempotencyResult { + /** + * Whether this is the first time processing this key + */ + isFirstTime: boolean + + /** + * The normalized idempotency key used for storage + */ + normalizedKey: string + + /** + * Previous result if this key was already processed + */ + previousResult?: any + + /** + * Storage method used ('redis', 'database', 'memory') + */ + storageMethod: 'redis' | 'database' | 'memory' +} + +export interface ProcessingResult { + success: boolean + result?: any + error?: string +} + +const DEFAULT_TTL = 60 * 60 * 24 * 7 // 7 days +const REDIS_KEY_PREFIX = 'idempotency:' +const MEMORY_CACHE_SIZE = 1000 + +const memoryCache = new Map< + string, + { + result: any + timestamp: number + ttl: number + } +>() + +/** + * Universal idempotency service for webhooks, triggers, and any other operations + * that need duplicate prevention. + */ +export class IdempotencyService { + private config: Required + + constructor(config: IdempotencyConfig = {}) { + this.config = { + ttlSeconds: config.ttlSeconds ?? DEFAULT_TTL, + namespace: config.namespace ?? 'default', + enableDatabaseFallback: config.enableDatabaseFallback ?? true, + } + } + + /** + * Generate a normalized idempotency key from various sources + */ + private normalizeKey( + provider: string, + identifier: string, + additionalContext?: Record + ): string { + const base = `${this.config.namespace}:${provider}:${identifier}` + + if (additionalContext && Object.keys(additionalContext).length > 0) { + // Sort keys for consistent hashing + const sortedKeys = Object.keys(additionalContext).sort() + const contextStr = sortedKeys.map((key) => `${key}=${additionalContext[key]}`).join('&') + return `${base}:${contextStr}` + } + + return base + } + + /** + * Check if an operation has already been processed + */ + async checkIdempotency( + provider: string, + identifier: string, + additionalContext?: Record + ): Promise { + const normalizedKey = this.normalizeKey(provider, identifier, additionalContext) + const redisKey = `${REDIS_KEY_PREFIX}${normalizedKey}` + + try { + const redis = getRedisClient() + if (redis) { + const cachedResult = await redis.get(redisKey) + if (cachedResult) { + logger.debug(`Idempotency hit in Redis: ${normalizedKey}`) + return { + isFirstTime: false, + normalizedKey, + previousResult: JSON.parse(cachedResult), + storageMethod: 'redis', + } + } + + logger.debug(`Idempotency miss in Redis: ${normalizedKey}`) + return { + isFirstTime: true, + normalizedKey, + storageMethod: 'redis', + } + } + } catch (error) { + logger.warn(`Redis idempotency check failed for ${normalizedKey}:`, error) + } + + if (this.config.enableDatabaseFallback) { + try { + const existing = await db + .select({ result: idempotencyKey.result, createdAt: idempotencyKey.createdAt }) + .from(idempotencyKey) + .where( + and( + eq(idempotencyKey.key, normalizedKey), + eq(idempotencyKey.namespace, this.config.namespace) + ) + ) + .limit(1) + + if (existing.length > 0) { + const item = existing[0] + const isExpired = Date.now() - item.createdAt.getTime() > this.config.ttlSeconds * 1000 + + if (!isExpired) { + logger.debug(`Idempotency hit in database: ${normalizedKey}`) + return { + isFirstTime: false, + normalizedKey, + previousResult: item.result, + storageMethod: 'database', + } + } + await db + .delete(idempotencyKey) + .where(eq(idempotencyKey.key, normalizedKey)) + .catch((err) => logger.warn(`Failed to clean up expired key ${normalizedKey}:`, err)) + } + + logger.debug(`Idempotency miss in database: ${normalizedKey}`) + return { + isFirstTime: true, + normalizedKey, + storageMethod: 'database', + } + } catch (error) { + logger.warn(`Database idempotency check failed for ${normalizedKey}:`, error) + } + } + + const memoryEntry = memoryCache.get(normalizedKey) + if (memoryEntry) { + const isExpired = Date.now() - memoryEntry.timestamp > memoryEntry.ttl * 1000 + if (!isExpired) { + logger.debug(`Idempotency hit in memory: ${normalizedKey}`) + return { + isFirstTime: false, + normalizedKey, + previousResult: memoryEntry.result, + storageMethod: 'memory', + } + } + memoryCache.delete(normalizedKey) + } + + logger.debug(`Idempotency miss in memory: ${normalizedKey}`) + return { + isFirstTime: true, + normalizedKey, + storageMethod: 'memory', + } + } + + /** + * Store the result of processing for future idempotency checks + */ + async storeResult( + normalizedKey: string, + result: ProcessingResult, + storageMethod: 'redis' | 'database' | 'memory' + ): Promise { + const serializedResult = JSON.stringify(result) + + try { + if (storageMethod === 'redis') { + const redis = getRedisClient() + if (redis) { + await redis.setex( + `${REDIS_KEY_PREFIX}${normalizedKey}`, + this.config.ttlSeconds, + serializedResult + ) + logger.debug(`Stored idempotency result in Redis: ${normalizedKey}`) + return + } + } + } catch (error) { + logger.warn(`Failed to store result in Redis for ${normalizedKey}:`, error) + } + + if (this.config.enableDatabaseFallback && storageMethod !== 'memory') { + try { + await db + .insert(idempotencyKey) + .values({ + key: normalizedKey, + namespace: this.config.namespace, + result: result, + createdAt: new Date(), + }) + .onConflictDoUpdate({ + target: [idempotencyKey.key, idempotencyKey.namespace], + set: { + result: result, + createdAt: new Date(), + }, + }) + + logger.debug(`Stored idempotency result in database: ${normalizedKey}`) + return + } catch (error) { + logger.warn(`Failed to store result in database for ${normalizedKey}:`, error) + } + } + + memoryCache.set(normalizedKey, { + result, + timestamp: Date.now(), + ttl: this.config.ttlSeconds, + }) + + if (memoryCache.size > MEMORY_CACHE_SIZE) { + const entries = Array.from(memoryCache.entries()) + const now = Date.now() + + entries.forEach(([key, entry]) => { + if (now - entry.timestamp > entry.ttl * 1000) { + memoryCache.delete(key) + } + }) + + if (memoryCache.size > MEMORY_CACHE_SIZE) { + const sortedEntries = entries + .filter(([key]) => memoryCache.has(key)) + .sort((a, b) => a[1].timestamp - b[1].timestamp) + + const toRemove = sortedEntries.slice(0, memoryCache.size - MEMORY_CACHE_SIZE) + toRemove.forEach(([key]) => memoryCache.delete(key)) + } + } + + logger.debug(`Stored idempotency result in memory: ${normalizedKey}`) + } + + /** + * Execute an operation with idempotency protection + */ + async executeWithIdempotency( + provider: string, + identifier: string, + operation: () => Promise, + additionalContext?: Record + ): Promise { + const idempotencyCheck = await this.checkIdempotency(provider, identifier, additionalContext) + + if (!idempotencyCheck.isFirstTime) { + logger.info(`Skipping duplicate operation: ${idempotencyCheck.normalizedKey}`) + + if (idempotencyCheck.previousResult?.success === false) { + throw new Error(idempotencyCheck.previousResult?.error || 'Previous operation failed') + } + + return idempotencyCheck.previousResult?.result as T + } + + try { + logger.debug(`Executing new operation: ${idempotencyCheck.normalizedKey}`) + const result = await operation() + + await this.storeResult( + idempotencyCheck.normalizedKey, + { success: true, result }, + idempotencyCheck.storageMethod + ) + + return result + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error' + await this.storeResult( + idempotencyCheck.normalizedKey, + { success: false, error: errorMessage }, + idempotencyCheck.storageMethod + ) + + throw error + } + } + + /** + * Create an idempotency key from a webhook payload + */ + static createWebhookIdempotencyKey( + webhookId: string, + payload: any, + headers?: Record + ): string { + const webhookIdHeader = + headers?.['x-webhook-id'] || + headers?.['x-shopify-webhook-id'] || + headers?.['x-github-delivery'] || + headers?.['stripe-signature']?.split(',')[0] + + if (webhookIdHeader) { + return `${webhookId}:${webhookIdHeader}` + } + + const payloadId = payload?.id || payload?.event_id || payload?.message?.id || payload?.data?.id + + if (payloadId) { + return `${webhookId}:${payloadId}` + } + + const payloadHash = crypto + .createHash('sha256') + .update(JSON.stringify(payload)) + .digest('hex') + .substring(0, 16) + + return `${webhookId}:${payloadHash}` + } + + /** + * Create an idempotency key for Gmail polling + */ + static createGmailIdempotencyKey(webhookId: string, emailId: string): string { + return `${webhookId}:${emailId}` + } + + /** + * Create an idempotency key for generic triggers + */ + static createTriggerIdempotencyKey( + triggerId: string, + eventId: string, + additionalContext?: Record + ): string { + const base = `${triggerId}:${eventId}` + if (additionalContext && Object.keys(additionalContext).length > 0) { + const contextStr = Object.keys(additionalContext) + .sort() + .map((key) => `${key}=${additionalContext[key]}`) + .join('&') + return `${base}:${contextStr}` + } + return base + } +} + +export const webhookIdempotency = new IdempotencyService({ + namespace: 'webhook', + ttlSeconds: 60 * 60 * 24 * 7, // 7 days +}) + +export const pollingIdempotency = new IdempotencyService({ + namespace: 'polling', + ttlSeconds: 60 * 60 * 24 * 3, // 3 days +}) + +export const triggerIdempotency = new IdempotencyService({ + namespace: 'trigger', + ttlSeconds: 60 * 60 * 24 * 1, // 1 day +}) diff --git a/apps/sim/lib/webhooks/gmail-polling-service.ts b/apps/sim/lib/webhooks/gmail-polling-service.ts index 08fcb8bc7a..f3d0c0c88d 100644 --- a/apps/sim/lib/webhooks/gmail-polling-service.ts +++ b/apps/sim/lib/webhooks/gmail-polling-service.ts @@ -1,7 +1,7 @@ import { and, eq } from 'drizzle-orm' import { nanoid } from 'nanoid' +import { pollingIdempotency } from '@/lib/idempotency/service' import { createLogger } from '@/lib/logs/console/logger' -import { hasProcessedMessage, markMessageAsProcessed } from '@/lib/redis' import { getBaseUrl } from '@/lib/urls/utils' import { getOAuthToken, refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' import { db } from '@/db' @@ -16,7 +16,6 @@ interface GmailWebhookConfig { maxEmailsPerPoll?: number lastCheckedTimestamp?: string historyId?: string - processedEmailIds?: string[] pollingInterval?: number includeRawEmail?: boolean } @@ -138,30 +137,10 @@ export async function pollGmailWebhooks() { logger.info(`[${requestId}] Found ${emails.length} new emails for webhook ${webhookId}`) - // Get processed email IDs (to avoid duplicates) - const processedEmailIds = config.processedEmailIds || [] - - // Filter out emails that have already been processed - const newEmails = emails.filter((email) => !processedEmailIds.includes(email.id)) - - if (newEmails.length === 0) { - logger.info( - `[${requestId}] All emails have already been processed for webhook ${webhookId}` - ) - await updateWebhookLastChecked( - webhookId, - now.toISOString(), - latestHistoryId || config.historyId - ) - return { success: true, webhookId, status: 'already_processed' } - } - - logger.info( - `[${requestId}] Processing ${newEmails.length} new emails for webhook ${webhookId}` - ) + logger.info(`[${requestId}] Processing ${emails.length} emails for webhook ${webhookId}`) // Process all emails (process each email as a separate workflow trigger) - const emailsToProcess = newEmails + const emailsToProcess = emails // Process emails const processed = await processEmails( @@ -172,24 +151,13 @@ export async function pollGmailWebhooks() { requestId ) - // Record which email IDs have been processed - const newProcessedIds = [...processedEmailIds, ...emailsToProcess.map((email) => email.id)] - // Keep only the most recent 100 IDs to prevent the list from growing too large - const trimmedProcessedIds = newProcessedIds.slice(-100) - - // Update webhook with latest history ID, timestamp, and processed email IDs - await updateWebhookData( - webhookId, - now.toISOString(), - latestHistoryId || config.historyId, - trimmedProcessedIds - ) + // Update webhook with latest history ID and timestamp + await updateWebhookData(webhookId, now.toISOString(), latestHistoryId || config.historyId) return { success: true, webhookId, emailsFound: emails.length, - newEmails: newEmails.length, emailsProcessed: processed, } } catch (error) { @@ -508,156 +476,157 @@ async function processEmails( for (const email of emails) { try { - // Deduplicate at Redis level (guards against races between cron runs) - const dedupeKey = `gmail:${webhookData.id}:${email.id}` - try { - const alreadyProcessed = await hasProcessedMessage(dedupeKey) - if (alreadyProcessed) { - logger.info( - `[${requestId}] Duplicate email ${email.id} for webhook ${webhookData.id} – skipping` - ) - continue - } - } catch (err) { - logger.warn(`[${requestId}] Redis check failed for ${email.id}, continuing`, err) - } - - // Extract useful information from email to create a simplified payload - // First, extract headers into a map for easy access - const headers: Record = {} - if (email.payload?.headers) { - for (const header of email.payload.headers) { - headers[header.name.toLowerCase()] = header.value - } - } + const result = await pollingIdempotency.executeWithIdempotency( + 'gmail', + `${webhookData.id}:${email.id}`, + async () => { + // Extract useful information from email to create a simplified payload + // First, extract headers into a map for easy access + const headers: Record = {} + if (email.payload?.headers) { + for (const header of email.payload.headers) { + headers[header.name.toLowerCase()] = header.value + } + } - // Extract and decode email body content - let textContent = '' - let htmlContent = '' + // Extract and decode email body content + let textContent = '' + let htmlContent = '' + + // Function to extract content from parts recursively + const extractContent = (part: any) => { + if (!part) return + + // Extract current part content if it exists + if (part.mimeType === 'text/plain' && part.body?.data) { + textContent = Buffer.from(part.body.data, 'base64').toString('utf-8') + } else if (part.mimeType === 'text/html' && part.body?.data) { + htmlContent = Buffer.from(part.body.data, 'base64').toString('utf-8') + } + + // Process nested parts + if (part.parts && Array.isArray(part.parts)) { + for (const subPart of part.parts) { + extractContent(subPart) + } + } + } - // Function to extract content from parts recursively - const extractContent = (part: any) => { - if (!part) return + // Extract content from the email payload + if (email.payload) { + extractContent(email.payload) + } - // Extract current part content if it exists - if (part.mimeType === 'text/plain' && part.body?.data) { - textContent = Buffer.from(part.body.data, 'base64').toString('utf-8') - } else if (part.mimeType === 'text/html' && part.body?.data) { - htmlContent = Buffer.from(part.body.data, 'base64').toString('utf-8') - } + // Parse date into standard format + let date: string | null = null + if (headers.date) { + try { + date = new Date(headers.date).toISOString() + } catch (_e) { + // Keep date as null if parsing fails + } + } else if (email.internalDate) { + // Use internalDate as fallback (convert from timestamp to ISO string) + date = new Date(Number.parseInt(email.internalDate)).toISOString() + } - // Process nested parts - if (part.parts && Array.isArray(part.parts)) { - for (const subPart of part.parts) { - extractContent(subPart) + // Extract attachment information if present + const attachments: Array<{ filename: string; mimeType: string; size: number }> = [] + + const findAttachments = (part: any) => { + if (!part) return + + if (part.filename && part.filename.length > 0) { + attachments.push({ + filename: part.filename, + mimeType: part.mimeType || 'application/octet-stream', + size: part.body?.size || 0, + }) + } + + // Look for attachments in nested parts + if (part.parts && Array.isArray(part.parts)) { + for (const subPart of part.parts) { + findAttachments(subPart) + } + } } - } - } - // Extract content from the email payload - if (email.payload) { - extractContent(email.payload) - } + if (email.payload) { + findAttachments(email.payload) + } - // Parse date into standard format - let date: string | null = null - if (headers.date) { - try { - date = new Date(headers.date).toISOString() - } catch (_e) { - // Keep date as null if parsing fails - } - } else if (email.internalDate) { - // Use internalDate as fallback (convert from timestamp to ISO string) - date = new Date(Number.parseInt(email.internalDate)).toISOString() - } + // Create simplified email object + const simplifiedEmail: SimplifiedEmail = { + id: email.id, + threadId: email.threadId, + subject: headers.subject || '[No Subject]', + from: headers.from || '', + to: headers.to || '', + cc: headers.cc || '', + date: date, + bodyText: textContent, + bodyHtml: htmlContent, + labels: email.labelIds || [], + hasAttachments: attachments.length > 0, + attachments: attachments, + } - // Extract attachment information if present - const attachments: Array<{ filename: string; mimeType: string; size: number }> = [] + // Prepare webhook payload with simplified email and optionally raw email + const payload: GmailWebhookPayload = { + email: simplifiedEmail, + timestamp: new Date().toISOString(), + ...(config.includeRawEmail ? { rawEmail: email } : {}), + } - const findAttachments = (part: any) => { - if (!part) return + logger.debug( + `[${requestId}] Sending ${config.includeRawEmail ? 'simplified + raw' : 'simplified'} email payload for ${email.id}` + ) - if (part.filename && part.filename.length > 0) { - attachments.push({ - filename: part.filename, - mimeType: part.mimeType || 'application/octet-stream', - size: part.body?.size || 0, + // Trigger the webhook + const webhookUrl = `${getBaseUrl()}/api/webhooks/trigger/${webhookData.path}` + + const response = await fetch(webhookUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Webhook-Secret': webhookData.secret || '', + 'User-Agent': 'SimStudio/1.0', + }, + body: JSON.stringify(payload), }) - } - // Look for attachments in nested parts - if (part.parts && Array.isArray(part.parts)) { - for (const subPart of part.parts) { - findAttachments(subPart) + if (!response.ok) { + const errorText = await response.text() + logger.error( + `[${requestId}] Failed to trigger webhook for email ${email.id}:`, + response.status, + errorText + ) + throw new Error(`Webhook request failed: ${response.status} - ${errorText}`) } - } - } - - if (email.payload) { - findAttachments(email.payload) - } - // Create simplified email object - const simplifiedEmail: SimplifiedEmail = { - id: email.id, - threadId: email.threadId, - subject: headers.subject || '[No Subject]', - from: headers.from || '', - to: headers.to || '', - cc: headers.cc || '', - date: date, - bodyText: textContent, - bodyHtml: htmlContent, - labels: email.labelIds || [], - hasAttachments: attachments.length > 0, - attachments: attachments, - } - - // Prepare webhook payload with simplified email and optionally raw email - const payload: GmailWebhookPayload = { - email: simplifiedEmail, - timestamp: new Date().toISOString(), - ...(config.includeRawEmail ? { rawEmail: email } : {}), - } + // Mark email as read if configured + if (config.markAsRead) { + await markEmailAsRead(accessToken, email.id) + } - logger.debug( - `[${requestId}] Sending ${config.includeRawEmail ? 'simplified + raw' : 'simplified'} email payload for ${email.id}` + return { + emailId: email.id, + webhookStatus: response.status, + processed: true, + } + } ) - // Trigger the webhook - const webhookUrl = `${getBaseUrl()}/api/webhooks/trigger/${webhookData.path}` - - const response = await fetch(webhookUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Webhook-Secret': webhookData.secret || '', - 'User-Agent': 'SimStudio/1.0', - }, - body: JSON.stringify(payload), - }) - - if (!response.ok) { - logger.error( - `[${requestId}] Failed to trigger webhook for email ${email.id}:`, - response.status, - await response.text() - ) - continue - } - - // Mark email as read if configured - if (config.markAsRead) { - await markEmailAsRead(accessToken, email.id) - } - + logger.info( + `[${requestId}] Successfully processed email ${email.id} for webhook ${webhookData.id}` + ) processedCount++ - - await markMessageAsProcessed(dedupeKey) } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error' logger.error(`[${requestId}] Error processing email ${email.id}:`, errorMessage) + // Continue processing other emails even if one fails } } @@ -706,12 +675,7 @@ async function updateWebhookLastChecked(webhookId: string, timestamp: string, hi .where(eq(webhook.id, webhookId)) } -async function updateWebhookData( - webhookId: string, - timestamp: string, - historyId?: string, - processedEmailIds?: string[] -) { +async function updateWebhookData(webhookId: string, timestamp: string, historyId?: string) { const existingConfig = (await db.select().from(webhook).where(eq(webhook.id, webhookId)))[0]?.providerConfig || {} @@ -722,7 +686,6 @@ async function updateWebhookData( ...existingConfig, lastCheckedTimestamp: timestamp, ...(historyId ? { historyId } : {}), - ...(processedEmailIds ? { processedEmailIds } : {}), }, updatedAt: new Date(), }) diff --git a/apps/sim/lib/webhooks/outlook-polling-service.ts b/apps/sim/lib/webhooks/outlook-polling-service.ts index 8f39549d17..4d227a88f6 100644 --- a/apps/sim/lib/webhooks/outlook-polling-service.ts +++ b/apps/sim/lib/webhooks/outlook-polling-service.ts @@ -1,8 +1,8 @@ import { and, eq } from 'drizzle-orm' import { htmlToText } from 'html-to-text' import { nanoid } from 'nanoid' +import { pollingIdempotency } from '@/lib/idempotency' import { createLogger } from '@/lib/logs/console/logger' -import { hasProcessedMessage, markMessageAsProcessed } from '@/lib/redis' import { getBaseUrl } from '@/lib/urls/utils' import { getOAuthToken, refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' import { db } from '@/db' @@ -17,7 +17,6 @@ interface OutlookWebhookConfig { markAsRead?: boolean maxEmailsPerPoll?: number lastCheckedTimestamp?: string - processedEmailIds?: string[] pollingInterval?: number includeRawEmail?: boolean } @@ -179,42 +178,24 @@ export async function pollOutlookWebhooks() { logger.info(`[${requestId}] Found ${emails.length} emails for webhook ${webhookId}`) - // Filter out already processed emails - const processedEmailIds = config.processedEmailIds || [] - const newEmails = emails.filter((email) => !processedEmailIds.includes(email.id)) - - if (!newEmails.length) { - logger.info(`[${requestId}] All emails already processed for webhook ${webhookId}`) - await updateWebhookLastChecked(webhookId, now.toISOString()) - return { success: true, webhookId, status: 'all_processed' } - } - - logger.info( - `[${requestId}] Processing ${newEmails.length} new emails for webhook ${webhookId}` - ) + logger.info(`[${requestId}] Processing ${emails.length} emails for webhook ${webhookId}`) // Process emails const processed = await processOutlookEmails( - newEmails, + emails, webhookData, config, accessToken, requestId ) - // Record which email IDs have been processed - const newProcessedIds = [...processedEmailIds, ...newEmails.map((email) => email.id)] - // Keep only the most recent 100 IDs to prevent the list from growing too large - const trimmedProcessedIds = newProcessedIds.slice(-100) - - // Update webhook with latest timestamp and processed email IDs - await updateWebhookData(webhookId, now.toISOString(), trimmedProcessedIds) + // Update webhook with latest timestamp + await updateWebhookLastChecked(webhookId, now.toISOString()) return { success: true, webhookId, emailsFound: emails.length, - newEmails: newEmails.length, emailsProcessed: processed, } } catch (error) { @@ -358,91 +339,94 @@ async function processOutlookEmails( for (const email of emails) { try { - // Check if we've already processed this email (Redis-based deduplication) - const redisKey = `outlook-email-${email.id}` - const alreadyProcessed = await hasProcessedMessage(redisKey) + const result = await pollingIdempotency.executeWithIdempotency( + 'outlook', + `${webhookData.id}:${email.id}`, + async () => { + // Convert to simplified format + const simplifiedEmail: SimplifiedOutlookEmail = { + id: email.id, + conversationId: email.conversationId, + subject: email.subject || '(No Subject)', + from: email.from?.emailAddress?.address || '', + to: email.toRecipients?.map((r) => r.emailAddress.address).join(', ') || '', + cc: email.ccRecipients?.map((r) => r.emailAddress.address).join(', ') || '', + date: email.receivedDateTime, + bodyText: (() => { + const content = email.body?.content || '' + const type = (email.body?.contentType || '').toLowerCase() + if (!content) { + return email.bodyPreview || '' + } + if (type === 'text' || type === 'text/plain') { + return content + } + return convertHtmlToPlainText(content) + })(), + bodyHtml: email.body?.content || '', + hasAttachments: email.hasAttachments, + isRead: email.isRead, + folderId: email.parentFolderId, + // Thread support fields + messageId: email.id, + threadId: email.conversationId, + } - if (alreadyProcessed) { - logger.debug(`[${requestId}] Email ${email.id} already processed, skipping`) - continue - } + // Create webhook payload + const payload: OutlookWebhookPayload = { + email: simplifiedEmail, + timestamp: new Date().toISOString(), + } - // Convert to simplified format - const simplifiedEmail: SimplifiedOutlookEmail = { - id: email.id, - conversationId: email.conversationId, - subject: email.subject || '(No Subject)', - from: email.from?.emailAddress?.address || '', - to: email.toRecipients?.map((r) => r.emailAddress.address).join(', ') || '', - cc: email.ccRecipients?.map((r) => r.emailAddress.address).join(', ') || '', - date: email.receivedDateTime, - bodyText: (() => { - const content = email.body?.content || '' - const type = (email.body?.contentType || '').toLowerCase() - if (!content) { - return email.bodyPreview || '' + // Include raw email if configured + if (config.includeRawEmail) { + payload.rawEmail = email } - if (type === 'text' || type === 'text/plain') { - return content + + logger.info( + `[${requestId}] Processing email: ${email.subject} from ${email.from?.emailAddress?.address}` + ) + + // Trigger the webhook + const webhookUrl = `${getBaseUrl()}/api/webhooks/trigger/${webhookData.path}` + + const response = await fetch(webhookUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Webhook-Secret': webhookData.secret || '', + 'User-Agent': 'SimStudio/1.0', + }, + body: JSON.stringify(payload), + }) + + if (!response.ok) { + const errorText = await response.text() + logger.error( + `[${requestId}] Failed to trigger webhook for email ${email.id}:`, + response.status, + errorText + ) + throw new Error(`Webhook request failed: ${response.status} - ${errorText}`) } - return convertHtmlToPlainText(content) - })(), - bodyHtml: email.body?.content || '', - hasAttachments: email.hasAttachments, - isRead: email.isRead, - folderId: email.parentFolderId, - // Thread support fields - messageId: email.id, - threadId: email.conversationId, - } - // Create webhook payload - const payload: OutlookWebhookPayload = { - email: simplifiedEmail, - timestamp: new Date().toISOString(), - } + // Mark email as read if configured + if (config.markAsRead) { + await markOutlookEmailAsRead(accessToken, email.id) + } - // Include raw email if configured - if (config.includeRawEmail) { - payload.rawEmail = email - } + return { + emailId: email.id, + webhookStatus: response.status, + processed: true, + } + } + ) logger.info( - `[${requestId}] Processing email: ${email.subject} from ${email.from?.emailAddress?.address}` + `[${requestId}] Successfully processed email ${email.id} for webhook ${webhookData.id}` ) - - // Trigger the webhook - const webhookUrl = `${getBaseUrl()}/api/webhooks/trigger/${webhookData.path}` - - const response = await fetch(webhookUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Webhook-Secret': webhookData.secret || '', - 'User-Agent': 'SimStudio/1.0', - }, - body: JSON.stringify(payload), - }) - - if (!response.ok) { - logger.error( - `[${requestId}] Failed to trigger webhook for email ${email.id}:`, - response.status, - await response.text() - ) - continue - } - - // Mark email as read if configured - if (config.markAsRead) { - await markOutlookEmailAsRead(accessToken, email.id) - } - - // Mark as processed in Redis (expires after 7 days) - await markMessageAsProcessed(redisKey, 7 * 24 * 60 * 60) - processedCount++ - logger.info(`[${requestId}] Successfully processed email ${email.id}`) } catch (error) { logger.error(`[${requestId}] Error processing email ${email.id}:`, error) } @@ -507,39 +491,3 @@ async function updateWebhookLastChecked(webhookId: string, timestamp: string) { logger.error(`Error updating webhook ${webhookId} last checked timestamp:`, error) } } - -async function updateWebhookData( - webhookId: string, - timestamp: string, - processedEmailIds: string[] -) { - try { - const currentWebhook = await db - .select({ providerConfig: webhook.providerConfig }) - .from(webhook) - .where(eq(webhook.id, webhookId)) - .limit(1) - - if (!currentWebhook.length) { - logger.error(`Webhook ${webhookId} not found`) - return - } - - const currentConfig = (currentWebhook[0].providerConfig as any) || {} - const updatedConfig = { - ...currentConfig, // Preserve ALL existing config including userId - lastCheckedTimestamp: timestamp, - processedEmailIds, - } - - await db - .update(webhook) - .set({ - providerConfig: updatedConfig, - updatedAt: new Date(), - }) - .where(eq(webhook.id, webhookId)) - } catch (error) { - logger.error(`Error updating webhook ${webhookId} data:`, error) - } -} diff --git a/apps/sim/vercel.json b/apps/sim/vercel.json index ee0ae55262..38fb972e40 100644 --- a/apps/sim/vercel.json +++ b/apps/sim/vercel.json @@ -15,6 +15,10 @@ { "path": "/api/logs/cleanup", "schedule": "0 0 * * *" + }, + { + "path": "/api/webhooks/cleanup/idempotency", + "schedule": "0 2 * * *" } ] } From ba21d274eced53ac1ddabac4470fef560be814d5 Mon Sep 17 00:00:00 2001 From: Adam Gough <77861281+aadamgough@users.noreply.github.com> Date: Mon, 15 Sep 2025 15:43:20 -0700 Subject: [PATCH 05/10] improvement(array-index): resolved variables for 2d arrays (#1328) * resolved variables for 2d arrays * added tests --------- Co-authored-by: Adam Gough Co-authored-by: waleedlatif1 --- apps/sim/executor/resolver/resolver.test.ts | 533 ++++++++++++++++++-- apps/sim/executor/resolver/resolver.ts | 62 +++ 2 files changed, 555 insertions(+), 40 deletions(-) diff --git a/apps/sim/executor/resolver/resolver.test.ts b/apps/sim/executor/resolver/resolver.test.ts index 3c6a84297c..8cd9a08508 100644 --- a/apps/sim/executor/resolver/resolver.test.ts +++ b/apps/sim/executor/resolver/resolver.test.ts @@ -2271,11 +2271,9 @@ describe('InputResolver', () => { const result = resolver.resolveInputs(testBlock, mockContext) - // Should include inputs without conditions expect(result).toHaveProperty('operation', 'upload') expect(result).toHaveProperty('alwaysVisible', 'always here') - // Should NOT include conditional field that doesn't match expect(result).not.toHaveProperty('conditionalField') }) @@ -2303,7 +2301,6 @@ describe('InputResolver', () => { ], }) - // Test upload_chunk operation const uploadChunkBlock: SerializedBlock = { id: 'knowledge-block', metadata: { id: 'knowledge', name: 'Knowledge Block' }, @@ -2324,7 +2321,6 @@ describe('InputResolver', () => { expect(result1).toHaveProperty('operation', 'upload_chunk') expect(result1).toHaveProperty('content', 'chunk content here') - // Test create_document operation const createDocBlock: SerializedBlock = { id: 'knowledge-block', metadata: { id: 'knowledge', name: 'Knowledge Block' }, @@ -2345,7 +2341,6 @@ describe('InputResolver', () => { expect(result2).toHaveProperty('operation', 'create_document') expect(result2).toHaveProperty('content', 'document content here') - // Test search operation (should NOT include content) const searchBlock: SerializedBlock = { id: 'knowledge-block', metadata: { id: 'knowledge', name: 'Knowledge Block' }, @@ -2368,8 +2363,471 @@ describe('InputResolver', () => { }) }) + describe('2D Array Indexing', () => { + let arrayResolver: InputResolver + let arrayContext: any + + beforeEach(() => { + const extendedWorkflow = { + ...sampleWorkflow, + blocks: [ + ...sampleWorkflow.blocks, + { + id: 'array-block', + metadata: { id: 'generic', name: 'Array Block' }, + position: { x: 100, y: 200 }, + config: { tool: 'generic', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: 'non-array-block', + metadata: { id: 'generic', name: 'Non Array Block' }, + position: { x: 300, y: 200 }, + config: { tool: 'generic', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: 'single-array-block', + metadata: { id: 'generic', name: 'Single Array Block' }, + position: { x: 400, y: 200 }, + config: { tool: 'generic', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + ], + connections: [ + ...sampleWorkflow.connections, + { source: 'starter-block', target: 'array-block' }, + { source: 'starter-block', target: 'non-array-block' }, + { source: 'starter-block', target: 'single-array-block' }, + ], + } + + const extendedAccessibilityMap = new Map>() + const allBlockIds = extendedWorkflow.blocks.map((b) => b.id) + const testBlockIds = ['test-block', 'function-test', 'condition-test'] + const allIds = [...allBlockIds, ...testBlockIds] + + extendedWorkflow.blocks.forEach((block) => { + const accessibleBlocks = new Set(allIds) + extendedAccessibilityMap.set(block.id, accessibleBlocks) + }) + + testBlockIds.forEach((testId) => { + const accessibleBlocks = new Set(allIds) + extendedAccessibilityMap.set(testId, accessibleBlocks) + }) + + arrayResolver = new InputResolver( + extendedWorkflow, + mockEnvironmentVars, + mockWorkflowVars, + undefined, + extendedAccessibilityMap + ) + + arrayContext = { + ...mockContext, + workflow: extendedWorkflow, + blockStates: new Map([ + ...mockContext.blockStates, + [ + 'array-block', + { + output: { + matrix: [ + ['a', 'b', 'c'], + ['d', 'e', 'f'], + ['g', 'h', 'i'], + ], + nestedData: { + values: [ + [10, 20, 30], + [40, 50, 60], + [70, 80, 90], + ], + }, + deepNested: [ + [ + [1, 2, 3], + [4, 5, 6], + ], + [ + [7, 8, 9], + [10, 11, 12], + ], + ], + }, + }, + ], + [ + 'non-array-block', + { + output: { + notAnArray: 'just a string', + }, + }, + ], + [ + 'single-array-block', + { + output: { + items: ['first', 'second', 'third'], + }, + }, + ], + ]), + activeExecutionPath: new Set([ + ...mockContext.activeExecutionPath, + 'array-block', + 'non-array-block', + 'single-array-block', + ]), + } + }) + + it.concurrent('should resolve basic 2D array access like matrix[0][1]', () => { + const block: SerializedBlock = { + id: 'test-block', + metadata: { id: 'generic', name: 'Test Block' }, + position: { x: 0, y: 0 }, + config: { + tool: 'generic', + params: { + value: '', + }, + }, + inputs: { + value: 'string', + }, + outputs: {}, + enabled: true, + } + + const result = arrayResolver.resolveInputs(block, arrayContext) + expect(result.value).toBe('b') + }) + + it.concurrent('should resolve 2D array access with different indices', () => { + const block: SerializedBlock = { + id: 'test-block', + metadata: { id: 'generic', name: 'Test Block' }, + position: { x: 0, y: 0 }, + config: { + tool: 'generic', + params: { + topLeft: '', + center: '', + bottomRight: '', + }, + }, + inputs: { + topLeft: 'string', + center: 'string', + bottomRight: 'string', + }, + outputs: {}, + enabled: true, + } + + const result = arrayResolver.resolveInputs(block, arrayContext) + expect(result.topLeft).toBe('a') + expect(result.center).toBe('e') + expect(result.bottomRight).toBe('i') + }) + + it.concurrent('should resolve property access combined with 2D array indexing', () => { + const block: SerializedBlock = { + id: 'test-block', + metadata: { id: 'generic', name: 'Test Block' }, + position: { x: 0, y: 0 }, + config: { + tool: 'generic', + params: { + value1: '', + value2: '', + value3: '', + }, + }, + inputs: { + value1: 'string', + value2: 'string', + value3: 'string', + }, + outputs: {}, + enabled: true, + } + + const result = arrayResolver.resolveInputs(block, arrayContext) + expect(result.value1).toBe('20') + expect(result.value2).toBe('60') + expect(result.value3).toBe('70') + }) + + it.concurrent('should resolve 3D array access (multiple nested indices)', () => { + const block: SerializedBlock = { + id: 'test-block', + metadata: { id: 'generic', name: 'Test Block' }, + position: { x: 0, y: 0 }, + config: { + tool: 'generic', + params: { + deep1: '', + deep2: '', + deep3: '', + deep4: '', + }, + }, + inputs: { + deep1: 'string', + deep2: 'string', + deep3: 'string', + deep4: 'string', + }, + outputs: {}, + enabled: true, + } + + const result = arrayResolver.resolveInputs(block, arrayContext) + expect(result.deep1).toBe('2') + expect(result.deep2).toBe('6') + expect(result.deep3).toBe('7') + expect(result.deep4).toBe('12') + }) + + it.concurrent('should handle start block with 2D array access', () => { + arrayContext.blockStates.set('starter-block', { + output: { + input: 'Hello World', + type: 'text', + data: [ + ['row1col1', 'row1col2'], + ['row2col1', 'row2col2'], + ], + }, + }) + + const block: SerializedBlock = { + id: 'test-block', + metadata: { id: 'generic', name: 'Test Block' }, + position: { x: 0, y: 0 }, + config: { + tool: 'generic', + params: { + value1: '', + value2: '', + value3: '', + value4: '', + }, + }, + inputs: { + value1: 'string', + value2: 'string', + value3: 'string', + value4: 'string', + }, + outputs: {}, + enabled: true, + } + + const result = arrayResolver.resolveInputs(block, arrayContext) + expect(result.value1).toBe('row1col1') + expect(result.value2).toBe('row1col2') + expect(result.value3).toBe('row2col1') + expect(result.value4).toBe('row2col2') + }) + + it.concurrent('should throw error for out of bounds 2D array access', () => { + const block: SerializedBlock = { + id: 'test-block', + metadata: { id: 'generic', name: 'Test Block' }, + position: { x: 0, y: 0 }, + config: { + tool: 'generic', + params: { + value: '', // Row 5 doesn't exist + }, + }, + inputs: { + value: 'string', + }, + outputs: {}, + enabled: true, + } + + expect(() => arrayResolver.resolveInputs(block, arrayContext)).toThrow( + /Array index 5 is out of bounds/ + ) + }) + + it.concurrent('should throw error for out of bounds second dimension access', () => { + const block: SerializedBlock = { + id: 'test-block', + metadata: { id: 'generic', name: 'Test Block' }, + position: { x: 0, y: 0 }, + config: { + tool: 'generic', + params: { + value: '', // Column 5 doesn't exist + }, + }, + inputs: { + value: 'string', + }, + outputs: {}, + enabled: true, + } + + expect(() => arrayResolver.resolveInputs(block, arrayContext)).toThrow( + /Array index 5 is out of bounds/ + ) + }) + + it.concurrent('should throw error when accessing non-array as array', () => { + const block: SerializedBlock = { + id: 'test-block', + metadata: { id: 'generic', name: 'Test Block' }, + position: { x: 0, y: 0 }, + config: { + tool: 'generic', + params: { + value: '', + }, + }, + inputs: { + value: 'string', + }, + outputs: {}, + enabled: true, + } + + expect(() => arrayResolver.resolveInputs(block, arrayContext)).toThrow(/Invalid path/) + }) + + it.concurrent('should throw error with invalid index format', () => { + const block: SerializedBlock = { + id: 'test-block', + metadata: { id: 'generic', name: 'Test Block' }, + position: { x: 0, y: 0 }, + config: { + tool: 'generic', + params: { + value: '', // Non-numeric index + }, + }, + inputs: { + value: 'string', + }, + outputs: {}, + enabled: true, + } + + expect(() => arrayResolver.resolveInputs(block, arrayContext)).toThrow( + /No value found at path/ + ) + }) + + it.concurrent('should maintain backward compatibility with single array indexing', () => { + // Data is already set up in beforeEach + + const block: SerializedBlock = { + id: 'test-block', + metadata: { id: 'generic', name: 'Test Block' }, + position: { x: 0, y: 0 }, + config: { + tool: 'generic', + params: { + first: '', + second: '', + third: '', + }, + }, + inputs: { + first: 'string', + second: 'string', + third: 'string', + }, + outputs: {}, + enabled: true, + } + + const result = arrayResolver.resolveInputs(block, arrayContext) + expect(result.first).toBe('first') + expect(result.second).toBe('second') + expect(result.third).toBe('third') + }) + + it.concurrent('should handle mixed single and multi-dimensional access in same block', () => { + const block: SerializedBlock = { + id: 'test-block', + metadata: { id: 'generic', name: 'Test Block' }, + position: { x: 0, y: 0 }, + config: { + tool: 'generic', + params: { + singleDim: '', // Should return the whole row + multiDim: '', // Should return specific element + }, + }, + inputs: { + singleDim: 'string', + multiDim: 'string', + }, + outputs: {}, + enabled: true, + } + + const result = arrayResolver.resolveInputs(block, arrayContext) + expect(result.singleDim).toEqual(['d', 'e', 'f']) // Whole row as array + expect(result.multiDim).toBe('e') // Specific element + }) + + it.concurrent('should properly format 2D array values for different block types', () => { + const functionBlock: SerializedBlock = { + id: 'function-test', + metadata: { id: BlockType.FUNCTION, name: 'Function Test' }, + position: { x: 0, y: 0 }, + config: { + tool: BlockType.FUNCTION, + params: { + code: 'return ', + }, + }, + inputs: {}, + outputs: {}, + enabled: true, + } + + const conditionBlock: SerializedBlock = { + id: 'condition-test', + metadata: { id: BlockType.CONDITION, name: 'Condition Test' }, + position: { x: 0, y: 0 }, + config: { + tool: BlockType.CONDITION, + params: { + conditions: ' === "b"', + }, + }, + inputs: {}, + outputs: {}, + enabled: true, + } + + const functionResult = arrayResolver.resolveInputs(functionBlock, arrayContext) + const conditionResult = arrayResolver.resolveInputs(conditionBlock, arrayContext) + + expect(functionResult.code).toBe('return "b"') // Should be quoted for function + expect(conditionResult.conditions).toBe(' === "b"') // Not resolved at input level + }) + }) + describe('Variable Reference Validation', () => { - it('should allow block references without dots like ', () => { + it.concurrent('should allow block references without dots like ', () => { const block: SerializedBlock = { id: 'test-block', metadata: { id: 'generic', name: 'Test Block' }, @@ -2392,7 +2850,7 @@ describe('InputResolver', () => { expect(result.content).not.toBe('Value from block') }) - it('should allow other block references without dots', () => { + it.concurrent('should allow other block references without dots', () => { const testAccessibility = new Map>() const allIds = [ 'starter-block', @@ -2406,14 +2864,6 @@ describe('InputResolver', () => { }) testAccessibility.set('test-block', new Set(allIds)) - const testResolver = new InputResolver( - sampleWorkflow, - mockEnvironmentVars, - mockWorkflowVars, - undefined, - testAccessibility - ) - const extendedWorkflow = { ...sampleWorkflow, blocks: [ @@ -2468,7 +2918,7 @@ describe('InputResolver', () => { expect(() => testResolverWithExtended.resolveInputs(block, extendedContext)).not.toThrow() }) - it('should reject operator expressions that look like comparisons', () => { + it.concurrent('should reject operator expressions that look like comparisons', () => { const block: SerializedBlock = { id: 'condition-block', metadata: { id: BlockType.CONDITION, name: 'Condition Block' }, @@ -2491,7 +2941,7 @@ describe('InputResolver', () => { expect(result.conditions).toBe('x < 5 && 8 > b') }) - it('should still allow regular dotted references', () => { + it.concurrent('should still allow regular dotted references', () => { const block: SerializedBlock = { id: 'test-block', metadata: { id: 'generic', name: 'Test Block' }, @@ -2520,33 +2970,36 @@ describe('InputResolver', () => { expect(result.variableRef).toBe('Hello') }) - it('should handle complex expressions with both valid references and operators', () => { - const block: SerializedBlock = { - id: 'condition-block', - metadata: { id: BlockType.CONDITION, name: 'Condition Block' }, - position: { x: 0, y: 0 }, - config: { - tool: 'condition', - params: { - conditions: - ' === "Hello" && x < 5 && 8 > y && !== null', + it.concurrent( + 'should handle complex expressions with both valid references and operators', + () => { + const block: SerializedBlock = { + id: 'condition-block', + metadata: { id: BlockType.CONDITION, name: 'Condition Block' }, + position: { x: 0, y: 0 }, + config: { + tool: 'condition', + params: { + conditions: + ' === "Hello" && x < 5 && 8 > y && !== null', + }, }, - }, - inputs: { - conditions: 'string', - }, - outputs: {}, - enabled: true, - } + inputs: { + conditions: 'string', + }, + outputs: {}, + enabled: true, + } - const result = resolver.resolveInputs(block, mockContext) + const result = resolver.resolveInputs(block, mockContext) - expect(result.conditions).toBe( - ' === "Hello" && x < 5 && 8 > y && !== null' - ) - }) + expect(result.conditions).toBe( + ' === "Hello" && x < 5 && 8 > y && !== null' + ) + } + ) - it('should reject numeric patterns that look like arithmetic', () => { + it.concurrent('should reject numeric patterns that look like arithmetic', () => { const block: SerializedBlock = { id: 'test-block', metadata: { id: 'generic', name: 'Test Block' }, diff --git a/apps/sim/executor/resolver/resolver.ts b/apps/sim/executor/resolver/resolver.ts index 1b96091260..c2fca222cb 100644 --- a/apps/sim/executor/resolver/resolver.ts +++ b/apps/sim/executor/resolver/resolver.ts @@ -562,6 +562,14 @@ export class InputResolver { } replacementValue = arrayValue[index] + } else if (/^(?:[^[]+(?:\[\d+\])+|(?:\[\d+\])+)$/.test(part)) { + // Enhanced: support multiple indices like "values[0][0]" + replacementValue = this.resolvePartWithIndices( + replacementValue, + part, + path, + 'starter block' + ) } else { // Regular property access with FileReference mapping replacementValue = resolvePropertyAccess(replacementValue, part) @@ -763,6 +771,14 @@ export class InputResolver { } replacementValue = arrayValue[index] + } else if (/^(?:[^[]+(?:\[\d+\])+|(?:\[\d+\])+)$/.test(part)) { + // Enhanced: support multiple indices like "values[0][0]" + replacementValue = this.resolvePartWithIndices( + replacementValue, + part, + path, + sourceBlock.metadata?.name || sourceBlock.id + ) } else { // Regular property access with FileReference mapping replacementValue = resolvePropertyAccess(replacementValue, part) @@ -1074,6 +1090,52 @@ export class InputResolver { return String(value) } + /** + * Applies a path part that may include multiple array indices, e.g. "values[0][0]". + */ + private resolvePartWithIndices( + base: any, + part: string, + fullPath: string, + sourceName: string + ): any { + let value = base + + // Extract leading property name if present + const propMatch = part.match(/^([^[]+)/) + let rest = part + if (propMatch) { + const prop = propMatch[1] + value = resolvePropertyAccess(value, prop) + rest = part.slice(prop.length) + if (value === undefined) { + throw new Error(`No value found at path "${fullPath}" in block "${sourceName}".`) + } + } + + // Iteratively apply each [index] + const indexRe = /^\[(\d+)\]/ + while (rest.length > 0) { + const m = rest.match(indexRe) + if (!m) { + throw new Error(`Invalid path "${part}" in "${fullPath}" for block "${sourceName}".`) + } + const idx = Number.parseInt(m[1], 10) + if (!Array.isArray(value)) { + throw new Error(`Invalid path "${part}" in "${fullPath}" for block "${sourceName}".`) + } + if (idx < 0 || idx >= value.length) { + throw new Error( + `Array index ${idx} is out of bounds in path "${fullPath}" for block "${sourceName}".` + ) + } + value = value[idx] + rest = rest.slice(m[0].length) + } + + return value + } + /** * Normalizes block name for consistent lookups. * Converts to lowercase and removes whitespace. From 4ce6bc94c3c305746315562dc54e6a5fa7e80b70 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 15 Sep 2025 15:54:41 -0700 Subject: [PATCH 06/10] fix(stripe): use latest version to fix event mismatch issues (#1336) * fix(stripe): use latest version to fix event mismatch issues * fix enterprise handling * cleanup * update better auth version * fix overage order of ops * upgrade better auth version * fix image typing * change image type to string | undefined --- apps/sim/lib/auth.ts | 16 +- apps/sim/lib/billing/core/usage.ts | 5 +- apps/sim/lib/billing/stripe-client.ts | 2 +- apps/sim/lib/billing/webhooks/enterprise.ts | 11 +- apps/sim/lib/billing/webhooks/invoices.ts | 187 +++++++++++--------- apps/sim/package.json | 8 +- bun.lock | 33 ++-- 7 files changed, 143 insertions(+), 119 deletions(-) diff --git a/apps/sim/lib/auth.ts b/apps/sim/lib/auth.ts index 1978cd4848..c1599670d5 100644 --- a/apps/sim/lib/auth.ts +++ b/apps/sim/lib/auth.ts @@ -48,7 +48,7 @@ const validStripeKey = env.STRIPE_SECRET_KEY let stripeClient = null if (validStripeKey) { stripeClient = new Stripe(env.STRIPE_SECRET_KEY || '', { - apiVersion: '2025-02-24.acacia', + apiVersion: '2025-08-27.basil', }) } @@ -592,7 +592,6 @@ export const auth = betterAuth({ id: uniqueId, name: 'Wealthbox User', email: `${uniqueId.replace(/[^a-zA-Z0-9]/g, '')}@wealthbox.user`, - image: null, emailVerified: false, createdAt: now, updatedAt: now, @@ -650,7 +649,6 @@ export const auth = betterAuth({ id: uniqueId, name: 'Supabase User', email: `${uniqueId.replace(/[^a-zA-Z0-9]/g, '')}@supabase.user`, - image: null, emailVerified: false, createdAt: now, updatedAt: now, @@ -760,7 +758,7 @@ export const auth = betterAuth({ id: profile.account_id, name: profile.name || profile.display_name || 'Confluence User', email: profile.email || `${profile.account_id}@atlassian.com`, - image: profile.picture || null, + image: profile.picture || undefined, emailVerified: true, // Assume verified since it's an Atlassian account createdAt: now, updatedAt: now, @@ -811,7 +809,7 @@ export const auth = betterAuth({ email: profile.email || `${profile.id}@discord.user`, image: profile.avatar ? `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.png` - : null, + : undefined, emailVerified: profile.verified || false, createdAt: now, updatedAt: now, @@ -881,7 +879,7 @@ export const auth = betterAuth({ id: profile.account_id, name: profile.name || profile.display_name || 'Jira User', email: profile.email || `${profile.account_id}@atlassian.com`, - image: profile.picture || null, + image: profile.picture || undefined, emailVerified: true, // Assume verified since it's an Atlassian account createdAt: now, updatedAt: now, @@ -949,7 +947,6 @@ export const auth = betterAuth({ id: profile.bot?.owner?.user?.id || profile.id, name: profile.name || profile.bot?.owner?.user?.name || 'Notion User', email: profile.person?.email || `${profile.id}@notion.user`, - image: null, // Notion API doesn't provide profile images emailVerified: !!profile.person?.email, createdAt: now, updatedAt: now, @@ -1000,7 +997,7 @@ export const auth = betterAuth({ id: data.id, name: data.name || 'Reddit User', email: `${data.name}@reddit.user`, // Reddit doesn't provide email in identity scope - image: data.icon_img || null, + image: data.icon_img || undefined, emailVerified: false, createdAt: now, updatedAt: now, @@ -1075,7 +1072,7 @@ export const auth = betterAuth({ emailVerified: true, createdAt: new Date(), updatedAt: new Date(), - image: viewer.avatarUrl || null, + image: viewer.avatarUrl || undefined, } } catch (error) { logger.error('Error in getUserInfo:', error) @@ -1138,7 +1135,6 @@ export const auth = betterAuth({ id: uniqueId, name: 'Slack Bot', email: `${uniqueId.replace(/[^a-zA-Z0-9]/g, '')}@slack.bot`, - image: null, emailVerified: false, createdAt: now, updatedAt: now, diff --git a/apps/sim/lib/billing/core/usage.ts b/apps/sim/lib/billing/core/usage.ts index 738fd5a749..b017dfdc4e 100644 --- a/apps/sim/lib/billing/core/usage.ts +++ b/apps/sim/lib/billing/core/usage.ts @@ -50,13 +50,12 @@ export async function getUserUsageData(userId: string): Promise { ]) if (userStatsData.length === 0) { + logger.error('User stats not found for userId', { userId }) throw new Error(`User stats not found for userId: ${userId}`) } const stats = userStatsData[0] - const currentUsage = Number.parseFloat( - stats.currentPeriodCost?.toString() ?? stats.totalCost.toString() - ) + const currentUsage = Number.parseFloat(stats.currentPeriodCost?.toString() ?? '0') // Determine usage limit based on plan type let limit: number diff --git a/apps/sim/lib/billing/stripe-client.ts b/apps/sim/lib/billing/stripe-client.ts index 93bcc64857..7a93a0bf45 100644 --- a/apps/sim/lib/billing/stripe-client.ts +++ b/apps/sim/lib/billing/stripe-client.ts @@ -38,7 +38,7 @@ const createStripeClientSingleton = () => { isInitializing = true stripeClient = new Stripe(env.STRIPE_SECRET_KEY || '', { - apiVersion: '2025-02-24.acacia', + apiVersion: '2025-08-27.basil', }) logger.info('Stripe client initialized successfully') diff --git a/apps/sim/lib/billing/webhooks/enterprise.ts b/apps/sim/lib/billing/webhooks/enterprise.ts index 18191e7852..44b2569576 100644 --- a/apps/sim/lib/billing/webhooks/enterprise.ts +++ b/apps/sim/lib/billing/webhooks/enterprise.ts @@ -98,6 +98,9 @@ export async function handleManualEnterpriseSubscription(event: Stripe.Event) { throw new Error('Enterprise subscription must include valid monthlyPrice in metadata') } + // Get the first subscription item which contains the period information + const referenceItem = stripeSubscription.items?.data?.[0] + const subscriptionRow = { id: crypto.randomUUID(), plan: 'enterprise', @@ -105,11 +108,11 @@ export async function handleManualEnterpriseSubscription(event: Stripe.Event) { stripeCustomerId, stripeSubscriptionId: stripeSubscription.id, status: stripeSubscription.status || null, - periodStart: stripeSubscription.current_period_start - ? new Date(stripeSubscription.current_period_start * 1000) + periodStart: referenceItem?.current_period_start + ? new Date(referenceItem.current_period_start * 1000) : null, - periodEnd: stripeSubscription.current_period_end - ? new Date(stripeSubscription.current_period_end * 1000) + periodEnd: referenceItem?.current_period_end + ? new Date(referenceItem.current_period_end * 1000) : null, cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end ?? null, seats, diff --git a/apps/sim/lib/billing/webhooks/invoices.ts b/apps/sim/lib/billing/webhooks/invoices.ts index 2557e2d10d..3d51cda603 100644 --- a/apps/sim/lib/billing/webhooks/invoices.ts +++ b/apps/sim/lib/billing/webhooks/invoices.ts @@ -53,8 +53,14 @@ export async function handleInvoicePaymentSucceeded(event: Stripe.Event) { try { const invoice = event.data.object as Stripe.Invoice - if (!invoice.subscription) return - const stripeSubscriptionId = String(invoice.subscription) + const subscription = invoice.parent?.subscription_details?.subscription + const stripeSubscriptionId = typeof subscription === 'string' ? subscription : subscription?.id + if (!stripeSubscriptionId) { + logger.info('No subscription found on invoice; skipping payment succeeded handler', { + invoiceId: invoice.id, + }) + return + } const records = await db .select() .from(subscriptionTable) @@ -156,7 +162,9 @@ export async function handleInvoicePaymentFailed(event: Stripe.Event) { attemptCount, }) // Block all users under this customer (org members or individual) - const stripeSubscriptionId = String(invoice.subscription || '') + // Overage invoices are manual invoices without parent.subscription_details + // We store the subscription ID in metadata when creating them + const stripeSubscriptionId = invoice.metadata?.subscriptionId as string | undefined if (stripeSubscriptionId) { const records = await db .select() @@ -203,10 +211,16 @@ export async function handleInvoiceFinalized(event: Stripe.Event) { try { const invoice = event.data.object as Stripe.Invoice // Only run for subscription renewal invoices (cycle boundary) - if (!invoice.subscription) return + const subscription = invoice.parent?.subscription_details?.subscription + const stripeSubscriptionId = typeof subscription === 'string' ? subscription : subscription?.id + if (!stripeSubscriptionId) { + logger.info('No subscription found on invoice; skipping finalized handler', { + invoiceId: invoice.id, + }) + return + } if (invoice.billing_reason && invoice.billing_reason !== 'subscription_cycle') return - const stripeSubscriptionId = String(invoice.subscription) const records = await db .select() .from(subscriptionTable) @@ -216,11 +230,9 @@ export async function handleInvoiceFinalized(event: Stripe.Event) { if (records.length === 0) return const sub = records[0] - // Always reset usage at cycle end for all plans - await resetUsageForSubscription({ plan: sub.plan, referenceId: sub.referenceId }) - - // Enterprise plans have no overages - skip overage invoice creation + // Enterprise plans have no overages - reset usage and exit if (sub.plan === 'enterprise') { + await resetUsageForSubscription({ plan: sub.plan, referenceId: sub.referenceId }) return } @@ -229,7 +241,7 @@ export async function handleInvoiceFinalized(event: Stripe.Event) { invoice.lines?.data?.[0]?.period?.end || invoice.period_end || Math.floor(Date.now() / 1000) const billingPeriod = new Date(periodEnd * 1000).toISOString().slice(0, 7) - // Compute overage (only for team and pro plans) + // Compute overage (only for team and pro plans), before resetting usage let totalOverage = 0 if (sub.plan === 'team') { const members = await db @@ -254,88 +266,101 @@ export async function handleInvoiceFinalized(event: Stripe.Event) { totalOverage = Math.max(0, usage.currentUsage - basePrice) } - if (totalOverage <= 0) return - - const customerId = String(invoice.customer) - const cents = Math.round(totalOverage * 100) - const itemIdemKey = `overage-item:${customerId}:${stripeSubscriptionId}:${billingPeriod}` - const invoiceIdemKey = `overage-invoice:${customerId}:${stripeSubscriptionId}:${billingPeriod}` + if (totalOverage > 0) { + const customerId = String(invoice.customer) + const cents = Math.round(totalOverage * 100) + const itemIdemKey = `overage-item:${customerId}:${stripeSubscriptionId}:${billingPeriod}` + const invoiceIdemKey = `overage-invoice:${customerId}:${stripeSubscriptionId}:${billingPeriod}` - // Inherit billing settings from the Stripe subscription/customer for autopay - const getPaymentMethodId = ( - pm: string | Stripe.PaymentMethod | null | undefined - ): string | undefined => (typeof pm === 'string' ? pm : pm?.id) + // Inherit billing settings from the Stripe subscription/customer for autopay + const getPaymentMethodId = ( + pm: string | Stripe.PaymentMethod | null | undefined + ): string | undefined => (typeof pm === 'string' ? pm : pm?.id) - let collectionMethod: 'charge_automatically' | 'send_invoice' = 'charge_automatically' - let defaultPaymentMethod: string | undefined - try { - const stripeSub = await stripe.subscriptions.retrieve(stripeSubscriptionId) - if (stripeSub.collection_method === 'send_invoice') { - collectionMethod = 'send_invoice' - } - const subDpm = getPaymentMethodId(stripeSub.default_payment_method) - if (subDpm) { - defaultPaymentMethod = subDpm - } else if (collectionMethod === 'charge_automatically') { - const custObj = await stripe.customers.retrieve(customerId) - if (custObj && !('deleted' in custObj)) { - const cust = custObj as Stripe.Customer - const custDpm = getPaymentMethodId(cust.invoice_settings?.default_payment_method) - if (custDpm) defaultPaymentMethod = custDpm + let collectionMethod: 'charge_automatically' | 'send_invoice' = 'charge_automatically' + let defaultPaymentMethod: string | undefined + try { + const stripeSub = await stripe.subscriptions.retrieve(stripeSubscriptionId) + if (stripeSub.collection_method === 'send_invoice') { + collectionMethod = 'send_invoice' + } + const subDpm = getPaymentMethodId(stripeSub.default_payment_method) + if (subDpm) { + defaultPaymentMethod = subDpm + } else if (collectionMethod === 'charge_automatically') { + const custObj = await stripe.customers.retrieve(customerId) + if (custObj && !('deleted' in custObj)) { + const cust = custObj as Stripe.Customer + const custDpm = getPaymentMethodId(cust.invoice_settings?.default_payment_method) + if (custDpm) defaultPaymentMethod = custDpm + } } + } catch (e) { + logger.error('Failed to retrieve subscription or customer', { error: e }) } - } catch (e) { - logger.error('Failed to retrieve subscription or customer', { error: e }) - } - // Create a draft invoice first so we can attach the item directly - const overageInvoice = await stripe.invoices.create( - { - customer: customerId, - collection_method: collectionMethod, - auto_advance: false, - ...(defaultPaymentMethod ? { default_payment_method: defaultPaymentMethod } : {}), - metadata: { - type: 'overage_billing', - billingPeriod, - subscriptionId: stripeSubscriptionId, + // Create a draft invoice first so we can attach the item directly + const overageInvoice = await stripe.invoices.create( + { + customer: customerId, + collection_method: collectionMethod, + auto_advance: false, + ...(defaultPaymentMethod ? { default_payment_method: defaultPaymentMethod } : {}), + metadata: { + type: 'overage_billing', + billingPeriod, + subscriptionId: stripeSubscriptionId, + }, }, - }, - { idempotencyKey: invoiceIdemKey } - ) + { idempotencyKey: invoiceIdemKey } + ) - // Attach the item to this invoice - await stripe.invoiceItems.create( - { - customer: customerId, - invoice: overageInvoice.id, - amount: cents, - currency: 'usd', - description: `Usage Based Overage – ${billingPeriod}`, - metadata: { - type: 'overage_billing', - billingPeriod, - subscriptionId: stripeSubscriptionId, + // Attach the item to this invoice + await stripe.invoiceItems.create( + { + customer: customerId, + invoice: overageInvoice.id, + amount: cents, + currency: 'usd', + description: `Usage Based Overage – ${billingPeriod}`, + metadata: { + type: 'overage_billing', + billingPeriod, + subscriptionId: stripeSubscriptionId, + }, }, - }, - { idempotencyKey: itemIdemKey } - ) + { idempotencyKey: itemIdemKey } + ) - // Finalize to trigger autopay (if charge_automatically and a PM is present) - const finalized = await stripe.invoices.finalizeInvoice(overageInvoice.id) - // Some manual invoices may remain open after finalize; ensure we pay immediately when possible - if (collectionMethod === 'charge_automatically' && finalized.status === 'open') { - try { - await stripe.invoices.pay(finalized.id, { - payment_method: defaultPaymentMethod, - }) - } catch (payError) { - logger.error('Failed to auto-pay overage invoice', { - error: payError, - invoiceId: finalized.id, - }) + // Finalize to trigger autopay (if charge_automatically and a PM is present) + const draftId = overageInvoice.id + if (typeof draftId !== 'string' || draftId.length === 0) { + logger.error('Stripe created overage invoice without id; aborting finalize') + } else { + const finalized = await stripe.invoices.finalizeInvoice(draftId) + // Some manual invoices may remain open after finalize; ensure we pay immediately when possible + if (collectionMethod === 'charge_automatically' && finalized.status === 'open') { + try { + const payId = finalized.id + if (typeof payId !== 'string' || payId.length === 0) { + logger.error('Finalized invoice missing id') + throw new Error('Finalized invoice missing id') + } + await stripe.invoices.pay(payId, { + payment_method: defaultPaymentMethod, + }) + } catch (payError) { + logger.error('Failed to auto-pay overage invoice', { + error: payError, + invoiceId: finalized.id, + }) + } + } } } + + // Finally, reset usage for this subscription after overage handling + await resetUsageForSubscription({ plan: sub.plan, referenceId: sub.referenceId }) } catch (error) { logger.error('Failed to handle invoice finalized', { error }) throw error diff --git a/apps/sim/package.json b/apps/sim/package.json index daa83498df..50410d5d96 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -26,17 +26,17 @@ "test:billing:suite": "bun run scripts/test-billing-suite.ts" }, "dependencies": { - "@e2b/code-interpreter": "^2.0.0", "@anthropic-ai/sdk": "^0.39.0", "@aws-sdk/client-s3": "^3.779.0", "@aws-sdk/s3-request-presigner": "^3.779.0", "@azure/communication-email": "1.0.0", "@azure/storage-blob": "12.27.0", - "@better-auth/stripe": "^1.2.9", + "@better-auth/stripe": "https://pkg.pr.new/better-auth/better-auth/@better-auth/stripe@4683", "@browserbasehq/stagehand": "^2.0.0", "@cerebras/cerebras_cloud_sdk": "^1.23.0", "@chatscope/chat-ui-kit-react": "2.1.1", "@chatscope/chat-ui-kit-styles": "1.4.0", + "@e2b/code-interpreter": "^2.0.0", "@hookform/resolvers": "^4.1.3", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-collector": "^0.25.0", @@ -75,7 +75,7 @@ "@vercel/og": "^0.6.5", "@vercel/speed-insights": "^1.2.0", "ai": "^4.3.2", - "better-auth": "^1.2.9", + "better-auth": "1.3.10", "browser-image-compression": "^2.0.2", "cheerio": "1.1.2", "class-variance-authority": "^0.7.1", @@ -127,7 +127,7 @@ "rtf-stream-parser": "3.8.0", "sharp": "0.34.3", "socket.io": "^4.8.1", - "stripe": "^17.7.0", + "stripe": "18.5.0", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "three": "0.177.0", diff --git a/bun.lock b/bun.lock index c93bd57a83..6de6da560d 100644 --- a/bun.lock +++ b/bun.lock @@ -62,7 +62,7 @@ "@aws-sdk/s3-request-presigner": "^3.779.0", "@azure/communication-email": "1.0.0", "@azure/storage-blob": "12.27.0", - "@better-auth/stripe": "^1.2.9", + "@better-auth/stripe": "https://pkg.pr.new/better-auth/better-auth/@better-auth/stripe@4683", "@browserbasehq/stagehand": "^2.0.0", "@cerebras/cerebras_cloud_sdk": "^1.23.0", "@chatscope/chat-ui-kit-react": "2.1.1", @@ -106,7 +106,7 @@ "@vercel/og": "^0.6.5", "@vercel/speed-insights": "^1.2.0", "ai": "^4.3.2", - "better-auth": "^1.2.9", + "better-auth": "1.3.10", "browser-image-compression": "^2.0.2", "cheerio": "1.1.2", "class-variance-authority": "^0.7.1", @@ -123,6 +123,7 @@ "geist": "1.4.2", "groq-sdk": "^0.15.0", "html-to-text": "^9.0.5", + "@better-auth/stripe": "https://pkg.pr.new/better-auth/better-auth/@better-auth/stripe@4683", "iconv-lite": "0.7.0", "input-otp": "^1.4.2", "ioredis": "^5.6.0", @@ -158,7 +159,7 @@ "rtf-stream-parser": "3.8.0", "sharp": "0.34.3", "socket.io": "^4.8.1", - "stripe": "^17.7.0", + "stripe": "18.5.0", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "three": "0.177.0", @@ -439,9 +440,9 @@ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], - "@better-auth/stripe": ["@better-auth/stripe@1.3.7", "", { "dependencies": { "better-auth": "^1.3.7" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-hmcWJu9RB1IOf+Mb8izQyLOAkRFKes8O0j/VoIFwLR0l2zWTknXKzkLUjadFPeKXHtV3i2n2++prjuVev6F6zw=="], + "@better-auth/stripe": ["@better-auth/stripe@https://pkg.pr.new/better-auth/better-auth/@better-auth/stripe@4683", { "dependencies": { "zod": "^4.1.5" }, "peerDependencies": { "better-auth": "1.3.10", "stripe": "^18" } }], - "@better-auth/utils": ["@better-auth/utils@0.2.6", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-3y/vaL5Ox33dBwgJ6ub3OPkVqr6B5xL2kgxNHG8eHZuryLyG/4JSPGqjbdRSgjuy9kALUZYDFl+ORIAxlWMSuA=="], + "@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="], "@better-fetch/fetch": ["@better-fetch/fetch@1.1.18", "", {}, "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA=="], @@ -717,9 +718,9 @@ "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.1", "", { "os": "win32", "cpu": "x64" }, "sha512-y+wTBxelk2xiNofmDOVU7O5WxTHcvOoL3srOM0kxTzKDjQ57kPU0tpnPJ/BWrRnsOwXEv0+3QSbGR7hY4n9LkQ=="], - "@noble/ciphers": ["@noble/ciphers@0.6.0", "", {}, "sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ=="], + "@noble/ciphers": ["@noble/ciphers@2.0.0", "", {}, "sha512-j/l6jpnpaIBM87cAYPJzi/6TgqmBv9spkqPyCXvRYsu5uxqh6tPJZDnD85yo8VWqzTuTQPgfv7NgT63u7kbwAQ=="], - "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "@noble/hashes": ["@noble/hashes@2.0.0", "", {}, "sha512-h8VUBlE8R42+XIDO229cgisD287im3kdY6nbNZJFjc6ZvKIXPYXe6Vc/t+kyjFdMFyt5JpapzTsEg8n63w5/lw=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -1633,9 +1634,9 @@ "bcryptjs": ["bcryptjs@3.0.2", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog=="], - "better-auth": ["better-auth@1.3.7", "", { "dependencies": { "@better-auth/utils": "0.2.6", "@better-fetch/fetch": "^1.1.18", "@noble/ciphers": "^0.6.0", "@noble/hashes": "^1.8.0", "@simplewebauthn/browser": "^13.1.2", "@simplewebauthn/server": "^13.1.2", "better-call": "^1.0.13", "defu": "^6.1.4", "jose": "^5.10.0", "kysely": "^0.28.5", "nanostores": "^0.11.4" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-/1fEyx2SGgJQM5ujozDCh9eJksnVkNU/J7Fk/tG5Y390l8nKbrPvqiFlCjlMM+scR+UABJbQzA6An7HT50LHyQ=="], + "better-auth": ["better-auth@1.3.10", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "^1.1.18", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "@simplewebauthn/browser": "^13.1.2", "@simplewebauthn/server": "^13.1.2", "better-call": "1.0.19", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.5" }, "peerDependencies": { "@lynx-js/react": "*", "@sveltejs/kit": "^2.0.0", "next": "^14.0.0 || ^15.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@sveltejs/kit", "next", "react", "react-dom", "solid-js", "svelte", "vue"] }, "sha512-cEdvbqJ2TlTXUSktHKs8V3rHdFYkEG7QmQZpLXGjXZX6F0nYbTk2QPsWXNhxbinFAlE2ca4virzuDsmsQlLIVw=="], - "better-call": ["better-call@1.0.16", "", { "dependencies": { "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-42dgJ1rOtc0anOoxjXPOWuel/Z/4aeO7EJ2SiXNwvlkySSgjXhNjAjTMWa8DL1nt6EXS3jl3VKC3mPsU/lUgVA=="], + "better-call": ["better-call@1.0.19", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-sI3GcA1SCVa3H+CDHl8W8qzhlrckwXOTKhqq3OOPXjgn5aTOMIqGY34zLY/pHA6tRRMjTUC3lz5Mi7EbDA24Kw=="], "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], @@ -2575,7 +2576,7 @@ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - "nanostores": ["nanostores@0.11.4", "", {}, "sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ=="], + "nanostores": ["nanostores@1.0.1", "", {}, "sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw=="], "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], @@ -3057,7 +3058,7 @@ "strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="], - "stripe": ["stripe@17.7.0", "", { "dependencies": { "@types/node": ">=8.1.0", "qs": "^6.11.0" } }, "sha512-aT2BU9KkizY9SATf14WhhYVv2uOapBWX0OFWF4xvcj1mPaNotlSc2CsxpS4DS46ZueSppmCF5BX1sNYBtwBvfw=="], + "stripe": ["stripe@18.5.0", "", { "dependencies": { "qs": "^6.11.0" }, "peerDependencies": { "@types/node": ">=12.x.x" }, "optionalPeers": ["@types/node"] }, "sha512-Hp+wFiEQtCB0LlNgcFh5uVyKznpDjzyUZ+CNVEf+I3fhlYvh7rZruIg+jOwzJRCpy0ZTPMjlzm7J2/M2N6d+DA=="], "strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], @@ -3363,6 +3364,8 @@ "@babel/template/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="], + "@better-auth/stripe/zod": ["zod@4.1.5", "", {}, "sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg=="], + "@browserbasehq/sdk/@types/node": ["@types/node@18.19.123", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-K7DIaHnh0mzVxreCR9qwgNxp3MH9dltPNIEddW9MYUlcKAzm+3grKNSTe2vCJHI1FaLpvpL5JGJrz1UZDKYvDg=="], "@browserbasehq/sdk/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], @@ -3747,7 +3750,9 @@ "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "better-auth/jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="], + "better-auth/jose": ["jose@6.1.0", "", {}, "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA=="], + + "better-auth/zod": ["zod@4.1.5", "", {}, "sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg=="], "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], @@ -3925,8 +3930,6 @@ "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "stripe/@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="], - "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], "sucrase/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], @@ -4405,8 +4408,6 @@ "sim/tailwindcss/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], - "stripe/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], - "sucrase/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], "sucrase/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], From 994c35f62c1232a0f016547c4a447a2cdac6307c Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 15 Sep 2025 16:04:28 -0700 Subject: [PATCH 07/10] fix(stripe): revert to stable versioning for better auth plugin (#1337) --- apps/sim/package.json | 2 +- bun.lock | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/sim/package.json b/apps/sim/package.json index 50410d5d96..e9bc8f629a 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -31,7 +31,7 @@ "@aws-sdk/s3-request-presigner": "^3.779.0", "@azure/communication-email": "1.0.0", "@azure/storage-blob": "12.27.0", - "@better-auth/stripe": "https://pkg.pr.new/better-auth/better-auth/@better-auth/stripe@4683", + "@better-auth/stripe": "1.3.10", "@browserbasehq/stagehand": "^2.0.0", "@cerebras/cerebras_cloud_sdk": "^1.23.0", "@chatscope/chat-ui-kit-react": "2.1.1", diff --git a/bun.lock b/bun.lock index 6de6da560d..c15132c952 100644 --- a/bun.lock +++ b/bun.lock @@ -62,7 +62,7 @@ "@aws-sdk/s3-request-presigner": "^3.779.0", "@azure/communication-email": "1.0.0", "@azure/storage-blob": "12.27.0", - "@better-auth/stripe": "https://pkg.pr.new/better-auth/better-auth/@better-auth/stripe@4683", + "@better-auth/stripe": "1.3.10", "@browserbasehq/stagehand": "^2.0.0", "@cerebras/cerebras_cloud_sdk": "^1.23.0", "@chatscope/chat-ui-kit-react": "2.1.1", @@ -123,7 +123,6 @@ "geist": "1.4.2", "groq-sdk": "^0.15.0", "html-to-text": "^9.0.5", - "@better-auth/stripe": "https://pkg.pr.new/better-auth/better-auth/@better-auth/stripe@4683", "iconv-lite": "0.7.0", "input-otp": "^1.4.2", "ioredis": "^5.6.0", @@ -440,7 +439,7 @@ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], - "@better-auth/stripe": ["@better-auth/stripe@https://pkg.pr.new/better-auth/better-auth/@better-auth/stripe@4683", { "dependencies": { "zod": "^4.1.5" }, "peerDependencies": { "better-auth": "1.3.10", "stripe": "^18" } }], + "@better-auth/stripe": ["@better-auth/stripe@1.3.10", "", { "dependencies": { "zod": "^4.1.5" }, "peerDependencies": { "better-auth": "1.3.10", "stripe": "^18" } }, "sha512-abvL0e/m61oiWSZ5Oq5kxluZaR8z9tXl/lP94Qje2kxQ8V1mr4ZU6DHNva3z2QXbD2Me4RDchhCACrnbNsDT1A=="], "@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="], From d4165f5be6e004f0283916a6991ee1b4f94c38a2 Mon Sep 17 00:00:00 2001 From: Waleed Date: Mon, 15 Sep 2025 17:31:35 -0700 Subject: [PATCH 08/10] feat(docs): added footer for page navigation, i18n for docs (#1339) * update infra and remove railway * feat(docs): added footer for page navigation, i18n for docs * Revert "update infra and remove railway" This reverts commit abfa2f8d51901247acc6397960210569e84d72b1. * added SEO-related stuff * fix image sizes * add missing pages * remove extraneous comments --- .github/workflows/i18n.yml | 126 + .gitignore | 3 +- apps/docs/app/(docs)/[[...slug]]/layout.tsx | 5 - apps/docs/app/(docs)/[[...slug]]/page.tsx | 58 - apps/docs/app/(docs)/layout.tsx | 58 - apps/docs/app/[lang]/[[...slug]]/page.tsx | 161 + apps/docs/app/[lang]/layout.tsx | 99 + apps/docs/app/layout.tsx | 80 +- apps/docs/app/llms.txt/route.ts | 1 - apps/docs/app/robots.txt/route.ts | 58 + apps/docs/app/sitemap.xml/route.ts | 54 + apps/docs/components/structured-data.tsx | 174 + apps/docs/components/ui/image.tsx | 4 +- apps/docs/components/ui/language-dropdown.tsx | 107 + apps/docs/content/docs/blocks/meta.json | 16 - apps/docs/content/docs/connections/meta.json | 5 - apps/docs/content/docs/copilot/meta.json | 5 - .../content/docs/{ => en}/blocks/agent.mdx | 0 .../docs/content/docs/{ => en}/blocks/api.mdx | 0 .../docs/{ => en}/blocks/condition.mdx | 0 .../docs/{ => en}/blocks/evaluator.mdx | 0 .../content/docs/{ => en}/blocks/function.mdx | 0 .../content/docs/{ => en}/blocks/index.mdx | 0 .../content/docs/{ => en}/blocks/loop.mdx | 0 .../content/docs/{ => en}/blocks/parallel.mdx | 0 .../content/docs/{ => en}/blocks/response.mdx | 0 .../content/docs/{ => en}/blocks/router.mdx | 0 .../content/docs/{ => en}/blocks/workflow.mdx | 0 .../docs/{ => en}/connections/basics.mdx | 0 .../{ => en}/connections/data-structure.mdx | 0 .../docs/{ => en}/connections/index.mdx | 0 .../docs/{ => en}/connections/tags.mdx | 0 .../content/docs/{ => en}/copilot/index.mdx | 0 .../content/docs/{ => en}/execution/api.mdx | 0 .../docs/{ => en}/execution/basics.mdx | 0 .../content/docs/{ => en}/execution/costs.mdx | 0 .../content/docs/{ => en}/execution/index.mdx | 0 .../docs/{ => en}/execution/logging.mdx | 0 .../docs/{ => en}/getting-started/index.mdx | 0 apps/docs/content/docs/en/index.mdx | 60 + .../docs/{ => en}/introduction/index.mdx | 8 +- .../docs/{ => en}/knowledgebase/index.mdx | 4 +- .../docs/{ => en}/knowledgebase/tags.mdx | 0 apps/docs/content/docs/{ => en}/mcp/index.mdx | 0 apps/docs/content/docs/{ => en}/meta.json | 0 .../permissions/roles-and-permissions.mdx | 0 .../content/docs/{ => en}/sdks/python.mdx | 0 .../content/docs/{ => en}/sdks/typescript.mdx | 0 .../content/docs/{ => en}/tools/airtable.mdx | 0 .../content/docs/{ => en}/tools/arxiv.mdx | 0 .../docs/{ => en}/tools/browser_use.mdx | 0 .../docs/content/docs/{ => en}/tools/clay.mdx | 0 .../docs/{ => en}/tools/confluence.mdx | 0 .../content/docs/{ => en}/tools/discord.mdx | 0 .../docs/{ => en}/tools/elevenlabs.mdx | 0 apps/docs/content/docs/{ => en}/tools/exa.mdx | 0 .../docs/content/docs/{ => en}/tools/file.mdx | 0 .../content/docs/{ => en}/tools/firecrawl.mdx | 0 .../docs/{ => en}/tools/generic_webhook.mdx | 0 .../content/docs/{ => en}/tools/github.mdx | 0 .../content/docs/{ => en}/tools/gmail.mdx | 0 .../docs/{ => en}/tools/google_calendar.mdx | 0 .../docs/{ => en}/tools/google_docs.mdx | 0 .../docs/{ => en}/tools/google_drive.mdx | 0 .../docs/{ => en}/tools/google_search.mdx | 0 .../docs/{ => en}/tools/google_sheets.mdx | 0 .../docs/{ => en}/tools/huggingface.mdx | 0 .../content/docs/{ => en}/tools/hunter.mdx | 0 .../docs/{ => en}/tools/image_generator.mdx | 0 .../content/docs/{ => en}/tools/index.mdx | 0 .../docs/content/docs/{ => en}/tools/jina.mdx | 0 .../docs/content/docs/{ => en}/tools/jira.mdx | 0 .../content/docs/{ => en}/tools/knowledge.mdx | 0 .../content/docs/{ => en}/tools/linear.mdx | 0 .../content/docs/{ => en}/tools/linkup.mdx | 0 .../docs/content/docs/{ => en}/tools/mem0.mdx | 0 .../content/docs/{ => en}/tools/memory.mdx | 0 .../docs/{ => en}/tools/microsoft_excel.mdx | 0 .../docs/{ => en}/tools/microsoft_planner.mdx | 0 .../docs/{ => en}/tools/microsoft_teams.mdx | 0 .../docs/{ => en}/tools/mistral_parse.mdx | 0 .../content/docs/{ => en}/tools/mongodb.mdx | 0 .../content/docs/{ => en}/tools/mysql.mdx | 0 .../content/docs/{ => en}/tools/notion.mdx | 0 .../content/docs/{ => en}/tools/onedrive.mdx | 0 .../content/docs/{ => en}/tools/openai.mdx | 0 .../content/docs/{ => en}/tools/outlook.mdx | 0 .../docs/{ => en}/tools/parallel_ai.mdx | 0 .../docs/{ => en}/tools/perplexity.mdx | 0 .../content/docs/{ => en}/tools/pinecone.mdx | 0 .../docs/{ => en}/tools/postgresql.mdx | 0 .../content/docs/{ => en}/tools/qdrant.mdx | 0 .../content/docs/{ => en}/tools/reddit.mdx | 0 apps/docs/content/docs/{ => en}/tools/s3.mdx | 0 .../content/docs/{ => en}/tools/schedule.mdx | 0 .../content/docs/{ => en}/tools/serper.mdx | 0 .../docs/{ => en}/tools/sharepoint.mdx | 0 .../content/docs/{ => en}/tools/slack.mdx | 0 .../content/docs/{ => en}/tools/stagehand.mdx | 0 .../docs/{ => en}/tools/stagehand_agent.mdx | 0 .../content/docs/{ => en}/tools/supabase.mdx | 0 .../content/docs/{ => en}/tools/tavily.mdx | 0 .../content/docs/{ => en}/tools/telegram.mdx | 0 .../content/docs/{ => en}/tools/thinking.mdx | 0 .../content/docs/{ => en}/tools/translate.mdx | 0 .../docs/{ => en}/tools/twilio_sms.mdx | 0 .../content/docs/{ => en}/tools/typeform.mdx | 0 .../content/docs/{ => en}/tools/vision.mdx | 0 .../content/docs/{ => en}/tools/wealthbox.mdx | 0 .../content/docs/{ => en}/tools/webhook.mdx | 0 .../content/docs/{ => en}/tools/whatsapp.mdx | 0 .../content/docs/{ => en}/tools/wikipedia.mdx | 0 apps/docs/content/docs/{ => en}/tools/x.mdx | 0 .../content/docs/{ => en}/tools/youtube.mdx | 0 .../docs/{ => en}/triggers/schedule.mdx | 0 .../docs/{ => en}/triggers/starter.mdx | 0 .../docs/{ => en}/triggers/webhook.mdx | 0 .../variables/environment-variables.mdx | 0 .../{ => en}/variables/workflow-variables.mdx | 0 .../docs/{ => en}/yaml/block-reference.mdx | 0 .../docs/{ => en}/yaml/blocks/agent.mdx | 0 .../content/docs/{ => en}/yaml/blocks/api.mdx | 0 .../docs/{ => en}/yaml/blocks/condition.mdx | 0 .../docs/{ => en}/yaml/blocks/evaluator.mdx | 0 .../docs/{ => en}/yaml/blocks/function.mdx | 0 .../docs/{ => en}/yaml/blocks/index.mdx | 0 .../docs/{ => en}/yaml/blocks/loop.mdx | 0 .../docs/{ => en}/yaml/blocks/parallel.mdx | 0 .../docs/{ => en}/yaml/blocks/response.mdx | 0 .../docs/{ => en}/yaml/blocks/router.mdx | 0 .../docs/{ => en}/yaml/blocks/starter.mdx | 0 .../docs/{ => en}/yaml/blocks/webhook.mdx | 0 .../docs/{ => en}/yaml/blocks/workflow.mdx | 0 .../content/docs/{ => en}/yaml/examples.mdx | 0 .../docs/content/docs/{ => en}/yaml/index.mdx | 0 apps/docs/content/docs/es/blocks/agent.mdx | 306 ++ apps/docs/content/docs/es/blocks/api.mdx | 232 + .../docs/content/docs/es/blocks/condition.mdx | 242 ++ .../docs/content/docs/es/blocks/evaluator.mdx | 199 + apps/docs/content/docs/es/blocks/function.mdx | 156 + apps/docs/content/docs/es/blocks/index.mdx | 129 + apps/docs/content/docs/es/blocks/loop.mdx | 211 + apps/docs/content/docs/es/blocks/parallel.mdx | 231 + apps/docs/content/docs/es/blocks/response.mdx | 246 ++ apps/docs/content/docs/es/blocks/router.mdx | 225 + apps/docs/content/docs/es/blocks/workflow.mdx | 168 + .../content/docs/es/connections/basics.mdx | 43 + .../docs/es/connections/data-structure.mdx | 195 + .../content/docs/es/connections/index.mdx | 42 + .../docs/content/docs/es/connections/tags.mdx | 109 + apps/docs/content/docs/es/copilot/index.mdx | 161 + apps/docs/content/docs/es/execution/api.mdx | 551 +++ .../docs/content/docs/es/execution/basics.mdx | 132 + apps/docs/content/docs/es/execution/costs.mdx | 186 + apps/docs/content/docs/es/execution/index.mdx | 136 + .../content/docs/es/execution/logging.mdx | 150 + .../content/docs/es/getting-started/index.mdx | 193 + apps/docs/content/docs/es/index.mdx | 60 + .../content/docs/es/introduction/index.mdx | 93 + .../content/docs/es/knowledgebase/index.mdx | 113 + .../content/docs/es/knowledgebase/tags.mdx | 108 + apps/docs/content/docs/es/mcp/index.mdx | 140 + .../es/permissions/roles-and-permissions.mdx | 161 + apps/docs/content/docs/es/sdks/python.mdx | 412 ++ apps/docs/content/docs/es/sdks/typescript.mdx | 607 +++ apps/docs/content/docs/es/tools/airtable.mdx | 161 + apps/docs/content/docs/es/tools/arxiv.mdx | 109 + .../content/docs/es/tools/browser_use.mdx | 90 + apps/docs/content/docs/es/tools/clay.mdx | 226 + .../docs/content/docs/es/tools/confluence.mdx | 97 + apps/docs/content/docs/es/tools/discord.mdx | 141 + .../docs/content/docs/es/tools/elevenlabs.mdx | 67 + apps/docs/content/docs/es/tools/exa.mdx | 148 + apps/docs/content/docs/es/tools/file.mdx | 77 + apps/docs/content/docs/es/tools/firecrawl.mdx | 124 + .../content/docs/es/tools/generic_webhook.mdx | 28 + apps/docs/content/docs/es/tools/github.mdx | 130 + apps/docs/content/docs/es/tools/gmail.mdx | 142 + .../content/docs/es/tools/google_calendar.mdx | 204 + .../content/docs/es/tools/google_docs.mdx | 144 + .../content/docs/es/tools/google_drive.mdx | 140 + .../content/docs/es/tools/google_search.mdx | 86 + .../content/docs/es/tools/google_sheets.mdx | 197 + .../content/docs/es/tools/huggingface.mdx | 98 + apps/docs/content/docs/es/tools/hunter.mdx | 206 + .../content/docs/es/tools/image_generator.mdx | 79 + apps/docs/content/docs/es/tools/index.mdx | 71 + apps/docs/content/docs/es/tools/jina.mdx | 92 + apps/docs/content/docs/es/tools/jira.mdx | 140 + apps/docs/content/docs/es/tools/knowledge.mdx | 121 + apps/docs/content/docs/es/tools/linear.mdx | 87 + apps/docs/content/docs/es/tools/linkup.mdx | 72 + apps/docs/content/docs/es/tools/mem0.mdx | 114 + apps/docs/content/docs/es/tools/memory.mdx | 120 + .../content/docs/es/tools/microsoft_excel.mdx | 164 + .../docs/es/tools/microsoft_planner.mdx | 173 + .../content/docs/es/tools/microsoft_teams.mdx | 199 + .../content/docs/es/tools/mistral_parse.mdx | 113 + apps/docs/content/docs/es/tools/mongodb.mdx | 260 ++ apps/docs/content/docs/es/tools/mysql.mdx | 175 + apps/docs/content/docs/es/tools/notion.mdx | 182 + apps/docs/content/docs/es/tools/onedrive.mdx | 120 + apps/docs/content/docs/es/tools/openai.mdx | 72 + apps/docs/content/docs/es/tools/outlook.mdx | 236 ++ .../content/docs/es/tools/parallel_ai.mdx | 101 + .../docs/content/docs/es/tools/perplexity.mdx | 68 + apps/docs/content/docs/es/tools/pinecone.mdx | 161 + .../docs/content/docs/es/tools/postgresql.mdx | 183 + apps/docs/content/docs/es/tools/qdrant.mdx | 178 + apps/docs/content/docs/es/tools/reddit.mdx | 87 + apps/docs/content/docs/es/tools/s3.mdx | 90 + apps/docs/content/docs/es/tools/schedule.mdx | 38 + apps/docs/content/docs/es/tools/serper.mdx | 108 + .../docs/content/docs/es/tools/sharepoint.mdx | 127 + apps/docs/content/docs/es/tools/slack.mdx | 137 + apps/docs/content/docs/es/tools/stagehand.mdx | 219 + .../content/docs/es/tools/stagehand_agent.mdx | 225 + apps/docs/content/docs/es/tools/supabase.mdx | 208 + apps/docs/content/docs/es/tools/tavily.mdx | 104 + apps/docs/content/docs/es/tools/telegram.mdx | 100 + apps/docs/content/docs/es/tools/thinking.mdx | 74 + apps/docs/content/docs/es/tools/translate.mdx | 89 + .../docs/content/docs/es/tools/twilio_sms.mdx | 67 + apps/docs/content/docs/es/tools/typeform.mdx | 119 + apps/docs/content/docs/es/tools/vision.mdx | 78 + apps/docs/content/docs/es/tools/wealthbox.mdx | 160 + apps/docs/content/docs/es/tools/webhook.mdx | 28 + apps/docs/content/docs/es/tools/whatsapp.mdx | 68 + apps/docs/content/docs/es/tools/wikipedia.mdx | 127 + apps/docs/content/docs/es/tools/x.mdx | 117 + apps/docs/content/docs/es/tools/youtube.mdx | 67 + .../content/docs/es/triggers/schedule.mdx | 73 + .../docs/content/docs/es/triggers/starter.mdx | 63 + .../docs/content/docs/es/triggers/webhook.mdx | 126 + .../es/variables/environment-variables.mdx | 96 + .../docs/es/variables/workflow-variables.mdx | 141 + .../content/docs/es/yaml/block-reference.mdx | 242 ++ .../content/docs/es/yaml/blocks/agent.mdx | 218 + apps/docs/content/docs/es/yaml/blocks/api.mdx | 429 ++ .../content/docs/es/yaml/blocks/condition.mdx | 165 + .../content/docs/es/yaml/blocks/evaluator.mdx | 255 ++ .../content/docs/es/yaml/blocks/function.mdx | 162 + .../content/docs/es/yaml/blocks/index.mdx | 151 + .../docs/content/docs/es/yaml/blocks/loop.mdx | 295 ++ .../content/docs/es/yaml/blocks/parallel.mdx | 312 ++ .../content/docs/es/yaml/blocks/response.mdx | 239 ++ .../content/docs/es/yaml/blocks/router.mdx | 200 + .../content/docs/es/yaml/blocks/starter.mdx | 183 + .../content/docs/es/yaml/blocks/webhook.mdx | 403 ++ .../content/docs/es/yaml/blocks/workflow.mdx | 299 ++ apps/docs/content/docs/es/yaml/examples.mdx | 273 ++ apps/docs/content/docs/es/yaml/index.mdx | 159 + apps/docs/content/docs/execution/meta.json | 5 - apps/docs/content/docs/fr/blocks/agent.mdx | 306 ++ apps/docs/content/docs/fr/blocks/api.mdx | 232 + .../docs/content/docs/fr/blocks/condition.mdx | 242 ++ .../docs/content/docs/fr/blocks/evaluator.mdx | 199 + apps/docs/content/docs/fr/blocks/function.mdx | 156 + apps/docs/content/docs/fr/blocks/index.mdx | 129 + apps/docs/content/docs/fr/blocks/loop.mdx | 211 + apps/docs/content/docs/fr/blocks/parallel.mdx | 231 + apps/docs/content/docs/fr/blocks/response.mdx | 246 ++ apps/docs/content/docs/fr/blocks/router.mdx | 225 + apps/docs/content/docs/fr/blocks/workflow.mdx | 168 + .../content/docs/fr/connections/basics.mdx | 43 + .../docs/fr/connections/data-structure.mdx | 195 + .../content/docs/fr/connections/index.mdx | 42 + .../docs/content/docs/fr/connections/tags.mdx | 109 + apps/docs/content/docs/fr/copilot/index.mdx | 161 + apps/docs/content/docs/fr/execution/api.mdx | 551 +++ .../docs/content/docs/fr/execution/basics.mdx | 132 + apps/docs/content/docs/fr/execution/costs.mdx | 186 + apps/docs/content/docs/fr/execution/index.mdx | 136 + .../content/docs/fr/execution/logging.mdx | 150 + .../content/docs/fr/getting-started/index.mdx | 193 + apps/docs/content/docs/fr/index.mdx | 60 + .../content/docs/fr/introduction/index.mdx | 85 + .../content/docs/fr/knowledgebase/index.mdx | 113 + .../content/docs/fr/knowledgebase/tags.mdx | 108 + apps/docs/content/docs/fr/mcp/index.mdx | 140 + .../fr/permissions/roles-and-permissions.mdx | 161 + apps/docs/content/docs/fr/sdks/python.mdx | 412 ++ apps/docs/content/docs/fr/sdks/typescript.mdx | 607 +++ apps/docs/content/docs/fr/tools/airtable.mdx | 161 + apps/docs/content/docs/fr/tools/arxiv.mdx | 109 + .../content/docs/fr/tools/browser_use.mdx | 90 + apps/docs/content/docs/fr/tools/clay.mdx | 226 + .../docs/content/docs/fr/tools/confluence.mdx | 97 + apps/docs/content/docs/fr/tools/discord.mdx | 141 + .../docs/content/docs/fr/tools/elevenlabs.mdx | 67 + apps/docs/content/docs/fr/tools/exa.mdx | 148 + apps/docs/content/docs/fr/tools/file.mdx | 77 + apps/docs/content/docs/fr/tools/firecrawl.mdx | 124 + .../content/docs/fr/tools/generic_webhook.mdx | 28 + apps/docs/content/docs/fr/tools/github.mdx | 131 + apps/docs/content/docs/fr/tools/gmail.mdx | 143 + .../content/docs/fr/tools/google_calendar.mdx | 204 + .../content/docs/fr/tools/google_docs.mdx | 144 + .../content/docs/fr/tools/google_drive.mdx | 140 + .../content/docs/fr/tools/google_search.mdx | 86 + .../content/docs/fr/tools/google_sheets.mdx | 197 + .../content/docs/fr/tools/huggingface.mdx | 98 + apps/docs/content/docs/fr/tools/hunter.mdx | 206 + .../content/docs/fr/tools/image_generator.mdx | 79 + apps/docs/content/docs/fr/tools/index.mdx | 71 + apps/docs/content/docs/fr/tools/jina.mdx | 92 + apps/docs/content/docs/fr/tools/jira.mdx | 140 + apps/docs/content/docs/fr/tools/knowledge.mdx | 121 + apps/docs/content/docs/fr/tools/linear.mdx | 87 + apps/docs/content/docs/fr/tools/linkup.mdx | 72 + apps/docs/content/docs/fr/tools/mem0.mdx | 114 + apps/docs/content/docs/fr/tools/memory.mdx | 120 + .../content/docs/fr/tools/microsoft_excel.mdx | 164 + .../docs/fr/tools/microsoft_planner.mdx | 173 + .../content/docs/fr/tools/microsoft_teams.mdx | 199 + .../content/docs/fr/tools/mistral_parse.mdx | 113 + apps/docs/content/docs/fr/tools/mongodb.mdx | 260 ++ apps/docs/content/docs/fr/tools/mysql.mdx | 175 + apps/docs/content/docs/fr/tools/notion.mdx | 182 + apps/docs/content/docs/fr/tools/onedrive.mdx | 120 + apps/docs/content/docs/fr/tools/openai.mdx | 72 + apps/docs/content/docs/fr/tools/outlook.mdx | 236 ++ .../content/docs/fr/tools/parallel_ai.mdx | 101 + .../docs/content/docs/fr/tools/perplexity.mdx | 68 + apps/docs/content/docs/fr/tools/pinecone.mdx | 161 + .../docs/content/docs/fr/tools/postgresql.mdx | 183 + apps/docs/content/docs/fr/tools/qdrant.mdx | 178 + apps/docs/content/docs/fr/tools/reddit.mdx | 87 + apps/docs/content/docs/fr/tools/s3.mdx | 90 + apps/docs/content/docs/fr/tools/schedule.mdx | 38 + apps/docs/content/docs/fr/tools/serper.mdx | 108 + .../docs/content/docs/fr/tools/sharepoint.mdx | 127 + apps/docs/content/docs/fr/tools/slack.mdx | 138 + apps/docs/content/docs/fr/tools/stagehand.mdx | 219 + .../content/docs/fr/tools/stagehand_agent.mdx | 225 + apps/docs/content/docs/fr/tools/supabase.mdx | 208 + apps/docs/content/docs/fr/tools/tavily.mdx | 104 + apps/docs/content/docs/fr/tools/telegram.mdx | 100 + apps/docs/content/docs/fr/tools/thinking.mdx | 74 + apps/docs/content/docs/fr/tools/translate.mdx | 89 + .../docs/content/docs/fr/tools/twilio_sms.mdx | 67 + apps/docs/content/docs/fr/tools/typeform.mdx | 119 + apps/docs/content/docs/fr/tools/vision.mdx | 78 + apps/docs/content/docs/fr/tools/wealthbox.mdx | 160 + apps/docs/content/docs/fr/tools/webhook.mdx | 28 + apps/docs/content/docs/fr/tools/whatsapp.mdx | 68 + apps/docs/content/docs/fr/tools/wikipedia.mdx | 127 + apps/docs/content/docs/fr/tools/x.mdx | 117 + apps/docs/content/docs/fr/tools/youtube.mdx | 67 + .../content/docs/fr/triggers/schedule.mdx | 73 + .../docs/content/docs/fr/triggers/starter.mdx | 63 + .../docs/content/docs/fr/triggers/webhook.mdx | 126 + .../fr/variables/environment-variables.mdx | 96 + .../docs/fr/variables/workflow-variables.mdx | 138 + .../content/docs/fr/yaml/block-reference.mdx | 242 ++ .../content/docs/fr/yaml/blocks/agent.mdx | 218 + apps/docs/content/docs/fr/yaml/blocks/api.mdx | 429 ++ .../content/docs/fr/yaml/blocks/condition.mdx | 165 + .../content/docs/fr/yaml/blocks/evaluator.mdx | 255 ++ .../content/docs/fr/yaml/blocks/function.mdx | 162 + .../content/docs/fr/yaml/blocks/index.mdx | 151 + .../docs/content/docs/fr/yaml/blocks/loop.mdx | 295 ++ .../content/docs/fr/yaml/blocks/parallel.mdx | 312 ++ .../content/docs/fr/yaml/blocks/response.mdx | 239 ++ .../content/docs/fr/yaml/blocks/router.mdx | 200 + .../content/docs/fr/yaml/blocks/starter.mdx | 183 + .../content/docs/fr/yaml/blocks/webhook.mdx | 403 ++ .../content/docs/fr/yaml/blocks/workflow.mdx | 299 ++ apps/docs/content/docs/fr/yaml/examples.mdx | 273 ++ apps/docs/content/docs/fr/yaml/index.mdx | 159 + apps/docs/content/docs/introduction/meta.json | 4 - .../docs/content/docs/knowledgebase/meta.json | 5 - apps/docs/content/docs/mcp/meta.json | 5 - apps/docs/content/docs/permissions/meta.json | 5 - apps/docs/content/docs/sdks/meta.json | 4 - apps/docs/content/docs/tools/meta.json | 71 - apps/docs/content/docs/triggers/meta.json | 5 - apps/docs/content/docs/variables/meta.json | 4 - apps/docs/content/docs/yaml/blocks/meta.json | 17 - apps/docs/content/docs/yaml/meta.json | 5 - apps/docs/content/docs/zh/blocks/agent.mdx | 303 ++ apps/docs/content/docs/zh/blocks/api.mdx | 232 + .../docs/content/docs/zh/blocks/condition.mdx | 241 ++ .../docs/content/docs/zh/blocks/evaluator.mdx | 199 + apps/docs/content/docs/zh/blocks/function.mdx | 156 + apps/docs/content/docs/zh/blocks/index.mdx | 129 + apps/docs/content/docs/zh/blocks/loop.mdx | 211 + apps/docs/content/docs/zh/blocks/parallel.mdx | 231 + apps/docs/content/docs/zh/blocks/response.mdx | 246 ++ apps/docs/content/docs/zh/blocks/router.mdx | 225 + apps/docs/content/docs/zh/blocks/workflow.mdx | 168 + .../content/docs/zh/connections/basics.mdx | 41 + .../docs/zh/connections/data-structure.mdx | 193 + .../content/docs/zh/connections/index.mdx | 41 + .../docs/content/docs/zh/connections/tags.mdx | 106 + apps/docs/content/docs/zh/copilot/index.mdx | 161 + apps/docs/content/docs/zh/execution/api.mdx | 551 +++ .../docs/content/docs/zh/execution/basics.mdx | 132 + apps/docs/content/docs/zh/execution/costs.mdx | 186 + apps/docs/content/docs/zh/execution/index.mdx | 135 + .../content/docs/zh/execution/logging.mdx | 150 + .../content/docs/zh/getting-started/index.mdx | 193 + apps/docs/content/docs/zh/index.mdx | 60 + .../content/docs/zh/introduction/index.mdx | 85 + .../content/docs/zh/knowledgebase/index.mdx | 113 + .../content/docs/zh/knowledgebase/tags.mdx | 108 + apps/docs/content/docs/zh/mcp/index.mdx | 140 + .../zh/permissions/roles-and-permissions.mdx | 161 + apps/docs/content/docs/zh/sdks/python.mdx | 412 ++ apps/docs/content/docs/zh/sdks/typescript.mdx | 607 +++ apps/docs/content/docs/zh/tools/airtable.mdx | 161 + apps/docs/content/docs/zh/tools/arxiv.mdx | 109 + .../content/docs/zh/tools/browser_use.mdx | 89 + apps/docs/content/docs/zh/tools/clay.mdx | 226 + .../docs/content/docs/zh/tools/confluence.mdx | 96 + apps/docs/content/docs/zh/tools/discord.mdx | 141 + .../docs/content/docs/zh/tools/elevenlabs.mdx | 67 + apps/docs/content/docs/zh/tools/exa.mdx | 148 + apps/docs/content/docs/zh/tools/file.mdx | 77 + apps/docs/content/docs/zh/tools/firecrawl.mdx | 124 + .../content/docs/zh/tools/generic_webhook.mdx | 28 + apps/docs/content/docs/zh/tools/github.mdx | 130 + apps/docs/content/docs/zh/tools/gmail.mdx | 142 + .../content/docs/zh/tools/google_calendar.mdx | 204 + .../content/docs/zh/tools/google_docs.mdx | 144 + .../content/docs/zh/tools/google_drive.mdx | 140 + .../content/docs/zh/tools/google_search.mdx | 86 + .../content/docs/zh/tools/google_sheets.mdx | 197 + .../content/docs/zh/tools/huggingface.mdx | 98 + apps/docs/content/docs/zh/tools/hunter.mdx | 206 + .../content/docs/zh/tools/image_generator.mdx | 79 + apps/docs/content/docs/zh/tools/index.mdx | 71 + apps/docs/content/docs/zh/tools/jina.mdx | 92 + apps/docs/content/docs/zh/tools/jira.mdx | 140 + apps/docs/content/docs/zh/tools/knowledge.mdx | 121 + apps/docs/content/docs/zh/tools/linear.mdx | 87 + apps/docs/content/docs/zh/tools/linkup.mdx | 72 + apps/docs/content/docs/zh/tools/mem0.mdx | 114 + apps/docs/content/docs/zh/tools/memory.mdx | 120 + .../content/docs/zh/tools/microsoft_excel.mdx | 164 + .../docs/zh/tools/microsoft_planner.mdx | 173 + .../content/docs/zh/tools/microsoft_teams.mdx | 199 + .../content/docs/zh/tools/mistral_parse.mdx | 113 + apps/docs/content/docs/zh/tools/mongodb.mdx | 260 ++ apps/docs/content/docs/zh/tools/mysql.mdx | 175 + apps/docs/content/docs/zh/tools/notion.mdx | 182 + apps/docs/content/docs/zh/tools/onedrive.mdx | 120 + apps/docs/content/docs/zh/tools/openai.mdx | 71 + apps/docs/content/docs/zh/tools/outlook.mdx | 236 ++ .../content/docs/zh/tools/parallel_ai.mdx | 101 + .../docs/content/docs/zh/tools/perplexity.mdx | 68 + apps/docs/content/docs/zh/tools/pinecone.mdx | 161 + .../docs/content/docs/zh/tools/postgresql.mdx | 183 + apps/docs/content/docs/zh/tools/qdrant.mdx | 178 + apps/docs/content/docs/zh/tools/reddit.mdx | 87 + apps/docs/content/docs/zh/tools/s3.mdx | 90 + apps/docs/content/docs/zh/tools/schedule.mdx | 38 + apps/docs/content/docs/zh/tools/serper.mdx | 108 + .../docs/content/docs/zh/tools/sharepoint.mdx | 127 + apps/docs/content/docs/zh/tools/slack.mdx | 136 + apps/docs/content/docs/zh/tools/stagehand.mdx | 219 + .../content/docs/zh/tools/stagehand_agent.mdx | 225 + apps/docs/content/docs/zh/tools/supabase.mdx | 208 + apps/docs/content/docs/zh/tools/tavily.mdx | 104 + apps/docs/content/docs/zh/tools/telegram.mdx | 99 + apps/docs/content/docs/zh/tools/thinking.mdx | 74 + apps/docs/content/docs/zh/tools/translate.mdx | 89 + .../docs/content/docs/zh/tools/twilio_sms.mdx | 67 + apps/docs/content/docs/zh/tools/typeform.mdx | 119 + apps/docs/content/docs/zh/tools/vision.mdx | 78 + apps/docs/content/docs/zh/tools/wealthbox.mdx | 160 + apps/docs/content/docs/zh/tools/webhook.mdx | 28 + apps/docs/content/docs/zh/tools/whatsapp.mdx | 68 + apps/docs/content/docs/zh/tools/wikipedia.mdx | 127 + apps/docs/content/docs/zh/tools/x.mdx | 117 + apps/docs/content/docs/zh/tools/youtube.mdx | 67 + .../content/docs/zh/triggers/schedule.mdx | 73 + .../docs/content/docs/zh/triggers/starter.mdx | 63 + .../docs/content/docs/zh/triggers/webhook.mdx | 126 + .../zh/variables/environment-variables.mdx | 96 + .../docs/zh/variables/workflow-variables.mdx | 137 + .../content/docs/zh/yaml/block-reference.mdx | 242 ++ .../content/docs/zh/yaml/blocks/agent.mdx | 218 + apps/docs/content/docs/zh/yaml/blocks/api.mdx | 429 ++ .../content/docs/zh/yaml/blocks/condition.mdx | 165 + .../content/docs/zh/yaml/blocks/evaluator.mdx | 255 ++ .../content/docs/zh/yaml/blocks/function.mdx | 162 + .../content/docs/zh/yaml/blocks/index.mdx | 151 + .../docs/content/docs/zh/yaml/blocks/loop.mdx | 295 ++ .../content/docs/zh/yaml/blocks/parallel.mdx | 312 ++ .../content/docs/zh/yaml/blocks/response.mdx | 239 ++ .../content/docs/zh/yaml/blocks/router.mdx | 200 + .../content/docs/zh/yaml/blocks/starter.mdx | 183 + .../content/docs/zh/yaml/blocks/webhook.mdx | 403 ++ .../content/docs/zh/yaml/blocks/workflow.mdx | 299 ++ apps/docs/content/docs/zh/yaml/examples.mdx | 273 ++ apps/docs/content/docs/zh/yaml/index.mdx | 159 + apps/docs/i18n.json | 17 + apps/docs/i18n.lock | 3746 +++++++++++++++++ apps/docs/lib/i18n.ts | 8 + apps/docs/lib/source.ts | 4 +- apps/docs/middleware.ts | 8 + apps/docs/public/llms.txt | 53 + .../{ => knowledgebase}/knowledgebase-2.png | Bin .../{ => knowledgebase}/knowledgebase.png | Bin 505 files changed, 60966 insertions(+), 310 deletions(-) create mode 100644 .github/workflows/i18n.yml delete mode 100644 apps/docs/app/(docs)/[[...slug]]/layout.tsx delete mode 100644 apps/docs/app/(docs)/[[...slug]]/page.tsx delete mode 100644 apps/docs/app/(docs)/layout.tsx create mode 100644 apps/docs/app/[lang]/[[...slug]]/page.tsx create mode 100644 apps/docs/app/[lang]/layout.tsx create mode 100644 apps/docs/app/robots.txt/route.ts create mode 100644 apps/docs/app/sitemap.xml/route.ts create mode 100644 apps/docs/components/structured-data.tsx create mode 100644 apps/docs/components/ui/language-dropdown.tsx delete mode 100644 apps/docs/content/docs/blocks/meta.json delete mode 100644 apps/docs/content/docs/connections/meta.json delete mode 100644 apps/docs/content/docs/copilot/meta.json rename apps/docs/content/docs/{ => en}/blocks/agent.mdx (100%) rename apps/docs/content/docs/{ => en}/blocks/api.mdx (100%) rename apps/docs/content/docs/{ => en}/blocks/condition.mdx (100%) rename apps/docs/content/docs/{ => en}/blocks/evaluator.mdx (100%) rename apps/docs/content/docs/{ => en}/blocks/function.mdx (100%) rename apps/docs/content/docs/{ => en}/blocks/index.mdx (100%) rename apps/docs/content/docs/{ => en}/blocks/loop.mdx (100%) rename apps/docs/content/docs/{ => en}/blocks/parallel.mdx (100%) rename apps/docs/content/docs/{ => en}/blocks/response.mdx (100%) rename apps/docs/content/docs/{ => en}/blocks/router.mdx (100%) rename apps/docs/content/docs/{ => en}/blocks/workflow.mdx (100%) rename apps/docs/content/docs/{ => en}/connections/basics.mdx (100%) rename apps/docs/content/docs/{ => en}/connections/data-structure.mdx (100%) rename apps/docs/content/docs/{ => en}/connections/index.mdx (100%) rename apps/docs/content/docs/{ => en}/connections/tags.mdx (100%) rename apps/docs/content/docs/{ => en}/copilot/index.mdx (100%) rename apps/docs/content/docs/{ => en}/execution/api.mdx (100%) rename apps/docs/content/docs/{ => en}/execution/basics.mdx (100%) rename apps/docs/content/docs/{ => en}/execution/costs.mdx (100%) rename apps/docs/content/docs/{ => en}/execution/index.mdx (100%) rename apps/docs/content/docs/{ => en}/execution/logging.mdx (100%) rename apps/docs/content/docs/{ => en}/getting-started/index.mdx (100%) create mode 100644 apps/docs/content/docs/en/index.mdx rename apps/docs/content/docs/{ => en}/introduction/index.mdx (92%) rename apps/docs/content/docs/{ => en}/knowledgebase/index.mdx (95%) rename apps/docs/content/docs/{ => en}/knowledgebase/tags.mdx (100%) rename apps/docs/content/docs/{ => en}/mcp/index.mdx (100%) rename apps/docs/content/docs/{ => en}/meta.json (100%) rename apps/docs/content/docs/{ => en}/permissions/roles-and-permissions.mdx (100%) rename apps/docs/content/docs/{ => en}/sdks/python.mdx (100%) rename apps/docs/content/docs/{ => en}/sdks/typescript.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/airtable.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/arxiv.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/browser_use.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/clay.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/confluence.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/discord.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/elevenlabs.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/exa.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/file.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/firecrawl.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/generic_webhook.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/github.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/gmail.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/google_calendar.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/google_docs.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/google_drive.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/google_search.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/google_sheets.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/huggingface.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/hunter.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/image_generator.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/index.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/jina.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/jira.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/knowledge.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/linear.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/linkup.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/mem0.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/memory.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/microsoft_excel.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/microsoft_planner.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/microsoft_teams.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/mistral_parse.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/mongodb.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/mysql.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/notion.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/onedrive.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/openai.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/outlook.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/parallel_ai.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/perplexity.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/pinecone.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/postgresql.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/qdrant.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/reddit.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/s3.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/schedule.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/serper.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/sharepoint.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/slack.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/stagehand.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/stagehand_agent.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/supabase.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/tavily.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/telegram.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/thinking.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/translate.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/twilio_sms.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/typeform.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/vision.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/wealthbox.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/webhook.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/whatsapp.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/wikipedia.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/x.mdx (100%) rename apps/docs/content/docs/{ => en}/tools/youtube.mdx (100%) rename apps/docs/content/docs/{ => en}/triggers/schedule.mdx (100%) rename apps/docs/content/docs/{ => en}/triggers/starter.mdx (100%) rename apps/docs/content/docs/{ => en}/triggers/webhook.mdx (100%) rename apps/docs/content/docs/{ => en}/variables/environment-variables.mdx (100%) rename apps/docs/content/docs/{ => en}/variables/workflow-variables.mdx (100%) rename apps/docs/content/docs/{ => en}/yaml/block-reference.mdx (100%) rename apps/docs/content/docs/{ => en}/yaml/blocks/agent.mdx (100%) rename apps/docs/content/docs/{ => en}/yaml/blocks/api.mdx (100%) rename apps/docs/content/docs/{ => en}/yaml/blocks/condition.mdx (100%) rename apps/docs/content/docs/{ => en}/yaml/blocks/evaluator.mdx (100%) rename apps/docs/content/docs/{ => en}/yaml/blocks/function.mdx (100%) rename apps/docs/content/docs/{ => en}/yaml/blocks/index.mdx (100%) rename apps/docs/content/docs/{ => en}/yaml/blocks/loop.mdx (100%) rename apps/docs/content/docs/{ => en}/yaml/blocks/parallel.mdx (100%) rename apps/docs/content/docs/{ => en}/yaml/blocks/response.mdx (100%) rename apps/docs/content/docs/{ => en}/yaml/blocks/router.mdx (100%) rename apps/docs/content/docs/{ => en}/yaml/blocks/starter.mdx (100%) rename apps/docs/content/docs/{ => en}/yaml/blocks/webhook.mdx (100%) rename apps/docs/content/docs/{ => en}/yaml/blocks/workflow.mdx (100%) rename apps/docs/content/docs/{ => en}/yaml/examples.mdx (100%) rename apps/docs/content/docs/{ => en}/yaml/index.mdx (100%) create mode 100644 apps/docs/content/docs/es/blocks/agent.mdx create mode 100644 apps/docs/content/docs/es/blocks/api.mdx create mode 100644 apps/docs/content/docs/es/blocks/condition.mdx create mode 100644 apps/docs/content/docs/es/blocks/evaluator.mdx create mode 100644 apps/docs/content/docs/es/blocks/function.mdx create mode 100644 apps/docs/content/docs/es/blocks/index.mdx create mode 100644 apps/docs/content/docs/es/blocks/loop.mdx create mode 100644 apps/docs/content/docs/es/blocks/parallel.mdx create mode 100644 apps/docs/content/docs/es/blocks/response.mdx create mode 100644 apps/docs/content/docs/es/blocks/router.mdx create mode 100644 apps/docs/content/docs/es/blocks/workflow.mdx create mode 100644 apps/docs/content/docs/es/connections/basics.mdx create mode 100644 apps/docs/content/docs/es/connections/data-structure.mdx create mode 100644 apps/docs/content/docs/es/connections/index.mdx create mode 100644 apps/docs/content/docs/es/connections/tags.mdx create mode 100644 apps/docs/content/docs/es/copilot/index.mdx create mode 100644 apps/docs/content/docs/es/execution/api.mdx create mode 100644 apps/docs/content/docs/es/execution/basics.mdx create mode 100644 apps/docs/content/docs/es/execution/costs.mdx create mode 100644 apps/docs/content/docs/es/execution/index.mdx create mode 100644 apps/docs/content/docs/es/execution/logging.mdx create mode 100644 apps/docs/content/docs/es/getting-started/index.mdx create mode 100644 apps/docs/content/docs/es/index.mdx create mode 100644 apps/docs/content/docs/es/introduction/index.mdx create mode 100644 apps/docs/content/docs/es/knowledgebase/index.mdx create mode 100644 apps/docs/content/docs/es/knowledgebase/tags.mdx create mode 100644 apps/docs/content/docs/es/mcp/index.mdx create mode 100644 apps/docs/content/docs/es/permissions/roles-and-permissions.mdx create mode 100644 apps/docs/content/docs/es/sdks/python.mdx create mode 100644 apps/docs/content/docs/es/sdks/typescript.mdx create mode 100644 apps/docs/content/docs/es/tools/airtable.mdx create mode 100644 apps/docs/content/docs/es/tools/arxiv.mdx create mode 100644 apps/docs/content/docs/es/tools/browser_use.mdx create mode 100644 apps/docs/content/docs/es/tools/clay.mdx create mode 100644 apps/docs/content/docs/es/tools/confluence.mdx create mode 100644 apps/docs/content/docs/es/tools/discord.mdx create mode 100644 apps/docs/content/docs/es/tools/elevenlabs.mdx create mode 100644 apps/docs/content/docs/es/tools/exa.mdx create mode 100644 apps/docs/content/docs/es/tools/file.mdx create mode 100644 apps/docs/content/docs/es/tools/firecrawl.mdx create mode 100644 apps/docs/content/docs/es/tools/generic_webhook.mdx create mode 100644 apps/docs/content/docs/es/tools/github.mdx create mode 100644 apps/docs/content/docs/es/tools/gmail.mdx create mode 100644 apps/docs/content/docs/es/tools/google_calendar.mdx create mode 100644 apps/docs/content/docs/es/tools/google_docs.mdx create mode 100644 apps/docs/content/docs/es/tools/google_drive.mdx create mode 100644 apps/docs/content/docs/es/tools/google_search.mdx create mode 100644 apps/docs/content/docs/es/tools/google_sheets.mdx create mode 100644 apps/docs/content/docs/es/tools/huggingface.mdx create mode 100644 apps/docs/content/docs/es/tools/hunter.mdx create mode 100644 apps/docs/content/docs/es/tools/image_generator.mdx create mode 100644 apps/docs/content/docs/es/tools/index.mdx create mode 100644 apps/docs/content/docs/es/tools/jina.mdx create mode 100644 apps/docs/content/docs/es/tools/jira.mdx create mode 100644 apps/docs/content/docs/es/tools/knowledge.mdx create mode 100644 apps/docs/content/docs/es/tools/linear.mdx create mode 100644 apps/docs/content/docs/es/tools/linkup.mdx create mode 100644 apps/docs/content/docs/es/tools/mem0.mdx create mode 100644 apps/docs/content/docs/es/tools/memory.mdx create mode 100644 apps/docs/content/docs/es/tools/microsoft_excel.mdx create mode 100644 apps/docs/content/docs/es/tools/microsoft_planner.mdx create mode 100644 apps/docs/content/docs/es/tools/microsoft_teams.mdx create mode 100644 apps/docs/content/docs/es/tools/mistral_parse.mdx create mode 100644 apps/docs/content/docs/es/tools/mongodb.mdx create mode 100644 apps/docs/content/docs/es/tools/mysql.mdx create mode 100644 apps/docs/content/docs/es/tools/notion.mdx create mode 100644 apps/docs/content/docs/es/tools/onedrive.mdx create mode 100644 apps/docs/content/docs/es/tools/openai.mdx create mode 100644 apps/docs/content/docs/es/tools/outlook.mdx create mode 100644 apps/docs/content/docs/es/tools/parallel_ai.mdx create mode 100644 apps/docs/content/docs/es/tools/perplexity.mdx create mode 100644 apps/docs/content/docs/es/tools/pinecone.mdx create mode 100644 apps/docs/content/docs/es/tools/postgresql.mdx create mode 100644 apps/docs/content/docs/es/tools/qdrant.mdx create mode 100644 apps/docs/content/docs/es/tools/reddit.mdx create mode 100644 apps/docs/content/docs/es/tools/s3.mdx create mode 100644 apps/docs/content/docs/es/tools/schedule.mdx create mode 100644 apps/docs/content/docs/es/tools/serper.mdx create mode 100644 apps/docs/content/docs/es/tools/sharepoint.mdx create mode 100644 apps/docs/content/docs/es/tools/slack.mdx create mode 100644 apps/docs/content/docs/es/tools/stagehand.mdx create mode 100644 apps/docs/content/docs/es/tools/stagehand_agent.mdx create mode 100644 apps/docs/content/docs/es/tools/supabase.mdx create mode 100644 apps/docs/content/docs/es/tools/tavily.mdx create mode 100644 apps/docs/content/docs/es/tools/telegram.mdx create mode 100644 apps/docs/content/docs/es/tools/thinking.mdx create mode 100644 apps/docs/content/docs/es/tools/translate.mdx create mode 100644 apps/docs/content/docs/es/tools/twilio_sms.mdx create mode 100644 apps/docs/content/docs/es/tools/typeform.mdx create mode 100644 apps/docs/content/docs/es/tools/vision.mdx create mode 100644 apps/docs/content/docs/es/tools/wealthbox.mdx create mode 100644 apps/docs/content/docs/es/tools/webhook.mdx create mode 100644 apps/docs/content/docs/es/tools/whatsapp.mdx create mode 100644 apps/docs/content/docs/es/tools/wikipedia.mdx create mode 100644 apps/docs/content/docs/es/tools/x.mdx create mode 100644 apps/docs/content/docs/es/tools/youtube.mdx create mode 100644 apps/docs/content/docs/es/triggers/schedule.mdx create mode 100644 apps/docs/content/docs/es/triggers/starter.mdx create mode 100644 apps/docs/content/docs/es/triggers/webhook.mdx create mode 100644 apps/docs/content/docs/es/variables/environment-variables.mdx create mode 100644 apps/docs/content/docs/es/variables/workflow-variables.mdx create mode 100644 apps/docs/content/docs/es/yaml/block-reference.mdx create mode 100644 apps/docs/content/docs/es/yaml/blocks/agent.mdx create mode 100644 apps/docs/content/docs/es/yaml/blocks/api.mdx create mode 100644 apps/docs/content/docs/es/yaml/blocks/condition.mdx create mode 100644 apps/docs/content/docs/es/yaml/blocks/evaluator.mdx create mode 100644 apps/docs/content/docs/es/yaml/blocks/function.mdx create mode 100644 apps/docs/content/docs/es/yaml/blocks/index.mdx create mode 100644 apps/docs/content/docs/es/yaml/blocks/loop.mdx create mode 100644 apps/docs/content/docs/es/yaml/blocks/parallel.mdx create mode 100644 apps/docs/content/docs/es/yaml/blocks/response.mdx create mode 100644 apps/docs/content/docs/es/yaml/blocks/router.mdx create mode 100644 apps/docs/content/docs/es/yaml/blocks/starter.mdx create mode 100644 apps/docs/content/docs/es/yaml/blocks/webhook.mdx create mode 100644 apps/docs/content/docs/es/yaml/blocks/workflow.mdx create mode 100644 apps/docs/content/docs/es/yaml/examples.mdx create mode 100644 apps/docs/content/docs/es/yaml/index.mdx delete mode 100644 apps/docs/content/docs/execution/meta.json create mode 100644 apps/docs/content/docs/fr/blocks/agent.mdx create mode 100644 apps/docs/content/docs/fr/blocks/api.mdx create mode 100644 apps/docs/content/docs/fr/blocks/condition.mdx create mode 100644 apps/docs/content/docs/fr/blocks/evaluator.mdx create mode 100644 apps/docs/content/docs/fr/blocks/function.mdx create mode 100644 apps/docs/content/docs/fr/blocks/index.mdx create mode 100644 apps/docs/content/docs/fr/blocks/loop.mdx create mode 100644 apps/docs/content/docs/fr/blocks/parallel.mdx create mode 100644 apps/docs/content/docs/fr/blocks/response.mdx create mode 100644 apps/docs/content/docs/fr/blocks/router.mdx create mode 100644 apps/docs/content/docs/fr/blocks/workflow.mdx create mode 100644 apps/docs/content/docs/fr/connections/basics.mdx create mode 100644 apps/docs/content/docs/fr/connections/data-structure.mdx create mode 100644 apps/docs/content/docs/fr/connections/index.mdx create mode 100644 apps/docs/content/docs/fr/connections/tags.mdx create mode 100644 apps/docs/content/docs/fr/copilot/index.mdx create mode 100644 apps/docs/content/docs/fr/execution/api.mdx create mode 100644 apps/docs/content/docs/fr/execution/basics.mdx create mode 100644 apps/docs/content/docs/fr/execution/costs.mdx create mode 100644 apps/docs/content/docs/fr/execution/index.mdx create mode 100644 apps/docs/content/docs/fr/execution/logging.mdx create mode 100644 apps/docs/content/docs/fr/getting-started/index.mdx create mode 100644 apps/docs/content/docs/fr/index.mdx create mode 100644 apps/docs/content/docs/fr/introduction/index.mdx create mode 100644 apps/docs/content/docs/fr/knowledgebase/index.mdx create mode 100644 apps/docs/content/docs/fr/knowledgebase/tags.mdx create mode 100644 apps/docs/content/docs/fr/mcp/index.mdx create mode 100644 apps/docs/content/docs/fr/permissions/roles-and-permissions.mdx create mode 100644 apps/docs/content/docs/fr/sdks/python.mdx create mode 100644 apps/docs/content/docs/fr/sdks/typescript.mdx create mode 100644 apps/docs/content/docs/fr/tools/airtable.mdx create mode 100644 apps/docs/content/docs/fr/tools/arxiv.mdx create mode 100644 apps/docs/content/docs/fr/tools/browser_use.mdx create mode 100644 apps/docs/content/docs/fr/tools/clay.mdx create mode 100644 apps/docs/content/docs/fr/tools/confluence.mdx create mode 100644 apps/docs/content/docs/fr/tools/discord.mdx create mode 100644 apps/docs/content/docs/fr/tools/elevenlabs.mdx create mode 100644 apps/docs/content/docs/fr/tools/exa.mdx create mode 100644 apps/docs/content/docs/fr/tools/file.mdx create mode 100644 apps/docs/content/docs/fr/tools/firecrawl.mdx create mode 100644 apps/docs/content/docs/fr/tools/generic_webhook.mdx create mode 100644 apps/docs/content/docs/fr/tools/github.mdx create mode 100644 apps/docs/content/docs/fr/tools/gmail.mdx create mode 100644 apps/docs/content/docs/fr/tools/google_calendar.mdx create mode 100644 apps/docs/content/docs/fr/tools/google_docs.mdx create mode 100644 apps/docs/content/docs/fr/tools/google_drive.mdx create mode 100644 apps/docs/content/docs/fr/tools/google_search.mdx create mode 100644 apps/docs/content/docs/fr/tools/google_sheets.mdx create mode 100644 apps/docs/content/docs/fr/tools/huggingface.mdx create mode 100644 apps/docs/content/docs/fr/tools/hunter.mdx create mode 100644 apps/docs/content/docs/fr/tools/image_generator.mdx create mode 100644 apps/docs/content/docs/fr/tools/index.mdx create mode 100644 apps/docs/content/docs/fr/tools/jina.mdx create mode 100644 apps/docs/content/docs/fr/tools/jira.mdx create mode 100644 apps/docs/content/docs/fr/tools/knowledge.mdx create mode 100644 apps/docs/content/docs/fr/tools/linear.mdx create mode 100644 apps/docs/content/docs/fr/tools/linkup.mdx create mode 100644 apps/docs/content/docs/fr/tools/mem0.mdx create mode 100644 apps/docs/content/docs/fr/tools/memory.mdx create mode 100644 apps/docs/content/docs/fr/tools/microsoft_excel.mdx create mode 100644 apps/docs/content/docs/fr/tools/microsoft_planner.mdx create mode 100644 apps/docs/content/docs/fr/tools/microsoft_teams.mdx create mode 100644 apps/docs/content/docs/fr/tools/mistral_parse.mdx create mode 100644 apps/docs/content/docs/fr/tools/mongodb.mdx create mode 100644 apps/docs/content/docs/fr/tools/mysql.mdx create mode 100644 apps/docs/content/docs/fr/tools/notion.mdx create mode 100644 apps/docs/content/docs/fr/tools/onedrive.mdx create mode 100644 apps/docs/content/docs/fr/tools/openai.mdx create mode 100644 apps/docs/content/docs/fr/tools/outlook.mdx create mode 100644 apps/docs/content/docs/fr/tools/parallel_ai.mdx create mode 100644 apps/docs/content/docs/fr/tools/perplexity.mdx create mode 100644 apps/docs/content/docs/fr/tools/pinecone.mdx create mode 100644 apps/docs/content/docs/fr/tools/postgresql.mdx create mode 100644 apps/docs/content/docs/fr/tools/qdrant.mdx create mode 100644 apps/docs/content/docs/fr/tools/reddit.mdx create mode 100644 apps/docs/content/docs/fr/tools/s3.mdx create mode 100644 apps/docs/content/docs/fr/tools/schedule.mdx create mode 100644 apps/docs/content/docs/fr/tools/serper.mdx create mode 100644 apps/docs/content/docs/fr/tools/sharepoint.mdx create mode 100644 apps/docs/content/docs/fr/tools/slack.mdx create mode 100644 apps/docs/content/docs/fr/tools/stagehand.mdx create mode 100644 apps/docs/content/docs/fr/tools/stagehand_agent.mdx create mode 100644 apps/docs/content/docs/fr/tools/supabase.mdx create mode 100644 apps/docs/content/docs/fr/tools/tavily.mdx create mode 100644 apps/docs/content/docs/fr/tools/telegram.mdx create mode 100644 apps/docs/content/docs/fr/tools/thinking.mdx create mode 100644 apps/docs/content/docs/fr/tools/translate.mdx create mode 100644 apps/docs/content/docs/fr/tools/twilio_sms.mdx create mode 100644 apps/docs/content/docs/fr/tools/typeform.mdx create mode 100644 apps/docs/content/docs/fr/tools/vision.mdx create mode 100644 apps/docs/content/docs/fr/tools/wealthbox.mdx create mode 100644 apps/docs/content/docs/fr/tools/webhook.mdx create mode 100644 apps/docs/content/docs/fr/tools/whatsapp.mdx create mode 100644 apps/docs/content/docs/fr/tools/wikipedia.mdx create mode 100644 apps/docs/content/docs/fr/tools/x.mdx create mode 100644 apps/docs/content/docs/fr/tools/youtube.mdx create mode 100644 apps/docs/content/docs/fr/triggers/schedule.mdx create mode 100644 apps/docs/content/docs/fr/triggers/starter.mdx create mode 100644 apps/docs/content/docs/fr/triggers/webhook.mdx create mode 100644 apps/docs/content/docs/fr/variables/environment-variables.mdx create mode 100644 apps/docs/content/docs/fr/variables/workflow-variables.mdx create mode 100644 apps/docs/content/docs/fr/yaml/block-reference.mdx create mode 100644 apps/docs/content/docs/fr/yaml/blocks/agent.mdx create mode 100644 apps/docs/content/docs/fr/yaml/blocks/api.mdx create mode 100644 apps/docs/content/docs/fr/yaml/blocks/condition.mdx create mode 100644 apps/docs/content/docs/fr/yaml/blocks/evaluator.mdx create mode 100644 apps/docs/content/docs/fr/yaml/blocks/function.mdx create mode 100644 apps/docs/content/docs/fr/yaml/blocks/index.mdx create mode 100644 apps/docs/content/docs/fr/yaml/blocks/loop.mdx create mode 100644 apps/docs/content/docs/fr/yaml/blocks/parallel.mdx create mode 100644 apps/docs/content/docs/fr/yaml/blocks/response.mdx create mode 100644 apps/docs/content/docs/fr/yaml/blocks/router.mdx create mode 100644 apps/docs/content/docs/fr/yaml/blocks/starter.mdx create mode 100644 apps/docs/content/docs/fr/yaml/blocks/webhook.mdx create mode 100644 apps/docs/content/docs/fr/yaml/blocks/workflow.mdx create mode 100644 apps/docs/content/docs/fr/yaml/examples.mdx create mode 100644 apps/docs/content/docs/fr/yaml/index.mdx delete mode 100644 apps/docs/content/docs/introduction/meta.json delete mode 100644 apps/docs/content/docs/knowledgebase/meta.json delete mode 100644 apps/docs/content/docs/mcp/meta.json delete mode 100644 apps/docs/content/docs/permissions/meta.json delete mode 100644 apps/docs/content/docs/sdks/meta.json delete mode 100644 apps/docs/content/docs/tools/meta.json delete mode 100644 apps/docs/content/docs/triggers/meta.json delete mode 100644 apps/docs/content/docs/variables/meta.json delete mode 100644 apps/docs/content/docs/yaml/blocks/meta.json delete mode 100644 apps/docs/content/docs/yaml/meta.json create mode 100644 apps/docs/content/docs/zh/blocks/agent.mdx create mode 100644 apps/docs/content/docs/zh/blocks/api.mdx create mode 100644 apps/docs/content/docs/zh/blocks/condition.mdx create mode 100644 apps/docs/content/docs/zh/blocks/evaluator.mdx create mode 100644 apps/docs/content/docs/zh/blocks/function.mdx create mode 100644 apps/docs/content/docs/zh/blocks/index.mdx create mode 100644 apps/docs/content/docs/zh/blocks/loop.mdx create mode 100644 apps/docs/content/docs/zh/blocks/parallel.mdx create mode 100644 apps/docs/content/docs/zh/blocks/response.mdx create mode 100644 apps/docs/content/docs/zh/blocks/router.mdx create mode 100644 apps/docs/content/docs/zh/blocks/workflow.mdx create mode 100644 apps/docs/content/docs/zh/connections/basics.mdx create mode 100644 apps/docs/content/docs/zh/connections/data-structure.mdx create mode 100644 apps/docs/content/docs/zh/connections/index.mdx create mode 100644 apps/docs/content/docs/zh/connections/tags.mdx create mode 100644 apps/docs/content/docs/zh/copilot/index.mdx create mode 100644 apps/docs/content/docs/zh/execution/api.mdx create mode 100644 apps/docs/content/docs/zh/execution/basics.mdx create mode 100644 apps/docs/content/docs/zh/execution/costs.mdx create mode 100644 apps/docs/content/docs/zh/execution/index.mdx create mode 100644 apps/docs/content/docs/zh/execution/logging.mdx create mode 100644 apps/docs/content/docs/zh/getting-started/index.mdx create mode 100644 apps/docs/content/docs/zh/index.mdx create mode 100644 apps/docs/content/docs/zh/introduction/index.mdx create mode 100644 apps/docs/content/docs/zh/knowledgebase/index.mdx create mode 100644 apps/docs/content/docs/zh/knowledgebase/tags.mdx create mode 100644 apps/docs/content/docs/zh/mcp/index.mdx create mode 100644 apps/docs/content/docs/zh/permissions/roles-and-permissions.mdx create mode 100644 apps/docs/content/docs/zh/sdks/python.mdx create mode 100644 apps/docs/content/docs/zh/sdks/typescript.mdx create mode 100644 apps/docs/content/docs/zh/tools/airtable.mdx create mode 100644 apps/docs/content/docs/zh/tools/arxiv.mdx create mode 100644 apps/docs/content/docs/zh/tools/browser_use.mdx create mode 100644 apps/docs/content/docs/zh/tools/clay.mdx create mode 100644 apps/docs/content/docs/zh/tools/confluence.mdx create mode 100644 apps/docs/content/docs/zh/tools/discord.mdx create mode 100644 apps/docs/content/docs/zh/tools/elevenlabs.mdx create mode 100644 apps/docs/content/docs/zh/tools/exa.mdx create mode 100644 apps/docs/content/docs/zh/tools/file.mdx create mode 100644 apps/docs/content/docs/zh/tools/firecrawl.mdx create mode 100644 apps/docs/content/docs/zh/tools/generic_webhook.mdx create mode 100644 apps/docs/content/docs/zh/tools/github.mdx create mode 100644 apps/docs/content/docs/zh/tools/gmail.mdx create mode 100644 apps/docs/content/docs/zh/tools/google_calendar.mdx create mode 100644 apps/docs/content/docs/zh/tools/google_docs.mdx create mode 100644 apps/docs/content/docs/zh/tools/google_drive.mdx create mode 100644 apps/docs/content/docs/zh/tools/google_search.mdx create mode 100644 apps/docs/content/docs/zh/tools/google_sheets.mdx create mode 100644 apps/docs/content/docs/zh/tools/huggingface.mdx create mode 100644 apps/docs/content/docs/zh/tools/hunter.mdx create mode 100644 apps/docs/content/docs/zh/tools/image_generator.mdx create mode 100644 apps/docs/content/docs/zh/tools/index.mdx create mode 100644 apps/docs/content/docs/zh/tools/jina.mdx create mode 100644 apps/docs/content/docs/zh/tools/jira.mdx create mode 100644 apps/docs/content/docs/zh/tools/knowledge.mdx create mode 100644 apps/docs/content/docs/zh/tools/linear.mdx create mode 100644 apps/docs/content/docs/zh/tools/linkup.mdx create mode 100644 apps/docs/content/docs/zh/tools/mem0.mdx create mode 100644 apps/docs/content/docs/zh/tools/memory.mdx create mode 100644 apps/docs/content/docs/zh/tools/microsoft_excel.mdx create mode 100644 apps/docs/content/docs/zh/tools/microsoft_planner.mdx create mode 100644 apps/docs/content/docs/zh/tools/microsoft_teams.mdx create mode 100644 apps/docs/content/docs/zh/tools/mistral_parse.mdx create mode 100644 apps/docs/content/docs/zh/tools/mongodb.mdx create mode 100644 apps/docs/content/docs/zh/tools/mysql.mdx create mode 100644 apps/docs/content/docs/zh/tools/notion.mdx create mode 100644 apps/docs/content/docs/zh/tools/onedrive.mdx create mode 100644 apps/docs/content/docs/zh/tools/openai.mdx create mode 100644 apps/docs/content/docs/zh/tools/outlook.mdx create mode 100644 apps/docs/content/docs/zh/tools/parallel_ai.mdx create mode 100644 apps/docs/content/docs/zh/tools/perplexity.mdx create mode 100644 apps/docs/content/docs/zh/tools/pinecone.mdx create mode 100644 apps/docs/content/docs/zh/tools/postgresql.mdx create mode 100644 apps/docs/content/docs/zh/tools/qdrant.mdx create mode 100644 apps/docs/content/docs/zh/tools/reddit.mdx create mode 100644 apps/docs/content/docs/zh/tools/s3.mdx create mode 100644 apps/docs/content/docs/zh/tools/schedule.mdx create mode 100644 apps/docs/content/docs/zh/tools/serper.mdx create mode 100644 apps/docs/content/docs/zh/tools/sharepoint.mdx create mode 100644 apps/docs/content/docs/zh/tools/slack.mdx create mode 100644 apps/docs/content/docs/zh/tools/stagehand.mdx create mode 100644 apps/docs/content/docs/zh/tools/stagehand_agent.mdx create mode 100644 apps/docs/content/docs/zh/tools/supabase.mdx create mode 100644 apps/docs/content/docs/zh/tools/tavily.mdx create mode 100644 apps/docs/content/docs/zh/tools/telegram.mdx create mode 100644 apps/docs/content/docs/zh/tools/thinking.mdx create mode 100644 apps/docs/content/docs/zh/tools/translate.mdx create mode 100644 apps/docs/content/docs/zh/tools/twilio_sms.mdx create mode 100644 apps/docs/content/docs/zh/tools/typeform.mdx create mode 100644 apps/docs/content/docs/zh/tools/vision.mdx create mode 100644 apps/docs/content/docs/zh/tools/wealthbox.mdx create mode 100644 apps/docs/content/docs/zh/tools/webhook.mdx create mode 100644 apps/docs/content/docs/zh/tools/whatsapp.mdx create mode 100644 apps/docs/content/docs/zh/tools/wikipedia.mdx create mode 100644 apps/docs/content/docs/zh/tools/x.mdx create mode 100644 apps/docs/content/docs/zh/tools/youtube.mdx create mode 100644 apps/docs/content/docs/zh/triggers/schedule.mdx create mode 100644 apps/docs/content/docs/zh/triggers/starter.mdx create mode 100644 apps/docs/content/docs/zh/triggers/webhook.mdx create mode 100644 apps/docs/content/docs/zh/variables/environment-variables.mdx create mode 100644 apps/docs/content/docs/zh/variables/workflow-variables.mdx create mode 100644 apps/docs/content/docs/zh/yaml/block-reference.mdx create mode 100644 apps/docs/content/docs/zh/yaml/blocks/agent.mdx create mode 100644 apps/docs/content/docs/zh/yaml/blocks/api.mdx create mode 100644 apps/docs/content/docs/zh/yaml/blocks/condition.mdx create mode 100644 apps/docs/content/docs/zh/yaml/blocks/evaluator.mdx create mode 100644 apps/docs/content/docs/zh/yaml/blocks/function.mdx create mode 100644 apps/docs/content/docs/zh/yaml/blocks/index.mdx create mode 100644 apps/docs/content/docs/zh/yaml/blocks/loop.mdx create mode 100644 apps/docs/content/docs/zh/yaml/blocks/parallel.mdx create mode 100644 apps/docs/content/docs/zh/yaml/blocks/response.mdx create mode 100644 apps/docs/content/docs/zh/yaml/blocks/router.mdx create mode 100644 apps/docs/content/docs/zh/yaml/blocks/starter.mdx create mode 100644 apps/docs/content/docs/zh/yaml/blocks/webhook.mdx create mode 100644 apps/docs/content/docs/zh/yaml/blocks/workflow.mdx create mode 100644 apps/docs/content/docs/zh/yaml/examples.mdx create mode 100644 apps/docs/content/docs/zh/yaml/index.mdx create mode 100644 apps/docs/i18n.json create mode 100644 apps/docs/i18n.lock create mode 100644 apps/docs/lib/i18n.ts create mode 100644 apps/docs/middleware.ts create mode 100644 apps/docs/public/llms.txt rename apps/docs/public/static/{ => knowledgebase}/knowledgebase-2.png (100%) rename apps/docs/public/static/{ => knowledgebase}/knowledgebase.png (100%) diff --git a/.github/workflows/i18n.yml b/.github/workflows/i18n.yml new file mode 100644 index 0000000000..b6966dc143 --- /dev/null +++ b/.github/workflows/i18n.yml @@ -0,0 +1,126 @@ +name: 'Auto-translate Documentation' + +on: + push: + branches: [ main ] + paths: + - 'apps/docs/content/docs/en/**' + - 'apps/docs/i18n.json' + pull_request: + branches: [ main ] + paths: + - 'apps/docs/content/docs/en/**' + - 'apps/docs/i18n.json' + workflow_dispatch: # Allow manual triggers + +jobs: + translate: + runs-on: ubuntu-latest + if: github.actor != 'github-actions[bot]' # Prevent infinite loops + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Run Lingo.dev translations + env: + LINGODOTDEV_API_KEY: ${{ secrets.LINGODOTDEV_API_KEY }} + run: | + cd apps/docs + bunx lingo.dev@latest i18n + + - name: Check for translation changes + id: changes + run: | + cd apps/docs + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + if [ -n "$(git status --porcelain content/docs)" ]; then + echo "changes=true" >> $GITHUB_OUTPUT + else + echo "changes=false" >> $GITHUB_OUTPUT + fi + + - name: Commit and push translation updates + if: steps.changes.outputs.changes == 'true' + run: | + cd apps/docs + git add content/docs/es/ content/docs/fr/ content/docs/zh/ i18n.lock + git commit -m "feat: update translations" + git push origin ${{ github.ref_name }} + + - name: Create Pull Request (for feature branches) + if: steps.changes.outputs.changes == 'true' && github.event_name == 'pull_request' + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "feat: update translations" + title: "🌐 Auto-update translations" + body: | + ## Summary + Automated translation updates for documentation. + + - Updated translations for modified English content + - Generated using Lingo.dev AI translation + - Maintains consistency with source documentation + + ## Test Plan + - [ ] Verify translated content accuracy + - [ ] Check that all links and references work correctly + - [ ] Ensure formatting and structure are preserved + branch: auto-translations + base: ${{ github.base_ref }} + labels: | + i18n + auto-generated + + verify-translations: + needs: translate + runs-on: ubuntu-latest + if: always() # Run even if translation fails + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: | + cd apps/docs + bun install + + - name: Build documentation to verify translations + run: | + cd apps/docs + bun run build + + - name: Report translation status + run: | + cd apps/docs + echo "## Translation Status Report" >> $GITHUB_STEP_SUMMARY + + en_count=$(find content/docs/en -name "*.mdx" | wc -l) + es_count=$(find content/docs/es -name "*.mdx" 2>/dev/null | wc -l || echo 0) + fr_count=$(find content/docs/fr -name "*.mdx" 2>/dev/null | wc -l || echo 0) + zh_count=$(find content/docs/zh -name "*.mdx" 2>/dev/null | wc -l || echo 0) + + es_percentage=$((es_count * 100 / en_count)) + fr_percentage=$((fr_count * 100 / en_count)) + zh_percentage=$((zh_count * 100 / en_count)) + + echo "- **🇪🇸 Spanish**: $es_count/$en_count files ($es_percentage%)" >> $GITHUB_STEP_SUMMARY + echo "- **🇫🇷 French**: $fr_count/$en_count files ($fr_percentage%)" >> $GITHUB_STEP_SUMMARY + echo "- **🇨🇳 Chinese**: $zh_count/$en_count files ($zh_percentage%)" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.gitignore b/.gitignore index d486100979..a6ec4da9ac 100644 --- a/.gitignore +++ b/.gitignore @@ -68,4 +68,5 @@ start-collector.sh .vscode ## Helm Chart Tests -helm/sim/test \ No newline at end of file +helm/sim/test +i18n.cache diff --git a/apps/docs/app/(docs)/[[...slug]]/layout.tsx b/apps/docs/app/(docs)/[[...slug]]/layout.tsx deleted file mode 100644 index 257c2d8c27..0000000000 --- a/apps/docs/app/(docs)/[[...slug]]/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import type { ReactNode } from 'react' - -export default function SlugLayout({ children }: { children: ReactNode }) { - return children -} diff --git a/apps/docs/app/(docs)/[[...slug]]/page.tsx b/apps/docs/app/(docs)/[[...slug]]/page.tsx deleted file mode 100644 index 5af882aa90..0000000000 --- a/apps/docs/app/(docs)/[[...slug]]/page.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import defaultMdxComponents from 'fumadocs-ui/mdx' -import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page' -import { notFound } from 'next/navigation' -import { source } from '@/lib/source' - -export const dynamic = 'force-dynamic' - -export default async function Page(props: { params: Promise<{ slug?: string[] }> }) { - const params = await props.params - const page = source.getPage(params.slug) - if (!page) notFound() - - const MDX = page.data.body - - return ( - On this page
, - single: false, - }} - article={{ - className: 'scroll-smooth max-sm:pb-16', - }} - tableOfContentPopover={{ - style: 'clerk', - enabled: true, - }} - footer={{ - enabled: false, - }} - > - {page.data.title} - {page.data.description} - - - - - ) -} - -export async function generateStaticParams() { - return source.generateParams() -} - -export async function generateMetadata(props: { params: Promise<{ slug?: string[] }> }) { - const params = await props.params - const page = source.getPage(params.slug) - if (!page) notFound() - - return { - title: page.data.title, - description: page.data.description, - } -} diff --git a/apps/docs/app/(docs)/layout.tsx b/apps/docs/app/(docs)/layout.tsx deleted file mode 100644 index 18bf0fac61..0000000000 --- a/apps/docs/app/(docs)/layout.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import type { ReactNode } from 'react' -import { DocsLayout } from 'fumadocs-ui/layouts/docs' -import { ExternalLink, GithubIcon } from 'lucide-react' -import Image from 'next/image' -import Link from 'next/link' -import { source } from '@/lib/source' - -const GitHubLink = () => ( -
- - - -
-) - -export default function Layout({ children }: { children: ReactNode }) { - return ( - <> - - Sim -
- ), - }} - links={[ - { - text: 'Visit Sim', - url: 'https://sim.ai', - icon: , - }, - ]} - sidebar={{ - defaultOpenLevel: 0, - collapsible: true, - footer: null, - banner: null, - }} - > - {children} - - - - ) -} diff --git a/apps/docs/app/[lang]/[[...slug]]/page.tsx b/apps/docs/app/[lang]/[[...slug]]/page.tsx new file mode 100644 index 0000000000..5dd543dcb3 --- /dev/null +++ b/apps/docs/app/[lang]/[[...slug]]/page.tsx @@ -0,0 +1,161 @@ +import { findNeighbour } from 'fumadocs-core/server' +import defaultMdxComponents from 'fumadocs-ui/mdx' +import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page' +import { ChevronLeft, ChevronRight } from 'lucide-react' +import Link from 'next/link' +import { notFound } from 'next/navigation' +import { StructuredData } from '@/components/structured-data' +import { source } from '@/lib/source' + +export const dynamic = 'force-dynamic' + +export default async function Page(props: { params: Promise<{ slug?: string[]; lang: string }> }) { + const params = await props.params + const page = source.getPage(params.slug, params.lang) + if (!page) notFound() + + const MDX = page.data.body + const baseUrl = 'https://docs.sim.ai' + + const pageTreeRecord = source.pageTree as Record + const pageTree = + pageTreeRecord[params.lang] ?? pageTreeRecord.en ?? Object.values(pageTreeRecord)[0] + const neighbours = pageTree ? findNeighbour(pageTree, page.url) : null + + const CustomFooter = () => ( +
+ {neighbours?.previous ? ( + + + {neighbours.previous.name} + + ) : ( +
+ )} + + {neighbours?.next ? ( + + {neighbours.next.name} + + + ) : ( +
+ )} +
+ ) + + return ( + <> + + On this page
, + single: false, + }} + article={{ + className: 'scroll-smooth max-sm:pb-16', + }} + tableOfContentPopover={{ + style: 'clerk', + enabled: true, + }} + footer={{ + enabled: true, + component: , + }} + > + {page.data.title} + {page.data.description} + + + + + + ) +} + +export async function generateStaticParams() { + return source.generateParams() +} + +export async function generateMetadata(props: { + params: Promise<{ slug?: string[]; lang: string }> +}) { + const params = await props.params + const page = source.getPage(params.slug, params.lang) + if (!page) notFound() + + const baseUrl = 'https://docs.sim.ai' + const fullUrl = `${baseUrl}${page.url}` + + return { + title: page.data.title, + description: + page.data.description || 'Sim visual workflow builder for AI applications documentation', + keywords: [ + 'AI workflow builder', + 'visual workflow editor', + 'AI automation', + 'workflow automation', + 'AI agents', + 'no-code AI', + 'drag and drop workflows', + page.data.title?.toLowerCase().split(' '), + ] + .flat() + .filter(Boolean), + authors: [{ name: 'Sim Team' }], + category: 'Developer Tools', + openGraph: { + title: page.data.title, + description: + page.data.description || 'Sim visual workflow builder for AI applications documentation', + url: fullUrl, + siteName: 'Sim Documentation', + type: 'article', + locale: params.lang, + alternateLocale: ['en', 'fr', 'zh'].filter((lang) => lang !== params.lang), + }, + twitter: { + card: 'summary', + title: page.data.title, + description: + page.data.description || 'Sim visual workflow builder for AI applications documentation', + }, + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + 'max-video-preview': -1, + 'max-image-preview': 'large', + 'max-snippet': -1, + }, + }, + canonical: fullUrl, + alternates: { + canonical: fullUrl, + languages: { + en: `${baseUrl}/en${page.url.replace(`/${params.lang}`, '')}`, + fr: `${baseUrl}/fr${page.url.replace(`/${params.lang}`, '')}`, + zh: `${baseUrl}/zh${page.url.replace(`/${params.lang}`, '')}`, + }, + }, + } +} diff --git a/apps/docs/app/[lang]/layout.tsx b/apps/docs/app/[lang]/layout.tsx new file mode 100644 index 0000000000..dc071493f3 --- /dev/null +++ b/apps/docs/app/[lang]/layout.tsx @@ -0,0 +1,99 @@ +import type { ReactNode } from 'react' +import { defineI18nUI } from 'fumadocs-ui/i18n' +import { DocsLayout } from 'fumadocs-ui/layouts/docs' +import { RootProvider } from 'fumadocs-ui/provider' +import { ExternalLink, GithubIcon } from 'lucide-react' +import { Inter } from 'next/font/google' +import Image from 'next/image' +import Link from 'next/link' +import { LanguageDropdown } from '@/components/ui/language-dropdown' +import { i18n } from '@/lib/i18n' +import { source } from '@/lib/source' +import '../global.css' +import { Analytics } from '@vercel/analytics/next' + +const inter = Inter({ + subsets: ['latin'], +}) + +const { provider } = defineI18nUI(i18n, { + translations: { + en: { + displayName: 'English', + }, + es: { + displayName: 'Español', + }, + fr: { + displayName: 'Français', + }, + zh: { + displayName: '简体中文', + }, + }, +}) + +const GitHubLink = () => ( +
+ + + +
+) + +type LayoutProps = { + children: ReactNode + params: Promise<{ lang: string }> +} + +export default async function Layout({ children, params }: LayoutProps) { + const { lang } = await params + + return ( + + + + + Sim + +
+ ), + }} + links={[ + { + text: 'Visit Sim', + url: 'https://sim.ai', + icon: , + }, + ]} + sidebar={{ + defaultOpenLevel: 0, + collapsible: true, + footer: null, + banner: null, + }} + > + {children} + + + + + + + ) +} diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx index beb280c968..c0028d90b3 100644 --- a/apps/docs/app/layout.tsx +++ b/apps/docs/app/layout.tsx @@ -1,30 +1,32 @@ import type { ReactNode } from 'react' -import { RootProvider } from 'fumadocs-ui/provider' -import { Inter } from 'next/font/google' -import './global.css' -import { Analytics } from '@vercel/analytics/next' -const inter = Inter({ - subsets: ['latin'], -}) - -export default function Layout({ children }: { children: ReactNode }) { - return ( - - - - {children} - - - - - ) +export default function RootLayout({ children }: { children: ReactNode }) { + return children } export const metadata = { - title: 'Sim', + metadataBase: new URL('https://docs.sim.ai'), + title: { + default: 'Sim Documentation - Visual Workflow Builder for AI Applications', + template: '%s', + }, description: - 'Build agents in seconds with a drag and drop workflow builder. Access comprehensive documentation to help you create efficient workflows and maximize your automation capabilities.', + 'Comprehensive documentation for Sim - the visual workflow builder for AI applications. Create powerful AI agents, automation workflows, and data processing pipelines by connecting blocks on a canvas—no coding required.', + keywords: [ + 'AI workflow builder', + 'visual workflow editor', + 'AI automation', + 'workflow automation', + 'AI agents', + 'no-code AI', + 'drag and drop workflows', + 'AI integrations', + 'workflow canvas', + 'AI development platform', + ], + authors: [{ name: 'Sim Team', url: 'https://sim.ai' }], + category: 'Developer Tools', + classification: 'Developer Documentation', manifest: '/favicon/site.webmanifest', icons: { icon: [ @@ -39,4 +41,40 @@ export const metadata = { statusBarStyle: 'default', title: 'Sim Docs', }, + openGraph: { + type: 'website', + locale: 'en_US', + alternateLocale: ['fr_FR', 'zh_CN'], + url: 'https://docs.sim.ai', + siteName: 'Sim Documentation', + title: 'Sim Documentation - Visual Workflow Builder for AI Applications', + description: + 'Comprehensive documentation for Sim - the visual workflow builder for AI applications. Create powerful AI agents, automation workflows, and data processing pipelines.', + }, + twitter: { + card: 'summary', + title: 'Sim Documentation - Visual Workflow Builder for AI Applications', + description: + 'Comprehensive documentation for Sim - the visual workflow builder for AI applications.', + creator: '@sim_ai', + }, + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + 'max-video-preview': -1, + 'max-image-preview': 'large', + 'max-snippet': -1, + }, + }, + alternates: { + canonical: 'https://docs.sim.ai', + languages: { + en: '/en', + fr: '/fr', + zh: '/zh', + }, + }, } diff --git a/apps/docs/app/llms.txt/route.ts b/apps/docs/app/llms.txt/route.ts index 6496e0d5ad..fb60ef33f4 100644 --- a/apps/docs/app/llms.txt/route.ts +++ b/apps/docs/app/llms.txt/route.ts @@ -1,7 +1,6 @@ import { getLLMText } from '@/lib/llms' import { source } from '@/lib/source' -// cached forever export const revalidate = false export async function GET() { diff --git a/apps/docs/app/robots.txt/route.ts b/apps/docs/app/robots.txt/route.ts new file mode 100644 index 0000000000..46e1a68d4a --- /dev/null +++ b/apps/docs/app/robots.txt/route.ts @@ -0,0 +1,58 @@ +export const revalidate = false + +export async function GET() { + const baseUrl = 'https://docs.sim.ai' + + const robotsTxt = `# Robots.txt for Sim Documentation +# Generated on ${new Date().toISOString()} + +User-agent: * +Allow: / + +# Allow all well-behaved crawlers +User-agent: Googlebot +Allow: / + +User-agent: Bingbot +Allow: / + +# AI and LLM crawlers +User-agent: GPTBot +Allow: / + +User-agent: ChatGPT-User +Allow: / + +User-agent: CCBot +Allow: / + +User-agent: anthropic-ai +Allow: / + +User-agent: Claude-Web +Allow: / + +# Disallow admin and internal paths (if any exist) +Disallow: /.next/ +Disallow: /api/internal/ +Disallow: /_next/static/ +Disallow: /admin/ + +# Allow but don't prioritize these +Allow: /api/search +Allow: /llms.txt +Allow: /llms.mdx/ + +# Sitemaps +Sitemap: ${baseUrl}/sitemap.xml + +# Additional resources for AI indexing +# See https://github.com/AnswerDotAI/llms-txt for more info +# LLM-friendly content available at: ${baseUrl}/llms.txt` + + return new Response(robotsTxt, { + headers: { + 'Content-Type': 'text/plain', + }, + }) +} diff --git a/apps/docs/app/sitemap.xml/route.ts b/apps/docs/app/sitemap.xml/route.ts new file mode 100644 index 0000000000..00f36ac09f --- /dev/null +++ b/apps/docs/app/sitemap.xml/route.ts @@ -0,0 +1,54 @@ +import { i18n } from '@/lib/i18n' +import { source } from '@/lib/source' + +export const revalidate = false + +export async function GET() { + const baseUrl = 'https://docs.sim.ai' + + const allPages = source.getPages() + + const urls = allPages + .flatMap((page) => { + const urlWithoutLang = page.url.replace(/^\/[a-z]{2}\//, '/') + + return i18n.languages.map((lang) => { + const url = + lang === i18n.defaultLanguage + ? `${baseUrl}${urlWithoutLang}` + : `${baseUrl}/${lang}${urlWithoutLang}` + + return ` + ${url} + ${new Date().toISOString().split('T')[0]} + weekly + ${urlWithoutLang === '/introduction' ? '1.0' : '0.8'} + ${i18n.languages.length > 1 ? generateAlternateLinks(baseUrl, urlWithoutLang) : ''} + ` + }) + }) + .join('\n') + + const sitemap = ` + +${urls} +` + + return new Response(sitemap, { + headers: { + 'Content-Type': 'application/xml', + }, + }) +} + +function generateAlternateLinks(baseUrl: string, urlWithoutLang: string): string { + return i18n.languages + .map((lang) => { + const url = + lang === i18n.defaultLanguage + ? `${baseUrl}${urlWithoutLang}` + : `${baseUrl}/${lang}${urlWithoutLang}` + return ` ` + }) + .join('\n') +} diff --git a/apps/docs/components/structured-data.tsx b/apps/docs/components/structured-data.tsx new file mode 100644 index 0000000000..c09e0136e3 --- /dev/null +++ b/apps/docs/components/structured-data.tsx @@ -0,0 +1,174 @@ +import Script from 'next/script' + +interface StructuredDataProps { + title: string + description: string + url: string + lang: string + dateModified?: string + breadcrumb?: Array<{ name: string; url: string }> +} + +export function StructuredData({ + title, + description, + url, + lang, + dateModified, + breadcrumb, +}: StructuredDataProps) { + const baseUrl = 'https://docs.sim.ai' + + const articleStructuredData = { + '@context': 'https://schema.org', + '@type': 'TechArticle', + headline: title, + description: description, + url: url, + datePublished: dateModified || new Date().toISOString(), + dateModified: dateModified || new Date().toISOString(), + author: { + '@type': 'Organization', + name: 'Sim Team', + url: baseUrl, + }, + publisher: { + '@type': 'Organization', + name: 'Sim', + url: baseUrl, + logo: { + '@type': 'ImageObject', + url: `${baseUrl}/static/logo.png`, + }, + }, + mainEntityOfPage: { + '@type': 'WebPage', + '@id': url, + }, + inLanguage: lang, + isPartOf: { + '@type': 'WebSite', + name: 'Sim Documentation', + url: baseUrl, + }, + potentialAction: { + '@type': 'ReadAction', + target: url, + }, + } + + const breadcrumbStructuredData = breadcrumb && { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + itemListElement: breadcrumb.map((item, index) => ({ + '@type': 'ListItem', + position: index + 1, + name: item.name, + item: item.url, + })), + } + + const websiteStructuredData = url === baseUrl && { + '@context': 'https://schema.org', + '@type': 'WebSite', + name: 'Sim Documentation', + url: baseUrl, + description: + 'Comprehensive documentation for Sim visual workflow builder for AI applications. Create powerful AI agents, automation workflows, and data processing pipelines.', + publisher: { + '@type': 'Organization', + name: 'Sim', + url: baseUrl, + }, + potentialAction: { + '@type': 'SearchAction', + target: { + '@type': 'EntryPoint', + urlTemplate: `${baseUrl}/search?q={search_term_string}`, + }, + 'query-input': 'required name=search_term_string', + }, + inLanguage: ['en', 'fr', 'zh'], + } + + const faqStructuredData = title.toLowerCase().includes('faq') && { + '@context': 'https://schema.org', + '@type': 'FAQPage', + mainEntity: [], + } + + const softwareStructuredData = { + '@context': 'https://schema.org', + '@type': 'SoftwareApplication', + name: 'Sim', + applicationCategory: 'DeveloperApplication', + operatingSystem: 'Any', + description: + 'Visual workflow builder for AI applications. Create powerful AI agents, automation workflows, and data processing pipelines by connecting blocks on a canvas—no coding required.', + url: baseUrl, + author: { + '@type': 'Organization', + name: 'Sim Team', + }, + offers: { + '@type': 'Offer', + category: 'Developer Tools', + }, + featureList: [ + 'Visual workflow builder with drag-and-drop interface', + 'AI agent creation and automation', + '80+ built-in integrations', + 'Real-time team collaboration', + 'Multiple deployment options', + 'Custom integrations via MCP protocol', + ], + } + + return ( + <> +