import { ApplicationError, type CodeExecutionMode, type IExecuteFunctions, type INodeExecutionData, type WorkflowExecuteMode, } from 'n8n-workflow'; import { isWrappableError, WrappedExecutionError } from './errors/WrappedExecutionError'; import { validateNoDisallowedMethodsInRunForEach } from './JsCodeValidator'; /** * JS Code execution sandbox that executes the JS code using task runner. */ export class JsTaskRunnerSandbox { constructor( private readonly jsCode: string, private readonly nodeMode: CodeExecutionMode, private readonly workflowMode: WorkflowExecuteMode, private readonly executeFunctions: IExecuteFunctions, private readonly chunkSize = 1000, ) {} async runCodeAllItems(): Promise { const itemIndex = 0; const executionResult = await this.executeFunctions.startJob( 'javascript', { code: this.jsCode, nodeMode: this.nodeMode, workflowMode: this.workflowMode, continueOnFail: this.executeFunctions.continueOnFail(), }, itemIndex, ); return executionResult.ok ? executionResult.result : this.throwExecutionError(executionResult.error); } async runCodeForEachItem(numInputItems: number): Promise { validateNoDisallowedMethodsInRunForEach(this.jsCode, 0); const itemIndex = 0; const chunks = this.chunkInputItems(numInputItems); let executionResults: INodeExecutionData[] = []; for (const chunk of chunks) { const executionResult = await this.executeFunctions.startJob( 'javascript', { code: this.jsCode, nodeMode: this.nodeMode, workflowMode: this.workflowMode, continueOnFail: this.executeFunctions.continueOnFail(), chunk: { startIndex: chunk.startIdx, count: chunk.count, }, }, itemIndex, ); if (!executionResult.ok) { return this.throwExecutionError(executionResult.error); } executionResults = executionResults.concat(executionResult.result); } return executionResults; } private throwExecutionError(error: unknown): never { if (error instanceof Error) { throw error; } else if (isWrappableError(error)) { // The error coming from task runner is not an instance of error, // so we need to wrap it in an error instance. throw new WrappedExecutionError(error); } throw new ApplicationError(`Unknown error: ${JSON.stringify(error)}`); } /** Chunks the input items into chunks of 1000 items each */ private chunkInputItems(numInputItems: number) { const numChunks = Math.ceil(numInputItems / this.chunkSize); const chunks = []; for (let i = 0; i < numChunks; i++) { const startIdx = i * this.chunkSize; const isLastChunk = i === numChunks - 1; const count = isLastChunk ? numInputItems - startIdx : this.chunkSize; chunks.push({ startIdx, count, }); } return chunks; } }