🔀 Merge branch 'master' into oauth-support

This commit is contained in:
Jan Oberhauser
2020-04-04 17:34:10 +02:00
376 changed files with 45850 additions and 2723 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "n8n-workflow",
"version": "0.20.0",
"version": "0.26.0",
"description": "Workflow base code of n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",

View File

@@ -193,16 +193,20 @@ export interface IExecuteContextData {
export interface IExecuteFunctions {
continueOnFail(): boolean;
evaluateExpression(expression: string, itemIndex: number): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[];
executeWorkflow(workflowInfo: IExecuteWorkflowInfo, inputData?: INodeExecutionData[]): Promise<any>; // tslint:disable-line:no-any
getContext(type: string): IContextObject;
getCredentials(type: string): ICredentialDataDecryptedObject | undefined;
getInputData(inputIndex?: number, inputName?: string): INodeExecutionData[];
getMode(): WorkflowExecuteMode;
getNode(): INode;
getNodeParameter(parameterName: string, itemIndex: number, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object; //tslint:disable-line:no-any
getWorkflowDataProxy(itemIndex: number): IWorkflowDataProxyData;
getWorkflowStaticData(type: string): IDataObject;
getRestApiUrl(): string;
getTimezone(): string;
getWorkflow(workflow: Workflow): IWorkflowMetadata;
prepareOutputData(outputData: INodeExecutionData[], outputIndex?: number): Promise<INodeExecutionData[][]>;
helpers: {
[key: string]: (...args: any[]) => any //tslint:disable-line:no-any
@@ -211,13 +215,17 @@ export interface IExecuteFunctions {
export interface IExecuteSingleFunctions {
continueOnFail(): boolean;
evaluateExpression(expression: string, itemIndex: number | undefined): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[];
getContext(type: string): IContextObject;
getCredentials(type: string): ICredentialDataDecryptedObject | undefined;
getInputData(inputIndex?: number, inputName?: string): INodeExecutionData;
getMode(): WorkflowExecuteMode;
getNode(): INode;
getNodeParameter(parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object; //tslint:disable-line:no-any
getRestApiUrl(): string;
getTimezone(): string;
getWorkflow(workflow: Workflow): IWorkflowMetadata;
getWorkflowDataProxy(): IWorkflowDataProxyData;
getWorkflowStaticData(type: string): IDataObject;
helpers: {
@@ -232,6 +240,7 @@ export interface IExecuteWorkflowInfo {
export interface ILoadOptionsFunctions {
getCredentials(type: string): ICredentialDataDecryptedObject | undefined;
getNode(): INode;
getNodeParameter(parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object; //tslint:disable-line:no-any
getCurrentNodeParameter(parameterName: string): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object | undefined;
getCurrentNodeParameters(): INodeParameters | undefined;
@@ -245,11 +254,13 @@ export interface ILoadOptionsFunctions {
export interface IHookFunctions {
getCredentials(type: string): ICredentialDataDecryptedObject | undefined;
getMode(): WorkflowExecuteMode;
getNode(): INode;
getNodeWebhookUrl: (name: string) => string | undefined;
getNodeParameter(parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object; //tslint:disable-line:no-any
getTimezone(): string;
getWebhookDescription(name: string): IWebhookDescription | undefined;
getWebhookName(): string;
getWorkflow(workflow: Workflow): IWorkflowMetadata;
getWorkflowStaticData(type: string): IDataObject;
helpers: {
[key: string]: (...args: any[]) => any //tslint:disable-line:no-any
@@ -260,9 +271,11 @@ export interface IPollFunctions {
__emit(data: INodeExecutionData[][]): void;
getCredentials(type: string): ICredentialDataDecryptedObject | undefined;
getMode(): WorkflowExecuteMode;
getNode(): INode;
getNodeParameter(parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object; //tslint:disable-line:no-any
getRestApiUrl(): string;
getTimezone(): string;
getWorkflow(workflow: Workflow): IWorkflowMetadata;
getWorkflowStaticData(type: string): IDataObject;
helpers: {
[key: string]: (...args: any[]) => any //tslint:disable-line:no-any
@@ -273,9 +286,11 @@ export interface ITriggerFunctions {
emit(data: INodeExecutionData[][]): void;
getCredentials(type: string): ICredentialDataDecryptedObject | undefined;
getMode(): WorkflowExecuteMode;
getNode(): INode;
getNodeParameter(parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object; //tslint:disable-line:no-any
getRestApiUrl(): string;
getTimezone(): string;
getWorkflow(workflow: Workflow): IWorkflowMetadata;
getWorkflowStaticData(type: string): IDataObject;
helpers: {
[key: string]: (...args: any[]) => any //tslint:disable-line:no-any
@@ -287,6 +302,7 @@ export interface IWebhookFunctions {
getCredentials(type: string): ICredentialDataDecryptedObject | undefined;
getHeaderData(): object;
getMode(): WorkflowExecuteMode;
getNode(): INode;
getNodeParameter(parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object; //tslint:disable-line:no-any
getNodeWebhookUrl: (name: string) => string | undefined;
getQueryData(): object;
@@ -295,6 +311,7 @@ export interface IWebhookFunctions {
getTimezone(): string;
getWebhookName(): string;
getWorkflowStaticData(type: string): IDataObject;
getWorkflow(workflow: Workflow): IWorkflowMetadata;
prepareOutputData(outputData: INodeExecutionData[], outputIndex?: number): Promise<INodeExecutionData[][]>;
helpers: {
[key: string]: (...args: any[]) => any //tslint:disable-line:no-any
@@ -534,7 +551,7 @@ export interface IWebhookData {
node: string;
path: string;
webhookDescription: IWebhookDescription;
workflow: Workflow;
workflowId: string;
workflowExecuteAdditionalData: IWorkflowExecuteAdditionalData;
}
@@ -554,8 +571,18 @@ export interface IWorkflowDataProxyData {
$binary: any; // tslint:disable-line:no-any
$data: any; // tslint:disable-line:no-any
$env: any; // tslint:disable-line:no-any
$evaluateExpression: any; // tslint:disable-line:no-any
$item: any; // tslint:disable-line:no-any
$json: any; // tslint:disable-line:no-any
$node: any; // tslint:disable-line:no-any
$parameter: any; // tslint:disable-line:no-any
$workflow: any; // tslint:disable-line:no-any
}
export interface IWorkflowMetadata {
id?: number | string;
name?: string;
active: boolean;
}
export type WebhookHttpMethod = 'GET' | 'POST';

View File

@@ -282,10 +282,10 @@ export function displayParameter(nodeValues: INodeParameters, parameter: INodePr
for (const propertyName of Object.keys(parameter.displayOptions.show)) {
if (propertyName.charAt(0) === '/') {
// Get the value from the root of the node
value = nodeValuesRoot[propertyName.slice(1)];
value = get(nodeValuesRoot, propertyName.slice(1));
} else {
// Get the value from current level
value = nodeValues[propertyName];
value = get(nodeValues, propertyName);
}
if (value === undefined || !parameter.displayOptions.show[propertyName].includes(value as string)) {
@@ -299,10 +299,10 @@ export function displayParameter(nodeValues: INodeParameters, parameter: INodePr
for (const propertyName of Object.keys(parameter.displayOptions.hide)) {
if (propertyName.charAt(0) === '/') {
// Get the value from the root of the node
value = nodeValuesRoot[propertyName.slice(1)];
value = get(nodeValuesRoot, propertyName.slice(1));
} else {
// Get the value from current level
value = nodeValues[propertyName];
value = get(nodeValues, propertyName);
}
if (value !== undefined && parameter.displayOptions.hide[propertyName].includes(value as string)) {
return false;
@@ -475,7 +475,7 @@ export function getParamterResolveOrder(nodePropertiesArray: INodeProperties[],
}
if (itterations > lastIndexReduction + nodePropertiesArray.length) {
throw new Error('Could not resolve parameter depenencies!');
throw new Error('Could not resolve parameter depenencies. Max itterations got reached!');
}
lastIndexLength = indexToResolve.length;
}
@@ -771,7 +771,7 @@ export function getNodeWebhooks(workflow: Workflow, node: INode, additionalData:
node: node.name,
path,
webhookDescription,
workflow,
workflowId: workflow.id,
workflowExecuteAdditionalData: additionalData,
});
}

View File

@@ -41,6 +41,7 @@ tmpl.tmpl.errorHandler = () => { };
export class Workflow {
id: string | undefined;
name: string | undefined;
nodes: INodes = {};
connectionsBySourceNode: IConnections;
connectionsByDestinationNode: IConnections;
@@ -52,15 +53,17 @@ export class Workflow {
// ids of registred webhooks of nodes
staticData: IDataObject;
constructor(id: string | undefined, nodes: INode[], connections: IConnections, active: boolean, nodeTypes: INodeTypes, staticData?: IDataObject, settings?: IWorkflowSettings) {
this.id = id;
this.nodeTypes = nodeTypes;
// constructor(id: string | undefined, nodes: INode[], connections: IConnections, active: boolean, nodeTypes: INodeTypes, staticData?: IDataObject, settings?: IWorkflowSettings) {
constructor(parameters: {id?: string, name?: string, nodes: INode[], connections: IConnections, active: boolean, nodeTypes: INodeTypes, staticData?: IDataObject, settings?: IWorkflowSettings}) {
this.id = parameters.id;
this.name = parameters.name;
this.nodeTypes = parameters.nodeTypes;
// Save nodes in workflow as object to be able to get the
// nodes easily by its name.
// Also directly add the default values of the node type.
let nodeType: INodeType | undefined;
for (const node of nodes) {
for (const node of parameters.nodes) {
this.nodes[node.name] = node;
nodeType = this.nodeTypes.getByName(node.type);
@@ -77,16 +80,16 @@ export class Workflow {
const nodeParameters = NodeHelpers.getNodeParameters(nodeType.description.properties, node.parameters, true, false);
node.parameters = nodeParameters !== null ? nodeParameters : {};
}
this.connectionsBySourceNode = connections;
this.connectionsBySourceNode = parameters.connections;
// Save also the connections by the destionation nodes
this.connectionsByDestinationNode = this.__getConnectionsByDestination(connections);
this.connectionsByDestinationNode = this.__getConnectionsByDestination(parameters.connections);
this.active = active || false;
this.active = parameters.active || false;
this.staticData = ObservableObject.create(staticData || {}, undefined, { ignoreEmptyOnFirstChild: true });
this.staticData = ObservableObject.create(parameters.staticData || {}, undefined, { ignoreEmptyOnFirstChild: true });
this.settings = settings || {};
this.settings = parameters.settings || {};
}
@@ -153,7 +156,8 @@ export class Workflow {
* @memberof Workflow
*/
convertObjectValueToString(value: object): string {
return `[Object: ${JSON.stringify(value)}]`;
const typeName = Array.isArray(value) ? 'Array' : 'Object';
return `[${typeName}: ${JSON.stringify(value)}]`;
}
@@ -732,6 +736,40 @@ export class Workflow {
/**
* Returns from which of the given nodes the workflow should get started from
*
* @param {string[]} nodeNames The potential start nodes
* @returns {(INode | undefined)}
* @memberof Workflow
*/
__getStartNode(nodeNames: string[]): INode | undefined {
// Check if there are any trigger or poll nodes and then return the first one
let node: INode;
let nodeType: INodeType;
for (const nodeName of nodeNames) {
node = this.nodes[nodeName];
nodeType = this.nodeTypes.getByName(node.type) as INodeType;
if (nodeType.trigger !== undefined || nodeType.poll !== undefined) {
return node;
}
}
// Check if there is the actual "start" node
const startNodeType = 'n8n-nodes-base.start';
for (const nodeName of nodeNames) {
node = this.nodes[nodeName];
if (node.type === startNodeType) {
return node;
}
}
return undefined;
}
/**
* Returns the start node to start the worfklow from
*
@@ -740,7 +778,6 @@ export class Workflow {
* @memberof Workflow
*/
getStartNode(destinationNode?: string): INode | undefined {
const startNodeType = 'n8n-nodes-base.start';
if (destinationNode) {
// Find the highest parent nodes of the given one
@@ -753,42 +790,17 @@ export class Workflow {
}
// Check which node to return as start node
// Check if there are any trigger or poll nodes and then return the first one
let node: INode;
let nodeType: INodeType;
for (const nodeName of nodeNames) {
node = this.nodes[nodeName];
nodeType = this.nodeTypes.getByName(node.type) as INodeType;
if (nodeType.trigger !== undefined || nodeType.poll !== undefined) {
return node;
}
}
// Check if there is the actual "start" node
for (const nodeName of nodeNames) {
node = this.nodes[nodeName];
if (node.type === startNodeType) {
return node;
}
const node = this.__getStartNode(nodeNames);
if (node !== undefined) {
return node;
}
// If none of the above did find anything simply return the
// first parent node in the list
return this.nodes[nodeNames[0]];
} else {
// No node given so start from "start" node
let node: INode;
for (const nodeName of Object.keys(this.nodes)) {
node = this.nodes[nodeName];
if (node.type === startNodeType) {
return node;
}
}
}
return undefined;
return this.__getStartNode(Object.keys(this.nodes));
}
@@ -886,62 +898,26 @@ export class Workflow {
// Generate a data proxy which allows to query workflow data
const dataProxy = new WorkflowDataProxy(this, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData);
const data = dataProxy.getDataProxy();
data.$evaluateExpression = (expression: string) => {
return this.resolveSimpleParameterValue('=' + expression, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, returnObjectAsString);
};
// Execute the expression
try {
const returnValue = tmpl.tmpl(parameterValue, dataProxy.getDataProxy());
if (typeof returnValue === 'object' && Object.keys(returnValue).length === 0) {
// When expression is incomplete it returns a Proxy which causes problems.
// Catch it with this code and return a proper error.
throw new Error('Expression is not valid.');
const returnValue = tmpl.tmpl(parameterValue, data);
if (returnValue !== null && typeof returnValue === 'object') {
if (returnObjectAsString === true) {
return this.convertObjectValueToString(returnValue);
}
}
if (returnObjectAsString === true && typeof returnValue === 'object') {
return this.convertObjectValueToString(returnValue);
}
return returnValue;
} catch (e) {
throw new Error('Expression is not valid.');
throw new Error(`Expression is not valid: ${e.message}`);
}
}
/**
* Executes the hooks of the node
*
* @param {string} hookName The name of the hook to execute
* @param {IWebhookData} webhookData
* @param {INodeExecuteFunctions} nodeExecuteFunctions
* @param {WorkflowExecuteMode} mode
* @returns {Promise<void>}
* @memberof Workflow
*/
async runNodeHooks(hookName: string, webhookData: IWebhookData, nodeExecuteFunctions: INodeExecuteFunctions, mode: WorkflowExecuteMode): Promise<void> {
const node = this.getNode(webhookData.node) as INode;
const nodeType = this.nodeTypes.getByName(node.type) as INodeType;
if (nodeType.description.hooks === undefined) {
return;
}
if (nodeType.description.hooks[hookName] === undefined) {
return;
}
if (nodeType.hooks === undefined && nodeType.description.hooks[hookName]!.length !== 0) {
// There should be hook functions but they do not exist
throw new Error('There are hooks defined to run but are not implemented.');
}
for (const hookDescription of nodeType.description.hooks[hookName]!) {
const thisArgs = nodeExecuteFunctions.getExecuteHookFunctions(this, node, webhookData.workflowExecuteAdditionalData, mode);
await nodeType.hooks![hookDescription.method].call(thisArgs);
}
}
/**
* Executes the Webhooks method of the node

View File

@@ -127,7 +127,7 @@ export class WorkflowDataProxy {
get(target, name, receiver) {
name = name.toString();
if (['binary', 'data'].includes(name)) {
if (['binary', 'data', 'json'].includes(name)) {
let executionData: INodeExecutionData[];
if (shortSyntax === false) {
// Long syntax got used to return data from node in path
@@ -180,7 +180,7 @@ export class WorkflowDataProxy {
throw new Error(`No data found for item-index: "${that.itemIndex}"`);
}
if (name === 'data') {
if (['data', 'json'].includes(name as string)) {
// JSON-Data
return executionData[that.itemIndex].json;
} else if (name === 'binary') {
@@ -240,6 +240,35 @@ export class WorkflowDataProxy {
/**
* Returns a proxt to query data from the workflow
*
* @private
* @returns
* @memberof WorkflowDataProxy
*/
private workflowGetter() {
const allowedValues = [
'active',
'id',
'name',
];
const that = this;
return new Proxy({}, {
get(target, name, receiver) {
if (!allowedValues.includes(name.toString())) {
throw new Error(`The key "${name.toString()}" is not supported!`);
}
// @ts-ignore
return that.workflow[name.toString()];
}
});
}
/**
* Returns a proxy to query data of all nodes
*
@@ -271,19 +300,22 @@ export class WorkflowDataProxy {
$binary: {}, // Placeholder
$data: {}, // Placeholder
$env: this.envGetter(),
$evaluateExpression: (expression: string) => { }, // Placeholder
$item: (itemIndex: number) => {
const dataProxy = new WorkflowDataProxy(this.workflow, this.runExecutionData, this.runIndex, itemIndex, this.activeNodeName, this.connectionInputData);
return dataProxy.getDataProxy();
},
$json: {}, // Placeholder
$node: this.nodeGetter(),
$parameter: this.nodeParameterGetter(this.activeNodeName),
$workflow: this.workflowGetter(),
};
return new Proxy(base, {
get(target, name, receiver) {
if (name === '$data') {
if (['$data', '$json'].includes(name as string)) {
// @ts-ignore
return that.nodeDataGetter(that.activeNodeName, true).data;
return that.nodeDataGetter(that.activeNodeName, true).json;
} else if (name === '$binary') {
// @ts-ignore
return that.nodeDataGetter(that.activeNodeName, true).binary;

View File

@@ -138,7 +138,7 @@ describe('Workflow', () => {
];
const nodeTypes = Helpers.NodeTypes();
const workflow = new Workflow(undefined, [], {}, false, nodeTypes);
const workflow = new Workflow({ nodes: [], connections: {}, active: false, nodeTypes });
for (const testData of tests) {
test(testData.description, () => {
@@ -565,7 +565,7 @@ describe('Workflow', () => {
executeNodes.push(createNodeData(node));
}
workflow = new Workflow(undefined, executeNodes, testData.input.connections as IConnections, false, nodeTypes);
workflow = new Workflow({ nodes: executeNodes, connections: testData.input.connections as IConnections, active: false, nodeTypes });
workflow.renameNode(testData.input.currentName, testData.input.newName);
resultNodes = {};
@@ -1039,7 +1039,7 @@ describe('Workflow', () => {
}
};
const workflow = new Workflow(undefined, nodes, connections, false, nodeTypes);
const workflow = new Workflow({ nodes, connections, active: false, nodeTypes });
const activeNodeName = testData.input.hasOwnProperty('Node3') ? 'Node3' : 'Node2';
const runExecutionData: IRunExecutionData = {
@@ -1126,7 +1126,7 @@ describe('Workflow', () => {
// };
// const nodeTypes = Helpers.NodeTypes();
// const workflow = new Workflow(nodes, connections, false, nodeTypes);
// const workflow = new Workflow({ nodes, connections, active: false, nodeTypes });
// const activeNodeName = 'Node2';
// // @ts-ignore
@@ -1193,7 +1193,7 @@ describe('Workflow', () => {
];
const connections: IConnections = {};
const workflow = new Workflow(undefined, nodes, connections, false, nodeTypes);
const workflow = new Workflow({ nodes, connections, active: false, nodeTypes });
const activeNodeName = 'Node1';
const runExecutionData: IRunExecutionData = {