2024-01-04 01:03:03 -08:00
import type {
IDataObject ,
IExecuteFunctions ,
ILoadOptionsFunctions ,
INodeExecutionData ,
INodeParameters ,
INodePropertyOptions ,
INodeType ,
INodeTypeBaseDescription ,
INodeTypeDescription ,
} from 'n8n-workflow' ;
import { NodeConnectionType , NodeOperationError } from 'n8n-workflow' ;
import { capitalize } from '@utils/utilities' ;
const configuredOutputs = ( parameters : INodeParameters ) = > {
const mode = parameters . mode as string ;
if ( mode === 'expression' ) {
return Array . from ( { length : parameters.numberOutputs as number } , ( _ , i ) = > ( {
type : ` ${ NodeConnectionType . Main } ` ,
displayName : i.toString ( ) ,
} ) ) ;
} else {
const rules = ( ( parameters . rules as IDataObject ) ? . values as IDataObject [ ] ) ? ? [ ] ;
const ruleOutputs = rules . map ( ( rule , index ) = > {
return {
type : ` ${ NodeConnectionType . Main } ` ,
displayName : rule.outputKey || index . toString ( ) ,
} ;
} ) ;
if ( ( parameters . options as IDataObject ) ? . fallbackOutput === 'extra' ) {
const renameFallbackOutput = ( parameters . options as IDataObject ) ? . renameFallbackOutput ;
ruleOutputs . push ( {
type : ` ${ NodeConnectionType . Main } ` ,
displayName : renameFallbackOutput || 'Fallback' ,
} ) ;
}
return ruleOutputs ;
}
} ;
export class SwitchV3 implements INodeType {
description : INodeTypeDescription ;
constructor ( baseDescription : INodeTypeBaseDescription ) {
this . description = {
. . . baseDescription ,
subtitle : ` =mode: {{( ${ capitalize } )( $ parameter["mode"])}} ` ,
version : [ 3 ] ,
defaults : {
name : 'Switch' ,
color : '#506000' ,
} ,
inputs : [ 'main' ] ,
outputs : ` ={{( ${ configuredOutputs } )( $ parameter)}} ` ,
properties : [
{
displayName : 'Mode' ,
name : 'mode' ,
type : 'options' ,
noDataExpression : true ,
options : [
{
name : 'Rules' ,
value : 'rules' ,
description : 'Build a matching rule for each output' ,
} ,
{
name : 'Expression' ,
value : 'expression' ,
description : 'Write an expression to return the output index' ,
} ,
] ,
default : 'rules' ,
description : 'How data should be routed' ,
} ,
{
displayName : 'Number of Outputs' ,
name : 'numberOutputs' ,
type : 'number' ,
displayOptions : {
show : {
mode : [ 'expression' ] ,
} ,
} ,
default : 4 ,
description : 'How many outputs to create' ,
} ,
{
displayName : 'Output Index' ,
name : 'output' ,
type : 'number' ,
validateType : 'number' ,
hint : 'The index to route the item to, starts at 0' ,
displayOptions : {
show : {
mode : [ 'expression' ] ,
} ,
} ,
// eslint-disable-next-line n8n-nodes-base/node-param-default-wrong-for-number
default : '={{}}' ,
description :
2024-01-18 00:50:54 -08:00
'The output index to send the input item to. Use an expression to calculate which input item should be routed to which output. The expression must return a number.' ,
2024-01-04 01:03:03 -08:00
} ,
{
displayName : 'Routing Rules' ,
name : 'rules' ,
placeholder : 'Add Routing Rule' ,
type : 'fixedCollection' ,
typeOptions : {
multipleValues : true ,
sortable : true ,
} ,
default : {
values : [
{
conditions : {
options : {
caseSensitive : true ,
leftValue : '' ,
typeValidation : 'strict' ,
} ,
conditions : [
{
leftValue : '' ,
rightValue : '' ,
operator : {
type : 'string' ,
operation : 'equals' ,
} ,
} ,
] ,
combinator : 'and' ,
} ,
} ,
] ,
} ,
displayOptions : {
show : {
mode : [ 'rules' ] ,
} ,
} ,
options : [
{
name : 'values' ,
displayName : 'Values' ,
values : [
{
displayName : 'Conditions' ,
name : 'conditions' ,
placeholder : 'Add Condition' ,
type : 'filter' ,
default : { } ,
typeOptions : {
multipleValues : false ,
filter : {
caseSensitive : '={{!$parameter.options.ignoreCase}}' ,
typeValidation :
'={{$parameter.options.looseTypeValidation ? "loose" : "strict"}}' ,
} ,
} ,
} ,
{
displayName : 'Rename Output' ,
name : 'renameOutput' ,
type : 'boolean' ,
default : false ,
} ,
{
displayName : 'Output Name' ,
name : 'outputKey' ,
type : 'string' ,
default : '' ,
description : 'The label of output to which to send data to if rule matches' ,
displayOptions : {
show : {
renameOutput : [ true ] ,
} ,
} ,
} ,
] ,
} ,
] ,
} ,
{
displayName : 'Options' ,
name : 'options' ,
type : 'collection' ,
placeholder : 'Add Option' ,
default : { } ,
displayOptions : {
show : {
mode : [ 'rules' ] ,
} ,
} ,
options : [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName : 'Fallback Output' ,
name : 'fallbackOutput' ,
type : 'options' ,
typeOptions : {
loadOptionsDependsOn : [ 'rules.values' , '/rules' , '/rules.values' ] ,
loadOptionsMethod : 'getFallbackOutputOptions' ,
} ,
default : 'none' ,
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description :
'If no rule matches the item will be sent to this output, by default they will be ignored' ,
} ,
{
displayName : 'Ignore Case' ,
description : 'Whether to ignore letter case when evaluating conditions' ,
name : 'ignoreCase' ,
type : 'boolean' ,
default : true ,
} ,
{
displayName : 'Less Strict Type Validation' ,
description : 'Whether to try casting value types based on the selected operator' ,
name : 'looseTypeValidation' ,
type : 'boolean' ,
default : true ,
} ,
{
displayName : 'Rename Fallback Output' ,
name : 'renameFallbackOutput' ,
type : 'string' ,
placeholder : 'e.g. Fallback' ,
default : '' ,
displayOptions : {
show : {
fallbackOutput : [ 'extra' ] ,
} ,
} ,
} ,
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
displayName : 'Send data to all matching outputs' ,
name : 'allMatchingOutputs' ,
type : 'boolean' ,
default : false ,
description :
'Whether to send data to all outputs meeting conditions (and not just the first one)' ,
} ,
] ,
} ,
] ,
} ;
}
methods = {
loadOptions : {
async getFallbackOutputOptions ( this : ILoadOptionsFunctions ) : Promise < INodePropertyOptions [ ] > {
const rules = ( this . getCurrentNodeParameter ( 'rules.values' ) as INodeParameters [ ] ) ? ? [ ] ;
const outputOptions : INodePropertyOptions [ ] = [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name : 'None (default)' ,
value : 'none' ,
description : 'Items will be ignored' ,
} ,
{
name : 'Extra Output' ,
value : 'extra' ,
description : 'Items will be sent to the extra, separate, output' ,
} ,
] ;
for ( const [ index , rule ] of rules . entries ( ) ) {
outputOptions . push ( {
name : ` Output ${ rule . outputKey || index } ` ,
value : index ,
description : ` Items will be sent to the same output as when matched rule ${ index + 1 } ` ,
} ) ;
}
return outputOptions ;
} ,
} ,
} ;
async execute ( this : IExecuteFunctions ) : Promise < INodeExecutionData [ ] [ ] > {
let returnData : INodeExecutionData [ ] [ ] = [ ] ;
const items = this . getInputData ( ) ;
const mode = this . getNodeParameter ( 'mode' , 0 ) as string ;
const checkIndexRange = ( returnDataLength : number , index : number , itemIndex = 0 ) = > {
if ( Number ( index ) === returnDataLength ) {
throw new NodeOperationError ( this . getNode ( ) , ` The ouput ${ index } is not allowed. ` , {
itemIndex ,
description : ` Output indexes are zero based, if you want to use the extra output use ${
index - 1
} ` ,
} ) ;
}
if ( index < 0 || index > returnDataLength ) {
throw new NodeOperationError ( this . getNode ( ) , ` The ouput ${ index } is not allowed ` , {
itemIndex ,
description : ` It has to be between 0 and ${ returnDataLength - 1 } ` ,
} ) ;
}
} ;
itemLoop : for ( let itemIndex = 0 ; itemIndex < items . length ; itemIndex ++ ) {
try {
const item = items [ itemIndex ] ;
item . pairedItem = { item : itemIndex } ;
if ( mode === 'expression' ) {
const numberOutputs = this . getNodeParameter ( 'numberOutputs' , itemIndex ) as number ;
if ( itemIndex === 0 ) {
returnData = new Array ( numberOutputs ) . fill ( 0 ) . map ( ( ) = > [ ] ) ;
}
const outputIndex = this . getNodeParameter ( 'output' , itemIndex ) as number ;
checkIndexRange ( returnData . length , outputIndex , itemIndex ) ;
returnData [ outputIndex ] . push ( item ) ;
} else if ( mode === 'rules' ) {
const rules = this . getNodeParameter ( 'rules.values' , itemIndex , [ ] ) as INodeParameters [ ] ;
if ( ! rules . length ) continue ;
const options = this . getNodeParameter ( 'options' , itemIndex , { } ) ;
const fallbackOutput = options . fallbackOutput ;
if ( itemIndex === 0 ) {
returnData = new Array ( rules . length ) . fill ( 0 ) . map ( ( ) = > [ ] ) ;
if ( fallbackOutput === 'extra' ) {
returnData . push ( [ ] ) ;
}
}
let matchFound = false ;
for ( const [ ruleIndex , rule ] of rules . entries ( ) ) {
let conditionPass ;
try {
conditionPass = this . getNodeParameter (
` rules.values[ ${ ruleIndex } ].conditions ` ,
itemIndex ,
false ,
{
extractValue : true ,
} ,
) as boolean ;
} catch ( error ) {
if ( ! options . looseTypeValidation ) {
error . description =
"Try to change the operator, switch ON the option 'Less Strict Type Validation', or change the type with an expression" ;
}
throw error ;
}
if ( conditionPass ) {
matchFound = true ;
checkIndexRange ( returnData . length , rule . output as number , itemIndex ) ;
returnData [ ruleIndex ] . push ( item ) ;
if ( ! options . allMatchingOutputs ) {
continue itemLoop ;
}
}
}
if ( fallbackOutput !== undefined && fallbackOutput !== 'none' && ! matchFound ) {
if ( fallbackOutput === 'extra' ) {
returnData [ returnData . length - 1 ] . push ( item ) ;
continue ;
}
checkIndexRange ( returnData . length , fallbackOutput as number , itemIndex ) ;
returnData [ fallbackOutput as number ] . push ( item ) ;
}
}
} catch ( error ) {
if ( this . continueOnFail ( ) ) {
returnData [ 0 ] . push ( { json : { error : error.message } } ) ;
continue ;
}
throw new NodeOperationError ( this . getNode ( ) , error ) ;
}
}
if ( ! returnData . length ) return [ [ ] ] ;
return returnData ;
}
}