feat(Code Node): create Code node (#3965)

* Introduce node deprecation (#3930)

 Introduce node deprecation

* 🚧 Scaffold out Code node

* 👕 Fix lint

* 📘 Create types file

* 🚚 Rename theme

* 🔥 Remove unneeded prop

*  Override keybindings

*  Expand lintings

*  Create editor content getter

* 🚚 Ensure all helpers use `$`

*  Add autocompletion

*  Filter out welcome note node

*  Convey error line number

*  Highlight error line

*  Restore logging from node

*  More autocompletions

*  Streamline completions

* ✏️ Update placeholders

*  Update linter to new methods

* 🔥 Remove `$nodeItem` completions

*  Re-update placeholders

* 🎨 Fix formatting

* 📦 Update `package-lock.json`

*  Refresh with multi-line empty string

*  Account for syntax errors

* 🔥 Remove unneeded variant

*  Minor improvements

*  Add more autocompletions

* 🚚 Rename extension

* 🔥 Remove outdated comments

* 🚚 Rename field

*  More autocompletions

*  Fix up error display when empty text

* 🔥 Remove logging

*  More error validation

* 🐛 Fix `pairedItem` to `pairedItem()`

*  Add item to validation info

* 📦 Update `package-lock.json`

*  Leftover fixes

*  Set `insertNewlineAndIndent`

* 📦 Update `package-lock.json`

* 📦 Re-update `package-lock.json`

* 👕 Add lint exception

* 📘 Add type to mixin type

* Clean up comment

*  Refactor completion per new requirements

*  Adjust placeholders

*  Add `json` autocompletions for `$input`

* 🎨 Set border

*  Restore local completion source

*  Implement autocompletion for imports

*  Add `.*` to follow user typing on autocompletion

* 📘 Fix typings in autocompletions

* 👕 Add linting for use of `item()`

* 📦 Update `package-lock.json`

* 🐛 Fix for `$items(nodeName)[0]`

*  Filter down built-in modules list

*  Refactor error handling

*  Linter and validation improvements

*  Apply review feedback

* ♻️ More general refactorings

*  Add dot notation utility

* Customize input handler

*  Support `.json.` completions

*  Adjust placeholder

*  Sort imports

* 🔥 Remove blank rows addition

*  Add more error validation

* 📦 Update `package-lock.json`

*  Make date logging consistent

* 🔧 Adjust linting highlight range

*  Add line numbers to each item mode errors

*  Allow for links in error descriptions

*  More input validation

*  Expand linting to loops

*  Deprecate Function and Function Item nodes

* 🐛 Fix placeholder syntax

* 📘 Narrow down type

* 🚚 Rename using kebab-case

* 🔥 Remove `mapGetters`

* ✏️ Fix casing

*  Adjust import for type

* ✏️ Fix quotes

* 🐛 Fix `activeNode` reference

*  Use constant

* 🔥 Remove logging

* ✏️ Fix typo

*  Add missing `notice`

* ✏️ Add tags

* ✏️ Fix alias

* ✏️ Update copy

* 🔥 Remove wrong linting

* ✏️ Update copy

*  Add validation for `null`

*  Add validation for non-object and non-array

*  Add validation for non-array with json

* ✏️ Intentionally use wrong spelling

*  More validation

* ✏️ More copy updates

* ✏️ Placeholder updates

*  Restore spelling

*  Fix var name

* ✏️ More copy updates

*  Add luxon autocompletions

*  Make scrollable

*  Fix comma from merge conflict resolution

* 📦 Update `package-lock.json`

* 👕 Fix lint detail

* 🎨 Set font family

*  Bring in expressions fix

* ♻️ Address feedback

*  Exclude codemirror packages from render chunks

* 🐛 Fix placeholder not showing on first load

* feat(editor-ui): Replace `lezer` with `esprima` in client linter (#4192)

* 🔥 Remove addition from misresolved conflict

*  Replace `lezer` with `esprima` in client linter

*  Add missing key

* 📦 Update `package-lock.json`

*  Match dependencies

* 📦 Update `package-lock.json`

* 📦 Re-update `package-lock.json`

*  Match whitespace

* 🐛 Fix selection

*  Expand validation

* 🔥 Remove validation

* ✏️ Update copy

* 🚚 Move to constants

*  More `null` validation

*  Support `all()` with index to access item

*  Gloss over n8n syntax error

* 🎨 Re-style diagnostic button

* 🔥 Remove `item` as `itemAlias`

*  Add linting for `item.json` in single item mode

*  Refactor to add label info descriptions

*  More autocompletions

* 👕 Fix lint

*  Simplify typings

* feat(nodes-base): Multiline autocompletion for `code-node-editor` (#4220)

*  Simplify typings

*  Consolidate helpers in utils

*  Multiline autocompletion for standalone vars

* 🔥 Remove unneeded mixins

* ✏️ Update copy

* ✏️ Prep TODOs

*  Multiline completion for `$input.method` + `$input.item`

* 🔥 Remove unused method

* 🔥 Remove another unused method

* 🚚 Move luxon strings to helpers

*  Multiline autocompletion for methods output

*  Refactor to use optional chaining

* 👕 Fix lint

* ✏️ Update TODOs

*  Multiline autocompletion for `json` fields

* 📘 Add typings

*  De-duplicate callback to forEach

* 🐛 Fix autocompletions not working with leading whitespace

* 🌐 Apply i18n

* 👕 Fix lint

* :constructor: Second-period var usage completions

* 👕 Fix lint

* 👕 Add exception

*  Add completion telemetry

* 📘 Add typing

*  Major refactoring to organize

* 🐛 Fix multiline `.all()[index]`

* 🐛 Do not autoclose square brackets prior to `.json`

* 🐛 Fix accessor for multiline `jsonField` completions

*  Add completions for half-assignments

* 🐛 Fix `jsonField` completions for `x.json`

* ✏️ Improve comments

* 🐛 Fix `.json[field]` for multiline matches

*  Cleanup

* 📦 Update `package-lock.json`

* 👕 Fix lint

* 🐛 Rely on original value for custom matcher

*  Create `customMatcherJsonFieldCompletions` to simplify setup

* 🐛 Include selector in `customMatcherJsonField` completions

* ✏️ Make naming consistent

* ✏️ Add docline

*  Finish self-review cleanup

* 🔥 Remove outdated comment

* 📌 Pin luxon to major-minor

* ✏️ Fix typo

* 📦 Update `package-lock.json`

* 📦 Update `package-lock.json`

* 📦 Re-update `package-lock.json`

*  Add `luxon` for Gmail node

* 📦 Update `package-lock.json`

*  Replace Function with Code in suggested nodes

* 🐛 Fix `$prevNode` completions

* ✏️ Update `$execution.mode` copy

*  Separate luxon getters from methods

*  Adjusting linter to tolerate `.binary`

*  Adjust top-level item keys check

*  Anticipate user expecting `item` to pre-exist

*  Add linting for legacy item access

*  Add hint for attempted `items` access

*  Add keybinding for toggling comments

* ✏️ Update copy of `all`, `first`, `last` and `itemMatching`

* 🐛 Make `input.all()` etc act on copies

* 📦 Update `package-lock.json`

* 🐛 Fix guard in `$input.last()`

* ♻️ Address Jan's feedback

* ⬆️ Upgrade `eslint-plugin-n8n-nodes-base`

* 📦 Update `package-lock.json`

* 🔥 Remove unneeded exceptions

*  Restore placeholder logic

*  Add placeholders to client

*  Account for shadow item

* ✏️ More completion info labels

* 👕 Fix lint

* ✏️ Update copy

* ✏️ Update copy

* ✏️ More copy updates

* 📦 Update `package-lock.json`

*  Add more validation

*  Add placheolder on first load

* Replace `Cmd` with `Mod`

* 📦 Update `package-lock.json`
This commit is contained in:
Iván Ovejero
2022-10-13 14:28:02 +02:00
committed by GitHub
parent 12e821528b
commit 1db4fa2bf8
54 changed files with 5127 additions and 1400 deletions

View File

@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.code",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"details": "The Code node allows you to execute JavaScript in your workflow.",
"categories": ["Development", "Core Nodes"],
"resources": {
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.code/"
}
]
},
"alias": ["cpde", "Javascript", "JS", "Script", "Custom Code", "Function"],
"subcategories": {
"Core Nodes": ["Data Transformation"]
}
}

View File

@@ -0,0 +1,138 @@
import {
IExecuteFunctions,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { getSandboxContext, Sandbox } from './Sandbox';
import { standardizeOutput } from './utils';
import type { CodeNodeMode } from './utils';
export class Code implements INodeType {
description: INodeTypeDescription = {
displayName: 'Code',
name: 'code',
icon: 'fa:code',
group: ['transform'],
version: 1,
description: 'Run custom JavaScript code',
defaults: {
name: 'Code',
color: '#FF9922',
},
inputs: ['main'],
outputs: ['main'],
parameterPane: 'wide',
properties: [
{
displayName: 'Mode',
name: 'mode',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Run Once for All Items',
value: 'runOnceForAllItems',
description: 'Run this code only once, no matter how many input items there are',
},
{
name: 'Run Once for Each Item',
value: 'runOnceForEachItem',
description: 'Run this code as many times as there are input items',
},
],
default: 'runOnceForAllItems',
},
{
displayName: 'JavaScript',
name: 'jsCode',
typeOptions: {
editor: 'codeNodeEditor',
},
type: 'string',
default: '', // set by component
description:
'JavaScript code to execute.<br><br>Tip: You can use luxon vars like <code>$today</code> for dates and <code>$jmespath</code> for querying JSON structures. <a href="https://docs.n8n.io/nodes/n8n-nodes-base.function">Learn more</a>.',
noDataExpression: true,
},
{
displayName:
'Type <code>$</code> for a list of <a target="_blank" href="https://docs.n8n.io/code-examples/methods-variables-reference/">special vars/methods</a>. Debug by using <code>console.log()</code> statements and viewing their output in the browser console.',
name: 'notice',
type: 'notice',
default: '',
},
],
};
async execute(this: IExecuteFunctions) {
let items = this.getInputData();
const nodeMode = this.getNodeParameter('mode', 0) as CodeNodeMode;
const workflowMode = this.getMode();
// ----------------------------------
// runOnceForAllItems
// ----------------------------------
if (nodeMode === 'runOnceForAllItems') {
const jsCodeAllItems = this.getNodeParameter('jsCode', 0) as string;
const context = getSandboxContext.call(this);
const sandbox = new Sandbox(context, workflowMode, nodeMode);
if (workflowMode === 'manual') {
sandbox.on('console.log', this.sendMessageToUI);
}
try {
items = await sandbox.runCode(jsCodeAllItems);
} catch (error) {
if (!this.continueOnFail()) return Promise.reject(error);
items = [{ json: { error: error.message } }];
}
for (const item of items) {
standardizeOutput(item.json);
}
return this.prepareOutputData(items);
}
// ----------------------------------
// runOnceForEachItem
// ----------------------------------
const returnData: INodeExecutionData[] = [];
for (let index = 0; index < items.length; index++) {
let item = items[index];
const jsCodeEachItem = this.getNodeParameter('jsCode', index) as string;
const context = getSandboxContext.call(this, index);
const sandbox = new Sandbox(context, workflowMode, nodeMode);
if (workflowMode === 'manual') {
sandbox.on('console.log', this.sendMessageToUI);
}
try {
item = await sandbox.runCode(jsCodeEachItem, index);
} catch (error) {
if (!this.continueOnFail()) return Promise.reject(error);
returnData.push({ json: { error: error.message } });
}
if (item) {
returnData.push({
json: standardizeOutput(item.json),
pairedItem: { item: index },
...(item.binary && { binary: item.binary }),
});
}
}
return this.prepareOutputData(returnData);
}
}

View File

@@ -0,0 +1,78 @@
export class ExecutionError extends Error {
description: string | null = null;
itemIndex: number | undefined = undefined;
context: { itemIndex: number } | undefined = undefined;
stack = '';
lineNumber: number | undefined = undefined;
constructor(error: Error & { stack: string }, itemIndex?: number) {
super();
this.itemIndex = itemIndex;
if (this.itemIndex !== undefined) {
this.context = { itemIndex: this.itemIndex };
}
this.stack = error.stack;
this.populateFromStack();
}
/**
* Populate error `message` and `description` from error `stack`.
*/
private populateFromStack() {
const stackRows = this.stack.split('\n');
if (stackRows.length === 0) {
this.message = 'Unknown error';
}
const messageRow = stackRows.find((line) => line.includes('Error:'));
const lineNumberRow = stackRows.find((line) => line.includes('Code:'));
const lineNumberDisplay = this.toLineNumberDisplay(lineNumberRow);
if (!messageRow) {
this.message = `Unknown error ${lineNumberDisplay}`;
return;
}
const [errorDetails, errorType] = this.toErrorDetailsAndType(messageRow);
if (errorType) this.description = errorType;
if (!errorDetails) {
this.message = `Unknown error ${lineNumberDisplay}`;
return;
}
this.message = `${errorDetails} ${lineNumberDisplay}`;
}
private toLineNumberDisplay(lineNumberRow?: string) {
const errorLineNumberMatch = lineNumberRow?.match(/Code:(?<lineNumber>\d+)/);
if (!errorLineNumberMatch?.groups?.lineNumber) return null;
const lineNumber = errorLineNumberMatch.groups.lineNumber;
this.lineNumber = Number(lineNumber);
if (!lineNumber) return '';
return this.itemIndex === undefined
? `[line ${lineNumber}]`
: `[line ${lineNumber}, for item ${this.itemIndex}]`;
}
private toErrorDetailsAndType(messageRow?: string) {
if (!messageRow) return [null, null];
const [errorDetails, errorType] = messageRow
.split(':')
.reverse()
.map((i) => i.trim());
return [errorDetails, errorType === 'Error' ? null : errorType];
}
}

View File

@@ -0,0 +1,268 @@
import { normalizeItems } from 'n8n-core';
import { NodeVM, NodeVMOptions } from 'vm2';
import { ValidationError } from './ValidationError';
import { ExecutionError } from './ExecutionError';
import { CodeNodeMode, isObject, SUPPORTED_ITEM_KEYS } from './utils';
import type { IExecuteFunctions, IWorkflowDataProxyData, WorkflowExecuteMode } from 'n8n-workflow';
export class Sandbox extends NodeVM {
private jsCode = '';
private itemIndex: number | undefined = undefined;
constructor(
context: ReturnType<typeof getSandboxContext>,
workflowMode: WorkflowExecuteMode,
private nodeMode: CodeNodeMode,
) {
super(Sandbox.getSandboxOptions(context, workflowMode));
}
static getSandboxOptions(
context: ReturnType<typeof getSandboxContext>,
workflowMode: WorkflowExecuteMode,
): NodeVMOptions {
const { NODE_FUNCTION_ALLOW_BUILTIN: builtIn, NODE_FUNCTION_ALLOW_EXTERNAL: external } =
process.env;
return {
console: workflowMode === 'manual' ? 'redirect' : 'inherit',
sandbox: context,
require: {
builtin: builtIn ? builtIn.split(',') : [],
external: external ? { modules: external.split(','), transitive: false } : false,
},
};
}
async runCode(jsCode: string, itemIndex?: number) {
this.jsCode = jsCode;
this.itemIndex = itemIndex;
return this.nodeMode === 'runOnceForAllItems' ? this.runCodeAllItems() : this.runCodeEachItem();
}
private async runCodeAllItems() {
const script = `module.exports = async function() {${this.jsCode}\n}()`;
const match = script.match(
/(?<disallowedSyntax>\)\.item(?!Matching)|\$input\.item(?!Matching)|\$json|\$binary|\$itemIndex)/,
); // disallow .item but tolerate .itemMatching
if (match?.groups?.disallowedSyntax) {
const { disallowedSyntax } = match.groups;
const lineNumber =
this.jsCode.split('\n').findIndex((line) => {
return line.includes(disallowedSyntax) && !line.startsWith('//') && !line.startsWith('*');
}) + 1;
const disallowedSyntaxFound = lineNumber !== 0;
if (disallowedSyntaxFound) {
throw new ValidationError({
message: `Can't use ${disallowedSyntax} here`,
description: "This is only available in 'Run Once for Each Item' mode",
itemIndex: this.itemIndex,
lineNumber,
});
}
}
let executionResult;
try {
executionResult = await this.run(script, __dirname);
} catch (error) {
// anticipate user expecting `items` to pre-exist as in Function Item node
if (error.message === 'items is not defined' && !/(let|const|var) items =/.test(script)) {
const quoted = error.message.replace('items', '`items`');
error.message = quoted + '. Did you mean `$input.all()`?';
}
throw new ExecutionError(error);
}
if (executionResult === null) return [];
if (executionResult === undefined || typeof executionResult !== 'object') {
throw new ValidationError({
message: "Code doesn't return items properly",
description:
'Please return an array of objects, one for each item you would like to output',
itemIndex: this.itemIndex,
});
}
if (Array.isArray(executionResult)) {
for (const item of executionResult) {
if (item.json !== undefined && !isObject(item.json)) {
throw new ValidationError({
message: "A 'json' property isn't an object",
description: "In the returned data, every key named 'json' must point to an object",
itemIndex: this.itemIndex,
});
}
// If at least one top-level key is a supported item key (`json`, `binary`, etc.),
// then validate all keys to be a supported item key, else allow user keys
// to be wrapped in `json` when normalizing items below.
if (
executionResult.some((item) =>
Object.keys(item).find((key) => SUPPORTED_ITEM_KEYS.has(key)),
)
) {
Object.keys(item).forEach((key) => {
if (SUPPORTED_ITEM_KEYS.has(key)) return;
throw new ValidationError({
message: `Unknown top-level item key: ${key}`,
description: 'Access the properties of an item under `.json`, e.g. `item.json`',
itemIndex: this.itemIndex,
});
});
}
if (item.binary !== undefined && !isObject(item.binary)) {
throw new ValidationError({
message: "A 'binary' property isn't an object",
description: "In the returned data, every key named 'binary must point to an object.",
itemIndex: this.itemIndex,
});
}
}
} else {
if (executionResult.json !== undefined && !isObject(executionResult.json)) {
throw new ValidationError({
message: "A 'json' property isn't an object",
description: "In the returned data, every key named 'json' must point to an object",
itemIndex: this.itemIndex,
});
}
if (executionResult.binary !== undefined && !isObject(executionResult.binary)) {
throw new ValidationError({
message: "A 'binary' property isn't an object",
description: "In the returned data, every key named 'binary must point to an object.",
itemIndex: this.itemIndex,
});
}
}
return normalizeItems(executionResult);
}
private async runCodeEachItem() {
const script = `module.exports = async function() {${this.jsCode}\n}()`;
const match = this.jsCode.match(/\$input\.(?<disallowedMethod>first|last|all|itemMatching)/);
if (match?.groups?.disallowedMethod) {
const { disallowedMethod } = match.groups;
const lineNumber =
this.jsCode.split('\n').findIndex((line) => {
return line.includes(disallowedMethod) && !line.startsWith('//') && !line.startsWith('*');
}) + 1;
const disallowedMethodFound = lineNumber !== 0;
if (disallowedMethodFound) {
throw new ValidationError({
message: `Can't use .${disallowedMethod}() here`,
description: "This is only available in 'Run Once for All Items' mode",
itemIndex: this.itemIndex,
lineNumber,
});
}
}
let executionResult;
try {
executionResult = await this.run(script, __dirname);
} catch (error) {
// anticipate user expecting `item` to pre-exist as in Function Item node
if (error.message === 'item is not defined' && !/(let|const|var) item =/.test(script)) {
const quoted = error.message.replace('item', '`item`');
error.message = quoted + '. Did you mean `$input.item.json`?';
}
throw new ExecutionError(error, this.itemIndex);
}
if (executionResult === null) return;
if (executionResult === undefined || typeof executionResult !== 'object') {
throw new ValidationError({
message: "Code doesn't return an object",
description: `Please return an object representing the output item. ('${executionResult}' was returned instead.)`,
itemIndex: this.itemIndex,
});
}
if (executionResult.json !== undefined && !isObject(executionResult.json)) {
throw new ValidationError({
message: "A 'json' property isn't an object",
description: "In the returned data, every key named 'json' must point to an object",
itemIndex: this.itemIndex,
});
}
if (executionResult.binary !== undefined && !isObject(executionResult.binary)) {
throw new ValidationError({
message: "A 'binary' property isn't an object",
description: "In the returned data, every key named 'binary must point to an object.",
itemIndex: this.itemIndex,
});
}
// If at least one top-level key is a supported item key (`json`, `binary`, etc.),
// and another top-level key is unrecognized, then the user mis-added a property
// directly on the item, when they intended to add it on the `json` property
Object.keys(executionResult).forEach((key) => {
if (SUPPORTED_ITEM_KEYS.has(key)) return;
throw new ValidationError({
message: `Unknown top-level item key: ${key}`,
description: 'Access the properties of an item under `.json`, e.g. `item.json`',
itemIndex: this.itemIndex,
});
});
if (Array.isArray(executionResult)) {
const firstSentence =
executionResult.length > 0
? `An array of ${typeof executionResult[0]}s was returned.`
: 'An empty array was returned.';
throw new ValidationError({
message: "Code doesn't return a single object",
description: `${firstSentence} If you need to output multiple items, please use the 'Run Once for All Items' mode instead`,
itemIndex: this.itemIndex,
});
}
return executionResult.json ? executionResult : { json: executionResult };
}
}
export function getSandboxContext(this: IExecuteFunctions, index?: number) {
const sandboxContext: Record<string, unknown> & { $item: (i: number) => IWorkflowDataProxyData } =
{
// from NodeExecuteFunctions
$getNodeParameter: this.getNodeParameter,
$getWorkflowStaticData: this.getWorkflowStaticData,
helpers: this.helpers,
// to bring in all $-prefixed vars and methods from WorkflowDataProxy
$item: this.getWorkflowDataProxy,
};
// $node, $items(), $parameter, $json, $env, etc.
Object.assign(sandboxContext, sandboxContext.$item(index ?? 0));
return sandboxContext;
}

View File

@@ -0,0 +1,39 @@
export class ValidationError extends Error {
description = '';
itemIndex: number | undefined = undefined;
context: { itemIndex: number } | undefined = undefined;
lineNumber: number | undefined = undefined;
constructor({
message,
description,
itemIndex,
lineNumber,
}: {
message: string;
description: string;
itemIndex?: number;
lineNumber?: number;
}) {
super();
this.lineNumber = lineNumber;
this.itemIndex = itemIndex;
if (this.lineNumber !== undefined && this.itemIndex !== undefined) {
this.message = `${message} [line ${lineNumber}, for item ${itemIndex}]`;
} else if (this.lineNumber !== undefined) {
this.message = `${message} [line ${lineNumber}]`;
} else if (this.itemIndex !== undefined) {
this.message = `${message} [item ${itemIndex}]`;
} else {
this.message = message;
}
this.description = description;
if (this.itemIndex !== undefined) {
this.context = { itemIndex: this.itemIndex };
}
}
}

View File

@@ -0,0 +1,29 @@
import type { IDataObject } from 'n8n-workflow';
/**
* Stringify any non-standard JS objects (e.g. `Date`, `RegExp`) inside output items at any depth.
*/
export function standardizeOutput(output: IDataObject) {
for (const [key, value] of Object.entries(output)) {
if (!isTraversable(value)) continue;
output[key] =
value.constructor.name !== 'Object'
? JSON.stringify(value) // Date, RegExp, etc.
: standardizeOutput(value);
}
return output;
}
export function isObject(maybe: unknown): maybe is { [key: string]: unknown } {
return typeof maybe === 'object' && maybe !== null && !Array.isArray(maybe);
}
function isTraversable(maybe: unknown): maybe is IDataObject {
return isObject(maybe) && Object.keys(maybe).length > 0;
}
export type CodeNodeMode = 'runOnceForAllItems' | 'runOnceForEachItem';
export const SUPPORTED_ITEM_KEYS = new Set(['json', 'binary', 'error', 'pairedItem', 'index']);

View File

@@ -14,6 +14,7 @@ export class Function implements INodeType {
description: INodeTypeDescription = {
displayName: 'Function',
name: 'function',
hidden: true,
icon: 'fa:code',
group: ['transform'],
version: 1,
@@ -26,6 +27,12 @@ export class Function implements INodeType {
inputs: ['main'],
outputs: ['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',

View File

@@ -14,6 +14,7 @@ export class FunctionItem implements INodeType {
description: INodeTypeDescription = {
displayName: 'Function Item',
name: 'functionItem',
hidden: true,
icon: 'fa:code',
group: ['transform'],
version: 1,
@@ -25,6 +26,12 @@ export class FunctionItem implements INodeType {
inputs: ['main'],
outputs: ['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',