feat(core): Add support for pairedItem (beta) (#3012)

*  Add pairedItem support

* 👕 Fix lint issue

* 🐛 Fix resolution in frontend

* 🐛 Fix resolution issue

* 🐛 Fix resolution in frontend

* 🐛 Fix another resolution issue in frontend

*  Try to automatically add pairedItem data if possible

*  Cleanup

*  Display expression errors in editor UI

* 🐛 Fix issue that it did not display errors in production

* 🐛 Fix auto-fix of missing pairedItem data

* 🐛 Fix frontend resolution for not executed nodes

*  Fail execution on pairedItem resolve issue and display information
about itemIndex and runIndex

*  Allow that pairedItem is only set to number if runIndex is 0

*  Improve Expression Errors

*  Remove no longer needed code

*  Make errors more helpful

*  Add additional errors

* 👕 Fix lint issue

*  Add pairedItem support to core nodes

*  Improve support in Merge-Node

*  Fix issue with not correctly converted incoming pairedItem data

* 🐛 Fix frontend resolve issue

* 🐛 Fix frontend parameter name display issue

*  Improve errors

* 👕 Fix lint issue

*  Improve errors

*  Make it possible to display parameter name in error messages

*  Improve error messages

*  Fix error message

*  Improve error messages

*  Add another error message

*  Simplify
This commit is contained in:
Jan Oberhauser
2022-06-03 17:25:07 +02:00
committed by GitHub
parent 450a9aafea
commit bdb84130d6
52 changed files with 1317 additions and 152 deletions

View File

@@ -257,6 +257,9 @@ export class Compression implements INodeType {
returnData.push({
json: items[i].json,
binary: binaryObject,
pairedItem: {
item: i,
},
});
}
@@ -314,6 +317,9 @@ export class Compression implements INodeType {
binary: {
[binaryPropertyOutput]: data,
},
pairedItem: {
item: i,
},
});
}
@@ -321,13 +327,23 @@ export class Compression implements INodeType {
returnData.push({
json: items[i].json,
binary: binaryObject,
pairedItem: {
item: i,
},
});
}
}
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ json: { error: error.message } });
returnData.push({
json: {
error: error.message,
},
pairedItem: {
item: i,
},
});
continue;
}
throw error;

View File

@@ -493,11 +493,17 @@ export class Crypto implements INodeType {
// Uses dot notation so copy all data
newItem = {
json: JSON.parse(JSON.stringify(item.json)),
pairedItem: {
item: i,
},
};
} else {
// Does not use dot notation so shallow copy is enough
newItem = {
json: { ...item.json },
pairedItem: {
item: i,
},
};
}
@@ -511,7 +517,14 @@ export class Crypto implements INodeType {
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ json: { error: (error as JsonObject).message } });
returnData.push({
json: {
error: (error as JsonObject).message,
},
pairedItem: {
item: i,
},
});
continue;
}
throw error;

View File

@@ -446,11 +446,17 @@ export class DateTime implements INodeType {
// Uses dot notation so copy all data
newItem = {
json: JSON.parse(JSON.stringify(item.json)),
pairedItem: {
item: i,
},
};
} else {
// Does not use dot notation so shallow copy is enough
newItem = {
json: { ...item.json },
pairedItem: {
item: i,
},
};
}
@@ -485,11 +491,17 @@ export class DateTime implements INodeType {
// Uses dot notation so copy all data
newItem = {
json: JSON.parse(JSON.stringify(item.json)),
pairedItem: {
item: i,
},
};
} else {
// Does not use dot notation so shallow copy is enough
newItem = {
json: { ...item.json },
pairedItem: {
item: i,
},
};
}
@@ -504,7 +516,14 @@ export class DateTime implements INodeType {
} catch (error) {
if (this.continueOnFail()) {
returnData.push({json:{ error: error.message }});
returnData.push({
json: {
error: error.message,
},
pairedItem: {
item: i,
},
});
continue;
}
throw error;

View File

@@ -1211,6 +1211,9 @@ export class EditImage implements INodeType {
const newItem: INodeExecutionData = {
json: item.json,
binary: {},
pairedItem: {
item: itemIndex,
},
};
if (operation === 'information') {
@@ -1394,7 +1397,14 @@ export class EditImage implements INodeType {
} catch (error) {
if (this.continueOnFail()) {
returnData.push({json:{ error: error.message }});
returnData.push({
json: {
error: error.message,
},
pairedItem: {
item: itemIndex,
},
});
continue;
}
throw error;

View File

@@ -204,11 +204,23 @@ export class EmailSend implements INodeType {
// Send the email
const info = await transporter.sendMail(mailOptions);
returnData.push({ json: info as unknown as IDataObject });
returnData.push({
json: info as unknown as IDataObject,
pairedItem: {
item: itemIndex,
},
});
}catch (error) {
} catch (error) {
if (this.continueOnFail()) {
returnData.push({json:{ error: error.message }});
returnData.push({
json: {
error: error.message,
},
pairedItem: {
item: itemIndex,
},
});
continue;
}
throw error;

View File

@@ -119,12 +119,22 @@ export class ExecuteCommand implements INodeType {
stderr,
stdout,
},
pairedItem: {
item: itemIndex,
},
},
);
} catch (error) {
if (this.continueOnFail()) {
returnItems.push({json:{ error: error.message }});
returnItems.push({
json: {
error: error.message,
},
pairedItem: {
item: itemIndex,
},
});
continue;
}
throw error;

View File

@@ -163,6 +163,9 @@ return item;`,
const returnItem: INodeExecutionData = {
json: cleanupData(jsonData),
pairedItem: {
item: itemIndex,
},
};
if (item.binary) {
@@ -172,7 +175,14 @@ return item;`,
returnData.push(returnItem);
} catch (error) {
if (this.continueOnFail()) {
returnData.push({json:{ error: error.message }});
returnData.push({
json: {
error: error.message,
},
pairedItem: {
item: itemIndex,
},
});
continue;
}
throw error;

View File

@@ -259,7 +259,14 @@ export class Git implements INodeType {
await git.add(pathsToAdd.split(','));
returnItems.push({ json: { success: true } });
returnItems.push({
json: {
success: true,
},
pairedItem: {
item: itemIndex,
},
});
} else if (operation === 'addConfig') {
// ----------------------------------
@@ -275,7 +282,14 @@ export class Git implements INodeType {
}
await git.addConfig(key, value, append);
returnItems.push({ json: { success: true } });
returnItems.push({
json: {
success: true,
},
pairedItem: {
item: itemIndex,
},
});
} else if (operation === 'clone') {
// ----------------------------------
@@ -287,7 +301,14 @@ export class Git implements INodeType {
await git.clone(sourceRepository, '.');
returnItems.push({ json: { success: true } });
returnItems.push({
json: {
success: true,
},
pairedItem: {
item: itemIndex,
},
});
} else if (operation === 'commit') {
// ----------------------------------
@@ -303,7 +324,14 @@ export class Git implements INodeType {
await git.commit(message, pathsToAdd);
returnItems.push({ json: { success: true } });
returnItems.push({
json: {
success: true,
},
pairedItem: {
item: itemIndex,
},
});
} else if (operation === 'fetch') {
// ----------------------------------
@@ -311,7 +339,14 @@ export class Git implements INodeType {
// ----------------------------------
await git.fetch();
returnItems.push({ json: { success: true } });
returnItems.push({
json: {
success: true,
},
pairedItem: {
item: itemIndex,
},
});
} else if (operation === 'log') {
// ----------------------------------
@@ -331,7 +366,12 @@ export class Git implements INodeType {
const log = await git.log(logOptions);
// @ts-ignore
returnItems.push(...this.helpers.returnJsonArray(log.all));
returnItems.push(...this.helpers.returnJsonArray(log.all).map(item => {
return {
...item,
pairedItem: { item: itemIndex },
};
}));
} else if (operation === 'pull') {
// ----------------------------------
@@ -339,7 +379,14 @@ export class Git implements INodeType {
// ----------------------------------
await git.pull();
returnItems.push({ json: { success: true } });
returnItems.push({
json: {
success: true,
},
pairedItem: {
item: itemIndex,
},
});
} else if (operation === 'push') {
// ----------------------------------
@@ -370,7 +417,14 @@ export class Git implements INodeType {
}
}
returnItems.push({ json: { success: true } });
returnItems.push({
json: {
success: true,
},
pairedItem: {
item: itemIndex,
},
});
} else if (operation === 'pushTags') {
// ----------------------------------
@@ -378,7 +432,14 @@ export class Git implements INodeType {
// ----------------------------------
await git.pushTags();
returnItems.push({ json: { success: true } });
returnItems.push({
json: {
success: true,
},
pairedItem: {
item: itemIndex,
},
});
} else if (operation === 'listConfig') {
// ----------------------------------
@@ -396,7 +457,12 @@ export class Git implements INodeType {
}
// @ts-ignore
returnItems.push(...this.helpers.returnJsonArray(data));
returnItems.push(...this.helpers.returnJsonArray(data).map(item => {
return {
...item,
pairedItem: { item: itemIndex },
};
}));
} else if (operation === 'status') {
// ----------------------------------
@@ -406,7 +472,12 @@ export class Git implements INodeType {
const status = await git.status();
// @ts-ignore
returnItems.push(...this.helpers.returnJsonArray([status]));
returnItems.push(...this.helpers.returnJsonArray([status]).map(item => {
return {
...item,
pairedItem: { item: itemIndex },
};
}));
} else if (operation === 'tag') {
// ----------------------------------
@@ -416,14 +487,27 @@ export class Git implements INodeType {
const name = this.getNodeParameter('name', itemIndex, '') as string;
await git.addTag(name);
returnItems.push({ json: { success: true } });
returnItems.push({
json: {
success: true,
},
pairedItem: {
item: itemIndex,
},
});
}
} catch (error) {
if (this.continueOnFail()) {
returnItems.push({ json: { error: error.toString() } });
returnItems.push({
json: {
error: error.toString(),
},
pairedItem: {
item: itemIndex,
},
});
continue;
}

View File

@@ -254,6 +254,9 @@ export class HtmlExtract implements INodeType {
const newItem: INodeExecutionData = {
json: {},
pairedItem: {
item: itemIndex,
},
};
// Itterate over all the defined values which should be extracted
@@ -277,7 +280,14 @@ export class HtmlExtract implements INodeType {
}
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ json: { error: error.message } });
returnData.push({
json: {
error: error.message,
},
pairedItem: {
item: itemIndex,
},
});
continue;
}
throw error;

View File

@@ -1232,6 +1232,9 @@ export class HttpRequest implements INodeType {
json: {
error: response.reason,
},
pairedItem: {
item: itemIndex,
},
},
);
continue;
@@ -1251,6 +1254,9 @@ export class HttpRequest implements INodeType {
const newItem: INodeExecutionData = {
json: {},
binary: {},
pairedItem: {
item: itemIndex,
},
};
if (items[itemIndex].binary !== undefined) {
@@ -1295,12 +1301,20 @@ export class HttpRequest implements INodeType {
returnItem[property] = response![property];
}
returnItems.push({ json: returnItem });
returnItems.push({
json: returnItem,
pairedItem: {
item: itemIndex,
},
});
} else {
returnItems.push({
json: {
[dataPropertyName]: response,
},
pairedItem: {
item: itemIndex,
},
});
}
} else {
@@ -1319,7 +1333,12 @@ export class HttpRequest implements INodeType {
}
}
returnItems.push({ json: returnItem });
returnItems.push({
json: returnItem,
pairedItem: {
item: itemIndex,
},
});
} else {
if (responseFormat === 'json' && typeof response === 'string') {
try {
@@ -1330,9 +1349,19 @@ export class HttpRequest implements INodeType {
}
if (options.splitIntoItems === true && Array.isArray(response)) {
response.forEach(item => returnItems.push({ json: item }));
response.forEach(item => returnItems.push({
json: item,
pairedItem: {
item: itemIndex,
},
}));
} else {
returnItems.push({ json: response });
returnItems.push({
json: response,
pairedItem: {
item: itemIndex,
},
});
}
}
}

View File

@@ -355,6 +355,9 @@ export class ICalendar implements INodeType {
binary: {
[binaryPropertyName]: binaryData,
},
pairedItem: {
item: i,
},
},
);
}

View File

@@ -752,7 +752,12 @@ return 0;`,
newItem = { ...newItem, [destinationFieldName as string || fieldToSplitOut as string]: element };
}
returnData.push({ json: newItem });
returnData.push({
json: newItem,
pairedItem: {
item: i,
},
});
}
}
}
@@ -790,8 +795,17 @@ return 0;`,
}
}
let newItem: INodeExecutionData;
newItem = { json: {} };
newItem = {
json: {},
pairedItem: Array.from({length}, (_, i) => i).map(index => {
return {
item: index,
};
}),
};
// tslint:disable-next-line: no-any
const values: { [key: string]: any } = {};
const outputFields: string[] = [];
@@ -899,9 +913,10 @@ return 0;`,
}
keys = fieldsToCompare.map(key => (key.trim()));
}
// This solution is O(nlogn)
// add original index to the items
const newItems = items.map((item, index) => ({ json: { ...item['json'], __INDEX: index, }, } as INodeExecutionData));
const newItems = items.map((item, index) => ({ json: { ...item['json'], __INDEX: index, }, pairedItem: { item: index, } } as INodeExecutionData));
//sort items using the compare keys
newItems.sort((a, b) => {
let result = 0;
@@ -962,7 +977,7 @@ return 0;`,
let data = items.filter((_, index) => !removedIndexes.includes(index));
if (removeOtherFields) {
data = data.map(item => ({ json: pick(item.json, ...keys) }));
data = data.map((item, index) => ({ json: pick(item.json, ...keys), pairedItem: { item: index, } }));
}
// return the filtered items

View File

@@ -44,6 +44,7 @@ export async function router(this: IExecuteFunctions): Promise<INodeExecutionDat
if (this.continueOnFail()) {
operationResult.push({json: this.getInputData(i)[0].json, error: err});
} else {
if (err.context) err.context.itemIndex = i;
throw err;
}
}

View File

@@ -6,6 +6,7 @@ import {
INodeExecutionData,
INodeType,
INodeTypeDescription,
IPairedItemData,
} from 'n8n-workflow';
@@ -261,6 +262,10 @@ export class Merge implements INodeType {
newItem = {
json: {},
pairedItem: [
dataInput1[i].pairedItem as IPairedItemData,
dataInput2[i].pairedItem as IPairedItemData,
],
};
if (dataInput1[i].binary !== undefined) {
@@ -305,7 +310,15 @@ export class Merge implements INodeType {
for (entry1 of dataInput1) {
for (entry2 of dataInput2) {
returnData.push({json: {...(entry1.json), ...(entry2.json)}});
returnData.push({
json: {
...(entry1.json), ...(entry2.json),
},
pairedItem: [
entry1.pairedItem as IPairedItemData,
entry2.pairedItem as IPairedItemData,
],
});
}
}
return [returnData];

View File

@@ -380,6 +380,9 @@ export class MoveBinaryData implements INodeType {
// Copy the whole JSON data as data on any level can be renamed
newItem = {
json: {},
pairedItem: {
item: itemIndex,
},
};
if (mode === 'binaryToJson') {

View File

@@ -76,6 +76,9 @@ export class ReadBinaryFile implements INodeType {
const newItem: INodeExecutionData = {
json: item.json,
binary: {},
pairedItem: {
item: itemIndex,
},
};
if (item.binary !== undefined) {
@@ -90,7 +93,14 @@ export class ReadBinaryFile implements INodeType {
} catch (error) {
if (this.continueOnFail()) {
returnData.push({json:{ error: error.message }});
returnData.push({
json: {
error: error.message,
},
pairedItem: {
item: itemIndex,
},
});
continue;
}
throw error;

View File

@@ -65,6 +65,9 @@ export class ReadBinaryFiles implements INodeType {
[dataPropertyName]: await this.helpers.prepareBinaryData(data, filePath),
},
json: {},
pairedItem: {
item: 0,
},
};
items.push(item);

View File

@@ -60,7 +60,14 @@ export class ReadPdf implements INodeType {
} catch (error) {
if (this.continueOnFail()) {
returnData.push({json:{ error: error.message }});
returnData.push({
json: {
error: error.message,
},
pairedItem: {
item: itemIndex,
},
});
continue;
}
throw error;

View File

@@ -88,6 +88,9 @@ export class RenameKeys implements INodeType {
// Copy the whole JSON data as data on any level can be renamed
newItem = {
json: JSON.parse(JSON.stringify(item.json)),
pairedItem: {
item: itemIndex,
},
};
if (item.binary !== undefined) {

View File

@@ -145,6 +145,7 @@ export class Set implements INodeType {
const newItem: INodeExecutionData = {
json: {},
pairedItem: item.pairedItem,
};
if (keepOnlySet !== true) {

View File

@@ -94,6 +94,12 @@ export class SplitInBatches implements INodeType {
return null;
}
returnItems.map((item, index) => {
item.pairedItem = {
item: index,
};
});
return this.prepareOutputData(returnItems);
}
}

View File

@@ -391,17 +391,36 @@ export class SpreadsheetFile implements INodeType {
if (options.headerRow === false) {
// Data was returned as an array - https://github.com/SheetJS/sheetjs#json
for (const rowData of sheetJson) {
newItems.push({ json: { row: rowData } } as INodeExecutionData);
newItems.push({
json: {
row: rowData,
},
pairedItem: {
item: i,
},
} as INodeExecutionData);
}
} else {
for (const rowData of sheetJson) {
newItems.push({ json: rowData } as INodeExecutionData);
newItems.push({
json: rowData,
pairedItem: {
item: i,
},
} as INodeExecutionData);
}
}
} catch (error) {
if (this.continueOnFail()) {
newItems.push({json:{ error: error.message }});
newItems.push({
json: {
error: error.message,
},
pairedItem: {
item: i,
},
});
continue;
}
throw error;
@@ -466,6 +485,9 @@ export class SpreadsheetFile implements INodeType {
const newItem: INodeExecutionData = {
json: {},
binary: {},
pairedItem: {
item: 0,
},
};
let fileName = `spreadsheet.${fileFormat}`;
@@ -478,7 +500,14 @@ export class SpreadsheetFile implements INodeType {
newItems.push(newItem);
} catch (error) {
if (this.continueOnFail()) {
newItems.push({json:{ error: error.message }});
newItems.push({
json: {
error: error.message,
},
pairedItem: {
item: 0,
},
});
} else {
throw error;
}

View File

@@ -281,7 +281,7 @@ export class Ssh implements INodeType {
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const returnItems: INodeExecutionData[] = [];
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
@@ -333,7 +333,12 @@ export class Ssh implements INodeType {
const command = this.getNodeParameter('command', i) as string;
const cwd = this.getNodeParameter('cwd', i) as string;
returnData.push(await ssh.execCommand(command, { cwd, }));
returnItems.push({
json: await ssh.execCommand(command, { cwd, }),
pairedItem: {
item: i,
},
});
}
}
@@ -352,6 +357,9 @@ export class Ssh implements INodeType {
const newItem: INodeExecutionData = {
json: items[i].json,
binary: {},
pairedItem: {
item: i,
},
};
if (items[i].binary !== undefined) {
@@ -395,7 +403,14 @@ export class Ssh implements INodeType {
await ssh.putFile(path, `${parameterPath}${(parameterPath.charAt(parameterPath.length - 1) === '/') ? '' : '/'}${fileName || binaryData.fileName}`);
returnData.push({ success: true });
returnItems.push({
json: {
success: true,
},
pairedItem: {
item: i,
},
});
}
}
} catch (error) {
@@ -407,7 +422,14 @@ export class Ssh implements INodeType {
},
};
} else {
returnData.push({ error: error.message });
returnItems.push({
json: {
error: error.message,
},
pairedItem: {
item: i,
},
});
}
continue;
}
@@ -428,7 +450,7 @@ export class Ssh implements INodeType {
// For file downloads the files get attached to the existing items
return this.prepareOutputData(items);
} else {
return [this.helpers.returnJsonArray(returnData)];
return this.prepareOutputData(returnItems);
}
}
}

View File

@@ -76,6 +76,9 @@ export class WriteBinaryFile implements INodeType {
const newItem: INodeExecutionData = {
json: {},
pairedItem: {
item: itemIndex,
},
};
Object.assign(newItem.json, item.json);
@@ -100,7 +103,14 @@ export class WriteBinaryFile implements INodeType {
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ json: { error: error.message } });
returnData.push({
json: {
error: error.message,
},
pairedItem: {
item: itemIndex,
},
});
continue;
}
throw error;

View File

@@ -262,13 +262,23 @@ export class Xml implements INodeType {
json: {
[dataPropertyName]: builder.buildObject(items[itemIndex].json),
},
pairedItem: {
item: itemIndex,
},
});
} else {
throw new NodeOperationError(this.getNode(), `The operation "${mode}" is not known!`);
}
} catch (error) {
if (this.continueOnFail()) {
items[itemIndex] = ({json:{ error: error.message }});
items[itemIndex] = ({
json: {
error: error.message,
},
pairedItem: {
item: itemIndex,
},
});
continue;
}
throw error;