diff --git a/packages/nodes-base/nodes/Code/Code.node.ts b/packages/nodes-base/nodes/Code/Code.node.ts index fe1de04acc..409504cd5e 100644 --- a/packages/nodes-base/nodes/Code/Code.node.ts +++ b/packages/nodes-base/nodes/Code/Code.node.ts @@ -1,7 +1,9 @@ +/* eslint-disable n8n-nodes-base/node-execute-block-wrong-error-thrown */ import { NodesConfig, TaskRunnersConfig } from '@n8n/config'; import { Container } from '@n8n/di'; import set from 'lodash/set'; import { + type INodeProperties, NodeConnectionTypes, UserError, type CodeExecutionMode, @@ -12,15 +14,19 @@ import { type INodeTypeDescription, } from 'n8n-workflow'; +type CodeNodeLanguageOption = CodeNodeEditorLanguage | 'pythonNative'; + import { javascriptCodeDescription } from './descriptions/JavascriptCodeDescription'; import { pythonCodeDescription } from './descriptions/PythonCodeDescription'; import { JavaScriptSandbox } from './JavaScriptSandbox'; import { JsTaskRunnerSandbox } from './JsTaskRunnerSandbox'; +import { NativePythonWithoutRunnerError } from './native-python-without-runner.error'; import { PythonSandbox } from './PythonSandbox'; +import { PythonTaskRunnerSandbox } from './PythonTaskRunnerSandbox'; import { getSandboxContext } from './Sandbox'; import { addPostExecutionWarning, standardizeOutput } from './utils'; -const { CODE_ENABLE_STDOUT } = process.env; +const { CODE_ENABLE_STDOUT, N8N_NATIVE_PYTHON_RUNNER } = process.env; class PythonDisabledError extends UserError { constructor() { @@ -30,6 +36,40 @@ class PythonDisabledError extends UserError { } } +const getV2LanguageProperty = (): INodeProperties => { + const options = [ + { + name: 'JavaScript', + value: 'javaScript', + }, + { + name: 'Python (Beta)', + value: 'python', + }, + ]; + + if (N8N_NATIVE_PYTHON_RUNNER === 'true') { + options.push({ + name: 'Python (Native) (Beta)', + value: 'pythonNative', + }); + } + + return { + displayName: 'Language', + name: 'language', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + '@version': [2], + }, + }, + options, + default: 'javaScript', + }; +}; + export class Code implements INodeType { description: INodeTypeDescription = { displayName: 'Code', @@ -65,28 +105,7 @@ export class Code implements INodeType { ], default: 'runOnceForAllItems', }, - { - displayName: 'Language', - name: 'language', - type: 'options', - noDataExpression: true, - displayOptions: { - show: { - '@version': [2], - }, - }, - options: [ - { - name: 'JavaScript', - value: 'javaScript', - }, - { - name: 'Python (Beta)', - value: 'python', - }, - ], - default: 'javaScript', - }, + getV2LanguageProperty(), { displayName: 'Language', name: 'language', @@ -106,22 +125,24 @@ export class Code implements INodeType { async execute(this: IExecuteFunctions) { const node = this.getNode(); - const language: CodeNodeEditorLanguage = + const language: CodeNodeLanguageOption = node.typeVersion === 2 - ? (this.getNodeParameter('language', 0) as CodeNodeEditorLanguage) + ? (this.getNodeParameter('language', 0) as CodeNodeLanguageOption) : 'javaScript'; if (language === 'python' && !Container.get(NodesConfig).pythonEnabled) { - // eslint-disable-next-line n8n-nodes-base/node-execute-block-wrong-error-thrown throw new PythonDisabledError(); } const runnersConfig = Container.get(TaskRunnersConfig); + const isRunnerEnabled = runnersConfig.enabled; + const nodeMode = this.getNodeParameter('mode', 0) as CodeExecutionMode; const workflowMode = this.getMode(); - const codeParameterName = language === 'python' ? 'pythonCode' : 'jsCode'; + const codeParameterName = + language === 'python' || language === 'pythonNative' ? 'pythonCode' : 'jsCode'; - if (runnersConfig.enabled && language === 'javaScript') { + if (language === 'javaScript' && isRunnerEnabled) { const code = this.getNodeParameter(codeParameterName, 0) as string; const sandbox = new JsTaskRunnerSandbox(code, nodeMode, workflowMode, this); const numInputItems = this.getInputData().length; @@ -131,6 +152,15 @@ export class Code implements INodeType { : [await sandbox.runCodeForEachItem(numInputItems)]; } + if (language === 'pythonNative' && !isRunnerEnabled) throw new NativePythonWithoutRunnerError(); + + if (language === 'pythonNative') { + const code = this.getNodeParameter(codeParameterName, 0) as string; + const sandbox = new PythonTaskRunnerSandbox(code, nodeMode, workflowMode, this); + + return [await sandbox.runUsingIncomingItems()]; + } + const getSandbox = (index = 0) => { const code = this.getNodeParameter(codeParameterName, index) as string; diff --git a/packages/nodes-base/nodes/Code/JsTaskRunnerSandbox.ts b/packages/nodes-base/nodes/Code/JsTaskRunnerSandbox.ts index 874ce8af8d..4f0419844f 100644 --- a/packages/nodes-base/nodes/Code/JsTaskRunnerSandbox.ts +++ b/packages/nodes-base/nodes/Code/JsTaskRunnerSandbox.ts @@ -1,13 +1,12 @@ import { - ApplicationError, type CodeExecutionMode, type IExecuteFunctions, type INodeExecutionData, type WorkflowExecuteMode, } from 'n8n-workflow'; -import { isWrappableError, WrappedExecutionError } from './errors/WrappedExecutionError'; import { validateNoDisallowedMethodsInRunForEach } from './JsCodeValidator'; +import { throwExecutionError } from './throw-execution-error'; /** * JS Code execution sandbox that executes the JS code using task runner. @@ -37,7 +36,7 @@ export class JsTaskRunnerSandbox { return executionResult.ok ? executionResult.result - : this.throwExecutionError('error' in executionResult ? executionResult.error : {}); + : throwExecutionError('error' in executionResult ? executionResult.error : {}); } async runCodeForEachItem(numInputItems: number): Promise { @@ -64,7 +63,7 @@ export class JsTaskRunnerSandbox { ); if (!executionResult.ok) { - return this.throwExecutionError('error' in executionResult ? executionResult.error : {}); + return throwExecutionError('error' in executionResult ? executionResult.error : {}); } executionResults = executionResults.concat(executionResult.result); @@ -73,18 +72,6 @@ export class JsTaskRunnerSandbox { 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); diff --git a/packages/nodes-base/nodes/Code/PythonTaskRunnerSandbox.ts b/packages/nodes-base/nodes/Code/PythonTaskRunnerSandbox.ts new file mode 100644 index 0000000000..0499b52778 --- /dev/null +++ b/packages/nodes-base/nodes/Code/PythonTaskRunnerSandbox.ts @@ -0,0 +1,46 @@ +import { + type CodeExecutionMode, + type IExecuteFunctions, + type INodeExecutionData, + type WorkflowExecuteMode, +} from 'n8n-workflow'; + +import { throwExecutionError } from './throw-execution-error'; + +export class PythonTaskRunnerSandbox { + constructor( + private readonly pythonCode: string, + private readonly nodeMode: CodeExecutionMode, + private readonly workflowMode: WorkflowExecuteMode, + private readonly executeFunctions: IExecuteFunctions, + ) {} + + /** + * Run a script by forwarding it to a Python task runner, together with input items. + * + * The Python runner receives input items together with the task, whereas the + * JavaScript runner does _not_ receive input items together with the task and + * instead retrieves them later, only if needed, via an RPC request. + */ + async runUsingIncomingItems() { + const itemIndex = 0; + + const taskSettings: Record = { + code: this.pythonCode, + nodeMode: this.nodeMode, + workflowMode: this.workflowMode, + continueOnFail: this.executeFunctions.continueOnFail(), + items: this.executeFunctions.getInputData(), + }; + + const executionResult = await this.executeFunctions.startJob( + 'python', + taskSettings, + itemIndex, + ); + + return executionResult.ok + ? executionResult.result + : throwExecutionError('error' in executionResult ? executionResult.error : {}); + } +} diff --git a/packages/nodes-base/nodes/Code/descriptions/PythonCodeDescription.ts b/packages/nodes-base/nodes/Code/descriptions/PythonCodeDescription.ts index e3c9d4cb43..a15bacd7a8 100644 --- a/packages/nodes-base/nodes/Code/descriptions/PythonCodeDescription.ts +++ b/packages/nodes-base/nodes/Code/descriptions/PythonCodeDescription.ts @@ -14,12 +14,15 @@ const commonDescription: INodeProperties = { noDataExpression: true, }; +const PRINT_INSTRUCTION = + 'Debug by using print() statements and viewing their output in the browser console.'; + export const pythonCodeDescription: INodeProperties[] = [ { ...commonDescription, displayOptions: { show: { - language: ['python'], + language: ['python', 'pythonNative'], mode: ['runOnceForAllItems'], }, }, @@ -28,14 +31,13 @@ export const pythonCodeDescription: INodeProperties[] = [ ...commonDescription, displayOptions: { show: { - language: ['python'], + language: ['python', 'pythonNative'], mode: ['runOnceForEachItem'], }, }, }, { - displayName: - 'Debug by using print() statements and viewing their output in the browser console.', + displayName: PRINT_INSTRUCTION, name: 'notice', type: 'notice', displayOptions: { @@ -45,4 +47,15 @@ export const pythonCodeDescription: INodeProperties[] = [ }, default: '', }, + { + displayName: `${PRINT_INSTRUCTION}

The native Python option does not support _ syntax and helpers, except for _items and _item.`, + name: 'notice', + type: 'notice', + displayOptions: { + show: { + language: ['pythonNative'], + }, + }, + default: '', + }, ]; diff --git a/packages/nodes-base/nodes/Code/native-python-without-runner.error.ts b/packages/nodes-base/nodes/Code/native-python-without-runner.error.ts new file mode 100644 index 0000000000..97cd142175 --- /dev/null +++ b/packages/nodes-base/nodes/Code/native-python-without-runner.error.ts @@ -0,0 +1,7 @@ +import { UserError } from 'n8n-workflow'; + +export class NativePythonWithoutRunnerError extends UserError { + constructor() { + super('To use native Python, please use runners by setting `N8N_RUNNERS_ENABLED=true`.'); + } +} diff --git a/packages/nodes-base/nodes/Code/throw-execution-error.ts b/packages/nodes-base/nodes/Code/throw-execution-error.ts new file mode 100644 index 0000000000..ad61803c18 --- /dev/null +++ b/packages/nodes-base/nodes/Code/throw-execution-error.ts @@ -0,0 +1,15 @@ +import { ApplicationError } from 'n8n-workflow'; + +import { isWrappableError, WrappedExecutionError } from './errors/WrappedExecutionError'; + +export function 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)}`); +}