diff --git a/packages/nodes-base/nodes/Switch.node.ts b/packages/nodes-base/nodes/Switch.node.ts new file mode 100644 index 0000000000..1c7acc5c7a --- /dev/null +++ b/packages/nodes-base/nodes/Switch.node.ts @@ -0,0 +1,532 @@ +import { IExecuteFunctions } from 'n8n-core'; +import { + INodeExecutionData, + INodeParameters, + INodeType, + INodeTypeDescription, + NodeParameterValue, +} from 'n8n-workflow'; + + +export class Switch implements INodeType { + description: INodeTypeDescription = { + displayName: 'Switch', + name: 'switch', + icon: 'fa:map-signs', + group: ['transform'], + version: 1, + description: 'Splits a stream depending on defined compare operations.', + defaults: { + name: 'Switch', + color: '#408000', + }, + inputs: ['main'], + outputs: ['main', 'main', 'main', 'main'], + outputNames: ['0', '1', '2', '3'], + properties: [ + { + displayName: 'Mode', + name: 'mode', + type: 'options', + options: [ + { + name: 'Expression', + value: 'expression', + description: 'Expression decides how to route date.', + }, + { + name: 'Rules', + value: 'rules', + description: 'Rules decide how to route data.', + }, + ], + default: 'rules', + description: 'How data should be routed.', + }, + + + // ---------------------------------- + // mode:expression + // ---------------------------------- + { + displayName: 'Output', + name: 'output', + type: 'number', + typeOptions: { + minValue: 0, + maxValue: 3, + }, + displayOptions: { + show: { + mode: [ + 'expression' + ], + }, + }, + default: 0, + description: 'The index of output to which to send data to.', + }, + + + // ---------------------------------- + // mode:rules + // ---------------------------------- + { + displayName: 'Data Type', + name: 'dataType', + type: 'options', + displayOptions: { + show: { + mode: [ + 'rules', + ], + }, + }, + options: [ + { + name: 'Boolean', + value: 'boolean', + }, + { + name: 'Number', + value: 'number', + }, + { + name: 'String', + value: 'string', + }, + ], + default: 'number', + description: 'The type of data to route on.', + }, + + // ---------------------------------- + // dataType:boolean + // ---------------------------------- + { + displayName: 'Value 1', + name: 'value1', + type: 'boolean', + displayOptions: { + show: { + dataType: [ + 'boolean' + ], + mode: [ + 'rules', + ], + }, + }, + default: false, + description: 'The value to compare with the second one.', + }, + { + displayName: 'Routing Rules', + name: 'rules', + placeholder: 'Add Routing Rule', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + dataType: [ + 'boolean' + ], + mode: [ + 'rules', + ], + }, + }, + description: 'The routing rules.', + default: {}, + options: [ + { + name: 'rules', + displayName: 'Boolean', + values: [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Equal', + value: 'equal' + }, + { + name: 'Not Equal', + value: 'notEqual' + }, + ], + default: 'equal', + description: 'Operation to decide where the the data should be mapped to.', + }, + { + displayName: 'Value 2', + name: 'value2', + type: 'boolean', + default: false, + description: 'The value to compare with the first one.', + }, + { + displayName: 'Output', + name: 'output', + type: 'number', + typeOptions: { + minValue: 0, + maxValue: 3, + }, + default: 0, + description: 'The index of output to which to send data to if rule matches.', + }, + ], + }, + ], + }, + + // ---------------------------------- + // dataType:number + // ---------------------------------- + { + displayName: 'Value 1', + name: 'value1', + type: 'number', + displayOptions: { + show: { + dataType: [ + 'number' + ], + mode: [ + 'rules', + ], + }, + }, + default: 0, + description: 'The value to compare with the second one.', + }, + { + displayName: 'Routing Rules', + name: 'rules', + placeholder: 'Add Routing Rule', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + dataType: [ + 'number' + ], + mode: [ + 'rules', + ], + }, + }, + description: 'The routing rules.', + default: {}, + options: [ + { + name: 'rules', + displayName: 'Numbers', + values: [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Smaller', + value: 'smaller' + }, + { + name: 'Equal', + value: 'equal' + }, + { + name: 'Not Equal', + value: 'notEqual' + }, + { + name: 'Larger', + value: 'larger' + }, + ], + default: 'smaller', + description: 'Operation to decide where the the data should be mapped to.', + }, + { + displayName: 'Value 2', + name: 'value2', + type: 'number', + default: 0, + description: 'The value to compare with the first one.', + }, + { + displayName: 'Output', + name: 'output', + type: 'number', + typeOptions: { + minValue: 0, + maxValue: 3, + }, + default: 0, + description: 'The index of output to which to send data to if rule matches.', + }, + ], + }, + ], + }, + + // ---------------------------------- + // dataType:string + // ---------------------------------- + { + displayName: 'Value 1', + name: 'value1', + type: 'string', + displayOptions: { + show: { + dataType: [ + 'string' + ], + mode: [ + 'rules', + ], + }, + }, + default: '', + description: 'The value to compare with the second one.', + }, + { + displayName: 'Routing Rules', + name: 'rules', + placeholder: 'Add Routing Rule', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + dataType: [ + 'string' + ], + mode: [ + 'rules', + ], + }, + }, + description: 'The routing rules.', + default: {}, + options: [ + { + name: 'rules', + displayName: 'Strings', + values: [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Contains', + value: 'contains' + }, + { + name: 'Equal', + value: 'equal' + }, + { + name: 'Not Contains', + value: 'notContains' + }, + { + name: 'Not Equal', + value: 'notEqual' + }, + { + name: 'Regex', + value: 'regex' + }, + ], + default: 'equal', + description: 'Operation to decide where the the data should be mapped to.', + }, + { + displayName: 'Value 2', + name: 'value2', + type: 'string', + displayOptions: { + hide: { + operation: [ + 'regex', + ], + }, + }, + default: '', + description: 'The value to compare with the first one.', + }, + { + displayName: 'Regex', + name: 'value2', + type: 'string', + displayOptions: { + show: { + operation: [ + 'regex', + ], + }, + }, + default: '', + placeholder: '/text/i', + description: 'The regex which has to match.', + }, + { + displayName: 'Output', + name: 'output', + type: 'number', + typeOptions: { + minValue: 0, + maxValue: 3, + }, + default: 0, + description: 'The index of output to which to send data to if rule matches.', + }, + ], + }, + ], + }, + + + { + displayName: 'Fallback Output', + name: 'fallbackOutput', + type: 'options', + displayOptions: { + show: { + mode: [ + 'rules', + ], + }, + }, + options: [ + { + name: 'None', + value: -1, + }, + { + name: '0', + value: 0, + }, + { + name: '1', + value: 1, + }, + { + name: '2', + value: 2, + }, + { + name: '3', + value: 3, + }, + ], + default: -1, + description: 'The output to which to route all items which do not match any of the rules.', + }, + + ], + }; + + + async execute(this: IExecuteFunctions): Promise { + const returnData: INodeExecutionData[][] = [ + [], + [], + [], + [], + ]; + + const items = this.getInputData(); + + let compareOperationResult: boolean; + let item: INodeExecutionData; + let mode: string; + let outputIndex: number; + let ruleData: INodeParameters; + let value1: NodeParameterValue; + + // The compare operations + const compareOperationFunctions: { + [key: string]: (value1: NodeParameterValue, value2: NodeParameterValue) => boolean; + } = { + contains: (value1: NodeParameterValue, value2: NodeParameterValue) => value1.toString().includes(value2.toString()), + notContains: (value1: NodeParameterValue, value2: NodeParameterValue) => !value1.toString().includes(value2.toString()), + equal: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 === value2, + notEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 !== value2, + larger: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 > value2, + smaller: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 < value2, + regex: (value1: NodeParameterValue, value2: NodeParameterValue) => { + const regexMatch = value2.toString().match(new RegExp('^/(.*?)/([gimy]*)$')); + + let regex: RegExp; + if (!regexMatch) { + regex = new RegExp(value2.toString()); + } else if (regexMatch.length === 1) { + regex = new RegExp(regexMatch[1]); + } else { + regex = new RegExp(regexMatch[1], regexMatch[2]); + } + + return !!value1.toString().match(regex); + }, + }; + + function checkIndexRange(index: number) { + if (index < 0 || index >= returnData.length) { + throw new Error(`The ouput ${index} is not allowed. It has to be between 0 and ${returnData.length - 1}!`); + } + } + + // Itterate over all items to check to which output they should be routed to + itemLoop: + for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { + item = items[itemIndex]; + mode = this.getNodeParameter('mode', itemIndex) as string; + + if (mode === 'expression') { + // One expression decides how to route item + + outputIndex = this.getNodeParameter('output', itemIndex) as number; + checkIndexRange(outputIndex); + + returnData[outputIndex].push(item); + } else if (mode === 'rules') { + // Rules decide how to route item + + value1 = this.getNodeParameter('value1', itemIndex) as NodeParameterValue; + + for (ruleData of this.getNodeParameter('rules.rules', itemIndex, []) as INodeParameters[]) { + // Check if the values passes + compareOperationResult = compareOperationFunctions[ruleData.operation as string](value1, ruleData.value2 as NodeParameterValue); + + if (compareOperationResult === true) { + // If rule matches add it to the correct output and continue with next item + checkIndexRange(ruleData.output as number); + returnData[ruleData.output as number].push(item); + continue itemLoop; + } + } + + // Check if a fallback output got defined and route accordingly + outputIndex = this.getNodeParameter('fallbackOutput', itemIndex) as number; + if (outputIndex !== -1) { + checkIndexRange(outputIndex); + returnData[outputIndex].push(item); + } + } + } + + return returnData; + } +} diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 56c313408f..9790b8279b 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -143,6 +143,7 @@ "dist/nodes/Start.node.js", "dist/nodes/Stripe/StripeTrigger.node.js", "dist/nodes/Shopify/ShopifyTrigger.node.js", + "dist/nodes/Switch.node.js", "dist/nodes/Telegram/Telegram.node.js", "dist/nodes/Telegram/TelegramTrigger.node.js", "dist/nodes/Todoist/Todoist.node.js",