n8n/packages/nodes-base/nodes/Function/Function.node.ts

230 lines
6.7 KiB
TypeScript
Raw 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 '@n8n/vm2';
import { NodeVM } from '@n8n/vm2';
import type {
IExecuteFunctions,
IBinaryKeyData,
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeConnectionType, deepCopy, NodeOperationError } from 'n8n-workflow';
import { vmResolver } from '../Code/JavaScriptSandbox';
export class Function implements INodeType {
description: INodeTypeDescription = {
displayName: 'Function',
name: 'function',
hidden: true,
icon: 'fa:code',
group: ['transform'],
version: 1,
description:
'Run custom function code which gets executed once and allows you to add, remove, change and replace items',
defaults: {
name: 'Function',
color: '#FF9922',
},
inputs: [NodeConnectionType.Main],
outputs: [NodeConnectionType.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: 'function',
editor: 'jsEditor',
rows: 10,
},
type: 'string',
default: `// Code here will run only once, no matter how many input items there are.
// More info and help:https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.function/
// Tip: You can use luxon for dates and $jmespath for querying JSON structures
// Loop over inputs and add a new field called 'myNewField' to the JSON of each one
for (item of items) {
item.json.myNewField = 1;
}
// You can write logs to the browser console
console.log('Done!');
return items;`,
description: 'The JavaScript code to execute',
noDataExpression: true,
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
// const item = this.getInputData();
let items = this.getInputData();
// Copy the items as they may get changed in the functions
items = deepCopy(items);
// Assign item indexes
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
items[itemIndex].index = itemIndex;
}
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;
};
// Define the global objects for the custom function
const sandbox = {
getNodeParameter: this.getNodeParameter,
getWorkflowStaticData: this.getWorkflowStaticData,
helpers: this.helpers,
items,
// To be able to access data of other items
$item: (index: number) => this.getWorkflowDataProxy(index),
getBinaryDataAsync: async (item: INodeExecutionData): 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 (item: INodeExecutionData, data: IBinaryKeyData) => {
// Ensure item is provided, else return a friendly error.
if (!item) {
throw new NodeOperationError(
this.getNode(),
'No item was provided to setBinaryDataAsync (item: INodeExecutionData, data: IBinaryKeyData).',
);
}
// Ensure data is provided, else return a friendly error.
if (!data) {
throw new NodeOperationError(
this.getNode(),
'No data was provided to setBinaryDataAsync (item: INodeExecutionData, 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, ...
// By default use data from first item
Object.assign(sandbox, sandbox.$item(0));
const mode = this.getMode();
const options: NodeVMOptions = {
console: mode === 'manual' ? 'redirect' : 'inherit',
sandbox,
require: vmResolver,
};
const vm = new NodeVM(options);
if (mode === 'manual') {
vm.on('console.log', this.sendMessageToUI);
}
// Get the code to execute
const functionCode = this.getNodeParameter('functionCode', 0) as string;
try {
// Execute the function code
items = await vm.run(`module.exports = async function() {${functionCode}\n}()`, __dirname);
items = this.helpers.normalizeItems(items);
// Do very basic validation of the data
if (items === undefined) {
throw new NodeOperationError(
this.getNode(),
'No data got returned. Always return an Array of items!',
);
}
if (!Array.isArray(items)) {
throw new NodeOperationError(
this.getNode(),
'Always an Array of items has to be returned!',
);
}
for (const item of items) {
if (item.json === undefined) {
throw new NodeOperationError(
this.getNode(),
'All returned items have to contain a property named "json"!',
);
}
if (typeof item.json !== 'object') {
throw new NodeOperationError(this.getNode(), 'The json-property has to be an object!');
}
item.json = cleanupData(item.json);
if (item.binary !== undefined) {
if (Array.isArray(item.binary) || typeof item.binary !== 'object') {
throw new NodeOperationError(
this.getNode(),
'The binary-property has to be an object!',
);
}
}
}
} catch (error) {
if (this.continueOnFail(error)) {
items = [{ json: { error: error.message } }];
} 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('Function')).split(':');
if (lineParts.length > 2) {
const lineNumber = lineParts.splice(-2, 1);
if (!isNaN(lineNumber as number)) {
error.message = `${error.message} [Line ${lineNumber}]`;
}
}
}
throw error;
}
}
return [items];
}
}