From 1e551a202f8d3996c6ce92efc0627ee95e0110c5 Mon Sep 17 00:00:00 2001 From: Omar Ajoue Date: Thu, 1 Jul 2021 09:04:24 +0200 Subject: [PATCH 01/29] :rotating_light: Add Workflow testing framework (#1814) * Added flag to simplify output to execute command and created executeAll Also created a command that lists workflows so it can be used by other applications that wish to interact with n8n via CLI. * Added compare funcionality that helps us identify possible breaking changes * :zap: Add JSON flag to output execution result in json format * Add execution time to execution result * Add --output flag to save JSON formated results * Fix typos * Adding usage examples and warning message * Changing command to always output JSON information * :zap: Add concurrency to executeAll command * :sparkles: Add IDs filter to executeAll command * :pencil2: Fix typos * Fix lint issues * Improvements to execute all * Added colors and beautified execute all command output * Improving premature termination of execute all command * Refactored output * Added detection for warnings instead of errors for a few cases * Fixed linting * Improved process finishing * Added encryption key init, removed messages from non-debug run and created sample github actions file * Updated test command to use correct encryption key * Correcting paths * Changed command name and changed concurrency to be slot based * Added the retry functionality * Improved typing and fixed executions filtering * Updated test description * Added concurrency * Added skip list and concurrency * Fixed termination behavior, removed unnecessary code and added main mode to executions * Added special edge cases handling * Added safe debug flag for non tty terminals * Removed comparison and retries for testing * Changed the way we run commands to add more debug info * Test adding concurrency * Testing with regular testing procedure without comparison * Adding compare flag * Fixing short output command * Upgraded concurrency to 16, fixed short output and covered nodes count * Fixed short output condition * Correcting short output information * Correcting the output for short output * Added a string to possible warnings list * Added example usages with more arguments * Improvements to code made by Ivan * Removed colorize output setting (always use colors), moved execution timeout to become static and removed node list from executions * Moving types to a separate file on execute batch command * Changed interfaces file extension to prevent oclif warnings * Updated workflow pdf files * :zap: Change rule when tests run + minor formatting fixes Co-authored-by: dali Co-authored-by: Jan Oberhauser --- .github/workflows/test-workflows.yml | 69 ++ packages/cli/commands/Interfaces.d.ts | 54 ++ packages/cli/commands/execute.ts | 14 +- packages/cli/commands/executeBatch.ts | 796 ++++++++++++++++++++ packages/cli/commands/import/credentials.ts | 3 + packages/cli/commands/import/workflow.ts | 6 + packages/cli/commands/list/workflow.ts | 67 ++ packages/cli/package.json | 2 + packages/cli/tsconfig.json | 3 +- packages/workflow/src/Interfaces.ts | 1 + 10 files changed, 1009 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/test-workflows.yml create mode 100644 packages/cli/commands/Interfaces.d.ts create mode 100644 packages/cli/commands/executeBatch.ts create mode 100644 packages/cli/commands/list/workflow.ts diff --git a/.github/workflows/test-workflows.yml b/.github/workflows/test-workflows.yml new file mode 100644 index 0000000000..909264e0ce --- /dev/null +++ b/.github/workflows/test-workflows.yml @@ -0,0 +1,69 @@ +name: Run test workflows + +on: + schedule: + - cron: "0 2 * * *" + workflow_dispatch: + + +jobs: + run-test-workflows: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [14.x] + steps: + - + name: Checkout + uses: actions/checkout@v2 + with: + path: n8n + - + name: Checkout workflows repo + uses: actions/checkout@v2 + with: + repository: n8n-io/test-workflows + path: test-workflows + - + name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - + name: npm install and build + run: | + cd n8n + npm install + npm run bootstrap + npm run build --if-present + env: + CI: true + shell: bash + - + name: Import credentials + run: n8n/packages/cli/bin/n8n import:credentials --input=test-workflows/credentials.json + shell: bash + env: + N8N_ENCRYPTION_KEY: ${{secrets.ENCRYPTION_KEY}} + - + name: Import workflows + run: n8n/packages/cli/bin/n8n import:workflow --separate --input=test-workflows/workflows + shell: bash + env: + N8N_ENCRYPTION_KEY: ${{secrets.ENCRYPTION_KEY}} + - + name: Copy static assets + run: | + cp n8n/assets/n8n-logo.png /tmp/n8n-logo.png + cp n8n/assets/n8n-screenshot.png /tmp/n8n-screenshot.png + cp n8n/node_modules/pdf-parse/test/data/05-versions-space.pdf /tmp/05-versions-space.pdf + cp n8n/node_modules/pdf-parse/test/data/04-valid.pdf /tmp/04-valid.pdf + shell: bash + - + name: Run tests + run: n8n/packages/cli/bin/n8n executeBatch --shallow --skipList=test-workflows/skipList.txt --shortOutput --concurrency=16 --compare=test-workflows/snapshots + shell: bash + env: + N8N_ENCRYPTION_KEY: ${{secrets.ENCRYPTION_KEY}} diff --git a/packages/cli/commands/Interfaces.d.ts b/packages/cli/commands/Interfaces.d.ts new file mode 100644 index 0000000000..aedd194539 --- /dev/null +++ b/packages/cli/commands/Interfaces.d.ts @@ -0,0 +1,54 @@ +interface IResult { + totalWorkflows: number; + summary: { + failedExecutions: number, + successfulExecutions: number, + warningExecutions: number, + errors: IExecutionError[], + warnings: IExecutionError[], + }; + coveredNodes: { + [nodeType: string]: number + }; + executions: IExecutionResult[]; +} +interface IExecutionResult { + workflowId: string | number; + workflowName: string; + executionTime: number; // Given in seconds with decimals for milisseconds + finished: boolean; + executionStatus: ExecutionStatus; + error?: string; + changes?: string; + coveredNodes: { + [nodeType: string]: number + }; +} + +interface IExecutionError { + workflowId: string | number; + error: string; +} + +interface IWorkflowExecutionProgress { + workflowId: string | number; + status: ExecutionStatus; +} + +interface INodeSpecialCases { + [nodeName: string]: INodeSpecialCase; +} + +interface INodeSpecialCase { + ignoredProperties?: string[]; + capResults?: number; +} + +type ExecutionStatus = 'success' | 'error' | 'warning' | 'running'; + +declare module 'json-diff' { + interface IDiffOptions { + keysOnly?: boolean; + } + export function diff(obj1: unknown, obj2: unknown, diffOptions: IDiffOptions): string; +} diff --git a/packages/cli/commands/execute.ts b/packages/cli/commands/execute.ts index 9eafd28c9e..48ae6a19f0 100644 --- a/packages/cli/commands/execute.ts +++ b/packages/cli/commands/execute.ts @@ -22,7 +22,7 @@ import { WorkflowRunner, } from '../src'; -import { +import { getLogger, } from '../src/Logger'; @@ -46,6 +46,9 @@ export class Execute extends Command { id: flags.string({ description: 'id of the workflow to execute', }), + rawOutput: flags.boolean({ + description: 'Outputs only JSON data, with no other text', + }), }; @@ -183,10 +186,11 @@ export class Execute extends Command { stack: error.stack, }; } - - console.info('Execution was successful:'); - console.info('===================================='); - console.info(JSON.stringify(data, null, 2)); + if (flags.rawOutput === undefined) { + this.log('Execution was successful:'); + this.log('===================================='); + } + this.log(JSON.stringify(data, null, 2)); } catch (e) { console.error('Error executing workflow. See log messages for details.'); logger.error('\nExecution error:'); diff --git a/packages/cli/commands/executeBatch.ts b/packages/cli/commands/executeBatch.ts new file mode 100644 index 0000000000..7bc6bb858d --- /dev/null +++ b/packages/cli/commands/executeBatch.ts @@ -0,0 +1,796 @@ +import * as fs from 'fs'; +import { + Command, + flags, +} from '@oclif/command'; + +import { + UserSettings, +} from 'n8n-core'; + +import { + INode, + INodeExecutionData, + ITaskData, +} from 'n8n-workflow'; + +import { + ActiveExecutions, + CredentialsOverwrites, + CredentialTypes, + Db, + ExternalHooks, + IExecutionsCurrentSummary, + IWorkflowDb, + IWorkflowExecutionDataProcess, + LoadNodesAndCredentials, + NodeTypes, + WorkflowCredentials, + WorkflowRunner, +} from '../src'; + +import { + sep, +} from 'path'; + +import { + diff, +} from 'json-diff'; + +import { + getLogger, +} from '../src/Logger'; + +import { + LoggerProxy, +} from 'n8n-workflow'; + +export class ExecuteBatch extends Command { + static description = '\nExecutes multiple workflows once'; + + static cancelled = false; + + static workflowExecutionsProgress: IWorkflowExecutionProgress[][]; + + static shallow = false; + + static compare: string; + + static snapshot: string; + + static concurrency = 1; + + static debug = false; + + static executionTimeout = 3 * 60 * 1000; + + static examples = [ + `$ n8n executeAll`, + `$ n8n executeAll --concurrency=10 --skipList=/data/skipList.txt`, + `$ n8n executeAll --debug --output=/data/output.json`, + `$ n8n executeAll --ids=10,13,15 --shortOutput`, + `$ n8n executeAll --snapshot=/data/snapshots --shallow`, + `$ n8n executeAll --compare=/data/previousExecutionData --retries=2`, + ]; + + static flags = { + help: flags.help({ char: 'h' }), + debug: flags.boolean({ + description: 'Toggles on displaying all errors and debug messages.', + }), + ids: flags.string({ + description: 'Specifies workflow IDs to get executed, separated by a comma.', + }), + concurrency: flags.integer({ + default: 1, + description: 'How many workflows can run in parallel. Defaults to 1 which means no concurrency.', + }), + output: flags.string({ + description: 'Enable execution saving, You must inform an existing folder to save execution via this param', + }), + snapshot: flags.string({ + description: 'Enables snapshot saving. You must inform an existing folder to save snapshots via this param.', + }), + compare: flags.string({ + description: 'Compares current execution with an existing snapshot. You must inform an existing folder where the snapshots are saved.', + }), + shallow: flags.boolean({ + description: 'Compares only if attributes output from node are the same, with no regards to neste JSON objects.', + }), + skipList: flags.string({ + description: 'File containing a comma separated list of workflow IDs to skip.', + }), + retries: flags.integer({ + description: 'Retries failed workflows up to N tries. Default is 1. Set 0 to disable.', + default: 1, + }), + shortOutput: flags.boolean({ + description: 'Omits the full execution information from output, displaying only summary.', + }), + }; + + /** + * Gracefully handles exit. + * @param {boolean} skipExit Whether to skip exit or number according to received signal + */ + static async stopProcess(skipExit: boolean | number = false) { + + if (ExecuteBatch.cancelled === true) { + process.exit(0); + } + + ExecuteBatch.cancelled = true; + const activeExecutionsInstance = ActiveExecutions.getInstance(); + const stopPromises = activeExecutionsInstance.getActiveExecutions().map(async execution => { + activeExecutionsInstance.stopExecution(execution.id); + }); + + await Promise.allSettled(stopPromises); + + setTimeout(() => { + process.exit(0); + }, 30000); + + let executingWorkflows = activeExecutionsInstance.getActiveExecutions() as IExecutionsCurrentSummary[]; + + let count = 0; + while (executingWorkflows.length !== 0) { + if (count++ % 4 === 0) { + console.log(`Waiting for ${executingWorkflows.length} active executions to finish...`); + executingWorkflows.map(execution => { + console.log(` - Execution ID ${execution.id}, workflow ID: ${execution.workflowId}`); + }); + } + await new Promise((resolve) => { + setTimeout(resolve, 500); + }); + executingWorkflows = activeExecutionsInstance.getActiveExecutions(); + } + // We may receive true but when called from `process.on` + // we get the signal (SIGNIT, etc.) + if (skipExit !== true) { + process.exit(0); + } + } + + formatJsonOutput(data: object) { + return JSON.stringify(data, null, 2); + } + + shouldBeConsideredAsWarning(errorMessage: string) { + + const warningStrings = [ + 'refresh token is invalid', + 'unable to connect to', + 'econnreset', + '429', + 'econnrefused', + 'missing a required parameter', + ]; + + errorMessage = errorMessage.toLowerCase(); + + for (let i = 0; i < warningStrings.length; i++) { + if (errorMessage.includes(warningStrings[i])) { + return true; + } + } + + return false; + } + + + async run() { + + process.on('SIGTERM', ExecuteBatch.stopProcess); + process.on('SIGINT', ExecuteBatch.stopProcess); + + const logger = getLogger(); + LoggerProxy.init(logger); + + const { flags } = this.parse(ExecuteBatch); + + ExecuteBatch.debug = flags.debug === true; + ExecuteBatch.concurrency = flags.concurrency || 1; + + const ids: number[] = []; + const skipIds: number[] = []; + + if (flags.snapshot !== undefined) { + if (fs.existsSync(flags.snapshot)) { + if (!fs.lstatSync(flags.snapshot).isDirectory()) { + console.log(`The parameter --snapshot must be an existing directory`); + return; + } + } else { + console.log(`The parameter --snapshot must be an existing directory`); + return; + } + + ExecuteBatch.snapshot = flags.snapshot; + } + if (flags.compare !== undefined) { + if (fs.existsSync(flags.compare)) { + if (!fs.lstatSync(flags.compare).isDirectory()) { + console.log(`The parameter --compare must be an existing directory`); + return; + } + } else { + console.log(`The parameter --compare must be an existing directory`); + return; + } + + ExecuteBatch.compare = flags.compare; + } + + if (flags.output !== undefined) { + if (fs.existsSync(flags.output)) { + if (fs.lstatSync(flags.output).isDirectory()) { + console.log(`The parameter --output must be a writable file`); + return; + } + } + } + + if (flags.ids !== undefined) { + const paramIds = flags.ids.split(','); + const re = /\d+/; + const matchedIds = paramIds.filter(id => id.match(re)).map(id => parseInt(id.trim(), 10)); + + if (matchedIds.length === 0) { + console.log(`The parameter --ids must be a list of numeric IDs separated by a comma.`); + return; + } + + ids.push(...matchedIds); + } + + if (flags.skipList !== undefined) { + if (fs.existsSync(flags.skipList)) { + const contents = fs.readFileSync(flags.skipList, { encoding: 'utf-8' }); + skipIds.push(...contents.split(',').map(id => parseInt(id.trim(), 10))); + } else { + console.log('Skip list file not found. Exiting.'); + return; + } + } + + if (flags.shallow === true) { + ExecuteBatch.shallow = true; + } + + + // Start directly with the init of the database to improve startup time + const startDbInitPromise = Db.init(); + + // Load all node and credential types + const loadNodesAndCredentials = LoadNodesAndCredentials(); + const loadNodesAndCredentialsPromise = loadNodesAndCredentials.init(); + + // Make sure the settings exist + await UserSettings.prepareUserSettings(); + + // Wait till the database is ready + await startDbInitPromise; + + let allWorkflows; + + const query = Db.collections!.Workflow!.createQueryBuilder('workflows'); + + if (ids.length > 0) { + query.andWhere(`workflows.id in (:...ids)`, { ids }); + } + + if (skipIds.length > 0) { + query.andWhere(`workflows.id not in (:...skipIds)`, { skipIds }); + } + + allWorkflows = await query.getMany() as IWorkflowDb[]; + + if (ExecuteBatch.debug === true) { + process.stdout.write(`Found ${allWorkflows.length} workflows to execute.\n`); + } + + // Wait till the n8n-packages have been read + await loadNodesAndCredentialsPromise; + + // Load the credentials overwrites if any exist + await CredentialsOverwrites().init(); + + // Load all external hooks + const externalHooks = ExternalHooks(); + await externalHooks.init(); + + // Add the found types to an instance other parts of the application can use + const nodeTypes = NodeTypes(); + await nodeTypes.init(loadNodesAndCredentials.nodeTypes); + const credentialTypes = CredentialTypes(); + await credentialTypes.init(loadNodesAndCredentials.credentialTypes); + + // Send a shallow copy of allWorkflows so we still have all workflow data. + const results = await this.runTests([...allWorkflows]); + + let { retries } = flags; + + while (retries > 0 && (results.summary.warningExecutions + results.summary.failedExecutions > 0) && ExecuteBatch.cancelled === false) { + const failedWorkflowIds = results.summary.errors.map(execution => execution.workflowId); + failedWorkflowIds.push(...results.summary.warnings.map(execution => execution.workflowId)); + + const newWorkflowList = allWorkflows.filter(workflow => failedWorkflowIds.includes(workflow.id)); + + const retryResults = await this.runTests(newWorkflowList); + + this.mergeResults(results, retryResults); + // By now, `results` has been updated with the new successful executions. + retries--; + } + + if (flags.output !== undefined) { + fs.writeFileSync(flags.output, this.formatJsonOutput(results)); + console.log('\nExecution finished.'); + console.log('Summary:'); + console.log(`\tSuccess: ${results.summary.successfulExecutions}`); + console.log(`\tFailures: ${results.summary.failedExecutions}`); + console.log(`\tWarnings: ${results.summary.warningExecutions}`); + console.log('\nNodes successfully tested:'); + Object.entries(results.coveredNodes).forEach(([nodeName, nodeCount]) => { + console.log(`\t${nodeName}: ${nodeCount}`); + }); + console.log('\nCheck the JSON file for more details.'); + } else { + if (flags.shortOutput === true) { + console.log(this.formatJsonOutput({ ...results, executions: results.executions.filter(execution => execution.executionStatus !== 'success') })); + } else { + console.log(this.formatJsonOutput(results)); + } + } + + await ExecuteBatch.stopProcess(true); + + if (results.summary.failedExecutions > 0) { + this.exit(1); + } + this.exit(0); + + } + + mergeResults(results: IResult, retryResults: IResult) { + + if (retryResults.summary.successfulExecutions === 0) { + // Nothing to replace. + return; + } + + // Find successful executions and replace them on previous result. + retryResults.executions.forEach(newExecution => { + if (newExecution.executionStatus === 'success') { + // Remove previous execution from list. + results.executions = results.executions.filter(previousExecutions => previousExecutions.workflowId !== newExecution.workflowId); + + const errorIndex = results.summary.errors.findIndex(summaryInformation => summaryInformation.workflowId === newExecution.workflowId); + if (errorIndex !== -1) { + // This workflow errored previously. Decrement error count. + results.summary.failedExecutions--; + // Remove from the list of errors. + results.summary.errors.splice(errorIndex, 1); + } + + const warningIndex = results.summary.warnings.findIndex(summaryInformation => summaryInformation.workflowId === newExecution.workflowId); + if (warningIndex !== -1) { + // This workflow errored previously. Decrement error count. + results.summary.warningExecutions--; + // Remove from the list of errors. + results.summary.warnings.splice(warningIndex, 1); + } + // Increment successful executions count and push it to all executions array. + results.summary.successfulExecutions++; + results.executions.push(newExecution); + } + }); + } + + async runTests(allWorkflows: IWorkflowDb[]): Promise { + const result: IResult = { + totalWorkflows: allWorkflows.length, + summary: { + failedExecutions: 0, + warningExecutions: 0, + successfulExecutions: 0, + errors: [], + warnings: [], + }, + coveredNodes: {}, + executions: [], + }; + + if (ExecuteBatch.debug) { + this.initializeLogs(); + } + + return new Promise(async (res) => { + const promisesArray = []; + for (let i = 0; i < ExecuteBatch.concurrency; i++) { + const promise = new Promise(async (resolve) => { + let workflow: IWorkflowDb | undefined; + while (allWorkflows.length > 0) { + workflow = allWorkflows.shift(); + if (ExecuteBatch.cancelled === true) { + process.stdout.write(`Thread ${i + 1} resolving and quitting.`); + resolve(true); + break; + } + // This if shouldn't be really needed + // but it's a concurrency precaution. + if (workflow === undefined) { + resolve(true); + return; + } + + if (ExecuteBatch.debug) { + ExecuteBatch.workflowExecutionsProgress[i].push({ + workflowId: workflow.id, + status: 'running', + }); + this.updateStatus(); + } + + await this.startThread(workflow).then((executionResult) => { + if (ExecuteBatch.debug) { + ExecuteBatch.workflowExecutionsProgress[i].pop(); + } + result.executions.push(executionResult); + if (executionResult.executionStatus === 'success') { + if (ExecuteBatch.debug) { + ExecuteBatch.workflowExecutionsProgress[i].push({ + workflowId: workflow!.id, + status: 'success', + }); + this.updateStatus(); + } + result.summary.successfulExecutions++; + const nodeNames = Object.keys(executionResult.coveredNodes); + + nodeNames.map(nodeName => { + if (result.coveredNodes[nodeName] === undefined) { + result.coveredNodes[nodeName] = 0; + } + result.coveredNodes[nodeName] += executionResult.coveredNodes[nodeName]; + }); + } else if (executionResult.executionStatus === 'warning') { + result.summary.warningExecutions++; + result.summary.warnings.push({ + workflowId: executionResult.workflowId, + error: executionResult.error!, + }); + if (ExecuteBatch.debug) { + ExecuteBatch.workflowExecutionsProgress[i].push({ + workflowId: workflow!.id, + status: 'warning', + }); + this.updateStatus(); + } + } else if (executionResult.executionStatus === 'error') { + result.summary.failedExecutions++; + result.summary.errors.push({ + workflowId: executionResult.workflowId, + error: executionResult.error!, + }); + if (ExecuteBatch.debug) { + ExecuteBatch.workflowExecutionsProgress[i].push({ + workflowId: workflow!.id, + status: 'error', + }); + this.updateStatus(); + } + } else { + throw new Error('Wrong execution status - cannot proceed'); + } + }); + } + + resolve(true); + }); + + promisesArray.push(promise); + } + + await Promise.allSettled(promisesArray); + + res(result); + }); + } + + updateStatus() { + + if (ExecuteBatch.cancelled === true) { + return; + } + + if (process.stdout.isTTY === true) { + process.stdout.moveCursor(0, - (ExecuteBatch.concurrency)); + process.stdout.cursorTo(0); + process.stdout.clearLine(0); + } + + + ExecuteBatch.workflowExecutionsProgress.map((concurrentThread, index) => { + let message = `${index + 1}: `; + concurrentThread.map((executionItem, workflowIndex) => { + let openColor = '\x1b[0m'; + const closeColor = '\x1b[0m'; + switch (executionItem.status) { + case 'success': + openColor = '\x1b[32m'; + break; + case 'error': + openColor = '\x1b[31m'; + break; + case 'warning': + openColor = '\x1b[33m'; + break; + default: + break; + } + message += (workflowIndex > 0 ? ', ' : '') + `${openColor}${executionItem.workflowId}${closeColor}`; + }); + if (process.stdout.isTTY === true) { + process.stdout.cursorTo(0); + process.stdout.clearLine(0); + } + process.stdout.write(message + '\n'); + }); + } + + initializeLogs() { + process.stdout.write('**********************************************\n'); + process.stdout.write(' n8n test workflows\n'); + process.stdout.write('**********************************************\n'); + process.stdout.write('\n'); + process.stdout.write('Batch number:\n'); + ExecuteBatch.workflowExecutionsProgress = []; + for (let i = 0; i < ExecuteBatch.concurrency; i++) { + ExecuteBatch.workflowExecutionsProgress.push([]); + process.stdout.write(`${i + 1}: \n`); + } + } + + startThread(workflowData: IWorkflowDb): Promise { + // This will be the object returned by the promise. + // It will be updated according to execution progress below. + const executionResult: IExecutionResult = { + workflowId: workflowData.id, + workflowName: workflowData.name, + executionTime: 0, + finished: false, + executionStatus: 'running', + coveredNodes: {}, + }; + + + + const requiredNodeTypes = ['n8n-nodes-base.start']; + let startNode: INode | undefined = undefined; + for (const node of workflowData.nodes) { + if (requiredNodeTypes.includes(node.type)) { + startNode = node; + break; + } + } + + // We have a cool feature here. + // On each node, on the Settings tab in the node editor you can change + // the `Notes` field to add special cases for comparison and snapshots. + // You need to set one configuration per line with the following possible keys: + // CAP_RESULTS_LENGTH=x where x is a number. Cap the number of rows from this node to x. + // This means if you set CAP_RESULTS_LENGTH=1 we will have only 1 row in the output + // IGNORED_PROPERTIES=x,y,z where x, y and z are JSON property names. Removes these + // properties from the JSON object (useful for optional properties that can + // cause the comparison to detect changes when not true). + const nodeEdgeCases = {} as INodeSpecialCases; + workflowData.nodes.forEach(node => { + executionResult.coveredNodes[node.type] = (executionResult.coveredNodes[node.type] || 0) + 1; + if (node.notes !== undefined && node.notes !== '') { + node.notes.split('\n').forEach(note => { + const parts = note.split('='); + if (parts.length === 2) { + if (nodeEdgeCases[node.name] === undefined) { + nodeEdgeCases[node.name] = {} as INodeSpecialCase; + } + if (parts[0] === 'CAP_RESULTS_LENGTH') { + nodeEdgeCases[node.name].capResults = parseInt(parts[1], 10); + } else if (parts[0] === 'IGNORED_PROPERTIES') { + nodeEdgeCases[node.name].ignoredProperties = parts[1].split(',').map(property => property.trim()); + } + } + }); + } + }); + + return new Promise(async (resolve) => { + if (startNode === undefined) { + // If the workflow does not contain a start-node we can not know what + // should be executed and with which data to start. + executionResult.error = 'Workflow cannot be started as it does not contain a "Start" node.'; + executionResult.executionStatus = 'warning'; + resolve(executionResult); + } + + let gotCancel = false; + + // Timeouts execution after 5 minutes. + const timeoutTimer = setTimeout(() => { + gotCancel = true; + executionResult.error = 'Workflow execution timed out.'; + executionResult.executionStatus = 'warning'; + resolve(executionResult); + }, ExecuteBatch.executionTimeout); + + + try { + const credentials = await WorkflowCredentials(workflowData!.nodes); + + const runData: IWorkflowExecutionDataProcess = { + credentials, + executionMode: 'cli', + startNodes: [startNode!.name], + workflowData: workflowData!, + }; + + const workflowRunner = new WorkflowRunner(); + const executionId = await workflowRunner.run(runData); + + const activeExecutions = ActiveExecutions.getInstance(); + const data = await activeExecutions.getPostExecutePromise(executionId); + if (gotCancel || ExecuteBatch.cancelled === true) { + clearTimeout(timeoutTimer); + // The promise was settled already so we simply ignore. + return; + } + + if (data === undefined) { + executionResult.error = 'Workflow did not return any data.'; + executionResult.executionStatus = 'error'; + } else { + executionResult.executionTime = (Date.parse(data.stoppedAt as unknown as string) - Date.parse(data.startedAt as unknown as string)) / 1000; + executionResult.finished = (data?.finished !== undefined) as boolean; + + if (data.data.resultData.error) { + executionResult.error = + data.data.resultData.error.hasOwnProperty('description') ? + // @ts-ignore + data.data.resultData.error.description : data.data.resultData.error.message; + if (data.data.resultData.lastNodeExecuted !== undefined) { + executionResult.error += ` on node ${data.data.resultData.lastNodeExecuted}`; + } + executionResult.executionStatus = 'error'; + + if (this.shouldBeConsideredAsWarning(executionResult.error || '')) { + executionResult.executionStatus = 'warning'; + } + } else { + if (ExecuteBatch.shallow === true) { + // What this does is guarantee that top-level attributes + // from the JSON are kept and the are the same type. + + // We convert nested JSON objects to a simple {object:true} + // and we convert nested arrays to ['json array'] + + // This reduces the chance of false positives but may + // result in not detecting deeper changes. + Object.keys(data.data.resultData.runData).map((nodeName: string) => { + data.data.resultData.runData[nodeName].map((taskData: ITaskData) => { + if (taskData.data === undefined) { + return; + } + Object.keys(taskData.data).map(connectionName => { + const connection = taskData.data![connectionName] as Array; + connection.map(executionDataArray => { + if (executionDataArray === null) { + return; + } + + if (nodeEdgeCases[nodeName] !== undefined && nodeEdgeCases[nodeName].capResults !== undefined) { + executionDataArray.splice(nodeEdgeCases[nodeName].capResults!); + } + + executionDataArray.map(executionData => { + if (executionData.json === undefined) { + return; + } + if (nodeEdgeCases[nodeName] !== undefined && nodeEdgeCases[nodeName].ignoredProperties !== undefined) { + nodeEdgeCases[nodeName].ignoredProperties!.forEach(ignoredProperty => delete executionData.json[ignoredProperty]); + } + + const jsonProperties = executionData.json; + + const nodeOutputAttributes = Object.keys(jsonProperties); + nodeOutputAttributes.map(attributeName => { + if (Array.isArray(jsonProperties[attributeName])) { + jsonProperties[attributeName] = ['json array']; + } else if (typeof jsonProperties[attributeName] === 'object') { + jsonProperties[attributeName] = { object: true }; + } + }); + }); + }); + + }); + }); + }); + } else { + // If not using shallow comparison then we only treat nodeEdgeCases. + const specialCases = Object.keys(nodeEdgeCases); + + specialCases.forEach(nodeName => { + data.data.resultData.runData[nodeName].map((taskData: ITaskData) => { + if (taskData.data === undefined) { + return; + } + Object.keys(taskData.data).map(connectionName => { + const connection = taskData.data![connectionName] as Array; + connection.map(executionDataArray => { + if (executionDataArray === null) { + return; + } + + if (nodeEdgeCases[nodeName].capResults !== undefined) { + executionDataArray.splice(nodeEdgeCases[nodeName].capResults!); + } + + if (nodeEdgeCases[nodeName].ignoredProperties !== undefined) { + executionDataArray.map(executionData => { + if (executionData.json === undefined) { + return; + } + nodeEdgeCases[nodeName].ignoredProperties!.forEach(ignoredProperty => delete executionData.json[ignoredProperty]); + }); + } + }); + + }); + }); + }); + } + + const serializedData = this.formatJsonOutput(data); + if (ExecuteBatch.compare === undefined) { + executionResult.executionStatus = 'success'; + } else { + const fileName = (ExecuteBatch.compare.endsWith(sep) ? ExecuteBatch.compare : ExecuteBatch.compare + sep) + `${workflowData.id}-snapshot.json`; + if (fs.existsSync(fileName) === true) { + + const contents = fs.readFileSync(fileName, { encoding: 'utf-8' }); + + const changes = diff(JSON.parse(contents), data, { keysOnly: true }); + + if (changes !== undefined) { + // we have structural changes. Report them. + executionResult.error = `Workflow may contain breaking changes`; + executionResult.changes = changes; + executionResult.executionStatus = 'error'; + } else { + executionResult.executionStatus = 'success'; + } + } else { + executionResult.error = 'Snapshot for not found.'; + executionResult.executionStatus = 'warning'; + } + } + // Save snapshots only after comparing - this is to make sure we're updating + // After comparing to existing verion. + if (ExecuteBatch.snapshot !== undefined) { + const fileName = (ExecuteBatch.snapshot.endsWith(sep) ? ExecuteBatch.snapshot : ExecuteBatch.snapshot + sep) + `${workflowData.id}-snapshot.json`; + fs.writeFileSync(fileName, serializedData); + } + } + } + } catch (e) { + executionResult.error = 'Workflow failed to execute.'; + executionResult.executionStatus = 'error'; + } + clearTimeout(timeoutTimer); + resolve(executionResult); + }); + } + +} diff --git a/packages/cli/commands/import/credentials.ts b/packages/cli/commands/import/credentials.ts index b038f33693..af2d7e0d46 100644 --- a/packages/cli/commands/import/credentials.ts +++ b/packages/cli/commands/import/credentials.ts @@ -65,6 +65,9 @@ export class ImportCredentialsCommand extends Command { try { await Db.init(); + + // Make sure the settings exist + await UserSettings.prepareUserSettings(); let i; const encryptionKey = await UserSettings.getEncryptionKey(); diff --git a/packages/cli/commands/import/workflow.ts b/packages/cli/commands/import/workflow.ts index 5b31041a44..65ddb77000 100644 --- a/packages/cli/commands/import/workflow.ts +++ b/packages/cli/commands/import/workflow.ts @@ -18,6 +18,9 @@ import { import * as fs from 'fs'; import * as glob from 'glob-promise'; import * as path from 'path'; +import { + UserSettings, +} from 'n8n-core'; export class ImportWorkflowsCommand extends Command { static description = 'Import workflows'; @@ -60,6 +63,9 @@ export class ImportWorkflowsCommand extends Command { try { await Db.init(); + + // Make sure the settings exist + await UserSettings.prepareUserSettings(); let i; if (flags.separate) { const files = await glob((flags.input.endsWith(path.sep) ? flags.input : flags.input + path.sep) + '*.json'); diff --git a/packages/cli/commands/list/workflow.ts b/packages/cli/commands/list/workflow.ts new file mode 100644 index 0000000000..6fdca2e253 --- /dev/null +++ b/packages/cli/commands/list/workflow.ts @@ -0,0 +1,67 @@ +import { + Command, + flags, +} from '@oclif/command'; + +import { + IDataObject +} from 'n8n-workflow'; + +import { + Db, +} from "../../src"; + + +export class ListWorkflowCommand extends Command { + static description = '\nList workflows'; + + static examples = [ + '$ n8n list:workflow', + '$ n8n list:workflow --active=true --onlyId', + '$ n8n list:workflow --active=false', + ]; + + static flags = { + help: flags.help({ char: 'h' }), + active: flags.string({ + description: 'Filters workflows by active status. Can be true or false', + }), + onlyId: flags.boolean({ + description: 'Outputs workflow IDs only, one per line.', + }), + }; + + async run() { + const { flags } = this.parse(ListWorkflowCommand); + + if (flags.active !== undefined && !['true', 'false'].includes(flags.active)) { + this.error('The --active flag has to be passed using true or false'); + } + + try { + await Db.init(); + + const findQuery: IDataObject = {}; + if (flags.active !== undefined) { + findQuery.active = flags.active === 'true'; + } + + const workflows = await Db.collections.Workflow!.find(findQuery); + if (flags.onlyId) { + workflows.forEach(workflow => console.log(workflow.id)); + } else { + workflows.forEach(workflow => console.log(workflow.id + "|" + workflow.name)); + } + + + } catch (e) { + console.error('\nGOT ERROR'); + console.log('===================================='); + console.error(e.message); + console.error(e.stack); + this.exit(1); + } + + this.exit(); + } +} diff --git a/packages/cli/package.json b/packages/cli/package.json index ca4a198df1..ce0467aa64 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -82,6 +82,7 @@ "dependencies": { "@oclif/command": "^1.5.18", "@oclif/errors": "^1.2.2", + "@types/json-diff": "^0.5.1", "@types/jsonwebtoken": "^8.5.2", "basic-auth": "^2.0.1", "bcryptjs": "^2.4.3", @@ -101,6 +102,7 @@ "glob-promise": "^3.4.0", "google-timezones-json": "^1.0.2", "inquirer": "^7.0.1", + "json-diff": "^0.5.4", "jsonwebtoken": "^8.5.1", "jwks-rsa": "~1.12.1", "localtunnel": "^2.0.0", diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 4aaf4747fc..aa44bc610f 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -1,7 +1,8 @@ { "compilerOptions": { "lib": [ - "es2017" + "es2017", + "ES2020.Promise" ], "types": [ "node", diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index e76da5fb2c..f48d605e2d 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -348,6 +348,7 @@ export interface INode { type: string; position: [number, number]; disabled?: boolean; + notes?: string; notesInFlow?: boolean; retryOnFail?: boolean; maxTries?: number; From a42bbf88f812d0b5a51ab8bdae5cd5b3b0dda3d5 Mon Sep 17 00:00:00 2001 From: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> Date: Thu, 1 Jul 2021 10:15:28 +0200 Subject: [PATCH 02/29] :zap: Enable tags selection (#1960) --- packages/editor-ui/src/components/WorkflowOpen.vue | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/editor-ui/src/components/WorkflowOpen.vue b/packages/editor-ui/src/components/WorkflowOpen.vue index 568b35a574..77e9e8a640 100644 --- a/packages/editor-ui/src/components/WorkflowOpen.vue +++ b/packages/editor-ui/src/components/WorkflowOpen.vue @@ -32,7 +32,7 @@ @@ -124,6 +124,11 @@ export default mixins( updateTagsFilter(tags: string[]) { this.filterTagIds = tags; }, + onTagClick(tagId: string) { + if (tagId !== 'count' && !this.filterTagIds.includes(tagId)) { + this.filterTagIds.push(tagId); + } + }, async openWorkflow (data: IWorkflowShortResponse, column: any) { // tslint:disable-line:no-any if (column.label !== 'Active') { From 5ad4bf0890729ccaf176b91f38c3b6c795b8ee0a Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 1 Jul 2021 10:17:02 +0200 Subject: [PATCH 03/29] :construction_worker: Automatically push changed credentials after test --- .github/workflows/test-workflows.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/test-workflows.yml b/.github/workflows/test-workflows.yml index 909264e0ce..15e813b7ae 100644 --- a/.github/workflows/test-workflows.yml +++ b/.github/workflows/test-workflows.yml @@ -67,3 +67,17 @@ jobs: shell: bash env: N8N_ENCRYPTION_KEY: ${{secrets.ENCRYPTION_KEY}} + - + name: Export credentials + run: n8n/packages/cli/bin/n8n export:credentials --output=test-workflows/credentials.json --all --pretty + shell: bash + env: + N8N_ENCRYPTION_KEY: ${{secrets.ENCRYPTION_KEY}} + - + name: Commit and push credential changes + run: | + cd test-workflows + git config --global user.name 'n8n test bot' + git config --global user.email 'n8n-test-bot@users.noreply.github.com' + git commit -am "Automated credential update" + git push From 5f76a5dc720fc9cad525038ca2824f59a2da54e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 2 Jul 2021 23:34:12 +0200 Subject: [PATCH 04/29] :zap: Expand Zoho node (#1763) * :zap: Initial refactor of Zoho node * :zap: Refactor out extra credentials parameter * :fire: Remove unused filters * :zap: Fix date of birth fields * :zap: Fix param casing * :zap: Adjust param types * :zap: Adjust invoice operations * :zap: Refactor types in adjusters * :zap: Add product resource * :zap: Refactor product details field * :zap: Adjust purchase order params * :zap: Adjust quote params * :zap: Adjust sales orders params * :fire: Remove old unused files * :zap: Add vendor resource * :zap: Fix minor details * :zap: Implement continueOnFail * :bug: Fix empty response for getAll * :zap: Simplify response for single item * :fire: Remove unused import * :hammer: Restore old node name * :zap: Prevent request on empty update * :zap: Apply Dali's suggestions * :zap: Improvements * :zap: Add filters for lead:getAll * :zap: Add upsert to all resources * :zap: Add filters to all getAll operations * :hammer: Restore continue on fail * :hammer: Refactor upsert addition * :hammer: Refactor getFields for readability * :zap: Add custom fields to all create-update ops * :zap: Implement custom fields adjuster * :fire: Remove logging * :shirt: Appease linter * :shirt: Refactor type helper for linter * :zap: Fix refactored type * :hammer: Refactor reduce for simplicity * :zap: Fix vendor:getAll filter options * :zap: Fix custom fields for product operations * :zap: Make sort_by into options param * :truck: Rename upsert operation * :pencil2: Add descriptions to upsert * :zap: Deduplicate system-defined check fields * :hammer: Re-order address fields * :pencil2: Generalize references in getAll fields * :fire: Remove extra comma * :zap: Make getFields helper more readable * :pencil2: Touch up description for account ID * :fire: Remove currency from contacts * :hammer: Resort emails and phones for contact * :bug: Fix sales cycle duration param type * :pencil2: Clarify descriptions with percentages * :hammer: Reorder total fields * :pencil2: Clarify percentages for discounts * :pencil2: Clarify percentages for commissions * :hammer: Convert currency to picklist * :pencil2: Add documentation links * :zap: Add resource loaders for picklists * :zap: Fix build * :hammer: Refactor product details * :zap: Add resolve data to all resources * :zap: Change resolve data toggle default * :zap: Restore lead:getFields operation * :fire: Remove upsert descriptions * :hammer: Change casing for upsert operations * :zap: Add operation descriptions * :hammer: Restore makeResolve default value * :hammer: Return nested details * :zap: Reposition Resolve Data toggles * :pencil2: Document breaking changes * Revert "Reposition Resolve Data toggles" This reverts commit 72ac41780b3ebec9cc6a5bf527e154ffe6ed884a. * :zap: Improvements Co-authored-by: ricardo --- packages/cli/BREAKING-CHANGES.md | 10 + .../nodes-base/nodes/Zoho/GenericFunctions.ts | 417 +++- .../nodes-base/nodes/Zoho/LeadDescription.ts | 706 ------- .../nodes-base/nodes/Zoho/LeadInterface.ts | 37 - .../nodes-base/nodes/Zoho/ZohoCrm.node.ts | 1751 +++++++++++++---- .../Zoho/descriptions/AccountDescription.ts | 415 ++++ .../Zoho/descriptions/ContactDescription.ts | 590 ++++++ .../Zoho/descriptions/DealDescription.ts | 385 ++++ .../Zoho/descriptions/InvoiceDescription.ts | 463 +++++ .../Zoho/descriptions/LeadDescription.ts | 694 +++++++ .../Zoho/descriptions/ProductDescription.ts | 352 ++++ .../descriptions/PurchaseOrderDescription.ts | 590 ++++++ .../Zoho/descriptions/QuoteDescription.ts | 459 +++++ .../descriptions/SalesOrderDescription.ts | 569 ++++++ .../nodes/Zoho/descriptions/SharedFields.ts | 577 ++++++ .../Zoho/descriptions/VendorDescription.ts | 301 +++ .../nodes/Zoho/descriptions/index.ts | 10 + packages/nodes-base/nodes/Zoho/types.d.ts | 103 + packages/nodes-base/nodes/Zoho/zoho.svg | 2 +- 19 files changed, 7297 insertions(+), 1134 deletions(-) delete mode 100644 packages/nodes-base/nodes/Zoho/LeadDescription.ts delete mode 100644 packages/nodes-base/nodes/Zoho/LeadInterface.ts create mode 100644 packages/nodes-base/nodes/Zoho/descriptions/AccountDescription.ts create mode 100644 packages/nodes-base/nodes/Zoho/descriptions/ContactDescription.ts create mode 100644 packages/nodes-base/nodes/Zoho/descriptions/DealDescription.ts create mode 100644 packages/nodes-base/nodes/Zoho/descriptions/InvoiceDescription.ts create mode 100644 packages/nodes-base/nodes/Zoho/descriptions/LeadDescription.ts create mode 100644 packages/nodes-base/nodes/Zoho/descriptions/ProductDescription.ts create mode 100644 packages/nodes-base/nodes/Zoho/descriptions/PurchaseOrderDescription.ts create mode 100644 packages/nodes-base/nodes/Zoho/descriptions/QuoteDescription.ts create mode 100644 packages/nodes-base/nodes/Zoho/descriptions/SalesOrderDescription.ts create mode 100644 packages/nodes-base/nodes/Zoho/descriptions/SharedFields.ts create mode 100644 packages/nodes-base/nodes/Zoho/descriptions/VendorDescription.ts create mode 100644 packages/nodes-base/nodes/Zoho/descriptions/index.ts create mode 100644 packages/nodes-base/nodes/Zoho/types.d.ts diff --git a/packages/cli/BREAKING-CHANGES.md b/packages/cli/BREAKING-CHANGES.md index 2a762e0476..a7ac4db33b 100644 --- a/packages/cli/BREAKING-CHANGES.md +++ b/packages/cli/BREAKING-CHANGES.md @@ -2,6 +2,16 @@ This list shows all the versions which include breaking changes and how to upgrade. +## 0.127.0 + +### What changed? + +For the Zoho node, the `lead:create` operation now requires a "Company" parameter, the parameter "Address" is now inside "Additional Options", and the parameters "Title" and "Is Duplicate Record" were removed. Also, the `lead:delete` operation now returns only the `id` of the deleted lead. + +### When is action necessary? + +If you are using `lead:create` with "Company" or "Address", reset the parameters; for the other two parameters, no action needed. If you are using the response from `lead:delete`, reselect the `id` key. + ## 0.118.0 ### What changed? diff --git a/packages/nodes-base/nodes/Zoho/GenericFunctions.ts b/packages/nodes-base/nodes/Zoho/GenericFunctions.ts index d5e07a3877..fa862de03b 100644 --- a/packages/nodes-base/nodes/Zoho/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zoho/GenericFunctions.ts @@ -1,59 +1,434 @@ -import { +import { OptionsWithUri, } from 'request'; import { IExecuteFunctions, - IExecuteSingleFunctions, - ILoadOptionsFunctions, + IHookFunctions, } from 'n8n-core'; + import { - IDataObject, NodeApiError + IDataObject, + ILoadOptionsFunctions, + NodeApiError, + NodeOperationError, } from 'n8n-workflow'; -export async function zohoApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any - const { oauthTokenData: { api_domain } } = this.getCredentials('zohoOAuth2Api') as { [key: string]: IDataObject }; +import { + flow, + sortBy, +} from 'lodash'; + +import { + AllFields, + CamelCaseResource, + DateType, + GetAllFilterOptions, + IdType, + LoadedFields, + LoadedLayouts, + LocationType, + NameType, + ProductDetails, + ResourceItems, + SnakeCaseResource, + ZohoOAuth2ApiCredentials, +} from './types'; + +export async function zohoApiRequest( + this: IExecuteFunctions | IHookFunctions | ILoadOptionsFunctions, + method: string, + endpoint: string, + body: IDataObject = {}, + qs: IDataObject = {}, + uri?: string, +) { + const { oauthTokenData } = this.getCredentials('zohoOAuth2Api') as ZohoOAuth2ApiCredentials; const options: OptionsWithUri = { - headers: { - 'Content-Type': 'application/json', - }, - method, body: { data: [ body, ], }, + method, qs, - uri: uri || `${api_domain}/crm/v2${resource}`, + uri: uri ?? `${oauthTokenData.api_domain}/crm/v2${endpoint}`, json: true, }; + + if (!Object.keys(body).length) { + delete options.body; + } + + if (!Object.keys(qs).length) { + delete options.qs; + } + try { - //@ts-ignore - return await this.helpers.requestOAuth2.call(this, 'zohoOAuth2Api', options); + const responseData = await this.helpers.requestOAuth2?.call(this, 'zohoOAuth2Api', options); + + if (responseData === undefined) return []; + + throwOnErrorStatus.call(this, responseData); + + return responseData; } catch (error) { throw new NodeApiError(this.getNode(), error); } } -export async function zohoApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any - +/** + * Make an authenticated API request to Zoho CRM API and return all items. + */ +export async function zohoApiRequestAllItems( + this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, + method: string, + endpoint: string, + body: IDataObject = {}, + qs: IDataObject = {}, +) { const returnData: IDataObject[] = []; let responseData; let uri: string | undefined; - query.per_page = 200; - query.page = 0; + qs.per_page = 200; + qs.page = 0; do { - responseData = await zohoApiRequest.call(this, method, endpoint, body, query, uri); + responseData = await zohoApiRequest.call(this, method, endpoint, body, qs, uri); + if (Array.isArray(responseData) && !responseData.length) return returnData; + returnData.push(...responseData.data); uri = responseData.info.more_records; - returnData.push.apply(returnData, responseData[propertyName]); - query.page++; + qs.page++; } while ( responseData.info.more_records !== undefined && responseData.info.more_records === true ); return returnData; -} \ No newline at end of file +} + +/** + * Handle a Zoho CRM API listing by returning all items or up to a limit. + */ +export async function handleListing( + this: IExecuteFunctions, + method: string, + endpoint: string, + body: IDataObject = {}, + qs: IDataObject = {}, +) { + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + + if (returnAll) { + return await zohoApiRequestAllItems.call(this, method, endpoint, body, qs); + } + + const responseData = await zohoApiRequestAllItems.call(this, method, endpoint, body, qs); + const limit = this.getNodeParameter('limit', 0) as number; + + return responseData.slice(0, limit); +} + +export function throwOnEmptyUpdate(this: IExecuteFunctions, resource: CamelCaseResource) { + throw new NodeOperationError( + this.getNode(), + `Please enter at least one field to update for the ${resource}.`, + ); +} + +export function throwOnMissingProducts( + this: IExecuteFunctions, + resource: CamelCaseResource, + productDetails: ProductDetails, + ) { + if (!productDetails.length) { + throw new NodeOperationError( + this.getNode(), + `Please enter at least one product for the ${resource}.`, + ); + } +} + +export function throwOnErrorStatus( + this: IExecuteFunctions | IHookFunctions | ILoadOptionsFunctions, + responseData: { data?: Array<{ status: string, message: string }> }, +) { + if (responseData?.data?.[0].status === 'error') { + throw new NodeOperationError(this.getNode(), responseData as Error); + } +} + +// ---------------------------------------- +// required field adjusters +// ---------------------------------------- + +/** + * Place a product ID at a nested position in a product details field. + */ +export const adjustProductDetails = (productDetails: ProductDetails) => { + return productDetails.map(p => { + return { + ...omit('product', p), + product: { id: p.id }, + quantity: p.quantity || 1, + }; + }); +}; + +// ---------------------------------------- +// additional field adjusters +// ---------------------------------------- + +/** + * Place a product ID at a nested position in a product details field. + * + * Only for updating products from Invoice, Purchase Order, Quote, and Sales Order. + */ +export const adjustProductDetailsOnUpdate = (allFields: AllFields) => { + if (!allFields.Product_Details) return allFields; + + return allFields.Product_Details.map(p => { + return { + ...omit('product', p), + product: { id: p.id }, + quantity: p.quantity || 1, + }; + }); +}; + +/** + * Place a location field's contents at the top level of the payload. + */ +const adjustLocationFields = (locationType: LocationType) => (allFields: AllFields) => { + const locationField = allFields[locationType]; + + if (!locationField) return allFields; + + return { + ...omit(locationType, allFields), + ...locationField.address_fields, + }; +}; + +const adjustAddressFields = adjustLocationFields('Address'); +const adjustBillingAddressFields = adjustLocationFields('Billing_Address'); +const adjustMailingAddressFields = adjustLocationFields('Mailing_Address'); +const adjustShippingAddressFields = adjustLocationFields('Shipping_Address'); +const adjustOtherAddressFields = adjustLocationFields('Other_Address'); + +/** + * Remove from a date field the timestamp set by the datepicker. + */ + const adjustDateField = (dateType: DateType) => (allFields: AllFields) => { + const dateField = allFields[dateType]; + + if (!dateField) return allFields; + + allFields[dateType] = dateField.split('T')[0]; + + return allFields; +}; + +const adjustDateOfBirthField = adjustDateField('Date_of_Birth'); +const adjustClosingDateField = adjustDateField('Closing_Date'); +const adjustInvoiceDateField = adjustDateField('Invoice_Date'); +const adjustDueDateField = adjustDateField('Due_Date'); +const adjustPurchaseOrderDateField = adjustDateField('PO_Date'); +const adjustValidTillField = adjustDateField('Valid_Till'); + +/** + * Place an ID field's value nested inside the payload. + */ +const adjustIdField = (idType: IdType, nameProperty: NameType) => (allFields: AllFields) => { + const idValue = allFields[idType]; + + if (!idValue) return allFields; + + return { + ...omit(idType, allFields), + [nameProperty]: { id: idValue }, + }; +}; + +const adjustAccountIdField = adjustIdField('accountId', 'Account_Name'); +const adjustContactIdField = adjustIdField('contactId', 'Full_Name'); +const adjustDealIdField = adjustIdField('dealId', 'Deal_Name'); + +const adjustCustomFields = (allFields: AllFields) => { + const { customFields, ...rest } = allFields; + + if (!customFields?.customFields.length) return allFields; + + return customFields.customFields.reduce((acc, cur) => { + acc[cur.fieldId] = cur.value; + return acc; + }, rest); +}; + +// ---------------------------------------- +// payload adjusters +// ---------------------------------------- + +export const adjustAccountPayload = flow( + adjustBillingAddressFields, + adjustShippingAddressFields, + adjustCustomFields, +); + +export const adjustContactPayload = flow( + adjustMailingAddressFields, + adjustOtherAddressFields, + adjustDateOfBirthField, + adjustCustomFields, +); + +export const adjustDealPayload = flow( + adjustClosingDateField, + adjustCustomFields, +); + +export const adjustInvoicePayload = flow( + adjustBillingAddressFields, + adjustShippingAddressFields, + adjustInvoiceDateField, + adjustDueDateField, + adjustAccountIdField, + adjustCustomFields, +); + +export const adjustInvoicePayloadOnUpdate = flow( + adjustInvoicePayload, + adjustProductDetailsOnUpdate, +); + +export const adjustLeadPayload = flow( + adjustAddressFields, + adjustCustomFields, +); + +export const adjustPurchaseOrderPayload = flow( + adjustBillingAddressFields, + adjustShippingAddressFields, + adjustDueDateField, + adjustPurchaseOrderDateField, + adjustCustomFields, +); + +export const adjustQuotePayload = flow( + adjustBillingAddressFields, + adjustShippingAddressFields, + adjustValidTillField, + adjustCustomFields, +); + +export const adjustSalesOrderPayload = flow( + adjustBillingAddressFields, + adjustShippingAddressFields, + adjustDueDateField, + adjustAccountIdField, + adjustContactIdField, + adjustDealIdField, + adjustCustomFields, +); + +export const adjustVendorPayload = flow( + adjustAddressFields, + adjustCustomFields, +); + +export const adjustProductPayload = adjustCustomFields; + +// ---------------------------------------- +// helpers +// ---------------------------------------- + +/** + * Create a copy of an object without a specific property. + */ +const omit = (propertyToOmit: string, { [propertyToOmit]: _, ...remainingObject }) => remainingObject; + +/** + * Convert items in a Zoho CRM API response into n8n load options. + */ +export const toLoadOptions = (items: ResourceItems, nameProperty: NameType) => + items.map((item) => ({ name: item[nameProperty], value: item.id })); + +/** + * Retrieve all fields for a resource, sorted alphabetically. + */ +export async function getFields( + this: ILoadOptionsFunctions, + resource: SnakeCaseResource, + { onlyCustom } = { onlyCustom: false }, +) { + const qs = { module: getModuleName(resource) }; + + let { fields } = await zohoApiRequest.call(this, 'GET', '/settings/fields', {}, qs) as LoadedFields; + + if (onlyCustom) { + fields = fields.filter(({ custom_field }) => custom_field); + } + + const options = fields.map(({ field_label, api_name }) => ({ name: field_label, value: api_name })); + + return sortBy(options, o => o.name); +} + +export function getModuleName(resource: string) { + const map: { [key: string]: string } = { + account: 'Accounts', + contact: 'Contacts', + deal: 'Deals', + invoice: 'Invoices', + lead: 'Leads', + product: 'Products', + purchaseOrder: 'Purchase_Orders', + salesOrder: 'Sales_Orders', + vendor: 'Vendors', + quote: 'Quotes', + }; + + return map[resource]; +} + +export async function getPicklistOptions( + this: ILoadOptionsFunctions, + resource: string, + targetField: string, +) { + const qs = { module: getModuleName(resource) }; + const responseData = await zohoApiRequest.call(this, 'GET', '/settings/layouts', {}, qs) as LoadedLayouts; + + const pickListOptions = responseData.layouts[0] + .sections.find(section => section.api_name === getSectionApiName(resource)) + ?.fields.find(f => f.api_name === targetField) + ?.pick_list_values; + + if (!pickListOptions) return []; + + return pickListOptions.map( + (option) => ({ name: option.display_value, value: option.actual_value }), + ); +} + + +function getSectionApiName(resource: string) { + if (resource === 'purchaseOrder') return 'Purchase Order Information'; + if (resource === 'salesOrder') return 'Sales Order Information'; + + return `${capitalizeInitial(resource)} Information`; +} + +/** + * Add filter options to a query string object. + */ +export const addGetAllFilterOptions = (qs: IDataObject, options: GetAllFilterOptions) => { + if (Object.keys(options).length) { + const { fields, ...rest } = options; + Object.assign(qs, fields && { fields: fields.join(',') }, rest); + } +}; + +export const capitalizeInitial = (str: string) => str[0].toUpperCase() + str.slice(1); diff --git a/packages/nodes-base/nodes/Zoho/LeadDescription.ts b/packages/nodes-base/nodes/Zoho/LeadDescription.ts deleted file mode 100644 index d232098ca7..0000000000 --- a/packages/nodes-base/nodes/Zoho/LeadDescription.ts +++ /dev/null @@ -1,706 +0,0 @@ -import { INodeProperties } from 'n8n-workflow'; - -export const leadOperations = [ - { - displayName: 'Operation', - name: 'operation', - type: 'options', - displayOptions: { - show: { - resource: [ - 'lead', - ], - }, - }, - options: [ - { - name: 'Create', - value: 'create', - description: 'Create a new lead', - }, - { - name: 'Delete', - value: 'delete', - description: 'Delete a lead', - }, - { - name: 'Get', - value: 'get', - description: 'Get data of a lead', - }, - { - name: 'Get All', - value: 'getAll', - description: 'Get data of all leads', - }, - { - name: 'Get Fields', - value: 'getFields', - description: `Get the fields' metadata`, - }, - { - name: 'Update', - value: 'update', - description: 'Update a lead', - }, - ], - default: 'create', - description: 'The operation to perform.', - }, -] as INodeProperties[]; - -export const leadFields = [ - -/* -------------------------------------------------------------------------- */ -/* lead:create */ -/* -------------------------------------------------------------------------- */ - { - displayName: 'Last Name', - name: 'lastName', - type: 'string', - default: '', - required: true, - displayOptions: { - show: { - resource: [ - 'lead', - ], - operation: [ - 'create', - ], - }, - }, - description: `User's last name`, - }, - { - displayName: 'Additional Fields', - name: 'additionalFields', - type: 'collection', - placeholder: 'Add Field', - default: {}, - displayOptions: { - show: { - operation: [ - 'create', - ], - resource: [ - 'lead', - ], - }, - }, - options: [ - { - displayName: 'Annual Revenue', - name: 'annualRevenue', - type: 'number', - typeOptions: { - numberPrecision: 2, - }, - default: 0, - }, - { - displayName: 'Company', - name: 'company', - type: 'string', - default: '', - }, - { - displayName: 'Description', - name: 'description', - type: 'string', - default: '', - }, - { - displayName: 'Email', - name: 'email', - type: 'string', - default: '', - }, - { - displayName: 'Email Opt Out', - name: 'emailOptOut', - type: 'boolean', - default: false, - }, - { - displayName: 'Fax', - name: 'fax', - type: 'string', - default: '', - }, - { - displayName: 'First Name', - name: 'firstName', - type: 'string', - default: '', - }, - { - displayName: 'Industry', - name: 'industry', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getIndustries', - }, - default: '', - }, - { - displayName: 'Is Record Duplicate', - name: 'isRecordDuplicate', - type: 'boolean', - default: false, - }, - { - displayName: 'Lead Source', - name: 'leadSource', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getLeadSources', - }, - default: '', - }, - { - displayName: 'Lead Status', - name: 'leadStatus', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getLeadStatuses', - }, - default: '', - }, - { - displayName: 'Mobile', - name: 'mobile', - type: 'string', - default: '', - }, - { - displayName: 'No. of Employees', - name: 'numberOfEmployees', - type: 'number', - default: 1, - }, - { - displayName: 'Owner', - name: 'owner', - type: 'options', - default: '', - typeOptions: { - loadOptionsMethod: 'getUsers', - }, - }, - { - displayName: 'Phone', - name: 'phone', - type: 'string', - default: '', - }, - { - displayName: 'Salutation', - name: 'salutation', - type: 'string', - default: '', - }, - { - displayName: 'Secondary Email', - name: 'secondaryEmail', - type: 'string', - default: '', - }, - { - displayName: 'Skype ID', - name: 'SkypeId', - type: 'string', - default: '', - }, - { - displayName: 'Title', - name: 'title', - type: 'string', - default: '', - }, - { - displayName: 'Twitter', - name: 'twitter', - type: 'string', - default: '', - }, - { - displayName: 'Website', - name: 'website', - type: 'string', - default: '', - }, - ], - }, - { - displayName: 'Address', - name: 'addressUi', - type: 'fixedCollection', - default: {}, - placeholder: 'Add Address', - typeOptions: { - multipleValues: false, - }, - required: false, - displayOptions: { - show: { - resource: [ - 'lead', - ], - operation: [ - 'create', - ], - }, - }, - options: [ - { - name: 'addressValues', - displayName: 'Address', - values: [ - { - displayName: 'Street', - name: 'street', - type: 'string', - default: '', - }, - { - displayName: 'City', - name: 'city', - type: 'string', - default: '', - }, - { - displayName: 'State', - name: 'state', - type: 'string', - default: '', - }, - { - displayName: 'Country', - name: 'country', - type: 'string', - default: '', - }, - { - displayName: 'Zip Code', - name: 'zipCode', - type: 'string', - default: '', - }, - ], - }, - ], - }, -/* -------------------------------------------------------------------------- */ -/* lead:update */ -/* -------------------------------------------------------------------------- */ - { - displayName: 'Lead ID', - name: 'leadId', - type: 'string', - default: '', - required: true, - displayOptions: { - show: { - resource: [ - 'lead', - ], - operation: [ - 'update', - ], - }, - }, - }, - { - displayName: 'Additional Fields', - name: 'additionalFields', - type: 'collection', - placeholder: 'Add Field', - default: {}, - displayOptions: { - show: { - operation: [ - 'update', - ], - resource: [ - 'lead', - ], - }, - }, - options: [ - { - displayName: 'Annual Revenue', - name: 'annualRevenue', - type: 'number', - typeOptions: { - numberPrecision: 2, - }, - default: 0, - }, - { - displayName: 'Company', - name: 'company', - type: 'string', - default: '', - }, - { - displayName: 'Description', - name: 'description', - type: 'string', - default: '', - }, - { - displayName: 'Email', - name: 'email', - type: 'string', - default: '', - }, - { - displayName: 'Email Opt Out', - name: 'emailOptOut', - type: 'boolean', - default: false, - }, - { - displayName: 'Fax', - name: 'fax', - type: 'string', - default: '', - }, - { - displayName: 'First Name', - name: 'firstName', - type: 'string', - default: '', - }, - { - displayName: 'Industry', - name: 'industry', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getIndustries', - }, - default: '', - }, - { - displayName: 'Is Record Duplicate', - name: 'isRecordDuplicate', - type: 'boolean', - default: false, - }, - { - displayName: 'Last Name', - name: 'lastName', - type: 'string', - default: '', - description: `User's last name`, - }, - { - displayName: 'Lead Source', - name: 'leadSource', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getLeadSources', - }, - default: '', - }, - { - displayName: 'Lead Status', - name: 'leadStatus', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getLeadStatuses', - }, - default: '', - }, - { - displayName: 'Mobile', - name: 'mobile', - type: 'string', - default: '', - }, - { - displayName: 'No. of Employees', - name: 'numberOfEmployees', - type: 'number', - default: 1, - }, - { - displayName: 'Owner', - name: 'owner', - type: 'options', - default: '', - typeOptions: { - loadOptionsMethod: 'getUsers', - }, - }, - { - displayName: 'Phone', - name: 'phone', - type: 'string', - default: '', - }, - { - displayName: 'Salutation', - name: 'salutation', - type: 'string', - default: '', - }, - { - displayName: 'Secondary Email', - name: 'secondaryEmail', - type: 'string', - default: '', - }, - { - displayName: 'Skype ID', - name: 'SkypeId', - type: 'string', - default: '', - }, - { - displayName: 'Title', - name: 'title', - type: 'string', - default: '', - }, - { - displayName: 'Twitter', - name: 'twitter', - type: 'string', - default: '', - }, - { - displayName: 'Website', - name: 'website', - type: 'string', - default: '', - }, - ], - }, - { - displayName: 'Address', - name: 'addressUi', - type: 'fixedCollection', - default: {}, - placeholder: 'Add Address', - typeOptions: { - multipleValues: false, - }, - required: false, - displayOptions: { - show: { - resource: [ - 'lead', - ], - operation: [ - 'update', - ], - }, - }, - options: [ - { - name: 'addressValues', - displayName: 'Address', - values: [ - { - displayName: 'Street', - name: 'street', - type: 'string', - default: '', - }, - { - displayName: 'City', - name: 'city', - type: 'string', - default: '', - }, - { - displayName: 'State', - name: 'state', - type: 'string', - default: '', - }, - { - displayName: 'Country', - name: 'country', - type: 'string', - default: '', - }, - { - displayName: 'Zip Code', - name: 'zipCode', - type: 'string', - default: '', - }, - ], - }, - ], - }, -/* -------------------------------------------------------------------------- */ -/* lead:get */ -/* -------------------------------------------------------------------------- */ - { - displayName: 'Lead ID', - name: 'leadId', - type: 'string', - default: '', - required: true, - displayOptions: { - show: { - resource: [ - 'lead', - ], - operation: [ - 'get', - ], - }, - }, - }, -/* -------------------------------------------------------------------------- */ -/* lead:getAll */ -/* -------------------------------------------------------------------------- */ - { - displayName: 'Return All', - name: 'returnAll', - type: 'boolean', - displayOptions: { - show: { - resource: [ - 'lead', - ], - operation: [ - 'getAll', - ], - }, - }, - default: false, - description: 'If all results should be returned or only up to a given limit.', - }, - { - displayName: 'Limit', - name: 'limit', - type: 'number', - displayOptions: { - show: { - resource: [ - 'lead', - ], - operation: [ - 'getAll', - ], - returnAll: [ - false, - ], - }, - }, - typeOptions: { - minValue: 1, - maxValue: 200, - }, - default: 100, - description: 'How many results to return.', - }, - { - displayName: 'Options', - name: 'options', - type: 'collection', - placeholder: 'Add Option', - default: {}, - displayOptions: { - show: { - resource: [ - 'lead', - ], - operation: [ - 'getAll', - ], - }, - }, - options: [ - { - displayName: 'Approved', - name: 'approved', - type: 'boolean', - default: true, - description: 'To get the list of approved records. Default value is true.', - }, - { - displayName: 'Converted', - name: 'converted', - type: 'boolean', - default: false, - description: 'To get the list of converted records. Default value is false', - }, - { - displayName: 'Fields', - name: 'fields', - type: 'multiOptions', - typeOptions: { - loadOptionsMethod: 'getLeadFields', - }, - default: [], - }, - { - displayName: 'Include Child', - name: 'includeChild', - type: 'boolean', - default: false, - description: 'To include records from the child territories. True includes child territory records', - }, - { - displayName: 'Sort By', - name: 'sortBy', - type: 'multiOptions', - typeOptions: { - loadOptionsMethod: 'getLeadFields', - }, - default: [], - }, - { - displayName: 'Sort Order', - name: 'sortOrder', - type: 'options', - options: [ - { - name: 'ASC', - value: 'asc', - }, - { - name: 'DESC', - value: 'desc', - }, - ], - default: 'desc', - description: 'Order sort attribute ascending or descending.', - }, - { - displayName: 'Territory ID', - name: 'territoryId', - type: 'string', - default: '', - description: 'To get the list of records based on the territory ', - }, - ], - }, -/* -------------------------------------------------------------------------- */ -/* lead:delete */ -/* -------------------------------------------------------------------------- */ - { - displayName: 'Lead ID', - name: 'leadId', - type: 'string', - default: '', - required: true, - displayOptions: { - show: { - resource: [ - 'lead', - ], - operation: [ - 'delete', - ], - }, - }, - }, -] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zoho/LeadInterface.ts b/packages/nodes-base/nodes/Zoho/LeadInterface.ts deleted file mode 100644 index 957f816cb6..0000000000 --- a/packages/nodes-base/nodes/Zoho/LeadInterface.ts +++ /dev/null @@ -1,37 +0,0 @@ -export interface ILead { - Annual_Revenue?: number; - City?: string; - Company?: string; - Country?: string; - Description?: string; - Designation?: string; - Email?: string; - Email_Opt_Out?: boolean; - Fax?: string; - First_Name?: string; - Industry?: string; - Is_Record_Duplicate?: boolean; - Last_Name?: string; - Lead_Owner?: string; - Lead_Source?: string; - Lead_Status?: string; - Mobile?: string; - No_of_Employees?: number; - Phone?: string; - Salutation?: string; - Secondary_Email?: string; - Skype_ID?: string; - State?: string; - Street?: string; - Twitter?: string; - Website?: string; - Zip_Code?: string; -} - -export interface IAddress { - street?: string; - city?: string; - state?: string; - country?: string; - zipCode?: string; -} diff --git a/packages/nodes-base/nodes/Zoho/ZohoCrm.node.ts b/packages/nodes-base/nodes/Zoho/ZohoCrm.node.ts index 8380f56049..da7c713551 100644 --- a/packages/nodes-base/nodes/Zoho/ZohoCrm.node.ts +++ b/packages/nodes-base/nodes/Zoho/ZohoCrm.node.ts @@ -6,37 +6,80 @@ import { IDataObject, ILoadOptionsFunctions, INodeExecutionData, - INodePropertyOptions, INodeType, INodeTypeDescription, } from 'n8n-workflow'; import { + addGetAllFilterOptions, + adjustAccountPayload, + adjustContactPayload, + adjustDealPayload, + adjustInvoicePayload, + adjustInvoicePayloadOnUpdate, + adjustLeadPayload, + adjustProductDetails, + adjustProductPayload, + adjustPurchaseOrderPayload, + adjustQuotePayload, + adjustSalesOrderPayload, + adjustVendorPayload, + getFields, + getModuleName, + getPicklistOptions, + handleListing, + throwOnEmptyUpdate, + throwOnMissingProducts, + toLoadOptions, zohoApiRequest, zohoApiRequestAllItems, } from './GenericFunctions'; import { - leadFields, - leadOperations, -} from './LeadDescription'; + CamelCaseResource, + GetAllFilterOptions, + LoadedAccounts, + LoadedContacts, + LoadedDeals, + LoadedProducts, + LoadedVendors, + ProductDetails, +} from './types'; import { - IAddress, - ILead, -} from './LeadInterface'; + accountFields, + accountOperations, + contactFields, + contactOperations, + dealFields, + dealOperations, + invoiceFields, + invoiceOperations, + leadFields, + leadOperations, + productFields, + productOperations, + purchaseOrderFields, + purchaseOrderOperations, + quoteFields, + quoteOperations, + salesOrderFields, + salesOrderOperations, + vendorFields, + vendorOperations, +} from './descriptions'; export class ZohoCrm implements INodeType { description: INodeTypeDescription = { displayName: 'Zoho CRM', name: 'zohoCrm', icon: 'file:zoho.svg', + group: ['transform'], subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - group: ['input'], version: 1, - description: 'Consume Zoho CRM API.', + description: 'Consume the Zoho API', defaults: { - name: 'Zoho CRM', + name: 'Zoho', color: '#CE2232', }, inputs: ['main'], @@ -53,134 +96,222 @@ export class ZohoCrm implements INodeType { name: 'resource', type: 'options', options: [ + { + name: 'Account', + value: 'account', + }, + { + name: 'Contact', + value: 'contact', + }, + { + name: 'Deal', + value: 'deal', + }, + { + name: 'Invoice', + value: 'invoice', + }, { name: 'Lead', value: 'lead', }, + { + name: 'Product', + value: 'product', + }, + { + name: 'Purchase Order', + value: 'purchaseOrder', + }, + { + name: 'Quote', + value: 'quote', + }, + { + name: 'Sales Order', + value: 'salesOrder', + }, + { + name: 'Vendor', + value: 'vendor', + }, ], - default: 'lead', - description: 'The resource to operate on.', + default: 'account', + description: 'Resource to consume', }, + ...accountOperations, + ...accountFields, + ...contactOperations, + ...contactFields, + ...dealOperations, + ...dealFields, + ...invoiceOperations, + ...invoiceFields, ...leadOperations, ...leadFields, + ...productOperations, + ...productFields, + ...purchaseOrderOperations, + ...purchaseOrderFields, + ...quoteOperations, + ...quoteFields, + ...salesOrderOperations, + ...salesOrderFields, + ...vendorOperations, + ...vendorFields, ], }; methods = { loadOptions: { - // Get all the available users to display them to user so that he can - // select them easily - async getUsers(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - const { users } = await zohoApiRequest.call(this, 'GET', '/users', {}, { type: 'AllUsers' }); - for (const user of users) { - const userName = `${user.first_name} ${user.last_name}`; - const userId = user.profile.id; - returnData.push({ - name: userName, - value: userId, - }); - } - return returnData; + // ---------------------------------------- + // resources + // ---------------------------------------- + + async getAccounts(this: ILoadOptionsFunctions) { + const accounts = await zohoApiRequestAllItems.call(this, 'GET', '/accounts') as LoadedAccounts; + return toLoadOptions(accounts, 'Account_Name'); }, - // Get all the available accounts to display them to user so that he can - // select them easily - async getAccounts(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - const qs: IDataObject = {}; - qs.sort_by = 'Created_Time'; - qs.sort_order = 'desc'; - const { data } = await zohoApiRequest.call(this, 'GET', '/accounts', {}, qs); - for (const account of data) { - const accountName = account.Account_Name; - const accountId = account.id; - returnData.push({ - name: accountName, - value: accountId, - }); - } - return returnData; + + async getContacts(this: ILoadOptionsFunctions) { + const contacts = await zohoApiRequestAllItems.call(this, 'GET', '/contacts') as LoadedContacts; + return toLoadOptions(contacts, 'Full_Name'); }, - // Get all the available lead statuses to display them to user so that he can - // select them easily - async getLeadStatuses(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - const qs: IDataObject = {}; - qs.module = 'leads'; - const { fields } = await zohoApiRequest.call(this, 'GET', '/settings/fields', {}, qs); - for (const field of fields) { - if (field.api_name === 'Lead_Status') { - for (const value of field.pick_list_values) { - const valueName = value.display_value; - const valueId = value.actual_value; - returnData.push({ - name: valueName, - value: valueId, - }); - return returnData; - } - } - } - return returnData; + + async getDeals(this: ILoadOptionsFunctions) { + const deals = await zohoApiRequestAllItems.call(this, 'GET', '/deals') as LoadedDeals; + return toLoadOptions(deals, 'Deal_Name'); }, - // Get all the available lead sources to display them to user so that he can - // select them easily - async getLeadSources(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - const qs: IDataObject = {}; - qs.module = 'leads'; - const { fields } = await zohoApiRequest.call(this, 'GET', '/settings/fields', {}, qs); - for (const field of fields) { - if (field.api_name === 'Lead_Source') { - for (const value of field.pick_list_values) { - const valueName = value.display_value; - const valueId = value.actual_value; - returnData.push({ - name: valueName, - value: valueId, - }); - return returnData; - } - } - } - return returnData; + + async getProducts(this: ILoadOptionsFunctions) { + const products = await zohoApiRequestAllItems.call(this, 'GET', '/products') as LoadedProducts; + return toLoadOptions(products, 'Product_Name'); }, - // Get all the available industries to display them to user so that he can - // select them easily - async getIndustries(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - const qs: IDataObject = {}; - qs.module = 'leads'; - const { fields } = await zohoApiRequest.call(this, 'GET', '/settings/fields', {}, qs); - for (const field of fields) { - if (field.api_name === 'Industry') { - for (const value of field.pick_list_values) { - const valueName = value.display_value; - const valueId = value.actual_value; - returnData.push({ - name: valueName, - value: valueId, - }); - return returnData; - } - } - } - return returnData; + + async getVendors(this: ILoadOptionsFunctions) { + const vendors = await zohoApiRequestAllItems.call(this, 'GET', '/vendors') as LoadedVendors; + return toLoadOptions(vendors, 'Vendor_Name'); }, - // Get all the available lead fields to display them to user so that he can - // select them easily - async getLeadFields(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - const qs: IDataObject = {}; - qs.module = 'leads'; - const { fields } = await zohoApiRequest.call(this, 'GET', '/settings/fields', {}, qs); - for (const field of fields) { - returnData.push({ - name: field.field_label, - value: field.api_name, - }); - } - return returnData; + + // ---------------------------------------- + // resource fields + // ---------------------------------------- + + // standard fields - called from `makeGetAllFields` + + async getAccountFields(this: ILoadOptionsFunctions) { + return getFields.call(this, 'account'); + }, + + async getContactFields(this: ILoadOptionsFunctions) { + return getFields.call(this, 'contact'); + }, + + async getDealFields(this: ILoadOptionsFunctions) { + return getFields.call(this, 'deal'); + }, + + async getInvoiceFields(this: ILoadOptionsFunctions) { + return getFields.call(this, 'invoice'); + }, + + async getLeadFields(this: ILoadOptionsFunctions) { + return getFields.call(this, 'lead'); + }, + + async getProductFields(this: ILoadOptionsFunctions) { + return getFields.call(this, 'product'); + }, + + async getPurchaseOrderFields(this: ILoadOptionsFunctions) { + return getFields.call(this, 'purchase_order'); + }, + + async getVendorOrderFields(this: ILoadOptionsFunctions) { + return getFields.call(this, 'vendor'); + }, + + async getQuoteFields(this: ILoadOptionsFunctions) { + return getFields.call(this, 'quote'); + }, + + async getSalesOrderFields(this: ILoadOptionsFunctions) { + return getFields.call(this, 'sales_order'); + }, + + async getVendorFields(this: ILoadOptionsFunctions) { + return getFields.call(this, 'vendor'); + }, + + // custom fields + + async getCustomAccountFields(this: ILoadOptionsFunctions) { + return getFields.call(this, 'account', { onlyCustom: true }); + }, + + async getCustomContactFields(this: ILoadOptionsFunctions) { + return getFields.call(this, 'contact', { onlyCustom: true }); + }, + + async getCustomDealFields(this: ILoadOptionsFunctions) { + return getFields.call(this, 'deal', { onlyCustom: true }); + }, + + async getCustomInvoiceFields(this: ILoadOptionsFunctions) { + return getFields.call(this, 'invoice', { onlyCustom: true }); + }, + + async getCustomLeadFields(this: ILoadOptionsFunctions) { + return getFields.call(this, 'lead', { onlyCustom: true }); + }, + + async getCustomProductFields(this: ILoadOptionsFunctions) { + return getFields.call(this, 'product', { onlyCustom: true }); + }, + + async getCustomPurchaseOrderFields(this: ILoadOptionsFunctions) { + return getFields.call(this, 'purchase_order', { onlyCustom: true }); + }, + + async getCustomVendorOrderFields(this: ILoadOptionsFunctions) { + return getFields.call(this, 'vendor', { onlyCustom: true }); + }, + + async getCustomQuoteFields(this: ILoadOptionsFunctions) { + return getFields.call(this, 'quote', { onlyCustom: true }); + }, + + async getCustomSalesOrderFields(this: ILoadOptionsFunctions) { + return getFields.call(this, 'sales_order', { onlyCustom: true }); + }, + + async getCustomVendorFields(this: ILoadOptionsFunctions) { + return getFields.call(this, 'vendor', { onlyCustom: true }); + }, + + // ---------------------------------------- + // resource picklist options + // ---------------------------------------- + + async getAccountType(this: ILoadOptionsFunctions) { + return getPicklistOptions.call(this, 'account', 'Account_Type'); + }, + + async getDealStage(this: ILoadOptionsFunctions) { + return getPicklistOptions.call(this, 'deal', 'Stage'); + }, + + async getPurchaseOrderStatus(this: ILoadOptionsFunctions) { + return getPicklistOptions.call(this, 'purchaseOrder', 'Status'); + }, + + async getSalesOrderStatus(this: ILoadOptionsFunctions) { + return getPicklistOptions.call(this, 'salesOrder', 'Status'); + }, + + async getQuoteStage(this: ILoadOptionsFunctions) { + return getPicklistOptions.call(this, 'quote', 'Quote_Stage'); }, }, }; @@ -188,265 +319,1147 @@ export class ZohoCrm implements INodeType { async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); const returnData: IDataObject[] = []; - const length = items.length as unknown as number; - const qs: IDataObject = {}; + + const resource = this.getNodeParameter('resource', 0) as CamelCaseResource; + const operation = this.getNodeParameter('operation', 0) as string; + const resolveData = this.getNodeParameter('resolveData', 0, false) as boolean; + let responseData; - for (let i = 0; i < length; i++) { - const resource = this.getNodeParameter('resource', 0) as string; - const operation = this.getNodeParameter('operation', 0) as string; - if (resource === 'lead') { - //https://www.zoho.com/crm/developer/docs/api/insert-records.html - if (operation === 'create') { - const lastName = this.getNodeParameter('lastName', i) as string; - const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; - const body: ILead = { - Last_Name: lastName, - }; - if (additionalFields.owner) { - body.Lead_Owner = additionalFields.owner as string; - } - if (additionalFields.company) { - body.Company = additionalFields.company as string; - } - if (additionalFields.firstName) { - body.First_Name = additionalFields.firstName as string; - } - if (additionalFields.email) { - body.Email = additionalFields.email as string; - } - if (additionalFields.title) { - body.Designation = additionalFields.title as string; - } - if (additionalFields.phone) { - body.Phone = additionalFields.phone as string; - } - if (additionalFields.mobile) { - body.Mobile = additionalFields.mobile as string; - } - if (additionalFields.leadStatus) { - body.Lead_Status = additionalFields.leadStatus as string; - } - if (additionalFields.fax) { - body.Fax = additionalFields.fax as string; - } - if (additionalFields.website) { - body.Website = additionalFields.website as string; - } - if (additionalFields.leadSource) { - body.Lead_Source = additionalFields.leadSource as string; - } - if (additionalFields.industry) { - body.Industry = additionalFields.industry as string; - } - if (additionalFields.numberOfEmployees) { - body.No_of_Employees = additionalFields.numberOfEmployees as number; - } - if (additionalFields.annualRevenue) { - body.Annual_Revenue = additionalFields.annualRevenue as number; - } - if (additionalFields.emailOptOut) { - body.Email_Opt_Out = additionalFields.emailOptOut as boolean; - } - if (additionalFields.skypeId) { - body.Skype_ID = additionalFields.skypeId as string; - } - if (additionalFields.salutation) { - body.Salutation = additionalFields.salutation as string; - } - if (additionalFields.secondaryEmail) { - body.Secondary_Email = additionalFields.secondaryEmail as string; - } - if (additionalFields.twitter) { - body.Twitter = additionalFields.twitter as string; - } - if (additionalFields.isRecordDuplicate) { - body.Is_Record_Duplicate = additionalFields.isRecordDuplicate as boolean; - } - if (additionalFields.description) { - body.Description = additionalFields.description as string; - } - const address = (this.getNodeParameter('addressUi', i) as IDataObject).addressValues as IAddress; - if (address) { - if (address.country) { - body.Country = address.country as string; - } - if (address.city) { - body.City = address.city as string; - } - if (address.state) { - body.State = address.state as string; - } - if (address.street) { - body.Street = address.street as string; - } - if (address.zipCode) { - body.Zip_Code = address.zipCode as string; - } - } - responseData = await zohoApiRequest.call(this, 'POST', '/leads', body); - responseData = responseData.data; - if (responseData.length) { - responseData = responseData[0].details; - } - } - //https://www.zoho.com/crm/developer/docs/api/update-specific-record.html - if (operation === 'update') { - const leadId = this.getNodeParameter('leadId', i) as string; - const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; - const body: ILead = {}; - if (additionalFields.lastName) { - body.Last_Name = additionalFields.lastName as string; - } - if (additionalFields.owner) { - body.Lead_Owner = additionalFields.owner as string; - } - if (additionalFields.company) { - body.Company = additionalFields.company as string; - } - if (additionalFields.firstName) { - body.First_Name = additionalFields.firstName as string; - } - if (additionalFields.email) { - body.Email = additionalFields.email as string; - } - if (additionalFields.title) { - body.Designation = additionalFields.title as string; - } - if (additionalFields.phone) { - body.Phone = additionalFields.phone as string; - } - if (additionalFields.mobile) { - body.Mobile = additionalFields.mobile as string; - } - if (additionalFields.leadStatus) { - body.Lead_Status = additionalFields.leadStatus as string; - } - if (additionalFields.fax) { - body.Fax = additionalFields.fax as string; - } - if (additionalFields.website) { - body.Website = additionalFields.website as string; - } - if (additionalFields.leadSource) { - body.Lead_Source = additionalFields.leadSource as string; - } - if (additionalFields.industry) { - body.Industry = additionalFields.industry as string; - } - if (additionalFields.numberOfEmployees) { - body.No_of_Employees = additionalFields.numberOfEmployees as number; - } - if (additionalFields.annualRevenue) { - body.Annual_Revenue = additionalFields.annualRevenue as number; - } - if (additionalFields.emailOptOut) { - body.Email_Opt_Out = additionalFields.emailOptOut as boolean; - } - if (additionalFields.skypeId) { - body.Skype_ID = additionalFields.skypeId as string; - } - if (additionalFields.salutation) { - body.Salutation = additionalFields.salutation as string; - } - if (additionalFields.secondaryEmail) { - body.Secondary_Email = additionalFields.secondaryEmail as string; - } - if (additionalFields.twitter) { - body.Twitter = additionalFields.twitter as string; - } - if (additionalFields.isRecordDuplicate) { - body.Is_Record_Duplicate = additionalFields.isRecordDuplicate as boolean; - } - if (additionalFields.description) { - body.Description = additionalFields.description as string; - } - const address = (this.getNodeParameter('addressUi', i) as IDataObject).addressValues as IAddress; - if (address) { - if (address.country) { - body.Country = address.country as string; - } - if (address.city) { - body.City = address.city as string; - } - if (address.state) { - body.State = address.state as string; - } - if (address.street) { - body.Street = address.street as string; - } - if (address.zipCode) { - body.Zip_Code = address.zipCode as string; - } - } - responseData = await zohoApiRequest.call(this, 'PUT', `/leads/${leadId}`, body); - responseData = responseData.data; + for (let i = 0; i < items.length; i++) { - if (responseData.length) { - responseData = responseData[0].details; - } - } - //https://www.zoho.com/crm/developer/docs/api/update-specific-record.html - if (operation === 'get') { - const leadId = this.getNodeParameter('leadId', i) as string; - responseData = await zohoApiRequest.call(this, 'GET', `/leads/${leadId}`); - if (responseData !== undefined) { + // https://www.zoho.com/crm/developer/docs/api/insert-records.html + // https://www.zoho.com/crm/developer/docs/api/get-records.html + // https://www.zoho.com/crm/developer/docs/api/update-specific-record.html + // https://www.zoho.com/crm/developer/docs/api/delete-specific-record.html + // https://www.zoho.com/crm/developer/docs/api/v2/upsert-records.html + + try { + + if (resource === 'account') { + + // ********************************************************************** + // account + // ********************************************************************** + + // https://www.zoho.com/crm/developer/docs/api/v2/accounts-response.html + // https://help.zoho.com/portal/en/kb/crm/customize-crm-account/customizing-fields/articles/standard-modules-fields#Accounts + + if (operation === 'create') { + + // ---------------------------------------- + // account: create + // ---------------------------------------- + + const body: IDataObject = { + Account_Name: this.getNodeParameter('accountName', i), + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, adjustAccountPayload(additionalFields)); + } + + responseData = await zohoApiRequest.call(this, 'POST', '/accounts', body); + responseData = responseData.data[0].details; + + } else if (operation === 'delete') { + + // ---------------------------------------- + // account: delete + // ---------------------------------------- + + const accountId = this.getNodeParameter('accountId', i); + + const endpoint = `/accounts/${accountId}`; + responseData = await zohoApiRequest.call(this, 'DELETE', endpoint); + responseData = responseData.data[0].details; + + } else if (operation === 'get') { + + // ---------------------------------------- + // account: get + // ---------------------------------------- + + const accountId = this.getNodeParameter('accountId', i); + + const endpoint = `/accounts/${accountId}`; + responseData = await zohoApiRequest.call(this, 'GET', endpoint); responseData = responseData.data; + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // account: getAll + // ---------------------------------------- + + const qs: IDataObject = {}; + const options = this.getNodeParameter('options', i) as GetAllFilterOptions; + + addGetAllFilterOptions(qs, options); + + responseData = await handleListing.call(this, 'GET', '/accounts', {}, qs); + + } else if (operation === 'update') { + + // ---------------------------------------- + // account: update + // ---------------------------------------- + + const body: IDataObject = {}; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (Object.keys(updateFields).length) { + Object.assign(body, adjustAccountPayload(updateFields)); + } else { + throwOnEmptyUpdate.call(this, resource); + } + + const accountId = this.getNodeParameter('accountId', i); + + const endpoint = `/accounts/${accountId}`; + responseData = await zohoApiRequest.call(this, 'PUT', endpoint, body); + responseData = responseData.data[0].details; + + } else if (operation === 'upsert') { + + // ---------------------------------------- + // account: upsert + // ---------------------------------------- + + const body: IDataObject = { + Account_Name: this.getNodeParameter('accountName', i), + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, adjustAccountPayload(additionalFields)); + } + + responseData = await zohoApiRequest.call(this, 'POST', '/accounts/upsert', body); + responseData = responseData.data[0].details; + + } + + } else if (resource === 'contact') { + + // ********************************************************************** + // contact + // ********************************************************************** + + // https://www.zoho.com/crm/developer/docs/api/v2/contacts-response.html + // https://help.zoho.com/portal/en/kb/crm/customize-crm-account/customizing-fields/articles/standard-modules-fields#Contacts + + if (operation === 'create') { + + // ---------------------------------------- + // contact: create + // ---------------------------------------- + + const body: IDataObject = { + Last_Name: this.getNodeParameter('lastName', i), + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, adjustContactPayload(additionalFields)); + } + + responseData = await zohoApiRequest.call(this, 'POST', '/contacts', body); + responseData = responseData.data[0].details; + + } else if (operation === 'delete') { + + // ---------------------------------------- + // contact: delete + // ---------------------------------------- + + const contactId = this.getNodeParameter('contactId', i); + + const endpoint = `/contacts/${contactId}`; + responseData = await zohoApiRequest.call(this, 'DELETE', endpoint); + responseData = responseData.data[0].details; + + } else if (operation === 'get') { + + // ---------------------------------------- + // contact: get + // ---------------------------------------- + + const contactId = this.getNodeParameter('contactId', i); + + const endpoint = `/contacts/${contactId}`; + responseData = await zohoApiRequest.call(this, 'GET', endpoint); + responseData = responseData.data; + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // contact: getAll + // ---------------------------------------- + + const qs: IDataObject = {}; + const options = this.getNodeParameter('options', i) as GetAllFilterOptions; + + addGetAllFilterOptions(qs, options); + + responseData = await handleListing.call(this, 'GET', '/contacts', {}, qs); + + } else if (operation === 'update') { + + // ---------------------------------------- + // contact: update + // ---------------------------------------- + + const body: IDataObject = {}; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (Object.keys(updateFields).length) { + Object.assign(body, adjustContactPayload(updateFields)); + } else { + throwOnEmptyUpdate.call(this, resource); + } + + const contactId = this.getNodeParameter('contactId', i); + + const endpoint = `/contacts/${contactId}`; + responseData = await zohoApiRequest.call(this, 'PUT', endpoint, body); + responseData = responseData.data[0].details; + + } else if (operation === 'upsert') { + + // ---------------------------------------- + // contact: upsert + // ---------------------------------------- + + const body: IDataObject = { + Last_Name: this.getNodeParameter('lastName', i), + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, adjustContactPayload(additionalFields)); + } + + responseData = await zohoApiRequest.call(this, 'POST', '/contacts/upsert', body); + responseData = responseData.data[0].details; + + } + + } else if (resource === 'deal') { + + // ********************************************************************** + // deal + // ********************************************************************** + + // https://www.zoho.com/crm/developer/docs/api/v2/deals-response.html + // https://help.zoho.com/portal/en/kb/crm/customize-crm-account/customizing-fields/articles/standard-modules-fields#Deals + + if (operation === 'create') { + + // ---------------------------------------- + // deal: create + // ---------------------------------------- + + const body: IDataObject = { + Deal_Name: this.getNodeParameter('dealName', i), + Stage: this.getNodeParameter('stage', i), + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, adjustDealPayload(additionalFields)); + } + + responseData = await zohoApiRequest.call(this, 'POST', '/deals', body); + responseData = responseData.data[0].details; + + } else if (operation === 'delete') { + + // ---------------------------------------- + // deal: delete + // ---------------------------------------- + + const dealId = this.getNodeParameter('dealId', i); + + responseData = await zohoApiRequest.call(this, 'DELETE', `/deals/${dealId}`); + responseData = responseData.data[0].details; + + } else if (operation === 'get') { + + // ---------------------------------------- + // deal: get + // ---------------------------------------- + + const dealId = this.getNodeParameter('dealId', i); + + responseData = await zohoApiRequest.call(this, 'GET', `/deals/${dealId}`); + responseData = responseData.data; + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // deal: getAll + // ---------------------------------------- + + const qs: IDataObject = {}; + const options = this.getNodeParameter('options', i) as GetAllFilterOptions; + + addGetAllFilterOptions(qs, options); + + responseData = await handleListing.call(this, 'GET', '/deals', {}, qs); + + } else if (operation === 'update') { + + // ---------------------------------------- + // deal: update + // ---------------------------------------- + + const body: IDataObject = {}; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (Object.keys(updateFields).length) { + Object.assign(body, adjustDealPayload(updateFields)); + } else { + throwOnEmptyUpdate.call(this, resource); + } + + const dealId = this.getNodeParameter('dealId', i); + + responseData = await zohoApiRequest.call(this, 'PUT', `/deals/${dealId}`, body); + responseData = responseData.data[0].details; + + } else if (operation === 'upsert') { + + // ---------------------------------------- + // deal: upsert + // ---------------------------------------- + + const body: IDataObject = { + Deal_Name: this.getNodeParameter('dealName', i), + Stage: this.getNodeParameter('stage', i), + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, adjustDealPayload(additionalFields)); + } + + responseData = await zohoApiRequest.call(this, 'POST', '/deals/upsert', body); + responseData = responseData.data[0].details; + + } + + } else if (resource === 'invoice') { + + // ********************************************************************** + // invoice + // ********************************************************************** + + // https://www.zoho.com/crm/developer/docs/api/v2/invoices-response.html + // https://help.zoho.com/portal/en/kb/crm/customize-crm-account/customizing-fields/articles/standard-modules-fields#Invoices + + if (operation === 'create') { + + // ---------------------------------------- + // invoice: create + // ---------------------------------------- + + const productDetails = this.getNodeParameter('Product_Details', i) as ProductDetails; + + throwOnMissingProducts.call(this, resource, productDetails); + + const body: IDataObject = { + Subject: this.getNodeParameter('subject', i), + Product_Details: adjustProductDetails(productDetails), + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, adjustInvoicePayload(additionalFields)); + } + + responseData = await zohoApiRequest.call(this, 'POST', '/invoices', body); + responseData = responseData.data[0].details; + + } else if (operation === 'delete') { + + // ---------------------------------------- + // invoice: delete + // ---------------------------------------- + + const invoiceId = this.getNodeParameter('invoiceId', i); + + const endpoint = `/invoices/${invoiceId}`; + responseData = await zohoApiRequest.call(this, 'DELETE', endpoint); + responseData = responseData.data[0].details; + + } else if (operation === 'get') { + + // ---------------------------------------- + // invoice: get + // ---------------------------------------- + + const invoiceId = this.getNodeParameter('invoiceId', i); + + const endpoint = `/invoices/${invoiceId}`; + responseData = await zohoApiRequest.call(this, 'GET', endpoint); + responseData = responseData.data; + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // invoice: getAll + // ---------------------------------------- + + const qs: IDataObject = {}; + const options = this.getNodeParameter('options', i) as GetAllFilterOptions; + + addGetAllFilterOptions(qs, options); + + responseData = await handleListing.call(this, 'GET', '/invoices', {}, qs); + + } else if (operation === 'update') { + + // ---------------------------------------- + // invoice: update + // ---------------------------------------- + + const body: IDataObject = {}; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (Object.keys(updateFields).length) { + Object.assign(body, adjustInvoicePayloadOnUpdate(updateFields)); + } else { + throwOnEmptyUpdate.call(this, resource); + } + + const invoiceId = this.getNodeParameter('invoiceId', i); + + const endpoint = `/invoices/${invoiceId}`; + + responseData = await zohoApiRequest.call(this, 'PUT', endpoint, body); + responseData = responseData.data[0].details; + + } else if (operation === 'upsert') { + + // ---------------------------------------- + // invoice: upsert + // ---------------------------------------- + + const productDetails = this.getNodeParameter('Product_Details', i) as ProductDetails; + + const body: IDataObject = { + Subject: this.getNodeParameter('subject', i), + Product_Details: adjustProductDetails(productDetails), + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, adjustInvoicePayload(additionalFields)); + } + + responseData = await zohoApiRequest.call(this, 'POST', '/invoices/upsert', body); + responseData = responseData.data[0].details; + + } + + } else if (resource === 'lead') { + + // ********************************************************************** + // lead + // ********************************************************************** + + // https://www.zoho.com/crm/developer/docs/api/v2/leads-response.html + // https://help.zoho.com/portal/en/kb/crm/customize-crm-account/customizing-fields/articles/standard-modules-fields#Leads + + if (operation === 'create') { + + // ---------------------------------------- + // lead: create + // ---------------------------------------- + + const body: IDataObject = { + Company: this.getNodeParameter('Company', i), + Last_Name: this.getNodeParameter('lastName', i), + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, adjustLeadPayload(additionalFields)); + } + + responseData = await zohoApiRequest.call(this, 'POST', '/leads', body); + responseData = responseData.data[0].details; + + } else if (operation === 'delete') { + + // ---------------------------------------- + // lead: delete + // ---------------------------------------- + + const leadId = this.getNodeParameter('leadId', i); + + responseData = await zohoApiRequest.call(this, 'DELETE', `/leads/${leadId}`); + responseData = responseData.data[0].details; + + } else if (operation === 'get') { + + // ---------------------------------------- + // lead: get + // ---------------------------------------- + + const leadId = this.getNodeParameter('leadId', i); + + responseData = await zohoApiRequest.call(this, 'GET', `/leads/${leadId}`); + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // lead: getAll + // ---------------------------------------- + + const qs: IDataObject = {}; + const options = this.getNodeParameter('options', i) as GetAllFilterOptions; + + addGetAllFilterOptions(qs, options); + + responseData = await handleListing.call(this, 'GET', '/leads', {}, qs); + + } else if (operation === 'getFields') { + + // ---------------------------------------- + // lead: getFields + // ---------------------------------------- + + responseData = await zohoApiRequest.call(this, 'GET', '/settings/fields', {}, { module: 'leads' }); + responseData = responseData.fields; + + } else if (operation === 'update') { + + // ---------------------------------------- + // lead: update + // ---------------------------------------- + + const body: IDataObject = {}; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (Object.keys(updateFields).length) { + Object.assign(body, adjustLeadPayload(updateFields)); + } else { + throwOnEmptyUpdate.call(this, resource); + } + + const leadId = this.getNodeParameter('leadId', i); + + responseData = await zohoApiRequest.call(this, 'PUT', `/leads/${leadId}`, body); + responseData = responseData.data[0].details; + + } else if (operation === 'upsert') { + + // ---------------------------------------- + // lead: upsert + // ---------------------------------------- + + const body: IDataObject = { + Company: this.getNodeParameter('Company', i), + Last_Name: this.getNodeParameter('lastName', i), + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, adjustLeadPayload(additionalFields)); + } + + responseData = await zohoApiRequest.call(this, 'POST', '/leads/upsert', body); + responseData = responseData.data[0].details; + + } + + } else if (resource === 'product') { + + // ********************************************************************** + // product + // ********************************************************************** + + // https://www.zoho.com/crm/developer/docs/api/v2/products-response.html + // https://help.zoho.com/portal/en/kb/crm/customize-crm-account/customizing-fields/articles/standard-modules-fields#Products + + if (operation === 'create') { + + // ---------------------------------------- + // product: create + // ---------------------------------------- + + const body: IDataObject = { + Product_Name: this.getNodeParameter('productName', i), + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, adjustProductPayload(additionalFields)); + } + + responseData = await zohoApiRequest.call(this, 'POST', '/products', body); + responseData = responseData.data[0].details; + + } else if (operation === 'delete') { + + // ---------------------------------------- + // product: delete + // ---------------------------------------- + + const productId = this.getNodeParameter('productId', i); + + const endpoint = `/products/${productId}`; + responseData = await zohoApiRequest.call(this, 'DELETE', endpoint); + responseData = responseData.data[0].details; + + } else if (operation === 'get') { + + // ---------------------------------------- + // product: get + // ---------------------------------------- + + const productId = this.getNodeParameter('productId', i); + + const endpoint = `/products/${productId}`; + responseData = await zohoApiRequest.call(this, 'GET', endpoint); + responseData = responseData.data; + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // product: getAll + // ---------------------------------------- + + const qs: IDataObject = {}; + const options = this.getNodeParameter('options', i) as GetAllFilterOptions; + + addGetAllFilterOptions(qs, options); + + responseData = await handleListing.call(this, 'GET', '/products', {}, qs); + + } else if (operation === 'update') { + + // ---------------------------------------- + // product: update + // ---------------------------------------- + + const body: IDataObject = {}; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (Object.keys(updateFields).length) { + Object.assign(body, adjustProductPayload(updateFields)); + } else { + throwOnEmptyUpdate.call(this, resource); + } + + const productId = this.getNodeParameter('productId', i); + + const endpoint = `/products/${productId}`; + responseData = await zohoApiRequest.call(this, 'PUT', endpoint, body); + responseData = responseData.data[0].details; + + } else if (operation === 'upsert') { + + // ---------------------------------------- + // product: upsert + // ---------------------------------------- + + const body: IDataObject = { + Product_Name: this.getNodeParameter('productName', i), + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, adjustProductPayload(additionalFields)); + } + + responseData = await zohoApiRequest.call(this, 'POST', '/products/upsert', body); + responseData = responseData.data[0].details; + + } + + } else if (resource === 'purchaseOrder') { + + // ********************************************************************** + // purchaseOrder + // ********************************************************************** + + // https://www.zoho.com/crm/developer/docs/api/v2/purchase-orders-response.html + // https://help.zoho.com/portal/en/kb/crm/customize-crm-account/customizing-fields/articles/standard-modules-fields#Purchase_Order + + if (operation === 'create') { + + // ---------------------------------------- + // purchaseOrder: create + // ---------------------------------------- + + const productDetails = this.getNodeParameter('Product_Details', i) as ProductDetails; + + throwOnMissingProducts.call(this, resource, productDetails); + + const body: IDataObject = { + Subject: this.getNodeParameter('subject', i), + Vendor_Name: { id: this.getNodeParameter('vendorId', i) }, + Product_Details: adjustProductDetails(productDetails), + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, adjustPurchaseOrderPayload(additionalFields)); + } + + responseData = await zohoApiRequest.call(this, 'POST', '/purchase_orders', body); + responseData = responseData.data[0].details; + + } else if (operation === 'delete') { + + // ---------------------------------------- + // purchaseOrder: delete + // ---------------------------------------- + + const purchaseOrderId = this.getNodeParameter('purchaseOrderId', i); + + const endpoint = `/purchase_orders/${purchaseOrderId}`; + responseData = await zohoApiRequest.call(this, 'DELETE', endpoint); + responseData = responseData.data[0].details; + + } else if (operation === 'get') { + + // ---------------------------------------- + // purchaseOrder: get + // ---------------------------------------- + + const purchaseOrderId = this.getNodeParameter('purchaseOrderId', i); + + const endpoint = `/purchase_orders/${purchaseOrderId}`; + responseData = await zohoApiRequest.call(this, 'GET', endpoint); + responseData = responseData.data; + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // purchaseOrder: getAll + // ---------------------------------------- + + const qs: IDataObject = {}; + const options = this.getNodeParameter('options', i) as GetAllFilterOptions; + + addGetAllFilterOptions(qs, options); + + responseData = await handleListing.call(this, 'GET', '/purchase_orders', {}, qs); + + } else if (operation === 'update') { + + // ---------------------------------------- + // purchaseOrder: update + // ---------------------------------------- + + const body: IDataObject = {}; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (Object.keys(updateFields).length) { + Object.assign(body, adjustPurchaseOrderPayload(updateFields)); + } else { + throwOnEmptyUpdate.call(this, resource); + } + + const purchaseOrderId = this.getNodeParameter('purchaseOrderId', i); + + const endpoint = `/purchase_orders/${purchaseOrderId}`; + responseData = await zohoApiRequest.call(this, 'PUT', endpoint, body); + responseData = responseData.data[0].details; + + } else if (operation === 'upsert') { + + // ---------------------------------------- + // purchaseOrder: upsert + // ---------------------------------------- + + const productDetails = this.getNodeParameter('Product_Details', i) as ProductDetails; + + const body: IDataObject = { + Subject: this.getNodeParameter('subject', i), + Vendor_Name: { id: this.getNodeParameter('vendorId', i) }, + Product_Details: adjustProductDetails(productDetails), + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, adjustPurchaseOrderPayload(additionalFields)); + } + + responseData = await zohoApiRequest.call(this, 'POST', '/purchase_orders/upsert', body); + responseData = responseData.data[0].details; + + } + + } else if (resource === 'quote') { + + // ********************************************************************** + // quote + // ********************************************************************** + + // https://www.zoho.com/crm/developer/docs/api/v2/quotes-response.html + // https://help.zoho.com/portal/en/kb/crm/customize-crm-account/customizing-fields/articles/standard-modules-fields#Quotes + + if (operation === 'create') { + + // ---------------------------------------- + // quote: create + // ---------------------------------------- + + const productDetails = this.getNodeParameter('Product_Details', i) as ProductDetails; + + throwOnMissingProducts.call(this, resource, productDetails); + + const body: IDataObject = { + Subject: this.getNodeParameter('subject', i), + Product_Details: adjustProductDetails(productDetails), + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, adjustQuotePayload(additionalFields)); + } + + responseData = await zohoApiRequest.call(this, 'POST', '/quotes', body); + responseData = responseData.data[0].details; + + } else if (operation === 'delete') { + + // ---------------------------------------- + // quote: delete + // ---------------------------------------- + + const quoteId = this.getNodeParameter('quoteId', i); + + responseData = await zohoApiRequest.call(this, 'DELETE', `/quotes/${quoteId}`); + responseData = responseData.data[0].details; + + } else if (operation === 'get') { + + // ---------------------------------------- + // quote: get + // ---------------------------------------- + + const quoteId = this.getNodeParameter('quoteId', i); + + responseData = await zohoApiRequest.call(this, 'GET', `/quotes/${quoteId}`); + responseData = responseData.data; + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // quote: getAll + // ---------------------------------------- + + const qs: IDataObject = {}; + const options = this.getNodeParameter('options', i) as GetAllFilterOptions; + + addGetAllFilterOptions(qs, options); + + responseData = await handleListing.call(this, 'GET', '/quotes', {}, qs); + + } else if (operation === 'update') { + + // ---------------------------------------- + // quote: update + // ---------------------------------------- + + const body: IDataObject = {}; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (Object.keys(updateFields).length) { + Object.assign(body, adjustQuotePayload(updateFields)); + } else { + throwOnEmptyUpdate.call(this, resource); + } + + const quoteId = this.getNodeParameter('quoteId', i); + + responseData = await zohoApiRequest.call(this, 'PUT', `/quotes/${quoteId}`, body); + responseData = responseData.data[0].details; + + } else if (operation === 'upsert') { + + // ---------------------------------------- + // quote: upsert + // ---------------------------------------- + + const productDetails = this.getNodeParameter('Product_Details', i) as ProductDetails; + + const body: IDataObject = { + Subject: this.getNodeParameter('subject', i), + Product_Details: adjustProductDetails(productDetails), + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, adjustQuotePayload(additionalFields)); + } + + responseData = await zohoApiRequest.call(this, 'POST', '/quotes/upsert', body); + responseData = responseData.data[0].details; + + } + + } else if (resource === 'salesOrder') { + + // ********************************************************************** + // salesOrder + // ********************************************************************** + + // https://www.zoho.com/crm/developer/docs/api/v2/sales-orders-response.html + // https://help.zoho.com/portal/en/kb/crm/customize-crm-account/customizing-fields/articles/standard-modules-fields#Sales_Orders + + if (operation === 'create') { + + // ---------------------------------------- + // salesOrder: create + // ---------------------------------------- + + const productDetails = this.getNodeParameter('Product_Details', i) as ProductDetails; + + const body: IDataObject = { + Account_Name: { id: this.getNodeParameter('accountId', i) }, + Subject: this.getNodeParameter('subject', i), + Product_Details: adjustProductDetails(productDetails), + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, adjustSalesOrderPayload(additionalFields)); + } + + responseData = await zohoApiRequest.call(this, 'POST', '/sales_orders', body); + responseData = responseData.data[0].details; + + } else if (operation === 'delete') { + + // ---------------------------------------- + // salesOrder: delete + // ---------------------------------------- + + const salesOrderId = this.getNodeParameter('salesOrderId', i); + + const endpoint = `/sales_orders/${salesOrderId}`; + responseData = await zohoApiRequest.call(this, 'DELETE', endpoint); + responseData = responseData.data[0].details; + + } else if (operation === 'get') { + + // ---------------------------------------- + // salesOrder: get + // ---------------------------------------- + + const salesOrderId = this.getNodeParameter('salesOrderId', i); + + const endpoint = `/sales_orders/${salesOrderId}`; + responseData = await zohoApiRequest.call(this, 'GET', endpoint); + responseData = responseData.data; + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // salesOrder: getAll + // ---------------------------------------- + + const qs: IDataObject = {}; + const options = this.getNodeParameter('options', i) as GetAllFilterOptions; + + addGetAllFilterOptions(qs, options); + + responseData = await handleListing.call(this, 'GET', '/sales_orders', {}, qs); + + } else if (operation === 'update') { + + // ---------------------------------------- + // salesOrder: update + // ---------------------------------------- + + const body: IDataObject = {}; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (Object.keys(updateFields).length) { + Object.assign(body, adjustSalesOrderPayload(updateFields)); + } else { + throwOnEmptyUpdate.call(this, resource); + } + + const salesOrderId = this.getNodeParameter('salesOrderId', i); + + const endpoint = `/sales_orders/${salesOrderId}`; + responseData = await zohoApiRequest.call(this, 'PUT', endpoint, body); + responseData = responseData.data[0].details; + + } else if (operation === 'upsert') { + + // ---------------------------------------- + // salesOrder: upsert + // ---------------------------------------- + + const productDetails = this.getNodeParameter('Product_Details', i) as ProductDetails; + + const body: IDataObject = { + Account_Name: { id: this.getNodeParameter('accountId', i) }, + Subject: this.getNodeParameter('subject', i), + Product_Details: adjustProductDetails(productDetails), + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, adjustSalesOrderPayload(additionalFields)); + } + + responseData = await zohoApiRequest.call(this, 'POST', '/sales_orders/upsert', body); + responseData = responseData.data[0].details; + + } + + } else if (resource === 'vendor') { + + // ********************************************************************** + // vendor + // ********************************************************************** + + // https://www.zoho.com/crm/developer/docs/api/v2/vendors-response.html + // https://help.zoho.com/portal/en/kb/crm/customize-crm-account/customizing-fields/articles/standard-modules-fields#Vendors + + if (operation === 'create') { + + // ---------------------------------------- + // vendor: create + // ---------------------------------------- + + const body: IDataObject = { + Vendor_Name: this.getNodeParameter('vendorName', i), + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, adjustVendorPayload(additionalFields)); + } + + responseData = await zohoApiRequest.call(this, 'POST', '/vendors', body); + responseData = responseData.data[0].details; + + } else if (operation === 'delete') { + + // ---------------------------------------- + // vendor: delete + // ---------------------------------------- + + const vendorId = this.getNodeParameter('vendorId', i); + + const endpoint = `/vendors/${vendorId}`; + responseData = await zohoApiRequest.call(this, 'DELETE', endpoint); + responseData = responseData.data[0].details; + + } else if (operation === 'get') { + + // ---------------------------------------- + // vendor: get + // ---------------------------------------- + + const vendorId = this.getNodeParameter('vendorId', i); + + const endpoint = `/vendors/${vendorId}`; + responseData = await zohoApiRequest.call(this, 'GET', endpoint); + responseData = responseData.data; + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // vendor: getAll + // ---------------------------------------- + + const qs: IDataObject = {}; + const options = this.getNodeParameter('options', i) as GetAllFilterOptions; + + addGetAllFilterOptions(qs, options); + + responseData = await handleListing.call(this, 'GET', '/vendors', {}, qs); + + } else if (operation === 'update') { + + // ---------------------------------------- + // vendor: update + // ---------------------------------------- + + const body: IDataObject = {}; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (Object.keys(updateFields).length) { + Object.assign(body, adjustVendorPayload(updateFields)); + } else { + throwOnEmptyUpdate.call(this, resource); + } + + const vendorId = this.getNodeParameter('vendorId', i); + + const endpoint = `/vendors/${vendorId}`; + responseData = await zohoApiRequest.call(this, 'PUT', endpoint, body); + responseData = responseData.data[0].details; + + } else if (operation === 'upsert') { + + // ---------------------------------------- + // vendor: upsert + // ---------------------------------------- + + const body: IDataObject = { + Vendor_Name: this.getNodeParameter('vendorName', i), + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, adjustVendorPayload(additionalFields)); + } + + responseData = await zohoApiRequest.call(this, 'POST', '/vendors/upsert', body); + responseData = responseData.data[0].details; + } } - //https://www.zoho.com/crm/developer/docs/api/get-records.html - if (operation === 'getAll') { - const returnAll = this.getNodeParameter('returnAll', i) as boolean; - const options = this.getNodeParameter('options', i) as IDataObject; - if (options.fields) { - qs.fields = (options.fields as string[]).join(','); - } - if (options.approved) { - qs.approved = options.approved as boolean; - } - if (options.converted) { - qs.converted = options.converted as boolean; - } - if (options.includeChild) { - qs.include_child = options.includeChild as boolean; - } - if (options.sortOrder) { - qs.sort_order = options.sortOrder as string; - } - if (options.sortBy) { - qs.sort_by = options.sortBy as string; - } - if (options.territoryId) { - qs.territory_id = options.territoryId as string; - } - if (returnAll) { - responseData = await zohoApiRequestAllItems.call(this, 'data', 'GET', '/leads', {}, qs); - } else { - qs.per_page = this.getNodeParameter('limit', i) as number; - responseData = await zohoApiRequest.call(this, 'GET', '/leads', {}, qs); - responseData = responseData.data; - } - } - //https://www.zoho.com/crm/developer/docs/api/delete-specific-record.html - if (operation === 'delete') { - const leadId = this.getNodeParameter('leadId', i) as string; - responseData = await zohoApiRequest.call(this, 'DELETE', `/leads/${leadId}`); - responseData = responseData.data; - } - //https://www.zoho.com/crm/developer/docs/api/field-meta.html - if (operation === 'getFields') { - qs.module = 'leads'; - responseData = await zohoApiRequest.call(this, 'GET', '/settings/fields', {}, qs); - responseData = responseData.fields; + + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; } + + throw error; } - if (Array.isArray(responseData)) { - returnData.push.apply(returnData, responseData as IDataObject[]); - } else if (responseData !== undefined) { - returnData.push(responseData as IDataObject); - } + + Array.isArray(responseData) + ? returnData.push(...responseData) + : returnData.push(responseData); } + return [this.helpers.returnJsonArray(returnData)]; } } diff --git a/packages/nodes-base/nodes/Zoho/descriptions/AccountDescription.ts b/packages/nodes-base/nodes/Zoho/descriptions/AccountDescription.ts new file mode 100644 index 0000000000..6351d49f4d --- /dev/null +++ b/packages/nodes-base/nodes/Zoho/descriptions/AccountDescription.ts @@ -0,0 +1,415 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + billingAddress, + currencies, + makeCustomFieldsFixedCollection, + makeGetAllFields, + shippingAddress, +} from './SharedFields'; + +export const accountOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'account', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create an account', + }, + { + name: 'Create or Update', + value: 'upsert', + description: 'Create a new record, or update the current one if it already exists (upsert)', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an account', + }, + { + name: 'Get', + value: 'get', + description: 'Get an account', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all accounts', + }, + { + name: 'Update', + value: 'update', + description: 'Update an account', + }, + ], + default: 'create', + description: 'Operation to perform', + }, +] as INodeProperties[]; + +export const accountFields = [ + // ---------------------------------------- + // account: create + // ---------------------------------------- + { + displayName: 'Account Name', + name: 'accountName', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'create', + ], + }, + }, + }, + + // ---------------------------------------- + // account: upsert + // ---------------------------------------- + { + displayName: 'Account Name', + name: 'accountName', + description: 'Name of the account. If a record with this account name exists it will be updated, otherwise a new one will be created.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'upsert', + ], + }, + }, + }, + + // ---------------------------------------- + // account: create + upsert + // ---------------------------------------- + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'create', + 'upsert', + ], + }, + }, + options: [ + { + displayName: 'Account Number', + name: 'Account_Number', + type: 'string', + default: '', + }, + { + displayName: 'Account Site', + name: 'Account_Site', + type: 'string', + default: '', + description: 'Name of the account’s location, e.g. Headquarters or London.', + }, + { + displayName: 'Account Type', + name: 'Account_Type', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getAccountType', + }, + default: [], + }, + { + displayName: 'Annual Revenue', + name: 'Annual_Revenue', + type: 'number', + default: '', + }, + billingAddress, + { + displayName: 'Contact Details', + name: 'Contact_Details', + type: 'string', + default: '', + }, + { + displayName: 'Currency', + name: 'Currency', + type: 'options', + default: 'USD', + description: 'Symbol of the currency in which revenue is generated.', + options: currencies, + }, + makeCustomFieldsFixedCollection('account'), + { + displayName: 'Description', + name: 'Description', + type: 'string', + default: '', + }, + { + displayName: 'Employees', + name: 'Employees', + type: 'number', + default: '', + description: 'Number of employees in the account’s company.', + }, + { + displayName: 'Exchange Rate', + name: 'Exchange_Rate', + type: 'number', + default: '', + description: 'Exchange rate of the default currency to the home currency.', + }, + { + displayName: 'Fax', + name: 'Fax', + type: 'string', + default: '', + }, + { + displayName: 'Industry', + name: 'Industry', + type: 'string', + default: '', + }, + { + displayName: 'Phone', + name: 'Phone', + type: 'string', + default: '', + }, + shippingAddress, + { + displayName: 'Ticker Symbol', + name: 'Ticker_Symbol', + type: 'string', + default: '', + }, + { + displayName: 'Website', + name: 'Website', + type: 'string', + default: '', + }, + ], + }, + + // ---------------------------------------- + // account: delete + // ---------------------------------------- + { + displayName: 'Account ID', + name: 'accountId', + description: 'ID of the account to delete. Can be found at the end of the URL.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------------- + // account: get + // ---------------------------------------- + { + displayName: 'Account ID', + name: 'accountId', + description: 'ID of the account to retrieve. Can be found at the end of the URL.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------------- + // account: getAll + // ---------------------------------------- + ...makeGetAllFields('account'), + + // ---------------------------------------- + // account: update + // ---------------------------------------- + { + displayName: 'Account ID', + name: 'accountId', + description: 'ID of the account to update. Can be found at the end of the URL.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Account Name', + name: 'Account_Name', + type: 'string', + default: '', + }, + { + displayName: 'Account Number', + name: 'Account_Number', + type: 'string', + default: '', + }, + { + displayName: 'Account Site', + name: 'Account_Site', + type: 'string', + default: '', + description: 'Name of the account’s location, e.g. Headquarters or London.', + }, + { + displayName: 'Account Type', + name: 'Account_Type', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getAccountType', + }, + default: [], + }, + { + displayName: 'Annual Revenue', + name: 'Annual_Revenue', + type: 'number', + default: '', + }, + billingAddress, + { + displayName: 'Contact Details', + name: 'Contact_Details', + type: 'string', + default: '', + }, + { + displayName: 'Currency', + name: 'Currency', + type: 'options', + default: 'USD', + description: 'Symbol of the currency in which revenue is generated.', + options: currencies, + }, + makeCustomFieldsFixedCollection('account'), + { + displayName: 'Description', + name: 'Description', + type: 'string', + default: '', + }, + { + displayName: 'Employees', + name: 'Employees', + type: 'number', + default: '', + description: 'Number of employees in the account’s company.', + }, + { + displayName: 'Exchange Rate', + name: 'Exchange_Rate', + type: 'number', + default: '', + description: 'Exchange rate of the default currency to the home currency.', + }, + { + displayName: 'Fax', + name: 'Fax', + type: 'string', + default: '', + }, + { + displayName: 'Industry', + name: 'Industry', + type: 'string', + default: '', + }, + { + displayName: 'Phone', + name: 'Phone', + type: 'string', + default: '', + }, + shippingAddress, + { + displayName: 'Ticker Symbol', + name: 'Ticker_Symbol', + type: 'string', + default: '', + }, + { + displayName: 'Website', + name: 'Website', + type: 'string', + default: '', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zoho/descriptions/ContactDescription.ts b/packages/nodes-base/nodes/Zoho/descriptions/ContactDescription.ts new file mode 100644 index 0000000000..fc58c8f6f4 --- /dev/null +++ b/packages/nodes-base/nodes/Zoho/descriptions/ContactDescription.ts @@ -0,0 +1,590 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + currencies, + mailingAddress, + makeCustomFieldsFixedCollection, + makeGetAllFields, + otherAddress, +} from './SharedFields'; + +export const contactOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'contact', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a contact', + }, + { + name: 'Create or Update', + value: 'upsert', + description: 'Create a new record, or update the current one if it already exists (upsert)', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a contact', + }, + { + name: 'Get', + value: 'get', + description: 'Get a contact', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all contacts', + }, + { + name: 'Update', + value: 'update', + description: 'Update a contact', + }, + ], + default: 'create', + description: 'Operation to perform', + }, +] as INodeProperties[]; + +export const contactFields = [ + // ---------------------------------------- + // contact: create + // ---------------------------------------- + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Assistant', + name: 'Assistant', + type: 'string', + default: '', + description: 'Name of the contact’s assistant.', + }, + makeCustomFieldsFixedCollection('contact'), + { + displayName: 'Date of Birth', + name: 'Date_of_Birth', + type: 'dateTime', + default: '', + }, + { + displayName: 'Department', + name: 'Department', + type: 'string', + default: '', + description: 'Company department to which the contact belongs.', + }, + { + displayName: 'Description', + name: 'Description', + type: 'string', + default: '', + }, + { + displayName: 'Email (Primary)', + name: 'Email', + type: 'string', + default: '', + }, + { + displayName: 'Email (Secondary)', + name: 'Secondary_Email', + type: 'string', + default: '', + }, + { + displayName: 'Fax', + name: 'Fax', + type: 'string', + default: '', + }, + { + displayName: 'First Name', + name: 'First_Name', + type: 'string', + default: '', + }, + { + displayName: 'Full Name', + name: 'Full_Name', + type: 'string', + default: '', + }, + mailingAddress, + { + displayName: 'Mobile', + name: 'Mobile', + type: 'string', + default: '', + }, + otherAddress, + { + displayName: 'Phone', + name: 'Phone', + type: 'string', + default: '', + }, + { + displayName: 'Phone (Assistant)', + name: 'Asst_Phone', + type: 'string', + default: '', + description: 'Phone number of the contact’s assistant.', + }, + { + displayName: 'Phone (Home)', + name: 'Home_Phone', + type: 'string', + default: '', + }, + { + displayName: 'Phone (Other)', + name: 'Other_Phone', + type: 'string', + default: '', + }, + { + displayName: 'Salutation', + name: 'Salutation', + type: 'string', + default: '', + }, + { + displayName: 'Skype ID', + name: 'Skype_ID', + type: 'string', + default: '', + }, + { + displayName: 'Title', + name: 'Title', + type: 'string', + default: '', + description: 'Position of the contact at their company.', + }, + { + displayName: 'Twitter', + name: 'Twitter', + type: 'string', + default: '', + }, + ], + }, + + // ---------------------------------------- + // contact: upsert + // ---------------------------------------- + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'upsert', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'upsert', + ], + }, + }, + options: [ + { + displayName: 'Assistant', + name: 'Assistant', + type: 'string', + default: '', + description: 'Name of the contact’s assistant.', + }, + makeCustomFieldsFixedCollection('contact'), + { + displayName: 'Date of Birth', + name: 'Date_of_Birth', + type: 'dateTime', + default: '', + }, + { + displayName: 'Department', + name: 'Department', + type: 'string', + default: '', + description: 'Company department to which the contact belongs.', + }, + { + displayName: 'Description', + name: 'Description', + type: 'string', + default: '', + }, + { + displayName: 'Email (Primary)', + name: 'Email', + type: 'string', + default: '', + description: 'Email of the contact. If a record with this email exists it will be updated, otherwise a new one will be created.', + }, + { + displayName: 'Email (Secondary)', + name: 'Secondary_Email', + type: 'string', + default: '', + }, + { + displayName: 'Fax', + name: 'Fax', + type: 'string', + default: '', + }, + { + displayName: 'First Name', + name: 'First_Name', + type: 'string', + default: '', + }, + { + displayName: 'Full Name', + name: 'Full_Name', + type: 'string', + default: '', + }, + mailingAddress, + { + displayName: 'Mobile', + name: 'Mobile', + type: 'string', + default: '', + }, + otherAddress, + { + displayName: 'Phone', + name: 'Phone', + type: 'string', + default: '', + }, + { + displayName: 'Phone (Assistant)', + name: 'Asst_Phone', + type: 'string', + default: '', + description: 'Phone number of the contact’s assistant.', + }, + { + displayName: 'Phone (Home)', + name: 'Home_Phone', + type: 'string', + default: '', + }, + { + displayName: 'Phone (Other)', + name: 'Other_Phone', + type: 'string', + default: '', + }, + { + displayName: 'Salutation', + name: 'Salutation', + type: 'string', + default: '', + }, + { + displayName: 'Skype ID', + name: 'Skype_ID', + type: 'string', + default: '', + }, + { + displayName: 'Title', + name: 'Title', + type: 'string', + default: '', + description: 'Position of the contact at their company.', + }, + { + displayName: 'Twitter', + name: 'Twitter', + type: 'string', + default: '', + }, + ], + }, + + // ---------------------------------------- + // contact: delete + // ---------------------------------------- + { + displayName: 'Contact ID', + name: 'contactId', + description: 'ID of the contact to delete.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------------- + // contact: get + // ---------------------------------------- + { + displayName: 'Contact ID', + name: 'contactId', + description: 'ID of the contact to retrieve.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------------- + // contact: getAll + // ---------------------------------------- + ...makeGetAllFields('contact'), + + // ---------------------------------------- + // contact: update + // ---------------------------------------- + { + displayName: 'Contact ID', + name: 'contactId', + description: 'ID of the contact to update.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Assistant', + name: 'Assistant', + type: 'string', + default: '', + }, + { + displayName: 'Assistant’s Phone', + name: 'Asst_Phone', + type: 'string', + default: '', + description: 'Phone number of the contact’s assistant.', + }, + { + displayName: 'Currency', + name: 'Currency', + type: 'options', + default: 'USD', + description: 'Symbol of the currency in which revenue is generated.', + options: currencies, + }, + makeCustomFieldsFixedCollection('contact'), + { + displayName: 'Date of Birth', + name: 'Date_of_Birth', + type: 'dateTime', + default: '', + }, + { + displayName: 'Department', + name: 'Department', + type: 'string', + default: '', + }, + { + displayName: 'Description', + name: 'Description', + type: 'string', + default: '', + }, + { + displayName: 'Email (Primary)', + name: 'Email', + type: 'string', + default: '', + }, + { + displayName: 'Email (Secondary)', + name: 'Secondary_Email', + type: 'string', + default: '', + }, + { + displayName: 'Fax', + name: 'Fax', + type: 'string', + default: '', + }, + { + displayName: 'First Name', + name: 'First_Name', + type: 'string', + default: '', + }, + { + displayName: 'Full Name', + name: 'Full_Name', + type: 'string', + default: '', + }, + { + displayName: 'Home Phone', + name: 'Home_Phone', + type: 'string', + default: '', + }, + { + displayName: 'Last Name', + name: 'Last_Name', + type: 'string', + default: '', + }, + mailingAddress, + { + displayName: 'Mobile', + name: 'Mobile', + type: 'string', + default: '', + }, + otherAddress, + { + displayName: 'Other Phone', + name: 'Other_Phone', + type: 'string', + default: '', + }, + { + displayName: 'Phone', + name: 'Phone', + type: 'string', + default: '', + }, + { + displayName: 'Salutation', + name: 'Salutation', + type: 'string', + default: '', + }, + { + displayName: 'Skype ID', + name: 'Skype_ID', + type: 'string', + default: '', + }, + { + displayName: 'Title', + name: 'Title', + type: 'string', + default: '', + description: 'Position of the contact at their company.', + }, + { + displayName: 'Twitter', + name: 'Twitter', + type: 'string', + default: '', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zoho/descriptions/DealDescription.ts b/packages/nodes-base/nodes/Zoho/descriptions/DealDescription.ts new file mode 100644 index 0000000000..312c9b855b --- /dev/null +++ b/packages/nodes-base/nodes/Zoho/descriptions/DealDescription.ts @@ -0,0 +1,385 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + currencies, + makeCustomFieldsFixedCollection, + makeGetAllFields, +} from './SharedFields'; + +export const dealOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'deal', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a deal', + }, + { + name: 'Create or Update', + value: 'upsert', + description: 'Create a new record, or update the current one if it already exists (upsert)', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a contact', + }, + { + name: 'Get', + value: 'get', + description: 'Get a contact', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all contacts', + }, + { + name: 'Update', + value: 'update', + description: 'Update a contact', + }, + ], + default: 'create', + description: 'Operation to perform', + }, +] as INodeProperties[]; + +export const dealFields = [ + // ---------------------------------------- + // deal: create + // ---------------------------------------- + { + displayName: 'Deal Name', + name: 'dealName', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'create', + ], + }, + }, + }, + + // ---------------------------------------- + // deal: upsert + // ---------------------------------------- + { + displayName: 'Deal Name', + name: 'dealName', + description: 'Name of the deal. If a record with this deal name exists it will be updated, otherwise a new one will be created.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'upsert', + ], + }, + }, + }, + // ---------------------------------------- + // deal: create + upsert + // ---------------------------------------- + { + displayName: 'Stage', + name: 'stage', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getDealStage', + }, + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'create', + 'upsert', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'create', + 'upsert', + ], + }, + }, + options: [ + { + displayName: 'Amount', + name: 'Amount', + type: 'number', + default: '', + description: 'Monetary amount of the deal.', + }, + { + displayName: 'Closing Date', + name: 'Closing_Date', + type: 'dateTime', + default: '', + }, + { + displayName: 'Currency', + name: 'Currency', + type: 'options', + default: 'USD', + description: 'Symbol of the currency in which revenue is generated.', + options: currencies, + }, + makeCustomFieldsFixedCollection('deal'), + { + displayName: 'Description', + name: 'Description', + type: 'string', + default: '', + }, + { + displayName: 'Lead Conversion Time', + name: 'Lead_Conversion_Time', + type: 'number', + default: '', + description: 'Averge number of days to convert the lead into a deal.', + }, + { + displayName: 'Next Step', + name: 'Next_Step', + type: 'string', + default: '', + description: 'Description of the next step in the sales process.', + }, + { + displayName: 'Overall Sales Duration', + name: 'Overall_Sales_Duration', + type: 'number', + default: '', + description: 'Averge number of days to convert the lead into a deal and to win the deal.', + }, + { + displayName: 'Probability', + name: 'Probability', + type: 'number', + typeOptions: { + minValue: 0, + maxValue: 100, + }, + default: '', + description: 'Probability of deal closure as a percentage. For example, enter 12 for 12%.', + }, + { + displayName: 'Sales Cycle Duration', + name: 'Sales_Cycle_Duration', + type: 'number', + default: 0, + description: 'Averge number of days for the deal to be won.', + }, + ], + }, + + // ---------------------------------------- + // deal: delete + // ---------------------------------------- + { + displayName: 'Deal ID', + name: 'dealId', + description: 'ID of the deal to delete.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------------- + // deal: get + // ---------------------------------------- + { + displayName: 'Deal ID', + name: 'dealId', + description: 'ID of the deal to retrieve.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------------- + // deal: getAll + // ---------------------------------------- + ...makeGetAllFields('deal'), + + // ---------------------------------------- + // deal: update + // ---------------------------------------- + { + displayName: 'Deal ID', + name: 'dealId', + description: 'ID of the deal to update.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Amount', + name: 'Amount', + type: 'number', + default: '', + description: 'Monetary amount of the deal.', + }, + { + displayName: 'Closing Date', + name: 'Closing_Date', + type: 'dateTime', + default: '', + }, + { + displayName: 'Currency', + name: 'Currency', + type: 'string', + default: '', + description: 'Symbol of the currency in which revenue is generated.', + }, + makeCustomFieldsFixedCollection('deal'), + { + displayName: 'Deal Name', + name: 'Deal_Name', + type: 'string', + default: '', + }, + { + displayName: 'Description', + name: 'Description', + type: 'string', + default: '', + }, + { + displayName: 'Lead Conversion Time', + name: 'Lead_Conversion_Time', + type: 'number', + default: '', + description: 'Averge number of days to convert the lead into a deal.', + }, + { + displayName: 'Next Step', + name: 'Next_Step', + type: 'string', + default: '', + description: 'Description of the next step in the sales process.', + }, + { + displayName: 'Overall Sales Duration', + name: 'Overall_Sales_Duration', + type: 'number', + default: '', + description: 'Averge number of days to convert the lead into a deal and to win the deal.', + }, + { + displayName: 'Probability', + name: 'Probability', + type: 'number', + typeOptions: { + minValue: 0, + maxValue: 100, + }, + default: '', + description: 'Probability of deal closure as a percentage. For example, enter 12 for 12%.', + }, + { + displayName: 'Sales Cycle Duration', + name: 'Sales_Cycle_Duration', + type: 'number', + default: 0, + description: 'Averge number of days to win the deal.', + }, + { + displayName: 'Stage', + name: 'Stage', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getDealStage', + }, + default: [], + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zoho/descriptions/InvoiceDescription.ts b/packages/nodes-base/nodes/Zoho/descriptions/InvoiceDescription.ts new file mode 100644 index 0000000000..4ede26523f --- /dev/null +++ b/packages/nodes-base/nodes/Zoho/descriptions/InvoiceDescription.ts @@ -0,0 +1,463 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + billingAddress, + currencies, + makeCustomFieldsFixedCollection, + makeGetAllFields, + productDetailsOptions, + shippingAddress, +} from './SharedFields'; + +export const invoiceOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create an invoice', + }, + { + name: 'Create or Update', + value: 'upsert', + description: 'Create a new record, or update the current one if it already exists (upsert)', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an invoice', + }, + { + name: 'Get', + value: 'get', + description: 'Get an invoice', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all invoices', + }, + { + name: 'Update', + value: 'update', + description: 'Update an invoice', + }, + ], + default: 'create', + description: 'Operation to perform', + }, +] as INodeProperties[]; + +export const invoiceFields = [ + // ---------------------------------------- + // invoice: create + // ---------------------------------------- + { + displayName: 'Subject', + name: 'subject', + description: 'Subject or title of the invoice.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'create', + ], + }, + }, + }, + + // ---------------------------------------- + // invoice: upsert + // ---------------------------------------- + { + displayName: 'Subject', + name: 'subject', + description: 'Subject or title of the invoice. If a record with this subject exists it will be updated, otherwise a new one will be created.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'upsert', + ], + }, + }, + }, + + // ---------------------------------------- + // invoice: create + upsert + // ---------------------------------------- + { + displayName: 'Products', + name: 'Product_Details', + type: 'collection', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Product', + }, + default: {}, + placeholder: 'Add Field', + options: productDetailsOptions, + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'create', + 'upsert', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'create', + 'upsert', + ], + }, + }, + options: [ + { + displayName: 'Account ID', + name: 'accountId', + type: 'options', + default: [], + typeOptions: { + loadOptionsMethod: 'getAccounts', + }, + description: 'ID of the account associated with this invoice.', + }, + { + displayName: 'Adjustment', + name: 'Adjustment', + type: 'number', + default: '', + description: 'Adjustment in the grand total, if any.', + }, + billingAddress, + { + displayName: 'Currency', + name: 'Currency', + type: 'options', + default: 'USD', + description: 'Symbol of the currency in which revenue is generated.', + options: currencies, + }, + makeCustomFieldsFixedCollection('invoice'), + { + displayName: 'Description', + name: 'Description', + type: 'string', + default: '', + }, + { + displayName: 'Due Date', + name: 'Due_Date', + type: 'dateTime', + default: '', + }, + { + displayName: 'Exchange Rate', + name: 'Exchange_Rate', + type: 'number', + default: '', + description: 'Exchange rate of the default currency to the home currency.', + }, + { + displayName: 'Grand Total', + name: 'Grand_Total', + type: 'number', + default: '', + description: 'Total amount for the product after deducting tax and discounts.', + }, + { + displayName: 'Invoice Date', + name: 'Invoice_Date', + type: 'dateTime', + default: '', + }, + { + displayName: 'Invoice Number', + name: 'Invoice_Number', + type: 'string', + default: '', + }, + { + displayName: 'Sales Commission', + name: 'Sales_Commission', + type: 'number', + default: '', + description: 'Commission of sales person on deal closure as a percentage. For example, enter 12 for 12%.', + }, + shippingAddress, + { + displayName: 'Status', + name: 'Status', + type: 'string', + default: '', + }, + { + displayName: 'Sub Total', + name: 'Sub_Total', + type: 'number', + default: '', + description: 'Total amount for the product excluding tax.', + }, + { + displayName: 'Tax', + name: 'Tax', + type: 'number', + default: '', + description: 'Tax amount as the sum of sales tax and value-added tax.', + }, + { + displayName: 'Terms and Conditions', + name: 'Terms_and_Conditions', + type: 'string', + default: '', + description: 'Terms and conditions associated with the invoice.', + }, + ], + }, + + // ---------------------------------------- + // invoice: delete + // ---------------------------------------- + { + displayName: 'Invoice ID', + name: 'invoiceId', + description: 'ID of the invoice to delete.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------------- + // invoice: get + // ---------------------------------------- + { + displayName: 'Invoice ID', + name: 'invoiceId', + description: 'ID of the invoice to retrieve.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------------- + // invoice: getAll + // ---------------------------------------- + ...makeGetAllFields('invoice'), + + // ---------------------------------------- + // invoice: update + // ---------------------------------------- + { + displayName: 'Invoice ID', + name: 'invoiceId', + description: 'ID of the invoice to update.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Account ID', + name: 'accountId', + type: 'options', + default: [], + typeOptions: { + loadOptionsMethod: 'getAccounts', + }, + description: 'ID of the account associated with this invoice.', + }, + { + displayName: 'Adjustment', + name: 'Adjustment', + type: 'number', + default: '', + description: 'Adjustment in the grand total, if any.', + }, + billingAddress, + { + displayName: 'Currency', + name: 'Currency', + type: 'options', + default: 'USD', + description: 'Symbol of the currency in which revenue is generated.', + options: currencies, + }, + makeCustomFieldsFixedCollection('invoice'), + { + displayName: 'Description', + name: 'Description', + type: 'string', + default: '', + }, + { + displayName: 'Due Date', + name: 'Due_Date', + type: 'dateTime', + default: '', + }, + { + displayName: 'Exchange Rate', + name: 'Exchange_Rate', + type: 'number', + default: '', + description: 'Exchange rate of the default currency to the home currency.', + }, + { + displayName: 'Grand Total', + name: 'Grand_Total', + type: 'number', + default: '', + description: 'Total amount for the product after deducting tax and discounts.', + }, + { + displayName: 'Invoice Date', + name: 'Invoice_Date', + type: 'dateTime', + default: '', + }, + { + displayName: 'Invoice Number', + name: 'Invoice_Number', + type: 'string', + default: '', + }, + { + displayName: 'Products', + name: 'Product_Details', + type: 'collection', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Product', + }, + default: {}, + placeholder: 'Add Field', + options: productDetailsOptions, + }, + { + displayName: 'Sales Commission', + name: 'Sales_Commission', + type: 'number', + default: '', + description: 'Commission of sales person on deal closure as a percentage. For example, enter 12 for 12%.', + }, + shippingAddress, + { + displayName: 'Status', + name: 'Status', + type: 'string', + default: '', + }, + { + displayName: 'Sub Total', + name: 'Sub_Total', + type: 'number', + default: '', + description: 'Total amount for the product excluding tax.', + }, + { + displayName: 'Subject', + name: 'Subject', + type: 'string', + default: '', + description: 'Subject or title of the invoice.', + }, + { + displayName: 'Tax', + name: 'Tax', + type: 'number', + default: '', + description: 'Tax amount as the sum of sales tax and value-added tax.', + }, + { + displayName: 'Terms and Conditions', + name: 'Terms_and_Conditions', + type: 'string', + default: '', + description: 'Terms and conditions associated with the invoice.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zoho/descriptions/LeadDescription.ts b/packages/nodes-base/nodes/Zoho/descriptions/LeadDescription.ts new file mode 100644 index 0000000000..1f26dd04e7 --- /dev/null +++ b/packages/nodes-base/nodes/Zoho/descriptions/LeadDescription.ts @@ -0,0 +1,694 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + address, + currencies, + makeCustomFieldsFixedCollection, + makeGetAllFields, +} from './SharedFields'; + +export const leadOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'lead', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a lead', + }, + { + name: 'Create or Update', + value: 'upsert', + description: 'Create a new record, or update the current one if it already exists (upsert)', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a lead', + }, + { + name: 'Get', + value: 'get', + description: 'Get a lead', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all leads', + }, + { + name: 'Get Fields', + value: 'getFields', + description: 'Get lead fields', + }, + { + name: 'Update', + value: 'update', + description: 'Update a lead', + }, + ], + default: 'create', + description: 'Operation to perform', + }, +] as INodeProperties[]; + +export const leadFields = [ + // ---------------------------------------- + // lead: create + // ---------------------------------------- + { + displayName: 'Company', + name: 'Company', + description: 'Company at which the lead works.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + address, + { + displayName: 'Annual Revenue', + name: 'Annual_Revenue', + type: 'number', + default: '', + description: 'Annual revenue of the lead’s company.', + }, + { + displayName: 'Currency', + name: 'Currency', + type: 'options', + default: 'USD', + description: 'Symbol of the currency in which revenue is generated.', + options: currencies, + }, + makeCustomFieldsFixedCollection('lead'), + { + displayName: 'Description', + name: 'Description', + type: 'string', + default: '', + }, + { + displayName: 'Designation', + name: 'Designation', + type: 'string', + default: '', + description: 'Position of the lead at their company.', + }, + { + displayName: 'Email', + name: 'Email', + type: 'string', + default: '', + }, + { + displayName: 'Email Opt Out', + name: 'Email_Opt_Out', + type: 'boolean', + default: false, + }, + { + displayName: 'Fax', + name: 'Fax', + type: 'string', + default: '', + }, + { + displayName: 'First Name', + name: 'First_Name', + type: 'string', + default: '', + }, + { + displayName: 'Full Name', + name: 'Full_Name', + type: 'string', + default: '', + }, + { + displayName: 'Industry', + name: 'Industry', + type: 'string', + default: '', + description: 'Industry to which the lead belongs.', + }, + { + displayName: 'Industry Type', + name: 'Industry_Type', + type: 'string', + default: '', + description: 'Type of industry to which the lead belongs.', + }, + { + displayName: 'Lead Source', + name: 'Lead_Source', + type: 'string', + default: '', + description: 'Source from which the lead was created.', + }, + { + displayName: 'Lead Status', + name: 'Lead_Status', + type: 'string', + default: '', + }, + { + displayName: 'Mobile', + name: 'Mobile', + type: 'string', + default: '', + }, + { + displayName: 'Number of Employees', + name: 'No_of_Employees', + type: 'number', + default: '', + description: 'Number of employees in the lead’s company.', + }, + { + displayName: 'Phone', + name: 'Phone', + type: 'string', + default: '', + }, + { + displayName: 'Salutation', + name: 'Salutation', + type: 'string', + default: '', + }, + { + displayName: 'Secondary Email', + name: 'Secondary_Email', + type: 'string', + default: '', + }, + { + displayName: 'Skype ID', + name: 'Skype_ID', + type: 'string', + default: '', + }, + { + displayName: 'Twitter', + name: 'Twitter', + type: 'string', + default: '', + }, + { + displayName: 'Website', + name: 'Website', + type: 'string', + default: '', + }, + ], + }, + + // ---------------------------------------- + // lead: upsert + // ---------------------------------------- + { + displayName: 'Company', + name: 'Company', + description: 'Company at which the lead works.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'upsert', + ], + }, + }, + }, + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'upsert', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'upsert', + ], + }, + }, + options: [ + address, + { + displayName: 'Annual Revenue', + name: 'Annual_Revenue', + type: 'number', + default: '', + description: 'Annual revenue of the lead’s company.', + }, + { + displayName: 'Currency', + name: 'Currency', + type: 'options', + default: 'USD', + description: 'Symbol of the currency in which revenue is generated.', + options: currencies, + }, + makeCustomFieldsFixedCollection('lead'), + { + displayName: 'Description', + name: 'Description', + type: 'string', + default: '', + }, + { + displayName: 'Designation', + name: 'Designation', + type: 'string', + default: '', + description: 'Position of the lead at their company.', + }, + { + displayName: 'Email', + name: 'Email', + type: 'string', + default: '', + description: 'Email of the lead. If a record with this email exists it will be updated, otherwise a new one will be created.', + }, + { + displayName: 'Email Opt Out', + name: 'Email_Opt_Out', + type: 'boolean', + default: false, + }, + { + displayName: 'Fax', + name: 'Fax', + type: 'string', + default: '', + }, + { + displayName: 'First Name', + name: 'First_Name', + type: 'string', + default: '', + }, + { + displayName: 'Full Name', + name: 'Full_Name', + type: 'string', + default: '', + }, + { + displayName: 'Industry', + name: 'Industry', + type: 'string', + default: '', + description: 'Industry to which the lead belongs.', + }, + { + displayName: 'Industry Type', + name: 'Industry_Type', + type: 'string', + default: '', + description: 'Type of industry to which the lead belongs.', + }, + { + displayName: 'Lead Source', + name: 'Lead_Source', + type: 'string', + default: '', + description: 'Source from which the lead was created.', + }, + { + displayName: 'Lead Status', + name: 'Lead_Status', + type: 'string', + default: '', + }, + { + displayName: 'Mobile', + name: 'Mobile', + type: 'string', + default: '', + }, + { + displayName: 'Number of Employees', + name: 'No_of_Employees', + type: 'number', + default: '', + description: 'Number of employees in the lead’s company.', + }, + { + displayName: 'Phone', + name: 'Phone', + type: 'string', + default: '', + }, + { + displayName: 'Salutation', + name: 'Salutation', + type: 'string', + default: '', + }, + { + displayName: 'Secondary Email', + name: 'Secondary_Email', + type: 'string', + default: '', + }, + { + displayName: 'Skype ID', + name: 'Skype_ID', + type: 'string', + default: '', + }, + { + displayName: 'Twitter', + name: 'Twitter', + type: 'string', + default: '', + }, + { + displayName: 'Website', + name: 'Website', + type: 'string', + default: '', + }, + ], + }, + + // ---------------------------------------- + // lead: delete + // ---------------------------------------- + { + displayName: 'Lead ID', + name: 'leadId', + description: 'ID of the lead to delete.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------------- + // lead: get + // ---------------------------------------- + { + displayName: 'Lead ID', + name: 'leadId', + description: 'ID of the lead to retrieve.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------------- + // lead: getAll + // ---------------------------------------- + ...makeGetAllFields('lead'), + + // ---------------------------------------- + // lead: update + // ---------------------------------------- + { + displayName: 'Lead ID', + name: 'leadId', + description: 'ID of the lead to update.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + address, + { + displayName: 'Annual Revenue', + name: 'Annual_Revenue', + type: 'number', + default: '', + description: 'Annual revenue of the lead’s company.', + }, + { + displayName: 'Company', + name: 'Company', + type: 'string', + default: '', + description: 'Company at which the lead works.', + }, + { + displayName: 'Currency', + name: 'Currency', + type: 'options', + default: 'USD', + description: 'Symbol of the currency in which revenue is generated.', + options: currencies, + }, + makeCustomFieldsFixedCollection('lead'), + { + displayName: 'Description', + name: 'Description', + type: 'string', + default: '', + }, + { + displayName: 'Designation', + name: 'Designation', + type: 'string', + default: '', + description: 'Position of the lead at their company.', + }, + { + displayName: 'Email', + name: 'Email', + type: 'string', + default: '', + }, + { + displayName: 'Email Opt Out', + name: 'Email_Opt_Out', + type: 'boolean', + default: false, + }, + { + displayName: 'Fax', + name: 'Fax', + type: 'string', + default: '', + }, + { + displayName: 'First Name', + name: 'First_Name', + type: 'string', + default: '', + }, + { + displayName: 'Full Name', + name: 'Full_Name', + type: 'string', + default: '', + }, + { + displayName: 'Industry', + name: 'Industry', + type: 'string', + default: '', + description: 'Industry to which the lead belongs.', + }, + { + displayName: 'Industry Type', + name: 'Industry_Type', + type: 'string', + default: '', + description: 'Type of industry to which the lead belongs.', + }, + { + displayName: 'Last Name', + name: 'Last_Name', + type: 'string', + default: '', + }, + { + displayName: 'Lead Source', + name: 'Lead_Source', + type: 'string', + default: '', + description: 'Source from which the lead was created.', + }, + { + displayName: 'Lead Status', + name: 'Lead_Status', + type: 'string', + default: '', + }, + { + displayName: 'Mobile', + name: 'Mobile', + type: 'string', + default: '', + }, + { + displayName: 'Number of Employees', + name: 'No_of_Employees', + type: 'number', + default: '', + description: 'Number of employees in the lead’s company.', + }, + { + displayName: 'Phone', + name: 'Phone', + type: 'string', + default: '', + }, + { + displayName: 'Salutation', + name: 'Salutation', + type: 'string', + default: '', + }, + { + displayName: 'Secondary Email', + name: 'Secondary_Email', + type: 'string', + default: '', + }, + { + displayName: 'Skype ID', + name: 'Skype_ID', + type: 'string', + default: '', + }, + { + displayName: 'Twitter', + name: 'Twitter', + type: 'string', + default: '', + }, + { + displayName: 'Website', + name: 'Website', + type: 'string', + default: '', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zoho/descriptions/ProductDescription.ts b/packages/nodes-base/nodes/Zoho/descriptions/ProductDescription.ts new file mode 100644 index 0000000000..19b6f880c6 --- /dev/null +++ b/packages/nodes-base/nodes/Zoho/descriptions/ProductDescription.ts @@ -0,0 +1,352 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + makeCustomFieldsFixedCollection, + makeGetAllFields, +} from './SharedFields'; + +export const productOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'product', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a product', + }, + { + name: 'Create or Update', + value: 'upsert', + description: 'Create a new record, or update the current one if it already exists (upsert)', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a product', + }, + { + name: 'Get', + value: 'get', + description: 'Get a product', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all products', + }, + { + name: 'Update', + value: 'update', + description: 'Update a product', + }, + ], + default: 'create', + description: 'Operation to perform', + }, +] as INodeProperties[]; + +export const productFields = [ + // ---------------------------------------- + // product: create + // ---------------------------------------- + { + displayName: 'Product Name', + name: 'productName', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'product', + ], + operation: [ + 'create', + ], + }, + }, + }, + + // ---------------------------------------- + // product: upsert + // ---------------------------------------- + { + displayName: 'Product Name', + name: 'productName', + description: 'Name of the product. If a record with this product name exists it will be updated, otherwise a new one will be created.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'product', + ], + operation: [ + 'upsert', + ], + }, + }, + }, + + // ---------------------------------------- + // product: create + upsert + // ---------------------------------------- + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'product', + ], + operation: [ + 'create', + 'upsert', + ], + }, + }, + options: [ + { + displayName: 'Commission Rate', + name: 'Commission_Rate', + type: 'number', + description: 'Commission rate for the product. For example, enter 12 for 12%.', + typeOptions: { + minValue: 0, + }, + default: 0, + }, + makeCustomFieldsFixedCollection('product'), + { + displayName: 'Description', + name: 'Description', + type: 'string', + default: '', + }, + { + displayName: 'Manufacturer', + name: 'Manufacturer', + type: 'string', + default: '', + }, + { + displayName: 'Product Active', + name: 'Product_Active', + type: 'boolean', + default: false, + }, + { + displayName: 'Product Category', + name: 'Product_Category', + type: 'string', + default: '', + }, + { + displayName: 'Quantity in Demand', + name: 'Qty_in_Demand', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + }, + { + displayName: 'Quantity in Stock', + name: 'Qty_in_Stock', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + }, + { + displayName: 'Taxable', + name: 'Taxable', + type: 'boolean', + default: false, + }, + { + displayName: 'Unit Price', + name: 'Unit_Price', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + }, + ], + }, + + // ---------------------------------------- + // product: delete + // ---------------------------------------- + { + displayName: 'Product ID', + name: 'productId', + description: 'ID of the product to delete.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'product', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------------- + // product: get + // ---------------------------------------- + { + displayName: 'Product ID', + name: 'productId', + description: 'ID of the product to retrieve.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'product', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------------- + // product: getAll + // ---------------------------------------- + ...makeGetAllFields('product'), + + // ---------------------------------------- + // product: update + // ---------------------------------------- + { + displayName: 'Product ID', + name: 'productId', + description: 'ID of the product to update.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'product', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'product', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Commission Rate', + name: 'Commission_Rate', + type: 'number', + description: 'Commission rate for the product. For example, enter 12 for 12%.', + typeOptions: { + minValue: 0, + }, + default: 0, + }, + makeCustomFieldsFixedCollection('product'), + { + displayName: 'Description', + name: 'Description', + type: 'string', + default: '', + }, + { + displayName: 'Manufacturer', + name: 'Manufacturer', + type: 'string', + default: '', + }, + { + displayName: 'Product Active', + name: 'Product_Active', + type: 'boolean', + default: false, + }, + { + displayName: 'Product Category', + name: 'Product_Category', + type: 'string', + default: '', + }, + { + displayName: 'Quantity in Demand', + name: 'Qty_in_Demand', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + }, + { + displayName: 'Quantity in Stock', + name: 'Qty_in_Stock', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + }, + { + displayName: 'Taxable', + name: 'Taxable', + type: 'boolean', + default: false, + }, + { + displayName: 'Unit Price', + name: 'Unit_Price', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zoho/descriptions/PurchaseOrderDescription.ts b/packages/nodes-base/nodes/Zoho/descriptions/PurchaseOrderDescription.ts new file mode 100644 index 0000000000..2ef0b573f8 --- /dev/null +++ b/packages/nodes-base/nodes/Zoho/descriptions/PurchaseOrderDescription.ts @@ -0,0 +1,590 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + billingAddress, + currencies, + makeCustomFieldsFixedCollection, + makeGetAllFields, + productDetailsOptions, + shippingAddress, +} from './SharedFields'; + +export const purchaseOrderOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'purchaseOrder', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a purchase order', + }, + { + name: 'Create or Update', + value: 'upsert', + description: 'Create a new record, or update the current one if it already exists (upsert)', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a purchase order', + }, + { + name: 'Get', + value: 'get', + description: 'Get a purchase order', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all purchase orders', + }, + { + name: 'Update', + value: 'update', + description: 'Update a purchase order', + }, + ], + default: 'create', + description: 'Operation to perform', + }, +] as INodeProperties[]; + +export const purchaseOrderFields = [ + // ---------------------------------------- + // purchaseOrder: create + // ---------------------------------------- + { + displayName: 'Subject', + name: 'subject', + description: 'Subject or title of the purchase order.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'purchaseOrder', + ], + operation: [ + 'create', + ], + }, + }, + }, + + // ---------------------------------------- + // purchaseOrder: upsert + // ---------------------------------------- + { + displayName: 'Subject', + name: 'subject', + description: 'Subject or title of the purchase order. If a record with this subject exists it will be updated, otherwise a new one will be created.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'purchaseOrder', + ], + operation: [ + 'upsert', + ], + }, + }, + }, + + // ---------------------------------------- + // purchaseOrder: create + upsert + // ---------------------------------------- + { + displayName: 'Vendor ID', + name: 'vendorId', + type: 'options', + default: [], + typeOptions: { + loadOptionsMethod: 'getVendors', + }, + description: 'ID of the vendor associated with the purchase order.', + displayOptions: { + show: { + resource: [ + 'purchaseOrder', + ], + operation: [ + 'create', + 'upsert', + ], + }, + }, + }, + { + displayName: 'Products', + name: 'Product_Details', + type: 'collection', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Product', + }, + default: {}, + placeholder: 'Add Field', + options: productDetailsOptions, + displayOptions: { + show: { + resource: [ + 'purchaseOrder', + ], + operation: [ + 'create', + 'upsert', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'purchaseOrder', + ], + operation: [ + 'create', + 'upsert', + ], + }, + }, + options: [ + { + displayName: 'Adjustment', + name: 'Adjustment', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Adjustment in the grand total, if any.', + }, + { + displayName: 'Billing Address', + name: 'Billing_Address', + type: 'fixedCollection', + default: {}, + placeholder: 'Add Billing Address Field', + options: [ + { + displayName: 'Billing Address Fields', + name: 'billing_address_fields', + values: [ + { + displayName: 'Billing City', + name: 'Billing_City', + type: 'string', + default: '', + }, + { + displayName: 'Billing Code', + name: 'Billing_Code', + type: 'string', + default: '', + }, + { + displayName: 'Billing Country', + name: 'Billing_Country', + type: 'string', + default: '', + }, + { + displayName: 'Billing State', + name: 'Billing_State', + type: 'string', + default: '', + }, + { + displayName: 'Billing Street', + name: 'Billing_Street', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Carrier', + name: 'Carrier', + type: 'string', + default: '', + description: 'Name of the carrier.', + }, + { + displayName: 'Currency', + name: 'Currency', + type: 'options', + default: 'USD', + description: 'Symbol of the currency in which revenue is generated.', + options: currencies, + }, + makeCustomFieldsFixedCollection('purchaseOrder'), + { + displayName: 'Description', + name: 'Description', + type: 'string', + default: '', + }, + { + displayName: 'Discount', + name: 'Discount', + type: 'number', + description: 'Discount applied to the purchase order. For example, enter 12 for 12%.', + default: 0, + typeOptions: { + minValue: 0, + }, + }, + { + displayName: 'Due Date', + name: 'Due_Date', + type: 'dateTime', + default: '', + }, + { + displayName: 'Exchange Rate', + name: 'Exchange_Rate', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Exchange rate of the default currency to the home currency.', + }, + { + displayName: 'Grand Total', + name: 'Grand_Total', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Total amount for the product after deducting tax and discounts.', + }, + { + displayName: 'PO Date', + name: 'PO_Date', + type: 'dateTime', + default: '', + description: 'Date on which the purchase order was issued.', + }, + { + displayName: 'PO Number', + name: 'PO_Number', + type: 'string', + default: '', + description: 'ID of the purchase order after creating a case.', + }, + { + displayName: 'Sales Commission', + name: 'Sales_Commission', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Commission of sales person on deal closure as a percentage. For example, enter 12 for 12%.', + }, + shippingAddress, + { + displayName: 'Status', + name: 'Status', + type: 'options', + default: [], + typeOptions: { + loadOptionsMethod: 'getPurchaseOrderStatus', + }, + description: 'Status of the purchase order.', + }, + { + displayName: 'Sub Total', + name: 'Sub_Total', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Total amount for the product excluding tax.', + }, + { + displayName: 'Tax', + name: 'Tax', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Tax amount as the sum of sales tax and value-added tax.', + }, + { + displayName: 'Terms and Conditions', + name: 'Terms_and_Conditions', + type: 'string', + default: '', + description: 'Terms and conditions associated with the purchase order.', + }, + { + displayName: 'Tracking Number', + name: 'Tracking_Number', + type: 'string', + default: '', + }, + ], + }, + + // ---------------------------------------- + // purchaseOrder: delete + // ---------------------------------------- + { + displayName: 'Purchase Order ID', + name: 'purchaseOrderId', + description: 'ID of the purchase order to delete.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'purchaseOrder', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------------- + // purchaseOrder: get + // ---------------------------------------- + { + displayName: 'Purchase Order ID', + name: 'purchaseOrderId', + description: 'ID of the purchase order to retrieve.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'purchaseOrder', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------------- + // purchaseOrder: getAll + // ---------------------------------------- + ...makeGetAllFields('purchaseOrder'), + + // ---------------------------------------- + // purchaseOrder: update + // ---------------------------------------- + { + displayName: 'Purchase Order ID', + name: 'purchaseOrderId', + description: 'ID of the purchase order to update.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'purchaseOrder', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'purchaseOrder', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Adjustment', + name: 'Adjustment', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Adjustment in the grand total, if any.', + }, + billingAddress, + { + displayName: 'Carrier', + name: 'Carrier', + type: 'string', + default: '', + description: 'Name of the carrier.', + }, + { + displayName: 'Currency', + name: 'Currency', + type: 'options', + default: 'USD', + description: 'Symbol of the currency in which revenue is generated.', + options: currencies, + }, + makeCustomFieldsFixedCollection('purchaseOrder'), + { + displayName: 'Description', + name: 'Description', + type: 'string', + default: '', + }, + { + displayName: 'Discount', + name: 'Discount', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + }, + { + displayName: 'Due Date', + name: 'Due_Date', + type: 'dateTime', + default: '', + }, + { + displayName: 'Exchange Rate', + name: 'Exchange_Rate', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Exchange rate of the default currency to the home currency.', + }, + { + displayName: 'Grand Total', + name: 'Grand_Total', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Total amount for the product after deducting tax and discounts.', + }, + { + displayName: 'PO Date', + name: 'PO_Date', + type: 'dateTime', + default: '', + description: 'Date on which the purchase order was issued.', + }, + { + displayName: 'PO Number', + name: 'PO_Number', + type: 'string', + default: '', + description: 'ID of the purchase order after creating a case.', + }, + // productDetails('purchaseOrder', 'update'), + { + displayName: 'Sales Commission', + name: 'Sales_Commission', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Commission of sales person on deal closure as a percentage. For example, enter 12 for 12%.', + }, + shippingAddress, + { + displayName: 'Status', + name: 'Status', + type: 'options', + default: [], + typeOptions: { + loadOptionsMethod: 'getPurchaseOrderStatus', + }, + description: 'Status of the purchase order.', + }, + { + displayName: 'Sub Total', + name: 'Sub_Total', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Total amount for the product excluding tax.', + }, + { + displayName: 'Subject', + name: 'Subject', + type: 'string', + default: '', + description: 'Subject or title of the purchase order.', + }, + { + displayName: 'Tax', + name: 'Tax', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Tax amount as the sum of sales tax and value-added tax.', + }, + { + displayName: 'Terms and Conditions', + name: 'Terms_and_Conditions', + type: 'string', + default: '', + description: 'Terms and conditions associated with the purchase order.', + }, + { + displayName: 'Tracking Number', + name: 'Tracking_Number', + type: 'string', + default: '', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zoho/descriptions/QuoteDescription.ts b/packages/nodes-base/nodes/Zoho/descriptions/QuoteDescription.ts new file mode 100644 index 0000000000..0ecf64adaf --- /dev/null +++ b/packages/nodes-base/nodes/Zoho/descriptions/QuoteDescription.ts @@ -0,0 +1,459 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + billingAddress, + currencies, + makeCustomFieldsFixedCollection, + makeGetAllFields, + productDetailsOptions, + shippingAddress, +} from './SharedFields'; + +export const quoteOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'quote', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a quote', + }, + { + name: 'Create or Update', + value: 'upsert', + description: 'Create a new record, or update the current one if it already exists (upsert)', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a quote', + }, + { + name: 'Get', + value: 'get', + description: 'Get a quote', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all quotes', + }, + { + name: 'Update', + value: 'update', + description: 'Update a quote', + }, + ], + default: 'create', + description: 'Operation to perform', + }, +] as INodeProperties[]; + +export const quoteFields = [ + // ---------------------------------------- + // quote: create + // ---------------------------------------- + { + displayName: 'Subject', + name: 'subject', + description: 'Subject or title of the quote.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'quote', + ], + operation: [ + 'create', + ], + }, + }, + }, + + // ---------------------------------------- + // quote: upsert + // ---------------------------------------- + { + displayName: 'Subject', + name: 'subject', + description: 'Subject or title of the quote. If a record with this subject exists it will be updated, otherwise a new one will be created.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'quote', + ], + operation: [ + 'upsert', + ], + }, + }, + }, + + // ---------------------------------------- + // quote: create + upsert + // ---------------------------------------- + { + displayName: 'Products', + name: 'Product_Details', + type: 'collection', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Product', + }, + default: {}, + placeholder: 'Add Field', + options: productDetailsOptions, + displayOptions: { + show: { + resource: [ + 'quote', + ], + operation: [ + 'create', + 'upsert', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'quote', + ], + operation: [ + 'create', + 'upsert', + ], + }, + }, + options: [ + { + displayName: 'Adjustment', + name: 'Adjustment', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Adjustment in the grand total, if any.', + }, + billingAddress, + { + displayName: 'Carrier', + name: 'Carrier', + type: 'string', + default: '', + }, + { + displayName: 'Currency', + name: 'Currency', + type: 'options', + default: 'USD', + description: 'Symbol of the currency in which revenue is generated.', + options: currencies, + }, + makeCustomFieldsFixedCollection('quote'), + { + displayName: 'Description', + name: 'Description', + type: 'string', + default: '', + }, + { + displayName: 'Exchange Rate', + name: 'Exchange_Rate', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Exchange rate of the default currency to the home currency.', + }, + { + displayName: 'Grand Total', + name: 'Grand_Total', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Total amount for the product after deducting tax and discounts.', + }, + { + displayName: 'Quote Stage', + name: 'Quote_Stage', + type: 'options', + default: [], + typeOptions: { + loadOptionsMethod: 'getQuoteStage', + }, + description: 'Stage of the quote.', + }, + shippingAddress, + { + displayName: 'Sub Total', + name: 'Sub_Total', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Total amount for the product excluding tax.', + }, + { + displayName: 'Tax', + name: 'Tax', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Total amount as the sum of sales tax and value-added tax.', + }, + { + displayName: 'Team', + name: 'Team', + type: 'string', + default: '', + description: 'Team for whom the quote is created.', + }, + { + displayName: 'Terms and Conditions', + name: 'Terms_and_Conditions', + type: 'string', + default: '', + description: 'Terms and conditions associated with the quote.', + }, + { + displayName: 'Valid Till', + name: 'Valid_Till', + type: 'dateTime', + default: '', + description: 'Date until when the quote is valid.', + }, + ], + }, + + // ---------------------------------------- + // quote: delete + // ---------------------------------------- + { + displayName: 'Quote ID', + name: 'quoteId', + description: 'ID of the quote to delete.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'quote', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------------- + // quote: get + // ---------------------------------------- + { + displayName: 'Quote ID', + name: 'quoteId', + description: 'ID of the quote to retrieve.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'quote', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------------- + // quote: getAll + // ---------------------------------------- + ...makeGetAllFields('quote'), + + // ---------------------------------------- + // quote: update + // ---------------------------------------- + { + displayName: 'Quote ID', + name: 'quoteId', + description: 'ID of the quote to update.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'quote', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'quote', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Adjustment', + name: 'Adjustment', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Adjustment in the grand total, if any.', + }, + billingAddress, + { + displayName: 'Carrier', + name: 'Carrier', + type: 'string', + default: '', + }, + { + displayName: 'Currency', + name: 'Currency', + type: 'options', + default: 'USD', + description: 'Symbol of the currency in which revenue is generated.', + options: currencies, + }, + makeCustomFieldsFixedCollection('quote'), + { + displayName: 'Description', + name: 'Description', + type: 'string', + default: '', + }, + { + displayName: 'Exchange Rate', + name: 'Exchange_Rate', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Exchange rate of the default currency to the home currency.', + }, + { + displayName: 'Grand Total', + name: 'Grand_Total', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Total amount for the product after deducting tax and discounts.', + }, + { + displayName: 'Quote Stage', + name: 'Quote_Stage', + type: 'options', + default: [], + typeOptions: { + loadOptionsMethod: 'getQuoteStage', + }, + description: 'Stage of the quote.', + }, + shippingAddress, + { + displayName: 'Sub Total', + name: 'Sub_Total', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Total amount for the product excluding tax.', + }, + { + displayName: 'Subject', + name: 'Subject', + type: 'string', + default: '', + description: 'Subject or title of the quote.', + }, + { + displayName: 'Tax', + name: 'Tax', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Tax amount as the sum of sales tax and value-added tax.', + }, + { + displayName: 'Team', + name: 'Team', + type: 'string', + default: '', + description: 'Team for whom the quote is created.', + }, + { + displayName: 'Terms and Conditions', + name: 'Terms_and_Conditions', + type: 'string', + default: '', + description: 'Terms and conditions associated with the quote.', + }, + { + displayName: 'Valid Till', + name: 'Valid_Till', + type: 'dateTime', + default: '', + description: 'Date until when the quote is valid.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zoho/descriptions/SalesOrderDescription.ts b/packages/nodes-base/nodes/Zoho/descriptions/SalesOrderDescription.ts new file mode 100644 index 0000000000..35547664cf --- /dev/null +++ b/packages/nodes-base/nodes/Zoho/descriptions/SalesOrderDescription.ts @@ -0,0 +1,569 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + billingAddress, + currencies, + makeCustomFieldsFixedCollection, + makeGetAllFields, + productDetailsOptions, + shippingAddress, +} from './SharedFields'; + +export const salesOrderOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'salesOrder', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a sales order', + }, + { + name: 'Create or Update', + value: 'upsert', + description: 'Create a new record, or update the current one if it already exists (upsert)', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a sales order', + }, + { + name: 'Get', + value: 'get', + description: 'Get a sales order', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all sales orders', + }, + { + name: 'Update', + value: 'update', + description: 'Update a sales order', + }, + ], + default: 'create', + description: 'Operation to perform', + }, +] as INodeProperties[]; + +export const salesOrderFields = [ + // ---------------------------------------- + // salesOrder: create + upsert + // ---------------------------------------- + { + displayName: 'Account ID', + name: 'accountId', + required: true, + type: 'options', + default: [], + typeOptions: { + loadOptionsMethod: 'getAccounts', + }, + displayOptions: { + show: { + resource: [ + 'salesOrder', + ], + operation: [ + 'create', + 'upsert', + ], + }, + }, + }, + + // ---------------------------------------- + // salesOrder: create + // ---------------------------------------- + { + displayName: 'Subject', + name: 'subject', + description: 'Subject or title of the sales order.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'salesOrder', + ], + operation: [ + 'create', + ], + }, + }, + }, + + // ---------------------------------------- + // salesOrder: upsert + // ---------------------------------------- + { + displayName: 'Subject', + name: 'subject', + description: 'Subject or title of the sales order. If a record with this subject exists it will be updated, otherwise a new one will be created.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'salesOrder', + ], + operation: [ + 'upsert', + ], + }, + }, + }, + + // ---------------------------------------- + // salesOrder: create + upsert + // ---------------------------------------- + { + displayName: 'Products', + name: 'Product_Details', + type: 'collection', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Product', + }, + default: {}, + placeholder: 'Add Field', + options: productDetailsOptions, + displayOptions: { + show: { + resource: [ + 'salesOrder', + ], + operation: [ + 'create', + 'upsert', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'salesOrder', + ], + operation: [ + 'create', + 'upsert', + ], + }, + }, + options: [ + { + displayName: 'Adjustment', + name: 'Adjustment', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Adjustment in the grand total, if any.', + }, + billingAddress, + { + displayName: 'Carrier', + name: 'Carrier', + type: 'string', + default: '', + description: 'Name of the carrier.', + }, + { + displayName: 'Contact ID', + name: 'contactId', + type: 'options', + default: [], + typeOptions: { + loadOptionsMethod: 'getContacts', + }, + }, + { + displayName: 'Currency', + name: 'Currency', + type: 'options', + default: 'USD', + description: 'Symbol of the currency in which revenue is generated.', + options: currencies, + }, + makeCustomFieldsFixedCollection('salesOrder'), + { + displayName: 'Deal ID', + name: 'dealId', + type: 'options', + default: [], + typeOptions: { + loadOptionsMethod: 'getDeals', + }, + }, + { + displayName: 'Description', + name: 'Description', + type: 'string', + default: '', + }, + { + displayName: 'Discount', + name: 'Discount', + type: 'number', + description: 'Discount applied to the sales order. For example, enter 12 for 12%.', + default: 0, + typeOptions: { + minValue: 0, + }, + }, + { + displayName: 'Due Date', + name: 'Due_Date', + type: 'dateTime', + default: '', + }, + { + displayName: 'Exchange Rate', + name: 'Exchange_Rate', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Exchange rate of the default currency to the home currency.', + }, + { + displayName: 'Grand Total', + name: 'Grand_Total', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Total amount for the product after deducting tax and discounts.', + }, + { + displayName: 'Sales Order Number', + name: 'SO_Number', + type: 'string', + default: '', + description: 'ID of the sales order after creating a case.', + }, + { + displayName: 'Sales Commission', + name: 'Sales_Commission', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Commission of sales person on deal closure as a percentage. For example, enter 12 for 12%.', + }, + shippingAddress, + { + displayName: 'Status', + name: 'Status', + type: 'options', + default: [], + typeOptions: { + loadOptionsMethod: 'getSalesOrderStatus', + }, + description: 'Status of the sales order.', + }, + { + displayName: 'Sub Total', + name: 'Sub_Total', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Total amount for the product excluding tax.', + }, + { + displayName: 'Tax', + name: 'Tax', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Tax amount as the sum of sales tax and value-added tax.', + }, + { + displayName: 'Terms and Conditions', + name: 'Terms_and_Conditions', + type: 'string', + default: '', + description: 'Terms and conditions associated with the purchase order.', + }, + ], + }, + + // ---------------------------------------- + // salesOrder: delete + // ---------------------------------------- + { + displayName: 'Sales Order ID', + name: 'salesOrderId', + description: 'ID of the sales order to delete.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'salesOrder', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------------- + // salesOrder: get + // ---------------------------------------- + { + displayName: 'Sales Order ID', + name: 'salesOrderId', + description: 'ID of the sales order to retrieve.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'salesOrder', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------------- + // salesOrder: getAll + // ---------------------------------------- + ...makeGetAllFields('salesOrder'), + + // ---------------------------------------- + // salesOrder: update + // ---------------------------------------- + { + displayName: 'Sales Order ID', + name: 'salesOrderId', + description: 'ID of the sales order to update.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'salesOrder', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'salesOrder', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Account ID', + name: 'accountId', + type: 'options', + default: [], + typeOptions: { + loadOptionsMethod: 'getAccounts', + }, + description: 'ID of the account associated with this invoice.', + }, + { + displayName: 'Adjustment', + name: 'Adjustment', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Adjustment in the grand total, if any.', + }, + billingAddress, + { + displayName: 'Carrier', + name: 'Carrier', + type: 'string', + default: '', + description: 'Name of the carrier.', + }, + { + displayName: 'Contact ID', + name: 'contactId', + type: 'options', + default: [], + typeOptions: { + loadOptionsMethod: 'getContacts', + }, + }, + { + displayName: 'Currency', + name: 'Currency', + type: 'options', + default: 'USD', + description: 'Symbol of the currency in which revenue is generated.', + options: currencies, + }, + makeCustomFieldsFixedCollection('salesOrder'), + { + displayName: 'Deal ID', + name: 'dealId', + type: 'options', + default: [], + typeOptions: { + loadOptionsMethod: 'getDeals', + }, + }, + { + displayName: 'Description', + name: 'Description', + type: 'string', + default: '', + }, + { + displayName: 'Discount', + name: 'Discount', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + }, + { + displayName: 'Due Date', + name: 'Due_Date', + type: 'dateTime', + default: '', + }, + { + displayName: 'Exchange Rate', + name: 'Exchange_Rate', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Exchange rate of the default currency to the home currency.', + }, + { + displayName: 'Grand Total', + name: 'Grand_Total', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Total amount for the product after deducting tax and discounts.', + }, + { + displayName: 'Sales Order Number', + name: 'SO_Number', + type: 'string', + default: '', + description: 'ID of the sales order after creating a case.', + }, + { + displayName: 'Sales Commission', + name: 'Sales_Commission', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Commission of sales person on deal closure as a percentage. For example, enter 12 for 12%.', + }, + shippingAddress, + { + displayName: 'Status', + name: 'Status', + type: 'options', + default: [], + typeOptions: { + loadOptionsMethod: 'getSalesOrderStatus', + }, + description: 'Status of the sales order.', + }, + { + displayName: 'Sub Total', + name: 'Sub_Total', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Total amount for the product excluding tax.', + }, + { + displayName: 'Subject', + name: 'Subject', + type: 'string', + default: '', + description: 'Subject or title of the sales order.', + }, + { + displayName: 'Tax', + name: 'Tax', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Tax amount as the sum of sales tax and value-added tax.', + }, + { + displayName: 'Terms and Conditions', + name: 'Terms_and_Conditions', + type: 'string', + default: '', + description: 'Terms and conditions associated with the purchase order.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zoho/descriptions/SharedFields.ts b/packages/nodes-base/nodes/Zoho/descriptions/SharedFields.ts new file mode 100644 index 0000000000..e7030ba28f --- /dev/null +++ b/packages/nodes-base/nodes/Zoho/descriptions/SharedFields.ts @@ -0,0 +1,577 @@ +import { capitalizeInitial } from '../GenericFunctions'; +import { CamelCaseResource } from '../types'; + +export const billingAddress = { + displayName: 'Billing Address', + name: 'Billing_Address', + type: 'fixedCollection', + default: {}, + placeholder: 'Add Billing Address Field', + options: [ + { + displayName: 'Billing Address Fields', + name: 'address_fields', + values: [ + { + displayName: 'Street', + name: 'Billing_Street', + type: 'string', + default: '', + }, + { + displayName: 'City', + name: 'Billing_City', + type: 'string', + default: '', + }, + { + displayName: 'State', + name: 'Billing_State', + type: 'string', + default: '', + }, + { + displayName: 'Country', + name: 'Billing_Country', + type: 'string', + default: '', + }, + { + displayName: 'Zip Code', + name: 'Billing_Code', + type: 'string', + default: '', + }, + ], + }, + ], +}; + +export const shippingAddress = { + displayName: 'Shipping Address', + name: 'Shipping_Address', + type: 'fixedCollection', + default: {}, + placeholder: 'Add Shipping Address Field', + options: [ + { + displayName: 'Shipping Address Fields', + name: 'address_fields', + values: [ + { + displayName: 'Street', + name: 'Shipping_Street', + type: 'string', + default: '', + }, + { + displayName: 'City', + name: 'Shipping_City', + type: 'string', + default: '', + }, + { + displayName: 'State', + name: 'Shipping_State', + type: 'string', + default: '', + }, + { + displayName: 'Country', + name: 'Shipping_Country', + type: 'string', + default: '', + }, + { + displayName: 'Zip Code', + name: 'Shipping_Code', + type: 'string', + default: '', + }, + ], + }, + ], +}; + +export const mailingAddress = { + displayName: 'Mailing Address', + name: 'Mailing_Address', + type: 'fixedCollection', + default: {}, + placeholder: 'Add Mailing Address Field', + options: [ + { + displayName: 'Mailing Address Fields', + name: 'address_fields', + values: [ + { + displayName: 'Street', + name: 'Mailing_Street', + type: 'string', + default: '', + }, + { + displayName: 'City', + name: 'Mailing_City', + type: 'string', + default: '', + }, + { + displayName: 'State', + name: 'Mailing_State', + type: 'string', + default: '', + }, + { + displayName: 'Country', + name: 'Mailing_Country', + type: 'string', + default: '', + }, + { + displayName: 'Zip Code', + name: 'Mailing_Zip', + type: 'string', + default: '', + }, + ], + }, + ], +}; + +export const otherAddress = { + displayName: 'Other Address', + name: 'Other_Address', + type: 'fixedCollection', + default: {}, + placeholder: 'Add Other Address Field', + options: [ + { + displayName: 'Other Address Fields', + name: 'address_fields', + values: [ + { + displayName: 'Street', + name: 'Other_Street', + type: 'string', + default: '', + }, + { + displayName: 'City', + name: 'Other_City', + type: 'string', + default: '', + }, + { + displayName: 'State', + name: 'Other_State', + type: 'string', + default: '', + }, + { + displayName: 'Zip Code', + name: 'Other_Zip', + type: 'string', + default: '', + }, + ], + }, + ], +}; + +export const address = { + displayName: 'Address', + name: 'Address', + type: 'fixedCollection', + default: {}, + placeholder: 'Add Address Field', + options: [ + { + displayName: 'Address Fields', + name: 'address_fields', + values: [ + { + displayName: 'Street', + name: 'Street', + type: 'string', + default: '', + }, + { + displayName: 'City', + name: 'City', + type: 'string', + default: '', + }, + { + displayName: 'State', + name: 'State', + type: 'string', + default: '', + }, + { + displayName: 'Country', + name: 'Country', + type: 'string', + default: '', + }, + { + displayName: 'Zip Code', + name: 'Zip_Code', + type: 'string', + default: '', + }, + ], + }, + ], +}; + +// displayName: 'Products', +// name: 'Product_Details', +// type: 'collection', +// typeOptions: { +// multipleValues: true, +// multipleValueButtonText: 'Add Product', +// }, +// default: {}, +// placeholder: 'Add Field', +// displayOptions: { +// show: { +// resource: [ +// resource, +// ], +// operation: [ +// operation, +// ], +// }, +// }, + +export const productDetailsOptions = [ + { + displayName: 'List Price', + name: 'list_price', + type: 'number', + default: '', + }, + { + displayName: 'Product ID', + name: 'id', + type: 'options', + default: [], + typeOptions: { + loadOptionsMethod: 'getProducts', + }, + }, + { + displayName: 'Product Description', + name: 'product_description', + type: 'string', + default: '', + }, + { + displayName: 'Quantity', + name: 'quantity', + type: 'number', + default: 1, + }, + { + displayName: 'Quantity in Stock', + name: 'quantity_in_stock', + type: 'number', + default: 0, + }, + { + displayName: 'Tax', + name: 'Tax', + type: 'number', + default: 0, + }, + { + displayName: 'Total', + name: 'total', + type: 'number', + default: 0, + }, + { + displayName: 'Total After Discount', + name: 'total_after_discount', + type: 'number', + default: 0, + }, + { + displayName: 'Total (Net)', + name: 'net_total', + type: 'number', + default: 0, + }, + { + displayName: 'Unit Price', + name: 'unit_price', + type: 'number', + default: 0, + }, +]; + +export const makeGetAllFields = (resource: CamelCaseResource) => { + const loadOptionsMethod = `get${capitalizeInitial(resource)}Fields`; + + return [ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + resource, + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 5, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + displayOptions: { + show: { + resource: [ + resource, + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + resource, + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Approved', + name: 'approved', + type: 'boolean', + default: true, + description: 'Retrieve only approved records. Defaults to true.', + }, + { + displayName: 'Converted', + name: 'converted', + type: 'boolean', + default: false, + description: 'Retrieve only converted records. Defaults to false.', + }, + { + displayName: 'Fields', + name: 'fields', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod, + }, + default: [], + description: 'Return only these fields.', + }, + { + displayName: 'Include Child', + name: 'include_child', + type: 'boolean', + default: false, + description: 'Retrieve only records from child territories.', + }, + { + displayName: 'Sort By', + name: 'sort_by', + type: 'options', + typeOptions: { + loadOptionsMethod, + }, + default: [], + description: 'Field to sort records by.', + }, + { + displayName: 'Sort Order', + name: 'sort_order', + type: 'options', + options: [ + { + name: 'Ascending', + value: 'asc', + }, + { + name: 'Descending', + value: 'desc', + }, + ], + default: 'desc', + description: 'Ascending or descending order sort order.', + }, + { + displayName: 'Territory ID', + name: 'territory_id', + type: 'string', + default: '', + description: 'Retrieve only records from this territory.', + }, + ], + }, + ]; +}; + +export const makeCustomFieldsFixedCollection = (resource: CamelCaseResource) => { + const loadOptionsMethod = `getCustom${capitalizeInitial(resource)}Fields`; + + return { + displayName: 'Custom Fields', + name: 'customFields', + placeholder: 'Add Custom Field', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + description: 'Filter by custom fields.', + default: {}, + options: [ + { + name: 'customFields', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field ID', + name: 'fieldId', + type: 'options', + typeOptions: { + loadOptionsMethod, + }, + default: '', + description: 'Custom field to set a value to.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value to set on custom field.', + }, + ], + }, + ], + }; +}; + +// https://www.zoho.com/subscriptions/help/supported-currencies.html + +export const currencies = [ + { name: 'US Dollar', value: 'USD' }, + { name: 'Euro', value: 'EUR' }, + { name: 'UAE Dirham', value: 'AED' }, + { name: 'Afghani', value: 'AFN' }, + { name: 'Lek', value: 'ALL' }, + { name: 'Argentine Peso', value: 'ARS' }, + { name: 'Australian Dollar', value: 'AUD' }, + { name: 'Azerbaijan Manat', value: 'AZN' }, + { name: 'Barbados Dollar', value: 'BBD' }, + { name: 'Taka', value: 'BDT' }, + { name: 'Bulgarian Lev', value: 'BGN' }, + { name: 'Bermudian Dollar', value: 'BMD' }, + { name: 'Brunei Dollar', value: 'BND' }, + { name: 'Boliviano', value: 'BOB' }, + { name: 'Brazilian Real', value: 'BRL' }, + { name: 'Bahamian Dollar', value: 'BSD' }, + { name: 'Pula', value: 'BWP' }, + { name: 'Belize Dollar', value: 'BZD' }, + { name: 'Canadian Dollar', value: 'CAD' }, + { name: 'Swiss Franc', value: 'CHF' }, + { name: 'Chilean Peso', value: 'CLP' }, + { name: 'Yuan Renminbi', value: 'CNY' }, + { name: 'Colombian Peso', value: 'COP' }, + { name: 'Costa Rican Colon', value: 'CRC' }, + { name: 'Czech Koruna', value: 'CZK' }, + { name: 'Danish Krone', value: 'DKK' }, + { name: 'Dominican Peso', value: 'DOP' }, + { name: 'Algerian Dinar', value: 'DZD' }, + { name: 'Egyptian Pound', value: 'EGP' }, + { name: 'Fiji Dollar', value: 'FJD' }, + { name: 'Pound Sterling', value: 'GBP' }, + { name: 'Quetzal', value: 'GTQ' }, + { name: 'Hong Kong Dollar', value: 'HKD' }, + { name: 'Lempira', value: 'HNL' }, + { name: 'Kuna', value: 'HRK' }, + { name: 'Forint', value: 'HUF' }, + { name: 'Rupiah', value: 'IDR' }, + { name: 'New Israeli Sheqel', value: 'ILS' }, + { name: 'Indian Rupee', value: 'INR' }, + { name: 'Jamaican Dollar', value: 'JMD' }, + { name: 'Yen', value: 'JPY' }, + { name: 'Kenyan Shilling', value: 'KES' }, + { name: 'Won', value: 'KRW' }, + { name: 'Tenge', value: 'KZT' }, + { name: 'Lao Kip', value: 'LAK' }, + { name: 'Lebanese Pound', value: 'LBP' }, + { name: 'Sri Lanka Rupee', value: 'LKR' }, + { name: 'Liberian Dollar', value: 'LRD' }, + { name: 'Moroccan Dirham', value: 'MAD' }, + { name: 'Kyat', value: 'MMK' }, + { name: 'Pataca', value: 'MOP' }, + { name: 'Ouguiya', value: 'MRO' }, + { name: 'Mauritius Rupee', value: 'MUR' }, + { name: 'Rufiyaa', value: 'MVR' }, + { name: 'Mexican Peso', value: 'MXN' }, + { name: 'Malaysian Ringgit', value: 'MYR' }, + { name: 'Cordoba Oro', value: 'NIO' }, + { name: 'Norwegian Krone', value: 'NOK' }, + { name: 'Nepalese Rupee', value: 'NPR' }, + { name: 'New Zealand Dollar', value: 'NZD' }, + { name: 'Sol', value: 'PEN' }, + { name: 'Kina', value: 'PGK' }, + { name: 'Philippine Peso', value: 'PHP' }, + { name: 'Pakistan Rupee', value: 'PKR' }, + { name: 'Zloty', value: 'PLN' }, + { name: 'Qatari Rial', value: 'QAR' }, + { name: 'Romanian Leu', value: 'RON' }, + { name: 'Russian Ruble', value: 'RUB' }, + { name: 'Saudi Riyal', value: 'SAR' }, + { name: 'Solomon Islands Dollar ', value: 'SBD' }, + { name: 'Seychelles Rupee', value: 'SCR' }, + { name: 'Swedish Krona', value: 'SEK' }, + { name: 'Singapore Dollar', value: 'SGD' }, + { name: 'Syrian Pound', value: 'SYP' }, + { name: 'Baht', value: 'THB' }, + { name: 'Pa’anga', value: 'TOP' }, + { name: 'Turkish Lira', value: 'TRY' }, + { name: 'Trinidad and Tobago Dollar', value: 'TTD' }, + { name: 'New Taiwan Dollar', value: 'TWD' }, + { name: 'Hryvnia', value: 'UAH' }, + { name: 'Dong', value: 'VND' }, + { name: 'Vatu', value: 'VUV' }, + { name: 'Tala', value: 'WST' }, + { name: 'East Caribbean Dollar', value: 'XCD' }, + { name: 'West African CFA Franc', value: 'XOF' }, + { name: 'Yemeni Rial', value: 'YER' }, + { name: 'Rand', value: 'ZAR' }, +]; diff --git a/packages/nodes-base/nodes/Zoho/descriptions/VendorDescription.ts b/packages/nodes-base/nodes/Zoho/descriptions/VendorDescription.ts new file mode 100644 index 0000000000..ff15df5ea7 --- /dev/null +++ b/packages/nodes-base/nodes/Zoho/descriptions/VendorDescription.ts @@ -0,0 +1,301 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + address, + currencies, + makeCustomFieldsFixedCollection, + makeGetAllFields, +} from './SharedFields'; + +export const vendorOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'vendor', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a vendor', + }, + { + name: 'Create or Update', + value: 'upsert', + description: 'Create a new record, or update the current one if it already exists (upsert)', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a vendor', + }, + { + name: 'Get', + value: 'get', + description: 'Get a vendor', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all vendors', + }, + { + name: 'Update', + value: 'update', + description: 'Update a vendor', + }, + ], + default: 'create', + description: 'Operation to perform', + }, +] as INodeProperties[]; + +export const vendorFields = [ + // ---------------------------------------- + // vendor: create + // ---------------------------------------- + { + displayName: 'Vendor Name', + name: 'vendorName', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'vendor', + ], + operation: [ + 'create', + ], + }, + }, + }, + + // ---------------------------------------- + // vendor: upsert + // ---------------------------------------- + { + displayName: 'Vendor Name', + name: 'vendorName', + description: 'Name of the vendor. If a record with this vendor name exists it will be updated, otherwise a new one will be created.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'vendor', + ], + operation: [ + 'upsert', + ], + }, + }, + }, + + // ---------------------------------------- + // vendor: create + upsert + // ---------------------------------------- + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'vendor', + ], + operation: [ + 'create', + 'upsert', + ], + }, + }, + options: [ + address, + { + displayName: 'Category', + name: 'Category', + type: 'string', + default: '', + }, + { + displayName: 'Currency', + name: 'Currency', + type: 'options', + default: 'USD', + options: currencies, + }, + makeCustomFieldsFixedCollection('vendor'), + { + displayName: 'Description', + name: 'Description', + type: 'string', + default: '', + }, + { + displayName: 'Email', + name: 'Email', + type: 'string', + default: '', + }, + { + displayName: 'Phone', + name: 'Phone', + type: 'string', + default: '', + }, + { + displayName: 'Website', + name: 'Website', + type: 'string', + default: '', + }, + ], + }, + + // ---------------------------------------- + // vendor: delete + // ---------------------------------------- + { + displayName: 'Vendor ID', + name: 'vendorId', + description: 'ID of the vendor to delete.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'vendor', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------------- + // vendor: get + // ---------------------------------------- + { + displayName: 'Vendor ID', + name: 'vendorId', + description: 'ID of the vendor to retrieve.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'vendor', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------------- + // vendor: getAll + // ---------------------------------------- + ...makeGetAllFields('vendor'), + + // ---------------------------------------- + // vendor: update + // ---------------------------------------- + { + displayName: 'Vendor ID', + name: 'vendorId', + description: 'ID of the vendor to update.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'vendor', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'vendor', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + address, + { + displayName: 'Category', + name: 'Category', + type: 'string', + default: '', + }, + { + displayName: 'Currency', + name: 'Currency', + type: 'string', + default: '', + }, + makeCustomFieldsFixedCollection('vendor'), + { + displayName: 'Description', + name: 'Description', + type: 'string', + default: '', + }, + { + displayName: 'Email', + name: 'Email', + type: 'string', + default: '', + }, + { + displayName: 'Phone', + name: 'Phone', + type: 'string', + default: '', + }, + { + displayName: 'Vendor Name', + name: 'Vendor_Name', + type: 'string', + default: '', + }, + { + displayName: 'Website', + name: 'Website', + type: 'string', + default: '', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zoho/descriptions/index.ts b/packages/nodes-base/nodes/Zoho/descriptions/index.ts new file mode 100644 index 0000000000..152b54ef2b --- /dev/null +++ b/packages/nodes-base/nodes/Zoho/descriptions/index.ts @@ -0,0 +1,10 @@ +export * from './AccountDescription'; +export * from './ContactDescription'; +export * from './DealDescription'; +export * from './InvoiceDescription'; +export * from './LeadDescription'; +export * from './ProductDescription'; +export * from './PurchaseOrderDescription'; +export * from './QuoteDescription'; +export * from './SalesOrderDescription'; +export * from './VendorDescription'; diff --git a/packages/nodes-base/nodes/Zoho/types.d.ts b/packages/nodes-base/nodes/Zoho/types.d.ts new file mode 100644 index 0000000000..46da58379c --- /dev/null +++ b/packages/nodes-base/nodes/Zoho/types.d.ts @@ -0,0 +1,103 @@ +import { IDataObject } from "n8n-workflow"; + +// ---------------------------------------- +// for generic functions +// ---------------------------------------- + +type Resource = 'account' | 'contact' | 'deal' | 'invoice' | 'lead' | 'product' | 'quote' | 'vendor'; + +export type CamelCaseResource = Resource | 'purchaseOrder' | 'salesOrder'; + +export type SnakeCaseResource = Resource | 'purchase_order' | 'sales_order'; + +export type GetAllFilterOptions = { + fields: string[], + [otherOptions: string]: unknown; +}; + +// ---------------------------------------- +// for auth +// ---------------------------------------- + +export type ZohoOAuth2ApiCredentials = { + oauthTokenData: { + api_domain: string; + }; +}; + +// ---------------------------------------- +// for field adjusters +// ---------------------------------------- + +export type IdType = 'accountId' | 'contactId' | 'dealId' | 'purchaseOrderId'; + +export type NameType = 'Account_Name' | 'Full_Name' | 'Deal_Name' | 'Product_Name' | 'Vendor_Name'; + +type LocationType = 'Address' | 'Billing_Address' | 'Mailing_Address' | 'Shipping_Address' | 'Other_Address'; + +type DateType = 'Date_of_Birth' | 'Closing_Date' | 'Due_Date' | 'Invoice_Date' | 'PO_Date' | 'Valid_Till'; + +export type AllFields = + { [Date in DateType]?: string } & + { [Location in LocationType]?: { address_fields: { [key: string]: string } } } & + { Account?: { subfields: { id: string; name: string; } } } & + { [key in 'accountId' | 'contactId' | 'dealId']?: string } & + { customFields?: { customFields: Array<{ fieldId: string; value: string; }> } } & + { Product_Details?: ProductDetails } & + IDataObject; + +export type ProductDetails = Array<{ id: string, quantity: number }>; + +export type ResourceItems = Array<{ [key: string]: string }>; + +// ---------------------------------------- +// for resource loaders +// ---------------------------------------- + +export type LoadedAccounts = Array<{ + Account_Name: string; + id: string; +}>; + +export type LoadedContacts = Array<{ + Full_Name: string; + id: string; +}>; + +export type LoadedDeals = Array<{ + Deal_Name: string; + id: string; +}>; + +export type LoadedFields = { + fields: Array<{ + field_label: string; + api_name: string; + custom_field: boolean; + }> +}; + +export type LoadedVendors = Array<{ + Vendor_Name: string; + id: string; +}>; + +export type LoadedProducts = Array<{ + Product_Name: string; + id: string; +}>; + +export type LoadedLayouts = { + layouts: Array<{ + sections: Array<{ + api_name: string; + fields: Array<{ + api_name: string; + pick_list_values: Array<{ + display_value: string; + actual_value: string; + }> + }> + }> + }> +} diff --git a/packages/nodes-base/nodes/Zoho/zoho.svg b/packages/nodes-base/nodes/Zoho/zoho.svg index 9fc3141978..203761c270 100644 --- a/packages/nodes-base/nodes/Zoho/zoho.svg +++ b/packages/nodes-base/nodes/Zoho/zoho.svg @@ -1 +1 @@ - \ No newline at end of file + From 40bcd9b20ec6bce8ada26ac0933a5eeaa76952c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 2 Jul 2021 23:41:40 +0200 Subject: [PATCH 05/29] :zap: Add last name to contact:update on Salesforce Node (#1965) --- packages/nodes-base/nodes/Salesforce/ContactDescription.ts | 7 +++++++ packages/nodes-base/nodes/Salesforce/Salesforce.node.ts | 3 +++ 2 files changed, 10 insertions(+) diff --git a/packages/nodes-base/nodes/Salesforce/ContactDescription.ts b/packages/nodes-base/nodes/Salesforce/ContactDescription.ts index 3ec78b6e11..4e9901fc6e 100644 --- a/packages/nodes-base/nodes/Salesforce/ContactDescription.ts +++ b/packages/nodes-base/nodes/Salesforce/ContactDescription.ts @@ -570,6 +570,13 @@ export const contactFields = [ description: `references the ID of a contact in Data.com. If a contact has a value in this field, it means that a contact was imported as a contact from Data.com.`, }, + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + default: '', + description: 'Last name of the contact. Limited to 80 characters.', + }, { displayName: 'Lead Source', name: 'leadSource', diff --git a/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts b/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts index 9afccd687f..08269b0ca7 100644 --- a/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts +++ b/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts @@ -1337,6 +1337,9 @@ export class Salesforce implements INodeType { if (!Object.keys(updateFields).length) { throw new NodeOperationError(this.getNode(), 'You must add at least one update field'); } + if (updateFields.lastName !== undefined) { + body.LastName = updateFields.lastName as string; + } if (updateFields.fax !== undefined) { body.Fax = updateFields.fax as string; } From 64961199bad8cd184befafdccb4a1d3023c4b33b Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Fri, 2 Jul 2021 17:58:18 -0400 Subject: [PATCH 06/29] :zap: Add file/folder share operation (#1951) * :zap: Add file/folder share operation * :zap: Improvements * :zap: Minor fixes Co-authored-by: Jan Oberhauser --- packages/nodes-base/nodes/Box/Box.node.ts | 123 +++++ .../nodes-base/nodes/Box/FileDescription.ts | 241 ++++++++++ .../nodes-base/nodes/Box/FolderDescription.ts | 424 ++++++++++++++++++ 3 files changed, 788 insertions(+) diff --git a/packages/nodes-base/nodes/Box/Box.node.ts b/packages/nodes-base/nodes/Box/Box.node.ts index 75006e46c2..9d462b60b7 100644 --- a/packages/nodes-base/nodes/Box/Box.node.ts +++ b/packages/nodes-base/nodes/Box/Box.node.ts @@ -29,6 +29,10 @@ import { import * as moment from 'moment-timezone'; +import { + noCase, +} from 'change-case'; + export class Box implements INodeType { description: INodeTypeDescription = { displayName: 'Box', @@ -81,6 +85,7 @@ export class Box implements INodeType { const length = items.length as unknown as number; const qs: IDataObject = {}; let responseData; + const timezone = this.getTimezone(); const resource = this.getNodeParameter('resource', 0) as string; const operation = this.getNodeParameter('operation', 0) as string; for (let i = 0; i < length; i++) { @@ -199,6 +204,51 @@ export class Box implements INodeType { } returnData.push.apply(returnData, responseData as IDataObject[]); } + // https://developer.box.com/reference/post-collaborations/ + if (operation === 'share') { + const fileId = this.getNodeParameter('fileId', i) as string; + const role = this.getNodeParameter('role', i) as string; + const accessibleBy = this.getNodeParameter('accessibleBy', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + // tslint:disable-next-line: no-any + const body: { accessible_by: IDataObject, [key: string]: any } = { + accessible_by: {}, + item: { + id: fileId, + type: 'file', + }, + role: (role === 'coOwner') ? 'co-owner' : noCase(role), + ...options, + }; + + if (body.fields) { + qs.fields = body.fields; + delete body.fields; + } + + if (body.expires_at) { + body.expires_at = moment.tz(body.expires_at, timezone).format(); + } + + if (body.notify) { + qs.notify = body.notify; + delete body.notify; + } + + if (accessibleBy === 'user') { + const useEmail = this.getNodeParameter('useEmail', i) as boolean; + if (useEmail) { + body.accessible_by['login'] = this.getNodeParameter('email', i) as string; + } else { + body.accessible_by['id'] = this.getNodeParameter('userId', i) as string; + } + } else { + body.accessible_by['id'] = this.getNodeParameter('groupId', i) as string; + } + + responseData = await boxApiRequest.call(this, 'POST', `/collaborations`, body, qs); + returnData.push(responseData as IDataObject); + } // https://developer.box.com/reference/post-files-content if (operation === 'upload') { const parentId = this.getNodeParameter('parentId', i) as string; @@ -356,6 +406,79 @@ export class Box implements INodeType { } returnData.push.apply(returnData, responseData as IDataObject[]); } + // https://developer.box.com/reference/post-collaborations/ + if (operation === 'share') { + const folderId = this.getNodeParameter('folderId', i) as string; + const role = this.getNodeParameter('role', i) as string; + const accessibleBy = this.getNodeParameter('accessibleBy', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + // tslint:disable-next-line: no-any + const body: { accessible_by: IDataObject, [key: string]: any } = { + accessible_by: {}, + item: { + id: folderId, + type: 'folder', + }, + role: (role === 'coOwner') ? 'co-owner' : noCase(role), + ...options, + }; + + if (body.fields) { + qs.fields = body.fields; + delete body.fields; + } + + if (body.expires_at) { + body.expires_at = moment.tz(body.expires_at, timezone).format(); + } + + if (body.notify) { + qs.notify = body.notify; + delete body.notify; + } + + if (accessibleBy === 'user') { + const useEmail = this.getNodeParameter('useEmail', i) as boolean; + if (useEmail) { + body.accessible_by['login'] = this.getNodeParameter('email', i) as string; + } else { + body.accessible_by['id'] = this.getNodeParameter('userId', i) as string; + } + } else { + body.accessible_by['id'] = this.getNodeParameter('groupId', i) as string; + } + + responseData = await boxApiRequest.call(this, 'POST', `/collaborations`, body, qs); + returnData.push(responseData as IDataObject); + } + //https://developer.box.com/guides/folders/single/move/ + if (operation === 'update') { + const folderId = this.getNodeParameter('folderId', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (updateFields.fields) { + qs.fields = updateFields.fields; + delete updateFields.fields; + } + + const body = { + ...updateFields, + } as IDataObject; + + if (body.parentId) { + body.parent = { + id: body.parentId, + }; + delete body.parentId; + } + + if (body.tags) { + body.tags = (body.tags as string).split(','); + } + + responseData = await boxApiRequest.call(this, 'PUT', `/folders/${folderId}`, body, qs); + returnData.push(responseData as IDataObject); + } } } if (resource === 'file' && operation === 'download') { diff --git a/packages/nodes-base/nodes/Box/FileDescription.ts b/packages/nodes-base/nodes/Box/FileDescription.ts index 9a3ae0e2c2..7aa5a87d72 100644 --- a/packages/nodes-base/nodes/Box/FileDescription.ts +++ b/packages/nodes-base/nodes/Box/FileDescription.ts @@ -40,6 +40,11 @@ export const fileOperations = [ value: 'search', description: 'Search files', }, + { + name: 'Share', + value: 'share', + description: 'Share a file', + }, { name: 'Upload', value: 'upload', @@ -496,6 +501,242 @@ export const fileFields = [ ], }, + /* -------------------------------------------------------------------------- */ + /* file:share */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'File ID', + name: 'fileId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'share', + ], + resource: [ + 'file', + ], + }, + }, + default: '', + description: 'The ID of the file to share.', + }, + { + displayName: 'Accessible By', + name: 'accessibleBy', + type: 'options', + options: [ + { + name: 'Group', + value: 'group', + }, + { + name: 'User', + value: 'user', + }, + ], + displayOptions: { + show: { + operation: [ + 'share', + ], + resource: [ + 'file', + ], + }, + }, + default: '', + description: 'The type of object the file will be shared with.', + }, + { + displayName: 'Use Email', + name: 'useEmail', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'share', + ], + resource: [ + 'file', + ], + accessibleBy: [ + 'user', + ], + }, + }, + default: true, + description: 'Whether identify the user by email or ID.', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + displayOptions: { + show: { + operation: [ + 'share', + ], + resource: [ + 'file', + ], + useEmail: [ + true, + ], + accessibleBy: [ + 'user', + ], + }, + }, + default: '', + description: `The user's email address to share the file with.`, + }, + { + displayName: 'User ID', + name: 'userId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'share', + ], + resource: [ + 'file', + ], + useEmail: [ + false, + ], + accessibleBy: [ + 'user', + ], + }, + }, + default: '', + description: `The user's ID to share the file with.`, + }, + { + displayName: 'Group ID', + name: 'groupId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'share', + ], + resource: [ + 'file', + ], + accessibleBy: [ + 'group', + ], + }, + }, + default: '', + description: `The group's ID to share the file with.`, + }, + { + displayName: 'Role', + name: 'role', + type: 'options', + options: [ + { + name: 'Co-Owner', + value: 'coOwner', + description: 'A Co-owner has all of functional read/write access that an editor does', + }, + { + name: 'Editor', + value: 'editor', + description: 'An editor has full read/write access to a folder or file', + }, + { + name: 'Previewer', + value: 'previewer', + description: 'A previewer has limited read access', + }, + { + name: 'Previewer Uploader', + value: 'previewerUploader', + description: 'This access level is a combination of Previewer and Uploader', + }, + { + name: 'Uploader', + value: 'uploader', + description: 'An uploader has limited write access', + }, + { + name: 'Viewer', + value: 'viewer', + description: 'A viewer has read access to a folder or file', + }, + { + name: 'Viewer Uploader', + value: 'viewerUploader', + description: 'This access level is a combination of Viewer and Uploader', + }, + ], + displayOptions: { + show: { + operation: [ + 'share', + ], + resource: [ + 'file', + ], + }, + }, + default: 'editor', + description: 'The level of access granted.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + displayOptions: { + show: { + operation: [ + 'share', + ], + resource: [ + 'file', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Can View Path', + name: 'can_view_path', + type: 'boolean', + default: false, + description: `Whether the invited users can see the entire parent path to the associated folder.
+ The user will not gain privileges in any parent folder and therefore cannot see content the user is not collaborated on.`, + }, + { + displayName: 'Expires At', + name: 'expires_at', + type: 'dateTime', + default: '', + description: 'Set the expiration date for the collaboration. At this date, the collaboration will be automatically removed from the item.', + }, + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'A comma-separated list of attributes to include in the response. This can be used to request fields that are not normally returned in a standard response.', + }, + { + displayName: 'Notify', + name: 'notify', + type: 'boolean', + default: false, + description: 'Whether if users should receive email notification for the action performed.', + }, + ], + }, + /* -------------------------------------------------------------------------- */ /* file:upload */ /* -------------------------------------------------------------------------- */ diff --git a/packages/nodes-base/nodes/Box/FolderDescription.ts b/packages/nodes-base/nodes/Box/FolderDescription.ts index 5e944b25d1..11ed33c2cf 100644 --- a/packages/nodes-base/nodes/Box/FolderDescription.ts +++ b/packages/nodes-base/nodes/Box/FolderDescription.ts @@ -35,6 +35,16 @@ export const folderOperations = [ value: 'search', description: 'Search files', }, + { + name: 'Share', + value: 'share', + description: 'Share a folder', + }, + { + name: 'Update', + value: 'update', + description: 'Update folder', + }, ], default: 'create', description: 'The operation to perform.', @@ -147,6 +157,7 @@ export const folderFields = [ default: '', description: 'Folder ID', }, + /* -------------------------------------------------------------------------- */ /* folder:delete */ /* -------------------------------------------------------------------------- */ @@ -441,4 +452,417 @@ export const folderFields = [ }, ], }, + + /* -------------------------------------------------------------------------- */ + /* folder:share */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Folder ID', + name: 'folderId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'share', + ], + resource: [ + 'folder', + ], + }, + }, + default: '', + description: 'The ID of the folder to share.', + }, + { + displayName: 'Accessible By', + name: 'accessibleBy', + type: 'options', + options: [ + { + name: 'User', + value: 'user', + }, + { + name: 'Group', + value: 'group', + }, + ], + displayOptions: { + show: { + operation: [ + 'share', + ], + resource: [ + 'folder', + ], + }, + }, + default: 'user', + description: 'The type of object the file will be shared with.', + }, + { + displayName: 'Use Email', + name: 'useEmail', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'share', + ], + resource: [ + 'folder', + ], + accessibleBy: [ + 'user', + ], + }, + }, + default: true, + description: 'Whether identify the user by email or ID.', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + displayOptions: { + show: { + operation: [ + 'share', + ], + resource: [ + 'folder', + ], + accessibleBy: [ + 'user', + ], + useEmail: [ + true, + ], + }, + }, + default: '', + description: `The user's email address to share the folder with.`, + }, + { + displayName: 'User ID', + name: 'userId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'share', + ], + resource: [ + 'folder', + ], + accessibleBy: [ + 'user', + ], + useEmail: [ + false, + ], + }, + }, + default: '', + description: `The user's ID to share the folder with.`, + }, + { + displayName: 'Group ID', + name: 'groupId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'share', + ], + resource: [ + 'folder', + ], + accessibleBy: [ + 'group', + ], + }, + }, + default: '', + description: `The group's ID to share the folder with.`, + }, + { + displayName: 'Role', + name: 'role', + type: 'options', + options: [ + { + name: 'Co-Owner', + value: 'coOwner', + description: 'A Co-owner has all of functional read/write access that an editor does', + }, + { + name: 'Editor', + value: 'editor', + description: 'An editor has full read/write access to a folder or file', + }, + { + name: 'Previewer', + value: 'previewer', + description: 'A previewer has limited read access', + }, + { + name: 'Previewer Uploader', + value: 'previewerUploader', + description: 'This access level is a combination of Previewer and Uploader', + }, + { + name: 'Uploader', + value: 'uploader', + description: 'An uploader has limited write access', + }, + { + name: 'Viewer', + value: 'viewer', + description: 'A viewer has read access to a folder or file', + }, + { + name: 'Viewer Uploader', + value: 'viewerUploader', + description: 'This access level is a combination of Viewer and Uploader', + }, + ], + displayOptions: { + show: { + operation: [ + 'share', + ], + resource: [ + 'folder', + ], + }, + }, + default: 'editor', + description: 'The level of access granted.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + displayOptions: { + show: { + operation: [ + 'share', + ], + resource: [ + 'folder', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Can View Path', + name: 'can_view_path', + type: 'boolean', + default: false, + description: `Whether the invited users can see the entire parent path to the associated folder.
+ The user will not gain privileges in any parent folder and therefore cannot see content the user is not collaborated on.`, + }, + { + displayName: 'Expires At', + name: 'expires_at', + type: 'dateTime', + default: '', + description: 'Set the expiration date for the collaboration. At this date, the collaboration will be automatically removed from the item.', + }, + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'A comma-separated list of attributes to include in the response. This can be used to request fields that are not normally returned in a standard response.', + }, + { + displayName: 'Notify', + name: 'notify', + type: 'boolean', + default: false, + description: 'Whether if users should receive email notification for the action performed.', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* folder:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Folder ID', + name: 'folderId', + required: true, + type: 'string', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'folder', + ], + }, + }, + default: '', + description: 'Folder ID', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'folder', + ], + }, + }, + default: {}, + placeholder: 'Add Field', + options: [ + { + displayName: 'Can Non-Owners Invite', + name: 'can_non_owners_invite', + type: 'boolean', + default: false, + description: 'Specifies if users who are not the owner of the folder can invite new collaborators to the folder.', + }, + { + displayName: 'Can Non-Owners View Colaborators', + name: 'can_non_owners_view_collaborators', + type: 'boolean', + default: false, + description: 'Restricts collaborators who are not the owner of this folder from viewing other collaborations on this folder.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'The optional description of this folder.', + }, + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'A comma-separated list of attributes to include in the response. This can be used to request fields that are not normally returned in a standard response.', + }, + { + displayName: 'Is Collaboration Restricted To Enterprise', + name: 'is_collaboration_restricted_to_enterprise', + type: 'boolean', + default: false, + description: 'Specifies if new invites to this folder are restricted to users within the enterprise. This does not affect existing collaborations.', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'The optional new name for this folder.', + }, + { + displayName: 'Parent ID', + name: 'parentId', + type: 'string', + default: '', + description: 'The parent folder for this folder. Use this to move the folder or to restore it out of the trash.', + }, + { + displayName: 'Shared Link', + name: 'shared_link', + type: 'collection', + typeOptions: { + multipleValues: false, + }, + description: 'Share link information.', + placeholder: 'Add Shared Link Config', + default: {}, + options: [ + { + displayName: 'Access', + name: 'access', + type: 'options', + options: [ + { + name: 'Collaborators', + value: 'collaborators', + description: 'Only those who have been invited to the folder', + }, + { + name: 'Company', + value: 'company', + description: 'only people within the company', + }, + { + name: 'Open', + value: 'open', + description: 'Anyone with the link', + }, + ], + default: 'open', + }, + { + displayName: 'Password', + name: 'password', + type: 'string', + displayOptions: { + show: { + access: [ + 'open', + ], + }, + }, + default: '', + description: 'The password required to access the shared link. Set the password to null to remove it.', + }, + { + displayName: 'Permissions', + name: 'permissions', + type: 'collection', + placeholder: 'Add Permition', + default: {}, + options: [ + { + displayName: 'Can Download', + name: 'can_download', + type: 'boolean', + default: false, + description: 'If the shared link allows for downloading of files.', + }, + { + displayName: 'Unshared At', + name: 'unshared_at', + type: 'dateTime', + default: '', + description: 'The timestamp at which this shared link will expire.', + }, + { + displayName: 'Vanity Name', + name: 'vanity_name', + type: 'string', + default: '', + description: 'Defines a custom vanity name to use in the shared link URL, for example https://app.box.com/v/my-shared-link.', + }, + ], + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + default: '', + description: 'The tags for this item. These tags are shown in the Box web app and mobile apps next to an item.', + }, + ], + }, + ], + }, ] as INodeProperties[]; From 05921de99ae3971e497297f83c20946d4ab8cbde Mon Sep 17 00:00:00 2001 From: Ahsan Virani Date: Sat, 3 Jul 2021 11:14:11 +0200 Subject: [PATCH 07/29] :bug: Fix bug on n8n Training Customer Datastore Node (#1964) --- packages/nodes-base/nodes/N8nTrainingCustomerDatastore.node.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/N8nTrainingCustomerDatastore.node.ts b/packages/nodes-base/nodes/N8nTrainingCustomerDatastore.node.ts index 0ccc9042c3..99705a7245 100644 --- a/packages/nodes-base/nodes/N8nTrainingCustomerDatastore.node.ts +++ b/packages/nodes-base/nodes/N8nTrainingCustomerDatastore.node.ts @@ -147,7 +147,7 @@ export class N8nTrainingCustomerDatastore implements INodeType { responseData = data; } else { const limit = this.getNodeParameter('limit', i) as number; - responseData = data.splice(0, limit); + responseData = data.slice(0, limit); } } From 6d8ea4bff5d474b3e72cc13b4c318f3d4844cb44 Mon Sep 17 00:00:00 2001 From: Kaito Udagawa Date: Sat, 3 Jul 2021 19:46:30 +0900 Subject: [PATCH 08/29] :bug: Fix Google Sheet to handle 0 correctly (#1937) * :bug: Fix Google Sheet to handle 0 correctly `if (condition) { statement }` will not be executed if the `condition` is `0` (number) so that appending 0 to Google Sheets results in an empty cell. Checking if the value is `null` or `undefined` is enough to guarantee that `toString` is callable. * :bug: Add semicolon --- packages/nodes-base/nodes/Google/Sheet/GoogleSheet.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/nodes-base/nodes/Google/Sheet/GoogleSheet.ts b/packages/nodes-base/nodes/Google/Sheet/GoogleSheet.ts index aca82ddea2..28d35d5afe 100644 --- a/packages/nodes-base/nodes/Google/Sheet/GoogleSheet.ts +++ b/packages/nodes-base/nodes/Google/Sheet/GoogleSheet.ts @@ -486,8 +486,9 @@ export class GoogleSheet { inputData.forEach((item) => { rowData = []; keyColumnOrder.forEach((key) => { - if (item.hasOwnProperty(key) && item[key]) { - rowData.push(item[key]!.toString()); + const data = item[key]; + if (item.hasOwnProperty(key) && data !== null && typeof data !== 'undefined') { + rowData.push(data.toString()); } else { rowData.push(''); } From 849c8ad2b733066622660fbcf3b5e8dc129d1ca7 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sat, 3 Jul 2021 08:07:48 -0400 Subject: [PATCH 09/29] :zap: Send operations in bulk to Airtable (#1945) * Add bulk operation to Airtable Node * :zap: Small improvements to #1942 * :zap: Resolve lint issues * :zap: Make by default bulk updates and fix issue with update Co-authored-by: Michael Chen Co-authored-by: Jan Oberhauser --- .../nodes/Airtable/Airtable.node.ts | 139 +++++++++++++----- 1 file changed, 100 insertions(+), 39 deletions(-) diff --git a/packages/nodes-base/nodes/Airtable/Airtable.node.ts b/packages/nodes-base/nodes/Airtable/Airtable.node.ts index 36c2846618..6f9c8fe506 100644 --- a/packages/nodes-base/nodes/Airtable/Airtable.node.ts +++ b/packages/nodes-base/nodes/Airtable/Airtable.node.ts @@ -390,7 +390,7 @@ export class Airtable implements INodeType { }, // ---------------------------------- - // append + update + // append + delete + update // ---------------------------------- { displayName: 'Options', @@ -401,12 +401,24 @@ export class Airtable implements INodeType { show: { operation: [ 'append', + 'delete', 'update', ], }, }, default: {}, options: [ + { + displayName: 'Bulk Size', + name: 'bulkSize', + type: 'number', + typeOptions: { + minValue: 1, + maxValue: 10, + }, + default: 10, + description: `Number of records to process at once.`, + }, { displayName: 'Ignore Fields', name: 'ignoreFields', @@ -428,6 +440,14 @@ export class Airtable implements INodeType { displayName: 'Typecast', name: 'typecast', type: 'boolean', + displayOptions: { + show: { + '/operation': [ + 'append', + 'update', + ], + }, + }, default: false, description: 'If the Airtable API should attempt mapping of string values for linked records & select options.', }, @@ -465,54 +485,81 @@ export class Airtable implements INodeType { let fields: string[]; let options: IDataObject; + const rows: IDataObject[] = []; + let bulkSize = 10; + for (let i = 0; i < items.length; i++) { addAllFields = this.getNodeParameter('addAllFields', i) as boolean; options = this.getNodeParameter('options', i, {}) as IDataObject; + bulkSize = options.bulkSize as number || bulkSize; + + const row: IDataObject = {}; if (addAllFields === true) { // Add all the fields the item has - body.fields = items[i].json; + row.fields = { ...items[i].json }; + // tslint:disable-next-line: no-any + delete (row.fields! as any).id; } else { // Add only the specified fields - body.fields = {} as IDataObject; + row.fields = {} as IDataObject; fields = this.getNodeParameter('fields', i, []) as string[]; for (const fieldName of fields) { // @ts-ignore - body.fields[fieldName] = items[i].json[fieldName]; + row.fields[fieldName] = items[i].json[fieldName]; } } - if (options.typecast === true) { - body['typecast'] = true; + rows.push(row); + + if (rows.length === bulkSize || i === items.length - 1) { + if (options.typecast === true) { + body['typecast'] = true; + } + + body['records'] = rows; + + responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs); + + returnData.push(...responseData.records); + // empty rows + rows.length = 0; } - - responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs); - - returnData.push(responseData); } } else if (operation === 'delete') { requestMethod = 'DELETE'; - let id: string; + const rows: string[] = []; + const options = this.getNodeParameter('options', 0, {}) as IDataObject; + const bulkSize = options.bulkSize as number || 10; + for (let i = 0; i < items.length; i++) { + let id: string; + id = this.getNodeParameter('id', i) as string; - endpoint = `${application}/${table}`; + rows.push(id); - // Make one request after another. This is slower but makes - // sure that we do not run into the rate limit they have in - // place and so block for 30 seconds. Later some global - // functionality in core should make it easy to make requests - // according to specific rules like not more than 5 requests - // per seconds. - qs.records = [id]; + if (rows.length === bulkSize || i === items.length - 1) { + endpoint = `${application}/${table}`; - responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs); + // Make one request after another. This is slower but makes + // sure that we do not run into the rate limit they have in + // place and so block for 30 seconds. Later some global + // functionality in core should make it easy to make requests + // according to specific rules like not more than 5 requests + // per seconds. + qs.records = rows; - returnData.push(...responseData.records); + responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs); + + returnData.push(...responseData.records); + // empty rows + rows.length = 0; + } } } else if (operation === 'list') { @@ -585,55 +632,69 @@ export class Airtable implements INodeType { requestMethod = 'PATCH'; - let id: string; let updateAllFields: boolean; let fields: string[]; let options: IDataObject; + + const rows: IDataObject[] = []; + let bulkSize = 10; + for (let i = 0; i < items.length; i++) { updateAllFields = this.getNodeParameter('updateAllFields', i) as boolean; options = this.getNodeParameter('options', i, {}) as IDataObject; + bulkSize = options.bulkSize as number || bulkSize; + + const row: IDataObject = {}; + row.fields = {} as IDataObject; if (updateAllFields === true) { // Update all the fields the item has - body.fields = items[i].json; + row.fields = { ...items[i].json }; + // remove id field + // tslint:disable-next-line: no-any + delete (row.fields! as any).id; if (options.ignoreFields && options.ignoreFields !== '') { const ignoreFields = (options.ignoreFields as string).split(',').map(field => field.trim()).filter(field => !!field); if (ignoreFields.length) { // From: https://stackoverflow.com/questions/17781472/how-to-get-a-subset-of-a-javascript-objects-properties - body.fields = Object.entries(items[i].json) + row.fields = Object.entries(items[i].json) .filter(([key]) => !ignoreFields.includes(key)) .reduce((obj, [key, val]) => Object.assign(obj, { [key]: val }), {}); } } } else { - // Update only the specified fields - body.fields = {} as IDataObject; - fields = this.getNodeParameter('fields', i, []) as string[]; for (const fieldName of fields) { // @ts-ignore - body.fields[fieldName] = items[i].json[fieldName]; + row.fields[fieldName] = items[i].json[fieldName]; } } - id = this.getNodeParameter('id', i) as string; + row.id = this.getNodeParameter('id', i) as string; - endpoint = `${application}/${table}`; + rows.push(row); - // Make one request after another. This is slower but makes - // sure that we do not run into the rate limit they have in - // place and so block for 30 seconds. Later some global - // functionality in core should make it easy to make requests - // according to specific rules like not more than 5 requests - // per seconds. + if (rows.length === bulkSize || i === items.length - 1) { + endpoint = `${application}/${table}`; - const data = { records: [{ id, fields: body.fields }], typecast: (options.typecast) ? true : false }; + // Make one request after another. This is slower but makes + // sure that we do not run into the rate limit they have in + // place and so block for 30 seconds. Later some global + // functionality in core should make it easy to make requests + // according to specific rules like not more than 5 requests + // per seconds. - responseData = await apiRequest.call(this, requestMethod, endpoint, data, qs); + const data = { records: rows, typecast: (options.typecast) ? true : false }; - returnData.push(...responseData.records); + responseData = await apiRequest.call(this, requestMethod, endpoint, data, qs); + + returnData.push(...responseData.records); + + // empty rows + rows.length = 0; + } } } else { From 2e40a0e64efd285a314c8c056eb9e0e09e6c3606 Mon Sep 17 00:00:00 2001 From: Snyk bot Date: Sat, 3 Jul 2021 15:19:57 +0300 Subject: [PATCH 10/29] :arrow_up: Set fflate@0.7.0 on n8n-nodes-base Snyk has created this PR to upgrade fflate from 0.6.10 to 0.7.0. See this package in npm: https://www.npmjs.com/package/fflate See this project in Snyk: https://app.snyk.io/org/janober/project/a08454f4-33a1-49bc-bb2a-f31792e94f42?utm_source=github&utm_medium=upgrade-pr --- packages/nodes-base/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 5be403bb31..7a2c4ca1ea 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -627,7 +627,7 @@ "cheerio": "1.0.0-rc.6", "cron": "^1.7.2", "eventsource": "^1.0.7", - "fflate": "^0.6.9", + "fflate": "^0.7.0", "formidable": "^1.2.1", "get-system-fonts": "^2.0.2", "glob-promise": "^3.4.0", From b49202687da0d33052008ebe6cd7f1135dc76d1f Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sat, 3 Jul 2021 14:30:02 +0200 Subject: [PATCH 11/29] :sparkles: Add support to display binary video content --- packages/editor-ui/src/components/BinaryDataDisplay.vue | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/editor-ui/src/components/BinaryDataDisplay.vue b/packages/editor-ui/src/components/BinaryDataDisplay.vue index c6ab2aa748..3a737c1f0f 100644 --- a/packages/editor-ui/src/components/BinaryDataDisplay.vue +++ b/packages/editor-ui/src/components/BinaryDataDisplay.vue @@ -14,6 +14,10 @@
Data to display did not get found
+ From 276eaea4bfb50cacfd350bb8e8be478596f345c1 Mon Sep 17 00:00:00 2001 From: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> Date: Sat, 3 Jul 2021 14:40:16 +0200 Subject: [PATCH 12/29] :zap: Clean up node description (#1883) Co-authored-by: Jan --- packages/nodes-base/nodes/Box/BoxTrigger.node.ts | 2 +- packages/nodes-base/nodes/Calendly/CalendlyTrigger.node.ts | 2 +- packages/nodes-base/nodes/Chargebee/ChargebeeTrigger.node.ts | 2 +- packages/nodes-base/nodes/Clockify/ClockifyTrigger.node.ts | 2 +- packages/nodes-base/nodes/ConvertKit/ConvertKit.node.ts | 2 +- packages/nodes-base/nodes/CrateDb/CrateDb.node.ts | 2 +- packages/nodes-base/nodes/CustomerIo/CustomerIoTrigger.node.ts | 2 +- packages/nodes-base/nodes/Discourse/Discourse.node.ts | 2 +- packages/nodes-base/nodes/EmailReadImap.node.ts | 2 +- packages/nodes-base/nodes/ExecuteCommand.node.ts | 2 +- packages/nodes-base/nodes/Facebook/FacebookTrigger.node.ts | 2 +- packages/nodes-base/nodes/FileMaker/FileMaker.node.ts | 2 +- packages/nodes-base/nodes/Ftp.node.ts | 2 +- packages/nodes-base/nodes/Function.node.ts | 2 +- packages/nodes-base/nodes/FunctionItem.node.ts | 2 +- packages/nodes-base/nodes/GetResponse/GetResponse.node.ts | 2 +- .../nodes-base/nodes/GetResponse/GetResponseTrigger.node.ts | 2 +- packages/nodes-base/nodes/Ghost/Ghost.node.ts | 2 +- packages/nodes-base/nodes/Github/Github.node.ts | 2 +- packages/nodes-base/nodes/Github/GithubTrigger.node.ts | 2 +- packages/nodes-base/nodes/Gitlab/Gitlab.node.ts | 2 +- packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts | 2 +- .../nodes-base/nodes/Google/BigQuery/GoogleBigQuery.node.ts | 2 +- .../nodes-base/nodes/Google/Calendar/GoogleCalendar.node.ts | 2 +- .../nodes-base/nodes/Google/Contacts/GoogleContacts.node.ts | 2 +- .../nodes-base/nodes/Google/Drive/GoogleDriveTrigger.node.ts | 2 +- packages/nodes-base/nodes/Google/Task/GoogleTasks.node.ts | 2 +- packages/nodes-base/nodes/Google/YouTube/YouTube.node.ts | 2 +- packages/nodes-base/nodes/Gotify/Gotify.node.ts | 2 +- packages/nodes-base/nodes/HelpScout/HelpScout.node.ts | 2 +- packages/nodes-base/nodes/HelpScout/HelpScoutTrigger.node.ts | 2 +- packages/nodes-base/nodes/HttpRequest.node.ts | 2 +- packages/nodes-base/nodes/Hubspot/HubspotTrigger.node.ts | 2 +- packages/nodes-base/nodes/If.node.ts | 2 +- packages/nodes-base/nodes/Intercom/Intercom.node.ts | 2 +- .../nodes-base/nodes/InvoiceNinja/InvoiceNinjaTrigger.node.ts | 2 +- packages/nodes-base/nodes/Iterable/Iterable.node.ts | 2 +- packages/nodes-base/nodes/Jira/JiraTrigger.node.ts | 2 +- packages/nodes-base/nodes/Keap/Keap.node.ts | 2 +- packages/nodes-base/nodes/Keap/KeapTrigger.node.ts | 2 +- packages/nodes-base/nodes/Line/Line.node.ts | 2 +- packages/nodes-base/nodes/LinkedIn/LinkedIn.node.ts | 2 +- packages/nodes-base/nodes/MailerLite/MailerLite.node.ts | 2 +- packages/nodes-base/nodes/MailerLite/MailerLiteTrigger.node.ts | 2 +- packages/nodes-base/nodes/Mailgun/Mailgun.node.ts | 2 +- packages/nodes-base/nodes/Mandrill/Mandrill.node.ts | 2 +- packages/nodes-base/nodes/Merge.node.ts | 2 +- packages/nodes-base/nodes/MessageBird/MessageBird.node.ts | 2 +- .../nodes-base/nodes/Microsoft/Excel/MicrosoftExcel.node.ts | 2 +- .../nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.ts | 2 +- packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts | 2 +- packages/nodes-base/nodes/Mindee/Mindee.node.ts | 2 +- packages/nodes-base/nodes/Mocean/Mocean.node.ts | 2 +- packages/nodes-base/nodes/MongoDb/mongo.node.options.ts | 2 +- packages/nodes-base/nodes/MoveBinaryData.node.ts | 2 +- packages/nodes-base/nodes/Msg91/Msg91.node.ts | 2 +- packages/nodes-base/nodes/MySql/MySql.node.ts | 2 +- packages/nodes-base/nodes/Nasa/Nasa.node.ts | 2 +- packages/nodes-base/nodes/OpenWeatherMap.node.ts | 2 +- packages/nodes-base/nodes/Phantombuster/Phantombuster.node.ts | 2 +- packages/nodes-base/nodes/PhilipsHue/PhilipsHue.node.ts | 2 +- packages/nodes-base/nodes/Pipedrive/PipedriveTrigger.node.ts | 2 +- packages/nodes-base/nodes/PostHog/PostHog.node.ts | 2 +- packages/nodes-base/nodes/Postgres/Postgres.node.ts | 2 +- packages/nodes-base/nodes/Postmark/PostmarkTrigger.node.ts | 2 +- packages/nodes-base/nodes/Pushbullet/Pushbullet.node.ts | 2 +- packages/nodes-base/nodes/Pushcut/Pushcut.node.ts | 2 +- packages/nodes-base/nodes/Pushcut/PushcutTrigger.node.ts | 2 +- packages/nodes-base/nodes/Pushover/Pushover.node.ts | 2 +- packages/nodes-base/nodes/QuestDb/QuestDb.node.ts | 2 +- packages/nodes-base/nodes/QuickBase/QuickBase.node.ts | 2 +- packages/nodes-base/nodes/Redis/Redis.node.ts | 2 +- packages/nodes-base/nodes/RenameKeys.node.ts | 2 +- packages/nodes-base/nodes/Sendy/Sendy.node.ts | 2 +- packages/nodes-base/nodes/Set.node.ts | 2 +- packages/nodes-base/nodes/Signl4/Signl4.node.ts | 2 +- packages/nodes-base/nodes/Snowflake/Snowflake.node.ts | 2 +- packages/nodes-base/nodes/SplitInBatches.node.ts | 2 +- packages/nodes-base/nodes/Spotify/Spotify.node.ts | 2 +- packages/nodes-base/nodes/SpreadsheetFile.node.ts | 2 +- packages/nodes-base/nodes/SseTrigger.node.ts | 2 +- packages/nodes-base/nodes/Stackby/Stackby.node.ts | 2 +- packages/nodes-base/nodes/Strapi/Strapi.node.ts | 2 +- packages/nodes-base/nodes/Strava/Strava.node.ts | 2 +- packages/nodes-base/nodes/Strava/StravaTrigger.node.ts | 2 +- .../nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts | 2 +- packages/nodes-base/nodes/Switch.node.ts | 2 +- packages/nodes-base/nodes/Telegram/Telegram.node.ts | 2 +- packages/nodes-base/nodes/Telegram/TelegramTrigger.node.ts | 2 +- packages/nodes-base/nodes/TheHive/TheHive.node.ts | 2 +- packages/nodes-base/nodes/TheHive/TheHiveTrigger.node.ts | 2 +- packages/nodes-base/nodes/Trello/TrelloTrigger.node.ts | 2 +- packages/nodes-base/nodes/Typeform/TypeformTrigger.node.ts | 2 +- packages/nodes-base/nodes/Webhook.node.ts | 2 +- packages/nodes-base/nodes/Wekan/Wekan.node.ts | 2 +- packages/nodes-base/nodes/Zoho/ZohoCrm.node.ts | 2 +- 96 files changed, 96 insertions(+), 96 deletions(-) diff --git a/packages/nodes-base/nodes/Box/BoxTrigger.node.ts b/packages/nodes-base/nodes/Box/BoxTrigger.node.ts index 534f11a993..ffd2a4ee34 100644 --- a/packages/nodes-base/nodes/Box/BoxTrigger.node.ts +++ b/packages/nodes-base/nodes/Box/BoxTrigger.node.ts @@ -21,7 +21,7 @@ export class BoxTrigger implements INodeType { icon: 'file:box.png', group: ['trigger'], version: 1, - description: 'Starts the workflow when a Box events occurs.', + description: 'Starts the workflow when Box events occur', defaults: { name: 'Box Trigger', color: '#00aeef', diff --git a/packages/nodes-base/nodes/Calendly/CalendlyTrigger.node.ts b/packages/nodes-base/nodes/Calendly/CalendlyTrigger.node.ts index 227f1e2cef..0a33857d5e 100644 --- a/packages/nodes-base/nodes/Calendly/CalendlyTrigger.node.ts +++ b/packages/nodes-base/nodes/Calendly/CalendlyTrigger.node.ts @@ -20,7 +20,7 @@ export class CalendlyTrigger implements INodeType { icon: 'file:calendly.svg', group: ['trigger'], version: 1, - description: 'Starts the workflow when Calendly events occur.', + description: 'Starts the workflow when Calendly events occur', defaults: { name: 'Calendly Trigger', color: '#374252', diff --git a/packages/nodes-base/nodes/Chargebee/ChargebeeTrigger.node.ts b/packages/nodes-base/nodes/Chargebee/ChargebeeTrigger.node.ts index 2c0f7fafcd..fb7fcdf035 100644 --- a/packages/nodes-base/nodes/Chargebee/ChargebeeTrigger.node.ts +++ b/packages/nodes-base/nodes/Chargebee/ChargebeeTrigger.node.ts @@ -17,7 +17,7 @@ export class ChargebeeTrigger implements INodeType { icon: 'file:chargebee.png', group: ['trigger'], version: 1, - description: 'Starts the workflow when Chargebee events occur.', + description: 'Starts the workflow when Chargebee events occur', defaults: { name: 'Chargebee Trigger', color: '#559922', diff --git a/packages/nodes-base/nodes/Clockify/ClockifyTrigger.node.ts b/packages/nodes-base/nodes/Clockify/ClockifyTrigger.node.ts index 5558862ba7..047a1b5ef0 100644 --- a/packages/nodes-base/nodes/Clockify/ClockifyTrigger.node.ts +++ b/packages/nodes-base/nodes/Clockify/ClockifyTrigger.node.ts @@ -26,7 +26,7 @@ export class ClockifyTrigger implements INodeType { name: 'clockifyTrigger', group: [ 'trigger' ], version: 1, - description: 'Watches Clockify For Events', + description: 'Listens to Clockify events', defaults: { name: 'Clockify Trigger', color: '#000000', diff --git a/packages/nodes-base/nodes/ConvertKit/ConvertKit.node.ts b/packages/nodes-base/nodes/ConvertKit/ConvertKit.node.ts index 806d2d942e..2d7ed96c52 100644 --- a/packages/nodes-base/nodes/ConvertKit/ConvertKit.node.ts +++ b/packages/nodes-base/nodes/ConvertKit/ConvertKit.node.ts @@ -48,7 +48,7 @@ export class ConvertKit implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume ConvertKit API.', + description: 'Consume ConvertKit API', defaults: { name: 'ConvertKit', color: '#fb6970', diff --git a/packages/nodes-base/nodes/CrateDb/CrateDb.node.ts b/packages/nodes-base/nodes/CrateDb/CrateDb.node.ts index 4dc1130b52..659001eec1 100644 --- a/packages/nodes-base/nodes/CrateDb/CrateDb.node.ts +++ b/packages/nodes-base/nodes/CrateDb/CrateDb.node.ts @@ -25,7 +25,7 @@ export class CrateDb implements INodeType { icon: 'file:cratedb.png', group: ['input'], version: 1, - description: 'Add and update data in CrateDB.', + description: 'Add and update data in CrateDB', defaults: { name: 'CrateDB', color: '#47889f', diff --git a/packages/nodes-base/nodes/CustomerIo/CustomerIoTrigger.node.ts b/packages/nodes-base/nodes/CustomerIo/CustomerIoTrigger.node.ts index b39a48333b..e0de9a5df8 100644 --- a/packages/nodes-base/nodes/CustomerIo/CustomerIoTrigger.node.ts +++ b/packages/nodes-base/nodes/CustomerIo/CustomerIoTrigger.node.ts @@ -31,7 +31,7 @@ export class CustomerIoTrigger implements INodeType { group: ['trigger'], icon: 'file:customerio.svg', version: 1, - description: 'Starts the workflow on a Customer.io update. (Beta)', + description: 'Starts the workflow on a Customer.io update (Beta)', defaults: { name: 'Customer.io Trigger', color: '#ffcd00', diff --git a/packages/nodes-base/nodes/Discourse/Discourse.node.ts b/packages/nodes-base/nodes/Discourse/Discourse.node.ts index 78f68374f5..7110e0759b 100644 --- a/packages/nodes-base/nodes/Discourse/Discourse.node.ts +++ b/packages/nodes-base/nodes/Discourse/Discourse.node.ts @@ -55,7 +55,7 @@ export class Discourse implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Discourse API.', + description: 'Consume Discourse API', defaults: { name: 'Discourse', color: '#000000', diff --git a/packages/nodes-base/nodes/EmailReadImap.node.ts b/packages/nodes-base/nodes/EmailReadImap.node.ts index fb90534a7c..1a046287b1 100644 --- a/packages/nodes-base/nodes/EmailReadImap.node.ts +++ b/packages/nodes-base/nodes/EmailReadImap.node.ts @@ -35,7 +35,7 @@ export class EmailReadImap implements INodeType { icon: 'fa:inbox', group: ['trigger'], version: 1, - description: 'Triggers the workflow when a new email gets received', + description: 'Triggers the workflow when a new email is received', defaults: { name: 'IMAP Email', color: '#44AA22', diff --git a/packages/nodes-base/nodes/ExecuteCommand.node.ts b/packages/nodes-base/nodes/ExecuteCommand.node.ts index 55494ce9f2..acf72d9dc7 100644 --- a/packages/nodes-base/nodes/ExecuteCommand.node.ts +++ b/packages/nodes-base/nodes/ExecuteCommand.node.ts @@ -51,7 +51,7 @@ export class ExecuteCommand implements INodeType { icon: 'fa:terminal', group: ['transform'], version: 1, - description: 'Executes a command on the host.', + description: 'Executes a command on the host', defaults: { name: 'Execute Command', color: '#886644', diff --git a/packages/nodes-base/nodes/Facebook/FacebookTrigger.node.ts b/packages/nodes-base/nodes/Facebook/FacebookTrigger.node.ts index 2f68521790..738134a81e 100644 --- a/packages/nodes-base/nodes/Facebook/FacebookTrigger.node.ts +++ b/packages/nodes-base/nodes/Facebook/FacebookTrigger.node.ts @@ -33,7 +33,7 @@ export class FacebookTrigger implements INodeType { group: ['trigger'], version: 1, subtitle: '={{$parameter["appId"] +"/"+ $parameter["object"]}}', - description: 'Starts the workflow when a Facebook events occurs.', + description: 'Starts the workflow when Facebook events occur', defaults: { name: 'Facebook Trigger', color: '#3B5998', diff --git a/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts b/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts index 402ff35f9d..8ddd3c2bc0 100644 --- a/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts +++ b/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts @@ -30,7 +30,7 @@ export class FileMaker implements INodeType { icon: 'file:filemaker.png', group: ['input'], version: 1, - description: 'Retrieve data from FileMaker data API.', + description: 'Retrieve data from the FileMaker data API', defaults: { name: 'FileMaker', color: '#665533', diff --git a/packages/nodes-base/nodes/Ftp.node.ts b/packages/nodes-base/nodes/Ftp.node.ts index ef254b055d..27f20a079e 100644 --- a/packages/nodes-base/nodes/Ftp.node.ts +++ b/packages/nodes-base/nodes/Ftp.node.ts @@ -45,7 +45,7 @@ export class Ftp implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["protocol"] + ": " + $parameter["operation"]}}', - description: 'Transfers files via FTP or SFTP.', + description: 'Transfers files via FTP or SFTP', defaults: { name: 'FTP', color: '#303050', diff --git a/packages/nodes-base/nodes/Function.node.ts b/packages/nodes-base/nodes/Function.node.ts index 3058468dae..6fcb75a244 100644 --- a/packages/nodes-base/nodes/Function.node.ts +++ b/packages/nodes-base/nodes/Function.node.ts @@ -15,7 +15,7 @@ export class Function implements INodeType { icon: 'fa:code', group: ['transform'], version: 1, - description: 'Run custom function code which gets executed once and allows to add, remove, change and replace items.', + description: 'Run custom function code which gets executed once and allows you to add, remove, change and replace items', defaults: { name: 'Function', color: '#FF9922', diff --git a/packages/nodes-base/nodes/FunctionItem.node.ts b/packages/nodes-base/nodes/FunctionItem.node.ts index 88142fa530..64cb55be2f 100644 --- a/packages/nodes-base/nodes/FunctionItem.node.ts +++ b/packages/nodes-base/nodes/FunctionItem.node.ts @@ -17,7 +17,7 @@ export class FunctionItem implements INodeType { icon: 'fa:code', group: ['transform'], version: 1, - description: 'Run custom function code which gets executed once per item.', + description: 'Run custom function code which gets executed once per item', defaults: { name: 'FunctionItem', color: '#ddbb33', diff --git a/packages/nodes-base/nodes/GetResponse/GetResponse.node.ts b/packages/nodes-base/nodes/GetResponse/GetResponse.node.ts index 09e772e1ac..0cdbc66ef5 100644 --- a/packages/nodes-base/nodes/GetResponse/GetResponse.node.ts +++ b/packages/nodes-base/nodes/GetResponse/GetResponse.node.ts @@ -31,7 +31,7 @@ export class GetResponse implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume GetResponse API.', + description: 'Consume GetResponse API', defaults: { name: 'GetResponse', color: '#00afec', diff --git a/packages/nodes-base/nodes/GetResponse/GetResponseTrigger.node.ts b/packages/nodes-base/nodes/GetResponse/GetResponseTrigger.node.ts index c1bd9cbbf7..911f82f94d 100644 --- a/packages/nodes-base/nodes/GetResponse/GetResponseTrigger.node.ts +++ b/packages/nodes-base/nodes/GetResponse/GetResponseTrigger.node.ts @@ -26,7 +26,7 @@ export class GetResponseTrigger implements INodeType { icon: 'file:getResponse.png', group: ['trigger'], version: 1, - description: 'Starts the workflow when GetResponse events occur.', + description: 'Starts the workflow when GetResponse events occur', defaults: { name: 'GetResponse Trigger', color: '#00afec', diff --git a/packages/nodes-base/nodes/Ghost/Ghost.node.ts b/packages/nodes-base/nodes/Ghost/Ghost.node.ts index 241ae39ec9..0e52947639 100644 --- a/packages/nodes-base/nodes/Ghost/Ghost.node.ts +++ b/packages/nodes-base/nodes/Ghost/Ghost.node.ts @@ -33,7 +33,7 @@ export class Ghost implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Ghost API.', + description: 'Consume Ghost API', defaults: { name: 'Ghost', color: '#15212a', diff --git a/packages/nodes-base/nodes/Github/Github.node.ts b/packages/nodes-base/nodes/Github/Github.node.ts index c1b27ffa02..d39fa72b5f 100644 --- a/packages/nodes-base/nodes/Github/Github.node.ts +++ b/packages/nodes-base/nodes/Github/Github.node.ts @@ -28,7 +28,7 @@ export class Github implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume GitHub API.', + description: 'Consume GitHub API', defaults: { name: 'GitHub', color: '#000000', diff --git a/packages/nodes-base/nodes/Github/GithubTrigger.node.ts b/packages/nodes-base/nodes/Github/GithubTrigger.node.ts index 64a5b6faf0..039173cb3f 100644 --- a/packages/nodes-base/nodes/Github/GithubTrigger.node.ts +++ b/packages/nodes-base/nodes/Github/GithubTrigger.node.ts @@ -25,7 +25,7 @@ export class GithubTrigger implements INodeType { group: ['trigger'], version: 1, subtitle: '={{$parameter["owner"] + "/" + $parameter["repository"] + ": " + $parameter["events"].join(", ")}}', - description: 'Starts the workflow when a Github events occurs.', + description: 'Starts the workflow when Github events occur', defaults: { name: 'Github Trigger', color: '#000000', diff --git a/packages/nodes-base/nodes/Gitlab/Gitlab.node.ts b/packages/nodes-base/nodes/Gitlab/Gitlab.node.ts index 4b0f10d9fe..fc2a175430 100644 --- a/packages/nodes-base/nodes/Gitlab/Gitlab.node.ts +++ b/packages/nodes-base/nodes/Gitlab/Gitlab.node.ts @@ -23,7 +23,7 @@ export class Gitlab implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Retrieve data from GitLab API.', + description: 'Retrieve data from GitLab API', defaults: { name: 'Gitlab', color: '#FC6D27', diff --git a/packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts b/packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts index 9098d987b7..3fda35e8ef 100644 --- a/packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts +++ b/packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts @@ -24,7 +24,7 @@ export class GitlabTrigger implements INodeType { group: ['trigger'], version: 1, subtitle: '={{$parameter["owner"] + "/" + $parameter["repository"] + ": " + $parameter["events"].join(", ")}}', - description: 'Starts the workflow when a GitLab event occurs.', + description: 'Starts the workflow when GitLab events occur', defaults: { name: 'Gitlab Trigger', color: '#FC6D27', diff --git a/packages/nodes-base/nodes/Google/BigQuery/GoogleBigQuery.node.ts b/packages/nodes-base/nodes/Google/BigQuery/GoogleBigQuery.node.ts index 77423a3301..fef36f86f1 100644 --- a/packages/nodes-base/nodes/Google/BigQuery/GoogleBigQuery.node.ts +++ b/packages/nodes-base/nodes/Google/BigQuery/GoogleBigQuery.node.ts @@ -32,7 +32,7 @@ export class GoogleBigQuery implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Google BigQuery API.', + description: 'Consume Google BigQuery API', defaults: { name: 'Google BigQuery', color: '#3E87E4', diff --git a/packages/nodes-base/nodes/Google/Calendar/GoogleCalendar.node.ts b/packages/nodes-base/nodes/Google/Calendar/GoogleCalendar.node.ts index 5c2d4a4d76..6cf7e674df 100644 --- a/packages/nodes-base/nodes/Google/Calendar/GoogleCalendar.node.ts +++ b/packages/nodes-base/nodes/Google/Calendar/GoogleCalendar.node.ts @@ -44,7 +44,7 @@ export class GoogleCalendar implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Google Calendar API.', + description: 'Consume Google Calendar API', defaults: { name: 'Google Calendar', color: '#3E87E4', diff --git a/packages/nodes-base/nodes/Google/Contacts/GoogleContacts.node.ts b/packages/nodes-base/nodes/Google/Contacts/GoogleContacts.node.ts index 0caa44bf7a..e777e943ef 100644 --- a/packages/nodes-base/nodes/Google/Contacts/GoogleContacts.node.ts +++ b/packages/nodes-base/nodes/Google/Contacts/GoogleContacts.node.ts @@ -33,7 +33,7 @@ export class GoogleContacts implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Google Contacts API.', + description: 'Consume Google Contacts API', defaults: { name: 'Google Contacts', color: '#1a73e8', diff --git a/packages/nodes-base/nodes/Google/Drive/GoogleDriveTrigger.node.ts b/packages/nodes-base/nodes/Google/Drive/GoogleDriveTrigger.node.ts index 3e0f7a31f3..e594b4656e 100644 --- a/packages/nodes-base/nodes/Google/Drive/GoogleDriveTrigger.node.ts +++ b/packages/nodes-base/nodes/Google/Drive/GoogleDriveTrigger.node.ts @@ -24,7 +24,7 @@ // group: ['trigger'], // version: 1, // subtitle: '={{$parameter["owner"] + "/" + $parameter["repository"] + ": " + $parameter["events"].join(", ")}}', -// description: 'Starts the workflow when a file on Google Drive got changed.', +// description: 'Starts the workflow when a file on Google Drive is changed', // defaults: { // name: 'Google Drive Trigger', // color: '#3f87f2', diff --git a/packages/nodes-base/nodes/Google/Task/GoogleTasks.node.ts b/packages/nodes-base/nodes/Google/Task/GoogleTasks.node.ts index ae711478c7..29c945d9db 100644 --- a/packages/nodes-base/nodes/Google/Task/GoogleTasks.node.ts +++ b/packages/nodes-base/nodes/Google/Task/GoogleTasks.node.ts @@ -29,7 +29,7 @@ export class GoogleTasks implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Google Tasks API.', + description: 'Consume Google Tasks API', defaults: { name: 'Google Tasks', color: '#3E87E4', diff --git a/packages/nodes-base/nodes/Google/YouTube/YouTube.node.ts b/packages/nodes-base/nodes/Google/YouTube/YouTube.node.ts index 9179277488..016634aaa5 100644 --- a/packages/nodes-base/nodes/Google/YouTube/YouTube.node.ts +++ b/packages/nodes-base/nodes/Google/YouTube/YouTube.node.ts @@ -55,7 +55,7 @@ export class YouTube implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume YouTube API.', + description: 'Consume YouTube API', defaults: { name: 'YouTube', color: '#FF0000', diff --git a/packages/nodes-base/nodes/Gotify/Gotify.node.ts b/packages/nodes-base/nodes/Gotify/Gotify.node.ts index 1c6179efc6..03e3eec7e2 100644 --- a/packages/nodes-base/nodes/Gotify/Gotify.node.ts +++ b/packages/nodes-base/nodes/Gotify/Gotify.node.ts @@ -22,7 +22,7 @@ export class Gotify implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Gotify API.', + description: 'Consume Gotify API', defaults: { name: 'Gotify', color: '#71c8ec', diff --git a/packages/nodes-base/nodes/HelpScout/HelpScout.node.ts b/packages/nodes-base/nodes/HelpScout/HelpScout.node.ts index 73dab6fef1..b2756f1a68 100644 --- a/packages/nodes-base/nodes/HelpScout/HelpScout.node.ts +++ b/packages/nodes-base/nodes/HelpScout/HelpScout.node.ts @@ -64,7 +64,7 @@ export class HelpScout implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Help Scout API.', + description: 'Consume HelpScout API', defaults: { name: 'HelpScout', color: '#1392ee', diff --git a/packages/nodes-base/nodes/HelpScout/HelpScoutTrigger.node.ts b/packages/nodes-base/nodes/HelpScout/HelpScoutTrigger.node.ts index 8ca2d098ec..292272a120 100644 --- a/packages/nodes-base/nodes/HelpScout/HelpScoutTrigger.node.ts +++ b/packages/nodes-base/nodes/HelpScout/HelpScoutTrigger.node.ts @@ -26,7 +26,7 @@ export class HelpScoutTrigger implements INodeType { icon: 'file:helpScout.svg', group: ['trigger'], version: 1, - description: 'Starts the workflow when HelpScout events occur.', + description: 'Starts the workflow when HelpScout events occur', defaults: { name: 'HelpScout Trigger', color: '#1392ee', diff --git a/packages/nodes-base/nodes/HttpRequest.node.ts b/packages/nodes-base/nodes/HttpRequest.node.ts index 0d9ac472c0..ae26e3dd98 100644 --- a/packages/nodes-base/nodes/HttpRequest.node.ts +++ b/packages/nodes-base/nodes/HttpRequest.node.ts @@ -32,7 +32,7 @@ export class HttpRequest implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["requestMethod"] + ": " + $parameter["url"]}}', - description: 'Makes a HTTP request and returns the received data', + description: 'Makes an HTTP request and returns the response data', defaults: { name: 'HTTP Request', color: '#2200DD', diff --git a/packages/nodes-base/nodes/Hubspot/HubspotTrigger.node.ts b/packages/nodes-base/nodes/Hubspot/HubspotTrigger.node.ts index 6281780e20..d6224f3948 100644 --- a/packages/nodes-base/nodes/Hubspot/HubspotTrigger.node.ts +++ b/packages/nodes-base/nodes/Hubspot/HubspotTrigger.node.ts @@ -37,7 +37,7 @@ export class HubspotTrigger implements INodeType { icon: 'file:hubspot.svg', group: ['trigger'], version: 1, - description: 'Starts the workflow when HubSpot events occur.', + description: 'Starts the workflow when HubSpot events occur', defaults: { name: 'Hubspot Trigger', color: '#ff7f64', diff --git a/packages/nodes-base/nodes/If.node.ts b/packages/nodes-base/nodes/If.node.ts index 278162d606..2a2a3bd0bd 100644 --- a/packages/nodes-base/nodes/If.node.ts +++ b/packages/nodes-base/nodes/If.node.ts @@ -16,7 +16,7 @@ export class If implements INodeType { icon: 'fa:map-signs', group: ['transform'], version: 1, - description: 'Splits a stream depending on defined compare operations.', + description: 'Splits a stream based on comparisons', defaults: { name: 'IF', color: '#408000', diff --git a/packages/nodes-base/nodes/Intercom/Intercom.node.ts b/packages/nodes-base/nodes/Intercom/Intercom.node.ts index d1f677dbcb..ac53077900 100644 --- a/packages/nodes-base/nodes/Intercom/Intercom.node.ts +++ b/packages/nodes-base/nodes/Intercom/Intercom.node.ts @@ -47,7 +47,7 @@ export class Intercom implements INodeType { group: ['output'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume intercom API', + description: 'Consume Intercom API', defaults: { name: 'Intercom', color: '#0575f3', diff --git a/packages/nodes-base/nodes/InvoiceNinja/InvoiceNinjaTrigger.node.ts b/packages/nodes-base/nodes/InvoiceNinja/InvoiceNinjaTrigger.node.ts index daffd871fc..1b1cce0dc9 100644 --- a/packages/nodes-base/nodes/InvoiceNinja/InvoiceNinjaTrigger.node.ts +++ b/packages/nodes-base/nodes/InvoiceNinja/InvoiceNinjaTrigger.node.ts @@ -20,7 +20,7 @@ export class InvoiceNinjaTrigger implements INodeType { icon: 'file:invoiceNinja.svg', group: ['trigger'], version: 1, - description: 'Starts the workflow when Invoice Ninja events occur.', + description: 'Starts the workflow when Invoice Ninja events occur', defaults: { name: 'Invoice Ninja Trigger', color: '#000000', diff --git a/packages/nodes-base/nodes/Iterable/Iterable.node.ts b/packages/nodes-base/nodes/Iterable/Iterable.node.ts index bde2a5acf2..44356f2143 100644 --- a/packages/nodes-base/nodes/Iterable/Iterable.node.ts +++ b/packages/nodes-base/nodes/Iterable/Iterable.node.ts @@ -42,7 +42,7 @@ export class Iterable implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Iterable API.', + description: 'Consume Iterable API', defaults: { name: 'Iterable', color: '#725ed8', diff --git a/packages/nodes-base/nodes/Jira/JiraTrigger.node.ts b/packages/nodes-base/nodes/Jira/JiraTrigger.node.ts index 64ad396372..af66c852cc 100644 --- a/packages/nodes-base/nodes/Jira/JiraTrigger.node.ts +++ b/packages/nodes-base/nodes/Jira/JiraTrigger.node.ts @@ -26,7 +26,7 @@ export class JiraTrigger implements INodeType { icon: 'file:jira.svg', group: ['trigger'], version: 1, - description: 'Starts the workflow when Jira events occurs.', + description: 'Starts the workflow when Jira events occur', defaults: { name: 'Jira Trigger', color: '#4185f7', diff --git a/packages/nodes-base/nodes/Keap/Keap.node.ts b/packages/nodes-base/nodes/Keap/Keap.node.ts index 8ddcf53f13..d335af4aa1 100644 --- a/packages/nodes-base/nodes/Keap/Keap.node.ts +++ b/packages/nodes-base/nodes/Keap/Keap.node.ts @@ -110,7 +110,7 @@ export class Keap implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Keap API.', + description: 'Consume Keap API', defaults: { name: 'Keap', color: '#79af53', diff --git a/packages/nodes-base/nodes/Keap/KeapTrigger.node.ts b/packages/nodes-base/nodes/Keap/KeapTrigger.node.ts index 4ea0ef21a9..4474bc2a2e 100644 --- a/packages/nodes-base/nodes/Keap/KeapTrigger.node.ts +++ b/packages/nodes-base/nodes/Keap/KeapTrigger.node.ts @@ -28,7 +28,7 @@ export class KeapTrigger implements INodeType { group: ['trigger'], version: 1, subtitle: '={{$parameter["eventId"]}}', - description: 'Starts the workflow when Infusionsoft events occur.', + description: 'Starts the workflow when Infusionsoft events occur', defaults: { name: 'Keap Trigger', color: '#79af53', diff --git a/packages/nodes-base/nodes/Line/Line.node.ts b/packages/nodes-base/nodes/Line/Line.node.ts index b4681c3ec3..57ae68b263 100644 --- a/packages/nodes-base/nodes/Line/Line.node.ts +++ b/packages/nodes-base/nodes/Line/Line.node.ts @@ -29,7 +29,7 @@ export class Line implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Line API.', + description: 'Consume Line API', defaults: { name: 'Line', color: '#00b900', diff --git a/packages/nodes-base/nodes/LinkedIn/LinkedIn.node.ts b/packages/nodes-base/nodes/LinkedIn/LinkedIn.node.ts index 2f7d3e0da9..3a93c55f7d 100644 --- a/packages/nodes-base/nodes/LinkedIn/LinkedIn.node.ts +++ b/packages/nodes-base/nodes/LinkedIn/LinkedIn.node.ts @@ -24,7 +24,7 @@ export class LinkedIn implements INodeType { icon: 'file:linkedin.png', group: ['input'], version: 1, - description: 'Consume LinkedIn Api', + description: 'Consume LinkedIn API', defaults: { name: 'LinkedIn', color: '#0075b4', diff --git a/packages/nodes-base/nodes/MailerLite/MailerLite.node.ts b/packages/nodes-base/nodes/MailerLite/MailerLite.node.ts index f45e900a38..1e383fd0b8 100644 --- a/packages/nodes-base/nodes/MailerLite/MailerLite.node.ts +++ b/packages/nodes-base/nodes/MailerLite/MailerLite.node.ts @@ -29,7 +29,7 @@ export class MailerLite implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Mailer Lite API.', + description: 'Consume Mailer Lite API', defaults: { name: 'MailerLite', color: '#58be72', diff --git a/packages/nodes-base/nodes/MailerLite/MailerLiteTrigger.node.ts b/packages/nodes-base/nodes/MailerLite/MailerLiteTrigger.node.ts index 658e059af0..a527c74fb6 100644 --- a/packages/nodes-base/nodes/MailerLite/MailerLiteTrigger.node.ts +++ b/packages/nodes-base/nodes/MailerLite/MailerLiteTrigger.node.ts @@ -21,7 +21,7 @@ export class MailerLiteTrigger implements INodeType { icon: 'file:mailerLite.png', group: ['trigger'], version: 1, - description: 'Starts the workflow when a MailerLite events occurs.', + description: 'Starts the workflow when MailerLite events occur', defaults: { name: 'MailerLite Trigger', color: '#58be72', diff --git a/packages/nodes-base/nodes/Mailgun/Mailgun.node.ts b/packages/nodes-base/nodes/Mailgun/Mailgun.node.ts index 596fed5147..b59db606d0 100644 --- a/packages/nodes-base/nodes/Mailgun/Mailgun.node.ts +++ b/packages/nodes-base/nodes/Mailgun/Mailgun.node.ts @@ -19,7 +19,7 @@ export class Mailgun implements INodeType { icon: 'file:mailgun.svg', group: ['output'], version: 1, - description: 'Sends an Email via Mailgun', + description: 'Sends an email via Mailgun', defaults: { name: 'Mailgun', color: '#c02428', diff --git a/packages/nodes-base/nodes/Mandrill/Mandrill.node.ts b/packages/nodes-base/nodes/Mandrill/Mandrill.node.ts index c29f66d0df..74a2a6fe27 100644 --- a/packages/nodes-base/nodes/Mandrill/Mandrill.node.ts +++ b/packages/nodes-base/nodes/Mandrill/Mandrill.node.ts @@ -105,7 +105,7 @@ export class Mandrill implements INodeType { group: ['output'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume mandrill API', + description: 'Consume Mandrill API', defaults: { name: 'Mandrill', color: '#c02428', diff --git a/packages/nodes-base/nodes/Merge.node.ts b/packages/nodes-base/nodes/Merge.node.ts index 7a8a0a3ac1..a6cb8ab4f7 100644 --- a/packages/nodes-base/nodes/Merge.node.ts +++ b/packages/nodes-base/nodes/Merge.node.ts @@ -17,7 +17,7 @@ export class Merge implements INodeType { group: ['transform'], version: 1, subtitle: '={{$parameter["mode"]}}', - description: 'Merges data of multiple streams once data of both is available', + description: 'Merges data of multiple streams once data from both is available', defaults: { name: 'Merge', color: '#00bbcc', diff --git a/packages/nodes-base/nodes/MessageBird/MessageBird.node.ts b/packages/nodes-base/nodes/MessageBird/MessageBird.node.ts index d61b7ccb33..c6b767a6f9 100644 --- a/packages/nodes-base/nodes/MessageBird/MessageBird.node.ts +++ b/packages/nodes-base/nodes/MessageBird/MessageBird.node.ts @@ -22,7 +22,7 @@ export class MessageBird implements INodeType { group: ['output'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Sending SMS', + description: 'Sends SMS via MessageBird', defaults: { name: 'MessageBird', color: '#2481d7', diff --git a/packages/nodes-base/nodes/Microsoft/Excel/MicrosoftExcel.node.ts b/packages/nodes-base/nodes/Microsoft/Excel/MicrosoftExcel.node.ts index 02f43789e0..22f11ec238 100644 --- a/packages/nodes-base/nodes/Microsoft/Excel/MicrosoftExcel.node.ts +++ b/packages/nodes-base/nodes/Microsoft/Excel/MicrosoftExcel.node.ts @@ -42,7 +42,7 @@ export class MicrosoftExcel implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Microsoft Excel API.', + description: 'Consume Microsoft Excel API', defaults: { name: 'Microsoft Excel', color: '#1c6d40', diff --git a/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.ts b/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.ts index 0574c1fe9d..65e374b343 100644 --- a/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.ts +++ b/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.ts @@ -36,7 +36,7 @@ export class MicrosoftOneDrive implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Microsoft OneDrive API.', + description: 'Consume Microsoft OneDrive API', defaults: { name: 'Microsoft OneDrive', color: '#1d4bab', diff --git a/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts b/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts index 3043600c99..db164c424b 100644 --- a/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts +++ b/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts @@ -38,7 +38,7 @@ export class MicrosoftSql implements INodeType { icon: 'file:mssql.svg', group: ['input'], version: 1, - description: 'Gets, add and update data in Microsoft SQL.', + description: 'Get, add and update data in Microsoft SQL', defaults: { name: 'Microsoft SQL', color: '#bcbcbd', diff --git a/packages/nodes-base/nodes/Mindee/Mindee.node.ts b/packages/nodes-base/nodes/Mindee/Mindee.node.ts index be504e88c9..53c3046d30 100644 --- a/packages/nodes-base/nodes/Mindee/Mindee.node.ts +++ b/packages/nodes-base/nodes/Mindee/Mindee.node.ts @@ -26,7 +26,7 @@ export class Mindee implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Mindee API.', + description: 'Consume Mindee API', defaults: { name: 'Mindee', color: '#e94950', diff --git a/packages/nodes-base/nodes/Mocean/Mocean.node.ts b/packages/nodes-base/nodes/Mocean/Mocean.node.ts index 3c2e9facfd..ae2f87ba43 100644 --- a/packages/nodes-base/nodes/Mocean/Mocean.node.ts +++ b/packages/nodes-base/nodes/Mocean/Mocean.node.ts @@ -17,7 +17,7 @@ export class Mocean implements INodeType { icon: 'file:mocean.png', group: ['transform'], version: 1, - description: 'Send SMS & voice messages via Mocean (https://moceanapi.com)', + description: 'Send SMS and voice messages via Mocean', defaults: { name: 'Mocean', color: '#772244', diff --git a/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts b/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts index 6d863987cc..f6e9601214 100644 --- a/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts +++ b/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts @@ -11,7 +11,7 @@ export const nodeDescription: INodeTypeDescription = { icon: 'file:mongodb.svg', group: ['input'], version: 1, - description: 'Find, insert and update documents in MongoDB.', + description: 'Find, insert and update documents in MongoDB', defaults: { name: 'MongoDB', color: '#13AA52', diff --git a/packages/nodes-base/nodes/MoveBinaryData.node.ts b/packages/nodes-base/nodes/MoveBinaryData.node.ts index 79e74f8244..ed6454d581 100644 --- a/packages/nodes-base/nodes/MoveBinaryData.node.ts +++ b/packages/nodes-base/nodes/MoveBinaryData.node.ts @@ -49,7 +49,7 @@ export class MoveBinaryData implements INodeType { group: ['transform'], version: 1, subtitle: '={{$parameter["mode"]==="binaryToJson" ? "Binary to JSON" : "JSON to Binary"}}', - description: 'Move data between binary and JSON properties.', + description: 'Move data between binary and JSON properties', defaults: { name: 'Move Binary Data', color: '#7722CC', diff --git a/packages/nodes-base/nodes/Msg91/Msg91.node.ts b/packages/nodes-base/nodes/Msg91/Msg91.node.ts index 6dc6b14b0a..9f605d7ee8 100644 --- a/packages/nodes-base/nodes/Msg91/Msg91.node.ts +++ b/packages/nodes-base/nodes/Msg91/Msg91.node.ts @@ -20,7 +20,7 @@ export class Msg91 implements INodeType { group: ['transform'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Send Transactional SMS', + description: 'Sends transactional SMS via MSG91', defaults: { name: 'Msg91', color: '#0000ff', diff --git a/packages/nodes-base/nodes/MySql/MySql.node.ts b/packages/nodes-base/nodes/MySql/MySql.node.ts index fd1de1af07..20eebf52de 100644 --- a/packages/nodes-base/nodes/MySql/MySql.node.ts +++ b/packages/nodes-base/nodes/MySql/MySql.node.ts @@ -18,7 +18,7 @@ export class MySql implements INodeType { icon: 'file:mysql.svg', group: ['input'], version: 1, - description: 'Get, add and update data in MySQL.', + description: 'Get, add and update data in MySQL', defaults: { name: 'MySQL', color: '#4279a2', diff --git a/packages/nodes-base/nodes/Nasa/Nasa.node.ts b/packages/nodes-base/nodes/Nasa/Nasa.node.ts index 5b523c5bc0..6a3d249e82 100644 --- a/packages/nodes-base/nodes/Nasa/Nasa.node.ts +++ b/packages/nodes-base/nodes/Nasa/Nasa.node.ts @@ -25,7 +25,7 @@ export class Nasa implements INodeType { group: ['transform'], version: 1, subtitle: '={{$parameter["operation"] + ":" + $parameter["resource"]}}', - description: 'Retrieve data the from NASA API', + description: 'Retrieve data from the NASA API', defaults: { name: 'NASA', color: '#0B3D91', diff --git a/packages/nodes-base/nodes/OpenWeatherMap.node.ts b/packages/nodes-base/nodes/OpenWeatherMap.node.ts index 4aaa76a4bc..305d7e8c2a 100644 --- a/packages/nodes-base/nodes/OpenWeatherMap.node.ts +++ b/packages/nodes-base/nodes/OpenWeatherMap.node.ts @@ -19,7 +19,7 @@ export class OpenWeatherMap implements INodeType { icon: 'fa:sun', group: ['input'], version: 1, - description: 'Gets current and future weather information.', + description: 'Gets current and future weather information', defaults: { name: 'OpenWeatherMap', color: '#554455', diff --git a/packages/nodes-base/nodes/Phantombuster/Phantombuster.node.ts b/packages/nodes-base/nodes/Phantombuster/Phantombuster.node.ts index 8c1616b4fe..da913acdb2 100644 --- a/packages/nodes-base/nodes/Phantombuster/Phantombuster.node.ts +++ b/packages/nodes-base/nodes/Phantombuster/Phantombuster.node.ts @@ -33,7 +33,7 @@ export class Phantombuster implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Phantombuster API.', + description: 'Consume Phantombuster API', defaults: { name: 'Phantombuster', color: '#62bfd7', diff --git a/packages/nodes-base/nodes/PhilipsHue/PhilipsHue.node.ts b/packages/nodes-base/nodes/PhilipsHue/PhilipsHue.node.ts index 908790a5cf..6286663353 100644 --- a/packages/nodes-base/nodes/PhilipsHue/PhilipsHue.node.ts +++ b/packages/nodes-base/nodes/PhilipsHue/PhilipsHue.node.ts @@ -29,7 +29,7 @@ export class PhilipsHue implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Philips Hue API.', + description: 'Consume Philips Hue API', defaults: { name: 'Philips Hue', color: '#063c9a', diff --git a/packages/nodes-base/nodes/Pipedrive/PipedriveTrigger.node.ts b/packages/nodes-base/nodes/Pipedrive/PipedriveTrigger.node.ts index eeadedf403..067f41eb45 100644 --- a/packages/nodes-base/nodes/Pipedrive/PipedriveTrigger.node.ts +++ b/packages/nodes-base/nodes/Pipedrive/PipedriveTrigger.node.ts @@ -43,7 +43,7 @@ export class PipedriveTrigger implements INodeType { icon: 'file:pipedrive.svg', group: ['trigger'], version: 1, - description: 'Starts the workflow when Pipedrive events occur.', + description: 'Starts the workflow when Pipedrive events occur', defaults: { name: 'Pipedrive Trigger', color: '#559922', diff --git a/packages/nodes-base/nodes/PostHog/PostHog.node.ts b/packages/nodes-base/nodes/PostHog/PostHog.node.ts index f56860731a..7f4dbce2c5 100644 --- a/packages/nodes-base/nodes/PostHog/PostHog.node.ts +++ b/packages/nodes-base/nodes/PostHog/PostHog.node.ts @@ -47,7 +47,7 @@ export class PostHog implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume PostHog API.', + description: 'Consume PostHog API', defaults: { name: 'PostHog', color: '#000000', diff --git a/packages/nodes-base/nodes/Postgres/Postgres.node.ts b/packages/nodes-base/nodes/Postgres/Postgres.node.ts index 30880f3f8d..2b7f49baab 100644 --- a/packages/nodes-base/nodes/Postgres/Postgres.node.ts +++ b/packages/nodes-base/nodes/Postgres/Postgres.node.ts @@ -18,7 +18,7 @@ export class Postgres implements INodeType { icon: 'file:postgres.svg', group: ['input'], version: 1, - description: 'Gets, add and update data in Postgres.', + description: 'Get, add and update data in Postgres', defaults: { name: 'Postgres', color: '#336791', diff --git a/packages/nodes-base/nodes/Postmark/PostmarkTrigger.node.ts b/packages/nodes-base/nodes/Postmark/PostmarkTrigger.node.ts index a66f099399..b85aa2d04f 100644 --- a/packages/nodes-base/nodes/Postmark/PostmarkTrigger.node.ts +++ b/packages/nodes-base/nodes/Postmark/PostmarkTrigger.node.ts @@ -22,7 +22,7 @@ export class PostmarkTrigger implements INodeType { icon: 'file:postmark.png', group: ['trigger'], version: 1, - description: 'Starts the workflow when Postmark events occur.', + description: 'Starts the workflow when Postmark events occur', defaults: { name: 'Postmark Trigger', color: '#fedd00', diff --git a/packages/nodes-base/nodes/Pushbullet/Pushbullet.node.ts b/packages/nodes-base/nodes/Pushbullet/Pushbullet.node.ts index 8e38f2c45d..ea95fe61ca 100644 --- a/packages/nodes-base/nodes/Pushbullet/Pushbullet.node.ts +++ b/packages/nodes-base/nodes/Pushbullet/Pushbullet.node.ts @@ -29,7 +29,7 @@ export class Pushbullet implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Pushbullet API.', + description: 'Consume Pushbullet API', defaults: { name: 'Pushbullet', color: '#457854', diff --git a/packages/nodes-base/nodes/Pushcut/Pushcut.node.ts b/packages/nodes-base/nodes/Pushcut/Pushcut.node.ts index 28fb18cb97..27d7242ebe 100644 --- a/packages/nodes-base/nodes/Pushcut/Pushcut.node.ts +++ b/packages/nodes-base/nodes/Pushcut/Pushcut.node.ts @@ -23,7 +23,7 @@ export class Pushcut implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Pushcut API.', + description: 'Consume Pushcut API', defaults: { name: 'Pushcut', color: '#1f2957', diff --git a/packages/nodes-base/nodes/Pushcut/PushcutTrigger.node.ts b/packages/nodes-base/nodes/Pushcut/PushcutTrigger.node.ts index 3e7e4df6c4..7922c70fbf 100644 --- a/packages/nodes-base/nodes/Pushcut/PushcutTrigger.node.ts +++ b/packages/nodes-base/nodes/Pushcut/PushcutTrigger.node.ts @@ -21,7 +21,7 @@ export class PushcutTrigger implements INodeType { icon: 'file:pushcut.png', group: ['trigger'], version: 1, - description: 'Starts the workflow when a Pushcut events occurs.', + description: 'Starts the workflow when Pushcut events occur', defaults: { name: 'Pushcut Trigger', color: '#1f2957', diff --git a/packages/nodes-base/nodes/Pushover/Pushover.node.ts b/packages/nodes-base/nodes/Pushover/Pushover.node.ts index 10115f9bcb..44cc8438dd 100644 --- a/packages/nodes-base/nodes/Pushover/Pushover.node.ts +++ b/packages/nodes-base/nodes/Pushover/Pushover.node.ts @@ -27,7 +27,7 @@ export class Pushover implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Pushover API.', + description: 'Consume Pushover API', defaults: { name: 'Pushover', color: '#4b9cea', diff --git a/packages/nodes-base/nodes/QuestDb/QuestDb.node.ts b/packages/nodes-base/nodes/QuestDb/QuestDb.node.ts index 03e26d41ed..50a4e7c9e4 100644 --- a/packages/nodes-base/nodes/QuestDb/QuestDb.node.ts +++ b/packages/nodes-base/nodes/QuestDb/QuestDb.node.ts @@ -21,7 +21,7 @@ export class QuestDb implements INodeType { icon: 'file:questdb.png', group: ['input'], version: 1, - description: 'Gets, add and update data in QuestDB.', + description: 'Get, add and update data in QuestDB', defaults: { name: 'QuestDB', color: '#2C4A79', diff --git a/packages/nodes-base/nodes/QuickBase/QuickBase.node.ts b/packages/nodes-base/nodes/QuickBase/QuickBase.node.ts index b26128c22d..c7e072ee75 100644 --- a/packages/nodes-base/nodes/QuickBase/QuickBase.node.ts +++ b/packages/nodes-base/nodes/QuickBase/QuickBase.node.ts @@ -46,7 +46,7 @@ export class QuickBase implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Integrate with the Quick Base RESTful API.', + description: 'Integrate with the Quick Base RESTful API', defaults: { name: 'Quick Base', color: '#73489d', diff --git a/packages/nodes-base/nodes/Redis/Redis.node.ts b/packages/nodes-base/nodes/Redis/Redis.node.ts index 012486462e..3202fa576e 100644 --- a/packages/nodes-base/nodes/Redis/Redis.node.ts +++ b/packages/nodes-base/nodes/Redis/Redis.node.ts @@ -20,7 +20,7 @@ export class Redis implements INodeType { icon: 'file:redis.svg', group: ['input'], version: 1, - description: 'Get, send and update data in Redis.', + description: 'Get, send and update data in Redis', defaults: { name: 'Redis', color: '#0033AA', diff --git a/packages/nodes-base/nodes/RenameKeys.node.ts b/packages/nodes-base/nodes/RenameKeys.node.ts index d427db9cf4..167be3234d 100644 --- a/packages/nodes-base/nodes/RenameKeys.node.ts +++ b/packages/nodes-base/nodes/RenameKeys.node.ts @@ -23,7 +23,7 @@ export class RenameKeys implements INodeType { icon: 'fa:edit', group: ['transform'], version: 1, - description: 'Renames keys.', + description: 'Renames keys', defaults: { name: 'Rename Keys', color: '#772244', diff --git a/packages/nodes-base/nodes/Sendy/Sendy.node.ts b/packages/nodes-base/nodes/Sendy/Sendy.node.ts index 7a010b8165..14548355f1 100644 --- a/packages/nodes-base/nodes/Sendy/Sendy.node.ts +++ b/packages/nodes-base/nodes/Sendy/Sendy.node.ts @@ -33,7 +33,7 @@ export class Sendy implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Sendy API.', + description: 'Consume Sendy API', defaults: { name: 'Sendy', color: '#000000', diff --git a/packages/nodes-base/nodes/Set.node.ts b/packages/nodes-base/nodes/Set.node.ts index 9c17ec7e50..01cd0ff8f0 100644 --- a/packages/nodes-base/nodes/Set.node.ts +++ b/packages/nodes-base/nodes/Set.node.ts @@ -16,7 +16,7 @@ export class Set implements INodeType { icon: 'fa:pen', group: ['input'], version: 1, - description: 'Sets values on the items and removes if selected all other values.', + description: 'Sets values on items and optionally remove other values', defaults: { name: 'Set', color: '#0000FF', diff --git a/packages/nodes-base/nodes/Signl4/Signl4.node.ts b/packages/nodes-base/nodes/Signl4/Signl4.node.ts index e54d7d0425..390d1fd1c2 100644 --- a/packages/nodes-base/nodes/Signl4/Signl4.node.ts +++ b/packages/nodes-base/nodes/Signl4/Signl4.node.ts @@ -24,7 +24,7 @@ export class Signl4 implements INodeType { group: ['transform'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume SIGNL4 API.', + description: 'Consume SIGNL4 API', defaults: { name: 'SIGNL4', color: '#53afe8', diff --git a/packages/nodes-base/nodes/Snowflake/Snowflake.node.ts b/packages/nodes-base/nodes/Snowflake/Snowflake.node.ts index cc8e753329..4da65da65a 100644 --- a/packages/nodes-base/nodes/Snowflake/Snowflake.node.ts +++ b/packages/nodes-base/nodes/Snowflake/Snowflake.node.ts @@ -25,7 +25,7 @@ export class Snowflake implements INodeType { icon: 'file:snowflake.svg', group: ['input'], version: 1, - description: 'Get, add and update data in Snowflake.', + description: 'Get, add and update data in Snowflake', defaults: { name: 'Snowflake', color: '#5ebbeb', diff --git a/packages/nodes-base/nodes/SplitInBatches.node.ts b/packages/nodes-base/nodes/SplitInBatches.node.ts index fb2fd6a582..ed7fd01d73 100644 --- a/packages/nodes-base/nodes/SplitInBatches.node.ts +++ b/packages/nodes-base/nodes/SplitInBatches.node.ts @@ -14,7 +14,7 @@ export class SplitInBatches implements INodeType { icon: 'fa:th-large', group: ['organization'], version: 1, - description: 'Saves the originally incoming data and with each iteration it returns a predefined amount of them.', + description: 'Split data into batches and iterate over each batch', defaults: { name: 'SplitInBatches', color: '#007755', diff --git a/packages/nodes-base/nodes/Spotify/Spotify.node.ts b/packages/nodes-base/nodes/Spotify/Spotify.node.ts index 1b3e4b48dd..012867f395 100644 --- a/packages/nodes-base/nodes/Spotify/Spotify.node.ts +++ b/packages/nodes-base/nodes/Spotify/Spotify.node.ts @@ -25,7 +25,7 @@ export class Spotify implements INodeType { icon: 'file:spotify.svg', group: ['input'], version: 1, - description: 'Access public song data via the Spotify API.', + description: 'Access public song data via the Spotify API', subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', defaults: { name: 'Spotify', diff --git a/packages/nodes-base/nodes/SpreadsheetFile.node.ts b/packages/nodes-base/nodes/SpreadsheetFile.node.ts index e2b8350a84..628aa0c882 100644 --- a/packages/nodes-base/nodes/SpreadsheetFile.node.ts +++ b/packages/nodes-base/nodes/SpreadsheetFile.node.ts @@ -53,7 +53,7 @@ export class SpreadsheetFile implements INodeType { icon: 'fa:table', group: ['transform'], version: 1, - description: 'Reads and writes data from a spreadsheet file.', + description: 'Reads and writes data from a spreadsheet file', defaults: { name: 'Spreadsheet File', color: '#2244FF', diff --git a/packages/nodes-base/nodes/SseTrigger.node.ts b/packages/nodes-base/nodes/SseTrigger.node.ts index 6d410a097e..69983c87c6 100644 --- a/packages/nodes-base/nodes/SseTrigger.node.ts +++ b/packages/nodes-base/nodes/SseTrigger.node.ts @@ -14,7 +14,7 @@ export class SseTrigger implements INodeType { icon: 'fa:cloud-download-alt', group: ['trigger'], version: 1, - description: 'Triggers workflow on a new Server-Sent Event', + description: 'Triggers the workflow when Server-Sent Events occur', defaults: { name: 'SSE Trigger', color: '#225577', diff --git a/packages/nodes-base/nodes/Stackby/Stackby.node.ts b/packages/nodes-base/nodes/Stackby/Stackby.node.ts index f06c6836de..6e595cd12e 100644 --- a/packages/nodes-base/nodes/Stackby/Stackby.node.ts +++ b/packages/nodes-base/nodes/Stackby/Stackby.node.ts @@ -23,7 +23,7 @@ export class Stackby implements INodeType { icon: 'file:stackby.png', group: ['transform'], version: 1, - description: 'Read, Write, and Delete Data in Stackby', + description: 'Read, write, and delete data in Stackby', defaults: { name: 'Stackby', color: '#772244', diff --git a/packages/nodes-base/nodes/Strapi/Strapi.node.ts b/packages/nodes-base/nodes/Strapi/Strapi.node.ts index 70e3357632..2091df3a9e 100644 --- a/packages/nodes-base/nodes/Strapi/Strapi.node.ts +++ b/packages/nodes-base/nodes/Strapi/Strapi.node.ts @@ -30,7 +30,7 @@ export class Strapi implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Strapi API.', + description: 'Consume Strapi API', defaults: { name: 'Strapi', color: '#725ed8', diff --git a/packages/nodes-base/nodes/Strava/Strava.node.ts b/packages/nodes-base/nodes/Strava/Strava.node.ts index 5fbf6d7b45..b420c0f519 100644 --- a/packages/nodes-base/nodes/Strava/Strava.node.ts +++ b/packages/nodes-base/nodes/Strava/Strava.node.ts @@ -29,7 +29,7 @@ export class Strava implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Strava API.', + description: 'Consume Strava API', defaults: { name: 'Strava', color: '#ea5929', diff --git a/packages/nodes-base/nodes/Strava/StravaTrigger.node.ts b/packages/nodes-base/nodes/Strava/StravaTrigger.node.ts index 0e78a63555..9500891224 100644 --- a/packages/nodes-base/nodes/Strava/StravaTrigger.node.ts +++ b/packages/nodes-base/nodes/Strava/StravaTrigger.node.ts @@ -26,7 +26,7 @@ export class StravaTrigger implements INodeType { icon: 'file:strava.svg', group: ['trigger'], version: 1, - description: 'Starts the workflow when a Strava events occurs.', + description: 'Starts the workflow when Strava events occur', defaults: { name: 'Strava Trigger', color: '#ea5929', diff --git a/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts b/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts index 8aa2b3dac1..e285003f6e 100644 --- a/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts +++ b/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts @@ -40,7 +40,7 @@ export class SurveyMonkeyTrigger implements INodeType { icon: 'file:surveyMonkey.svg', group: ['trigger'], version: 1, - description: 'Starts the workflow when Survey Monkey events occur.', + description: 'Starts the workflow when Survey Monkey events occur', defaults: { name: 'SurveyMonkey Trigger', color: '#53b675', diff --git a/packages/nodes-base/nodes/Switch.node.ts b/packages/nodes-base/nodes/Switch.node.ts index 665aac4d84..8d90399324 100644 --- a/packages/nodes-base/nodes/Switch.node.ts +++ b/packages/nodes-base/nodes/Switch.node.ts @@ -16,7 +16,7 @@ export class Switch implements INodeType { icon: 'fa:map-signs', group: ['transform'], version: 1, - description: 'Route items depending on defined expression or rules.', + description: 'Route items depending on defined expression or rules', defaults: { name: 'Switch', color: '#506000', diff --git a/packages/nodes-base/nodes/Telegram/Telegram.node.ts b/packages/nodes-base/nodes/Telegram/Telegram.node.ts index e55c477419..8f5ee6a254 100644 --- a/packages/nodes-base/nodes/Telegram/Telegram.node.ts +++ b/packages/nodes-base/nodes/Telegram/Telegram.node.ts @@ -24,7 +24,7 @@ export class Telegram implements INodeType { group: ['output'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Sends data to Telegram.', + description: 'Sends data to Telegram', defaults: { name: 'Telegram', color: '#0088cc', diff --git a/packages/nodes-base/nodes/Telegram/TelegramTrigger.node.ts b/packages/nodes-base/nodes/Telegram/TelegramTrigger.node.ts index b55b4264e0..554983cef9 100644 --- a/packages/nodes-base/nodes/Telegram/TelegramTrigger.node.ts +++ b/packages/nodes-base/nodes/Telegram/TelegramTrigger.node.ts @@ -27,7 +27,7 @@ export class TelegramTrigger implements INodeType { group: ['trigger'], version: 1, subtitle: '=Updates: {{$parameter["updates"].join(", ")}}', - description: 'Starts the workflow on a Telegram update.', + description: 'Starts the workflow on a Telegram update', defaults: { name: 'Telegram Trigger', color: '#0088cc', diff --git a/packages/nodes-base/nodes/TheHive/TheHive.node.ts b/packages/nodes-base/nodes/TheHive/TheHive.node.ts index e3021879c0..5b3a93c7f0 100644 --- a/packages/nodes-base/nodes/TheHive/TheHive.node.ts +++ b/packages/nodes-base/nodes/TheHive/TheHive.node.ts @@ -72,7 +72,7 @@ export class TheHive implements INodeType { group: ['transform'], subtitle: '={{$parameter["operation"]}} : {{$parameter["resource"]}}', version: 1, - description: 'Consume TheHive APIs', + description: 'Consume TheHive API', defaults: { name: 'TheHive', color: '#f3d02f', diff --git a/packages/nodes-base/nodes/TheHive/TheHiveTrigger.node.ts b/packages/nodes-base/nodes/TheHive/TheHiveTrigger.node.ts index fcd211d12e..82a07b5099 100644 --- a/packages/nodes-base/nodes/TheHive/TheHiveTrigger.node.ts +++ b/packages/nodes-base/nodes/TheHive/TheHiveTrigger.node.ts @@ -17,7 +17,7 @@ export class TheHiveTrigger implements INodeType { icon: 'file:thehive.svg', group: ['trigger'], version: 1, - description: 'Starts the workflow when a TheHive event occurs.', + description: 'Starts the workflow when TheHive events occur', defaults: { name: 'TheHive Trigger', color: '#f3d02f', diff --git a/packages/nodes-base/nodes/Trello/TrelloTrigger.node.ts b/packages/nodes-base/nodes/Trello/TrelloTrigger.node.ts index 0ecf55fa70..0ed7aa2e86 100644 --- a/packages/nodes-base/nodes/Trello/TrelloTrigger.node.ts +++ b/packages/nodes-base/nodes/Trello/TrelloTrigger.node.ts @@ -24,7 +24,7 @@ export class TrelloTrigger implements INodeType { icon: 'file:trello.svg', group: ['trigger'], version: 1, - description: 'Starts the workflow when Trello events occur.', + description: 'Starts the workflow when Trello events occur', defaults: { name: 'Trello Trigger', color: '#026aa7', diff --git a/packages/nodes-base/nodes/Typeform/TypeformTrigger.node.ts b/packages/nodes-base/nodes/Typeform/TypeformTrigger.node.ts index 5cd9cac97a..f6e4a4def7 100644 --- a/packages/nodes-base/nodes/Typeform/TypeformTrigger.node.ts +++ b/packages/nodes-base/nodes/Typeform/TypeformTrigger.node.ts @@ -28,7 +28,7 @@ export class TypeformTrigger implements INodeType { group: ['trigger'], version: 1, subtitle: '=Form ID: {{$parameter["formId"]}}', - description: 'Starts the workflow on a Typeform form submission.', + description: 'Starts the workflow on a Typeform form submission', defaults: { name: 'Typeform Trigger', color: '#404040', diff --git a/packages/nodes-base/nodes/Webhook.node.ts b/packages/nodes-base/nodes/Webhook.node.ts index 5526d93782..f51a0fb692 100644 --- a/packages/nodes-base/nodes/Webhook.node.ts +++ b/packages/nodes-base/nodes/Webhook.node.ts @@ -45,7 +45,7 @@ export class Webhook implements INodeType { name: 'webhook', group: ['trigger'], version: 1, - description: 'Starts the workflow when a webhook got called.', + description: 'Starts the workflow when a webhook is called', defaults: { name: 'Webhook', color: '#885577', diff --git a/packages/nodes-base/nodes/Wekan/Wekan.node.ts b/packages/nodes-base/nodes/Wekan/Wekan.node.ts index 87f99d9df2..ced73ee665 100644 --- a/packages/nodes-base/nodes/Wekan/Wekan.node.ts +++ b/packages/nodes-base/nodes/Wekan/Wekan.node.ts @@ -56,7 +56,7 @@ export class Wekan implements INodeType { group: ['transform'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Open-Source Kanban', + description: 'Consume Wekan API', defaults: { name: 'Wekan', color: '#006581', diff --git a/packages/nodes-base/nodes/Zoho/ZohoCrm.node.ts b/packages/nodes-base/nodes/Zoho/ZohoCrm.node.ts index da7c713551..ec0a90e16f 100644 --- a/packages/nodes-base/nodes/Zoho/ZohoCrm.node.ts +++ b/packages/nodes-base/nodes/Zoho/ZohoCrm.node.ts @@ -77,7 +77,7 @@ export class ZohoCrm implements INodeType { group: ['transform'], subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', version: 1, - description: 'Consume the Zoho API', + description: 'Consume Zoho CRM API', defaults: { name: 'Zoho', color: '#CE2232', From ac892343292f398325f56de4dd5e6dc1769cd61a Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sat, 3 Jul 2021 14:47:31 +0200 Subject: [PATCH 13/29] :construction_worker: Push credentials also if the tests fail --- .github/workflows/test-workflows.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test-workflows.yml b/.github/workflows/test-workflows.yml index 15e813b7ae..3a9064884d 100644 --- a/.github/workflows/test-workflows.yml +++ b/.github/workflows/test-workflows.yml @@ -69,12 +69,14 @@ jobs: N8N_ENCRYPTION_KEY: ${{secrets.ENCRYPTION_KEY}} - name: Export credentials + if: always() run: n8n/packages/cli/bin/n8n export:credentials --output=test-workflows/credentials.json --all --pretty shell: bash env: N8N_ENCRYPTION_KEY: ${{secrets.ENCRYPTION_KEY}} - name: Commit and push credential changes + if: always() run: | cd test-workflows git config --global user.name 'n8n test bot' From bac13ba821f677a733888105235f80515d7170ed Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sat, 3 Jul 2021 15:05:54 +0200 Subject: [PATCH 14/29] :construction_worker: Try to fix push to test-workflows --- .github/workflows/test-workflows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-workflows.yml b/.github/workflows/test-workflows.yml index 3a9064884d..8250a5a9b4 100644 --- a/.github/workflows/test-workflows.yml +++ b/.github/workflows/test-workflows.yml @@ -82,4 +82,4 @@ jobs: git config --global user.name 'n8n test bot' git config --global user.email 'n8n-test-bot@users.noreply.github.com' git commit -am "Automated credential update" - git push + git push --force --quiet "https://janober:${{ secrets.TOKEN }}@github.com/n8n-io/test-workflows.git" main:main From 1b46ea5d30d4d7ee026a58197ef3aca628e918eb Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sun, 4 Jul 2021 12:57:53 -0400 Subject: [PATCH 15/29] :bug: Fix issue setting passphrase correctly (#1966) --- packages/nodes-base/nodes/Ssh/Ssh.node.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/Ssh/Ssh.node.ts b/packages/nodes-base/nodes/Ssh/Ssh.node.ts index 293203e3eb..acbb3cb2c0 100644 --- a/packages/nodes-base/nodes/Ssh/Ssh.node.ts +++ b/packages/nodes-base/nodes/Ssh/Ssh.node.ts @@ -320,7 +320,7 @@ export class Ssh implements INodeType { privateKey: path, } as any; // tslint:disable-line: no-any - if (!credentials.passphrase) { + if (credentials.passphrase) { options.passphrase = credentials.passphrase as string; } From 6e5be51ceff1cffe48f2a4934c7da31c074852c0 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 4 Jul 2021 18:12:44 +0000 Subject: [PATCH 16/29] =?UTF-8?q?:bookmark:=20Release=C2=A0n8n-workflow@0.?= =?UTF-8?q?63.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/workflow/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/workflow/package.json b/packages/workflow/package.json index 0adc605196..0fa3d4943d 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -1,6 +1,6 @@ { "name": "n8n-workflow", - "version": "0.62.0", + "version": "0.63.0", "description": "Workflow base code of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From 1532d27102e5f20cce94c102cf36e9efb915d25e Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 4 Jul 2021 18:13:00 +0000 Subject: [PATCH 17/29] :arrow_up: Set n8n-workflow@0.63.0 on n8n-core --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 71871514bf..c33a6ccb04 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -47,7 +47,7 @@ "file-type": "^14.6.2", "lodash.get": "^4.4.2", "mime-types": "^2.1.27", - "n8n-workflow": "~0.62.0", + "n8n-workflow": "~0.63.0", "oauth-1.0a": "^2.2.6", "p-cancelable": "^2.0.0", "request": "^2.88.2", From 8b943738631664e44cc1f98d52e878a586f94d47 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 4 Jul 2021 18:13:01 +0000 Subject: [PATCH 18/29] =?UTF-8?q?:bookmark:=20Release=C2=A0n8n-core@0.76.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index c33a6ccb04..5151bfbb5b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "n8n-core", - "version": "0.75.0", + "version": "0.76.0", "description": "Core functionality of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From d4c47a62c03f18c15b8ee224d7406a610f9dbc1e Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 4 Jul 2021 18:13:16 +0000 Subject: [PATCH 19/29] :arrow_up: Set n8n-core@0.76.0 and n8n-workflow@0.63.0 on n8n-node-dev --- packages/node-dev/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/node-dev/package.json b/packages/node-dev/package.json index 594fe74133..99260de7ff 100644 --- a/packages/node-dev/package.json +++ b/packages/node-dev/package.json @@ -59,8 +59,8 @@ "change-case": "^4.1.1", "copyfiles": "^2.1.1", "inquirer": "^7.0.1", - "n8n-core": "~0.75.0", - "n8n-workflow": "~0.62.0", + "n8n-core": "~0.76.0", + "n8n-workflow": "~0.63.0", "oauth-1.0a": "^2.2.6", "replace-in-file": "^6.0.0", "request": "^2.88.2", From 0fcd0cb87a1a2730592fa6a56dd93d0103fc825a Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 4 Jul 2021 18:13:16 +0000 Subject: [PATCH 20/29] =?UTF-8?q?:bookmark:=20Release=C2=A0n8n-node-dev@0.?= =?UTF-8?q?16.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/node-dev/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node-dev/package.json b/packages/node-dev/package.json index 99260de7ff..e6d7bceaa9 100644 --- a/packages/node-dev/package.json +++ b/packages/node-dev/package.json @@ -1,6 +1,6 @@ { "name": "n8n-node-dev", - "version": "0.15.0", + "version": "0.16.0", "description": "CLI to simplify n8n credentials/node development", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From 0d62098571d20c9bed03e598807c73c198459378 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 4 Jul 2021 18:13:35 +0000 Subject: [PATCH 21/29] :arrow_up: Set n8n-core@0.76.0 and n8n-workflow@0.63.0 on n8n-nodes-base --- packages/nodes-base/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 7a2c4ca1ea..ea109d0926 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -611,7 +611,7 @@ "@types/xml2js": "^0.4.3", "gulp": "^4.0.0", "jest": "^26.4.2", - "n8n-workflow": "~0.62.0", + "n8n-workflow": "~0.63.0", "ts-jest": "^26.3.0", "tslint": "^6.1.2", "typescript": "~3.9.7" @@ -649,7 +649,7 @@ "mqtt": "4.2.6", "mssql": "^6.2.0", "mysql2": "~2.2.0", - "n8n-core": "~0.75.0", + "n8n-core": "~0.76.0", "node-ssh": "^11.0.0", "nodemailer": "^6.5.0", "pdf-parse": "^1.1.1", From 0b41c7466b25317948765f5813f29995cffb8228 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 4 Jul 2021 18:13:35 +0000 Subject: [PATCH 22/29] =?UTF-8?q?:bookmark:=20Release=C2=A0n8n-nodes-base@?= =?UTF-8?q?0.124.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/nodes-base/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index ea109d0926..3c42c04fd1 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -1,6 +1,6 @@ { "name": "n8n-nodes-base", - "version": "0.123.1", + "version": "0.124.0", "description": "Base nodes of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From ba7b48751798e4b819472a3753a03d578953751c Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 4 Jul 2021 18:14:34 +0000 Subject: [PATCH 23/29] :arrow_up: Set n8n-workflow@0.63.0 on n8n-editor-ui --- packages/editor-ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 54e7eb8f52..83bd501618 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -68,7 +68,7 @@ "lodash.debounce": "^4.0.8", "lodash.get": "^4.4.2", "lodash.set": "^4.3.2", - "n8n-workflow": "~0.62.0", + "n8n-workflow": "~0.63.0", "node-sass": "^4.12.0", "normalize-wheel": "^1.0.1", "prismjs": "^1.17.1", From 80a5c8ac60700651853e7a43e9e4d94a145a558f Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 4 Jul 2021 18:14:34 +0000 Subject: [PATCH 24/29] =?UTF-8?q?:bookmark:=20Release=C2=A0n8n-editor-ui@0?= =?UTF-8?q?.97.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/editor-ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 83bd501618..c7b4d1489d 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -1,6 +1,6 @@ { "name": "n8n-editor-ui", - "version": "0.96.1", + "version": "0.97.0", "description": "Workflow Editor UI for n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From 97eb22db04cdb36e11fd3dfd781244494c02bdfc Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 4 Jul 2021 18:15:30 +0000 Subject: [PATCH 25/29] :arrow_up: Set n8n-core@0.76.0, n8n-editor-ui@0.97.0, n8n-nodes-base@0.124.0 and n8n-workflow@0.63.0 on n8n --- packages/cli/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index ce0467aa64..d90f56f4f6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -108,10 +108,10 @@ "localtunnel": "^2.0.0", "lodash.get": "^4.4.2", "mysql2": "~2.2.0", - "n8n-core": "~0.75.0", - "n8n-editor-ui": "~0.96.1", - "n8n-nodes-base": "~0.123.1", - "n8n-workflow": "~0.62.0", + "n8n-core": "~0.76.0", + "n8n-editor-ui": "~0.97.0", + "n8n-nodes-base": "~0.124.0", + "n8n-workflow": "~0.63.0", "oauth-1.0a": "^2.2.6", "open": "^7.0.0", "pg": "^8.3.0", From 297d9fe77d49a0c432365a5146aeba251e8a5d2c Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 4 Jul 2021 18:15:30 +0000 Subject: [PATCH 26/29] =?UTF-8?q?:bookmark:=20Release=C2=A0n8n@0.127.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cli/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index d90f56f4f6..4d866ec729 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "0.126.1", + "version": "0.127.0", "description": "n8n Workflow Automation Tool", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From 8d235e94cb79f5c0c542b68e5a0c3718bd094e45 Mon Sep 17 00:00:00 2001 From: Omar Ajoue Date: Tue, 6 Jul 2021 23:25:25 +0200 Subject: [PATCH 27/29] :zap: Performance improvements for executions count on Postgres (#1888) * Performance improvements for executions count on Postgres As reported by a community member https://community.n8n.io/t/stress-load-testing/4846/5 and https://github.com/n8n-io/n8n/issues/1578, when using postgres with a big volume of executions, the executions list's performance degrades. This PR is aimed at Postgres specifically by querying postgres' stats collector instead of running a full table scan, providing a good estimate. More can be read here: https://www.citusdata.com/blog/2016/10/12/count-performance/ * Removed order of magnitude so we display closer numbers * Making count based on statistics only when not applying filters * :zap: Minor styling improvements Co-authored-by: Jan Oberhauser --- packages/cli/src/Interfaces.ts | 1 + packages/cli/src/Server.ts | 44 ++++++++++++++++--- packages/editor-ui/src/Interface.ts | 1 + .../src/components/ExecutionsList.vue | 13 ++++-- 4 files changed, 50 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index df965c50e7..e1c09cf55e 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -188,6 +188,7 @@ export interface IExecutionsListResponse { count: number; // results: IExecutionShortResponse[]; results: IExecutionsSummary[]; + estimated: boolean; } export interface IExecutionsStopData { diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 0a31144999..ee2f50f2cf 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -33,6 +33,7 @@ import { CredentialsHelper, CredentialsOverwrites, CredentialTypes, + DatabaseType, Db, ExternalHooks, GenericHelpers, @@ -88,6 +89,7 @@ import { IRunData, IWorkflowBase, IWorkflowCredentials, + LoggerProxy, Workflow, WorkflowExecuteMode, } from 'n8n-workflow'; @@ -1612,8 +1614,7 @@ class App { executingWorkflowIds.push(...this.activeExecutionsInstance.getActiveExecutions().map(execution => execution.id.toString()) as string[]); const countFilter = JSON.parse(JSON.stringify(filter)); - countFilter.select = ['id']; - countFilter.where = {id: Not(In(executingWorkflowIds))}; + countFilter.id = Not(In(executingWorkflowIds)); const resultsQuery = await Db.collections.Execution! .createQueryBuilder("execution") @@ -1645,10 +1646,10 @@ class App { const resultsPromise = resultsQuery.getMany(); - const countPromise = Db.collections.Execution!.count(countFilter); + const countPromise = getExecutionsCount(countFilter); const results: IExecutionFlattedDb[] = await resultsPromise; - const count = await countPromise; + const countedObjects = await countPromise; const returnResults: IExecutionsSummary[] = []; @@ -1667,8 +1668,9 @@ class App { } return { - count, + count: countedObjects.count, results: returnResults, + estimated: countedObjects.estimate, }; })); @@ -2161,3 +2163,35 @@ export async function start(): Promise { await app.externalHooks.run('n8n.ready', [app]); }); } + +async function getExecutionsCount(countFilter: IDataObject): Promise<{ count: number; estimate: boolean; }> { + + const dbType = await GenericHelpers.getConfigValue('database.type') as DatabaseType; + const filteredFields = Object.keys(countFilter).filter(field => field !== 'id'); + + // Do regular count for other databases than pgsql and + // if we are filtering based on workflowId or finished fields. + if (dbType !== 'postgresdb' || filteredFields.length > 0) { + const count = await Db.collections.Execution!.count(countFilter); + return { count, estimate: false }; + } + + try { + // Get an estimate of rows count. + const estimateRowsNumberSql = "SELECT n_live_tup FROM pg_stat_all_tables WHERE relname = 'execution_entity';"; + const rows: Array<{ n_live_tup: string }> = await Db.collections.Execution!.query(estimateRowsNumberSql); + + const estimate = parseInt(rows[0].n_live_tup, 10); + // If over 100k, return just an estimate. + if (estimate > 100000) { + // if less than 100k, we get the real count as even a full + // table scan should not take so long. + return { count: estimate, estimate: true }; + } + } catch (err) { + LoggerProxy.warn('Unable to get executions count from postgres: ' + err); + } + + const count = await Db.collections.Execution!.count(countFilter); + return { count, estimate: false }; +} diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index f119699f6e..2fa997a545 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -325,6 +325,7 @@ export interface IExecutionShortResponse { export interface IExecutionsListResponse { count: number; results: IExecutionsSummary[]; + estimated: boolean; } export interface IExecutionsCurrentSummaryExtended { diff --git a/packages/editor-ui/src/components/ExecutionsList.vue b/packages/editor-ui/src/components/ExecutionsList.vue index e0e129a737..1ecfc4caf8 100644 --- a/packages/editor-ui/src/components/ExecutionsList.vue +++ b/packages/editor-ui/src/components/ExecutionsList.vue @@ -1,6 +1,6 @@