Files
n8n/packages/nodes-base/nodes/FunctionItem/FunctionItem.node.ts
alighasami 3d5eaf9445
Some checks failed
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
first commit
2026-03-17 16:22:57 +03:30

246 lines
7.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { NodeVMOptions } from 'vm2';
import { NodeVM } from 'vm2';
import type {
IExecuteFunctions,
IBinaryKeyData,
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeConnectionTypes, deepCopy, NodeOperationError } from 'n8n-workflow';
import { vmResolver } from '../Code/JavaScriptSandbox';
export class FunctionItem implements INodeType {
description: INodeTypeDescription = {
displayName: 'Function Item',
name: 'functionItem',
hidden: true,
icon: 'fa:code',
group: ['transform'],
version: 1,
description: 'Run custom function code which gets executed once per item',
defaults: {
name: 'Function Item',
color: '#ddbb33',
},
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
properties: [
{
displayName: 'A newer version of this node type is available, called the Code node',
name: 'notice',
type: 'notice',
default: '',
},
{
displayName: 'JavaScript Code',
name: 'functionCode',
typeOptions: {
alwaysOpenEditWindow: true,
codeAutocomplete: 'functionItem',
editor: 'jsEditor',
rows: 10,
},
type: 'string',
default: `// Code here will run once per input item.
// More info and help: https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.functionitem/
// Tip: You can use luxon for dates and $jmespath for querying JSON structures
// Add a new field called 'myNewField' to the JSON of the item
item.myNewField = 1;
// You can write logs to the browser console
console.log('Done!');
return item;`,
description: 'The JavaScript code to execute for each item',
noDataExpression: true,
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const length = items.length;
let item: INodeExecutionData;
const cleanupData = (inputData: IDataObject): IDataObject => {
Object.keys(inputData).map((key) => {
if (inputData[key] !== null && typeof inputData[key] === 'object') {
if (inputData[key].constructor.name === 'Object') {
// Is regular node.js object so check its data
inputData[key] = cleanupData(inputData[key] as IDataObject);
} else {
// Is some special object like a Date so stringify
inputData[key] = deepCopy(inputData[key]);
}
}
});
return inputData;
};
for (let itemIndex = 0; itemIndex < length; itemIndex++) {
const mode = this.getMode();
try {
item = items[itemIndex];
item.index = itemIndex;
// Copy the items as they may get changed in the functions
item = deepCopy(item);
// Define the global objects for the custom function
const sandbox = {
/** @deprecated for removal - replaced by getBinaryDataAsync() */
getBinaryData: (): IBinaryKeyData | undefined => {
if (mode === 'manual') {
this.sendMessageToUI(
'getBinaryData(...) is deprecated and will be removed in a future version. Please consider switching to getBinaryDataAsync(...) instead.',
);
}
return item.binary;
},
/** @deprecated for removal - replaced by setBinaryDataAsync() */
setBinaryData: async (data: IBinaryKeyData) => {
if (mode === 'manual') {
this.sendMessageToUI(
'setBinaryData(...) is deprecated and will be removed in a future version. Please consider switching to setBinaryDataAsync(...) instead.',
);
}
item.binary = data;
},
getNodeParameter: this.getNodeParameter.bind(this),
getWorkflowStaticData: this.getWorkflowStaticData.bind(this),
helpers: this.helpers,
item: item.json,
getBinaryDataAsync: async (): Promise<IBinaryKeyData | undefined> => {
// Fetch Binary Data, if available. Cannot check item with `if (item?.index)`, as index may be 0.
if (item?.binary && item?.index !== undefined && item?.index !== null) {
for (const binaryPropertyName of Object.keys(item.binary)) {
item.binary[binaryPropertyName].data = (
await this.helpers.getBinaryDataBuffer(item.index, binaryPropertyName)
)?.toString('base64');
}
}
// Return Data
return item.binary;
},
setBinaryDataAsync: async (data: IBinaryKeyData) => {
// Ensure data is present
if (!data) {
throw new NodeOperationError(
this.getNode(),
'No data was provided to setBinaryDataAsync (data: IBinaryKeyData).',
);
}
// Set Binary Data
for (const binaryPropertyName of Object.keys(data)) {
const binaryItem = data[binaryPropertyName];
data[binaryPropertyName] = await this.helpers.setBinaryDataBuffer(
binaryItem,
Buffer.from(binaryItem.data, 'base64'),
);
}
// Set Item Reference
item.binary = data;
},
};
// Make it possible to access data via $node, $parameter, ...
const dataProxy = this.getWorkflowDataProxy(itemIndex);
Object.assign(sandbox, dataProxy);
const options: NodeVMOptions = {
console: mode === 'manual' ? 'redirect' : 'inherit',
sandbox,
require: vmResolver,
};
const vm = new NodeVM(options as unknown as NodeVMOptions);
if (mode === 'manual') {
vm.on('console.log', this.sendMessageToUI.bind(this));
}
// Get the code to execute
const functionCode = this.getNodeParameter('functionCode', itemIndex) as string;
let jsonData: IDataObject;
try {
// Execute the function code
jsonData = await vm.run(
`module.exports = async function() {${functionCode}\n}()`,
__dirname,
);
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ json: { error: error.message } });
continue;
} else {
// Try to find the line number which contains the error and attach to error message
const stackLines = error.stack.split('\n');
if (stackLines.length > 0) {
stackLines.shift();
const lineParts = stackLines
.find((line: string) => line.includes('FunctionItem'))
.split(':');
if (lineParts.length > 2) {
const lineNumber = lineParts.splice(-2, 1);
if (!isNaN(lineNumber as number)) {
error.message = `${error.message} [Line ${lineNumber} | Item Index: ${itemIndex}]`;
throw error;
}
}
}
error.message = `${error.message} [Item Index: ${itemIndex}]`;
throw error;
}
}
// Do very basic validation of the data
if (jsonData === undefined) {
throw new NodeOperationError(
this.getNode(),
'No data got returned. Always an object has to be returned!',
);
}
const returnItem: INodeExecutionData = {
json: cleanupData(jsonData),
pairedItem: {
item: itemIndex,
},
};
if (item.binary) {
returnItem.binary = item.binary;
}
returnData.push(returnItem);
} catch (error) {
if (this.continueOnFail()) {
returnData.push({
json: {
error: error.message,
},
pairedItem: {
item: itemIndex,
},
});
continue;
}
throw error;
}
}
return [returnData];
}
}