import {
	IExecuteFunctions,
} from 'n8n-core';
import {
	IDataObject,
	ILoadOptionsFunctions,
	INodePropertyOptions,
	INodeTypeDescription,
	INodeExecutionData,
	INodeType,
} from 'n8n-workflow';

import {
	asanaApiRequest,
} from './GenericFunctions';

export class Asana implements INodeType {
	description: INodeTypeDescription = {
		displayName: 'Asana',
		name: 'asana',
		icon: 'file:asana.png',
		group: ['input'],
		version: 1,
		subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
		description: 'Access and edit Asana tasks',
		defaults: {
			name: 'Asana',
			color: '#339922',
		},
		inputs: ['main'],
		outputs: ['main'],
		credentials: [
			{
				name: 'asanaApi',
				required: true,
			}
		],
		properties: [
			{
				displayName: 'Resource',
				name: 'resource',
				type: 'options',
				options: [
					{
						name: 'Task',
						value: 'task',
					},
					{
						name: 'User',
						value: 'user',
					},
				],
				default: 'task',
				description: 'The resource to operate on.',
			},

			// ----------------------------------
			//         task
			// ----------------------------------
			{
				displayName: 'Operation',
				name: 'operation',
				type: 'options',
				displayOptions: {
					show: {
						resource: [
							'task',
						],
					},
				},
				options: [
					{
						name: 'Create',
						value: 'create',
						description: 'Create a task',
					},
					{
						name: 'Delete',
						value: 'delete',
						description: 'Delete a task',
					},
					{
						name: 'Get',
						value: 'get',
						description: 'Get data of task',
					},
					{
						name: 'Update',
						value: 'update',
						description: 'Update a task',
					},
					{
						name: 'Search',
						value: 'search',
						description: 'Search for tasks',
					},
				],
				default: 'create',
				description: 'The operation to perform.',
			},

			// ----------------------------------
			//         task:create
			// ----------------------------------
			{
				displayName: 'Workspace',
				name: 'workspace',
				type: 'options',
				typeOptions: {
					loadOptionsMethod: 'getWorkspaces',
				},
				options: [],
				default: '',
				required: true,
				displayOptions: {
					show: {
						operation: [
							'create',
						],
						resource: [
							'task',
						],
					},
				},
				description: 'The workspace to create the task in',
			},
			{
				displayName: 'Name',
				name: 'name',
				type: 'string',
				default: '',
				required: true,
				displayOptions: {
					show: {
						operation: [
							'create',
						],
						resource: [
							'task',
						],
					},
				},
				description: 'The name of the task to create',
			},

			// ----------------------------------
			//         delete
			// ----------------------------------
			{
				displayName: 'Task ID',
				name: 'id',
				type: 'string',
				default: '',
				required: true,
				displayOptions: {
					show: {
						operation: [
							'delete',
						],
						resource: [
							'task',
						],
					},
				},
				description: 'The ID of the task to delete.',
			},

			// ----------------------------------
			//         get
			// ----------------------------------
			{
				displayName: 'Task ID',
				name: 'id',
				type: 'string',
				default: '',
				required: true,
				displayOptions: {
					show: {
						operation: [
							'get',
						],
						resource: [
							'task',
						],
					},
				},
				description: 'The ID of the task to get the data of.',
			},

			// ----------------------------------
			//         update
			// ----------------------------------
			{
				displayName: 'Task ID',
				name: 'id',
				type: 'string',
				default: '',
				required: true,
				displayOptions: {
					show: {
						operation: [
							'update',
						],
						resource: [
							'task',
						],
					},
				},
				description: 'The ID of the task to update the data of.',
			},

			// ----------------------------------
			//         search
			// ----------------------------------
			{
				displayName: 'Workspace',
				name: 'workspace',
				type: 'options',
				typeOptions: {
					loadOptionsMethod: 'getWorkspaces',
				},
				options: [],
				default: '',
				required: true,
				displayOptions: {
					show: {
						operation: [
							'search',
						],
						resource: [
							'task',
						],
					},
				},
				description: 'The workspace to create the task in',
			},
			{
				displayName: 'Search Properties',
				name: 'searchTaskProperties',
				type: 'collection',
				displayOptions: {
					show: {
						operation: [
							'search',
						],
						resource: [
							'task',
						],
					},
				},
				default: {},
				description: 'Properties to search for',
				placeholder: 'Add Search Property',
				options: [
					// TODO: Add "assignee" and "assignee_status"
					{
						displayName: 'Text',
						name: 'text',
						type: 'string',
						typeOptions: {
							alwaysOpenEditWindow: true,
							rows: 5,
						},
						default: '',
						description: 'Text to search for in name or notes.',
					},
					{
						displayName: 'Completed',
						name: 'completed',
						type: 'boolean',
						default: false,
						description: 'If the task is marked completed.',
					},
				],
			},

			// ----------------------------------
			//         create/update
			// ----------------------------------
			{
				displayName: 'Other Properties',
				name: 'otherProperties',
				type: 'collection',
				displayOptions: {
					show: {
						resource: [
							'task',
						],
						operation: [
							'create',
							'update',
						],
					},
				},
				default: {},
				description: 'Other properties to set',
				placeholder: 'Add Property',
				options: [
					{
						displayName: 'Name',
						name: 'name',
						type: 'string',
						default: '',
						displayOptions: {
							show: {
								'/operation': [
									'update',
								],
							},
						},
						description: 'The new name of the task',
					},
					// TODO: Add "assignee" and "assignee_status"
					{
						displayName: 'Notes',
						name: 'notes',
						type: 'string',
						typeOptions: {
							alwaysOpenEditWindow: true,
							rows: 5,
						},
						default: '',
						description: 'The task notes',
					},
					{
						displayName: 'Completed',
						name: 'completed',
						type: 'boolean',
						default: false,
						description: 'If the task should be marked completed.',
					},
					{
						displayName: 'Due On',
						name: 'due_on',
						type: 'dateTime',
						default: '',
						description: 'Date on which the time is due.',
					},
					{
						displayName: 'Liked',
						name: 'liked',
						type: 'boolean',
						default: false,
						description: 'If the task is liked by the authorized user.',
					},
				],
			},

			// ----------------------------------
			//         user
			// ----------------------------------
			{
				displayName: 'Operation',
				name: 'operation',
				type: 'options',
				displayOptions: {
					show: {
						resource: [
							'user',
						],
					},
				},
				options: [
					{
						name: 'Get All',
						value: 'getAll',
						description: 'Data of all users',
					},
					{
						name: 'Get',
						value: 'get',
						description: 'Get data of user',
					},
				],
				default: 'get',
				description: 'The operation to perform.',
			},

			// ----------------------------------
			//         user:get
			// ----------------------------------
			{
				displayName: 'Id',
				name: 'userId',
				type: 'string',
				default: '',
				required: true,
				displayOptions: {
					show: {
						operation: [
							'get',
						],
						resource: [
							'user',
						],
					},
				},
				description: 'An identifier for the user to get data of. Can be one of an
email address,the globally unique identifier for the user,
or the keyword me to indicate the current user making the request.',
			},

			// ----------------------------------
			//         user:getAll
			// ----------------------------------
			{
				displayName: 'Workspace',
				name: 'workspace',
				type: 'options',
				typeOptions: {
					loadOptionsMethod: 'getWorkspaces',
				},
				options: [],
				default: '',
				required: true,
				displayOptions: {
					show: {
						operation: [
							'getAll',
						],
						resource: [
							'user',
						],
					},
				},
				description: 'The workspace in which to get users.',
			},
		],
	};

	methods = {
		loadOptions: {
			// Get all the available workspaces to display them to user so that he can
			// select them easily
			async getWorkspaces(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
				const endpoint = 'workspaces';
				const responseData = await asanaApiRequest.call(this, 'GET', endpoint, {});

				if (responseData.data === undefined) {
					throw new Error('No data got returned');
				}

				const returnData: INodePropertyOptions[] = [];
				for (const workspaceData of responseData.data) {
					if (workspaceData.resource_type !== 'workspace') {
						// Not sure if for some reason also ever other resources
						// get returned but just in case filter them out
						continue;
					}

					returnData.push({
						name: workspaceData.name,
						value: workspaceData.gid,
					});
				}

				return returnData;
			}
		},
	};

	async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
		const items = this.getInputData();
		const returnData: IDataObject[] = [];

		const credentials = this.getCredentials('asanaApi');

		if (credentials === undefined) {
			throw new Error('No credentials got returned!');
		}

		const resource = this.getNodeParameter('resource', 0) as string;
		const operation = this.getNodeParameter('operation', 0) as string;

		let endpoint = '';
		let requestMethod = '';

		let body: IDataObject;
		let qs: IDataObject;

		for (let i = 0; i < items.length; i++) {
			body = {};
			qs = {};

			if (resource === 'task') {
				if (operation === 'create') {
					// ----------------------------------
					//         create
					// ----------------------------------

					requestMethod = 'POST';
					endpoint = 'tasks';

					body.name = this.getNodeParameter('name', i) as string;
					// body.notes = this.getNodeParameter('taskNotes', 0) as string;
					body.workspace = this.getNodeParameter('workspace', i) as string;

					const otherProperties = this.getNodeParameter('otherProperties', i) as IDataObject;
					Object.assign(body, otherProperties);

				} else if (operation === 'delete') {
					// ----------------------------------
					//         delete
					// ----------------------------------

					requestMethod = 'DELETE';
					endpoint = 'tasks/' + this.getNodeParameter('id', i) as string;

				} else if (operation === 'get') {
					// ----------------------------------
					//         get
					// ----------------------------------

					requestMethod = 'GET';
					endpoint = 'tasks/' + this.getNodeParameter('id', i) as string;

				} else if (operation === 'update') {
					// ----------------------------------
					//         update
					// ----------------------------------

					requestMethod = 'PUT';
					endpoint = 'tasks/' + this.getNodeParameter('id', i) as string;

					const otherProperties = this.getNodeParameter('otherProperties', i) as IDataObject;
					Object.assign(body, otherProperties);

				} else if (operation === 'search') {
					// ----------------------------------
					//         search
					// ----------------------------------

					const workspaceId = this.getNodeParameter('workspace', i) as string;

					requestMethod = 'GET';
					endpoint = `workspaces/${workspaceId}/tasks/search`;

					const searchTaskProperties = this.getNodeParameter('searchTaskProperties', i) as IDataObject;
					Object.assign(qs, searchTaskProperties);

				} else {
					throw new Error(`The operation "${operation}" is not known!`);
				}
			} else if (resource === 'user') {
				if (operation === 'get') {
					// ----------------------------------
					//         get
					// ----------------------------------

					const userId = this.getNodeParameter('userId', i) as string;

					requestMethod = 'GET';
					endpoint = `users/${userId}`;

				} else if (operation === 'getAll') {
					// ----------------------------------
					//         getAll
					// ----------------------------------

					const workspaceId = this.getNodeParameter('workspace', i) as string;

					requestMethod = 'GET';
					endpoint = `workspaces/${workspaceId}/users`;

				} else {
					throw new Error(`The operation "${operation}" is not known!`);
				}
			} else {
				throw new Error(`The resource "${resource}" is not known!`);
			}
			const responseData = await asanaApiRequest.call(this, requestMethod, endpoint, body);

			returnData.push(responseData.data as IDataObject);
		}

		return [this.helpers.returnJsonArray(returnData)];
	}
}


import mixins from 'vue-typed-mixins';
export default mixins(
copyPaste,
workflowHelpers,
)
.extend({
name: 'NodeWebhooks',
props: [

keep keep keep keep add keep keep keep keep keep

<template>

	<expression-edit :dialogVisible="expressionEditDialogVisible" :value="value" :parameter="parameter" :path="path" @closeDialog="closeExpressionEditDialog" @valueChanged="expressionUpdated"></expression-edit>

			<text-edit :dialogVisible="textEditDialogVisible" :value="value" :parameter="parameter" @closeDialog="closeTextEditDialog" @valueChanged="expressionUpdated"></text-edit>

				<prism-editor v-if="!codeEditDialogVisible" :lineNumbers="true" :readonly="true" :code="displayValue" language="js"></prism-editor>

			<el-input v-else ref="inputField" size="small" :type="getStringInputType" :rows="getArgument('rows')" :value="displayValue" :disabled="isReadOnly" @change="valueChanged" @keydown.stop @focus="setFocus" :title="displayTitle" :placeholder="isValueExpression?'':parameter.placeholder">

			</el-input>

			<el-date-picker
				v-model="tempValue"
				ref="inputField"
				type="datetime"
				size="small"
				:value="displayValue"
				:title="displayTitle"
				:disabled="isReadOnly"
				:placeholder="parameter.placeholder?parameter.placeholder:'Select date and time'"
				:picker-options="dateTimePickerOptions"
				@change="valueChanged"
				@focus="setFocus"
				@keydown.stop
			>
			</el-date-picker>

			<el-input-number ref="inputField" size="small" :value="displayValue" :max="getArgument('maxValue')" :min="getArgument('minValue')" :precision="getArgument('numberPrecision')" :step="getArgument('numberStepSize')" :disabled="isReadOnly" @change="valueChanged" @focus="setFocus" @keydown.stop :title="displayTitle" :placeholder="parameter.placeholder"></el-input-number>

		<el-select
			v-else-if="parameter.type === 'options'"
			ref="inputField"
			size="small"
			filterable
			:value="displayValue"
			:loading="remoteParameterOptionsLoading"
			:disabled="isReadOnly || remoteParameterOptionsLoading"
			:title="displayTitle"
			@change="valueChanged"
			@keydown.stop
			@focus="setFocus"
		>
			<el-option
				v-for="option in parameterOptions"
				:value="option.value"
				:key="option.value"
				:label="option.name"
			>

{{ option.name }}

			</el-option>
		</el-select>

		<el-select
			v-else-if="parameter.type === 'multiOptions'"
			ref="inputField"
			size="small"
			filterable
			multiple
			:value="displayValue"
			:loading="remoteParameterOptionsLoading"
			:disabled="isReadOnly || remoteParameterOptionsLoading"
			@change="valueChanged"
			@keydown.stop
			@focus="setFocus"
			:title="displayTitle"
		>
			<el-option v-for="option in parameterOptions" :value="option.value" :key="option.value" :label="option.name" >

{{ option.name }}

			</el-option>
		</el-select>

			<el-color-picker :value="displayValue" :disabled="isReadOnly" @change="valueChanged" size="small" class="color-picker" @focus="setFocus" :title="displayTitle" ></el-color-picker>
			<el-input size="small" type="text" :value="displayValue" :disabled="isReadOnly" @change="valueChanged" @keydown.stop @focus="setFocus" :title="displayTitle" ></el-input>

			<el-switch ref="inputField" :value="displayValue" @change="valueChanged" active-color="#13ce66" :disabled="isValueExpression || isReadOnly"></el-switch>

Edit Expression

			<el-dropdown trigger="click" @command="optionSelected" size="mini">

				<el-dropdown-menu slot="dropdown">
					<el-dropdown-item command="addExpression" v-if="parameter.noDataExpression !== true && !isValueExpression">Add Expression</el-dropdown-item>
					<el-dropdown-item command="removeExpression" v-if="parameter.noDataExpression !== true && isValueExpression">Remove Expression</el-dropdown-item>
					<el-dropdown-item command="resetValue" :disabled="isDefault" divided>Reset Value</el-dropdown-item>
				</el-dropdown-menu>
			</el-dropdown>

		<el-tooltip placement="top" effect="light">

		</el-tooltip>

</template>


import {
	IActivationError,
	Db,
	NodeTypes,
	IResponseCallbackData,
	IWorkflowDb,
	IWorkflowExecutionDataProcess,
	ResponseHelper,
	WebhookHelpers,
	WorkflowCredentials,
	WorkflowHelpers,
	WorkflowRunner,
	WorkflowExecuteAdditionalData,
} from './';

import {
	ActiveWorkflows,
	ActiveWebhooks,
	NodeExecuteFunctions,
} from 'n8n-core';

import {
	IExecuteData,
	IGetExecuteTriggerFunctions,
	INode,
	INodeExecutionData,
	IRunExecutionData,
	IWebhookData,
	IWorkflowExecuteAdditionalData as IWorkflowExecuteAdditionalDataWorkflow,
	WebhookHttpMethod,
	Workflow,
	WorkflowExecuteMode,
} from 'n8n-workflow';

import * as express from 'express';

export class ActiveWorkflowRunner {
	private activeWorkflows: ActiveWorkflows | null = null;
	private activeWebhooks: ActiveWebhooks | null = null;
	private activationErrors: {
		[key: string]: IActivationError;
	} = {};

	async init() {
		// Get the active workflows from database
		const workflowsData: IWorkflowDb[] = await Db.collections.Workflow!.find({ active: true }) as IWorkflowDb[];

		this.activeWebhooks = new ActiveWebhooks();

		// Add them as active workflows
		this.activeWorkflows = new ActiveWorkflows();

		if (workflowsData.length !== 0) {
			console.log('\n ================================');
			console.log('   Start Active Workflows:');
			console.log(' ================================');

			for (const workflowData of workflowsData) {
				console.log(`   - ${workflowData.name}`);
				try {
					await this.add(workflowData.id.toString(), workflowData);
					console.log(`     => Started`);
				} catch (error) {
					console.log(`     => ERROR: Workflow could not be activated:`);
					console.log(`               ${error.message}`);
				}
			}
		}
	}

	/**
	 * Removes all the currently active workflows
	 *
	 * @returns {Promise<void>}
	 * @memberof ActiveWorkflowRunner
	 */
	async removeAll(): Promise<void> {
		if (this.activeWorkflows === null) {
			return;
		}

		const activeWorkflows = this.activeWorkflows.allActiveWorkflows();

		const removePromises = [];
		for (const workflowId of activeWorkflows) {
			removePromises.push(this.remove(workflowId));
		}

		await Promise.all(removePromises);
		return;
	}

	/**
	 * Checks if a webhook for the given method and path exists and executes the workflow.
	 *
	 * @param {WebhookHttpMethod} httpMethod
	 * @param {string} path
	 * @param {express.Request} req
	 * @param {express.Response} res
	 * @returns {Promise<object>}
	 * @memberof ActiveWorkflowRunner
	 */
	async executeWebhook(httpMethod: WebhookHttpMethod, path: string, req: express.Request, res: express.Response): Promise<IResponseCallbackData> {
		if (this.activeWorkflows === null) {
			throw new ResponseHelper.ResponseError('The "activeWorkflows" instance did not get initialized yet.', 404, 404);
		}

		const webhookData: IWebhookData | undefined = this.activeWebhooks!.get(httpMethod, path);

		if (webhookData === undefined) {
			// The requested webhook is not registred
			throw new ResponseHelper.ResponseError('The requested webhook is not registred.', 404, 404);
		}

		// Get the node which has the webhook defined to know where to start from and to
		// get additional data
		const workflowStartNode = webhookData.workflow.getNode(webhookData.node);
		if (workflowStartNode === null) {
			throw new ResponseHelper.ResponseError('Could not find node to process webhook.', 404, 404);
		}
		const executionMode = 'webhook';

		const workflowData = await Db.collections.Workflow!.findOne(webhookData.workflow.id!);

		if (workflowData === undefined) {
			throw new ResponseHelper.ResponseError(`Could not find workflow with id "${webhookData.workflow.id}"`, 404, 404);
		}

		return new Promise((resolve, reject) => {
			WebhookHelpers.executeWebhook(webhookData, workflowData, workflowStartNode, executionMode, undefined, req, res, (error: Error | null, data: object) => {
				if (error !== null) {
					return reject(error);
				}
				resolve(data);
			});
		});
	}

	/**
	 * Returns the ids of the currently active workflows
	 *
	 * @returns {string[]}
	 * @memberof ActiveWorkflowRunner
	 */
	getActiveWorkflows(): string[] {
		if (this.activeWorkflows === null) {
			return [];
		}

		return this.activeWorkflows.allActiveWorkflows();
	}

	/**
	 * Returns if the workflow is active
	 *
	 * @param {string} id The id of the workflow to check
	 * @returns {boolean}
	 * @memberof ActiveWorkflowRunner
	 */
	isActive(id: string): boolean {
		if (this.activeWorkflows !== null) {
			return this.activeWorkflows.isActive(id);
		}

		return false;
	}

	/**
	 * Return error if there was a problem activating the workflow
	 *
	 * @param {string} id The id of the workflow to return the error of
	 * @returns {(IActivationError | undefined)}
	 * @memberof ActiveWorkflowRunner
	 */
	getActivationError(id: string): IActivationError | undefined {
		if (this.activationErrors[id] === undefined) {
			return undefined;
		}

		return this.activationErrors[id];
	}

	/**
	 * Adds all the webhooks of the workflow
	 *
	 * @param {Workflow} workflow
	 * @param {IWorkflowExecuteAdditionalDataWorkflow} additionalData
	 * @param {WorkflowExecuteMode} mode
	 * @returns {Promise<void>}
	 * @memberof ActiveWorkflowRunner
	 */
	async addWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode): Promise<void> {
		const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData);

		for (const webhookData of webhooks) {
			await this.activeWebhooks!.add(webhookData, mode);
		}
	}

	/**
	 * Remove all the webhooks of the workflow
	 *
	 * @param {string} workflowId
	 * @returns
	 * @memberof ActiveWorkflowRunner
	 */
	removeWorkflowWebhooks(workflowId: string): Promise<boolean> {
		return this.activeWebhooks!.removeByWorkflowId(workflowId);
	}

	/**
	 * Return trigger function which gets the global functions from n8n-core
	 * and overwrites the emit to be able to start it in subprocess
	 *
	 * @param {IWorkflowDb} workflowData
	 * @param {IWorkflowExecuteAdditionalDataWorkflow} additionalData
	 * @param {WorkflowExecuteMode} mode
	 * @returns {IGetExecuteTriggerFunctions}
	 * @memberof ActiveWorkflowRunner
	 */
	getExecuteTriggerFunctions(workflowData: IWorkflowDb, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode): IGetExecuteTriggerFunctions{
		return ((workflow: Workflow, node: INode) => {
			const returnFunctions = NodeExecuteFunctions.getExecuteTriggerFunctions(workflow, node, additionalData, mode);
			returnFunctions.emit = (data: INodeExecutionData[][]): void => {

				const nodeExecutionStack: IExecuteData[] = [
					{
						node,
						data: {
							main: data,
						}
					}
				];

				const executionData: IRunExecutionData = {
					startData: {},
					resultData: {
						runData: {},
					},
					executionData: {
						contextData: {},
						nodeExecutionStack,
						waitingExecution: {},
					},
				};

				// Start the workflow
				const runData: IWorkflowExecutionDataProcess = {
					credentials: additionalData.credentials,
					executionMode: mode,
					executionData,
					workflowData,
				};

				const workflowRunner = new WorkflowRunner();
				workflowRunner.run(runData, true);
			};
			return returnFunctions;
		});
	}

	/**
	 * Makes a workflow active
	 *
	 * @param {string} workflowId The id of the workflow to activate
	 * @param {IWorkflowDb} [workflowData] If workflowData is given it saves the DB query
	 * @returns {Promise<void>}
	 * @memberof ActiveWorkflowRunner
	 */
	async add(workflowId: string, workflowData?: IWorkflowDb): Promise<void> {
		if (this.activeWorkflows === null) {
			throw new Error(`The "activeWorkflows" instance did not get initialized yet.`);
		}

		let workflowInstance: Workflow;
		try {
			if (workflowData === undefined) {
				workflowData = await Db.collections.Workflow!.findOne(workflowId) as IWorkflowDb;
			}

			if (!workflowData) {
				throw new Error(`Could not find workflow with id "${workflowId}".`);
			}
			const nodeTypes = NodeTypes();
			workflowInstance = new Workflow(workflowId, workflowData.nodes, workflowData.connections, workflowData.active, nodeTypes, workflowData.staticData, workflowData.settings);

			const canBeActivated = workflowInstance.checkIfWorkflowCanBeActivated(['n8n-nodes-base.start']);
			if (canBeActivated === false) {
				throw new Error(`The workflow can not be activated because it does not contain any nodes which could start the workflow. Only workflows which have trigger or webhook nodes can be activated.`);
			}

			const mode = 'trigger';
			const credentials = await WorkflowCredentials(workflowData.nodes);
			const additionalData = await WorkflowExecuteAdditionalData.getBase(mode, credentials);
			const getTriggerFunctions = this.getExecuteTriggerFunctions(workflowData, additionalData, mode);

			// Add the workflows which have webhooks defined
			await this.addWorkflowWebhooks(workflowInstance, additionalData, mode);
			await this.activeWorkflows.add(workflowId, workflowInstance, additionalData, getTriggerFunctions);

			if (this.activationErrors[workflowId] !== undefined) {
				// If there were any activation errors delete them
				delete this.activationErrors[workflowId];
			}
		} catch (error) {
			// There was a problem activating the workflow

			// Save the error
			this.activationErrors[workflowId] = {
				time: new Date().getTime(),
				error: {
					message: error.message,
				},
			};

			throw error;
		}

		// If for example webhooks get created it sometimes has to save the
		// id of them in the static data. So make sure that data gets persisted.
		await WorkflowHelpers.saveStaticData(workflowInstance!);
	}

	/**
	 * Makes a workflow inactive
	 *
	 * @param {string} workflowId The id of the workflow to deactivate
	 * @returns {Promise<void>}
	 * @memberof ActiveWorkflowRunner
	 */
	async remove(workflowId: string): Promise<void> {
		if (this.activeWorkflows !== null) {
			const workflowData = this.activeWorkflows.get(workflowId);

			// Remove all the webhooks of the workflow
			await this.removeWorkflowWebhooks(workflowId);

			if (workflowData) {
				// Save the static workflow data if needed
				await WorkflowHelpers.saveStaticData(workflowData.workflow);
			}

			if (this.activationErrors[workflowId] !== undefined) {
				// If there were any activation errors delete them
				delete this.activationErrors[workflowId];
			}

			// Remove the workflow from the "list" of active workflows
			return this.activeWorkflows.remove(workflowId);
		}

		throw new Error(`The "activeWorkflows" instance did not get initialized yet.`);
	}
}

let workflowRunnerInstance: ActiveWorkflowRunner | undefined;

export function getInstance(): ActiveWorkflowRunner {
	if (workflowRunnerInstance === undefined) {
		workflowRunnerInstance = new ActiveWorkflowRunner();
	}

	return workflowRunnerInstance;
}


// From: https://gist.github.com/compulim/8b49b0a744a3eeb2205e2b9506201e50
export interface IDeferredPromise<T> {
	promise: () => Promise<T>;
	reject: (error: Error) => void;
	resolve: (result: T) => void;
}

export function createDeferredPromise<T>(): Promise<IDeferredPromise<T>> {
	return new Promise<IDeferredPromise<T>>(resolveCreate => {
		const promise = new Promise<T>((resolve, reject) => {
			resolveCreate({ promise: () => promise, resolve, reject });
		});
	});
}


// https://docs.cypress.io/api/introduction/api.html

describe('My First Test', () => {
	it('Visits the app root url', () => {
		cy.visit('/');
		cy.contains('h1', 'Welcome to Your Vue.js + TypeScript App');
	});
});


import CodeEdit from '@/components/CodeEdit.vue';

keep keep keep add keep keep keep keep keep

import {
	IConnections,
	ICredentialsDecrypted,
	ICredentialsEncrypted,
	ICredentialType,
	IDataObject,
	GenericValue,
	IWorkflowSettings as IWorkflowSettingsWorkflow,
	INode,
	INodeCredentials,
	INodeIssues,
	INodePropertyOptions,
	INodeTypeDescription,
	IRunExecutionData,
	IRun,
	IRunData,
	ITaskData,
	WorkflowExecuteMode,
} from 'n8n-workflow';

import {
	PaintStyle,
} from 'jsplumb';

declare module 'jsplumb' {
	interface Anchor {
		lastReturnValue: number[];
	}

	interface Connection {
		// bind(event: string, (connection: Connection): void;): void; // tslint:disable-line:no-any
		bind(event: string, callback: Function): void; // tslint:disable-line:no-any
		removeOverlay(name: string): void;
		removeOverlays(): void;
		setParameter(name: string, value: any): void; // tslint:disable-line:no-any
		setPaintStyle(arg0: PaintStyle): void;
		addOverlay(arg0: any[]): void; // tslint:disable-line:no-any
		setConnector(arg0: any[]): void; // tslint:disable-line:no-any
	}

	interface Endpoint {
		getOverlay(name: string): any; // tslint:disable-line:no-any
	}

	interface Overlay {
		setVisible(visible: boolean): void;
	}

	interface OnConnectionBindInfo {
		originalSourceEndpoint: Endpoint;
		originalTargetEndpoint: Endpoint;
		getParameters(): { index: number };
	}
}

// EndpointOptions from jsplumb seems incomplete and wrong so we define an own one
export interface IEndpointOptions {
	anchor?: any; // tslint:disable-line:no-any
	createEndpoint?: boolean;
	dragAllowedWhenFull?: boolean;
	dropOptions?: any; // tslint:disable-line:no-any
	dragProxy?: any; // tslint:disable-line:no-any
	endpoint?: string;
	endpointStyle?: object;
	isSource?: boolean;
	isTarget?: boolean;
	maxConnections?: number;
	overlays?: any; // tslint:disable-line:no-any
	parameters?: any; // tslint:disable-line:no-any
	uuid?: string;
}

export interface IConnectionsUi {
	[key: string]: {
		[key: string]: IEndpointOptions;
	};
}

export interface IUpdateInformation {
	name: string;
	key: string;
	value: string | number; // with null makes problems in NodeSettings.vue
	node?: string;
	oldValue?: string | number;
}

export interface INodeUpdatePropertiesInformation {
	name: string; // Node-Name
	properties: {
		[key: string]: IDataObject;
	};
}

export type XYPositon = [number, number];

export type MessageType = 'success' | 'warning' | 'info' | 'error';

export interface INodeUi extends INode {
	position: XYPositon;
	color?: string;
	notes?: string;
	issues?: INodeIssues;
	_jsPlumb?: {
		endpoints?: {
			[key: string]: IEndpointOptions[];
		};
	};
}

export interface INodeTypesMaxCount {
	[key: string]: {
		exist: number;
		max: number;
		nodeNames: string[];
	};
}

export interface IRestApi {
	getActiveWorkflows(): Promise<string[]>;
	getActivationError(id: string): Promise<IActivationError | undefined >;
	getCurrentExecutions(filter: object): Promise<IExecutionsCurrentSummaryExtended[]>;
	getPastExecutions(filter: object, limit: number, lastId?: string | number): Promise<IExecutionsListResponse>;
	stopCurrentExecution(executionId: string): Promise<IExecutionsStopData>;
	makeRestApiRequest(method: string, endpoint: string, data?: any): Promise<any>; // tslint:disable-line:no-any
	getSettings(): Promise<IN8nUISettings>;
	getNodeTypes(): Promise<INodeTypeDescription[]>;
	getNodeParameterOptions(nodeType: string, methodName: string, credentials?: INodeCredentials): Promise<INodePropertyOptions[]>;
	removeTestWebhook(workflowId: string): Promise<boolean>;
	runWorkflow(runData: IStartRunData): Promise<IExecutionPushResponse>;
	createNewWorkflow(sendData: IWorkflowData): Promise<IWorkflowDb>;
	updateWorkflow(id: string, data: IWorkflowDataUpdate): Promise<IWorkflowDb>;
	deleteWorkflow(name: string): Promise<void>;
	getWorkflow(id: string): Promise<IWorkflowDb>;
	getWorkflows(filter?: object): Promise<IWorkflowShortResponse[]>;
	getWorkflowFromUrl(url: string): Promise<IWorkflowDb>;
	createNewCredentials(sendData: ICredentialsDecrypted): Promise<ICredentialsResponse>;
	deleteCredentials(id: string): Promise<void>;
	updateCredentials(id: string, data: ICredentialsDecrypted): Promise<ICredentialsResponse>;
	getAllCredentials(filter?: object): Promise<ICredentialsResponse[]>;
	getCredentials(id: string, includeData?: boolean): Promise<ICredentialsDecryptedResponse | ICredentialsResponse | undefined>;
	getCredentialTypes(): Promise<ICredentialType[]>;
	getExecution(id: string): Promise<IExecutionResponse>;
	deleteExecutions(sendData: IExecutionDeleteFilter): Promise<void>;
	retryExecution(id: string, loadWorkflow?: boolean): Promise<boolean>;
	getTimezones(): Promise<IDataObject>;
}

export interface IBinaryDisplayData {
	index: number;
	key: string;
	node: string;
	outputIndex: number;
	runIndex: number;
}

export interface IStartRunData {
	workflowData: IWorkflowData;
	startNodes?: string[];
	destinationNode?: string;
	runData?: IRunData;
}

export interface IRunDataUi {
	node?: string;
	workflowData: IWorkflowData;
}

export interface ITableData {
	columns: string[];
	data: GenericValue[][];
}

export interface IVariableItemSelected {
	variable: string;
}

export interface IVariableSelectorOption {
	name: string;
	key?: string;
	value?: string;
	options?: IVariableSelectorOption[] | null;
	allowParentSelect?: boolean;
	dataType?: string;
}

// Simple version of n8n-workflow.Workflow
export interface IWorkflowData {
	id?: string;
	name?: string;
	active?: boolean;
	nodes: INode[];
	connections: IConnections;
	settings?: IWorkflowSettings;
}

export interface IWorkflowDataUpdate {
	name?: string;
	nodes?: INode[];
	connections?: IConnections;
	settings?: IWorkflowSettings;
	active?: boolean;
}

// Almost identical to cli.Interfaces.ts
export interface IWorkflowDb {
	id: string;
	name: string;
	active: boolean;
	createdAt: number | string;
	updatedAt: number | string;
	nodes: INodeUi[];
	connections: IConnections;
	settings?: IWorkflowSettings;
}

// Identical to cli.Interfaces.ts
export interface IWorkflowShortResponse {
	id: string;
	name: string;
	active: boolean;
	createdAt: number | string;
	updatedAt: number | string;
}

// Identical or almost identical to cli.Interfaces.ts

export interface IActivationError {
	time: number;
	error: {
		message: string;
	};
}

export interface ICredentialsResponse extends ICredentialsEncrypted {
	id?: string;
	createdAt: number | string;
	updatedAt: number | string;
}

export interface ICredentialsBase {
	createdAt: number | string;
	updatedAt: number | string;
}

export interface ICredentialsDecryptedResponse extends ICredentialsBase, ICredentialsDecrypted{
	id: string;
}

export interface IExecutionBase {
	id?: number | string;
	finished: boolean;
	mode: WorkflowExecuteMode;
	retryOf?: string;
	retrySuccessId?: string;
	startedAt: Date;
	stoppedAt?: Date;
	workflowId?: string; // To be able to filter executions easily //
}

export interface IExecutionFlatted extends IExecutionBase {
	data: string;
	workflowData: IWorkflowDb;
}

export interface IExecutionFlattedResponse extends IExecutionFlatted {
	id: string;
}

export interface IExecutionPushResponse {
	executionId?: string;
	waitingForWebhook?: boolean;
}

export interface IExecutionResponse extends IExecutionBase {
	id: string;
	data: IRunExecutionData;
	workflowData: IWorkflowDb;
}

export interface IExecutionShortResponse {
	id: string;
	workflowData: {
		id: string;
		name: string;
	};
	mode: WorkflowExecuteMode;
	finished: boolean;
	startedAt: Date;
	stoppedAt: Date;
	executionTime?: number;
}

export interface IExecutionsListResponse {
	count: number;
	results: IExecutionsSummary[];
}

export interface IExecutionsCurrentSummaryExtended {
	id?: string;
	idActive: string;
	finished?: boolean;
	mode: WorkflowExecuteMode;
	retryOf?: string;
	retrySuccessId?: string;
	startedAt: Date;
	stoppedAt?: Date;
	workflowId: string;
	workflowName?: string;
}

export interface IExecutionsStopData {
	finished?: boolean;
	mode: WorkflowExecuteMode;
	startedAt: Date;
	stoppedAt: Date;
}

export interface IExecutionsSummary {
	id?: string; // executionIdDb
	idActive?: string; // executionIdActive
	mode: WorkflowExecuteMode;
	finished?: boolean;
	retryOf?: string;
	retrySuccessId?: string;
	startedAt: Date;
	stoppedAt?: Date;
	workflowId: string;
	workflowName?: string;
}

export interface IExecutionDeleteFilter {
	deleteBefore?: Date;
	filters?: IDataObject;
	ids?: string[];
}

export interface IPushData {
	data: IPushDataExecutionFinished | IPushDataNodeExecuteAfter | IPushDataNodeExecuteBefore | IPushDataTestWebhook;
	type: IPushDataType;
}

export type IPushDataType = 'executionFinished' | 'executionStarted' | 'nodeExecuteAfter' | 'nodeExecuteBefore' | 'testWebhookDeleted' | 'testWebhookReceived';

export interface IPushDataExecutionStarted {
	executionId: string;
	mode: WorkflowExecuteMode;
	startedAt: Date;
	retryOf?: string;
	workflowId: string;
	workflowName?: string;
}

export interface IPushDataExecutionFinished {
	data: IRun;
	executionIdActive: string;
	executionIdDb?: string;
	retryOf?: string;
}

export interface IPushDataExecutionStarted {
	executionId: string;
}

export interface IPushDataNodeExecuteAfter {
	data: ITaskData;
	executionId: string;
	nodeName: string;
}

export interface IPushDataNodeExecuteBefore {
	executionId: string;
	nodeName: string;
}

export interface IPushDataTestWebhook {
	executionId: string;
	workflowId: string;
}

export interface IN8nUISettings {
	endpointWebhook: string;
	endpointWebhookTest: string;
	saveDataErrorExecution: string;
	saveDataSuccessExecution: string;
	saveManualExecutions: boolean;
	timezone: string;
	urlBaseWebhook: string;
	versionCli: string;
}

export interface IWorkflowSettings extends IWorkflowSettingsWorkflow {
	errorWorkflow?: string;
	saveDataErrorExecution?: string;
	saveDataSuccessExecution?: string;
	saveManualExecutions?: boolean;
	timezone?: string;
}


import {
	INodeType,
	INodeTypes,
	INodeTypeData,
} from 'n8n-workflow';

class NodeTypesClass implements INodeTypes {

	nodeTypes: INodeTypeData = {};

	async init(nodeTypes: INodeTypeData): Promise<void> {
		this.nodeTypes = nodeTypes;
	}

	getAll(): INodeType[] {
		return Object.values(this.nodeTypes).map((data) => data.type);
	}

	getByName(nodeType: string): INodeType | undefined {
		return this.nodeTypes[nodeType].type;
	}
}

let nodeTypesInstance: NodeTypesClass | undefined;

export function NodeTypes(): NodeTypesClass {
	if (nodeTypesInstance === undefined) {
		nodeTypesInstance = new NodeTypesClass();
	}

	return nodeTypesInstance;
}


n8n Workflow Automation Tool

echo "Building project..." && rm -rf dist && tsc -b && oclif-dev manifest

automate automation IaaS iPaaS n8n workflow


if (this.node === null || this.remoteMethod === undefined || this.remoteParameterOptionsLoading) {

getResolveNodeParameters (nodeParameters: INodeParameters): INodeParameters {

for (const key of Object.keys(nodeParameters)) {

returnData[key] = (nodeParameters[key] as string[]).map(value => {

return this.resolveExpression(value as string) as string;

} else if (typeof nodeParameters[key] === 'object') {

returnData[key] = this.getResolveNodeParameters(nodeParameters[key] as INodeParameters);

returnData[key] = this.resolveExpression(nodeParameters[key] as string);

import Vue from 'vue';
import { parse } from 'flatted';

import axios, { AxiosRequestConfig } from 'axios';
import {
	IActivationError,
	ICredentialsDecryptedResponse,
	ICredentialsResponse,
	IExecutionsCurrentSummaryExtended,
	IExecutionDeleteFilter,
	IExecutionPushResponse,
	IExecutionResponse,
	IExecutionFlattedResponse,
	IExecutionsListResponse,
	IExecutionsStopData,
	IN8nUISettings,
	IStartRunData,
	IWorkflowDb,
	IWorkflowShortResponse,
	IRestApi,
	IWorkflowData,
	IWorkflowDataUpdate,
} from '@/Interface';
import {
	ICredentialsDecrypted,
	ICredentialType,
	IDataObject,
	INodeCredentials,
	INodePropertyOptions,
	INodeTypeDescription,
} from 'n8n-workflow';

/**
 * Unflattens the Execution data.
 *
 * @export
 * @param {IExecutionFlattedResponse} fullExecutionData The data to unflatten
 * @returns {IExecutionResponse}
 */
function unflattenExecutionData (fullExecutionData: IExecutionFlattedResponse): IExecutionResponse {
	// Unflatten the data
	const returnData: IExecutionResponse = {
		...fullExecutionData,
		workflowData: fullExecutionData.workflowData as IWorkflowDb,
		data: parse(fullExecutionData.data),
	};

	returnData.finished = returnData.finished ? returnData.finished : false;

	if (fullExecutionData.id) {
		returnData.id = fullExecutionData.id;
	}

	return returnData;
}

export class ResponseError extends Error {
	// The HTTP status code of response
	httpStatusCode?: number;

	// The error code in the resonse
	errorCode?: number;

	// The stack trace of the server
	serverStackTrace?: string;

	/**
	 * Creates an instance of ResponseError.
	 * @param {string} message The error message
	 * @param {number} [errorCode] The error code which can be used by frontend to identify the actual error
	 * @param {number} [httpStatusCode] The HTTP status code the response should have
	 * @param {string} [stack] The stack trace
	 * @memberof ResponseError
	 */
	constructor (message: string, errorCode?: number, httpStatusCode?: number, stack?: string) {
		super(message);
		this.name = 'ResponseError';

		if (errorCode) {
			this.errorCode = errorCode;
		}
		if (httpStatusCode) {
			this.httpStatusCode = httpStatusCode;
		}
		if (stack) {
			this.serverStackTrace = stack;
		}
	}
}

export const restApi = Vue.extend({
	methods: {
		restApi (): IRestApi {
			const self = this;
			return {
				async makeRestApiRequest (method: string, endpoint: string, data?: IDataObject): Promise<any> { // tslint:disable-line:no-any
					try {
						const options: AxiosRequestConfig = {
							method,
							url: endpoint,
							baseURL: self.$store.getters.getRestUrl,
							headers: {
								sessionid: self.$store.getters.sessionId,
							},
						};
						if (['PATCH', 'POST', 'PUT'].includes(method)) {
							options.data = data;
						} else {
							options.params = data;
						}

						const response = await axios.request(options);
						return response.data.data;
					} catch (error) {
						if (error.message === 'Network Error') {
							throw new ResponseError('API-Server can not be reached. It is probably down.');
						}

						const errorResponseData = error.response.data;
						if (errorResponseData !== undefined && errorResponseData.message !== undefined) {
							throw new ResponseError(errorResponseData.message, errorResponseData.code, error.response.status, errorResponseData.stack);
						}

						throw error;
					}
				},
				getActiveWorkflows: (): Promise<string[]> => {
					return self.restApi().makeRestApiRequest('GET', `/active`);
				},
				getActivationError: (id: string): Promise<IActivationError | undefined> => {
					return self.restApi().makeRestApiRequest('GET', `/active/error/${id}`);
				},
				getCurrentExecutions: (filter: object): Promise<IExecutionsCurrentSummaryExtended[]> => {
					let sendData = {};
					if (filter) {
						sendData = {
							filter,
						};
					}
					return self.restApi().makeRestApiRequest('GET', `/executions-current`, sendData);
				},
				stopCurrentExecution: (executionId: string): Promise<IExecutionsStopData> => {
					return self.restApi().makeRestApiRequest('POST', `/executions-current/${executionId}/stop`);
				},
				getSettings: (): Promise<IN8nUISettings> => {
					return self.restApi().makeRestApiRequest('GET', `/settings`);
				},

				// Returns all node-types
				getNodeTypes: (): Promise<INodeTypeDescription[]> => {
					return self.restApi().makeRestApiRequest('GET', `/node-types`);
				},

				// Returns all the parameter options from the server
				getNodeParameterOptions: (nodeType: string, methodName: string, credentials?: INodeCredentials): Promise<INodePropertyOptions[]> => {
					const sendData = {
						nodeType,
						methodName,
						credentials,
						currentNodeParameters: this.$store.getters.activeNode.parameters,
					};
					return self.restApi().makeRestApiRequest('GET', '/node-parameter-options', sendData);
				},

				// Removes a test webhook
				removeTestWebhook: (workflowId: string): Promise<boolean> => {
					return self.restApi().makeRestApiRequest('DELETE', `/test-webhook/${workflowId}`);
				},

				// Execute a workflow
				runWorkflow: async (startRunData: IStartRunData): Promise<IExecutionPushResponse> => {
					return self.restApi().makeRestApiRequest('POST', `/workflows/run`, startRunData);
				},

				// Creates new credentials
				createNewWorkflow: (sendData: IWorkflowData): Promise<IWorkflowDb> => {
					return self.restApi().makeRestApiRequest('POST', `/workflows`, sendData);
				},

				// Updates an existing workflow
				updateWorkflow: (id: string, data: IWorkflowDataUpdate): Promise<IWorkflowDb> => {
					return self.restApi().makeRestApiRequest('PATCH', `/workflows/${id}`, data);
				},

				// Deletes a workflow
				deleteWorkflow: (name: string): Promise<void> => {
					return self.restApi().makeRestApiRequest('DELETE', `/workflows/${name}`);
				},

				// Returns the workflow with the given name
				getWorkflow: (id: string): Promise<IWorkflowDb> => {
					return self.restApi().makeRestApiRequest('GET', `/workflows/${id}`);
				},

				// Returns all saved workflows
				getWorkflows: (filter?: object): Promise<IWorkflowShortResponse[]> => {
					let sendData;
					if (filter) {
						sendData = {
							filter,
						};
					}
					return self.restApi().makeRestApiRequest('GET', `/workflows`, sendData);
				},

				// Returns a workflow from a given URL
				getWorkflowFromUrl: (url: string): Promise<IWorkflowDb> => {
					return self.restApi().makeRestApiRequest('GET', `/workflows/from-url`, { url });
				},

				// Creates a new workflow
				createNewCredentials: (sendData: ICredentialsDecrypted): Promise<ICredentialsResponse> => {
					return self.restApi().makeRestApiRequest('POST', `/credentials`, sendData);
				},

				// Deletes a credentials
				deleteCredentials: (id: string): Promise<void> => {
					return self.restApi().makeRestApiRequest('DELETE', `/credentials/${id}`);
				},

				// Updates existing credentials
				updateCredentials: (id: string, data: ICredentialsDecrypted): Promise<ICredentialsResponse> => {
					return self.restApi().makeRestApiRequest('PATCH', `/credentials/${id}`, data);
				},

				// Returns the credentials with the given id
				getCredentials: (id: string, includeData?: boolean): Promise<ICredentialsDecryptedResponse | ICredentialsResponse | undefined> => {
					let sendData;
					if (includeData) {
						sendData = {
							includeData,
						};
					}
					return self.restApi().makeRestApiRequest('GET', `/credentials/${id}`, sendData);
				},

				// Returns all saved credentials
				getAllCredentials: (filter?: object): Promise<ICredentialsResponse[]> => {
					let sendData;
					if (filter) {
						sendData = {
							filter,
						};
					}

					return self.restApi().makeRestApiRequest('GET', `/credentials`, sendData);
				},

				// Returns all credential types
				getCredentialTypes: (): Promise<ICredentialType[]> => {
					return self.restApi().makeRestApiRequest('GET', `/credential-types`);
				},

				// Returns the execution with the given name
				getExecution: async (id: string): Promise<IExecutionResponse> => {
					const response = await self.restApi().makeRestApiRequest('GET', `/executions/${id}`);
					return unflattenExecutionData(response);
				},

				// Deletes executions
				deleteExecutions: (sendData: IExecutionDeleteFilter): Promise<void> => {
					return self.restApi().makeRestApiRequest('POST', `/executions/delete`, sendData);
				},

				// Returns the execution with the given name
				retryExecution: (id: string, loadWorkflow?: boolean): Promise<boolean> => {
					let sendData;
					if (loadWorkflow === true) {
						sendData = {
							loadWorkflow: true,
						};
					}
					return self.restApi().makeRestApiRequest('POST', `/executions/${id}/retry`, sendData);
				},

				// Returns all saved executions
				// TODO: For sure needs some kind of default filter like last day, with max 10 results, ...
				getPastExecutions: (filter: object, limit: number, lastId?: string | number): Promise<IExecutionsListResponse> => {
					let sendData = {};
					if (filter) {
						sendData = {
							filter,
							lastId,
							limit,
						};
					}

					return self.restApi().makeRestApiRequest('GET', `/executions`, sendData);
				},

				// Returns all the available timezones
				getTimezones: (): Promise<IDataObject> => {
					return self.restApi().makeRestApiRequest('GET', `/options/timezones`);
				},
			};
		},
	},
});


import {
	IExecuteFunctions,
	IHookFunctions,
	ILoadOptionsFunctions,
} from 'n8n-core';

import { OptionsWithUri } from 'request';
import { IDataObject } from 'n8n-workflow';

export interface IAttachment {
	fields: {
		item?: object[];
	};
}

/**
 * Make an API request to Telegram
 *
 * @param {IHookFunctions} this
 * @param {string} method
 * @param {string} url
 * @param {object} body
 * @returns {Promise<any>}
 */
export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: object, query?: IDataObject): Promise<any> { // tslint:disable-line:no-any
	const credentials = this.getCredentials('mattermostApi');

	if (credentials === undefined) {
		throw new Error('No credentials got returned!');
	}

	query = query || {};

	const options: OptionsWithUri = {
		method,
		body,
		qs: query,
		uri: `${credentials.baseUrl}/api/v4/${endpoint}`,
		headers: {
			Authorization: `Bearer ${credentials.accessToken}`,
			'content-type': 'application/json; charset=utf-8'
		},
		json: true
	};

	try {
		return await this.helpers.request!(options);
	} catch (error) {

		if (error.statusCode === 401) {
			// Return a clear error
			throw new Error('The Mattermost credentials are not valid!');
		}

		if (error.response && error.response.body && error.response.body.message) {
			// Try to return the error prettier
			const errorBody = error.response.body;
			throw new Error(`Mattermost error response: ${errorBody.message}`);
		}

		// Expected error data did not get returned so throw the actual error
		throw error;
	}
}


import {
	ActiveExecutions,
	IProcessMessageDataHook,
	ITransferNodeTypes,
	IWorkflowExecutionDataProcess,
	IWorkflowExecutionDataProcessWithExecution,
	NodeTypes,
	Push,
	WorkflowExecuteAdditionalData,
	WorkflowHelpers,
} from './';

import {
	IProcessMessage,
} from 'n8n-core';

import {
	IExecutionError,
	INode,
	IRun,
	IWorkflowExecuteHooks,
	WorkflowExecuteMode,
} from 'n8n-workflow';

import { join as pathJoin } from 'path';
import { fork } from 'child_process';

export class WorkflowRunner {
	activeExecutions: ActiveExecutions.ActiveExecutions;
	push: Push.Push;

	constructor() {
		this.push = Push.getInstance();
		this.activeExecutions = ActiveExecutions.getInstance();
	}

	/**
	 * Returns the data of the node types that are needed
	 * to execute the given nodes
	 *
	 * @param {INode[]} nodes
	 * @returns {ITransferNodeTypes}
	 * @memberof WorkflowRunner
	 */
	getNodeTypeData(nodes: INode[]): ITransferNodeTypes {
		const nodeTypes = NodeTypes();

		// Check which node-types have to be loaded
		const neededNodeTypes: string[] = [];
		for (const node of nodes) {
			if (!neededNodeTypes.includes(node.type)) {
				neededNodeTypes.push(node.type);
			}
		}

		// Get all the data of the needed node types that they
		// can be loaded again in the process
		const returnData: ITransferNodeTypes = {};
		for (const nodeTypeName of neededNodeTypes) {
			if (nodeTypes.nodeTypes[nodeTypeName] === undefined) {
				throw new Error(`The NodeType "${nodeTypeName}" could not be found!`);
			}

			returnData[nodeTypeName] = {
				className: nodeTypes.nodeTypes[nodeTypeName].type.constructor.name,
				sourcePath: nodeTypes.nodeTypes[nodeTypeName].sourcePath,
			};
		}

		return returnData;
	}

	/**
	 * The process did send a hook message so execute the appropiate hook
	 *
	 * @param {IWorkflowExecuteHooks} hookFunctions
	 * @param {IProcessMessageDataHook} hookData
	 * @memberof WorkflowRunner
	 */
	processHookMessage(hookFunctions: IWorkflowExecuteHooks, hookData: IProcessMessageDataHook) {
		if (hookFunctions[hookData.hook] !== undefined && Array.isArray(hookFunctions[hookData.hook])) {

			for (const hookFunction of hookFunctions[hookData.hook]!) {
				// TODO: Not sure if that is 100% correct or something is still missing like to wait
				hookFunction.apply(this, hookData.parameters)
					.catch((error: Error) => {
						// Catch all errors here because when "executeHook" gets called
						// we have the most time no "await" and so the errors would so
						// not be uncaught by anything.

						// TODO: Add proper logging
						console.error(`There was a problem executing hook: "${hookData.hook}"`);
						console.error('Parameters:');
						console.error(hookData.parameters);
						console.error('Error:');
						console.error(error);
					});
			}
		}
	}

	/**
	 * The process did error
	 *
	 * @param {IExecutionError} error
	 * @param {Date} startedAt
	 * @param {WorkflowExecuteMode} executionMode
	 * @param {string} executionId
	 * @memberof WorkflowRunner
	 */
	processError(error: IExecutionError, startedAt: Date, executionMode: WorkflowExecuteMode, executionId: string) {
		const fullRunData: IRun = {
			data: {
				resultData: {
					error,
					runData: {},
				},
			},
			finished: false,
			mode: executionMode,
			startedAt,
			stoppedAt: new Date(),
		};

		// Remove from active execution with empty data. That will
		// set the execution to failed.
		this.activeExecutions.remove(executionId, fullRunData);

		// Also send to Editor UI
		WorkflowExecuteAdditionalData.pushExecutionFinished(fullRunData, executionId);
	}

	/**
	 * Run the workflow in subprocess
	 *
	 * @param {IWorkflowExecutionDataProcess} data
	 * @param {boolean} [loadStaticData] If set will the static data be loaded from
	 *                                   the workflow and added to input data
	 * @returns {Promise<string>}
	 * @memberof WorkflowRunner
	 */
	async run(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean): Promise<string> {
		const startedAt = new Date();
		const subprocess = fork(pathJoin(__dirname, 'WorkflowRunnerProcess.js'));

		if (loadStaticData === true && data.workflowData.id) {
			data.workflowData.staticData = await WorkflowHelpers.getStaticDataById(data.workflowData.id as string);
		}

		// Register the active execution
		const executionId = this.activeExecutions.add(subprocess, data);

		const nodeTypeData = this.getNodeTypeData(data.workflowData.nodes);

		(data as unknown as IWorkflowExecutionDataProcessWithExecution).executionId = executionId;
		(data as unknown as IWorkflowExecutionDataProcessWithExecution).nodeTypeData = nodeTypeData;

		const hookFunctions = WorkflowExecuteAdditionalData.getHookMethods(data, executionId);

		// Send all data to subprocess it needs to run the workflow
		subprocess.send({ type: 'startWorkflow', data } as IProcessMessage);

		// Listen to data from the subprocess
		subprocess.on('message', (message: IProcessMessage) => {
			if (message.type === 'end') {
				this.activeExecutions.remove(executionId!, message.data.runData);
			} else if (message.type === 'processError') {

				const executionError = message.data.executionError as IExecutionError;

				this.processError(executionError, startedAt, data.executionMode, executionId);

			} else if (message.type === 'processHook') {
				this.processHookMessage(hookFunctions, message.data as IProcessMessageDataHook);
			}
		});

		// Also get informed when the processes does exit especially when it did crash
		subprocess.on('exit', (code, signal) => {
			if (code !== 0) {
				// Process did exit with error code, so something went wrong.
				const executionError = {
					message: 'Workflow execution process did crash for an unknown reason!',
				} as IExecutionError;

				this.processError(executionError, startedAt, data.executionMode, executionId);
			}
		});

		return executionId;
	}
}


import {
	IConnections,
	INode,
	INodeExecutionData,
	INodeParameters,
	IRunExecutionData,
	Workflow,
} from '../src';

process.env.TEST_VARIABLE_1 = 'valueEnvVariable1';

import * as Helpers from './Helpers';

interface StubNode {
	name: string;
	parameters: INodeParameters;
}

describe('Workflow', () => {

	describe('renameNodeInExpressions', () => {

		const tests = [
			{
				description: 'do nothing if there is no expression',
				input: {
					currentName: 'Node1',
					newName: 'Node1New',
					parameters: {
						value1: 'value1Node1',
						value2: 'value2Node1',
					}
				},
				output: {
					value1: 'value1Node1',
					value2: 'value2Node1',
				},
			},
			{
				description: 'should work with dot notation',
				input: {
					currentName: 'Node1',
					newName: 'NewName',
					parameters: {
						value1: '={{$node.Node1.data.value1 + \'Node1\'}}',
						value2: '={{$node.Node1.data.value2 + \' - \' + $node.Node1.data.value2}}',
					},
				},
				output: {
					value1: '={{$node.NewName.data.value1 + \'Node1\'}}',
					value2: '={{$node.NewName.data.value2 + \' - \' + $node.NewName.data.value2}}',
				},
			},
			{
				description: 'should work with ["nodeName"]',
				input: {
					currentName: 'Node1',
					newName: 'NewName',
					parameters: {
						value1: '={{$node["Node1"]["data"]["value1"] + \'Node1\'}}',
						value2: '={{$node["Node1"]["data"]["value2"] + \' - \' + $node["Node1"]["data"]["value2"]}}',
					},
				},
				output: {
					value1: '={{$node["NewName"]["data"]["value1"] + \'Node1\'}}',
					value2: '={{$node["NewName"]["data"]["value2"] + \' - \' + $node["NewName"]["data"]["value2"]}}',
				},
			},
			{
				description: 'should work with [\'nodeName\']',
				input: {
					currentName: 'Node1',
					newName: 'NewName',
					parameters: {
						value1: '={{$node[\'Node1\'][\'data\'][\'value1\'] + \'Node1\'}}',
						value2: '={{$node[\'Node1\'][\'data\'][\'value2\'] + \' - \' + $node[\'Node1\'][\'data\'][\'value2\']}}',
					},
				},
				output: {
					value1: '={{$node[\'NewName\'][\'data\'][\'value1\'] + \'Node1\'}}',
					value2: '={{$node[\'NewName\'][\'data\'][\'value2\'] + \' - \' + $node[\'NewName\'][\'data\'][\'value2\']}}',
				},
			},
			{
				description: 'should work on lower levels',
				input: {
					currentName: 'Node1',
					newName: 'NewName',
					parameters: {
						level1a: '={{$node.Node1.data.value1 + \'Node1\'}}',
						level1b: [
							{
								value2a: '={{$node.Node1.data.value1 + \'Node1\'}}',
								value2b: '={{$node.Node1.data.value1 + \'Node1\'}}',
							}
						],
						level1c: {
							value2a: {
								value3a: '={{$node.Node1.data.value1 + \'Node1\'}}',
								value3b: [
									{
										value4a: '={{$node.Node1.data.value1 + \'Node1\'}}',
										value4b: {
											value5a: '={{$node.Node1.data.value1 + \'Node1\'}}',
											value5b: '={{$node.Node1.data.value1 + \'Node1\'}}',
										},
									}
								]
							},
						}
					} as INodeParameters,
				},
				output: {
					level1a: '={{$node.NewName.data.value1 + \'Node1\'}}',
					level1b: [
						{
							value2a: '={{$node.NewName.data.value1 + \'Node1\'}}',
							value2b: '={{$node.NewName.data.value1 + \'Node1\'}}',
						}
					],
					level1c: {
						value2a: {
							value3a: '={{$node.NewName.data.value1 + \'Node1\'}}',
							value3b: [
								{
									value4a: '={{$node.NewName.data.value1 + \'Node1\'}}',
									value4b: {
										value5a: '={{$node.NewName.data.value1 + \'Node1\'}}',
										value5b: '={{$node.NewName.data.value1 + \'Node1\'}}',
									},
								}
							]
						},
					}
				},
			},
		];

		const nodeTypes = Helpers.NodeTypes();
		const workflow = new Workflow(undefined, [], {}, false, nodeTypes);

		for (const testData of tests) {
			test(testData.description, () => {
				const result = workflow.renameNodeInExpressions(testData.input.parameters, testData.input.currentName, testData.input.newName);
				expect(result).toEqual(testData.output);
			});
		}
	});

	describe('renameNode', () => {

		const tests = [
			{
				description: 'rename node without connections',
				input: {
					currentName: 'Node1',
					newName: 'Node1New',
					nodes: [
						{
							name: 'Node1',
							parameters: {
								value1: 'value1Node1',
								value2: 'value2Node1',
							},
						}
					],
					connections: {}
				},
				output: {
					nodes: [
						{
							name: 'Node1New',
							parameters: {
								value1: 'value1Node1',
								value2: 'value2Node1',
							},
						}
					],
					connections: {}
				},
			},
			{
				description: 'rename node with one output connection',
				input: {
					currentName: 'Node1',
					newName: 'Node1New',
					nodes: [
						{
							name: 'Node1',
							parameters: {
								value1: 'value1Node1',
								value2: 'value2Node1',
							},
						},
						{
							name: 'Node2',
							parameters: {
								value1: 'value1Node2',
								value2: 'value2Node2',
							},
						},
					],
					connections: {
						Node1: {
							main: [
								[
									{
										node: 'Node2',
										type: 'main',
										index: 0,
									},
								],
							],
						},
					},
				},
				output: {
					nodes: [
						{
							name: 'Node1New',
							parameters: {
								value1: 'value1Node1',
								value2: 'value2Node1',
							},
						},
						{
							name: 'Node2',
							parameters: {
								value1: 'value1Node2',
								value2: 'value2Node2',
							},
						},
					],
					connections: {
						Node1New: {
							main: [
								[
									{
										node: 'Node2',
										type: 'main',
										index: 0,
									},
								],
							],
						},
					},
				},
			},
			{
				description: 'rename node with one input connection',
				input: {
					currentName: 'Node2',
					newName: 'Node2New',
					nodes: [
						{
							name: 'Node1',
							parameters: {
								value1: 'value1Node1',
								value2: 'value2Node1',
							},
						},
						{
							name: 'Node2',
							parameters: {
								value1: 'value1Node2',
								value2: 'value2Node2',
							},
						},
					],
					connections: {
						Node1: {
							main: [
								[
									{
										node: 'Node2',
										type: 'main',
										index: 0,
									},
								],
							],
						},
					},
				},
				output: {
					nodes: [
						{
							name: 'Node1',
							parameters: {
								value1: 'value1Node1',
								value2: 'value2Node1',
							},
						},
						{
							name: 'Node2New',
							parameters: {
								value1: 'value1Node2',
								value2: 'value2Node2',
							},
						},
					],
					connections: {
						Node1: {
							main: [
								[
									{
										node: 'Node2New',
										type: 'main',
										index: 0,
									},
								],
							],
						},
					},
				},
			},
			{
				description: 'rename node with multiple input and output connection',
				input: {
					currentName: 'Node3',
					newName: 'Node3New',
					nodes: [
						{
							name: 'Node1',
							parameters: {
								value1: 'value1Node1',
								value2: 'value2Node1',
							},
						},
						{
							name: 'Node2',
							parameters: {
								value1: 'value1Node2',
								value2: 'value2Node2',
							},
						},
						{
							name: 'Node3',
							parameters: {
								value1: 'value1Node3',
								value2: 'value2Node3',
							},
						},
						{
							name: 'Node4',
							parameters: {
								value1: 'value1Node4',
								value2: 'value2Node4',
							},
						},
						{
							name: 'Node5',
							parameters: {
								value1: 'value1Node5',
								value2: 'value2Node5',
							},
						},
					],
					connections: {
						Node1: {
							main: [
								[
									{
										node: 'Node3',
										type: 'main',
										index: 0,
									},
								],
							],
						},
						Node2: {
							main: [
								[
									{
										node: 'Node3',
										type: 'main',
										index: 0,
									},
									{
										node: 'Node5',
										type: 'main',
										index: 0,
									}
								]
							]
						},
						Node3: {
							main: [
								[
									{
										node: 'Node4',
										type: 'main',
										index: 0,
									},
									{
										node: 'Node5',
										type: 'main',
										index: 0,
									},
								],
							],
						},
					},
				},
				output: {
					nodes: [
						{
							name: 'Node1',
							parameters: {
								value1: 'value1Node1',
								value2: 'value2Node1',
							},
						},
						{
							name: 'Node2',
							parameters: {
								value1: 'value1Node2',
								value2: 'value2Node2',
							},
						},
						{
							name: 'Node3New',
							parameters: {
								value1: 'value1Node3',
								value2: 'value2Node3',
							},
						},
						{
							name: 'Node4',
							parameters: {
								value1: 'value1Node4',
								value2: 'value2Node4',
							},
						},
						{
							name: 'Node5',
							parameters: {
								value1: 'value1Node5',
								value2: 'value2Node5',
							},
						},
					],
					connections: {
						Node1: {
							main: [
								[
									{
										node: 'Node3New',
										type: 'main',
										index: 0,
									},
								],
							],
						},
						Node2: {
							main: [
								[
									{
										node: 'Node3New',
										type: 'main',
										index: 0,
									},
									{
										node: 'Node5',
										type: 'main',
										index: 0,
									},
								],
							],
						},
						Node3New: {
							main: [
								[
									{
										node: 'Node4',
										type: 'main',
										index: 0,
									},
									{
										node: 'Node5',
										type: 'main',
										index: 0,
									},
								],
							],
						},
					},
				},
			},
			// This does just a basic test if "renameNodeInExpressions" gets used. More complex
			// tests with different formats and levels are in the separate tests for the function
			// "renameNodeInExpressions"
			{
				description: 'change name also in expressions which use node-name (dot notation)',
				input: {
					currentName: 'Node1',
					newName: 'Node1New',
					nodes: [
						{
							name: 'Node1',
							parameters: {
								value1: 'value1Node1',
								value2: 'value2Node1',
							},
						},
						{
							name: 'Node2',
							parameters: {
								value1: '={{$node.Node1.data.value1 + \'Node1\'}}',
								value2: '={{$node.Node1.data.value2 + \' - \' + $node.Node1.data.value2}}',
							},
						}
					],
					connections: {}
				},
				output: {
					nodes: [
						{
							name: 'Node1New',
							parameters: {
								value1: 'value1Node1',
								value2: 'value2Node1',
							},
						},
						{
							name: 'Node2',
							parameters: {
								value1: '={{$node.Node1New.data.value1 + \'Node1\'}}',
								value2: '={{$node.Node1New.data.value2 + \' - \' + $node.Node1New.data.value2}}',
							},
						}
					],
					connections: {}
				},
			},
		];

		const nodeTypes = Helpers.NodeTypes();
		let workflow: Workflow;

		function createNodeData(stubData: StubNode): INode {
			return {
				name: stubData.name,
				parameters: stubData.parameters,
				type: 'test.set',
				typeVersion: 1,
				position: [
					100,
					100,
				],
			};
		}

		let executeNodes: INode[];
		let resultNodes: {
			[key: string]: INode;
		};

		for (const testData of tests) {
			test(testData.description, () => {

				executeNodes = [];
				for (const node of testData.input.nodes) {
					executeNodes.push(createNodeData(node));
				}

				workflow = new Workflow(undefined, executeNodes, testData.input.connections as IConnections, false, nodeTypes);
				workflow.renameNode(testData.input.currentName, testData.input.newName);

				resultNodes = {};
				for (const node of testData.output.nodes) {
					resultNodes[node.name] = createNodeData(node);
				}

				expect(workflow.nodes).toEqual(resultNodes);
				expect(workflow.connectionsBySourceNode).toEqual(testData.output.connections);
			});
		}

	});

	describe('getParameterValue', () => {

		const tests = [
			{
				description: 'read simple not expression value',
				input: {
					Node1: {
						parameters: {
							value1: 'valueNode1',
						}
					},
					Node2: {
						parameters: {
							value1: 'valueNode2',
						},
					}
				},
				output: {
					value1: 'valueNode2'
				},
			},
			{
				description: 'read simple math expression',
				input: {
					Node1: {
						parameters: {
							value1: '',
						}
					},
					Node2: {
						parameters: {
							value1: '={{1+2}}',
						},
					}
				},
				output: {
					value1: 3
				},
			},
			{
				description: 'read data from node-output-data with with long "$node.{NODE}.data" syntax',
				input: {
					Node1: {
						parameters: {
							value1: 'valueNode1',
						}
					},
					Node2: {
						parameters: {
							value1: '={{$node.Node1.data.value1}}',
						},
					}
				},
				output: {
					value1: 'valueNode1'
				},
			},
			{
				description: 'read data from node-output-data with with long "$node.{NODE}.data" syntax add value and append text',
				input: {
					Node1: {
						parameters: {
							value1: 1,
						}
					},
					Node2: {
						parameters: {
							value1: '={{$node.Node1.data.value1 + 2}} asdf',
						},
					}
				},
				output: {
					value1: '3 asdf'
				},
			},
			{
				description: 'read deep-data from node-output-data with with long "$node.{NODE}.data" syntax with JavaScript Code',
				input: {
					Node1: {
						parameters: {
							value1: 'whatever',
						},
						// Overwrite the output data
						outputJson: {
							value1: {
								a: 1,
								b: 2,
								c: 3,
							},
						},
					},
					Node2: {
						parameters: {
							value1: '={{Object.keys($node.Node1.data.value1).join(", ")}}',
						},
					},
				},
				output: {
					value1: 'a, b, c',
				},
			},
			{
				description: 'read data from incoming-node-data with with short "data" syntax',
				input: {
					Node1: {
						parameters: {
							value1: 'valueNode1',
						}
					},
					Node2: {
						parameters: {
							value1: '={{$data.value1}}'
						},
					}
				},
				output: {
					value1: 'valueNode1',
				},
			},
			{
				description: 'read deep-data from incoming-node-data with with short "data" syntax',
				input: {
					Node1: {
						parameters: {
							value1: 'whatever',
						},
						// Overwrite the output data
						outputJson: {
							value1: {
								a: {
									b: 'deepDataNode1',
								},
							},
						},
					},
					Node2: {
						parameters: {
							value1: '={{$data.value1.a.b}}',
						},
					},
				},
				output: {
					value1: 'deepDataNode1',
				},
			},
			{
				description: 'read deep-data from node-output-data with with long "$node.{NODE}.data" syntax',
				input: {
					Node1: {
						parameters: {
							value1: 'whatever',
						},
						// Overwrite the output data
						outputJson: {
							value1: {
								a: {
									b: 'deepDataNode1',
								},
							},
						},
					},
					Node2: {
						parameters: {
							value1: '={{$node.Node1.data.value1.a.b}}',
						},
					},
				},
				output: {
					value1: 'deepDataNode1',
				},
			},
			{
				description: 'read binary-string-data from incoming-node-data with with short "$binary" syntax',
				input: {
					Node1: {
						parameters: {
							value1: 'whatever',
						},
						// Overwrite the output data
						outputBinary: {
							binaryKey: {
								data: '',
								type: '',
								fileName: 'test-file1.jpg',
							}
						}
					},
					Node2: {
						parameters: {
							value1: '={{$binary.binaryKey.fileName}}',
						},
					},
				},
				output: {
					value1: 'test-file1.jpg',
				},
			},
			{
				description: 'read binary-string-data from node-output-data with with long "$node.{NODE}.binary" syntax',
				input: {
					Node1: {
						parameters: {
							value1: 'whatever',
						},
						// Overwrite the output data
						outputBinary: {
							binaryKey: {
								data: '',
								type: '',
								fileName: 'test-file1.jpg',
							}
						}
					},
					Node2: {
						parameters: {
							value1: '={{$node.Node1.binary.binaryKey.fileName}}',
						},
					},
				},
				output: {
					value1: 'test-file1.jpg',
				},
			},
			{
				description: 'read parameter from other node with with long "$node.parameter" syntax',
				input: {
					Node1: {
						parameters: {
							value1: 'valueNode1',
						}
					},
					Node2: {
						parameters: {
							value1: '={{$node.Node1.parameter.value1}}'
						},
					}
				},
				output: {
					value1: 'valueNode1',
				},
			},
			{
				description: 'read environment data "$env" syntax which exists',
				input: {
					Node1: {
						parameters: {
							value1: 'valueNode1',
						}
					},
					Node2: {
						parameters: {
							value1: '={{$env.TEST_VARIABLE_1}}'
						},
					}
				},
				output: {
					value1: 'valueEnvVariable1',
				},
			},
			{
				description: 'read environment data "$env" syntax which does not exists',
				input: {
					Node1: {
						parameters: {
							value1: 'valueNode1',
						}
					},
					Node2: {
						parameters: {
							value1: '={{$env.DOES_NOT_EXIST}}'
						},
					}
				},
				output: {
					value1: undefined,
				},
			},
			{
				description: 'read parameter from current node with short "$parameter" syntax',
				input: {
					Node1: {
						parameters: {
							value1: 'valueNode1',
						}
					},
					Node2: {
						parameters: {
							value1: 'valueNode2',
							value2: '={{$parameter.value1}}',
						},
					}
				},
				output: {
					value1: 'valueNode2',
					value2: 'valueNode2',
				},
			},
			{
				description: 'return resolved value when referencing another property with expression (long "$node.{NODE}.data" syntax)',
				input: {
					Node1: {
						parameters: {
							value1: 'valueNode1',
						}
					},
					Node2: {
						parameters: {
							value1: '={{$node.Node1.data.value1}}',
							value2: '={{$parameter.value1}}',
						},
					}
				},
				output: {
					value1: 'valueNode1',
					value2: 'valueNode1',
				},
			},
			{
				description: 'return resolved value when referencing another property with expression (short "data" syntax)',
				input: {
					Node1: {
						parameters: {
							value1: 'valueNode1',
						}
					},
					Node2: {
						parameters: {
							value1: '={{$data.value1}}',
							value2: '={{$parameter.value1}}',
						},
					}
				},
				output: {
					value1: 'valueNode1',
					value2: 'valueNode1',
				},
			},
			{
				description: 'return resolved value when referencing another property with expression when a node has spaces (long "$node["{NODE}"].parameter" syntax)',
				input: {
					Node1: {
						parameters: {
							value1: 'valueNode1',
						}
					},
					Node2: {
						parameters: {
							value1: '={{$node["Node 4 with spaces"].parameter.value1}}',
						},
					}
				},
				output: {
					value1: 'default-value1',
				},
			},
			// TODO: Make that this test does not fail!
			// {
			//     description: 'return resolved value when short "data" syntax got used in expression on paramter of not active node which got referenced by active one',
			//     input: {
			//         Node1: {
			//             parameters: {
			//                 value1: 'valueNode1',
			//             }
			//         },
			//         Node2: {
			//             parameters: {
			//                 value1: '={{$data.value1}}-Node2',
			//             },
			//         },
			//         Node3: {
			//             parameters: {
			//                 value1: '={{$data.value1}}-Node3+1',
			//                 value2: '={{node.Node2.data.value1}}-Node3+2',
			//             },
			//         }
			//     },
			//     output: {
			//         value1: 'valueNode1-Node2-Node3+1',
			//         value2: 'valueNode1-Node2-Node3+2',
			//     },
			// },
		];

		const nodeTypes = Helpers.NodeTypes();

		for (const testData of tests) {
			test(testData.description, () => {

				const nodes: INode[] = [
					{
						"name": "Node1",
						"parameters": testData.input.Node1.parameters,
						"type": "test.set",
						"typeVersion": 1,
						"position": [
							100,
							100,
						],
					},
					{
						"name": "Node2",
						"parameters": testData.input.Node2.parameters,
						"type": "test.set",
						"typeVersion": 1,
						"position": [
							100,
							200,
						],
					},
					{
						"name": "Node3",
						// @ts-ignore
						"parameters": testData.input.hasOwnProperty('Node3') ? testData.input.Node3.parameters : {},
						"type": "test.set",
						"typeVersion": 1,
						"position": [
							100,
							300,
						],
					},
					{
						"name": "Node 4 with spaces",
						// @ts-ignore
						"parameters": testData.input.hasOwnProperty('Node4') ? testData.input.Node4.parameters : {},
						"type": "test.set",
						"typeVersion": 1,
						"position": [
							100,
							400,
						],
					}
				];
				const connections: IConnections = {
					"Node1": {
						"main": [
							[
								{
									"node": "Node2",
									"type": "main",
									"index": 0
								}
							]
						]
					},
					"Node2": {
						"main": [
							[
								{
									"node": "Node3",
									"type": "main",
									"index": 0
								}
							]
						]
					}
				};

				const workflow = new Workflow(undefined, nodes, connections, false, nodeTypes);
				const activeNodeName = testData.input.hasOwnProperty('Node3') ? 'Node3' : 'Node2';

				const runExecutionData: IRunExecutionData = {
					resultData: {
						runData: {
							Node1: [
								{
									startTime: 1,
									executionTime: 1,
									// @ts-ignore
									data: {
										main: [
											[
												{
													// @ts-ignore
													json: testData.input.Node1.outputJson || testData.input.Node1.parameters,
													// @ts-ignore
													binary: testData.input.Node1.outputBinary
												}
											]
										]
									}
								}
							]
						}
					}
				};

				const itemIndex = 0;
				const runIndex = 0;
				const connectionInputData: INodeExecutionData[] = runExecutionData.resultData.runData!['Node1']![0]!.data!.main[0]!;

				for (const parameterName of Object.keys(testData.output)) {
					const parameterValue = nodes.find((node) => node.name === activeNodeName)!.parameters[parameterName];
					const result = workflow.getParameterValue(parameterValue, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData);
					// @ts-ignore
					expect(result).toEqual(testData.output[parameterName]);
				}
			});
		}

		// test('should be able to set and read key data without initial data set', () => {

		//     const nodes: Node[] = [
		//         {
		//             "name": "Node1",
		//             "parameters": {
		//                 "value": "outputSet1"
		//             },
		//             "type": "test.set",
		//             "typeVersion": 1,
		//             "position": [
		//                 100,
		//                 200
		//             ]
		//         },
		//         {
		//             "name": "Node2",
		//             "parameters": {
		//                 "name": "=[data.propertyName]"
		//             },
		//             "type": "test.set",
		//             "typeVersion": 1,
		//             "position": [
		//                 100,
		//                 300
		//             ]
		//         }
		//     ];
		//     const connections: Connections = {
		//         "Node1": {
		//             "main": [
		//                 [
		//                     {
		//                         "node": "Node2",
		//                         "type": "main",
		//                         "index": 0
		//                     }
		//                 ]
		//             ]
		//         }
		//     };

		//     const nodeTypes = Helpers.NodeTypes();
		//     const workflow = new Workflow(nodes, connections, false, nodeTypes);
		//     const activeNodeName = 'Node2';

		//     // @ts-ignore
		//     const parameterValue = nodes.find((node) => node.name === activeNodeName).parameters.name;
		//     // const parameterValue = '=[data.propertyName]'; // TODO: Make this dynamic from node-data via "activeNodeName"!
		//     const runData: RunData = {
		//         Node1: [
		//             {
		//                 startTime: 1,
		//                 executionTime: 1,
		//                 data: {
		//                     main: [
		//                         [
		//                             {
		//                                 json: {
		//                                     propertyName: 'outputSet1'
		//                                 }
		//                             }
		//                         ]
		//                     ]
		//                 }
		//             }
		//         ]
		//     };

		//     const itemIndex = 0;
		//     const connectionInputData: NodeExecutionData[] = runData!['Node1']![0]!.data!.main[0]!;

		//     const result = workflow.getParameterValue(parameterValue, runData, itemIndex, activeNodeName, connectionInputData);

		//     expect(result).toEqual('outputSet1');
		// });

		test('should also resolve all child parameters when the parent get requested', () => {

			const nodeTypes = Helpers.NodeTypes();

			const nodes: INode[] = [
				{
					"name": "Node1",
					"parameters": {
						"values": {
							"string": [
								{
									"name": "name1",
									"value": "value1"
								},
								{
									"name": "name2",
									"value": "={{$parameter.values.string[0].value}}A"
								}
							]
						}
					},
					"type": "test.setMulti",
					"typeVersion": 1,
					"position": [
						100,
						100,
					],
				},
			];
			const connections: IConnections = {};

			const workflow = new Workflow(undefined, nodes, connections, false, nodeTypes);
			const activeNodeName = 'Node1';

			const runExecutionData: IRunExecutionData = {
				resultData: {
					runData: {
						Node1: [
							{
								startTime: 1,
								executionTime: 1,
								// @ts-ignore
								data: {
									main: [
										[
											{}
										]
									]
								}
							}
						]
					}
				}
			};

			const itemIndex = 0;
			const runIndex = 0;
			const connectionInputData: INodeExecutionData[] = runExecutionData.resultData.runData!['Node1']![0]!.data!.main[0]!;
			const parameterName = 'values';

			const parameterValue = nodes.find((node) => node.name === activeNodeName)!.parameters[parameterName];
			const result = workflow.getParameterValue(parameterValue, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData);

			expect(result).toEqual({
				string: [
					{
						name: 'name1',
						value: 'value1',
					},
					{
						name: 'name2',
						value: 'value1A',
					},
				]
			});

		});

	});

});


// Get the resolved parameter values of the current node

const currentNodeParameters = this.$store.getters.activeNode.parameters;

const resolvedNodeParameters = this.getResolveNodeParameters(currentNodeParameters);