From 4ec458d56b8d35b5cadf17ea136079866935040b Mon Sep 17 00:00:00 2001 From: ricardo Date: Fri, 22 Jan 2021 10:17:12 -0500 Subject: [PATCH 001/239] :bug: Fixes issue handling error responses in Rocketchat node (#1367) --- .../nodes/Rocketchat/GenericFunctions.ts | 20 +- .../nodes/Rocketchat/Rocketchat.node.ts | 193 +++++++++--------- .../nodes/Rocketchat/rocketchat.png | Bin 1276 -> 0 bytes .../nodes/Rocketchat/rocketchat.svg | 1 + 4 files changed, 114 insertions(+), 100 deletions(-) delete mode 100644 packages/nodes-base/nodes/Rocketchat/rocketchat.png create mode 100644 packages/nodes-base/nodes/Rocketchat/rocketchat.svg diff --git a/packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts b/packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts index 51e145b384..a632a8f895 100644 --- a/packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts @@ -1,12 +1,14 @@ -import { OptionsWithUri } from 'request'; +import { + OptionsWithUri, +} from 'request'; + import { IExecuteFunctions, - IExecuteSingleFunctions, IHookFunctions, ILoadOptionsFunctions, } from 'n8n-core'; -export async function rocketchatApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, resource: string, method: string, operation: string, body: any = {}, headers?: object): Promise { // tslint:disable-line:no-any +export async function rocketchatApiRequest(this: IExecuteFunctions | ILoadOptionsFunctions, resource: string, method: string, operation: string, body: any = {}, headers?: object): Promise { // tslint:disable-line:no-any const credentials = this.getCredentials('rocketchatApi'); if (credentials === undefined) { @@ -29,13 +31,15 @@ export async function rocketchatApiRequest(this: IHookFunctions | IExecuteFuncti try { return await this.helpers.request!(options); } catch (error) { - let errorMessage = error.message; + if (error.response && error.response.body && error.response.body.error) { - if (error.response.body.error) { - errorMessage = error.response.body.error; + const errorMessage = error.response.body.error; + // Try to return the error prettier + throw new Error( + `Rocketchat error response [${error.statusCode}]: ${errorMessage}`, + ); } - - throw new Error(`Rocket.chat error response [${error.statusCode}]: ${errorMessage}`); + throw error; } } diff --git a/packages/nodes-base/nodes/Rocketchat/Rocketchat.node.ts b/packages/nodes-base/nodes/Rocketchat/Rocketchat.node.ts index d9bc0cf491..295a7add40 100644 --- a/packages/nodes-base/nodes/Rocketchat/Rocketchat.node.ts +++ b/packages/nodes-base/nodes/Rocketchat/Rocketchat.node.ts @@ -1,13 +1,15 @@ import { - IExecuteSingleFunctions, + IExecuteFunctions, } from 'n8n-core'; + import { IDataObject, INodeExecutionData, INodeType, INodeTypeDescription, } from 'n8n-workflow'; - import { + +import { rocketchatApiRequest, validateJSON } from './GenericFunctions'; @@ -50,7 +52,7 @@ export class Rocketchat implements INodeType { description: INodeTypeDescription = { displayName: 'RocketChat', name: 'rocketchat', - icon: 'file:rocketchat.png', + icon: 'file:rocketchat.svg', group: ['output'], version: 1, subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}', @@ -395,106 +397,113 @@ export class Rocketchat implements INodeType { ], }; - async executeSingle(this: IExecuteSingleFunctions): Promise { - const resource = this.getNodeParameter('resource') as string; - const operation = this.getNodeParameter('operation') as string; - let response; - - if (resource === 'chat') { - //https://rocket.chat/docs/developer-guides/rest-api/chat/postmessage - if (operation === 'postMessage') { - const channel = this.getNodeParameter('channel') as string; - const text = this.getNodeParameter('text') as string; - const options = this.getNodeParameter('options') as IDataObject; - const jsonActive = this.getNodeParameter('jsonParameters') as boolean; - - const body: IPostMessageBody = { - channel, - text, - }; - - if (options.alias) { - body.alias = options.alias as string; - } - if (options.avatar) { - body.avatar = options.avatar as string; - } - if (options.emoji) { - body.emoji = options.emoji as string; - } - - if (!jsonActive) { - const optionsAttachments = this.getNodeParameter('attachments') as IDataObject[]; - if (optionsAttachments.length > 0) { - const attachments: IAttachment[] = []; - for (let i = 0; i < optionsAttachments.length; i++) { - const attachment: IAttachment = {}; - for (const option of Object.keys(optionsAttachments[i])) { - if (option === 'color') { - attachment.color = optionsAttachments[i][option] as string; - } else if (option === 'text') { - attachment.text = optionsAttachments[i][option] as string; - } else if (option === 'ts') { - attachment.ts = optionsAttachments[i][option] as string; - } else if (option === 'messageLinks') { - attachment.message_link = optionsAttachments[i][option] as string; - } else if (option === 'thumbUrl') { - attachment.thumb_url = optionsAttachments[i][option] as string; - } else if (option === 'collapsed') { - attachment.collapsed = optionsAttachments[i][option] as boolean; - } else if (option === 'authorName') { - attachment.author_name = optionsAttachments[i][option] as string; - } else if (option === 'authorLink') { - attachment.author_link = optionsAttachments[i][option] as string; - } else if (option === 'authorIcon') { - attachment.author_icon = optionsAttachments[i][option] as string; - } else if (option === 'title') { - attachment.title = optionsAttachments[i][option] as string; - } else if (option === 'titleLink') { - attachment.title_link = optionsAttachments[i][option] as string; - } else if (option === 'titleLinkDownload') { - attachment.title_link_download = optionsAttachments[i][option] as boolean; - } else if (option === 'imageUrl') { - attachment.image_url = optionsAttachments[i][option] as string; - } else if (option === 'audioUrl') { - attachment.audio_url = optionsAttachments[i][option] as string; - } else if (option === 'videoUrl') { - attachment.video_url = optionsAttachments[i][option] as string; - } else if (option === 'fields') { - const fieldsValues = (optionsAttachments[i][option] as IDataObject).fieldsValues as IDataObject[]; - if (fieldsValues.length > 0) { - const fields: IField[] = []; - for (let i = 0; i < fieldsValues.length; i++) { - const field: IField = {}; - for (const key of Object.keys(fieldsValues[i])) { - if (key === 'short') { - field.short = fieldsValues[i][key] as boolean; - } else if (key === 'title') { - field.title = fieldsValues[i][key] as string; - } else if (key === 'value') { - field.value = fieldsValues[i][key] as string; + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const length = (items.length as unknown) as number; + let responseData; + const returnData: IDataObject[] = []; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { + if (resource === 'chat') { + //https://rocket.chat/docs/developer-guides/rest-api/chat/postmessage + if (operation === 'postMessage') { + const channel = this.getNodeParameter('channel', i) as string; + const text = this.getNodeParameter('text', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + const jsonActive = this.getNodeParameter('jsonParameters', i) as boolean; + + const body: IPostMessageBody = { + channel, + text, + }; + + if (options.alias) { + body.alias = options.alias as string; + } + if (options.avatar) { + body.avatar = options.avatar as string; + } + if (options.emoji) { + body.emoji = options.emoji as string; + } + + if (!jsonActive) { + const optionsAttachments = this.getNodeParameter('attachments', i) as IDataObject[]; + if (optionsAttachments.length > 0) { + const attachments: IAttachment[] = []; + for (let i = 0; i < optionsAttachments.length; i++) { + const attachment: IAttachment = {}; + for (const option of Object.keys(optionsAttachments[i])) { + if (option === 'color') { + attachment.color = optionsAttachments[i][option] as string; + } else if (option === 'text') { + attachment.text = optionsAttachments[i][option] as string; + } else if (option === 'ts') { + attachment.ts = optionsAttachments[i][option] as string; + } else if (option === 'messageLinks') { + attachment.message_link = optionsAttachments[i][option] as string; + } else if (option === 'thumbUrl') { + attachment.thumb_url = optionsAttachments[i][option] as string; + } else if (option === 'collapsed') { + attachment.collapsed = optionsAttachments[i][option] as boolean; + } else if (option === 'authorName') { + attachment.author_name = optionsAttachments[i][option] as string; + } else if (option === 'authorLink') { + attachment.author_link = optionsAttachments[i][option] as string; + } else if (option === 'authorIcon') { + attachment.author_icon = optionsAttachments[i][option] as string; + } else if (option === 'title') { + attachment.title = optionsAttachments[i][option] as string; + } else if (option === 'titleLink') { + attachment.title_link = optionsAttachments[i][option] as string; + } else if (option === 'titleLinkDownload') { + attachment.title_link_download = optionsAttachments[i][option] as boolean; + } else if (option === 'imageUrl') { + attachment.image_url = optionsAttachments[i][option] as string; + } else if (option === 'audioUrl') { + attachment.audio_url = optionsAttachments[i][option] as string; + } else if (option === 'videoUrl') { + attachment.video_url = optionsAttachments[i][option] as string; + } else if (option === 'fields') { + const fieldsValues = (optionsAttachments[i][option] as IDataObject).fieldsValues as IDataObject[]; + if (fieldsValues.length > 0) { + const fields: IField[] = []; + for (let i = 0; i < fieldsValues.length; i++) { + const field: IField = {}; + for (const key of Object.keys(fieldsValues[i])) { + if (key === 'short') { + field.short = fieldsValues[i][key] as boolean; + } else if (key === 'title') { + field.title = fieldsValues[i][key] as string; + } else if (key === 'value') { + field.value = fieldsValues[i][key] as string; + } } + fields.push(field); + attachment.fields = fields; } - fields.push(field); - attachment.fields = fields; } } } + attachments.push(attachment); } - attachments.push(attachment); + body.attachments = attachments; } - body.attachments = attachments; + } else { + body.attachments = validateJSON(this.getNodeParameter('attachmentsJson', i) as string); } - } else { - body.attachments = validateJSON(this.getNodeParameter('attachmentsJson') as string); + + responseData = await rocketchatApiRequest.call(this, '/chat', 'POST', 'postMessage', body); } - - response = await rocketchatApiRequest.call(this, '/chat', 'POST', 'postMessage', body); + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); } } - return { - json: response, - }; + return [this.helpers.returnJsonArray(returnData)]; } } diff --git a/packages/nodes-base/nodes/Rocketchat/rocketchat.png b/packages/nodes-base/nodes/Rocketchat/rocketchat.png deleted file mode 100644 index e44281abc89fbcfa92f48ab589e23132db964902..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1276 zcmYLHYfush5T-mm^E54+u`rByh=`!2HCJJ&rv&**B`Ga4>n^qIWs0xM2U?Mk`DV=2 z%1rB~>?O^mh@c2~rXmkz5&~38gUWoBs0ZE5HS_K6?C!VU{@A7N57}#D1+)SH05$}? zf4DJXO=f9ksz8uF0Kf|I{vcw2G3CPDIL<%7gSd(}f_uTv9SHRQ;5egAGz6OK?A8f) zHF~ZyngcV!n7VaAQAXwuk?*wsWV@gy@@tSujR$oVKwNlmjWDF!6t3(mY0`J+hMM48^G-@n0Bx;3j||>pe#oS8;b03!E|g!us6Y3j+5U%1Oy`AG@3Y&lSfhHoP|S{k@qJmd8%- z3)=FMyi4E4oK|GIXK#n(d6b5&3Qf0E7J4Pmjp!9`6vxu147O!tF@uIHZM%ylGoNn| z4Pm@ST_>aP{MeLg!NBfJ%qa>hOq(i^$tqJOmd@OI?=`FsPO%A=rxK(A>(F+{ zgH>kDL`i}^&Nobc!n}`w-MM}1)}6a|d@)<&W=*&BS%(=XNTNk1xF$bV2)?ugn751scW9t^ru zJ3D_jE;b*#l!Wb~dmMl%i??UsN@}0wfAle2U$BhBTO)8Gc!V}J?;-C*6HYL#0$$V2 z-YeNiH_twh;Q=bPh!Mq-67g}iqz&y3 zpJ~75dasq-v~6K+vDS>O0V{gacI1r5F{Obd>dTW%y?2!DpC`eA1*+DFD+&E#t9Kgi z4Ba8Z$;lc+YT}-Gj6|YOc^$7}xUbxoHT3mjmJsamzPY*)x`K_WxLPNqj^T4(MV@UQ kw*Cer+gBh$YqC2+H#BKN2kxtXGQK;25D?=3$S*eUUuy(+mH+?% diff --git a/packages/nodes-base/nodes/Rocketchat/rocketchat.svg b/packages/nodes-base/nodes/Rocketchat/rocketchat.svg new file mode 100644 index 0000000000..5a72e53f7e --- /dev/null +++ b/packages/nodes-base/nodes/Rocketchat/rocketchat.svg @@ -0,0 +1 @@ + \ No newline at end of file From 70d2a988cc7bb47ec6cef8890bdb30250dcf5503 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 24 Jan 2021 20:38:16 +0100 Subject: [PATCH 002/239] :zap: Minor improvements on Rocketchat Node --- packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts | 8 ++++++-- packages/nodes-base/nodes/Rocketchat/Rocketchat.node.ts | 8 ++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts b/packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts index a632a8f895..12ae258f7c 100644 --- a/packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts @@ -1,4 +1,4 @@ -import { +import { OptionsWithUri, } from 'request'; @@ -16,7 +16,11 @@ export async function rocketchatApiRequest(this: IExecuteFunctions | ILoadOption } const headerWithAuthentication = Object.assign({}, headers, - { 'X-Auth-Token': credentials.authKey, 'X-User-Id': credentials.userId }); + { + 'X-Auth-Token': credentials.authKey, + 'X-User-Id': credentials.userId, + } + ); const options: OptionsWithUri = { headers: headerWithAuthentication, diff --git a/packages/nodes-base/nodes/Rocketchat/Rocketchat.node.ts b/packages/nodes-base/nodes/Rocketchat/Rocketchat.node.ts index 295a7add40..f027fdc1db 100644 --- a/packages/nodes-base/nodes/Rocketchat/Rocketchat.node.ts +++ b/packages/nodes-base/nodes/Rocketchat/Rocketchat.node.ts @@ -412,12 +412,12 @@ export class Rocketchat implements INodeType { const text = this.getNodeParameter('text', i) as string; const options = this.getNodeParameter('options', i) as IDataObject; const jsonActive = this.getNodeParameter('jsonParameters', i) as boolean; - + const body: IPostMessageBody = { channel, text, }; - + if (options.alias) { body.alias = options.alias as string; } @@ -427,7 +427,7 @@ export class Rocketchat implements INodeType { if (options.emoji) { body.emoji = options.emoji as string; } - + if (!jsonActive) { const optionsAttachments = this.getNodeParameter('attachments', i) as IDataObject[]; if (optionsAttachments.length > 0) { @@ -493,7 +493,7 @@ export class Rocketchat implements INodeType { } else { body.attachments = validateJSON(this.getNodeParameter('attachmentsJson', i) as string); } - + responseData = await rocketchatApiRequest.call(this, '/chat', 'POST', 'postMessage', body); } } From 79d544bbd4f5186ced5a94c605d164f4b254776e Mon Sep 17 00:00:00 2001 From: Tysmith17 Date: Thu, 28 Jan 2021 10:38:57 -0700 Subject: [PATCH 003/239] add deal description to hubspot --- packages/nodes-base/nodes/Hubspot/DealDescription.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/nodes-base/nodes/Hubspot/DealDescription.ts b/packages/nodes-base/nodes/Hubspot/DealDescription.ts index 7bd4597b8a..01c5e80aa3 100644 --- a/packages/nodes-base/nodes/Hubspot/DealDescription.ts +++ b/packages/nodes-base/nodes/Hubspot/DealDescription.ts @@ -170,6 +170,12 @@ export const dealFields = [ }, ], }, + { + displayName: 'Deal Description', + name: 'description', + type: 'string', + default: '', + }, { displayName: 'Deal Name', name: 'dealName', From ddb1f5a22d97eeb2a6a4110cdeddee18d32e7661 Mon Sep 17 00:00:00 2001 From: Tysmith17 Date: Fri, 5 Feb 2021 10:42:33 -0700 Subject: [PATCH 004/239] add missing file --- packages/nodes-base/nodes/Hubspot/Hubspot.node.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts index fd2e720447..a7bcefc5a4 100644 --- a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts +++ b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts @@ -1880,6 +1880,12 @@ export class Hubspot implements INodeType { value: additionalFields.pipeline as string, }); } + if (additionalFields.description) { + body.properties.push({ + name: 'description', + value: additionalFields.description as string, + }); + } if (additionalFields.customPropertiesUi) { const customProperties = (additionalFields.customPropertiesUi as IDataObject).customPropertiesValues as IDataObject[]; if (customProperties) { @@ -1936,6 +1942,12 @@ export class Hubspot implements INodeType { value: updateFields.pipeline as string, }); } + if (updateFields.description) { + body.properties.push({ + name: 'description', + value: updateFields.description as string, + }); + } if (updateFields.customPropertiesUi) { const customProperties = (updateFields.customPropertiesUi as IDataObject).customPropertiesValues as IDataObject[]; if (customProperties) { From 53234770a7affaa5021c287203165ff252bb0abf Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 11 Feb 2021 14:55:59 +0100 Subject: [PATCH 005/239] :bug: Fix issue of current execution query with unsaved running workflow --- packages/cli/src/Server.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 8149d776bb..a391c33126 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -1472,7 +1472,7 @@ class App { } const resultsPromise = resultsQuery.getMany(); - + const countPromise = Db.collections.Execution!.count(countFilter); const results: IExecutionFlattedDb[] = await resultsPromise; @@ -1557,7 +1557,7 @@ class App { delete data!.executionData!.resultData.error; const length = data!.executionData!.resultData.runData[lastNodeExecuted].length; if (length > 0 && data!.executionData!.resultData.runData[lastNodeExecuted][length - 1].error !== undefined) { - // Remove results only if it is an error. + // Remove results only if it is an error. // If we are retrying due to a crash, the information is simply success info from last node data!.executionData!.resultData.runData[lastNodeExecuted].pop(); // Stack will determine what to run next @@ -1651,7 +1651,7 @@ class App { ]) .orderBy('execution.id', 'DESC') .andWhere(`execution.id IN (:...ids)`, {ids: currentlyRunningExecutionIds}); - + if (req.query.filter) { const filter = JSON.parse(req.query.filter as string); if (filter.workflowId !== undefined) { @@ -1674,12 +1674,12 @@ class App { const executingWorkflows = this.activeExecutionsInstance.getActiveExecutions(); const returnData: IExecutionsSummary[] = []; - + let filter: any = {}; // tslint:disable-line:no-any if (req.query.filter) { filter = JSON.parse(req.query.filter as string); } - + for (const data of executingWorkflows) { if (filter.workflowId !== undefined && filter.workflowId !== data.workflowId) { continue; @@ -1687,14 +1687,14 @@ class App { returnData.push( { idActive: data.id.toString(), - workflowId: data.workflowId.toString(), + workflowId: data.workflowId === undefined ? '' : data.workflowId.toString(), mode: data.mode, retryOf: data.retryOf, startedAt: new Date(data.startedAt), } ); } - + return returnData; } })); @@ -1721,26 +1721,26 @@ class App { stoppedAt: fullExecutionData.stoppedAt ? new Date(fullExecutionData.stoppedAt) : undefined, finished: fullExecutionData.finished, }; - + return returnData; - + } else { const executionId = req.params.id; - + // Stopt he execution and wait till it is done and we got the data const result = await this.activeExecutionsInstance.stopExecution(executionId); - + if (result === undefined) { throw new Error(`The execution id "${executionId}" could not be found.`); } - + const returnData: IExecutionsStopData = { mode: result.mode, startedAt: new Date(result.startedAt), stoppedAt: result.stoppedAt ? new Date(result.stoppedAt) : undefined, finished: result.finished, }; - + return returnData; } })); From 24f4d6e277705d9e59024f807c4393226b0b564e Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 12 Feb 2021 00:28:59 +0100 Subject: [PATCH 006/239] :whale: Create and push docker image "latest-debian" --- .github/workflows/docker-images.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/docker-images.yml b/.github/workflows/docker-images.yml index 6b5da8106b..1a9b81d09f 100644 --- a/.github/workflows/docker-images.yml +++ b/.github/workflows/docker-images.yml @@ -32,3 +32,7 @@ jobs: run: docker build --build-arg N8N_VERSION=${{steps.vars.outputs.tag}} -t n8nio/n8n:${{steps.vars.outputs.tag}}-debian docker/images/n8n-debian - name: Push Docker image of version (Debian) run: docker push n8nio/n8n:${{steps.vars.outputs.tag}}-debian + - name: Tag Docker image with latest (Debian) + run: docker tag n8nio/n8n:${{steps.vars.outputs.tag}}-debian n8nio/n8n:latest-debian + - name: Push docker images of latest (Debian) + run: docker push n8nio/n8n:latest-debian From 5baa31b0532710c18fd05c8afcfb84e2463c8ddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Sat, 13 Feb 2021 13:27:08 -0300 Subject: [PATCH 007/239] :sparkles: Add QuickBooks node (#1365) * Add OAuth2 credentials * Adjust credentials params * Add node to listing * Add initial node scaffolding * Remove unused credentials params * Add customer search with select statement * Add pagination to customer search * Add customer creation functionality * Add customer update functionality * Small formatting fix * Adjust property name casing for consistency * Adjust customer operations for consistency * Handle large QuickBooks listings * Add initial estimate resource description * Add estimate resource retrieval operations * Refactor customer billing address * Simplify customer billing address * Fix casing for customer additional fields * Adjust types to accommodate loadOptions functions * Add loadOptions for customers to estimate * Sort customer update fields alphabetically * Refactor estimate line into standalone file * Add stub for PDF retrieval operation * Add invoice resource description and execute branches * Implement estimate PDF download functionality * Place descriptions in their own dir * Add get and getAll for invoices * Add send functionality to invoices * Refactor handling of binary data * Add invoice voiding functionality * Add invoice deletion functionality * Refactor resources into subdirs and add interfaces * Add get and getAll for bill * Add payment description * Add get and getAll for payment * Make variables in endpoints consistent * Refactor interfaces for consistency * Add interface for item resource * Fill in fields for all resources * Minor fixes in defaults and descriptions * Refactor loader * Add all resources to execute function * Fix line property duplication * Add get and getAll for vendor * Optimize description imports * Add creation for customer and bill * Add update operation for bill * Refactor create and update for customer * Implement employee create and update * Implement create and update for estimate * Make address params more consistent * Add create and update to payment * Add item operations * Add create and delete operations for invoice * Refactor binary data handler * Refactor generic functions * Add create and update operations for vendor * Fix build * Fix total amount in bill:update * Fix balance in bill:update * Remove currency from bill:update * Implement reference retrieval in bill:update * Fix failing params in customer:update * Fix param in employee:update * Implement reference retrieval in estimate:update * Fix failing params in estimate:update * Fix failing params in invoice:update * Fix failing param in vendor:update * Implement reference retrieval in payment:update * Remove unused interfaces * Rename line property function * Remove hared directory * Refactor reference and sync token retrieval * Fix line structure in bill:create * Fix line structure in estimate:create * Flatten responses * Refactor line processing * Remove unused interfaces * Add endpoint documentation * Fix payment:void content type * Fix default for bill line item * Hide auth URI query parameters * Hide auth header parameter * Add switch for credentials environment * Adjust OAuth2 callback to accommodate realmId * Retrieve realmId from OAuth2 flow * :zap: Improvements * Reposition dividers * Add IDs to display names of reference fields * Add estimate:delete and bill:delete * Load items in lines for bill, estimate and invoice * Add filename for binary property in PDF download * :zap: Improvements * Adjust field description * Implement estimate:send * Adjust field description * Adjust custom field descriptions * Add missing period to description * :zap: Minor improvements on QuickBooks-Node * Add descriptions for bill * Add descriptions for customer * Add descriptions for employee * Add descriptions for estimate * Add descriptions for invoice * Add descriptions for payment * Add descriptions for vendor Co-authored-by: ricardo Co-authored-by: Jan Oberhauser --- packages/cli/src/Server.ts | 13 +- .../QuickBooksOAuth2Api.credentials.ts | 68 ++ .../nodes/QuickBooks/GenericFunctions.ts | 436 ++++++++ .../nodes/QuickBooks/QuickBooks.node.ts | 990 ++++++++++++++++++ .../Bill/BillAdditionalFieldsOptions.ts | 86 ++ .../descriptions/Bill/BillDescription.ts | 330 ++++++ .../CustomerAdditionalFieldsOptions.ts | 151 +++ .../Customer/CustomerDescription.ts | 222 ++++ .../EmployeeAdditionalFieldsOptions.ts | 91 ++ .../Employee/EmployeeDescription.ts | 236 +++++ .../EstimateAdditionalFieldsOptions.ts | 227 ++++ .../Estimate/EstimateDescription.ts | 425 ++++++++ .../Invoice/InvoiceAdditionalFieldsOptions.ts | 183 ++++ .../Invoice/InvoiceDescription.ts | 451 ++++++++ .../descriptions/Item/ItemDescription.ts | 129 +++ .../Payment/PaymentAdditionalFieldsOptions.ts | 9 + .../Payment/PaymentDescription.ts | 399 +++++++ .../descriptions/Shared.interface.ts | 48 + .../Vendor/VendorAdditionalFieldsOptions.ts | 117 +++ .../descriptions/Vendor/VendorDescription.ts | 222 ++++ .../nodes/QuickBooks/descriptions/index.ts | 8 + .../nodes/QuickBooks/quickbooks.svg | 1 + packages/nodes-base/package.json | 2 + 23 files changed, 4841 insertions(+), 3 deletions(-) create mode 100644 packages/nodes-base/credentials/QuickBooksOAuth2Api.credentials.ts create mode 100644 packages/nodes-base/nodes/QuickBooks/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/QuickBooks/QuickBooks.node.ts create mode 100644 packages/nodes-base/nodes/QuickBooks/descriptions/Bill/BillAdditionalFieldsOptions.ts create mode 100644 packages/nodes-base/nodes/QuickBooks/descriptions/Bill/BillDescription.ts create mode 100644 packages/nodes-base/nodes/QuickBooks/descriptions/Customer/CustomerAdditionalFieldsOptions.ts create mode 100644 packages/nodes-base/nodes/QuickBooks/descriptions/Customer/CustomerDescription.ts create mode 100644 packages/nodes-base/nodes/QuickBooks/descriptions/Employee/EmployeeAdditionalFieldsOptions.ts create mode 100644 packages/nodes-base/nodes/QuickBooks/descriptions/Employee/EmployeeDescription.ts create mode 100644 packages/nodes-base/nodes/QuickBooks/descriptions/Estimate/EstimateAdditionalFieldsOptions.ts create mode 100644 packages/nodes-base/nodes/QuickBooks/descriptions/Estimate/EstimateDescription.ts create mode 100644 packages/nodes-base/nodes/QuickBooks/descriptions/Invoice/InvoiceAdditionalFieldsOptions.ts create mode 100644 packages/nodes-base/nodes/QuickBooks/descriptions/Invoice/InvoiceDescription.ts create mode 100644 packages/nodes-base/nodes/QuickBooks/descriptions/Item/ItemDescription.ts create mode 100644 packages/nodes-base/nodes/QuickBooks/descriptions/Payment/PaymentAdditionalFieldsOptions.ts create mode 100644 packages/nodes-base/nodes/QuickBooks/descriptions/Payment/PaymentDescription.ts create mode 100644 packages/nodes-base/nodes/QuickBooks/descriptions/Shared.interface.ts create mode 100644 packages/nodes-base/nodes/QuickBooks/descriptions/Vendor/VendorAdditionalFieldsOptions.ts create mode 100644 packages/nodes-base/nodes/QuickBooks/descriptions/Vendor/VendorDescription.ts create mode 100644 packages/nodes-base/nodes/QuickBooks/descriptions/index.ts create mode 100644 packages/nodes-base/nodes/QuickBooks/quickbooks.svg diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index a391c33126..c52e2f66bc 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -395,7 +395,8 @@ class App { })); //support application/x-www-form-urlencoded post data - this.app.use(bodyParser.urlencoded({ extended: false, + this.app.use(bodyParser.urlencoded({ + extended: false, verify: (req, res, buf) => { // @ts-ignore req.rawBody = buf; @@ -725,7 +726,7 @@ class App { // Make a copy of the object. If we don't do this, then when // The method below is called the properties are removed for good // This happens because nodes are returned as reference. - const nodeInfo: INodeTypeDescription = {...nodeData.description}; + const nodeInfo: INodeTypeDescription = { ...nodeData.description }; if (req.query.includeProperties !== 'true') { // @ts-ignore delete nodeInfo.properties; @@ -1310,6 +1311,8 @@ class App { // Verify and store app code. Generate access tokens and store for respective credential. this.app.get(`/${this.restEndpoint}/oauth2-credential/callback`, async (req: express.Request, res: express.Response) => { + + // realmId it's currently just use for the quickbook OAuth2 flow const { code, state: stateEncoded } = req.query; if (code === undefined || stateEncoded === undefined) { @@ -1384,6 +1387,10 @@ class App { const oauthToken = await oAuthObj.code.getToken(`${oAuth2Parameters.redirectUri}?${queryParameters}`, options); + if (Object.keys(req.query).length > 2) { + _.set(oauthToken.data, 'callbackQueryString', _.omit(req.query, 'state', 'code')); + } + if (oauthToken === undefined) { const errorResponse = new ResponseHelper.ResponseError('Unable to get access tokens!', undefined, 404); return ResponseHelper.sendErrorResponse(res, errorResponse); @@ -1510,7 +1517,7 @@ class App { } if (req.query.unflattedResponse === 'true') { - const fullExecutionData = ResponseHelper.unflattenExecutionData(result); + const fullExecutionData = ResponseHelper.unflattenExecutionData(result); return fullExecutionData as IExecutionResponse; } else { // Convert to response format in which the id is a string diff --git a/packages/nodes-base/credentials/QuickBooksOAuth2Api.credentials.ts b/packages/nodes-base/credentials/QuickBooksOAuth2Api.credentials.ts new file mode 100644 index 0000000000..f48109bb26 --- /dev/null +++ b/packages/nodes-base/credentials/QuickBooksOAuth2Api.credentials.ts @@ -0,0 +1,68 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +const scopes = [ + 'com.intuit.quickbooks.accounting', + 'com.intuit.quickbooks.payment', +]; + +// https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization + +export class QuickBooksOAuth2Api implements ICredentialType { + name = 'quickBooksOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'QuickBooks OAuth2 API'; + documentationUrl = 'quickbooks'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://appcenter.intuit.com/connect/oauth2', + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer', + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: scopes.join(' '), + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'header', + }, + { + displayName: 'Environment', + name: 'environment', + type: 'options' as NodePropertyTypes, + default: 'production', + options: [ + { + name: 'Production', + value: 'production', + }, + { + name: 'Sandbox', + value: 'sandbox', + }, + ], + }, + ]; +} diff --git a/packages/nodes-base/nodes/QuickBooks/GenericFunctions.ts b/packages/nodes-base/nodes/QuickBooks/GenericFunctions.ts new file mode 100644 index 0000000000..9218f82bad --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/GenericFunctions.ts @@ -0,0 +1,436 @@ +import { + IExecuteFunctions, + IHookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, +} from 'n8n-workflow'; + +import { + CustomField, + GeneralAddress, + Ref, +} from './descriptions/Shared.interface'; + +import { + capitalCase, +} from 'change-case'; + +import { + pickBy, +} from 'lodash'; + +import { + OptionsWithUri, +} from 'request'; + +/** + * Make an authenticated API request to QuickBooks. + */ +export async function quickBooksApiRequest( + this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, + method: string, + endpoint: string, + qs: IDataObject, + body: IDataObject, + option: IDataObject = {}, +): Promise { // tslint:disable-line:no-any + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + let isDownload = false; + + if (['estimate', 'invoice', 'payment'].includes(resource) && operation === 'get') { + isDownload = this.getNodeParameter('download', 0) as boolean; + } + + const productionUrl = 'https://quickbooks.api.intuit.com'; + const sandboxUrl = 'https://sandbox-quickbooks.api.intuit.com'; + + const credentials = this.getCredentials('quickBooksOAuth2Api') as IDataObject; + + const options: OptionsWithUri = { + headers: { + 'user-agent': 'n8n', + }, + method, + uri: `${credentials.environment === 'sandbox' ? sandboxUrl : productionUrl}${endpoint}`, + qs, + body, + json: !isDownload, + }; + + if (!Object.keys(body).length) { + delete options.body; + } + + if (!Object.keys(qs).length) { + delete options.qs; + } + + if (Object.keys(option)) { + Object.assign(options, option); + } + + if (isDownload) { + options.headers!['Accept'] = 'application/pdf'; + } + + if (resource === 'invoice' && operation === 'send') { + options.headers!['Content-Type'] = 'application/octet-stream'; + } + + if ( + (resource === 'invoice' && (operation === 'void' || operation === 'delete')) || + (resource === 'payment' && (operation === 'void' || operation === 'delete')) + ) { + options.headers!['Content-Type'] = 'application/json'; + } + + try { + return await this.helpers.requestOAuth2!.call(this, 'quickBooksOAuth2Api', options); + } catch (error) { + + const errors = error.error.Fault.Error; + + if (errors && Array.isArray(errors)) { + const errorMessage = errors.map( + (e) => `QuickBooks error response [${e.code}]: ${e.Message} - Detail: ${e.Detail}`, + ).join('|'); + + throw new Error(errorMessage); + } + + throw error; + } +} + +/** + * Make an authenticated API request to QuickBooks and return all results. + */ +export async function quickBooksApiRequestAllItems( + this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, + method: string, + endpoint: string, + qs: IDataObject, + body: IDataObject, + resource: string, +): Promise { // tslint:disable-line:no-any + + let responseData; + let startPosition = 1; + const maxResults = 1000; + const returnData: IDataObject[] = []; + + const maxCount = await getCount.call(this, method, endpoint, qs); + + const originalQuery = qs.query; + + do { + qs.query = `${originalQuery} MAXRESULTS ${maxResults} STARTPOSITION ${startPosition}`; + responseData = await quickBooksApiRequest.call(this, method, endpoint, qs, body); + returnData.push(...responseData.QueryResponse[capitalCase(resource)]); + startPosition += maxResults; + + } while (maxCount > returnData.length); + + return returnData; +} + +async function getCount( + this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, + method: string, + endpoint: string, + qs: IDataObject, +): Promise { // tslint:disable-line:no-any + + const responseData = await quickBooksApiRequest.call(this, method, endpoint, qs, {}); + + return responseData.QueryResponse.totalCount; +} + +/** + * Handles a QuickBooks listing by returning all items or up to a limit. + */ +export async function handleListing( + this: IExecuteFunctions, + i: number, + endpoint: string, + resource: string, +): Promise { // tslint:disable-line:no-any + let responseData; + + const qs = { + query: `SELECT * FROM ${resource}`, + } as IDataObject; + + const returnAll = this.getNodeParameter('returnAll', i); + + const filters = this.getNodeParameter('filters', i) as IDataObject; + if (filters.query) { + qs.query += ` ${filters.query}`; + } + + if (returnAll) { + return await quickBooksApiRequestAllItems.call(this, 'GET', endpoint, qs, {}, resource); + } else { + const limit = this.getNodeParameter('limit', i) as number; + qs.query += ` MAXRESULTS ${limit}`; + responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, qs, {}); + responseData = responseData.QueryResponse[capitalCase(resource)]; + return responseData; + } +} + +/** + * Get the SyncToken required for delete and void operations in QuickBooks. + */ +export async function getSyncToken( + this: IExecuteFunctions, + i: number, + companyId: string, + resource: string, +) { + const resourceId = this.getNodeParameter(`${resource}Id`, i); + const getEndpoint = `/v3/company/${companyId}/${resource}/${resourceId}`; + const propertyName = capitalCase(resource); + const { [propertyName]: { SyncToken } } = await quickBooksApiRequest.call(this, 'GET', getEndpoint, {}, {}); + + return SyncToken; +} + +/** + * Get the reference and SyncToken required for update operations in QuickBooks. + */ +export async function getRefAndSyncToken( + this: IExecuteFunctions, + i: number, + companyId: string, + resource: string, + ref: string, +) { + const resourceId = this.getNodeParameter(`${resource}Id`, i); + const endpoint = `/v3/company/${companyId}/${resource}/${resourceId}`; + const responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {}); + + return { + ref: responseData[capitalCase(resource)][ref], + syncToken: responseData[capitalCase(resource)].SyncToken, + }; + +} + +/** + * Populate node items with binary data. + */ +export async function handleBinaryData( + this: IExecuteFunctions, + items: INodeExecutionData[], + i: number, + companyId: string, + resource: string, + resourceId: string, +) { + const binaryProperty = this.getNodeParameter('binaryProperty', i) as string; + const fileName = this.getNodeParameter('fileName', i) as string; + const endpoint = `/v3/company/${companyId}/${resource}/${resourceId}/pdf`; + const data = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {}, { encoding: null }); + + items[i].binary = items[i].binary ?? {}; + items[i].binary![binaryProperty] = await this.helpers.prepareBinaryData(data); + items[i].binary![binaryProperty].fileName = fileName; + items[i].binary![binaryProperty].fileExtension = 'pdf'; + + return items; +} + +export async function loadResource( + this: ILoadOptionsFunctions, + resource: string, +) { + const returnData: INodePropertyOptions[] = []; + + const qs = { + query: `SELECT * FROM ${resource}`, + } as IDataObject; + + const { oauthTokenData: { callbackQueryString: { realmId } } } = this.getCredentials('quickBooksOAuth2Api') as { oauthTokenData: { callbackQueryString: { realmId: string } } }; + const endpoint = `/v3/company/${realmId}/query`; + + const resourceItems = await quickBooksApiRequestAllItems.call(this, 'GET', endpoint, qs, {}, resource); + + if (resource === 'preferences') { + const { SalesFormsPrefs: { CustomField } } = resourceItems[0]; + const customFields = CustomField[1].CustomField; + for (const customField of customFields) { + const length = customField.Name.length; + returnData.push({ + name: customField.StringValue, + value: customField.Name.charAt(length - 1), + }); + } + return returnData; + } + + resourceItems.forEach((resourceItem: { DisplayName: string, Name: string, Id: string }) => { + returnData.push({ + name: resourceItem.DisplayName || resourceItem.Name, + value: resourceItem.Id, + }); + }); + + return returnData; +} + +/** + * Populate the `Line` property in a request body. + */ +export function processLines( + this: IExecuteFunctions, + body: IDataObject, + lines: IDataObject[], + resource: string, +) { + + lines.forEach((line) => { + if (resource === 'bill') { + + if (line.DetailType === 'AccountBasedExpenseLineDetail') { + line.AccountBasedExpenseLineDetail = { + AccountRef: { + value: line.accountId, + }, + }; + delete line.accountId; + } else if (line.DetailType === 'ItemBasedExpenseLineDetail') { + line.ItemBasedExpenseLineDetail = { + ItemRef: { + value: line.itemId, + }, + }; + delete line.itemId; + } + + } else if (resource === 'estimate') { + if (line.DetailType === 'SalesItemLineDetail') { + line.SalesItemLineDetail = { + ItemRef: { + value: line.itemId, + }, + }; + delete line.itemId; + } + + } else if (resource === 'invoice') { + if (line.DetailType === 'SalesItemLineDetail') { + line.SalesItemLineDetail = { + ItemRef: { + value: line.itemId, + }, + }; + delete line.itemId; + } + } + + }); + + return lines; +} + +/** + * Populate update fields or additional fields into a request body. + */ +export function populateFields( + this: IExecuteFunctions, + body: IDataObject, + fields: IDataObject, + resource: string, +) { + + Object.entries(fields).forEach(([key, value]) => { + + if (resource === 'bill') { + + if (key.endsWith('Ref')) { + const { details } = value as { details: Ref }; + body[key] = { + name: details.name, + value: details.value, + }; + + } else { + body[key] = value; + } + + } else if (['customer', 'employee', 'vendor'].includes(resource)) { + + if (key === 'BillAddr') { + const { details } = value as { details: GeneralAddress }; + body.BillAddr = pickBy(details, detail => detail !== ''); + + } else if (key === 'PrimaryEmailAddr') { + body.PrimaryEmailAddr = { + Address: value, + }; + + } else if (key === 'PrimaryPhone') { + body.PrimaryPhone = { + FreeFormNumber: value, + }; + + } else { + body[key] = value; + } + + } else if (resource === 'estimate' || resource === 'invoice') { + + if (key === 'BillAddr' || key === 'ShipAddr') { + const { details } = value as { details: GeneralAddress }; + body[key] = pickBy(details, detail => detail !== ''); + + } else if (key === 'BillEmail') { + body.BillEmail = { + Address: value, + }; + + } else if (key === 'CustomFields') { + const { Field } = value as { Field: CustomField[] }; + body.CustomField = Field; + const length = (body.CustomField as CustomField[]).length; + for (let i = 0; i < length; i++) { + //@ts-ignore + body.CustomField[i]['Type'] = 'StringType'; + } + + } else if (key === 'CustomerMemo') { + body.CustomerMemo = { + value, + }; + + } else if (key.endsWith('Ref')) { + const { details } = value as { details: Ref }; + body[key] = { + name: details.name, + value: details.value, + }; + + } else if (key === 'TotalTax') { + body.TxnTaxDetail = { + TotalTax: value, + }; + + } else { + body[key] = value; + } + + } else if (resource === 'payment') { + body[key] = value; + } + }); + return body; +} diff --git a/packages/nodes-base/nodes/QuickBooks/QuickBooks.node.ts b/packages/nodes-base/nodes/QuickBooks/QuickBooks.node.ts new file mode 100644 index 0000000000..4fe8f983df --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/QuickBooks.node.ts @@ -0,0 +1,990 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + billFields, + billOperations, + customerFields, + customerOperations, + employeeFields, + employeeOperations, + estimateFields, + estimateOperations, + invoiceFields, + invoiceOperations, + itemFields, + itemOperations, + paymentFields, + paymentOperations, + vendorFields, + vendorOperations, +} from './descriptions'; + +import { + getRefAndSyncToken, + getSyncToken, + handleBinaryData, + handleListing, + loadResource, + populateFields, + processLines, + quickBooksApiRequest, +} from './GenericFunctions'; + +import { + capitalCase, +} from 'change-case'; + +import { + isEmpty, +} from 'lodash'; + +export class QuickBooks implements INodeType { + description: INodeTypeDescription = { + displayName: 'QuickBooks', + name: 'quickbooks', + icon: 'file:quickbooks.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume the QuickBooks API', + defaults: { + name: 'QuickBooks', + color: '#2CA01C', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'quickBooksOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Bill', + value: 'bill', + }, + { + name: 'Customer', + value: 'customer', + }, + { + name: 'Employee', + value: 'employee', + }, + { + name: 'Estimate', + value: 'estimate', + }, + { + name: 'Invoice', + value: 'invoice', + }, + { + name: 'Item', + value: 'item', + }, + { + name: 'Payment', + value: 'payment', + }, + { + name: 'Vendor', + value: 'vendor', + }, + ], + default: 'customer', + description: 'Resource to consume', + }, + ...billOperations, + ...billFields, + ...customerOperations, + ...customerFields, + ...employeeOperations, + ...employeeFields, + ...estimateOperations, + ...estimateFields, + ...invoiceOperations, + ...invoiceFields, + ...itemOperations, + ...itemFields, + ...paymentOperations, + ...paymentFields, + ...vendorOperations, + ...vendorFields, + ], + }; + + methods = { + loadOptions: { + async getCustomers(this: ILoadOptionsFunctions) { + return await loadResource.call(this, 'customer'); + }, + + async getCustomFields(this: ILoadOptionsFunctions) { + return await loadResource.call(this, 'preferences'); + }, + + async getItems(this: ILoadOptionsFunctions) { + return await loadResource.call(this, 'item'); + }, + + async getVendors(this: ILoadOptionsFunctions) { + return await loadResource.call(this, 'vendor'); + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + let responseData; + const returnData: IDataObject[] = []; + + const { oauthTokenData } = this.getCredentials('quickBooksOAuth2Api') as IDataObject; + // @ts-ignore + const companyId = oauthTokenData.callbackQueryString.realmId; + + for (let i = 0; i < items.length; i++) { + + if (resource === 'bill') { + + // ********************************************************************* + // bill + // ********************************************************************* + + // https://developer.intuit.com/app/developer/qbo/docs/api/accounting/most-commonly-used/estimate + + if (operation === 'create') { + + // ---------------------------------- + // bill: create + // ---------------------------------- + + const lines = this.getNodeParameter('Line', i) as IDataObject[]; + + if (!lines.length) { + throw new Error(`Please enter at least one line for the ${resource}.`); + } + + if (lines.some(line => line.DetailType === undefined || line.Amount === undefined || line.Description === undefined)) { + throw new Error('Please enter detail type, amount and description for every line.'); + } + + lines.forEach(line => { + if (line.DetailType === 'AccountBasedExpenseLineDetail' && line.accountId === undefined) { + throw new Error('Please enter an account ID for the associated line.'); + } else if (line.DetailType === 'ItemBasedExpenseLineDetail' && line.itemId === undefined) { + throw new Error('Please enter an item ID for the associated line.'); + } + }); + + let body = { + VendorRef: { + value: this.getNodeParameter('VendorRef', i), + }, + } as IDataObject; + + body.Line = processLines.call(this, body, lines, resource); + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + body = populateFields.call(this, body, additionalFields, resource); + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'delete') { + + // ---------------------------------- + // bill: delete + // ---------------------------------- + + const qs = { + operation: 'delete', + } as IDataObject; + + const body = { + Id: this.getNodeParameter('billId', i), + SyncToken: await getSyncToken.call(this, i, companyId, resource), + } as IDataObject; + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, qs, body); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'get') { + + // ---------------------------------- + // bill: get + // ---------------------------------- + + const billId = this.getNodeParameter('billId', i); + const endpoint = `/v3/company/${companyId}/${resource}/${billId}`; + responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {}); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'getAll') { + + // ---------------------------------- + // bill: getAll + // ---------------------------------- + + const endpoint = `/v3/company/${companyId}/query`; + responseData = await handleListing.call(this, i, endpoint, resource); + + } else if (operation === 'update') { + + // ---------------------------------- + // bill: update + // ---------------------------------- + + const { ref, syncToken } = await getRefAndSyncToken.call(this, i, companyId, resource, 'VendorRef'); + + let body = { + Id: this.getNodeParameter('billId', i), + SyncToken: syncToken, + sparse: true, + VendorRef: { + name: ref.name, + value: ref.value, + }, + } as IDataObject; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (isEmpty(updateFields)) { + throw new Error(`Please enter at least one field to update for the ${resource}.`); + } + + body = populateFields.call(this, body, updateFields, resource); + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData[capitalCase(resource)]; + + } + + } else if (resource === 'customer') { + + // ********************************************************************* + // customer + // ********************************************************************* + + // https://developer.intuit.com/app/developer/qbo/docs/api/accounting/most-commonly-used/customer + + if (operation === 'create') { + + // ---------------------------------- + // customer: create + // ---------------------------------- + + let body = { + DisplayName: this.getNodeParameter('displayName', i), + } as IDataObject; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + body = populateFields.call(this, body, additionalFields, resource); + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'get') { + + // ---------------------------------- + // customer: get + // ---------------------------------- + + const customerId = this.getNodeParameter('customerId', i); + const endpoint = `/v3/company/${companyId}/${resource}/${customerId}`; + responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {}); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'getAll') { + + // ---------------------------------- + // customer: getAll + // ---------------------------------- + + const endpoint = `/v3/company/${companyId}/query`; + responseData = await handleListing.call(this, i, endpoint, resource); + + } else if (operation === 'update') { + + // ---------------------------------- + // customer: update + // ---------------------------------- + + let body = { + Id: this.getNodeParameter('customerId', i), + SyncToken: await getSyncToken.call(this, i, companyId, resource), + sparse: true, + } as IDataObject; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (isEmpty(updateFields)) { + throw new Error(`Please enter at least one field to update for the ${resource}.`); + } + + body = populateFields.call(this, body, updateFields, resource); + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData[capitalCase(resource)]; + + } + + } else if (resource === 'employee') { + + // ********************************************************************* + // employee + // ********************************************************************* + + if (operation === 'create') { + + // ---------------------------------- + // employee: create + // ---------------------------------- + + let body = { + FamilyName: this.getNodeParameter('FamilyName', i), + GivenName: this.getNodeParameter('GivenName', i), + } as IDataObject; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + body = populateFields.call(this, body, additionalFields, resource); + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'get') { + + // ---------------------------------- + // employee: get + // ---------------------------------- + + const employeeId = this.getNodeParameter('employeeId', i); + const endpoint = `/v3/company/${companyId}/${resource}/${employeeId}`; + responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {}); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'getAll') { + + // ---------------------------------- + // employee: getAll + // ---------------------------------- + + const endpoint = `/v3/company/${companyId}/query`; + responseData = await handleListing.call(this, i, endpoint, resource); + + } else if (operation === 'update') { + + // ---------------------------------- + // employee: update + // ---------------------------------- + + let body = { + Id: this.getNodeParameter('employeeId', i), + SyncToken: await getSyncToken.call(this, i, companyId, resource), + sparse: true, + } as IDataObject; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (isEmpty(updateFields)) { + throw new Error(`Please enter at least one field to update for the ${resource}.`); + } + + body = populateFields.call(this, body, updateFields, resource); + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData[capitalCase(resource)]; + + } + + } else if (resource === 'estimate') { + + // ********************************************************************* + // estimate + // ********************************************************************* + + // https://developer.intuit.com/app/developer/qbo/docs/api/accounting/most-commonly-used/estimate + + if (operation === 'create') { + + // ---------------------------------- + // estimate: create + // ---------------------------------- + + const lines = this.getNodeParameter('Line', i) as IDataObject[]; + + if (!lines.length) { + throw new Error(`Please enter at least one line for the ${resource}.`); + } + + if (lines.some(line => line.DetailType === undefined || line.Amount === undefined || line.Description === undefined)) { + throw new Error('Please enter detail type, amount and description for every line.'); + } + + lines.forEach(line => { + if (line.DetailType === 'SalesItemLineDetail' && line.itemId === undefined) { + throw new Error('Please enter an item ID for the associated line.'); + } + }); + + let body = { + CustomerRef: { + value: this.getNodeParameter('CustomerRef', i), + }, + } as IDataObject; + + body.Line = processLines.call(this, body, lines, resource); + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + body = populateFields.call(this, body, additionalFields, resource); + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'delete') { + + // ---------------------------------- + // estimate: delete + // ---------------------------------- + + const qs = { + operation: 'delete', + } as IDataObject; + + const body = { + Id: this.getNodeParameter('estimateId', i), + SyncToken: await getSyncToken.call(this, i, companyId, resource), + } as IDataObject; + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, qs, body); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'get') { + + // ---------------------------------- + // estimate: get + // ---------------------------------- + + const estimateId = this.getNodeParameter('estimateId', i) as string; + const download = this.getNodeParameter('download', i) as boolean; + + if (download) { + + responseData = await handleBinaryData.call(this, items, i, companyId, resource, estimateId); + + } else { + + const endpoint = `/v3/company/${companyId}/${resource}/${estimateId}`; + responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {}); + responseData = responseData[capitalCase(resource)]; + + } + + } else if (operation === 'getAll') { + + // ---------------------------------- + // estimate: getAll + // ---------------------------------- + + const endpoint = `/v3/company/${companyId}/query`; + responseData = await handleListing.call(this, i, endpoint, resource); + + } else if (operation === 'send') { + + // ---------------------------------- + // estimate: send + // ---------------------------------- + + const estimateId = this.getNodeParameter('estimateId', i) as string; + + const qs = { + sendTo: this.getNodeParameter('email', i) as string, + } as IDataObject; + + const endpoint = `/v3/company/${companyId}/${resource}/${estimateId}/send`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, qs, {}); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'update') { + + // ---------------------------------- + // estimate: update + // ---------------------------------- + + const { ref, syncToken } = await getRefAndSyncToken.call(this, i, companyId, resource, 'CustomerRef'); + + let body = { + Id: this.getNodeParameter('estimateId', i), + SyncToken: syncToken, + sparse: true, + CustomerRef: { + name: ref.name, + value: ref.value, + }, + } as IDataObject; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (isEmpty(updateFields)) { + throw new Error(`Please enter at least one field to update for the ${resource}.`); + } + + body = populateFields.call(this, body, updateFields, resource); + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData[capitalCase(resource)]; + + } + + } else if (resource === 'invoice') { + + // ********************************************************************* + // invoice + // ********************************************************************* + + // https://developer.intuit.com/app/developer/qbo/docs/api/accounting/most-commonly-used/invoice + + if (operation === 'create') { + + // ---------------------------------- + // invoice: create + // ---------------------------------- + + const lines = this.getNodeParameter('Line', i) as IDataObject[]; + + if (!lines.length) { + throw new Error(`Please enter at least one line for the ${resource}.`); + } + + if (lines.some(line => line.DetailType === undefined || line.Amount === undefined || line.Description === undefined)) { + throw new Error('Please enter detail type, amount and description for every line.'); + } + + lines.forEach(line => { + if (line.DetailType === 'SalesItemLineDetail' && line.itemId === undefined) { + throw new Error('Please enter an item ID for the associated line.'); + } + }); + + let body = { + CustomerRef: { + value: this.getNodeParameter('CustomerRef', i), + }, + } as IDataObject; + + body.Line = processLines.call(this, body, lines, resource); + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + body = populateFields.call(this, body, additionalFields, resource); + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'delete') { + + // ---------------------------------- + // invoice: delete + // ---------------------------------- + + const qs = { + operation: 'delete', + } as IDataObject; + + const body = { + Id: this.getNodeParameter('invoiceId', i), + SyncToken: await getSyncToken.call(this, i, companyId, resource), + } as IDataObject; + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, qs, body); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'get') { + + // ---------------------------------- + // invoice: get + // ---------------------------------- + + const invoiceId = this.getNodeParameter('invoiceId', i) as string; + const download = this.getNodeParameter('download', i) as boolean; + + if (download) { + + responseData = await handleBinaryData.call(this, items, i, companyId, resource, invoiceId); + + } else { + + const endpoint = `/v3/company/${companyId}/${resource}/${invoiceId}`; + responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {}); + responseData = responseData[capitalCase(resource)]; + + } + + } else if (operation === 'getAll') { + + // ---------------------------------- + // invoice: getAll + // ---------------------------------- + + const endpoint = `/v3/company/${companyId}/query`; + responseData = await handleListing.call(this, i, endpoint, resource); + + } else if (operation === 'send') { + + // ---------------------------------- + // invoice: send + // ---------------------------------- + + const invoiceId = this.getNodeParameter('invoiceId', i) as string; + + const qs = { + sendTo: this.getNodeParameter('email', i) as string, + } as IDataObject; + + const endpoint = `/v3/company/${companyId}/${resource}/${invoiceId}/send`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, qs, {}); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'update') { + + // ---------------------------------- + // invoice: update + // ---------------------------------- + + const { ref, syncToken } = await getRefAndSyncToken.call(this, i, companyId, resource, 'CustomerRef'); + + let body = { + Id: this.getNodeParameter('invoiceId', i), + SyncToken: syncToken, + sparse: true, + CustomerRef: { + name: ref.name, + value: ref.value, + }, + } as IDataObject; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (isEmpty(updateFields)) { + throw new Error(`Please enter at least one field to update for the ${resource}.`); + } + + body = populateFields.call(this, body, updateFields, resource); + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'void') { + + // ---------------------------------- + // invoice: void + // ---------------------------------- + + const qs = { + Id: this.getNodeParameter('invoiceId', i), + SyncToken: await getSyncToken.call(this, i, companyId, resource), + operation: 'void', + } as IDataObject; + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, qs, {}); + responseData = responseData[capitalCase(resource)]; + + } + + } else if (resource === 'item') { + + // ********************************************************************* + // item + // ********************************************************************* + + // https://developer.intuit.com/app/developer/qbo/docs/api/accounting/most-commonly-used/item + + if (operation === 'get') { + + // ---------------------------------- + // item: get + // ---------------------------------- + + const item = this.getNodeParameter('itemId', i); + const endpoint = `/v3/company/${companyId}/${resource}/${item}`; + responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {}); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'getAll') { + + // ---------------------------------- + // item: getAll + // ---------------------------------- + + const endpoint = `/v3/company/${companyId}/query`; + responseData = await handleListing.call(this, i, endpoint, resource); + + } + + } else if (resource === 'payment') { + + // ********************************************************************* + // payment + // ********************************************************************* + + // https://developer.intuit.com/app/developer/qbo/docs/api/accounting/most-commonly-used/payment + + if (operation === 'create') { + + // ---------------------------------- + // payment: create + // ---------------------------------- + + let body = { + CustomerRef: { + value: this.getNodeParameter('CustomerRef', i), + }, + TotalAmt: this.getNodeParameter('TotalAmt', i), + } as IDataObject; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + body = populateFields.call(this, body, additionalFields, resource); + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'delete') { + + // ---------------------------------- + // payment: delete + // ---------------------------------- + + const qs = { + operation: 'delete', + } as IDataObject; + + const body = { + Id: this.getNodeParameter('paymentId', i), + SyncToken: await getSyncToken.call(this, i, companyId, resource), + } as IDataObject; + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, qs, body); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'get') { + + // ---------------------------------- + // payment: get + // ---------------------------------- + + const paymentId = this.getNodeParameter('paymentId', i) as string; + const download = this.getNodeParameter('download', i) as boolean; + + if (download) { + + responseData = await handleBinaryData.call(this, items, i, companyId, resource, paymentId); + + } else { + + const endpoint = `/v3/company/${companyId}/${resource}/${paymentId}`; + responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {}); + responseData = responseData[capitalCase(resource)]; + + } + + } else if (operation === 'getAll') { + + // ---------------------------------- + // payment: getAll + // ---------------------------------- + + const endpoint = `/v3/company/${companyId}/query`; + responseData = await handleListing.call(this, i, endpoint, resource); + + } else if (operation === 'send') { + + // ---------------------------------- + // payment: send + // ---------------------------------- + + const paymentId = this.getNodeParameter('paymentId', i) as string; + + const qs = { + sendTo: this.getNodeParameter('email', i) as string, + } as IDataObject; + + const endpoint = `/v3/company/${companyId}/${resource}/${paymentId}/send`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, qs, {}); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'update') { + + // ---------------------------------- + // payment: update + // ---------------------------------- + + const { ref, syncToken } = await getRefAndSyncToken.call(this, i, companyId, resource, 'CustomerRef'); + + let body = { + Id: this.getNodeParameter('paymentId', i), + SyncToken: syncToken, + sparse: true, + CustomerRef: { + name: ref.name, + value: ref.value, + }, + } as IDataObject; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (isEmpty(updateFields)) { + throw new Error(`Please enter at least one field to update for the ${resource}.`); + } + + body = populateFields.call(this, body, updateFields, resource); + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'void') { + + // ---------------------------------- + // payment: void + // ---------------------------------- + + const qs = { + Id: this.getNodeParameter('paymentId', i), + SyncToken: await getSyncToken.call(this, i, companyId, resource), + operation: 'void', + } as IDataObject; + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, qs, {}); + responseData = responseData[capitalCase(resource)]; + + } + + } else if (resource === 'vendor') { + + // ********************************************************************* + // vendor + // ********************************************************************* + + // https://developer.intuit.com/app/developer/qbo/docs/api/accounting/most-commonly-used/vendor + + if (operation === 'create') { + + // ---------------------------------- + // vendor: create + // ---------------------------------- + + let body = { + DisplayName: this.getNodeParameter('displayName', i), + } as IDataObject; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + body = populateFields.call(this, body, additionalFields, resource); + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'get') { + + // ---------------------------------- + // vendor: get + // ---------------------------------- + + const vendorId = this.getNodeParameter('vendorId', i); + const endpoint = `/v3/company/${companyId}/${resource}/${vendorId}`; + responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {}); + responseData = responseData[capitalCase(resource)]; + + } else if (operation === 'getAll') { + + // ---------------------------------- + // vendor: getAll + // ---------------------------------- + + const endpoint = `/v3/company/${companyId}/query`; + responseData = await handleListing.call(this, i, endpoint, resource); + + } else if (operation === 'update') { + + // ---------------------------------- + // vendor: update + // ---------------------------------- + + let body = { + Id: this.getNodeParameter('vendorId', i), + SyncToken: await getSyncToken.call(this, i, companyId, resource), + sparse: true, + } as IDataObject; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (isEmpty(updateFields)) { + throw new Error(`Please enter at least one field to update for the ${resource}.`); + } + + body = populateFields.call(this, body, updateFields, resource); + + const endpoint = `/v3/company/${companyId}/${resource}`; + responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body); + responseData = responseData[capitalCase(resource)]; + + } + + } + Array.isArray(responseData) + ? returnData.push(...responseData) + : returnData.push(responseData); + } + + const download = this.getNodeParameter('download', 0, false) as boolean; + + if (['invoice', 'estimate', 'payment'].includes(resource) && ['get'].includes(operation) && download) { + return this.prepareOutputData(responseData); + } else { + return [this.helpers.returnJsonArray(returnData)]; + } + } +} diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Bill/BillAdditionalFieldsOptions.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Bill/BillAdditionalFieldsOptions.ts new file mode 100644 index 0000000000..5d872f6461 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Bill/BillAdditionalFieldsOptions.ts @@ -0,0 +1,86 @@ +export const billAdditionalFieldsOptions = [ + { + displayName: 'Accounts Payable Account', + name: 'APAccountRef', + placeholder: 'Add APA Fields', + description: 'Accounts Payable account to which the bill will be credited.', + type: 'fixedCollection', + default: {}, + options: [ + { + displayName: 'Details', + name: 'details', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + }, + { + displayName: 'ID', + name: 'value', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Balance', + name: 'Balance', + description: 'The balance reflecting any payments made against the transaction.', + type: 'string', + default: '', + }, + { + displayName: 'Due Date', + name: 'DueDate', + description: 'Date when the payment of the transaction is due.', + type: 'dateTime', + default: '', + }, + { + displayName: 'Sales Term', + name: 'SalesTermRef', + description: 'Sales term associated with the transaction.', + placeholder: 'Add Sales Term Fields', + type: 'fixedCollection', + default: {}, + options: [ + { + displayName: 'Details', + name: 'details', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + }, + { + displayName: 'ID', + name: 'value', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Total Amount', + name: 'TotalAmt', + description: 'Total amount of the transaction.', + type: 'number', + default: 0, + }, + { + displayName: 'Transaction Date', + name: 'TxnDate', + description: 'Date when the transaction occurred.', + type: 'dateTime', + default: '', + }, +]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Bill/BillDescription.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Bill/BillDescription.ts new file mode 100644 index 0000000000..9a1dcff0f9 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Bill/BillDescription.ts @@ -0,0 +1,330 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + billAdditionalFieldsOptions, +} from './BillAdditionalFieldsOptions'; + +export const billOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Delete', + value: 'delete', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Update', + value: 'update', + }, + ], + displayOptions: { + show: { + resource: [ + 'bill', + ], + }, + }, + }, +] as INodeProperties[]; + +export const billFields = [ + // ---------------------------------- + // bill: create + // ---------------------------------- + { + displayName: 'For Vendor', + name: 'VendorRef', + type: 'options', + required: true, + description: 'The ID of the vendor who the bill is for.', + default: [], + typeOptions: { + loadOptionsMethod: 'getVendors', + }, + displayOptions: { + show: { + resource: [ + 'bill', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Line', + name: 'Line', + type: 'collection', + placeholder: 'Add Line Item Property', + description: 'Individual line item of a transaction.', + typeOptions: { + multipleValues: true, + }, + default: {}, + displayOptions: { + show: { + resource: [ + 'bill', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Detail Type', + name: 'DetailType', + type: 'options', + default: 'ItemBasedExpenseLineDetail', + options: [ + { + name: 'Account-Based Expense Line Detail', + value: 'AccountBasedExpenseLineDetail', + }, + { + name: 'Item-Based Expense Line Detail', + value: 'ItemBasedExpenseLineDetail', + }, + ], + }, + { + displayName: 'Item', + name: 'itemId', + type: 'options', + default: [], + typeOptions: { + loadOptionsMethod: 'getItems', + }, + }, + { + displayName: 'Account ID', + name: 'accountId', + type: 'string', + default: '', + }, + { + displayName: 'Amount', + name: 'Amount', + description: 'Monetary amount of the line item.', + type: 'number', + default: 0, + }, + { + displayName: 'Description', + name: 'Description', + description: 'Textual description of the line item.', + type: 'string', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, + { + displayName: 'Position', + name: 'LineNum', + description: 'Position of the line item relative to others.', + type: 'number', + default: 1, + }, + ], + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'bill', + ], + operation: [ + 'create', + ], + }, + }, + options: billAdditionalFieldsOptions, + }, + + // ---------------------------------- + // bill: delete + // ---------------------------------- + { + displayName: 'Bill ID', + name: 'billId', + type: 'string', + required: true, + default: '', + description: 'The ID of the bill to delete.', + displayOptions: { + show: { + resource: [ + 'bill', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------- + // bill: get + // ---------------------------------- + { + displayName: 'Bill ID', + name: 'billId', + type: 'string', + required: true, + default: '', + description: 'The ID of the bill to retrieve.', + displayOptions: { + show: { + resource: [ + 'bill', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------- + // bill: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'bill', + ], + 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: [ + 'bill', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + placeholder: 'WHERE Metadata.LastUpdatedTime > \'2021-01-01\'', + description: 'The condition for selecting bills. See the guide for supported syntax.', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, + ], + displayOptions: { + show: { + resource: [ + 'bill', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + + // ---------------------------------- + // bill: update + // ---------------------------------- + { + displayName: 'Bill ID', + name: 'billId', + type: 'string', + required: true, + default: '', + description: 'The ID of the bill to update.', + displayOptions: { + show: { + resource: [ + 'bill', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + required: true, + displayOptions: { + show: { + resource: [ + 'bill', + ], + operation: [ + 'update', + ], + }, + }, + // filter out fields that cannot be updated + options: billAdditionalFieldsOptions.filter(property => property.name !== 'TotalAmt' && property.name !== 'Balance'), + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Customer/CustomerAdditionalFieldsOptions.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Customer/CustomerAdditionalFieldsOptions.ts new file mode 100644 index 0000000000..db037f9ff5 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Customer/CustomerAdditionalFieldsOptions.ts @@ -0,0 +1,151 @@ +export const customerAdditionalFieldsOptions = [ + { + displayName: 'Active', + name: 'Active', + description: 'Whether the customer is currently enabled for use by QuickBooks.', + type: 'boolean', + default: true, + }, + { + displayName: 'Balance', + name: 'Balance', + description: 'Open balance amount or amount unpaid by the customer.', + type: 'string', + default: '', + }, + { + displayName: 'Balance With Jobs', + name: 'BalanceWithJobs', + description: 'Cumulative open balance amount for the customer (or job) and all its sub-jobs.', + type: 'number', + default: 0, + }, + { + displayName: 'Billing Address', + name: 'BillAddr', + placeholder: 'Add Billing Address Fields', + type: 'fixedCollection', + default: {}, + options: [ + { + displayName: 'Details', + name: 'details', + values: [ + { + displayName: 'City', + name: 'City', + type: 'string', + default: '', + }, + { + displayName: 'Line 1', + name: 'Line1', + type: 'string', + default: '', + }, + { + displayName: 'Postal Code', + name: 'PostalCode', + type: 'string', + default: '', + }, + { + displayName: 'Latitude', + name: 'Lat', + type: 'string', + default: '', + }, + { + displayName: 'Longitude', + name: 'Long', + type: 'string', + default: '', + }, + { + displayName: 'Country Subdivision Code', + name: 'CountrySubDivisionCode', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Bill With Parent', + name: 'BillWithParent', + description: 'Bill this customer together with its parent.', + type: 'boolean', + default: false, + }, + { + displayName: 'Company Name', + name: 'CompanyName', + type: 'string', + default: '', + }, + { + displayName: 'Family Name', + name: 'FamilyName', + type: 'string', + default: '', + }, + { + displayName: 'Fully Qualified Name', + name: 'FullyQualifiedName', + type: 'string', + default: '', + }, + { + displayName: 'Given Name', + name: 'GivenName', + type: 'string', + default: '', + }, + { + displayName: 'Preferred Delivery Method', + name: 'PreferredDeliveryMethod', + type: 'options', + default: 'Print', + options: [ + { + name: 'Print', + value: 'Print', + }, + { + name: 'Email', + value: 'Email', + }, + { + name: 'None', + value: 'None', + }, + ], + }, + { + displayName: 'Primary Email Address', + name: 'PrimaryEmailAddr', + type: 'string', + default: '', + }, + { + displayName: 'Primary Phone', + name: 'PrimaryPhone', + type: 'string', + default: '', + }, + { + displayName: 'Print-On-Check Name', + name: 'PrintOnCheckName', + description: 'Name of the customer as printed on a check.', + type: 'string', + default: '', + }, + { + displayName: 'Taxable', + name: 'Taxable', + description: 'Whether transactions for this customer are taxable.', + type: 'boolean', + default: false, + }, +]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Customer/CustomerDescription.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Customer/CustomerDescription.ts new file mode 100644 index 0000000000..f4735d2b55 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Customer/CustomerDescription.ts @@ -0,0 +1,222 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + customerAdditionalFieldsOptions, +} from './CustomerAdditionalFieldsOptions'; + +export const customerOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Update', + value: 'update', + }, + ], + displayOptions: { + show: { + resource: [ + 'customer', + ], + }, + }, + }, +] as INodeProperties[]; + +export const customerFields = [ + // ---------------------------------- + // customer: create + // ---------------------------------- + { + displayName: 'Display Name', + name: 'displayName', + type: 'string', + required: true, + default: '', + description: 'The display name of the customer to create.', + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'create', + ], + }, + }, + options: customerAdditionalFieldsOptions, + }, + + // ---------------------------------- + // customer: get + // ---------------------------------- + { + displayName: 'Customer ID', + name: 'customerId', + type: 'string', + required: true, + default: '', + description: 'The ID of the customer to retrieve.', + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------- + // customer: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'customer', + ], + 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: [ + 'customer', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + placeholder: 'WHERE Metadata.LastUpdatedTime > \'2021-01-01\'', + description: 'The condition for selecting customers. See the guide for supported syntax.', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, + ], + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + + // ---------------------------------- + // customer: update + // ---------------------------------- + { + displayName: 'Customer ID', + name: 'customerId', + type: 'string', + required: true, + default: '', + description: 'The ID of the customer to update.', + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + required: true, + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'update', + ], + }, + }, + options: customerAdditionalFieldsOptions, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Employee/EmployeeAdditionalFieldsOptions.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Employee/EmployeeAdditionalFieldsOptions.ts new file mode 100644 index 0000000000..e10304ad33 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Employee/EmployeeAdditionalFieldsOptions.ts @@ -0,0 +1,91 @@ +export const employeeAdditionalFieldsOptions = [ + { + displayName: 'Active', + name: 'Active', + description: 'Whether the employee is currently enabled for use by QuickBooks.', + type: 'boolean', + default: false, + }, + { + displayName: 'Billable Time', + name: 'BillableTime', + type: 'boolean', + default: false, + }, + { + displayName: 'Display Name', + name: 'DisplayName', + type: 'string', + default: '', + }, + { + displayName: 'Billing Address', + name: 'BillAddr', + placeholder: 'Add Billing Address Fields', + type: 'fixedCollection', + default: {}, + options: [ + { + displayName: 'Details', + name: 'details', + values: [ + { + displayName: 'City', + name: 'City', + type: 'string', + default: '', + }, + { + displayName: 'Line 1', + name: 'Line1', + type: 'string', + default: '', + }, + { + displayName: 'Postal Code', + name: 'PostalCode', + type: 'string', + default: '', + }, + { + displayName: 'Latitude', + name: 'Lat', + type: 'string', + default: '', + }, + { + displayName: 'Longitude', + name: 'Long', + type: 'string', + default: '', + }, + { + displayName: 'Country Subdivision Code', + name: 'CountrySubDivisionCode', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Primary Phone', + name: 'PrimaryPhone', + type: 'string', + default: '', + }, + { + displayName: 'Print-On-Check Name', + name: 'PrintOnCheckName', + description: 'Name of the employee as printed on a check.', + type: 'string', + default: '', + }, + { + displayName: 'Social Security Number', + name: 'SSN', + type: 'string', + default: '', + }, +]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Employee/EmployeeDescription.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Employee/EmployeeDescription.ts new file mode 100644 index 0000000000..7e6b001aed --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Employee/EmployeeDescription.ts @@ -0,0 +1,236 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + employeeAdditionalFieldsOptions, +} from './EmployeeAdditionalFieldsOptions'; + +export const employeeOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Update', + value: 'update', + }, + ], + displayOptions: { + show: { + resource: [ + 'employee', + ], + }, + }, + }, +] as INodeProperties[]; + +export const employeeFields = [ + // ---------------------------------- + // employee: create + // ---------------------------------- + { + displayName: 'Family Name', + name: 'FamilyName', + type: 'string', + default: '', + displayOptions: { + show: { + resource: [ + 'employee', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Given Name', + name: 'GivenName', + type: 'string', + default: '', + displayOptions: { + show: { + resource: [ + 'employee', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'employee', + ], + operation: [ + 'create', + ], + }, + }, + options: employeeAdditionalFieldsOptions, + }, + + // ---------------------------------- + // employee: get + // ---------------------------------- + { + displayName: 'Employee ID', + name: 'employeeId', + type: 'string', + required: true, + default: '', + description: 'The ID of the employee to retrieve.', + displayOptions: { + show: { + resource: [ + 'employee', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------- + // employee: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'employee', + ], + 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: [ + 'employee', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + placeholder: 'WHERE Metadata.LastUpdatedTime > \'2021-01-01\'', + description: 'The condition for selecting employees. See the guide for supported syntax.', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, + ], + displayOptions: { + show: { + resource: [ + 'employee', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + + // ---------------------------------- + // employee: update + // ---------------------------------- + { + displayName: 'Employee ID', + name: 'employeeId', + type: 'string', + required: true, + default: '', + description: 'The ID of the employee to update.', + displayOptions: { + show: { + resource: [ + 'employee', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + required: true, + displayOptions: { + show: { + resource: [ + 'employee', + ], + operation: [ + 'update', + ], + }, + }, + options: employeeAdditionalFieldsOptions, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Estimate/EstimateAdditionalFieldsOptions.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Estimate/EstimateAdditionalFieldsOptions.ts new file mode 100644 index 0000000000..9532b7f442 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Estimate/EstimateAdditionalFieldsOptions.ts @@ -0,0 +1,227 @@ +export const estimateAdditionalFieldsOptions = [ + { + displayName: 'Apply Tax After Discount', + name: 'ApplyTaxAfterDiscount', + type: 'boolean', + default: false, + }, + { + displayName: 'Billing Address', + name: 'BillAddr', + placeholder: 'Add Billing Address Fields', + type: 'fixedCollection', + default: {}, + options: [ + { + displayName: 'Details', + name: 'details', + values: [ + { + displayName: 'City', + name: 'City', + type: 'string', + default: '', + }, + { + displayName: 'Line 1', + name: 'Line1', + type: 'string', + default: '', + }, + { + displayName: 'Postal Code', + name: 'PostalCode', + type: 'string', + default: '', + }, + { + displayName: 'Latitude', + name: 'Lat', + type: 'string', + default: '', + }, + { + displayName: 'Longitude', + name: 'Long', + type: 'string', + default: '', + }, + { + displayName: 'Country Subdivision Code', + name: 'CountrySubDivisionCode', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Billing Email', + name: 'BillEmail', + description: 'E-mail address to which the estimate will be sent.', + type: 'string', + default: '', + }, + { + displayName: 'Custom Fields', + name: 'CustomFields', + placeholder: 'Add Custom Fields', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + displayName: 'Field', + name: 'Field', + values: [ + { + displayName: 'Field Definition ID', + name: 'DefinitionId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCustomFields', + }, + default: '', + description: 'ID of the field to set.', + }, + { + displayName: 'Field Value', + name: 'StringValue', + type: 'string', + default: '', + description: 'Value of the field to set.', + }, + ], + }, + ], + }, + { + displayName: 'Customer Memo', + name: 'CustomerMemo', + description: 'User-entered message to the customer. This message is visible to end user on their transactions.', + type: 'string', + default: '', + }, + { + displayName: 'Document Number', + name: 'DocNumber', + description: 'Reference number for the transaction.', + type: 'string', + default: '', + }, + { + displayName: 'Email Status', + name: 'EmailStatus', + type: 'options', + default: 'NotSet', + options: [ + { + name: 'Not Set', + value: 'NotSet', + }, + { + name: 'Need To Send', + value: 'NeedToSend', + }, + { + name: 'Email Sent', + value: 'EmailSent', + }, + ], + }, + { + displayName: 'Print Status', + name: 'PrintStatus', + type: 'options', + default: 'NotSet', + options: [ + { + name: 'Not Set', + value: 'NotSet', + }, + { + name: 'Need To Print', + value: 'NeedToPrint', + }, + { + name: 'PrintComplete', + value: 'PrintComplete', + }, + ], + }, + { + displayName: 'Shipping Address', + name: 'ShipAddr', + placeholder: 'Add Shippping Address Fields', + type: 'fixedCollection', + default: {}, + options: [ + { + displayName: 'Details', + name: 'details', + values: [ + { + displayName: 'City', + name: 'City', + type: 'string', + default: '', + }, + { + displayName: 'Line 1', + name: 'Line1', + type: 'string', + default: '', + }, + { + displayName: 'Postal Code', + name: 'PostalCode', + type: 'string', + default: '', + }, + { + displayName: 'Latitude', + name: 'Lat', + type: 'string', + default: '', + }, + { + displayName: 'Longitude', + name: 'Long', + type: 'string', + default: '', + }, + { + displayName: 'Country Subdivision Code', + name: 'CountrySubDivisionCode', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Total Amount', + name: 'TotalAmt', + description: 'Total amount of the transaction.', + type: 'number', + default: 0, + }, + { + displayName: 'Transaction Date', + name: 'TxnDate', + description: 'Date when the transaction occurred.', + type: 'dateTime', + default: '', + }, + { + displayName: 'Total Tax', + name: 'TotalTax', + description: 'Total amount of tax incurred.', + type: 'number', + default: 0, + }, +]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Estimate/EstimateDescription.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Estimate/EstimateDescription.ts new file mode 100644 index 0000000000..2768c675f0 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Estimate/EstimateDescription.ts @@ -0,0 +1,425 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + estimateAdditionalFieldsOptions, +} from './EstimateAdditionalFieldsOptions'; + +export const estimateOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Delete', + value: 'delete', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Send', + value: 'send', + }, + { + name: 'Update', + value: 'update', + }, + ], + displayOptions: { + show: { + resource: [ + 'estimate', + ], + }, + }, + }, +] as INodeProperties[]; + +export const estimateFields = [ + // ---------------------------------- + // estimate: create + // ---------------------------------- + { + displayName: 'For Customer', + name: 'CustomerRef', + type: 'options', + required: true, + description: 'The ID of the customer who the estimate is for.', + default: [], + typeOptions: { + loadOptionsMethod: 'getCustomers', + }, + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Line', + name: 'Line', + type: 'collection', + placeholder: 'Add Line Item Property', + description: 'Individual line item of a transaction.', + typeOptions: { + multipleValues: true, + }, + default: {}, + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Detail Type', + name: 'DetailType', + type: 'options', + default: 'SalesItemLineDetail', + options: [ + { + name: 'Sales Item Line Detail', + value: 'SalesItemLineDetail', + }, + ], + }, + { + displayName: 'Item', + name: 'itemId', + type: 'options', + default: [], + typeOptions: { + loadOptionsMethod: 'getItems', + }, + }, + { + displayName: 'Amount', + name: 'Amount', + description: 'Monetary amount of the line item.', + type: 'number', + default: 0, + }, + { + displayName: 'Description', + name: 'Description', + description: 'Textual description of the line item.', + type: 'string', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, + { + displayName: 'Position', + name: 'LineNum', + description: 'Position of the line item relative to others.', + type: 'number', + default: 1, + }, + ], + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'create', + ], + }, + }, + options: estimateAdditionalFieldsOptions, + }, + + // ---------------------------------- + // estimate: delete + // ---------------------------------- + { + displayName: 'Estimate ID', + name: 'estimateId', + type: 'string', + required: true, + default: '', + description: 'The ID of the estimate to delete.', + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------- + // estimate: get + // ---------------------------------- + { + displayName: 'Estimate ID', + name: 'estimateId', + type: 'string', + required: true, + default: '', + description: 'The ID of the estimate to retrieve.', + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Download', + name: 'download', + type: 'boolean', + required: true, + default: false, + description: 'Download the estimate as a PDF file.', + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Binary Property', + name: 'binaryProperty', + type: 'string', + required: true, + default: 'data', + description: 'Name of the binary property to which to write to.', + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'get', + ], + download: [ + true, + ], + }, + }, + }, + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + required: true, + default: '', + placeholder: 'data.pdf', + description: 'Name of the file that will be downloaded.', + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'get', + ], + download: [ + true, + ], + }, + }, + }, + + // ---------------------------------- + // estimate: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'estimate', + ], + 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: [ + 'estimate', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + placeholder: 'WHERE Metadata.LastUpdatedTime > \'2021-01-01\'', + description: 'The condition for selecting estimates. See the guide for supported syntax.', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, + ], + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + + // ---------------------------------- + // estimate: send + // ---------------------------------- + { + displayName: 'Estimate ID', + name: 'estimateId', + type: 'string', + required: true, + default: '', + description: 'The ID of the estimate to send.', + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'send', + ], + }, + }, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + required: true, + default: '', + description: 'The email of the recipient of the estimate.', + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'send', + ], + }, + }, + }, + + // ---------------------------------- + // estimate: update + // ---------------------------------- + { + displayName: 'Estimate ID', + name: 'estimateId', + type: 'string', + required: true, + default: '', + description: 'The ID of the estimate to update.', + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + required: true, + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'update', + ], + }, + }, + // filter out fields that cannot be updated + options: estimateAdditionalFieldsOptions.filter(property => property.name !== 'TotalAmt' && property.name !== 'TotalTax'), + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Invoice/InvoiceAdditionalFieldsOptions.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Invoice/InvoiceAdditionalFieldsOptions.ts new file mode 100644 index 0000000000..49a70e085e --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Invoice/InvoiceAdditionalFieldsOptions.ts @@ -0,0 +1,183 @@ +export const invoiceAdditionalFieldsOptions = [ + { + displayName: 'Balance', + name: 'Balance', + description: 'The balance reflecting any payments made against the transaction.', + type: 'number', + default: 0, + }, + { + displayName: 'Billing Address', + name: 'BillAddr', + placeholder: 'Add Billing Address Fields', + type: 'fixedCollection', + default: {}, + options: [ + { + displayName: 'Details', + name: 'details', + values: [ + { + displayName: 'City', + name: 'City', + type: 'string', + default: '', + }, + { + displayName: 'Line 1', + name: 'Line1', + type: 'string', + default: '', + }, + { + displayName: 'Postal Code', + name: 'PostalCode', + type: 'string', + default: '', + }, + { + displayName: 'Latitude', + name: 'Lat', + type: 'string', + default: '', + }, + { + displayName: 'Longitude', + name: 'Long', + type: 'string', + default: '', + }, + { + displayName: 'Country Subdivision Code', + name: 'CountrySubDivisionCode', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Billing Email', + name: 'BillEmail', + description: 'E-mail address to which the invoice will be sent.', + type: 'string', + default: '', + }, + { + displayName: 'Customer Memo', + name: 'CustomerMemo', + description: 'User-entered message to the customer. This message is visible to end user on their transactions.', + type: 'string', + default: '', + }, + { + displayName: 'Custom Fields', + name: 'CustomFields', + placeholder: 'Add Custom Fields', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + displayName: 'Field', + name: 'Field', + values: [ + { + displayName: 'Field Definition ID', + name: 'DefinitionId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCustomFields', + }, + default: '', + description: 'ID of the field to set.', + }, + { + displayName: 'Field Value', + name: 'StringValue', + type: 'string', + default: '', + description: 'Value of the field to set.', + }, + ], + }, + ], + }, + { + displayName: 'Document Number', + name: 'DocNumber', + description: 'Reference number for the transaction.', + type: 'string', + default: '', + }, + { + displayName: 'Due Date', + name: 'DueDate', + description: 'Date when the payment of the transaction is due.', + type: 'dateTime', + default: '', + }, + { + displayName: 'Email Status', + name: 'EmailStatus', + type: 'options', + default: 'NotSet', + options: [ + { + name: 'Not Set', + value: 'NotSet', + }, + { + name: 'Need To Send', + value: 'NeedToSend', + }, + { + name: 'Email Sent', + value: 'EmailSent', + }, + ], + }, + { + displayName: 'Print Status', + name: 'PrintStatus', + type: 'options', + default: 'NotSet', + options: [ + { + name: 'Not Set', + value: 'NotSet', + }, + { + name: 'Need To Print', + value: 'NeedToPrint', + }, + { + name: 'PrintComplete', + value: 'PrintComplete', + }, + ], + }, + { + displayName: 'Shipping Address', + name: 'ShipAddr', + type: 'string', + default: '', + }, + { + displayName: 'Total Amount', + name: 'TotalAmt', + description: 'Total amount of the transaction.', + type: 'number', + default: 0, + }, + { + displayName: 'Transaction Date', + name: 'TxnDate', + description: 'Date when the transaction occurred.', + type: 'dateTime', + default: '', + }, +]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Invoice/InvoiceDescription.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Invoice/InvoiceDescription.ts new file mode 100644 index 0000000000..d9a4eee478 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Invoice/InvoiceDescription.ts @@ -0,0 +1,451 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + invoiceAdditionalFieldsOptions +} from './InvoiceAdditionalFieldsOptions'; + +export const invoiceOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Delete', + value: 'delete', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Send', + value: 'send', + }, + { + name: 'Update', + value: 'update', + }, + { + name: 'Void', + value: 'void', + }, + ], + displayOptions: { + show: { + resource: [ + 'invoice', + ], + }, + }, + }, +] as INodeProperties[]; + +export const invoiceFields = [ + // ---------------------------------- + // invoice: create + // ---------------------------------- + { + displayName: 'For Customer', + name: 'CustomerRef', + type: 'options', + required: true, + description: 'The ID of the customer who the invoice is for.', + default: [], + typeOptions: { + loadOptionsMethod: 'getCustomers', + }, + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Line', + name: 'Line', + type: 'collection', + placeholder: 'Add Line Item Property', + description: 'Individual line item of a transaction.', + typeOptions: { + multipleValues: true, + }, + default: {}, + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Detail Type', + name: 'DetailType', + type: 'options', + default: 'SalesItemLineDetail', + options: [ + { + name: 'Sales Item Line Detail', + value: 'SalesItemLineDetail', + }, + ], + }, + { + displayName: 'Item', + name: 'itemId', + type: 'options', + default: [], + typeOptions: { + loadOptionsMethod: 'getItems', + }, + }, + { + displayName: 'Amount', + name: 'Amount', + description: 'Monetary amount of the line item.', + type: 'number', + default: 0, + }, + { + displayName: 'Description', + name: 'Description', + description: 'Textual description of the line item.', + type: 'string', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, + { + displayName: 'Position', + name: 'LineNum', + description: 'Position of the line item relative to others.', + type: 'number', + default: 1, + }, + ], + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'create', + ], + }, + }, + options: invoiceAdditionalFieldsOptions, + }, + + // ---------------------------------- + // invoice: delete + // ---------------------------------- + { + displayName: 'Invoice ID', + name: 'invoiceId', + type: 'string', + required: true, + default: '', + description: 'The ID of the invoice to delete.', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------- + // invoice: get + // ---------------------------------- + { + displayName: 'Invoice ID', + name: 'invoiceId', + type: 'string', + required: true, + default: '', + description: 'The ID of the invoice to retrieve.', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Download', + name: 'download', + type: 'boolean', + required: true, + default: false, + description: 'Download the invoice as a PDF file.', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Binary Property', + name: 'binaryProperty', + type: 'string', + required: true, + default: 'data', + description: 'Name of the binary property to which to write to.', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'get', + ], + download: [ + true, + ], + }, + }, + }, + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + required: true, + default: '', + placeholder: 'data.pdf', + description: 'Name of the file that will be downloaded.', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'get', + ], + download: [ + true, + ], + }, + }, + }, + + // ---------------------------------- + // invoice: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + 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: [ + 'invoice', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + placeholder: 'WHERE Metadata.LastUpdatedTime > \'2021-01-01\'', + description: 'The condition for selecting invoices. See the guide for supported syntax.', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, + ], + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + + // ---------------------------------- + // invoice: send + // ---------------------------------- + { + displayName: 'Invoice ID', + name: 'invoiceId', + type: 'string', + required: true, + default: '', + description: 'The ID of the invoice to send.', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'send', + ], + }, + }, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + required: true, + default: '', + description: 'The email of the recipient of the invoice.', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'send', + ], + }, + }, + }, + + // ---------------------------------- + // invoice: void + // ---------------------------------- + { + displayName: 'Invoice ID', + name: 'invoiceId', + type: 'string', + required: true, + default: '', + description: 'The ID of the invoice to void.', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'void', + ], + }, + }, + }, + + // ---------------------------------- + // invoice: update + // ---------------------------------- + { + displayName: 'Invoice ID', + name: 'invoiceId', + type: 'string', + required: true, + default: '', + description: 'The ID of the invoice to update.', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + required: true, + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'update', + ], + }, + }, + // filter out fields that cannot be updated + options: invoiceAdditionalFieldsOptions.filter(property => property.name !== 'TotalAmt' && property.name !== 'Balance'), + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Item/ItemDescription.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Item/ItemDescription.ts new file mode 100644 index 0000000000..91a23cabe8 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Item/ItemDescription.ts @@ -0,0 +1,129 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const itemOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + ], + displayOptions: { + show: { + resource: [ + 'item', + ], + }, + }, + }, +] as INodeProperties[]; + +export const itemFields = [ + // ---------------------------------- + // item: get + // ---------------------------------- + { + displayName: 'Item ID', + name: 'itemId', + type: 'string', + required: true, + default: '', + description: 'The ID of the item to retrieve.', + displayOptions: { + show: { + resource: [ + 'item', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------- + // item: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'item', + ], + 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: [ + 'item', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + placeholder: 'WHERE Metadata.LastUpdatedTime > \'2021-01-01\'', + description: 'The condition for selecting items. See the guide for supported syntax.', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, + ], + displayOptions: { + show: { + resource: [ + 'item', + ], + operation: [ + 'getAll', + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Payment/PaymentAdditionalFieldsOptions.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Payment/PaymentAdditionalFieldsOptions.ts new file mode 100644 index 0000000000..e683fd2e2b --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Payment/PaymentAdditionalFieldsOptions.ts @@ -0,0 +1,9 @@ +export const paymentAdditionalFieldsOptions = [ + { + displayName: 'Transaction Date', + name: 'TxnDate', + description: 'Date when the transaction occurred.', + type: 'dateTime', + default: '', + }, +]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Payment/PaymentDescription.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Payment/PaymentDescription.ts new file mode 100644 index 0000000000..9b6c58bc85 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Payment/PaymentDescription.ts @@ -0,0 +1,399 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + paymentAdditionalFieldsOptions +} from './PaymentAdditionalFieldsOptions'; + +export const paymentOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Delete', + value: 'delete', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Send', + value: 'send', + }, + { + name: 'Update', + value: 'update', + }, + { + name: 'Void', + value: 'void', + }, + ], + displayOptions: { + show: { + resource: [ + 'payment', + ], + }, + }, + }, +] as INodeProperties[]; + +export const paymentFields = [ + // ---------------------------------- + // payment: create + // ---------------------------------- + { + displayName: 'For Customer ID', + name: 'CustomerRef', + type: 'options', + required: true, + description: 'The ID of the customer who the payment is for.', + default: [], + typeOptions: { + loadOptionsMethod: 'getCustomers', + }, + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Total Amount', + name: 'TotalAmt', + description: 'Total amount of the transaction.', + type: 'number', + default: 0, + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'create', + ], + }, + }, + options: paymentAdditionalFieldsOptions, + }, + + // ---------------------------------- + // payment: delete + // ---------------------------------- + { + displayName: 'Payment ID', + name: 'paymentId', + type: 'string', + required: true, + default: '', + description: 'The ID of the payment to delete.', + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------- + // payment: get + // ---------------------------------- + { + displayName: 'Payment ID', + name: 'paymentId', + type: 'string', + required: true, + default: '', + description: 'The ID of the payment to retrieve.', + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Download', + name: 'download', + type: 'boolean', + required: true, + default: false, + description: 'Download estimate as PDF file', + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Binary Property', + name: 'binaryProperty', + type: 'string', + required: true, + default: 'data', + description: 'Name of the binary property to which to write to.', + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'get', + ], + download: [ + true, + ], + }, + }, + }, + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + required: true, + default: '', + placeholder: 'data.pdf', + description: 'Name of the file that will be downloaded.', + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'get', + ], + download: [ + true, + ], + }, + }, + }, + + // ---------------------------------- + // payment: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'payment', + ], + 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: [ + 'payment', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + placeholder: 'WHERE Metadata.LastUpdatedTime > \'2021-01-01\'', + description: 'The condition for selecting payments. See the guide for supported syntax.', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, + ], + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + + // ---------------------------------- + // payment: send + // ---------------------------------- + { + displayName: 'Payment ID', + name: 'paymentId', + type: 'string', + required: true, + default: '', + description: 'The ID of the payment to send.', + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'send', + ], + }, + }, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + required: true, + default: '', + description: 'The email of the recipient of the payment.', + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'send', + ], + }, + }, + }, + + // ---------------------------------- + // payment: void + // ---------------------------------- + { + displayName: 'Payment ID', + name: 'paymentId', + type: 'string', + required: true, + default: '', + description: 'The ID of the payment to void.', + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'void', + ], + }, + }, + }, + + // ---------------------------------- + // payment: update + // ---------------------------------- + { + displayName: 'Payment ID', + name: 'paymentId', + type: 'string', + required: true, + default: '', + description: 'The ID of the payment to update.', + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + required: true, + displayOptions: { + show: { + resource: [ + 'payment', + ], + operation: [ + 'update', + ], + }, + }, + options: paymentAdditionalFieldsOptions, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Shared.interface.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Shared.interface.ts new file mode 100644 index 0000000000..b3994c66eb --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Shared.interface.ts @@ -0,0 +1,48 @@ +export interface BillingAddress { + Line4: string; + Line3: string; + Line2: string; + Line1: string; + Long: string; + Lat: string; +} + +export interface BillEmail { + Address: string; +} + +export interface CustomField { + DefinitionId: string; + Name: string; +} + +export interface CustomerMemo { + value: string; +} + +export interface GeneralAddress { + City: string; + Line1: string; + PostalCode: string; + Lat: string; + Long: string; + CountrySubDivisionCode: string; +} + +export interface LinkedTxn { + TxnId: string; + TxnType: string; +} + +export interface PrimaryEmailAddr { + Address: string; +} + +export interface PrimaryPhone { + FreeFormNumber: string; +} + +export interface Ref { + value: string; + name?: string; +} diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Vendor/VendorAdditionalFieldsOptions.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Vendor/VendorAdditionalFieldsOptions.ts new file mode 100644 index 0000000000..68b27a208b --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Vendor/VendorAdditionalFieldsOptions.ts @@ -0,0 +1,117 @@ +export const vendorAdditionalFieldsOptions = [ + { + displayName: 'Account Number', + name: 'AcctNum', + type: 'string', + default: '', + }, + { + displayName: 'Active', + name: 'Active', + description: 'Whether the employee is currently enabled for use by QuickBooks.', + type: 'boolean', + default: false, + }, + { + displayName: 'Balance', + name: 'Balance', + description: 'The balance reflecting any payments made against the transaction.', + type: 'number', + default: 0, + }, + { + displayName: 'Billing Address', + name: 'BillAddr', + placeholder: 'Add Billing Address Fields', + type: 'fixedCollection', + default: {}, + options: [ + { + displayName: 'Details', + name: 'details', + values: [ + { + displayName: 'City', + name: 'City', + type: 'string', + default: '', + }, + { + displayName: 'Line 1', + name: 'Line1', + type: 'string', + default: '', + }, + { + displayName: 'Postal Code', + name: 'PostalCode', + type: 'string', + default: '', + }, + { + displayName: 'Latitude', + name: 'Lat', + type: 'string', + default: '', + }, + { + displayName: 'Longitude', + name: 'Long', + type: 'string', + default: '', + }, + { + displayName: 'Country Subdivision Code', + name: 'CountrySubDivisionCode', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Company Name', + name: 'CompanyName', + type: 'string', + default: '', + }, + { + displayName: 'Family Name', + name: 'FamilyName', + type: 'string', + default: '', + }, + { + displayName: 'Given Name', + name: 'GivenName', + type: 'string', + default: '', + }, + { + displayName: 'Primary Email Address', + name: 'PrimaryEmailAddr', + type: 'string', + default: '', + }, + { + displayName: 'Primary Phone', + name: 'PrimaryPhone', + type: 'string', + default: '', + }, + { + displayName: 'Print-On-Check Name', + name: 'PrintOnCheckName', + description: 'Name of the vendor as printed on a check.', + type: 'string', + default: '', + }, + { + displayName: 'Vendor 1099', + name: 'Vendor1099', + description: 'Whether the vendor is an independent contractor, given a 1099-MISC form at the end of the year.', + type: 'boolean', + default: false, + }, +]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Vendor/VendorDescription.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Vendor/VendorDescription.ts new file mode 100644 index 0000000000..09c1030912 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Vendor/VendorDescription.ts @@ -0,0 +1,222 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + vendorAdditionalFieldsOptions, +} from './VendorAdditionalFieldsOptions'; + +export const vendorOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Update', + value: 'update', + }, + ], + displayOptions: { + show: { + resource: [ + 'vendor', + ], + }, + }, + }, +] as INodeProperties[]; + +export const vendorFields = [ + // ---------------------------------- + // vendor: create + // ---------------------------------- + { + displayName: 'Display Name', + name: 'displayName', + type: 'string', + required: true, + default: '', + description: 'The display name of the vendor to create.', + displayOptions: { + show: { + resource: [ + 'vendor', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'vendor', + ], + operation: [ + 'create', + ], + }, + }, + options: vendorAdditionalFieldsOptions, + }, + + // ---------------------------------- + // vendor: get + // ---------------------------------- + { + displayName: 'Vendor ID', + name: 'vendorId', + type: 'string', + required: true, + default: '', + description: 'The ID of the vendor to retrieve.', + displayOptions: { + show: { + resource: [ + 'vendor', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------- + // vendor: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'vendor', + ], + 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: [ + 'vendor', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + placeholder: 'WHERE Metadata.LastUpdatedTime > \'2021-01-01\'', + description: 'The condition for selecting vendors. See the guide for supported syntax.', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, + ], + displayOptions: { + show: { + resource: [ + 'vendor', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + + // ---------------------------------- + // vendor: update + // ---------------------------------- + { + displayName: 'Vendor ID', + name: 'vendorId', + type: 'string', + required: true, + default: '', + description: 'The ID of the vendor to update.', + displayOptions: { + show: { + resource: [ + 'vendor', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + required: true, + displayOptions: { + show: { + resource: [ + 'vendor', + ], + operation: [ + 'update', + ], + }, + }, + options: vendorAdditionalFieldsOptions, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/index.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/index.ts new file mode 100644 index 0000000000..d8573f7223 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/index.ts @@ -0,0 +1,8 @@ +export * from './Bill/BillDescription'; +export * from './Customer/CustomerDescription'; +export * from './Employee/EmployeeDescription'; +export * from './Estimate/EstimateDescription'; +export * from './Invoice/InvoiceDescription'; +export * from './Item/ItemDescription'; +export * from './Payment/PaymentDescription'; +export * from './Vendor/VendorDescription'; diff --git a/packages/nodes-base/nodes/QuickBooks/quickbooks.svg b/packages/nodes-base/nodes/QuickBooks/quickbooks.svg new file mode 100644 index 0000000000..ac04a2e791 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBooks/quickbooks.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 6d91522707..0f958e35b4 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -177,6 +177,7 @@ "dist/credentials/PushcutApi.credentials.js", "dist/credentials/QuestDb.credentials.js", "dist/credentials/QuickBaseApi.credentials.js", + "dist/credentials/QuickBooksOAuth2Api.credentials.js", "dist/credentials/RabbitMQ.credentials.js", "dist/credentials/RedditOAuth2Api.credentials.js", "dist/credentials/Redis.credentials.js", @@ -427,6 +428,7 @@ "dist/nodes/Pushover/Pushover.node.js", "dist/nodes/QuestDb/QuestDb.node.js", "dist/nodes/QuickBase/QuickBase.node.js", + "dist/nodes/QuickBooks/QuickBooks.node.js", "dist/nodes/RabbitMQ/RabbitMQ.node.js", "dist/nodes/RabbitMQ/RabbitMQTrigger.node.js", "dist/nodes/ReadBinaryFile.node.js", From 53693886dff4bbce3c5743046bb7f3054a393b30 Mon Sep 17 00:00:00 2001 From: Tanay Pant <7481165+tanay1337@users.noreply.github.com> Date: Sat, 13 Feb 2021 17:46:35 +0100 Subject: [PATCH 008/239] :sparkles: Add Demio node (#1434) * :sparkles: Add Demio node * :zap: Improvements * :zap: Minor improvement on Demio-Node Co-authored-by: ricardo Co-authored-by: Jan Oberhauser --- .../credentials/DemioApi.credentials.ts | 24 ++ packages/nodes-base/nodes/Demio/Demio.node.ts | 211 +++++++++++ .../nodes/Demio/EventDescription.ts | 345 ++++++++++++++++++ .../nodes/Demio/GenericFunctions.ts | 48 +++ .../nodes/Demio/ReportDescription.ts | 124 +++++++ packages/nodes-base/nodes/Demio/demio.svg | 1 + packages/nodes-base/package.json | 2 + 7 files changed, 755 insertions(+) create mode 100644 packages/nodes-base/credentials/DemioApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Demio/Demio.node.ts create mode 100644 packages/nodes-base/nodes/Demio/EventDescription.ts create mode 100644 packages/nodes-base/nodes/Demio/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Demio/ReportDescription.ts create mode 100644 packages/nodes-base/nodes/Demio/demio.svg diff --git a/packages/nodes-base/credentials/DemioApi.credentials.ts b/packages/nodes-base/credentials/DemioApi.credentials.ts new file mode 100644 index 0000000000..dc122e2f44 --- /dev/null +++ b/packages/nodes-base/credentials/DemioApi.credentials.ts @@ -0,0 +1,24 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class DemioApi implements ICredentialType { + name = 'demioApi'; + displayName = 'Demio API'; + documentationUrl = 'demio'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'API Secret', + name: 'apiSecret', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Demio/Demio.node.ts b/packages/nodes-base/nodes/Demio/Demio.node.ts new file mode 100644 index 0000000000..f0b21b483d --- /dev/null +++ b/packages/nodes-base/nodes/Demio/Demio.node.ts @@ -0,0 +1,211 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + demioApiRequest, +} from './GenericFunctions'; + +import { + eventFields, + eventOperations, +} from './EventDescription'; + +import { + reportFields, + reportOperations, +} from './ReportDescription'; + +export class Demio implements INodeType { + description: INodeTypeDescription = { + displayName: 'Demio', + name: 'demio', + icon: 'file:demio.svg', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume the Demio API', + defaults: { + name: 'Demio', + color: '#02bf6f', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'demioApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Event', + value: 'event', + }, + { + name: 'Report', + value: 'report', + }, + ], + default: 'event', + description: 'Resource to consume.', + }, + // Event + ...eventOperations, + ...eventFields, + // Report + ...reportOperations, + ...reportFields, + ], + }; + + methods = { + loadOptions: { + // Get all the events to display them to user so that he can + // select them easily + async getEvents( + this: ILoadOptionsFunctions, + ): Promise { + const returnData: INodePropertyOptions[] = []; + const events = await demioApiRequest.call( + this, + 'GET', + `/events`, + {}, + { type: 'upcoming' }, + ); + for (const event of events) { + returnData.push({ + name: event.name, + value: event.id, + }); + } + return returnData; + }, + + // Get all the sessions to display them to user so that he can + // select them easily + async getEventSessions( + this: ILoadOptionsFunctions, + ): Promise { + const eventId = this.getCurrentNodeParameter('eventId') as string; + const qs: IDataObject = {}; + + const resource = this.getCurrentNodeParameter('resource') as string; + + if (resource !== 'report') { + qs.active = true; + } + + const returnData: INodePropertyOptions[] = []; + const { dates } = await demioApiRequest.call( + this, + 'GET', + `/event/${eventId}`, + {}, + ); + for (const date of dates) { + returnData.push({ + name: date.datetime, + value: date.date_id, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + for (let i = 0; i < length; i++) { + if (resource === 'event') { + if (operation === 'get') { + const id = this.getNodeParameter('eventId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.date_id !== undefined) { + responseData = await demioApiRequest.call(this, 'GET', `/event/${id}/date/${additionalFields.date_id}`); + } else { + Object.assign(qs, additionalFields); + responseData = await demioApiRequest.call(this, 'GET', `/event/${id}`, {}, qs); + } + } + if (operation === 'getAll') { + const filters = this.getNodeParameter('filters', i) as IDataObject; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + Object.assign(qs, filters); + + responseData = await demioApiRequest.call(this, 'GET', `/events`, {}, qs); + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.splice(0, limit); + } + + } + if (operation === 'register') { + const eventId = this.getNodeParameter('eventId', i) as string; + const firstName = this.getNodeParameter('firstName', i) as string; + const email = this.getNodeParameter('email', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + name: firstName, + email, + id: eventId, + }; + + Object.assign(body, additionalFields); + + if (additionalFields.customFieldsUi) { + const customFields = (additionalFields.customFieldsUi as IDataObject || {}).customFieldsValues as IDataObject[] || []; + const data = customFields.reduce((obj, value) => Object.assign(obj, { [`${value.fieldId}`]: value.value }), {}); + Object.assign(body, data); + delete additionalFields.customFields; + } + + responseData = await demioApiRequest.call(this, 'PUT', `/event/register`, body); + } + } + if (resource === 'report') { + if (operation === 'get') { + const sessionId = this.getNodeParameter('dateId', i) as string; + const filters = this.getNodeParameter('filters', i) as IDataObject; + + Object.assign(qs, filters); + + responseData = await demioApiRequest.call(this, 'GET', `/report/${sessionId}/participants`, {}, qs); + responseData = responseData.participants; + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else { + returnData.push(responseData as IDataObject); + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Demio/EventDescription.ts b/packages/nodes-base/nodes/Demio/EventDescription.ts new file mode 100644 index 0000000000..882ec45dad --- /dev/null +++ b/packages/nodes-base/nodes/Demio/EventDescription.ts @@ -0,0 +1,345 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const eventOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'event', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get an event', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all events', + }, + { + name: 'Register', + value: 'register', + description: 'Register someone to an event', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const eventFields = [ + + /* -------------------------------------------------------------------------- */ + /* event:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'event', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'event', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Automated', + value: 'automated', + }, + { + name: 'Past', + value: 'past', + }, + { + name: 'Upcoming', + value: 'upcoming', + }, + ], + default: '', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* event:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Event ID', + name: 'eventId', + type: 'string', + default: '', + required: true, + description: 'Event ID', + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Active', + name: 'active', + type: 'boolean', + default: false, + description: 'Return only active dates in series', + }, + { + displayName: 'Session ID', + name: 'date_id', + type: 'string', + default: '', + description: 'Event Date ID', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* event:register */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Event ID', + name: 'eventId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getEvents', + }, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'register', + ], + }, + }, + default: '', + description: 'Event ID', + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + required: true, + description: 'The registrant\'s first name', + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'register', + ], + }, + }, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + required: true, + description: 'The registrant\'s email address', + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'register', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'register', + ], + }, + }, + options: [ + { + displayName: 'Company', + name: 'company', + type: 'string', + default: '', + description: 'The value for the predefined Company field.', + }, + { + displayName: 'Custom Fields', + name: 'customFieldsUi', + placeholder: 'Add Field', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'customFieldsValues', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field ID', + name: 'fieldId', + type: 'string', + default: '', + description: 'Each custom field\'s unique identifier
can be found within the Event\'s Registration block in the Customize tab.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'The value to set on custom field.', + }, + ], + }, + ], + }, + { + displayName: 'Event Registration URL', + name: 'ref_url', + type: 'string', + default: '', + description: 'Event Registration page URL. It can be useful when you
do not know Event ID, but have Event link.', + }, + { + displayName: 'GDPR', + name: 'gdpr', + type: 'string', + default: '', + description: 'The value for the predefined GDPR field.', + }, + { + displayName: 'Last Name', + name: 'last_name', + type: 'string', + default: '', + description: 'The value for the predefined Last Name field.', + }, + { + displayName: 'Phone Number', + name: 'phone_number', + type: 'string', + default: '', + description: 'The value for the predefined Phone Number field.', + }, + { + displayName: 'Session ID', + name: 'date_id', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getEventSessions', + loadOptionsDependsOn: [ + 'eventId', + ], + }, + default: '', + description: 'Event Session ID', + }, + { + displayName: 'Website', + name: 'website', + type: 'string', + default: '', + description: 'The value for the predefined Website field.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Demio/GenericFunctions.ts b/packages/nodes-base/nodes/Demio/GenericFunctions.ts new file mode 100644 index 0000000000..3d96faacca --- /dev/null +++ b/packages/nodes-base/nodes/Demio/GenericFunctions.ts @@ -0,0 +1,48 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function demioApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + try { + const credentials = this.getCredentials('demioApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + let options: OptionsWithUri = { + headers: { + 'Api-Key': credentials.apiKey, + 'Api-Secret': credentials.apiSecret, + }, + method, + qs, + body, + uri: uri || `https://my.demio.com/api/v1${resource}`, + json: true, + }; + + options = Object.assign({}, options, option); + + return await this.helpers.request!(options); + } catch (error) { + + if (error.response && error.response.body && error.response.body.message) { + // Try to return the error prettier + const errorBody = error.response.body; + throw new Error(`Demio error response [${error.statusCode}]: ${errorBody.message}`); + } + + // Expected error data did not get returned so throw the actual error + throw error; + } +} diff --git a/packages/nodes-base/nodes/Demio/ReportDescription.ts b/packages/nodes-base/nodes/Demio/ReportDescription.ts new file mode 100644 index 0000000000..b83982f220 --- /dev/null +++ b/packages/nodes-base/nodes/Demio/ReportDescription.ts @@ -0,0 +1,124 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const reportOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'report', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get an event report', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const reportFields = [ + + /* -------------------------------------------------------------------------- */ + /* report:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Event ID', + name: 'eventId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getEvents', + }, + displayOptions: { + show: { + resource: [ + 'report', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + description: 'Event ID', + }, + { + displayName: 'Session ID', + name: 'dateId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getEventSessions', + loadOptionsDependsOn: [ + 'eventId', + ], + }, + default: '', + required: true, + description: 'ID of the session', + displayOptions: { + show: { + resource: [ + 'report', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'report', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Attended', + value: 'attended', + }, + { + name: 'Banned', + value: 'banned', + }, + { + name: 'Completed', + value: 'completed', + }, + { + name: 'Did Not Attend', + value: 'did-not-attend', + }, + { + name: 'Left Early', + value: 'left-early', + }, + ], + default: '', + description: 'Filter results by participation status', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Demio/demio.svg b/packages/nodes-base/nodes/Demio/demio.svg new file mode 100644 index 0000000000..93889f1d02 --- /dev/null +++ b/packages/nodes-base/nodes/Demio/demio.svg @@ -0,0 +1 @@ + diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 0f958e35b4..f3d2dfe3a9 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -61,6 +61,7 @@ "dist/credentials/CustomerIoApi.credentials.js", "dist/credentials/S3.credentials.js", "dist/credentials/CrateDb.credentials.js", + "dist/credentials/DemioApi.credentials.js", "dist/credentials/DiscourseApi.credentials.js", "dist/credentials/DisqusApi.credentials.js", "dist/credentials/DriftApi.credentials.js", @@ -303,6 +304,7 @@ "dist/nodes/CustomerIo/CustomerIo.node.js", "dist/nodes/CustomerIo/CustomerIoTrigger.node.js", "dist/nodes/DateTime.node.js", + "dist/nodes/Demio/Demio.node.js", "dist/nodes/Discord/Discord.node.js", "dist/nodes/Discourse/Discourse.node.js", "dist/nodes/Disqus/Disqus.node.js", From deaa015e61899bd82e7db4fe32ffe11940f2430b Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sat, 13 Feb 2021 20:40:27 +0100 Subject: [PATCH 009/239] :zap: Do only send manual executions to starting session & cleanup --- packages/cli/src/Interfaces.ts | 6 +- packages/cli/src/Server.ts | 4 +- .../cli/src/WorkflowExecuteAdditionalData.ts | 93 +++++++++---------- packages/cli/src/WorkflowRunner.ts | 9 +- packages/editor-ui/src/Interface.ts | 9 +- .../src/components/ExecutionsList.vue | 5 +- .../src/components/mixins/pushConnection.ts | 4 +- packages/editor-ui/src/store.ts | 8 +- packages/editor-ui/src/views/NodeView.vue | 5 +- 9 files changed, 61 insertions(+), 82 deletions(-) diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index 8253d3c396..d5f8f1af3c 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -179,8 +179,7 @@ export interface IExecutionsStopData { } export interface IExecutionsSummary { - id?: string; // executionIdDb - idActive?: string; // executionIdActive + id: string; finished?: boolean; mode: WorkflowExecuteMode; retryOf?: string; @@ -327,8 +326,7 @@ export type IPushDataType = 'executionFinished' | 'executionStarted' | 'nodeExec export interface IPushDataExecutionFinished { data: IRun; - executionIdActive: string; - executionIdDb?: string; + executionId: string; retryOf?: string; } diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index c52e2f66bc..aed8de09a0 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -1670,7 +1670,7 @@ class App { return results.map(result => { return { - idActive: result.id, + id: result.id, workflowId: result.workflowId, mode: result.mode, retryOf: result.retryOf !== null ? result.retryOf : undefined, @@ -1693,7 +1693,7 @@ class App { } returnData.push( { - idActive: data.id.toString(), + id: data.id.toString(), workflowId: data.workflowId === undefined ? '' : data.workflowId.toString(), mode: data.mode, retryOf: data.retryOf, diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index 2ef00c0473..2d0855bcbd 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -117,42 +117,6 @@ function pruneExecutionData(): void { } -/** - * Pushes the execution out to all connected clients - * - * @param {WorkflowExecuteMode} mode The mode in which the workflow got started in - * @param {IRun} fullRunData The RunData of the finished execution - * @param {string} executionIdActive The id of the finished execution - * @param {string} [executionIdDb] The database id of finished execution - */ -export function pushExecutionFinished(mode: WorkflowExecuteMode, fullRunData: IRun, executionIdActive: string, executionIdDb?: string, retryOf?: string) { - // Clone the object except the runData. That one is not supposed - // to be send. Because that data got send piece by piece after - // each node which finished executing - const pushRunData = { - ...fullRunData, - data: { - ...fullRunData.data, - resultData: { - ...fullRunData.data.resultData, - runData: {}, - }, - }, - }; - - // Push data to editor-ui once workflow finished - const sendData: IPushDataExecutionFinished = { - executionIdActive, - executionIdDb, - data: pushRunData, - retryOf, - }; - - const pushInstance = Push.getInstance(); - pushInstance.send('executionFinished', sendData); -} - - /** * Returns hook functions to push data to Editor-UI * @@ -192,25 +156,52 @@ function hookFunctionsPush(): IWorkflowExecuteHooks { ], workflowExecuteBefore: [ async function (this: WorkflowHooks): Promise { - // Push data to editor-ui once workflow finished - if (this.mode === 'manual') { - const pushInstance = Push.getInstance(); - pushInstance.send('executionStarted', { - executionId: this.executionId, - mode: this.mode, - startedAt: new Date(), - retryOf: this.retryOf, - workflowId: this.workflowData.id as string, - workflowName: this.workflowData.name, - }); + // Push data to session which started the workflow + if (this.sessionId === undefined) { + return; } + const pushInstance = Push.getInstance(); + pushInstance.send('executionStarted', { + executionId: this.executionId, + mode: this.mode, + startedAt: new Date(), + retryOf: this.retryOf, + workflowId: this.workflowData.id as string, + workflowName: this.workflowData.name, + }, this.sessionId); }, ], workflowExecuteAfter: [ async function (this: WorkflowHooks, fullRunData: IRun, newStaticData: IDataObject): Promise { - if (this.mode === 'manual') { - pushExecutionFinished(this.mode, fullRunData, this.executionId, undefined, this.retryOf); + // Push data to session which started the workflow + if (this.sessionId === undefined) { + return; } + + // Clone the object except the runData. That one is not supposed + // to be send. Because that data got send piece by piece after + // each node which finished executing + const pushRunData = { + ...fullRunData, + data: { + ...fullRunData.data, + resultData: { + ...fullRunData.data.resultData, + runData: {}, + }, + }, + }; + + // Push data to editor-ui once workflow finished + // TODO: Look at this again + const sendData: IPushDataExecutionFinished = { + executionId: this.executionId, + data: pushRunData, + retryOf: this.retryOf, + }; + + const pushInstance = Push.getInstance(); + pushInstance.send('executionFinished', sendData, this.sessionId); }, ], }; @@ -243,7 +234,7 @@ export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowEx if (execution === undefined) { // Something went badly wrong if this happens. // This check is here mostly to make typescript happy. - return undefined; + return undefined; } const fullExecutionData: IExecutionResponse = ResponseHelper.unflattenExecutionData(execution); @@ -282,7 +273,7 @@ export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowEx // Set last executed node so that it may resume on failure fullExecutionData.data.resultData.lastNodeExecuted = nodeName; - + const flattenedExecutionData = ResponseHelper.flattenExecutionData(fullExecutionData); await Db.collections.Execution!.update(this.executionId, flattenedExecutionData as IExecutionFlattedDb); diff --git a/packages/cli/src/WorkflowRunner.ts b/packages/cli/src/WorkflowRunner.ts index 5338a4a46d..e382ac2827 100644 --- a/packages/cli/src/WorkflowRunner.ts +++ b/packages/cli/src/WorkflowRunner.ts @@ -101,9 +101,6 @@ export class WorkflowRunner { // Remove from active execution with empty data. That will // set the execution to failed. this.activeExecutions.remove(executionId, fullRunData); - - // Also send to Editor UI - WorkflowExecuteAdditionalData.pushExecutionFinished(executionMode, fullRunData, executionId); } /** @@ -175,7 +172,7 @@ export class WorkflowRunner { workflowExecution = workflowExecute.processRunExecutionData(workflow); } else if (data.runData === undefined || data.startNodes === undefined || data.startNodes.length === 0 || data.destinationNode === undefined) { // Execute all nodes - + // Can execute without webhook so go on const workflowExecute = new WorkflowExecute(additionalData, data.executionMode); workflowExecution = workflowExecute.run(workflow, undefined, data.destinationNode); @@ -298,7 +295,7 @@ export class WorkflowRunner { }, queueRecoveryInterval * 1000); }); - + const clearWatchdogInterval = () => { if (watchDogInterval) { clearInterval(watchDogInterval); @@ -332,7 +329,7 @@ export class WorkflowRunner { await jobData; } - + const executionDb = await Db.collections.Execution!.findOne(executionId) as IExecutionFlattedDb; const fullExecutionData = ResponseHelper.unflattenExecutionData(executionDb) as IExecutionResponse; diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index b4a5e248ca..972ef0cbd8 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -314,8 +314,7 @@ export interface IExecutionsListResponse { } export interface IExecutionsCurrentSummaryExtended { - id?: string; - idActive: string; + id: string; finished?: boolean; mode: WorkflowExecuteMode; retryOf?: string; @@ -334,8 +333,7 @@ export interface IExecutionsStopData { } export interface IExecutionsSummary { - id?: string; // executionIdDb - idActive?: string; // executionIdActive + id: string; mode: WorkflowExecuteMode; finished?: boolean; retryOf?: string; @@ -370,8 +368,7 @@ export interface IPushDataExecutionStarted { export interface IPushDataExecutionFinished { data: IRun; - executionIdActive: string; - executionIdDb?: string; + executionId: string; retryOf?: string; } diff --git a/packages/editor-ui/src/components/ExecutionsList.vue b/packages/editor-ui/src/components/ExecutionsList.vue index 806fcd5853..e07b018981 100644 --- a/packages/editor-ui/src/components/ExecutionsList.vue +++ b/packages/editor-ui/src/components/ExecutionsList.vue @@ -57,7 +57,6 @@ @@ -126,8 +125,8 @@