mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
refactor(core): Include native Python option in Code node (#18331)
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
|
/* eslint-disable n8n-nodes-base/node-execute-block-wrong-error-thrown */
|
||||||
import { NodesConfig, TaskRunnersConfig } from '@n8n/config';
|
import { NodesConfig, TaskRunnersConfig } from '@n8n/config';
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
import set from 'lodash/set';
|
import set from 'lodash/set';
|
||||||
import {
|
import {
|
||||||
|
type INodeProperties,
|
||||||
NodeConnectionTypes,
|
NodeConnectionTypes,
|
||||||
UserError,
|
UserError,
|
||||||
type CodeExecutionMode,
|
type CodeExecutionMode,
|
||||||
@@ -12,15 +14,19 @@ import {
|
|||||||
type INodeTypeDescription,
|
type INodeTypeDescription,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
type CodeNodeLanguageOption = CodeNodeEditorLanguage | 'pythonNative';
|
||||||
|
|
||||||
import { javascriptCodeDescription } from './descriptions/JavascriptCodeDescription';
|
import { javascriptCodeDescription } from './descriptions/JavascriptCodeDescription';
|
||||||
import { pythonCodeDescription } from './descriptions/PythonCodeDescription';
|
import { pythonCodeDescription } from './descriptions/PythonCodeDescription';
|
||||||
import { JavaScriptSandbox } from './JavaScriptSandbox';
|
import { JavaScriptSandbox } from './JavaScriptSandbox';
|
||||||
import { JsTaskRunnerSandbox } from './JsTaskRunnerSandbox';
|
import { JsTaskRunnerSandbox } from './JsTaskRunnerSandbox';
|
||||||
|
import { NativePythonWithoutRunnerError } from './native-python-without-runner.error';
|
||||||
import { PythonSandbox } from './PythonSandbox';
|
import { PythonSandbox } from './PythonSandbox';
|
||||||
|
import { PythonTaskRunnerSandbox } from './PythonTaskRunnerSandbox';
|
||||||
import { getSandboxContext } from './Sandbox';
|
import { getSandboxContext } from './Sandbox';
|
||||||
import { addPostExecutionWarning, standardizeOutput } from './utils';
|
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 {
|
class PythonDisabledError extends UserError {
|
||||||
constructor() {
|
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 {
|
export class Code implements INodeType {
|
||||||
description: INodeTypeDescription = {
|
description: INodeTypeDescription = {
|
||||||
displayName: 'Code',
|
displayName: 'Code',
|
||||||
@@ -65,28 +105,7 @@ export class Code implements INodeType {
|
|||||||
],
|
],
|
||||||
default: 'runOnceForAllItems',
|
default: 'runOnceForAllItems',
|
||||||
},
|
},
|
||||||
{
|
getV2LanguageProperty(),
|
||||||
displayName: 'Language',
|
|
||||||
name: 'language',
|
|
||||||
type: 'options',
|
|
||||||
noDataExpression: true,
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
'@version': [2],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: 'JavaScript',
|
|
||||||
value: 'javaScript',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Python (Beta)',
|
|
||||||
value: 'python',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
default: 'javaScript',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
displayName: 'Language',
|
displayName: 'Language',
|
||||||
name: 'language',
|
name: 'language',
|
||||||
@@ -106,22 +125,24 @@ export class Code implements INodeType {
|
|||||||
|
|
||||||
async execute(this: IExecuteFunctions) {
|
async execute(this: IExecuteFunctions) {
|
||||||
const node = this.getNode();
|
const node = this.getNode();
|
||||||
const language: CodeNodeEditorLanguage =
|
const language: CodeNodeLanguageOption =
|
||||||
node.typeVersion === 2
|
node.typeVersion === 2
|
||||||
? (this.getNodeParameter('language', 0) as CodeNodeEditorLanguage)
|
? (this.getNodeParameter('language', 0) as CodeNodeLanguageOption)
|
||||||
: 'javaScript';
|
: 'javaScript';
|
||||||
|
|
||||||
if (language === 'python' && !Container.get(NodesConfig).pythonEnabled) {
|
if (language === 'python' && !Container.get(NodesConfig).pythonEnabled) {
|
||||||
// eslint-disable-next-line n8n-nodes-base/node-execute-block-wrong-error-thrown
|
|
||||||
throw new PythonDisabledError();
|
throw new PythonDisabledError();
|
||||||
}
|
}
|
||||||
|
|
||||||
const runnersConfig = Container.get(TaskRunnersConfig);
|
const runnersConfig = Container.get(TaskRunnersConfig);
|
||||||
|
const isRunnerEnabled = runnersConfig.enabled;
|
||||||
|
|
||||||
const nodeMode = this.getNodeParameter('mode', 0) as CodeExecutionMode;
|
const nodeMode = this.getNodeParameter('mode', 0) as CodeExecutionMode;
|
||||||
const workflowMode = this.getMode();
|
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 code = this.getNodeParameter(codeParameterName, 0) as string;
|
||||||
const sandbox = new JsTaskRunnerSandbox(code, nodeMode, workflowMode, this);
|
const sandbox = new JsTaskRunnerSandbox(code, nodeMode, workflowMode, this);
|
||||||
const numInputItems = this.getInputData().length;
|
const numInputItems = this.getInputData().length;
|
||||||
@@ -131,6 +152,15 @@ export class Code implements INodeType {
|
|||||||
: [await sandbox.runCodeForEachItem(numInputItems)];
|
: [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 getSandbox = (index = 0) => {
|
||||||
const code = this.getNodeParameter(codeParameterName, index) as string;
|
const code = this.getNodeParameter(codeParameterName, index) as string;
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
ApplicationError,
|
|
||||||
type CodeExecutionMode,
|
type CodeExecutionMode,
|
||||||
type IExecuteFunctions,
|
type IExecuteFunctions,
|
||||||
type INodeExecutionData,
|
type INodeExecutionData,
|
||||||
type WorkflowExecuteMode,
|
type WorkflowExecuteMode,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { isWrappableError, WrappedExecutionError } from './errors/WrappedExecutionError';
|
|
||||||
import { validateNoDisallowedMethodsInRunForEach } from './JsCodeValidator';
|
import { validateNoDisallowedMethodsInRunForEach } from './JsCodeValidator';
|
||||||
|
import { throwExecutionError } from './throw-execution-error';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JS Code execution sandbox that executes the JS code using task runner.
|
* JS Code execution sandbox that executes the JS code using task runner.
|
||||||
@@ -37,7 +36,7 @@ export class JsTaskRunnerSandbox {
|
|||||||
|
|
||||||
return executionResult.ok
|
return executionResult.ok
|
||||||
? executionResult.result
|
? executionResult.result
|
||||||
: this.throwExecutionError('error' in executionResult ? executionResult.error : {});
|
: throwExecutionError('error' in executionResult ? executionResult.error : {});
|
||||||
}
|
}
|
||||||
|
|
||||||
async runCodeForEachItem(numInputItems: number): Promise<INodeExecutionData[]> {
|
async runCodeForEachItem(numInputItems: number): Promise<INodeExecutionData[]> {
|
||||||
@@ -64,7 +63,7 @@ export class JsTaskRunnerSandbox {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!executionResult.ok) {
|
if (!executionResult.ok) {
|
||||||
return this.throwExecutionError('error' in executionResult ? executionResult.error : {});
|
return throwExecutionError('error' in executionResult ? executionResult.error : {});
|
||||||
}
|
}
|
||||||
|
|
||||||
executionResults = executionResults.concat(executionResult.result);
|
executionResults = executionResults.concat(executionResult.result);
|
||||||
@@ -73,18 +72,6 @@ export class JsTaskRunnerSandbox {
|
|||||||
return executionResults;
|
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 */
|
/** Chunks the input items into chunks of 1000 items each */
|
||||||
private chunkInputItems(numInputItems: number) {
|
private chunkInputItems(numInputItems: number) {
|
||||||
const numChunks = Math.ceil(numInputItems / this.chunkSize);
|
const numChunks = Math.ceil(numInputItems / this.chunkSize);
|
||||||
|
|||||||
46
packages/nodes-base/nodes/Code/PythonTaskRunnerSandbox.ts
Normal file
46
packages/nodes-base/nodes/Code/PythonTaskRunnerSandbox.ts
Normal file
@@ -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<string, unknown> = {
|
||||||
|
code: this.pythonCode,
|
||||||
|
nodeMode: this.nodeMode,
|
||||||
|
workflowMode: this.workflowMode,
|
||||||
|
continueOnFail: this.executeFunctions.continueOnFail(),
|
||||||
|
items: this.executeFunctions.getInputData(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const executionResult = await this.executeFunctions.startJob<INodeExecutionData[]>(
|
||||||
|
'python',
|
||||||
|
taskSettings,
|
||||||
|
itemIndex,
|
||||||
|
);
|
||||||
|
|
||||||
|
return executionResult.ok
|
||||||
|
? executionResult.result
|
||||||
|
: throwExecutionError('error' in executionResult ? executionResult.error : {});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,12 +14,15 @@ const commonDescription: INodeProperties = {
|
|||||||
noDataExpression: true,
|
noDataExpression: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PRINT_INSTRUCTION =
|
||||||
|
'Debug by using <code>print()</code> statements and viewing their output in the browser console.';
|
||||||
|
|
||||||
export const pythonCodeDescription: INodeProperties[] = [
|
export const pythonCodeDescription: INodeProperties[] = [
|
||||||
{
|
{
|
||||||
...commonDescription,
|
...commonDescription,
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
show: {
|
show: {
|
||||||
language: ['python'],
|
language: ['python', 'pythonNative'],
|
||||||
mode: ['runOnceForAllItems'],
|
mode: ['runOnceForAllItems'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -28,14 +31,13 @@ export const pythonCodeDescription: INodeProperties[] = [
|
|||||||
...commonDescription,
|
...commonDescription,
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
show: {
|
show: {
|
||||||
language: ['python'],
|
language: ['python', 'pythonNative'],
|
||||||
mode: ['runOnceForEachItem'],
|
mode: ['runOnceForEachItem'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName:
|
displayName: PRINT_INSTRUCTION,
|
||||||
'Debug by using <code>print()</code> statements and viewing their output in the browser console.',
|
|
||||||
name: 'notice',
|
name: 'notice',
|
||||||
type: 'notice',
|
type: 'notice',
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
@@ -45,4 +47,15 @@ export const pythonCodeDescription: INodeProperties[] = [
|
|||||||
},
|
},
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: `${PRINT_INSTRUCTION}<br><br>The native Python option does not support <code>_</code> syntax and helpers, except for <code>_items</code> and <code>_item</code>.`,
|
||||||
|
name: 'notice',
|
||||||
|
type: 'notice',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
language: ['pythonNative'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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`.');
|
||||||
|
}
|
||||||
|
}
|
||||||
15
packages/nodes-base/nodes/Code/throw-execution-error.ts
Normal file
15
packages/nodes-base/nodes/Code/throw-execution-error.ts
Normal file
@@ -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)}`);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user