CredentialTypes, Db, ExternalHooks, IWorkflowBase, IWorkflowExecutionDataProcess, LoadNodesAndCredentials,

/* eslint-disable @typescript-eslint/prefer-optional-chain */
/* eslint-disable array-callback-return */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable no-await-in-loop */
/* eslint-disable no-async-promise-executor */
/* eslint-disable no-param-reassign */
/* eslint-disable @typescript-eslint/unbound-method */
/* eslint-disable no-console */
import * as fs from 'fs';
import { Command, flags } from '@oclif/command';

import { UserSettings } from 'n8n-core';

// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { INode, ITaskData, LoggerProxy } from 'n8n-workflow';

import { sep } from 'path';

import { diff } from 'json-diff';

// eslint-disable-next-line import/no-extraneous-dependencies
import { pick } from 'lodash';
import { getLogger } from '../src/Logger';

import {
	ActiveExecutions,
	CredentialsOverwrites,
	CredentialTypes,
	Db,
	ExternalHooks,
	IWorkflowDb,
	IWorkflowExecutionDataProcess,
	LoadNodesAndCredentials,
	NodeTypes,
	WorkflowRunner,
} from '../src';

export class ExecuteBatch extends Command {
	static description = '\nExecutes multiple workflows once';

	static cancelled = false;

	static workflowExecutionsProgress: IWorkflowExecutionProgress[][];

	static shallow = false;

	static compare: string;

	static snapshot: string;

	static concurrency = 1;

	static debug = false;

	static executionTimeout = 3 * 60 * 1000;

	static examples = [
		`$ n8n executeAll`,
		`$ n8n executeAll --concurrency=10 --skipList=/data/skipList.txt`,
		`$ n8n executeAll --debug --output=/data/output.json`,
		`$ n8n executeAll --ids=10,13,15 --shortOutput`,
		`$ n8n executeAll --snapshot=/data/snapshots --shallow`,
		`$ n8n executeAll --compare=/data/previousExecutionData --retries=2`,
	];

	static flags = {
		help: flags.help({ char: 'h' }),
		debug: flags.boolean({
			description: 'Toggles on displaying all errors and debug messages.',
		}),
		ids: flags.string({
			description: 'Specifies workflow IDs to get executed, separated by a comma.',
		}),
		concurrency: flags.integer({
			default: 1,
			description:
				'How many workflows can run in parallel. Defaults to 1 which means no concurrency.',
		}),
		output: flags.string({
			description:
				'Enable execution saving, You must inform an existing folder to save execution via this param',
		}),
		snapshot: flags.string({
			description:
				'Enables snapshot saving. You must inform an existing folder to save snapshots via this param.',
		}),
		compare: flags.string({
			description:
				'Compares current execution with an existing snapshot. You must inform an existing folder where the snapshots are saved.',
		}),
		shallow: flags.boolean({
			description:
				'Compares only if attributes output from node are the same, with no regards to neste JSON objects.',
		}),
		skipList: flags.string({
			description: 'File containing a comma separated list of workflow IDs to skip.',
		}),
		retries: flags.integer({
			description: 'Retries failed workflows up to N tries. Default is 1. Set 0 to disable.',
			default: 1,
		}),
		shortOutput: flags.boolean({
			description: 'Omits the full execution information from output, displaying only summary.',
		}),
	};

	/**
	 * Gracefully handles exit.
	 * @param {boolean} skipExit Whether to skip exit or number according to received signal
	 */
	// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
	static async stopProcess(skipExit: boolean | number = false) {
		if (ExecuteBatch.cancelled) {
			process.exit(0);
		}

		ExecuteBatch.cancelled = true;
		const activeExecutionsInstance = ActiveExecutions.getInstance();
		const stopPromises = activeExecutionsInstance.getActiveExecutions().map(async (execution) => {
			// eslint-disable-next-line @typescript-eslint/no-floating-promises
			activeExecutionsInstance.stopExecution(execution.id);
		});

		await Promise.allSettled(stopPromises);

		setTimeout(() => {
			process.exit(0);
		}, 30000);

		let executingWorkflows = activeExecutionsInstance.getActiveExecutions();

		let count = 0;
		while (executingWorkflows.length !== 0) {
			if (count++ % 4 === 0) {
				console.log(`Waiting for ${executingWorkflows.length} active executions to finish...`);
				executingWorkflows.map((execution) => {
					console.log(` - Execution ID ${execution.id}, workflow ID: ${execution.workflowId}`);
				});
			}
			// eslint-disable-next-line no-await-in-loop
			await new Promise((resolve) => {
				setTimeout(resolve, 500);
			});
			executingWorkflows = activeExecutionsInstance.getActiveExecutions();
		}
		// We may receive true but when called from `process.on`
		// we get the signal (SIGNIT, etc.)
		if (skipExit !== true) {
			process.exit(0);
		}
	}

	// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
	formatJsonOutput(data: object) {
		return JSON.stringify(data, null, 2);
	}

	// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
	shouldBeConsideredAsWarning(errorMessage: string) {
		const warningStrings = [
			'refresh token is invalid',
			'unable to connect to',
			'econnreset',
			'429',
			'econnrefused',
			'missing a required parameter',
			'insufficient credit balance',
			'request timed out',
			'status code 401',
		];

		// eslint-disable-next-line no-param-reassign
		errorMessage = errorMessage.toLowerCase();

		for (let i = 0; i < warningStrings.length; i++) {
			if (errorMessage.includes(warningStrings[i])) {
				return true;
			}
		}

		return false;
	}

	// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
	async run() {
		process.on('SIGTERM', ExecuteBatch.stopProcess);
		process.on('SIGINT', ExecuteBatch.stopProcess);

		const logger = getLogger();
		LoggerProxy.init(logger);

		// eslint-disable-next-line @typescript-eslint/no-shadow
		const { flags } = this.parse(ExecuteBatch);

		ExecuteBatch.debug = flags.debug;
		ExecuteBatch.concurrency = flags.concurrency || 1;

		const ids: number[] = [];
		const skipIds: number[] = [];

		if (flags.snapshot !== undefined) {
			if (fs.existsSync(flags.snapshot)) {
				if (!fs.lstatSync(flags.snapshot).isDirectory()) {
					console.log(`The parameter --snapshot must be an existing directory`);
					return;
				}
			} else {
				console.log(`The parameter --snapshot must be an existing directory`);
				return;
			}

			ExecuteBatch.snapshot = flags.snapshot;
		}
		if (flags.compare !== undefined) {
			if (fs.existsSync(flags.compare)) {
				if (!fs.lstatSync(flags.compare).isDirectory()) {
					console.log(`The parameter --compare must be an existing directory`);
					return;
				}
			} else {
				console.log(`The parameter --compare must be an existing directory`);
				return;
			}

			ExecuteBatch.compare = flags.compare;
		}

		if (flags.output !== undefined) {
			if (fs.existsSync(flags.output)) {
				if (fs.lstatSync(flags.output).isDirectory()) {
					console.log(`The parameter --output must be a writable file`);
					return;
				}
			}
		}

		if (flags.ids !== undefined) {
			const paramIds = flags.ids.split(',');
			const re = /\d+/;
			const matchedIds = paramIds.filter((id) => re.exec(id)).map((id) => parseInt(id.trim(), 10));

			if (matchedIds.length === 0) {
				console.log(`The parameter --ids must be a list of numeric IDs separated by a comma.`);
				return;
			}

			ids.push(...matchedIds);
		}

		if (flags.skipList !== undefined) {
			if (fs.existsSync(flags.skipList)) {
				const contents = fs.readFileSync(flags.skipList, { encoding: 'utf-8' });
				skipIds.push(...contents.split(',').map((id) => parseInt(id.trim(), 10)));
			} else {
				console.log('Skip list file not found. Exiting.');
				return;
			}
		}

		if (flags.shallow) {
			ExecuteBatch.shallow = true;
		}

		// Start directly with the init of the database to improve startup time
		const startDbInitPromise = Db.init();

		// Load all node and credential types
		const loadNodesAndCredentials = LoadNodesAndCredentials();
		const loadNodesAndCredentialsPromise = loadNodesAndCredentials.init();

		// Make sure the settings exist
		await UserSettings.prepareUserSettings();

		// Wait till the database is ready
		await startDbInitPromise;

		let allWorkflows;

		const query = Db.collections.Workflow!.createQueryBuilder('workflows');

		if (ids.length > 0) {
			query.andWhere(`workflows.id in (:...ids)`, { ids });
		}

		if (skipIds.length > 0) {
			query.andWhere(`workflows.id not in (:...skipIds)`, { skipIds });
		}

		// eslint-disable-next-line prefer-const
		allWorkflows = (await query.getMany()) as IWorkflowDb[];

		if (ExecuteBatch.debug) {
			process.stdout.write(`Found ${allWorkflows.length} workflows to execute.\n`);
		}

		// Wait till the n8n-packages have been read
		await loadNodesAndCredentialsPromise;

		// Load the credentials overwrites if any exist
		await CredentialsOverwrites().init();

		// Load all external hooks
		const externalHooks = ExternalHooks();
		await externalHooks.init();

		// Add the found types to an instance other parts of the application can use
		const nodeTypes = NodeTypes();
		await nodeTypes.init(loadNodesAndCredentials.nodeTypes);
		const credentialTypes = CredentialTypes();
		await credentialTypes.init(loadNodesAndCredentials.credentialTypes);

		// Send a shallow copy of allWorkflows so we still have all workflow data.
		const results = await this.runTests([...allWorkflows]);

		let { retries } = flags;

		while (
			retries > 0 &&
			results.summary.warningExecutions + results.summary.failedExecutions > 0 &&
			!ExecuteBatch.cancelled
		) {
			const failedWorkflowIds = results.summary.errors.map((execution) => execution.workflowId);
			failedWorkflowIds.push(...results.summary.warnings.map((execution) => execution.workflowId));

			const newWorkflowList = allWorkflows.filter((workflow) =>
				failedWorkflowIds.includes(workflow.id),
			);

			// eslint-disable-next-line no-await-in-loop
			const retryResults = await this.runTests(newWorkflowList);

			this.mergeResults(results, retryResults);
			// By now, `results` has been updated with the new successful executions.
			retries--;
		}

		if (flags.output !== undefined) {
			fs.writeFileSync(flags.output, this.formatJsonOutput(results));
			console.log('\nExecution finished.');
			console.log('Summary:');
			console.log(`\tSuccess: ${results.summary.successfulExecutions}`);
			console.log(`\tFailures: ${results.summary.failedExecutions}`);
			console.log(`\tWarnings: ${results.summary.warningExecutions}`);
			console.log('\nNodes successfully tested:');
			Object.entries(results.coveredNodes).forEach(([nodeName, nodeCount]) => {
				console.log(`\t${nodeName}: ${nodeCount}`);
			});
			console.log('\nCheck the JSON file for more details.');
		} else if (flags.shortOutput) {
			console.log(
				this.formatJsonOutput({
					...results,
					executions: results.executions.filter(
						(execution) => execution.executionStatus !== 'success',
					),
				}),
			);
		} else {
			console.log(this.formatJsonOutput(results));
		}

		await ExecuteBatch.stopProcess(true);

		if (results.summary.failedExecutions > 0) {
			this.exit(1);
		}
		this.exit(0);
	}

	// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
	mergeResults(results: IResult, retryResults: IResult) {
		if (retryResults.summary.successfulExecutions === 0) {
			// Nothing to replace.
			return;
		}

		// Find successful executions and replace them on previous result.
		retryResults.executions.forEach((newExecution) => {
			if (newExecution.executionStatus === 'success') {
				// Remove previous execution from list.
				results.executions = results.executions.filter(
					(previousExecutions) => previousExecutions.workflowId !== newExecution.workflowId,
				);

				const errorIndex = results.summary.errors.findIndex(
					(summaryInformation) => summaryInformation.workflowId === newExecution.workflowId,
				);
				if (errorIndex !== -1) {
					// This workflow errored previously. Decrement error count.
					results.summary.failedExecutions--;
					// Remove from the list of errors.
					results.summary.errors.splice(errorIndex, 1);
				}

				const warningIndex = results.summary.warnings.findIndex(
					(summaryInformation) => summaryInformation.workflowId === newExecution.workflowId,
				);
				if (warningIndex !== -1) {
					// This workflow errored previously. Decrement error count.
					results.summary.warningExecutions--;
					// Remove from the list of errors.
					results.summary.warnings.splice(warningIndex, 1);
				}
				// Increment successful executions count and push it to all executions array.
				results.summary.successfulExecutions++;
				results.executions.push(newExecution);
			}
		});
	}

	async runTests(allWorkflows: IWorkflowDb[]): Promise<IResult> {
		const result: IResult = {
			totalWorkflows: allWorkflows.length,
			summary: {
				failedExecutions: 0,
				warningExecutions: 0,
				successfulExecutions: 0,
				errors: [],
				warnings: [],
			},
			coveredNodes: {},
			executions: [],
		};

		if (ExecuteBatch.debug) {
			this.initializeLogs();
		}

		return new Promise(async (res) => {
			const promisesArray = [];
			for (let i = 0; i < ExecuteBatch.concurrency; i++) {
				const promise = new Promise(async (resolve) => {
					let workflow: IWorkflowDb | undefined;
					while (allWorkflows.length > 0) {
						workflow = allWorkflows.shift();
						if (ExecuteBatch.cancelled) {
							process.stdout.write(`Thread ${i + 1} resolving and quitting.`);
							resolve(true);
							break;
						}
						// This if shouldn't be really needed
						// but it's a concurrency precaution.
						if (workflow === undefined) {
							resolve(true);
							return;
						}

						if (ExecuteBatch.debug) {
							ExecuteBatch.workflowExecutionsProgress[i].push({
								workflowId: workflow.id,
								status: 'running',
							});
							this.updateStatus();
						}

						// eslint-disable-next-line @typescript-eslint/no-loop-func
						await this.startThread(workflow).then((executionResult) => {
							if (ExecuteBatch.debug) {
								ExecuteBatch.workflowExecutionsProgress[i].pop();
							}
							result.executions.push(executionResult);
							if (executionResult.executionStatus === 'success') {
								if (ExecuteBatch.debug) {
									ExecuteBatch.workflowExecutionsProgress[i].push({
										workflowId: workflow!.id,
										status: 'success',
									});
									this.updateStatus();
								}
								result.summary.successfulExecutions++;
								const nodeNames = Object.keys(executionResult.coveredNodes);

								nodeNames.map((nodeName) => {
									if (result.coveredNodes[nodeName] === undefined) {
										result.coveredNodes[nodeName] = 0;
									}
									result.coveredNodes[nodeName] += executionResult.coveredNodes[nodeName];
								});
							} else if (executionResult.executionStatus === 'warning') {
								result.summary.warningExecutions++;
								result.summary.warnings.push({
									workflowId: executionResult.workflowId,
									error: executionResult.error!,
								});
								if (ExecuteBatch.debug) {
									ExecuteBatch.workflowExecutionsProgress[i].push({
										workflowId: workflow!.id,
										status: 'warning',
									});
									this.updateStatus();
								}
							} else if (executionResult.executionStatus === 'error') {
								result.summary.failedExecutions++;
								result.summary.errors.push({
									workflowId: executionResult.workflowId,
									error: executionResult.error!,
								});
								if (ExecuteBatch.debug) {
									ExecuteBatch.workflowExecutionsProgress[i].push({
										workflowId: workflow!.id,
										status: 'error',
									});
									this.updateStatus();
								}
							} else {
								throw new Error('Wrong execution status - cannot proceed');
							}
						});
					}

					resolve(true);
				});

				promisesArray.push(promise);
			}

			await Promise.allSettled(promisesArray);

			res(result);
		});
	}

	// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
	updateStatus() {
		if (ExecuteBatch.cancelled) {
			return;
		}

		if (process.stdout.isTTY) {
			process.stdout.moveCursor(0, -ExecuteBatch.concurrency);
			process.stdout.cursorTo(0);
			process.stdout.clearLine(0);
		}

		ExecuteBatch.workflowExecutionsProgress.map((concurrentThread, index) => {
			let message = `${index + 1}: `;
			concurrentThread.map((executionItem, workflowIndex) => {
				let openColor = '\x1b[0m';
				const closeColor = '\x1b[0m';
				switch (executionItem.status) {
					case 'success':
						openColor = '\x1b[32m';
						break;
					case 'error':
						openColor = '\x1b[31m';
						break;
					case 'warning':
						openColor = '\x1b[33m';
						break;
					default:
						break;
				}
				message += `${workflowIndex > 0 ? ', ' : ''}${openColor}${
					executionItem.workflowId
				}${closeColor}`;
			});
			if (process.stdout.isTTY) {
				process.stdout.cursorTo(0);
				process.stdout.clearLine(0);
			}
			process.stdout.write(`${message}\n`);
		});
	}

	// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
	initializeLogs() {
		process.stdout.write('**********************************************\n');
		process.stdout.write('              n8n test workflows\n');
		process.stdout.write('**********************************************\n');
		process.stdout.write('\n');
		process.stdout.write('Batch number:\n');
		ExecuteBatch.workflowExecutionsProgress = [];
		for (let i = 0; i < ExecuteBatch.concurrency; i++) {
			ExecuteBatch.workflowExecutionsProgress.push([]);
			process.stdout.write(`${i + 1}: \n`);
		}
	}

	async startThread(workflowData: IWorkflowDb): Promise<IExecutionResult> {
		// This will be the object returned by the promise.
		// It will be updated according to execution progress below.
		const executionResult: IExecutionResult = {
			workflowId: workflowData.id,
			workflowName: workflowData.name,
			executionTime: 0,
			finished: false,
			executionStatus: 'running',
			coveredNodes: {},
		};

		const requiredNodeTypes = ['n8n-nodes-base.start'];
		let startNode: INode | undefined;
		// eslint-disable-next-line no-restricted-syntax
		for (const node of workflowData.nodes) {
			if (requiredNodeTypes.includes(node.type)) {
				startNode = node;
				break;
			}
		}

		// We have a cool feature here.
		// On each node, on the Settings tab in the node editor you can change
		// the `Notes` field to add special cases for comparison and snapshots.
		// You need to set one configuration per line with the following possible keys:
		// CAP_RESULTS_LENGTH=x where x is a number. Cap the number of rows from this node to x.
		// This means if you set CAP_RESULTS_LENGTH=1 we will have only 1 row in the output
		// IGNORED_PROPERTIES=x,y,z where x, y and z are JSON property names. Removes these
		//    properties from the JSON object (useful for optional properties that can
		//    cause the comparison to detect changes when not true).
		const nodeEdgeCases = {} as INodeSpecialCases;
		workflowData.nodes.forEach((node) => {
			executionResult.coveredNodes[node.type] = (executionResult.coveredNodes[node.type] || 0) + 1;
			if (node.notes !== undefined && node.notes !== '') {
				node.notes.split('\n').forEach((note) => {
					const parts = note.split('=');
					if (parts.length === 2) {
						if (nodeEdgeCases[node.name] === undefined) {
							nodeEdgeCases[node.name] = {} as INodeSpecialCase;
						}
						if (parts[0] === 'CAP_RESULTS_LENGTH') {
							nodeEdgeCases[node.name].capResults = parseInt(parts[1], 10);
						} else if (parts[0] === 'IGNORED_PROPERTIES') {
							nodeEdgeCases[node.name].ignoredProperties = parts[1]
								.split(',')
								.map((property) => property.trim());
						} else if (parts[0] === 'KEEP_ONLY_PROPERTIES') {
							nodeEdgeCases[node.name].keepOnlyProperties = parts[1]
								.split(',')
								.map((property) => property.trim());
						}
					}
				});
			}
		});

		return new Promise(async (resolve) => {
			if (startNode === undefined) {
				// If the workflow does not contain a start-node we can not know what
				// should be executed and with which data to start.
				executionResult.error = 'Workflow cannot be started as it does not contain a "Start" node.';
				executionResult.executionStatus = 'warning';
				resolve(executionResult);
			}

			let gotCancel = false;

			// Timeouts execution after 5 minutes.
			const timeoutTimer = setTimeout(() => {
				gotCancel = true;
				executionResult.error = 'Workflow execution timed out.';
				executionResult.executionStatus = 'warning';
				resolve(executionResult);
			}, ExecuteBatch.executionTimeout);

			try {
				const runData: IWorkflowExecutionDataProcess = {
					executionMode: 'cli',
					startNodes: [startNode!.name],
					workflowData,
				};

				const workflowRunner = new WorkflowRunner();
				const executionId = await workflowRunner.run(runData);

				const activeExecutions = ActiveExecutions.getInstance();
				const data = await activeExecutions.getPostExecutePromise(executionId);
				if (gotCancel || ExecuteBatch.cancelled) {
					clearTimeout(timeoutTimer);
					// The promise was settled already so we simply ignore.
					return;
				}

				if (data === undefined) {
					executionResult.error = 'Workflow did not return any data.';
					executionResult.executionStatus = 'error';
				} else {
					executionResult.executionTime =
						(Date.parse(data.stoppedAt as unknown as string) -
							Date.parse(data.startedAt as unknown as string)) /
						1000;
					executionResult.finished = data?.finished !== undefined;

					if (data.data.resultData.error) {
						// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, no-prototype-builtins
						executionResult.error = data.data.resultData.error.hasOwnProperty('description')
							? // @ts-ignore
							  data.data.resultData.error.description
							: data.data.resultData.error.message;
						if (data.data.resultData.lastNodeExecuted !== undefined) {
							executionResult.error += ` on node ${data.data.resultData.lastNodeExecuted}`;
						}
						executionResult.executionStatus = 'error';

						if (this.shouldBeConsideredAsWarning(executionResult.error || '')) {
							executionResult.executionStatus = 'warning';
						}
					} else {
						if (ExecuteBatch.shallow) {
							// What this does is guarantee that top-level attributes
							// from the JSON are kept and the are the same type.

							// We convert nested JSON objects to a simple {object:true}
							// and we convert nested arrays to ['json array']

							// This reduces the chance of false positives but may
							// result in not detecting deeper changes.
							Object.keys(data.data.resultData.runData).map((nodeName: string) => {
								data.data.resultData.runData[nodeName].map((taskData: ITaskData) => {
									if (taskData.data === undefined) {
										return;
									}
									Object.keys(taskData.data).map((connectionName) => {
										const connection = taskData.data![connectionName];
										connection.map((executionDataArray) => {
											if (executionDataArray === null) {
												return;
											}

											if (
												nodeEdgeCases[nodeName] !== undefined &&
												nodeEdgeCases[nodeName].capResults !== undefined
											) {
												executionDataArray.splice(nodeEdgeCases[nodeName].capResults!);
											}

											executionDataArray.map((executionData) => {
												if (executionData.json === undefined) {
													return;
												}
												if (
													nodeEdgeCases[nodeName] !== undefined &&
													nodeEdgeCases[nodeName].ignoredProperties !== undefined
												) {
													nodeEdgeCases[nodeName].ignoredProperties!.forEach(
														(ignoredProperty) => delete executionData.json[ignoredProperty],
													);
												}

												let keepOnlyFields = [] as string[];
												if (
													nodeEdgeCases[nodeName] !== undefined &&
													nodeEdgeCases[nodeName].keepOnlyProperties !== undefined
												) {
													keepOnlyFields = nodeEdgeCases[nodeName].keepOnlyProperties!;
												}
												executionData.json =
													keepOnlyFields.length > 0
														? pick(executionData.json, keepOnlyFields)
														: executionData.json;
												const jsonProperties = executionData.json;

												const nodeOutputAttributes = Object.keys(jsonProperties);
												nodeOutputAttributes.map((attributeName) => {
													if (Array.isArray(jsonProperties[attributeName])) {
														jsonProperties[attributeName] = ['json array'];
													} else if (typeof jsonProperties[attributeName] === 'object') {
														jsonProperties[attributeName] = { object: true };
													}
												});
											});
										});
									});
								});
							});
						} else {
							// If not using shallow comparison then we only treat nodeEdgeCases.
							const specialCases = Object.keys(nodeEdgeCases);

							specialCases.forEach((nodeName) => {
								data.data.resultData.runData[nodeName].map((taskData: ITaskData) => {
									if (taskData.data === undefined) {
										return;
									}
									Object.keys(taskData.data).map((connectionName) => {
										const connection = taskData.data![connectionName];
										connection.map((executionDataArray) => {
											if (executionDataArray === null) {
												return;
											}

											if (nodeEdgeCases[nodeName].capResults !== undefined) {
												executionDataArray.splice(nodeEdgeCases[nodeName].capResults!);
											}

											if (nodeEdgeCases[nodeName].ignoredProperties !== undefined) {
												executionDataArray.map((executionData) => {
													if (executionData.json === undefined) {
														return;
													}
													nodeEdgeCases[nodeName].ignoredProperties!.forEach(
														(ignoredProperty) => delete executionData.json[ignoredProperty],
													);
												});
											}
										});
									});
								});
							});
						}

						const serializedData = this.formatJsonOutput(data);
						if (ExecuteBatch.compare === undefined) {
							executionResult.executionStatus = 'success';
						} else {
							const fileName = `${
								ExecuteBatch.compare.endsWith(sep)
									? ExecuteBatch.compare
									: ExecuteBatch.compare + sep
							}${workflowData.id}-snapshot.json`;
							if (fs.existsSync(fileName)) {
								const contents = fs.readFileSync(fileName, { encoding: 'utf-8' });

								const changes = diff(JSON.parse(contents), data, { keysOnly: true });

								if (changes !== undefined) {
									// we have structural changes. Report them.
									executionResult.error = `Workflow may contain breaking changes`;
									executionResult.changes = changes;
									executionResult.executionStatus = 'error';
								} else {
									executionResult.executionStatus = 'success';
								}
							} else {
								executionResult.error = 'Snapshot for not found.';
								executionResult.executionStatus = 'warning';
							}
						}
						// Save snapshots only after comparing - this is to make sure we're updating
						// After comparing to existing verion.
						if (ExecuteBatch.snapshot !== undefined) {
							const fileName = `${
								ExecuteBatch.snapshot.endsWith(sep)
									? ExecuteBatch.snapshot
									: ExecuteBatch.snapshot + sep
							}${workflowData.id}-snapshot.json`;
							fs.writeFileSync(fileName, serializedData);
						}
					}
				}
			} catch (e) {
				executionResult.error = 'Workflow failed to execute.';
				executionResult.executionStatus = 'error';
			}
			clearTimeout(timeoutTimer);
			resolve(executionResult);
		});
	}
}


import { MigrationInterface, QueryRunner } from 'typeorm';
import config = require('../../../../config');

export class CertifyCorrectCollation1623936588000 implements MigrationInterface {
	name = 'CertifyCorrectCollation1623936588000';

	async up(queryRunner: QueryRunner): Promise<void> {
		const tablePrefix = config.get('database.tablePrefix');
		const databaseType = config.get('database.type');

		if (databaseType === 'mariadb') {
			// This applies to MySQL only.
			return;
		}

		const checkCollationExistence = await queryRunner.query(`show collation where collation like 'utf8mb4_0900_ai_ci';`);
		let collation = 'utf8mb4_general_ci';
		if (checkCollationExistence.length > 0) {
			collation = 'utf8mb4_0900_ai_ci';
		}

		const databaseName = config.get(`database.mysqldb.database`);

		await queryRunner.query(`ALTER DATABASE \`${databaseName}\` CHARACTER SET utf8mb4 COLLATE ${collation};`);

		for (const tableName of [
			'credentials_entity',
			'execution_entity',
			'tag_entity',
			'webhook_entity',
			'workflow_entity',
			'workflows_tags',
		]) {
			await queryRunner.query(`ALTER TABLE ${tablePrefix}${tableName} CONVERT TO CHARACTER SET utf8mb4 COLLATE ${collation};`);
		}
	}

	async down(queryRunner: QueryRunner): Promise<void> {
		// There is nothing to undo in this case as we already expect default collation to be utf8mb4
		// This migration exists simply to enforce that n8n will work with
		// older mysql versions
	}

}


The Edit Image node allows you to manipulate and edit images. Use this node when you want to:

- Blur an image
- Add a border to an image
- Create a new image
- Crop an image
- Composite an image on top of another
- Draw on an image
- Get information about the image
- Rotate an image
- Change the size of an image
- Shear an image along the X or Y axis
- Add text to the image


import {
	INodeProperties,
} from 'n8n-workflow';

export const agentRoleOperations = [
	{
		displayName: 'Operation',
		name: 'operation',
		type: 'options',
		displayOptions: {
			show: {
				resource: [
					'agentRole',
				],
			},
		},
		options: [
			{
				name: 'Get',
				value: 'get',
				description: 'Retrieve an agent role',
			},
			{
				name: 'Get All',
				value: 'getAll',
				description: 'Retrieve all agent roles',
			},
		],
		default: 'get',
	},
] as INodeProperties[];

export const agentRoleFields = [
	// ----------------------------------------
	//              agentRole: get
	// ----------------------------------------
	{
		displayName: 'Agent Role ID',
		name: 'agentRoleId',
		description: 'ID of the agent role to retrieve',
		type: 'string',
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: [
					'agentRole',
				],
				operation: [
					'get',
				],
			},
		},
	},

	// ----------------------------------------
	//            agentRole: getAll
	// ----------------------------------------
	{
		displayName: 'Return All',
		name: 'returnAll',
		type: 'boolean',
		default: false,
		description: 'Whether to return all results or only up to a given limit',
		displayOptions: {
			show: {
				resource: [
					'agentRole',
				],
				operation: [
					'getAll',
				],
			},
		},
	},
	{
		displayName: 'Limit',
		name: 'limit',
		type: 'number',
		default: 50,
		description: 'How many results to return',
		typeOptions: {
			minValue: 1,
		},
		displayOptions: {
			show: {
				resource: [
					'agentRole',
				],
				operation: [
					'getAll',
				],
				returnAll: [
					false,
				],
			},
		},
	},
] as INodeProperties[];


// Add the found types to an instance other parts of the application can use

const credentialTypes = CredentialTypes();

const instanceId = await UserSettings.getInstanceId();


import {
	INodeProperties,
} from 'n8n-workflow';

export const projectOperations = [
	{
		displayName: 'Operation',
		name: 'operation',
		type: 'options',
		displayOptions: {
			show: {
				resource: [
					'project',
				],
			},
		},
		options: [
			{
				name: 'Create',
				value: 'create',
				description: 'Create a project',
			},
			{
				name: 'Delete',
				value: 'delete',
				description: 'Delete a project',
			},
			{
				name: 'Get',
				value: 'get',
				description: 'Get a project',
			},
			{
				name: 'Get All',
				value: 'getAll',
				description: 'Get all projects',
			},
			{
				name: 'Update',
				value: 'update',
				description: 'Update a project',
			},
		],
		default: 'create',
		description: 'The operation to perform.',
	},
] as INodeProperties[];

export const projectFields = [

	/* -------------------------------------------------------------------------- */
	/*                                 project:create                             */
	/* -------------------------------------------------------------------------- */
	{
		displayName: 'Project Name',
		name: 'name',
		type: 'string',
		required: true,
		default: '',
		description: 'Name of project being created.',
		displayOptions: {
			show: {
				resource: [
					'project',
				],
				operation: [
					'create',
				],
			},
		},
	},
	{
		displayName: 'Additional Fields',
		name: 'additionalFields',
		type: 'collection',
		placeholder: 'Add Field',
		displayOptions: {
			show: {
				operation: [
					'create',
				],
				resource: [
					'project',
				],
			},
		},
		default: {},
		options: [
			{
				displayName: 'Billable',
				name: 'billable',
				type: 'boolean',
				default: true,
			},
			{
				displayName: 'Color',
				name: 'color',
				type: 'color',
				default: '#0000FF',
			},
			{
				displayName: 'Client ID',
				name: 'clientId',
				type: 'options',
				typeOptions: {
					loadOptionsDependsOn: [
						'workspaceId',
					],
					loadOptionsMethod: 'loadClientsForWorkspace',
				},
				default: '',
			},
			{
				displayName: 'Estimate',
				name: 'estimateUi',
				placeholder: 'Add Estimate',
				type: 'fixedCollection',
				default: {},
				typeOptions: {
					multipleValues: false,
				},
				options: [
					{
						displayName: 'Estimate',
						name: 'estimateValues',
						values: [
							{
								displayName: 'Estimate',
								name: 'estimate',
								type: 'number',
								default: 0,
							},
							{
								displayName: 'Type',
								name: 'type',
								type: 'options',
								options: [
									{
										name: 'Auto',
										value: 'AUTO',
									},
									{
										name: 'Manual',
										value: 'MANUAL',
									},
								],
								default: 'AUTO',
							},
						],
					},
				],
			},
			{
				displayName: 'Is Public',
				name: 'isPublic',
				type: 'boolean',
				default: true,
			},
			{
				displayName: 'Note',
				name: 'note',
				type: 'string',
				default: '',
				description: 'Note about the project',
			},
		],
	},
	/* -------------------------------------------------------------------------- */
	/*                                 project:delete                             */
	/* -------------------------------------------------------------------------- */
	{
		displayName: 'Project ID',
		name: 'projectId',
		type: 'string',
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: [
					'project',
				],
				operation: [
					'delete',
				],
			},
		},
	},
	/* -------------------------------------------------------------------------- */
	/*                                 project:get                                */
	/* -------------------------------------------------------------------------- */
	{
		displayName: 'Project ID',
		name: 'projectId',
		type: 'string',
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: [
					'project',
				],
				operation: [
					'get',
				],
			},
		},
	},
	/* -------------------------------------------------------------------------- */
	/*                                 project:getAll                             */
	/* -------------------------------------------------------------------------- */
	{
		displayName: 'Return All',
		name: 'returnAll',
		type: 'boolean',
		displayOptions: {
			show: {
				operation: [
					'getAll',
				],
				resource: [
					'project',
				],
			},
		},
		default: false,
		description: 'If all results should be returned or only up to a given limit.',
	},
	{
		displayName: 'Limit',
		name: 'limit',
		type: 'number',
		displayOptions: {
			show: {
				operation: [
					'getAll',
				],
				resource: [
					'project',
				],
				returnAll: [
					false,
				],
			},
		},
		typeOptions: {
			minValue: 1,
			maxValue: 500,
		},
		default: 100,
		description: 'How many results to return.',
	},
	{
		displayName: 'Additional Fields',
		name: 'additionalFields',
		type: 'collection',
		placeholder: 'Add Field',
		displayOptions: {
			show: {
				operation: [
					'getAll',
				],
				resource: [
					'project',
				],
			},
		},
		default: {},
		options: [
			{
				displayName: 'Archived',
				name: 'archived',
				type: 'boolean',
				default: true,
			},
			{
				displayName: 'Billable',
				name: 'billable',
				type: 'boolean',
				default: true,
			},
			{
				displayName: 'Client IDs',
				name: 'clients',
				type: 'multiOptions',
				typeOptions: {
					loadOptionsDependsOn: [
						'workspaceId',
					],
					loadOptionsMethod: 'loadClientsForWorkspace',
				},
				default: [],
			},
			{
				displayName: 'Contains Client',
				name: 'contains-client',
				type: 'boolean',
				default: false,
				description: 'If provided, projects will be filtered by whether they have a client.;			',
			},
			{
				displayName: 'Client Status',
				name: 'client-status',
				type: 'options',
				options: [
					{
						name: 'Active',
						value: 'ACTIVE',
					},
					{
						name: 'Archived',
						value: 'ARCHIVED',
					},
				],
				default: '',
				description: 'If provided, projects will be filtered by whether they have a client.',
			},
			{
				displayName: 'Contains User',
				name: 'contains-user',
				type: 'boolean',
				default: false,
				description: 'If provided, projects will be filtered by whether they have users.',
			},
			{
				displayName: 'Is Template',
				name: 'is-template',
				type: 'boolean',
				default: false,
				description: 'If provided, projects will be filtered by whether they are used as a template.',
			},
			{
				displayName: 'Name',
				name: 'name',
				type: 'string',
				default: '',
			},
			{
				displayName: 'Sort Column',
				name: 'sort-column',
				type: 'options',
				options: [
					{
						name: 'Name',
						value: 'NAME',
					},
					{
						name: 'Client Name',
						value: 'CLIENT_NAME',
					},
					{
						name: 'Duration',
						value: 'DURATION',
					},
				],
				default: '',
			},
			{
				displayName: 'Sort Order',
				name: 'sort-order',
				type: 'options',
				options: [
					{
						name: 'Ascending',
						value: 'ASCENDING',
					},
					{
						name: 'Descending',
						value: 'DESCENDING',
					},
				],
				default: '',
			},
			{
				displayName: 'User IDs',
				name: 'users',
				type: 'options',
				typeOptions: {
					loadOptionsDependsOn: [
						'workspaceId',
					],
					loadOptionsMethod: 'loadUsersForWorkspace',
				},
				default: '',
			},
			{
				displayName: 'User Status',
				name: 'user-status',
				type: 'options',
				options: [
					{
						name: 'Active',
						value: 'ACTIVE',
					},
					{
						name: 'Archived',
						value: 'ARCHIVED',
					},
				],
				default: '',
				description: 'If provided, projects will be filtered by whether they have a client.',
			},
		],
	},

	/* -------------------------------------------------------------------------- */
	/*                                 project:update                             */
	/* -------------------------------------------------------------------------- */
	{
		displayName: 'Project ID',
		name: 'projectId',
		type: 'string',
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: [
					'project',
				],
				operation: [
					'update',
				],
			},
		},
	},
	{
		displayName: 'Update Fields',
		name: 'updateFields',
		type: 'collection',
		placeholder: 'Add Field',
		displayOptions: {
			show: {
				operation: [
					'update',
				],
				resource: [
					'project',
				],
			},
		},
		default: {},
		options: [
			{
				displayName: 'Billable',
				name: 'billable',
				type: 'boolean',
				default: true,
			},
			{
				displayName: 'Color',
				name: 'color',
				type: 'color',
				default: '#0000FF',
			},
			{
				displayName: 'Client ID',
				name: 'clientId',
				type: 'options',
				typeOptions: {
					loadOptionsDependsOn: [
						'workspaceId',
					],
					loadOptionsMethod: 'loadClientsForWorkspace',
				},
				default: '',
			},
			{
				displayName: 'Estimate',
				name: 'estimateUi',
				placeholder: 'Add Estimate',
				type: 'fixedCollection',
				default: {},
				typeOptions: {
					multipleValues: false,
				},
				options: [
					{
						displayName: 'Estimate',
						name: 'estimateValues',
						values: [
							{
								displayName: 'Estimate',
								name: 'estimate',
								type: 'number',
								default: 0,
							},
							{
								displayName: 'Type',
								name: 'type',
								type: 'options',
								options: [
									{
										name: 'Auto',
										value: 'AUTO',
									},
									{
										name: 'Manual',
										value: 'MANUAL',
									},
								],
								default: 'AUTO',
							},
						],
					},
				],
			},
			{
				displayName: 'Is Public',
				name: 'isPublic',
				type: 'boolean',
				default: false,
			},
			{
				displayName: 'Name',
				name: 'name',
				type: 'string',
				default: '',
			},
			{
				displayName: 'Note',
				name: 'note',
				type: 'string',
				default: '',
				description: 'Note about the project',
			},
		],
	},

] as INodeProperties[];


<template>
	<div :class="$style.container">
		<el-row>
			<el-col :span="8" :class="$style.accessLabel">
				<n8n-text :compact="true" :bold="true">Allow use by</n8n-text>
			</el-col>
			<el-col :span="16">
				<div
					v-for="node in nodesWithAccess"
					:key="node.name"
					:class="$style.valueLabel"
				>
					<el-checkbox
						:label="node.displayName"
						:value="!!nodeAccess[node.name]"
						@change="(val) => onNodeAccessChange(node.name, val)"
					/>
				</div>
			</el-col>
		</el-row>
		<el-row v-if="currentCredential">
			<el-col :span="8" :class="$style.label">
				<n8n-text :compact="true" :bold="true">Created</n8n-text>
			</el-col>
			<el-col :span="16" :class="$style.valueLabel">
				<n8n-text :compact="true"><TimeAgo :date="currentCredential.createdAt" :capitalize="true" /></n8n-text>
			</el-col>
		</el-row>
		<el-row v-if="currentCredential">
			<el-col :span="8" :class="$style.label">
				<n8n-text :compact="true" :bold="true">Last modified</n8n-text>
			</el-col>
			<el-col :span="16" :class="$style.valueLabel">
				<n8n-text :compact="true"><TimeAgo :date="currentCredential.updatedAt" :capitalize="true" /></n8n-text>
			</el-col>
		</el-row>
		<el-row v-if="currentCredential">
			<el-col :span="8" :class="$style.label">
				<n8n-text :compact="true" :bold="true">ID</n8n-text>
			</el-col>
			<el-col :span="16" :class="$style.valueLabel">
				<n8n-text :compact="true">{{currentCredential.id}}</n8n-text>
			</el-col>
		</el-row>
	</div>
</template>

<script lang="ts">
import Vue from 'vue';

import TimeAgo from '../TimeAgo.vue';

export default Vue.extend({
	name: 'CredentialInfo',
	props: ['nodesWithAccess', 'nodeAccess', 'currentCredential'],
	components: {
		TimeAgo,
	},
	methods: {
		onNodeAccessChange(name: string, value: string) {
			this.$emit('accessChange', {
				name,
				value,
			});
		},
	},
});
</script>

<style lang="scss" module>
.container {
	> * {
		margin-bottom: var(--spacing-l);
	}
}

.label {
	font-weight: var(--font-weight-bold);
	max-width: 230px;
}

.accessLabel {
	composes: label;
	margin-top: var(--spacing-5xs);
}

.valueLabel {
	font-weight: var(--font-weight-regular);
}

</style>


import {
	ICredentialType,
	INodeProperties,
} from 'n8n-workflow';

export class CalendlyApi implements ICredentialType {
	name = 'calendlyApi';
	displayName = 'Calendly API';
	documentationUrl = 'calendly';
	properties: INodeProperties[] = [
		{
			displayName: 'API Key',
			name: 'apiKey',
			type: 'string',
			default: '',
		},
	];
}


ActiveExecutions, CredentialsOverwrites, CredentialTypes, Db, ExternalHooks, IWorkflowDb, IWorkflowExecutionDataProcess, LoadNodesAndCredentials, NodeTypes,

keep keep keep keep add keep keep keep keep

/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable no-console */
import { promises as fs } from 'fs';
import { Command, flags } from '@oclif/command';
import { UserSettings } from 'n8n-core';
import { INode, LoggerProxy } from 'n8n-workflow';

import {
	ActiveExecutions,
	CredentialsOverwrites,
	CredentialTypes,
	Db,
	ExternalHooks,
	IWorkflowBase,
	IWorkflowExecutionDataProcess,
	LoadNodesAndCredentials,
	NodeTypes,
	WorkflowHelpers,
	WorkflowRunner,
} from '../src';

import { getLogger } from '../src/Logger';

export class Execute extends Command {
	static description = '\nExecutes a given workflow';

	static examples = [`$ n8n execute --id=5`, `$ n8n execute --file=workflow.json`];

	static flags = {
		help: flags.help({ char: 'h' }),
		file: flags.string({
			description: 'path to a workflow file to execute',
		}),
		id: flags.string({
			description: 'id of the workflow to execute',
		}),
		rawOutput: flags.boolean({
			description: 'Outputs only JSON data, with no other text',
		}),
	};

	// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
	async run() {
		const logger = getLogger();
		LoggerProxy.init(logger);

		// eslint-disable-next-line @typescript-eslint/no-shadow
		const { flags } = this.parse(Execute);

		// Start directly with the init of the database to improve startup time
		const startDbInitPromise = Db.init();

		// Load all node and credential types
		const loadNodesAndCredentials = LoadNodesAndCredentials();
		const loadNodesAndCredentialsPromise = loadNodesAndCredentials.init();

		if (!flags.id && !flags.file) {
			console.info(`Either option "--id" or "--file" have to be set!`);
			return;
		}

		if (flags.id && flags.file) {
			console.info(`Either "id" or "file" can be set never both!`);
			return;
		}

		let workflowId: string | undefined;
		let workflowData: IWorkflowBase | undefined;
		if (flags.file) {
			// Path to workflow is given
			try {
				// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
				workflowData = JSON.parse(await fs.readFile(flags.file, 'utf8'));
			} catch (error) {
				// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
				if (error.code === 'ENOENT') {
					console.info(`The file "${flags.file}" could not be found.`);
					return;
				}

				throw error;
			}

			// Do a basic check if the data in the file looks right
			// TODO: Later check with the help of TypeScript data if it is valid or not
			if (
				workflowData === undefined ||
				workflowData.nodes === undefined ||
				workflowData.connections === undefined
			) {
				console.info(`The file "${flags.file}" does not contain valid workflow data.`);
				return;
			}
			// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
			workflowId = workflowData.id!.toString();
		}

		// Wait till the database is ready
		await startDbInitPromise;

		if (flags.id) {
			// Id of workflow is given
			workflowId = flags.id;
			// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
			workflowData = await Db.collections.Workflow!.findOne(workflowId);
			if (workflowData === undefined) {
				console.info(`The workflow with the id "${workflowId}" does not exist.`);
				process.exit(1);
			}
		}

		// Make sure the settings exist
		await UserSettings.prepareUserSettings();

		// Wait till the n8n-packages have been read
		await loadNodesAndCredentialsPromise;

		// Load the credentials overwrites if any exist
		const credentialsOverwrites = CredentialsOverwrites();
		await credentialsOverwrites.init();

		// Load all external hooks
		const externalHooks = ExternalHooks();
		await externalHooks.init();

		// Add the found types to an instance other parts of the application can use
		const nodeTypes = NodeTypes();
		await nodeTypes.init(loadNodesAndCredentials.nodeTypes);
		const credentialTypes = CredentialTypes();
		await credentialTypes.init(loadNodesAndCredentials.credentialTypes);

		if (!WorkflowHelpers.isWorkflowIdValid(workflowId)) {
			workflowId = undefined;
		}

		// Check if the workflow contains the required "Start" node
		// "requiredNodeTypes" are also defined in editor-ui/views/NodeView.vue
		const requiredNodeTypes = ['n8n-nodes-base.start'];
		let startNode: INode | undefined;
		// eslint-disable-next-line no-restricted-syntax, @typescript-eslint/no-non-null-assertion
		for (const node of workflowData!.nodes) {
			if (requiredNodeTypes.includes(node.type)) {
				startNode = node;
				break;
			}
		}

		if (startNode === undefined) {
			// If the workflow does not contain a start-node we can not know what
			// should be executed and with which data to start.
			console.info(`The workflow does not contain a "Start" node. So it can not be executed.`);
			// eslint-disable-next-line consistent-return
			return Promise.resolve();
		}

		try {
			const runData: IWorkflowExecutionDataProcess = {
				executionMode: 'cli',
				startNodes: [startNode.name],
				// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
				workflowData: workflowData!,
			};

			const workflowRunner = new WorkflowRunner();
			const executionId = await workflowRunner.run(runData);

			const activeExecutions = ActiveExecutions.getInstance();
			const data = await activeExecutions.getPostExecutePromise(executionId);

			if (data === undefined) {
				throw new Error('Workflow did not return any data!');
			}

			if (data.data.resultData.error) {
				console.info('Execution was NOT successful. See log message for details.');
				logger.info('Execution error:');
				logger.info('====================================');
				logger.info(JSON.stringify(data, null, 2));

				const { error } = data.data.resultData;
				// eslint-disable-next-line @typescript-eslint/no-throw-literal
				throw {
					...error,
					stack: error.stack,
				};
			}
			if (flags.rawOutput === undefined) {
				this.log('Execution was successful:');
				this.log('====================================');
			}
			this.log(JSON.stringify(data, null, 2));
		} catch (e) {
			console.error('Error executing workflow. See log messages for details.');
			logger.error('\nExecution error:');
			logger.info('====================================');
			logger.error(e.message);
			logger.error(e.stack);
			this.exit(1);
		}

		this.exit();
	}
}


import {
	OptionsWithUri,
} from 'request';

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

import {
	IDataObject, NodeApiError, NodeOperationError,
} from 'n8n-workflow';

export async function eventbriteApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
	let options: OptionsWithUri = {
		headers: {},
		method,
		qs,
		body,
		uri: uri ||`https://www.eventbriteapi.com/v3${resource}`,
		json: true,
	};
	options = Object.assign({}, options, option);
	if (Object.keys(options.body).length === 0) {
		delete options.body;
	}

	const authenticationMethod = this.getNodeParameter('authentication', 0);

	try {
		if (authenticationMethod === 'privateKey') {
			const credentials = await this.getCredentials('eventbriteApi');
			if (credentials === undefined) {
				throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
			}

			options.headers!['Authorization'] = `Bearer ${credentials.apiKey}`;

			return await this.helpers.request!(options);
		} else {
			return await this.helpers.requestOAuth2!.call(this, 'eventbriteOAuth2Api', options);
		}
	} catch (error) {
		throw new NodeApiError(this.getNode(), error);
	}
}

/**
 * Make an API request to paginated flow endpoint
 * and return all results
 */
export async function eventbriteApiRequestAllItems(this: IHookFunctions | IExecuteFunctions| ILoadOptionsFunctions, propertyName: string, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any

	const returnData: IDataObject[] = [];

	let responseData;

	do {
		responseData = await eventbriteApiRequest.call(this, method, resource, body, query);
		query.continuation = responseData.pagination.continuation;
		returnData.push.apply(returnData, responseData[propertyName]);
	} while (
		responseData.pagination !== undefined &&
		responseData.pagination.has_more_items !== undefined &&
		responseData.pagination.has_more_items !== false
	);

	return returnData;
}


import {
	INodeProperties,
} from 'n8n-workflow';

export const itemOperations = [
	{
		displayName: 'Operation',
		name: 'operation',
		type: 'options',
		default: 'get',
		description: 'Operation to perform',
		options: [
			{
				name: 'Create',
				value: 'create',
			},
			{
				name: 'Delete',
				value: 'delete',
			},
			{
				name: 'Get',
				value: 'get',
			},
			{
				name: 'Get All',
				value: 'getAll',
			},
			{
				name: 'Update',
				value: 'update',
			},
		],
		displayOptions: {
			show: {
				resource: [
					'item',
				],
			},
		},
	},
] as INodeProperties[];

export const itemFields = [
	// ----------------------------------
	//         item: create
	// ----------------------------------
	{
		displayName: 'Site ID',
		name: 'siteId',
		type: 'options',
		required: true,
		typeOptions: {
			loadOptionsMethod: 'getSites',
		},
		default: '',
		displayOptions: {
			show: {
				resource: [
					'item',
				],
				operation: [
					'create',
				],
			},
		},
		description: 'ID of the site containing the collection whose items to add to.',
	},
	{
		displayName: 'Collection ID',
		name: 'collectionId',
		type: 'options',
		required: true,
		typeOptions: {
			loadOptionsMethod: 'getCollections',
			loadOptionsDependsOn: [
				'siteId',
			],
		},
		default: '',
		displayOptions: {
			show: {
				resource: [
					'item',
				],
				operation: [
					'create',
				],
			},
		},
		description: 'ID of the collection to add an item to.',
	},
	{
		displayName: 'Live',
		name: 'live',
		type: 'boolean',
		required: true,
		default: false,
		displayOptions: {
			show: {
				resource: [
					'item',
				],
				operation: [
					'create',
				],
			},
		},
		description: 'Whether the item should be published on the live site.',
	},
	{
		displayName: 'Fields',
		name: 'fieldsUi',
		placeholder: 'Add Field',
		type: 'fixedCollection',
		typeOptions: {
			multipleValues: true,
		},
		default: {},
		displayOptions: {
			show: {
				resource: [
					'item',
				],
				operation: [
					'create',
				],
			},
		},
		options: [
			{
				displayName: 'Field',
				name: 'fieldValues',
				values: [
					{
						displayName: 'Field ID',
						name: 'fieldId',
						type: 'options',
						typeOptions: {
							loadOptionsMethod: 'getFields',
							loadOptionsDependsOn: [
								'collectionId',
							],
						},
						default: '',
						description: 'Field to set for the item to create.',
					},
					{
						displayName: 'Field Value',
						name: 'fieldValue',
						type: 'string',
						default: '',
						description: 'Value to set for the item to create.',
					},
				],
			},
		],
	},

	// ----------------------------------
	//         item: get
	// ----------------------------------
	{
		displayName: 'Site ID',
		name: 'siteId',
		type: 'options',
		required: true,
		typeOptions: {
			loadOptionsMethod: 'getSites',
		},
		default: '',
		displayOptions: {
			show: {
				resource: [
					'item',
				],
				operation: [
					'delete',
					'get',
				],
			},
		},
		description: 'ID of the site containing the collection whose items to operate on.',
	},
	{
		displayName: 'Collection ID',
		name: 'collectionId',
		type: 'options',
		required: true,
		typeOptions: {
			loadOptionsMethod: 'getCollections',
			loadOptionsDependsOn: [
				'siteId',
			],
		},
		default: '',
		displayOptions: {
			show: {
				resource: [
					'item',
				],
				operation: [
					'delete',
					'get',
				],
			},
		},
		description: 'ID of the collection whose items to operate on.',
	},
	{
		displayName: 'Item ID',
		name: 'itemId',
		type: 'string',
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: [
					'item',
				],
				operation: [
					'delete',
					'get',
				],
			},
		},
		description: 'ID of the item to operate on.',
	},
	// ----------------------------------
	//         item: update
	// ----------------------------------
	{
		displayName: 'Site ID',
		name: 'siteId',
		type: 'options',
		required: true,
		typeOptions: {
			loadOptionsMethod: 'getSites',
		},
		default: '',
		displayOptions: {
			show: {
				resource: [
					'item',
				],
				operation: [
					'update',
				],
			},
		},
		description: 'ID of the site containing the collection whose items to update.',
	},
	{
		displayName: 'Collection ID',
		name: 'collectionId',
		type: 'options',
		required: true,
		typeOptions: {
			loadOptionsMethod: 'getCollections',
			loadOptionsDependsOn: [
				'siteId',
			],
		},
		default: '',
		displayOptions: {
			show: {
				resource: [
					'item',
				],
				operation: [
					'update',
				],
			},
		},
		description: 'ID of the collection whose items to update.',
	},
	{
		displayName: 'Item ID',
		name: 'itemId',
		type: 'string',
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: [
					'item',
				],
				operation: [
					'update',
				],
			},
		},
		description: 'ID of the item to update.',
	},
	{
		displayName: 'Live',
		name: 'live',
		type: 'boolean',
		required: true,
		default: false,
		displayOptions: {
			show: {
				resource: [
					'item',
				],
				operation: [
					'update',
				],
			},
		},
		description: 'Whether the item should be published on the live site.',
	},
	{
		displayName: 'Fields',
		name: 'fieldsUi',
		placeholder: 'Add Field',
		type: 'fixedCollection',
		typeOptions: {
			multipleValues: true,
		},
		default: {},
		displayOptions: {
			show: {
				resource: [
					'item',
				],
				operation: [
					'update',
				],
			},
		},
		options: [
			{
				displayName: 'Field',
				name: 'fieldValues',
				values: [
					{
						displayName: 'Field ID',
						name: 'fieldId',
						type: 'options',
						typeOptions: {
							loadOptionsMethod: 'getFields',
							loadOptionsDependsOn: [
								'collectionId',
							],
						},
						default: '',
						description: 'Field to set for the item to update.',
					},
					{
						displayName: 'Field Value',
						name: 'fieldValue',
						type: 'string',
						default: '',
						description: 'Value to set for the item to update.',
					},
				],
			},
		],
	},
	// ----------------------------------
	//         item:getAll
	// ----------------------------------
	{
		displayName: 'Site ID',
		name: 'siteId',
		type: 'options',
		required: true,
		typeOptions: {
			loadOptionsMethod: 'getSites',
		},
		default: '',
		displayOptions: {
			show: {
				resource: [
					'item',
				],
				operation: [
					'getAll',
				],
			},
		},
		description: 'ID of the site containing the collection whose items to retrieve.',
	},
	{
		displayName: 'Collection ID',
		name: 'collectionId',
		type: 'options',
		required: true,
		typeOptions: {
			loadOptionsMethod: 'getCollections',
			loadOptionsDependsOn: [
				'siteId',
			],
		},
		default: '',
		displayOptions: {
			show: {
				resource: [
					'item',
				],
				operation: [
					'getAll',
				],
			},
		},
		description: 'ID of the collection whose items to retrieve.',
	},
	{
		displayName: 'Return All',
		name: 'returnAll',
		type: 'boolean',
		displayOptions: {
			show: {
				resource: [
					'item',
				],
				operation: [
					'getAll',
				],
			},
		},
		default: false,
		description: 'If all results should be returned or only up to a given limit.',
	},
	{
		displayName: 'Limit',
		name: 'limit',
		type: 'number',
		displayOptions: {
			show: {
				resource: [
					'item',
				],
				operation: [
					'getAll',
				],
				returnAll: [
					false,
				],
			},
		},
		typeOptions: {
			minValue: 1,
			maxValue: 100,
		},
		default: 100,
		description: 'How many results to return.',
	},
] as INodeProperties[];


import {
	IDataObject,
} from 'n8n-workflow';
export enum AlertStatus {
	NEW = 'New',
	UPDATED = 'Updated',
	IGNORED = 'Ignored',
	IMPORTED = 'Imported',
}
export enum TLP {
	white,
	green,
	amber,
	red,
}

export interface IAlert {
	// Required attributes
	id?: string;
	title?: string;
	description?: string;
	severity?: number;
	date?: Date;
	tags?: string[];
	tlp?: TLP;
	status?: AlertStatus;
	type?: string;
	source?: string;
	sourceRef?: string;
	artifacts?: IDataObject[];
	follow?: boolean;

	// Optional attributes
	caseTemplate?: string;

	// Backend generated attributes
	lastSyncDate?: Date;
	case?: string;

	createdBy?: string;
	createdAt?: Date;
	updatedBy?: string;
	upadtedAt?: Date;
}


static executionTimeout = 3 * 60 * 1000;

`$ n8n executeAll --concurrency=10 --skipList=/data/skipList.txt`,

`$ n8n executeAll --debug --output=/data/output.json`,

`$ n8n executeAll --ids=10,13,15 --shortOutput`,

`$ n8n executeAll --snapshot=/data/snapshots --shallow`,

`$ n8n executeAll --compare=/data/previousExecutionData --retries=2`,

keep keep keep keep keep replace replace replace replace replace replace keep keep keep keep keep

`$ n8n executeBatch --concurrency=10 --skipList=/data/skipList.txt`,

`$ n8n executeBatch --debug --output=/data/output.json`,

`$ n8n executeBatch --ids=10,13,15 --shortOutput`,

`$ n8n executeBatch --snapshot=/data/snapshots --shallow`,

`$ n8n executeBatch --compare=/data/previousExecutionData --retries=2`,


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

import {
	IDataObject,
	INodeExecutionData,
	INodeType,
	INodeTypeDescription,
} from 'n8n-workflow';

import {
	pipelineFields,
	pipelineOperations,
} from './PipelineDescription';

import {
	circleciApiRequest,
	circleciApiRequestAllItems,
} from './GenericFunctions';

export class CircleCi implements INodeType {
	description: INodeTypeDescription = {
		displayName: 'CircleCI',
		name: 'circleCi',
		icon: 'file:circleCi.png',
		group: ['output'],
		version: 1,
		subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
		description: 'Consume CircleCI API',
		defaults: {
			name: 'CircleCI',
			color: '#04AA51',
		},
		inputs: ['main'],
		outputs: ['main'],
		credentials: [
			{
				name: 'circleCiApi',
				required: true,
			},
		],
		properties: [
			{
				displayName: 'Resource',
				name: 'resource',
				type: 'options',
				options: [
					{
						name: ' Pipeline',
						value: 'pipeline',
					},
				],
				default: 'pipeline',
				description: 'Resource to consume.',
			},
			...pipelineOperations,
			...pipelineFields,
		],
	};

	async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
		const items = this.getInputData();
		const returnData: IDataObject[] = [];
		const length = items.length as unknown as number;
		const qs: IDataObject = {};
		let responseData;
		const resource = this.getNodeParameter('resource', 0) as string;
		const operation = this.getNodeParameter('operation', 0) as string;

		for (let i = 0; i < length; i++) {
			try {
				if (resource === 'pipeline') {
					if (operation === 'get') {
						const vcs = this.getNodeParameter('vcs', i) as string;
						let slug = this.getNodeParameter('projectSlug', i) as string;
						const pipelineNumber = this.getNodeParameter('pipelineNumber', i) as number;

						slug = slug.replace(new RegExp(/\//g), '%2F');

						const endpoint = `/project/${vcs}/${slug}/pipeline/${pipelineNumber}`;

						responseData = await circleciApiRequest.call(this, 'GET', endpoint, {}, qs);
					}
					if (operation === 'getAll') {
						const vcs = this.getNodeParameter('vcs', i) as string;
						const filters = this.getNodeParameter('filters', i) as IDataObject;
						const returnAll = this.getNodeParameter('returnAll', i) as boolean;
						let slug = this.getNodeParameter('projectSlug', i) as string;

						slug = slug.replace(new RegExp(/\//g), '%2F');

						if (filters.branch) {
							qs.branch = filters.branch;
						}

						const endpoint = `/project/${vcs}/${slug}/pipeline`;

						if (returnAll === true) {
							responseData = await circleciApiRequestAllItems.call(this, 'items', 'GET', endpoint, {}, qs);

						} else {
							qs.limit = this.getNodeParameter('limit', i) as number;
							responseData = await circleciApiRequest.call(this, 'GET', endpoint, {}, qs);
							responseData = responseData.items;
							responseData = responseData.splice(0, qs.limit);
						}
					}

					if (operation === 'trigger') {
						const vcs = this.getNodeParameter('vcs', i) as string;
						let slug = this.getNodeParameter('projectSlug', i) as string;

						const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;

						slug = slug.replace(new RegExp(/\//g), '%2F');

						const endpoint = `/project/${vcs}/${slug}/pipeline`;

						const body: IDataObject = {};

						if (additionalFields.branch) {
							body.branch = additionalFields.branch as string;
						}

						if (additionalFields.tag) {
							body.tag = additionalFields.tag as string;
						}

						responseData = await circleciApiRequest.call(this, 'POST', endpoint, body, qs);
					}
				}
				if (Array.isArray(responseData)) {
					returnData.push.apply(returnData, responseData as IDataObject[]);
				} else {
					returnData.push(responseData as IDataObject);
				}
			} catch (error) {
				if (this.continueOnFail()) {
					returnData.push({ error: error.message });
					continue;
				}
				throw error;
			}
		}
		return [this.helpers.returnJsonArray(returnData)];
	}
}


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

import {
	OptionsWithUri,
} from 'request';

import {
	IDataObject,
	ILoadOptionsFunctions,
	NodeApiError,
	NodeOperationError,
} from 'n8n-workflow';

import {
	Accumulator,
	BaserowCredentials,
	LoadedResource,
} from './types';

/**
 * Make a request to Baserow API.
 */
export async function baserowApiRequest(
	this: IExecuteFunctions | ILoadOptionsFunctions,
	method: string,
	endpoint: string,
	body: IDataObject = {},
	qs: IDataObject = {},
	jwtToken: string,
) {
	const credentials = await this.getCredentials('baserowApi') as BaserowCredentials;

	if (credentials === undefined) {
		throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
	}

	const options: OptionsWithUri = {
		headers: {
			Authorization: `JWT ${jwtToken}`,
		},
		method,
		body,
		qs,
		uri: `${credentials.host}${endpoint}`,
		json: true,
	};

	if (Object.keys(qs).length === 0) {
		delete options.qs;
	}

	if (Object.keys(body).length === 0) {
		delete options.body;
	}

	try {
		return await this.helpers.request!(options);
	} catch (error) {
		throw new NodeApiError(this.getNode(), error);
	}
}

/**
 * Get all results from a paginated query to Baserow API.
 */
export async function baserowApiRequestAllItems(
	this: IExecuteFunctions,
	method: string,
	endpoint: string,
	body: IDataObject,
	qs: IDataObject = {},
	jwtToken: string,
): Promise<IDataObject[]> {
	const returnData: IDataObject[] = [];
	let responseData;

	qs.page = 1;
	qs.size = 100;

	const returnAll = this.getNodeParameter('returnAll', 0, false) as boolean;
	const limit = this.getNodeParameter('limit', 0, 0) as number;

	do {
		responseData = await baserowApiRequest.call(this, method, endpoint, body, qs, jwtToken);
		returnData.push(...responseData.results);

		if (!returnAll && returnData.length > limit) {
			return returnData.slice(0, limit);
		}

		qs.page += 1;
	} while (responseData.next !== null);

	return returnData;
}

/**
 * Get a JWT token based on Baserow account username and password.
 */
export async function getJwtToken(
	this: IExecuteFunctions | ILoadOptionsFunctions,
	{ username, password, host }: BaserowCredentials,
) {
	const options: OptionsWithUri = {
		method: 'POST',
		body: {
			username,
			password,
		},
		uri: `${host}/api/user/token-auth/`,
		json: true,
	};

	try {
		const { token } = await this.helpers.request!(options) as { token: string };
		return token;
	} catch (error) {
		throw new NodeApiError(this.getNode(), error);
	}
}

export async function getFieldNamesAndIds(
	this: IExecuteFunctions,
	tableId: string,
	jwtToken: string,
) {
	const endpoint = `/api/database/fields/table/${tableId}/`;
	const response = await baserowApiRequest.call(this, 'GET', endpoint, {}, {}, jwtToken) as LoadedResource[];

	return {
		names: response.map((field) => field.name),
		ids: response.map((field) => `field_${field.id}`),
	};
}

export const toOptions = (items: LoadedResource[]) =>
	items.map(({ name, id }) => ({ name, value: id }));

/**
 * Responsible for mapping field IDs `field_n` to names and vice versa.
 */
export class TableFieldMapper {
	nameToIdMapping: Record<string, string> = {};
	idToNameMapping: Record<string, string> = {};
	mapIds = true;

	async getTableFields(
		this: IExecuteFunctions,
		table: string,
		jwtToken: string,
	): Promise<LoadedResource[]> {
		const endpoint = `/api/database/fields/table/${table}/`;
		return await baserowApiRequest.call(this, 'GET', endpoint, {}, {}, jwtToken);
	}

	createMappings(tableFields: LoadedResource[]) {
		this.nameToIdMapping = this.createNameToIdMapping(tableFields);
		this.idToNameMapping = this.createIdToNameMapping(tableFields);
	}

	private createIdToNameMapping(responseData: LoadedResource[]) {
		return responseData.reduce<Accumulator>((acc, cur) => {
			acc[`field_${cur.id}`] = cur.name;
			return acc;
		}, {});
	}

	private createNameToIdMapping(responseData: LoadedResource[]) {
		return responseData.reduce<Accumulator>((acc, cur) => {
			acc[cur.name] = `field_${cur.id}`;
			return acc;
		}, {});
	}

	setField(field: string) {
		return this.mapIds ? field : this.nameToIdMapping[field] ?? field;
	}

	idsToNames(obj: Record<string, unknown>) {
		Object.entries(obj).forEach(([key, value]) => {
			if (this.idToNameMapping[key] !== undefined) {
				delete obj[key];
				obj[this.idToNameMapping[key]] = value;
			}
		});
	}

	 namesToIds(obj: Record<string, unknown>) {
		Object.entries(obj).forEach(([key, value]) => {
			if (this.nameToIdMapping[key] !== undefined) {
				delete obj[key];
				obj[this.nameToIdMapping[key]] = value;
			}
		});
	}
}


import {
	ICredentialType,
	INodeProperties,
} from 'n8n-workflow';

export class SeaTableApi implements ICredentialType {
	name = 'seaTableApi';
	displayName = 'SeaTable API';
	documentationUrl = 'seaTable';
	properties: INodeProperties[] = [
		{
			displayName: 'Environment',
			name: 'environment',
			type: 'options',
			default: 'cloudHosted',
			options: [
				{
					name: 'Cloud-hosted',
					value: 'cloudHosted',
				},
				{
					name: 'Self-hosted',
					value: 'selfHosted',
				},
			],
		},
		{
			displayName: 'Self-hosted domain',
			name: 'domain',
			type: 'string',
			default: '',
			placeholder: 'https://www.mydomain.com',
			displayOptions: {
				show: {
					environment: [
						'selfHosted',
					],
				},
			},
		},
		{
			displayName: 'API Token (of a Base)',
			name: 'token',
			type: 'string',
			default: '',
		},
	];
}


import {
	INodeProperties,
} from 'n8n-workflow';

export const feedOperations: INodeProperties[] = [
	{
		displayName: 'Operation',
		name: 'operation',
		type: 'options',
		displayOptions: {
			show: {
				resource: [
					'feed',
				],
			},
		},
		noDataExpression: true,
		options: [
			{
				name: 'Create',
				value: 'create',
			},
			{
				name: 'Disable',
				value: 'disable',
			},
			{
				name: 'Enable',
				value: 'enable',
			},
			{
				name: 'Get',
				value: 'get',
			},
			{
				name: 'Get All',
				value: 'getAll',
			},
			{
				name: 'Update',
				value: 'update',
			},
		],
		default: 'create',
	},
];

export const feedFields: INodeProperties[] = [
	// ----------------------------------------
	//               feed: create
	// ----------------------------------------
	{
		displayName: 'Name',
		name: 'name',
		type: 'string',
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: [
					'feed',
				],
				operation: [
					'create',
				],
			},
		},
	},
	{
		displayName: 'Provider',
		name: 'provider',
		type: 'string',
		default: '',
		required: true,
		displayOptions: {
			show: {
				resource: [
					'feed',
				],
				operation: [
					'create',
				],
			},
		},
	},
	{
		displayName: 'URL',
		name: 'url',
		type: 'string',
		default: '',
		placeholder: 'https://example.com',
		required: true,
		displayOptions: {
			show: {
				resource: [
					'feed',
				],
				operation: [
					'create',
				],
			},
		},
	},
	{
		displayName: 'Additional Fields',
		name: 'additionalFields',
		type: 'collection',
		placeholder: 'Add Field',
		default: {},
		displayOptions: {
			show: {
				resource: [
					'feed',
				],
				operation: [
					'create',
				],
			},
		},
		options: [
			{
				displayName: 'Distribution',
				name: 'distribution',
				type: 'options',
				default: 0,
				description: 'Who will be able to see this event once published',
				options: [
					{
						name: 'Your Organization Only',
						value: 0,
					},
					{
						name: 'This Community Only',
						value: 1,
					},
					{
						name: 'Connected Communities',
						value: 2,
					},
					{
						name: 'All Communities',
						value: 3,
					},
					{
						name: 'Sharing Group',
						value: 4,
					},
					{
						name: 'Inherit Event',
						value: 5,
					},
				],
			},
			{
				displayName: 'Rules',
				name: 'json',
				type: 'string',
				default: '',
				description: 'Filter rules for the feed',
			},
		],
	},

	// ----------------------------------------
	//              feed: disable
	// ----------------------------------------
	{
		displayName: 'Feed ID',
		name: 'feedId',
		description: 'UUID or numeric ID of the feed',
		type: 'string',
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: [
					'feed',
				],
				operation: [
					'disable',
				],
			},
		},
	},

	// ----------------------------------------
	//               feed: enable
	// ----------------------------------------
	{
		displayName: 'Feed ID',
		name: 'feedId',
		description: 'UUID or numeric ID of the feed',
		type: 'string',
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: [
					'feed',
				],
				operation: [
					'enable',
				],
			},
		},
	},

	// ----------------------------------------
	//                feed: get
	// ----------------------------------------
	{
		displayName: 'Feed ID',
		name: 'feedId',
		description: 'UUID or numeric ID of the feed',
		type: 'string',
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: [
					'feed',
				],
				operation: [
					'get',
				],
			},
		},
	},
	{
		displayName: 'Return All',
		name: 'returnAll',
		type: 'boolean',
		default: false,
		description: 'Whether to return all results or only up to a given limit',
		displayOptions: {
			show: {
				resource: [
					'feed',
				],
				operation: [
					'getAll',
				],
			},
		},
	},
	{
		displayName: 'Limit',
		name: 'limit',
		type: 'number',
		default: 50,
		description: 'Max number of results to return',
		typeOptions: {
			minValue: 1,
		},
		displayOptions: {
			show: {
				resource: [
					'feed',
				],
				operation: [
					'getAll',
				],
				returnAll: [
					false,
				],
			},
		},
	},

	// ----------------------------------------
	//               feed: update
	// ----------------------------------------
	{
		displayName: 'Feed ID',
		name: 'feedId',
		description: 'ID of the feed to update',
		type: 'string',
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: [
					'feed',
				],
				operation: [
					'update',
				],
			},
		},
	},
	{
		displayName: 'Update Fields',
		name: 'updateFields',
		type: 'collection',
		placeholder: 'Add Field',
		default: {},
		displayOptions: {
			show: {
				resource: [
					'feed',
				],
				operation: [
					'update',
				],
			},
		},
		options: [
			{
				displayName: 'Distribution',
				name: 'distribution',
				type: 'options',
				default: 0,
				description: 'Who will be able to see this event once published',
				options: [
					{
						name: 'Your Organization Only',
						value: 0,
					},
					{
						name: 'This Community Only',
						value: 1,
					},
					{
						name: 'Connected Communities',
						value: 2,
					},
					{
						name: 'All Communities',
						value: 3,
					},
					{
						name: 'Sharing Group',
						value: 4,
					},
					{
						name: 'Inherit Event',
						value: 5,
					},
				],
			},
			{
				displayName: 'Name',
				name: 'name',
				type: 'string',
				default: '',
			},
			{
				displayName: 'Provider',
				name: 'provider',
				type: 'string',
				default: '',
			},
			{
				displayName: 'Rules',
				name: 'rules',
				type: 'json',
				default: '',
				description: 'Filter rules for the feed',
			},
			{
				displayName: 'URL',
				name: 'url',
				type: 'string',
				default: '',
			},
		],
	},
];


import {
	INodeProperties,
} from 'n8n-workflow';

export const customerSourceOperations = [
	{
		displayName: 'Operation',
		name: 'operation',
		type: 'options',
		displayOptions: {
			show: {
				resource: [
					'customerSource',
				],
			},
		},
		options: [
			{
				name: 'Get All',
				value: 'getAll',
			},
		],
		default: 'getAll',
		description: 'Operation to perform',
	},
] as INodeProperties[];

export const customerSourceFields = [
	// ----------------------------------------
	//        customerSource: getAll
	// ----------------------------------------
	{
		displayName: 'Return All',
		name: 'returnAll',
		type: 'boolean',
		default: false,
		description: 'Return all results.',
		displayOptions: {
			show: {
				resource: [
					'customerSource',
				],
				operation: [
					'getAll',
				],
			},
		},
	},
	{
		displayName: 'Limit',
		name: 'limit',
		type: 'number',
		default: 5,
		description: 'The number of results to return.',
		typeOptions: {
			minValue: 1,
			maxValue: 1000,
		},
		displayOptions: {
			show: {
				resource: [
					'customerSource',
				],
				operation: [
					'getAll',
				],
				returnAll: [
					false,
				],
			},
		},
	},
] as INodeProperties[];


/**
 * Captures any pasted data and sends it to method "receivedCopyPasteData" which has to be
 * defined on the component which uses this mixin
 */
import Vue from 'vue';
import { debounce } from 'lodash';

export const copyPaste = Vue.extend({
	data () {
		return {
			copyPasteElementsGotCreated: false,
		};
	},
	mounted () {
		if (this.copyPasteElementsGotCreated === true) {
			return;
		}

		this.copyPasteElementsGotCreated = true;
		// Define the style of the html elements that get created to make
		// sure that they are not visible
		const style = document.createElement('style');
		style.type = 'text/css';
		style.innerHTML = `
			.hidden-copy-paste {
			position: absolute;
			bottom: 0;
			left: 0;
			width: 10px;
			height: 10px;
			display: block;
			font-size: 1px;
			z-index: -1;
			color: transparent;
			background: transparent;
			overflow: hidden;
			border: none;
			padding: 0;
			resize: none;
			outline: none;
			-webkit-user-select: text;
			user-select: text;
		}
		`;
		document.getElementsByTagName('head')[0].appendChild(style);

		// Code is mainly from
		// https://www.lucidchart.com/techblog/2014/12/02/definitive-guide-copying-pasting-javascript/
		const isSafari = navigator.appVersion.search('Safari') !== -1 && navigator.appVersion.search('Chrome') === -1 && navigator.appVersion.search('CrMo') === -1 && navigator.appVersion.search('CriOS') === -1;
		const isIe = (navigator.userAgent.toLowerCase().indexOf('msie') !== -1 || navigator.userAgent.toLowerCase().indexOf('trident') !== -1);

		const hiddenInput = document.createElement('input');
		hiddenInput.setAttribute('type', 'text');
		hiddenInput.setAttribute('id', 'hidden-input-copy-paste');
		hiddenInput.setAttribute('class', 'hidden-copy-paste');

		document.body.append(hiddenInput);

		let ieClipboardDiv: HTMLDivElement | null = null;
		if (isIe) {
			ieClipboardDiv = document.createElement('div');
			ieClipboardDiv.setAttribute('id', 'hidden-ie-clipboard-copy-paste');
			ieClipboardDiv.setAttribute('class', 'hidden-copy-paste');
			ieClipboardDiv.setAttribute('contenteditable', 'true');
			document.body.append(ieClipboardDiv);

			document.addEventListener('beforepaste', () => {
				// @ts-ignore
				if (hiddenInput.is(':focus')) {
					this.focusIeClipboardDiv(ieClipboardDiv as HTMLDivElement);
				}
			}, true);
		}

		let userInput = '';
		const hiddenInputListener = (text: string) => { };

		hiddenInput.addEventListener('input', (e) => {
			const value = hiddenInput.value;
			userInput += value;
			hiddenInputListener(userInput);

			// There is a bug (sometimes) with Safari and the input area can't be updated during
			// the input event, so we update the input area after the event is done being processed
			if (isSafari) {
				hiddenInput.focus();
				setTimeout(() => { this.focusHiddenArea(hiddenInput); }, 0);
			} else {
				this.focusHiddenArea(hiddenInput);
			}
		});

		// Set clipboard event listeners on the document.
		['paste'].forEach((event) => {
			document.addEventListener(event, debounce((e) => {
				// Check if the event got emitted from a message box or from something
				// else which should ignore the copy/paste
				// @ts-ignore
				const path = e.path || (e.composedPath && e.composedPath());
				for (let index = 0; index < path.length; index++) {
					if (path[index].className && typeof path[index].className === 'string' && (
						path[index].className.includes('el-message-box') || path[index].className.includes('ignore-key-press')
					)) {
						return;
					}
				}

				if (ieClipboardDiv !== null) {
					this.ieClipboardEvent(event, ieClipboardDiv);
				} else {
					this.standardClipboardEvent(event, e as ClipboardEvent);
					// @ts-ignore
					if (!document.activeElement || (document.activeElement && ['textarea', 'text', 'email', 'password'].indexOf(document.activeElement.type) === -1)) {
						// That it still allows to paste into text, email, password & textarea-fiels we
						// check if we can identify the active element and if so only
						// run it if something else is selected.
						this.focusHiddenArea(hiddenInput);
						e.preventDefault();
					}
				}
			}, 1000, { leading: true }));
		});
	},
	methods: {
		receivedCopyPasteData (plainTextData: string, event?: ClipboardEvent): void {
			// THIS HAS TO BE DEFINED IN COMPONENT!
		},

		// For every browser except IE, we can easily get and set data on the clipboard
		standardClipboardEvent (clipboardEventName: string, event: ClipboardEvent) {
			const clipboardData = event.clipboardData;
			if (clipboardData !== null && clipboardEventName === 'paste') {
				const clipboardText = clipboardData.getData('text/plain');
				this.receivedCopyPasteData(clipboardText, event);
			}
		},

		// For IE, we can get/set Text or URL just as we normally would
		ieClipboardEvent (clipboardEventName: string, ieClipboardDiv: HTMLDivElement) {
			// @ts-ignore
			const clipboardData = window.clipboardData;
			if (clipboardEventName === 'paste') {
				const clipboardText = clipboardData.getData('Text');
				// @ts-ignore
				ieClipboardDiv.empty();
				this.receivedCopyPasteData(clipboardText);
			}
		},

		// Focuses an element to be ready for copy/paste (used exclusively for IE)
		focusIeClipboardDiv (ieClipboardDiv: HTMLDivElement) {
			ieClipboardDiv.focus();
			const range = document.createRange();
			// @ts-ignore
			range.selectNodeContents((ieClipboardDiv.get(0)));
			const selection = window.getSelection();
			if (selection !== null) {
				selection.removeAllRanges();
				selection.addRange(range);
			}
		},

		focusHiddenArea (hiddenInput: HTMLInputElement) {
			// In order to ensure that the browser will fire clipboard events, we always need to have something selected
			hiddenInput.value = ' ';
			hiddenInput.focus();
			hiddenInput.select();
		},

		/**
		 * Copies given data to clipboard
		 */
		copyToClipboard (value: string): void {
			// FROM: https://hackernoon.com/copying-text-to-clipboard-with-javascript-df4d4988697f
			const element = document.createElement('textarea'); // Create a <textarea> element
			element.value = value; // Set its value to the string that you want copied
			element.setAttribute('readonly', ''); // Make it readonly to be tamper-proof
			element.style.position = 'absolute';
			element.style.left = '-9999px'; // Move outside the screen to make it invisible
			document.body.appendChild(element); // Append the <textarea> element to the HTML document

			const selection = document.getSelection();
			if (selection === null) {
				return;
			}

			const selected = selection.rangeCount > 0 // Check if there is any content selected previously
				? selection.getRangeAt(0) // Store selection if found
				: false; // Mark as false to know no selection existed before
			element.select(); // Select the <textarea> content
			document.execCommand('copy'); // Copy - only works as a result of a user action (e.g. click events)
			document.body.removeChild(element); // Remove the <textarea> element
			if (selected) {
				// If a selection existed before copying
				selection.removeAllRanges(); // Unselect everything on the HTML document
				selection.addRange(selected); // Restore the original selection
			}
		},

	},
});


keep keep keep add keep keep keep keep keep

.list
  lists()
  padding-top: $list-top-padding
  padding-bottom: $list-bottom-padding
  transition: height .3s $transition.ease-in-out

  .input-group
    margin: 0

  &__sub-header
    height: $list-item-single-height
    display: flex
    align-items: center
    font-size: $list-sub-header-height-font-size
    color: $grey.darken-1
    font-weight: $list-sub-header-font-weight
    padding: 0 $list-right-padding 0 $list-left-padding

    &--inset
      margin-left: $list-sub-header-inset-margin

  > .list__item ~ .list__item
    margin-top: 0

  &__item
    position: relative

    li.disabled
      pointer-events: none

  &__tile
    color: $color-root
    display: flex
    height: $list-item-single-height
    text-decoration: none
    align-items: center
    padding: 0 16px
    margin: 0
    transition: $primary-transition
    -webkit-tap-highlight-color: rgba(0, 0, 0, 0)

    &:after
      content: ''
      position: absolute
      left: 0
      top: 0
      height: 1px
      opacity: 0
      width: 100%
      transition: $primary-transition
      background-color: $material-twelve-percent-dark

    a&:hover
      background: $material-twelve-percent-dark

    &__content,
    &__action,
    &__avatar
      height: 100%

    &__title, &__sub-title
      white-space: nowrap
      overflow: hidden
      text-overflow: ellipsis
      width: 100%

    &__title
      font-size: $list-title-font-size
      line-height: 1.15
      font-weight: $list-title-font-weight
      transition: $primary-transition
      position: relative

    &__sub-title
      color: $grey.base
      font-size: $list-sub-title-font-size
      line-height: 1.3
      font-weight: $list-sub-title-font-weight

    &__action, &__avatar
      display: flex
      justify-content: flex-start
      align-items: center
      flex: 1 0 56px

    &__action
      align-items: flex-start
      flex-direction: column
      justify-content: center

      .icon
        transition: $primary-transition

      &-text
        color: $grey.base
        font-size: $list-action-text-font-size

      &--stack
        align-items: flex-end
        justify-content: space-between
        padding-top: $list-top-padding
        padding-bottom: $list-bottom-padding

    &__avatar
      img, .icon
        height: 42px
        width: 42px
        border-radius: 50%

    &__content
      text-align: left
      flex: 0 1 100%
      font-size: $list-content-font-size
      overflow: hidden
      display: flex
      align-items: flex-start
      justify-content: center
      flex-direction: column

      + .list__tile__avatar
        justify-content: flex-end

      + .list__tile__action:not(.list__tile__action--stack)
        flex: 1 0 48px
        align-items: center

    &--active
      .list__tile__title
        color: $theme.primary

    &--disabled
      pointer-events: none
      opacity: 0.4

    &--avatar
      height: $list-item-avatar-single-height

  &--dense
    padding-top: $list-item-dense-top-padding

    .list__tile
      height: $list-item-dense-single-height
      font-size: $list-item-dense-font-size

    .list__tile__title
      font-size: $list-item-dense-font-size
      font-weight: $list-title-dense-font-weight

    .list__tile__sub-title
      font-size: $list-item-dense-font-size

  &--two-line
    .list__tile
      height: $list-item-double-height

    &.list--dense
      .list__tile
        height: $list-item-dense-double-height

  &--three-line
    .list__tile
      height: $list-item-triple-height

    .list__tile__action
      align-items: flex-start

    .list__tile__sub-title
      white-space: initial
      -webkit-line-clamp: 2
      -webkit-box-orient: vertical
      display: -webkit-box

    &.list--dense
      .list__tile
        height: $list-item-dense-triple-height

  &--group
    overflow: hidden
    position: relative
    padding: 0

    &:after
      content: ''
      position: absolute
      left: 0
      bottom: 0
      height: 1px
      opacity: 0
      width: 100%
      background-color: $material-twelve-percent-dark

    .list__tile
      padding-left: $list-item-left-padding

      .list__tile__title
        font-weight: $list-group-tile-title-font-weight

    .list__tile--active
      .list__tile__title
        color: $theme.primary
        font-weight: $list-group-tile-title-active-font-weight

    &__header
      + .list--group
        &:after
          opacity: 1

      &--active
        .list__tile
          background: $material-twelve-percent-dark

          &:after
            opacity: 1

          .list__tile__title
            color: inherit

          .list__tile__action:last-of-type
            .icon
              transform: rotate(-180deg)

  &--sub-header
    padding-top: 0


import Badge from './badge'
import ClickOutside from './click-outside'
import Ripple from './ripple'
import Tooltip from './tooltip'

export default {
  Badge,
  ClickOutside,
  Ripple,
  Tooltip
}


.parallax
  position: relative
  overflow: hidden

  &__image-container
    position: absolute
    top: 0
    left: 0
    right: 0
    bottom: 0
    z-index: 1

  &__image
    position: absolute
    bottom: 0
    left: 50%
    min-width: 100%
    min-height: 100%
    display: none
    z-index: 1

  &__content
    color: #FFFFFF
    height: 100%
    z-index: 2
    position: relative
    display: flex
    flex-direction: column
    justify-content: center
    padding: 0 1rem


.right
  float: right !important

.left
  float: left !important


<template lang="pug">
  div(
    class="list--group__container"
    v-click-outside="closeConditional"
  )
    div(
      class="list--group__header"
      v-bind:class="classes"
      v-on:click="toggle"
    )
      slot(name="item")

    transition(
      v-on:enter="enter"
      v-on:leave="leave"
    )
      ul(
        class="list list--group"
        v-show="isActive"
        v-bind:style="styles"
        ref="group"
      )
        slot
</template>
<script>
  import { closestParentTag, addOnceEventListener } from '../../util/helpers'

  export default {
    name: 'list-group',

    data () {
      return {
        isActive: this.active,
        height: 0
      }
    },

    props: {
      active: Boolean,

      group: String
    },

    computed: {
      classes () {
        return {
          'list--group__header--active': this.isActive
        }
      },

      list () {
        return closestParentTag.call(this, 'v-list')
      },

      styles () {
        return {
          height: `${this.height}px`
        }
      }
    },

    watch: {
      active () {
        this.isActive = this.active
      },

      isActive () {
        if (this.isActive !== this.active) {
          this.$emit('active', this.active)
        }
      },

      '$route' (to) {
        if (this.group) {
          this.isActive = this.matchRoute(to.path)
        }
      }
    },

    mounted () {
      if (this.group) {
        this.isActive = this.matchRoute(this.$route.path)
      }

      this.height = this.$refs.group.scrollHeight
    },

    methods: {
      closeConditional (e) {
        return this.list.$el.contains(e.target)
      },

      enter (el, done) {
        el.style.display = 'block'
        const scrollHeight = el.scrollHeight
        this.height = 0

        setTimeout(() => (this.height = scrollHeight), 50)

        addOnceEventListener(el, 'transitionend', done)
      },

      leave (el, done) {
        this.height = 0
        addOnceEventListener(el, 'transitionend', done)
      },

      matchRoute (to) {
        return to.match(this.group) !== null
      },

      toggle () {
        this.isActive = !this.isActive
      }
    }
  }
</script>


// 1. start the dev server using production config
process.env.NODE_ENV = 'testing'
var server = require('../../build/dev-server.js')

// 2. run the nightwatch test suite against it
// to run in additional browsers:
//    1. add an entry in test/e2e/nightwatch.conf.json under "test_settings"
//    2. add it to the --env flag below
// or override the environment flag, for example: `npm run e2e -- --env chrome,firefox`
// For more information on Nightwatch's config file, see
// http://nightwatchjs.org/guide#settings-file
var opts = process.argv.slice(2)
if (opts.indexOf('--config') === -1) {
  opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js'])
}
if (opts.indexOf('--env') === -1) {
  opts = opts.concat(['--env', 'chrome'])
}

var spawn = require('cross-spawn')
var runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' })

runner.on('exit', function (code) {
  server.close()
  process.exit(code)
})

runner.on('error', function (err) {
  server.close()
  throw err
})


<template lang="pug">
  ul(
    class="breadcrumbs"
    v-bind:class="classes"
    v-bind:items="items"
  )
    slot
</template>

<script>
  export default {
    name: 'breadcrumbs',

    props: {
      divider: {
        type: String,
        default: '/'
      },

      icons: Boolean
    },

    computed: {
      classes () {
        return {
          'breadcrumbs--with-icons': this.icons
        }
      }
    },

    mounted () {
      this.$vuetify.load(this.init)
    },

    methods: {
      init () {
        this.$children.forEach(i => (i.$el.dataset.divider = this.divider))
      }
    }
  }
</script>


import Alerts from './alerts/index'
import App from './app/index'
import Breadcrumbs from './breadcrumbs/index'
import Buttons from './buttons/index'
import Cards from './cards/index'
import Carousel from './carousel/index'
import Chips from './chips/index'
import ExpansionPanel from './expansion-panel/index'
import Dividers from './dividers/index'
import Footer from './footer/index'
import Forms from './forms/index'
import Grid from './grid/index'
import Icons from './icons/index'
import Lists from './lists/index'
import Menu from './menus/index'
import Modal from './modal/index'
import Toolbar from './toolbar/index'
import Overlay from './overlay/index'
import Pagination from './pagination/index'
import Parallax from './parallax/index'
import Progress from './progress/index'
import Sidebar from './sidebar/index'
import Tables from './tables/index'
import Tabs from './tabs/index'
import Transitions from './transitions/_index'

export default Object.assign({},
  Alerts,
  App,
  Breadcrumbs,
  Buttons,
  Cards,
  Carousel,
  Chips,
  Dividers,
  ExpansionPanel,
  Footer,
  Forms,
  Grid,
  Icons,
  Lists,
  Menu,
  Modal,
  Toolbar,
  Overlay,
  Pagination,
  Parallax,
  Progress,
  Sidebar,
  Tables,
  Tabs,
  Transitions
)


-webkit-tap-highlight-color: rgba(0, 0, 0, 0)

keep keep keep keep add keep keep keep keep keep


import { createSimpleTransition } from '../../util/helpers'

const SlideXTransition = createSimpleTransition('slide-x-transition')
const SlideYTransition = createSimpleTransition('slide-y-transition')
const ScaleTransition = createSimpleTransition('scale-transition')
const TabTransition = createSimpleTransition('tab-transition')
const TabReverseTransition = createSimpleTransition('tab-reverse-transition')
const CarouselTransition = createSimpleTransition('carousel-transition')
const CarouselReverseTransition = createSimpleTransition('carousel-reverse-transition')
const ModalTransition = createSimpleTransition('modal-transition')
const ModalBottomTransition = createSimpleTransition('modal-bottom-transition')
const FadeTransition = createSimpleTransition('fade-transition')
const MenuTransition = createSimpleTransition('menu-transition')

export default {
  SlideXTransition,
  SlideYTransition,
  ScaleTransition,
  FadeTransition,
  TabTransition,
  TabReverseTransition,
  ModalTransition,
  ModalBottomTransition,
  MenuTransition,
  CarouselTransition,
  CarouselReverseTransition
}


<template lang="pug">
  nav(
    class="toolbar"
    v-bind:class="classes"
  )
    slot
</template>

<script>
  export default {
    name: 'toolbar',

    props: {
      fixed: Boolean
    },

    computed: {
      classes () {
        return {
          'toolbar--fixed': this.fixed
        }
      }
    }
  }
</script>


.slider
  height: 500px
  width: 100%
  background: #000000
  position: relative
  overflow: hidden
  @extend .z-depth-1

  &__item
    display: flex
    align-items: center
    justify-content: center
    flex: 1 0 100%
    height: 100%
    width: 100%
    background-size: cover
    background-position: center center
    transition: .2s ease-out

  &__left
    position: absolute
    top: 50%
    z-index: 2
    left: 5px
    transform: translateY(-50%)

    .btn
      color: #FFFFFF
      margin: 0 !important
      height: auto
      width: auto

      i
        font-size: 48px

      &:hover
        background: none

  &__right
    @extend .slider__left
    left: initial
    right: 5px

  &__controls
    background: rgba(0, 0, 0, .5)
    align-items: center
    bottom: 0
    display: flex
    justify-content: center
    left: 0
    position: absolute
    height: 50px
    list-style-type: none
    width: 100%
    z-index: 2

    &__item
      color: #FFFFFF
      margin: 0 1rem !important

      i
        opacity: .5
        transition: $primary-transition

      &--active
        i
          opacity: 1
          vertical-align: middle
          font-size: 2rem !important

      &:hover
        background: none

        i
          opacity: .8