mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08:00
fix(core): Fix pairedItem for alwaysOutputData & multi identical resolve (#6405)
This commit is contained in:
parent
c3ba0123ad
commit
4b0e0b7970
|
@ -20,6 +20,7 @@ import type {
|
|||
INode,
|
||||
INodeConnections,
|
||||
INodeExecutionData,
|
||||
IPairedItemData,
|
||||
IPinData,
|
||||
IRun,
|
||||
IRunData,
|
||||
|
@ -971,32 +972,35 @@ export class WorkflowExecute {
|
|||
workflowId: workflow.id,
|
||||
});
|
||||
|
||||
if (nodeSuccessData) {
|
||||
// Check if the output data contains pairedItem data
|
||||
if (nodeSuccessData?.length) {
|
||||
// Check if the output data contains pairedItem data and if not try
|
||||
// to automatically fix it
|
||||
|
||||
const isSingleInputAndOutput =
|
||||
executionData.data.main.length === 1 && executionData.data.main[0]?.length === 1;
|
||||
|
||||
const isSameNumberOfItems =
|
||||
nodeSuccessData.length === 1 &&
|
||||
executionData.data.main.length === 1 &&
|
||||
executionData.data.main[0]?.length === nodeSuccessData[0].length;
|
||||
|
||||
checkOutputData: for (const outputData of nodeSuccessData) {
|
||||
if (outputData === null) {
|
||||
continue;
|
||||
}
|
||||
for (const [index, item] of outputData.entries()) {
|
||||
if (!item.pairedItem) {
|
||||
if (item.pairedItem === undefined) {
|
||||
// The pairedItem data is missing, so check if it can get automatically fixed
|
||||
if (
|
||||
executionData.data.main.length === 1 &&
|
||||
executionData.data.main[0]?.length === 1
|
||||
) {
|
||||
if (isSingleInputAndOutput) {
|
||||
// The node has one input and one incoming item, so we know
|
||||
// that all items must originate from that single
|
||||
item.pairedItem = {
|
||||
item: 0,
|
||||
};
|
||||
} else if (
|
||||
nodeSuccessData.length === 1 &&
|
||||
executionData.data.main.length === 1 &&
|
||||
executionData.data.main[0]?.length === nodeSuccessData[0].length
|
||||
) {
|
||||
// The node has one input and one output. The number of items on both is
|
||||
// identical so we can make the reasonable assumption that each of the input
|
||||
// items is the origin of the corresponding output items
|
||||
} else if (isSameNumberOfItems) {
|
||||
// The number of oncoming and outcoming items is identical so we can
|
||||
// make the reasonable assumption that each of the input items
|
||||
// is the origin of the corresponding output items
|
||||
item.pairedItem = {
|
||||
item: index,
|
||||
};
|
||||
|
@ -1018,10 +1022,26 @@ export class WorkflowExecute {
|
|||
|
||||
if (nodeSuccessData === null || nodeSuccessData[0][0] === undefined) {
|
||||
if (executionData.node.alwaysOutputData === true) {
|
||||
const pairedItem: IPairedItemData[] = [];
|
||||
|
||||
// Get pairedItem from all input items
|
||||
executionData.data.main.forEach((inputData, inputIndex) => {
|
||||
if (!inputData) {
|
||||
return;
|
||||
}
|
||||
inputData.forEach((item, itemIndex) => {
|
||||
pairedItem.push({
|
||||
item: itemIndex,
|
||||
input: inputIndex,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
nodeSuccessData = nodeSuccessData || [];
|
||||
nodeSuccessData[0] = [
|
||||
{
|
||||
json: {},
|
||||
pairedItem,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -1,868 +0,0 @@
|
|||
import set from 'lodash/set';
|
||||
|
||||
import type {
|
||||
ICredentialDataDecryptedObject,
|
||||
IDeferredPromise,
|
||||
IExecuteWorkflowInfo,
|
||||
IHttpRequestHelper,
|
||||
IHttpRequestOptions,
|
||||
INode,
|
||||
INodeCredentialsDetails,
|
||||
INodeExecutionData,
|
||||
INodeParameters,
|
||||
INodeType,
|
||||
INodeTypeData,
|
||||
INodeTypes,
|
||||
IRun,
|
||||
ITaskData,
|
||||
IVersionedNodeType,
|
||||
IWorkflowBase,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
NodeParameterValue,
|
||||
} from 'n8n-workflow';
|
||||
import { deepCopy } from 'n8n-workflow';
|
||||
import { ICredentialsHelper, NodeHelpers, WorkflowHooks } from 'n8n-workflow';
|
||||
import { Credentials } from '@/Credentials';
|
||||
import type { IExecuteFunctions } from '@/Interfaces';
|
||||
|
||||
export class CredentialsHelper extends ICredentialsHelper {
|
||||
async authenticate(
|
||||
credentials: ICredentialDataDecryptedObject,
|
||||
typeName: string,
|
||||
requestParams: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
return requestParams;
|
||||
}
|
||||
|
||||
async preAuthentication(
|
||||
helpers: IHttpRequestHelper,
|
||||
credentials: ICredentialDataDecryptedObject,
|
||||
typeName: string,
|
||||
node: INode,
|
||||
credentialsExpired: boolean,
|
||||
): Promise<ICredentialDataDecryptedObject | undefined> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getParentTypes(name: string): string[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
async getDecrypted(
|
||||
nodeCredentials: INodeCredentialsDetails,
|
||||
type: string,
|
||||
): Promise<ICredentialDataDecryptedObject> {
|
||||
return {};
|
||||
}
|
||||
|
||||
async getCredentials(
|
||||
nodeCredentials: INodeCredentialsDetails,
|
||||
type: string,
|
||||
): Promise<Credentials> {
|
||||
return new Credentials({ id: null, name: '' }, '', [], '');
|
||||
}
|
||||
|
||||
async updateCredentials(
|
||||
nodeCredentials: INodeCredentialsDetails,
|
||||
type: string,
|
||||
data: ICredentialDataDecryptedObject,
|
||||
): Promise<void> {}
|
||||
}
|
||||
|
||||
class NodeTypesClass implements INodeTypes {
|
||||
nodeTypes: INodeTypeData = {
|
||||
'n8n-nodes-base.if': {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
description: {
|
||||
displayName: 'If',
|
||||
name: 'if',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
description: 'Splits a stream depending on defined compare operations.',
|
||||
defaults: {
|
||||
name: 'IF',
|
||||
color: '#408000',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main', 'main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Conditions',
|
||||
name: 'conditions',
|
||||
placeholder: 'Add Condition',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
description: 'The type of values to compare.',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'boolean',
|
||||
displayName: 'Boolean',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Value 1',
|
||||
name: 'value1',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'The value to compare with the second one.',
|
||||
},
|
||||
{
|
||||
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.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
displayName: 'Number',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Value 1',
|
||||
name: 'value1',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description: 'The value to compare with the second one.',
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Smaller',
|
||||
value: 'smaller',
|
||||
},
|
||||
{
|
||||
name: 'Smaller Equal',
|
||||
value: 'smallerEqual',
|
||||
},
|
||||
{
|
||||
name: 'Equal',
|
||||
value: 'equal',
|
||||
},
|
||||
{
|
||||
name: 'Not Equal',
|
||||
value: 'notEqual',
|
||||
},
|
||||
{
|
||||
name: 'Larger',
|
||||
value: 'larger',
|
||||
},
|
||||
{
|
||||
name: 'Larger Equal',
|
||||
value: 'largerEqual',
|
||||
},
|
||||
{
|
||||
name: 'Is Empty',
|
||||
value: 'isEmpty',
|
||||
},
|
||||
],
|
||||
default: 'smaller',
|
||||
description: 'Operation to decide where the the data should be mapped to.',
|
||||
},
|
||||
{
|
||||
displayName: 'Value 2',
|
||||
name: 'value2',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
operation: ['isEmpty'],
|
||||
},
|
||||
},
|
||||
default: 0,
|
||||
description: 'The value to compare with the first one.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'string',
|
||||
displayName: 'String',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Value 1',
|
||||
name: 'value1',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The value to compare with the second one.',
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Contains',
|
||||
value: 'contains',
|
||||
},
|
||||
{
|
||||
name: 'Ends With',
|
||||
value: 'endsWith',
|
||||
},
|
||||
{
|
||||
name: 'Equal',
|
||||
value: 'equal',
|
||||
},
|
||||
{
|
||||
name: 'Not Contains',
|
||||
value: 'notContains',
|
||||
},
|
||||
{
|
||||
name: 'Not Equal',
|
||||
value: 'notEqual',
|
||||
},
|
||||
{
|
||||
name: 'Regex',
|
||||
value: 'regex',
|
||||
},
|
||||
{
|
||||
name: 'Starts With',
|
||||
value: 'startsWith',
|
||||
},
|
||||
{
|
||||
name: 'Is Empty',
|
||||
value: 'isEmpty',
|
||||
},
|
||||
],
|
||||
default: 'equal',
|
||||
description: 'Operation to decide where the the data should be mapped to.',
|
||||
},
|
||||
{
|
||||
displayName: 'Value 2',
|
||||
name: 'value2',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
operation: ['isEmpty', '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: 'Combine',
|
||||
name: 'combineOperation',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'ALL',
|
||||
description: 'Only if all conditions are met it goes into "true" branch.',
|
||||
value: 'all',
|
||||
},
|
||||
{
|
||||
name: 'ANY',
|
||||
description: 'If any of the conditions is met it goes into "true" branch.',
|
||||
value: 'any',
|
||||
},
|
||||
],
|
||||
default: 'all',
|
||||
description:
|
||||
'If multiple rules got set this settings decides if it is true as soon as ANY condition matches or only if ALL get meet.',
|
||||
},
|
||||
],
|
||||
},
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const returnDataTrue: INodeExecutionData[] = [];
|
||||
const returnDataFalse: INodeExecutionData[] = [];
|
||||
|
||||
const items = this.getInputData();
|
||||
|
||||
let item: INodeExecutionData;
|
||||
let combineOperation: string;
|
||||
|
||||
// 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()),
|
||||
endsWith: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||
(value1 as string).endsWith(value2 as string),
|
||||
equal: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 === value2,
|
||||
notEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 !== value2,
|
||||
larger: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||
(value1 || 0) > (value2 || 0),
|
||||
largerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||
(value1 || 0) >= (value2 || 0),
|
||||
smaller: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||
(value1 || 0) < (value2 || 0),
|
||||
smallerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||
(value1 || 0) <= (value2 || 0),
|
||||
startsWith: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||
(value1 as string).startsWith(value2 as string),
|
||||
isEmpty: (value1: NodeParameterValue) =>
|
||||
[undefined, null, ''].includes(value1 as string),
|
||||
regex: (value1: NodeParameterValue, value2: NodeParameterValue) => {
|
||||
const regexMatch = (value2 || '')
|
||||
.toString()
|
||||
.match(new RegExp('^/(.*?)/([gimusy]*)$'));
|
||||
|
||||
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);
|
||||
},
|
||||
};
|
||||
|
||||
// The different dataTypes to check the values in
|
||||
const dataTypes = ['boolean', 'number', 'string'];
|
||||
|
||||
// Iterate over all items to check which ones should be output as via output "true" and
|
||||
// which ones via output "false"
|
||||
let dataType: string;
|
||||
let compareOperationResult: boolean;
|
||||
itemLoop: for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
item = items[itemIndex];
|
||||
|
||||
let compareData: INodeParameters;
|
||||
|
||||
combineOperation = this.getNodeParameter('combineOperation', itemIndex) as string;
|
||||
|
||||
// Check all the values of the different dataTypes
|
||||
for (dataType of dataTypes) {
|
||||
// Check all the values of the current dataType
|
||||
for (compareData of this.getNodeParameter(
|
||||
`conditions.${dataType}`,
|
||||
itemIndex,
|
||||
[],
|
||||
) as INodeParameters[]) {
|
||||
// Check if the values passes
|
||||
compareOperationResult = compareOperationFunctions[compareData.operation as string](
|
||||
compareData.value1 as NodeParameterValue,
|
||||
compareData.value2 as NodeParameterValue,
|
||||
);
|
||||
|
||||
if (compareOperationResult && combineOperation === 'any') {
|
||||
// If it passes and the operation is "any" we do not have to check any
|
||||
// other ones as it should pass anyway. So go on with the next item.
|
||||
returnDataTrue.push(item);
|
||||
continue itemLoop;
|
||||
} else if (!compareOperationResult && combineOperation === 'all') {
|
||||
// If it fails and the operation is "all" we do not have to check any
|
||||
// other ones as it should be not pass anyway. So go on with the next item.
|
||||
returnDataFalse.push(item);
|
||||
continue itemLoop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (combineOperation === 'all') {
|
||||
// If the operation is "all" it means the item did match all conditions
|
||||
// so it passes.
|
||||
returnDataTrue.push(item);
|
||||
} else {
|
||||
// If the operation is "any" it means the the item did not match any condition.
|
||||
returnDataFalse.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return [returnDataTrue, returnDataFalse];
|
||||
},
|
||||
},
|
||||
},
|
||||
'n8n-nodes-base.merge': {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
description: {
|
||||
displayName: 'Merge',
|
||||
name: 'merge',
|
||||
icon: 'fa:clone',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
description: 'Merges data of multiple streams once data of both is available',
|
||||
defaults: {
|
||||
name: 'Merge',
|
||||
color: '#00cc22',
|
||||
},
|
||||
inputs: ['main', 'main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Mode',
|
||||
name: 'mode',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Append',
|
||||
value: 'append',
|
||||
description:
|
||||
'Combines data of both inputs. The output will contain items of input 1 and input 2.',
|
||||
},
|
||||
{
|
||||
name: 'Pass-through',
|
||||
value: 'passThrough',
|
||||
description:
|
||||
'Passes through data of one input. The output will contain only items of the defined input.',
|
||||
},
|
||||
{
|
||||
name: 'Wait',
|
||||
value: 'wait',
|
||||
description:
|
||||
'Waits till data of both inputs is available and will then output a single empty item.',
|
||||
},
|
||||
],
|
||||
default: 'append',
|
||||
description:
|
||||
'How data should be merged. If it should simply<br />be appended or merged depending on a property.',
|
||||
},
|
||||
{
|
||||
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.',
|
||||
},
|
||||
],
|
||||
},
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
// const itemsInput2 = this.getInputData(1);
|
||||
|
||||
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));
|
||||
}
|
||||
} 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: {} });
|
||||
}
|
||||
|
||||
return [returnData];
|
||||
},
|
||||
},
|
||||
},
|
||||
'n8n-nodes-base.noOp': {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
description: {
|
||||
displayName: 'No Operation, do nothing',
|
||||
name: 'noOp',
|
||||
icon: 'fa:arrow-right',
|
||||
group: ['organization'],
|
||||
version: 1,
|
||||
description: 'No Operation',
|
||||
defaults: {
|
||||
name: 'NoOp',
|
||||
color: '#b0b0b0',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [],
|
||||
},
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
return this.prepareOutputData(items);
|
||||
},
|
||||
},
|
||||
},
|
||||
'n8n-nodes-base.versionTest': {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
description: {
|
||||
displayName: 'Version Test',
|
||||
name: 'versionTest',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Tests if versioning works',
|
||||
defaults: {
|
||||
name: 'Version Test',
|
||||
color: '#0000FF',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Display V1',
|
||||
name: 'versionTest',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1],
|
||||
},
|
||||
},
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
displayName: 'Display V2',
|
||||
name: 'versionTest',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [2],
|
||||
},
|
||||
},
|
||||
default: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
const newItem: INodeExecutionData = {
|
||||
json: {
|
||||
versionFromParameter: this.getNodeParameter('versionTest', itemIndex),
|
||||
versionFromNode: this.getNode().typeVersion,
|
||||
},
|
||||
};
|
||||
|
||||
returnData.push(newItem);
|
||||
}
|
||||
|
||||
return this.prepareOutputData(returnData);
|
||||
},
|
||||
},
|
||||
},
|
||||
'n8n-nodes-base.set': {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
description: {
|
||||
displayName: 'Set',
|
||||
name: 'set',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Sets a value',
|
||||
defaults: {
|
||||
name: 'Set',
|
||||
color: '#0000FF',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Keep Only Set',
|
||||
name: 'keepOnlySet',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'If only the values set on this node should be<br />kept and all others removed.',
|
||||
},
|
||||
{
|
||||
displayName: 'Values to Set',
|
||||
name: 'values',
|
||||
placeholder: 'Add Value',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
description: 'The value to set.',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'boolean',
|
||||
displayName: 'Boolean',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: 'propertyName',
|
||||
description:
|
||||
'Name of the property to write data to.<br />Supports dot-notation.<br />Example: "data.person[0].name"',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'The boolean value to write in the property.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
displayName: 'Number',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: 'propertyName',
|
||||
description:
|
||||
'Name of the property to write data to.<br />Supports dot-notation.<br />Example: "data.person[0].name"',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description: 'The number value to write in the property.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'string',
|
||||
displayName: 'String',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: 'propertyName',
|
||||
description:
|
||||
'Name of the property to write data to.<br />Supports dot-notation.<br />Example: "data.person[0].name"',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The string value to write in the property.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Dot Notation',
|
||||
name: 'dotNotation',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'<p>By default, dot-notation is used in property names. This means that "a.b" will set the property "b" underneath "a" so { "a": { "b": value} }.</p><p>If that is not intended this can be deactivated, it will then set { "a.b": value } instead.</p>',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
|
||||
if (items.length === 0) {
|
||||
items.push({ json: {} });
|
||||
}
|
||||
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
let item: INodeExecutionData;
|
||||
let keepOnlySet: boolean;
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
keepOnlySet = this.getNodeParameter('keepOnlySet', itemIndex, false) as boolean;
|
||||
item = items[itemIndex];
|
||||
const options = this.getNodeParameter('options', itemIndex, {});
|
||||
|
||||
const newItem: INodeExecutionData = {
|
||||
json: {},
|
||||
};
|
||||
|
||||
if (!keepOnlySet) {
|
||||
if (item.binary !== undefined) {
|
||||
// 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.
|
||||
newItem.binary = {};
|
||||
Object.assign(newItem.binary, item.binary);
|
||||
}
|
||||
|
||||
newItem.json = deepCopy(item.json);
|
||||
}
|
||||
|
||||
// Add boolean values
|
||||
(this.getNodeParameter('values.boolean', itemIndex, []) as INodeParameters[]).forEach(
|
||||
(setItem) => {
|
||||
if (options.dotNotation === false) {
|
||||
newItem.json[setItem.name as string] = !!setItem.value;
|
||||
} else {
|
||||
set(newItem.json, setItem.name as string, !!setItem.value);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Add number values
|
||||
(this.getNodeParameter('values.number', itemIndex, []) as INodeParameters[]).forEach(
|
||||
(setItem) => {
|
||||
if (options.dotNotation === false) {
|
||||
newItem.json[setItem.name as string] = setItem.value;
|
||||
} else {
|
||||
set(newItem.json, setItem.name as string, setItem.value);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Add string values
|
||||
(this.getNodeParameter('values.string', itemIndex, []) as INodeParameters[]).forEach(
|
||||
(setItem) => {
|
||||
if (options.dotNotation === false) {
|
||||
newItem.json[setItem.name as string] = setItem.value;
|
||||
} else {
|
||||
set(newItem.json, setItem.name as string, setItem.value);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
returnData.push(newItem);
|
||||
}
|
||||
|
||||
return this.prepareOutputData(returnData);
|
||||
},
|
||||
},
|
||||
},
|
||||
'n8n-nodes-base.start': {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
description: {
|
||||
displayName: 'Start',
|
||||
name: 'start',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Starts the workflow execution from this node',
|
||||
defaults: {
|
||||
name: 'Start',
|
||||
color: '#553399',
|
||||
},
|
||||
inputs: [],
|
||||
outputs: ['main'],
|
||||
properties: [],
|
||||
},
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
|
||||
return this.prepareOutputData(items);
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
getByName(nodeType: string): INodeType | IVersionedNodeType {
|
||||
return this.nodeTypes[nodeType].type;
|
||||
}
|
||||
|
||||
getByNameAndVersion(nodeType: string, version?: number): INodeType {
|
||||
return NodeHelpers.getVersionedNodeType(this.nodeTypes[nodeType].type, version);
|
||||
}
|
||||
}
|
||||
|
||||
let nodeTypesInstance: NodeTypesClass | undefined;
|
||||
|
||||
export function NodeTypes(): NodeTypesClass {
|
||||
if (nodeTypesInstance === undefined) {
|
||||
nodeTypesInstance = new NodeTypesClass();
|
||||
}
|
||||
|
||||
return nodeTypesInstance;
|
||||
}
|
||||
|
||||
export function WorkflowExecuteAdditionalData(
|
||||
waitPromise: IDeferredPromise<IRun>,
|
||||
nodeExecutionOrder: string[],
|
||||
): IWorkflowExecuteAdditionalData {
|
||||
const hookFunctions = {
|
||||
nodeExecuteAfter: [
|
||||
async (nodeName: string, data: ITaskData): Promise<void> => {
|
||||
nodeExecutionOrder.push(nodeName);
|
||||
},
|
||||
],
|
||||
workflowExecuteAfter: [
|
||||
async (fullRunData: IRun): Promise<void> => {
|
||||
waitPromise.resolve(fullRunData);
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const workflowData: IWorkflowBase = {
|
||||
name: '',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
active: true,
|
||||
nodes: [],
|
||||
connections: {},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
return {
|
||||
credentialsHelper: new CredentialsHelper(''),
|
||||
hooks: new WorkflowHooks(hookFunctions, 'trigger', '1', workflowData),
|
||||
executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo): Promise<any> => {},
|
||||
sendMessageToUI: (message: string) => {},
|
||||
restApiUrl: '',
|
||||
encryptionKey: 'test',
|
||||
timezone: 'America/New_York',
|
||||
webhookBaseUrl: 'webhook',
|
||||
webhookWaitingBaseUrl: 'webhook-waiting',
|
||||
webhookTestBaseUrl: 'webhook-test',
|
||||
userId: '123',
|
||||
};
|
||||
}
|
|
@ -17,7 +17,7 @@ import {
|
|||
getBinaryDataBuffer,
|
||||
proxyRequestToAxios,
|
||||
} from '@/NodeExecuteFunctions';
|
||||
import { initLogger } from './utils';
|
||||
import { initLogger } from './helpers/utils';
|
||||
|
||||
const temporaryDir = mkdtempSync(join(tmpdir(), 'n8n'));
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
2067
packages/core/test/helpers/constants.ts
Normal file
2067
packages/core/test/helpers/constants.ts
Normal file
File diff suppressed because it is too large
Load diff
237
packages/core/test/helpers/index.ts
Normal file
237
packages/core/test/helpers/index.ts
Normal file
|
@ -0,0 +1,237 @@
|
|||
import path from 'path';
|
||||
import { readdirSync, readFileSync } from 'fs';
|
||||
|
||||
const BASE_DIR = path.resolve(__dirname, '../../..');
|
||||
|
||||
import type {
|
||||
ICredentialDataDecryptedObject,
|
||||
IDataObject,
|
||||
IDeferredPromise,
|
||||
IExecuteWorkflowInfo,
|
||||
IHttpRequestHelper,
|
||||
IHttpRequestOptions,
|
||||
INode,
|
||||
INodeCredentialsDetails,
|
||||
INodeType,
|
||||
INodeTypeData,
|
||||
INodeTypes,
|
||||
IRun,
|
||||
ITaskData,
|
||||
IVersionedNodeType,
|
||||
IWorkflowBase,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
NodeLoadingDetails,
|
||||
WorkflowTestData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { ICredentialsHelper, NodeHelpers, WorkflowHooks } from 'n8n-workflow';
|
||||
import { Credentials } from '@/Credentials';
|
||||
|
||||
import { predefinedNodesTypes } from './constants';
|
||||
|
||||
export class CredentialsHelper extends ICredentialsHelper {
|
||||
async authenticate(
|
||||
credentials: ICredentialDataDecryptedObject,
|
||||
typeName: string,
|
||||
requestParams: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
return requestParams;
|
||||
}
|
||||
|
||||
async preAuthentication(
|
||||
helpers: IHttpRequestHelper,
|
||||
credentials: ICredentialDataDecryptedObject,
|
||||
typeName: string,
|
||||
node: INode,
|
||||
credentialsExpired: boolean,
|
||||
): Promise<ICredentialDataDecryptedObject | undefined> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getParentTypes(name: string): string[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
async getDecrypted(
|
||||
nodeCredentials: INodeCredentialsDetails,
|
||||
type: string,
|
||||
): Promise<ICredentialDataDecryptedObject> {
|
||||
return {};
|
||||
}
|
||||
|
||||
async getCredentials(
|
||||
nodeCredentials: INodeCredentialsDetails,
|
||||
type: string,
|
||||
): Promise<Credentials> {
|
||||
return new Credentials({ id: null, name: '' }, '', [], '');
|
||||
}
|
||||
|
||||
async updateCredentials(
|
||||
nodeCredentials: INodeCredentialsDetails,
|
||||
type: string,
|
||||
data: ICredentialDataDecryptedObject,
|
||||
): Promise<void> {}
|
||||
}
|
||||
|
||||
class NodeTypesClass implements INodeTypes {
|
||||
nodeTypes: INodeTypeData;
|
||||
|
||||
constructor(nodeTypes?: INodeTypeData) {
|
||||
if (nodeTypes) {
|
||||
this.nodeTypes = nodeTypes;
|
||||
} else {
|
||||
this.nodeTypes = predefinedNodesTypes;
|
||||
}
|
||||
}
|
||||
|
||||
getByName(nodeType: string): INodeType | IVersionedNodeType {
|
||||
return this.nodeTypes[nodeType].type;
|
||||
}
|
||||
|
||||
getByNameAndVersion(nodeType: string, version?: number): INodeType {
|
||||
return NodeHelpers.getVersionedNodeType(this.nodeTypes[nodeType].type, version);
|
||||
}
|
||||
}
|
||||
|
||||
let nodeTypesInstance: NodeTypesClass | undefined;
|
||||
|
||||
export function NodeTypes(nodeTypes?: INodeTypeData): NodeTypesClass {
|
||||
if (nodeTypesInstance === undefined || nodeTypes !== undefined) {
|
||||
nodeTypesInstance = new NodeTypesClass(nodeTypes);
|
||||
}
|
||||
|
||||
return nodeTypesInstance;
|
||||
}
|
||||
|
||||
export function WorkflowExecuteAdditionalData(
|
||||
waitPromise: IDeferredPromise<IRun>,
|
||||
nodeExecutionOrder: string[],
|
||||
): IWorkflowExecuteAdditionalData {
|
||||
const hookFunctions = {
|
||||
nodeExecuteAfter: [
|
||||
async (nodeName: string, data: ITaskData): Promise<void> => {
|
||||
nodeExecutionOrder.push(nodeName);
|
||||
},
|
||||
],
|
||||
workflowExecuteAfter: [
|
||||
async (fullRunData: IRun): Promise<void> => {
|
||||
waitPromise.resolve(fullRunData);
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const workflowData: IWorkflowBase = {
|
||||
name: '',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
active: true,
|
||||
nodes: [],
|
||||
connections: {},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
return {
|
||||
credentialsHelper: new CredentialsHelper(''),
|
||||
hooks: new WorkflowHooks(hookFunctions, 'trigger', '1', workflowData),
|
||||
executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo) => {},
|
||||
sendMessageToUI: (message: string) => {},
|
||||
restApiUrl: '',
|
||||
encryptionKey: 'test',
|
||||
timezone: 'America/New_York',
|
||||
webhookBaseUrl: 'webhook',
|
||||
webhookWaitingBaseUrl: 'webhook-waiting',
|
||||
webhookTestBaseUrl: 'webhook-test',
|
||||
userId: '123',
|
||||
};
|
||||
}
|
||||
|
||||
const preparePinData = (pinData: IDataObject) => {
|
||||
const returnData = Object.keys(pinData).reduce(
|
||||
(acc, key) => {
|
||||
const data = pinData[key] as IDataObject[];
|
||||
acc[key] = [data];
|
||||
return acc;
|
||||
},
|
||||
{} as {
|
||||
[key: string]: IDataObject[][];
|
||||
},
|
||||
);
|
||||
return returnData;
|
||||
};
|
||||
|
||||
const readJsonFileSync = <T>(filePath: string) =>
|
||||
JSON.parse(readFileSync(path.join(BASE_DIR, filePath), 'utf-8')) as T;
|
||||
|
||||
export function getNodeTypes(testData: WorkflowTestData[] | WorkflowTestData) {
|
||||
if (!Array.isArray(testData)) {
|
||||
testData = [testData];
|
||||
}
|
||||
|
||||
const nodeTypes: INodeTypeData = {};
|
||||
|
||||
const nodes = [...new Set(testData.flatMap((data) => data.input.workflowData.nodes))];
|
||||
|
||||
const nodeNames = nodes.map((n) => n.type);
|
||||
|
||||
const knownNodes = readJsonFileSync<Record<string, NodeLoadingDetails>>(
|
||||
'nodes-base/dist/known/nodes.json',
|
||||
);
|
||||
|
||||
for (const nodeName of nodeNames) {
|
||||
if (!nodeName.startsWith('n8n-nodes-base.')) {
|
||||
throw new Error(`Unknown node type: ${nodeName}`);
|
||||
}
|
||||
const loadInfo = knownNodes[nodeName.replace('n8n-nodes-base.', '')];
|
||||
if (!loadInfo) {
|
||||
throw new Error(`Unknown node type: ${nodeName}`);
|
||||
}
|
||||
const sourcePath = loadInfo.sourcePath.replace(/^dist\//, './').replace(/\.js$/, '.ts');
|
||||
const nodeSourcePath = path.join(BASE_DIR, 'nodes-base', sourcePath);
|
||||
const node = new (require(nodeSourcePath)[loadInfo.className])() as INodeType;
|
||||
nodeTypes[nodeName] = {
|
||||
sourcePath: '',
|
||||
type: node,
|
||||
};
|
||||
}
|
||||
|
||||
return nodeTypes;
|
||||
}
|
||||
|
||||
const getWorkflowFilenames = (dirname: string, testFolder = 'workflows') => {
|
||||
const workflows: string[] = [];
|
||||
|
||||
const filenames: string[] = readdirSync(`${dirname}${path.sep}${testFolder}`);
|
||||
|
||||
filenames.forEach((file) => {
|
||||
if (file.endsWith('.json')) {
|
||||
workflows.push(path.join('core', 'test', testFolder, file));
|
||||
}
|
||||
});
|
||||
|
||||
return workflows;
|
||||
};
|
||||
|
||||
export const workflowToTests = (dirname: string, testFolder = 'workflows') => {
|
||||
const workflowFiles: string[] = getWorkflowFilenames(dirname, testFolder);
|
||||
|
||||
const testCases: WorkflowTestData[] = [];
|
||||
|
||||
for (const filePath of workflowFiles) {
|
||||
const description = filePath.replace('.json', '');
|
||||
const workflowData = readJsonFileSync<IWorkflowBase>(filePath);
|
||||
if (workflowData.pinData === undefined) {
|
||||
throw new Error('Workflow data does not contain pinData');
|
||||
}
|
||||
|
||||
const nodeData = preparePinData(workflowData.pinData);
|
||||
|
||||
delete workflowData.pinData;
|
||||
|
||||
const input = { workflowData };
|
||||
const output = { nodeData };
|
||||
|
||||
testCases.push({ description, input, output });
|
||||
}
|
||||
return testCases;
|
||||
};
|
565
packages/core/test/workflows/paired_items_fix.json
Normal file
565
packages/core/test/workflows/paired_items_fix.json
Normal file
|
@ -0,0 +1,565 @@
|
|||
{
|
||||
"name": "paired items fix",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"values": {
|
||||
"string": [
|
||||
{
|
||||
"name": "setting",
|
||||
"value": "hello"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"name": "Set",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 1,
|
||||
"position": [500, 680],
|
||||
"id": "18333790-db22-4235-92e6-b7dec8c20b77"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"boolean": [
|
||||
{
|
||||
"value1": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "4d4af5e5-860d-416f-b2d7-f0f87f380355",
|
||||
"name": "IF",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 1,
|
||||
"position": [1080, 500],
|
||||
"alwaysOutputData": true
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"values": {
|
||||
"string": [
|
||||
{
|
||||
"value": "={{ $('Set').item.json }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "26569caf-084d-4d5b-a575-c8e439358d10",
|
||||
"name": "Set1",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 2,
|
||||
"position": [1340, 480]
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "f4f91c8c-e695-422b-97ad-802b10c7d868",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 980]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "return [\n {\n 'thing': 1,\n 'letter': 'a'\n },\n {\n 'thing': 2,\n 'letter': 'b'\n },\n {\n 'thing': 3,\n 'letter': 'c'\n }\n]"
|
||||
},
|
||||
"id": "5eb81a1f-b845-408a-9fcc-e75e607212fa",
|
||||
"name": "Code",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 1,
|
||||
"position": [840, 500]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"functionCode": "return [\n {\n 'number': 1,\n 'letter': 'a'\n },\n {\n 'number': 2,\n 'letter': 'b'\n },\n {\n 'number': 3,\n 'letter': 'c'\n }\n]"
|
||||
},
|
||||
"name": "Generate new items",
|
||||
"type": "n8n-nodes-base.function",
|
||||
"typeVersion": 1,
|
||||
"position": [840, 860],
|
||||
"id": "dd5d92f2-5893-4591-9f22-051f50e1b348"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"values": {
|
||||
"number": [
|
||||
{
|
||||
"name": "numberOriginal",
|
||||
"value": "={{ $('Generate new items').item.json.number }}"
|
||||
}
|
||||
],
|
||||
"string": [
|
||||
{
|
||||
"name": "letterOriginal",
|
||||
"value": "={{ $('Generate new items').item.json.letter }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "ebb23410-831b-4f8f-834a-0ca22eb7c050",
|
||||
"name": "Set3",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 2,
|
||||
"position": [1320, 860]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"functionCode": "return [\n {\n 'json': {\n 'originalItem': 'third'\n },\n 'pairedItem': 2\n },\n {\n 'json': {\n 'originalItem': 'first'\n },\n 'pairedItem': 0\n },\n {\n 'json': {\n 'originalItem': 'second'\n },\n 'pairedItem': 1\n }\n]"
|
||||
},
|
||||
"name": "Mix up pairing",
|
||||
"type": "n8n-nodes-base.function",
|
||||
"typeVersion": 1,
|
||||
"position": [1080, 860],
|
||||
"id": "33ee2a0e-edc9-4197-94a2-4f77735240ff"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "### Always output data & multiple possible output resolve which are identical",
|
||||
"height": 258,
|
||||
"width": 855
|
||||
},
|
||||
"id": "3dc9ccfa-ef78-4022-8bbc-45ef8eb3a207",
|
||||
"name": "Sticky Note1",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [780, 400]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "getAllPeople"
|
||||
},
|
||||
"id": "5ba8d43b-9fa3-4ba0-9c08-3199a9d2d602",
|
||||
"name": "cuctomers",
|
||||
"type": "n8n-nodes-base.n8nTrainingCustomerDatastore",
|
||||
"typeVersion": 1,
|
||||
"position": [640, 1340]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"id": "00114764-691d-40b4-ae11-c5206a9448e3",
|
||||
"name": "result",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 2,
|
||||
"position": [1380, 1180],
|
||||
"alwaysOutputData": true
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nconst data = [];\nfor (const [index, entry] of $input.all().entries()) {\n entry.json.myNewField = index;\n entry.pairedItem = 0;\n data.push(entry);\n}\n\nreturn data;"
|
||||
},
|
||||
"id": "d3aa3bc3-3e5a-42d2-a26d-ea0c273ea3e8",
|
||||
"name": "changePairedindex",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 1,
|
||||
"position": [920, 1180]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"keepOnlySet": true,
|
||||
"values": {
|
||||
"string": [
|
||||
{
|
||||
"name": "=nameOriginalItem",
|
||||
"value": "={{ $('cuctomers').item.json.name }}"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"value": "={{ $json.name }}"
|
||||
}
|
||||
],
|
||||
"boolean": [
|
||||
{
|
||||
"name": "test",
|
||||
"value": "={{ $('cuctomers').item.json.id === $json.id }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "af18482d-4a88-4ffb-b6e3-67be45cdfad1",
|
||||
"name": "checkWithOriginal",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 2,
|
||||
"position": [1140, 1180]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"id": "3bad8f81-2fac-4e6e-bb7c-3a4921674005",
|
||||
"name": "loop",
|
||||
"type": "n8n-nodes-base.splitInBatches",
|
||||
"typeVersion": 2,
|
||||
"position": [920, 1420]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"keepOnlySet": true,
|
||||
"values": {
|
||||
"string": [
|
||||
{
|
||||
"name": "=nameOriginalItem",
|
||||
"value": "={{ $('cuctomers').item.json.name }}"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"value": "={{ $json.name }}"
|
||||
}
|
||||
],
|
||||
"boolean": [
|
||||
{
|
||||
"name": "test",
|
||||
"value": "={{ $('cuctomers').item.json.id === $json.id }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "865691b7-e4b8-487e-a5ec-80387118ea61",
|
||||
"name": "testAfterLoop",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 2,
|
||||
"position": [1180, 1620]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"id": "8c5d3a1c-e34b-4937-bc22-88b418391002",
|
||||
"name": "result1",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 2,
|
||||
"position": [1380, 1620],
|
||||
"alwaysOutputData": true
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nconst data = [];\nfor (const [index, entry] of $input.all().entries()) {\n entry.json.myNewField = index;\n entry.pairedItem = 0;\n data.push(entry);\n}\n\nreturn data;"
|
||||
},
|
||||
"id": "ad476a3a-d491-406f-903d-022cb0f0ef3c",
|
||||
"name": "changePairedindex1",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 1,
|
||||
"position": [1380, 1400]
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Set3": [
|
||||
{
|
||||
"json": {
|
||||
"originalItem": "third",
|
||||
"numberOriginal": 3,
|
||||
"letterOriginal": "c"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"originalItem": "first",
|
||||
"numberOriginal": 1,
|
||||
"letterOriginal": "a"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"originalItem": "second",
|
||||
"numberOriginal": 2,
|
||||
"letterOriginal": "b"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 2
|
||||
}
|
||||
}
|
||||
],
|
||||
"Set1": [
|
||||
{
|
||||
"json": {
|
||||
"propertyName": {
|
||||
"setting": "hello"
|
||||
}
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": [
|
||||
{
|
||||
"json": {
|
||||
"test": true,
|
||||
"nameOriginalItem": "Jay Gatsby",
|
||||
"name": "Jay Gatsby"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"test": false,
|
||||
"nameOriginalItem": "Jay Gatsby",
|
||||
"name": "José Arcadio Buendía"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"test": false,
|
||||
"nameOriginalItem": "Jay Gatsby",
|
||||
"name": "Max Sendak"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"test": false,
|
||||
"nameOriginalItem": "Jay Gatsby",
|
||||
"name": "Zaphod Beeblebrox"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"test": false,
|
||||
"nameOriginalItem": "Jay Gatsby",
|
||||
"name": "Edmund Pevensie"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"result1": [
|
||||
{
|
||||
"json": {
|
||||
"test": true,
|
||||
"nameOriginalItem": "Jay Gatsby",
|
||||
"name": "Jay Gatsby"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"test": false,
|
||||
"nameOriginalItem": "Jay Gatsby",
|
||||
"name": "José Arcadio Buendía"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"test": false,
|
||||
"nameOriginalItem": "Jay Gatsby",
|
||||
"name": "Max Sendak"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"test": false,
|
||||
"nameOriginalItem": "Jay Gatsby",
|
||||
"name": "Zaphod Beeblebrox"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"test": false,
|
||||
"nameOriginalItem": "Jay Gatsby",
|
||||
"name": "Edmund Pevensie"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 4
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"Set": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Code",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Generate new items",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"IF": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Set1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Set",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "cuctomers",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Code": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "IF",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Generate new items": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Mix up pairing",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Mix up pairing": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Set3",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"cuctomers": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "changePairedindex",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "loop",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"changePairedindex": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "checkWithOriginal",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"checkWithOriginal": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"loop": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "changePairedindex1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "testAfterLoop",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"testAfterLoop": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "result1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"changePairedindex1": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "loop",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {},
|
||||
"versionId": "6f6ee01c-8c99-493f-a30c-6a5ed2b71750",
|
||||
"id": "169",
|
||||
"meta": {
|
||||
"instanceId": "36203ea1ce3cef713fa25999bd9874ae26b9e4c2c3a90a365f2882a154d031d0"
|
||||
},
|
||||
"tags": []
|
||||
}
|
|
@ -19,6 +19,7 @@ import type {
|
|||
IHttpRequestOptions,
|
||||
ILogger,
|
||||
INode,
|
||||
INodeCredentials,
|
||||
INodeCredentialsDetails,
|
||||
INodeType,
|
||||
INodeTypeData,
|
||||
|
@ -29,10 +30,10 @@ import type {
|
|||
IWorkflowBase,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
NodeLoadingDetails,
|
||||
WorkflowTestData,
|
||||
} from 'n8n-workflow';
|
||||
import { ICredentialsHelper, LoggerProxy, NodeHelpers, WorkflowHooks } from 'n8n-workflow';
|
||||
import { executeWorkflow } from './ExecuteWorkflow';
|
||||
import type { WorkflowTestData } from './types';
|
||||
|
||||
import { FAKE_CREDENTIALS_DATA } from './FakeCredentialsMap';
|
||||
|
||||
|
@ -237,7 +238,7 @@ export function setup(testData: WorkflowTestData[] | WorkflowTestData) {
|
|||
const nodes = [...new Set(testData.flatMap((data) => data.input.workflowData.nodes))];
|
||||
const credentialNames = nodes
|
||||
.filter((n) => n.credentials)
|
||||
.flatMap(({ credentials }) => Object.keys(credentials!));
|
||||
.flatMap(({ credentials }) => Object.keys(credentials as INodeCredentials));
|
||||
for (const credentialName of credentialNames) {
|
||||
const loadInfo = knownCredentials[credentialName];
|
||||
if (!loadInfo) {
|
||||
|
|
|
@ -1,23 +1,3 @@
|
|||
import type { INode, IConnections } from 'n8n-workflow';
|
||||
|
||||
export interface WorkflowTestData {
|
||||
description: string;
|
||||
input: {
|
||||
workflowData: {
|
||||
nodes: INode[];
|
||||
connections: IConnections;
|
||||
settings?: {
|
||||
saveManualExecutions: boolean;
|
||||
callerPolicy: string;
|
||||
timezone: string;
|
||||
saveExecutionProgress: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
output: {
|
||||
nodeExecutionOrder?: string[];
|
||||
nodeData: {
|
||||
[key: string]: any[][];
|
||||
};
|
||||
};
|
||||
}
|
||||
import type { WorkflowTestData } from 'n8n-workflow';
|
||||
//TODO: remove, update import in tests
|
||||
export { WorkflowTestData };
|
||||
|
|
|
@ -1763,6 +1763,23 @@ export interface IWorkflowSettings {
|
|||
executionTimeout?: number;
|
||||
}
|
||||
|
||||
export interface WorkflowTestData {
|
||||
description: string;
|
||||
input: {
|
||||
workflowData: {
|
||||
nodes: INode[];
|
||||
connections: IConnections;
|
||||
settings?: IWorkflowSettings;
|
||||
};
|
||||
};
|
||||
output: {
|
||||
nodeExecutionOrder?: string[];
|
||||
nodeData: {
|
||||
[key: string]: any[][];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export type LogTypes = 'debug' | 'verbose' | 'info' | 'warn' | 'error';
|
||||
|
||||
export interface ILogger {
|
||||
|
|
|
@ -806,6 +806,13 @@ export class WorkflowDataProxy {
|
|||
.filter((result) => result !== null);
|
||||
|
||||
if (results.length !== 1) {
|
||||
// Check if the results are all the same
|
||||
const firstResult = results[0];
|
||||
if (results.every((result) => result === firstResult)) {
|
||||
// All results are the same so return the first one
|
||||
return firstResult;
|
||||
}
|
||||
|
||||
throw createExpressionError('Invalid expression', {
|
||||
messageTemplate: 'Invalid expression under ‘%%PARAMETER%%’',
|
||||
functionality: 'pairedItem',
|
||||
|
|
Loading…
Reference in a new issue