From 42936c89d06e0aa10f597f5ca12e5cc24281a1c0 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 1 Apr 2025 17:29:36 -0700 Subject: [PATCH] fix(autoconnect): fixed autoconnect error for conditional block (#215) * fix(autoconnect): fix bug where autoconnect didn't work for conditional block * improvement(ui): file upload * fix twilio block error * run format --------- Co-authored-by: Emir Karabeg --- sim/app/api/schedules/execute/route.ts | 13 +- sim/app/api/webhooks/trigger/[path]/route.ts | 9 +- .../api/workflows/[id]/execute/route.test.ts | 22 +- sim/app/api/workflows/[id]/execute/route.ts | 39 ++-- .../sub-block/hooks/use-sub-block-value.ts | 24 +-- sim/app/w/[id]/workflow.tsx | 180 +++++++++------- sim/blocks/blocks/twilio.ts | 19 +- sim/blocks/blocks/typeform.ts | 125 +++++------ sim/blocks/index.ts | 4 +- sim/components/icons.tsx | 10 +- sim/components/ui/command.tsx | 6 + sim/executor/handlers.ts | 26 ++- sim/executor/index.ts | 137 ++++++------ sim/executor/resolver.ts | 201 ++++++++++-------- sim/lib/oauth.ts | 11 +- sim/stores/workflows/subblock/store.ts | 8 +- sim/tools/airtable/index.test.ts | 30 ++- sim/tools/airtable/index.ts | 4 +- sim/tools/airtable/read.ts | 14 +- sim/tools/airtable/types.ts | 2 +- sim/tools/airtable/write.ts | 17 +- sim/tools/file/index.ts | 2 +- sim/tools/index.ts | 4 +- sim/tools/twilio/index.ts | 2 +- sim/tools/twilio/{sendSMS.ts => send.ts} | 39 ++-- sim/tools/twilio/types.ts | 8 +- sim/tools/typeform/files.test.ts | 26 +-- sim/tools/typeform/files.ts | 74 +++---- sim/tools/typeform/index.test.ts | 134 ++++++------ sim/tools/typeform/index.ts | 4 +- sim/tools/typeform/insights.test.ts | 44 ++-- sim/tools/typeform/insights.ts | 52 ++--- sim/tools/typeform/responses.test.ts | 14 +- sim/tools/typeform/responses.ts | 48 +++-- sim/tools/utils.ts | 4 +- 35 files changed, 730 insertions(+), 626 deletions(-) rename sim/tools/twilio/{sendSMS.ts => send.ts} (84%) diff --git a/sim/app/api/schedules/execute/route.ts b/sim/app/api/schedules/execute/route.ts index 8983475897..f38d71f8dc 100644 --- a/sim/app/api/schedules/execute/route.ts +++ b/sim/app/api/schedules/execute/route.ts @@ -320,7 +320,7 @@ export async function GET(req: NextRequest) { ) logger.info(`[${requestId}] Executing workflow ${schedule.workflowId}`) - + // Get workflow variables let workflowVariables = {} if (workflowRecord.variables) { @@ -332,15 +332,20 @@ export async function GET(req: NextRequest) { // Otherwise use as is (already parsed JSON) workflowVariables = workflowRecord.variables } - logger.debug(`[${requestId}] Loaded ${Object.keys(workflowVariables).length} workflow variables for: ${schedule.workflowId}`) + logger.debug( + `[${requestId}] Loaded ${Object.keys(workflowVariables).length} workflow variables for: ${schedule.workflowId}` + ) } catch (error) { - logger.error(`[${requestId}] Failed to parse workflow variables: ${schedule.workflowId}`, error) + logger.error( + `[${requestId}] Failed to parse workflow variables: ${schedule.workflowId}`, + error + ) // Continue execution even if variables can't be parsed } } else { logger.debug(`[${requestId}] No workflow variables found for: ${schedule.workflowId}`) } - + const executor = new Executor( serializedWorkflow, processedBlockStates, // Use the processed block states diff --git a/sim/app/api/webhooks/trigger/[path]/route.ts b/sim/app/api/webhooks/trigger/[path]/route.ts index b97e01c4d2..568c52ff92 100644 --- a/sim/app/api/webhooks/trigger/[path]/route.ts +++ b/sim/app/api/webhooks/trigger/[path]/route.ts @@ -631,9 +631,14 @@ async function processWebhook( // Otherwise use as is (already parsed JSON) workflowVariables = foundWorkflow.variables } - logger.debug(`[${requestId}] Loaded ${Object.keys(workflowVariables).length} workflow variables for: ${foundWorkflow.id}`) + logger.debug( + `[${requestId}] Loaded ${Object.keys(workflowVariables).length} workflow variables for: ${foundWorkflow.id}` + ) } catch (error) { - logger.error(`[${requestId}] Failed to parse workflow variables: ${foundWorkflow.id}`, error) + logger.error( + `[${requestId}] Failed to parse workflow variables: ${foundWorkflow.id}`, + error + ) // Continue execution even if variables can't be parsed } } else { diff --git a/sim/app/api/workflows/[id]/execute/route.test.ts b/sim/app/api/workflows/[id]/execute/route.test.ts index 6e37fe4042..cb58885184 100644 --- a/sim/app/api/workflows/[id]/execute/route.test.ts +++ b/sim/app/api/workflows/[id]/execute/route.test.ts @@ -260,15 +260,16 @@ describe('Workflow Execution API Route', () => { // Verify execute was called with the input body expect(executeMock).toHaveBeenCalledWith('workflow-id') - + // Updated expectations to match actual implementation // The structure should match: serializedWorkflow, processedBlockStates, decryptedEnvVars, processedInput, workflowVariables expect(Executor).toHaveBeenCalledWith( expect.anything(), // serializedWorkflow expect.anything(), // processedBlockStates expect.anything(), // decryptedEnvVars - expect.objectContaining({ // processedInput - input: requestBody + expect.objectContaining({ + // processedInput + input: requestBody, }), expect.anything() // workflowVariables ) @@ -313,8 +314,9 @@ describe('Workflow Execution API Route', () => { expect.anything(), // serializedWorkflow expect.anything(), // processedBlockStates expect.anything(), // decryptedEnvVars - expect.objectContaining({ // processedInput - input: structuredInput + expect.objectContaining({ + // processedInput + input: structuredInput, }), expect.anything() // workflowVariables ) @@ -463,8 +465,8 @@ describe('Workflow Execution API Route', () => { it('should pass workflow variables to the Executor', async () => { // Create mock variables for the workflow const workflowVariables = { - 'variable1': { id: 'var1', name: 'variable1', type: 'string', value: '"test value"' }, - 'variable2': { id: 'var2', name: 'variable2', type: 'boolean', value: 'true' } + variable1: { id: 'var1', name: 'variable1', type: 'string', value: '"test value"' }, + variable2: { id: 'var2', name: 'variable2', type: 'boolean', value: 'true' }, } // Mock workflow with variables @@ -539,15 +541,15 @@ describe('Workflow Execution API Route', () => { // Verify the Executor was constructed with workflow variables expect(executorConstructorMock).toHaveBeenCalled() - + // Check that the 5th parameter (workflow variables) was passed const executorCalls = executorConstructorMock.mock.calls expect(executorCalls.length).toBeGreaterThan(0) - + // Each call to the constructor should have at least 5 parameters const lastCall = executorCalls[executorCalls.length - 1] expect(lastCall.length).toBeGreaterThanOrEqual(5) - + // The 5th parameter should be the workflow variables expect(lastCall[4]).toEqual(workflowVariables) }) diff --git a/sim/app/api/workflows/[id]/execute/route.ts b/sim/app/api/workflows/[id]/execute/route.ts index 627c51d59f..a775776830 100644 --- a/sim/app/api/workflows/[id]/execute/route.ts +++ b/sim/app/api/workflows/[id]/execute/route.ts @@ -38,17 +38,22 @@ async function executeWorkflow(workflow: any, requestId: string, input?: any) { } // Log input to help debug - logger.info(`[${requestId}] Executing workflow with input:`, - input ? JSON.stringify(input, null, 2) : 'No input provided'); - + logger.info( + `[${requestId}] Executing workflow with input:`, + input ? JSON.stringify(input, null, 2) : 'No input provided' + ) + // Validate and structure input for maximum compatibility - let processedInput = input; + let processedInput = input if (input && typeof input === 'object') { // Ensure input is properly structured for the starter block if (input.input === undefined) { // If input is not already nested, structure it properly - processedInput = { input: input }; - logger.info(`[${requestId}] Restructured input for workflow:`, JSON.stringify(processedInput, null, 2)); + processedInput = { input: input } + logger.info( + `[${requestId}] Restructured input for workflow:`, + JSON.stringify(processedInput, null, 2) + ) } } @@ -174,7 +179,9 @@ async function executeWorkflow(workflow: any, requestId: string, input?: any) { // Otherwise use as is (already parsed JSON) workflowVariables = workflow.variables } - logger.debug(`[${requestId}] Loaded ${Object.keys(workflowVariables).length} workflow variables for: ${workflowId}`) + logger.debug( + `[${requestId}] Loaded ${Object.keys(workflowVariables).length} workflow variables for: ${workflowId}` + ) } catch (error) { logger.error(`[${requestId}] Failed to parse workflow variables: ${workflowId}`, error) // Continue execution even if variables can't be parsed @@ -188,13 +195,13 @@ async function executeWorkflow(workflow: any, requestId: string, input?: any) { const serializedWorkflow = new Serializer().serializeWorkflow(mergedStates, edges, loops) const executor = new Executor( - serializedWorkflow, - processedBlockStates, - decryptedEnvVars, - processedInput, + serializedWorkflow, + processedBlockStates, + decryptedEnvVars, + processedInput, workflowVariables ) - + const result = await executor.execute(workflowId) logger.info(`[${requestId}] Workflow execution completed: ${workflowId}`, { @@ -278,7 +285,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ const bodyText = await request.text() logger.info(`[${requestId}] Raw request body:`, bodyText) - + let body = {} if (bodyText && bodyText.trim()) { try { @@ -293,9 +300,9 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ } // Don't double-nest the input if it's already structured - const hasContent = Object.keys(body).length > 0; - const input = hasContent ? { input: body } : {}; - + const hasContent = Object.keys(body).length > 0 + const input = hasContent ? { input: body } : {} + logger.info(`[${requestId}] Input passed to workflow:`, JSON.stringify(input, null, 2)) // Execute workflow with the structured input diff --git a/sim/app/w/[id]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts b/sim/app/w/[id]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts index bd02efd90e..7a16bb60b3 100644 --- a/sim/app/w/[id]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts +++ b/sim/app/w/[id]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts @@ -1,12 +1,12 @@ import { useCallback, useEffect, useRef } from 'react' +import { isEqual } from 'lodash' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' -import { isEqual } from 'lodash' /** * Custom hook to get and set values for a sub-block in a workflow. * Handles complex object values properly by using deep equality comparison. - * + * * @param blockId The ID of the block containing the sub-block * @param subBlockId The ID of the sub-block * @param triggerWorkflowUpdate Whether to trigger a workflow update when the value changes @@ -30,10 +30,7 @@ export function useSubBlockValue( // Get value from subblock store const storeValue = useSubBlockStore( - useCallback( - (state) => state.getValue(blockId, subBlockId), - [blockId, subBlockId] - ) + useCallback((state) => state.getValue(blockId, subBlockId), [blockId, subBlockId]) ) // Update the ref if the store value changes @@ -51,16 +48,19 @@ export function useSubBlockValue( // Use deep comparison to avoid unnecessary updates for complex objects if (!isEqual(valueRef.current, newValue)) { valueRef.current = newValue - + // Ensure we're passing the actual value, not a reference that might change - const valueCopy = newValue === null - ? null - : (typeof newValue === 'object' ? JSON.parse(JSON.stringify(newValue)) : newValue) - + const valueCopy = + newValue === null + ? null + : typeof newValue === 'object' + ? JSON.parse(JSON.stringify(newValue)) + : newValue + // Update the subblock store with the new value // The store's setValue method will now trigger the debounced sync automatically useSubBlockStore.getState().setValue(blockId, subBlockId, valueCopy) - + if (triggerWorkflowUpdate) { useWorkflowStore.getState().triggerUpdate() } diff --git a/sim/app/w/[id]/workflow.tsx b/sim/app/w/[id]/workflow.tsx index 8788839500..ebbdac04af 100644 --- a/sim/app/w/[id]/workflow.tsx +++ b/sim/app/w/[id]/workflow.tsx @@ -80,6 +80,50 @@ function WorkflowContent() { } }, []) + // Handle drops + const findClosestOutput = useCallback( + (newNodePosition: { x: number; y: number }) => { + const existingBlocks = Object.entries(blocks) + .filter(([_, block]) => block.enabled) + .map(([id, block]) => ({ + id, + type: block.type, + position: block.position, + distance: Math.sqrt( + Math.pow(block.position.x - newNodePosition.x, 2) + + Math.pow(block.position.y - newNodePosition.y, 2) + ), + })) + .sort((a, b) => a.distance - b.distance) + + return existingBlocks[0] ? existingBlocks[0] : null + }, + [blocks] + ) + + // Determine the appropriate source handle based on block type + const determineSourceHandle = useCallback((block: { id: string; type: string }) => { + // Default source handle + let sourceHandle = 'source' + + // For condition blocks, use the first condition handle + if (block.type === 'condition') { + // Get just the first condition handle from the DOM + const conditionHandles = document.querySelectorAll( + `[data-nodeid^="${block.id}"][data-handleid^="condition-"]` + ) + if (conditionHandles.length > 0) { + // Extract the full handle ID from the first condition handle + const handleId = conditionHandles[0].getAttribute('data-handleid') + if (handleId) { + sourceHandle = handleId + } + } + } + + return sourceHandle + }, []) + // Listen for toolbar block click events useEffect(() => { const handleAddBlockFromToolbar = (event: CustomEvent) => { @@ -112,13 +156,16 @@ function WorkflowContent() { // Auto-connect logic const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled if (isAutoConnectEnabled && type !== 'starter') { - const closestBlockId = findClosestOutput(centerPosition) - if (closestBlockId) { + const closestBlock = findClosestOutput(centerPosition) + if (closestBlock) { + // Get appropriate source handle + const sourceHandle = determineSourceHandle(closestBlock) + addEdge({ id: crypto.randomUUID(), - source: closestBlockId, + source: closestBlock.id, target: id, - sourceHandle: 'source', + sourceHandle, targetHandle: 'target', type: 'custom', }) @@ -134,7 +181,60 @@ function WorkflowContent() { handleAddBlockFromToolbar as EventListener ) } - }, [project, blocks, addBlock, addEdge]) + }, [project, blocks, addBlock, addEdge, findClosestOutput, determineSourceHandle]) + + // Update the onDrop handler + const onDrop = useCallback( + (event: React.DragEvent) => { + event.preventDefault() + + try { + const data = JSON.parse(event.dataTransfer.getData('application/json')) + if (data.type === 'connectionBlock') return + + const reactFlowBounds = event.currentTarget.getBoundingClientRect() + const position = project({ + x: event.clientX - reactFlowBounds.left, + y: event.clientY - reactFlowBounds.top, + }) + + const blockConfig = getBlock(data.type) + if (!blockConfig) { + logger.error('Invalid block type:', { data }) + return + } + + const id = crypto.randomUUID() + const name = `${blockConfig.name} ${ + Object.values(blocks).filter((b) => b.type === data.type).length + 1 + }` + + addBlock(id, data.type, name, position) + + // Auto-connect logic + const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled + if (isAutoConnectEnabled && data.type !== 'starter') { + const closestBlock = findClosestOutput(position) + if (closestBlock) { + // Get appropriate source handle + const sourceHandle = determineSourceHandle(closestBlock) + + addEdge({ + id: crypto.randomUUID(), + source: closestBlock.id, + target: id, + sourceHandle, + targetHandle: 'target', + type: 'workflowEdge', + }) + } + } + } catch (err) { + logger.error('Error dropping block:', { err }) + } + }, + [project, blocks, addBlock, addEdge, findClosestOutput, determineSourceHandle] + ) // Init workflow useEffect(() => { @@ -305,76 +405,6 @@ function WorkflowContent() { [addEdge] ) - // Handle drops - const findClosestOutput = useCallback( - (newNodePosition: { x: number; y: number }) => { - const existingBlocks = Object.entries(blocks) - .filter(([_, block]) => block.enabled && block.type !== 'condition') - .map(([id, block]) => ({ - id, - position: block.position, - distance: Math.sqrt( - Math.pow(block.position.x - newNodePosition.x, 2) + - Math.pow(block.position.y - newNodePosition.y, 2) - ), - })) - .sort((a, b) => a.distance - b.distance) - - return existingBlocks[0]?.id - }, - [blocks] - ) - - // Update the onDrop handler - const onDrop = useCallback( - (event: React.DragEvent) => { - event.preventDefault() - - try { - const data = JSON.parse(event.dataTransfer.getData('application/json')) - if (data.type === 'connectionBlock') return - - const reactFlowBounds = event.currentTarget.getBoundingClientRect() - const position = project({ - x: event.clientX - reactFlowBounds.left, - y: event.clientY - reactFlowBounds.top, - }) - - const blockConfig = getBlock(data.type) - if (!blockConfig) { - logger.error('Invalid block type:', { data }) - return - } - - const id = crypto.randomUUID() - const name = `${blockConfig.name} ${ - Object.values(blocks).filter((b) => b.type === data.type).length + 1 - }` - - addBlock(id, data.type, name, position) - - // Auto-connect logic - const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled - if (isAutoConnectEnabled && data.type !== 'starter') { - const closestBlockId = findClosestOutput(position) - if (closestBlockId) { - addEdge({ - id: crypto.randomUUID(), - source: closestBlockId, - target: id, - sourceHandle: 'source', - targetHandle: 'target', - type: 'custom', - }) - } - } - } catch (err) { - logger.error('Error dropping block:', { err }) - } - }, - [project, blocks, addBlock, addEdge, findClosestOutput] - ) - // Update onPaneClick to only handle edge selection const onPaneClick = useCallback(() => { setSelectedEdgeId(null) diff --git a/sim/blocks/blocks/twilio.ts b/sim/blocks/blocks/twilio.ts index 09c9ce66da..365e2ca018 100644 --- a/sim/blocks/blocks/twilio.ts +++ b/sim/blocks/blocks/twilio.ts @@ -1,13 +1,12 @@ import { TwilioIcon } from '@/components/icons' -import { BlockCategory, BlockConfig, BlockIcon } from '../types' import { TwilioSMSBlockOutput } from '@/tools/twilio/types' +import { BlockCategory, BlockConfig, BlockIcon } from '../types' export const TwilioSMSBlock: BlockConfig = { type: 'twilio_sms', name: 'Twilio SMS', description: 'Send SMS messages via Twilio', - longDescription: - 'Send text messages to single or multiple recipients using the Twilio API.', + longDescription: 'Send text messages to single or multiple recipients using the Twilio API.', category: 'tools', bgColor: '#F22F46', // Twilio brand color icon: TwilioIcon, @@ -47,12 +46,12 @@ export const TwilioSMSBlock: BlockConfig = { type: 'short-input', layout: 'full', placeholder: 'e.g. +1234567890', - } + }, ], tools: { access: ['twilio_send_sms'], config: { - tool: () => 'twilio_send_sms' + tool: () => 'twilio_send_sms', }, }, inputs: { @@ -60,16 +59,16 @@ export const TwilioSMSBlock: BlockConfig = { message: { type: 'string', required: true }, accountSid: { type: 'string', required: true }, authToken: { type: 'string', required: true }, - fromNumber: { type: 'string', required: true } + fromNumber: { type: 'string', required: true }, }, outputs: { response: { type: { success: 'boolean', - messageId: 'string', - status: 'string', - error: 'string' + messageId: 'any', + status: 'any', + error: 'any', }, }, }, -} +} diff --git a/sim/blocks/blocks/typeform.ts b/sim/blocks/blocks/typeform.ts index fd3d156626..e7ac99cc25 100644 --- a/sim/blocks/blocks/typeform.ts +++ b/sim/blocks/blocks/typeform.ts @@ -3,72 +3,75 @@ import { ToolResponse } from '@/tools/types' import { BlockConfig } from '../types' interface TypeformResponse extends ToolResponse { - output: { - total_items: number - page_count: number - items: Array<{ - landing_id: string - token: string - landed_at: string - submitted_at: string - metadata: { - user_agent: string - platform: string - referer: string - network_id: string - browser: string + output: + | { + total_items: number + page_count: number + items: Array<{ + landing_id: string + token: string + landed_at: string + submitted_at: string + metadata: { + user_agent: string + platform: string + referer: string + network_id: string + browser: string + } + answers: Array<{ + field: { + id: string + type: string + ref: string + } + type: string + [key: string]: any // For different answer types (text, boolean, number, etc.) + }> + hidden: Record + calculated: { + score: number + } + variables: Array<{ + key: string + type: string + [key: string]: any // For different variable types + }> + }> } - answers: Array<{ - field: { + | { + fileUrl: string + contentType: string + filename: string + } + | { + fields: Array<{ + dropoffs: number id: string - type: string + label: string ref: string + title: string + type: string + views: number + }> + form: { + platforms: Array<{ + average_time: number + completion_rate: number + platform: string + responses_count: number + total_visits: number + unique_visits: number + }> + summary: { + average_time: number + completion_rate: number + responses_count: number + total_visits: number + unique_visits: number + } } - type: string - [key: string]: any // For different answer types (text, boolean, number, etc.) - }> - hidden: Record - calculated: { - score: number } - variables: Array<{ - key: string - type: string - [key: string]: any // For different variable types - }> - }> - } | { - fileUrl: string - contentType: string - filename: string - } | { - fields: Array<{ - dropoffs: number - id: string - label: string - ref: string - title: string - type: string - views: number - }> - form: { - platforms: Array<{ - average_time: number - completion_rate: number - platform: string - responses_count: number - total_visits: number - unique_visits: number - }> - summary: { - average_time: number - completion_rate: number - responses_count: number - total_visits: number - unique_visits: number - } - } - } } export const TypeformBlock: BlockConfig = { diff --git a/sim/blocks/index.ts b/sim/blocks/index.ts index ddfd03b3ae..adf5e258b6 100644 --- a/sim/blocks/index.ts +++ b/sim/blocks/index.ts @@ -1,5 +1,6 @@ // Import blocks import { AgentBlock } from './blocks/agent' +import { AirtableBlock } from './blocks/airtable' import { ApiBlock } from './blocks/api' import { ConditionBlock } from './blocks/condition' import { ConfluenceBlock } from './blocks/confluence' @@ -28,14 +29,13 @@ import { StarterBlock } from './blocks/starter' import { SupabaseBlock } from './blocks/supabase' import { TavilyBlock } from './blocks/tavily' import { TranslateBlock } from './blocks/translate' +import { TwilioSMSBlock } from './blocks/twilio' import { TypeformBlock } from './blocks/typeform' import { VisionBlock } from './blocks/vision' import { WhatsAppBlock } from './blocks/whatsapp' import { XBlock } from './blocks/x' import { YouTubeBlock } from './blocks/youtube' -import { AirtableBlock } from './blocks/airtable' import { BlockConfig } from './types' -import { TwilioSMSBlock } from './blocks/twilio' // Export blocks for ease of use export { diff --git a/sim/components/icons.tsx b/sim/components/icons.tsx index e5d7bffeaa..c5843f8d6e 100644 --- a/sim/components/icons.tsx +++ b/sim/components/icons.tsx @@ -1745,11 +1745,11 @@ export function TwilioIcon(props: SVGProps) { fill="none" aria-hidden="true" > - - - - - + + + + + ) } diff --git a/sim/components/ui/command.tsx b/sim/components/ui/command.tsx index c5aee351a7..bd25c67ceb 100644 --- a/sim/components/ui/command.tsx +++ b/sim/components/ui/command.tsx @@ -54,6 +54,12 @@ import { cn } from '@/lib/utils' // This file is not typed correctly from shadcn, so we're disabling the type checker // @ts-nocheck +// This file is not typed correctly from shadcn, so we're disabling the type checker +// @ts-nocheck + +// This file is not typed correctly from shadcn, so we're disabling the type checker +// @ts-nocheck + const Command = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { diff --git a/sim/executor/handlers.ts b/sim/executor/handlers.ts index 2670c37190..59798d5b3d 100644 --- a/sim/executor/handlers.ts +++ b/sim/executor/handlers.ts @@ -819,32 +819,38 @@ export class ApiBlockHandler implements BlockHandler { } try { - let processedInputs = { ...inputs }; - + let processedInputs = { ...inputs } + // Handle body specifically to ensure it's properly processed for API requests if (processedInputs.body !== undefined) { // If body is a string that looks like JSON, parse it if (typeof processedInputs.body === 'string') { try { // Trim whitespace before checking for JSON pattern - const trimmedBody = processedInputs.body.trim(); + const trimmedBody = processedInputs.body.trim() if (trimmedBody.startsWith('{') || trimmedBody.startsWith('[')) { - processedInputs.body = JSON.parse(trimmedBody); - logger.info('[ApiBlockHandler] Parsed JSON body:', JSON.stringify(processedInputs.body, null, 2)); + processedInputs.body = JSON.parse(trimmedBody) + logger.info( + '[ApiBlockHandler] Parsed JSON body:', + JSON.stringify(processedInputs.body, null, 2) + ) } } catch (e) { - logger.info('[ApiBlockHandler] Failed to parse body as JSON, using as string:', e); + logger.info('[ApiBlockHandler] Failed to parse body as JSON, using as string:', e) // Keep as string if parsing fails } } else if (processedInputs.body === null) { // Convert null to undefined for consistency with API expectations - processedInputs.body = undefined; + processedInputs.body = undefined } } - + // Ensure the final processed body is logged - logger.info('[ApiBlockHandler] Final processed request body:', JSON.stringify(processedInputs.body, null, 2)); - + logger.info( + '[ApiBlockHandler] Final processed request body:', + JSON.stringify(processedInputs.body, null, 2) + ) + const result = await executeTool(block.config.tool, { ...processedInputs, _context: { workflowId: context.workflowId }, diff --git a/sim/executor/index.ts b/sim/executor/index.ts index 6c1c1b728c..95d9117c3e 100644 --- a/sim/executor/index.ts +++ b/sim/executor/index.ts @@ -45,12 +45,12 @@ export class Executor { this.validateWorkflow() if (workflowInput) { - this.workflowInput = workflowInput; - logger.info('[Executor] Using workflow input:', JSON.stringify(this.workflowInput, null, 2)); + this.workflowInput = workflowInput + logger.info('[Executor] Using workflow input:', JSON.stringify(this.workflowInput, null, 2)) } else { - this.workflowInput = {}; + this.workflowInput = {} } - + this.loopManager = new LoopManager(workflow.loops || {}) this.resolver = new InputResolver( workflow, @@ -421,70 +421,75 @@ export class Executor { }) } else { */ - // No input format defined or not an array, - // Handle API call - prioritize using the input as-is - if (this.workflowInput && typeof this.workflowInput === 'object') { - // For API calls, extract input from the nested structure if it exists - const inputData = this.workflowInput.input !== undefined - ? this.workflowInput.input // Use the nested input data - : this.workflowInput; // Fallback to direct input - - // Create starter output with both formats for maximum compatibility - const starterOutput = { - response: { - input: inputData, - ...inputData, // Make fields directly accessible at response level - }, - } + // No input format defined or not an array, + // Handle API call - prioritize using the input as-is + if (this.workflowInput && typeof this.workflowInput === 'object') { + // For API calls, extract input from the nested structure if it exists + const inputData = + this.workflowInput.input !== undefined + ? this.workflowInput.input // Use the nested input data + : this.workflowInput // Fallback to direct input - logger.info(`[Executor] API input for starter block:`, { - type: typeof inputData, - isArray: Array.isArray(inputData), - keys: Object.keys(inputData), - inputData: JSON.stringify(inputData, null, 2), - }); - - logger.info(`[Executor] Final starter block output:`, JSON.stringify(starterOutput, null, 2)); - - context.blockStates.set(starterBlock.id, { - output: starterOutput, - executed: true, - executionTime: 0, - }) - } else { - // Fallback for other cases - const starterOutput = { - response: { - input: this.workflowInput, - }, - } - - logger.info(`[Executor] Simple starter output:`, JSON.stringify(starterOutput, null, 2)); - - context.blockStates.set(starterBlock.id, { - output: starterOutput, - executed: true, - executionTime: 0, - }) + // Create starter output with both formats for maximum compatibility + const starterOutput = { + response: { + input: inputData, + ...inputData, // Make fields directly accessible at response level + }, } + + logger.info(`[Executor] API input for starter block:`, { + type: typeof inputData, + isArray: Array.isArray(inputData), + keys: Object.keys(inputData), + inputData: JSON.stringify(inputData, null, 2), + }) + + logger.info( + `[Executor] Final starter block output:`, + JSON.stringify(starterOutput, null, 2) + ) + + context.blockStates.set(starterBlock.id, { + output: starterOutput, + executed: true, + executionTime: 0, + }) + } else { + // Fallback for other cases + const starterOutput = { + response: { + input: this.workflowInput, + }, + } + + logger.info(`[Executor] Simple starter output:`, JSON.stringify(starterOutput, null, 2)) + + context.blockStates.set(starterBlock.id, { + output: starterOutput, + executed: true, + executionTime: 0, + }) + } //} // End of inputFormat conditional } catch (e) { logger.warn('Error processing starter block input format:', e) - + // Fallback to raw input with both paths accessible // Ensure we handle both input formats - const inputData = this.workflowInput.input !== undefined - ? this.workflowInput.input // Use nested input if available - : this.workflowInput; // Fallback to direct input - + const inputData = + this.workflowInput.input !== undefined + ? this.workflowInput.input // Use nested input if available + : this.workflowInput // Fallback to direct input + const starterOutput = { response: { input: inputData, ...inputData, // Add input fields directly at response level too }, } - - logger.info(`[Executor] Fallback starter output:`, JSON.stringify(starterOutput, null, 2)); + + logger.info(`[Executor] Fallback starter output:`, JSON.stringify(starterOutput, null, 2)) context.blockStates.set(starterBlock.id, { output: starterOutput, @@ -493,8 +498,8 @@ export class Executor { }) } // Ensure the starter block is in the active execution path - context.activeExecutionPath.add(starterBlock.id); - // Mark the starter block as executed + context.activeExecutionPath.add(starterBlock.id) + // Mark the starter block as executed context.executedBlocks.add(starterBlock.id) // Add all blocks connected to the starter to the active execution path @@ -688,18 +693,24 @@ export class Executor { if (starterBlock) { const starterState = context.blockStates.get(starterBlock.id) if (!starterState) { - logger.warn(`Starter block state not found when executing ${block.metadata?.name || blockId}. This may cause reference errors.`) + logger.warn( + `Starter block state not found when executing ${block.metadata?.name || blockId}. This may cause reference errors.` + ) } else { - logger.debug(`Starter block state available for ${block.metadata?.name || blockId}:`, - JSON.stringify(starterState.output || {}, null, 2)); + logger.debug( + `Starter block state available for ${block.metadata?.name || blockId}:`, + JSON.stringify(starterState.output || {}, null, 2) + ) } } // Resolve inputs (which will look up references to other blocks including starter) const inputs = this.resolver.resolveInputs(block, context) - logger.debug(`Resolved inputs for ${block.metadata?.name || blockId}:`, - JSON.stringify(inputs, null, 2)); - + logger.debug( + `Resolved inputs for ${block.metadata?.name || blockId}:`, + JSON.stringify(inputs, null, 2) + ) + // Find the appropriate handler const handler = this.blockHandlers.find((h) => h.canHandle(block)) if (!handler) { diff --git a/sim/executor/resolver.ts b/sim/executor/resolver.ts index 676b95a0c1..46de909c51 100644 --- a/sim/executor/resolver.ts +++ b/sim/executor/resolver.ts @@ -1,7 +1,7 @@ +import { createLogger } from '@/lib/logs/console-logger' import { SerializedBlock, SerializedWorkflow } from '@/serializer/types' import { LoopManager } from './loops' import { ExecutionContext } from './types' -import { createLogger } from '@/lib/logs/console-logger' const logger = createLogger('InputResolver') @@ -82,28 +82,28 @@ export class InputResolver { // Special handling for different block types const isFunctionBlock = block.metadata?.id === 'function' const isApiBlock = block.metadata?.id === 'api' - + // For function blocks, we need special handling for code input if (isFunctionBlock && key === 'code') { // For code input in function blocks, we don't want to parse JSON result[key] = resolvedValue - logger.debug(`[resolveInputs] Function block code input preserved as string`); - } + logger.debug(`[resolveInputs] Function block code input preserved as string`) + } // For API blocks, handle body input specially else if (isApiBlock && key === 'body') { try { // If it's JSON-looking, preserve its structure if (resolvedValue.trim().startsWith('{') || resolvedValue.trim().startsWith('[')) { result[key] = JSON.parse(resolvedValue) - logger.debug(`[resolveInputs] API block body parsed as JSON object`); + logger.debug(`[resolveInputs] API block body parsed as JSON object`) } else { result[key] = resolvedValue - logger.debug(`[resolveInputs] API block body preserved as string`); + logger.debug(`[resolveInputs] API block body preserved as string`) } } catch { // If parsing fails, keep as string result[key] = resolvedValue - logger.debug(`[resolveInputs] API block body JSON parsing failed, keeping as string`); + logger.debug(`[resolveInputs] API block body JSON parsing failed, keeping as string`) } } // For other inputs, try to convert JSON strings to objects @@ -111,7 +111,7 @@ export class InputResolver { try { if (resolvedValue.startsWith('{') || resolvedValue.startsWith('[')) { result[key] = JSON.parse(resolvedValue) - logger.debug(`[resolveInputs] Parsed JSON value for ${key}`); + logger.debug(`[resolveInputs] Parsed JSON value for ${key}`) } else { result[key] = resolvedValue } @@ -158,16 +158,18 @@ export class InputResolver { if (foundVariable) { const [_, variable] = foundVariable - + // Process variable value based on its type let processedValue = variable.value - + // Handle string values that could be stored with quotes if (variable.type === 'string' && typeof processedValue === 'string') { // If the string value starts and ends with quotes, remove them const trimmed = processedValue.trim() - if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || - (trimmed.startsWith("'") && trimmed.endsWith("'"))) { + if ( + (trimmed.startsWith('"') && trimmed.endsWith('"')) || + (trimmed.startsWith("'") && trimmed.endsWith("'")) + ) { // Remove the quotes and unescape any escaped quotes processedValue = trimmed.slice(1, -1).replace(/\\"/g, '"').replace(/\\'/g, "'") } @@ -184,30 +186,32 @@ export class InputResolver { } } // Handle object/array values that might be stored as JSON strings - else if ((variable.type === 'object' || variable.type === 'array') && - typeof processedValue === 'string') { + else if ( + (variable.type === 'object' || variable.type === 'array') && + typeof processedValue === 'string' + ) { try { processedValue = JSON.parse(processedValue) } catch (e) { // Keep as string if parsing fails } } - + // Determine if this needs to be a code-compatible string literal - const needsCodeStringLiteral = this.needsCodeStringLiteral(currentBlock, value); - + const needsCodeStringLiteral = this.needsCodeStringLiteral(currentBlock, value) + // Format the processed value for insertion into the string based on context - let formattedValue: string; - + let formattedValue: string + if (variable.type === 'string' && needsCodeStringLiteral) { // For code contexts like function and condition blocks, properly quote strings - formattedValue = JSON.stringify(processedValue); + formattedValue = JSON.stringify(processedValue) } else if (typeof processedValue === 'object' && processedValue !== null) { // For objects, always stringify - formattedValue = JSON.stringify(processedValue); + formattedValue = JSON.stringify(processedValue) } else { // For other types in normal contexts, use simple string conversion - formattedValue = String(processedValue); + formattedValue = String(processedValue) } resolvedValue = resolvedValue.replace(match, formattedValue) @@ -220,51 +224,51 @@ export class InputResolver { /** * Determines if a value needs to be formatted as a code-compatible string literal * based on the block type and context. Handles JavaScript and other code contexts. - * + * * @param block - The block where the value is being used * @param expression - The expression containing the value * @returns Whether the value should be formatted as a string literal */ private needsCodeStringLiteral(block?: SerializedBlock, expression?: string): boolean { - if (!block) return false; - + if (!block) return false + // These block types execute code and need properly formatted string literals - const codeExecutionBlocks = ['function', 'condition']; - + const codeExecutionBlocks = ['function', 'condition'] + // Check if this is a block that executes code if (block.metadata?.id && codeExecutionBlocks.includes(block.metadata.id)) { - return true; + return true } - + // Check if the expression is likely part of code if (expression) { const codeIndicators = [ // Function/method calls - /\(\s*$/, // Function call - /\.\w+\s*\(/, // Method call - + /\(\s*$/, // Function call + /\.\w+\s*\(/, // Method call + // JavaScript/Python operators - /[=<>!+\-*/%](?:==?)?/, // Common operators - /\+=|-=|\*=|\/=|%=|\*\*=?/, // Assignment operators - + /[=<>!+\-*/%](?:==?)?/, // Common operators + /\+=|-=|\*=|\/=|%=|\*\*=?/, // Assignment operators + // JavaScript keywords /\b(if|else|for|while|return|var|let|const|function)\b/, - + // Python keywords /\b(if|else|elif|for|while|def|return|import|from|as|class|with|try|except)\b/, - + // Common code patterns - /^['"]use strict['"];?$/, // JS strict mode - /\$\{.+?\}/, // JS template literals - /f['"].*?['"]/, // Python f-strings - /\bprint\s*\(/, // Python print - /\bconsole\.\w+\(/ // JS console methods - ]; - - return codeIndicators.some(pattern => pattern.test(expression)); + /^['"]use strict['"];?$/, // JS strict mode + /\$\{.+?\}/, // JS template literals + /f['"].*?['"]/, // Python f-strings + /\bprint\s*\(/, // Python print + /\bconsole\.\w+\(/, // JS console methods + ] + + return codeIndicators.some((pattern) => pattern.test(expression)) } - - return false; + + return false } /** @@ -301,8 +305,8 @@ export class InputResolver { blockRef, pathParts, currentBlock: currentBlock.id, - currentBlockType: currentBlock.metadata?.id - }); + currentBlockType: currentBlock.metadata?.id, + }) // Special case for "start" references // This allows users to reference the starter block using @@ -311,80 +315,100 @@ export class InputResolver { // Find the starter block const starterBlock = this.workflow.blocks.find((block) => block.metadata?.id === 'starter') if (starterBlock) { - logger.debug(`[resolveBlockReferences] Found starter block with ID: ${starterBlock.id}`); - + logger.debug(`[resolveBlockReferences] Found starter block with ID: ${starterBlock.id}`) + const blockState = context.blockStates.get(starterBlock.id) if (blockState) { - logger.debug(`[resolveBlockReferences] Starter block state:`, JSON.stringify(blockState, null, 2)); - + logger.debug( + `[resolveBlockReferences] Starter block state:`, + JSON.stringify(blockState, null, 2) + ) + // Navigate through the path parts let replacementValue: any = blockState.output - + // Log the initial output value from the starter block - logger.debug(`[resolveBlockReferences] Initial starter output:`, JSON.stringify(replacementValue, null, 2)); - + logger.debug( + `[resolveBlockReferences] Initial starter output:`, + JSON.stringify(replacementValue, null, 2) + ) + for (const part of pathParts) { - logger.debug(`[resolveBlockReferences] Navigating path part: ${part}`, { - currentValue: typeof replacementValue === 'object' ? - JSON.stringify(replacementValue) : replacementValue - }); - + logger.debug(`[resolveBlockReferences] Navigating path part: ${part}`, { + currentValue: + typeof replacementValue === 'object' + ? JSON.stringify(replacementValue) + : replacementValue, + }) + if (!replacementValue || typeof replacementValue !== 'object') { - logger.warn(`[resolveBlockReferences] Invalid path "${part}" - replacementValue is not an object:`, replacementValue); + logger.warn( + `[resolveBlockReferences] Invalid path "${part}" - replacementValue is not an object:`, + replacementValue + ) throw new Error(`Invalid path "${part}" in "${path}" for starter block.`) } - + replacementValue = replacementValue[part] - + if (replacementValue === undefined) { - logger.warn(`[resolveBlockReferences] No value found at path "${part}" in starter block.`); + logger.warn( + `[resolveBlockReferences] No value found at path "${part}" in starter block.` + ) throw new Error(`No value found at path "${path}" in starter block.`) } } // Format the value based on block type and path - let formattedValue: string; - + let formattedValue: string + // Special handling for all blocks referencing starter input if (blockRef.toLowerCase() === 'start' && pathParts.join('.').includes('input')) { - const blockType = currentBlock.metadata?.id; - + const blockType = currentBlock.metadata?.id + // Format based on which block is consuming this value if (typeof replacementValue === 'object' && replacementValue !== null) { // For function blocks, preserve the object structure for code usage if (blockType === 'function') { - logger.debug(`[resolveBlockReferences] Special handling for function input:`, - JSON.stringify(replacementValue, null, 2)); - formattedValue = JSON.stringify(replacementValue); + logger.debug( + `[resolveBlockReferences] Special handling for function input:`, + JSON.stringify(replacementValue, null, 2) + ) + formattedValue = JSON.stringify(replacementValue) } // For API blocks, handle body special case else if (blockType === 'api') { - logger.debug(`[resolveBlockReferences] Special handling for API input:`, - JSON.stringify(replacementValue, null, 2)); - formattedValue = JSON.stringify(replacementValue); - } + logger.debug( + `[resolveBlockReferences] Special handling for API input:`, + JSON.stringify(replacementValue, null, 2) + ) + formattedValue = JSON.stringify(replacementValue) + } // For condition blocks, ensure proper formatting else if (blockType === 'condition') { - logger.debug(`[resolveBlockReferences] Special handling for condition input:`, - JSON.stringify(replacementValue, null, 2)); - formattedValue = this.stringifyForCondition(replacementValue); + logger.debug( + `[resolveBlockReferences] Special handling for condition input:`, + JSON.stringify(replacementValue, null, 2) + ) + formattedValue = this.stringifyForCondition(replacementValue) } // For all other blocks, stringify objects else { - formattedValue = JSON.stringify(replacementValue); + formattedValue = JSON.stringify(replacementValue) } } else { // For primitive values - formattedValue = String(replacementValue); + formattedValue = String(replacementValue) } } else { // Standard handling for non-input references - formattedValue = typeof replacementValue === 'object' - ? JSON.stringify(replacementValue) - : String(replacementValue); + formattedValue = + typeof replacementValue === 'object' + ? JSON.stringify(replacementValue) + : String(replacementValue) } - - logger.debug(`[resolveBlockReferences] Resolved value:`, formattedValue); + + logger.debug(`[resolveBlockReferences] Resolved value:`, formattedValue) resolvedValue = resolvedValue.replace(match, formattedValue) continue } @@ -578,7 +602,10 @@ export class InputResolver { if (currentBlock.metadata?.id === 'condition') { formattedValue = this.stringifyForCondition(replacementValue) - } else if (typeof replacementValue === 'string' && this.needsCodeStringLiteral(currentBlock, value)) { + } else if ( + typeof replacementValue === 'string' && + this.needsCodeStringLiteral(currentBlock, value) + ) { // For code blocks, quote string values properly for the given language formattedValue = JSON.stringify(replacementValue) } else { diff --git a/sim/lib/oauth.ts b/sim/lib/oauth.ts index 292b3bc707..9f62911def 100644 --- a/sim/lib/oauth.ts +++ b/sim/lib/oauth.ts @@ -1,5 +1,6 @@ import { ReactNode } from 'react' import { + AirtableIcon, ConfluenceIcon, GithubIcon, GmailIcon, @@ -10,14 +11,20 @@ import { GoogleSheetsIcon, SupabaseIcon, xIcon, - AirtableIcon, } from '@/components/icons' import { createLogger } from '@/lib/logs/console-logger' const logger = createLogger('OAuth') // Define the base OAuth provider type -export type OAuthProvider = 'google' | 'github' | 'x' | 'supabase' | 'confluence' | 'airtable' | string +export type OAuthProvider = + | 'google' + | 'github' + | 'x' + | 'supabase' + | 'confluence' + | 'airtable' + | string export type OAuthService = | 'google' | 'google-email' diff --git a/sim/stores/workflows/subblock/store.ts b/sim/stores/workflows/subblock/store.ts index f5f57a491b..9c432ab982 100644 --- a/sim/stores/workflows/subblock/store.ts +++ b/sim/stores/workflows/subblock/store.ts @@ -59,7 +59,7 @@ export const useSubBlockStore = create()( // Persist to localStorage for backup const currentValues = get().workflowValues[activeWorkflowId] || {} saveSubblockValues(activeWorkflowId, currentValues) - + // Trigger debounced sync to DB get().syncWithDB() }, @@ -83,7 +83,7 @@ export const useSubBlockStore = create()( })) saveSubblockValues(activeWorkflowId, {}) - + // Trigger sync to DB immediately on clear workflowSync.sync() }, @@ -121,14 +121,14 @@ export const useSubBlockStore = create()( // Save to localStorage saveSubblockValues(workflowId, values) }, - + // Debounced sync function to trigger DB sync syncWithDB: () => { // Clear any existing timeout if (syncDebounceTimer) { clearTimeout(syncDebounceTimer) } - + // Set new timeout syncDebounceTimer = setTimeout(() => { // Trigger workflow sync to DB diff --git a/sim/tools/airtable/index.test.ts b/sim/tools/airtable/index.test.ts index 677d75ac1e..ba933df947 100644 --- a/sim/tools/airtable/index.test.ts +++ b/sim/tools/airtable/index.test.ts @@ -5,7 +5,7 @@ */ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { ToolTester } from '../__test-utils__/test-tools' -import { airtableReadTool, airtableWriteTool, airtableUpdateTool } from './index' +import { airtableReadTool, airtableUpdateTool, airtableWriteTool } from './index' describe('Airtable Tools Integration', () => { let tester: ToolTester @@ -30,7 +30,7 @@ describe('Airtable Tools Integration', () => { tableId: 'table456', accessToken: 'token789', maxRecords: 100, - filterFormula: "Status='Active'" + filterFormula: "Status='Active'", } const url = tester.getRequestUrl(params) @@ -46,9 +46,9 @@ describe('Airtable Tools Integration', () => { const mockData = { records: [ { id: 'rec1', fields: { Name: 'Test 1' } }, - { id: 'rec2', fields: { Name: 'Test 2' } } + { id: 'rec2', fields: { Name: 'Test 2' } }, ], - offset: 'next_page_token' + offset: 'next_page_token', } tester.setup(mockData) @@ -56,7 +56,7 @@ describe('Airtable Tools Integration', () => { const result = await tester.execute({ baseId: 'base123', tableId: 'table456', - accessToken: 'token789' + accessToken: 'token789', }) expect(result.success).toBe(true) @@ -76,7 +76,7 @@ describe('Airtable Tools Integration', () => { baseId: 'base123', tableId: 'table456', accessToken: 'token789', - records: [{ fields: { Name: 'New Record' } }] + records: [{ fields: { Name: 'New Record' } }], } const url = tester.getRequestUrl(params) @@ -90,9 +90,7 @@ describe('Airtable Tools Integration', () => { test('should handle successful write response', async () => { const mockData = { - records: [ - { id: 'rec1', fields: { Name: 'New Record' } } - ] + records: [{ id: 'rec1', fields: { Name: 'New Record' } }], } tester.setup(mockData) @@ -101,7 +99,7 @@ describe('Airtable Tools Integration', () => { baseId: 'base123', tableId: 'table456', accessToken: 'token789', - records: [{ fields: { Name: 'New Record' } }] + records: [{ fields: { Name: 'New Record' } }], }) expect(result.success).toBe(true) @@ -121,7 +119,7 @@ describe('Airtable Tools Integration', () => { tableId: 'table456', recordId: 'rec789', accessToken: 'token789', - fields: { Name: 'Updated Record' } + fields: { Name: 'Updated Record' }, } const url = tester.getRequestUrl(params) @@ -136,7 +134,7 @@ describe('Airtable Tools Integration', () => { test('should handle successful update response', async () => { const mockData = { id: 'rec789', - fields: { Name: 'Updated Record' } + fields: { Name: 'Updated Record' }, } tester.setup(mockData) @@ -146,7 +144,7 @@ describe('Airtable Tools Integration', () => { tableId: 'table456', recordId: 'rec789', accessToken: 'token789', - fields: { Name: 'Updated Record' } + fields: { Name: 'Updated Record' }, }) expect(result.success).toBe(true) @@ -158,17 +156,17 @@ describe('Airtable Tools Integration', () => { test('should handle error responses', async () => { tester = new ToolTester(airtableReadTool) - + const errorMessage = 'Invalid API key' tester.setup({ error: errorMessage }, { ok: false, status: 401 }) const result = await tester.execute({ baseId: 'base123', tableId: 'table456', - accessToken: 'invalid_token' + accessToken: 'invalid_token', }) expect(result.success).toBe(false) expect(result.error).toContain('Failed to read Airtable records') }) -}) \ No newline at end of file +}) diff --git a/sim/tools/airtable/index.ts b/sim/tools/airtable/index.ts index 23ec73b475..d2a8d9b4ad 100644 --- a/sim/tools/airtable/index.ts +++ b/sim/tools/airtable/index.ts @@ -1,7 +1,7 @@ import { readTool } from './read' -import { writeTool } from './write' import { updateTool } from './update' +import { writeTool } from './write' export const airtableReadTool = readTool export const airtableWriteTool = writeTool -export const airtableUpdateTool = updateTool \ No newline at end of file +export const airtableUpdateTool = updateTool diff --git a/sim/tools/airtable/read.ts b/sim/tools/airtable/read.ts index 169af835b3..08f01ba9c7 100644 --- a/sim/tools/airtable/read.ts +++ b/sim/tools/airtable/read.ts @@ -6,12 +6,12 @@ export const readTool: ToolConfig = { name: 'Airtable Read Records', description: 'Read records from an Airtable table', version: '1.0.0', - + oauth: { required: true, provider: 'airtable', }, - + params: { accessToken: { type: 'string', @@ -39,7 +39,7 @@ export const readTool: ToolConfig = { description: 'Formula to filter records', }, }, - + request: { url: (params) => { const url = `https://api.airtable.com/v0/${params.baseId}/${params.tableId}` @@ -54,11 +54,11 @@ export const readTool: ToolConfig = { }, method: 'GET', headers: (params) => ({ - 'Authorization': `Bearer ${params.accessToken}`, + Authorization: `Bearer ${params.accessToken}`, 'Content-Type': 'application/json', }), }, - + transformResponse: async (response) => { const data = await response.json() return { @@ -72,8 +72,8 @@ export const readTool: ToolConfig = { }, } }, - + transformError: (error) => { return `Failed to read Airtable records: ${error.message}` }, -} \ No newline at end of file +} diff --git a/sim/tools/airtable/types.ts b/sim/tools/airtable/types.ts index eb864de67f..48fc1b7daa 100644 --- a/sim/tools/airtable/types.ts +++ b/sim/tools/airtable/types.ts @@ -66,4 +66,4 @@ export interface AirtableUpdateParams { tableId: string recordId: string fields: Record -} \ No newline at end of file +} diff --git a/sim/tools/airtable/write.ts b/sim/tools/airtable/write.ts index 8680994168..43644749fd 100644 --- a/sim/tools/airtable/write.ts +++ b/sim/tools/airtable/write.ts @@ -6,12 +6,12 @@ export const writeTool: ToolConfig = name: 'Airtable Write Records', description: 'Write new records to an Airtable table', version: '1.0.0', - + oauth: { required: true, provider: 'airtable', }, - + params: { accessToken: { type: 'string', @@ -34,18 +34,17 @@ export const writeTool: ToolConfig = description: 'Array of records to create', }, }, - + request: { - url: (params) => - `https://api.airtable.com/v0/${params.baseId}/${params.tableId}`, + url: (params) => `https://api.airtable.com/v0/${params.baseId}/${params.tableId}`, method: 'POST', headers: (params) => ({ - 'Authorization': `Bearer ${params.accessToken}`, + Authorization: `Bearer ${params.accessToken}`, 'Content-Type': 'application/json', }), body: (params) => ({ records: params.records }), }, - + transformResponse: async (response) => { const data = await response.json() return { @@ -58,8 +57,8 @@ export const writeTool: ToolConfig = }, } }, - + transformError: (error) => { return `Failed to write Airtable records: ${error.message}` }, -} \ No newline at end of file +} diff --git a/sim/tools/file/index.ts b/sim/tools/file/index.ts index 11d06e3daa..855c19b501 100644 --- a/sim/tools/file/index.ts +++ b/sim/tools/file/index.ts @@ -1,3 +1,3 @@ import { fileParserTool } from './parser' -export const fileParseTool = fileParserTool \ No newline at end of file +export const fileParseTool = fileParserTool diff --git a/sim/tools/index.ts b/sim/tools/index.ts index ffb618d56e..6dfb41f2d9 100644 --- a/sim/tools/index.ts +++ b/sim/tools/index.ts @@ -1,6 +1,7 @@ import { createLogger } from '@/lib/logs/console-logger' import { useCustomToolsStore } from '@/stores/custom-tools/store' import { useEnvironmentStore } from '@/stores/settings/environment/store' +import { airtableReadTool, airtableUpdateTool, airtableWriteTool } from './airtable' import { confluenceListTool, confluenceRetrieveTool, confluenceUpdateTool } from './confluence' import { docsCreateTool, docsReadTool, docsWriteTool } from './docs' import { driveDownloadTool, driveListTool, driveUploadTool } from './drive' @@ -37,6 +38,7 @@ import { sheetsReadTool, sheetsUpdateTool, sheetsWriteTool } from './sheets' import { slackMessageTool } from './slack/message' import { supabaseInsertTool, supabaseQueryTool, supabaseUpdateTool } from './supabase' import { tavilyExtractTool, tavilySearchTool } from './tavily' +import { sendSMSTool } from './twilio/send' import { typeformFilesTool, typeformInsightsTool, typeformResponsesTool } from './typeform' import { OAuthTokenPayload, ToolConfig, ToolResponse } from './types' import { formatRequestParams, validateToolRequest } from './utils' @@ -44,8 +46,6 @@ import { visionTool } from './vision/vision' import { whatsappSendMessageTool } from './whatsapp' import { xReadTool, xSearchTool, xUserTool, xWriteTool } from './x' import { youtubeSearchTool } from './youtube/search' -import { sendSMSTool } from './twilio/sendSMS' -import { airtableReadTool, airtableWriteTool, airtableUpdateTool } from './airtable' const logger = createLogger('Tools') diff --git a/sim/tools/twilio/index.ts b/sim/tools/twilio/index.ts index 805225163e..c9579561c5 100644 --- a/sim/tools/twilio/index.ts +++ b/sim/tools/twilio/index.ts @@ -1,3 +1,3 @@ -import { sendSMSTool } from './sendSMS' +import { sendSMSTool } from './send' export const twilioSendSMSTool = sendSMSTool diff --git a/sim/tools/twilio/sendSMS.ts b/sim/tools/twilio/send.ts similarity index 84% rename from sim/tools/twilio/sendSMS.ts rename to sim/tools/twilio/send.ts index 4350c9791f..b8dc3c9a28 100644 --- a/sim/tools/twilio/sendSMS.ts +++ b/sim/tools/twilio/send.ts @@ -1,8 +1,8 @@ import { createLogger } from '@/lib/logs/console-logger' import { ToolConfig } from '../types' -import { TwilioSMSBlockOutput, TwilioSendSMSParams } from './types' +import { TwilioSendSMSParams, TwilioSMSBlockOutput } from './types' -const logger = createLogger('Twilio Send SMS Tool') +const logger = createLogger('Twilio Send SMS Tool') export const sendSMSTool: ToolConfig = { id: 'twilio_send_sms', @@ -14,30 +14,30 @@ export const sendSMSTool: ToolConfig phoneNumbers: { type: 'string', required: true, - description: 'Phone numbers to send the message to, separated by newlines' + description: 'Phone numbers to send the message to, separated by newlines', }, message: { type: 'string', required: true, - description: 'Message to send' + description: 'Message to send', }, accountSid: { type: 'string', required: true, description: 'Twilio Account SID', - requiredForToolCall: true + requiredForToolCall: true, }, authToken: { type: 'string', required: true, description: 'Twilio Auth Token', - requiredForToolCall: true + requiredForToolCall: true, }, fromNumber: { type: 'string', required: true, - description: 'Twilio phone number to send the message from' - } + description: 'Twilio phone number to send the message from', + }, }, request: { @@ -45,7 +45,7 @@ export const sendSMSTool: ToolConfig if (!params.accountSid) { throw new Error('Twilio Account SID is required') } - const url = `https://api.twilio.com/2010-04-01/Accounts/${params.accountSid}/Messages.json`; + const url = `https://api.twilio.com/2010-04-01/Accounts/${params.accountSid}/Messages.json` return url }, method: 'POST', @@ -57,7 +57,7 @@ export const sendSMSTool: ToolConfig const authToken = Buffer.from(`${params.accountSid}:${params.authToken}`).toString('base64') const headers = { Authorization: `Basic ${authToken}`, - 'Content-Type': 'application/x-www-form-urlencoded' + 'Content-Type': 'application/x-www-form-urlencoded', } return headers }, @@ -74,27 +74,28 @@ export const sendSMSTool: ToolConfig // Get first phone number if multiple are provided const toNumber = params.phoneNumbers.split('\n')[0].trim() - + // Create a URLSearchParams object and convert to string const formData = new URLSearchParams() formData.append('To', toNumber) formData.append('From', params.fromNumber) formData.append('Body', params.message) - + const formDataString = formData.toString() return { body: formDataString } - } + }, }, transformResponse: async (response) => { const data = await response.json() if (!response.ok) { - const errorMessage = data.error?.message || data.message || `Failed to send SMS (HTTP ${response.status})` + const errorMessage = + data.error?.message || data.message || `Failed to send SMS (HTTP ${response.status})` logger.error('Twilio API error:', data) throw new Error(errorMessage) } - + logger.info('Twilio Response:', data) logger.info('Twilio Response type:', typeof data) return { @@ -102,14 +103,14 @@ export const sendSMSTool: ToolConfig output: { success: true, messageId: data.sid, - status: data.status + status: data.status, }, - error: undefined + error: undefined, } }, transformError: (error) => { logger.error('Twilio tool error:', { error }) return `SMS sending failed: ${error.message || 'Unknown error occurred'}` - } -} \ No newline at end of file + }, +} diff --git a/sim/tools/twilio/types.ts b/sim/tools/twilio/types.ts index 1176c4aa76..b03260f468 100644 --- a/sim/tools/twilio/types.ts +++ b/sim/tools/twilio/types.ts @@ -10,9 +10,9 @@ export interface TwilioSendSMSParams { export interface TwilioSMSBlockOutput extends ToolResponse { output: { - success: boolean - messageId?: string - status?: string - error?: string + success: boolean + messageId?: string + status?: string + error?: string } } diff --git a/sim/tools/typeform/files.test.ts b/sim/tools/typeform/files.test.ts index c599365c75..86e3d5d45f 100644 --- a/sim/tools/typeform/files.test.ts +++ b/sim/tools/typeform/files.test.ts @@ -16,7 +16,7 @@ describe('Typeform Files Tool', () => { // Mock file response const mockFileResponseHeaders = { 'content-type': 'application/pdf', - 'content-disposition': 'attachment; filename="test-file.pdf"' + 'content-disposition': 'attachment; filename="test-file.pdf"', } beforeEach(() => { @@ -35,7 +35,7 @@ describe('Typeform Files Tool', () => { responseId: 'resp456', fieldId: 'field789', filename: 'test-file.pdf', - apiKey: 'test-token' + apiKey: 'test-token', } expect(tester.getRequestUrl(params)).toBe( @@ -50,7 +50,7 @@ describe('Typeform Files Tool', () => { fieldId: 'field789', filename: 'test-file.pdf', inline: true, - apiKey: 'test-token' + apiKey: 'test-token', } const url = tester.getRequestUrl(params) @@ -63,7 +63,7 @@ describe('Typeform Files Tool', () => { responseId: 'resp&with#chars', fieldId: 'field-id', filename: 'file name.pdf', - apiKey: 'test-token' + apiKey: 'test-token', } const url = tester.getRequestUrl(params) @@ -80,7 +80,7 @@ describe('Typeform Files Tool', () => { responseId: 'resp456', fieldId: 'field789', filename: 'test-file.pdf', - apiKey: 'test-token' + apiKey: 'test-token', } const headers = tester.getRequestHeaders(params) @@ -93,7 +93,7 @@ describe('Typeform Files Tool', () => { test('should transform file data correctly', async () => { // Setup mock response for binary file data tester.setup('file-content-binary-data', { - headers: mockFileResponseHeaders + headers: mockFileResponseHeaders, }) // Execute the tool @@ -102,7 +102,7 @@ describe('Typeform Files Tool', () => { responseId: 'resp456', fieldId: 'field789', filename: 'test-file.pdf', - apiKey: 'test-token' + apiKey: 'test-token', }) // Check the result @@ -115,7 +115,7 @@ describe('Typeform Files Tool', () => { test('should handle missing content-disposition header', async () => { // Setup mock response without content-disposition tester.setup('file-content-binary-data', { - headers: { 'content-type': 'application/pdf' } + headers: { 'content-type': 'application/pdf' }, }) // Execute the tool @@ -124,7 +124,7 @@ describe('Typeform Files Tool', () => { responseId: 'resp456', fieldId: 'field789', filename: 'test-file.pdf', - apiKey: 'test-token' + apiKey: 'test-token', }) // Check the result @@ -147,7 +147,7 @@ describe('Typeform Files Tool', () => { responseId: 'resp456', fieldId: 'field789', filename: 'nonexistent.pdf', - apiKey: 'test-token' + apiKey: 'test-token', }) // Check error handling @@ -165,7 +165,7 @@ describe('Typeform Files Tool', () => { responseId: 'resp456', fieldId: 'field789', filename: 'test-file.pdf', - apiKey: 'invalid-token' + apiKey: 'invalid-token', }) // Check error handling @@ -183,7 +183,7 @@ describe('Typeform Files Tool', () => { responseId: 'resp456', fieldId: 'field789', filename: 'test-file.pdf', - apiKey: 'test-token' + apiKey: 'test-token', }) // Check error handling @@ -191,4 +191,4 @@ describe('Typeform Files Tool', () => { expect(result.error).toBeDefined() }) }) -}) \ No newline at end of file +}) diff --git a/sim/tools/typeform/files.ts b/sim/tools/typeform/files.ts index 1938fcb7e2..d445e7009c 100644 --- a/sim/tools/typeform/files.ts +++ b/sim/tools/typeform/files.ts @@ -60,88 +60,88 @@ export const filesTool: ToolConfig = const encodedResponseId = encodeURIComponent(params.responseId) const encodedFieldId = encodeURIComponent(params.fieldId) const encodedFilename = encodeURIComponent(params.filename) - + let url = `https://api.typeform.com/forms/${encodedFormId}/responses/${encodedResponseId}/fields/${encodedFieldId}/files/${encodedFilename}` - + // Add the inline parameter if provided if (params.inline !== undefined) { url += `?inline=${params.inline}` } - + return url }, method: 'GET', headers: (params) => ({ - 'Authorization': `Bearer ${params.apiKey}`, + Authorization: `Bearer ${params.apiKey}`, 'Content-Type': 'application/json', }), }, transformResponse: async (response: Response, params?: TypeformFilesParams) => { if (!response.ok) { - let errorMessage = response.statusText || 'Unknown error'; - + let errorMessage = response.statusText || 'Unknown error' + try { - const errorData = await response.json(); + const errorData = await response.json() if (errorData && errorData.message) { - errorMessage = errorData.message; + errorMessage = errorData.message } else if (errorData && errorData.description) { - errorMessage = errorData.description; + errorMessage = errorData.description } else if (typeof errorData === 'string') { - errorMessage = errorData; + errorMessage = errorData } } catch (e) { // If we can't parse the error as JSON, just use the status text } - - throw new Error(`Typeform API error (${response.status}): ${errorMessage}`); + + throw new Error(`Typeform API error (${response.status}): ${errorMessage}`) } - + // For file downloads, we get the file directly - const contentType = response.headers.get('content-type') || 'application/octet-stream'; - const contentDisposition = response.headers.get('content-disposition') || ''; - + const contentType = response.headers.get('content-type') || 'application/octet-stream' + const contentDisposition = response.headers.get('content-disposition') || '' + // Try to extract filename from content-disposition if possible - let filename = ''; - const filenameMatch = contentDisposition.match(/filename="(.+?)"/); + let filename = '' + const filenameMatch = contentDisposition.match(/filename="(.+?)"/) if (filenameMatch && filenameMatch[1]) { - filename = filenameMatch[1]; + filename = filenameMatch[1] } - + // Get file URL from the response URL or construct it from parameters if not available - let fileUrl = response.url; - + let fileUrl = response.url + // If the response URL is not available (common in test environments), construct it from params if (!fileUrl && params) { - const encodedFormId = encodeURIComponent(params.formId); - const encodedResponseId = encodeURIComponent(params.responseId); - const encodedFieldId = encodeURIComponent(params.fieldId); - const encodedFilename = encodeURIComponent(params.filename); - - fileUrl = `https://api.typeform.com/forms/${encodedFormId}/responses/${encodedResponseId}/fields/${encodedFieldId}/files/${encodedFilename}`; - + const encodedFormId = encodeURIComponent(params.formId) + const encodedResponseId = encodeURIComponent(params.responseId) + const encodedFieldId = encodeURIComponent(params.fieldId) + const encodedFilename = encodeURIComponent(params.filename) + + fileUrl = `https://api.typeform.com/forms/${encodedFormId}/responses/${encodedResponseId}/fields/${encodedFieldId}/files/${encodedFilename}` + if (params.inline !== undefined) { - fileUrl += `?inline=${params.inline}`; + fileUrl += `?inline=${params.inline}` } } - + return { success: true, output: { fileUrl: fileUrl || '', contentType, - filename - } - }; + filename, + }, + } }, transformError: (error) => { if (error instanceof Error) { return `Failed to retrieve Typeform file: ${error.message}` } - + if (typeof error === 'object' && error !== null) { return `Failed to retrieve Typeform file: ${JSON.stringify(error)}` } - + return `Failed to retrieve Typeform file: An unknown error occurred` }, -} \ No newline at end of file +} diff --git a/sim/tools/typeform/index.test.ts b/sim/tools/typeform/index.test.ts index e5a172ab6d..1f63f8ec14 100644 --- a/sim/tools/typeform/index.test.ts +++ b/sim/tools/typeform/index.test.ts @@ -7,26 +7,26 @@ * work correctly together and can be properly used from the block. */ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { typeformFilesTool, typeformResponsesTool, typeformInsightsTool } from './index' import { ToolTester } from '../__test-utils__/test-tools' +import { typeformFilesTool, typeformInsightsTool, typeformResponsesTool } from './index' describe('Typeform Tools Integration', () => { describe('Typeform Responses Tool Export', () => { let tester: ToolTester - + beforeEach(() => { tester = new ToolTester(typeformResponsesTool) }) - + afterEach(() => { tester.cleanup() vi.resetAllMocks() }) - + test('should use the correct tool ID', () => { expect(typeformResponsesTool.id).toBe('typeform_responses') }) - + test('should handle basic responses request', async () => { // Setup mock response data const mockData = { @@ -41,45 +41,45 @@ describe('Typeform Tools Integration', () => { }, ], } - + tester.setup(mockData) - + // Execute the tool const result = await tester.execute({ formId: 'test-form', apiKey: 'test-api-key', }) - + expect(result.success).toBe(true) expect(result.output.total_items).toBe(1) }) }) - + describe('Typeform Files Tool Export', () => { let tester: ToolTester - + beforeEach(() => { tester = new ToolTester(typeformFilesTool) }) - + afterEach(() => { tester.cleanup() vi.resetAllMocks() }) - + test('should use the correct tool ID', () => { expect(typeformFilesTool.id).toBe('typeform_files') }) - + test('should handle basic file request', async () => { // Setup mock response with file headers tester.setup('binary-file-content', { headers: { 'content-type': 'application/pdf', - 'content-disposition': 'attachment; filename="test.pdf"' - } + 'content-disposition': 'attachment; filename="test.pdf"', + }, }) - + // Execute the tool const result = await tester.execute({ formId: 'test-form', @@ -88,29 +88,29 @@ describe('Typeform Tools Integration', () => { filename: 'test.pdf', apiKey: 'test-api-key', }) - + expect(result.success).toBe(true) expect(result.output.contentType).toBe('application/pdf') expect(result.output.filename).toBe('test.pdf') }) }) - + describe('Typeform Insights Tool Export', () => { let tester: ToolTester - + beforeEach(() => { tester = new ToolTester(typeformInsightsTool) }) - + afterEach(() => { tester.cleanup() vi.resetAllMocks() }) - + test('should use the correct tool ID', () => { expect(typeformInsightsTool.id).toBe('typeform_insights') }) - + test('should handle basic insights request', async () => { // Setup mock response data const mockData = { @@ -122,8 +122,8 @@ describe('Typeform Tools Integration', () => { ref: 'ref123', title: 'What is your name?', type: 'short_text', - views: 100 - } + views: 100, + }, ], form: { platforms: [ @@ -133,40 +133,40 @@ describe('Typeform Tools Integration', () => { platform: 'desktop', responses_count: 80, total_visits: 120, - unique_visits: 100 - } + unique_visits: 100, + }, ], summary: { average_time: 140000, completion_rate: 72.3, responses_count: 120, total_visits: 180, - unique_visits: 150 - } - } + unique_visits: 150, + }, + }, } - + tester.setup(mockData) - + // Execute the tool const result = await tester.execute({ formId: 'test-form', apiKey: 'test-api-key', }) - + expect(result.success).toBe(true) expect(result.output.form.summary.responses_count).toBe(120) expect(result.output.fields).toHaveLength(1) }) }) - + describe('End-to-End Flow', () => { // This test simulates using both tools together in a workflow - + test('should be able to get responses and then file', async () => { // First set up responses tester const responsesTester = new ToolTester(typeformResponsesTool) - + // Mock responses data with a file upload const mockResponsesData = { total_items: 1, @@ -189,35 +189,35 @@ describe('Typeform Tools Integration', () => { }, ], } - + responsesTester.setup(mockResponsesData) - + // Get responses const responsesResult = await responsesTester.execute({ formId: 'test-form', apiKey: 'test-api-key', }) - + expect(responsesResult.success).toBe(true) - + // Now get the response ID and field ID const responseId = responsesResult.output.items[0].token expect(responseId).toBe('response-id') - + const fieldId = responsesResult.output.items[0].answers[0].field.id expect(fieldId).toBe('file-field') - + // Now set up files tester const filesTester = new ToolTester(typeformFilesTool) - + // Mock file data filesTester.setup('binary-file-data', { headers: { 'content-type': 'application/pdf', - 'content-disposition': 'attachment; filename="uploaded.pdf"' - } + 'content-disposition': 'attachment; filename="uploaded.pdf"', + }, }) - + // Get file using the response ID and field ID from previous request const filesResult = await filesTester.execute({ formId: 'test-form', @@ -226,20 +226,20 @@ describe('Typeform Tools Integration', () => { filename: 'uploaded.pdf', apiKey: 'test-api-key', }) - + expect(filesResult.success).toBe(true) expect(filesResult.output.contentType).toBe('application/pdf') expect(filesResult.output.filename).toBe('uploaded.pdf') - + // Clean up responsesTester.cleanup() filesTester.cleanup() }) - + test('should be able to get responses and then insights', async () => { // First set up responses tester const responsesTester = new ToolTester(typeformResponsesTool) - + // Mock responses data const mockResponsesData = { total_items: 10, @@ -253,21 +253,21 @@ describe('Typeform Tools Integration', () => { }, ], } - + responsesTester.setup(mockResponsesData) - + // Get responses const responsesResult = await responsesTester.execute({ formId: 'test-form', apiKey: 'test-api-key', }) - + expect(responsesResult.success).toBe(true) expect(responsesResult.output.total_items).toBe(10) - + // Now set up insights tester const insightsTester = new ToolTester(typeformInsightsTool) - + // Mock insights data const mockInsightsData = { fields: [ @@ -278,8 +278,8 @@ describe('Typeform Tools Integration', () => { ref: 'ref123', title: 'What is your name?', type: 'short_text', - views: 100 - } + views: 100, + }, ], form: { platforms: [ @@ -289,37 +289,37 @@ describe('Typeform Tools Integration', () => { platform: 'desktop', responses_count: 80, total_visits: 120, - unique_visits: 100 - } + unique_visits: 100, + }, ], summary: { average_time: 140000, completion_rate: 72.3, responses_count: 120, total_visits: 180, - unique_visits: 150 - } - } + unique_visits: 150, + }, + }, } - + insightsTester.setup(mockInsightsData) - + // Get insights for the same form const insightsResult = await insightsTester.execute({ formId: 'test-form', apiKey: 'test-api-key', }) - + expect(insightsResult.success).toBe(true) expect(insightsResult.output.form.summary.responses_count).toBe(120) - + // Verify we can analyze the data by looking at completion rates expect(insightsResult.output.form.summary.completion_rate).toBe(72.3) expect(insightsResult.output.form.platforms[0].platform).toBe('desktop') - + // Clean up responsesTester.cleanup() insightsTester.cleanup() }) }) -}) \ No newline at end of file +}) diff --git a/sim/tools/typeform/index.ts b/sim/tools/typeform/index.ts index 0e8dbf78e1..9034fb468d 100644 --- a/sim/tools/typeform/index.ts +++ b/sim/tools/typeform/index.ts @@ -1,7 +1,7 @@ -import { responsesTool } from './responses' import { filesTool } from './files' import { insightsTool } from './insights' +import { responsesTool } from './responses' export const typeformResponsesTool = responsesTool export const typeformFilesTool = filesTool -export const typeformInsightsTool = insightsTool \ No newline at end of file +export const typeformInsightsTool = insightsTool diff --git a/sim/tools/typeform/insights.test.ts b/sim/tools/typeform/insights.test.ts index c8b80d893f..f5e957ebf1 100644 --- a/sim/tools/typeform/insights.test.ts +++ b/sim/tools/typeform/insights.test.ts @@ -23,7 +23,7 @@ describe('Typeform Insights Tool', () => { ref: 'ref123', title: 'What is your name?', type: 'short_text', - views: 100 + views: 100, }, { dropoffs: 10, @@ -32,8 +32,8 @@ describe('Typeform Insights Tool', () => { ref: 'ref456', title: 'How did you hear about us?', type: 'multiple_choice', - views: 95 - } + views: 95, + }, ], form: { platforms: [ @@ -43,7 +43,7 @@ describe('Typeform Insights Tool', () => { platform: 'desktop', responses_count: 80, total_visits: 120, - unique_visits: 100 + unique_visits: 100, }, { average_time: 180000, @@ -51,17 +51,17 @@ describe('Typeform Insights Tool', () => { platform: 'mobile', responses_count: 40, total_visits: 60, - unique_visits: 50 - } + unique_visits: 50, + }, ], summary: { average_time: 140000, completion_rate: 72.3, responses_count: 120, total_visits: 180, - unique_visits: 150 - } - } + unique_visits: 150, + }, + }, } beforeEach(() => { @@ -77,18 +77,16 @@ describe('Typeform Insights Tool', () => { test('should construct correct URL for insights endpoint', () => { const params = { formId: 'form123', - apiKey: 'test-token' + apiKey: 'test-token', } - expect(tester.getRequestUrl(params)).toBe( - 'https://api.typeform.com/insights/form123/summary' - ) + expect(tester.getRequestUrl(params)).toBe('https://api.typeform.com/insights/form123/summary') }) test('should handle special characters in form ID', () => { const params = { formId: 'form/with/special?chars', - apiKey: 'test-token' + apiKey: 'test-token', } const url = tester.getRequestUrl(params) @@ -102,7 +100,7 @@ describe('Typeform Insights Tool', () => { test('should include correct authorization header', () => { const params = { formId: 'form123', - apiKey: 'test-token' + apiKey: 'test-token', } const headers = tester.getRequestHeaders(params) @@ -119,21 +117,21 @@ describe('Typeform Insights Tool', () => { // Execute the tool const result = await tester.execute({ formId: 'form123', - apiKey: 'test-token' + apiKey: 'test-token', }) // Check the result expect(result.success).toBe(true) - + // Verify form summary data expect(result.output.form.summary.responses_count).toBe(120) expect(result.output.form.summary.completion_rate).toBe(72.3) - + // Verify platforms data expect(result.output.form.platforms).toHaveLength(2) expect(result.output.form.platforms[0].platform).toBe('desktop') expect(result.output.form.platforms[1].platform).toBe('mobile') - + // Verify fields data expect(result.output.fields).toHaveLength(2) expect(result.output.fields[0].title).toBe('What is your name?') @@ -149,7 +147,7 @@ describe('Typeform Insights Tool', () => { // Execute the tool const result = await tester.execute({ formId: 'nonexistent', - apiKey: 'test-token' + apiKey: 'test-token', }) // Check error handling @@ -164,7 +162,7 @@ describe('Typeform Insights Tool', () => { // Execute the tool const result = await tester.execute({ formId: 'form123', - apiKey: 'invalid-token' + apiKey: 'invalid-token', }) // Check error handling @@ -179,7 +177,7 @@ describe('Typeform Insights Tool', () => { // Execute the tool const result = await tester.execute({ formId: 'form123', - apiKey: 'test-token' + apiKey: 'test-token', }) // Check error handling @@ -187,4 +185,4 @@ describe('Typeform Insights Tool', () => { expect(result.error).toBeDefined() }) }) -}) \ No newline at end of file +}) diff --git a/sim/tools/typeform/insights.ts b/sim/tools/typeform/insights.ts index 0d494fd7a6..215fefb6da 100644 --- a/sim/tools/typeform/insights.ts +++ b/sim/tools/typeform/insights.ts @@ -64,32 +64,32 @@ export const insightsTool: ToolConfig ({ - 'Authorization': `Bearer ${params.apiKey}`, + Authorization: `Bearer ${params.apiKey}`, 'Content-Type': 'application/json', }), }, transformResponse: async (response: Response) => { if (!response.ok) { - let errorMessage = response.statusText || 'Unknown error'; - let errorDetails = ''; - + let errorMessage = response.statusText || 'Unknown error' + let errorDetails = '' + try { - const errorData = await response.json(); - console.log('Typeform API error response:', JSON.stringify(errorData, null, 2)); - + const errorData = await response.json() + console.log('Typeform API error response:', JSON.stringify(errorData, null, 2)) + if (errorData && errorData.message) { - errorMessage = errorData.message; + errorMessage = errorData.message } else if (errorData && errorData.description) { - errorMessage = errorData.description; + errorMessage = errorData.description } else if (typeof errorData === 'string') { - errorMessage = errorData; + errorMessage = errorData } - + // Extract more details if available if (errorData && errorData.details) { - errorDetails = ` Details: ${JSON.stringify(errorData.details)}`; + errorDetails = ` Details: ${JSON.stringify(errorData.details)}` } - + // Special handling for 403 errors if (response.status === 403) { return { @@ -100,33 +100,33 @@ export const insightsTool: ToolConfig { if (error instanceof Error) { return `Failed to retrieve Typeform insights: ${error.message}` } - + if (typeof error === 'object' && error !== null) { return `Failed to retrieve Typeform insights: ${JSON.stringify(error)}` } - + return `Failed to retrieve Typeform insights: An unknown error occurred` }, -} \ No newline at end of file +} diff --git a/sim/tools/typeform/responses.test.ts b/sim/tools/typeform/responses.test.ts index 62dde7af5e..3dbd163b2c 100644 --- a/sim/tools/typeform/responses.test.ts +++ b/sim/tools/typeform/responses.test.ts @@ -95,9 +95,7 @@ describe('Typeform Responses Tool', () => { apiKey: 'test-token', } - expect(tester.getRequestUrl(params)).toBe( - 'https://api.typeform.com/forms/form123/responses' - ) + expect(tester.getRequestUrl(params)).toBe('https://api.typeform.com/forms/form123/responses') }) test('should add pageSize parameter to URL when provided', () => { @@ -155,9 +153,7 @@ describe('Typeform Responses Tool', () => { completed: 'all', } - expect(tester.getRequestUrl(params)).toBe( - 'https://api.typeform.com/forms/form123/responses' - ) + expect(tester.getRequestUrl(params)).toBe('https://api.typeform.com/forms/form123/responses') }) test('should combine multiple parameters correctly', () => { @@ -207,13 +203,13 @@ describe('Typeform Responses Tool', () => { expect(result.success).toBe(true) expect(result.output.total_items).toBe(2) expect(result.output.items).toHaveLength(2) - + // Check first response const firstResponse = result.output.items[0] expect(firstResponse.token).toBe('response-id-1') expect(firstResponse.answers).toHaveLength(1) expect(firstResponse.answers[0].text).toBe('Sample answer') - + // Check second response const secondResponse = result.output.items[1] expect(secondResponse.token).toBe('response-id-2') @@ -266,4 +262,4 @@ describe('Typeform Responses Tool', () => { expect(result.error).toBeDefined() }) }) -}) \ No newline at end of file +}) diff --git a/sim/tools/typeform/responses.ts b/sim/tools/typeform/responses.ts index 734fced892..a870f6d0ee 100644 --- a/sim/tools/typeform/responses.ts +++ b/sim/tools/typeform/responses.ts @@ -87,73 +87,75 @@ export const responsesTool: ToolConfig { const url = `https://api.typeform.com/forms/${params.formId}/responses` - + const queryParams = [] - + if (params.pageSize) { queryParams.push(`page_size=${params.pageSize}`) } - + if (params.since) { queryParams.push(`since=${encodeURIComponent(params.since)}`) } - + if (params.until) { queryParams.push(`until=${encodeURIComponent(params.until)}`) } - + if (params.completed && params.completed !== 'all') { queryParams.push(`completed=${params.completed}`) } - + return queryParams.length > 0 ? `${url}?${queryParams.join('&')}` : url }, method: 'GET', headers: (params) => ({ - 'Authorization': `Bearer ${params.apiKey}`, + Authorization: `Bearer ${params.apiKey}`, 'Content-Type': 'application/json', }), }, transformResponse: async (response: Response) => { if (!response.ok) { - let errorMessage = response.statusText || 'Unknown error'; - + let errorMessage = response.statusText || 'Unknown error' + try { - const errorData = await response.json(); + const errorData = await response.json() if (errorData && errorData.message) { - errorMessage = errorData.message; + errorMessage = errorData.message } else if (errorData && errorData.description) { - errorMessage = errorData.description; + errorMessage = errorData.description } else if (typeof errorData === 'string') { - errorMessage = errorData; + errorMessage = errorData } } catch (e) { // If we can't parse the error as JSON, just use the status text } - - throw new Error(`Typeform API error (${response.status}): ${errorMessage}`); + + throw new Error(`Typeform API error (${response.status}): ${errorMessage}`) } - + try { - const data = await response.json(); - + const data = await response.json() + return { success: true, output: data, - }; + } } catch (error) { - throw new Error(`Failed to parse Typeform response: ${error instanceof Error ? error.message : 'Unknown error'}`); + throw new Error( + `Failed to parse Typeform response: ${error instanceof Error ? error.message : 'Unknown error'}` + ) } }, transformError: (error) => { if (error instanceof Error) { return `Failed to retrieve Typeform responses: ${error.message}` } - + if (typeof error === 'object' && error !== null) { return `Failed to retrieve Typeform responses: ${JSON.stringify(error)}` } - + return `Failed to retrieve Typeform responses: An unknown error occurred` }, -} \ No newline at end of file +} diff --git a/sim/tools/utils.ts b/sim/tools/utils.ts index fa4ffd19af..4ba93999d8 100644 --- a/sim/tools/utils.ts +++ b/sim/tools/utils.ts @@ -48,7 +48,9 @@ export function formatRequestParams(tool: ToolConfig, params: Record