pull:初次提交
This commit is contained in:
32
n8n-n8n-1.109.2/packages/nodes-base/nodes/Merge/v3/actions/mode/append.ts
Executable file
32
n8n-n8n-1.109.2/packages/nodes-base/nodes/Merge/v3/actions/mode/append.ts
Executable file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
type IExecuteFunctions,
|
||||
type INodeExecutionData,
|
||||
type INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { numberInputsProperty } from '../../helpers/descriptions';
|
||||
|
||||
export const properties: INodeProperties[] = [numberInputsProperty];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
mode: ['append'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
inputsData: INodeExecutionData[][],
|
||||
): Promise<INodeExecutionData[][]> {
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
for (let i = 0; i < inputsData.length; i++) {
|
||||
returnData.push.apply(returnData, inputsData[i]);
|
||||
}
|
||||
|
||||
return [returnData];
|
||||
}
|
||||
110
n8n-n8n-1.109.2/packages/nodes-base/nodes/Merge/v3/actions/mode/chooseBranch.ts
Executable file
110
n8n-n8n-1.109.2/packages/nodes-base/nodes/Merge/v3/actions/mode/chooseBranch.ts
Executable file
@@ -0,0 +1,110 @@
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { preparePairedItemDataArray, updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { numberInputsProperty } from '../../helpers/descriptions';
|
||||
|
||||
export const properties: INodeProperties[] = [
|
||||
numberInputsProperty,
|
||||
{
|
||||
displayName: 'Output Type',
|
||||
name: 'chooseBranchMode',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Wait for All Inputs to Arrive',
|
||||
value: 'waitForAll',
|
||||
},
|
||||
],
|
||||
default: 'waitForAll',
|
||||
},
|
||||
{
|
||||
displayName: 'Output',
|
||||
name: 'output',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Data of Specified Input',
|
||||
value: 'specifiedInput',
|
||||
},
|
||||
{
|
||||
name: 'A Single, Empty Item',
|
||||
value: 'empty',
|
||||
},
|
||||
],
|
||||
default: 'specifiedInput',
|
||||
displayOptions: {
|
||||
show: {
|
||||
chooseBranchMode: ['waitForAll'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Use Data of Input',
|
||||
name: 'useDataOfInput',
|
||||
type: 'options',
|
||||
default: 1,
|
||||
displayOptions: {
|
||||
show: {
|
||||
output: ['specifiedInput'],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
loadOptionsMethod: 'getInputs',
|
||||
loadOptionsDependsOn: ['numberInputs'],
|
||||
},
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
|
||||
description: 'The number of the input to use data of',
|
||||
validateType: 'number',
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
mode: ['chooseBranch'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
inputsData: INodeExecutionData[][],
|
||||
): Promise<INodeExecutionData[][]> {
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
const chooseBranchMode = this.getNodeParameter('chooseBranchMode', 0) as string;
|
||||
|
||||
if (chooseBranchMode === 'waitForAll') {
|
||||
const output = this.getNodeParameter('output', 0) as string;
|
||||
|
||||
if (output === 'specifiedInput') {
|
||||
const useDataOfInput = this.getNodeParameter('useDataOfInput', 0) as number;
|
||||
if (useDataOfInput > inputsData.length) {
|
||||
throw new NodeOperationError(this.getNode(), `Input ${useDataOfInput} doesn't exist`, {
|
||||
description: `The node has only ${inputsData.length} inputs, so selecting input ${useDataOfInput} is not possible.`,
|
||||
});
|
||||
}
|
||||
|
||||
const inputData = inputsData[useDataOfInput - 1];
|
||||
|
||||
returnData.push.apply(returnData, inputData);
|
||||
}
|
||||
if (output === 'empty') {
|
||||
const pairedItem = [
|
||||
...this.getInputData(0).map((inputData) => inputData.pairedItem),
|
||||
...this.getInputData(1).map((inputData) => inputData.pairedItem),
|
||||
].flatMap(preparePairedItemDataArray);
|
||||
|
||||
returnData.push({
|
||||
json: {},
|
||||
pairedItem,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return [returnData];
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import merge from 'lodash/merge';
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
IPairedItemData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { clashHandlingProperties, fuzzyCompareProperty } from '../../helpers/descriptions';
|
||||
import type { ClashResolveOptions } from '../../helpers/interfaces';
|
||||
import { addSuffixToEntriesKeys, selectMergeMethod } from '../../helpers/utils';
|
||||
|
||||
export const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
options: [clashHandlingProperties, fuzzyCompareProperty],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
mode: ['combine'],
|
||||
combineBy: ['combineAll'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
inputsData: INodeExecutionData[][],
|
||||
): Promise<INodeExecutionData[][]> {
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
const clashHandling = this.getNodeParameter(
|
||||
'options.clashHandling.values',
|
||||
0,
|
||||
{},
|
||||
) as ClashResolveOptions;
|
||||
|
||||
let input1 = inputsData[0];
|
||||
let input2 = inputsData[1];
|
||||
|
||||
if (clashHandling.resolveClash === 'preferInput1') {
|
||||
[input1, input2] = [input2, input1];
|
||||
}
|
||||
|
||||
if (clashHandling.resolveClash === 'addSuffix') {
|
||||
input1 = addSuffixToEntriesKeys(input1, '1');
|
||||
input2 = addSuffixToEntriesKeys(input2, '2');
|
||||
}
|
||||
|
||||
const mergeIntoSingleObject = selectMergeMethod(clashHandling);
|
||||
|
||||
if (!input1 || !input2) {
|
||||
return [returnData];
|
||||
}
|
||||
|
||||
let entry1: INodeExecutionData;
|
||||
let entry2: INodeExecutionData;
|
||||
|
||||
for (entry1 of input1) {
|
||||
for (entry2 of input2) {
|
||||
returnData.push({
|
||||
json: {
|
||||
...mergeIntoSingleObject(entry1.json, entry2.json),
|
||||
},
|
||||
binary: {
|
||||
...merge({}, entry1.binary, entry2.binary),
|
||||
},
|
||||
pairedItem: [entry1.pairedItem as IPairedItemData, entry2.pairedItem as IPairedItemData],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return [returnData];
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { clashHandlingProperties, fuzzyCompareProperty } from '../../helpers/descriptions';
|
||||
import type {
|
||||
ClashResolveOptions,
|
||||
MatchFieldsJoinMode,
|
||||
MatchFieldsOptions,
|
||||
MatchFieldsOutput,
|
||||
} from '../../helpers/interfaces';
|
||||
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];
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import merge from 'lodash/merge';
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
IPairedItemData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { clashHandlingProperties, numberInputsProperty } from '../../helpers/descriptions';
|
||||
import type { ClashResolveOptions } from '../../helpers/interfaces';
|
||||
import { addSuffixToEntriesKeys, selectMergeMethod } from '../../helpers/utils';
|
||||
|
||||
export const properties: INodeProperties[] = [
|
||||
numberInputsProperty,
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
...clashHandlingProperties,
|
||||
default: { values: { resolveClash: 'addSuffix' } },
|
||||
},
|
||||
{
|
||||
displayName: 'Include Any Unpaired Items',
|
||||
name: 'includeUnpaired',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether unpaired items should be included in the result when there are differing numbers of items among the inputs',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
mode: ['combine'],
|
||||
combineBy: ['combineByPosition'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
inputsData: INodeExecutionData[][],
|
||||
): Promise<INodeExecutionData[][]> {
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
const clashHandling = this.getNodeParameter(
|
||||
'options.clashHandling.values',
|
||||
0,
|
||||
{},
|
||||
) as ClashResolveOptions;
|
||||
const includeUnpaired = this.getNodeParameter('options.includeUnpaired', 0, false) as boolean;
|
||||
|
||||
let preferredInputIndex: number;
|
||||
|
||||
if (clashHandling?.resolveClash?.includes('preferInput')) {
|
||||
preferredInputIndex = Number(clashHandling.resolveClash.replace('preferInput', '')) - 1;
|
||||
} else {
|
||||
preferredInputIndex = inputsData.length - 1;
|
||||
}
|
||||
|
||||
const preferred = inputsData[preferredInputIndex];
|
||||
|
||||
if (clashHandling.resolveClash === 'addSuffix') {
|
||||
for (const [inputIndex, input] of inputsData.entries()) {
|
||||
inputsData[inputIndex] = addSuffixToEntriesKeys(input, String(inputIndex + 1));
|
||||
}
|
||||
}
|
||||
|
||||
let numEntries: number;
|
||||
if (includeUnpaired) {
|
||||
numEntries = Math.max(...inputsData.map((input) => input.length), preferred.length);
|
||||
} else {
|
||||
numEntries = Math.min(...inputsData.map((input) => input.length), preferred.length);
|
||||
if (numEntries === 0) {
|
||||
this.addExecutionHints({
|
||||
message: 'Consider enabling "Include Any Unpaired Items" in options or check your inputs',
|
||||
});
|
||||
return [returnData];
|
||||
}
|
||||
}
|
||||
|
||||
const mergeIntoSingleObject = selectMergeMethod(clashHandling);
|
||||
|
||||
for (let i = 0; i < numEntries; i++) {
|
||||
const preferredEntry = preferred[i] ?? ({} as INodeExecutionData);
|
||||
const restEntries = inputsData.map((input) => input[i] ?? ({} as INodeExecutionData));
|
||||
|
||||
const json = {
|
||||
...mergeIntoSingleObject(
|
||||
{},
|
||||
...restEntries.map((entry) => entry.json ?? {}),
|
||||
preferredEntry.json ?? {},
|
||||
),
|
||||
};
|
||||
|
||||
const binary = {
|
||||
...merge({}, ...restEntries.map((entry) => entry.binary ?? {}), preferredEntry.binary ?? {}),
|
||||
};
|
||||
|
||||
const pairedItem = [
|
||||
...restEntries.map((entry) => entry.pairedItem as IPairedItemData).flat(),
|
||||
preferredEntry.pairedItem as IPairedItemData,
|
||||
].filter((item) => item !== undefined);
|
||||
|
||||
returnData.push({ json, binary, pairedItem });
|
||||
}
|
||||
|
||||
return [returnData];
|
||||
}
|
||||
256
n8n-n8n-1.109.2/packages/nodes-base/nodes/Merge/v3/actions/mode/combineBySql.ts
Executable file
256
n8n-n8n-1.109.2/packages/nodes-base/nodes/Merge/v3/actions/mode/combineBySql.ts
Executable file
@@ -0,0 +1,256 @@
|
||||
import { Container } from '@n8n/di';
|
||||
import alasql from 'alasql';
|
||||
import type { Database } from 'alasql';
|
||||
import { ErrorReporter } from 'n8n-core';
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INode,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
IPairedItemData,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { getResolvables, updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { numberInputsProperty } from '../../helpers/descriptions';
|
||||
import { modifySelectQuery, rowToExecutionData } from '../../helpers/utils';
|
||||
|
||||
type OperationOptions = {
|
||||
emptyQueryResult: 'success' | 'empty';
|
||||
};
|
||||
|
||||
export const properties: INodeProperties[] = [
|
||||
numberInputsProperty,
|
||||
{
|
||||
displayName: 'Query',
|
||||
name: 'query',
|
||||
type: 'string',
|
||||
default: 'SELECT * FROM input1 LEFT JOIN input2 ON input1.name = input2.id',
|
||||
noDataExpression: true,
|
||||
description: 'Input data available as tables with corresponding number, e.g. input1, input2',
|
||||
hint: 'Supports <a href="https://github.com/alasql/alasql/wiki/Supported-SQL-statements" target="_blank">most</a> of the SQL-99 language',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
rows: 5,
|
||||
editor: 'sqlEditor',
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Empty Query Result',
|
||||
name: 'emptyQueryResult',
|
||||
type: 'options',
|
||||
description: 'What to return if the query executed successfully but returned no results',
|
||||
options: [
|
||||
{
|
||||
name: 'Success',
|
||||
value: 'success',
|
||||
},
|
||||
{
|
||||
name: 'Empty Result',
|
||||
value: 'empty',
|
||||
},
|
||||
],
|
||||
default: 'empty',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [3.2],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
mode: ['combineBySql'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
const prepareError = (node: INode, error: Error) => {
|
||||
let message = '';
|
||||
if (typeof error === 'string') {
|
||||
message = error;
|
||||
} else {
|
||||
message = error.message;
|
||||
}
|
||||
throw new NodeOperationError(node, error, {
|
||||
message: 'Issue while executing query',
|
||||
description: message,
|
||||
itemIndex: 0,
|
||||
});
|
||||
};
|
||||
|
||||
async function executeSelectWithMappedPairedItems(
|
||||
node: INode,
|
||||
inputsData: INodeExecutionData[][],
|
||||
query: string,
|
||||
returnSuccessItemIfEmpty: boolean,
|
||||
): Promise<INodeExecutionData[][]> {
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
const db: typeof Database = new (alasql as any).Database(node.id);
|
||||
|
||||
try {
|
||||
for (let i = 0; i < inputsData.length; i++) {
|
||||
const inputData = inputsData[i];
|
||||
|
||||
db.exec(`CREATE TABLE input${i + 1}`);
|
||||
db.tables[`input${i + 1}`].data = inputData.map((entry) => ({
|
||||
...entry.json,
|
||||
pairedItem: entry.pairedItem,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
throw new NodeOperationError(node, error, {
|
||||
message: 'Issue while creating table from',
|
||||
description: error.message,
|
||||
itemIndex: 0,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const result: IDataObject[] = db.exec(modifySelectQuery(query, inputsData.length));
|
||||
|
||||
for (const item of result) {
|
||||
if (Array.isArray(item)) {
|
||||
returnData.push(...item.map((entry) => rowToExecutionData(entry)));
|
||||
} else if (typeof item === 'object') {
|
||||
returnData.push(rowToExecutionData(item));
|
||||
}
|
||||
}
|
||||
|
||||
if (!returnData.length && returnSuccessItemIfEmpty) {
|
||||
returnData.push({ json: { success: true } });
|
||||
}
|
||||
} catch (error) {
|
||||
prepareError(node, error as Error);
|
||||
} finally {
|
||||
delete alasql.databases[node.id];
|
||||
}
|
||||
|
||||
return [returnData];
|
||||
}
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
inputsData: INodeExecutionData[][],
|
||||
): Promise<INodeExecutionData[][]> {
|
||||
const node = this.getNode();
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
const pairedItem: IPairedItemData[] = [];
|
||||
const options = this.getNodeParameter('options', 0, {}) as OperationOptions;
|
||||
|
||||
let query = this.getNodeParameter('query', 0) as string;
|
||||
|
||||
for (const resolvable of getResolvables(query)) {
|
||||
query = query.replace(resolvable, this.evaluateExpression(resolvable, 0) as string);
|
||||
}
|
||||
|
||||
const isSelectQuery = node.typeVersion >= 3.1 ? query.toLowerCase().startsWith('select') : false;
|
||||
const returnSuccessItemIfEmpty =
|
||||
node.typeVersion <= 3.1 ? true : options.emptyQueryResult === 'success';
|
||||
|
||||
if (isSelectQuery) {
|
||||
try {
|
||||
return await executeSelectWithMappedPairedItems(
|
||||
node,
|
||||
inputsData,
|
||||
query,
|
||||
returnSuccessItemIfEmpty,
|
||||
);
|
||||
} catch (error) {
|
||||
Container.get(ErrorReporter).error(error, {
|
||||
extra: {
|
||||
nodeName: node.name,
|
||||
nodeType: node.type,
|
||||
nodeVersion: node.typeVersion,
|
||||
workflowId: this.getWorkflow().id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const db: typeof Database = new (alasql as any).Database(node.id);
|
||||
|
||||
try {
|
||||
for (let i = 0; i < inputsData.length; i++) {
|
||||
const inputData = inputsData[i];
|
||||
|
||||
inputData.forEach((item, index) => {
|
||||
if (item.pairedItem === undefined) {
|
||||
item.pairedItem = index;
|
||||
}
|
||||
|
||||
if (typeof item.pairedItem === 'number') {
|
||||
pairedItem.push({
|
||||
item: item.pairedItem,
|
||||
input: i,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(item.pairedItem)) {
|
||||
const pairedItems = item.pairedItem
|
||||
.filter((p) => p !== undefined)
|
||||
.map((p) => (typeof p === 'number' ? { item: p } : p))
|
||||
.map((p) => {
|
||||
return {
|
||||
item: p.item,
|
||||
input: i,
|
||||
};
|
||||
});
|
||||
pairedItem.push(...pairedItems);
|
||||
return;
|
||||
}
|
||||
|
||||
pairedItem.push({
|
||||
item: item.pairedItem.item,
|
||||
input: i,
|
||||
});
|
||||
});
|
||||
|
||||
db.exec(`CREATE TABLE input${i + 1}`);
|
||||
db.tables[`input${i + 1}`].data = inputData.map((entry) => entry.json);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new NodeOperationError(node, error, {
|
||||
message: 'Issue while creating table from',
|
||||
description: error.message,
|
||||
itemIndex: 0,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const result: IDataObject[] = db.exec(query);
|
||||
|
||||
for (const item of result) {
|
||||
if (Array.isArray(item)) {
|
||||
returnData.push(...item.map((json) => ({ json, pairedItem })));
|
||||
} else if (typeof item === 'object') {
|
||||
returnData.push({ json: item, pairedItem });
|
||||
}
|
||||
}
|
||||
|
||||
if (!returnData.length && returnSuccessItemIfEmpty) {
|
||||
returnData.push({ json: { success: true }, pairedItem });
|
||||
}
|
||||
} catch (error) {
|
||||
prepareError(node, error as Error);
|
||||
} finally {
|
||||
delete alasql.databases[node.id];
|
||||
}
|
||||
|
||||
return [returnData];
|
||||
}
|
||||
77
n8n-n8n-1.109.2/packages/nodes-base/nodes/Merge/v3/actions/mode/index.ts
Executable file
77
n8n-n8n-1.109.2/packages/nodes-base/nodes/Merge/v3/actions/mode/index.ts
Executable file
@@ -0,0 +1,77 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import * as append from './append';
|
||||
import * as chooseBranch from './chooseBranch';
|
||||
import * as combineAll from './combineAll';
|
||||
import * as combineByFields from './combineByFields';
|
||||
import * as combineByPosition from './combineByPosition';
|
||||
import * as combineBySql from './combineBySql';
|
||||
|
||||
export { append, chooseBranch, combineAll, combineByFields, combineBySql, combineByPosition };
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Mode',
|
||||
name: 'mode',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Append',
|
||||
value: 'append',
|
||||
description: 'Output items of each input, one after the other',
|
||||
},
|
||||
{
|
||||
name: 'Combine',
|
||||
value: 'combine',
|
||||
description: 'Merge matching items together',
|
||||
},
|
||||
{
|
||||
name: 'SQL Query',
|
||||
value: 'combineBySql',
|
||||
description: 'Write a query to do the merge',
|
||||
},
|
||||
{
|
||||
name: 'Choose Branch',
|
||||
value: 'chooseBranch',
|
||||
description: 'Output data from a specific branch, without modifying it',
|
||||
},
|
||||
],
|
||||
default: 'append',
|
||||
description: 'How input data should be merged',
|
||||
},
|
||||
{
|
||||
displayName: 'Combine By',
|
||||
name: 'combineBy',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Matching Fields',
|
||||
value: 'combineByFields',
|
||||
description: 'Combine items with the same field values',
|
||||
},
|
||||
{
|
||||
name: 'Position',
|
||||
value: 'combineByPosition',
|
||||
description: 'Combine items based on their order',
|
||||
},
|
||||
{
|
||||
name: 'All Possible Combinations',
|
||||
value: 'combineAll',
|
||||
description: 'Every pairing of every two items (cross join)',
|
||||
},
|
||||
],
|
||||
default: 'combineByFields',
|
||||
description: 'How input data should be merged',
|
||||
displayOptions: {
|
||||
show: { mode: ['combine'] },
|
||||
},
|
||||
},
|
||||
...append.description,
|
||||
...combineAll.description,
|
||||
...combineByFields.description,
|
||||
...combineBySql.description,
|
||||
...combineByPosition.description,
|
||||
...chooseBranch.description,
|
||||
];
|
||||
7
n8n-n8n-1.109.2/packages/nodes-base/nodes/Merge/v3/actions/node.type.ts
Executable file
7
n8n-n8n-1.109.2/packages/nodes-base/nodes/Merge/v3/actions/node.type.ts
Executable file
@@ -0,0 +1,7 @@
|
||||
export type MergeType =
|
||||
| 'append'
|
||||
| 'combineByFields'
|
||||
| 'combineBySql'
|
||||
| 'combineByPosition'
|
||||
| 'combineAll'
|
||||
| 'chooseBranch';
|
||||
17
n8n-n8n-1.109.2/packages/nodes-base/nodes/Merge/v3/actions/router.ts
Executable file
17
n8n-n8n-1.109.2/packages/nodes-base/nodes/Merge/v3/actions/router.ts
Executable file
@@ -0,0 +1,17 @@
|
||||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import * as mode from './mode';
|
||||
import type { MergeType } from './node.type';
|
||||
import { getNodeInputsData } from '../helpers/utils';
|
||||
|
||||
export async function router(this: IExecuteFunctions) {
|
||||
const inputsData = getNodeInputsData.call(this);
|
||||
let operationMode = this.getNodeParameter('mode', 0) as string;
|
||||
|
||||
if (operationMode === 'combine') {
|
||||
const combineBy = this.getNodeParameter('combineBy', 0) as string;
|
||||
operationMode = combineBy;
|
||||
}
|
||||
|
||||
return await mode[operationMode as MergeType].execute.call(this, inputsData);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
|
||||
import { NodeConnectionTypes, type INodeTypeDescription } from 'n8n-workflow';
|
||||
|
||||
import * as mode from './mode';
|
||||
import { configuredInputs } from '../helpers/utils';
|
||||
|
||||
export const versionDescription: INodeTypeDescription = {
|
||||
displayName: 'Merge',
|
||||
name: 'merge',
|
||||
group: ['transform'],
|
||||
description: 'Merges data of multiple streams once data from both is available',
|
||||
version: [3, 3.1, 3.2],
|
||||
defaults: {
|
||||
name: 'Merge',
|
||||
},
|
||||
inputs: `={{(${configuredInputs})($parameter)}}`,
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
// If mode is chooseBranch data from both branches is required
|
||||
// to continue, else data from any input suffices
|
||||
requiredInputs: '={{ $parameter["mode"] === "chooseBranch" ? [0, 1] : 1 }}',
|
||||
properties: [...mode.description],
|
||||
};
|
||||
Reference in New Issue
Block a user