import addStream from 'add-stream';
import createTempFile from 'tempfile';
import conventionalChangelog from 'conventional-changelog';
import { resolve } from 'path';
import { createReadStream, createWriteStream } from 'fs';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import stream from 'stream';
import { promisify } from 'util';
import packageJson from '../../package.json' assert { type: 'json' };

const pipeline = promisify(stream.pipeline);

const baseDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
const fullChangelogFile = resolve(baseDir, 'CHANGELOG.md');
const versionChangelogFile = resolve(baseDir, `CHANGELOG-${packageJson.version}.md`);

const changelogStream = conventionalChangelog({
	preset: 'angular',
	releaseCount: 1,
	tagPrefix: 'n8n@',
	transform: (commit, callback) => {
		callback(null, commit.header.includes('(no-changelog)') ? undefined : commit);
	},
}).on('error', (err) => {
	console.error(err.stack);
	process.exit(1);
});

// We need to duplicate the stream here to pipe the changelog into two separate files
const stream1 = new stream.PassThrough();
const stream2 = new stream.PassThrough();
changelogStream.pipe(stream1);
changelogStream.pipe(stream2);

await pipeline(stream1, createWriteStream(versionChangelogFile));

// Since we can't read and write from the same file at the same time,
// we use a temporary file to output the updated changelog to.
const tmpFile = createTempFile();
await pipeline(stream2, addStream(createReadStream(fullChangelogFile)), createWriteStream(tmpFile)),
	await pipeline(createReadStream(tmpFile), createWriteStream(fullChangelogFile));


<template>

			<n8n-heading size="xlarge">
				{{ title }}
			</n8n-heading>

			<n8n-form-inputs
				:inputs="inputs"
				:eventBus="formBus"
				:columnView="true"
				@input="onInput"
				@submit="onSubmit"
			/>

				<n8n-link size="medium" theme="text" @click="onSecondaryButtonClick">
					{{ secondaryButtonText }}
				</n8n-link>

			<n8n-button
				v-if="buttonText"
				:label="buttonText"
				:loading="buttonLoading"
				size="large"
				@click="onButtonClick"
			/>

			<n8n-link v-if="redirectText && redirectLink" :to="redirectLink">
				{{ redirectText }}
			</n8n-link>

		<slot></slot>

</template>


import type { IExecuteFunctions, IDataObject, INodeExecutionData, JsonObject } from 'n8n-workflow';
import type pgPromise from 'pg-promise';
import type pg from 'pg-promise/typescript/pg-subset';
import { getResolvables } from '@utils/utilities';

/**
 * Returns of a shallow copy of the items which only contains the json data and
 * of that only the define properties
 *
 * @param {INodeExecutionData[]} items The items to copy
 * @param {string[]} properties The properties it should include
 */
export function getItemsCopy(
	items: INodeExecutionData[],
	properties: string[],
	guardedColumns?: { [key: string]: string },
): IDataObject[] {
	let newItem: IDataObject;
	return items.map((item) => {
		newItem = {};
		if (guardedColumns) {
			Object.keys(guardedColumns).forEach((column) => {
				newItem[column] = item.json[guardedColumns[column]];
			});
		} else {
			for (const property of properties) {
				newItem[property] = item.json[property];
			}
		}
		return newItem;
	});
}

/**
 * Returns of a shallow copy of the item which only contains the json data and
 * of that only the define properties
 *
 * @param {INodeExecutionData} item The item to copy
 * @param {string[]} properties The properties it should include
 */
export function getItemCopy(
	item: INodeExecutionData,
	properties: string[],
	guardedColumns?: { [key: string]: string },
): IDataObject {
	const newItem: IDataObject = {};
	if (guardedColumns) {
		Object.keys(guardedColumns).forEach((column) => {
			newItem[column] = item.json[guardedColumns[column]];
		});
	} else {
		for (const property of properties) {
			newItem[property] = item.json[property];
		}
	}
	return newItem;
}

/**
 * Returns a returning clause from a comma separated string
 * @param {pgPromise.IMain<{}, pg.IClient>} pgp The pgPromise instance
 * @param string returning The comma separated string
 */
export function generateReturning(pgp: pgPromise.IMain<{}, pg.IClient>, returning: string): string {
	return (
		' RETURNING ' +
		returning
			.split(',')
			.map((returnedField) => pgp.as.name(returnedField.trim()))
			.join(', ')
	);
}

export function wrapData(data: IDataObject[]): INodeExecutionData[] {
	if (!Array.isArray(data)) {
		return [{ json: data }];
	}
	return data.map((item) => ({
		json: item,
	}));
}

/**
 * Executes the given SQL query on the database.
 *
 * @param {Function} getNodeParam The getter for the Node's parameters
 * @param {pgPromise.IMain<{}, pg.IClient>} pgp The pgPromise instance
 * @param {pgPromise.IDatabase<{}, pg.IClient>} db The pgPromise database connection
 * @param {input[]} input The Node's input data
 */
export async function pgQuery(
	// eslint-disable-next-line @typescript-eslint/ban-types
	getNodeParam: Function,
	pgp: pgPromise.IMain<{}, pg.IClient>,
	db: pgPromise.IDatabase<{}, pg.IClient>,
	items: INodeExecutionData[],
	continueOnFail: boolean,
	overrideMode?: string,
): Promise<IDataObject[]> {
	const additionalFields = getNodeParam('additionalFields', 0) as IDataObject;

	let valuesArray = [] as string[][];
	if (additionalFields.queryParams) {
		const propertiesString = additionalFields.queryParams as string;
		const properties = propertiesString.split(',').map((column) => column.trim());
		const paramsItems = getItemsCopy(items, properties);
		valuesArray = paramsItems.map((row) => properties.map((col) => row[col])) as string[][];
	}

	const allQueries = [] as Array<{ query: string; values?: string[] }>;
	for (let i = 0; i < items.length; i++) {
		const query = getNodeParam('query', i) as string;
		const values = valuesArray[i];
		const queryFormat = { query, values };
		allQueries.push(queryFormat);
	}

	const mode = overrideMode ? overrideMode : ((additionalFields.mode ?? 'multiple') as string);
	if (mode === 'multiple') {
		return (await db.multi(pgp.helpers.concat(allQueries))).flat(1);
	} else if (mode === 'transaction') {
		return db.tx(async (t) => {
			const result: IDataObject[] = [];
			for (let i = 0; i < allQueries.length; i++) {
				try {
					Array.prototype.push.apply(
						result,
						await t.any(allQueries[i].query, allQueries[i].values),
					);
				} catch (err) {
					if (!continueOnFail) throw err;
					result.push({
						...items[i].json,
						code: (err as JsonObject).code,
						message: (err as JsonObject).message,
					});
					return result;
				}
			}
			return result;
		});
	} else if (mode === 'independently') {
		return db.task(async (t) => {
			const result: IDataObject[] = [];
			for (let i = 0; i < allQueries.length; i++) {
				try {
					Array.prototype.push.apply(
						result,
						await t.any(allQueries[i].query, allQueries[i].values),
					);
				} catch (err) {
					if (!continueOnFail) throw err;
					result.push({
						...items[i].json,
						code: (err as JsonObject).code,
						message: (err as JsonObject).message,
					});
				}
			}
			return result;
		});
	}
	throw new Error('multiple, independently or transaction are valid options');
}

export async function pgQueryV2(
	this: IExecuteFunctions,
	pgp: pgPromise.IMain<{}, pg.IClient>,
	db: pgPromise.IDatabase<{}, pg.IClient>,
	items: INodeExecutionData[],
	continueOnFail: boolean,
	options?: {
		overrideMode?: string;
		resolveExpression?: boolean;
	},
): Promise<IDataObject[]> {
	const additionalFields = this.getNodeParameter('additionalFields', 0);

	let valuesArray = [] as string[][];
	if (additionalFields.queryParams) {
		const propertiesString = additionalFields.queryParams as string;
		const properties = propertiesString.split(',').map((column) => column.trim());
		const paramsItems = getItemsCopy(items, properties);
		valuesArray = paramsItems.map((row) => properties.map((col) => row[col])) as string[][];
	}

	type QueryWithValues = { query: string; values?: string[] };
	const allQueries = new Array<QueryWithValues>();
	for (let i = 0; i < items.length; i++) {
		let query = this.getNodeParameter('query', i) as string;

		if (options?.resolveExpression) {
			for (const resolvable of getResolvables(query)) {
				query = query.replace(resolvable, this.evaluateExpression(resolvable, i) as string);
			}
		}

		const values = valuesArray[i];
		const queryFormat = { query, values };
		allQueries.push(queryFormat);
	}

	const mode = options?.overrideMode
		? options.overrideMode
		: ((additionalFields.mode ?? 'multiple') as string);
	if (mode === 'multiple') {
		return (await db.multi(pgp.helpers.concat(allQueries)))
			.map((result, i) => {
				return this.helpers.constructExecutionMetaData(wrapData(result as IDataObject[]), {
					itemData: { item: i },
				});
			})
			.flat();
	} else if (mode === 'transaction') {
		return db.tx(async (t) => {
			const result: INodeExecutionData[] = [];
			for (let i = 0; i < allQueries.length; i++) {
				try {
					const transactionResult = await t.any(allQueries[i].query, allQueries[i].values);
					const executionData = this.helpers.constructExecutionMetaData(
						wrapData(transactionResult as IDataObject[]),
						{ itemData: { item: i } },
					);
					result.push(...executionData);
				} catch (err) {
					if (!continueOnFail) throw err;
					result.push({
						json: { ...items[i].json },
						code: (err as JsonObject).code,
						message: (err as JsonObject).message,
						pairedItem: { item: i },
					} as INodeExecutionData);
					return result;
				}
			}
			return result;
		});
	} else if (mode === 'independently') {
		return db.task(async (t) => {
			const result: INodeExecutionData[] = [];
			for (let i = 0; i < allQueries.length; i++) {
				try {
					const transactionResult = await t.any(allQueries[i].query, allQueries[i].values);
					const executionData = this.helpers.constructExecutionMetaData(
						wrapData(transactionResult as IDataObject[]),
						{ itemData: { item: i } },
					);
					result.push(...executionData);
				} catch (err) {
					if (!continueOnFail) throw err;
					result.push({
						json: { ...items[i].json },
						code: (err as JsonObject).code,
						message: (err as JsonObject).message,
						pairedItem: { item: i },
					} as INodeExecutionData);
				}
			}
			return result;
		});
	}
	throw new Error('multiple, independently or transaction are valid options');
}

/**
 * Inserts the given items into the database.
 *
 * @param {Function} getNodeParam The getter for the Node's parameters
 * @param {pgPromise.IMain<{}, pg.IClient>} pgp The pgPromise instance
 * @param {pgPromise.IDatabase<{}, pg.IClient>} db The pgPromise database connection
 * @param {INodeExecutionData[]} items The items to be inserted
 */
export async function pgInsert(
	// eslint-disable-next-line @typescript-eslint/ban-types
	getNodeParam: Function,
	pgp: pgPromise.IMain<{}, pg.IClient>,
	db: pgPromise.IDatabase<{}, pg.IClient>,
	items: INodeExecutionData[],
	continueOnFail: boolean,
	overrideMode?: string,
): Promise<IDataObject[]> {
	const table = getNodeParam('table', 0) as string;
	const schema = getNodeParam('schema', 0) as string;
	const columnString = getNodeParam('columns', 0) as string;
	const guardedColumns: { [key: string]: string } = {};

	const columns = columnString
		.split(',')
		.map((column) => column.trim().split(':'))
		.map(([name, cast], i) => {
			guardedColumns[`column${i}`] = name;
			return { name, cast, prop: `column${i}` };
		});

	const columnNames = columns.map((column) => column.name);

	const cs = new pgp.helpers.ColumnSet(columns, { table: { table, schema } });

	const additionalFields = getNodeParam('additionalFields', 0) as IDataObject;
	const mode = overrideMode ? overrideMode : ((additionalFields.mode ?? 'multiple') as string);

	const returning = generateReturning(pgp, getNodeParam('returnFields', 0) as string);
	if (mode === 'multiple') {
		const query =
			pgp.helpers.insert(getItemsCopy(items, columnNames, guardedColumns), cs) + returning;
		return db.any(query);
	} else if (mode === 'transaction') {
		return db.tx(async (t) => {
			const result: IDataObject[] = [];
			for (let i = 0; i < items.length; i++) {
				const itemCopy = getItemCopy(items[i], columnNames, guardedColumns);
				try {
					result.push(await t.one(pgp.helpers.insert(itemCopy, cs) + returning));
				} catch (err) {
					if (!continueOnFail) throw err;
					result.push({
						...itemCopy,
						code: (err as JsonObject).code,
						message: (err as JsonObject).message,
					});
					return result;
				}
			}
			return result;
		});
	} else if (mode === 'independently') {
		return db.task(async (t) => {
			const result: IDataObject[] = [];
			for (let i = 0; i < items.length; i++) {
				const itemCopy = getItemCopy(items[i], columnNames, guardedColumns);
				try {
					const insertResult = await t.oneOrNone(pgp.helpers.insert(itemCopy, cs) + returning);
					if (insertResult !== null) {
						result.push(insertResult as IDataObject);
					}
				} catch (err) {
					if (!continueOnFail) {
						throw err;
					}
					result.push({
						...itemCopy,
						code: (err as JsonObject).code,
						message: (err as JsonObject).message,
					});
				}
			}
			return result;
		});
	}

	throw new Error('multiple, independently or transaction are valid options');
}

/**
 * Inserts the given items into the database.
 *
 * @param {Function} getNodeParam The getter for the Node's parameters
 * @param {pgPromise.IMain<{}, pg.IClient>} pgp The pgPromise instance
 * @param {pgPromise.IDatabase<{}, pg.IClient>} db`` The pgPromise database connection
 * @param {INodeExecutionData[]} items The items to be inserted
 */
export async function pgInsertV2(
	this: IExecuteFunctions,
	pgp: pgPromise.IMain<{}, pg.IClient>,
	db: pgPromise.IDatabase<{}, pg.IClient>,
	items: INodeExecutionData[],
	continueOnFail: boolean,
	overrideMode?: string,
): Promise<IDataObject[]> {
	const table = this.getNodeParameter('table', 0) as string;
	const schema = this.getNodeParameter('schema', 0) as string;
	const columnString = this.getNodeParameter('columns', 0) as string;
	const guardedColumns: { [key: string]: string } = {};

	const columns = columnString
		.split(',')
		.map((column) => column.trim().split(':'))
		.map(([name, cast], i) => {
			guardedColumns[`column${i}`] = name;
			return { name, cast, prop: `column${i}` };
		});

	const columnNames = columns.map((column) => column.name);

	const cs = new pgp.helpers.ColumnSet(columns, { table: { table, schema } });

	const additionalFields = this.getNodeParameter('additionalFields', 0);
	const mode = overrideMode ? overrideMode : ((additionalFields.mode ?? 'multiple') as string);

	const returning = generateReturning(pgp, this.getNodeParameter('returnFields', 0) as string);
	if (mode === 'multiple') {
		const query =
			pgp.helpers.insert(getItemsCopy(items, columnNames, guardedColumns), cs) + returning;
		const queryResult = await db.any(query);
		return queryResult
			.map((result, i) => {
				return this.helpers.constructExecutionMetaData(wrapData(result as IDataObject[]), {
					itemData: { item: i },
				});
			})
			.flat();
	} else if (mode === 'transaction') {
		return db.tx(async (t) => {
			const result: IDataObject[] = [];
			for (let i = 0; i < items.length; i++) {
				const itemCopy = getItemCopy(items[i], columnNames, guardedColumns);
				try {
					const insertResult = await t.one(pgp.helpers.insert(itemCopy, cs) + returning);
					result.push(
						...this.helpers.constructExecutionMetaData(wrapData(insertResult as IDataObject[]), {
							itemData: { item: i },
						}),
					);
				} catch (err) {
					if (!continueOnFail) throw err;
					result.push({
						json: { ...itemCopy },
						code: (err as JsonObject).code,
						message: (err as JsonObject).message,
						pairedItem: { item: i },
					} as INodeExecutionData);
					return result;
				}
			}
			return result;
		});
	} else if (mode === 'independently') {
		return db.task(async (t) => {
			const result: IDataObject[] = [];
			for (let i = 0; i < items.length; i++) {
				const itemCopy = getItemCopy(items[i], columnNames, guardedColumns);
				try {
					const insertResult = await t.oneOrNone(pgp.helpers.insert(itemCopy, cs) + returning);
					if (insertResult !== null) {
						const executionData = this.helpers.constructExecutionMetaData(
							wrapData(insertResult as IDataObject[]),
							{
								itemData: { item: i },
							},
						);
						result.push(...executionData);
					}
				} catch (err) {
					if (!continueOnFail) {
						throw err;
					}
					result.push({
						json: { ...itemCopy },
						code: (err as JsonObject).code,
						message: (err as JsonObject).message,
						pairedItem: { item: i },
					} as INodeExecutionData);
				}
			}
			return result;
		});
	}

	throw new Error('multiple, independently or transaction are valid options');
}

/**
 * Updates the given items in the database.
 *
 * @param {Function} getNodeParam The getter for the Node's parameters
 * @param {pgPromise.IMain<{}, pg.IClient>} pgp The pgPromise instance
 * @param {pgPromise.IDatabase<{}, pg.IClient>} db The pgPromise database connection
 * @param {INodeExecutionData[]} items The items to be updated
 */
export async function pgUpdate(
	// eslint-disable-next-line @typescript-eslint/ban-types
	getNodeParam: Function,
	pgp: pgPromise.IMain<{}, pg.IClient>,
	db: pgPromise.IDatabase<{}, pg.IClient>,
	items: INodeExecutionData[],
	continueOnFail = false,
): Promise<IDataObject[]> {
	const table = getNodeParam('table', 0) as string;
	const schema = getNodeParam('schema', 0) as string;
	const updateKey = getNodeParam('updateKey', 0) as string;
	const columnString = getNodeParam('columns', 0) as string;
	const guardedColumns: { [key: string]: string } = {};

	const columns: Array<{ name: string; cast: string; prop: string }> = columnString
		.split(',')
		.map((column) => column.trim().split(':'))
		.map(([name, cast], i) => {
			guardedColumns[`column${i}`] = name;
			return { name, cast, prop: `column${i}` };
		});

	const updateKeys = updateKey.split(',').map((key, i) => {
		const [name, cast] = key.trim().split(':');
		const targetCol = columns.find((column) => column.name === name);
		const updateColumn = { name, cast, prop: targetCol ? targetCol.prop : `updateColumn${i}` };
		if (!targetCol) {
			guardedColumns[updateColumn.prop] = name;
			columns.unshift(updateColumn);
		} else if (!targetCol.cast) {
			targetCol.cast = updateColumn.cast || targetCol.cast;
		}
		return updateColumn;
	});

	const additionalFields = getNodeParam('additionalFields', 0) as IDataObject;
	const mode = additionalFields.mode ?? ('multiple' as string);

	const cs = new pgp.helpers.ColumnSet(columns, { table: { table, schema } });

	// Prepare the data to update and copy it to be returned
	const columnNames = columns.map((column) => column.name);
	const updateItems = getItemsCopy(items, columnNames, guardedColumns);

	const returning = generateReturning(pgp, getNodeParam('returnFields', 0) as string);
	if (mode === 'multiple') {
		const query =
			(pgp.helpers.update(updateItems, cs) as string) +
			' WHERE ' +
			updateKeys
				.map((entry) => {
					const key = pgp.as.name(entry.name);
					return 'v.' + key + ' = t.' + key;
				})
				.join(' AND ') +
			returning;
		return db.any(query);
	} else {
		const where =
			' WHERE ' +
			// eslint-disable-next-line n8n-local-rules/no-interpolation-in-regular-string
			updateKeys.map((entry) => pgp.as.name(entry.name) + ' = ${' + entry.prop + '}').join(' AND ');
		if (mode === 'transaction') {
			return db.tx(async (t) => {
				const result: IDataObject[] = [];
				for (let i = 0; i < items.length; i++) {
					const itemCopy = getItemCopy(items[i], columnNames, guardedColumns);
					try {
						Array.prototype.push.apply(
							result,
							await t.any(
								(pgp.helpers.update(itemCopy, cs) as string) +
									pgp.as.format(where, itemCopy) +
									returning,
							),
						);
					} catch (err) {
						if (!continueOnFail) throw err;
						result.push({
							...itemCopy,
							code: (err as JsonObject).code,
							message: (err as JsonObject).message,
						});
						return result;
					}
				}
				return result;
			});
		} else if (mode === 'independently') {
			return db.task(async (t) => {
				const result: IDataObject[] = [];
				for (let i = 0; i < items.length; i++) {
					const itemCopy = getItemCopy(items[i], columnNames, guardedColumns);
					try {
						Array.prototype.push.apply(
							result,
							await t.any(
								(pgp.helpers.update(itemCopy, cs) as string) +
									pgp.as.format(where, itemCopy) +
									returning,
							),
						);
					} catch (err) {
						if (!continueOnFail) throw err;
						result.push({
							...itemCopy,
							code: (err as JsonObject).code,
							message: (err as JsonObject).message,
						});
					}
				}
				return result;
			});
		}
	}
	throw new Error('multiple, independently or transaction are valid options');
}

/**
 * Updates the given items in the database.
 *
 * @param {Function} getNodeParam The getter for the Node's parameters
 * @param {pgPromise.IMain<{}, pg.IClient>} pgp The pgPromise instance
 * @param {pgPromise.IDatabase<{}, pg.IClient>} db The pgPromise database connection
 * @param {INodeExecutionData[]} items The items to be updated
 */
export async function pgUpdateV2(
	this: IExecuteFunctions,
	pgp: pgPromise.IMain<{}, pg.IClient>,
	db: pgPromise.IDatabase<{}, pg.IClient>,
	items: INodeExecutionData[],
	continueOnFail = false,
): Promise<IDataObject[]> {
	const table = this.getNodeParameter('table', 0) as string;
	const schema = this.getNodeParameter('schema', 0) as string;
	const updateKey = this.getNodeParameter('updateKey', 0) as string;
	const columnString = this.getNodeParameter('columns', 0) as string;
	const guardedColumns: { [key: string]: string } = {};

	const columns: Array<{ name: string; cast: string; prop: string }> = columnString
		.split(',')
		.map((column) => column.trim().split(':'))
		.map(([name, cast], i) => {
			guardedColumns[`column${i}`] = name;
			return { name, cast, prop: `column${i}` };
		});

	const updateKeys = updateKey.split(',').map((key, i) => {
		const [name, cast] = key.trim().split(':');
		const targetCol = columns.find((column) => column.name === name);
		const updateColumn = { name, cast, prop: targetCol ? targetCol.prop : `updateColumn${i}` };
		if (!targetCol) {
			guardedColumns[updateColumn.prop] = name;
			columns.unshift(updateColumn);
		} else if (!targetCol.cast) {
			targetCol.cast = updateColumn.cast || targetCol.cast;
		}
		return updateColumn;
	});

	const additionalFields = this.getNodeParameter('additionalFields', 0);
	const mode = additionalFields.mode ?? ('multiple' as string);

	const cs = new pgp.helpers.ColumnSet(columns, { table: { table, schema } });

	// Prepare the data to update and copy it to be returned
	const columnNames = columns.map((column) => column.name);
	const updateItems = getItemsCopy(items, columnNames, guardedColumns);

	const returning = generateReturning(pgp, this.getNodeParameter('returnFields', 0) as string);
	if (mode === 'multiple') {
		const query =
			(pgp.helpers.update(updateItems, cs) as string) +
			' WHERE ' +
			updateKeys
				.map((entry) => {
					const key = pgp.as.name(entry.name);
					return 'v.' + key + ' = t.' + key;
				})
				.join(' AND ') +
			returning;
		const updateResult = await db.any(query);
		return updateResult;
	} else {
		const where =
			' WHERE ' +
			// eslint-disable-next-line n8n-local-rules/no-interpolation-in-regular-string
			updateKeys.map((entry) => pgp.as.name(entry.name) + ' = ${' + entry.prop + '}').join(' AND ');
		if (mode === 'transaction') {
			return db.tx(async (t) => {
				const result: IDataObject[] = [];
				for (let i = 0; i < items.length; i++) {
					const itemCopy = getItemCopy(items[i], columnNames, guardedColumns);
					try {
						const transactionResult = await t.any(
							(pgp.helpers.update(itemCopy, cs) as string) +
								pgp.as.format(where, itemCopy) +
								returning,
						);
						const executionData = this.helpers.constructExecutionMetaData(
							wrapData(transactionResult as IDataObject[]),
							{ itemData: { item: i } },
						);
						result.push(...executionData);
					} catch (err) {
						if (!continueOnFail) throw err;
						result.push({
							...itemCopy,
							code: (err as JsonObject).code,
							message: (err as JsonObject).message,
						});
						return result;
					}
				}
				return result;
			});
		} else if (mode === 'independently') {
			return db.task(async (t) => {
				const result: IDataObject[] = [];
				for (let i = 0; i < items.length; i++) {
					const itemCopy = getItemCopy(items[i], columnNames, guardedColumns);
					try {
						const independentResult = await t.any(
							(pgp.helpers.update(itemCopy, cs) as string) +
								pgp.as.format(where, itemCopy) +
								returning,
						);
						const executionData = this.helpers.constructExecutionMetaData(
							wrapData(independentResult as IDataObject[]),
							{ itemData: { item: i } },
						);
						result.push(...executionData);
					} catch (err) {
						if (!continueOnFail) throw err;
						result.push({
							json: { ...items[i].json },
							code: (err as JsonObject).code,
							message: (err as JsonObject).message,
							pairedItem: { item: i },
						} as INodeExecutionData);
					}
				}
				return result;
			});
		}
	}
	throw new Error('multiple, independently or transaction are valid options');
}


<template>
	<div :class="$style.label">
		<span :class="$style.name" v-text="item.key" />
	</div>
</template>

<script lang="ts" setup>
import type { LabelItemProps } from '@/Interface';

export interface Props {
	item: LabelItemProps;
}
defineProps<Props>();
</script>

<style lang="scss" module>
.label {
	margin-left: var(--spacing-s);
	margin-right: var(--spacing-s);
	margin-bottom: var(--spacing-4xs);
	letter-spacing: 1px;
	padding-top: var(--spacing-s);
	font-style: normal;
	font-weight: 700;
	font-size: 10px;
	line-height: 12px;
	text-transform: uppercase;
	color: var(--color-text-base);
	cursor: default;
}
</style>


import conventionalChangelog from 'conventional-changelog';

import { createReadStream, createWriteStream } from 'fs';


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

import * as placeholders from './placeholders';

export const documentOperations: INodeProperties[] = [
	{
		displayName: 'Operation',
		name: 'operation',
		type: 'options',
		noDataExpression: true,
		displayOptions: {
			show: {
				resource: ['document'],
			},
		},
		options: [
			{
				name: 'Create',
				value: 'create',
				description: 'Create a document',
				action: 'Create a document',
			},
			{
				name: 'Delete',
				value: 'delete',
				description: 'Delete a document',
				action: 'Delete a document',
			},
			{
				name: 'Get',
				value: 'get',
				description: 'Get a document',
				action: 'Get a document',
			},
			{
				name: 'Get Many',
				value: 'getAll',
				description: 'Get many documents',
				action: 'Get many documents',
			},
			{
				name: 'Update',
				value: 'update',
				description: 'Update a document',
				action: 'Update a document',
			},
		],
		default: 'get',
	},
];

export const documentFields: INodeProperties[] = [
	// ----------------------------------------
	//             document: delete
	// ----------------------------------------
	{
		displayName: 'Index ID',
		name: 'indexId',
		description: 'ID of the index containing the document to delete',
		type: 'string',
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: ['document'],
				operation: ['delete'],
			},
		},
	},
	{
		displayName: 'Document ID',
		name: 'documentId',
		description: 'ID of the document to delete',
		type: 'string',
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: ['document'],
				operation: ['delete'],
			},
		},
	},

	// ----------------------------------------
	//              document: get
	// ----------------------------------------
	{
		displayName: 'Index ID',
		name: 'indexId',
		description: 'ID of the index containing the document to retrieve',
		type: 'string',
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: ['document'],
				operation: ['get'],
			},
		},
	},
	{
		displayName: 'Document ID',
		name: 'documentId',
		description: 'ID of the document to retrieve',
		type: 'string',
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: ['document'],
				operation: ['get'],
			},
		},
	},
	{
		displayName: 'Simplify',
		name: 'simple',
		type: 'boolean',
		default: true,
		description: 'Whether to return a simplified version of the response instead of the raw data',
		displayOptions: {
			show: {
				resource: ['document'],
				operation: ['get'],
			},
		},
	},
	{
		displayName: 'Options',
		name: 'options',
		type: 'collection',
		placeholder: 'Add Option',
		default: {},
		displayOptions: {
			show: {
				resource: ['document'],
				operation: ['get'],
			},
		},
		options: [
			{
				displayName: 'Source Excludes',
				name: '_source_excludes',
				description: 'Comma-separated list of source fields to exclude from the response',
				type: 'string',
				default: '',
			},
			{
				displayName: 'Source Includes',
				name: '_source_includes',
				description: 'Comma-separated list of source fields to include in the response',
				type: 'string',
				default: '',
			},
			{
				displayName: 'Stored Fields',
				name: 'stored_fields',
				description:
					'Whether to retrieve the document fields stored in the index rather than the document _source. Defaults to false.',
				type: 'boolean',
				default: false,
			},
		],
	},

	// ----------------------------------------
	//             document: getAll
	// ----------------------------------------
	{
		displayName: 'Index ID',
		name: 'indexId',
		description: 'ID of the index containing the documents to retrieve',
		type: 'string',
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: ['document'],
				operation: ['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: ['document'],
				operation: ['getAll'],
			},
		},
	},
	{
		displayName:
			'By default, you cannot page through more than 10,000 hits. To page through more hits, add "Sort" from options.',
		name: 'paginateNotice',
		type: 'notice',
		default: '',
		displayOptions: {
			show: {
				resource: ['document'],
				operation: ['getAll'],
				returnAll: [true],
			},
		},
	},
	{
		displayName: 'Limit',
		name: 'limit',
		type: 'number',
		default: 50,
		description: 'Max number of results to return',
		typeOptions: {
			minValue: 1,
		},
		displayOptions: {
			show: {
				resource: ['document'],
				operation: ['getAll'],
				returnAll: [false],
			},
		},
	},
	{
		displayName: 'Simplify',
		name: 'simple',
		type: 'boolean',
		default: true,
		description: 'Whether to return a simplified version of the response instead of the raw data',
		displayOptions: {
			show: {
				resource: ['document'],
				operation: ['getAll'],
			},
		},
	},
	{
		displayName: 'Options',
		name: 'options',
		type: 'collection',
		placeholder: 'Add Option',
		default: {},
		displayOptions: {
			show: {
				resource: ['document'],
				operation: ['getAll'],
			},
		},
		options: [
			{
				displayName: 'Allow No Indices',
				name: 'allow_no_indices',
				// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
				description:
					'If false, return an error if any of the following targets only missing/closed indices: wildcard expression, index alias, or _all value. Defaults to true.',
				type: 'boolean',
				default: true,
			},
			{
				displayName: 'Allow Partial Search Results',
				name: 'allow_partial_search_results',
				// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
				description:
					'
If true, return partial results if there are shard request timeouts or shard failures.

If false, returns an error with no partial results. Defaults to true.
.',
				type: 'boolean',
				default: true,
			},
			{
				displayName: 'Batched Reduce Size',
				name: 'batched_reduce_size',
				description:
					'Number of shard results that should be reduced at once on the coordinating node. Defaults to 512.',
				type: 'number',
				typeOptions: {
					minValue: 2,
				},
				default: 512,
			},
			{
				displayName: 'CCS Minimize Roundtrips',
				name: 'ccs_minimize_roundtrips',
				description:
					'Whether network round-trips between the coordinating node and the remote clusters are minimized when executing cross-cluster search (CCS) requests. Defaults to true.',
				type: 'boolean',
				default: true,
			},
			{
				displayName: 'Doc Value Fields',
				name: 'docvalue_fields',
				description:
					'Comma-separated list of fields to return as the docvalue representation of a field for each hit',
				type: 'string',
				default: '',
			},
			{
				displayName: 'Expand Wildcards',
				name: 'expand_wildcards',
				description:
					'Type of index that wildcard expressions can match. Defaults to open',
				type: 'options',
				options: [
					{
						name: 'All',
						value: 'all',
					},
					{
						name: 'Closed',
						value: 'closed',
					},
					{
						name: 'Hidden',
						value: 'hidden',
					},
					{
						name: 'None',
						value: 'none',
					},
					{
						name: 'Open',
						value: 'open',
					},
				],
				default: 'open',
			},
			{
				displayName: 'Explain',
				name: 'explain',
				description:
					'Whether to return detailed information about score computation as part of a hit. Defaults to false.',
				type: 'boolean',
				default: false,
			},
			{
				displayName: 'Ignore Throttled',
				name: 'ignore_throttled',
				description:
					'Whether concrete, expanded or aliased indices are ignored when frozen. Defaults to true.',
				type: 'boolean',
				default: true,
			},
			{
				displayName: 'Ignore Unavailable',
				name: 'ignore_unavailable',
				description:
					'Whether missing or closed indices are not included in the response. Defaults to false.',
				type: 'boolean',
				default: false,
			},
			{
				displayName: 'Max Concurrent Shard Requests',
				name: 'max_concurrent_shard_requests',
				description:
					'Define the number of shard requests per node this search executes concurrently. Defaults to 5.',
				type: 'number',
				default: 5,
			},
			{
				displayName: 'Pre-Filter Shard Size',
				name: 'pre_filter_shard_size',
				description:
					'Define a threshold that enforces a pre-filter roundtrip to prefilter search shards based on query rewriting. Only used if the number of shards the search request expands to exceeds the threshold.',
				type: 'number',
				typeOptions: {
					minValue: 1,
				},
				default: 1,
			},
			{
				displayName: 'Query',
				name: 'query',
				description:
					'Query in the Elasticsearch Query DSL',
				type: 'json',
				typeOptions: {
					alwaysOpenEditWindow: true,
				},
				default: '',
				placeholder: placeholders.query,
			},
			{
				displayName: 'Request Cache',
				name: 'request_cache',
				description:
					'Whether the caching of search results is enabled for requests where size is 0. See Elasticsearch shard request cache settings.',
				type: 'boolean',
				default: false,
			},
			{
				displayName: 'Routing',
				name: 'routing',
				description: 'Target this primary shard',
				type: 'string',
				default: '',
			},
			{
				displayName: 'Search Type',
				name: 'search_type',
				description:
					'How distributed term frequencies are calculated for relevance scoring. Defaults to Query then Fetch.',
				type: 'options',
				options: [
					{
						name: 'DFS Query Then Fetch',
						value: 'dfs_query_then_fetch',
					},
					{
						name: 'Query Then Fetch',
						value: 'query_then_fetch',
					},
				],
				default: 'query_then_fetch',
			},
			{
				displayName: 'Sequence Number and Primary Term',
				name: 'seq_no_primary_term',
				description:
					'Whether to return the sequence number and primary term of the last modification of each hit. See Optimistic concurrency control.',
				type: 'boolean',
				default: false,
			},
			{
				displayName: 'Sort',
				name: 'sort',
				description: 'Comma-separated list of field:direction pairs',
				type: 'string',
				default: '',
			},
			{
				displayName: 'Source Excludes',
				name: '_source_excludes',
				description: 'Comma-separated list of source fields to exclude from the response',
				type: 'string',
				default: '',
			},
			{
				displayName: 'Source Includes',
				name: '_source_includes',
				description: 'Comma-separated list of source fields to include in the response',
				type: 'string',
				default: '',
			},
			{
				displayName: 'Stats',
				name: 'stats',
				description: 'Tag of the request for logging and statistical purposes',
				type: 'string',
				default: '',
			},
			{
				displayName: 'Stored Fields',
				name: 'stored_fields',
				description:
					'Whether to retrieve the document fields stored in the index rather than the document _source. Defaults to false.',
				type: 'boolean',
				default: false,
			},
			{
				displayName: 'Terminate After',
				name: 'terminate_after',
				description: 'Max number of documents to collect for each shard',
				type: 'number',
				default: 0,
			},
			{
				displayName: 'Timeout',
				name: 'timeout',
				description:
					'Period to wait for active shards. Defaults to 1m (one minute). See the Elasticsearch time units reference',
				type: 'string',
				default: '1m',
			},
			{
				displayName: 'Track Scores',
				name: 'track_scores',
				description:
					'Whether to calculate and return document scores, even if the scores are not used for sorting. Defaults to false.',
				type: 'boolean',
				default: false,
			},
			{
				displayName: 'Track Total Hits',
				name: 'track_total_hits',
				description: 'Number of hits matching the query to count accurately. Defaults to 10000.',
				type: 'number',
				default: 10000,
			},
			{
				displayName: 'Version',
				name: 'version',
				description: 'Whether to return document version as part of a hit. Defaults to false.',
				type: 'boolean',
				default: false,
			},
		],
	},

	// ----------------------------------------
	//             document: create
	// ----------------------------------------
	{
		displayName: 'Index ID',
		name: 'indexId',
		description: 'ID of the index to add the document to',
		type: 'string',
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: ['document'],
				operation: ['create'],
			},
		},
	},
	{
		displayName: 'Data to Send',
		name: 'dataToSend',
		type: 'options',
		options: [
			{
				name: 'Define Below for Each Column',
				value: 'defineBelow',
				description: 'Set the value for each destination column',
			},
			{
				name: 'Auto-Map Input Data to Columns',
				value: 'autoMapInputData',
				description: 'Use when node input properties match destination column names',
			},
		],
		displayOptions: {
			show: {
				resource: ['document'],
				operation: ['create'],
			},
		},
		default: 'defineBelow',
		description: 'Whether to insert the input data this node receives in the new row',
	},
	{
		displayName: 'Inputs to Ignore',
		name: 'inputsToIgnore',
		type: 'string',
		displayOptions: {
			show: {
				resource: ['document'],
				operation: ['create'],
				dataToSend: ['autoMapInputData'],
			},
		},
		default: '',
		description:
			'List of input properties to avoid sending, separated by commas. Leave empty to send all properties.',
		placeholder: 'Enter properties...',
	},
	{
		displayName: 'Fields to Send',
		name: 'fieldsUi',
		placeholder: 'Add Field',
		type: 'fixedCollection',
		typeOptions: {
			multipleValueButtonText: 'Add Field to Send',
			multipleValues: true,
		},
		displayOptions: {
			show: {
				resource: ['document'],
				operation: ['create'],
				dataToSend: ['defineBelow'],
			},
		},
		default: {},
		options: [
			{
				displayName: 'Field',
				name: 'fieldValues',
				values: [
					{
						displayName: 'Field Name',
						name: 'fieldId',
						type: 'string',
						default: '',
					},
					{
						displayName: 'Field Value',
						name: 'fieldValue',
						type: 'string',
						default: '',
					},
				],
			},
		],
	},
	{
		displayName: 'Additional Fields',
		name: 'additionalFields',
		type: 'collection',
		placeholder: 'Add Field',
		default: {},
		displayOptions: {
			show: {
				resource: ['document'],
				operation: ['create'],
			},
		},
		options: [
			{
				displayName: 'Document ID',
				name: 'documentId',
				description: 'ID of the document to create and add to the index',
				type: 'string',
				default: '',
			},
			{
				displayName: 'Routing',
				name: 'routing',
				description: 'Target this primary shard',
				type: 'string',
				default: '',
			},
			{
				displayName: 'Timeout',
				name: 'timeout',
				description:
					'Period to wait for active shards. Defaults to 1m (one minute). See the Elasticsearch time units reference',
				type: 'string',
				default: '1m',
			},
		],
	},
	{
		displayName: 'Options',
		name: 'options',
		type: 'collection',
		placeholder: 'Add Field',
		default: {},
		displayOptions: {
			show: {
				resource: ['document'],
				operation: ['create'],
			},
		},
		options: [
			{
				displayName: 'Pipeline ID',
				name: 'pipeline',
				description: 'ID of the pipeline to use to preprocess incoming documents',
				type: 'string',
				default: '',
			},
			{
				displayName: 'Refresh',
				name: 'refresh',
				description:
					'If true, Elasticsearch refreshes the affected shards to make this operation visible to search,if wait_for then wait for a refresh to make this operation visible to search,if false do nothing with refreshes',
				type: 'options',
				default: 'false',
				options: [
					{
						name: 'True',
						value: 'true',
						description: 'Refreshes the affected shards to make this operation visible to search',
					},
					{
						name: 'Wait For',
						value: 'wait_for',
						description: 'Wait for a refresh to make this operation visible',
					},
					{
						name: 'False',
						value: 'false',
						description: 'Do nothing with refreshes',
					},
				],
			},
		],
	},

	// ----------------------------------------
	//             document: update
	// ----------------------------------------
	{
		displayName: 'Index ID',
		name: 'indexId',
		description: 'ID of the document to update',
		type: 'string',
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: ['document'],
				operation: ['update'],
			},
		},
	},
	{
		displayName: 'Document ID',
		name: 'documentId',
		description: 'ID of the document to update',
		type: 'string',
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: ['document'],
				operation: ['update'],
			},
		},
	},
	{
		displayName: 'Data to Send',
		name: 'dataToSend',
		type: 'options',
		options: [
			{
				name: 'Define Below for Each Column',
				value: 'defineBelow',
				description: 'Set the value for each destination column',
			},
			{
				name: 'Auto-Map Input Data to Columns',
				value: 'autoMapInputData',
				description: 'Use when node input properties match destination column names',
			},
		],
		displayOptions: {
			show: {
				resource: ['document'],
				operation: ['update'],
			},
		},
		default: 'defineBelow',
		description: 'Whether to insert the input data this node receives in the new row',
	},
	{
		displayName: 'Inputs to Ignore',
		name: 'inputsToIgnore',
		type: 'string',
		displayOptions: {
			show: {
				resource: ['document'],
				operation: ['update'],
				dataToSend: ['autoMapInputData'],
			},
		},
		default: '',
		description:
			'List of input properties to avoid sending, separated by commas. Leave empty to send all properties.',
		placeholder: 'Enter properties...',
	},
	{
		displayName: 'Fields to Send',
		name: 'fieldsUi',
		placeholder: 'Add Field',
		type: 'fixedCollection',
		typeOptions: {
			multipleValueButtonText: 'Add Field to Send',
			multipleValues: true,
		},
		displayOptions: {
			show: {
				resource: ['document'],
				operation: ['update'],
				dataToSend: ['defineBelow'],
			},
		},
		default: {},
		options: [
			{
				displayName: 'Field',
				name: 'fieldValues',
				values: [
					{
						displayName: 'Field Name',
						name: 'fieldId',
						type: 'string',
						default: '',
					},
					{
						displayName: 'Field Value',
						name: 'fieldValue',
						type: 'string',
						default: '',
					},
				],
			},
		],
	},
	{
		displayName: 'Options',
		name: 'options',
		type: 'collection',
		placeholder: 'Add Field',
		default: {},
		displayOptions: {
			show: {
				resource: ['document'],
				operation: ['update'],
			},
		},
		options: [
			{
				displayName: 'Refresh',
				name: 'refresh',
				description:
					'If true, Elasticsearch refreshes the affected shards to make this operation visible to search,if wait_for then wait for a refresh to make this operation visible to search,if false do nothing with refreshes',
				type: 'options',
				default: 'false',
				options: [
					{
						name: 'True',
						value: 'true',
						description: 'Refreshes the affected shards to make this operation visible to search',
					},
					{
						name: 'Wait For',
						value: 'wait_for',
						description: 'Wait for a refresh to make this operation visible',
					},
					{
						name: 'False',
						value: 'false',
						description: 'Do nothing with refreshes',
					},
				],
			},
		],
	},
];


import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
import { allProperties } from './descriptions';

export class CrowdDev implements INodeType {
	description: INodeTypeDescription = {
		displayName: 'crowd.dev',
		name: 'crowdDev',
		icon: 'file:crowdDev.svg',
		group: ['transform'],
		version: 1,
		subtitle: '={{ $parameter["operation"] + ": " + $parameter["resource"] }}',
		description:
			'crowd.dev is an open-source suite of community and data tools built to unlock community-led growth for your organization.',
		defaults: {
			name: 'crowd.dev',
		},
		inputs: ['main'],
		outputs: ['main'],
		credentials: [
			{
				name: 'crowdDevApi',
				required: true,
			},
		],
		requestDefaults: {
			baseURL: '={{$credentials.url}}/api/tenant/{{$credentials.tenantId}}',
			json: true,
			skipSslCertificateValidation: '={{ $credentials.allowUnauthorizedCerts }}',
		},
		properties: allProperties,
	};
}


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

import {
	organizationOperations,
	spaceFields,
	spaceOperations,
	userFields,
	userOperations,
	workItemFields,
	workItemOperations,
} from './descriptions';

import type { LoadOptions } from './GenericFunctions';
import { createLoadOptions, kitemakerRequest, kitemakerRequestAllItems } from './GenericFunctions';

import {
	getAllSpaces,
	getAllUsers,
	getAllWorkItems,
	getLabels,
	getOrganization,
	getSpaces,
	getStatuses,
	getUsers,
	getWorkItem,
	getWorkItems,
} from './queries';

import { createWorkItem, editWorkItem } from './mutations';

export class Kitemaker implements INodeType {
	description: INodeTypeDescription = {
		displayName: 'Kitemaker',
		name: 'kitemaker',
		icon: 'file:kitemaker.svg',
		group: ['input'],
		version: 1,
		subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
		description: 'Consume the Kitemaker GraphQL API',
		defaults: {
			name: 'Kitemaker',
		},
		inputs: ['main'],
		outputs: ['main'],
		credentials: [
			{
				name: 'kitemakerApi',
				required: true,
			},
		],
		properties: [
			{
				displayName: 'Resource',
				name: 'resource',
				type: 'options',
				noDataExpression: true,
				options: [
					{
						name: 'Organization',
						value: 'organization',
					},
					{
						name: 'Space',
						value: 'space',
					},
					{
						name: 'User',
						value: 'user',
					},
					{
						name: 'Work Item',
						value: 'workItem',
					},
				],
				default: 'workItem',
				required: true,
			},
			...organizationOperations,
			...spaceOperations,
			...spaceFields,
			...userOperations,
			...userFields,
			...workItemOperations,
			...workItemFields,
		],
	};

	methods = {
		loadOptions: {
			async getLabels(this: ILoadOptionsFunctions) {
				const responseData = await kitemakerRequest.call(this, { query: getLabels });
				const {
					data: {
						organization: { spaces },
					},
				} = responseData;

				return createLoadOptions(spaces[0].labels as LoadOptions[]);
			},

			async getSpaces(this: ILoadOptionsFunctions) {
				const responseData = await kitemakerRequest.call(this, { query: getSpaces });
				const {
					data: {
						organization: { spaces },
					},
				} = responseData;

				return createLoadOptions(spaces as LoadOptions[]);
			},

			async getStatuses(this: ILoadOptionsFunctions) {
				const spaceId = this.getNodeParameter('spaceId', 0) as string;
				if (!spaceId.length) {
					throw new NodeOperationError(
						this.getNode(),
						'Please choose a space to set for the work item to create.',
					);
				}

				const responseData = await kitemakerRequest.call(this, { query: getStatuses });
				const {
					data: {
						organization: { spaces },
					},
				} = responseData;
				const space = spaces.find((e: { [x: string]: string }) => e.id === spaceId);

				return createLoadOptions(space.statuses as LoadOptions[]);
			},

			async getUsers(this: ILoadOptionsFunctions) {
				const responseData = await kitemakerRequest.call(this, { query: getUsers });
				const {
					data: {
						organization: { users },
					},
				} = responseData;

				return createLoadOptions(users as LoadOptions[]);
			},

			async getWorkItems(this: ILoadOptionsFunctions) {
				const spaceId = this.getNodeParameter('spaceId', 0) as string;

				const responseData = await kitemakerRequest.call(this, {
					query: getWorkItems,
					variables: { spaceId },
				});

				const {
					data: {
						workItems: { workItems },
					},
				} = responseData;

				return createLoadOptions(workItems as LoadOptions[]);
			},
		},
	};

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

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

		let responseData;
		const returnData: INodeExecutionData[] = [];

		// https://github.com/kitemakerhq/docs/blob/main/kitemaker.graphql

		for (let i = 0; i < items.length; i++) {
			if (resource === 'organization') {
				// *********************************************************************
				//                           organization
				// *********************************************************************

				if (operation === 'get') {
					// ----------------------------------
					//         organization: get
					// ----------------------------------

					responseData = await kitemakerRequest.call(this, {
						query: getOrganization,
					});

					responseData = responseData.data.organization;
				}
			} else if (resource === 'space') {
				// *********************************************************************
				//                             space
				// *********************************************************************

				if (operation === 'getAll') {
					// ----------------------------------
					//          space: getAll
					// ----------------------------------

					const allItems = await kitemakerRequestAllItems.call(this, {
						query: getAllSpaces,
						variables: {},
					});

					responseData = allItems;
				}
			} else if (resource === 'user') {
				// *********************************************************************
				//                             user
				// *********************************************************************

				if (operation === 'getAll') {
					// ----------------------------------
					//          user: getAll
					// ----------------------------------

					const allItems = await kitemakerRequestAllItems.call(this, {
						query: getAllUsers,
						variables: {},
					});

					responseData = allItems;
				}
			} else if (resource === 'workItem') {
				// *********************************************************************
				//                             workItem
				// *********************************************************************

				if (operation === 'create') {
					// ----------------------------------
					//         workItem: create
					// ----------------------------------

					const input = {
						title: this.getNodeParameter('title', i) as string,
						statusId: this.getNodeParameter('statusId', i) as string[],
					};

					if (!input.statusId.length) {
						throw new NodeOperationError(
							this.getNode(),
							'Please enter a status to set for the work item to create.',
							{ itemIndex: i },
						);
					}

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

					if (Object.keys(additionalFields).length) {
						Object.assign(input, additionalFields);
					}

					responseData = await kitemakerRequest.call(this, {
						query: createWorkItem,
						variables: { input },
					});

					responseData = responseData.data.createWorkItem.workItem;
				} else if (operation === 'get') {
					// ----------------------------------
					//         workItem: get
					// ----------------------------------

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

					responseData = await kitemakerRequest.call(this, {
						query: getWorkItem,
						variables: { workItemId },
					});

					responseData = responseData.data.workItem;
				} else if (operation === 'getAll') {
					// ----------------------------------
					//         workItem: getAll
					// ----------------------------------

					const allItems = await kitemakerRequestAllItems.call(this, {
						query: getAllWorkItems,
						variables: {
							spaceId: this.getNodeParameter('spaceId', i) as string,
						},
					});

					responseData = allItems;
				} else if (operation === 'update') {
					// ----------------------------------
					//         workItem: update
					// ----------------------------------

					const input = {
						id: this.getNodeParameter('workItemId', i),
					};

					const updateFields = this.getNodeParameter('updateFields', i);

					if (!Object.keys(updateFields).length) {
						throw new NodeOperationError(
							this.getNode(),
							'Please enter at least one field to update for the work item.',
							{ itemIndex: i },
						);
					}

					Object.assign(input, updateFields);

					responseData = await kitemakerRequest.call(this, {
						query: editWorkItem,
						variables: { input },
					});

					responseData = responseData.data.editWorkItem.workItem;
				}
			}

			const executionData = this.helpers.constructExecutionMetaData(
				this.helpers.returnJsonArray(responseData as IDataObject),
				{ itemData: { item: i } },
			);

			returnData.push(...executionData);
		}

		return this.prepareOutputData(returnData);
	}
}


import packageJson from '../../package.json' assert { type: 'json' };

keep keep keep keep keep replace replace keep keep

import { pipeline } from 'stream/promises';


import type {
	ITriggerFunctions,
	INodeType,
	INodeTypeDescription,
	ITriggerResponse,
	TriggerTime,
} from 'n8n-workflow';
import { NodeHelpers, toCronExpression } from 'n8n-workflow';

import { CronJob } from 'cron';

export class Cron implements INodeType {
	description: INodeTypeDescription = {
		displayName: 'Cron',
		name: 'cron',
		icon: 'fa:clock',
		group: ['trigger', 'schedule'],
		version: 1,
		hidden: true,
		description: 'Triggers the workflow at a specific time',
		eventTriggerDescription: '',
		activationMessage:
			'Your cron trigger will now trigger executions on the schedule you have defined.',
		defaults: {
			name: 'Cron',
			color: '#29a568',
		},
		// eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node
		inputs: [],
		outputs: ['main'],
		properties: [
			{
				displayName:
					'This workflow will run on the schedule you define here once you activate it.

For testing, you can also trigger it manually: by going back to the canvas and clicking ‘execute workflow’',
				name: 'notice',
				type: 'notice',
				default: '',
			},
			{
				displayName: 'Trigger Times',
				name: 'triggerTimes',
				type: 'fixedCollection',
				typeOptions: {
					multipleValues: true,
					multipleValueButtonText: 'Add Time',
				},
				default: {},
				description: 'Triggers for the workflow',
				placeholder: 'Add Cron Time',
				options: NodeHelpers.cronNodeOptions,
			},
		],
	};

	async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> {
		const triggerTimes = this.getNodeParameter('triggerTimes') as unknown as {
			item: TriggerTime[];
		};

		// Get all the trigger times
		const cronTimes = (triggerTimes.item || []).map(toCronExpression);

		// The trigger function to execute when the cron-time got reached
		// or when manually triggered
		const executeTrigger = () => {
			this.emit([this.helpers.returnJsonArray([{}])]);
		};

		const timezone = this.getTimezone();

		// Start the cron-jobs
		const cronJobs = cronTimes.map(
			(cronTime) => new CronJob(cronTime, executeTrigger, undefined, true, timezone),
		);

		// Stop the cron-jobs
		async function closeFunction() {
			for (const cronJob of cronJobs) {
				cronJob.stop();
			}
		}

		async function manualTriggerFunction() {
			executeTrigger();
		}

		return {
			closeFunction,
			manualTriggerFunction,
		};
	}
}


import type { DeleteResult, FindOptionsWhere } from 'typeorm';
import { In, Not, Raw, LessThan } from 'typeorm';
import { Container } from 'typedi';
import type { ExecutionStatus } from 'n8n-workflow';

import * as Db from '@/Db';
import type { IExecutionBase, IExecutionFlattedDb } from '@/Interfaces';
import { ExecutionRepository } from '@db/repositories';

function getStatusCondition(status: ExecutionStatus) {
	const condition: Pick<FindOptionsWhere<IExecutionFlattedDb>, 'status'> = {};

	if (status === 'success') {
		condition.status = 'success';
	} else if (status === 'waiting') {
		condition.status = 'waiting';
	} else if (status === 'error') {
		condition.status = In(['error', 'crashed', 'failed']);
	}

	return condition;
}

export async function getExecutions(params: {
	limit: number;
	includeData?: boolean;
	lastId?: string;
	workflowIds?: string[];
	status?: ExecutionStatus;
	excludedExecutionsIds?: string[];
}): Promise<IExecutionBase[]> {
	let where: FindOptionsWhere<IExecutionFlattedDb> = {};

	if (params.lastId && params.excludedExecutionsIds?.length) {
		where.id = Raw((id) => `${id} < :lastId AND ${id} NOT IN (:...excludedExecutionsIds)`, {
			lastId: params.lastId,
			excludedExecutionsIds: params.excludedExecutionsIds,
		});
	} else if (params.lastId) {
		where.id = LessThan(params.lastId);
	} else if (params.excludedExecutionsIds?.length) {
		where.id = Not(In(params.excludedExecutionsIds));
	}

	if (params.status) {
		where = { ...where, ...getStatusCondition(params.status) };
	}

	if (params.workflowIds) {
		where = { ...where, workflowId: In(params.workflowIds) };
	}

	return Container.get(ExecutionRepository).findMultipleExecutions(
		{
			select: [
				'id',
				'mode',
				'retryOf',
				'retrySuccessId',
				'startedAt',
				'stoppedAt',
				'workflowId',
				'waitTill',
				'finished',
			],
			where,
			order: { id: 'DESC' },
			take: params.limit,
			relations: ['executionData'],
		},
		{
			includeData: params.includeData,
			unflattenData: true,
		},
	);
}

export async function getExecutionsCount(data: {
	limit: number;
	lastId?: string;
	workflowIds?: string[];
	status?: ExecutionStatus;
	excludedWorkflowIds?: string[];
}): Promise<number> {
	// TODO: Consider moving this to the repository as well
	const executions = await Db.collections.Execution.count({
		where: {
			...(data.lastId && { id: LessThan(data.lastId) }),
			...(data.status && { ...getStatusCondition(data.status) }),
			...(data.workflowIds && { workflowId: In(data.workflowIds) }),
			...(data.excludedWorkflowIds && { workflowId: Not(In(data.excludedWorkflowIds)) }),
		},
		take: data.limit,
	});

	return executions;
}

export async function getExecutionInWorkflows(
	id: string,
	workflowIds: string[],
	includeData?: boolean,
): Promise<IExecutionBase | undefined> {
	return Container.get(ExecutionRepository).findSingleExecution(id, {
		where: {
			workflowId: In(workflowIds),
		},
		includeData,
		unflattenData: true,
	});
}

export async function deleteExecution(execution: IExecutionBase): Promise<DeleteResult> {
	return Container.get(ExecutionRepository).deleteExecution(execution.id as string);
}


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

export const sessionOperations: INodeProperties[] = [
	{
		displayName: 'Operation',
		name: 'operation',
		type: 'options',
		noDataExpression: true,
		default: 'get',
		options: [
			{
				name: 'Get',
				value: 'get',
				action: 'Get a session',
			},
			{
				name: 'Get Many',
				value: 'getAll',
				action: 'Get many sessions',
			},
			{
				name: 'Get Details',
				value: 'getDetails',
				action: 'Get details on a session',
			},
		],
		displayOptions: {
			show: {
				resource: ['session'],
			},
		},
	},
];

export const sessionFields: INodeProperties[] = [
	// ----------------------------------
	//         session: 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: ['session'],
				operation: ['getAll'],
			},
		},
	},
	{
		displayName: 'Limit',
		name: 'limit',
		type: 'number',
		default: 10,
		description: 'Max number of results to return',
		typeOptions: {
			minValue: 1,
			maxValue: 100,
		},
		displayOptions: {
			show: {
				resource: ['session'],
				operation: ['getAll'],
				returnAll: [false],
			},
		},
	},
	{
		displayName: 'Additional Fields',
		name: 'additionalFields',
		type: 'collection',
		placeholder: 'Add Field',
		displayOptions: {
			show: {
				resource: ['session'],
				operation: ['getAll'],
			},
		},
		default: {},
		options: [
			{
				displayName: 'Time Range',
				name: 'times',
				type: 'fixedCollection',
				placeholder: 'Add Time Range',
				required: true,
				default: {},
				options: [
					{
						displayName: 'Times Properties',
						name: 'timesProperties',
						values: [
							{
								displayName: 'Start Time',
								name: 'fromTime',
								type: 'dateTime',
								description: 'Start of the datetime range for the session',
								default: '',
							},
							{
								displayName: 'End Time',
								name: 'toTime',
								type: 'dateTime',
								description: 'End of the datetime range for the session',
								default: '',
							},
						],
					},
				],
			},
			{
				displayName: 'Webinar Key Name or ID',
				name: 'webinarKey',
				type: 'options',
				typeOptions: {
					loadOptionsMethod: 'getWebinars',
				},
				default: {},
				description:
					'Webinar by which to filter the sessions to retrieve. Choose from the list, or specify an ID using an expression.',
			},
		],
	},

	// ----------------------------------
	//      session: shared fields
	// ----------------------------------
	{
		displayName: 'Webinar Key Name or ID',
		name: 'webinarKey',
		type: 'options',
		typeOptions: {
			loadOptionsMethod: 'getWebinars',
		},
		required: true,
		default: [],
		description:
			'Key of the webinar to which the session belongs. Choose from the list, or specify an ID using an expression.',
		displayOptions: {
			show: {
				resource: ['session'],
				operation: ['get', 'getDetails'],
			},
		},
	},
	{
		displayName: 'Session Key',
		name: 'sessionKey',
		type: 'string',
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: ['session'],
				operation: ['get', 'getDetails'],
			},
		},
	},

	// ----------------------------------
	//        session: getDetails
	// ----------------------------------
	{
		displayName: 'Details',
		name: 'details',
		type: 'options',
		default: 'performance',
		options: [
			{
				name: 'Performance',
				value: 'performance',
				description: 'Performance details for a webinar session',
			},
			{
				name: 'Polls',
				value: 'polls',
				description: 'Questions and answers for polls from a webinar session',
			},
			{
				name: 'Questions',
				value: 'questions',
				description: 'Questions and answers for a past webinar session',
			},
			{
				name: 'Surveys',
				value: 'surveys',
				description: 'Surveys for a past webinar session',
			},
		],
		displayOptions: {
			show: {
				resource: ['session'],
				operation: ['getDetails'],
			},
		},
	},
];


const pipeline = promisify(stream.pipeline);

const baseDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..');

const fullChangelogFile = resolve(baseDir, 'CHANGELOG.md');

const versionChangelogFile = resolve(baseDir, `CHANGELOG-${packageJson.version}.md`);

keep keep replace replace keep keep keep keep


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

import {
	mapEndpoint,
	serviceNowApiRequest,
	serviceNowDownloadAttachment,
	serviceNowRequestAllItems,
	sortData,
} from './GenericFunctions';

import { attachmentFields, attachmentOperations } from './AttachmentDescription';

import { businessServiceFields, businessServiceOperations } from './BusinessServiceDescription';

import {
	configurationItemsFields,
	configurationItemsOperations,
} from './ConfigurationItemsDescription';

import { departmentFields, departmentOperations } from './DepartmentDescription';

import { dictionaryFields, dictionaryOperations } from './DictionaryDescription';

import { incidentFields, incidentOperations } from './IncidentDescription';

import { tableRecordFields, tableRecordOperations } from './TableRecordDescription';

import { userFields, userOperations } from './UserDescription';

import { userGroupFields, userGroupOperations } from './UserGroupDescription';

import { userRoleFields, userRoleOperations } from './UserRoleDescription';

export class ServiceNow implements INodeType {
	description: INodeTypeDescription = {
		displayName: 'ServiceNow',
		name: 'serviceNow',
		icon: 'file:servicenow.svg',
		group: ['output'],
		version: 1,
		subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
		description: 'Consume ServiceNow API',
		defaults: {
			name: 'ServiceNow',
		},
		inputs: ['main'],
		outputs: ['main'],
		credentials: [
			{
				name: 'serviceNowOAuth2Api',
				required: true,
				displayOptions: {
					show: {
						authentication: ['oAuth2'],
					},
				},
			},
			{
				name: 'serviceNowBasicApi',
				required: true,
				displayOptions: {
					show: {
						authentication: ['basicAuth'],
					},
				},
			},
		],
		properties: [
			{
				displayName: 'Authentication',
				name: 'authentication',
				type: 'options',
				options: [
					{
						name: 'Basic Auth',
						value: 'basicAuth',
					},
					{
						name: 'OAuth2',
						value: 'oAuth2',
					},
				],
				default: 'oAuth2',
				description: 'Authentication method to use',
			},
			{
				displayName: 'Resource',
				name: 'resource',
				type: 'options',
				noDataExpression: true,
				options: [
					{
						name: 'Attachment',
						value: 'attachment',
					},
					{
						name: 'Business Service',
						value: 'businessService',
					},
					{
						name: 'Configuration Item',
						value: 'configurationItems',
					},
					{
						name: 'Department',
						value: 'department',
					},
					{
						name: 'Dictionary',
						value: 'dictionary',
					},
					{
						name: 'Incident',
						value: 'incident',
					},
					{
						name: 'Table Record',
						value: 'tableRecord',
					},
					{
						name: 'User',
						value: 'user',
					},
					{
						name: 'User Group',
						value: 'userGroup',
					},
					{
						name: 'User Role',
						value: 'userRole',
					},
				],
				default: 'user',
			},
			// ATTACHMENT SERVICE
			...attachmentOperations,
			...attachmentFields,
			// BUSINESS SERVICE
			...businessServiceOperations,
			...businessServiceFields,
			// CONFIGURATION ITEMS
			...configurationItemsOperations,
			...configurationItemsFields,
			// DEPARTMENT
			...departmentOperations,
			...departmentFields,
			// DICTIONARY
			...dictionaryOperations,
			...dictionaryFields,
			// INCIDENT
			...incidentOperations,
			...incidentFields,
			// TABLE RECORD
			...tableRecordOperations,
			...tableRecordFields,
			// USER
			...userOperations,
			...userFields,
			// USER GROUP
			...userGroupOperations,
			...userGroupFields,
			// USER ROLE
			...userRoleOperations,
			...userRoleFields,
		],
	};

	methods = {
		loadOptions: {
			async getTables(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
				const returnData: INodePropertyOptions[] = [];
				const response = await serviceNowApiRequest.call(
					this,
					'GET',
					'/now/doc/table/schema',
					{},
					{},
				);
				for (const table of response.result) {
					returnData.push({
						name: table.label,
						value: table.value,
						description: table.value,
					});
				}
				return sortData(returnData);
			},
			// Get all the table column to display them to user
			async getColumns(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
				const resource = this.getNodeParameter('resource', 0) as string;
				const operation = this.getNodeParameter('operation', 0) as string;
				const returnData: INodePropertyOptions[] = [];
				let tableName;
				if (resource === 'tableRecord') {
					tableName = this.getNodeParameter('tableName') as string;
				} else {
					tableName = mapEndpoint(resource, operation);
				}

				const qs = {
					sysparm_query: `name=${tableName}`,
					sysparm_fields: 'column_label,element',
				};
				const response = await serviceNowApiRequest.call(
					this,
					'GET',
					'/now/table/sys_dictionary',
					{},
					qs,
				);
				for (const column of response.result) {
					if (column.element) {
						returnData.push({
							name: column.column_label,
							value: column.element,
						});
					}
				}
				return sortData(returnData);
			},
			async getBusinessServices(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
				const returnData: INodePropertyOptions[] = [];
				const qs = {
					sysparm_fields: 'name,sys_id',
				};
				const response = await serviceNowApiRequest.call(
					this,
					'GET',
					'/now/table/cmdb_ci_service',
					{},
					qs,
				);

				for (const column of response.result) {
					returnData.push({
						name: column.name,
						value: column.sys_id,
					});
				}
				return sortData(returnData);
			},
			async getUsers(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
				const returnData: INodePropertyOptions[] = [];
				const resource = this.getNodeParameter('resource', 0);
				const operation = this.getNodeParameter('operation', 0);

				const qs = {
					sysparm_fields: 'sys_id,user_name',
				};
				if (resource === 'incident' && operation === 'create') {
					const additionalFields = this.getNodeParameter('additionalFields') as IDataObject;
					const group = additionalFields.assignment_group;

					const response = await serviceNowRequestAllItems.call(
						this,
						'GET',
						'/now/table/sys_user_grmember',
						{},
						{
							sysparm_query: `group=${group}^`,
						},
					);

					for (const column of response) {
						if (column.user) {
							const responseData = await serviceNowApiRequest.call(
								this,
								'GET',
								`/now/table/sys_user/${column.user.value}`,
								{},
								{},
							);
							const user = responseData.result;

							returnData.push({
								name: user.user_name,
								value: user.sys_id,
							});
						}
					}
				} else {
					const response = await serviceNowRequestAllItems.call(
						this,
						'GET',
						'/now/table/sys_user',
						{},
						qs,
					);

					for (const column of response) {
						if (column.user_name) {
							returnData.push({
								name: column.user_name,
								value: column.sys_id,
							});
						}
					}
				}
				return sortData(returnData);
			},
			async getAssignmentGroups(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
				const returnData: INodePropertyOptions[] = [];
				const qs = {
					sysparm_fields: 'sys_id,name',
				};
				const response = await serviceNowRequestAllItems.call(
					this,
					'GET',
					'/now/table/sys_user_group',
					{},
					qs,
				);

				for (const column of response) {
					if (column.name) {
						returnData.push({
							name: column.name,
							value: column.sys_id,
						});
					}
				}
				return sortData(returnData);
			},
			async getUserRoles(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
				const returnData: INodePropertyOptions[] = [];
				const qs = {
					sysparm_fields: 'sys_id,name',
				};
				const response = await serviceNowRequestAllItems.call(
					this,
					'GET',
					'/now/table/sys_user_role',
					{},
					qs,
				);

				for (const column of response) {
					if (column.name) {
						returnData.push({
							name: column.name,
							value: column.sys_id,
						});
					}
				}
				return sortData(returnData);
			},
			async getConfigurationItems(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
				const returnData: INodePropertyOptions[] = [];
				const qs = {
					sysparm_fields: 'sys_id,name,sys_class_name',
				};
				const response = await serviceNowRequestAllItems.call(
					this,
					'GET',
					'/now/table/cmdb_ci',
					{},
					qs,
				);

				for (const column of response) {
					if (column.name) {
						returnData.push({
							name: column.name,
							value: column.sys_id,
							description: column.sys_class_name,
						});
					}
				}
				return sortData(returnData);
			},
			async getIncidentCategories(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
				const returnData: INodePropertyOptions[] = [];
				const qs = {
					sysparm_fields: 'label,value',
					sysparm_query: 'element=category^name=incident',
				};
				const response = await serviceNowRequestAllItems.call(
					this,
					'GET',
					'/now/table/sys_choice',
					{},
					qs,
				);

				for (const column of response) {
					returnData.push({
						name: column.label,
						value: column.value,
					});
				}
				return sortData(returnData);
			},
			async getIncidentSubcategories(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
				const returnData: INodePropertyOptions[] = [];
				const operation = this.getNodeParameter('operation');
				let category;
				if (operation === 'update') {
					const updateFields = this.getNodeParameter('updateFields') as IDataObject;
					category = updateFields.category;
				} else {
					const additionalFields = this.getNodeParameter('additionalFields') as IDataObject;
					category = additionalFields.category;
				}
				const qs = {
					sysparm_fields: 'label,value',
					sysparm_query: `name=incident^element=subcategory^dependent_value=${category}`,
				};
				const response = await serviceNowRequestAllItems.call(
					this,
					'GET',
					'/now/table/sys_choice',
					{},
					qs,
				);

				for (const column of response) {
					returnData.push({
						name: column.label,
						value: column.value,
					});
				}

				return sortData(returnData);
			},
			async getIncidentStates(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
				const returnData: INodePropertyOptions[] = [];
				const qs = {
					sysparm_fields: 'label,value',
					sysparm_query: 'element=state^name=incident',
				};
				const response = await serviceNowRequestAllItems.call(
					this,
					'GET',
					'/now/table/sys_choice',
					{},
					qs,
				);

				for (const column of response) {
					returnData.push({
						name: column.label,
						value: column.value,
					});
				}
				return sortData(returnData);
			},
			async getIncidentResolutionCodes(
				this: ILoadOptionsFunctions,
			): Promise<INodePropertyOptions[]> {
				const returnData: INodePropertyOptions[] = [];
				const qs = {
					sysparm_fields: 'label,value',
					sysparm_query: 'element=close_code^name=incident',
				};
				const response = await serviceNowRequestAllItems.call(
					this,
					'GET',
					'/now/table/sys_choice',
					{},
					qs,
				);

				for (const column of response) {
					returnData.push({
						name: column.label,
						value: column.value,
					});
				}
				return sortData(returnData);
			},
			async getIncidentHoldReasons(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
				const returnData: INodePropertyOptions[] = [];
				const qs = {
					sysparm_fields: 'label,value',
					sysparm_query: 'element=hold_reason^name=incident',
				};
				const response = await serviceNowRequestAllItems.call(
					this,
					'GET',
					'/now/table/sys_choice',
					{},
					qs,
				);

				for (const column of response) {
					returnData.push({
						name: column.label,
						value: column.value,
					});
				}
				return sortData(returnData);
			},
		},
	};

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

		for (let i = 0; i < length; i++) {
			try {
				// https://developer.servicenow.com/dev.do#!/reference/api/rome/rest/c_AttachmentAPI
				if (resource === 'attachment') {
					if (operation === 'get') {
						const attachmentsSysId = this.getNodeParameter('attachmentId', i) as string;
						const download = this.getNodeParameter('download', i);
						const endpoint = `/now/attachment/${attachmentsSysId}`;

						const response = await serviceNowApiRequest.call(this, 'GET', endpoint, {});
						const fileMetadata = response.result;

						responseData = {
							json: fileMetadata,
						};

						if (download) {
							const outputField = this.getNodeParameter('outputField', i) as string;
							responseData = {
								...responseData,
								binary: {
									[outputField]: await serviceNowDownloadAttachment.call(
										this,
										endpoint,
										fileMetadata.file_name as string,
										fileMetadata.content_type as string,
									),
								},
							};
						}
					} else if (operation === 'getAll') {
						const download = this.getNodeParameter('download', i);
						const tableName = this.getNodeParameter('tableName', i) as string;
						const returnAll = this.getNodeParameter('returnAll', i);
						const options = this.getNodeParameter('options', i);

						qs = {} as IDataObject;

						qs.sysparm_query = `table_name=${tableName}`;

						if (options.queryFilter) {
							qs.sysparm_query = `${qs.sysparm_query}^${options.queryFilter}`;
						}

						if (!returnAll) {
							const limit = this.getNodeParameter('limit', i);
							qs.sysparm_limit = limit;
							const response = await serviceNowApiRequest.call(
								this,
								'GET',
								'/now/attachment',
								{},
								qs,
							);
							responseData = response.result;
						} else {
							responseData = await serviceNowRequestAllItems.call(
								this,
								'GET',
								'/now/attachment',
								{},
								qs,
							);
						}

						if (download) {
							const outputField = this.getNodeParameter('outputField', i) as string;
							const responseDataWithAttachments: IDataObject[] = [];

							for (const data of responseData as IDataObject[]) {
								responseDataWithAttachments.push({
									json: data,
									binary: {
										[outputField]: await serviceNowDownloadAttachment.call(
											this,
											`/now/attachment/${data.sys_id}`,
											data.file_name as string,
											data.content_type as string,
										),
									},
								});
							}

							responseData = responseDataWithAttachments;
						} else {
							responseData = (responseData as IDataObject[]).map((data) => ({ json: data }));
						}
					} else if (operation === 'upload') {
						const tableName = this.getNodeParameter('tableName', i) as string;
						const recordId = this.getNodeParameter('id', i) as string;
						const inputDataFieldName = this.getNodeParameter('inputDataFieldName', i) as string;
						const options = this.getNodeParameter('options', i);

						const binaryData = this.helpers.assertBinaryData(i, inputDataFieldName);

						const headers: IDataObject = {
							'Content-Type': binaryData.mimeType,
						};

						const query: IDataObject = {
							table_name: tableName,
							table_sys_id: recordId,
							file_name: binaryData.fileName
								? binaryData.fileName
								: `${inputDataFieldName}.${binaryData.fileExtension}`,
							...options,
						};

						const body = await this.helpers.getBinaryDataBuffer(i, inputDataFieldName);

						const response = await serviceNowApiRequest.call(
							this,
							'POST',
							'/now/attachment/file',
							body,
							query,
							'',
							{ headers },
						);
						responseData = response.result;
					} else if (operation === 'delete') {
						const attachmentsSysId = this.getNodeParameter('attachmentId', i) as string;
						await serviceNowApiRequest.call(this, 'DELETE', `/now/attachment/${attachmentsSysId}`);
						responseData = { success: true };
					}
				} else if (resource === 'businessService') {
					if (operation === 'getAll') {
						const returnAll = this.getNodeParameter('returnAll', i);
						qs = this.getNodeParameter('options', i);

						if (qs.sysparm_fields && typeof qs.sysparm_fields !== 'string') {
							qs.sysparm_fields = (qs.sysparm_fields as string[]).join(',');
						}

						if (!returnAll) {
							const limit = this.getNodeParameter('limit', i);
							qs.sysparm_limit = limit;
							const response = await serviceNowApiRequest.call(
								this,
								'GET',
								'/now/table/cmdb_ci_service',
								{},
								qs,
							);
							responseData = response.result;
						} else {
							responseData = await serviceNowRequestAllItems.call(
								this,
								'GET',
								'/now/table/cmdb_ci_service',
								{},
								qs,
							);
						}
					}
				} else if (resource === 'configurationItems') {
					if (operation === 'getAll') {
						const returnAll = this.getNodeParameter('returnAll', i);
						qs = this.getNodeParameter('options', i);

						if (qs.sysparm_fields && typeof qs.sysparm_fields !== 'string') {
							qs.sysparm_fields = (qs.sysparm_fields as string[]).join(',');
						}

						if (!returnAll) {
							const limit = this.getNodeParameter('limit', i);
							qs.sysparm_limit = limit;
							const response = await serviceNowApiRequest.call(
								this,
								'GET',
								'/now/table/cmdb_ci',
								{},
								qs,
							);
							responseData = response.result;
						} else {
							responseData = await serviceNowRequestAllItems.call(
								this,
								'GET',
								'/now/table/cmdb_ci',
								{},
								qs,
							);
						}
					}
				} else if (resource === 'department') {
					if (operation === 'getAll') {
						const returnAll = this.getNodeParameter('returnAll', i);
						qs = this.getNodeParameter('options', i);

						if (qs.sysparm_fields && typeof qs.sysparm_fields !== 'string') {
							qs.sysparm_fields = (qs.sysparm_fields as string[]).join(',');
						}

						if (!returnAll) {
							const limit = this.getNodeParameter('limit', i);
							qs.sysparm_limit = limit;
							const response = await serviceNowApiRequest.call(
								this,
								'GET',
								'/now/table/cmn_department',
								{},
								qs,
							);
							responseData = response.result;
						} else {
							responseData = await serviceNowRequestAllItems.call(
								this,
								'GET',
								'/now/table/cmn_department',
								{},
								qs,
							);
						}
					}
				} else if (resource === 'dictionary') {
					if (operation === 'getAll') {
						const returnAll = this.getNodeParameter('returnAll', i);
						qs = this.getNodeParameter('options', i);

						if (qs.sysparm_fields && typeof qs.sysparm_fields !== 'string') {
							qs.sysparm_fields = (qs.sysparm_fields as string[]).join(',');
						}

						if (!returnAll) {
							const limit = this.getNodeParameter('limit', i);
							qs.sysparm_limit = limit;
							const response = await serviceNowApiRequest.call(
								this,
								'GET',
								'/now/table/sys_dictionary',
								{},
								qs,
							);
							responseData = response.result;
						} else {
							responseData = await serviceNowRequestAllItems.call(
								this,
								'GET',
								'/now/table/sys_dictionary',
								{},
								qs,
							);
						}
					}
				} else if (resource === 'incident') {
					if (operation === 'create') {
						const shortDescription = this.getNodeParameter('short_description', i) as string;
						const additionalFields = this.getNodeParameter('additionalFields', i);
						const body = {
							short_description: shortDescription,
							...additionalFields,
						};

						const response = await serviceNowApiRequest.call(
							this,
							'POST',
							'/now/table/incident',
							body,
						);
						responseData = response.result;
					} else if (operation === 'delete') {
						const id = this.getNodeParameter('id', i) as string;
						responseData = await serviceNowApiRequest.call(
							this,
							'DELETE',
							`/now/table/incident/${id}`,
						);
						responseData = { success: true };
					} else if (operation === 'get') {
						const id = this.getNodeParameter('id', i) as string;
						qs = this.getNodeParameter('options', i);

						if (qs.sysparm_fields && typeof qs.sysparm_fields !== 'string') {
							qs.sysparm_fields = (qs.sysparm_fields as string[]).join(',');
						}

						const response = await serviceNowApiRequest.call(
							this,
							'GET',
							`/now/table/incident/${id}`,
							{},
							qs,
						);
						responseData = response.result;
					} else if (operation === 'getAll') {
						const returnAll = this.getNodeParameter('returnAll', i);
						qs = this.getNodeParameter('options', i);

						if (qs.sysparm_fields && typeof qs.sysparm_fields !== 'string') {
							qs.sysparm_fields = (qs.sysparm_fields as string[]).join(',');
						}

						if (!returnAll) {
							const limit = this.getNodeParameter('limit', i);
							qs.sysparm_limit = limit;
							const response = await serviceNowApiRequest.call(
								this,
								'GET',
								'/now/table/incident',
								{},
								qs,
							);
							responseData = response.result;
						} else {
							responseData = await serviceNowRequestAllItems.call(
								this,
								'GET',
								'/now/table/incident',
								{},
								qs,
							);
						}
					} else if (operation === 'update') {
						const id = this.getNodeParameter('id', i) as string;
						const body = this.getNodeParameter('updateFields', i);

						const response = await serviceNowApiRequest.call(
							this,
							'PATCH',
							`/now/table/incident/${id}`,
							body,
						);
						responseData = response.result;
					} else {
						throw new NodeOperationError(
							this.getNode(),
							`The operation "${operation}" is not known!`,
							{ itemIndex: i },
						);
					}
				} else if (resource === 'tableRecord') {
					if (operation === 'create') {
						const tableName = this.getNodeParameter('tableName', i) as string;
						const dataToSend = this.getNodeParameter('dataToSend', i) as string;
						let body = {};

						if (dataToSend === 'mapInput') {
							const inputsToIgnore = (this.getNodeParameter('inputsToIgnore', i) as string)
								.split(',')
								.map((field) => field.trim());
							body = Object.entries(items[i].json)
								.filter(([key]) => !inputsToIgnore.includes(key))
								.reduce((obj, [key, val]) => Object.assign(obj, { [key]: val }), {});
						} else if (dataToSend === 'columns') {
							const fieldsToSend = this.getNodeParameter('fieldsToSend', i) as {
								field: IDataObject[];
							};
							body = fieldsToSend.field.reduce((obj, field) => {
								obj[field.column as string] = field.value;
								return obj;
							}, {});
						}

						const response = await serviceNowApiRequest.call(
							this,
							'POST',
							`/now/table/${tableName}`,
							body,
						);
						responseData = response.result;
					} else if (operation === 'delete') {
						const tableName = this.getNodeParameter('tableName', i) as string;
						const id = this.getNodeParameter('id', i) as string;
						responseData = await serviceNowApiRequest.call(
							this,
							'DELETE',
							`/now/table/${tableName}/${id}`,
						);
						responseData = { success: true };
					} else if (operation === 'get') {
						const tableName = this.getNodeParameter('tableName', i) as string;
						const id = this.getNodeParameter('id', i) as string;
						qs = this.getNodeParameter('options', i);

						if (qs.sysparm_fields && typeof qs.sysparm_fields !== 'string') {
							qs.sysparm_fields = (qs.sysparm_fields as string[]).join(',');
						}

						const response = await serviceNowApiRequest.call(
							this,
							'GET',
							`/now/table/${tableName}/${id}`,
							{},
							qs,
						);
						responseData = response.result;
					} else if (operation === 'getAll') {
						const tableName = this.getNodeParameter('tableName', i) as string;
						const returnAll = this.getNodeParameter('returnAll', i);
						qs = this.getNodeParameter('options', i);

						if (qs.sysparm_fields && typeof qs.sysparm_fields !== 'string') {
							qs.sysparm_fields = (qs.sysparm_fields as string[]).join(',');
						}

						if (!returnAll) {
							const limit = this.getNodeParameter('limit', i);
							qs.sysparm_limit = limit;
							const response = await serviceNowApiRequest.call(
								this,
								'GET',
								`/now/table/${tableName}`,
								{},
								qs,
							);
							responseData = response.result;
						} else {
							responseData = await serviceNowRequestAllItems.call(
								this,
								'GET',
								`/now/table/${tableName}`,
								{},
								qs,
							);
						}
					} else if (operation === 'update') {
						const tableName = this.getNodeParameter('tableName', i) as string;
						const id = this.getNodeParameter('id', i) as string;
						const dataToSend = this.getNodeParameter('dataToSend', i) as string;
						let body = {};

						if (dataToSend === 'mapInput') {
							const inputsToIgnore = (this.getNodeParameter('inputsToIgnore', i) as string)
								.split(',')
								.map((field) => field.trim());
							body = Object.entries(items[i].json)
								.filter(([key]) => !inputsToIgnore.includes(key))
								.reduce((obj, [key, val]) => Object.assign(obj, { [key]: val }), {});
						} else if (dataToSend === 'columns') {
							const fieldsToSend = this.getNodeParameter('fieldsToSend', i) as {
								field: IDataObject[];
							};
							body = fieldsToSend.field.reduce((obj, field) => {
								obj[field.column as string] = field.value;
								return obj;
							}, {});
						}

						const response = await serviceNowApiRequest.call(
							this,
							'PATCH',
							`/now/table/${tableName}/${id}`,
							body,
						);
						responseData = response.result;
					} else {
						throw new NodeOperationError(
							this.getNode(),
							`The operation "${operation}" is not known!`,
							{ itemIndex: i },
						);
					}
				} else if (resource === 'user') {
					if (operation === 'create') {
						const body = this.getNodeParameter('additionalFields', i);

						const response = await serviceNowApiRequest.call(
							this,
							'POST',
							'/now/table/sys_user',
							body,
						);
						responseData = response.result;
					} else if (operation === 'delete') {
						const id = this.getNodeParameter('id', i) as string;
						responseData = await serviceNowApiRequest.call(
							this,
							'DELETE',
							`/now/table/sys_user/${id}`,
						);
						responseData = { success: true };
					} else if (operation === 'get') {
						const getOption = this.getNodeParameter('getOption', i) as string;
						qs = this.getNodeParameter('options', i);

						if (qs.sysparm_fields && typeof qs.sysparm_fields !== 'string') {
							qs.sysparm_fields = (qs.sysparm_fields as string[]).join(',');
						}

						if (getOption === 'id') {
							const id = this.getNodeParameter('id', i) as string;
							const response = await serviceNowApiRequest.call(
								this,
								'GET',
								`/now/table/sys_user/${id}`,
								{},
								qs,
							);
							responseData = response.result;
						} else {
							const userName = this.getNodeParameter('user_name', i) as string;
							qs.sysparm_query = `user_name=${userName}`;
							qs.sysparm_limit = 1;
							const response = await serviceNowApiRequest.call(
								this,
								'GET',
								'/now/table/sys_user',
								{},
								qs,
							);
							responseData = response.result;
						}
					} else if (operation === 'getAll') {
						const returnAll = this.getNodeParameter('returnAll', i);
						qs = this.getNodeParameter('options', i);

						if (qs.sysparm_fields && typeof qs.sysparm_fields !== 'string') {
							qs.sysparm_fields = (qs.sysparm_fields as string[]).join(',');
						}

						if (!returnAll) {
							const limit = this.getNodeParameter('limit', i);
							qs.sysparm_limit = limit;
							const response = await serviceNowApiRequest.call(
								this,
								'GET',
								'/now/table/sys_user',
								{},
								qs,
							);
							responseData = response.result;
						} else {
							responseData = await serviceNowRequestAllItems.call(
								this,
								'GET',
								'/now/table/sys_user',
								{},
								qs,
							);
						}
					} else if (operation === 'update') {
						const id = this.getNodeParameter('id', i) as string;
						const body = this.getNodeParameter('updateFields', i);

						const response = await serviceNowApiRequest.call(
							this,
							'PATCH',
							`/now/table/sys_user/${id}`,
							body,
						);
						responseData = response.result;
					} else {
						throw new NodeOperationError(
							this.getNode(),
							`The operation "${operation}" is not known!`,
							{ itemIndex: i },
						);
					}
				} else if (resource === 'userGroup') {
					if (operation === 'getAll') {
						const returnAll = this.getNodeParameter('returnAll', i);
						qs = this.getNodeParameter('options', i);

						if (qs.sysparm_fields && typeof qs.sysparm_fields !== 'string') {
							qs.sysparm_fields = (qs.sysparm_fields as string[]).join(',');
						}

						if (!returnAll) {
							const limit = this.getNodeParameter('limit', i);
							qs.sysparm_limit = limit;
							const response = await serviceNowApiRequest.call(
								this,
								'GET',
								'/now/table/sys_user_group',
								{},
								qs,
							);
							responseData = response.result;
						} else {
							responseData = await serviceNowRequestAllItems.call(
								this,
								'GET',
								'/now/table/sys_user_group',
								{},
								qs,
							);
						}
					} else {
						throw new NodeOperationError(
							this.getNode(),
							`The operation "${operation}" is not known!`,
							{ itemIndex: i },
						);
					}
				} else if (resource === 'userRole') {
					if (operation === 'getAll') {
						const returnAll = this.getNodeParameter('returnAll', i);
						qs = this.getNodeParameter('options', i);

						if (qs.sysparm_fields && typeof qs.sysparm_fields !== 'string') {
							qs.sysparm_fields = (qs.sysparm_fields as string[]).join(',');
						}

						if (!returnAll) {
							const limit = this.getNodeParameter('limit', i);
							qs.sysparm_limit = limit;
							const response = await serviceNowApiRequest.call(
								this,
								'GET',
								'/now/table/sys_user_role',
								{},
								qs,
							);
							responseData = response.result;
						} else {
							responseData = await serviceNowRequestAllItems.call(
								this,
								'GET',
								'/now/table/sys_user_role',
								{},
								qs,
							);
						}
					} else {
						throw new NodeOperationError(
							this.getNode(),
							`The operation "${operation}" is not known!`,
							{ itemIndex: i },
						);
					}
				} else {
					throw new NodeOperationError(this.getNode(), `The resource "${resource}" is not known!`, {
						itemIndex: i,
					});
				}
			} catch (error) {
				if (this.continueOnFail()) {
					const executionErrorData = this.helpers.constructExecutionMetaData(
						this.helpers.returnJsonArray({ error: error.message }),
						{ itemData: { item: i } },
					);
					returnData.push(...executionErrorData);
					continue;
				}

				throw error;
			}

			const executionData = this.helpers.constructExecutionMetaData(
				this.helpers.returnJsonArray(responseData),
				{ itemData: { item: i } },
			);

			returnData.push(...executionData);
		}

		if (resource === 'attachment') {
			if (operation === 'get' || operation === 'getAll') {
				return this.prepareOutputData(returnData);
			}
		}
		return this.prepareOutputData(returnData);
	}
}