/** @jest-environment setup-polly-jest/jest-environment-node */ import * as request from 'supertest'; import { INestApplication } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { getDataSourceToken } from '@nestjs/typeorm'; import { WorkflowExecution } from '../../src/entities/workflow_execution.entity'; import { setupPolly } from 'setup-polly-jest'; import * as NodeHttpAdapter from '@pollyjs/adapter-node-http'; import * as FSPersister from '@pollyjs/persister-fs'; import * as path from 'path'; import { parse } from 'flatted'; import { clearDB, setupOrganizationAndUser, createNestAppInstance, createCompleteWorkflow, createBundle, authenticateUser, WorkflowNode, WorkflowEdge, WorkflowQuery, } from '../workflows.helper'; const executeWorkflow = async ( nestApp: INestApplication, workflow: any, user: any, options: { environmentId?: string; parameters?: Record; } = {} ): Promise => { const { tokenCookie } = await authenticateUser(nestApp, user.email); const response = await request(nestApp.getHttpServer()) .post('/api/workflow_executions') .set('tj-workspace-id', user.defaultOrganizationId || user.organizationId) .set('Cookie', tokenCookie) .send({ appId: workflow.id, executeUsing: 'app', userId: user.id, environmentId: options.environmentId, parameters: options.parameters }); if (response.statusCode !== 201) { throw new Error(`Workflow execution failed: ${response.body.message || 'Unknown error'}`); } return response.body.workflowExecution; }; const getWorkflowExecutionDetails = async ( nestApp: INestApplication, executionId: string ) => { const defaultDataSource = nestApp.get(getDataSourceToken('default')); const workflowExecution = await defaultDataSource .getRepository(WorkflowExecution) .findOne({ where: { id: executionId } }); if (!workflowExecution) { throw new Error(`Workflow execution ${executionId} not found`); } const executionNodes = await defaultDataSource .getRepository('WorkflowExecutionNode') .find({ where: { workflowExecutionId: executionId } }); return { execution: workflowExecution, nodes: executionNodes }; }; /** * @group workflows */ const context = setupPolly({ adapters: [NodeHttpAdapter as any], persister: FSPersister as any, persisterOptions: { fs: { // Store recordings as __fixtures__/spec-file-name/* recordingsDir: path.resolve( __dirname, `../__fixtures__/${path.basename(__filename).replace(/\.[tj]s$/, '')}` ), }, }, recordFailedRequests: true, recordIfMissing: true, mode: (process.env.POLLY_MODE as any) || 'replay', matchRequestsBy: { method: true, headers: { exclude: ['user-agent', 'accept-encoding', 'connection', 'host', 'cookie'], }, body: true, url: { protocol: true, hostname: true, port: true, pathname: true, query: true, }, }, }); let app: INestApplication; beforeAll(async () => { app = await createNestAppInstance({ edition: 'ee', isGetContext: true }); }); beforeEach(async () => { await clearDB(app); // Configure Polly.js to only pass through localhost calls // External API calls will be recorded context.polly.server .any() .filter(req => { const url = new URL(req.url); // Pass through localhost and internal calls without recording return url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname.includes('host.docker.internal'); }) .passthrough(); // All other requests (external APIs) will be handled by Polly's recording mechanism }); afterEach(async () => { jest.resetAllMocks(); jest.clearAllMocks(); }); afterAll(async () => { if (app) await app.close(); }); describe('workflow executions controller', () => { describe('POST /api/workflow_executions', () => { describe('Basic workflow execution', () => { it('should execute a simple workflow with start trigger and response', async () => { const { user } = await setupOrganizationAndUser(app, { email: 'admin@tooljet.io', password: 'password', firstName: 'Admin', lastName: 'User' }); const nodes: WorkflowNode[] = [ { id: 'start-1', type: 'input', data: { nodeType: 'start', label: 'Start trigger' }, position: { x: 100, y: 250 }, sourcePosition: 'right' }, { id: 'response-1', type: 'output', data: { nodeType: 'response', label: 'Response', code: 'return { message: "Workflow executed successfully" }', nodeName: 'response1' }, position: { x: 400, y: 250 }, targetPosition: 'left' } ]; const edges: WorkflowEdge[] = [ { id: 'edge-1', source: 'start-1', target: 'response-1', type: 'workflow' } ]; const { app: workflow, appVersion } = await createCompleteWorkflow(app, user, { name: 'Basic Workflow', nodes, edges, queries: [] }); const execution = await executeWorkflow(app, workflow, user, { environmentId: appVersion.currentEnvironmentId }); // Get workflow execution details const { execution: workflowExecution, nodes: executionNodes } = await getWorkflowExecutionDetails(app, execution.id); // Verify execution status expect(workflowExecution.executed).toBe(true); expect(execution.id).toBeDefined(); expect(execution.appVersionId).toBe(appVersion.id); // Verify nodes executed const executedNodes = executionNodes.filter((n: any) => n.executed); const executedNodeIds = executedNodes.map((n: any) => n.idOnWorkflowDefinition); expect(executedNodeIds).toContain('start-1'); // Verify response node result contains expected message const responseNode = executionNodes.find((n: any) => n.idOnWorkflowDefinition === 'response-1'); expect(responseNode).toBeDefined(); expect(responseNode.executed).toBe(true); const responseResult = JSON.parse(responseNode.result); expect(responseResult).toContain('Workflow executed successfully'); }); }); describe('Workflow with query nodes', () => { it('should execute workflow with RunJS query node', async () => { const { user } = await setupOrganizationAndUser(app, { email: 'admin@tooljet.io', password: 'password', firstName: 'Admin', lastName: 'User' }); const nodes: WorkflowNode[] = [ { id: 'start-1', type: 'input', data: { nodeType: 'start', label: 'Start trigger' }, position: { x: 100, y: 250 }, sourcePosition: 'right' }, { id: 'runjs-1', type: 'query', data: { idOnDefinition: 'query-runjs-1', kind: 'runjs', options: {} }, position: { x: 350, y: 250 }, sourcePosition: 'right', targetPosition: 'left' }, { id: 'response-1', type: 'output', data: { nodeType: 'response', label: 'Response', code: 'return { result: runjs1.data }', nodeName: 'response1' }, position: { x: 600, y: 250 }, targetPosition: 'left' } ]; const edges: WorkflowEdge[] = [ { id: 'edge-1', source: 'start-1', target: 'runjs-1', type: 'workflow' }, { id: 'edge-2', source: 'runjs-1', target: 'response-1', type: 'workflow' } ]; const queries: WorkflowQuery[] = [ { idOnDefinition: 'query-runjs-1', dataSourceKind: 'runjs', name: 'runjs1', options: { code: ` const numbers = [1, 2, 3, 4, 5]; const sum = numbers.reduce((acc, val) => acc + val, 0); return { sum: sum, numbers: numbers }; `, parameters: [] } } ]; const { app: workflow, appVersion } = await createCompleteWorkflow(app, user, { name: 'RunJS Workflow', nodes, edges, queries }); const execution = await executeWorkflow(app, workflow, user, { environmentId: appVersion.currentEnvironmentId }); // Get workflow execution details const { execution: workflowExecution, nodes: executionNodes } = await getWorkflowExecutionDetails(app, execution.id); // Verify execution status expect(workflowExecution.executed).toBe(true); // Verify nodes executed const executedNodes = executionNodes.filter((n: any) => n.executed); const executedNodeIds = executedNodes.map((n: any) => n.idOnWorkflowDefinition); expect(executedNodeIds).toContain('start-1'); expect(executedNodeIds).toContain('runjs-1'); // Verify RunJS node result contains the numbers array const runjsNode = executionNodes.find((n: any) => n.idOnWorkflowDefinition === 'runjs-1'); expect(runjsNode).toBeDefined(); expect(runjsNode.executed).toBe(true); const runjsResult = JSON.parse(runjsNode.result); expect(runjsResult).toEqual(expect.arrayContaining([[1, 2, 3, 4, 5]])); // Verify response node result contains the numbers array const responseNode = executionNodes.find((n: any) => n.idOnWorkflowDefinition === 'response-1'); expect(responseNode).toBeDefined(); expect(responseNode.executed).toBe(true); const responseResult = JSON.parse(responseNode.result); expect(responseResult).toEqual(expect.arrayContaining([[1, 2, 3, 4, 5]])); }); it('should execute workflow with REST API query node', async () => { const { user } = await setupOrganizationAndUser(app, { email: 'admin@tooljet.io', password: 'password', firstName: 'Admin', lastName: 'User' }); const nodes: WorkflowNode[] = [ { id: 'start-1', type: 'input', data: { nodeType: 'start', label: 'Start trigger' }, position: { x: 100, y: 250 }, sourcePosition: 'right' }, { id: 'restapi-1', type: 'query', data: { idOnDefinition: 'query-restapi-1', kind: 'restapi', options: {} }, position: { x: 350, y: 250 }, sourcePosition: 'right', targetPosition: 'left' }, { id: 'response-1', type: 'output', data: { nodeType: 'response', label: 'Response', code: 'return { data: restapi1.data }', nodeName: 'response1' }, position: { x: 600, y: 250 }, targetPosition: 'left' } ]; const edges: WorkflowEdge[] = [ { id: 'edge-1', source: 'start-1', target: 'restapi-1', type: 'workflow' }, { id: 'edge-2', source: 'restapi-1', target: 'response-1', type: 'workflow' } ]; const queries: WorkflowQuery[] = [ { idOnDefinition: 'query-restapi-1', dataSourceKind: 'restapi', name: 'restapi1', options: { method: 'get', url: 'https://jsonplaceholder.typicode.com/users/1', headers: [['Accept', 'application/json']], body: [['', '']], url_params: [['', '']], cookies: [['', '']], json_body: null, body_toggle: false, transformationLanguage: 'javascript', enableTransformation: false, raw_body: null, arrayValuesChanged: true } } ]; const { app: workflow, appVersion } = await createCompleteWorkflow(app, user, { name: 'REST API Workflow', nodes, edges, queries }); const execution = await executeWorkflow(app, workflow, user, { environmentId: appVersion.currentEnvironmentId }); // Get workflow execution details const { execution: workflowExecution, nodes: executionNodes } = await getWorkflowExecutionDetails(app, execution.id); // Verify execution status expect(workflowExecution.executed).toBe(true); // Verify nodes executed const executedNodes = executionNodes.filter((n: any) => n.executed); const executedNodeIds = executedNodes.map((n: any) => n.idOnWorkflowDefinition); expect(executedNodeIds).toContain('start-1'); expect(executedNodeIds).toContain('restapi-1'); // Verify REST API node executed and has result const restapiNode = executionNodes.find((n: any) => n.idOnWorkflowDefinition === 'restapi-1'); expect(restapiNode).toBeDefined(); expect(restapiNode.executed).toBe(true); expect(restapiNode.result).toBeTruthy(); // Parse and validate REST API response const restapiResult = JSON.parse(restapiNode.result); expect(Array.isArray(restapiResult)).toBe(true); expect(restapiResult.length).toBeGreaterThan(0); // The result should contain status and data info - check for 'ok' status expect(restapiResult).toContain('ok'); // Find the user data object in the result array const userData = restapiResult.find((item: any) => typeof item === 'object' && item !== null && item.hasOwnProperty('id') ); expect(userData).toBeDefined(); // Validate the JSONPlaceholder user data structure expect(userData).toHaveProperty('id', 1); expect(userData).toHaveProperty('name'); expect(userData).toHaveProperty('username'); expect(userData).toHaveProperty('email'); expect(userData).toHaveProperty('address'); expect(userData).toHaveProperty('phone'); expect(userData).toHaveProperty('website'); expect(userData).toHaveProperty('company'); // Verify specific expected values from JSONPlaceholder user 1 expect(restapiResult).toContain('Leanne Graham'); expect(restapiResult).toContain('Bret'); expect(restapiResult).toContain('Sincere@april.biz'); // Verify response node executed successfully const responseNode = executionNodes.find((n: any) => n.idOnWorkflowDefinition === 'response-1'); expect(responseNode).toBeDefined(); expect(responseNode.executed).toBe(true); expect(responseNode.result).toBeTruthy(); // Parse and validate response node result const responseResult = JSON.parse(responseNode.result); expect(Array.isArray(responseResult)).toBe(true); expect(responseResult.length).toBeGreaterThan(0); // Response should contain the user data expect(responseResult).toContain('Leanne Graham'); expect(responseResult).toContain('Bret'); }); }); describe('NPM package support', () => { it('should execute workflow with setup script using lodash', async () => { const { user } = await setupOrganizationAndUser(app, { email: 'admin@tooljet.io', password: 'password', firstName: 'Admin', lastName: 'User' }); const setupScript = { javascript: ` const _ = require('lodash'); const processNumbers = (numbers) => ({ sum: _.sum(numbers), max: _.max(numbers), min: _.min(numbers), sorted: _.sortBy(numbers) }); `}; const dependencies = { 'lodash': '4.17.21' }; const nodes: WorkflowNode[] = [ { id: 'start-1', type: 'input', data: { nodeType: 'start', label: 'Start trigger' }, position: { x: 100, y: 250 }, sourcePosition: 'right' }, { id: 'runjs-1', type: 'query', data: { idOnDefinition: 'query-runjs-lodash', kind: 'runjs', options: {} }, position: { x: 350, y: 250 }, sourcePosition: 'right', targetPosition: 'left' }, { id: 'response-1', type: 'output', data: { nodeType: 'response', label: 'Response', code: 'return { result: runjs1.data }', nodeName: 'response1' }, position: { x: 600, y: 250 }, targetPosition: 'left' } ]; const edges: WorkflowEdge[] = [ { id: 'edge-1', source: 'start-1', target: 'runjs-1', type: 'workflow' }, { id: 'edge-2', source: 'runjs-1', target: 'response-1', type: 'workflow' } ]; const queries: WorkflowQuery[] = [ { idOnDefinition: 'query-runjs-lodash', dataSourceKind: 'runjs', name: 'runjs1', options: { code: ` const numbers = [10, 5, 8, 3, 12, 7]; return processNumbers(numbers); `, parameters: [] } } ]; const { app: workflow, appVersion } = await createCompleteWorkflow(app, user, { name: 'NPM Package Workflow', setupScript, dependencies, nodes, edges, queries }); await createBundle(app, appVersion.id, dependencies, 'javascript'); const execution = await executeWorkflow(app, workflow, user, { environmentId: appVersion.currentEnvironmentId }); // Get workflow execution details const { execution: workflowExecution, nodes: executionNodes } = await getWorkflowExecutionDetails(app, execution.id); // Verify execution status expect(workflowExecution.executed).toBe(true); // Verify nodes executed const executedNodes = executionNodes.filter((n: any) => n.executed); const executedNodeIds = executedNodes.map((n: any) => n.idOnWorkflowDefinition); expect(executedNodeIds).toContain('start-1'); expect(executedNodeIds).toContain('runjs-1'); // Verify RunJS node executed and has result const runjsNode = executionNodes.find((n: any) => n.idOnWorkflowDefinition === 'runjs-1'); expect(runjsNode).toBeDefined(); expect(runjsNode.executed).toBe(true); // Parse the flatted-encoded result and verify the actual object structure const parsedResult = parse(runjsNode.result); expect(parsedResult).toMatchObject({ data: { sum: 45, max: 12, min: 3, sorted: [3, 5, 7, 8, 10, 12] }, status: "ok" }); // Verify response node contains the lodash results const responseNode = executionNodes.find((n: any) => n.idOnWorkflowDefinition === 'response-1'); expect(responseNode).toBeDefined(); expect(responseNode.executed).toBe(true); // Parse the response node result and verify it contains the same lodash results const parsedResponseResult = parse(responseNode.result); expect(parsedResponseResult).toMatchObject({ data: { result: { sum: 45, max: 12, min: 3, sorted: [3, 5, 7, 8, 10, 12] } }, status: "ok" }); }); it('should execute workflow with REST API query using NPM packages in template expressions', async () => { const { user } = await setupOrganizationAndUser(app, { email: 'admin@tooljet.io', password: 'password', firstName: 'Admin', lastName: 'User' }); const setupScript = { javascript: `const _ = require('lodash');` }; const dependencies = { 'lodash': '4.17.21' }; const nodes: WorkflowNode[] = [ { id: 'start-1', type: 'input', data: { nodeType: 'start', label: 'Start trigger' }, position: { x: 100, y: 250 }, sourcePosition: 'right' }, { id: 'restapi-1', type: 'query', data: { idOnDefinition: 'query-restapi-lodash', kind: 'restapi', options: {} }, position: { x: 350, y: 250 }, sourcePosition: 'right', targetPosition: 'left' }, { id: 'response-1', type: 'output', data: { nodeType: 'response', label: 'Response', code: 'return { data: restapi1.data }', nodeName: 'response1' }, position: { x: 600, y: 250 }, targetPosition: 'left' } ]; const edges: WorkflowEdge[] = [ { id: 'edge-1', source: 'start-1', target: 'restapi-1', type: 'workflow' }, { id: 'edge-2', source: 'restapi-1', target: 'response-1', type: 'workflow' } ]; const queries: WorkflowQuery[] = [ { idOnDefinition: 'query-restapi-lodash', dataSourceKind: 'restapi', name: 'restapi1', options: { method: 'get', url: 'https://reqres.in/api/users?page={{ _.toLower("2") }}', headers: [ ['x-custom-header', '{{ _.startCase("hello world") }}'], ['Accept', 'application/json'] ], body: [['', '']], url_params: [['', '']], cookies: [['', '']], json_body: null, body_toggle: false, transformationLanguage: 'javascript', enableTransformation: false, raw_body: null, arrayValuesChanged: true } } ]; const { app: workflow, appVersion } = await createCompleteWorkflow(app, user, { name: 'REST API Template Workflow', setupScript, dependencies, nodes, edges, queries }); await createBundle(app, appVersion.id, dependencies, 'javascript'); // Observe the actual outbound request to verify template resolution const observedRequests: Array<{ url: string; headers: Record | undefined }> = []; const captureRoute = context.polly.server .any() .filter((req: any) => { try { const u = new URL(req.url); return u.hostname === 'reqres.in' && u.pathname === '/api/users'; } catch (_) { return false; } }); captureRoute.on('request', (req: any) => { // Try multiple header access shapes for robustness across adapters let headers: Record | undefined = undefined; try { if (typeof req.getHeader === 'function') { headers = { 'x-custom-header': req.getHeader('x-custom-header'), accept: req.getHeader('accept'), } as any; } else if (req.headers) { headers = req.headers as any; } } catch (_) { } observedRequests.push({ url: req.url, headers }); }); const execution = await executeWorkflow(app, workflow, user, { environmentId: appVersion.currentEnvironmentId }); // Get workflow execution details const { execution: workflowExecution, nodes: executionNodes } = await getWorkflowExecutionDetails(app, execution.id); // Verify execution status expect(workflowExecution.executed).toBe(true); // Verify nodes executed const executedNodes = executionNodes.filter((n: any) => n.executed); const executedNodeIds = executedNodes.map((n: any) => n.idOnWorkflowDefinition); expect(executedNodeIds).toContain('start-1'); expect(executedNodeIds).toContain('restapi-1'); // Verify REST API node executed and has result const restapiNode = executionNodes.find((n: any) => n.idOnWorkflowDefinition === 'restapi-1'); expect(restapiNode).toBeDefined(); expect(restapiNode.executed).toBe(true); expect(restapiNode.result).toBeTruthy(); // Parse and validate REST API response (should contain resolved template expressions) const restapiResult = JSON.parse(restapiNode.result); expect(Array.isArray(restapiResult)).toBe(true); expect(restapiResult.length).toBeGreaterThan(0); // The result should contain status and data info - check for 'ok' status expect(restapiResult).toContain('ok'); // Verify response node executed successfully const responseNode = executionNodes.find((n: any) => n.idOnWorkflowDefinition === 'response-1'); expect(responseNode).toBeDefined(); expect(responseNode.executed).toBe(true); expect(responseNode.result).toBeTruthy(); // Parse and validate response node result const responseResult = JSON.parse(responseNode.result); expect(Array.isArray(responseResult)).toBe(true); expect(responseResult.length).toBeGreaterThan(0); // Response should contain the API data with resolved template expressions expect(responseResult).toContain('ok'); // Assert the outbound request had resolved URL and headers expect(observedRequests.length).toBeGreaterThan(0); const { url: observedUrl, headers: observedHeaders } = observedRequests[0]; const parsedUrl = new URL(observedUrl); expect(parsedUrl.searchParams.get('page')).toBe('2'); // {{ toLower("2") }} => 2 const headerVal = observedHeaders && ( observedHeaders['x-custom-header'] || observedHeaders['X-Custom-Header'] || (typeof (observedHeaders as any).get === 'function' ? (observedHeaders as any).get('x-custom-header') : undefined) ); expect(headerVal).toBe('Hello World'); // {{ startCase("hello world") }} => Hello World }); }); describe('Python (runpy) execution', () => { it('should execute workflow with RunPy query node', async () => { const { user } = await setupOrganizationAndUser(app, { email: 'admin@tooljet.io', password: 'password', firstName: 'Admin', lastName: 'User' }); const nodes: WorkflowNode[] = [ { id: 'start-1', type: 'input', data: { nodeType: 'start', label: 'Start trigger' }, position: { x: 100, y: 250 }, sourcePosition: 'right' }, { id: 'runpy-1', type: 'query', data: { idOnDefinition: 'query-runpy-1', kind: 'runpy', options: {} }, position: { x: 350, y: 250 }, sourcePosition: 'right', targetPosition: 'left' }, { id: 'response-1', type: 'output', data: { nodeType: 'response', label: 'Response', code: 'return { result: runpy1.data }', nodeName: 'response1' }, position: { x: 600, y: 250 }, targetPosition: 'left' } ]; const edges: WorkflowEdge[] = [ { id: 'edge-1', source: 'start-1', target: 'runpy-1', type: 'workflow' }, { id: 'edge-2', source: 'runpy-1', target: 'response-1', type: 'workflow' } ]; const queries: WorkflowQuery[] = [ { idOnDefinition: 'query-runpy-1', dataSourceKind: 'runpy', name: 'runpy1', options: { code: `result = sum([1, 2, 3, 4, 5])`, } } ]; const { app: workflow, appVersion } = await createCompleteWorkflow(app, user, { name: 'RunPy Workflow', nodes, edges, queries }); const execution = await executeWorkflow(app, workflow, user, { environmentId: appVersion.currentEnvironmentId }); // Get workflow execution details const { execution: workflowExecution, nodes: executionNodes } = await getWorkflowExecutionDetails(app, execution.id); // Verify execution status expect(workflowExecution.executed).toBe(true); // Verify nodes executed const executedNodes = executionNodes.filter((n: any) => n.executed); const executedNodeIds = executedNodes.map((n: any) => n.idOnWorkflowDefinition); expect(executedNodeIds).toContain('start-1'); expect(executedNodeIds).toContain('runpy-1'); // Verify RunPy node executed and has result const runpyNode = executionNodes.find((n: any) => n.idOnWorkflowDefinition === 'runpy-1'); expect(runpyNode).toBeDefined(); expect(runpyNode.executed).toBe(true); // Parse and validate Python result (sum of 1+2+3+4+5 = 15) const parsedResult = parse(runpyNode.result); expect(parsedResult.status).toBe('ok'); expect(parsedResult.data).toBe(15); }, 60000); it('should execute workflow with Python bundle using pydash', async () => { const { user } = await setupOrganizationAndUser(app, { email: 'admin@tooljet.io', password: 'password', firstName: 'Admin', lastName: 'User' }); const nodes: WorkflowNode[] = [ { id: 'start-1', type: 'input', data: { nodeType: 'start', label: 'Start trigger' }, position: { x: 100, y: 250 }, sourcePosition: 'right' }, { id: 'runpy-1', type: 'query', data: { idOnDefinition: 'query-runpy-pydash', kind: 'runpy', options: {} }, position: { x: 350, y: 250 }, sourcePosition: 'right', targetPosition: 'left' }, { id: 'response-1', type: 'output', data: { nodeType: 'response', label: 'Response', code: 'return { result: runpy1.data }', nodeName: 'response1' }, position: { x: 600, y: 250 }, targetPosition: 'left' } ]; const edges: WorkflowEdge[] = [ { id: 'edge-1', source: 'start-1', target: 'runpy-1', type: 'workflow' }, { id: 'edge-2', source: 'runpy-1', target: 'response-1', type: 'workflow' } ]; const queries: WorkflowQuery[] = [ { idOnDefinition: 'query-runpy-pydash', dataSourceKind: 'runpy', name: 'runpy1', options: { code: ` import pydash result = pydash.map_([1, 2, 3], lambda x: x * 2) `.trim(), } } ]; const { app: workflow, appVersion } = await createCompleteWorkflow(app, user, { name: 'RunPy Pydash Workflow', nodes, edges, queries }); // Create Python bundle with pydash await createBundle(app, appVersion.id, 'pydash==8.0.3', 'python'); const execution = await executeWorkflow(app, workflow, user, { environmentId: appVersion.currentEnvironmentId }); // Get workflow execution details const { execution: workflowExecution, nodes: executionNodes } = await getWorkflowExecutionDetails(app, execution.id); // Verify execution status expect(workflowExecution.executed).toBe(true); // Verify nodes executed const executedNodes = executionNodes.filter((n: any) => n.executed); const executedNodeIds = executedNodes.map((n: any) => n.idOnWorkflowDefinition); expect(executedNodeIds).toContain('start-1'); expect(executedNodeIds).toContain('runpy-1'); // Verify RunPy node executed and has pydash result const runpyNode = executionNodes.find((n: any) => n.idOnWorkflowDefinition === 'runpy-1'); expect(runpyNode).toBeDefined(); expect(runpyNode.executed).toBe(true); // Parse and validate pydash result (map [1,2,3] * 2 = [2,4,6]) const parsedResult = parse(runpyNode.result); expect(parsedResult.status).toBe('ok'); expect(parsedResult.data).toEqual([2, 4, 6]); }, 120000); it('should access state variables in runpy node', async () => { const { user } = await setupOrganizationAndUser(app, { email: 'admin@tooljet.io', password: 'password', firstName: 'Admin', lastName: 'User' }); const nodes: WorkflowNode[] = [ { id: 'start-1', type: 'input', data: { nodeType: 'start', label: 'Start trigger' }, position: { x: 100, y: 250 }, sourcePosition: 'right' }, { id: 'runjs-init', type: 'query', data: { idOnDefinition: 'query-runjs-init', kind: 'runjs', options: {} }, position: { x: 250, y: 250 }, sourcePosition: 'right', targetPosition: 'left' }, { id: 'runpy-1', type: 'query', data: { idOnDefinition: 'query-runpy-state', kind: 'runpy', options: {} }, position: { x: 450, y: 250 }, sourcePosition: 'right', targetPosition: 'left' }, { id: 'response-1', type: 'output', data: { nodeType: 'response', label: 'Response', code: 'return { result: runpy1.data }', nodeName: 'response1' }, position: { x: 650, y: 250 }, targetPosition: 'left' } ]; const edges: WorkflowEdge[] = [ { id: 'edge-1', source: 'start-1', target: 'runjs-init', type: 'workflow' }, { id: 'edge-2', source: 'runjs-init', target: 'runpy-1', type: 'workflow' }, { id: 'edge-3', source: 'runpy-1', target: 'response-1', type: 'workflow' } ]; const queries: WorkflowQuery[] = [ { idOnDefinition: 'query-runjs-init', dataSourceKind: 'runjs', name: 'runjs1', options: { code: `return { numbers: [1, 2, 3], multiplier: 2 };`, parameters: [] } }, { idOnDefinition: 'query-runpy-state', dataSourceKind: 'runpy', name: 'runpy1', options: { code: ` numbers = runjs1['data']['numbers'] multiplier = runjs1['data']['multiplier'] result = [x * multiplier for x in numbers] `.trim(), } } ]; const { app: workflow, appVersion } = await createCompleteWorkflow(app, user, { name: 'RunPy State Workflow', nodes, edges, queries }); const execution = await executeWorkflow(app, workflow, user, { environmentId: appVersion.currentEnvironmentId }); // Get workflow execution details const { execution: workflowExecution, nodes: executionNodes } = await getWorkflowExecutionDetails(app, execution.id); // Verify execution status expect(workflowExecution.executed).toBe(true); // Verify nodes executed const executedNodes = executionNodes.filter((n: any) => n.executed); const executedNodeIds = executedNodes.map((n: any) => n.idOnWorkflowDefinition); expect(executedNodeIds).toContain('runjs-init'); expect(executedNodeIds).toContain('runpy-1'); // Verify RunPy node accessed state and produced correct result const runpyNode = executionNodes.find((n: any) => n.idOnWorkflowDefinition === 'runpy-1'); expect(runpyNode).toBeDefined(); expect(runpyNode.executed).toBe(true); // Parse and validate result ([1,2,3] * 2 = [2,4,6]) const parsedResult = parse(runpyNode.result); expect(parsedResult.status).toBe('ok'); expect(parsedResult.data).toEqual([2, 4, 6]); }, 60000); }); describe('Mixed JavaScript and Python execution', () => { it('should execute workflow with both RunJS and RunPy nodes using their respective bundles', async () => { const { user } = await setupOrganizationAndUser(app, { email: 'admin@tooljet.io', password: 'password', firstName: 'Admin', lastName: 'User' }); const setupScript = { javascript: ` const _ = require('lodash'); const processNumbers = (numbers) => _.sortBy(numbers); `.trim() }; const jsDependencies = { 'lodash': '4.17.21' }; const nodes: WorkflowNode[] = [ { id: 'start-1', type: 'input', data: { nodeType: 'start', label: 'Start trigger' }, position: { x: 100, y: 250 }, sourcePosition: 'right' }, { id: 'runjs-1', type: 'query', data: { idOnDefinition: 'query-runjs-mixed', kind: 'runjs', options: {} }, position: { x: 300, y: 250 }, sourcePosition: 'right', targetPosition: 'left' }, { id: 'runpy-1', type: 'query', data: { idOnDefinition: 'query-runpy-mixed', kind: 'runpy', options: {} }, position: { x: 500, y: 250 }, sourcePosition: 'right', targetPosition: 'left' }, { id: 'response-1', type: 'output', data: { nodeType: 'response', label: 'Response', code: 'return { jsResult: runjs1.data, pyResult: runpy1.data }', nodeName: 'response1' }, position: { x: 700, y: 250 }, targetPosition: 'left' } ]; const edges: WorkflowEdge[] = [ { id: 'edge-1', source: 'start-1', target: 'runjs-1', type: 'workflow' }, { id: 'edge-2', source: 'runjs-1', target: 'runpy-1', type: 'workflow' }, { id: 'edge-3', source: 'runpy-1', target: 'response-1', type: 'workflow' } ]; const queries: WorkflowQuery[] = [ { idOnDefinition: 'query-runjs-mixed', dataSourceKind: 'runjs', name: 'runjs1', options: { code: ` const numbers = [3, 1, 4, 1, 5, 9, 2, 6]; return processNumbers(numbers); `.trim(), parameters: [] } }, { idOnDefinition: 'query-runpy-mixed', dataSourceKind: 'runpy', name: 'runpy1', options: { code: ` import pydash # Get sorted numbers from JS node and calculate sum sorted_numbers = runjs1['data'] result = pydash.sum_(sorted_numbers) `.trim(), } } ]; const { app: workflow, appVersion } = await createCompleteWorkflow(app, user, { name: 'Mixed JS and Python Workflow', setupScript, dependencies: jsDependencies, nodes, edges, queries }); // Create both JavaScript and Python bundles await createBundle(app, appVersion.id, jsDependencies, 'javascript'); await createBundle(app, appVersion.id, 'pydash==8.0.3', 'python'); const execution = await executeWorkflow(app, workflow, user, { environmentId: appVersion.currentEnvironmentId }); // Get workflow execution details const { execution: workflowExecution, nodes: executionNodes } = await getWorkflowExecutionDetails(app, execution.id); // Verify execution status expect(workflowExecution.executed).toBe(true); // Verify all nodes executed const executedNodes = executionNodes.filter((n: any) => n.executed); const executedNodeIds = executedNodes.map((n: any) => n.idOnWorkflowDefinition); expect(executedNodeIds).toContain('start-1'); expect(executedNodeIds).toContain('runjs-1'); expect(executedNodeIds).toContain('runpy-1'); // Verify RunJS node used lodash to sort numbers const runjsNode = executionNodes.find((n: any) => n.idOnWorkflowDefinition === 'runjs-1'); expect(runjsNode).toBeDefined(); expect(runjsNode.executed).toBe(true); const runjsResult = parse(runjsNode.result); expect(runjsResult.status).toBe('ok'); expect(runjsResult.data).toEqual([1, 1, 2, 3, 4, 5, 6, 9]); // sorted // Verify RunPy node used pydash to sum the sorted numbers const runpyNode = executionNodes.find((n: any) => n.idOnWorkflowDefinition === 'runpy-1'); expect(runpyNode).toBeDefined(); expect(runpyNode.executed).toBe(true); const runpyResult = parse(runpyNode.result); expect(runpyResult.status).toBe('ok'); expect(runpyResult.data).toBe(31); // sum of [1,1,2,3,4,5,6,9] = 31 // Verify response node contains both results const responseNode = executionNodes.find((n: any) => n.idOnWorkflowDefinition === 'response-1'); expect(responseNode).toBeDefined(); expect(responseNode.executed).toBe(true); }, 180000); }); }); describe('GET /api/workflow_executions/:id/status', () => { it('should retrieve workflow execution status', async () => { const { user } = await setupOrganizationAndUser(app, { email: 'admin@tooljet.io', password: 'password', firstName: 'Admin', lastName: 'User' }); const { app: workflow, appVersion } = await createCompleteWorkflow(app, user, { name: 'Status Check Workflow', nodes: [ { id: 'start-1', type: 'input', data: { nodeType: 'start', label: 'Start trigger' }, position: { x: 100, y: 250 }, sourcePosition: 'right' } ], edges: [], queries: [] }); const execution = await executeWorkflow(app, workflow, user, { environmentId: appVersion.currentEnvironmentId }); const { tokenCookie } = await authenticateUser(app, user.email); const statusResponse = await request(app.getHttpServer()) .get(`/api/workflow_executions/${execution.id}/status`) .set('tj-workspace-id', user.defaultOrganizationId) .set('Cookie', tokenCookie); expect(statusResponse.statusCode).toBe(200); expect(statusResponse.body).toHaveProperty('status'); }); }); describe('GET /api/workflow_executions/:id', () => { it('should retrieve workflow execution details including logs', async () => { const { user } = await setupOrganizationAndUser(app, { email: 'admin@tooljet.io', password: 'password', firstName: 'Admin', lastName: 'User' }); const { app: workflow, appVersion } = await createCompleteWorkflow(app, user, { name: 'Details Check Workflow', nodes: [ { id: 'start-1', type: 'input', data: { nodeType: 'start', label: 'Start trigger' }, position: { x: 100, y: 250 }, sourcePosition: 'right' } ], edges: [], queries: [] }); const execution = await executeWorkflow(app, workflow, user, { environmentId: appVersion.currentEnvironmentId }); const { tokenCookie } = await authenticateUser(app, user.email); const detailsResponse = await request(app.getHttpServer()) .get(`/api/workflow_executions/${execution.id}`) .set('tj-workspace-id', user.defaultOrganizationId) .set('Cookie', tokenCookie); expect(detailsResponse.statusCode).toBe(200); expect(detailsResponse.body).toHaveProperty('id'); expect(detailsResponse.body.id).toBe(execution.id); }); }); describe('GET /api/workflow_executions/all/:appVersionId', () => { it('should list all executions for an app version', async () => { const { user } = await setupOrganizationAndUser(app, { email: 'admin@tooljet.io', password: 'password', firstName: 'Admin', lastName: 'User' }); const { app: workflow, appVersion } = await createCompleteWorkflow(app, user, { name: 'List Executions Workflow', nodes: [ { id: 'start-1', type: 'input', data: { nodeType: 'start', label: 'Start trigger' }, position: { x: 100, y: 250 }, sourcePosition: 'right' } ], edges: [], queries: [] }); // Execute workflow twice await executeWorkflow(app, workflow, user, { environmentId: appVersion.currentEnvironmentId }); await executeWorkflow(app, workflow, user, { environmentId: appVersion.currentEnvironmentId }); const { tokenCookie } = await authenticateUser(app, user.email); const listResponse = await request(app.getHttpServer()) .get(`/api/workflow_executions/all/${appVersion.id}`) .set('tj-workspace-id', user.defaultOrganizationId) .set('Cookie', tokenCookie); expect(listResponse.statusCode).toBe(200); expect(Array.isArray(listResponse.body)).toBe(true); expect(listResponse.body.length).toBe(2); }); }); describe('GET /api/workflow_executions', () => { it('should retrieve paginated execution logs', async () => { const { user } = await setupOrganizationAndUser(app, { email: 'admin@tooljet.io', password: 'password', firstName: 'Admin', lastName: 'User' }); const { app: workflow, appVersion } = await createCompleteWorkflow(app, user, { name: 'Pagination Test Workflow', nodes: [ { id: 'start-1', type: 'input', data: { nodeType: 'start', label: 'Start trigger' }, position: { x: 100, y: 250 }, sourcePosition: 'right' } ], edges: [], queries: [] }); // Execute workflow multiple times for (let i = 0; i < 3; i++) { await executeWorkflow(app, workflow, user, { environmentId: appVersion.currentEnvironmentId }); } const { tokenCookie } = await authenticateUser(app, user.email); const logsResponse = await request(app.getHttpServer()) .get(`/api/workflow_executions?appVersionId=${appVersion.id}&page=1&per_page=2`) .set('tj-workspace-id', user.defaultOrganizationId) .set('Cookie', tokenCookie); expect(logsResponse.statusCode).toBe(200); expect(logsResponse.body).toHaveProperty('data'); expect(Array.isArray(logsResponse.body.data)).toBe(true); }); }); describe('GET /api/workflow_executions/:id/nodes', () => { it('should retrieve execution nodes with pagination', async () => { const { user } = await setupOrganizationAndUser(app, { email: 'admin@tooljet.io', password: 'password', firstName: 'Admin', lastName: 'User' }); const nodes: WorkflowNode[] = [ { id: 'start-1', type: 'input', data: { nodeType: 'start', label: 'Start trigger' }, position: { x: 100, y: 250 }, sourcePosition: 'right' }, { id: 'runjs-1', type: 'query', data: { idOnDefinition: 'query-runjs-1', kind: 'runjs', options: {} }, position: { x: 350, y: 250 }, sourcePosition: 'right', targetPosition: 'left' } ]; const edges: WorkflowEdge[] = [ { id: 'edge-1', source: 'start-1', target: 'runjs-1', type: 'workflow' } ]; const queries: WorkflowQuery[] = [ { idOnDefinition: 'query-runjs-1', dataSourceKind: 'runjs', name: 'runjs1', options: { code: `return { message: "Hello from RunJS" };`, parameters: [] } } ]; const { app: workflow, appVersion } = await createCompleteWorkflow(app, user, { name: 'Nodes Test Workflow', nodes, edges, queries }); const execution = await executeWorkflow(app, workflow, user, { environmentId: appVersion.currentEnvironmentId }); const { tokenCookie } = await authenticateUser(app, user.email); const nodesResponse = await request(app.getHttpServer()) .get(`/api/workflow_executions/${execution.id}/nodes?page=1&per_page=10`) .set('tj-workspace-id', user.defaultOrganizationId) .set('Cookie', tokenCookie); expect(nodesResponse.statusCode).toBe(200); expect(nodesResponse.body).toHaveProperty('data'); expect(Array.isArray(nodesResponse.body.data)).toBe(true); }); }); });