mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-16 01:24:05 -08:00
421 lines
10 KiB
TypeScript
421 lines
10 KiB
TypeScript
import type {
|
|
IDataObject,
|
|
IExecuteFunctions,
|
|
INodeExecutionData,
|
|
INodeProperties,
|
|
} from 'n8n-workflow';
|
|
|
|
import { updateDisplayOptions } from '@utils/utilities';
|
|
|
|
import type {
|
|
ClashResolveOptions,
|
|
MatchFieldsJoinMode,
|
|
MatchFieldsOptions,
|
|
MatchFieldsOutput,
|
|
} from '../../helpers/interfaces';
|
|
import { clashHandlingProperties, fuzzyCompareProperty } from '../../helpers/descriptions';
|
|
import {
|
|
addSourceField,
|
|
addSuffixToEntriesKeys,
|
|
checkInput,
|
|
checkMatchFieldsInput,
|
|
findMatches,
|
|
mergeMatched,
|
|
} from '../../helpers/utils';
|
|
|
|
const multipleMatchesProperty: INodeProperties = {
|
|
displayName: 'Multiple Matches',
|
|
name: 'multipleMatches',
|
|
type: 'options',
|
|
default: 'all',
|
|
options: [
|
|
{
|
|
name: 'Include All Matches',
|
|
value: 'all',
|
|
description: 'Output multiple items if there are multiple matches',
|
|
},
|
|
{
|
|
name: 'Include First Match Only',
|
|
value: 'first',
|
|
description: 'Only ever output a single item per match',
|
|
},
|
|
],
|
|
};
|
|
|
|
export const properties: INodeProperties[] = [
|
|
{
|
|
displayName: 'Fields To Match Have Different Names',
|
|
name: 'advanced',
|
|
type: 'boolean',
|
|
default: false,
|
|
description: 'Whether name(s) of field to match are different in input 1 and input 2',
|
|
},
|
|
{
|
|
displayName: 'Fields to Match',
|
|
name: 'fieldsToMatchString',
|
|
type: 'string',
|
|
// eslint-disable-next-line n8n-nodes-base/node-param-placeholder-miscased-id
|
|
placeholder: 'e.g. id, name',
|
|
default: '',
|
|
requiresDataPath: 'multiple',
|
|
description: 'Specify the fields to use for matching input items',
|
|
hint: 'Drag or type the input field name',
|
|
displayOptions: {
|
|
show: {
|
|
advanced: [false],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Fields to Match',
|
|
name: 'mergeByFields',
|
|
type: 'fixedCollection',
|
|
placeholder: 'Add Fields to Match',
|
|
default: { values: [{ field1: '', field2: '' }] },
|
|
typeOptions: {
|
|
multipleValues: true,
|
|
},
|
|
description: 'Specify the fields to use for matching input items',
|
|
displayOptions: {
|
|
show: {
|
|
advanced: [true],
|
|
},
|
|
},
|
|
options: [
|
|
{
|
|
displayName: 'Values',
|
|
name: 'values',
|
|
values: [
|
|
{
|
|
displayName: 'Input 1 Field',
|
|
name: 'field1',
|
|
type: 'string',
|
|
default: '',
|
|
// eslint-disable-next-line n8n-nodes-base/node-param-placeholder-miscased-id
|
|
placeholder: 'e.g. id',
|
|
hint: 'Drag or type the input field name',
|
|
requiresDataPath: 'single',
|
|
},
|
|
{
|
|
displayName: 'Input 2 Field',
|
|
name: 'field2',
|
|
type: 'string',
|
|
default: '',
|
|
// eslint-disable-next-line n8n-nodes-base/node-param-placeholder-miscased-id
|
|
placeholder: 'e.g. id',
|
|
hint: 'Drag or type the input field name',
|
|
requiresDataPath: 'single',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
displayName: 'Output Type',
|
|
name: 'joinMode',
|
|
type: 'options',
|
|
description: 'How to select the items to send to output',
|
|
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
|
|
options: [
|
|
{
|
|
name: 'Keep Matches',
|
|
value: 'keepMatches',
|
|
description: 'Items that match, merged together (inner join)',
|
|
},
|
|
{
|
|
name: 'Keep Non-Matches',
|
|
value: 'keepNonMatches',
|
|
description: "Items that don't match",
|
|
},
|
|
{
|
|
name: 'Keep Everything',
|
|
value: 'keepEverything',
|
|
description: "Items that match merged together, plus items that don't match (outer join)",
|
|
},
|
|
{
|
|
name: 'Enrich Input 1',
|
|
value: 'enrichInput1',
|
|
description: 'All of input 1, with data from input 2 added in (left join)',
|
|
},
|
|
{
|
|
name: 'Enrich Input 2',
|
|
value: 'enrichInput2',
|
|
description: 'All of input 2, with data from input 1 added in (right join)',
|
|
},
|
|
],
|
|
default: 'keepMatches',
|
|
},
|
|
{
|
|
displayName: 'Output Data From',
|
|
name: 'outputDataFrom',
|
|
type: 'options',
|
|
options: [
|
|
{
|
|
name: 'Both Inputs Merged Together',
|
|
value: 'both',
|
|
},
|
|
{
|
|
name: 'Input 1',
|
|
value: 'input1',
|
|
},
|
|
{
|
|
name: 'Input 2',
|
|
value: 'input2',
|
|
},
|
|
],
|
|
default: 'both',
|
|
displayOptions: {
|
|
show: {
|
|
joinMode: ['keepMatches'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Output Data From',
|
|
name: 'outputDataFrom',
|
|
type: 'options',
|
|
options: [
|
|
{
|
|
name: 'Both Inputs Appended Together',
|
|
value: 'both',
|
|
},
|
|
{
|
|
name: 'Input 1',
|
|
value: 'input1',
|
|
},
|
|
{
|
|
name: 'Input 2',
|
|
value: 'input2',
|
|
},
|
|
],
|
|
default: 'both',
|
|
displayOptions: {
|
|
show: {
|
|
joinMode: ['keepNonMatches'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Options',
|
|
name: 'options',
|
|
type: 'collection',
|
|
placeholder: 'Add option',
|
|
default: {},
|
|
options: [
|
|
{
|
|
...clashHandlingProperties,
|
|
displayOptions: {
|
|
hide: {
|
|
'/joinMode': ['keepMatches', 'keepNonMatches'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
...clashHandlingProperties,
|
|
displayOptions: {
|
|
show: {
|
|
'/joinMode': ['keepMatches'],
|
|
'/outputDataFrom': ['both'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Disable Dot Notation',
|
|
name: 'disableDotNotation',
|
|
type: 'boolean',
|
|
default: false,
|
|
description:
|
|
'Whether to disallow referencing child fields using `parent.child` in the field name',
|
|
},
|
|
fuzzyCompareProperty,
|
|
{
|
|
...multipleMatchesProperty,
|
|
displayOptions: {
|
|
show: {
|
|
'/joinMode': ['keepMatches'],
|
|
'/outputDataFrom': ['both'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
...multipleMatchesProperty,
|
|
displayOptions: {
|
|
show: {
|
|
'/joinMode': ['enrichInput1', 'enrichInput2', 'keepEverything'],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const displayOptions = {
|
|
show: {
|
|
mode: ['combine'],
|
|
combineBy: ['combineByFields'],
|
|
},
|
|
};
|
|
|
|
export const description = updateDisplayOptions(displayOptions, properties);
|
|
|
|
export async function execute(
|
|
this: IExecuteFunctions,
|
|
inputsData: INodeExecutionData[][],
|
|
): Promise<INodeExecutionData[]> {
|
|
const returnData: INodeExecutionData[] = [];
|
|
const advanced = this.getNodeParameter('advanced', 0) as boolean;
|
|
let matchFields;
|
|
|
|
if (advanced) {
|
|
matchFields = this.getNodeParameter('mergeByFields.values', 0, []) as IDataObject[];
|
|
} else {
|
|
matchFields = (this.getNodeParameter('fieldsToMatchString', 0, '') as string)
|
|
.split(',')
|
|
.map((f) => {
|
|
const field = f.trim();
|
|
return { field1: field, field2: field };
|
|
});
|
|
}
|
|
|
|
matchFields = checkMatchFieldsInput(matchFields);
|
|
|
|
const joinMode = this.getNodeParameter('joinMode', 0) as MatchFieldsJoinMode;
|
|
const outputDataFrom = this.getNodeParameter('outputDataFrom', 0, 'both') as MatchFieldsOutput;
|
|
const options = this.getNodeParameter('options', 0, {}) as MatchFieldsOptions;
|
|
|
|
options.joinMode = joinMode;
|
|
options.outputDataFrom = outputDataFrom;
|
|
|
|
const nodeVersion = this.getNode().typeVersion;
|
|
|
|
let input1 = inputsData[0];
|
|
let input2 = inputsData[1];
|
|
|
|
if (nodeVersion < 2.1) {
|
|
input1 = checkInput(
|
|
this.getInputData(0),
|
|
matchFields.map((pair) => pair.field1),
|
|
options.disableDotNotation || false,
|
|
'Input 1',
|
|
);
|
|
if (!input1) return returnData;
|
|
|
|
input2 = checkInput(
|
|
this.getInputData(1),
|
|
matchFields.map((pair) => pair.field2),
|
|
options.disableDotNotation || false,
|
|
'Input 2',
|
|
);
|
|
} else {
|
|
if (!input1) return returnData;
|
|
}
|
|
|
|
if (input1.length === 0 || input2.length === 0) {
|
|
if (!input1.length && joinMode === 'keepNonMatches' && outputDataFrom === 'input1')
|
|
return returnData;
|
|
if (!input2.length && joinMode === 'keepNonMatches' && outputDataFrom === 'input2')
|
|
return returnData;
|
|
|
|
if (joinMode === 'keepMatches') {
|
|
// Stop the execution
|
|
return [];
|
|
} else if (joinMode === 'enrichInput1' && input1.length === 0) {
|
|
// No data to enrich so stop
|
|
return [];
|
|
} else if (joinMode === 'enrichInput2' && input2.length === 0) {
|
|
// No data to enrich so stop
|
|
return [];
|
|
} else {
|
|
// Return the data of any of the inputs that contains data
|
|
return [...input1, ...input2];
|
|
}
|
|
}
|
|
|
|
if (!input1) return returnData;
|
|
|
|
if (!input2 || !matchFields.length) {
|
|
if (
|
|
joinMode === 'keepMatches' ||
|
|
joinMode === 'keepEverything' ||
|
|
joinMode === 'enrichInput2'
|
|
) {
|
|
return returnData;
|
|
}
|
|
return input1;
|
|
}
|
|
|
|
const matches = findMatches(input1, input2, matchFields, options);
|
|
|
|
if (joinMode === 'keepMatches' || joinMode === 'keepEverything') {
|
|
let output: INodeExecutionData[] = [];
|
|
const clashResolveOptions = this.getNodeParameter(
|
|
'options.clashHandling.values',
|
|
0,
|
|
{},
|
|
) as ClashResolveOptions;
|
|
|
|
if (outputDataFrom === 'input1') {
|
|
output = matches.matched.map((match) => match.entry);
|
|
}
|
|
if (outputDataFrom === 'input2') {
|
|
output = matches.matched2;
|
|
}
|
|
if (outputDataFrom === 'both') {
|
|
output = mergeMatched(matches.matched, clashResolveOptions);
|
|
}
|
|
|
|
if (joinMode === 'keepEverything') {
|
|
let unmatched1 = matches.unmatched1;
|
|
let unmatched2 = matches.unmatched2;
|
|
if (clashResolveOptions.resolveClash === 'addSuffix') {
|
|
unmatched1 = addSuffixToEntriesKeys(unmatched1, '1');
|
|
unmatched2 = addSuffixToEntriesKeys(unmatched2, '2');
|
|
}
|
|
output = [...output, ...unmatched1, ...unmatched2];
|
|
}
|
|
|
|
returnData.push(...output);
|
|
}
|
|
|
|
if (joinMode === 'keepNonMatches') {
|
|
if (outputDataFrom === 'input1') {
|
|
return matches.unmatched1;
|
|
}
|
|
if (outputDataFrom === 'input2') {
|
|
return matches.unmatched2;
|
|
}
|
|
if (outputDataFrom === 'both') {
|
|
let output: INodeExecutionData[] = [];
|
|
output = output.concat(addSourceField(matches.unmatched1, 'input1'));
|
|
output = output.concat(addSourceField(matches.unmatched2, 'input2'));
|
|
return output;
|
|
}
|
|
}
|
|
|
|
if (joinMode === 'enrichInput1' || joinMode === 'enrichInput2') {
|
|
const clashResolveOptions = this.getNodeParameter(
|
|
'options.clashHandling.values',
|
|
0,
|
|
{},
|
|
) as ClashResolveOptions;
|
|
|
|
const mergedEntries = mergeMatched(matches.matched, clashResolveOptions, joinMode);
|
|
|
|
if (joinMode === 'enrichInput1') {
|
|
if (clashResolveOptions.resolveClash === 'addSuffix') {
|
|
returnData.push(...mergedEntries, ...addSuffixToEntriesKeys(matches.unmatched1, '1'));
|
|
} else {
|
|
returnData.push(...mergedEntries, ...matches.unmatched1);
|
|
}
|
|
} else {
|
|
if (clashResolveOptions.resolveClash === 'addSuffix') {
|
|
returnData.push(...mergedEntries, ...addSuffixToEntriesKeys(matches.unmatched2, '2'));
|
|
} else {
|
|
returnData.push(...mergedEntries, ...matches.unmatched2);
|
|
}
|
|
}
|
|
}
|
|
|
|
return returnData;
|
|
}
|