mirror of
https://github.com/ToolJet/ToolJet
synced 2026-04-21 21:47:17 +00:00
Replace two separate helper functions with a single createBundle function
that takes an explicit language parameter ('javascript' | 'python').
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1597 lines
50 KiB
TypeScript
1597 lines
50 KiB
TypeScript
/** @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<string, any>;
|
|
} = {}
|
|
): Promise<any> => {
|
|
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<DataSource>(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<string, any> | 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<string, any> | 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);
|
|
});
|
|
});
|
|
});
|