Allow to define which nodes change incoming data and which

not to not copy data which does not have to get copied
This commit is contained in:
Jan Oberhauser 2019-08-01 18:26:38 +02:00
parent 1d42a16d25
commit 47c2ef54d1
35 changed files with 472 additions and 9 deletions

View file

@ -599,8 +599,57 @@ export class WorkflowExecute {
break;
}
let nodeInputData = executionData.data;
const changesIncomingData = workflow.getNodeChangesData(executionData.node);
if (executionData.node.disabled !== true) {
if (changesIncomingData.value !== false) {
// Data has to be copied
// Copy only the data which is needed
const copyKeys = changesIncomingData.keys!.split(',');
const allInputsData = executionData.data.main;
let inputData: INodeExecutionData[] | null;
const newAllInputsData: Array<INodeExecutionData[] | null> = [];
let newEntries: INodeExecutionData[];
let newEntry: INodeExecutionData;
for (let inputIndex = 0; inputIndex < allInputsData.length; inputIndex++) {
inputData = allInputsData[inputIndex];
if (inputData === null) {
newAllInputsData.push(null);
continue;
}
newEntries = [];
for (const entry of inputData) {
newEntry = {
json: {},
};
for (const key of Object.keys(entry)) {
if (copyKeys.includes(key)) {
newEntry[key] = JSON.parse(JSON.stringify(entry[key]));
} else {
// Data does NOT have to get copied
newEntry[key] = entry[key];
}
}
newEntries.push(newEntry);
}
newAllInputsData.push(newEntries);
}
nodeInputData = {
main: newAllInputsData,
};
}
}
runExecutionData.resultData.lastNodeExecuted = executionData.node.name;
nodeSuccessData = await workflow.runNode(executionData.node, JSON.parse(JSON.stringify(executionData.data)), runExecutionData, runIndex, this.additionalData, NodeExecuteFunctions, this.mode);
nodeSuccessData = await workflow.runNode(executionData.node, nodeInputData, runExecutionData, runIndex, this.additionalData, NodeExecuteFunctions, this.mode);
if (nodeSuccessData === null) {
// If null gets returned it means that the node did succeed

View file

@ -157,7 +157,7 @@ Property overview
The following properties can be set in the node description:
- **credentials** [optional]: Credentials the node requests access to
- **changesIncomingData** [optional]: Object with "keys" and "value" If value is set to true it means that incomign data gets changed. If that is the case then "keys" can additionally be set to define which data (binary,json) gets changed.
- **defaults** [required]: Default "name" and "color" to set on node when it gets created
- **displayName** [required]: Name to display users in Editor UI
- **description** [required]: Description to display users in Editor UI

View file

@ -31,6 +31,9 @@ export class Cron implements INodeType {
},
inputs: [],
outputs: ['main'],
changesIncomingData: {
value: false,
},
properties: [
{
displayName: 'Trigger Times',

View file

@ -30,6 +30,9 @@ export class EmailReadImap implements INodeType {
required: true,
}
],
changesIncomingData: {
value: false,
},
properties: [
{
displayName: 'Mailbox Name',

View file

@ -31,6 +31,9 @@ export class EmailSend implements INodeType {
required: true,
}
],
changesIncomingData: {
value: false,
},
properties: [
// TODO: Add cc, bcc and choice for text as text or html (maybe also from name)
{

View file

@ -21,6 +21,9 @@ export class ErrorTrigger implements INodeType {
},
inputs: [],
outputs: ['main'],
changesIncomingData: {
value: false,
},
properties: []
};

View file

@ -58,6 +58,9 @@ export class ExecuteCommand implements INodeType {
},
inputs: ['main'],
outputs: ['main'],
changesIncomingData: {
value: false,
},
properties: [
{
displayName: 'Execute Once',

View file

@ -21,6 +21,10 @@ export class Function implements INodeType {
},
inputs: ['main'],
outputs: ['main'],
changesIncomingData: {
value: true,
keys: 'json,binary',
},
properties: [
{
displayName: 'Function',

View file

@ -23,6 +23,10 @@ export class FunctionItem implements INodeType {
},
inputs: ['main'],
outputs: ['main'],
changesIncomingData: {
value: true,
keys: 'json,binary',
},
properties: [
{
displayName: 'Function',

View file

@ -27,6 +27,9 @@ export class GoogleSheets implements INodeType {
},
inputs: ['main'],
outputs: ['main'],
changesIncomingData: {
value: false,
},
credentials: [
{
name: 'googleApi',

View file

@ -32,6 +32,9 @@ export class HttpRequest implements INodeType {
},
inputs: ['main'],
outputs: ['main'],
changesIncomingData: {
value: false,
},
credentials: [
{
name: 'httpBasicAuth',

View file

@ -23,6 +23,9 @@ export class If implements INodeType {
inputs: ['main'],
outputs: ['main', 'main'],
outputNames: ['true', 'false'],
changesIncomingData: {
value: false,
},
properties: [
{
displayName: 'Conditions',

View file

@ -20,6 +20,9 @@ export class Interval implements INodeType {
},
inputs: [],
outputs: ['main'],
changesIncomingData: {
value: false,
},
properties: [
{
displayName: 'Interval',

View file

@ -22,6 +22,10 @@ export class Merge implements INodeType {
},
inputs: ['main', 'main'],
outputs: ['main'],
changesIncomingData: {
value: '={{$parameter["mode"] === "merge"}}',
keys: 'json',
},
properties: [
{
displayName: 'Mode',

View file

@ -20,6 +20,9 @@ export class NoOp implements INodeType {
},
inputs: ['main'],
outputs: ['main'],
changesIncomingData: {
value: false,
},
properties: [
],
};

View file

@ -30,6 +30,9 @@ export class OpenWeatherMap implements INodeType {
required: true,
}
],
changesIncomingData: {
value: false,
},
properties: [
{
displayName: 'Operation',

View file

@ -27,6 +27,10 @@ export class ReadBinaryFile implements INodeType {
},
inputs: ['main'],
outputs: ['main'],
changesIncomingData: {
value: true,
keys: 'binary',
},
properties: [
{
displayName: 'File Path',

View file

@ -29,6 +29,9 @@ export class ReadBinaryFiles implements INodeType {
},
inputs: ['main'],
outputs: ['main'],
changesIncomingData: {
value: false,
},
properties: [
{
displayName: 'File Selector',

View file

@ -21,6 +21,10 @@ export class ReadFileFromUrl implements INodeType {
},
inputs: ['main'],
outputs: ['main'],
changesIncomingData: {
value: true,
keys: 'binary',
},
properties: [
{
displayName: 'URL',

View file

@ -25,6 +25,10 @@ export class ReadPdf implements INodeType {
},
inputs: ['main'],
outputs: ['main'],
changesIncomingData: {
value: true,
keys: 'json',
},
properties: [
{
displayName: 'Binary Property',

View file

@ -30,6 +30,10 @@ export class RenameKeys implements INodeType {
},
inputs: ['main'],
outputs: ['main'],
changesIncomingData: {
value: true,
keys: 'json',
},
properties: [
{
displayName: 'Keys',

View file

@ -22,6 +22,9 @@ export class RssFeedRead implements INodeType {
},
inputs: ['main'],
outputs: ['main'],
changesIncomingData: {
value: false,
},
properties: [
{
displayName: 'URL',

View file

@ -20,6 +20,10 @@ export class Set implements INodeType {
name: 'Set',
color: '#0000FF',
},
changesIncomingData: {
value: true,
keys: 'json',
},
inputs: ['main'],
outputs: ['main'],
properties: [

View file

@ -33,6 +33,9 @@ export class Slack implements INodeType {
required: true,
}
],
changesIncomingData: {
value: false,
},
properties: [
{
displayName: 'Resource',

View file

@ -20,6 +20,10 @@ export class SplitInBatches implements INodeType {
},
inputs: ['main'],
outputs: ['main'],
changesIncomingData: {
value: true,
keys: 'json',
},
properties: [
{
displayName: 'Batch Size',

View file

@ -58,6 +58,9 @@ export class SpreadsheetFile implements INodeType {
},
inputs: ['main'],
outputs: ['main'],
changesIncomingData: {
value: false,
},
properties: [
{
displayName: 'Operation',

View file

@ -21,6 +21,9 @@ export class Start implements INodeType {
},
inputs: [],
outputs: ['main'],
changesIncomingData: {
value: false,
},
properties: [
],
};

View file

@ -33,6 +33,9 @@ export class Trello implements INodeType {
required: true,
}
],
changesIncomingData: {
value: false,
},
properties: [
{
displayName: 'Resource',

View file

@ -37,6 +37,9 @@ export class TrelloTrigger implements INodeType {
required: true,
},
],
changesIncomingData: {
value: false,
},
webhooks: [
{
name: 'setup',

View file

@ -33,6 +33,9 @@ export class Twilio implements INodeType {
required: true,
}
],
changesIncomingData: {
value: false,
},
properties: [
{
displayName: 'Resource',

View file

@ -43,6 +43,9 @@ export class Webhook implements INodeType {
},
inputs: [],
outputs: ['main'],
changesIncomingData: {
value: false,
},
credentials: [
{
name: 'httpBasicAuth',

View file

@ -31,6 +31,9 @@ export class WriteBinaryFile implements INodeType {
},
inputs: ['main'],
outputs: ['main'],
changesIncomingData: {
value: false,
},
properties: [
{
displayName: 'File Name',

View file

@ -394,6 +394,11 @@ export interface IWorfklowIssues {
[key: string]: INodeIssues;
}
export interface IChangesIncomingData {
value?: string | boolean;
keys?: string;
}
export interface INodeTypeDescription {
displayName: string;
name: string;
@ -409,6 +414,7 @@ export interface INodeTypeDescription {
credentials?: INodeCredentialDescription[];
maxNodes?: number; // How many nodes of that type can be created in a workflow
subtitle?: string;
changesIncomingData?: IChangesIncomingData;
hooks?: {
[key: string]: INodeHookDescription[] | undefined;
activate?: INodeHookDescription[];

View file

@ -1,33 +1,36 @@
import {
IChangesIncomingData,
IConnection,
IConnections,
IDataObject,
INode,
NodeHelpers,
INodes,
INodeExecuteFunctions,
INodeExecutionData,
INodeParameters,
INodeIssues,
NodeParameterValue,
INodeType,
INodeTypes,
ObservableObject,
IObservableObject,
IRunExecutionData,
ITaskDataConnections,
ITriggerResponse,
IWebhookData,
IWebhookResonseData,
WebhookSetupMethodNames,
WorkflowDataProxy,
IWorfklowIssues,
IWorkflowExecuteAdditionalData,
WorkflowExecuteMode,
IWorkflowSettings,
NodeHelpers,
NodeParameterValue,
ObservableObject,
WebhookSetupMethodNames,
WorkflowDataProxy,
WorkflowExecuteMode,
} from './';
// @ts-ignore
import * as tmpl from 'riot-tmpl';
import { IConnection, IDataObject, IObservableObject } from './Interfaces';
// Set it to use double curly brackets instead of single ones
tmpl.brackets.set('{{ }}');
@ -329,6 +332,77 @@ export class Workflow {
}
/**
* Returns if the node with the given name changes the
* incoming data
*
* @param {INode} node The node to get the data of
* @returns {boolean}
* @memberof Workflow
*/
getNodeChangesData(node: INode): IChangesIncomingData {
const nodeType = this.nodeTypes.getByName(node.type);
if (nodeType === undefined) {
throw new Error(`The node type "${node.type}" of node "${node.name}" does not known.`);
}
const returnData: IChangesIncomingData = {
value: true,
keys: 'binary,json',
};
if (nodeType.description.changesIncomingData === undefined) {
// If not specifially defined that it does not change the data
// assume that it does as it would mess up everything afterwards
// if it returns "false" and then would still change data.
return returnData;
}
// Get the value for "value"
if (typeof nodeType.description.changesIncomingData.value === 'boolean') {
returnData.value = nodeType.description.changesIncomingData.value;
} else if (nodeType.description.changesIncomingData.value !== undefined) {
const changesIncomingDataValue = this.getSimpleParameterValue(node, nodeType.description.changesIncomingData.value, 'true') as boolean | string;
if (typeof changesIncomingDataValue === 'boolean') {
returnData.value = changesIncomingDataValue;
} else {
returnData.value = !(changesIncomingDataValue.toString().toLowerCase() === 'false');
}
}
// Get the value for "keys"
if (returnData.value === true && nodeType.description.changesIncomingData.keys !== undefined) {
const changesIncomingDataKeys = this.getSimpleParameterValue(node, nodeType.description.changesIncomingData.keys, 'binary,json') as string;
// Check if data is valid
if (typeof changesIncomingDataKeys !== 'string') {
throw new Error(`The data "${changesIncomingDataKeys}" for "changesIncomingData.keys" is not of type string.`);
}
const dataKeys = changesIncomingDataKeys.split(',');
const validKeys = ['binary', 'json'];
for (const key of dataKeys) {
if (!validKeys.includes(key)) {
throw new Error(`The key "${key}" for "changesIncomingData.keys" is not valid. Only the following keys are allowed: ${validKeys.join(',')}`);
}
}
// Data is valid so set it
returnData.keys = changesIncomingDataKeys;
}
if (returnData.value === false) {
return {
value: false,
};
}
return returnData;
}
/**
* Renames nodes in expressions
*

View file

@ -3,6 +3,9 @@ import {
INode,
INodeExecutionData,
INodeParameters,
INodeType,
INodeTypes,
INodeTypesObject,
IRunExecutionData,
Workflow,
} from '../src';
@ -149,6 +152,238 @@ describe('Workflow', () => {
});
describe('getNodeChangesData', () => {
const tests = [
{
description: 'should return boolean false if boolean false is set',
input: {
nodeParameters: {},
changesIncomingData: {
value: false,
},
},
output: {
value: false,
},
},
{
description: 'should return boolean false if string false is set',
input: {
nodeParameters: {},
changesIncomingData: {
value: 'false',
},
},
output: {
value: false,
},
},
{
description: 'should return only boolean false if boolean false is set with keys',
input: {
nodeParameters: {},
changesIncomingData: {
value: false,
keys: 'binary,json',
},
},
output: {
value: false,
},
},
{
description: 'should return boolean true with all keys if boolean true is set',
input: {
nodeParameters: {},
changesIncomingData: {
value: true,
},
},
output: {
value: true,
keys: 'binary,json',
},
},
{
description: 'should return boolean true with defined keys if boolean true is set',
input: {
nodeParameters: {},
changesIncomingData: {
value: true,
keys: 'json',
},
},
output: {
value: true,
keys: 'json',
},
},
{
description: 'should return boolean true with all keys if string true is set',
input: {
nodeParameters: {},
changesIncomingData: {
value: 'true',
},
},
output: {
value: true,
keys: 'binary,json',
},
},
{
description: 'should return boolean false when expression returns boolean false',
input: {
nodeParameters: {},
changesIncomingData: {
value: '={{false}}',
},
},
output: {
value: false,
},
},
{
description: 'should return boolean false when parameter-expression to boolean parameter returns boolean false',
input: {
nodeParameters: {
nodeBooleanParameter: false,
},
changesIncomingData: {
value: '={{$parameter["nodeBooleanParameter"]}}',
},
},
output: {
value: false,
},
},
{
description: 'should return boolean false when parameter-expression to boolean parameter returns boolean false',
input: {
nodeParameters: {
nodeBooleanParameter: true,
},
changesIncomingData: {
value: '={{$parameter["nodeBooleanParameter"]}}',
},
},
output: {
value: true,
keys: 'binary,json',
},
},
{
description: 'should return boolean false when parameter-expression to string parameter returns string false',
input: {
nodeParameters: {
nodeStringParameter: 'false',
},
changesIncomingData: {
value: '={{$parameter["nodeStringParameter"]}}',
},
},
output: {
value: false,
},
},
{
description: 'should return boolean true when parameter-expression to string parameter returns string true',
input: {
nodeParameters: {
nodeStringParameter: 'true',
},
changesIncomingData: {
value: '={{$parameter["nodeStringParameter"]}}',
},
},
output: {
value: true,
keys: 'binary,json',
},
},
];
for (const testData of tests) {
test(testData.description, async () => {
const node: INode = {
name: 'Test node',
parameters: {},
type: 'test.set',
typeVersion: 1,
position: [
100,
100,
],
};
class NodeTypesClass implements INodeTypes {
nodeTypes: INodeTypesObject = {};
async init(nodeTypes: INodeTypesObject): Promise<void> {
this.nodeTypes = nodeTypes;
}
getAll(): INodeType[] {
return Object.values(this.nodeTypes);
}
getByName(nodeType: string): INodeType {
return this.nodeTypes[nodeType];
}
}
const nodeTypeData: INodeType = {
description: {
displayName: 'Set',
name: 'set',
group: ['input'],
version: 1,
description: 'Sets a value',
defaults: {
name: 'Set',
color: '#0000FF',
},
inputs: ['main'],
outputs: ['main'],
properties: [
{
displayName: 'Node String Parameter',
name: 'nodeStringParameter',
type: 'string',
default: 'default-value',
},
{
displayName: 'Node Boolean Parameter',
name: 'nodeBooleanParameter',
type: 'boolean',
default: false,
},
]
}
};
Object.assign(nodeTypeData.description, { changesIncomingData: testData.input.changesIncomingData });
Object.assign(node.parameters, testData.input.nodeParameters);
const nodeTypesData: INodeTypesObject = {
'test.set': nodeTypeData,
};
const nodeTypes = new NodeTypesClass();
await nodeTypes.init(nodeTypesData);
const workflow = new Workflow(undefined, [ node ], {}, false, nodeTypes);
const nodeChangesData = workflow.getNodeChangesData(node);
expect(nodeChangesData).toEqual(testData.output);
});
}
});
describe('renameNode', () => {
const tests = [