From 6b6e8dfc33c884a21f7615f6ba8667cd5995d4ad Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Wed, 8 May 2024 14:02:36 +0300 Subject: [PATCH] feat(Simulate Node): New node (no-changelog) (#9109) --- packages/cli/src/CredentialsHelper.ts | 4 + packages/cli/src/NodeTypes.ts | 4 + .../services/credentials-tester.service.ts | 4 + packages/core/src/NodeExecuteFunctions.ts | 1 + packages/editor-ui/src/components/Node.vue | 23 ++- packages/editor-ui/src/constants.ts | 2 + .../nodes/Simulate/Simulate.node.json | 11 ++ .../nodes/Simulate/Simulate.node.ts | 131 ++++++++++++++++++ .../nodes/Simulate/SimulateTrigger.node.json | 11 ++ .../nodes/Simulate/SimulateTrigger.node.ts | 79 +++++++++++ .../nodes-base/nodes/Simulate/descriptions.ts | 44 ++++++ .../nodes/Simulate/methods/index.ts | 1 + .../nodes/Simulate/methods/loadOptions.ts | 26 ++++ packages/nodes-base/package.json | 2 + packages/workflow/src/Interfaces.ts | 2 + 15 files changed, 344 insertions(+), 1 deletion(-) create mode 100644 packages/nodes-base/nodes/Simulate/Simulate.node.json create mode 100644 packages/nodes-base/nodes/Simulate/Simulate.node.ts create mode 100644 packages/nodes-base/nodes/Simulate/SimulateTrigger.node.json create mode 100644 packages/nodes-base/nodes/Simulate/SimulateTrigger.node.ts create mode 100644 packages/nodes-base/nodes/Simulate/descriptions.ts create mode 100644 packages/nodes-base/nodes/Simulate/methods/index.ts create mode 100644 packages/nodes-base/nodes/Simulate/methods/loadOptions.ts diff --git a/packages/cli/src/CredentialsHelper.ts b/packages/cli/src/CredentialsHelper.ts index 323fdc760f..c20ddd4798 100644 --- a/packages/cli/src/CredentialsHelper.ts +++ b/packages/cli/src/CredentialsHelper.ts @@ -23,6 +23,7 @@ import type { INodeTypes, IWorkflowExecuteAdditionalData, IExecuteData, + IDataObject, } from 'n8n-workflow'; import { ICredentialsHelper, NodeHelpers, Workflow, ApplicationError } from 'n8n-workflow'; @@ -57,6 +58,9 @@ const mockNodesData: INodeTypeData = { }; const mockNodeTypes: INodeTypes = { + getKnownTypes(): IDataObject { + return {}; + }, getByName(nodeType: string): INodeType | IVersionedNodeType { return mockNodesData[nodeType]?.type; }, diff --git a/packages/cli/src/NodeTypes.ts b/packages/cli/src/NodeTypes.ts index 6ab17f0d97..4d9e773167 100644 --- a/packages/cli/src/NodeTypes.ts +++ b/packages/cli/src/NodeTypes.ts @@ -54,6 +54,10 @@ export class NodeTypes implements INodeTypes { } } + getKnownTypes() { + return this.loadNodesAndCredentials.knownNodes; + } + private getNode(type: string): LoadedClass { const { loadedNodes, knownNodes } = this.loadNodesAndCredentials; if (type in loadedNodes) { diff --git a/packages/cli/src/services/credentials-tester.service.ts b/packages/cli/src/services/credentials-tester.service.ts index a144c607e5..bcb48e495c 100644 --- a/packages/cli/src/services/credentials-tester.service.ts +++ b/packages/cli/src/services/credentials-tester.service.ts @@ -23,6 +23,7 @@ import type { INodeTypeData, INodeTypes, ICredentialTestFunctions, + IDataObject, } from 'n8n-workflow'; import { VersionedNodeType, @@ -54,6 +55,9 @@ const mockNodesData: INodeTypeData = { }; const mockNodeTypes: INodeTypes = { + getKnownTypes(): IDataObject { + return {}; + }, getByName(nodeType: string): INodeType | IVersionedNodeType { return mockNodesData[nodeType]?.type; }, diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 5a0811884b..5b374460fd 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -2839,6 +2839,7 @@ const getCommonWorkflowFunctions = ( } return output; }, + getKnownNodeTypes: () => workflow.nodeTypes.getKnownTypes(), getRestApiUrl: () => additionalData.restApiUrl, getInstanceBaseUrl: () => additionalData.instanceBaseUrl, getInstanceId: () => Container.get(InstanceSettings).instanceId, diff --git a/packages/editor-ui/src/components/Node.vue b/packages/editor-ui/src/components/Node.vue index 2463736000..99b4d00f1f 100644 --- a/packages/editor-ui/src/components/Node.vue +++ b/packages/editor-ui/src/components/Node.vue @@ -98,7 +98,7 @@ { + const items = this.getInputData(); + let returnItems: INodeExecutionData[] = []; + + const output = this.getNodeParameter('output', 0) as string; + + if (output === 'all') { + returnItems = items; + } else if (output === 'specify') { + const numberOfItems = this.getNodeParameter('numberOfItems', 0) as number; + + returnItems = items.slice(0, numberOfItems); + } else if (output === 'custom') { + let jsonOutput = this.getNodeParameter('jsonOutput', 0); + + if (typeof jsonOutput === 'string') { + try { + jsonOutput = jsonParse(jsonOutput); + } catch (error) { + throw new NodeOperationError(this.getNode(), 'Invalid JSON'); + } + } + + if (!Array.isArray(jsonOutput)) { + jsonOutput = [jsonOutput]; + } + + for (const item of jsonOutput as IDataObject[]) { + returnItems.push({ json: item }); + } + } + + const executionDuration = this.getNodeParameter('executionDuration', 0) as number; + + if (executionDuration > 0) { + await sleep(executionDuration); + } + + return [returnItems]; + } +} diff --git a/packages/nodes-base/nodes/Simulate/SimulateTrigger.node.json b/packages/nodes-base/nodes/Simulate/SimulateTrigger.node.json new file mode 100644 index 0000000000..95c288764f --- /dev/null +++ b/packages/nodes-base/nodes/Simulate/SimulateTrigger.node.json @@ -0,0 +1,11 @@ +{ + "node": "n8n-nodes-base.simulateTrigger", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": ["Core Nodes"], + "resources": {}, + "alias": ["placeholder", "stub", "dummy"], + "subcategories": { + "Core Nodes": ["Helpers"] + } +} diff --git a/packages/nodes-base/nodes/Simulate/SimulateTrigger.node.ts b/packages/nodes-base/nodes/Simulate/SimulateTrigger.node.ts new file mode 100644 index 0000000000..4532908936 --- /dev/null +++ b/packages/nodes-base/nodes/Simulate/SimulateTrigger.node.ts @@ -0,0 +1,79 @@ +import { sleep, NodeOperationError, jsonParse } from 'n8n-workflow'; +import type { + IDataObject, + ITriggerFunctions, + INodeExecutionData, + INodeType, + INodeTypeDescription, + ITriggerResponse, +} from 'n8n-workflow'; +import { + executionDurationProperty, + iconSelector, + jsonOutputProperty, + subtitleProperty, +} from './descriptions'; +import { loadOptions } from './methods'; + +export class SimulateTrigger implements INodeType { + description: INodeTypeDescription = { + hidden: true, + displayName: 'Simulate Trigger', + name: 'simulateTrigger', + subtitle: '={{$parameter.subtitle || undefined}}', + icon: 'fa:arrow-right', + group: ['trigger'], + version: 1, + description: 'Simulate a trigger node', + defaults: { + name: 'Simulate Trigger', + color: '#b0b0b0', + }, + inputs: [], + outputs: ['main'], + properties: [ + { ...iconSelector, default: 'n8n-nodes-base.manualTrigger' }, + subtitleProperty, + { ...jsonOutputProperty, displayName: 'Output (JSON)' }, + executionDurationProperty, + ], + }; + + methods = { loadOptions }; + + async trigger(this: ITriggerFunctions): Promise { + const returnItems: INodeExecutionData[] = []; + + let jsonOutput = this.getNodeParameter('jsonOutput', 0); + + if (typeof jsonOutput === 'string') { + try { + jsonOutput = jsonParse(jsonOutput); + } catch (error) { + throw new NodeOperationError(this.getNode(), 'Invalid JSON'); + } + } + + if (!Array.isArray(jsonOutput)) { + jsonOutput = [jsonOutput]; + } + + for (const item of jsonOutput as IDataObject[]) { + returnItems.push({ json: item }); + } + + const executionDuration = this.getNodeParameter('executionDuration', 0) as number; + + if (executionDuration > 0) { + await sleep(executionDuration); + } + + const manualTriggerFunction = async () => { + this.emit([returnItems]); + }; + + return { + manualTriggerFunction, + }; + } +} diff --git a/packages/nodes-base/nodes/Simulate/descriptions.ts b/packages/nodes-base/nodes/Simulate/descriptions.ts new file mode 100644 index 0000000000..d5a4597774 --- /dev/null +++ b/packages/nodes-base/nodes/Simulate/descriptions.ts @@ -0,0 +1,44 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const iconSelector: INodeProperties = { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Icon to Display on Canvas', + name: 'icon', + type: 'options', + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options + description: 'Select a type of node to show corresponding icon', + default: 'n8n-nodes-base.noOp', + typeOptions: { + loadOptionsMethod: 'getNodeTypes', + }, +}; + +export const subtitleProperty: INodeProperties = { + displayName: 'Subtitle', + name: 'subtitle', + type: 'string', + default: '', + placeholder: "e.g. 'record: read'", +}; + +export const jsonOutputProperty: INodeProperties = { + displayName: 'JSON', + name: 'jsonOutput', + type: 'json', + typeOptions: { + rows: 5, + }, + default: '[\n {\n "my_field_1": "value",\n "my_field_2": 1\n }\n]', + validateType: 'array', +}; + +export const executionDurationProperty: INodeProperties = { + displayName: 'Execution Duration (MS)', + name: 'executionDuration', + type: 'number', + default: 150, + description: 'Execution duration in milliseconds', + typeOptions: { + minValue: 0, + }, +}; diff --git a/packages/nodes-base/nodes/Simulate/methods/index.ts b/packages/nodes-base/nodes/Simulate/methods/index.ts new file mode 100644 index 0000000000..65ff6192a3 --- /dev/null +++ b/packages/nodes-base/nodes/Simulate/methods/index.ts @@ -0,0 +1 @@ +export * as loadOptions from './loadOptions'; diff --git a/packages/nodes-base/nodes/Simulate/methods/loadOptions.ts b/packages/nodes-base/nodes/Simulate/methods/loadOptions.ts new file mode 100644 index 0000000000..205d9555d9 --- /dev/null +++ b/packages/nodes-base/nodes/Simulate/methods/loadOptions.ts @@ -0,0 +1,26 @@ +import type { ILoadOptionsFunctions, INodePropertyOptions } from 'n8n-workflow'; + +export async function getNodeTypes(this: ILoadOptionsFunctions): Promise { + const types = this.getKnownNodeTypes() as { + [key: string]: { + className: string; + }; + }; + + const returnData: INodePropertyOptions[] = []; + + let typeNames = Object.keys(types); + + if (this.getNode().type === 'n8n-nodes-base.simulateTrigger') { + typeNames = typeNames.filter((type) => type.toLowerCase().includes('trigger')); + } + + for (const type of typeNames) { + returnData.push({ + name: types[type].className, + value: type, + }); + } + + return returnData; +} diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 74100843d7..02ded5c2d9 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -717,6 +717,8 @@ "dist/nodes/Shopify/Shopify.node.js", "dist/nodes/Shopify/ShopifyTrigger.node.js", "dist/nodes/Signl4/Signl4.node.js", + "dist/nodes/Simulate/Simulate.node.js", + "dist/nodes/Simulate/SimulateTrigger.node.js", "dist/nodes/Slack/Slack.node.js", "dist/nodes/Sms77/Sms77.node.js", "dist/nodes/Snowflake/Snowflake.node.js", diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 4becafcb07..55df8fb953 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -815,6 +815,7 @@ export interface FunctionsBase { getInstanceId(): string; getChildNodes(nodeName: string): NodeTypeAndVersion[]; getParentNodes(nodeName: string): NodeTypeAndVersion[]; + getKnownNodeTypes(): IDataObject; getMode?: () => WorkflowExecuteMode; getActivationMode?: () => WorkflowActivateMode; @@ -1839,6 +1840,7 @@ export type WebhookResponseMode = 'onReceived' | 'lastNode' | 'responseNode'; export interface INodeTypes { getByName(nodeType: string): INodeType | IVersionedNodeType; getByNameAndVersion(nodeType: string, version?: number): INodeType; + getKnownTypes(): IDataObject; } export type LoadingDetails = {