2019-10-09 12:52:45 -07:00
import { get } from 'lodash' ;
2019-06-23 03:35:23 -07:00
import { IExecuteFunctions } from 'n8n-core' ;
import {
GenericValue ,
INodeExecutionData ,
INodeType ,
INodeTypeDescription ,
} from 'n8n-workflow' ;
export class Merge implements INodeType {
description : INodeTypeDescription = {
displayName : 'Merge' ,
name : 'merge' ,
2019-07-26 02:27:46 -07:00
icon : 'fa:code-branch' ,
2019-06-23 03:35:23 -07:00
group : [ 'transform' ] ,
version : 1 ,
2019-08-01 09:22:48 -07:00
subtitle : '={{$parameter["mode"]}}' ,
2019-07-11 05:54:18 -07:00
description : 'Merges data of multiple streams once data of both is available' ,
2019-06-23 03:35:23 -07:00
defaults : {
name : 'Merge' ,
2019-07-26 02:41:08 -07:00
color : '#00bbcc' ,
2019-06-23 03:35:23 -07:00
} ,
inputs : [ 'main' , 'main' ] ,
outputs : [ 'main' ] ,
2019-08-02 06:56:05 -07:00
inputNames : [ 'Input 1' , 'Input 2' ] ,
2019-06-23 03:35:23 -07:00
properties : [
{
displayName : 'Mode' ,
name : 'mode' ,
type : 'options' ,
options : [
{
name : 'Append' ,
2019-07-11 05:54:18 -07:00
value : 'append' ,
description : 'Combines data of both inputs. The output will contain items of input 1 and input 2.' ,
2019-06-23 03:35:23 -07:00
} ,
2019-11-03 04:26:18 -08:00
{
name : 'Keep Key Matches' ,
value : 'keepKeyMatches' ,
description : 'Keeps data of input 1 if it does find a match with data of input 2.' ,
} ,
2019-06-23 03:35:23 -07:00
{
2019-08-02 01:26:05 -07:00
name : 'Merge By Index' ,
value : 'mergeByIndex' ,
description : 'Merges data of both inputs. The output will contain items of input 1 merged with data of input 2. Merge happens depending on the index of the items. So first item of input 1 will be merged with first item of input 2 and so on.' ,
} ,
{
name : 'Merge By Key' ,
value : 'mergeByKey' ,
2019-07-11 05:54:18 -07:00
description : 'Merges data of both inputs. The output will contain items of input 1 merged with data of input 2. Merge happens depending on a defined key.' ,
} ,
{
name : 'Pass-through' ,
value : 'passThrough' ,
description : 'Passes through data of one input. The output will conain only items of the defined input.' ,
} ,
2019-11-03 04:26:18 -08:00
{
name : 'Remove Key Matches' ,
value : 'removeKeyMatches' ,
description : 'Keeps data of input 1 if it does NOT find match with data of input 2.' ,
} ,
2019-07-11 05:54:18 -07:00
{
name : 'Wait' ,
value : 'wait' ,
2019-08-02 06:56:05 -07:00
description : 'Waits till data of both inputs is available and will then output a single empty item. If supposed to wait for multiple nodes they have to get attached to input 2. Node will not output any data.' ,
2019-06-23 03:35:23 -07:00
} ,
] ,
default : 'append' ,
2019-08-02 01:26:05 -07:00
description : 'How data of branches should be merged.' ,
} ,
{
displayName : 'Join' ,
name : 'join' ,
type : 'options' ,
displayOptions : {
show : {
mode : [
'mergeByIndex'
] ,
} ,
} ,
options : [
{
name : 'Inner Join' ,
value : 'inner' ,
description : 'Merges as many items as both inputs contain. (Example: Input1 = 5 items, Input2 = 3 items | Output will contain 3 items)' ,
} ,
{
name : 'Left Join' ,
value : 'left' ,
description : 'Merges as many items as first input contains. (Example: Input1 = 3 items, Input2 = 5 items | Output will contain 3 items)' ,
} ,
{
name : 'Outer Join' ,
value : 'outer' ,
description : 'Merges as many items as input contains with most items. (Example: Input1 = 3 items, Input2 = 5 items | Output will contain 5 items)' ,
} ,
] ,
default : 'left' ,
description : 'How many items the output will contain<br />if inputs contain different amount of items.' ,
2019-06-23 03:35:23 -07:00
} ,
{
displayName : 'Property Input 1' ,
name : 'propertyName1' ,
type : 'string' ,
default : '' ,
2019-07-11 05:54:18 -07:00
required : true ,
2019-06-23 03:35:23 -07:00
displayOptions : {
show : {
mode : [
2019-11-03 04:26:18 -08:00
'keepKeyMatches' ,
'mergeByKey' ,
'removeKeyMatches' ,
2019-06-23 03:35:23 -07:00
] ,
} ,
} ,
description : 'Name of property which decides which items to merge of input 1.' ,
} ,
{
displayName : 'Property Input 2' ,
name : 'propertyName2' ,
type : 'string' ,
default : '' ,
2019-07-11 05:54:18 -07:00
required : true ,
2019-06-23 03:35:23 -07:00
displayOptions : {
show : {
mode : [
2019-11-03 04:26:18 -08:00
'keepKeyMatches' ,
'mergeByKey' ,
'removeKeyMatches' ,
2019-06-23 03:35:23 -07:00
] ,
} ,
} ,
description : 'Name of property which decides which items to merge of input 2.' ,
} ,
2019-07-11 05:54:18 -07:00
{
displayName : 'Output Data' ,
name : 'output' ,
type : 'options' ,
displayOptions : {
show : {
mode : [
'passThrough'
] ,
} ,
} ,
options : [
{
name : 'Input 1' ,
value : 'input1' ,
} ,
{
name : 'Input 2' ,
value : 'input2' ,
} ,
] ,
default : 'input1' ,
description : 'Defines of which input the data should be used as output of node.' ,
} ,
2019-06-23 03:35:23 -07:00
]
} ;
async execute ( this : IExecuteFunctions ) : Promise < INodeExecutionData [ ] [ ] > {
const returnData : INodeExecutionData [ ] = [ ] ;
const mode = this . getNodeParameter ( 'mode' , 0 ) as string ;
if ( mode === 'append' ) {
// Simply appends the data
for ( let i = 0 ; i < 2 ; i ++ ) {
returnData . push . apply ( returnData , this . getInputData ( i ) ) ;
}
2019-08-02 01:26:05 -07:00
} else if ( mode === 'mergeByIndex' ) {
// Merges data by index
const join = this . getNodeParameter ( 'join' , 0 ) as string ;
const dataInput1 = this . getInputData ( 0 ) ;
const dataInput2 = this . getInputData ( 1 ) ;
if ( dataInput1 === undefined || dataInput1 . length === 0 ) {
if ( [ 'inner' , 'left' ] . includes ( join ) ) {
// When "inner" or "left" join return empty if first
// input does not contain any items
return [ returnData ] ;
}
// For "outer" return data of second input
return [ dataInput2 ] ;
}
if ( dataInput2 === undefined || dataInput2 . length === 0 ) {
if ( [ 'left' , 'outer' ] . includes ( join ) ) {
// When "left" or "outer" join return data of first input
return [ dataInput1 ] ;
}
// For "inner" return empty
return [ returnData ] ;
}
// Default "left"
let numEntries = dataInput1 . length ;
if ( join === 'inner' ) {
numEntries = Math . min ( dataInput1 . length , dataInput2 . length ) ;
} else if ( join === 'outer' ) {
numEntries = Math . max ( dataInput1 . length , dataInput2 . length ) ;
}
let newItem : INodeExecutionData ;
for ( let i = 0 ; i < numEntries ; i ++ ) {
if ( i >= dataInput1 . length ) {
returnData . push ( dataInput2 [ i ] ) ;
continue ;
}
if ( i >= dataInput2 . length ) {
returnData . push ( dataInput1 [ i ] ) ;
continue ;
}
newItem = {
json : { } ,
} ;
if ( dataInput1 [ i ] . binary !== undefined ) {
newItem . binary = { } ;
// Create a shallow copy of the binary data so that the old
// data references which do not get changed still stay behind
// but the incoming data does not get changed.
Object . assign ( newItem . binary , dataInput1 [ i ] . binary ) ;
}
// Create also a shallow copy of the json data
Object . assign ( newItem . json , dataInput1 [ i ] . json ) ;
// Copy json data
for ( const key of Object . keys ( dataInput2 [ i ] . json ) ) {
newItem . json [ key ] = dataInput2 [ i ] . json [ key ] ;
}
// Copy binary data
if ( dataInput2 [ i ] . binary !== undefined ) {
if ( newItem . binary === undefined ) {
newItem . binary = { } ;
}
for ( const key of Object . keys ( dataInput2 [ i ] . binary ! ) ) {
newItem . binary [ key ] = dataInput2 [ i ] . binary ! [ key ] ;
}
}
returnData . push ( newItem ) ;
}
2019-11-03 04:26:18 -08:00
} else if ( [ 'keepKeyMatches' , 'mergeByKey' , 'removeKeyMatches' ] . includes ( mode ) ) {
2019-06-23 03:35:23 -07:00
const dataInput1 = this . getInputData ( 0 ) ;
if ( ! dataInput1 ) {
// If it has no input data from first input return nothing
return [ returnData ] ;
}
const propertyName1 = this . getNodeParameter ( 'propertyName1' , 0 ) as string ;
const propertyName2 = this . getNodeParameter ( 'propertyName2' , 0 ) as string ;
const dataInput2 = this . getInputData ( 1 ) ;
if ( ! dataInput2 || ! propertyName1 || ! propertyName2 ) {
2019-11-03 04:26:18 -08:00
// Second input does not have any data or the property names are not defined
if ( mode === 'keepKeyMatches' ) {
// For "keepKeyMatches" return nothing
return [ returnData ] ;
}
// For "mergeByKey" and "removeKeyMatches" return the data from the first input
2019-06-23 03:35:23 -07:00
return [ dataInput1 ] ;
}
// Get the data to copy
const copyData : {
[ key : string ] : INodeExecutionData ;
} = { } ;
let entry : INodeExecutionData ;
for ( entry of dataInput2 ) {
2019-10-09 12:52:45 -07:00
const key = get ( entry . json , propertyName2 ) ;
if ( ! entry . json || ! key ) {
2019-06-23 03:35:23 -07:00
// Entry does not have the property so skip it
continue ;
}
2019-10-09 12:52:45 -07:00
copyData [ key as string ] = entry ;
2019-06-23 03:35:23 -07:00
}
2019-11-03 04:26:18 -08:00
// Copy data on entries or add matching entries
2019-06-23 03:35:23 -07:00
let referenceValue : GenericValue ;
let key : string ;
for ( entry of dataInput1 ) {
2019-10-09 12:52:45 -07:00
referenceValue = get ( entry . json , propertyName1 ) ;
2019-06-23 03:35:23 -07:00
2019-11-03 04:26:18 -08:00
if ( referenceValue === undefined ) {
// Entry does not have the property
if ( mode === 'removeKeyMatches' ) {
// For "removeKeyMatches" add item
returnData . push ( entry ) ;
}
// For "mergeByKey" and "keepKeyMatches" skip item
2019-06-23 03:35:23 -07:00
continue ;
}
if ( ! [ 'string' , 'number' ] . includes ( typeof referenceValue ) ) {
2019-11-03 04:26:18 -08:00
if ( referenceValue !== null && referenceValue . constructor . name !== 'Data' ) {
// Reference value is not of comparable type
if ( mode === 'removeKeyMatches' ) {
// For "removeKeyMatches" add item
returnData . push ( entry ) ;
}
// For "mergeByKey" and "keepKeyMatches" skip item
continue ;
}
2019-06-23 03:35:23 -07:00
}
if ( typeof referenceValue === 'number' ) {
referenceValue = referenceValue . toString ( ) ;
2019-11-03 04:26:18 -08:00
} else if ( referenceValue !== null && referenceValue . constructor . name === 'Date' ) {
referenceValue = ( referenceValue as Date ) . toISOString ( ) ;
2019-06-23 03:35:23 -07:00
}
if ( copyData . hasOwnProperty ( referenceValue as string ) ) {
2019-11-03 04:26:18 -08:00
// Item with reference value got found
2019-06-23 03:35:23 -07:00
if ( [ 'null' , 'undefined' ] . includes ( typeof referenceValue ) ) {
2019-11-03 04:26:18 -08:00
// The reference value is null or undefined
if ( mode === 'removeKeyMatches' ) {
// For "removeKeyMatches" add item
returnData . push ( entry ) ;
}
// For "mergeByKey" and "keepKeyMatches" skip item
2019-06-23 03:35:23 -07:00
continue ;
}
2019-08-01 13:55:33 -07:00
2019-11-03 04:26:18 -08:00
// Match exists
if ( mode === 'removeKeyMatches' ) {
// For "removeKeyMatches" we can skip the item as it has a match
continue ;
} else if ( mode === 'mergeByKey' ) {
// Copy the entry as the data gets changed
entry = JSON . parse ( JSON . stringify ( entry ) ) ;
for ( key of Object . keys ( copyData [ referenceValue as string ] . json ) ) {
// TODO: Currently only copies json data and no binary one
entry . json [ key ] = copyData [ referenceValue as string ] . json [ key ] ;
}
} else {
// For "keepKeyMatches" we add it as it is
returnData . push ( entry ) ;
continue ;
}
} else {
// No item for reference value got found
if ( mode === 'removeKeyMatches' ) {
// For "removeKeyMatches" we can add it if not match got found
returnData . push ( entry ) ;
continue ;
2019-06-23 03:35:23 -07:00
}
}
2019-11-03 04:26:18 -08:00
if ( mode === 'mergeByKey' ) {
// For "mergeByKey" we always add the entry anyway but then the unchanged one
returnData . push ( entry ) ;
}
2019-06-23 03:35:23 -07:00
}
2019-08-01 13:55:33 -07:00
return [ returnData ] ;
2019-07-11 05:54:18 -07:00
} else if ( mode === 'passThrough' ) {
const output = this . getNodeParameter ( 'output' , 0 ) as string ;
if ( output === 'input1' ) {
returnData . push . apply ( returnData , this . getInputData ( 0 ) ) ;
} else {
returnData . push . apply ( returnData , this . getInputData ( 1 ) ) ;
}
} else if ( mode === 'wait' ) {
returnData . push ( { json : { } } ) ;
2019-06-23 03:35:23 -07:00
}
return [ returnData ] ;
}
}