From 740cb8a6fc3fc96dd54c448109ca198e3d2f9736 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 7 Jan 2020 18:29:11 -0600 Subject: [PATCH 001/165] :zap: Some small changes to basic OAuth support --- packages/cli/src/Server.ts | 50 +++--- packages/cli/templates/oauth-callback.html | 9 ++ packages/editor-ui/src/Interface.ts | 4 +- .../src/components/CredentialsInput.vue | 77 +++++++++ .../src/components/CredentialsList.vue | 18 +-- .../src/components/mixins/restApi.ts | 6 +- packages/editor-ui/src/main.ts | 2 + packages/editor-ui/src/router.ts | 2 +- .../credentials/OAuth2Api.credentials.ts | 93 +++++------ packages/nodes-base/nodes/OAuth.node.ts | 149 +++++++----------- 10 files changed, 224 insertions(+), 186 deletions(-) create mode 100644 packages/cli/templates/oauth-callback.html diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index f5a608331a..0a93f18e34 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -2,6 +2,7 @@ import * as express from 'express'; import { dirname as pathDirname, join as pathJoin, + resolve as pathResolve, } from 'path'; import { getConnectionManager, @@ -850,7 +851,7 @@ class App { // ---------------------------------------- - // Returns all the credential types which are defined in the loaded n8n-modules + // Authorize OAuth Data this.app.get('/rest/oauth2-credential/auth', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { if (req.query.id === undefined) { throw new Error('Required credential id is missing!'); @@ -877,13 +878,13 @@ class App { throw new Error('Unable to read OAuth credentials'); } - let token = new csrf(); + const token = new csrf(); // Generate a CSRF prevention token and send it as a OAuth2 state stringma/ERR oauthCredentials.csrfSecret = token.secretSync(); const state = { - 'token': token.create(oauthCredentials.csrfSecret), - 'cid': req.query.id - } + token: token.create(oauthCredentials.csrfSecret), + cid: req.query.id + }; const stateEncodedStr = Buffer.from(JSON.stringify(state)).toString('base64') as string; const oAuthObj = new clientOAuth2({ @@ -891,9 +892,9 @@ class App { clientSecret: _.get(oauthCredentials, 'clientSecret', '') as string, accessTokenUri: _.get(oauthCredentials, 'accessTokenUrl', '') as string, authorizationUri: _.get(oauthCredentials, 'authUrl', '') as string, - redirectUri: _.get(oauthCredentials, 'callbackUrl', WebhookHelpers.getWebhookBaseUrl()) as string, + redirectUri: `${WebhookHelpers.getWebhookBaseUrl()}rest/oauth2-credential/callback`, scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ','), - state: stateEncodedStr + state: stateEncodedStr, }); credentials.setData(oauthCredentials, encryptionKey); @@ -913,42 +914,46 @@ class App { // ---------------------------------------- // Verify and store app code. Generate access tokens and store for respective credential. - this.app.get('/rest/oauth2-credential/callback', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.get('/rest/oauth2-credential/callback', async (req: express.Request, res: express.Response) => { const {code, state: stateEncoded} = req.query; + if (code === undefined || stateEncoded === undefined) { - throw new Error('Insufficient parameters for OAuth2 callback') + throw new Error('Insufficient parameters for OAuth2 callback'); } let state; try { state = JSON.parse(Buffer.from(stateEncoded, 'base64').toString()); } catch (error) { - throw new Error('Invalid state format returned'); + const errorResponse = new ResponseHelper.ResponseError('Invalid state format returned', undefined, 503); + return ResponseHelper.sendErrorResponse(res, errorResponse); } const result = await Db.collections.Credentials!.findOne(state.cid); if (result === undefined) { - res.status(404).send('The credential is not known.'); - return ''; + const errorResponse = new ResponseHelper.ResponseError('The credential is not known.', undefined, 404); + return ResponseHelper.sendErrorResponse(res, errorResponse); } let encryptionKey = undefined; encryptionKey = await UserSettings.getEncryptionKey(); if (encryptionKey === undefined) { - throw new Error('No encryption key got found to decrypt the credentials!'); + const errorResponse = new ResponseHelper.ResponseError('No encryption key got found to decrypt the credentials!', undefined, 503); + return ResponseHelper.sendErrorResponse(res, errorResponse); } const credentials = new Credentials(result.name, result.type, result.nodesAccess, result.data); (result as ICredentialsDecryptedDb).data = credentials.getData(encryptionKey!); const oauthCredentials = (result as ICredentialsDecryptedDb).data; if (oauthCredentials === undefined) { - throw new Error('Unable to read OAuth credentials'); + const errorResponse = new ResponseHelper.ResponseError('Unable to read OAuth credentials!', undefined, 503); + return ResponseHelper.sendErrorResponse(res, errorResponse); } - let token = new csrf(); + const token = new csrf(); if (oauthCredentials.csrfSecret === undefined || !token.verify(oauthCredentials.csrfSecret as string, state.token)) { - res.status(404).send('The OAuth2 callback state is invalid.'); - return ''; + const errorResponse = new ResponseHelper.ResponseError('The OAuth2 callback state is invalid!', undefined, 404); + return ResponseHelper.sendErrorResponse(res, errorResponse); } const oAuthObj = new clientOAuth2({ @@ -956,13 +961,15 @@ class App { clientSecret: _.get(oauthCredentials, 'clientSecret', '') as string, accessTokenUri: _.get(oauthCredentials, 'accessTokenUrl', '') as string, authorizationUri: _.get(oauthCredentials, 'authUrl', '') as string, - redirectUri: _.get(oauthCredentials, 'callbackUrl', WebhookHelpers.getWebhookBaseUrl()) as string, + redirectUri: `${WebhookHelpers.getWebhookBaseUrl()}rest/oauth2-credential/callback`, scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ',') }); const oauthToken = await oAuthObj.code.getToken(req.originalUrl); + if (oauthToken === undefined) { - throw new Error('Unable to get access tokens'); + const errorResponse = new ResponseHelper.ResponseError('Unable to get access tokens!', undefined, 404); + return ResponseHelper.sendErrorResponse(res, errorResponse); } oauthCredentials.oauthTokenData = JSON.stringify(oauthToken.data); @@ -974,8 +981,9 @@ class App { // Save the credentials in DB await Db.collections.Credentials!.update(state.cid, newCredentialsData); - return 'Success!'; - })); + res.sendFile(pathResolve('templates/oauth-callback.html')); + }); + // ---------------------------------------- // Executions diff --git a/packages/cli/templates/oauth-callback.html b/packages/cli/templates/oauth-callback.html new file mode 100644 index 0000000000..e479c5ea9e --- /dev/null +++ b/packages/cli/templates/oauth-callback.html @@ -0,0 +1,9 @@ + + + +Got connected. The window can be closed now. + diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index a79ebdd8fd..7c692881a8 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -145,8 +145,8 @@ export interface IRestApi { deleteExecutions(sendData: IExecutionDeleteFilter): Promise; retryExecution(id: string, loadWorkflow?: boolean): Promise; getTimezones(): Promise; - OAuth2CredentialAuthorize(sendData: ICredentialsResponse): Promise; - OAuth2Callback(code: string, state: string): Promise; + oAuth2CredentialAuthorize(sendData: ICredentialsResponse): Promise; + oAuth2Callback(code: string, state: string): Promise; } export interface IBinaryDisplayData { diff --git a/packages/editor-ui/src/components/CredentialsInput.vue b/packages/editor-ui/src/components/CredentialsInput.vue index 0830b2a2a0..f7616f4706 100644 --- a/packages/editor-ui/src/components/CredentialsInput.vue +++ b/packages/editor-ui/src/components/CredentialsInput.vue @@ -13,6 +13,26 @@ + + + OAuth + + + + + + + Is connected + + + + + + Is NOT connected + + + +
Credential Data: @@ -152,6 +172,16 @@ export default mixins( }; }); }, + isOAuthType (): boolean { + return this.credentialData && this.credentialData.type === 'oAuth2Api'; + }, + isOAuthConnected (): boolean { + if (this.isOAuthType === false) { + return false; + } + + return !!this.credentialData.data.oauthTokenData; + }, }, methods: { valueChanged (parameterData: IUpdateInformation) { @@ -189,6 +219,48 @@ export default mixins( this.$emit('credentialsCreated', result); }, + async oAuth2CredentialAuthorize () { + let url; + try { + url = await this.restApi().oAuth2CredentialAuthorize(this.credentialData) as string; + } catch (error) { + this.$showError(error, 'OAuth Authorization Error', 'Error generating authorization URL:'); + return; + } + + const params = `scrollbars=no,resizable=yes,status=no,titlebar=noe,location=no,toolbar=no,menubar=no,width=500,height=700`; + const oauthPopup = window.open(url, 'OAuth2 Authorization', params); + + const receiveMessage = (event: MessageEvent) => { + // // TODO: Add check that it came from n8n + // if (event.origin !== 'http://example.org:8080') { + // return; + // } + + if (event.data === 'success') { + + // Set some kind of data that status changes. + // As data does not get displayed directly it does not matter what data. + this.credentialData.data.oauthTokenData = {}; + + // Close the window + if (oauthPopup) { + oauthPopup.close(); + } + + this.$showMessage({ + title: 'Connected', + message: 'Got connected!', + type: 'success', + }); + } + + // Make sure that the event gets removed again + window.removeEventListener('message', receiveMessage, false); + }; + + window.addEventListener('message', receiveMessage, false); + }, async updateCredentials () { const nodesAccess: ICredentialNodeAccess[] = []; const addedNodeTypes: string[] = []; @@ -301,6 +373,11 @@ export default mixins( line-height: 1.75em; } + .oauth-information { + line-height: 2.5em; + margin-top: 2em; + } + .parameter-wrapper { line-height: 3em; diff --git a/packages/editor-ui/src/components/CredentialsList.vue b/packages/editor-ui/src/components/CredentialsList.vue index eeece990bc..c1d3ac93ff 100644 --- a/packages/editor-ui/src/components/CredentialsList.vue +++ b/packages/editor-ui/src/components/CredentialsList.vue @@ -25,12 +25,10 @@ + width="120"> @@ -93,20 +91,6 @@ export default mixins( this.editCredentials = null; this.credentialEditDialogVisible = true; }, - async OAuth2CredentialAuthorize (credential: ICredentialsResponse) { - let url; - try { - url = await this.restApi().OAuth2CredentialAuthorize(credential) as string; - } catch (error) { - this.$showError(error, 'OAuth Authorization Error', 'Error generating authorization URL:'); - return; - } - - const params = `scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no,width=0,height=0,left=-1000,top=-1000`; - const oauthPopup = window.open(url, 'OAuth2 Authorization', params); - - console.log(oauthPopup); - }, editCredential (credential: ICredentialsResponse) { const editCredentials = { id: credential.id, diff --git a/packages/editor-ui/src/components/mixins/restApi.ts b/packages/editor-ui/src/components/mixins/restApi.ts index a2cdbd6584..be114786a9 100644 --- a/packages/editor-ui/src/components/mixins/restApi.ts +++ b/packages/editor-ui/src/components/mixins/restApi.ts @@ -253,15 +253,15 @@ export const restApi = Vue.extend({ }, // Get OAuth2 Authorization URL using the stored credentials - OAuth2CredentialAuthorize: (sendData: ICredentialsResponse): Promise => { + oAuth2CredentialAuthorize: (sendData: ICredentialsResponse): Promise => { return self.restApi().makeRestApiRequest('GET', `/oauth2-credential/auth`, sendData); }, // Verify OAuth2 provider callback and kick off token generation - OAuth2Callback: (code: string, state: string): Promise => { + oAuth2Callback: (code: string, state: string): Promise => { const sendData = { 'code': code, - 'state': state + 'state': state, }; return self.restApi().makeRestApiRequest('POST', `/oauth2-credential/callback`, sendData); diff --git a/packages/editor-ui/src/main.ts b/packages/editor-ui/src/main.ts index f9a7f879a0..0e6018f3aa 100644 --- a/packages/editor-ui/src/main.ts +++ b/packages/editor-ui/src/main.ts @@ -71,6 +71,7 @@ import { faSave, faSearchMinus, faSearchPlus, + faSignInAlt, faSlidersH, faSpinner, faStop, @@ -145,6 +146,7 @@ library.add(faRss); library.add(faSave); library.add(faSearchMinus); library.add(faSearchPlus); +library.add(faSignInAlt); library.add(faSlidersH); library.add(faSpinner); library.add(faStop); diff --git a/packages/editor-ui/src/router.ts b/packages/editor-ui/src/router.ts index f33d028f94..e82b30b588 100644 --- a/packages/editor-ui/src/router.ts +++ b/packages/editor-ui/src/router.ts @@ -21,7 +21,7 @@ export default new Router({ }, { path: '/oauth2/callback', - name: 'OAuth2Callback', + name: 'oAuth2Callback', components: { }, }, diff --git a/packages/nodes-base/credentials/OAuth2Api.credentials.ts b/packages/nodes-base/credentials/OAuth2Api.credentials.ts index 452fdb8f57..99e8e025de 100644 --- a/packages/nodes-base/credentials/OAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/OAuth2Api.credentials.ts @@ -1,56 +1,49 @@ import { - ICredentialType, - NodePropertyTypes, + ICredentialType, + NodePropertyTypes, } from 'n8n-workflow'; export class OAuth2Api implements ICredentialType { - name = 'OAuth2Api'; - displayName = 'OAuth2 API'; - properties = [ - { - displayName: 'Authorization URL', - name: 'authUrl', - type: 'string' as NodePropertyTypes, - default: '', - required: true, - }, - { - displayName: 'Access Token URL', - name: 'accessTokenUrl', - type: 'string' as NodePropertyTypes, - default: '', - required: true, - }, - { - displayName: 'Callback URL', - name: 'callbackUrl', - type: 'string' as NodePropertyTypes, - default: '', - required: true, - }, - { - displayName: 'Client ID', - name: 'clientId', - type: 'string' as NodePropertyTypes, - default: '', - required: true, - }, - { - displayName: 'Client Secret', - name: 'clientSecret', - type: 'string' as NodePropertyTypes, - typeOptions: { - password: true, - }, - default: '', - required: true, - }, - { - displayName: 'Scope', - name: 'scope', - type: 'string' as NodePropertyTypes, - default: '', - }, - ]; + name = 'oAuth2Api'; + displayName = 'OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'string' as NodePropertyTypes, + default: '', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'string' as NodePropertyTypes, + default: '', + required: true, + }, + { + displayName: 'Client ID', + name: 'clientId', + type: 'string' as NodePropertyTypes, + default: '', + required: true, + }, + { + displayName: 'Client Secret', + name: 'clientSecret', + type: 'string' as NodePropertyTypes, + typeOptions: { + password: true, + }, + default: '', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; } diff --git a/packages/nodes-base/nodes/OAuth.node.ts b/packages/nodes-base/nodes/OAuth.node.ts index 189ae9e408..bf2f193a2f 100644 --- a/packages/nodes-base/nodes/OAuth.node.ts +++ b/packages/nodes-base/nodes/OAuth.node.ts @@ -1,104 +1,69 @@ import { IExecuteFunctions } from 'n8n-core'; import { - GenericValue, - IDataObject, - INodeExecutionData, - INodeType, - INodeTypeDescription, + INodeExecutionData, + INodeType, + INodeTypeDescription, } from 'n8n-workflow'; -import { set } from 'lodash'; - -import * as util from 'util'; -import { connectionFields } from './ActiveCampaign/ConnectionDescription'; - export class OAuth implements INodeType { - description: INodeTypeDescription = { - displayName: 'OAuth', - name: 'oauth', + description: INodeTypeDescription = { + displayName: 'OAuth', + name: 'oauth', icon: 'fa:code-branch', - group: ['input'], - version: 1, - description: 'Gets, sends data to Oauth API Endpoint and receives generic information.', - defaults: { - name: 'OAuth', - color: '#0033AA', - }, - inputs: ['main'], - outputs: ['main'], - credentials: [ - { - name: 'OAuth2Api', - required: true, - } - ], - properties: [ - { - displayName: 'Operation', - name: 'operation', - type: 'options', - options: [ - { - name: 'Get', - value: 'get', - description: 'Returns the value of a key from oauth.', - }, - ], - default: 'get', - description: 'The operation to perform.', - }, + group: ['input'], + version: 1, + description: 'Gets, sends data to Oauth API Endpoint and receives generic information.', + defaults: { + name: 'OAuth', + color: '#0033AA', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'oAuth2Api', + required: true, + } + ], + properties: [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Get', + value: 'get', + description: 'Returns the OAuth token data.', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, - // ---------------------------------- - // get - // ---------------------------------- - { - displayName: 'Name', - name: 'propertyName', - type: 'string', - displayOptions: { - show: { - operation: [ - 'get' - ], - }, - }, - default: 'propertyName', - required: true, - description: 'Name of the property to write received data to.
Supports dot-notation.
Example: "data.person[0].name"', - }, - ] - }; + ] + }; - async execute(this: IExecuteFunctions): Promise { - const credentials = this.getCredentials('OAuth2Api'); - if (credentials === undefined) { - throw new Error('No credentials got returned!'); - } + async execute(this: IExecuteFunctions): Promise { + const credentials = this.getCredentials('oAuth2Api'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } - if (credentials.oauthTokenData === undefined) { - throw new Error('OAuth credentials not connected'); - } + if (credentials.oauthTokenData === undefined) { + throw new Error('OAuth credentials not connected'); + } - const operation = this.getNodeParameter('operation', 0) as string; - if (operation === 'get') { - const items = this.getInputData(); - const returnItems: INodeExecutionData[] = []; + const operation = this.getNodeParameter('operation', 0) as string; + if (operation === 'get') { + // credentials.oauthTokenData has the refreshToken and accessToken available + // it would be nice to have credentials.getOAuthToken() which returns the accessToken + // and also handles an error case where if the token is to be refreshed, it does so + // without knowledge of the node. - let item: INodeExecutionData; - - // credentials.oauthTokenData has the refreshToken and accessToken available - // it would be nice to have credentials.getOAuthToken() which returns the accessToken - // and also handles an error case where if the token is to be refreshed, it does so - // without knowledge of the node. - console.log('Got OAuth credentials!', credentials.oauthTokenData); - - for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { - item = { json: { itemIndex } }; - returnItems.push(item); - } - return [returnItems]; - } else { - throw new Error('Unknown operation'); - } - } + return [this.helpers.returnJsonArray(JSON.parse(credentials.oauthTokenData as string))]; + } else { + throw new Error('Unknown operation'); + } + } } From 8228b8505feb0a321a51caad9e123db8f336ba2b Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 13 Jan 2020 20:46:58 -0600 Subject: [PATCH 002/165] :zap: Abstract OAuth signing and make credentials extendable --- packages/cli/src/Interfaces.ts | 2 +- packages/cli/src/Server.ts | 6 +- .../cli/src/WorkflowExecuteAdditionalData.ts | 38 ++++++++ packages/core/package.json | 1 + packages/core/src/Interfaces.ts | 14 +++ packages/core/src/NodeExecuteFunctions.ts | 91 ++++++++++++++++++- packages/core/test/Helpers.ts | 2 + .../src/components/CredentialsEdit.vue | 18 +++- .../GithubOAuth2Api.credentials.ts | 15 +++ packages/nodes-base/nodes/HttpRequest.node.ts | 24 ++++- packages/nodes-base/nodes/OAuth.node.ts | 80 +++++++++++++++- packages/nodes-base/package.json | 1 + packages/workflow/src/Interfaces.ts | 6 +- 13 files changed, 283 insertions(+), 15 deletions(-) create mode 100644 packages/nodes-base/credentials/GithubOAuth2Api.credentials.ts diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index 4000c30785..a5837b8fc6 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -70,7 +70,7 @@ export interface ICredentialsBase { updatedAt: Date; } -export interface ICredentialsDb extends ICredentialsBase, ICredentialsEncrypted{ +export interface ICredentialsDb extends ICredentialsBase, ICredentialsEncrypted { id: number | string | ObjectID; } diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 0a93f18e34..900dcb012f 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -758,11 +758,7 @@ class App { const findQuery = {} as FindManyOptions; // Make sure the variable has an expected value - if (req.query.includeData === 'true') { - req.query.includeData = true; - } else { - req.query.includeData = false; - } + req.query.includeData = (req.query.includeData === 'true' || req.query.includeData === true); if (req.query.includeData !== true) { // Return only the fields we need diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index d5b471def9..e234c11410 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -1,5 +1,6 @@ import { Db, + ICredentialsDb, IExecutionDb, IExecutionFlattedDb, IPushDataExecutionFinished, @@ -13,11 +14,13 @@ import { } from './'; import { + Credentials, UserSettings, WorkflowExecute, } from 'n8n-core'; import { + ICredentialDataDecryptedObject, IDataObject, IExecuteData, IExecuteWorkflowInfo, @@ -369,6 +372,40 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi } +/** + * Updates credentials with new data + * + * @export + * @param {string} name Name of the credentials to update + * @param {string} type Type of the credentials to update + * @param {ICredentialDataDecryptedObject} data The new credential data + * @param {string} encryptionKey The encryption key to use + * @returns {Promise} + */ +export async function updateCredentials(name: string, type: string, data: ICredentialDataDecryptedObject, encryptionKey: string): Promise { + const foundCredentials = await Db.collections.Credentials!.find({ name, type }); + + if (!foundCredentials.length) { + throw new Error(`Could not find credentials for type "${type}" with name "${name}".`); + } + + const credentialsDb = foundCredentials[0]; + + // Encrypt the data + const credentials = new Credentials(credentialsDb.name, credentialsDb.type, credentialsDb.nodesAccess); + credentials.setData(data, encryptionKey); + const newCredentialsData = credentials.getDataToSave() as ICredentialsDb; + + // Add special database related data + newCredentialsData.updatedAt = this.getCurrentDate(); + + // TODO: also add user automatically depending on who is logged in, if anybody is logged in + + // Save the credentials in DB + await Db.collections.Credentials!.save(newCredentialsData); +} + + /** * Returns the base additional data without webhooks * @@ -395,6 +432,7 @@ export async function getBase(credentials: IWorkflowCredentials, currentNodePara executeWorkflow, restApiUrl: urlBaseWebhook + config.get('endpoints.rest') as string, timezone, + updateCredentials, webhookBaseUrl, webhookTestBaseUrl, currentNodeParameters, diff --git a/packages/core/package.json b/packages/core/package.json index 2e20ad20e9..97673d21ca 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -39,6 +39,7 @@ "typescript": "~3.7.4" }, "dependencies": { + "client-oauth2": "^4.2.5", "cron": "^1.7.2", "crypto-js": "^3.1.9-1", "lodash.get": "^4.4.2", diff --git a/packages/core/src/Interfaces.ts b/packages/core/src/Interfaces.ts index 3b9a501969..8e0666a0b1 100644 --- a/packages/core/src/Interfaces.ts +++ b/packages/core/src/Interfaces.ts @@ -1,4 +1,5 @@ import { + IAllExecuteFunctions, IBinaryData, ICredentialType, IDataObject, @@ -18,6 +19,7 @@ import { } from 'n8n-workflow'; +import { OptionsWithUri } from 'request'; import * as requestPromise from 'request-promise-native'; interface Constructable { @@ -35,6 +37,7 @@ export interface IExecuteFunctions extends IExecuteFunctionsBase { helpers: { prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; request: requestPromise.RequestPromiseAPI, + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; }; } @@ -44,6 +47,7 @@ export interface IExecuteSingleFunctions extends IExecuteSingleFunctionsBase { helpers: { prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; request: requestPromise.RequestPromiseAPI, + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any }; } @@ -52,15 +56,22 @@ export interface IPollFunctions extends IPollFunctionsBase { helpers: { prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; request: requestPromise.RequestPromiseAPI, + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; }; } +export interface IResponseError extends Error { + statusCode?: number; +} + + export interface ITriggerFunctions extends ITriggerFunctionsBase { helpers: { prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; request: requestPromise.RequestPromiseAPI, + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; }; } @@ -84,6 +95,7 @@ export interface IUserSettings { export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase { helpers: { request?: requestPromise.RequestPromiseAPI, + requestOAuth?: (this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions) => Promise, // tslint:disable-line:no-any }; } @@ -91,6 +103,7 @@ export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase { export interface IHookFunctions extends IHookFunctionsBase { helpers: { request: requestPromise.RequestPromiseAPI, + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any }; } @@ -99,6 +112,7 @@ export interface IWebhookFunctions extends IWebhookFunctionsBase { helpers: { prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; request: requestPromise.RequestPromiseAPI, + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; }; } diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 2f90d0118f..cfdcdc8061 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -2,11 +2,13 @@ import { Credentials, IHookFunctions, ILoadOptionsFunctions, + IResponseError, IWorkflowSettings, BINARY_ENCODING, } from './'; import { + IAllExecuteFunctions, IBinaryData, IContextObject, ICredentialDataDecryptedObject, @@ -34,9 +36,11 @@ import { WorkflowExecuteMode, } from 'n8n-workflow'; -import { get } from 'lodash'; +import * as clientOAuth2 from 'client-oauth2'; +import { get, unset } from 'lodash'; import * as express from "express"; import * as path from 'path'; +import { OptionsWithUri } from 'request'; import * as requestPromise from 'request-promise-native'; import { Magic, MAGIC_MIME_TYPE } from 'mmmagic'; @@ -101,6 +105,70 @@ export async function prepareBinaryData(binaryData: Buffer, filePath?: string, m +/** + * Makes a request using OAuth data for authentication + * + * @export + * @param {IAllExecuteFunctions} this + * @param {string} credentialsType + * @param {(OptionsWithUri | requestPromise.RequestPromiseOptions)} requestOptions + * @param {INode} node + * @param {IWorkflowExecuteAdditionalData} additionalData + * @returns + */ +export function requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, node: INode, additionalData: IWorkflowExecuteAdditionalData) { + const credentials = this.getCredentials(credentialsType) as ICredentialDataDecryptedObject; + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + if (credentials.oauthTokenData === undefined) { + throw new Error('OAuth credentials not connected!'); + } + + const oAuthClient = new clientOAuth2({}); + const oauthTokenData = JSON.parse(credentials.oauthTokenData as string); + const token = oAuthClient.createToken(oauthTokenData); + + // Signs the request by adding authorization headers or query parameters depending + // on the token-type used. + const newRequestOptions = token.sign(requestOptions as clientOAuth2.RequestObject); + + return this.helpers.request!(newRequestOptions) + .catch(async (error: IResponseError) => { + // TODO: Check if also other codes are possible + if (error.statusCode === 401) { + // TODO: Whole refresh process is not tested yet + // Token is probably not valid anymore. So try refresh it. + const newToken = await token.refresh(); + + const newCredentialsData = newToken.data; + unset(newCredentialsData, 'csrfSecret'); + unset(newCredentialsData, 'oauthTokenData'); + + // Find the name of the credentials + if (!node.credentials || !node.credentials[credentialsType]) { + throw new Error(`The node "${node.name}" does not have credentials of type "${credentialsType}"!`); + } + const name = node.credentials[credentialsType]; + + // Save the refreshed token + await additionalData.updateCredentials(name, credentialsType, newCredentialsData, additionalData.encryptionKey); + + // Make the request again with the new token + const newRequestOptions = newToken.sign(requestOptions as clientOAuth2.RequestObject); + + return this.helpers.request!(newRequestOptions); + } + + // Unknown error so simply throw it + throw error; + }); +} + + + /** * Takes generic input data and brings it into the json format n8n uses. * @@ -355,6 +423,9 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio helpers: { prepareBinaryData, request: requestPromise, + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { + return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); + }, returnJsonArray, }, }; @@ -406,6 +477,9 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi helpers: { prepareBinaryData, request: requestPromise, + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { + return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); + }, returnJsonArray, }, }; @@ -484,6 +558,9 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx helpers: { prepareBinaryData, request: requestPromise, + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { + return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); + }, returnJsonArray, }, }; @@ -563,6 +640,9 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData: helpers: { prepareBinaryData, request: requestPromise, + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { + return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); + }, }, }; })(workflow, runExecutionData, connectionInputData, inputData, node, itemIndex); @@ -610,6 +690,9 @@ export function getLoadOptionsFunctions(workflow: Workflow, node: INode, additio }, helpers: { request: requestPromise, + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { + return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); + }, }, }; return that; @@ -665,6 +748,9 @@ export function getExecuteHookFunctions(workflow: Workflow, node: INode, additio }, helpers: { request: requestPromise, + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { + return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); + }, }, }; return that; @@ -747,6 +833,9 @@ export function getExecuteWebhookFunctions(workflow: Workflow, node: INode, addi helpers: { prepareBinaryData, request: requestPromise, + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { + return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); + }, returnJsonArray, }, }; diff --git a/packages/core/test/Helpers.ts b/packages/core/test/Helpers.ts index a0a68f4a42..8a92f35718 100644 --- a/packages/core/test/Helpers.ts +++ b/packages/core/test/Helpers.ts @@ -1,6 +1,7 @@ import { set } from 'lodash'; import { + ICredentialDataDecryptedObject, IExecuteWorkflowInfo, INodeExecutionData, INodeParameters, @@ -280,6 +281,7 @@ export function WorkflowExecuteAdditionalData(waitPromise: IDeferredPromise => {}, webhookBaseUrl: 'webhook', webhookTestBaseUrl: 'webhook-test', }; diff --git a/packages/editor-ui/src/components/CredentialsEdit.vue b/packages/editor-ui/src/components/CredentialsEdit.vue index 7be7a1326a..735c1adfc2 100644 --- a/packages/editor-ui/src/components/CredentialsEdit.vue +++ b/packages/editor-ui/src/components/CredentialsEdit.vue @@ -169,13 +169,21 @@ export default mixins( }, methods: { getCredentialTypeData (name: string): ICredentialType | null { - for (const credentialData of this.credentialTypes) { - if (credentialData.name === name) { - return credentialData; - } + let credentialData = this.$store.getters.credentialType(name); + + if (credentialData === null || credentialData.extends === undefined) { + return credentialData; } - return null; + // Credentials extends another one. So get the properties of the one it + // extends and add them. + credentialData = JSON.parse(JSON.stringify(credentialData)); + for (const credentialTypeName of credentialData.extends) { + const data = this.$store.getters.credentialType(credentialTypeName); + credentialData.properties.push.apply(credentialData.properties, data.properties); + } + + return credentialData; }, credentialsCreated (data: ICredentialsDecryptedResponse): void { this.$emit('credentialsCreated', data); diff --git a/packages/nodes-base/credentials/GithubOAuth2Api.credentials.ts b/packages/nodes-base/credentials/GithubOAuth2Api.credentials.ts new file mode 100644 index 0000000000..74df45d8a6 --- /dev/null +++ b/packages/nodes-base/credentials/GithubOAuth2Api.credentials.ts @@ -0,0 +1,15 @@ +import { + ICredentialType, +} from 'n8n-workflow'; + + +export class GithubOAuth2Api implements ICredentialType { + name = 'githubOAuth2Api'; + // name = 'oAuth2Api/githubOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Github OAuth2 API'; + properties = [ + ]; +} diff --git a/packages/nodes-base/nodes/HttpRequest.node.ts b/packages/nodes-base/nodes/HttpRequest.node.ts index 32da2e4019..c5d9b89d9a 100644 --- a/packages/nodes-base/nodes/HttpRequest.node.ts +++ b/packages/nodes-base/nodes/HttpRequest.node.ts @@ -67,6 +67,17 @@ export class HttpRequest implements INodeType { }, }, }, + { + name: 'oAuth2Api', + required: true, + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, + }, ], properties: [ { @@ -86,6 +97,10 @@ export class HttpRequest implements INodeType { name: 'Header Auth', value: 'headerAuth' }, + { + name: 'OAuth2', + value: 'oAuth2' + }, { name: 'None', value: 'none' @@ -504,6 +519,7 @@ export class HttpRequest implements INodeType { const httpBasicAuth = this.getCredentials('httpBasicAuth'); const httpDigestAuth = this.getCredentials('httpDigestAuth'); const httpHeaderAuth = this.getCredentials('httpHeaderAuth'); + const oAuth2Api = this.getCredentials('oAuth2Api'); let requestOptions: OptionsWithUri; let setUiParameter: IDataObject; @@ -658,7 +674,13 @@ export class HttpRequest implements INodeType { } // Now that the options are all set make the actual http request - const response = await this.helpers.request(requestOptions); + let response; + + if (oAuth2Api !== undefined) { + response = await this.helpers.requestOAuth.call(this, 'oAuth2Api', requestOptions); + } else { + response = await this.helpers.request(requestOptions); + } if (responseFormat === 'file') { const dataPropertyName = this.getNodeParameter('dataPropertyName', 0) as string; diff --git a/packages/nodes-base/nodes/OAuth.node.ts b/packages/nodes-base/nodes/OAuth.node.ts index bf2f193a2f..4f5ff5debb 100644 --- a/packages/nodes-base/nodes/OAuth.node.ts +++ b/packages/nodes-base/nodes/OAuth.node.ts @@ -1,3 +1,5 @@ +import { OptionsWithUri } from 'request'; + import { IExecuteFunctions } from 'n8n-core'; import { INodeExecutionData, @@ -36,11 +38,71 @@ export class OAuth implements INodeType { value: 'get', description: 'Returns the OAuth token data.', }, + { + name: 'Request', + value: 'request', + description: 'Make an OAuth signed requ.', + }, ], default: 'get', description: 'The operation to perform.', }, - + { + displayName: 'Request Method', + name: 'requestMethod', + type: 'options', + displayOptions: { + show: { + operation: [ + 'request', + ], + }, + }, + options: [ + { + name: 'DELETE', + value: 'DELETE' + }, + { + name: 'GET', + value: 'GET' + }, + { + name: 'HEAD', + value: 'HEAD' + }, + { + name: 'PATCH', + value: 'PATCH' + }, + { + name: 'POST', + value: 'POST' + }, + { + name: 'PUT', + value: 'PUT' + }, + ], + default: 'GET', + description: 'The request method to use.', + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + displayOptions: { + show: { + operation: [ + 'request', + ], + }, + }, + default: '', + placeholder: 'http://example.com/index.html', + description: 'The URL to make the request to.', + required: true, + }, ] }; @@ -62,6 +124,22 @@ export class OAuth implements INodeType { // without knowledge of the node. return [this.helpers.returnJsonArray(JSON.parse(credentials.oauthTokenData as string))]; + } else if (operation === 'request') { + const url = this.getNodeParameter('url', 0) as string; + const requestMethod = this.getNodeParameter('requestMethod', 0) as string; + + // Authorization Code Grant + const requestOptions: OptionsWithUri = { + headers: { + 'User-Agent': 'some-user', + }, + method: requestMethod, + uri: url, + json: true, + }; + + const responseData = await this.helpers.requestOAuth.call(this, 'oAuth2Api', requestOptions); + return [this.helpers.returnJsonArray(responseData)]; } else { throw new Error('Unknown operation'); } diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index fa50146007..0ae62f9f07 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -40,6 +40,7 @@ "dist/credentials/FileMaker.credentials.js", "dist/credentials/FlowApi.credentials.js", "dist/credentials/GithubApi.credentials.js", + "dist/credentials/GithubOAuth2Api.credentials.js", "dist/credentials/GitlabApi.credentials.js", "dist/credentials/GoogleApi.credentials.js", "dist/credentials/HttpBasicAuth.credentials.js", diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 8fd424ba19..0a69f2e53a 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -2,6 +2,8 @@ import { Workflow } from './Workflow'; import { WorkflowHooks } from './WorkflowHooks'; import * as express from 'express'; +export type IAllExecuteFunctions = IExecuteFunctions | IExecuteSingleFunctions | IHookFunctions | ILoadOptionsFunctions | IPollFunctions | ITriggerFunctions | IWebhookFunctions; + export interface IBinaryData { [key: string]: string | undefined; data: string; @@ -58,6 +60,7 @@ export interface ICredentialsEncrypted { export interface ICredentialType { name: string; displayName: string; + extends?: string[]; properties: INodeProperties[]; } @@ -78,7 +81,7 @@ export interface ICredentialData { } // The encrypted credentials which the nodes can access -export type CredentialInformation = string | number | boolean; +export type CredentialInformation = string | number | boolean | IDataObject; // The encrypted credentials which the nodes can access @@ -649,6 +652,7 @@ export interface IWorkflowExecuteAdditionalData { httpRequest?: express.Request; restApiUrl: string; timezone: string; + updateCredentials: (name: string, type: string, data: ICredentialDataDecryptedObject, encryptionKey: string) => Promise; webhookBaseUrl: string; webhookTestBaseUrl: string; currentNodeParameters? : INodeParameters[]; From eb285ef711e8a1cf8c1438fcd4f22c81c670993e Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sat, 25 Jan 2020 23:48:38 -0800 Subject: [PATCH 003/165] :zap: Make it possible to set credentials to fixed values --- packages/cli/commands/execute.ts | 5 ++ packages/cli/commands/start.ts | 5 ++ packages/cli/config/index.ts | 13 +++ packages/cli/src/CredentialTypes.ts | 16 ++++ packages/cli/src/CredentialsHelper.ts | 81 +++++++++++++++++++ packages/cli/src/CredentialsOverwrites.ts | 56 +++++++++++++ packages/cli/src/Interfaces.ts | 4 + packages/cli/src/LoadNodesAndCredentials.ts | 2 +- packages/cli/src/NodeTypes.ts | 2 +- packages/cli/src/Server.ts | 49 ++++++----- .../cli/src/WorkflowExecuteAdditionalData.ts | 40 +-------- packages/cli/src/index.ts | 2 + packages/core/src/Credentials.ts | 17 +--- packages/core/src/NodeExecuteFunctions.ts | 32 +++----- packages/core/src/UserSettings.ts | 1 - packages/core/test/Helpers.ts | 17 +++- .../src/components/CredentialsEdit.vue | 2 +- .../src/components/CredentialsInput.vue | 51 +++++++++--- packages/workflow/src/Interfaces.ts | 38 ++++++++- 19 files changed, 316 insertions(+), 117 deletions(-) create mode 100644 packages/cli/src/CredentialsHelper.ts create mode 100644 packages/cli/src/CredentialsOverwrites.ts diff --git a/packages/cli/commands/execute.ts b/packages/cli/commands/execute.ts index 448af3de3d..211f9e6479 100644 --- a/packages/cli/commands/execute.ts +++ b/packages/cli/commands/execute.ts @@ -6,6 +6,7 @@ import { import { ActiveExecutions, + CredentialsOverwrites, Db, GenericHelpers, IWorkflowBase, @@ -100,6 +101,10 @@ export class Execute extends Command { // Wait till the n8n-packages have been read await loadNodesAndCredentialsPromise; + // Load the credentials overwrites if any exist + const credentialsOverwrites = CredentialsOverwrites(); + await credentialsOverwrites.init(); + // Add the found types to an instance other parts of the application can use const nodeTypes = NodeTypes(); await nodeTypes.init(loadNodesAndCredentials.nodeTypes); diff --git a/packages/cli/commands/start.ts b/packages/cli/commands/start.ts index e8f1694b9e..15d225ef16 100644 --- a/packages/cli/commands/start.ts +++ b/packages/cli/commands/start.ts @@ -11,6 +11,7 @@ import * as config from '../config'; import { ActiveWorkflowRunner, CredentialTypes, + CredentialsOverwrites, Db, GenericHelpers, LoadNodesAndCredentials, @@ -112,6 +113,10 @@ export class Start extends Command { const loadNodesAndCredentials = LoadNodesAndCredentials(); await loadNodesAndCredentials.init(); + // Load the credentials overwrites if any exist + const credentialsOverwrites = CredentialsOverwrites(); + await credentialsOverwrites.init(); + // Add the found types to an instance other parts of the application can use const nodeTypes = NodeTypes(); await nodeTypes.init(loadNodesAndCredentials.nodeTypes); diff --git a/packages/cli/config/index.ts b/packages/cli/config/index.ts index f8b776ec82..3df13d0440 100644 --- a/packages/cli/config/index.ts +++ b/packages/cli/config/index.ts @@ -54,6 +54,19 @@ const config = convict({ }, }, + credentials: { + overwrite: { + // Allows to set default values for credentials which + // get automatically prefilled and the user does not get + // displayed and can not change. + // Format: { CREDENTIAL_NAME: { PARAMTER: VALUE }} + doc: 'Overwrites for credentials', + format: '*', + default: '{}', + env: 'CREDENTIALS_OVERWRITE' + } + }, + executions: { // If a workflow executes all the data gets saved by default. This // could be a problem when a workflow gets executed a lot and processes diff --git a/packages/cli/src/CredentialTypes.ts b/packages/cli/src/CredentialTypes.ts index dc14801cbc..20a0e36e9e 100644 --- a/packages/cli/src/CredentialTypes.ts +++ b/packages/cli/src/CredentialTypes.ts @@ -3,6 +3,9 @@ import { ICredentialTypes as ICredentialTypesInterface, } from 'n8n-workflow'; +import { + CredentialsOverwrites, +} from './'; class CredentialTypesClass implements ICredentialTypesInterface { @@ -13,6 +16,19 @@ class CredentialTypesClass implements ICredentialTypesInterface { async init(credentialTypes: { [key: string]: ICredentialType }): Promise { this.credentialTypes = credentialTypes; + + // Load the credentials overwrites if any exist + const credentialsOverwrites = CredentialsOverwrites().getAll(); + + for (const credentialType of Object.keys(credentialsOverwrites)) { + if (credentialTypes[credentialType] === undefined) { + continue; + } + + // Add which properties got overwritten that the Editor-UI knows + // which properties it should hide + credentialTypes[credentialType].__overwrittenProperties = Object.keys(credentialsOverwrites[credentialType]); + } } getAll(): ICredentialType[] { diff --git a/packages/cli/src/CredentialsHelper.ts b/packages/cli/src/CredentialsHelper.ts new file mode 100644 index 0000000000..a5dee733f2 --- /dev/null +++ b/packages/cli/src/CredentialsHelper.ts @@ -0,0 +1,81 @@ +import { + Credentials, +} from 'n8n-core'; + +import { + ICredentialDataDecryptedObject, + ICredentialsHelper, +} from 'n8n-workflow'; + +import { + CredentialsOverwrites, + Db, + ICredentialsDb, +} from './'; + + +export class CredentialsHelper extends ICredentialsHelper { + + /** + * Returns the credentials instance + * + * @param {string} name Name of the credentials to return instance of + * @param {string} type Type of the credentials to return instance of + * @returns {Credentials} + * @memberof CredentialsHelper + */ + getCredentials(name: string, type: string): Credentials { + if (!this.workflowCredentials[type]) { + throw new Error(`No credentials of type "${type}" exist.`); + } + if (!this.workflowCredentials[type][name]) { + throw new Error(`No credentials with name "${name}" exist for type "${type}".`); + } + const credentialData = this.workflowCredentials[type][name]; + + return new Credentials(credentialData.name, credentialData.type, credentialData.nodesAccess, credentialData.data); + } + + + /** + * Returns the decrypted credential data with applied overwrites + * + * @param {string} name Name of the credentials to return data of + * @param {string} type Type of the credentials to return data of + * @returns {ICredentialDataDecryptedObject} + * @memberof CredentialsHelper + */ + getDecrypted(name: string, type: string): ICredentialDataDecryptedObject { + const credentials = this.getCredentials(name, type); + + // Load and apply the credentials overwrites if any exist + const credentialsOverwrites = CredentialsOverwrites(); + return credentialsOverwrites.applyOverwrite(credentials.type, credentials.getData(this.encryptionKey)); + } + + + /** + * Updates credentials in the database + * + * @param {string} name Name of the credentials to set data of + * @param {string} type Type of the credentials to set data of + * @param {ICredentialDataDecryptedObject} data The data to set + * @returns {Promise} + * @memberof CredentialsHelper + */ + async updateCredentials(name: string, type: string, data: ICredentialDataDecryptedObject): Promise { + const credentials = await this.getCredentials(name, type); + + credentials.setData(data, this.encryptionKey); + const newCredentialsData = credentials.getDataToSave() as ICredentialsDb; + + // Add special database related data + newCredentialsData.updatedAt = new Date(); + + // TODO: also add user automatically depending on who is logged in, if anybody is logged in + + // Save the credentials in DB + await Db.collections.Credentials!.save(newCredentialsData); + } + +} diff --git a/packages/cli/src/CredentialsOverwrites.ts b/packages/cli/src/CredentialsOverwrites.ts new file mode 100644 index 0000000000..6d49092c37 --- /dev/null +++ b/packages/cli/src/CredentialsOverwrites.ts @@ -0,0 +1,56 @@ +import { + ICredentialDataDecryptedObject, +} from 'n8n-workflow'; + +import { + ICredentialsOverwrite, + GenericHelpers, +} from './'; + + +class CredentialsOverwritesClass { + + private overwriteData: ICredentialsOverwrite = {}; + + async init() { + const data = await GenericHelpers.getConfigValue('credentials.overwrite') as string; + + try { + this.overwriteData = JSON.parse(data); + } catch (error) { + throw new Error(`The credentials-overwrite is not valid JSON.`); + } + } + + applyOverwrite(type: string, data: ICredentialDataDecryptedObject) { + const overwrites = this.get(type); + + if (overwrites === undefined) { + return data; + } + + const returnData = JSON.parse(JSON.stringify(data)); + Object.assign(returnData, overwrites); + + return returnData; + } + + get(type: string): ICredentialDataDecryptedObject | undefined { + return this.overwriteData[type]; + } + + getAll(): ICredentialsOverwrite { + return this.overwriteData; + } +} + + +let credentialsOverwritesInstance: CredentialsOverwritesClass | undefined; + +export function CredentialsOverwrites(): CredentialsOverwritesClass { + if (credentialsOverwritesInstance === undefined) { + credentialsOverwritesInstance = new CredentialsOverwritesClass(); + } + + return credentialsOverwritesInstance; +} diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index a5837b8fc6..7542d018b1 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -1,4 +1,5 @@ import { + ICredentialDataDecryptedObject, ICredentialsDecrypted, ICredentialsEncrypted, IDataObject, @@ -34,6 +35,9 @@ export interface ICustomRequest extends Request { parsedUrl: Url | undefined; } +export interface ICredentialsOverwrite { + [key: string]: ICredentialDataDecryptedObject; +} export interface IDatabaseCollections { Credentials: Repository | null; diff --git a/packages/cli/src/LoadNodesAndCredentials.ts b/packages/cli/src/LoadNodesAndCredentials.ts index eccfcb0804..25fc77504d 100644 --- a/packages/cli/src/LoadNodesAndCredentials.ts +++ b/packages/cli/src/LoadNodesAndCredentials.ts @@ -137,7 +137,7 @@ class LoadNodesAndCredentialsClass { } } - this.credentialTypes[credentialName] = tempCredential; + this.credentialTypes[tempCredential.name] = tempCredential; } diff --git a/packages/cli/src/NodeTypes.ts b/packages/cli/src/NodeTypes.ts index 66b2363bfe..f600e8b734 100644 --- a/packages/cli/src/NodeTypes.ts +++ b/packages/cli/src/NodeTypes.ts @@ -15,7 +15,7 @@ class NodeTypesClass implements INodeTypes { // Some nodeTypes need to get special parameters applied like the // polling nodes the polling times for (const nodeTypeData of Object.values(nodeTypes)) { - const applyParameters = NodeHelpers.getSpecialNodeParameters(nodeTypeData.type) + const applyParameters = NodeHelpers.getSpecialNodeParameters(nodeTypeData.type); if (applyParameters.length) { nodeTypeData.type.description.properties.unshift.apply(nodeTypeData.type.description.properties, applyParameters); diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 900dcb012f..9da615f7a3 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -18,6 +18,7 @@ import * as csrf from 'csrf'; import { ActiveExecutions, ActiveWorkflowRunner, + CredentialsOverwrites, CredentialTypes, Db, IActivationError, @@ -648,6 +649,10 @@ class App { this.app.post('/rest/credentials', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const incomingData = req.body; + if (!incomingData.name || incomingData.name.length < 3) { + throw new ResponseHelper.ResponseError(`Credentials name must be at least 3 characters long.`, undefined, 400); + } + // Add the added date for node access permissions for (const nodeAccess of incomingData.nodesAccess) { nodeAccess.date = this.getCurrentDate(); @@ -684,6 +689,7 @@ class App { // Save the credentials in DB const result = await Db.collections.Credentials!.save(newCredentialsData); + result.data = incomingData.data; // Convert to response format in which the id is a string (result as unknown as ICredentialsResponse).id = result.id.toString(); @@ -805,14 +811,6 @@ class App { const results = await Db.collections.Credentials!.find(findQuery) as unknown as ICredentialsResponse[]; - let encryptionKey = undefined; - if (req.query.includeData === true) { - encryptionKey = await UserSettings.getEncryptionKey(); - if (encryptionKey === undefined) { - throw new Error('No encryption key got found to decrypt the credentials!'); - } - } - let result; for (result of results) { (result as ICredentialsDecryptedResponse).id = result.id.toString(); @@ -866,19 +864,17 @@ class App { } const credentials = new Credentials(result.name, result.type, result.nodesAccess, result.data); - (result as ICredentialsDecryptedDb).data = credentials.getData(encryptionKey!); - (result as ICredentialsDecryptedResponse).id = result.id.toString(); + const savedCredentialsData = credentials.getData(encryptionKey); - const oauthCredentials = (result as ICredentialsDecryptedDb).data; - if (oauthCredentials === undefined) { - throw new Error('Unable to read OAuth credentials'); - } + // Load the credentials overwrites if any exist + const credentialsOverwrites = CredentialsOverwrites(); + const oauthCredentials = credentialsOverwrites.applyOverwrite(credentials.type, savedCredentialsData); const token = new csrf(); // Generate a CSRF prevention token and send it as a OAuth2 state stringma/ERR - oauthCredentials.csrfSecret = token.secretSync(); + const csrfSecret = token.secretSync(); const state = { - token: token.create(oauthCredentials.csrfSecret), + token: token.create(csrfSecret), cid: req.query.id }; const stateEncodedStr = Buffer.from(JSON.stringify(state)).toString('base64') as string; @@ -893,7 +889,8 @@ class App { state: stateEncodedStr, }); - credentials.setData(oauthCredentials, encryptionKey); + savedCredentialsData.csrfSecret = csrfSecret; + credentials.setData(savedCredentialsData, encryptionKey); const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb; // Add special database related data @@ -939,12 +936,11 @@ class App { } const credentials = new Credentials(result.name, result.type, result.nodesAccess, result.data); - (result as ICredentialsDecryptedDb).data = credentials.getData(encryptionKey!); - const oauthCredentials = (result as ICredentialsDecryptedDb).data; - if (oauthCredentials === undefined) { - const errorResponse = new ResponseHelper.ResponseError('Unable to read OAuth credentials!', undefined, 503); - return ResponseHelper.sendErrorResponse(res, errorResponse); - } + const savedCredentialsData = credentials.getData(encryptionKey!); + + // Load the credentials overwrites if any exist + const credentialsOverwrites = CredentialsOverwrites(); + const oauthCredentials = credentialsOverwrites.applyOverwrite(credentials.type, savedCredentialsData); const token = new csrf(); if (oauthCredentials.csrfSecret === undefined || !token.verify(oauthCredentials.csrfSecret as string, state.token)) { @@ -968,9 +964,10 @@ class App { return ResponseHelper.sendErrorResponse(res, errorResponse); } - oauthCredentials.oauthTokenData = JSON.stringify(oauthToken.data); - _.unset(oauthCredentials, 'csrfSecret'); - credentials.setData(oauthCredentials, encryptionKey); + savedCredentialsData.oauthTokenData = JSON.stringify(oauthToken.data); + _.unset(savedCredentialsData, 'csrfSecret'); + + credentials.setData(savedCredentialsData, encryptionKey); const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb; // Add special database related data newCredentialsData.updatedAt = this.getCurrentDate(); diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index e234c11410..99fde65d1c 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -1,6 +1,6 @@ import { + CredentialsHelper, Db, - ICredentialsDb, IExecutionDb, IExecutionFlattedDb, IPushDataExecutionFinished, @@ -14,13 +14,11 @@ import { } from './'; import { - Credentials, UserSettings, WorkflowExecute, } from 'n8n-core'; import { - ICredentialDataDecryptedObject, IDataObject, IExecuteData, IExecuteWorkflowInfo, @@ -372,40 +370,6 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi } -/** - * Updates credentials with new data - * - * @export - * @param {string} name Name of the credentials to update - * @param {string} type Type of the credentials to update - * @param {ICredentialDataDecryptedObject} data The new credential data - * @param {string} encryptionKey The encryption key to use - * @returns {Promise} - */ -export async function updateCredentials(name: string, type: string, data: ICredentialDataDecryptedObject, encryptionKey: string): Promise { - const foundCredentials = await Db.collections.Credentials!.find({ name, type }); - - if (!foundCredentials.length) { - throw new Error(`Could not find credentials for type "${type}" with name "${name}".`); - } - - const credentialsDb = foundCredentials[0]; - - // Encrypt the data - const credentials = new Credentials(credentialsDb.name, credentialsDb.type, credentialsDb.nodesAccess); - credentials.setData(data, encryptionKey); - const newCredentialsData = credentials.getDataToSave() as ICredentialsDb; - - // Add special database related data - newCredentialsData.updatedAt = this.getCurrentDate(); - - // TODO: also add user automatically depending on who is logged in, if anybody is logged in - - // Save the credentials in DB - await Db.collections.Credentials!.save(newCredentialsData); -} - - /** * Returns the base additional data without webhooks * @@ -428,11 +392,11 @@ export async function getBase(credentials: IWorkflowCredentials, currentNodePara return { credentials, + credentialsHelper: new CredentialsHelper(credentials, encryptionKey), encryptionKey, executeWorkflow, restApiUrl: urlBaseWebhook + config.get('endpoints.rest') as string, timezone, - updateCredentials, webhookBaseUrl, webhookTestBaseUrl, currentNodeParameters, diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 0b7ae6ad0a..3916e79edd 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,4 +1,6 @@ +export * from './CredentialsHelper'; export * from './CredentialTypes'; +export * from './CredentialsOverwrites'; export * from './Interfaces'; export * from './LoadNodesAndCredentials'; export * from './NodeTypes'; diff --git a/packages/core/src/Credentials.ts b/packages/core/src/Credentials.ts index 2559d78dda..e692f597cd 100644 --- a/packages/core/src/Credentials.ts +++ b/packages/core/src/Credentials.ts @@ -1,25 +1,14 @@ import { - ICredentialDataDecryptedObject, CredentialInformation, + ICredentialDataDecryptedObject, + ICredentials, ICredentialsEncrypted, - ICredentialNodeAccess, } from 'n8n-workflow'; import { enc, AES } from 'crypto-js'; -export class Credentials implements ICredentialsEncrypted { - name: string; - type: string; - data: string | undefined; - nodesAccess: ICredentialNodeAccess[]; - - constructor(name: string, type: string, nodesAccess: ICredentialNodeAccess[], data?: string) { - this.name = name; - this.type = type; - this.nodesAccess = nodesAccess; - this.data = data; - } +export class Credentials extends ICredentials { /** diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index cfdcdc8061..2c13b2c389 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -1,5 +1,4 @@ import { - Credentials, IHookFunctions, ILoadOptionsFunctions, IResponseError, @@ -154,7 +153,7 @@ export function requestOAuth(this: IAllExecuteFunctions, credentialsType: string const name = node.credentials[credentialsType]; // Save the refreshed token - await additionalData.updateCredentials(name, credentialsType, newCredentialsData, additionalData.encryptionKey); + await additionalData.credentialsHelper.updateCredentials(name, credentialsType, newCredentialsData); // Make the request again with the new token const newRequestOptions = newToken.sign(requestOptions as clientOAuth2.RequestObject); @@ -244,20 +243,7 @@ export function getCredentials(workflow: Workflow, node: INode, type: string, ad const name = node.credentials[type]; - if (!additionalData.credentials[type]) { - throw new Error(`No credentials of type "${type}" exist.`); - } - if (!additionalData.credentials[type][name]) { - throw new Error(`No credentials with name "${name}" exist for type "${type}".`); - } - const credentialData = additionalData.credentials[type][name]; - - const credentials = new Credentials(name, type, credentialData.nodesAccess, credentialData.data); - const decryptedDataObject = credentials.getData(additionalData.encryptionKey, node.type); - - if (decryptedDataObject === null) { - throw new Error('Could not get the credentials'); - } + const decryptedDataObject = additionalData.credentialsHelper.getDecrypted(name, type); return decryptedDataObject; } @@ -423,7 +409,7 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); }, returnJsonArray, @@ -477,7 +463,7 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); }, returnJsonArray, @@ -558,7 +544,7 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); }, returnJsonArray, @@ -640,7 +626,7 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData: helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); }, }, @@ -690,7 +676,7 @@ export function getLoadOptionsFunctions(workflow: Workflow, node: INode, additio }, helpers: { request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); }, }, @@ -748,7 +734,7 @@ export function getExecuteHookFunctions(workflow: Workflow, node: INode, additio }, helpers: { request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); }, }, @@ -833,7 +819,7 @@ export function getExecuteWebhookFunctions(workflow: Workflow, node: INode, addi helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); }, returnJsonArray, diff --git a/packages/core/src/UserSettings.ts b/packages/core/src/UserSettings.ts index af32d5e36c..211341ed25 100644 --- a/packages/core/src/UserSettings.ts +++ b/packages/core/src/UserSettings.ts @@ -143,7 +143,6 @@ export async function writeUserSettings(userSettings: IUserSettings, settingsPat */ export async function getUserSettings(settingsPath?: string, ignoreCache?: boolean): Promise { if (settingsCache !== undefined && ignoreCache !== true) { - return settingsCache; } diff --git a/packages/core/test/Helpers.ts b/packages/core/test/Helpers.ts index 8a92f35718..790025dcda 100644 --- a/packages/core/test/Helpers.ts +++ b/packages/core/test/Helpers.ts @@ -2,6 +2,7 @@ import { set } from 'lodash'; import { ICredentialDataDecryptedObject, + ICredentialsHelper, IExecuteWorkflowInfo, INodeExecutionData, INodeParameters, @@ -16,11 +17,25 @@ import { } from 'n8n-workflow'; import { + Credentials, IDeferredPromise, IExecuteFunctions, } from '../src'; +export class CredentialsHelper extends ICredentialsHelper { + getDecrypted(name: string, type: string): ICredentialDataDecryptedObject { + return {}; + } + + getCredentials(name: string, type: string): Credentials { + return new Credentials('', '', [], ''); + } + + async updateCredentials(name: string, type: string, data: ICredentialDataDecryptedObject): Promise {} +} + + class NodeTypesClass implements INodeTypes { nodeTypes: INodeTypeData = { @@ -276,12 +291,12 @@ export function WorkflowExecuteAdditionalData(waitPromise: IDeferredPromise => {}, // tslint:disable-line:no-any restApiUrl: '', encryptionKey: 'test', timezone: 'America/New_York', - updateCredentials: async (name: string, type: string, data: ICredentialDataDecryptedObject, encryptionKey: string): Promise => {}, webhookBaseUrl: 'webhook', webhookTestBaseUrl: 'webhook-test', }; diff --git a/packages/editor-ui/src/components/CredentialsEdit.vue b/packages/editor-ui/src/components/CredentialsEdit.vue index 735c1adfc2..256cf70311 100644 --- a/packages/editor-ui/src/components/CredentialsEdit.vue +++ b/packages/editor-ui/src/components/CredentialsEdit.vue @@ -8,7 +8,7 @@ Credential type: - +
-
+
Credential Data:
- + {{parameter.displayName}}: @@ -97,7 +97,11 @@ import { restApi } from '@/components/mixins/restApi'; import { nodeHelpers } from '@/components/mixins/nodeHelpers'; import { showMessage } from '@/components/mixins/showMessage'; -import { ICredentialsDecryptedResponse, IUpdateInformation } from '@/Interface'; +import { + ICredentialsDecryptedResponse, + ICredentialsResponse, + IUpdateInformation, +} from '@/Interface'; import { CredentialInformation, ICredentialDataDecryptedObject, @@ -105,6 +109,7 @@ import { ICredentialType, ICredentialNodeAccess, INodeCredentialDescription, + INodeProperties, INodeTypeDescription, } from 'n8n-workflow'; @@ -172,15 +177,20 @@ export default mixins( }; }); }, + credentialProperties (): INodeProperties[] { + return this.credentialTypeData.properties.filter((propertyData: INodeProperties) => { + return !this.credentialTypeData.__overwrittenProperties || !this.credentialTypeData.__overwrittenProperties.includes(propertyData.name); + }); + }, isOAuthType (): boolean { - return this.credentialData && this.credentialData.type === 'oAuth2Api'; + return this.credentialTypeData.name === 'oAuth2Api' || (this.credentialTypeData.extends !== undefined && this.credentialTypeData.extends.includes('oAuth2Api')); }, isOAuthConnected (): boolean { if (this.isOAuthType === false) { return false; } - return !!this.credentialData.data.oauthTokenData; + return this.credentialData !== null && !!this.credentialData.data.oauthTokenData; }, }, methods: { @@ -192,7 +202,7 @@ export default mixins( tempValue[name] = parameterData.value; Vue.set(this, 'propertyValue', tempValue); }, - async createCredentials (): Promise { + async createCredentials (doNotEmitData?: boolean): Promise { const nodesAccess = this.nodesAccess.map((nodeType) => { return { nodeType, @@ -211,18 +221,35 @@ export default mixins( result = await this.restApi().createNewCredentials(newCredentials); } catch (error) { this.$showError(error, 'Problem Creating Credentials', 'There was a problem creating the credentials:'); - return; + return null; } // Add also to local store this.$store.commit('addCredentials', result); - this.$emit('credentialsCreated', result); + if (doNotEmitData !== true) { + this.$emit('credentialsCreated', result); + } + + return result; }, async oAuth2CredentialAuthorize () { let url; + + let credentialData = this.credentialData; + let newCredentials = false; + if (!credentialData) { + // Credentials did not get created yet. So create first before + // doing oauth authorize + credentialData = await this.createCredentials(true); + newCredentials = true; + if (credentialData === null) { + return; + } + } + try { - url = await this.restApi().oAuth2CredentialAuthorize(this.credentialData) as string; + url = await this.restApi().oAuth2CredentialAuthorize(credentialData) as string; } catch (error) { this.$showError(error, 'OAuth Authorization Error', 'Error generating authorization URL:'); return; @@ -241,13 +268,17 @@ export default mixins( // Set some kind of data that status changes. // As data does not get displayed directly it does not matter what data. - this.credentialData.data.oauthTokenData = {}; + credentialData.data.oauthTokenData = {}; // Close the window if (oauthPopup) { oauthPopup.close(); } + if (newCredentials === true) { + this.$emit('credentialsCreated', credentialData); + } + this.$showMessage({ title: 'Connected', message: 'Got connected!', diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 0a69f2e53a..6b8f98fe04 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -35,6 +35,27 @@ export interface IGetCredentials { get(type: string, name: string): Promise; } +export abstract class ICredentials { + name: string; + type: string; + data: string | undefined; + nodesAccess: ICredentialNodeAccess[]; + + constructor(name: string, type: string, nodesAccess: ICredentialNodeAccess[], data?: string) { + this.name = name; + this.type = type; + this.nodesAccess = nodesAccess; + this.data = data; + } + + abstract getData(encryptionKey: string, nodeType?: string): ICredentialDataDecryptedObject; + abstract getDataKey(key: string, encryptionKey: string, nodeType?: string): CredentialInformation; + abstract getDataToSave(): ICredentialsEncrypted; + abstract hasNodeAccess(nodeType: string): boolean; + abstract setData(data: ICredentialDataDecryptedObject, encryptionKey: string): void; + abstract setDataKey(key: string, data: CredentialInformation, encryptionKey: string): void; +} + // Defines which nodes are allowed to access the credentials and // when that access got grented from which user export interface ICredentialNodeAccess { @@ -57,11 +78,26 @@ export interface ICredentialsEncrypted { data?: string; } +export abstract class ICredentialsHelper { + encryptionKey: string; + workflowCredentials: IWorkflowCredentials; + + constructor(workflowCredentials: IWorkflowCredentials, encryptionKey: string) { + this.encryptionKey = encryptionKey; + this.workflowCredentials = workflowCredentials; + } + + abstract getCredentials(name: string, type: string): ICredentials; + abstract getDecrypted(name: string, type: string): ICredentialDataDecryptedObject; + abstract updateCredentials(name: string, type: string, data: ICredentialDataDecryptedObject): Promise; +} + export interface ICredentialType { name: string; displayName: string; extends?: string[]; properties: INodeProperties[]; + __overwrittenProperties?: string[]; } export interface ICredentialTypes { @@ -644,6 +680,7 @@ export interface IWorkflowExecuteHooks { export interface IWorkflowExecuteAdditionalData { credentials: IWorkflowCredentials; + credentialsHelper: ICredentialsHelper; encryptionKey: string; executeWorkflow: (workflowInfo: IExecuteWorkflowInfo, additionalData: IWorkflowExecuteAdditionalData, inputData?: INodeExecutionData[]) => Promise; // tslint:disable-line:no-any // hooks?: IWorkflowExecuteHooks; @@ -652,7 +689,6 @@ export interface IWorkflowExecuteAdditionalData { httpRequest?: express.Request; restApiUrl: string; timezone: string; - updateCredentials: (name: string, type: string, data: ICredentialDataDecryptedObject, encryptionKey: string) => Promise; webhookBaseUrl: string; webhookTestBaseUrl: string; currentNodeParameters? : INodeParameters[]; From 5594543ec82fbbb1551abce1738a9e04235edcec Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sat, 8 Feb 2020 16:14:28 -0800 Subject: [PATCH 004/165] :zap: Fix OAuth UI issues and allow to set additional query parameters --- packages/cli/src/Server.ts | 11 ++-- packages/editor-ui/src/Interface.ts | 7 +++ .../src/components/CredentialsEdit.vue | 25 +++++---- .../src/components/CredentialsInput.vue | 53 +++++++++++++------ .../src/components/NodeCredentials.vue | 17 +++--- .../credentials/OAuth2Api.credentials.ts | 8 +++ 6 files changed, 86 insertions(+), 35 deletions(-) diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 9da615f7a3..1d82c4d807 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -731,8 +731,6 @@ class App { // Encrypt the data const credentials = new Credentials(incomingData.name, incomingData.type, incomingData.nodesAccess); - _.unset(incomingData.data, 'csrfSecret'); - _.unset(incomingData.data, 'oauthTokenData'); credentials.setData(incomingData.data, encryptionKey); const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb; @@ -899,7 +897,14 @@ class App { // Update the credentials in DB await Db.collections.Credentials!.update(req.query.id, newCredentialsData); - return oAuthObj.code.getUri(); + const authQueryParameters = _.get(oauthCredentials, 'authQueryParameters', '') as string; + let returnUri = oAuthObj.code.getUri(); + + if (authQueryParameters) { + returnUri += '&' + authQueryParameters; + } + + return returnUri; })); // ---------------------------------------- diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 7c692881a8..e0e6355359 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -157,6 +157,13 @@ export interface IBinaryDisplayData { runIndex: number; } +export interface ICredentialsCreatedEvent { + data: ICredentialsDecryptedResponse; + options: { + closeDialog: boolean, + }; +} + export interface IStartRunData { workflowData: IWorkflowData; startNodes?: string[]; diff --git a/packages/editor-ui/src/components/CredentialsEdit.vue b/packages/editor-ui/src/components/CredentialsEdit.vue index 256cf70311..cd3b6c1f74 100644 --- a/packages/editor-ui/src/components/CredentialsEdit.vue +++ b/packages/editor-ui/src/components/CredentialsEdit.vue @@ -31,7 +31,10 @@ import Vue from 'vue'; import { restApi } from '@/components/mixins/restApi'; import { showMessage } from '@/components/mixins/showMessage'; import CredentialsInput from '@/components/CredentialsInput.vue'; -import { ICredentialsDecryptedResponse } from '@/Interface'; +import { + ICredentialsCreatedEvent, + ICredentialsDecryptedResponse, +} from '@/Interface'; import { ICredentialType, @@ -185,27 +188,31 @@ export default mixins( return credentialData; }, - credentialsCreated (data: ICredentialsDecryptedResponse): void { - this.$emit('credentialsCreated', data); + credentialsCreated (eventData: ICredentialsCreatedEvent): void { + this.$emit('credentialsCreated', eventData); this.$showMessage({ title: 'Credentials created', - message: `The credential "${data.name}" got created!`, + message: `The credential "${eventData.data.name}" got created!`, type: 'success', }); - this.closeDialog(); + if (eventData.options.closeDialog === true) { + this.closeDialog(); + } }, - credentialsUpdated (data: ICredentialsDecryptedResponse): void { - this.$emit('credentialsUpdated', data); + credentialsUpdated (eventData: ICredentialsCreatedEvent): void { + this.$emit('credentialsUpdated', eventData); this.$showMessage({ title: 'Credentials updated', - message: `The credential "${data.name}" got updated!`, + message: `The credential "${eventData.data.name}" got updated!`, type: 'success', }); - this.closeDialog(); + if (eventData.options.closeDialog === true) { + this.closeDialog(); + } }, closeDialog (): void { // Handle the close externally as the visible parameter is an external prop diff --git a/packages/editor-ui/src/components/CredentialsInput.vue b/packages/editor-ui/src/components/CredentialsInput.vue index d8be6d09a9..bc5b6225d3 100644 --- a/packages/editor-ui/src/components/CredentialsInput.vue +++ b/packages/editor-ui/src/components/CredentialsInput.vue @@ -79,10 +79,10 @@
- + Save - + Create
@@ -141,6 +141,7 @@ export default mixins( credentialsName: 'The name the credentials should be saved as. Use a name
which makes it clear to what exactly they give access to.
For credentials of an Email account that could be the Email address itself.', nodesWithAccess: 'The nodes which allowed to use this credentials.', }, + credentialDataTemp: null as ICredentialsDecryptedResponse | null, nodesAccess: [] as string[], name: '', propertyValue: {} as ICredentialDataDecryptedObject, @@ -182,6 +183,13 @@ export default mixins( return !this.credentialTypeData.__overwrittenProperties || !this.credentialTypeData.__overwrittenProperties.includes(propertyData.name); }); }, + credentialDataDynamic (): ICredentialsDecryptedResponse | null { + if (this.credentialData) { + return this.credentialData; + } + + return this.credentialDataTemp; + }, isOAuthType (): boolean { return this.credentialTypeData.name === 'oAuth2Api' || (this.credentialTypeData.extends !== undefined && this.credentialTypeData.extends.includes('oAuth2Api')); }, @@ -190,7 +198,7 @@ export default mixins( return false; } - return this.credentialData !== null && !!this.credentialData.data.oauthTokenData; + return this.credentialDataDynamic !== null && !!this.credentialDataDynamic.data!.oauthTokenData; }, }, methods: { @@ -202,7 +210,7 @@ export default mixins( tempValue[name] = parameterData.value; Vue.set(this, 'propertyValue', tempValue); }, - async createCredentials (doNotEmitData?: boolean): Promise { + async createCredentials (closeDialog: boolean): Promise { const nodesAccess = this.nodesAccess.map((nodeType) => { return { nodeType, @@ -227,29 +235,30 @@ export default mixins( // Add also to local store this.$store.commit('addCredentials', result); - if (doNotEmitData !== true) { - this.$emit('credentialsCreated', result); - } + this.$emit('credentialsCreated', {data: result, options: { closeDialog }}); return result; }, async oAuth2CredentialAuthorize () { let url; - let credentialData = this.credentialData; + let credentialData = this.credentialDataDynamic; let newCredentials = false; if (!credentialData) { // Credentials did not get created yet. So create first before // doing oauth authorize - credentialData = await this.createCredentials(true); + credentialData = await this.createCredentials(false) as ICredentialsDecryptedResponse; newCredentials = true; if (credentialData === null) { return; } + } else { + // Exists already but got maybe changed. So save first + credentialData = await this.updateCredentials(false) as ICredentialsDecryptedResponse; } try { - url = await this.restApi().oAuth2CredentialAuthorize(credentialData) as string; + url = await this.restApi().oAuth2CredentialAuthorize(credentialData as ICredentialsResponse) as string; } catch (error) { this.$showError(error, 'OAuth Authorization Error', 'Error generating authorization URL:'); return; @@ -268,7 +277,17 @@ export default mixins( // Set some kind of data that status changes. // As data does not get displayed directly it does not matter what data. - credentialData.data.oauthTokenData = {}; + if (this.credentialData === null) { + // Are new credentials so did not get send via "credentialData" + this.credentialDataTemp = credentialData as ICredentialsDecryptedResponse; + Vue.set(this.credentialDataTemp.data, 'oauthTokenData', {}); + } else { + // Credentials did already exist so can be set directly + Vue.set(this.credentialData.data, 'oauthTokenData', {}); + } + + // Save that OAuth got authorized locally + this.$store.commit('updateCredentials', this.credentialDataDynamic); // Close the window if (oauthPopup) { @@ -276,7 +295,7 @@ export default mixins( } if (newCredentials === true) { - this.$emit('credentialsCreated', credentialData); + this.$emit('credentialsCreated', {data: credentialData, options: { closeDialog: false }}); } this.$showMessage({ @@ -292,13 +311,13 @@ export default mixins( window.addEventListener('message', receiveMessage, false); }, - async updateCredentials () { + async updateCredentials (closeDialog: boolean): Promise { const nodesAccess: ICredentialNodeAccess[] = []; const addedNodeTypes: string[] = []; // Add Node-type which already had access to keep the original added date let nodeAccessData: ICredentialNodeAccess; - for (nodeAccessData of (this.credentialData as ICredentialsDecryptedResponse).nodesAccess) { + for (nodeAccessData of (this.credentialDataDynamic as ICredentialsDecryptedResponse).nodesAccess) { if (this.nodesAccess.includes((nodeAccessData.nodeType))) { nodesAccess.push(nodeAccessData); addedNodeTypes.push(nodeAccessData.nodeType); @@ -323,7 +342,7 @@ export default mixins( let result; try { - result = await this.restApi().updateCredentials((this.credentialData as ICredentialsDecryptedResponse).id as string, newCredentials); + result = await this.restApi().updateCredentials((this.credentialDataDynamic as ICredentialsDecryptedResponse).id as string, newCredentials); } catch (error) { this.$showError(error, 'Problem Updating Credentials', 'There was a problem updating the credentials:'); return; @@ -336,7 +355,9 @@ export default mixins( // which have now a different name this.updateNodesCredentialsIssues(); - this.$emit('credentialsUpdated', result); + this.$emit('credentialsUpdated', {data: result, options: { closeDialog }}); + + return result; }, init () { if (this.credentialData) { diff --git a/packages/editor-ui/src/components/NodeCredentials.vue b/packages/editor-ui/src/components/NodeCredentials.vue index 213f142840..8284c272e1 100644 --- a/packages/editor-ui/src/components/NodeCredentials.vue +++ b/packages/editor-ui/src/components/NodeCredentials.vue @@ -45,6 +45,7 @@ import Vue from 'vue'; import { restApi } from '@/components/mixins/restApi'; import { + ICredentialsCreatedEvent, ICredentialsResponse, INodeUi, INodeUpdatePropertiesInformation, @@ -134,21 +135,23 @@ export default mixins( closeCredentialNewDialog () { this.credentialNewDialogVisible = false; }, - async credentialsCreated (data: ICredentialsResponse) { - await this.credentialsUpdated(data); + async credentialsCreated (eventData: ICredentialsCreatedEvent) { + await this.credentialsUpdated(eventData.data as ICredentialsResponse); }, - credentialsUpdated (data: ICredentialsResponse) { - if (!this.credentialTypesNode.includes(data.type)) { + credentialsUpdated (eventData: ICredentialsCreatedEvent) { + if (!this.credentialTypesNode.includes(eventData.data.type)) { return; } this.init(); - Vue.set(this.credentials, data.type, data.name); + Vue.set(this.credentials, eventData.data.type, eventData.data.name); // Makes sure that it does also get set correctly on the node not just the UI - this.credentialSelected(data.type); + this.credentialSelected(eventData.data.type); - this.closeCredentialNewDialog(); + if (eventData.options.closeDialog === true) { + this.closeCredentialNewDialog(); + } }, credentialInputWrapperStyle (credentialType: string) { let deductWidth = 0; diff --git a/packages/nodes-base/credentials/OAuth2Api.credentials.ts b/packages/nodes-base/credentials/OAuth2Api.credentials.ts index 99e8e025de..efa5127e6e 100644 --- a/packages/nodes-base/credentials/OAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/OAuth2Api.credentials.ts @@ -45,5 +45,13 @@ export class OAuth2Api implements ICredentialType { type: 'string' as NodePropertyTypes, default: '', }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'string' as NodePropertyTypes, + default: '', + description: 'For some services additional query parameters have to be set which can be defined here.', + placeholder: 'access_type=offline', + }, ]; } From 15e92ca49450d55da991eba6dd4cf6d8797ded3e Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sat, 8 Feb 2020 18:46:41 -0800 Subject: [PATCH 005/165] :zap: Fix some minor issues --- .../editor-ui/src/components/CredentialsInput.vue | 11 +++++++---- packages/editor-ui/src/components/NodeCredentials.vue | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/editor-ui/src/components/CredentialsInput.vue b/packages/editor-ui/src/components/CredentialsInput.vue index bc5b6225d3..708f72631e 100644 --- a/packages/editor-ui/src/components/CredentialsInput.vue +++ b/packages/editor-ui/src/components/CredentialsInput.vue @@ -255,6 +255,9 @@ export default mixins( } else { // Exists already but got maybe changed. So save first credentialData = await this.updateCredentials(false) as ICredentialsDecryptedResponse; + if (credentialData === null) { + return; + } } try { @@ -279,8 +282,8 @@ export default mixins( // As data does not get displayed directly it does not matter what data. if (this.credentialData === null) { // Are new credentials so did not get send via "credentialData" - this.credentialDataTemp = credentialData as ICredentialsDecryptedResponse; - Vue.set(this.credentialDataTemp.data, 'oauthTokenData', {}); + Vue.set(this, 'credentialDataTemp', credentialData); + Vue.set(this.credentialDataTemp!.data!, 'oauthTokenData', {}); } else { // Credentials did already exist so can be set directly Vue.set(this.credentialData.data, 'oauthTokenData', {}); @@ -311,7 +314,7 @@ export default mixins( window.addEventListener('message', receiveMessage, false); }, - async updateCredentials (closeDialog: boolean): Promise { + async updateCredentials (closeDialog: boolean): Promise { const nodesAccess: ICredentialNodeAccess[] = []; const addedNodeTypes: string[] = []; @@ -345,7 +348,7 @@ export default mixins( result = await this.restApi().updateCredentials((this.credentialDataDynamic as ICredentialsDecryptedResponse).id as string, newCredentials); } catch (error) { this.$showError(error, 'Problem Updating Credentials', 'There was a problem updating the credentials:'); - return; + return null; } // Update also in local store diff --git a/packages/editor-ui/src/components/NodeCredentials.vue b/packages/editor-ui/src/components/NodeCredentials.vue index 8284c272e1..1eb9cf7d3b 100644 --- a/packages/editor-ui/src/components/NodeCredentials.vue +++ b/packages/editor-ui/src/components/NodeCredentials.vue @@ -136,7 +136,7 @@ export default mixins( this.credentialNewDialogVisible = false; }, async credentialsCreated (eventData: ICredentialsCreatedEvent) { - await this.credentialsUpdated(eventData.data as ICredentialsResponse); + await this.credentialsUpdated(eventData); }, credentialsUpdated (eventData: ICredentialsCreatedEvent) { if (!this.credentialTypesNode.includes(eventData.data.type)) { From 928bf4dc685f58c3c049cf419c309d7d045a0b3b Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sat, 8 Feb 2020 21:25:46 -0800 Subject: [PATCH 006/165] :zap: Fix OAuth-Token refresh --- packages/cli/src/CredentialsHelper.ts | 13 ++++++++++++- packages/cli/src/Server.ts | 2 +- packages/core/src/NodeExecuteFunctions.ts | 19 +++++++++++-------- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/CredentialsHelper.ts b/packages/cli/src/CredentialsHelper.ts index a5dee733f2..d9ee2b6baf 100644 --- a/packages/cli/src/CredentialsHelper.ts +++ b/packages/cli/src/CredentialsHelper.ts @@ -66,6 +66,12 @@ export class CredentialsHelper extends ICredentialsHelper { async updateCredentials(name: string, type: string, data: ICredentialDataDecryptedObject): Promise { const credentials = await this.getCredentials(name, type); + if (Db.collections!.Credentials === null) { + // The first time executeWorkflow gets called the Database has + // to get initialized first + await Db.init(); + } + credentials.setData(data, this.encryptionKey); const newCredentialsData = credentials.getDataToSave() as ICredentialsDb; @@ -75,7 +81,12 @@ export class CredentialsHelper extends ICredentialsHelper { // TODO: also add user automatically depending on who is logged in, if anybody is logged in // Save the credentials in DB - await Db.collections.Credentials!.save(newCredentialsData); + const findQuery = { + name, + type, + }; + + await Db.collections.Credentials!.update(findQuery, newCredentialsData); } } diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 1d82c4d807..365bb1765b 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -969,7 +969,7 @@ class App { return ResponseHelper.sendErrorResponse(res, errorResponse); } - savedCredentialsData.oauthTokenData = JSON.stringify(oauthToken.data); + savedCredentialsData.oauthTokenData = oauthToken.data; _.unset(savedCredentialsData, 'csrfSecret'); credentials.setData(savedCredentialsData, encryptionKey); diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 2c13b2c389..69dd2c92e1 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -36,8 +36,8 @@ import { } from 'n8n-workflow'; import * as clientOAuth2 from 'client-oauth2'; -import { get, unset } from 'lodash'; -import * as express from "express"; +import { get } from 'lodash'; +import * as express from 'express'; import * as path from 'path'; import { OptionsWithUri } from 'request'; import * as requestPromise from 'request-promise-native'; @@ -126,8 +126,13 @@ export function requestOAuth(this: IAllExecuteFunctions, credentialsType: string throw new Error('OAuth credentials not connected!'); } - const oAuthClient = new clientOAuth2({}); - const oauthTokenData = JSON.parse(credentials.oauthTokenData as string); + const oAuthClient = new clientOAuth2({ + clientId: credentials.clientId as string, + clientSecret: credentials.clientSecret as string, + accessTokenUri: credentials.accessTokenUrl as string, + }); + + const oauthTokenData = credentials.oauthTokenData as clientOAuth2.Data; const token = oAuthClient.createToken(oauthTokenData); // Signs the request by adding authorization headers or query parameters depending @@ -142,9 +147,7 @@ export function requestOAuth(this: IAllExecuteFunctions, credentialsType: string // Token is probably not valid anymore. So try refresh it. const newToken = await token.refresh(); - const newCredentialsData = newToken.data; - unset(newCredentialsData, 'csrfSecret'); - unset(newCredentialsData, 'oauthTokenData'); + credentials.oauthTokenData = newToken.data; // Find the name of the credentials if (!node.credentials || !node.credentials[credentialsType]) { @@ -153,7 +156,7 @@ export function requestOAuth(this: IAllExecuteFunctions, credentialsType: string const name = node.credentials[credentialsType]; // Save the refreshed token - await additionalData.credentialsHelper.updateCredentials(name, credentialsType, newCredentialsData); + await additionalData.credentialsHelper.updateCredentials(name, credentialsType, credentials); // Make the request again with the new token const newRequestOptions = newToken.sign(requestOptions as clientOAuth2.RequestObject); From 96741460e38d6b8f5c2a4d90b4cf02af44768265 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sun, 9 Feb 2020 10:11:15 -0500 Subject: [PATCH 007/165] :sparkles: Github OAuth support --- packages/cli/src/Server.ts | 2 +- .../nodes/Github/GenericFunctions.ts | 23 ++++++---- .../nodes-base/nodes/Github/Github.node.ts | 43 ++++++++++++++++--- 3 files changed, 52 insertions(+), 16 deletions(-) diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 365bb1765b..560636029b 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -979,7 +979,7 @@ class App { // Save the credentials in DB await Db.collections.Credentials!.update(state.cid, newCredentialsData); - res.sendFile(pathResolve('templates/oauth-callback.html')); + res.sendFile(pathResolve('../templates/oauth-callback.html')); }); diff --git a/packages/nodes-base/nodes/Github/GenericFunctions.ts b/packages/nodes-base/nodes/Github/GenericFunctions.ts index 0b4c8452ab..b851eefe50 100644 --- a/packages/nodes-base/nodes/Github/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Github/GenericFunctions.ts @@ -17,17 +17,24 @@ import { * @returns {Promise} */ export async function githubApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: object, query?: object): Promise { // tslint:disable-line:no-any - const credentials = this.getCredentials('githubApi'); - if (credentials === undefined) { - throw new Error('No credentials got returned!'); + const githubApiCredentials = this.getCredentials('githubApi'); + const oAuth2ApiCrendetials = this.getCredentials('oAuth2Api'); + let headers = {} + if (githubApiCredentials !== undefined) { + headers = { + Authorization: `token ${githubApiCredentials.accessToken}`, + 'User-Agent': githubApiCredentials.user, + }; + } else { + const { access_token } = oAuth2ApiCrendetials!.oauthTokenData as IDataObject; + headers = { + Authorization: `token ${access_token}`, + 'User-Agent': 'Node js', + }; } - const options = { method, - headers: { - 'Authorization': `token ${credentials.accessToken}`, - 'User-Agent': credentials.user, - }, + headers, body, qs: query, uri: `https://api.github.com${endpoint}`, diff --git a/packages/nodes-base/nodes/Github/Github.node.ts b/packages/nodes-base/nodes/Github/Github.node.ts index 6174a1bd47..a8588b0cdd 100644 --- a/packages/nodes-base/nodes/Github/Github.node.ts +++ b/packages/nodes-base/nodes/Github/Github.node.ts @@ -33,9 +33,44 @@ export class Github implements INodeType { { name: 'githubApi', required: true, - } + displayOptions: { + show: { + authentication: [ + 'accessToken', + ], + }, + }, + }, + { + name: 'oAuth2Api', + required: true, + displayOptions: { + show: { + authentication: [ + 'oauth2', + ], + }, + }, + }, ], properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'Access Token', + value: 'accessToken', + }, + { + name: 'OAuth2', + value: 'oauth2', + }, + ], + default: 'accessToken', + description: 'The resource to operate on.', + }, { displayName: 'Resource', name: 'resource', @@ -1088,12 +1123,6 @@ export class Github implements INodeType { const items = this.getInputData(); const returnData: IDataObject[] = []; - const credentials = this.getCredentials('githubApi'); - - if (credentials === undefined) { - throw new Error('No credentials got returned!'); - } - // Operations which overwrite the returned data const overwriteDataOperations = [ 'file:create', From 6bff3dc199f66eee46c296f79560cdea99425479 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 9 Feb 2020 13:33:40 -0800 Subject: [PATCH 008/165] :zap: Add parameter type "hidden" --- .../editor-ui/src/components/CredentialsEdit.vue | 15 ++++++++++++++- .../src/components/ParameterInputList.vue | 4 ++++ .../credentials/GithubOAuth2Api.credentials.ts | 8 +++++++- packages/workflow/src/Interfaces.ts | 2 +- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/editor-ui/src/components/CredentialsEdit.vue b/packages/editor-ui/src/components/CredentialsEdit.vue index cd3b6c1f74..782e615a33 100644 --- a/packages/editor-ui/src/components/CredentialsEdit.vue +++ b/packages/editor-ui/src/components/CredentialsEdit.vue @@ -34,6 +34,7 @@ import CredentialsInput from '@/components/CredentialsInput.vue'; import { ICredentialsCreatedEvent, ICredentialsDecryptedResponse, + INodeProperties, } from '@/Interface'; import { @@ -181,9 +182,21 @@ export default mixins( // Credentials extends another one. So get the properties of the one it // extends and add them. credentialData = JSON.parse(JSON.stringify(credentialData)); + let existingIndex: number; for (const credentialTypeName of credentialData.extends) { const data = this.$store.getters.credentialType(credentialTypeName); - credentialData.properties.push.apply(credentialData.properties, data.properties); + + for (const property of data.properties) { + existingIndex = credentialData.properties.findIndex(element => element.name === property.name); + + if (existingIndex === -1) { + // Property does not exist yet, so add + credentialData.properties.push(property); + } else { + // Property exists already, so overwrite + credentialData.properties[existingIndex] = property; + } + } } return credentialData; diff --git a/packages/editor-ui/src/components/ParameterInputList.vue b/packages/editor-ui/src/components/ParameterInputList.vue index 4bad3a3417..939452e3a0 100644 --- a/packages/editor-ui/src/components/ParameterInputList.vue +++ b/packages/editor-ui/src/components/ParameterInputList.vue @@ -149,6 +149,10 @@ export default mixins( this.$emit('valueChanged', parameterData); }, displayNodeParameter (parameter: INodeProperties): boolean { + if (parameter.type === 'hidden') { + return false; + } + if (parameter.displayOptions === undefined) { // If it is not defined no need to do a proper check return true; diff --git a/packages/nodes-base/credentials/GithubOAuth2Api.credentials.ts b/packages/nodes-base/credentials/GithubOAuth2Api.credentials.ts index 74df45d8a6..0bf12444f2 100644 --- a/packages/nodes-base/credentials/GithubOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/GithubOAuth2Api.credentials.ts @@ -1,15 +1,21 @@ import { ICredentialType, + NodePropertyTypes, } from 'n8n-workflow'; export class GithubOAuth2Api implements ICredentialType { name = 'githubOAuth2Api'; - // name = 'oAuth2Api/githubOAuth2Api'; extends = [ 'oAuth2Api', ]; displayName = 'Github OAuth2 API'; properties = [ + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: '', + }, ]; } diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 6b8f98fe04..7052dd2431 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -364,7 +364,7 @@ export interface INodeParameters { } -export type NodePropertyTypes = 'boolean' | 'collection' | 'color' | 'dateTime' | 'fixedCollection' | 'json' | 'multiOptions' | 'number' | 'options' | 'string'; +export type NodePropertyTypes = 'boolean' | 'collection' | 'color' | 'dateTime' | 'fixedCollection' | 'hidden' | 'json' | 'multiOptions' | 'number' | 'options' | 'string'; export type EditorTypes = 'code'; From c1574a73ac0b2a6cdea7ca60869012096fca4229 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 9 Feb 2020 15:39:14 -0800 Subject: [PATCH 009/165] :zap: Improved Github Oauth support and generic Oauth functionality --- packages/cli/src/Server.ts | 2 +- .../src/components/CredentialsEdit.vue | 59 +++++++++++++------ .../src/components/CredentialsInput.vue | 34 +++++++---- .../GithubOAuth2Api.credentials.ts | 20 +++++++ .../nodes/Github/GenericFunctions.ts | 36 +++++------ .../nodes-base/nodes/Github/Github.node.ts | 2 +- 6 files changed, 104 insertions(+), 49 deletions(-) diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 560636029b..fdc9e94e4e 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -979,7 +979,7 @@ class App { // Save the credentials in DB await Db.collections.Credentials!.update(state.cid, newCredentialsData); - res.sendFile(pathResolve('../templates/oauth-callback.html')); + res.sendFile(pathResolve(__dirname, '../../templates/oauth-callback.html')); }); diff --git a/packages/editor-ui/src/components/CredentialsEdit.vue b/packages/editor-ui/src/components/CredentialsEdit.vue index 782e615a33..187953ef52 100644 --- a/packages/editor-ui/src/components/CredentialsEdit.vue +++ b/packages/editor-ui/src/components/CredentialsEdit.vue @@ -34,11 +34,11 @@ import CredentialsInput from '@/components/CredentialsInput.vue'; import { ICredentialsCreatedEvent, ICredentialsDecryptedResponse, - INodeProperties, } from '@/Interface'; import { ICredentialType, + INodeProperties, } from 'n8n-workflow'; import mixins from 'vue-typed-mixins'; @@ -172,6 +172,42 @@ export default mixins( }, }, methods: { + mergeCredentialProperties (mainProperties: INodeProperties[], addProperties: INodeProperties[]): void { + let existingIndex: number; + for (const property of addProperties) { + existingIndex = mainProperties.findIndex(element => element.name === property.name); + + if (existingIndex === -1) { + // Property does not exist yet, so add + mainProperties.push(property); + } else { + // Property exists already, so overwrite + mainProperties[existingIndex] = property; + } + } + }, + getCredentialProperties (name: string): INodeProperties[] { + let credentialsData = this.$store.getters.credentialType(name); + + if (credentialsData === null) { + throw new Error(`Could not find credentials of type: ${name}`); + } + + if (credentialsData.extends === undefined) { + return credentialsData.properties; + } + + const combineProperties = [] as INodeProperties[]; + for (const credentialsTypeName of credentialsData.extends) { + const mergeCredentialProperties = this.getCredentialProperties(credentialsTypeName); + this.mergeCredentialProperties(combineProperties, mergeCredentialProperties); + } + + // The properties defined on the parent credentials take presidence + this.mergeCredentialProperties(combineProperties, credentialsData.properties); + + return combineProperties; + }, getCredentialTypeData (name: string): ICredentialType | null { let credentialData = this.$store.getters.credentialType(name); @@ -179,25 +215,14 @@ export default mixins( return credentialData; } + // TODO: The credential-extend-resolve-logic is currently not needed in the backend + // as the whole credential data gets saved with the defaults. That logic should, + // however, probably also get improved in the future. + // Credentials extends another one. So get the properties of the one it // extends and add them. credentialData = JSON.parse(JSON.stringify(credentialData)); - let existingIndex: number; - for (const credentialTypeName of credentialData.extends) { - const data = this.$store.getters.credentialType(credentialTypeName); - - for (const property of data.properties) { - existingIndex = credentialData.properties.findIndex(element => element.name === property.name); - - if (existingIndex === -1) { - // Property does not exist yet, so add - credentialData.properties.push(property); - } else { - // Property exists already, so overwrite - credentialData.properties[existingIndex] = property; - } - } - } + credentialData.properties = this.getCredentialProperties(credentialData.name); return credentialData; }, diff --git a/packages/editor-ui/src/components/CredentialsInput.vue b/packages/editor-ui/src/components/CredentialsInput.vue index 708f72631e..76aa1c6672 100644 --- a/packages/editor-ui/src/components/CredentialsInput.vue +++ b/packages/editor-ui/src/components/CredentialsInput.vue @@ -41,18 +41,21 @@
- - - {{parameter.displayName}}: - -
- -
-
- - - -
+
+ + + {{parameter.displayName}}: + +
+ +
+
+ + + +
+
+ @@ -239,6 +242,13 @@ export default mixins( return result; }, + displayNodeParameter (parameter: INodeProperties): boolean { + if (parameter.type === 'hidden') { + return false; + } + + return true; + }, async oAuth2CredentialAuthorize () { let url; diff --git a/packages/nodes-base/credentials/GithubOAuth2Api.credentials.ts b/packages/nodes-base/credentials/GithubOAuth2Api.credentials.ts index 0bf12444f2..9466ce447b 100644 --- a/packages/nodes-base/credentials/GithubOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/GithubOAuth2Api.credentials.ts @@ -11,6 +11,26 @@ export class GithubOAuth2Api implements ICredentialType { ]; displayName = 'Github OAuth2 API'; properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://github.com/login/oauth/authorize', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://github.com/login/oauth/access_token', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: 'repo,admin:repo_hook,admin:org,admin:org_hook,gist,notifications,user,write:packages,read:packages,delete:packages,worfklow', + }, { displayName: 'Auth URI Query Parameters', name: 'authQueryParameters', diff --git a/packages/nodes-base/nodes/Github/GenericFunctions.ts b/packages/nodes-base/nodes/Github/GenericFunctions.ts index b851eefe50..97cc69aba4 100644 --- a/packages/nodes-base/nodes/Github/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Github/GenericFunctions.ts @@ -1,3 +1,5 @@ +import { OptionsWithUri } from 'request'; + import { IExecuteFunctions, IHookFunctions, @@ -17,24 +19,13 @@ import { * @returns {Promise} */ export async function githubApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: object, query?: object): Promise { // tslint:disable-line:no-any - const githubApiCredentials = this.getCredentials('githubApi'); - const oAuth2ApiCrendetials = this.getCredentials('oAuth2Api'); - let headers = {} - if (githubApiCredentials !== undefined) { - headers = { - Authorization: `token ${githubApiCredentials.accessToken}`, - 'User-Agent': githubApiCredentials.user, - }; - } else { - const { access_token } = oAuth2ApiCrendetials!.oauthTokenData as IDataObject; - headers = { - Authorization: `token ${access_token}`, - 'User-Agent': 'Node js', - }; - } - const options = { + const authenticationMethod = this.getNodeParameter('authentication', 0, 'accessToken') as string; + + const options: OptionsWithUri = { method, - headers, + headers: { + 'User-Agent': 'n8n', + }, body, qs: query, uri: `https://api.github.com${endpoint}`, @@ -42,7 +33,16 @@ export async function githubApiRequest(this: IHookFunctions | IExecuteFunctions, }; try { - return await this.helpers.request(options); + if (authenticationMethod === 'accessToken') { + const credentials = this.getCredentials('githubApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + options.headers!.Authorization = `token ${credentials.accessToken}`; + return await this.helpers.request(options); + } else { + return await this.helpers.requestOAuth.call(this, 'githubOAuth2Api', options); + } } catch (error) { if (error.statusCode === 401) { // Return a clear error diff --git a/packages/nodes-base/nodes/Github/Github.node.ts b/packages/nodes-base/nodes/Github/Github.node.ts index a8588b0cdd..81befb410c 100644 --- a/packages/nodes-base/nodes/Github/Github.node.ts +++ b/packages/nodes-base/nodes/Github/Github.node.ts @@ -42,7 +42,7 @@ export class Github implements INodeType { }, }, { - name: 'oAuth2Api', + name: 'githubOAuth2Api', required: true, displayOptions: { show: { From 2eaeb4f4c29b53f46b98987cb1d51a82c5927910 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Mon, 10 Feb 2020 15:55:28 -0500 Subject: [PATCH 010/165] :sparkles: salesforce node --- .../SalesforceOAuth2Api.credentials.ts | 40 + .../nodes/Salesforce/AccountDescription.ts | 700 ++++++ .../nodes/Salesforce/AccountInterface.ts | 26 + .../nodes/Salesforce/AttachmentDescription.ts | 347 +++ .../nodes/Salesforce/AttachmentInterface.ts | 10 + .../Salesforce/CampaignMemberInterface.ts | 7 + .../nodes/Salesforce/CaseDescription.ts | 563 +++++ .../nodes/Salesforce/CaseInterface.ts | 25 + .../nodes/Salesforce/ContactDescription.ts | 835 ++++++++ .../nodes/Salesforce/ContactInterface.ts | 34 + .../nodes/Salesforce/GenericFunctions.ts | 50 + .../nodes/Salesforce/LeadDescription.ts | 762 +++++++ .../nodes/Salesforce/LeadInterface.ts | 26 + .../nodes/Salesforce/NoteInterface.ts | 8 + .../Salesforce/OpportunityDescription.ts | 625 ++++++ .../nodes/Salesforce/OpportunityInterface.ts | 17 + .../nodes/Salesforce/Salesforce.node.ts | 1893 +++++++++++++++++ .../nodes/Salesforce/TaskDescription.ts | 810 +++++++ .../nodes/Salesforce/TaskInterface.ts | 27 + .../nodes/Salesforce/salesforce.png | Bin 0 -> 3210 bytes packages/nodes-base/package.json | 2 + 21 files changed, 6807 insertions(+) create mode 100644 packages/nodes-base/credentials/SalesforceOAuth2Api.credentials.ts create mode 100644 packages/nodes-base/nodes/Salesforce/AccountDescription.ts create mode 100644 packages/nodes-base/nodes/Salesforce/AccountInterface.ts create mode 100644 packages/nodes-base/nodes/Salesforce/AttachmentDescription.ts create mode 100644 packages/nodes-base/nodes/Salesforce/AttachmentInterface.ts create mode 100644 packages/nodes-base/nodes/Salesforce/CampaignMemberInterface.ts create mode 100644 packages/nodes-base/nodes/Salesforce/CaseDescription.ts create mode 100644 packages/nodes-base/nodes/Salesforce/CaseInterface.ts create mode 100644 packages/nodes-base/nodes/Salesforce/ContactDescription.ts create mode 100644 packages/nodes-base/nodes/Salesforce/ContactInterface.ts create mode 100644 packages/nodes-base/nodes/Salesforce/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Salesforce/LeadDescription.ts create mode 100644 packages/nodes-base/nodes/Salesforce/LeadInterface.ts create mode 100644 packages/nodes-base/nodes/Salesforce/NoteInterface.ts create mode 100644 packages/nodes-base/nodes/Salesforce/OpportunityDescription.ts create mode 100644 packages/nodes-base/nodes/Salesforce/OpportunityInterface.ts create mode 100644 packages/nodes-base/nodes/Salesforce/Salesforce.node.ts create mode 100644 packages/nodes-base/nodes/Salesforce/TaskDescription.ts create mode 100644 packages/nodes-base/nodes/Salesforce/TaskInterface.ts create mode 100644 packages/nodes-base/nodes/Salesforce/salesforce.png diff --git a/packages/nodes-base/credentials/SalesforceOAuth2Api.credentials.ts b/packages/nodes-base/credentials/SalesforceOAuth2Api.credentials.ts new file mode 100644 index 0000000000..23739bf118 --- /dev/null +++ b/packages/nodes-base/credentials/SalesforceOAuth2Api.credentials.ts @@ -0,0 +1,40 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class SalesforceOAuth2Api implements ICredentialType { + name = 'salesforceOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Salesforce OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://login.salesforce.com/services/oauth2/authorize', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'string' as NodePropertyTypes, + default: 'https://yourcompany.salesforce.com/services/oauth2/token', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: 'full', + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Salesforce/AccountDescription.ts b/packages/nodes-base/nodes/Salesforce/AccountDescription.ts new file mode 100644 index 0000000000..0c9d336065 --- /dev/null +++ b/packages/nodes-base/nodes/Salesforce/AccountDescription.ts @@ -0,0 +1,700 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const accountOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'account', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create an account', + }, + { + name: 'Update', + value: 'update', + description: 'Update an account', + }, + { + name: 'Get', + value: 'get', + description: 'Get an account', + }, + { + name: 'Get Summary', + value: 'getSummary', + description: `Returns an overview of account's metadata.`, + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all accounts', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an account', + }, + { + name: 'Add Note', + value: 'addNote', + description: 'Add note to an account', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const accountFields = [ + +/* -------------------------------------------------------------------------- */ +/* account:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Name', + name: 'name', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'create', + ] + }, + }, + description: 'Name of the account. Maximum size is 255 characters.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Fax', + name: 'fax', + type: 'string', + default: '', + description: 'Fax number for the account.', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getAccountTypes', + }, + description: 'Type of account', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: 'Phone number for the account.', + }, + { + displayName: 'Jigsaw', + name: 'jigsaw', + type: 'string', + default: '', + description: 'references the ID of a company in Data.com', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'The owner of the account.', + }, + { + displayName: 'SicDesc', + name: 'sicDesc', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + description: 'A brief description of an organization’s line of business, based on its SIC code.', + }, + { + displayName: 'Website', + name: 'website', + type: 'string', + default: '', + description: 'The website of this account. Maximum of 255 characters.', + }, + { + displayName: 'Industry', + name: 'industry', + type: 'string', + default: '', + description: 'The website of this account. Maximum of 255 characters.', + }, + { + displayName: 'Parent Id', + name: 'parentId', + type: 'string', + default: '', + description: 'ID of the parent object, if any.', + }, + { + displayName: 'Billing City', + name: 'billingCity', + type: 'string', + default: '', + description: 'Details for the billing address of this account. Maximum size is 40 characters.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Text description of the account. Limited to 32,000 KB.', + }, + { + displayName: 'Billing State', + name: 'billingState', + type: 'string', + default: '', + description: 'Details for the billing address of this account. Maximum size is 80 characters.', + }, + { + displayName: 'Shipping City', + name: 'shippingCity', + type: 'string', + default: '', + description: 'Details of the shipping address for this account. City maximum size is 40 characters', + }, + { + displayName: 'Account Source', + name: 'accountSource', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getAccountSources', + }, + default: '', + description: 'The source of the account record', + }, + { + displayName: 'Annual Revenue', + name: 'annualRevenue', + type: 'number', + typeOptions: { + numberPrecision: 2, + }, + default: '', + description: 'Estimated annual revenue of the account.', + }, + { + displayName: 'Billing Street', + name: 'billingStreet', + type: 'string', + default: '', + description: 'Street address for the billing address of this account.', + }, + { + displayName: 'Shipping State', + name: 'shippingState', + type: 'string', + default: '', + description: 'Details of the shipping address for this account. State maximum size is 80 characters.', + }, + { + displayName: 'Billing Country', + name: 'billingCountry', + type: 'string', + default: '', + description: 'Details for the billing address of this account. Maximum size is 80 characters.', + }, + { + displayName: 'Shipping Street', + name: 'shippingStreet', + type: 'string', + default: '', + description: 'The street address of the shipping address for this account. Maximum of 255 characters.', + }, + { + displayName: 'Shipping Country', + name: 'shippingCountry', + type: 'string', + default: '', + description: 'Details of the shipping address for this account. Country maximum size is 80 characters.', + }, + { + displayName: 'Billing Postal Code', + name: 'billingPostalCode', + type: 'string', + default: '', + description: 'Details for the billing address of this account. Maximum size is 20 characters.', + }, + { + displayName: 'Number Of Employees', + name: 'numberOfEmployees', + type: 'integer', + default: '', + description: 'Number of employees', + }, + { + displayName: 'Shipping Postal Code', + name: 'shippingPostalCode', + type: 'string', + default: '', + description: 'Details of the shipping address for this account. Postal code maximum size is 20 characters.', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* account:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Account ID', + name: 'accountId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'update', + ] + }, + }, + description: 'Id of account that needs to be fetched', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Name of the account. Maximum size is 255 characters.', + }, + { + displayName: 'Fax', + name: 'fax', + type: 'string', + default: '', + description: 'Fax number for the account.', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getAccountTypes', + }, + description: 'Type of account', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: 'Phone number for the account.', + }, + { + displayName: 'Jigsaw', + name: 'jigsaw', + type: 'string', + default: '', + description: 'references the ID of a company in Data.com', + }, + { + displayName: 'Owner', + name: 'ownerId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'The owner of the account.', + }, + { + displayName: 'SicDesc', + name: 'sicDesc', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + description: 'A brief description of an organization’s line of business, based on its SIC code.', + }, + { + displayName: 'Website', + name: 'website', + type: 'string', + default: '', + description: 'The website of this account. Maximum of 255 characters.', + }, + { + displayName: 'Industry', + name: 'industry', + type: 'string', + default: '', + description: 'The website of this account. Maximum of 255 characters.', + }, + { + displayName: 'Parent Id', + name: 'parentId', + type: 'string', + default: '', + description: 'ID of the parent object, if any.', + }, + { + displayName: 'Billing City', + name: 'billingCity', + type: 'string', + default: '', + description: 'Details for the billing address of this account. Maximum size is 40 characters.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Text description of the account. Limited to 32,000 KB.', + }, + { + displayName: 'Billing State', + name: 'billingState', + type: 'string', + default: '', + description: 'Details for the billing address of this account. Maximum size is 80 characters.', + }, + { + displayName: 'Shipping City', + name: 'shippingCity', + type: 'string', + default: '', + description: 'Details of the shipping address for this account. City maximum size is 40 characters', + }, + { + displayName: 'Account Source', + name: 'accountSource', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getAccountSources', + }, + default: '', + description: 'The source of the account record', + }, + { + displayName: 'Annual Revenue', + name: 'annualRevenue', + type: 'number', + typeOptions: { + numberPrecision: 2, + }, + default: '', + description: 'Estimated annual revenue of the account.', + }, + { + displayName: 'Billing Street', + name: 'billingStreet', + type: 'string', + default: '', + description: 'Street address for the billing address of this account.', + }, + { + displayName: 'Shipping State', + name: 'shippingState', + type: 'string', + default: '', + description: 'Details of the shipping address for this account. State maximum size is 80 characters.', + }, + { + displayName: 'Billing Country', + name: 'billingCountry', + type: 'string', + default: '', + description: 'Details for the billing address of this account. Maximum size is 80 characters.', + }, + { + displayName: 'Shipping Street', + name: 'shippingStreet', + type: 'string', + default: '', + description: 'The street address of the shipping address for this account. Maximum of 255 characters.', + }, + { + displayName: 'Shipping Country', + name: 'shippingCountry', + type: 'string', + default: '', + description: 'Details of the shipping address for this account. Country maximum size is 80 characters.', + }, + { + displayName: 'Billing Postal Code', + name: 'billingPostalCode', + type: 'string', + default: '', + description: 'Details for the billing address of this account. Maximum size is 20 characters.', + }, + { + displayName: 'Number Of Employees', + name: 'numberOfEmployees', + type: 'integer', + default: '', + description: 'Number of employees', + }, + { + displayName: 'Shipping Postal Code', + name: 'shippingPostalCode', + type: 'string', + default: '', + description: 'Details of the shipping address for this account. Postal code maximum size is 20 characters.', + }, + ], + }, + +/* -------------------------------------------------------------------------- */ +/* account:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Account ID', + name: 'accountId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'get', + ] + }, + }, + description: 'Id of account that needs to be fetched', + }, +/* -------------------------------------------------------------------------- */ +/* account:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Account ID', + name: 'accountId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'delete', + ] + }, + }, + description: 'Id of account that needs to be fetched', + }, +/* -------------------------------------------------------------------------- */ +/* account:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'Fields to include separated by ,', + }, + ] + }, +/* -------------------------------------------------------------------------- */ +/* account:addNote */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Account ID', + name: 'accountId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'addNote', + ] + }, + }, + description: 'Id of account that needs to be fetched', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'addNote', + ] + }, + }, + description: 'Title of the note.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'addNote', + ], + }, + }, + options: [ + { + displayName: 'Body', + name: 'body', + type: 'string', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + description: 'Body of the note. Limited to 32 KB.', + }, + { + displayName: 'Owner', + name: 'ownerId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'ID of the user who owns the note.', + }, + { + displayName: 'Is Private', + name: 'isPrivate', + type: 'boolean', + default: false, + description: 'If true, only the note owner or a user with the “Modify All Data” permission can view the note or query it via the API', + }, + ] + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Salesforce/AccountInterface.ts b/packages/nodes-base/nodes/Salesforce/AccountInterface.ts new file mode 100644 index 0000000000..c42b485105 --- /dev/null +++ b/packages/nodes-base/nodes/Salesforce/AccountInterface.ts @@ -0,0 +1,26 @@ +export interface IAccount { + Name?: string; + Fax?: string; + Type?: string; + Phone?: string; + Jigsaw?: string; + OwnerId?: string; + SicDesc?: string; + Website?: string; + Industry?: string; + ParentId?: string; + BillingCity?: string; + Description?: string; + BillingState?: string; + ShippingStreet?: string; + ShippingCity?:string; + AccountSource?: string; + AnnualRevenue?: number; + BillingStreet?: string; + ShippingState?: string; + BillingCountry?: string; + ShippingCountry?: string; + BillingPostalCode?: string; + NumberOfEmployees?: string; + ShippingPostalCode?: string; +} diff --git a/packages/nodes-base/nodes/Salesforce/AttachmentDescription.ts b/packages/nodes-base/nodes/Salesforce/AttachmentDescription.ts new file mode 100644 index 0000000000..60158a7e52 --- /dev/null +++ b/packages/nodes-base/nodes/Salesforce/AttachmentDescription.ts @@ -0,0 +1,347 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const attachmentOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'attachment', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a attachment', + }, + { + name: 'Update', + value: 'update', + description: 'Update a attachment', + }, + { + name: 'Get', + value: 'get', + description: 'Get a attachment', + }, + { + name: 'Get Summary', + value: 'getSummary', + description: `Returns an overview of attachment's metadata.`, + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all attachments', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a attachment', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const attachmentFields = [ + +/* -------------------------------------------------------------------------- */ +/* attachment:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Parent ID', + name: 'parentId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'attachment', + ], + operation: [ + 'create' + ], + }, + }, + description: '', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'attachment', + ], + operation: [ + 'create' + ], + }, + }, + description: 'Required. Name of the attached file. Maximum size is 255 characters. Label is File Name.', + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + required: true, + displayOptions: { + show: { + resource: [ + 'attachment', + ], + operation: [ + 'create' + ], + }, + }, + placeholder: '', + description: 'Name of the binary property which contains
the data for the file to be uploaded.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'attachment', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'ID of the User who owns the attachment.', + }, + { + displayName: 'Is Private', + name: 'isPrivate', + type: 'boolean', + default: false, + description: 'Indicates whether this record is viewable only by the owner and administrators (true) or viewable by all otherwise-allowed users (false). ', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: `Text description of the Document. Limit: 255 characters.`, + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* attachment:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Attachment ID', + name: 'attachmentId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'attachment', + ], + operation: [ + 'update', + ] + }, + }, + description: 'Id of attachment that needs to be fetched', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'attachment', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Required. Name of the attached file. Maximum size is 255 characters. Label is File Name.', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'ID of the User who owns the attachment.', + }, + { + displayName: 'Is Private', + name: 'isPrivate', + type: 'boolean', + default: false, + description: 'Indicates whether this record is viewable only by the owner and administrators (true) or viewable by all otherwise-allowed users (false). ', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: `Text description of the Document. Limit: 255 characters.`, + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + placeholder: '', + description: 'Name of the binary property which contains
the data for the file to be uploaded.', + }, + ], + }, + +/* -------------------------------------------------------------------------- */ +/* attachment:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Attachment ID', + name: 'attachmentId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'attachment', + ], + operation: [ + 'get', + ] + }, + }, + description: 'Id of attachment that needs to be fetched', + }, +/* -------------------------------------------------------------------------- */ +/* attachment:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Attachment ID', + name: 'attachmentId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'attachment', + ], + operation: [ + 'delete', + ] + }, + }, + description: 'Id of attachment that needs to be fetched', + }, +/* -------------------------------------------------------------------------- */ +/* attachment:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'attachment', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'attachment', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'attachment', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'Fields to include separated by ,', + }, + ] + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Salesforce/AttachmentInterface.ts b/packages/nodes-base/nodes/Salesforce/AttachmentInterface.ts new file mode 100644 index 0000000000..e63558a496 --- /dev/null +++ b/packages/nodes-base/nodes/Salesforce/AttachmentInterface.ts @@ -0,0 +1,10 @@ + +export interface IAttachment { + ParentId?: string; + Name?: string; + Body?: string; + OwnerId?: string; + IsPrivate?: boolean; + ContentType?: string; + Description?: string; +} diff --git a/packages/nodes-base/nodes/Salesforce/CampaignMemberInterface.ts b/packages/nodes-base/nodes/Salesforce/CampaignMemberInterface.ts new file mode 100644 index 0000000000..3b025c0ce9 --- /dev/null +++ b/packages/nodes-base/nodes/Salesforce/CampaignMemberInterface.ts @@ -0,0 +1,7 @@ + +export interface ICampaignMember { + CampaignId?: string; + ContactId?: string; + LeadId?: string; + Status?: string; +} diff --git a/packages/nodes-base/nodes/Salesforce/CaseDescription.ts b/packages/nodes-base/nodes/Salesforce/CaseDescription.ts new file mode 100644 index 0000000000..9b9c458bac --- /dev/null +++ b/packages/nodes-base/nodes/Salesforce/CaseDescription.ts @@ -0,0 +1,563 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const caseOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'case', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a case', + }, + { + name: 'Update', + value: 'update', + description: 'Update a case', + }, + { + name: 'Get', + value: 'get', + description: 'Get a case', + }, + { + name: 'Get Summary', + value: 'getSummary', + description: `Returns an overview of case's metadata.`, + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all cases', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a case', + }, + { + name: 'Add Comment', + value: 'addComment', + description: 'Add a comment to a case', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const caseFields = [ + +/* -------------------------------------------------------------------------- */ +/* case:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Type', + name: 'type', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getCaseTypes', + }, + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: 'The type of case', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Origin', + name: 'origin', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCaseOrigins', + }, + default: '', + description: 'The source of the case, such as Email, Phone, or Web. Label is Case Origin.', + }, + { + displayName: 'Reason', + name: 'reason', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCaseReasons', + }, + default: '', + description: 'The reason why the case was created, such as Instructions not clear, or User didn’t attend training.', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCaseStatuses', + }, + default: '', + description: 'The status of the case, such as “New,” “Closed,” or “Escalated.” This field directly controls the IsClosed flag', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'The owner of the case.', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'string', + default: '', + description: 'The subject of the case. Limit: 255 characters.', + }, + { + displayName: 'Parent Id', + name: 'ParentId', + type: 'string', + default: '', + description: 'The ID of the parent case in the hierarchy. The label is Parent Case.', + }, + { + displayName: 'Priority', + name: 'priority', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCasePriorities', + }, + default: '', + description: 'The importance or urgency of the case, such as High, Medium, or Low.', + }, + { + displayName: 'Account Id', + name: 'accountId', + type: 'string', + default: '', + description: 'ID of the account associated with this case.', + }, + { + displayName: 'Contact Id', + name: 'contactId', + type: 'string', + default: '', + description: 'IID of the associated Contact.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'A text description of the case. Limit: 32 KB.', + }, + { + displayName: 'Is Escalated', + name: 'isEscalated', + type: 'boolean', + default: false, + description: 'Indicates whether the case has been escalated (true) or not.', + }, + { + displayName: 'Supplied Name', + name: 'suppliedName', + type: 'string', + default: '', + description: `The name that was entered when the case was created. This field can't be updated after the case has been created`, + }, + { + displayName: 'Supplied Email', + name: 'suppliedEmail', + type: 'string', + default: '', + description: `The email address that was entered when the case was created. This field can't be updated after the case has been created.`, + }, + { + displayName: 'Supplied Phone', + name: 'suppliedPhone', + type: 'string', + default: '', + description: `The phone number that was entered when the case was created. This field can't be updated after the case has been created.`, + }, + { + displayName: 'Supplied Company', + name: 'suppliedCompany', + type: 'string', + default: '', + description: `The company name that was entered when the case was created. This field can't be updated after the case has been created..`, + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* case:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Case ID', + name: 'caseId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'update', + ] + }, + }, + description: 'Id of case that needs to be fetched', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCaseTypes', + }, + default: '', + description: 'The type of case', + }, + { + displayName: 'Origin', + name: 'origin', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCaseOrigins', + }, + default: '', + description: 'The source of the case, such as Email, Phone, or Web. Label is Case Origin.', + }, + { + displayName: 'Reason', + name: 'reason', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCaseReasons', + }, + default: '', + description: 'The reason why the case was created, such as Instructions not clear, or User didn’t attend training.', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCaseStatuses', + }, + default: '', + description: 'The status of the case, such as “New,” “Closed,” or “Escalated.” This field directly controls the IsClosed flag', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'The owner of the case.', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'string', + default: '', + description: 'The subject of the case. Limit: 255 characters.', + }, + { + displayName: 'Parent Id', + name: 'ParentId', + type: 'string', + default: '', + description: 'The ID of the parent case in the hierarchy. The label is Parent Case.', + }, + { + displayName: 'Priority', + name: 'priority', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCasePriorities', + }, + default: '', + description: 'The importance or urgency of the case, such as High, Medium, or Low.', + }, + { + displayName: 'Account Id', + name: 'accountId', + type: 'string', + default: '', + description: 'ID of the account associated with this case.', + }, + { + displayName: 'Contact Id', + name: 'contactId', + type: 'string', + default: '', + description: 'IID of the associated Contact.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'A text description of the case. Limit: 32 KB.', + }, + { + displayName: 'Is Escalated', + name: 'isEscalated', + type: 'boolean', + default: false, + description: 'Indicates whether the case has been escalated (true) or not.', + }, + { + displayName: 'Supplied Name', + name: 'suppliedName', + type: 'string', + default: '', + description: `The name that was entered when the case was created. This field can't be updated after the case has been created`, + }, + { + displayName: 'Supplied Email', + name: 'suppliedEmail', + type: 'string', + default: '', + description: `The email address that was entered when the case was created. This field can't be updated after the case has been created.`, + }, + { + displayName: 'Supplied Phone', + name: 'suppliedPhone', + type: 'string', + default: '', + description: `The phone number that was entered when the case was created. This field can't be updated after the case has been created.`, + }, + { + displayName: 'Supplied Company', + name: 'suppliedCompany', + type: 'string', + default: '', + description: `The company name that was entered when the case was created. This field can't be updated after the case has been created..`, + }, + ], + }, + +/* -------------------------------------------------------------------------- */ +/* case:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Case ID', + name: 'caseId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'get', + ] + }, + }, + description: 'Id of case that needs to be fetched', + }, +/* -------------------------------------------------------------------------- */ +/* case:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Case ID', + name: 'caseId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'delete', + ] + }, + }, + description: 'Id of case that needs to be fetched', + }, +/* -------------------------------------------------------------------------- */ +/* case:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'Fields to include separated by ,', + }, + ] + }, + +/* -------------------------------------------------------------------------- */ +/* case:addComment */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Case ID', + name: 'caseId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'addComment', + ] + }, + }, + description: 'Id of case that needs to be fetched', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'case', + ], + operation: [ + 'addComment', + ], + }, + }, + options: [ + { + displayName: 'Comment Body', + name: 'commentBody', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Text of the CaseComment. The maximum size of the comment body is 4,000 bytes. Label is Body.', + }, + { + displayName: 'Is Published', + name: 'isPublished', + type: 'boolean', + default: false, + description: 'Indicates whether the CaseComment is visible to customers in the Self-Service portal (true) or not (false). ', + }, + ] + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Salesforce/CaseInterface.ts b/packages/nodes-base/nodes/Salesforce/CaseInterface.ts new file mode 100644 index 0000000000..169d47c14d --- /dev/null +++ b/packages/nodes-base/nodes/Salesforce/CaseInterface.ts @@ -0,0 +1,25 @@ + +export interface ICase { + Type?: string; + Origin?: string; + Reason?: string; + Status?: string; + OwnerId?: string; + Subject?: string; + ParentId?: string; + Priority?: string; + AccountId?: string; + ContactId?: string; + Description?: string; + IsEscalated?: boolean; + SuppliedName?: string; + SuppliedEmail?: string; + SuppliedPhone?: string; + SuppliedCompany?: string; +} + +export interface ICaseComment { + CommentBody?: string; + ParentId?: string; + IsPublished?: boolean; +} diff --git a/packages/nodes-base/nodes/Salesforce/ContactDescription.ts b/packages/nodes-base/nodes/Salesforce/ContactDescription.ts new file mode 100644 index 0000000000..bda804e6d6 --- /dev/null +++ b/packages/nodes-base/nodes/Salesforce/ContactDescription.ts @@ -0,0 +1,835 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const contactOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'contact', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a contact', + }, + { + name: 'Update', + value: 'update', + description: 'Update a contact', + }, + { + name: 'Get', + value: 'get', + description: 'Get a contact', + }, + { + name: 'Get Summary', + value: 'getSummary', + description: `Returns an overview of contact's metadata.`, + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all contacts', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a contact', + }, + { + name: 'Add Lead To Campaign', + value: 'addToCampaign', + description: 'Add lead to a campaign', + }, + { + name: 'Add Note', + value: 'addNote', + description: 'Add note to a contact', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const contactFields = [ + +/* -------------------------------------------------------------------------- */ +/* contact:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Last Name', + name: 'lastname', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'create', + ] + }, + }, + description: 'Required. Last name of the contact. Limited to 80 characters.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Fax', + name: 'fax', + type: 'string', + default: '', + description: 'Fax number for the contact. Label is Business Fax.', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'Email address for the contact.', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: 'Phone number for the contact.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title of the contact such as CEO or Vice President.', + }, + { + displayName: 'Jigsaw', + name: 'jigsaw', + type: 'string', + default: '', + description: `references the ID of a contact in Data.com. + If a contact has a value in this field, it means that a contact was imported as a contact from Data.com.`, + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'The owner of the contact.', + }, + { + displayName: 'Account', + name: 'acconuntId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getAccounts', + }, + default: '', + description: 'ID of the account that is the parent of this contact.', + }, + { + displayName: 'Birthdate', + name: 'birthdate', + type: 'string', + default: '', + description: 'The birthdate of the contact.', + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: 'First name of the contact. Maximum size is 40 characters.', + }, + { + displayName: 'Home Phone', + name: 'homePhone', + type: 'string', + default: '', + description: 'Home telephone number for the contact', + }, + { + displayName: 'Other City', + name: 'otherCity', + type: 'string', + default: '', + }, + { + displayName: 'Department', + name: 'department', + type: 'string', + default: '', + description: 'The department of the contact.', + }, + { + displayName: 'Lead Source', + name: 'leadSource', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLeadSources', + }, + default: '', + description: 'Source from which the lead was obtained.', + }, + { + displayName: 'Other Phone', + name: 'otherPhone', + type: 'string', + default: '', + description: 'Telephone for alternate address.', + }, + { + displayName: 'Other State', + name: 'otherState', + type: 'string', + default: '', + }, + { + displayName: 'Salutation', + name: 'salutation', + type: 'string', + default: '', + description: 'Honorific abbreviation, word, or phrase to be used in front of name in greetings, such as Dr. or Mrs.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'A description of the contact. Label is Contact Description. Limit: 32 KB.', + }, + { + displayName: 'Mailing City', + name: 'mailingCity', + type: 'string', + default: '', + }, + { + displayName: 'Mobile Phone', + name: 'mobilePhone', + type: 'string', + default: '', + description: `Contact’s mobile phone number.`, + }, + { + displayName: 'Other Street', + name: 'otherStreet', + type: 'string', + default: '', + description: 'Street for alternate address.', + }, + { + displayName: 'Mailing State', + name: 'mailingState', + type: 'string', + default: '', + }, + { + displayName: 'Other Country', + name: 'otherCountry', + type: 'string', + default: '', + }, + { + displayName: 'Assistant Name', + name: 'assistantName', + type: 'string', + default: '', + description: 'The name of the assistant.', + }, + { + displayName: 'Mailing Street', + name: 'mailingStreet', + type: 'string', + default: '', + description: 'Street address for mailing address.', + }, + { + displayName: 'Assistant Phone', + name: 'Assistant Phone', + type: 'string', + default: '', + description: 'The telephone number of the assistant.', + }, + { + displayName: 'Mailing Country', + name: 'mailingCountry', + type: 'string', + default: '', + }, + { + displayName: 'Other Postal Code', + name: 'otherPostalCode', + type: 'string', + default: '', + }, + { + displayName: 'Email Bounced Date', + name: 'otherPostalCode', + type: 'dateTime', + default: '', + description: 'If bounce management is activated and an email sent to the contact bounces, the date and time the bounce occurred.', + }, + { + displayName: 'Mailing Postal Code', + name: 'mailingPostalCode', + type: 'string', + default: '', + }, + { + displayName: 'Email Bounced Reason', + name: 'emailBouncedReason', + type: 'string', + default: '', + description: 'If bounce management is activated and an email sent to the contact bounces, the reason the bounce occurred.', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* contact:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'update', + ] + }, + }, + description: 'Id of contact that needs to be fetched', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Fax', + name: 'fax', + type: 'string', + default: '', + description: 'Fax number for the contact. Label is Business Fax.', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'Email address for the contact.', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: 'Phone number for the contact.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title of the contact such as CEO or Vice President.', + }, + { + displayName: 'Jigsaw', + name: 'jigsaw', + type: 'string', + default: '', + description: `references the ID of a contact in Data.com. + If a contact has a value in this field, it means that a contact was imported as a contact from Data.com.`, + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'The owner of the contact.', + }, + { + displayName: 'Account', + name: 'acconuntId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getAccounts', + }, + default: '', + description: 'ID of the account that is the parent of this contact.', + }, + { + displayName: 'Birthdate', + name: 'birthdate', + type: 'string', + default: '', + description: 'The birthdate of the contact.', + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: 'First name of the contact. Maximum size is 40 characters.', + }, + { + displayName: 'Home Phone', + name: 'homePhone', + type: 'string', + default: '', + description: 'Home telephone number for the contact.', + }, + { + displayName: 'Other City', + name: 'otherCity', + type: 'string', + default: '', + }, + { + displayName: 'Department', + name: 'department', + type: 'string', + default: '', + description: 'The department of the contact.', + }, + { + displayName: 'Lead Source', + name: 'leadSource', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLeadSources', + }, + default: '', + description: 'Source from which the lead was obtained.', + }, + { + displayName: 'Other Phone', + name: 'otherPhone', + type: 'string', + default: '', + description: 'Telephone for alternate address.', + }, + { + displayName: 'Other State', + name: 'otherState', + type: 'string', + default: '', + }, + { + displayName: 'Salutation', + name: 'salutation', + type: 'string', + default: '', + description: 'Honorific abbreviation, word, or phrase to be used in front of name in greetings, such as Dr. or Mrs.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'A description of the contact. Label is Contact Description. Limit: 32 KB.', + }, + { + displayName: 'Mailing City', + name: 'mailingCity', + type: 'string', + default: '', + }, + { + displayName: 'Mobile Phone', + name: 'mobilePhone', + type: 'string', + default: '', + description: `Contact’s mobile phone number.`, + }, + { + displayName: 'Other Street', + name: 'otherStreet', + type: 'string', + default: '', + description: 'Street for alternate address.', + }, + { + displayName: 'Mailing State', + name: 'mailingState', + type: 'string', + default: '', + }, + { + displayName: 'Other Country', + name: 'otherCountry', + type: 'string', + default: '', + }, + { + displayName: 'Assistant Name', + name: 'assistantName', + type: 'string', + default: '', + description: 'The name of the assistant.', + }, + { + displayName: 'Mailing Street', + name: 'mailingStreet', + type: 'string', + default: '', + description: 'Street address for mailing address.', + }, + { + displayName: 'Assistant Phone', + name: 'Assistant Phone', + type: 'string', + default: '', + description: 'The telephone number of the assistant.', + }, + { + displayName: 'Mailing Country', + name: 'mailingCountry', + type: 'string', + default: '', + }, + { + displayName: 'Other Postal Code', + name: 'otherPostalCode', + type: 'string', + default: '', + }, + { + displayName: 'Email Bounced Date', + name: 'emailBouncedDate', + type: 'dateTime', + default: '', + description: 'If bounce management is activated and an email sent to the contact bounces, the date and time the bounce occurred.', + }, + { + displayName: 'Mailing Postal Code', + name: 'mailingPostalCode', + type: 'string', + default: '', + }, + { + displayName: 'Email Bounced Reason', + name: 'emailBouncedReason', + type: 'string', + default: '', + description: 'If bounce management is activated and an email sent to the contact bounces, the reason the bounce occurred.', + }, + ], + }, + +/* -------------------------------------------------------------------------- */ +/* contact:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'get', + ] + }, + }, + description: 'Id of contact that needs to be fetched', + }, +/* -------------------------------------------------------------------------- */ +/* contact:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'delete', + ] + }, + }, + description: 'Id of contact that needs to be fetched', + }, +/* -------------------------------------------------------------------------- */ +/* contact:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'Fields to include separated by ,', + }, + ] + }, + +/* -------------------------------------------------------------------------- */ +/* contact:addToCampaign */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'addToCampaign', + ] + }, + }, + description: 'Id of contact that needs to be fetched', + }, + { + displayName: 'Campaign', + name: 'campaignId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'addToCampaign', + ] + }, + }, + description: 'Id of the campaign that needs to be fetched', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'addToCampaign', + ], + }, + }, + options: [ + { + displayName: 'Status', + name: 'status', + type: 'string', + default: '', + description: 'Controls the HasResponded flag on this object', + }, + ] + }, +/* -------------------------------------------------------------------------- */ +/* contact:addNote */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'addNote', + ] + }, + }, + description: 'Id of contact that needs to be fetched', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'addNote', + ] + }, + }, + description: 'Title of the note.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'addNote', + ], + }, + }, + options: [ + { + displayName: 'Body', + name: 'body', + type: 'string', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + description: 'Body of the note. Limited to 32 KB.', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'ID of the user who owns the note.', + }, + { + displayName: 'Is Private', + name: 'isPrivate', + type: 'boolean', + default: false, + description: 'If true, only the note owner or a user with the “Modify All Data” permission can view the note or query it via the API', + }, + ] + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Salesforce/ContactInterface.ts b/packages/nodes-base/nodes/Salesforce/ContactInterface.ts new file mode 100644 index 0000000000..e51090b88f --- /dev/null +++ b/packages/nodes-base/nodes/Salesforce/ContactInterface.ts @@ -0,0 +1,34 @@ + +export interface IContact { + LastName?: string; + Fax?: string; + Email?: string; + Phone?: string; + Title?: string; + Jigsaw?: string; + OwnerId?: string; + AccountId?: string; + Birthdate?:string; + FirstName?: string; + HomePhone?: string; + OtherCity?: string; + Department?: string; + LeadSource?: string; + OtherPhone?: string; + OtherState?: string; + Salutation?: string; + Description?: string; + MailingCity?: string; + MobilePhone?: string; + OtherStreet?: string; + MailingState?: string; + OtherCountry?: string; + AssistantName?: string; + MailingStreet?: string; + AssistantPhone?: string; + MailingCountry?: string; + OtherPostalCode?: string; + MailingPostalCode?: string; + EmailBouncedDate?: string; + EmailBouncedReason?: string; +} diff --git a/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts b/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts new file mode 100644 index 0000000000..33fec16bf3 --- /dev/null +++ b/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts @@ -0,0 +1,50 @@ +import { OptionsWithUri } from 'request'; +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; +import { + IDataObject +} from 'n8n-workflow'; + +export async function salesforceApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('salesforceOAuth2Api'); + const subdomain = (credentials!.accessTokenUrl as string).split('.')[0].split('/')[2]; + const options: OptionsWithUri = { + method, + body, + qs, + uri: uri || `https://${subdomain}.salesforce.com/services/data/v39.0${resource}`, + json: true + }; + try { + //@ts-ignore + return await this.helpers.requestOAuth.call(this, 'salesforceOAuth2Api', options); + } catch (error) { + if (error.response && error.response.body && error.response.body[0].message) { + // Try to return the error prettier + throw new Error(`Salesforce error response [${error.statusCode}]: ${error.response.body[0].message}`); + } + throw error; + } +} + +export async function salesforceApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + let uri: string | undefined; + + do { + responseData = await salesforceApiRequest.call(this, method, endpoint, body, query, uri); + uri = responseData.nextRecordsUrl; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData.nextRecordsUrl !== undefined && + responseData.nextRecordsUrl !== null + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Salesforce/LeadDescription.ts b/packages/nodes-base/nodes/Salesforce/LeadDescription.ts new file mode 100644 index 0000000000..ba9134741d --- /dev/null +++ b/packages/nodes-base/nodes/Salesforce/LeadDescription.ts @@ -0,0 +1,762 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const leadOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'lead', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a lead', + }, + { + name: 'Update', + value: 'update', + description: 'Update a lead', + }, + { + name: 'Get', + value: 'get', + description: 'Get a lead', + }, + { + name: 'Get Summary', + value: 'getSummary', + description: `Returns an overview of Lead's metadata.`, + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all leads', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a lead', + }, + { + name: 'Add Lead To Campaign', + value: 'addToCampaign', + description: 'Add lead to a campaign', + }, + { + name: 'Add Note', + value: 'addNote', + description: 'Add note to a lead', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const leadFields = [ + +/* -------------------------------------------------------------------------- */ +/* lead:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Company', + name: 'company', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'create', + ] + }, + }, + description: 'Company of the lead. If person account record types have been enabled, and if the value of Company is null, the lead converts to a person account.', + }, + { + displayName: 'Last Name', + name: 'lastname', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'create', + ] + }, + }, + description: 'Required. Last name of the lead. Limited to 80 characters.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'City', + name: 'city', + type: 'string', + default: '', + description: 'City for the address of the lead.', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'Email address for the lead.', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: 'Phone number for the lead.', + }, + { + displayName: 'State', + name: 'state', + type: 'string', + default: '', + description: 'State for the address of the lead.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title for the lead, for example CFO or CEO.', + }, + { + displayName: 'Jigsaw', + name: 'jigsaw', + type: 'string', + default: '', + description: `references the ID of a contact in Data.com. + If a lead has a value in this field, it means that a contact was imported as a lead from Data.com.`, + }, + { + displayName: 'Rating', + name: 'rating', + type: 'string', + default: '', + description: 'Rating of the lead.', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLeadStatuses', + }, + default: '', + description: 'Status code for this converted lead.', + }, + { + displayName: 'Street', + name: 'street', + type: 'string', + default: '', + description: 'Street number and name for the address of the lead', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'The owner of the lead.', + }, + { + displayName: 'Website', + name: 'website', + type: 'string', + default: '', + description: 'Website for the lead.', + }, + { + displayName: 'Industry', + name: 'industry', + type: 'string', + default: '', + description: 'Website for the lead.', + }, + { + displayName: 'Fist Name', + name: 'firstname', + type: 'string', + default: '', + description: 'First name of the lead. Limited to 40 characters.', + }, + { + displayName: 'Lead Source', + name: 'leadSource', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLeadSources', + }, + default: '', + description: 'Source from which the lead was obtained.', + }, + { + displayName: 'Postal Code', + name: 'postalCode', + type: 'string', + default: '', + description: 'Postal code for the address of the lead. Label is Zip/Postal Code.', + }, + { + displayName: 'Salutation', + name: 'salutation', + type: 'string', + default: '', + description: 'Salutation for the lead.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Description of the lead.', + }, + { + displayName: 'Annual Revenue', + name: 'annualRevenue', + type: 'number', + typeOptions: { + numberPrecision: 2, + numberStepSize: 1, + }, + default: '', + description: 'Annual revenue for the company of the lead.', + }, + { + displayName: 'Number Of Employees', + name: 'numberOfEmployees', + type: 'number', + typeOptions: { + numberStepSize: 1, + }, + default: '', + description: 'Number of employees at the lead’s company. Label is Employees.', + }, + { + displayName: 'Is Unread By Owner', + name: 'IsUnreadByOwner', + type: 'Boolean', + default: false, + description: 'If true, lead has been assigned, but not yet viewed. See Unread Leads for more information. Label is Unread By Owner.', + }, + ] + }, +/* -------------------------------------------------------------------------- */ +/* lead:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Lead ID', + name: 'leadId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'update', + ] + }, + }, + description: 'Id of Lead that needs to be fetched', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Company', + name: 'company', + type: 'string', + default: '', + description: 'Company of the lead. If person account record types have been enabled, and if the value of Company is null, the lead converts to a person account.', + }, + { + displayName: 'Last Name', + name: 'lastname', + type: 'string', + default: '', + description: 'Required. Last name of the lead. Limited to 80 characters.', + }, + { + displayName: 'City', + name: 'city', + type: 'string', + default: '', + description: 'City for the address of the lead.', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'Email address for the lead.', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: 'Phone number for the lead.', + }, + { + displayName: 'State', + name: 'state', + type: 'string', + default: '', + description: 'State for the address of the lead.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title for the lead, for example CFO or CEO.', + }, + { + displayName: 'Jigsaw', + name: 'jigsaw', + type: 'string', + default: '', + description: `references the ID of a contact in Data.com. + If a lead has a value in this field, it means that a contact was imported as a lead from Data.com.`, + }, + { + displayName: 'Rating', + name: 'rating', + type: 'string', + default: '', + description: 'Rating of the lead.', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLeadStatuses', + }, + default: '', + description: 'Status code for this converted lead.', + }, + { + displayName: 'Street', + name: 'street', + type: 'string', + default: '', + description: 'Street number and name for the address of the lead', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'The owner of the lead.', + }, + { + displayName: 'Website', + name: 'website', + type: 'string', + default: '', + description: 'Website for the lead.', + }, + { + displayName: 'Industry', + name: 'industry', + type: 'string', + default: '', + description: 'Website for the lead.', + }, + { + displayName: 'Fist Name', + name: 'firstname', + type: 'string', + default: '', + description: 'First name of the lead. Limited to 40 characters.', + }, + { + displayName: 'Lead Source', + name: 'leadSource', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLeadSources', + }, + default: '', + description: 'Source from which the lead was obtained.', + }, + { + displayName: 'Postal Code', + name: 'postalCode', + type: 'string', + default: '', + description: 'Postal code for the address of the lead. Label is Zip/Postal Code.', + }, + { + displayName: 'Salutation', + name: 'salutation', + type: 'string', + default: '', + description: 'Salutation for the lead.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Description of the lead.', + }, + { + displayName: 'Annual Revenue', + name: 'annualRevenue', + type: 'number', + typeOptions: { + numberPrecision: 2, + numberStepSize: 1, + }, + default: '', + description: 'Annual revenue for the company of the lead.', + }, + { + displayName: 'Number Of Employees', + name: 'numberOfEmployees', + type: 'number', + typeOptions: { + numberStepSize: 1, + }, + default: '', + description: 'Number of employees at the lead’s company. Label is Employees.', + }, + { + displayName: 'Is Unread By Owner', + name: 'IsUnreadByOwner', + type: 'Boolean', + default: false, + description: 'If true, lead has been assigned, but not yet viewed. See Unread Leads for more information. Label is Unread By Owner.', + }, + ] + }, + +/* -------------------------------------------------------------------------- */ +/* lead:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Lead ID', + name: 'leadId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'get', + ] + }, + }, + description: 'Id of Lead that needs to be fetched', + }, +/* -------------------------------------------------------------------------- */ +/* lead:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Lead ID', + name: 'leadId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'delete', + ] + }, + }, + description: 'Id of Lead that needs to be fetched', + }, +/* -------------------------------------------------------------------------- */ +/* lead:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'Fields to include separated by ,', + }, + ] + }, +/* -------------------------------------------------------------------------- */ +/* contact:addToCampaign */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Lead ID', + name: 'leadId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'addToCampaign', + ] + }, + }, + description: 'Id of contact that needs to be fetched', + }, + { + displayName: 'Campaign', + name: 'campaignId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'addToCampaign', + ] + }, + }, + description: 'Id of the campaign that needs to be fetched', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'addToCampaign', + ], + }, + }, + options: [ + { + displayName: 'Status', + name: 'status', + type: 'string', + default: '', + description: 'Controls the HasResponded flag on this object', + }, + ] + }, +/* -------------------------------------------------------------------------- */ +/* lead:addNote */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Lead ID', + name: 'leadId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'addNote', + ] + }, + }, + description: 'Id of lead that needs to be fetched', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'addNote', + ] + }, + }, + description: 'Title of the note.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'addNote', + ], + }, + }, + options: [ + { + displayName: 'Body', + name: 'body', + type: 'string', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + description: 'Body of the note. Limited to 32 KB.', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'ID of the user who owns the note.', + }, + { + displayName: 'Is Private', + name: 'isPrivate', + type: 'boolean', + default: false, + description: 'If true, only the note owner or a user with the “Modify All Data” permission can view the note or query it via the API', + }, + ] + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Salesforce/LeadInterface.ts b/packages/nodes-base/nodes/Salesforce/LeadInterface.ts new file mode 100644 index 0000000000..c6cd047394 --- /dev/null +++ b/packages/nodes-base/nodes/Salesforce/LeadInterface.ts @@ -0,0 +1,26 @@ + +export interface ILead { + Company?: string; + LastName?: string; + Email?: string; + City?: string; + Phone?: string; + State?: string; + Title?: string; + Jigsaw?: string; + Rating?: string; + Status?: string; + Street?: string; + Country?: string; + OwnerId?: string; + Website?: string; + Industry?: string; + FirstName?: string; + LeadSource?: string; + PostalCode?: string; + Salutation?: string; + Description?: string; + AnnualRevenue?: number; + IsUnreadByOwner?: boolean; + NumberOfEmployees?: number; +} diff --git a/packages/nodes-base/nodes/Salesforce/NoteInterface.ts b/packages/nodes-base/nodes/Salesforce/NoteInterface.ts new file mode 100644 index 0000000000..2036cfea21 --- /dev/null +++ b/packages/nodes-base/nodes/Salesforce/NoteInterface.ts @@ -0,0 +1,8 @@ + +export interface INote { + Title?: string; + ParentId?: string; + Body?: string; + OwnerId?: string; + IsPrivate?: boolean; +} diff --git a/packages/nodes-base/nodes/Salesforce/OpportunityDescription.ts b/packages/nodes-base/nodes/Salesforce/OpportunityDescription.ts new file mode 100644 index 0000000000..89efd96478 --- /dev/null +++ b/packages/nodes-base/nodes/Salesforce/OpportunityDescription.ts @@ -0,0 +1,625 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const opportunityOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'opportunity', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create an opportunity', + }, + { + name: 'Update', + value: 'update', + description: 'Update an opportunity', + }, + { + name: 'Get', + value: 'get', + description: 'Get an opportunity', + }, + { + name: 'Get Summary', + value: 'getSummary', + description: `Returns an overview of opportunity's metadata.`, + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all opportunitys', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an opportunity', + }, + { + name: 'Add Note', + value: 'addNote', + description: 'Add note to an opportunity', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const opportunityFields = [ + +/* -------------------------------------------------------------------------- */ +/* opportunity:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Name', + name: 'name', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'opportunity', + ], + operation: [ + 'create', + ] + }, + }, + description: 'Required. Last name of the opportunity. Limited to 80 characters.', + }, + { + displayName: 'Close Date', + name: 'closeDate', + type: 'dateTime', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'opportunity', + ], + operation: [ + 'create', + ] + }, + }, + description: 'Required. Date when the opportunity is expected to close.', + }, + { + displayName: 'Stage Name', + name: 'stageName', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getStages' + }, + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'opportunity', + ], + operation: [ + 'create', + ] + }, + }, + description: 'Required. Date when the opportunity is expected to close.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'opportunity', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + default: '', + options: [ + { + name: 'Business', + valie: 'Business', + }, + { + name: 'New Business', + valie: 'New Business', + }, + ], + description: 'Type of opportunity. For example, Existing Business or New Business. Label is Opportunity Type.', + }, + { + displayName: 'Amount', + name: 'amount', + type: 'number', + typeOptions: { + numberPrecision: 2, + }, + default: '', + description: 'Estimated total sale amount', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: 'Phone number for the opportunity.', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'The owner of the opportunity.', + }, + { + displayName: 'Next Step', + name: 'nextStep', + type: 'string', + default: '', + description: 'Description of next task in closing opportunity. Limit: 255 characters.', + }, + { + displayName: 'Account', + name: 'accountId', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getAccounts', + }, + description: 'ID of the account associated with this opportunity.', + }, + { + displayName: 'Campaign', + name: 'campaignId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + default: '', + description: 'Id of the campaign that needs to be fetched', + }, + { + displayName: 'Lead Source', + name: 'leadSource', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLeadSources', + }, + default: '', + description: 'Source from which the lead was obtained.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'A description of the opportunity. Label is Contact Description. Limit: 32 KB.', + }, + { + displayName: 'Probability', + name: 'probability', + type: 'number', + typeOptions: { + numberPrecision: 1, + }, + default: '', + description: 'Percentage of estimated confidence in closing the opportunity', + }, + { + displayName: 'Pricebook2 Id', + name: 'pricebook2Id', + type: 'string', + default: '', + description: 'ID of a related Pricebook2 object', + }, + { + displayName: 'Forecast Category Name', + name: 'forecastCategoryName', + type: 'string', + default: '', + description: 'It is implied, but not directly controlled, by the StageName field', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* opportunity:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Opportunity ID', + name: 'opportunityId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'opportunity', + ], + operation: [ + 'update', + ] + }, + }, + description: 'Id of opportunity that needs to be fetched', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'opportunity', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Required. Last name of the opportunity. Limited to 80 characters.', + }, + { + displayName: 'Close Date', + name: 'closeDate', + type: 'dateTime', + default: '', + description: 'Required. Date when the opportunity is expected to close.', + }, + { + displayName: 'Stage Name', + name: 'stageName', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getStages' + }, + default: '', + description: 'Required. Date when the opportunity is expected to close.', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + default: '', + options: [ + { + name: 'Business', + valie: 'Business', + }, + { + name: 'New Business', + valie: 'New Business', + }, + ], + description: 'Type of opportunity. For example, Existing Business or New Business. Label is Opportunity Type.', + }, + { + displayName: 'Amount', + name: 'amount', + type: 'number', + typeOptions: { + numberPrecision: 2, + }, + default: '', + description: 'Estimated total sale amount', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: 'Phone number for the opportunity.', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'The owner of the opportunity.', + }, + { + displayName: 'Next Step', + name: 'nextStep', + type: 'string', + default: '', + description: 'Description of next task in closing opportunity. Limit: 255 characters.', + }, + { + displayName: 'Account', + name: 'accountId', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getAccounts', + }, + description: 'ID of the account associated with this opportunity.', + }, + { + displayName: 'Campaign', + name: 'campaignId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + default: '', + description: 'Id of the campaign that needs to be fetched', + }, + { + displayName: 'Lead Source', + name: 'leadSource', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLeadSources', + }, + default: '', + description: 'Source from which the lead was obtained.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'A description of the opportunity. Label is Contact Description. Limit: 32 KB.', + }, + { + displayName: 'Probability', + name: 'probability', + type: 'number', + typeOptions: { + numberPrecision: 1, + }, + default: '', + description: 'Percentage of estimated confidence in closing the opportunity', + }, + { + displayName: 'Pricebook2 Id', + name: 'pricebook2Id', + type: 'string', + default: '', + description: 'ID of a related Pricebook2 object', + }, + { + displayName: 'Forecast Category Name', + name: 'forecastCategoryName', + type: 'string', + default: '', + description: 'It is implied, but not directly controlled, by the StageName field', + }, + ], + }, + +/* -------------------------------------------------------------------------- */ +/* opportunity:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Opportunity ID', + name: 'opportunityId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'opportunity', + ], + operation: [ + 'get', + ] + }, + }, + description: 'Id of opportunity that needs to be fetched', + }, +/* -------------------------------------------------------------------------- */ +/* opportunity:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Opportunity ID', + name: 'opportunityId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'opportunity', + ], + operation: [ + 'delete', + ] + }, + }, + description: 'Id of opportunity that needs to be fetched', + }, +/* -------------------------------------------------------------------------- */ +/* opportunity:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'opportunity', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'opportunity', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'opportunity', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'Fields to include separated by ,', + }, + ] + }, + +/* -------------------------------------------------------------------------- */ +/* opportunity:addNote */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Opportunity ID', + name: 'opportunityId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'opportunity', + ], + operation: [ + 'addNote', + ] + }, + }, + description: 'Id of opportunity that needs to be fetched', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'opportunity', + ], + operation: [ + 'addNote', + ] + }, + }, + description: 'Title of the note.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'opportunity', + ], + operation: [ + 'addNote', + ], + }, + }, + options: [ + { + displayName: 'Body', + name: 'body', + type: 'string', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + description: 'Body of the note. Limited to 32 KB.', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'ID of the user who owns the note.', + }, + { + displayName: 'Is Private', + name: 'isPrivate', + type: 'boolean', + default: false, + description: 'If true, only the note owner or a user with the “Modify All Data” permission can view the note or query it via the API', + }, + ] + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Salesforce/OpportunityInterface.ts b/packages/nodes-base/nodes/Salesforce/OpportunityInterface.ts new file mode 100644 index 0000000000..bad4852648 --- /dev/null +++ b/packages/nodes-base/nodes/Salesforce/OpportunityInterface.ts @@ -0,0 +1,17 @@ + +export interface IOpportunity { + Name?: string; + StageName?: string; + CloseDate?: string; + Type?: string; + Amount?: number; + OwnerId?: string; + NextStep?: string; + AccountId?: string; + CampaignId?: string; + LeadSource?: string; + Description?: string; + Probability?: number; + Pricebook2Id?:string; + ForecastCategoryName?: string; +} diff --git a/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts b/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts new file mode 100644 index 0000000000..0c07520d61 --- /dev/null +++ b/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts @@ -0,0 +1,1893 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; +import { + IDataObject, + ILoadOptionsFunctions, + INodeTypeDescription, + INodeExecutionData, + INodeType, + INodePropertyOptions, +} from 'n8n-workflow'; +import { + salesforceApiRequest, + salesforceApiRequestAllItems, +} from './GenericFunctions'; +import { + leadFields, + leadOperations, +} from './LeadDescription'; +import { + contactFields, + contactOperations, +} from './ContactDescription'; +import { + opportunityOperations, + opportunityFields, + } from './OpportunityDescription'; + import { + accountOperations, + accountFields, + } from './AccountDescription'; + import { + caseOperations, + caseFields, + } from './CaseDescription'; + import { + taskOperations, + taskFields, + } from './TaskDescription'; + import { + attachmentOperations, + attachmentFields, + } from './AttachmentDescription'; + import { + IOpportunity, +} from './OpportunityInterface'; +import { + ICampaignMember, +} from './CampaignMemberInterface'; +import { + ILead, +} from './LeadInterface'; +import { + IContact, + } from './ContactInterface'; + import { + IAccount, + } from './AccountInterface'; + import { + INote, +} from './NoteInterface'; +import { + ICase, + ICaseComment, +} from './CaseInterface'; +import { + ITask, +} from './TaskInterface'; +import { + IAttachment, +} from './AttachmentInterface'; + +export class Salesforce implements INodeType { + description: INodeTypeDescription = { + displayName: 'Salesforce', + name: 'salesforce', + icon: 'file:salesforce.png', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Salesforce API', + defaults: { + name: 'Salesforce', + color: '#429fd9', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'salesforceOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Lead', + value: 'lead', + description: 'Represents a prospect or potential .', + }, + { + name: 'Contact', + value: 'contact', + description: 'Represents a contact, which is an individual associated with an account.', + }, + { + name: 'Opportunity', + value: 'opportunity', + description: 'Represents an opportunity, which is a sale or pending deal.', + }, + { + name: 'Account', + value: 'account', + description: 'Represents an individual account, which is an organization or person involved with your business (such as customers, competitors, and partners).', + }, + { + name: 'Case', + value: 'case', + description: 'Represents a case, which is a customer issue or problem.', + }, + { + name: 'Task', + value: 'task', + description: 'Represents a business activity such as making a phone call or other to-do items. In the user interface, and records are collectively referred to as activities.', + }, + { + name: 'Attachment', + value: 'attachment', + description: 'Represents a file that a has uploaded and attached to a parent object.', + }, + ], + default: 'lead', + description: 'Resource to consume.', + }, + ...leadOperations, + ...leadFields, + ...contactOperations, + ...contactFields, + ...opportunityOperations, + ...opportunityFields, + ...accountOperations, + ...accountFields, + ...caseOperations, + ...caseFields, + ...taskOperations, + ...taskFields, + ...attachmentOperations, + ...attachmentFields, + ], + }; + + methods = { + loadOptions: { + // Get all the lead statuses to display them to user so that he can + // select them easily + async getLeadStatuses(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const qs = { + q: 'SELECT id, MasterLabel FROM LeadStatus', + }; + const statuses = await salesforceApiRequestAllItems.call(this, 'records', 'GET', '/query', {}, qs); + for (const status of statuses) { + const statusName = status.MasterLabel; + const statusId = status.Id; + returnData.push({ + name: statusName, + value: statusId, + }); + } + return returnData; + }, + // Get all the users to display them to user so that he can + // select them easily + async getUsers(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const qs = { + q: 'SELECT id, Name FROM User', + }; + const users = await salesforceApiRequestAllItems.call(this, 'records', 'GET', '/query', {}, qs); + for (const user of users) { + const userName = user.Name; + const userId = user.Id; + returnData.push({ + name: userName, + value: userId, + }); + } + return returnData; + }, + // Get all the lead sources to display them to user so that he can + // select them easily + async getLeadSources(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + //find a way to filter this object to get just the lead sources instead of the whole object + const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/lead/describe'); + for (const field of fields) { + if (field.name === 'LeadSource') { + for (const pickValue of field.picklistValues) { + const pickValueName = pickValue.label; + const pickValueId = pickValue.value; + returnData.push({ + name: pickValueName, + value: pickValueId, + }); + } + } + } + return returnData; + }, + // Get all the accounts to display them to user so that he can + // select them easily + async getAccounts(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const qs = { + q: 'SELECT id, Name FROM Account', + }; + const accounts = await salesforceApiRequestAllItems.call(this, 'records', 'GET', '/query', {}, qs); + for (const account of accounts) { + const accountName = account.Name; + const accountId = account.Id; + returnData.push({ + name: accountName, + value: accountId, + }); + } + return returnData; + }, + // Get all the campaigns to display them to user so that he can + // select them easily + async getCampaigns(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const qs = { + q: 'SELECT id, Name FROM Campaign', + }; + const campaigns = await salesforceApiRequestAllItems.call(this, 'records', 'GET', '/query', {}, qs); + for (const campaign of campaigns) { + const campaignName = campaign.Name; + const campaignId = campaign.Id; + returnData.push({ + name: campaignName, + value: campaignId, + }); + } + return returnData; + }, + // Get all the stages to display them to user so that he can + // select them easily + async getStages(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + //find a way to filter this object to get just the lead sources instead of the whole object + const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/opportunity/describe'); + for (const field of fields) { + if (field.name === 'StageName') { + for (const pickValue of field.picklistValues) { + const pickValueName = pickValue.label; + const pickValueId = pickValue.value; + returnData.push({ + name: pickValueName, + value: pickValueId, + }); + } + } + } + return returnData; + }, + // Get all the stages to display them to user so that he can + // select them easily + async getAccountTypes(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + //find a way to filter this object to get just the lead sources instead of the whole object + const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/account/describe'); + for (const field of fields) { + if (field.name === 'Type') { + for (const pickValue of field.picklistValues) { + const pickValueName = pickValue.label; + const pickValueId = pickValue.value; + returnData.push({ + name: pickValueName, + value: pickValueId, + }); + } + } + } + return returnData; + }, + // Get all the account sources to display them to user so that he can + // select them easily + async getAccountSources(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + //find a way to filter this object to get just the lead sources instead of the whole object + const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/account/describe'); + for (const field of fields) { + if (field.name === 'AccountSource') { + for (const pickValue of field.picklistValues) { + const pickValueName = pickValue.label; + const pickValueId = pickValue.value; + returnData.push({ + name: pickValueName, + value: pickValueId, + }); + } + } + } + return returnData; + }, + // Get all the case types to display them to user so that he can + // select them easily + async getCaseTypes(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + //find a way to filter this object to get just the lead sources instead of the whole object + const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/case/describe'); + for (const field of fields) { + if (field.name === 'Type') { + for (const pickValue of field.picklistValues) { + const pickValueName = pickValue.label; + const pickValueId = pickValue.value; + returnData.push({ + name: pickValueName, + value: pickValueId, + }); + } + } + } + return returnData; + }, + // Get all the case statuses to display them to user so that he can + // select them easily + async getCaseStatuses(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + //find a way to filter this object to get just the lead sources instead of the whole object + const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/case/describe'); + for (const field of fields) { + if (field.name === 'Status') { + for (const pickValue of field.picklistValues) { + const pickValueName = pickValue.label; + const pickValueId = pickValue.value; + returnData.push({ + name: pickValueName, + value: pickValueId, + }); + } + } + } + return returnData; + }, + // Get all the case reasons to display them to user so that he can + // select them easily + async getCaseReasons(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + //find a way to filter this object to get just the lead sources instead of the whole object + const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/case/describe'); + for (const field of fields) { + if (field.name === 'Reason') { + for (const pickValue of field.picklistValues) { + const pickValueName = pickValue.label; + const pickValueId = pickValue.value; + returnData.push({ + name: pickValueName, + value: pickValueId, + }); + } + } + } + return returnData; + }, + // Get all the case origins to display them to user so that he can + // select them easily + async getCaseOrigins(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + //find a way to filter this object to get just the lead sources instead of the whole object + const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/case/describe'); + for (const field of fields) { + if (field.name === 'Origin') { + for (const pickValue of field.picklistValues) { + const pickValueName = pickValue.label; + const pickValueId = pickValue.value; + returnData.push({ + name: pickValueName, + value: pickValueId, + }); + } + } + } + return returnData; + }, + // Get all the case priorities to display them to user so that he can + // select them easily + async getCasePriorities(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + //find a way to filter this object to get just the lead sources instead of the whole object + const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/case/describe'); + for (const field of fields) { + if (field.name === 'Priority') { + for (const pickValue of field.picklistValues) { + const pickValueName = pickValue.label; + const pickValueId = pickValue.value; + returnData.push({ + name: pickValueName, + value: pickValueId, + }); + } + } + } + return returnData; + }, + // Get all the task statuses to display them to user so that he can + // select them easily + async getTaskStatuses(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + //find a way to filter this object to get just the lead sources instead of the whole object + const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/task/describe'); + for (const field of fields) { + if (field.name === 'Status') { + for (const pickValue of field.picklistValues) { + const pickValueName = pickValue.label; + const pickValueId = pickValue.value; + returnData.push({ + name: pickValueName, + value: pickValueId, + }); + } + } + } + return returnData; + }, + // Get all the task subjects to display them to user so that he can + // select them easily + async getTaskSubjects(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + //find a way to filter this object to get just the lead sources instead of the whole object + const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/task/describe'); + for (const field of fields) { + if (field.name === 'Subject') { + for (const pickValue of field.picklistValues) { + const pickValueName = pickValue.label; + const pickValueId = pickValue.value; + returnData.push({ + name: pickValueName, + value: pickValueId, + }); + } + } + } + return returnData; + }, + // Get all the task call types to display them to user so that he can + // select them easily + async getTaskCallTypes(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + //find a way to filter this object to get just the lead sources instead of the whole object + const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/task/describe'); + for (const field of fields) { + if (field.name === 'CallType') { + for (const pickValue of field.picklistValues) { + const pickValueName = pickValue.label; + const pickValueId = pickValue.value; + returnData.push({ + name: pickValueName, + value: pickValueId, + }); + } + } + } + return returnData; + }, + // Get all the task call priorities to display them to user so that he can + // select them easily + async getTaskPriorities(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + //find a way to filter this object to get just the lead sources instead of the whole object + const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/task/describe'); + for (const field of fields) { + if (field.name === 'Priority') { + for (const pickValue of field.picklistValues) { + const pickValueName = pickValue.label; + const pickValueId = pickValue.value; + returnData.push({ + name: pickValueName, + value: pickValueId, + }); + } + } + } + return returnData; + }, + // Get all the task recurrence types to display them to user so that he can + // select them easily + async getTaskRecurrenceTypes(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + //find a way to filter this object to get just the lead sources instead of the whole object + const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/task/describe'); + for (const field of fields) { + if (field.name === 'RecurrenceType') { + for (const pickValue of field.picklistValues) { + const pickValueName = pickValue.label; + const pickValueId = pickValue.value; + returnData.push({ + name: pickValueName, + value: pickValueId, + }); + } + } + } + return returnData; + }, + // Get all the task recurrence instances to display them to user so that he can + // select them easily + async getTaskRecurrenceInstances(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + //find a way to filter this object to get just the lead sources instead of the whole object + const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/task/describe'); + for (const field of fields) { + if (field.name === 'RecurrenceInstance') { + for (const pickValue of field.picklistValues) { + const pickValueName = pickValue.label; + const pickValueId = pickValue.value; + returnData.push({ + name: pickValueName, + value: pickValueId, + }); + } + } + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + let responseData; + const qs: 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 === 'lead') { + //https://developer.salesforce.com/docs/api-explorer/sobject/Lead/post-lead + if (operation === 'create') { + const company = this.getNodeParameter('company', i) as string; + const lastname = this.getNodeParameter('lastname', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: ILead = { + Company: company, + LastName: lastname, + }; + if (additionalFields.email) { + body.Email = additionalFields.email as string; + } + if (additionalFields.city) { + body.City = additionalFields.city as string; + } + if (additionalFields.phone) { + body.Phone = additionalFields.phone as string; + } + if (additionalFields.state) { + body.State = additionalFields.state as string; + } + if (additionalFields.title) { + body.Title = additionalFields.title as string; + } + if (additionalFields.jigsaw) { + body.Jigsaw = additionalFields.jigsaw as string; + } + if (additionalFields.rating) { + body.Rating = additionalFields.rating as string; + } + if (additionalFields.status) { + body.Status = additionalFields.status as string; + } + if (additionalFields.street) { + body.Street = additionalFields.street as string; + } + if (additionalFields.country) { + body.Country = additionalFields.country as string; + } + if (additionalFields.owner) { + body.OwnerId = additionalFields.owner as string; + } + if (additionalFields.website) { + body.Website = additionalFields.website as string; + } + if (additionalFields.industry) { + body.Industry = additionalFields.industry as string; + } + if (additionalFields.firstName) { + body.FirstName = additionalFields.firstName as string; + } + if (additionalFields.leadSource) { + body.LeadSource = additionalFields.leadSource as string; + } + if (additionalFields.postalCode) { + body.PostalCode = additionalFields.postalCode as string; + } + if (additionalFields.salutation) { + body.Salutation = additionalFields.salutation as string; + } + if (additionalFields.description) { + body.Description = additionalFields.description as string; + } + if (additionalFields.annualRevenue) { + body.AnnualRevenue = additionalFields.annualRevenue as number; + } + if (additionalFields.isUnreadByOwner) { + body.IsUnreadByOwner = additionalFields.isUnreadByOwner as boolean; + } + if (additionalFields.numberOfEmployees) { + body.NumberOfEmployees = additionalFields.numberOfEmployees as number; + } + responseData = await salesforceApiRequest.call(this, 'POST', '/sobjects/lead', body); + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Lead/patch-lead-id + if (operation === 'update') { + const leadId = this.getNodeParameter('leadId', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + const body: ILead = {}; + if (!Object.keys(updateFields).length) { + throw new Error('You must add at least one update field'); + } + if (updateFields.lastname) { + body.LastName = updateFields.lastname as string; + } + if (updateFields.company) { + body.Company = updateFields.company as string; + } + if (updateFields.email) { + body.Email = updateFields.email as string; + } + if (updateFields.city) { + body.City = updateFields.city as string; + } + if (updateFields.phone) { + body.Phone = updateFields.phone as string; + } + if (updateFields.state) { + body.State = updateFields.state as string; + } + if (updateFields.title) { + body.Title = updateFields.title as string; + } + if (updateFields.jigsaw) { + body.Jigsaw = updateFields.jigsaw as string; + } + if (updateFields.rating) { + body.Rating = updateFields.rating as string; + } + if (updateFields.status) { + body.Status = updateFields.status as string; + } + if (updateFields.street) { + body.Street = updateFields.street as string; + } + if (updateFields.country) { + body.Country = updateFields.country as string; + } + if (updateFields.owner) { + body.OwnerId = updateFields.owner as string; + } + if (updateFields.website) { + body.Website = updateFields.website as string; + } + if (updateFields.industry) { + body.Industry = updateFields.industry as string; + } + if (updateFields.firstName) { + body.FirstName = updateFields.firstName as string; + } + if (updateFields.leadSource) { + body.LeadSource = updateFields.leadSource as string; + } + if (updateFields.postalCode) { + body.PostalCode = updateFields.postalCode as string; + } + if (updateFields.salutation) { + body.Salutation = updateFields.salutation as string; + } + if (updateFields.description) { + body.Description = updateFields.description as string; + } + if (updateFields.annualRevenue) { + body.AnnualRevenue = updateFields.annualRevenue as number; + } + if (updateFields.isUnreadByOwner) { + body.IsUnreadByOwner = updateFields.isUnreadByOwner as boolean; + } + if (updateFields.numberOfEmployees) { + body.NumberOfEmployees = updateFields.numberOfEmployees as number; + } + responseData = await salesforceApiRequest.call(this, 'PATCH', `/sobjects/lead/${leadId}`, body); + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Lead/get-lead-id + if (operation === 'get') { + const leadId = this.getNodeParameter('leadId', i) as string; + responseData = await salesforceApiRequest.call(this, 'GET', `/sobjects/lead/${leadId}`); + } + //https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_query.htm + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const options = this.getNodeParameter('options', i) as IDataObject; + const fields = ['id']; + if (options.fields) { + // @ts-ignore + fields.push(...options.fields.split(',')) + } + try { + if (returnAll) { + qs.q = `SELECT ${fields.join(',')} FROM Lead`, + responseData = await salesforceApiRequestAllItems.call(this, 'records', 'GET', '/query', {}, qs); + } else { + const limit = this.getNodeParameter('limit', i) as number; + qs.q = `SELECT ${fields.join(',')} FROM Lead Limit ${limit}`; + responseData = await salesforceApiRequestAllItems.call(this, 'records', 'GET', '/query', {}, qs); + } + } catch(err) { + throw new Error(`Salesforce Error: ${err}`); + } + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Lead/delete-lead-id + if (operation === 'delete') { + const leadId = this.getNodeParameter('leadId', i) as string; + try { + responseData = await salesforceApiRequest.call(this, 'DELETE', `/sobjects/lead/${leadId}`); + } catch(err) { + throw new Error(`Salesforce Error: ${err}`); + } + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Lead/get-lead + if (operation === 'getSummary') { + responseData = await salesforceApiRequest.call(this, 'GET', '/sobjects/lead'); + } + //https://developer.salesforce.com/docs/api-explorer/sobject/CampaignMember + if (operation === 'addToCampaign') { + const leadId = this.getNodeParameter('leadId', i) as string; + const campaignId = this.getNodeParameter('campaignId', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + const body: ICampaignMember = { + LeadId: leadId, + CampaignId: campaignId, + }; + if (options.status) { + body.Status = options.status as string; + } + responseData = await salesforceApiRequest.call(this, 'POST', '/sobjects/CampaignMember', body); + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Note/post-note + if (operation === 'addNote') { + const leadId = this.getNodeParameter('leadId', i) as string; + const title = this.getNodeParameter('title', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + const body: INote = { + Title: title, + ParentId: leadId, + }; + if (options.body) { + body.Body = options.body as string; + } + if (options.owner) { + body.OwnerId = options.owner as string; + } + if (options.isPrivate) { + body.IsPrivate = options.isPrivate as boolean; + } + responseData = await salesforceApiRequest.call(this, 'POST', '/sobjects/note', body); + } + } + if (resource === 'contact') { + //https://developer.salesforce.com/docs/api-explorer/sobject/Contact/post-contact + if (operation === 'create') { + const lastname = this.getNodeParameter('lastname', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IContact = { + LastName: lastname, + }; + if (additionalFields.fax) { + body.Fax = additionalFields.fax as string; + } + if (additionalFields.email) { + body.Email = additionalFields.email as string; + } + if (additionalFields.phone) { + body.Phone = additionalFields.phone as string; + } + if (additionalFields.title) { + body.Title = additionalFields.title as string; + } + if (additionalFields.jigsaw) { + body.Jigsaw = additionalFields.jigsaw as string; + } + if (additionalFields.owner) { + body.OwnerId = additionalFields.owner as string; + } + if (additionalFields.acconuntId) { + body.AccountId = additionalFields.acconuntId as string; + } + if (additionalFields.birthdate) { + body.Birthdate = additionalFields.birthdate as string; + } + if (additionalFields.firstName) { + body.FirstName = additionalFields.firstName as string; + } + if (additionalFields.homePhone) { + body.HomePhone = additionalFields.homePhone as string; + } + if (additionalFields.otherCity) { + body.OtherCity = additionalFields.otherCity as string; + } + if (additionalFields.department) { + body.Department = additionalFields.department as string; + } + if (additionalFields.leadSource) { + body.LeadSource = additionalFields.leadSource as string; + } + if (additionalFields.otherPhone) { + body.OtherPhone = additionalFields.otherPhone as string; + } + if (additionalFields.otherState) { + body.OtherState = additionalFields.otherState as string; + } + if (additionalFields.salutation) { + body.Salutation = additionalFields.salutation as string; + } + if (additionalFields.description) { + body.Description = additionalFields.description as string; + } + if (additionalFields.mailingCity) { + body.MailingCity = additionalFields.mailingCity as string; + } + if (additionalFields.mobilePhone) { + body.MobilePhone = additionalFields.mobilePhone as string; + } + if (additionalFields.otherStreet) { + body.OtherStreet = additionalFields.otherStreet as string; + } + if (additionalFields.mailingState) { + body.MailingState = additionalFields.mailingState as string; + } + if (additionalFields.otherCountry) { + body.OtherCountry = additionalFields.otherCountry as string; + } + if (additionalFields.assistantName) { + body.AssistantName = additionalFields.assistantName as string; + } + if (additionalFields.mailingStreet) { + body.MailingStreet = additionalFields.mailingStreet as string; + } + if (additionalFields.assistantPhone) { + body.AssistantPhone = additionalFields.assistantPhone as string; + } + if (additionalFields.mailingCountry) { + body.MailingCountry = additionalFields.mailingCountry as string; + } + if (additionalFields.otherPostalCode) { + body.OtherPostalCode = additionalFields.otherPostalCode as string; + } + if (additionalFields.emailBouncedDate) { + body.EmailBouncedDate = additionalFields.emailBouncedDate as string; + } + if (additionalFields.mailingPostalCode) { + body.MailingPostalCode = additionalFields.mailingPostalCode as string; + } + if (additionalFields.emailBouncedReason) { + body.EmailBouncedReason = additionalFields.emailBouncedReason as string; + } + responseData = await salesforceApiRequest.call(this, 'POST', '/sobjects/contact', body); + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Contact/patch-contact-id + if (operation === 'update') { + const contactId = this.getNodeParameter('contactId', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + const body: IContact = {}; + if (!Object.keys(updateFields).length) { + throw new Error('You must add at least one update field'); + } + if (updateFields.fax) { + body.Fax = updateFields.fax as string; + } + if (updateFields.email) { + body.Email = updateFields.email as string; + } + if (updateFields.phone) { + body.Phone = updateFields.phone as string; + } + if (updateFields.title) { + body.Title = updateFields.title as string; + } + if (updateFields.jigsaw) { + body.Jigsaw = updateFields.jigsaw as string; + } + if (updateFields.owner) { + body.OwnerId = updateFields.owner as string; + } + if (updateFields.acconuntId) { + body.AccountId = updateFields.acconuntId as string; + } + if (updateFields.birthdate) { + body.Birthdate = updateFields.birthdate as string; + } + if (updateFields.firstName) { + body.FirstName = updateFields.firstName as string; + } + if (updateFields.homePhone) { + body.HomePhone = updateFields.homePhone as string; + } + if (updateFields.otherCity) { + body.OtherCity = updateFields.otherCity as string; + } + if (updateFields.department) { + body.Department = updateFields.department as string; + } + if (updateFields.leadSource) { + body.LeadSource = updateFields.leadSource as string; + } + if (updateFields.otherPhone) { + body.OtherPhone = updateFields.otherPhone as string; + } + if (updateFields.otherState) { + body.OtherState = updateFields.otherState as string; + } + if (updateFields.salutation) { + body.Salutation = updateFields.salutation as string; + } + if (updateFields.description) { + body.Description = updateFields.description as string; + } + if (updateFields.mailingCity) { + body.MailingCity = updateFields.mailingCity as string; + } + if (updateFields.mobilePhone) { + body.MobilePhone = updateFields.mobilePhone as string; + } + if (updateFields.otherStreet) { + body.OtherStreet = updateFields.otherStreet as string; + } + if (updateFields.mailingState) { + body.MailingState = updateFields.mailingState as string; + } + if (updateFields.otherCountry) { + body.OtherCountry = updateFields.otherCountry as string; + } + if (updateFields.assistantName) { + body.AssistantName = updateFields.assistantName as string; + } + if (updateFields.mailingStreet) { + body.MailingStreet = updateFields.mailingStreet as string; + } + if (updateFields.assistantPhone) { + body.AssistantPhone = updateFields.assistantPhone as string; + } + if (updateFields.mailingCountry) { + body.MailingCountry = updateFields.mailingCountry as string; + } + if (updateFields.otherPostalCode) { + body.OtherPostalCode = updateFields.otherPostalCode as string; + } + if (updateFields.emailBouncedDate) { + body.EmailBouncedDate = updateFields.emailBouncedDate as string; + } + if (updateFields.mailingPostalCode) { + body.MailingPostalCode = updateFields.mailingPostalCode as string; + } + if (updateFields.emailBouncedReason) { + body.EmailBouncedReason = updateFields.emailBouncedReason as string; + } + responseData = await salesforceApiRequest.call(this, 'PATCH', `/sobjects/contact/${contactId}`, body); + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Contact/get-contact-id + if (operation === 'get') { + const contactId = this.getNodeParameter('contactId', i) as string; + responseData = await salesforceApiRequest.call(this, 'GET', `/sobjects/contact/${contactId}`); + } + //https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_query.htm + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const options = this.getNodeParameter('options', i) as IDataObject; + const fields = ['id']; + if (options.fields) { + // @ts-ignore + fields.push(...options.fields.split(',')) + } + try { + if (returnAll) { + qs.q = `SELECT ${fields.join(',')} FROM Contact`, + responseData = await salesforceApiRequestAllItems.call(this, 'records', 'GET', '/query', {}, qs); + } else { + const limit = this.getNodeParameter('limit', i) as number; + qs.q = `SELECT ${fields.join(',')} FROM Contact Limit ${limit}`; + responseData = await salesforceApiRequestAllItems.call(this, 'records', 'GET', '/query', {}, qs); + } + } catch(err) { + throw new Error(`Salesforce Error: ${err}`); + } + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Contact/delete-contact-id + if (operation === 'delete') { + const contactId = this.getNodeParameter('contactId', i) as string; + try { + responseData = await salesforceApiRequest.call(this, 'DELETE', `/sobjects/contact/${contactId}`); + } catch(err) { + throw new Error(`Salesforce Error: ${err}`); + } + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Contact/get-contact + if (operation === 'getSummary') { + responseData = await salesforceApiRequest.call(this, 'GET', '/sobjects/contact'); + } + //https://developer.salesforce.com/docs/api-explorer/sobject/CampaignMember + if (operation === 'addToCampaign') { + const contactId = this.getNodeParameter('contactId', i) as string; + const campaignId = this.getNodeParameter('campaignId', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + const body: ICampaignMember = { + ContactId: contactId, + CampaignId: campaignId, + }; + if (options.status) { + body.Status = options.status as string; + } + responseData = await salesforceApiRequest.call(this, 'POST', '/sobjects/CampaignMember', body); + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Note/post-note + if (operation === 'addNote') { + const contactId = this.getNodeParameter('contactId', i) as string; + const title = this.getNodeParameter('title', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + const body: INote = { + Title: title, + ParentId: contactId, + }; + if (options.body) { + body.Body = options.body as string; + } + if (options.owner) { + body.OwnerId = options.owner as string; + } + if (options.isPrivate) { + body.IsPrivate = options.isPrivate as boolean; + } + responseData = await salesforceApiRequest.call(this, 'POST', '/sobjects/note', body); + } + } + if (resource === 'opportunity') { + //https://developer.salesforce.com/docs/api-explorer/sobject/Opportunity/post-opportunity + if (operation === 'create') { + const name = this.getNodeParameter('name', i) as string; + const closeDate = this.getNodeParameter('closeDate', i) as string; + const stageName = this.getNodeParameter('stageName', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IOpportunity = { + Name: name, + CloseDate: closeDate, + StageName: stageName, + }; + if (additionalFields.type) { + body.Type = additionalFields.type as string; + } + if (additionalFields.ammount) { + body.Amount = additionalFields.ammount as number; + } + if (additionalFields.owner) { + body.OwnerId = additionalFields.owner as string; + } + if (additionalFields.nextStep) { + body.NextStep = additionalFields.nextStep as string; + } + if (additionalFields.accountId) { + body.AccountId = additionalFields.accountId as string; + } + if (additionalFields.campaignId) { + body.CampaignId = additionalFields.campaignId as string; + } + if (additionalFields.leadSource) { + body.LeadSource = additionalFields.leadSource as string; + } + if (additionalFields.description) { + body.Description = additionalFields.description as string; + } + if (additionalFields.probability) { + body.Probability = additionalFields.probability as number; + } + if (additionalFields.pricebook2Id) { + body.Pricebook2Id = additionalFields.pricebook2Id as string; + } + if (additionalFields.forecastCategoryName) { + body.ForecastCategoryName = additionalFields.forecastCategoryName as string; + } + responseData = await salesforceApiRequest.call(this, 'POST', '/sobjects/opportunity', body); + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Opportunity/post-opportunity + if (operation === 'update') { + const opportunityId = this.getNodeParameter('opportunityId', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + const body: IOpportunity = {}; + if (updateFields.name) { + body.Name = updateFields.name as string; + } + if (updateFields.closeDate) { + body.CloseDate = updateFields.closeDate as string; + } + if (updateFields.stageName) { + body.StageName = updateFields.stageName as string; + } + if (updateFields.type) { + body.Type = updateFields.type as string; + } + if (updateFields.ammount) { + body.Amount = updateFields.ammount as number; + } + if (updateFields.owner) { + body.OwnerId = updateFields.owner as string; + } + if (updateFields.nextStep) { + body.NextStep = updateFields.nextStep as string; + } + if (updateFields.accountId) { + body.AccountId = updateFields.accountId as string; + } + if (updateFields.campaignId) { + body.CampaignId = updateFields.campaignId as string; + } + if (updateFields.leadSource) { + body.LeadSource = updateFields.leadSource as string; + } + if (updateFields.description) { + body.Description = updateFields.description as string; + } + if (updateFields.probability) { + body.Probability = updateFields.probability as number; + } + if (updateFields.pricebook2Id) { + body.Pricebook2Id = updateFields.pricebook2Id as string; + } + if (updateFields.forecastCategoryName) { + body.ForecastCategoryName = updateFields.forecastCategoryName as string; + } + responseData = await salesforceApiRequest.call(this, 'PATCH', `/sobjects/opportunity/${opportunityId}`, body); + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Opportunity/get-opportunity-id + if (operation === 'get') { + const opportunityId = this.getNodeParameter('opportunityId', i) as string; + responseData = await salesforceApiRequest.call(this, 'GET', `/sobjects/opportunity/${opportunityId}`); + } + //https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_query.htm + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const options = this.getNodeParameter('options', i) as IDataObject; + const fields = ['id']; + if (options.fields) { + // @ts-ignore + fields.push(...options.fields.split(',')) + } + try { + if (returnAll) { + qs.q = `SELECT ${fields.join(',')} FROM Opportunity`, + responseData = await salesforceApiRequestAllItems.call(this, 'records', 'GET', '/query', {}, qs); + } else { + const limit = this.getNodeParameter('limit', i) as number; + qs.q = `SELECT ${fields.join(',')} FROM Opportunity Limit ${limit}`; + responseData = await salesforceApiRequestAllItems.call(this, 'records', 'GET', '/query', {}, qs); + } + } catch(err) { + throw new Error(`Salesforce Error: ${err}`); + } + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Opportunity/delete-opportunity-id + if (operation === 'delete') { + const opportunityId = this.getNodeParameter('opportunityId', i) as string; + try { + responseData = await salesforceApiRequest.call(this, 'DELETE', `/sobjects/opportunity/${opportunityId}`); + } catch(err) { + throw new Error(`Salesforce Error: ${err}`); + } + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Opportunity/get-opportunity + if (operation === 'getSummary') { + responseData = await salesforceApiRequest.call(this, 'GET', '/sobjects/opportunity'); + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Note/post-note + if (operation === 'addNote') { + const opportunityId = this.getNodeParameter('opportunityId', i) as string; + const title = this.getNodeParameter('title', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + const body: INote = { + Title: title, + ParentId: opportunityId, + }; + if (options.body) { + body.Body = options.body as string; + } + if (options.owner) { + body.OwnerId = options.owner as string; + } + if (options.isPrivate) { + body.IsPrivate = options.isPrivate as boolean; + } + responseData = await salesforceApiRequest.call(this, 'POST', '/sobjects/note', body); + } + } + if (resource === 'account') { + //https://developer.salesforce.com/docs/api-explorer/sobject/Account/post-account + if (operation === 'create') { + const name = this.getNodeParameter('name', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IAccount = { + Name: name, + }; + if (additionalFields.fax) { + body.Fax = additionalFields.fax as string; + } + if (additionalFields.type) { + body.Type = additionalFields.type as string; + } + if (additionalFields.jigsaw) { + body.Jigsaw = additionalFields.jigsaw as string; + } + if (additionalFields.phone) { + body.Phone = additionalFields.phone as string; + } + if (additionalFields.owner) { + body.OwnerId = additionalFields.owner as string; + } + if (additionalFields.sicDesc) { + body.SicDesc = additionalFields.sicDesc as string; + } + if (additionalFields.website) { + body.Website = additionalFields.website as string; + } + if (additionalFields.industry) { + body.Industry = additionalFields.industry as string; + } + if (additionalFields.parentId) { + body.ParentId = additionalFields.parentId as string; + } + if (additionalFields.billingCity) { + body.BillingCity = additionalFields.billingCity as string; + } + if (additionalFields.description) { + body.Description = additionalFields.description as string; + } + if (additionalFields.billingState) { + body.BillingState = additionalFields.billingState as string; + } + if (additionalFields.shippingCity) { + body.ShippingCity = additionalFields.shippingCity as string; + } + if (additionalFields.accountSource) { + body.AccountSource = additionalFields.accountSource as string; + } + if (additionalFields.annualRevenue) { + body.AnnualRevenue = additionalFields.annualRevenue as number; + } + if (additionalFields.billingStreet) { + body.BillingStreet = additionalFields.billingStreet as string; + } + if (additionalFields.shippingState) { + body.ShippingState = additionalFields.shippingState as string; + } + if (additionalFields.billingCountry) { + body.BillingCountry = additionalFields.billingCountry as string; + } + if (additionalFields.shippingStreet) { + body.ShippingStreet = additionalFields.shippingStreet as string; + } + if (additionalFields.shippingCountry) { + body.ShippingCountry = additionalFields.shippingCountry as string; + } + if (additionalFields.billingPostalCode) { + body.BillingPostalCode = additionalFields.billingPostalCode as string; + } + if (additionalFields.numberOfEmployees) { + body.NumberOfEmployees = additionalFields.numberOfEmployees as string; + } + if (additionalFields.shippingPostalCode) { + body.ShippingPostalCode = additionalFields.shippingPostalCode as string; + } + if (additionalFields.shippingPostalCode) { + body.ShippingPostalCode = additionalFields.shippingPostalCode as string; + } + responseData = await salesforceApiRequest.call(this, 'POST', '/sobjects/account', body); + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Account/patch-account-id + if (operation === 'update') { + const accountId = this.getNodeParameter('accountId', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + const body: IAccount = {}; + if (updateFields.name) { + body.Name = updateFields.name as string; + } + if (updateFields.fax) { + body.Fax = updateFields.fax as string; + } + if (updateFields.type) { + body.Type = updateFields.type as string; + } + if (updateFields.jigsaw) { + body.Jigsaw = updateFields.jigsaw as string; + } + if (updateFields.phone) { + body.Phone = updateFields.phone as string; + } + if (updateFields.owner) { + body.OwnerId = updateFields.owner as string; + } + if (updateFields.sicDesc) { + body.SicDesc = updateFields.sicDesc as string; + } + if (updateFields.website) { + body.Website = updateFields.website as string; + } + if (updateFields.industry) { + body.Industry = updateFields.industry as string; + } + if (updateFields.parentId) { + body.ParentId = updateFields.parentId as string; + } + if (updateFields.billingCity) { + body.BillingCity = updateFields.billingCity as string; + } + if (updateFields.description) { + body.Description = updateFields.description as string; + } + if (updateFields.billingState) { + body.BillingState = updateFields.billingState as string; + } + if (updateFields.shippingCity) { + body.ShippingCity = updateFields.shippingCity as string; + } + if (updateFields.accountSource) { + body.AccountSource = updateFields.accountSource as string; + } + if (updateFields.annualRevenue) { + body.AnnualRevenue = updateFields.annualRevenue as number; + } + if (updateFields.billingStreet) { + body.BillingStreet = updateFields.billingStreet as string; + } + if (updateFields.shippingState) { + body.ShippingState = updateFields.shippingState as string; + } + if (updateFields.billingCountry) { + body.BillingCountry = updateFields.billingCountry as string; + } + if (updateFields.shippingStreet) { + body.ShippingStreet = updateFields.shippingStreet as string; + } + if (updateFields.shippingCountry) { + body.ShippingCountry = updateFields.shippingCountry as string; + } + if (updateFields.billingPostalCode) { + body.BillingPostalCode = updateFields.billingPostalCode as string; + } + if (updateFields.numberOfEmployees) { + body.NumberOfEmployees = updateFields.numberOfEmployees as string; + } + if (updateFields.shippingPostalCode) { + body.ShippingPostalCode = updateFields.shippingPostalCode as string; + } + if (updateFields.shippingPostalCode) { + body.ShippingPostalCode = updateFields.shippingPostalCode as string; + } + responseData = await salesforceApiRequest.call(this, 'PATCH', `/sobjects/account/${accountId}`, body); + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Account/get-account-id + if (operation === 'get') { + const accountId = this.getNodeParameter('accountId', i) as string; + responseData = await salesforceApiRequest.call(this, 'GET', `/sobjects/account/${accountId}`); + } + //https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_query.htm + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const options = this.getNodeParameter('options', i) as IDataObject; + const fields = ['id']; + if (options.fields) { + // @ts-ignore + fields.push(...options.fields.split(',')) + } + try { + if (returnAll) { + qs.q = `SELECT ${fields.join(',')} FROM Account`, + responseData = await salesforceApiRequestAllItems.call(this, 'records', 'GET', '/query', {}, qs); + } else { + const limit = this.getNodeParameter('limit', i) as number; + qs.q = `SELECT ${fields.join(',')} FROM Account Limit ${limit}`; + responseData = await salesforceApiRequestAllItems.call(this, 'records', 'GET', '/query', {}, qs); + } + } catch(err) { + throw new Error(`Salesforce Error: ${err}`); + } + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Account/delete-account-id + if (operation === 'delete') { + const accountId = this.getNodeParameter('accountId', i) as string; + try { + responseData = await salesforceApiRequest.call(this, 'DELETE', `/sobjects/account/${accountId}`); + } catch(err) { + throw new Error(`Salesforce Error: ${err}`); + } + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Account/get-account + if (operation === 'getSummary') { + responseData = await salesforceApiRequest.call(this, 'GET', '/sobjects/account'); + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Note/post-note + if (operation === 'addNote') { + const accountId = this.getNodeParameter('accountId', i) as string; + const title = this.getNodeParameter('title', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + const body: INote = { + Title: title, + ParentId: accountId, + }; + if (options.body) { + body.Body = options.body as string; + } + if (options.owner) { + body.OwnerId = options.owner as string; + } + if (options.isPrivate) { + body.IsPrivate = options.isPrivate as boolean; + } + responseData = await salesforceApiRequest.call(this, 'POST', '/sobjects/note', body); + } + } + if (resource === 'case') { + //https://developer.salesforce.com/docs/api-explorer/sobject/Case/post-case + if (operation === 'create') { + const type = this.getNodeParameter('type', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: ICase = { + Type: type, + }; + if (additionalFields.origin) { + body.Origin = additionalFields.origin as string; + } + if (additionalFields.reason) { + body.Reason = additionalFields.reason as string; + } + if (additionalFields.owner) { + body.OwnerId = additionalFields.owner as string; + } + if (additionalFields.subject) { + body.Subject = additionalFields.subject as string; + } + if (additionalFields.parentId) { + body.ParentId = additionalFields.parentId as string; + } + if (additionalFields.priority) { + body.Priority = additionalFields.priority as string; + } + if (additionalFields.accountId) { + body.AccountId = additionalFields.accountId as string; + } + if (additionalFields.contactId) { + body.ContactId = additionalFields.contactId as string; + } + if (additionalFields.description) { + body.Description = additionalFields.description as string; + } + if (additionalFields.isEscalated) { + body.IsEscalated = additionalFields.isEscalated as boolean; + } + if (additionalFields.suppliedName) { + body.SuppliedName = additionalFields.suppliedName as string; + } + if (additionalFields.suppliedEmail) { + body.SuppliedEmail = additionalFields.suppliedEmail as string; + } + if (additionalFields.suppliedPhone) { + body.SuppliedPhone = additionalFields.suppliedPhone as string; + } + if (additionalFields.suppliedCompany) { + body.SuppliedCompany = additionalFields.suppliedCompany as string; + } + responseData = await salesforceApiRequest.call(this, 'POST', '/sobjects/case', body); + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Case/patch-case-id + if (operation === 'update') { + const caseId = this.getNodeParameter('caseId', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + const body: ICase = {}; + if (updateFields.type) { + body.Type = updateFields.type as string; + } + if (updateFields.origin) { + body.Origin = updateFields.origin as string; + } + if (updateFields.reason) { + body.Reason = updateFields.reason as string; + } + if (updateFields.owner) { + body.OwnerId = updateFields.owner as string; + } + if (updateFields.subject) { + body.Subject = updateFields.subject as string; + } + if (updateFields.parentId) { + body.ParentId = updateFields.parentId as string; + } + if (updateFields.priority) { + body.Priority = updateFields.priority as string; + } + if (updateFields.accountId) { + body.AccountId = updateFields.accountId as string; + } + if (updateFields.contactId) { + body.ContactId = updateFields.contactId as string; + } + if (updateFields.description) { + body.Description = updateFields.description as string; + } + if (updateFields.isEscalated) { + body.IsEscalated = updateFields.isEscalated as boolean; + } + if (updateFields.suppliedName) { + body.SuppliedName = updateFields.suppliedName as string; + } + if (updateFields.suppliedEmail) { + body.SuppliedEmail = updateFields.suppliedEmail as string; + } + if (updateFields.suppliedPhone) { + body.SuppliedPhone = updateFields.suppliedPhone as string; + } + if (updateFields.suppliedCompany) { + body.SuppliedCompany = updateFields.suppliedCompany as string; + } + responseData = await salesforceApiRequest.call(this, 'PATCH', `/sobjects/case/${caseId}`, body); + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Case/get-case-id + if (operation === 'get') { + const caseId = this.getNodeParameter('caseId', i) as string; + responseData = await salesforceApiRequest.call(this, 'GET', `/sobjects/case/${caseId}`); + } + //https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_query.htm + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const options = this.getNodeParameter('options', i) as IDataObject; + const fields = ['id']; + if (options.fields) { + // @ts-ignore + fields.push(...options.fields.split(',')) + } + try { + if (returnAll) { + qs.q = `SELECT ${fields.join(',')} FROM Case`, + responseData = await salesforceApiRequestAllItems.call(this, 'records', 'GET', '/query', {}, qs); + } else { + const limit = this.getNodeParameter('limit', i) as number; + qs.q = `SELECT ${fields.join(',')} FROM Case Limit ${limit}`; + responseData = await salesforceApiRequestAllItems.call(this, 'records', 'GET', '/query', {}, qs); + } + } catch(err) { + throw new Error(`Salesforce Error: ${err}`); + } + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Case/delete-case-id + if (operation === 'delete') { + const caseId = this.getNodeParameter('caseId', i) as string; + try { + responseData = await salesforceApiRequest.call(this, 'DELETE', `/sobjects/case/${caseId}`); + } catch(err) { + throw new Error(`Salesforce Error: ${err}`); + } + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Case/get-case + if (operation === 'getSummary') { + responseData = await salesforceApiRequest.call(this, 'GET', '/sobjects/case'); + } + //https://developer.salesforce.com/docs/api-explorer/sobject/CaseComment/post-casecomment + if (operation === 'addComment') { + const caseId = this.getNodeParameter('caseId', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + const body: ICaseComment = { + ParentId: caseId, + }; + if (options.commentBody) { + body.CommentBody = options.commentBody as string; + } + if (options.isPublished) { + body.IsPublished = options.isPublished as boolean; + } + responseData = await salesforceApiRequest.call(this, 'POST', '/sobjects/casecomment', body); + } + } + if (resource === 'task') { + //https://developer.salesforce.com/docs/api-explorer/sobject/Task/post-task + if (operation === 'create') { + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const status = this.getNodeParameter('status', i) as string; + const body: ITask = { + Status: status, + }; + if (additionalFields.whoId) { + body.WhoId = additionalFields.whoId as string; + } + if (additionalFields.whatId) { + body.WhatId = additionalFields.whatId as string; + } + if (additionalFields.owner) { + body.OwnerId = additionalFields.owner as string; + } + if (additionalFields.subject) { + body.Subject = additionalFields.subject as string; + } + if (additionalFields.callType) { + body.CallType = additionalFields.callType as string; + } + if (additionalFields.priority) { + body.Priority = additionalFields.priority as string; + } + if (additionalFields.callObject) { + body.CallObject = additionalFields.callObject as string; + } + if (additionalFields.description) { + body.Description = additionalFields.description as string; + } + if (additionalFields.activityDate) { + body.ActivityDate = additionalFields.activityDate as string; + } + if (additionalFields.isReminderSet) { + body.IsReminderSet = additionalFields.isReminderSet as boolean; + } + if (additionalFields.recurrenceType) { + body.RecurrenceType = additionalFields.recurrenceType as string; + } + if (additionalFields.callDisposition) { + body.CallDisposition = additionalFields.callDisposition as string; + } + if (additionalFields.reminderDateTime) { + body.ReminderDateTime = additionalFields.reminderDateTime as string; + } + if (additionalFields.recurrenceInstance) { + body.RecurrenceInstance = additionalFields.recurrenceInstance as string; + } + if (additionalFields.recurrenceInterval) { + body.RecurrenceInterval = additionalFields.recurrenceInterval as number; + } + if (additionalFields.recurrenceDayOfMonth) { + body.RecurrenceDayOfMonth = additionalFields.recurrenceDayOfMonth as number; + } + if (additionalFields.callDurationInSeconds) { + body.CallDurationInSeconds = additionalFields.callDurationInSeconds as number; + } + if (additionalFields.recurrenceEndDateOnly) { + body.RecurrenceEndDateOnly = additionalFields.recurrenceEndDateOnly as string; + } + if (additionalFields.recurrenceMonthOfYear) { + body.RecurrenceMonthOfYear = additionalFields.recurrenceMonthOfYear as string; + } + if (additionalFields.recurrenceDayOfWeekMask) { + body.RecurrenceDayOfWeekMask = additionalFields.recurrenceDayOfWeekMask as string; + } + if (additionalFields.recurrenceStartDateOnly) { + body.RecurrenceStartDateOnly = additionalFields.recurrenceStartDateOnly as string; + } + if (additionalFields.recurrenceTimeZoneSidKey) { + body.RecurrenceTimeZoneSidKey = additionalFields.recurrenceTimeZoneSidKey as string; + } + if (additionalFields.recurrenceRegeneratedType) { + body.RecurrenceRegeneratedType = additionalFields.recurrenceRegeneratedType as string; + } + responseData = await salesforceApiRequest.call(this, 'POST', '/sobjects/task', body); + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Task/patch-task-id + if (operation === 'update') { + const taskId = this.getNodeParameter('taskId', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + const body: ITask = {}; + if (updateFields.whoId) { + body.WhoId = updateFields.whoId as string; + } + if (updateFields.status) { + body.Status = updateFields.status as string; + } + if (updateFields.whatId) { + body.WhatId = updateFields.whatId as string; + } + if (updateFields.owner) { + body.OwnerId = updateFields.owner as string; + } + if (updateFields.subject) { + body.Subject = updateFields.subject as string; + } + if (updateFields.callType) { + body.CallType = updateFields.callType as string; + } + if (updateFields.priority) { + body.Priority = updateFields.priority as string; + } + if (updateFields.callObject) { + body.CallObject = updateFields.callObject as string; + } + if (updateFields.description) { + body.Description = updateFields.description as string; + } + if (updateFields.activityDate) { + body.ActivityDate = updateFields.activityDate as string; + } + if (updateFields.isReminderSet) { + body.IsReminderSet = updateFields.isReminderSet as boolean; + } + if (updateFields.recurrenceType) { + body.RecurrenceType = updateFields.recurrenceType as string; + } + if (updateFields.callDisposition) { + body.CallDisposition = updateFields.callDisposition as string; + } + if (updateFields.reminderDateTime) { + body.ReminderDateTime = updateFields.reminderDateTime as string; + } + if (updateFields.recurrenceInstance) { + body.RecurrenceInstance = updateFields.recurrenceInstance as string; + } + if (updateFields.recurrenceInterval) { + body.RecurrenceInterval = updateFields.recurrenceInterval as number; + } + if (updateFields.recurrenceDayOfMonth) { + body.RecurrenceDayOfMonth = updateFields.recurrenceDayOfMonth as number; + } + if (updateFields.callDurationInSeconds) { + body.CallDurationInSeconds = updateFields.callDurationInSeconds as number; + } + if (updateFields.recurrenceEndDateOnly) { + body.RecurrenceEndDateOnly = updateFields.recurrenceEndDateOnly as string; + } + if (updateFields.recurrenceMonthOfYear) { + body.RecurrenceMonthOfYear = updateFields.recurrenceMonthOfYear as string; + } + if (updateFields.recurrenceDayOfWeekMask) { + body.RecurrenceDayOfWeekMask = updateFields.recurrenceDayOfWeekMask as string; + } + if (updateFields.recurrenceStartDateOnly) { + body.RecurrenceStartDateOnly = updateFields.recurrenceStartDateOnly as string; + } + if (updateFields.recurrenceTimeZoneSidKey) { + body.RecurrenceTimeZoneSidKey = updateFields.recurrenceTimeZoneSidKey as string; + } + if (updateFields.recurrenceRegeneratedType) { + body.RecurrenceRegeneratedType = updateFields.recurrenceRegeneratedType as string; + } + responseData = await salesforceApiRequest.call(this, 'PATCH', `/sobjects/task/${taskId}`, body); + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Task/get-task-id + if (operation === 'get') { + const taskId = this.getNodeParameter('taskId', i) as string; + responseData = await salesforceApiRequest.call(this, 'GET', `/sobjects/task/${taskId}`); + } + //https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_query.htm + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const options = this.getNodeParameter('options', i) as IDataObject; + const fields = ['id']; + if (options.fields) { + // @ts-ignore + fields.push(...options.fields.split(',')) + } + try { + if (returnAll) { + qs.q = `SELECT ${fields.join(',')} FROM Task`, + responseData = await salesforceApiRequestAllItems.call(this, 'records', 'GET', '/query', {}, qs); + } else { + const limit = this.getNodeParameter('limit', i) as number; + qs.q = `SELECT ${fields.join(',')} FROM Task Limit ${limit}`; + responseData = await salesforceApiRequestAllItems.call(this, 'records', 'GET', '/query', {}, qs); + } + } catch(err) { + throw new Error(`Salesforce Error: ${err}`); + } + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Task/delete-task-id + if (operation === 'delete') { + const taskId = this.getNodeParameter('taskId', i) as string; + try { + responseData = await salesforceApiRequest.call(this, 'DELETE', `/sobjects/task/${taskId}`); + } catch(err) { + throw new Error(`Salesforce Error: ${err}`); + } + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Task/get-task + if (operation === 'getSummary') { + responseData = await salesforceApiRequest.call(this, 'GET', '/sobjects/task'); + } + } + if (resource === 'attachment') { + //https://developer.salesforce.com/docs/api-explorer/sobject/Attachment/post-attachment + if (operation === 'create') { + const name = this.getNodeParameter('name', i) as string; + const parentId = this.getNodeParameter('parentId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string; + const body: IAttachment = { + Name: name, + ParentId: parentId, + }; + if (items[i].binary && items[i].binary![binaryPropertyName]) { + body.Body = items[i].binary![binaryPropertyName].data; + body.ContentType = items[i].binary![binaryPropertyName].mimeType; + } else { + throw new Error(`The property ${binaryPropertyName} does not exist`); + } + if (additionalFields.description) { + body.Description = additionalFields.description as string; + } + if (additionalFields.owner) { + body.OwnerId = additionalFields.owner as string; + } + if (additionalFields.isPrivate) { + body.IsPrivate = additionalFields.isPrivate as boolean; + } + responseData = await salesforceApiRequest.call(this, 'POST', '/sobjects/attachment', body); + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Attachment/patch-attachment-id + if (operation === 'update') { + const attachmentId = this.getNodeParameter('attachmentId', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + const body: IAttachment = {}; + if (updateFields.binaryPropertyName) { + const binaryPropertyName = updateFields.binaryPropertyName as string; + if (items[i].binary && items[i].binary![binaryPropertyName]) { + body.Body = items[i].binary![binaryPropertyName].data; + body.ContentType = items[i].binary![binaryPropertyName].mimeType; + } else { + throw new Error(`The property ${binaryPropertyName} does not exist`); + } + } + if (updateFields.name) { + body.Name = updateFields.name as string; + } + if (updateFields.description) { + body.Description = updateFields.description as string; + } + if (updateFields.owner) { + body.OwnerId = updateFields.owner as string; + } + if (updateFields.isPrivate) { + body.IsPrivate = updateFields.isPrivate as boolean; + } + responseData = await salesforceApiRequest.call(this, 'PATCH', `/sobjects/attachment/${attachmentId}`, body); + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Attachment/get-attachment-id + if (operation === 'get') { + const attachmentId = this.getNodeParameter('attachmentId', i) as string; + responseData = await salesforceApiRequest.call(this, 'GET', `/sobjects/attachment/${attachmentId}`); + } + //https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_query.htm + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const options = this.getNodeParameter('options', i) as IDataObject; + const fields = ['id']; + if (options.fields) { + // @ts-ignore + fields.push(...options.fields.split(',')) + } + try { + if (returnAll) { + qs.q = `SELECT ${fields.join(',')} FROM Attachment`, + responseData = await salesforceApiRequestAllItems.call(this, 'records', 'GET', '/query', {}, qs); + } else { + const limit = this.getNodeParameter('limit', i) as number; + qs.q = `SELECT ${fields.join(',')} FROM Attachment Limit ${limit}`; + responseData = await salesforceApiRequestAllItems.call(this, 'records', 'GET', '/query', {}, qs); + } + } catch(err) { + throw new Error(`Salesforce Error: ${err}`); + } + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Attachment/delete-attachment-id + if (operation === 'delete') { + const attachmentId = this.getNodeParameter('attachmentId', i) as string; + try { + responseData = await salesforceApiRequest.call(this, 'DELETE', `/sobjects/attachment/${attachmentId}`); + } catch(err) { + throw new Error(`Salesforce Error: ${err}`); + } + } + //https://developer.salesforce.com/docs/api-explorer/sobject/Attachment/get-attachment-id + if (operation === 'getSummary') { + responseData = await salesforceApiRequest.call(this, 'GET', '/sobjects/attachment'); + } + } + 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/Salesforce/TaskDescription.ts b/packages/nodes-base/nodes/Salesforce/TaskDescription.ts new file mode 100644 index 0000000000..79e13ec34f --- /dev/null +++ b/packages/nodes-base/nodes/Salesforce/TaskDescription.ts @@ -0,0 +1,810 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const taskOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'task', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a task', + }, + { + name: 'Update', + value: 'update', + description: 'Update a task', + }, + { + name: 'Get', + value: 'get', + description: 'Get a task', + }, + { + name: 'Get Summary', + value: 'getSummary', + description: `Returns an overview of task's metadata.`, + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all tasks', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a task', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const taskFields = [ + +/* -------------------------------------------------------------------------- */ +/* task:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Status', + name: 'status', + type: 'options', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'create', + ], + }, + }, + typeOptions: { + loadOptionsMethod: 'getTaskStatuses', + }, + description: 'The current status of the task, such as In Progress or Completed.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Who Id', + name: 'whoId', + type: 'string', + default: '', + description: `The WhoId represents a human such as a lead or a contact.
+ WhoIds are polymorphic. Polymorphic means a WhoId is equivalent to a contact’s ID or a lead’s ID.`, + }, + { + displayName: 'What Id', + name: 'whatId', + type: 'string', + default: '', + description: `The WhatId represents nonhuman objects such as accounts, opportunities,
+ campaigns, cases, or custom objects. WhatIds are polymorphic. Polymorphic means a
+ WhatId is equivalent to the ID of a related object.`, + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'ID of the User who owns the record.', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getTaskSubjects', + }, + description: 'The subject line of the task, such as “Call” or “Send Quote.” Limit: 255 characters.', + }, + { + displayName: 'Call Type', + name: 'callType', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getTaskCallTypes', + }, + description: 'The type of call being answered: Inbound, Internal, or Outbound.', + }, + { + displayName: 'Priority', + name: 'priority', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getTaskPriorities', + }, + description: `Indicates the importance or urgency of a task, such as high or low.`, + }, + { + displayName: 'Call Object', + name: 'callObject', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: `Name of a call center. Limit is 255 characters.
+ Not subject to field-level security, available for any user in an
+ organization with Salesforce CRM Call Center.`, + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + description: 'Contains a text description of the task.', + }, + { + displayName: 'Activity Date', + name: 'activityDate', + type: 'dateTime', + default: '', + description: `Represents the due date of the task.
+ This field has a timestamp that is always set to midnight
+ in the Coordinated Universal Time (UTC) time zone.`, + }, + { + displayName: 'Is ReminderSet', + name: 'isReminderSet', + type: 'boolean', + default: false, + description: 'Indicates whether a popup reminder has been set for the task (true) or not (false).', + }, + { + displayName: 'Recurrence Type', + name: 'recurrenceType', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getTaskRecurrenceTypes' + }, + description: 'Website for the task.', + }, + { + displayName: 'Call Disposition', + name: 'callDisposition', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: `Represents the result of a given call, for example, “we'll call back,” or “call
+ unsuccessful.” Limit is 255 characters. Not subject to field-level security, available for any user
+ in an organization with Salesforce CRM Call Center.`, + }, + { + displayName: 'Reminder Date Time', + name: 'reminderDateTime', + type: 'dateTime', + default: '', + description: `Represents the time when the reminder is scheduled to fire,
+ if IsReminderSet is set to true. If IsReminderSet is set to false, then the
+ user may have deselected the reminder checkbox in the Salesforce user interface,
+ or the reminder has already fired at the time indicated by the value.`, + }, + { + displayName: 'Recurrence Instance', + name: 'recurrenceInstance', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTaskRecurrenceInstances', + }, + default: '', + description: `The frequency of the recurring task. For example, “2nd” or “3rd.”`, + }, + { + displayName: 'Recurrence Interval', + name: 'recurrenceInterval', + type: 'number', + default: '', + description: 'The interval between recurring tasks.', + }, + { + displayName: 'Recurrence Day Of Month', + name: 'recurrenceDayOfMonth', + type: 'number', + default: '', + description: 'The day of the month in which the task repeats.', + }, + { + displayName: 'Call Duration In Seconds', + name: 'callDurationInSeconds', + type: 'number', + default: '', + description: `Duration of the call in seconds. Not subject to field-level security,
+ available for any user in an organization with Salesforce CRM Call Cente`, + }, + { + displayName: 'Recurrence End Date Only', + name: 'recurrenceEndDateOnly', + type: 'dateTime', + default: '', + description: `The last date on which the task repeats. This field has a timestamp that
+ is always set to midnight in the Coordinated Universal Time (UTC) time zone.`, + }, + { + displayName: 'Recurrence Month Of Year', + name: 'recurrenceMonthOfYear', + type: 'options', + options: [ + { + name: 'January', + value: 'January' + }, + { + name: 'February', + value: 'February' + }, + { + name: 'March', + value: 'March' + }, + { + name: 'April', + value: 'April' + }, + { + name: 'May', + value: 'May' + }, + { + name: 'June', + value: 'June' + }, + { + name: 'July', + value: 'July' + }, + { + name: 'August', + value: 'August' + }, + { + name: 'September', + value: 'September' + }, + { + name: 'October', + value: 'October' + }, + { + name: 'November', + value: 'November' + }, + { + name: 'December', + value: 'December' + } + ], + default: '', + description: 'The month of the year in which the task repeats.', + }, + { + displayName: 'Recurrence Day Of Week Mask', + name: 'recurrenceDayOfWeekMask', + type: 'number', + default: '', + description: `The day or days of the week on which the task repeats.
+ This field contains a bitmask. The values are as follows: Sunday = 1 Monday = 2
+ Tuesday = 4 Wednesday = 8 Thursday = 16 Friday = 32 Saturday = 64
+ Multiple days are represented as the sum of their numerical values.
+ For example, Tuesday and Thursday = 4 + 16 = 20.`, + }, + { + displayName: 'recurrence Start Date Only', + name: 'recurrenceEndDateOnly', + type: 'dateTime', + default: '', + description: `The date when the recurring task begins.
+ Must be a date and time before RecurrenceEndDateOnly.`, + }, + { + displayName: 'Recurrence TimeZone SidKey', + name: 'recurrenceTimeZoneSidKey', + type: 'string', + default: '', + description: `The time zone associated with the recurring task.
+ For example, “UTC-8:00” for Pacific Standard Time.`, + }, + { + displayName: 'Recurrence Regenerated Type', + name: 'recurrenceRegeneratedType', + type: 'options', + default: '', + options: [ + { + name: 'After due date', + value: 'RecurrenceRegenerateAfterDueDate' + }, + { + name: 'After date completed', + value: 'RecurrenceRegenerateAfterToday' + }, + { + name: '(Task Closed)', + value: 'RecurrenceRegenerated' + } + ], + description: `Represents what triggers a repeating task to repeat.
+ Add this field to a page layout together with the RecurrenceInterval field,
+ which determines the number of days between the triggering date (due date or close date)
+ and the due date of the next repeating task in the series.Label is Repeat This Task.`, + }, + ] + }, +/* -------------------------------------------------------------------------- */ +/* task:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Task ID', + name: 'taskId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'update', + ] + }, + }, + description: 'Id of task that needs to be fetched', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Who Id', + name: 'whoId', + type: 'string', + default: '', + description: `The WhoId represents a human such as a lead or a contact.
+ WhoIds are polymorphic. Polymorphic means a WhoId is equivalent to a contact’s ID or a lead’s ID.`, + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getTaskStatuses', + }, + description: 'The current status of the task, such as In Progress or Completed.', + }, + { + displayName: 'What Id', + name: 'whatId', + type: 'string', + default: '', + description: `The WhatId represents nonhuman objects such as accounts, opportunities,
+ campaigns, cases, or custom objects. WhatIds are polymorphic. Polymorphic means a
+ WhatId is equivalent to the ID of a related object.`, + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'ID of the User who owns the record.', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getTaskSubjects', + }, + description: 'The subject line of the task, such as “Call” or “Send Quote.” Limit: 255 characters.', + }, + { + displayName: 'Call Type', + name: 'callType', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getTaskCallTypes', + }, + description: 'The type of call being answered: Inbound, Internal, or Outbound.', + }, + { + displayName: 'Priority', + name: 'priority', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getTaskPriorities', + }, + description: `Indicates the importance or urgency of a task, such as high or low.`, + }, + { + displayName: 'Call Object', + name: 'callObject', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: `Name of a call center. Limit is 255 characters.
+ Not subject to field-level security, available for any user in an
+ organization with Salesforce CRM Call Center.`, + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + description: 'Contains a text description of the task.', + }, + { + displayName: 'Activity Date', + name: 'activityDate', + type: 'dateTime', + default: '', + description: `Represents the due date of the task.
+ This field has a timestamp that is always set to midnight
+ in the Coordinated Universal Time (UTC) time zone.`, + }, + { + displayName: 'Is ReminderSet', + name: 'isReminderSet', + type: 'boolean', + default: false, + description: 'Indicates whether a popup reminder has been set for the task (true) or not (false).', + }, + { + displayName: 'Recurrence Type', + name: 'recurrenceType', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getTaskRecurrenceTypes' + }, + description: 'Website for the task.', + }, + { + displayName: 'Call Disposition', + name: 'callDisposition', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: `Represents the result of a given call, for example, “we'll call back,” or “call
+ unsuccessful.” Limit is 255 characters. Not subject to field-level security, available for any user
+ in an organization with Salesforce CRM Call Center.`, + }, + { + displayName: 'Reminder Date Time', + name: 'reminderDateTime', + type: 'dateTime', + default: '', + description: `Represents the time when the reminder is scheduled to fire,
+ if IsReminderSet is set to true. If IsReminderSet is set to false, then the
+ user may have deselected the reminder checkbox in the Salesforce user interface,
+ or the reminder has already fired at the time indicated by the value.`, + }, + { + displayName: 'Recurrence Instance', + name: 'recurrenceInstance', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTaskRecurrenceInstances', + }, + default: '', + description: `The frequency of the recurring task. For example, “2nd” or “3rd.”`, + }, + { + displayName: 'Recurrence Interval', + name: 'recurrenceInterval', + type: 'number', + default: '', + description: 'The interval between recurring tasks.', + }, + { + displayName: 'Recurrence Day Of Month', + name: 'recurrenceDayOfMonth', + type: 'number', + default: '', + description: 'The day of the month in which the task repeats.', + }, + { + displayName: 'Call Duration In Seconds', + name: 'callDurationInSeconds', + type: 'number', + default: '', + description: `Duration of the call in seconds. Not subject to field-level security,
+ available for any user in an organization with Salesforce CRM Call Cente`, + }, + { + displayName: 'Recurrence End Date Only', + name: 'recurrenceEndDateOnly', + type: 'dateTime', + default: '', + description: `The last date on which the task repeats. This field has a timestamp that
+ is always set to midnight in the Coordinated Universal Time (UTC) time zone.`, + }, + { + displayName: 'Recurrence Month Of Year', + name: 'recurrenceMonthOfYear', + type: 'options', + options: [ + { + name: 'January', + value: 'January' + }, + { + name: 'February', + value: 'February' + }, + { + name: 'March', + value: 'March' + }, + { + name: 'April', + value: 'April' + }, + { + name: 'May', + value: 'May' + }, + { + name: 'June', + value: 'June' + }, + { + name: 'July', + value: 'July' + }, + { + name: 'August', + value: 'August' + }, + { + name: 'September', + value: 'September' + }, + { + name: 'October', + value: 'October' + }, + { + name: 'November', + value: 'November' + }, + { + name: 'December', + value: 'December' + } + ], + default: '', + description: 'The month of the year in which the task repeats.', + }, + { + displayName: 'Recurrence Day Of Week Mask', + name: 'recurrenceDayOfWeekMask', + type: 'number', + default: '', + description: `The day or days of the week on which the task repeats.
+ This field contains a bitmask. The values are as follows: Sunday = 1 Monday = 2
+ Tuesday = 4 Wednesday = 8 Thursday = 16 Friday = 32 Saturday = 64
+ Multiple days are represented as the sum of their numerical values.
+ For example, Tuesday and Thursday = 4 + 16 = 20.`, + }, + { + displayName: 'recurrence Start Date Only', + name: 'recurrenceEndDateOnly', + type: 'dateTime', + default: '', + description: `The date when the recurring task begins.
+ Must be a date and time before RecurrenceEndDateOnly.`, + }, + { + displayName: 'Recurrence TimeZone SidKey', + name: 'recurrenceTimeZoneSidKey', + type: 'string', + default: '', + description: `The time zone associated with the recurring task.
+ For example, “UTC-8:00” for Pacific Standard Time.`, + }, + { + displayName: 'Recurrence Regenerated Type', + name: 'recurrenceRegeneratedType', + type: 'options', + default: '', + options: [ + { + name: 'After due date', + value: 'RecurrenceRegenerateAfterDueDate' + }, + { + name: 'After date completed', + value: 'RecurrenceRegenerateAfterToday' + }, + { + name: '(Task Closed)', + value: 'RecurrenceRegenerated' + } + ], + description: `Represents what triggers a repeating task to repeat.
+ Add this field to a page layout together with the RecurrenceInterval field,
+ which determines the number of days between the triggering date (due date or close date)
+ and the due date of the next repeating task in the series.Label is Repeat This Task.`, + }, + ] + }, + +/* -------------------------------------------------------------------------- */ +/* task:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Task ID', + name: 'taskId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'get', + ] + }, + }, + description: 'Id of task that needs to be fetched', + }, +/* -------------------------------------------------------------------------- */ +/* task:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Task ID', + name: 'taskId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'delete', + ] + }, + }, + description: 'Id of task that needs to be fetched', + }, +/* -------------------------------------------------------------------------- */ +/* task:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'Fields to include separated by ,', + }, + ] + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Salesforce/TaskInterface.ts b/packages/nodes-base/nodes/Salesforce/TaskInterface.ts new file mode 100644 index 0000000000..06f932e799 --- /dev/null +++ b/packages/nodes-base/nodes/Salesforce/TaskInterface.ts @@ -0,0 +1,27 @@ + +export interface ITask { + WhoId?: string; + Status?: string; + WhatId?: string; + OwnerId?: string; + Subject?: string; + CallType?: string; + Priority?: string; + CallObject?: string; + Description?: string; + ActivityDate?: string; + IsReminderSet?: boolean; + RecurrenceType?: string; + CallDisposition?:string; + ReminderDateTime?: string; + RecurrenceInstance?: string; + RecurrenceInterval?: number; + RecurrenceDayOfMonth?: number; + CallDurationInSeconds?: number; + RecurrenceEndDateOnly?: string; + RecurrenceMonthOfYear?: string; + RecurrenceDayOfWeekMask?: string; + RecurrenceStartDateOnly?: string; + RecurrenceTimeZoneSidKey?: string; + RecurrenceRegeneratedType?: string; +} diff --git a/packages/nodes-base/nodes/Salesforce/salesforce.png b/packages/nodes-base/nodes/Salesforce/salesforce.png new file mode 100644 index 0000000000000000000000000000000000000000..17876e8dea85a243646b59dc3590f58e1f938232 GIT binary patch literal 3210 zcmd5<`9DtaMj_0Yv5b8wP0C)@ zY}uEYF*6ujma)$m^EvbWen0=i=e+K7Kj(E{&vjk*bHC2{;l$ZknI1fF^Z)<=2VrJL zaB#I_-#naPKJ{gC4O}<^;iiT_X}8220B`~}7Iwy_eilG}N6nO69OA9698Feg8|(p5Oo>3|oZF0{h7U)yP0Li382n z`M3CgrXY8cO$XJ`)8HPE2nM*H1EhdGW5)lV`w47=%n8b7ks%lgsL8fKf`SwdG#P>> z134rB>>u<(kvQ2WVv9h~;OW>Ik~_l*4k4S!$wnz`9Ttl-n+RkRK?;yT0J5k68k_)e zABxC{B(lJfW)Xl)0vCv86M%F)@Cgqomg&hhNkFGSfQ0Er6Na z+hf&CZ$gS!Ah}e(_Qhi2`jI@6b?xlvDqSR>WK%mE*|R)BqcfO$$Z;BCc(stY9^AR~ zXK5#Gght)mt)^_05Z4>1TQ1EDcbgYTYYZTH49LV=FP=*UzY8D;ZeyIX3@wjV;mM4Z{6*@1wR-pUg3zc9m@Y z^&X6Vo#EDb@7uWI(s>*b7j-;RO>W(r$K^@@#_7x(H=fpwugS-%{jz^gB(TK)rVvW& zm_#B=I`Z1l+-vmu6PK;!$g91^lWOop$hEK15@k}$l(kn)@_ZKqfBEL`y3+cOz32@V zs$WQL>-@rh7;5mAIX$$RY~=pQO3%NwW3Ucbc`B_h2NhQEoR>aV5vSmET2!MwuH|P) zQKqk}-isUcH>$nc3(K)R3^Y-Sv-O#4%8Z7ZLjvU|R6>mI=l>?0DGj_*1xU6RRL;b@ z!>Sa#Cmb83WvVW%m`7SBkc`AgDJytXRd4N=wV;Ystr(abRH`f?v%coaY33pBA2gGM z;9#?p#a{zF<#?pfjX1+$T_-IV94VL~-~`{)7bW$zj()xWBaSfT6VId861er4nDKeJ zUw-^Ku;Qe=b_nN?DYoJaT@Xl-Dd&1)Gjh+m?M?WBm0InRLn;z=8O`tFXAUpHW}~*o znk#1g0j^TM@cDLX>gmjCl{R^Jy$*`=xD+iHhau7LQLx$E`A`}AdI z8)sjR7~GY;V_Bo6ONy$H)097&;_S#!tTL0xKsdQ37P!XYZlBRwYLH9}7x3i^ocdy{7FUR!O34f4${aGLVUd15s@ zA9rWW(@t~rNQ#Zubj;-&gD+!(N4$ES!UXMMEuX^kPZmD!&zdH@#91JTFx2%=InA@VhJdDC!zYwnvL7VJa&^{dU{*)8eJi5)RBt%hqGVI_Va&N!8Vc_pRRf8i9IV$45jpvr zH%ME*EguR`Nq97olH`5ar`=))qqT3SH}ug5H^&t719u6Mj*jrqn<_>3EzzaTL4#iZ zsBRp;WD|OpbEvbjAf>zxb9BZ#_aRp-Yv)L@uI>ZjsvSd>u_d>yN{KT@U6WZsagG)%b`WWsazZLn zO5%dkQW8pk@R3Ruy&tS`KQ4b>X|JiB-Yu3{@u5RCjE@x%*&Xg@mxbRILSSp~-ldF0 zOy^C3cD*Ebf}Ve!A~8JH)!-LzycnvVVI1u7!_~+B*y-9)*PV9*{;}a(64SU_7X1e} z5$3srjT8n6rjo)^DM3U$v-j_Ogj9K3pRYVcTl0j8kb>w%t+7b*h(n zQ-m=!WTXC2L6xx@4Pk>Y1cz#q)1YP1iB@{9Q3QQ81D9%gnjtByZfr% z?I$+&S-)(8ZVqDQHr%g<-93j$?|Ybs5x$*v$MRZC<%t9X6I;af5C*mr=IkKZqp5}0 zobdsgsA4X+_ezsbu#&DRe|zo0v(HkdHK{Y{v*Kp@K!9DHn9`Nw4&rWHmS{&Ajq-OL zW>niV;}IgGd!^r=`fs(hwCIT6Ol+-k9+h+pue8usA@Lmi{1xC z*NL|wMQcS=CG^)3eP0f8cX(A2kKXG%bN-^Llw*L0ykFYl1?%y=3N?DBBxiB3S-u2a z!FpUF!jl{iH;)yP^~HCmmAcXLeB1)0loKD{+MCF_>TEso>K&JY!so8=Z)l@6=YlB# zJ>osbgqil+k{7c%hrC{1aTobej+m0;w;ocW`YtNVCzTGF3RYYaYB`2ThrCg@(opkm zda*REB)F?IFndw>&b9Y^Azp+Y**lKfk9XFWRPt?sq$(e$hbzy>>CT+270;$j7YT!& zC!RAN8B*6raT>q*5A~ZuYl|ftqD}@3XINy59xA#Usy(o(7N(dscSq}0xk%K9h6Ode zc*pMZ#x>Ce3YqL`5w+>mB_t06v)`|57{2Dq-q@_N%`46jT7&XV=EViR z*AA;xx=|LlHd=$yvq&9%ds383uErtV^D{ALvWqgjZNVSQbL%-&yiK$5>QA9Kqy9#> ztwta|g6}%-xaZ}1*APru#{RONIPR5q>NRulYZR@0+kD!uq<0OvFUhtpTngRN=sOiM z`(~Gj>RUzdj*c`y($VH(IEW2#5hDQ|4N7ZP@Hz+?X7QJLTdT-;GTq5-T=a$d*x z>DW51D&yBrqv!suCTCAy9`}TGG9j+-%`>g??G#?^Dc=W7Mp-fkl`hcc#IN2-7@NA_ zGp?a)*lW|?99@ZbHFa86nQ@d3F literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 0ae62f9f07..938424c6c9 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -69,6 +69,7 @@ "dist/credentials/SlackApi.credentials.js", "dist/credentials/Smtp.credentials.js", "dist/credentials/StripeApi.credentials.js", + "dist/credentials/SalesforceOAuth2Api.credentials.js", "dist/credentials/TelegramApi.credentials.js", "dist/credentials/TodoistApi.credentials.js", "dist/credentials/TrelloApi.credentials.js", @@ -160,6 +161,7 @@ "dist/nodes/Stripe/StripeTrigger.node.js", "dist/nodes/Shopify/ShopifyTrigger.node.js", "dist/nodes/Switch.node.js", + "dist/nodes/Salesforce/Salesforce.node.js", "dist/nodes/Telegram/Telegram.node.js", "dist/nodes/Telegram/TelegramTrigger.node.js", "dist/nodes/Todoist/Todoist.node.js", From 8e87723858c244d8a635b1606e7fae588714f7ba Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Wed, 12 Feb 2020 10:04:43 -0500 Subject: [PATCH 011/165] Zoho node --- packages/cli/src/Server.ts | 18 +- .../credentials/ZohoOAuth2Api.credentials.ts | 74 ++++++++ .../nodes-base/nodes/Zoho/GenericFunctions.ts | 58 ++++++ .../nodes-base/nodes/Zoho/LeadDescription.ts | 171 ++++++++++++++++++ .../nodes-base/nodes/Zoho/ZohoCrm.node.ts | 94 ++++++++++ packages/nodes-base/nodes/Zoho/zohoCrm.png | Bin 0 -> 5758 bytes packages/nodes-base/package.json | 6 +- 7 files changed, 417 insertions(+), 4 deletions(-) create mode 100644 packages/nodes-base/credentials/ZohoOAuth2Api.credentials.ts create mode 100644 packages/nodes-base/nodes/Zoho/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Zoho/LeadDescription.ts create mode 100644 packages/nodes-base/nodes/Zoho/ZohoCrm.node.ts create mode 100644 packages/nodes-base/nodes/Zoho/zohoCrm.png diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index fdc9e94e4e..e2e17a9740 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -913,7 +913,7 @@ class App { // Verify and store app code. Generate access tokens and store for respective credential. this.app.get('/rest/oauth2-credential/callback', async (req: express.Request, res: express.Response) => { - const {code, state: stateEncoded} = req.query; + const {code, state: stateEncoded } = req.query; if (code === undefined || stateEncoded === undefined) { throw new Error('Insufficient parameters for OAuth2 callback'); @@ -953,6 +953,20 @@ class App { return ResponseHelper.sendErrorResponse(res, errorResponse); } + let options = {}; + + // Here we need a variable that can be set on the credentials + // so that base on that include the credentails on the body or + // leave it as default with woukd include the credentails on the header. + // if (thatvariableistrue) { + // options = { + // body: { + // client_id: _.get(oauthCredentials, 'clientId') as string, + // client_secret: _.get(oauthCredentials, 'clientSecret', '') as string, + // }, + // } + // } + const oAuthObj = new clientOAuth2({ clientId: _.get(oauthCredentials, 'clientId') as string, clientSecret: _.get(oauthCredentials, 'clientSecret', '') as string, @@ -962,7 +976,7 @@ class App { scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ',') }); - const oauthToken = await oAuthObj.code.getToken(req.originalUrl); + const oauthToken = await oAuthObj.code.getToken(req.originalUrl, options); if (oauthToken === undefined) { const errorResponse = new ResponseHelper.ResponseError('Unable to get access tokens!', undefined, 404); diff --git a/packages/nodes-base/credentials/ZohoOAuth2Api.credentials.ts b/packages/nodes-base/credentials/ZohoOAuth2Api.credentials.ts new file mode 100644 index 0000000000..3617458a94 --- /dev/null +++ b/packages/nodes-base/credentials/ZohoOAuth2Api.credentials.ts @@ -0,0 +1,74 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class ZohoOAuth2Api implements ICredentialType { + name = 'zohoOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Zoho OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'options' as NodePropertyTypes, + options: [ + { + name: 'https://accounts.zoho.com/oauth/v2/auth', + value: 'https://accounts.zoho.com/oauth/v2/auth', + description: 'For the EU, AU, and IN domains', + }, + { + name: 'https://accounts.zoho.com.cn/oauth/v2/auth', + value: 'https://accounts.zoho.com.cn/oauth/v2/auth', + description: 'For the CN domain', + }, + ], + default: 'https://accounts.zoho.com/oauth/v2/auth', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'options' as NodePropertyTypes, + options: [ + { + name: 'US - https://accounts.zoho.com/oauth/v2/token', + value: 'https://accounts.zoho.com/oauth/v2/token', + }, + { + name: 'AU - https://accounts.zoho.com.au/oauth/v2/token', + value: 'https://accounts.zoho.com.au/oauth/v2/token', + }, + { + name: 'EU - https://accounts.zoho.eu/oauth/v2/token', + value: 'https://accounts.zoho.eu/oauth/v2/token', + }, + { + name: 'IN - https://accounts.zoho.in/oauth/v2/token', + value: 'https://accounts.zoho.in/oauth/v2/token', + }, + { + name: 'CN - https://accounts.zoho.com.cn/oauth/v2/token', + value: ' https://accounts.zoho.com.cn/oauth/v2/token', + }, + ], + default: 'https://accounts.zoho.com/oauth/v2/token', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: 'ZohoCRM.modules.ALL', + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: 'access_type=online', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Zoho/GenericFunctions.ts b/packages/nodes-base/nodes/Zoho/GenericFunctions.ts new file mode 100644 index 0000000000..ab6f00146c --- /dev/null +++ b/packages/nodes-base/nodes/Zoho/GenericFunctions.ts @@ -0,0 +1,58 @@ +import { OptionsWithUri } from 'request'; +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; +import { + IDataObject +} from 'n8n-workflow'; + +export async function zohoApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('zohoOAuth2Api'); + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + //@ts-ignore + Authorization: `Zoho-oauthtoken ${credentials!.oauthTokenData.access_token}` + }, + method, + body: { + data: [ + body, + ], + }, + qs, + uri: uri || `https://www.zohoapis.com/crm/v2${resource}`, + json: true + }; + try { + //@ts-ignore + return await this.helpers.requestOAuth.call(this, 'zohoOAuth2Api', options); + } catch (error) { + if (error.response && error.response.body && error.response.body.message) { + // Try to return the error prettier + throw new Error(`Zoho error response [${error.statusCode}]: ${error.response.body.message}`); + } + throw error; + } +} + +export async function zohoApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + let uri: string | undefined; + + do { + responseData = await zohoApiRequest.call(this, method, endpoint, body, query, uri); + uri = responseData.nextRecordsUrl; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData.nextRecordsUrl !== undefined && + responseData.nextRecordsUrl !== null + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Zoho/LeadDescription.ts b/packages/nodes-base/nodes/Zoho/LeadDescription.ts new file mode 100644 index 0000000000..198abbe472 --- /dev/null +++ b/packages/nodes-base/nodes/Zoho/LeadDescription.ts @@ -0,0 +1,171 @@ +import { INodeProperties } from "n8n-workflow"; + +export const leadOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'lead', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new lead', + }, + { + name: 'Get', + value: 'get', + description: 'Get data of a lead', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get data of all leads', + }, + { + name: 'Update', + value: 'update', + description: 'Update new lead', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a lead', + } + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const leadFields = [ + +/* -------------------------------------------------------------------------- */ +/* lead:create */ +/* -------------------------------------------------------------------------- */ + + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'create', + ], + }, + }, + description: `User's last name`, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'lead', + ], + }, + }, + options: [ + { + displayName: 'Avatar', + name: 'avatar', + type: 'string', + default: '', + description: 'An avatar image URL. note: the image url needs to be https.', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Name of the user', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: 'The phone number of the user', + }, + { + displayName: 'Unsubscribed From Emails', + name: 'unsubscribedFromEmails', + type: 'boolean', + default: false, + description: 'Whether the Lead is unsubscribed from emails', + }, + { + displayName: 'Update Last Request At', + name: 'updateLastRequestAt', + type: 'boolean', + default: false, + description: 'A boolean value, which if true, instructs Intercom to update the
users last_request_at value to the current API service time in
UTC. default value if not sent is false.', + }, + { + displayName: 'Companies', + name: 'companies', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getCompanies', + }, + default: [], + description: 'Identifies the companies this user belongs to.', + }, + { + displayName: 'UTM Source', + name: 'utmSource', + type: 'string', + default: '', + description: 'An avatar image URL. note: the image url needs to be https.', + }, + { + displayName: 'UTM Medium', + name: 'utmMedium', + type: 'string', + default: '', + description: 'Identifies what type of link was used', + }, + { + displayName: 'UTM Campaign', + name: 'utmCampaign', + type: 'string', + default: '', + description: 'Identifies a specific product promotion or strategic campaign', + }, + { + displayName: 'UTM Term', + name: 'utmTerm', + type: 'string', + default: '', + description: 'Identifies search terms', + }, + { + displayName: 'UTM Content', + name: 'utmContent', + type: 'string', + default: '', + description: 'Identifies what specifically was clicked to bring the user to the site', + }, + ] + }, + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zoho/ZohoCrm.node.ts b/packages/nodes-base/nodes/Zoho/ZohoCrm.node.ts new file mode 100644 index 0000000000..2032a57921 --- /dev/null +++ b/packages/nodes-base/nodes/Zoho/ZohoCrm.node.ts @@ -0,0 +1,94 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeTypeDescription, + INodeType, +} from 'n8n-workflow'; + +import { + zohoApiRequest, + zohoApiRequestAllItems, +} from './GenericFunctions'; + +import { + leadOperations, + leadFields, +} from './LeadDescription'; + +export class ZohoCrm implements INodeType { + description: INodeTypeDescription = { + displayName: 'Zoho CRM', + name: 'zohoCrm', + icon: 'file:zohoCrm.png', + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + group: ['input'], + version: 1, + description: 'Consume Zoho CRM API.', + defaults: { + name: 'Zoho CRM', + color: '#CE2232', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'zohoOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Lead', + value: 'lead', + }, + ], + default: 'lead', + description: 'The resource to operate on.', + }, + ...leadOperations, + ...leadFields, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + let responseData; + for (let i = 0; i < length; i++) { + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + if (resource === 'lead') { + if (operation === 'create') { + const lastName = this.getNodeParameter('lastName', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body = { + Last_Name: lastName, + }; + // if (additionalFields.email) { + // // @ts-ignore + // body.email = additionalFields.email as string; + // } + responseData = await zohoApiRequest.call(this, 'POST', '/leads', body); + responseData = responseData.data; + + } + } + 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/Zoho/zohoCrm.png b/packages/nodes-base/nodes/Zoho/zohoCrm.png new file mode 100644 index 0000000000000000000000000000000000000000..8d541a45140da9a4089eb8d3ef5025ee533c6144 GIT binary patch literal 5758 zcmY*-bzBr&*DWw<=c`|NL^qorq(JrZ=SHFR8cloSO`5%y4HGlYpb6lw4H$bM8p z3O+9F&0URYk@j{DE`ms5$iEE1$Ms(r2BH0z;%X}l(NTIsD}it}r{#fiL)jrBAX-{l zxU-pspqixgf6E_t!VoK0S4Tk@%-!7`>dpm4I9tLv1Oxs0(*#Bxjmcsv{f-26|=8w*Q^+h<~|1$qS?7u#6*kAMi=Q984^k4L`RS^&z_P@_2 z0#fW^)*A!o)l3q(Xe*qoD6Lo!%363DKh(vb_*7S%fyl8FJ+qV6 zfz}vn6f8-lN{>lJnr1B71Bjvrh61cUgm zt#!m&5GQo?A}ZnJ%;=)crJ-it1?7eBn69k9R15ew-TUc$5TkdD?YJ+UNJ5Kb7yG=L z{Q!IMNjnmO^`^RFLqSAL)e)|T^fRvWRGE=-A>X|0I|H9w&*F1e{4}ahSEsGa6c1m| z&H2e7&W2f4ReM~B%s`g)wg`iIIU~$Pn+PDPQpd?&EIO1oDDi?Z zd#vo%Mpy*cjPeyHSLYAkq&yp6TIm67$4L|(TQ!OQn9z`_V�fasDkaWhf$zpnpB5 zim3|b<>=IjSe->-=&ML{%?2_Z`V(6N5+^wek?yE3Ud_>8#KZaW(0>30hSmKgdrCDC z{o_a>XR8ZE<#^{FCZs#I4oIzV`w!{FllkgSXIpNA9?q}n9J;T_Gz#6@)xraU9Ccws z<@phc>f+1(NaczU-oZ|(3^YTzI;eP@Y|FF|Br30&R0nRQD+TIe0^b! z<;6^{$t2)50_KrX8)^-CF;oa8>Yn8;J0(a zJAz|lrKHs5m|6g|^mmG6;xT{1KbdG*>o#lpkDsQ$%`8S!x1-DXsHv>P9%mob>fryR z6b60JnRlsn+pm0o!7ITb%4sBIi!Ar;9B*4Ln@88r+` zMavwwA|*uQ5a>F}jum+}1G<>_{w`PM;5*wAPsLy_Z5Su}J2xPob*LXp1Er+-$10pk zfEwD=h}<9{6%qqm18<4z8#=2lnVBxlbS-z%b@M8skgZhRn||)_bN(-OuHT2< zp8#OvkGr)^pQ6oN2DzsiCqE7gA5^|7`_fYwHr6D%)s9?v0=h1}!L9UI!R7HI({0G< zttn~bs>9;OnUdA>nFWP+4_+80sGL6Uo^YIC)y}qOrDgPUjrWwX{XSv&= zC95we1a!%`iMF|bW$d?Fdr0*6QpsS8I4)*L8@FnQB%1S11i48q=ek%V73O{>c}Cqd z*sFtK77bw!37U@%Chb#cwNVOpOZ>QEcPAY#VW41pdsM~uZHo-wCvNg z;jw zjb&I4$|DG{uFro@M(^ph@RNv6$9F~I^!^Bq^SuHvMc5REGUcGTMo8(3u_tU~-n70G z?+LDf=5QJ8N1vYhod>JzSWX(PRhsyws=v>@H(6`=^aJ=$_xXjAK}U5v6BjQ25}j|3 zW$2LF_JtJ`SUPaPP3|}C$m8DY%v#UB@b|BMEii^LLMeeAp(k_7U5C06;DMmC$(+5WqL)&bc3h~B9+B}4$SB*2Hr|P}_*DECe$aa;hz4dFn@u~F1zOBIR zwhGP{=Z_A&7fXO{0CkM9wY5G@)|7h1&Z_MdYNoN4SDsyc5P z%&c&2YT=G6Iy`h2Ly!-+X7p6=ORw;adzw2Ycs_%zRF1W($~nz{${&7G{q}y7i^26c z&qLv-gT6PyWb-iDEcmIso!${B!(je(+Tv~HR5q{ssYo4WCB8n5$gO-)YBjJMALR&1 zP`_-BLF~iY91LtG2k`AisVF=+&AXj44fTWg3^7s3(xiO3rBqak=CneB0rojTyIraM zZ}flJ?T9mRT^_8oraGyxhYpVI&1Q5YfNKODuqM)sW+u#ZM6{nD;8P_jXdAxQMIRim zKkyR_;X`0OY*r|IMCd1dxm~zm6mn6W`QCI${&3CpY z=W}}NPI#pX)%_na)N;mTFf`SRdL7+sx05Qa%t%j{zw)?L<`A57enh-raozd!U|JUB z6L>KjtI#J@x#{!Uknd7s*b&la*@NDIQ@=FFIZ}pKDFCkAo8gc*eYClOmFmBni3jdMj6&@8bECfBC6P3)slOi*$Yrfg z-N3zU$T2)gZP4TOsa8CsX1coM%)-Z`7A8ja+7~Rs0TwgrnTF zVU6O-tfe`^Od(I{tBdR5S=W6^d`XExqzyw=FDDPWusc$ooF&_ORsFoWi!%$-IZL8z zF~iANr4H=G!;2{xSC^N_{d1i2kFGI;eM?w6{ny-a4GfuehfbVM>zebwMD;l8h*7M}D7Lh%H)jdJ) zS3jk~v&eQLgdZZTJa^@PEsWd63CrH388W{$r4|u%*3r;vj9f?_+vv*<)r9KW7168O z);2f87A7ZCrt6AdHl6n?!$q6!EVHJp_Yop1%_}R0RyN|neCF>4odLYHClt_D?%-#l zcvV-+Jd4xb0+*zIV0h0@3oRe4KPyK;6}MbGM5M65{vo`0c3z3?!%s-!vCxgZam7rwt0@FMMsKd`>?Gb|W&zq6XS zs3n%0^E79Y%{F14?WFF+WQaiRL*XZPQe-W`_3Zlno5&Q4gps*@T1Q`1pm@vMx%d=Hh>$EVJ8Fd+q=`?pMKppj| zBb%Kra96v`fW=?P1&V(-CNDA9L0S)+KG_j+w79*{;B+1S?;3K zBOjN=dI$I9>~69?_u}HrLxg2^;&<{~v<5t3o9rK87=d@shcX2Gai5HHl;=xJn)!N|nPC8l3IbIptdcwp3MB=rm|JVsuJt^RFnabs@?WSym&A7g~e zzqjJGqI3ZpmGHB$ydByVp6nWYdS$stcu73M29@k0g!XQjZ)*%Xwk zVVi4WzbLi3Ym2)ap<41}uGuLn6dwp#@gsEt1oSF$wtErASSLnKx zLQ@DC7#JBb_KlA(QG|)Np)!sB;&oJ`VOwH*lCdfMw64%f8E$%6X>y6XMadC3!;>T6 z+P)vN>TRIOION|DLgs|Y zalGh184|Be`LXj)6pF7p>UxB-0Bz715=u$EuVNV^=$$^iEdwW4xU;@}7!c|8P#L$z j&8+YEHyQoH@WcAalioLV`mFKqr?8xqvSfw$yMX@zY&_=O literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 0ae62f9f07..b62e3bc649 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -79,7 +79,8 @@ "dist/credentials/TypeformApi.credentials.js", "dist/credentials/TogglApi.credentials.js", "dist/credentials/VeroApi.credentials.js", - "dist/credentials/WordpressApi.credentials.js" + "dist/credentials/WordpressApi.credentials.js", + "dist/credentials/ZohoOAuth2Api.credentials.js" ], "nodes": [ "dist/nodes/ActiveCampaign/ActiveCampaign.node.js", @@ -172,7 +173,8 @@ "dist/nodes/WriteBinaryFile.node.js", "dist/nodes/Webhook.node.js", "dist/nodes/Wordpress/Wordpress.node.js", - "dist/nodes/Xml.node.js" + "dist/nodes/Xml.node.js", + "dist/nodes/Zoho/ZohoCrm.node.js" ] }, "devDependencies": { From f64a1a9bf10d7eb0cb5a0b5f5555d8f9ee32bf09 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 12 Feb 2020 22:35:56 -0800 Subject: [PATCH 012/165] :zap: Minor improvements zu Salesforce-Node --- .../nodes/Salesforce/AccountDescription.ts | 558 ++++++++-------- .../nodes/Salesforce/AttachmentDescription.ts | 88 +-- .../nodes/Salesforce/CaseDescription.ts | 174 ++--- .../nodes/Salesforce/ContactDescription.ts | 550 ++++++++-------- .../nodes/Salesforce/GenericFunctions.ts | 2 +- .../nodes/Salesforce/LeadDescription.ts | 472 +++++++------- .../Salesforce/OpportunityDescription.ts | 448 ++++++------- .../nodes/Salesforce/Salesforce.node.ts | 199 +++--- .../nodes/Salesforce/TaskDescription.ts | 600 +++++++++--------- packages/nodes-base/package.json | 4 +- 10 files changed, 1553 insertions(+), 1542 deletions(-) diff --git a/packages/nodes-base/nodes/Salesforce/AccountDescription.ts b/packages/nodes-base/nodes/Salesforce/AccountDescription.ts index 0c9d336065..0944711f4e 100644 --- a/packages/nodes-base/nodes/Salesforce/AccountDescription.ts +++ b/packages/nodes-base/nodes/Salesforce/AccountDescription.ts @@ -13,40 +13,40 @@ export const accountOperations = [ }, }, options: [ + { + name: 'Add Note', + value: 'addNote', + description: 'Add note to an account', + }, { name: 'Create', value: 'create', description: 'Create an account', }, - { - name: 'Update', - value: 'update', - description: 'Update an account', - }, { name: 'Get', value: 'get', description: 'Get an account', }, - { - name: 'Get Summary', - value: 'getSummary', - description: `Returns an overview of account's metadata.`, - }, { name: 'Get All', value: 'getAll', description: 'Get all accounts', }, + { + name: 'Get Summary', + value: 'getSummary', + description: `Returns an overview of account's metadata.`, + }, { name: 'Delete', value: 'delete', description: 'Delete an account', }, { - name: 'Add Note', - value: 'addNote', - description: 'Add note to an account', + name: 'Update', + value: 'update', + description: 'Update an account', }, ], default: 'create', @@ -94,108 +94,6 @@ export const accountFields = [ }, }, options: [ - { - displayName: 'Fax', - name: 'fax', - type: 'string', - default: '', - description: 'Fax number for the account.', - }, - { - displayName: 'Type', - name: 'type', - type: 'options', - default: '', - typeOptions: { - loadOptionsMethod: 'getAccountTypes', - }, - description: 'Type of account', - }, - { - displayName: 'Phone', - name: 'phone', - type: 'string', - default: '', - description: 'Phone number for the account.', - }, - { - displayName: 'Jigsaw', - name: 'jigsaw', - type: 'string', - default: '', - description: 'references the ID of a company in Data.com', - }, - { - displayName: 'Owner', - name: 'owner', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getUsers', - }, - default: '', - description: 'The owner of the account.', - }, - { - displayName: 'SicDesc', - name: 'sicDesc', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - description: 'A brief description of an organization’s line of business, based on its SIC code.', - }, - { - displayName: 'Website', - name: 'website', - type: 'string', - default: '', - description: 'The website of this account. Maximum of 255 characters.', - }, - { - displayName: 'Industry', - name: 'industry', - type: 'string', - default: '', - description: 'The website of this account. Maximum of 255 characters.', - }, - { - displayName: 'Parent Id', - name: 'parentId', - type: 'string', - default: '', - description: 'ID of the parent object, if any.', - }, - { - displayName: 'Billing City', - name: 'billingCity', - type: 'string', - default: '', - description: 'Details for the billing address of this account. Maximum size is 40 characters.', - }, - { - displayName: 'Description', - name: 'description', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'Text description of the account. Limited to 32,000 KB.', - }, - { - displayName: 'Billing State', - name: 'billingState', - type: 'string', - default: '', - description: 'Details for the billing address of this account. Maximum size is 80 characters.', - }, - { - displayName: 'Shipping City', - name: 'shippingCity', - type: 'string', - default: '', - description: 'Details of the shipping address for this account. City maximum size is 40 characters', - }, { displayName: 'Account Source', name: 'accountSource', @@ -217,18 +115,11 @@ export const accountFields = [ description: 'Estimated annual revenue of the account.', }, { - displayName: 'Billing Street', - name: 'billingStreet', + displayName: 'Billing City', + name: 'billingCity', type: 'string', default: '', - description: 'Street address for the billing address of this account.', - }, - { - displayName: 'Shipping State', - name: 'shippingState', - type: 'string', - default: '', - description: 'Details of the shipping address for this account. State maximum size is 80 characters.', + description: 'Details for the billing address of this account. Maximum size is 40 characters.', }, { displayName: 'Billing Country', @@ -237,20 +128,6 @@ export const accountFields = [ default: '', description: 'Details for the billing address of this account. Maximum size is 80 characters.', }, - { - displayName: 'Shipping Street', - name: 'shippingStreet', - type: 'string', - default: '', - description: 'The street address of the shipping address for this account. Maximum of 255 characters.', - }, - { - displayName: 'Shipping Country', - name: 'shippingCountry', - type: 'string', - default: '', - description: 'Details of the shipping address for this account. Country maximum size is 80 characters.', - }, { displayName: 'Billing Postal Code', name: 'billingPostalCode', @@ -258,6 +135,51 @@ export const accountFields = [ default: '', description: 'Details for the billing address of this account. Maximum size is 20 characters.', }, + { + displayName: 'Billing State', + name: 'billingState', + type: 'string', + default: '', + description: 'Details for the billing address of this account. Maximum size is 80 characters.', + }, + { + displayName: 'Billing Street', + name: 'billingStreet', + type: 'string', + default: '', + description: 'Street address for the billing address of this account.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Text description of the account. Limited to 32,000 KB.', + }, + { + displayName: 'Fax', + name: 'fax', + type: 'string', + default: '', + description: 'Fax number for the account.', + }, + { + displayName: 'Jigsaw', + name: 'jigsaw', + type: 'string', + default: '', + description: 'references the ID of a company in Data.com', + }, + { + displayName: 'Industry', + name: 'industry', + type: 'string', + default: '', + description: 'The website of this account. Maximum of 255 characters.', + }, { displayName: 'Number Of Employees', name: 'numberOfEmployees', @@ -265,6 +187,63 @@ export const accountFields = [ default: '', description: 'Number of employees', }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'The owner of the account.', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: 'Phone number for the account.', + }, + { + displayName: 'SicDesc', + name: 'sicDesc', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + description: 'A brief description of an organization’s line of business, based on its SIC code.', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getAccountTypes', + }, + description: 'Type of account', + }, + { + displayName: 'Parent Id', + name: 'parentId', + type: 'string', + default: '', + description: 'ID of the parent object, if any.', + }, + { + displayName: 'Shipping City', + name: 'shippingCity', + type: 'string', + default: '', + description: 'Details of the shipping address for this account. City maximum size is 40 characters', + }, + { + displayName: 'Shipping Country', + name: 'shippingCountry', + type: 'string', + default: '', + description: 'Details of the shipping address for this account. Country maximum size is 80 characters.', + }, { displayName: 'Shipping Postal Code', name: 'shippingPostalCode', @@ -272,6 +251,27 @@ export const accountFields = [ default: '', description: 'Details of the shipping address for this account. Postal code maximum size is 20 characters.', }, + { + displayName: 'Shipping State', + name: 'shippingState', + type: 'string', + default: '', + description: 'Details of the shipping address for this account. State maximum size is 80 characters.', + }, + { + displayName: 'Shipping Street', + name: 'shippingStreet', + type: 'string', + default: '', + description: 'The street address of the shipping address for this account. Maximum of 255 characters.', + }, + { + displayName: 'Website', + name: 'website', + type: 'string', + default: '', + description: 'The website of this account. Maximum of 255 characters.', + }, ], }, /* -------------------------------------------------------------------------- */ @@ -312,115 +312,6 @@ export const accountFields = [ }, }, options: [ - { - displayName: 'Name', - name: 'name', - type: 'string', - default: '', - description: 'Name of the account. Maximum size is 255 characters.', - }, - { - displayName: 'Fax', - name: 'fax', - type: 'string', - default: '', - description: 'Fax number for the account.', - }, - { - displayName: 'Type', - name: 'type', - type: 'options', - default: '', - typeOptions: { - loadOptionsMethod: 'getAccountTypes', - }, - description: 'Type of account', - }, - { - displayName: 'Phone', - name: 'phone', - type: 'string', - default: '', - description: 'Phone number for the account.', - }, - { - displayName: 'Jigsaw', - name: 'jigsaw', - type: 'string', - default: '', - description: 'references the ID of a company in Data.com', - }, - { - displayName: 'Owner', - name: 'ownerId', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getUsers', - }, - default: '', - description: 'The owner of the account.', - }, - { - displayName: 'SicDesc', - name: 'sicDesc', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - description: 'A brief description of an organization’s line of business, based on its SIC code.', - }, - { - displayName: 'Website', - name: 'website', - type: 'string', - default: '', - description: 'The website of this account. Maximum of 255 characters.', - }, - { - displayName: 'Industry', - name: 'industry', - type: 'string', - default: '', - description: 'The website of this account. Maximum of 255 characters.', - }, - { - displayName: 'Parent Id', - name: 'parentId', - type: 'string', - default: '', - description: 'ID of the parent object, if any.', - }, - { - displayName: 'Billing City', - name: 'billingCity', - type: 'string', - default: '', - description: 'Details for the billing address of this account. Maximum size is 40 characters.', - }, - { - displayName: 'Description', - name: 'description', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'Text description of the account. Limited to 32,000 KB.', - }, - { - displayName: 'Billing State', - name: 'billingState', - type: 'string', - default: '', - description: 'Details for the billing address of this account. Maximum size is 80 characters.', - }, - { - displayName: 'Shipping City', - name: 'shippingCity', - type: 'string', - default: '', - description: 'Details of the shipping address for this account. City maximum size is 40 characters', - }, { displayName: 'Account Source', name: 'accountSource', @@ -442,18 +333,11 @@ export const accountFields = [ description: 'Estimated annual revenue of the account.', }, { - displayName: 'Billing Street', - name: 'billingStreet', + displayName: 'Billing City', + name: 'billingCity', type: 'string', default: '', - description: 'Street address for the billing address of this account.', - }, - { - displayName: 'Shipping State', - name: 'shippingState', - type: 'string', - default: '', - description: 'Details of the shipping address for this account. State maximum size is 80 characters.', + description: 'Details for the billing address of this account. Maximum size is 40 characters.', }, { displayName: 'Billing Country', @@ -462,20 +346,6 @@ export const accountFields = [ default: '', description: 'Details for the billing address of this account. Maximum size is 80 characters.', }, - { - displayName: 'Shipping Street', - name: 'shippingStreet', - type: 'string', - default: '', - description: 'The street address of the shipping address for this account. Maximum of 255 characters.', - }, - { - displayName: 'Shipping Country', - name: 'shippingCountry', - type: 'string', - default: '', - description: 'Details of the shipping address for this account. Country maximum size is 80 characters.', - }, { displayName: 'Billing Postal Code', name: 'billingPostalCode', @@ -483,6 +353,85 @@ export const accountFields = [ default: '', description: 'Details for the billing address of this account. Maximum size is 20 characters.', }, + { + displayName: 'Billing State', + name: 'billingState', + type: 'string', + default: '', + description: 'Details for the billing address of this account. Maximum size is 80 characters.', + }, + { + displayName: 'Billing Street', + name: 'billingStreet', + type: 'string', + default: '', + description: 'Street address for the billing address of this account.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Text description of the account. Limited to 32,000 KB.', + }, + { + displayName: 'Fax', + name: 'fax', + type: 'string', + default: '', + description: 'Fax number for the account.', + }, + { + displayName: 'Industry', + name: 'industry', + type: 'string', + default: '', + description: 'The website of this account. Maximum of 255 characters.', + }, + { + displayName: 'Jigsaw', + name: 'jigsaw', + type: 'string', + default: '', + description: 'references the ID of a company in Data.com', + }, + { + displayName: 'Owner', + name: 'ownerId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'The owner of the account.', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: 'Phone number for the account.', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getAccountTypes', + }, + description: 'Type of account', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Name of the account. Maximum size is 255 characters.', + }, { displayName: 'Number Of Employees', name: 'numberOfEmployees', @@ -490,6 +439,36 @@ export const accountFields = [ default: '', description: 'Number of employees', }, + { + displayName: 'Parent Id', + name: 'parentId', + type: 'string', + default: '', + description: 'ID of the parent object, if any.', + }, + { + displayName: 'SicDesc', + name: 'sicDesc', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + description: 'A brief description of an organization’s line of business, based on its SIC code.', + }, + { + displayName: 'Shipping City', + name: 'shippingCity', + type: 'string', + default: '', + description: 'Details of the shipping address for this account. City maximum size is 40 characters', + }, + { + displayName: 'Shipping Country', + name: 'shippingCountry', + type: 'string', + default: '', + description: 'Details of the shipping address for this account. Country maximum size is 80 characters.', + }, { displayName: 'Shipping Postal Code', name: 'shippingPostalCode', @@ -497,6 +476,27 @@ export const accountFields = [ default: '', description: 'Details of the shipping address for this account. Postal code maximum size is 20 characters.', }, + { + displayName: 'Shipping State', + name: 'shippingState', + type: 'string', + default: '', + description: 'Details of the shipping address for this account. State maximum size is 80 characters.', + }, + { + displayName: 'Shipping Street', + name: 'shippingStreet', + type: 'string', + default: '', + description: 'The street address of the shipping address for this account. Maximum of 255 characters.', + }, + { + displayName: 'Website', + name: 'website', + type: 'string', + default: '', + description: 'The website of this account. Maximum of 255 characters.', + }, ], }, @@ -678,6 +678,13 @@ export const accountFields = [ }, description: 'Body of the note. Limited to 32 KB.', }, + { + displayName: 'Is Private', + name: 'isPrivate', + type: 'boolean', + default: false, + description: 'If true, only the note owner or a user with the “Modify All Data” permission can view the note or query it via the API', + }, { displayName: 'Owner', name: 'ownerId', @@ -688,13 +695,6 @@ export const accountFields = [ default: '', description: 'ID of the user who owns the note.', }, - { - displayName: 'Is Private', - name: 'isPrivate', - type: 'boolean', - default: false, - description: 'If true, only the note owner or a user with the “Modify All Data” permission can view the note or query it via the API', - }, ] }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Salesforce/AttachmentDescription.ts b/packages/nodes-base/nodes/Salesforce/AttachmentDescription.ts index 60158a7e52..ec61acd43a 100644 --- a/packages/nodes-base/nodes/Salesforce/AttachmentDescription.ts +++ b/packages/nodes-base/nodes/Salesforce/AttachmentDescription.ts @@ -19,29 +19,29 @@ export const attachmentOperations = [ description: 'Create a attachment', }, { - name: 'Update', - value: 'update', - description: 'Update a attachment', + name: 'Delete', + value: 'delete', + description: 'Delete a attachment', }, { name: 'Get', value: 'get', description: 'Get a attachment', }, - { - name: 'Get Summary', - value: 'getSummary', - description: `Returns an overview of attachment's metadata.`, - }, { name: 'Get All', value: 'getAll', description: 'Get all attachments', }, { - name: 'Delete', - value: 'delete', - description: 'Delete a attachment', + name: 'Get Summary', + value: 'getSummary', + description: `Returns an overview of attachment's metadata.`, + }, + { + name: 'Update', + value: 'update', + description: 'Update a attachment', }, ], default: 'create', @@ -127,14 +127,11 @@ export const attachmentFields = [ }, options: [ { - displayName: 'Owner', - name: 'owner', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getUsers', - }, + displayName: 'Description', + name: 'description', + type: 'string', default: '', - description: 'ID of the User who owns the attachment.', + description: `Text description of the Document. Limit: 255 characters.`, }, { displayName: 'Is Private', @@ -144,11 +141,14 @@ export const attachmentFields = [ description: 'Indicates whether this record is viewable only by the owner and administrators (true) or viewable by all otherwise-allowed users (false). ', }, { - displayName: 'Description', - name: 'description', - type: 'string', + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, default: '', - description: `Text description of the Document. Limit: 255 characters.`, + description: 'ID of the User who owns the attachment.', }, ], }, @@ -190,6 +190,28 @@ export const attachmentFields = [ }, }, options: [ + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + placeholder: '', + description: 'Name of the binary property which contains
the data for the file to be uploaded.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: `Text description of the Document. Limit: 255 characters.`, + }, + { + displayName: 'Is Private', + name: 'isPrivate', + type: 'boolean', + default: false, + description: 'Indicates whether this record is viewable only by the owner and administrators (true) or viewable by all otherwise-allowed users (false). ', + }, { displayName: 'Name', name: 'name', @@ -207,28 +229,6 @@ export const attachmentFields = [ default: '', description: 'ID of the User who owns the attachment.', }, - { - displayName: 'Is Private', - name: 'isPrivate', - type: 'boolean', - default: false, - description: 'Indicates whether this record is viewable only by the owner and administrators (true) or viewable by all otherwise-allowed users (false). ', - }, - { - displayName: 'Description', - name: 'description', - type: 'string', - default: '', - description: `Text description of the Document. Limit: 255 characters.`, - }, - { - displayName: 'Binary Property', - name: 'binaryPropertyName', - type: 'string', - default: 'data', - placeholder: '', - description: 'Name of the binary property which contains
the data for the file to be uploaded.', - }, ], }, diff --git a/packages/nodes-base/nodes/Salesforce/CaseDescription.ts b/packages/nodes-base/nodes/Salesforce/CaseDescription.ts index 9b9c458bac..439d9d92e2 100644 --- a/packages/nodes-base/nodes/Salesforce/CaseDescription.ts +++ b/packages/nodes-base/nodes/Salesforce/CaseDescription.ts @@ -13,40 +13,40 @@ export const caseOperations = [ }, }, options: [ + { + name: 'Add Comment', + value: 'addComment', + description: 'Add a comment to a case', + }, { name: 'Create', value: 'create', description: 'Create a case', }, - { - name: 'Update', - value: 'update', - description: 'Update a case', - }, { name: 'Get', value: 'get', description: 'Get a case', }, - { - name: 'Get Summary', - value: 'getSummary', - description: `Returns an overview of case's metadata.`, - }, { name: 'Get All', value: 'getAll', description: 'Get all cases', }, + { + name: 'Get Summary', + value: 'getSummary', + description: `Returns an overview of case's metadata.`, + }, { name: 'Delete', value: 'delete', description: 'Delete a case', }, { - name: 'Add Comment', - value: 'addComment', - description: 'Add a comment to a case', + name: 'Update', + value: 'update', + description: 'Update a case', }, ], default: 'create', @@ -97,70 +97,6 @@ export const caseFields = [ }, }, options: [ - { - displayName: 'Origin', - name: 'origin', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getCaseOrigins', - }, - default: '', - description: 'The source of the case, such as Email, Phone, or Web. Label is Case Origin.', - }, - { - displayName: 'Reason', - name: 'reason', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getCaseReasons', - }, - default: '', - description: 'The reason why the case was created, such as Instructions not clear, or User didn’t attend training.', - }, - { - displayName: 'Status', - name: 'status', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getCaseStatuses', - }, - default: '', - description: 'The status of the case, such as “New,” “Closed,” or “Escalated.” This field directly controls the IsClosed flag', - }, - { - displayName: 'Owner', - name: 'owner', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getUsers', - }, - default: '', - description: 'The owner of the case.', - }, - { - displayName: 'Subject', - name: 'subject', - type: 'string', - default: '', - description: 'The subject of the case. Limit: 255 characters.', - }, - { - displayName: 'Parent Id', - name: 'ParentId', - type: 'string', - default: '', - description: 'The ID of the parent case in the hierarchy. The label is Parent Case.', - }, - { - displayName: 'Priority', - name: 'priority', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getCasePriorities', - }, - default: '', - description: 'The importance or urgency of the case, such as High, Medium, or Low.', - }, { displayName: 'Account Id', name: 'accountId', @@ -190,11 +126,75 @@ export const caseFields = [ description: 'Indicates whether the case has been escalated (true) or not.', }, { - displayName: 'Supplied Name', - name: 'suppliedName', + displayName: 'Origin', + name: 'origin', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCaseOrigins', + }, + default: '', + description: 'The source of the case, such as Email, Phone, or Web. Label is Case Origin.', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'The owner of the case.', + }, + { + displayName: 'Parent Id', + name: 'ParentId', type: 'string', default: '', - description: `The name that was entered when the case was created. This field can't be updated after the case has been created`, + description: 'The ID of the parent case in the hierarchy. The label is Parent Case.', + }, + { + displayName: 'Priority', + name: 'priority', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCasePriorities', + }, + default: '', + description: 'The importance or urgency of the case, such as High, Medium, or Low.', + }, + { + displayName: 'Reason', + name: 'reason', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCaseReasons', + }, + default: '', + description: 'The reason why the case was created, such as Instructions not clear, or User didn’t attend training.', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCaseStatuses', + }, + default: '', + description: 'The status of the case, such as “New,” “Closed,” or “Escalated.” This field directly controls the IsClosed flag', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'string', + default: '', + description: 'The subject of the case. Limit: 255 characters.', + }, + { + displayName: 'Supplied Company', + name: 'suppliedCompany', + type: 'string', + default: '', + description: `The company name that was entered when the case was created. This field can't be updated after the case has been created..`, }, { displayName: 'Supplied Email', @@ -203,6 +203,13 @@ export const caseFields = [ default: '', description: `The email address that was entered when the case was created. This field can't be updated after the case has been created.`, }, + { + displayName: 'Supplied Name', + name: 'suppliedName', + type: 'string', + default: '', + description: `The name that was entered when the case was created. This field can't be updated after the case has been created`, + }, { displayName: 'Supplied Phone', name: 'suppliedPhone', @@ -210,13 +217,6 @@ export const caseFields = [ default: '', description: `The phone number that was entered when the case was created. This field can't be updated after the case has been created.`, }, - { - displayName: 'Supplied Company', - name: 'suppliedCompany', - type: 'string', - default: '', - description: `The company name that was entered when the case was created. This field can't be updated after the case has been created..`, - }, ], }, /* -------------------------------------------------------------------------- */ diff --git a/packages/nodes-base/nodes/Salesforce/ContactDescription.ts b/packages/nodes-base/nodes/Salesforce/ContactDescription.ts index bda804e6d6..741e9c3841 100644 --- a/packages/nodes-base/nodes/Salesforce/ContactDescription.ts +++ b/packages/nodes-base/nodes/Salesforce/ContactDescription.ts @@ -13,15 +13,25 @@ export const contactOperations = [ }, }, options: [ + { + name: 'Add Lead To Campaign', + value: 'addToCampaign', + description: 'Add lead to a campaign', + }, + { + name: 'Add Note', + value: 'addNote', + description: 'Add note to a contact', + }, { name: 'Create', value: 'create', description: 'Create a contact', }, { - name: 'Update', - value: 'update', - description: 'Update a contact', + name: 'Delete', + value: 'delete', + description: 'Delete a contact', }, { name: 'Get', @@ -39,19 +49,9 @@ export const contactOperations = [ description: 'Get all contacts', }, { - name: 'Delete', - value: 'delete', - description: 'Delete a contact', - }, - { - name: 'Add Lead To Campaign', - value: 'addToCampaign', - description: 'Add lead to a campaign', - }, - { - name: 'Add Note', - value: 'addNote', - description: 'Add note to a contact', + name: 'Update', + value: 'update', + description: 'Update a contact', }, ], default: 'create', @@ -99,52 +99,6 @@ export const contactFields = [ }, }, options: [ - { - displayName: 'Fax', - name: 'fax', - type: 'string', - default: '', - description: 'Fax number for the contact. Label is Business Fax.', - }, - { - displayName: 'Email', - name: 'email', - type: 'string', - default: '', - description: 'Email address for the contact.', - }, - { - displayName: 'Phone', - name: 'phone', - type: 'string', - default: '', - description: 'Phone number for the contact.', - }, - { - displayName: 'Title', - name: 'title', - type: 'string', - default: '', - description: 'Title of the contact such as CEO or Vice President.', - }, - { - displayName: 'Jigsaw', - name: 'jigsaw', - type: 'string', - default: '', - description: `references the ID of a contact in Data.com. - If a contact has a value in this field, it means that a contact was imported as a contact from Data.com.`, - }, - { - displayName: 'Owner', - name: 'owner', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getUsers', - }, - default: '', - description: 'The owner of the contact.', - }, { displayName: 'Account', name: 'acconuntId', @@ -156,11 +110,67 @@ export const contactFields = [ description: 'ID of the account that is the parent of this contact.', }, { - displayName: 'Birthdate', - name: 'birthdate', + displayName: 'Assistant Name', + name: 'assistantName', type: 'string', default: '', - description: 'The birthdate of the contact.', + description: 'The name of the assistant.', + }, + { + displayName: 'Assistant Phone', + name: 'Assistant Phone', + type: 'string', + default: '', + description: 'The telephone number of the assistant.', + }, + { + displayName: 'Birth Date', + name: 'birthdate', + type: 'dateTime', + default: '', + description: 'The birth date of the contact.', + }, + { + displayName: 'Department', + name: 'department', + type: 'string', + default: '', + description: 'The department of the contact.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'A description of the contact. Label is Contact Description. Limit: 32 KB.', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'Email address for the contact.', + }, + { + displayName: 'Email Bounced Date', + name: 'otherPostalCode', + type: 'dateTime', + default: '', + description: 'If bounce management is activated and an email sent to the contact bounces, the date and time the bounce occurred.', + }, + { + displayName: 'Email Bounced Reason', + name: 'emailBouncedReason', + type: 'string', + default: '', + description: 'If bounce management is activated and an email sent to the contact bounces, the reason the bounce occurred.', + }, + { + displayName: 'Fax', + name: 'fax', + type: 'string', + default: '', + description: 'Fax number for the contact. Label is Business Fax.', }, { displayName: 'First Name', @@ -177,17 +187,12 @@ export const contactFields = [ description: 'Home telephone number for the contact', }, { - displayName: 'Other City', - name: 'otherCity', + displayName: 'Jigsaw', + name: 'jigsaw', type: 'string', default: '', - }, - { - displayName: 'Department', - name: 'department', - type: 'string', - default: '', - description: 'The department of the contact.', + description: `references the ID of a contact in Data.com. + If a contact has a value in this field, it means that a contact was imported as a contact from Data.com.`, }, { displayName: 'Lead Source', @@ -199,39 +204,18 @@ export const contactFields = [ default: '', description: 'Source from which the lead was obtained.', }, - { - displayName: 'Other Phone', - name: 'otherPhone', - type: 'string', - default: '', - description: 'Telephone for alternate address.', - }, - { - displayName: 'Other State', - name: 'otherState', - type: 'string', - default: '', - }, - { - displayName: 'Salutation', - name: 'salutation', - type: 'string', - default: '', - description: 'Honorific abbreviation, word, or phrase to be used in front of name in greetings, such as Dr. or Mrs.', - }, - { - displayName: 'Description', - name: 'description', - type: 'string', - default: '', - description: 'A description of the contact. Label is Contact Description. Limit: 32 KB.', - }, { displayName: 'Mailing City', name: 'mailingCity', type: 'string', default: '', }, + { + displayName: 'Mailing Country', + name: 'mailingCountry', + type: 'string', + default: '', + }, { displayName: 'Mobile Phone', name: 'mobilePhone', @@ -240,11 +224,10 @@ export const contactFields = [ description: `Contact’s mobile phone number.`, }, { - displayName: 'Other Street', - name: 'otherStreet', + displayName: 'Mailing Postal Code', + name: 'mailingPostalCode', type: 'string', default: '', - description: 'Street for alternate address.', }, { displayName: 'Mailing State', @@ -252,6 +235,19 @@ export const contactFields = [ type: 'string', default: '', }, + { + displayName: 'Mailing Street', + name: 'mailingStreet', + type: 'string', + default: '', + description: 'Street address for mailing address.', + }, + { + displayName: 'Other City', + name: 'otherCity', + type: 'string', + default: '', + }, { displayName: 'Other Country', name: 'otherCountry', @@ -259,31 +255,11 @@ export const contactFields = [ default: '', }, { - displayName: 'Assistant Name', - name: 'assistantName', - type: 'string', - default: '', - description: 'The name of the assistant.', - }, - { - displayName: 'Mailing Street', - name: 'mailingStreet', - type: 'string', - default: '', - description: 'Street address for mailing address.', - }, - { - displayName: 'Assistant Phone', - name: 'Assistant Phone', - type: 'string', - default: '', - description: 'The telephone number of the assistant.', - }, - { - displayName: 'Mailing Country', - name: 'mailingCountry', + displayName: 'Other Phone', + name: 'otherPhone', type: 'string', default: '', + description: 'Telephone for alternate address.', }, { displayName: 'Other Postal Code', @@ -292,24 +268,48 @@ export const contactFields = [ default: '', }, { - displayName: 'Email Bounced Date', - name: 'otherPostalCode', - type: 'dateTime', - default: '', - description: 'If bounce management is activated and an email sent to the contact bounces, the date and time the bounce occurred.', - }, - { - displayName: 'Mailing Postal Code', - name: 'mailingPostalCode', + displayName: 'Other State', + name: 'otherState', type: 'string', default: '', }, { - displayName: 'Email Bounced Reason', - name: 'emailBouncedReason', + displayName: 'Other Street', + name: 'otherStreet', type: 'string', default: '', - description: 'If bounce management is activated and an email sent to the contact bounces, the reason the bounce occurred.', + description: 'Street for alternate address.', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'The owner of the contact.', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: 'Phone number for the contact.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title of the contact such as CEO or Vice President.', + }, + { + displayName: 'Salutation', + name: 'salutation', + type: 'string', + default: '', + description: 'Honorific abbreviation, word, or phrase to be used in front of name in greetings, such as Dr. or Mrs.', }, ], }, @@ -351,52 +351,6 @@ export const contactFields = [ }, }, options: [ - { - displayName: 'Fax', - name: 'fax', - type: 'string', - default: '', - description: 'Fax number for the contact. Label is Business Fax.', - }, - { - displayName: 'Email', - name: 'email', - type: 'string', - default: '', - description: 'Email address for the contact.', - }, - { - displayName: 'Phone', - name: 'phone', - type: 'string', - default: '', - description: 'Phone number for the contact.', - }, - { - displayName: 'Title', - name: 'title', - type: 'string', - default: '', - description: 'Title of the contact such as CEO or Vice President.', - }, - { - displayName: 'Jigsaw', - name: 'jigsaw', - type: 'string', - default: '', - description: `references the ID of a contact in Data.com. - If a contact has a value in this field, it means that a contact was imported as a contact from Data.com.`, - }, - { - displayName: 'Owner', - name: 'owner', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getUsers', - }, - default: '', - description: 'The owner of the contact.', - }, { displayName: 'Account', name: 'acconuntId', @@ -408,11 +362,67 @@ export const contactFields = [ description: 'ID of the account that is the parent of this contact.', }, { - displayName: 'Birthdate', - name: 'birthdate', + displayName: 'Assistant Name', + name: 'assistantName', type: 'string', default: '', - description: 'The birthdate of the contact.', + description: 'The name of the assistant.', + }, + { + displayName: 'Assistant Phone', + name: 'Assistant Phone', + type: 'string', + default: '', + description: 'The telephone number of the assistant.', + }, + { + displayName: 'Birth Date', + name: 'birthdate', + type: 'dateTime', + default: '', + description: 'The birth date of the contact.', + }, + { + displayName: 'Department', + name: 'department', + type: 'string', + default: '', + description: 'The department of the contact.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'A description of the contact. Label is Contact Description. Limit: 32 KB.', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'Email address for the contact.', + }, + { + displayName: 'Email Bounced Date', + name: 'emailBouncedDate', + type: 'dateTime', + default: '', + description: 'If bounce management is activated and an email sent to the contact bounces, the date and time the bounce occurred.', + }, + { + displayName: 'Email Bounced Reason', + name: 'emailBouncedReason', + type: 'string', + default: '', + description: 'If bounce management is activated and an email sent to the contact bounces, the reason the bounce occurred.', + }, + { + displayName: 'Fax', + name: 'fax', + type: 'string', + default: '', + description: 'Fax number for the contact. Label is Business Fax.', }, { displayName: 'First Name', @@ -429,17 +439,12 @@ export const contactFields = [ description: 'Home telephone number for the contact.', }, { - displayName: 'Other City', - name: 'otherCity', + displayName: 'Jigsaw', + name: 'jigsaw', type: 'string', default: '', - }, - { - displayName: 'Department', - name: 'department', - type: 'string', - default: '', - description: 'The department of the contact.', + description: `references the ID of a contact in Data.com. + If a contact has a value in this field, it means that a contact was imported as a contact from Data.com.`, }, { displayName: 'Lead Source', @@ -451,39 +456,37 @@ export const contactFields = [ default: '', description: 'Source from which the lead was obtained.', }, - { - displayName: 'Other Phone', - name: 'otherPhone', - type: 'string', - default: '', - description: 'Telephone for alternate address.', - }, - { - displayName: 'Other State', - name: 'otherState', - type: 'string', - default: '', - }, - { - displayName: 'Salutation', - name: 'salutation', - type: 'string', - default: '', - description: 'Honorific abbreviation, word, or phrase to be used in front of name in greetings, such as Dr. or Mrs.', - }, - { - displayName: 'Description', - name: 'description', - type: 'string', - default: '', - description: 'A description of the contact. Label is Contact Description. Limit: 32 KB.', - }, { displayName: 'Mailing City', name: 'mailingCity', type: 'string', default: '', }, + { + displayName: 'Mailing Country', + name: 'mailingCountry', + type: 'string', + default: '', + }, + { + displayName: 'Mailing State', + name: 'mailingState', + type: 'string', + default: '', + }, + { + displayName: 'Mailing Street', + name: 'mailingStreet', + type: 'string', + default: '', + description: 'Street address for mailing address.', + }, + { + displayName: 'Mailing Postal Code', + name: 'mailingPostalCode', + type: 'string', + default: '', + }, { displayName: 'Mobile Phone', name: 'mobilePhone', @@ -492,15 +495,8 @@ export const contactFields = [ description: `Contact’s mobile phone number.`, }, { - displayName: 'Other Street', - name: 'otherStreet', - type: 'string', - default: '', - description: 'Street for alternate address.', - }, - { - displayName: 'Mailing State', - name: 'mailingState', + displayName: 'Other City', + name: 'otherCity', type: 'string', default: '', }, @@ -511,31 +507,11 @@ export const contactFields = [ default: '', }, { - displayName: 'Assistant Name', - name: 'assistantName', - type: 'string', - default: '', - description: 'The name of the assistant.', - }, - { - displayName: 'Mailing Street', - name: 'mailingStreet', - type: 'string', - default: '', - description: 'Street address for mailing address.', - }, - { - displayName: 'Assistant Phone', - name: 'Assistant Phone', - type: 'string', - default: '', - description: 'The telephone number of the assistant.', - }, - { - displayName: 'Mailing Country', - name: 'mailingCountry', + displayName: 'Other Phone', + name: 'otherPhone', type: 'string', default: '', + description: 'Telephone for alternate address.', }, { displayName: 'Other Postal Code', @@ -544,24 +520,48 @@ export const contactFields = [ default: '', }, { - displayName: 'Email Bounced Date', - name: 'emailBouncedDate', - type: 'dateTime', - default: '', - description: 'If bounce management is activated and an email sent to the contact bounces, the date and time the bounce occurred.', - }, - { - displayName: 'Mailing Postal Code', - name: 'mailingPostalCode', + displayName: 'Other State', + name: 'otherState', type: 'string', default: '', }, { - displayName: 'Email Bounced Reason', - name: 'emailBouncedReason', + displayName: 'Other Street', + name: 'otherStreet', type: 'string', default: '', - description: 'If bounce management is activated and an email sent to the contact bounces, the reason the bounce occurred.', + description: 'Street for alternate address.', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'The owner of the contact.', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: 'Phone number for the contact.', + }, + { + displayName: 'Salutation', + name: 'salutation', + type: 'string', + default: '', + description: 'Honorific abbreviation, word, or phrase to be used in front of name in greetings, such as Dr. or Mrs.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title of the contact such as CEO or Vice President.', }, ], }, @@ -813,6 +813,13 @@ export const contactFields = [ }, description: 'Body of the note. Limited to 32 KB.', }, + { + displayName: 'Is Private', + name: 'isPrivate', + type: 'boolean', + default: false, + description: 'If true, only the note owner or a user with the “Modify All Data” permission can view the note or query it via the API', + }, { displayName: 'Owner', name: 'owner', @@ -823,13 +830,6 @@ export const contactFields = [ default: '', description: 'ID of the user who owns the note.', }, - { - displayName: 'Is Private', - name: 'isPrivate', - type: 'boolean', - default: false, - description: 'If true, only the note owner or a user with the “Modify All Data” permission can view the note or query it via the API', - }, ] }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts b/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts index 33fec16bf3..c8cd97d9fa 100644 --- a/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts @@ -22,7 +22,7 @@ export async function salesforceApiRequest(this: IExecuteFunctions | IExecuteSin //@ts-ignore return await this.helpers.requestOAuth.call(this, 'salesforceOAuth2Api', options); } catch (error) { - if (error.response && error.response.body && error.response.body[0].message) { + if (error.response && error.response.body && error.response.body[0] && error.response.body[0].message) { // Try to return the error prettier throw new Error(`Salesforce error response [${error.statusCode}]: ${error.response.body[0].message}`); } diff --git a/packages/nodes-base/nodes/Salesforce/LeadDescription.ts b/packages/nodes-base/nodes/Salesforce/LeadDescription.ts index ba9134741d..6c3c22c39e 100644 --- a/packages/nodes-base/nodes/Salesforce/LeadDescription.ts +++ b/packages/nodes-base/nodes/Salesforce/LeadDescription.ts @@ -13,36 +13,6 @@ export const leadOperations = [ }, }, options: [ - { - name: 'Create', - value: 'create', - description: 'Create a lead', - }, - { - name: 'Update', - value: 'update', - description: 'Update a lead', - }, - { - name: 'Get', - value: 'get', - description: 'Get a lead', - }, - { - name: 'Get Summary', - value: 'getSummary', - description: `Returns an overview of Lead's metadata.`, - }, - { - name: 'Get All', - value: 'getAll', - description: 'Get all leads', - }, - { - name: 'Delete', - value: 'delete', - description: 'Delete a lead', - }, { name: 'Add Lead To Campaign', value: 'addToCampaign', @@ -53,6 +23,36 @@ export const leadOperations = [ value: 'addNote', description: 'Add note to a lead', }, + { + name: 'Create', + value: 'create', + description: 'Create a lead', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a lead', + }, + { + name: 'Get', + value: 'get', + description: 'Get a lead', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all leads', + }, + { + name: 'Get Summary', + value: 'getSummary', + description: `Returns an overview of Lead's metadata.`, + }, + { + name: 'Update', + value: 'update', + description: 'Update a lead', + }, ], default: 'create', description: 'The operation to perform.', @@ -117,6 +117,17 @@ export const leadFields = [ }, }, options: [ + { + displayName: 'Annual Revenue', + name: 'annualRevenue', + type: 'number', + typeOptions: { + numberPrecision: 2, + numberStepSize: 1, + }, + default: '', + description: 'Annual revenue for the company of the lead.', + }, { displayName: 'City', name: 'city', @@ -124,6 +135,16 @@ export const leadFields = [ default: '', description: 'City for the address of the lead.', }, + { + displayName: 'Description', + name: 'description', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Description of the lead.', + }, { displayName: 'Email', name: 'email', @@ -132,25 +153,25 @@ export const leadFields = [ description: 'Email address for the lead.', }, { - displayName: 'Phone', - name: 'phone', + displayName: 'Fist Name', + name: 'firstname', type: 'string', default: '', - description: 'Phone number for the lead.', + description: 'First name of the lead. Limited to 40 characters.', }, { - displayName: 'State', - name: 'state', + displayName: 'Industry', + name: 'industry', type: 'string', default: '', - description: 'State for the address of the lead.', + description: 'Website for the lead.', }, { - displayName: 'Title', - name: 'title', - type: 'string', - default: '', - description: 'Title for the lead, for example CFO or CEO.', + displayName: 'Is Unread By Owner', + name: 'IsUnreadByOwner', + type: 'Boolean', + default: false, + description: 'If true, lead has been assigned, but not yet viewed. See Unread Leads for more information. Label is Unread By Owner.', }, { displayName: 'Jigsaw', @@ -160,6 +181,50 @@ export const leadFields = [ description: `references the ID of a contact in Data.com. If a lead has a value in this field, it means that a contact was imported as a lead from Data.com.`, }, + { + displayName: 'Lead Source', + name: 'leadSource', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLeadSources', + }, + default: '', + description: 'Source from which the lead was obtained.', + }, + { + displayName: 'Number Of Employees', + name: 'numberOfEmployees', + type: 'number', + typeOptions: { + numberStepSize: 1, + }, + default: '', + description: 'Number of employees at the lead’s company. Label is Employees.', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'The owner of the lead.', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: 'Phone number for the lead.', + }, + { + displayName: 'Postal Code', + name: 'postalCode', + type: 'string', + default: '', + description: 'Postal code for the address of the lead. Label is Zip/Postal Code.', + }, { displayName: 'Rating', name: 'rating', @@ -167,6 +232,20 @@ export const leadFields = [ default: '', description: 'Rating of the lead.', }, + { + displayName: 'Salutation', + name: 'salutation', + type: 'string', + default: '', + description: 'Salutation for the lead.', + }, + { + displayName: 'State', + name: 'state', + type: 'string', + default: '', + description: 'State for the address of the lead.', + }, { displayName: 'Status', name: 'status', @@ -185,14 +264,11 @@ export const leadFields = [ description: 'Street number and name for the address of the lead', }, { - displayName: 'Owner', - name: 'owner', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getUsers', - }, + displayName: 'Title', + name: 'title', + type: 'string', default: '', - description: 'The owner of the lead.', + description: 'Title for the lead, for example CFO or CEO.', }, { displayName: 'Website', @@ -201,82 +277,6 @@ export const leadFields = [ default: '', description: 'Website for the lead.', }, - { - displayName: 'Industry', - name: 'industry', - type: 'string', - default: '', - description: 'Website for the lead.', - }, - { - displayName: 'Fist Name', - name: 'firstname', - type: 'string', - default: '', - description: 'First name of the lead. Limited to 40 characters.', - }, - { - displayName: 'Lead Source', - name: 'leadSource', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getLeadSources', - }, - default: '', - description: 'Source from which the lead was obtained.', - }, - { - displayName: 'Postal Code', - name: 'postalCode', - type: 'string', - default: '', - description: 'Postal code for the address of the lead. Label is Zip/Postal Code.', - }, - { - displayName: 'Salutation', - name: 'salutation', - type: 'string', - default: '', - description: 'Salutation for the lead.', - }, - { - displayName: 'Description', - name: 'description', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'Description of the lead.', - }, - { - displayName: 'Annual Revenue', - name: 'annualRevenue', - type: 'number', - typeOptions: { - numberPrecision: 2, - numberStepSize: 1, - }, - default: '', - description: 'Annual revenue for the company of the lead.', - }, - { - displayName: 'Number Of Employees', - name: 'numberOfEmployees', - type: 'number', - typeOptions: { - numberStepSize: 1, - }, - default: '', - description: 'Number of employees at the lead’s company. Label is Employees.', - }, - { - displayName: 'Is Unread By Owner', - name: 'IsUnreadByOwner', - type: 'Boolean', - default: false, - description: 'If true, lead has been assigned, but not yet viewed. See Unread Leads for more information. Label is Unread By Owner.', - }, ] }, /* -------------------------------------------------------------------------- */ @@ -318,18 +318,15 @@ export const leadFields = [ }, options: [ { - displayName: 'Company', - name: 'company', - type: 'string', + displayName: 'Annual Revenue', + name: 'annualRevenue', + type: 'number', + typeOptions: { + numberPrecision: 2, + numberStepSize: 1, + }, default: '', - description: 'Company of the lead. If person account record types have been enabled, and if the value of Company is null, the lead converts to a person account.', - }, - { - displayName: 'Last Name', - name: 'lastname', - type: 'string', - default: '', - description: 'Required. Last name of the lead. Limited to 80 characters.', + description: 'Annual revenue for the company of the lead.', }, { displayName: 'City', @@ -338,6 +335,23 @@ export const leadFields = [ default: '', description: 'City for the address of the lead.', }, + { + displayName: 'Company', + name: 'company', + type: 'string', + default: '', + description: 'Company of the lead. If person account record types have been enabled, and if the value of Company is null, the lead converts to a person account.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Description of the lead.', + }, { displayName: 'Email', name: 'email', @@ -346,25 +360,25 @@ export const leadFields = [ description: 'Email address for the lead.', }, { - displayName: 'Phone', - name: 'phone', + displayName: 'Fist Name', + name: 'firstname', type: 'string', default: '', - description: 'Phone number for the lead.', + description: 'First name of the lead. Limited to 40 characters.', }, { - displayName: 'State', - name: 'state', + displayName: 'Industry', + name: 'industry', type: 'string', default: '', - description: 'State for the address of the lead.', + description: 'Website for the lead.', }, { - displayName: 'Title', - name: 'title', - type: 'string', - default: '', - description: 'Title for the lead, for example CFO or CEO.', + displayName: 'Is Unread By Owner', + name: 'IsUnreadByOwner', + type: 'Boolean', + default: false, + description: 'If true, lead has been assigned, but not yet viewed. See Unread Leads for more information. Label is Unread By Owner.', }, { displayName: 'Jigsaw', @@ -374,6 +388,57 @@ export const leadFields = [ description: `references the ID of a contact in Data.com. If a lead has a value in this field, it means that a contact was imported as a lead from Data.com.`, }, + { + displayName: 'Last Name', + name: 'lastname', + type: 'string', + default: '', + description: 'Required. Last name of the lead. Limited to 80 characters.', + }, + { + displayName: 'Lead Source', + name: 'leadSource', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLeadSources', + }, + default: '', + description: 'Source from which the lead was obtained.', + }, + { + displayName: 'Number Of Employees', + name: 'numberOfEmployees', + type: 'number', + typeOptions: { + numberStepSize: 1, + }, + default: '', + description: 'Number of employees at the lead’s company. Label is Employees.', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'The owner of the lead.', + }, + { + displayName: 'Postal Code', + name: 'postalCode', + type: 'string', + default: '', + description: 'Postal code for the address of the lead. Label is Zip/Postal Code.', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: 'Phone number for the lead.', + }, { displayName: 'Rating', name: 'rating', @@ -381,6 +446,20 @@ export const leadFields = [ default: '', description: 'Rating of the lead.', }, + { + displayName: 'Salutation', + name: 'salutation', + type: 'string', + default: '', + description: 'Salutation for the lead.', + }, + { + displayName: 'State', + name: 'state', + type: 'string', + default: '', + description: 'State for the address of the lead.', + }, { displayName: 'Status', name: 'status', @@ -399,14 +478,11 @@ export const leadFields = [ description: 'Street number and name for the address of the lead', }, { - displayName: 'Owner', - name: 'owner', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getUsers', - }, + displayName: 'Title', + name: 'title', + type: 'string', default: '', - description: 'The owner of the lead.', + description: 'Title for the lead, for example CFO or CEO.', }, { displayName: 'Website', @@ -415,82 +491,6 @@ export const leadFields = [ default: '', description: 'Website for the lead.', }, - { - displayName: 'Industry', - name: 'industry', - type: 'string', - default: '', - description: 'Website for the lead.', - }, - { - displayName: 'Fist Name', - name: 'firstname', - type: 'string', - default: '', - description: 'First name of the lead. Limited to 40 characters.', - }, - { - displayName: 'Lead Source', - name: 'leadSource', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getLeadSources', - }, - default: '', - description: 'Source from which the lead was obtained.', - }, - { - displayName: 'Postal Code', - name: 'postalCode', - type: 'string', - default: '', - description: 'Postal code for the address of the lead. Label is Zip/Postal Code.', - }, - { - displayName: 'Salutation', - name: 'salutation', - type: 'string', - default: '', - description: 'Salutation for the lead.', - }, - { - displayName: 'Description', - name: 'description', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'Description of the lead.', - }, - { - displayName: 'Annual Revenue', - name: 'annualRevenue', - type: 'number', - typeOptions: { - numberPrecision: 2, - numberStepSize: 1, - }, - default: '', - description: 'Annual revenue for the company of the lead.', - }, - { - displayName: 'Number Of Employees', - name: 'numberOfEmployees', - type: 'number', - typeOptions: { - numberStepSize: 1, - }, - default: '', - description: 'Number of employees at the lead’s company. Label is Employees.', - }, - { - displayName: 'Is Unread By Owner', - name: 'IsUnreadByOwner', - type: 'Boolean', - default: false, - description: 'If true, lead has been assigned, but not yet viewed. See Unread Leads for more information. Label is Unread By Owner.', - }, ] }, @@ -740,6 +740,13 @@ export const leadFields = [ }, description: 'Body of the note. Limited to 32 KB.', }, + { + displayName: 'Is Private', + name: 'isPrivate', + type: 'boolean', + default: false, + description: 'If true, only the note owner or a user with the “Modify All Data” permission can view the note or query it via the API', + }, { displayName: 'Owner', name: 'owner', @@ -750,13 +757,6 @@ export const leadFields = [ default: '', description: 'ID of the user who owns the note.', }, - { - displayName: 'Is Private', - name: 'isPrivate', - type: 'boolean', - default: false, - description: 'If true, only the note owner or a user with the “Modify All Data” permission can view the note or query it via the API', - }, ] }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Salesforce/OpportunityDescription.ts b/packages/nodes-base/nodes/Salesforce/OpportunityDescription.ts index 89efd96478..8627e3b57f 100644 --- a/packages/nodes-base/nodes/Salesforce/OpportunityDescription.ts +++ b/packages/nodes-base/nodes/Salesforce/OpportunityDescription.ts @@ -13,40 +13,40 @@ export const opportunityOperations = [ }, }, options: [ + { + name: 'Add Note', + value: 'addNote', + description: 'Add note to an opportunity', + }, { name: 'Create', value: 'create', description: 'Create an opportunity', }, - { - name: 'Update', - value: 'update', - description: 'Update an opportunity', - }, - { - name: 'Get', - value: 'get', - description: 'Get an opportunity', - }, - { - name: 'Get Summary', - value: 'getSummary', - description: `Returns an overview of opportunity's metadata.`, - }, - { - name: 'Get All', - value: 'getAll', - description: 'Get all opportunitys', - }, { name: 'Delete', value: 'delete', description: 'Delete an opportunity', }, { - name: 'Add Note', - value: 'addNote', - description: 'Add note to an opportunity', + name: 'Get', + value: 'get', + description: 'Get an opportunity', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all opportunitys', + }, + { + name: 'Get Summary', + value: 'getSummary', + description: `Returns an overview of opportunity's metadata.`, + }, + { + name: 'Update', + value: 'update', + description: 'Update an opportunity', }, ], default: 'create', @@ -133,6 +133,101 @@ export const opportunityFields = [ }, }, options: [ + { + displayName: 'Account', + name: 'accountId', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getAccounts', + }, + description: 'ID of the account associated with this opportunity.', + }, + { + displayName: 'Amount', + name: 'amount', + type: 'number', + typeOptions: { + numberPrecision: 2, + }, + default: '', + description: 'Estimated total sale amount', + }, + { + displayName: 'Campaign', + name: 'campaignId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + default: '', + description: 'Id of the campaign that needs to be fetched', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'A description of the opportunity. Label is Contact Description. Limit: 32 KB.', + }, + { + displayName: 'Forecast Category Name', + name: 'forecastCategoryName', + type: 'string', + default: '', + description: 'It is implied, but not directly controlled, by the StageName field', + }, + { + displayName: 'Lead Source', + name: 'leadSource', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLeadSources', + }, + default: '', + description: 'Source from which the lead was obtained.', + }, + { + displayName: 'Next Step', + name: 'nextStep', + type: 'string', + default: '', + description: 'Description of next task in closing opportunity. Limit: 255 characters.', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'The owner of the opportunity.', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: 'Phone number for the opportunity.', + }, + { + displayName: 'Pricebook2 Id', + name: 'pricebook2Id', + type: 'string', + default: '', + description: 'ID of a related Pricebook2 object', + }, + { + displayName: 'Probability', + name: 'probability', + type: 'number', + typeOptions: { + numberPrecision: 1, + }, + default: '', + description: 'Percentage of estimated confidence in closing the opportunity', + }, { displayName: 'Type', name: 'type', @@ -150,101 +245,6 @@ export const opportunityFields = [ ], description: 'Type of opportunity. For example, Existing Business or New Business. Label is Opportunity Type.', }, - { - displayName: 'Amount', - name: 'amount', - type: 'number', - typeOptions: { - numberPrecision: 2, - }, - default: '', - description: 'Estimated total sale amount', - }, - { - displayName: 'Phone', - name: 'phone', - type: 'string', - default: '', - description: 'Phone number for the opportunity.', - }, - { - displayName: 'Owner', - name: 'owner', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getUsers', - }, - default: '', - description: 'The owner of the opportunity.', - }, - { - displayName: 'Next Step', - name: 'nextStep', - type: 'string', - default: '', - description: 'Description of next task in closing opportunity. Limit: 255 characters.', - }, - { - displayName: 'Account', - name: 'accountId', - type: 'options', - default: '', - typeOptions: { - loadOptionsMethod: 'getAccounts', - }, - description: 'ID of the account associated with this opportunity.', - }, - { - displayName: 'Campaign', - name: 'campaignId', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getCampaigns', - }, - default: '', - description: 'Id of the campaign that needs to be fetched', - }, - { - displayName: 'Lead Source', - name: 'leadSource', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getLeadSources', - }, - default: '', - description: 'Source from which the lead was obtained.', - }, - { - displayName: 'Description', - name: 'description', - type: 'string', - default: '', - description: 'A description of the opportunity. Label is Contact Description. Limit: 32 KB.', - }, - { - displayName: 'Probability', - name: 'probability', - type: 'number', - typeOptions: { - numberPrecision: 1, - }, - default: '', - description: 'Percentage of estimated confidence in closing the opportunity', - }, - { - displayName: 'Pricebook2 Id', - name: 'pricebook2Id', - type: 'string', - default: '', - description: 'ID of a related Pricebook2 object', - }, - { - displayName: 'Forecast Category Name', - name: 'forecastCategoryName', - type: 'string', - default: '', - description: 'It is implied, but not directly controlled, by the StageName field', - }, ], }, /* -------------------------------------------------------------------------- */ @@ -286,11 +286,34 @@ export const opportunityFields = [ }, options: [ { - displayName: 'Name', - name: 'name', - type: 'string', + displayName: 'Account', + name: 'accountId', + type: 'options', default: '', - description: 'Required. Last name of the opportunity. Limited to 80 characters.', + typeOptions: { + loadOptionsMethod: 'getAccounts', + }, + description: 'ID of the account associated with this opportunity.', + }, + { + displayName: 'Amount', + name: 'amount', + type: 'number', + typeOptions: { + numberPrecision: 2, + }, + default: '', + description: 'Estimated total sale amount', + }, + { + displayName: 'Campaign', + name: 'campaignId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + default: '', + description: 'Id of the campaign that needs to be fetched', }, { displayName: 'Close Date', @@ -299,6 +322,78 @@ export const opportunityFields = [ default: '', description: 'Required. Date when the opportunity is expected to close.', }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'A description of the opportunity. Label is Contact Description. Limit: 32 KB.', + }, + { + displayName: 'Forecast Category Name', + name: 'forecastCategoryName', + type: 'string', + default: '', + description: 'It is implied, but not directly controlled, by the StageName field', + }, + { + displayName: 'Lead Source', + name: 'leadSource', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLeadSources', + }, + default: '', + description: 'Source from which the lead was obtained.', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Required. Last name of the opportunity. Limited to 80 characters.', + }, + { + displayName: 'Next Step', + name: 'nextStep', + type: 'string', + default: '', + description: 'Description of next task in closing opportunity. Limit: 255 characters.', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'The owner of the opportunity.', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: 'Phone number for the opportunity.', + }, + { + displayName: 'Pricebook2 Id', + name: 'pricebook2Id', + type: 'string', + default: '', + description: 'ID of a related Pricebook2 object', + }, + { + displayName: 'Probability', + name: 'probability', + type: 'number', + typeOptions: { + numberPrecision: 1, + }, + default: '', + description: 'Percentage of estimated confidence in closing the opportunity', + }, { displayName: 'Stage Name', name: 'stageName', @@ -326,101 +421,6 @@ export const opportunityFields = [ ], description: 'Type of opportunity. For example, Existing Business or New Business. Label is Opportunity Type.', }, - { - displayName: 'Amount', - name: 'amount', - type: 'number', - typeOptions: { - numberPrecision: 2, - }, - default: '', - description: 'Estimated total sale amount', - }, - { - displayName: 'Phone', - name: 'phone', - type: 'string', - default: '', - description: 'Phone number for the opportunity.', - }, - { - displayName: 'Owner', - name: 'owner', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getUsers', - }, - default: '', - description: 'The owner of the opportunity.', - }, - { - displayName: 'Next Step', - name: 'nextStep', - type: 'string', - default: '', - description: 'Description of next task in closing opportunity. Limit: 255 characters.', - }, - { - displayName: 'Account', - name: 'accountId', - type: 'options', - default: '', - typeOptions: { - loadOptionsMethod: 'getAccounts', - }, - description: 'ID of the account associated with this opportunity.', - }, - { - displayName: 'Campaign', - name: 'campaignId', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getCampaigns', - }, - default: '', - description: 'Id of the campaign that needs to be fetched', - }, - { - displayName: 'Lead Source', - name: 'leadSource', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getLeadSources', - }, - default: '', - description: 'Source from which the lead was obtained.', - }, - { - displayName: 'Description', - name: 'description', - type: 'string', - default: '', - description: 'A description of the opportunity. Label is Contact Description. Limit: 32 KB.', - }, - { - displayName: 'Probability', - name: 'probability', - type: 'number', - typeOptions: { - numberPrecision: 1, - }, - default: '', - description: 'Percentage of estimated confidence in closing the opportunity', - }, - { - displayName: 'Pricebook2 Id', - name: 'pricebook2Id', - type: 'string', - default: '', - description: 'ID of a related Pricebook2 object', - }, - { - displayName: 'Forecast Category Name', - name: 'forecastCategoryName', - type: 'string', - default: '', - description: 'It is implied, but not directly controlled, by the StageName field', - }, ], }, @@ -603,6 +603,13 @@ export const opportunityFields = [ }, description: 'Body of the note. Limited to 32 KB.', }, + { + displayName: 'Is Private', + name: 'isPrivate', + type: 'boolean', + default: false, + description: 'If true, only the note owner or a user with the “Modify All Data” permission can view the note or query it via the API', + }, { displayName: 'Owner', name: 'owner', @@ -613,13 +620,6 @@ export const opportunityFields = [ default: '', description: 'ID of the user who owns the note.', }, - { - displayName: 'Is Private', - name: 'isPrivate', - type: 'boolean', - default: false, - description: 'If true, only the note owner or a user with the “Modify All Data” permission can view the note or query it via the API', - }, ] }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts b/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts index 0c07520d61..5048175745 100644 --- a/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts +++ b/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts @@ -1,14 +1,48 @@ import { IExecuteFunctions, } from 'n8n-core'; + import { IDataObject, ILoadOptionsFunctions, - INodeTypeDescription, INodeExecutionData, INodeType, + INodeTypeDescription, INodePropertyOptions, } from 'n8n-workflow'; + +import { + accountFields, + accountOperations, +} from './AccountDescription'; +import { + IAccount, +} from './AccountInterface'; +import { + attachmentFields, + attachmentOperations, +} from './AttachmentDescription'; +import { + IAttachment, +} from './AttachmentInterface'; +import { + ICampaignMember, +} from './CampaignMemberInterface'; +import { + caseFields, + caseOperations, +} from './CaseDescription'; +import { + ICase, + ICaseComment, +} from './CaseInterface'; +import { + contactFields, + contactOperations, +} from './ContactDescription'; +import { + IContact, +} from './ContactInterface'; import { salesforceApiRequest, salesforceApiRequestAllItems, @@ -17,58 +51,27 @@ import { leadFields, leadOperations, } from './LeadDescription'; -import { - contactFields, - contactOperations, -} from './ContactDescription'; -import { - opportunityOperations, - opportunityFields, - } from './OpportunityDescription'; - import { - accountOperations, - accountFields, - } from './AccountDescription'; - import { - caseOperations, - caseFields, - } from './CaseDescription'; - import { - taskOperations, - taskFields, - } from './TaskDescription'; - import { - attachmentOperations, - attachmentFields, - } from './AttachmentDescription'; - import { - IOpportunity, -} from './OpportunityInterface'; -import { - ICampaignMember, -} from './CampaignMemberInterface'; import { ILead, } from './LeadInterface'; import { - IContact, - } from './ContactInterface'; - import { - IAccount, - } from './AccountInterface'; - import { INote, } from './NoteInterface'; import { - ICase, - ICaseComment, -} from './CaseInterface'; + opportunityFields, + opportunityOperations, +} from './OpportunityDescription'; +import { + IOpportunity, +} from './OpportunityInterface'; +import { + taskFields, + taskOperations, +} from './TaskDescription'; import { ITask, } from './TaskInterface'; -import { - IAttachment, -} from './AttachmentInterface'; + export class Salesforce implements INodeType { description: INodeTypeDescription = { @@ -97,41 +100,42 @@ export class Salesforce implements INodeType { name: 'resource', type: 'options', options: [ - { - name: 'Lead', - value: 'lead', - description: 'Represents a prospect or potential .', - }, - { - name: 'Contact', - value: 'contact', - description: 'Represents a contact, which is an individual associated with an account.', - }, - { - name: 'Opportunity', - value: 'opportunity', - description: 'Represents an opportunity, which is a sale or pending deal.', - }, { name: 'Account', value: 'account', description: 'Represents an individual account, which is an organization or person involved with your business (such as customers, competitors, and partners).', }, + { + name: 'Attachment', + value: 'attachment', + description: 'Represents a file that a has uploaded and attached to a parent object.', + }, { name: 'Case', value: 'case', description: 'Represents a case, which is a customer issue or problem.', }, + { + name: 'Contact', + value: 'contact', + description: 'Represents a contact, which is an individual associated with an account.', + }, + { + name: 'Lead', + value: 'lead', + description: 'Represents a prospect or potential .', + }, + { + name: 'Opportunity', + value: 'opportunity', + description: 'Represents an opportunity, which is a sale or pending deal.', + }, { name: 'Task', value: 'task', description: 'Represents a business activity such as making a phone call or other to-do items. In the user interface, and records are collectively referred to as activities.', }, - { - name: 'Attachment', - value: 'attachment', - description: 'Represents a file that a has uploaded and attached to a parent object.', - }, + ], default: 'lead', description: 'Resource to consume.', @@ -195,7 +199,7 @@ export class Salesforce implements INodeType { // select them easily async getLeadSources(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - //find a way to filter this object to get just the lead sources instead of the whole object + // TODO: find a way to filter this object to get just the lead sources instead of the whole object const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/lead/describe'); for (const field of fields) { if (field.name === 'LeadSource') { @@ -251,7 +255,7 @@ export class Salesforce implements INodeType { // select them easily async getStages(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - //find a way to filter this object to get just the lead sources instead of the whole object + // TODO: find a way to filter this object to get just the lead sources instead of the whole object const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/opportunity/describe'); for (const field of fields) { if (field.name === 'StageName') { @@ -271,7 +275,7 @@ export class Salesforce implements INodeType { // select them easily async getAccountTypes(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - //find a way to filter this object to get just the lead sources instead of the whole object + // TODO: find a way to filter this object to get just the lead sources instead of the whole object const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/account/describe'); for (const field of fields) { if (field.name === 'Type') { @@ -291,7 +295,7 @@ export class Salesforce implements INodeType { // select them easily async getAccountSources(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - //find a way to filter this object to get just the lead sources instead of the whole object + // TODO: find a way to filter this object to get just the lead sources instead of the whole object const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/account/describe'); for (const field of fields) { if (field.name === 'AccountSource') { @@ -311,7 +315,7 @@ export class Salesforce implements INodeType { // select them easily async getCaseTypes(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - //find a way to filter this object to get just the lead sources instead of the whole object + // TODO: find a way to filter this object to get just the lead sources instead of the whole object const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/case/describe'); for (const field of fields) { if (field.name === 'Type') { @@ -331,7 +335,7 @@ export class Salesforce implements INodeType { // select them easily async getCaseStatuses(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - //find a way to filter this object to get just the lead sources instead of the whole object + // TODO: find a way to filter this object to get just the lead sources instead of the whole object const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/case/describe'); for (const field of fields) { if (field.name === 'Status') { @@ -351,7 +355,7 @@ export class Salesforce implements INodeType { // select them easily async getCaseReasons(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - //find a way to filter this object to get just the lead sources instead of the whole object + // TODO: find a way to filter this object to get just the lead sources instead of the whole object const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/case/describe'); for (const field of fields) { if (field.name === 'Reason') { @@ -371,7 +375,7 @@ export class Salesforce implements INodeType { // select them easily async getCaseOrigins(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - //find a way to filter this object to get just the lead sources instead of the whole object + // TODO: find a way to filter this object to get just the lead sources instead of the whole object const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/case/describe'); for (const field of fields) { if (field.name === 'Origin') { @@ -391,7 +395,7 @@ export class Salesforce implements INodeType { // select them easily async getCasePriorities(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - //find a way to filter this object to get just the lead sources instead of the whole object + // TODO: find a way to filter this object to get just the lead sources instead of the whole object const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/case/describe'); for (const field of fields) { if (field.name === 'Priority') { @@ -411,7 +415,7 @@ export class Salesforce implements INodeType { // select them easily async getTaskStatuses(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - //find a way to filter this object to get just the lead sources instead of the whole object + // TODO: find a way to filter this object to get just the lead sources instead of the whole object const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/task/describe'); for (const field of fields) { if (field.name === 'Status') { @@ -431,7 +435,7 @@ export class Salesforce implements INodeType { // select them easily async getTaskSubjects(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - //find a way to filter this object to get just the lead sources instead of the whole object + // TODO: find a way to filter this object to get just the lead sources instead of the whole object const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/task/describe'); for (const field of fields) { if (field.name === 'Subject') { @@ -451,7 +455,7 @@ export class Salesforce implements INodeType { // select them easily async getTaskCallTypes(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - //find a way to filter this object to get just the lead sources instead of the whole object + // TODO: find a way to filter this object to get just the lead sources instead of the whole object const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/task/describe'); for (const field of fields) { if (field.name === 'CallType') { @@ -471,7 +475,7 @@ export class Salesforce implements INodeType { // select them easily async getTaskPriorities(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - //find a way to filter this object to get just the lead sources instead of the whole object + // TODO: find a way to filter this object to get just the lead sources instead of the whole object const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/task/describe'); for (const field of fields) { if (field.name === 'Priority') { @@ -491,7 +495,7 @@ export class Salesforce implements INodeType { // select them easily async getTaskRecurrenceTypes(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - //find a way to filter this object to get just the lead sources instead of the whole object + // TODO: find a way to filter this object to get just the lead sources instead of the whole object const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/task/describe'); for (const field of fields) { if (field.name === 'RecurrenceType') { @@ -511,7 +515,7 @@ export class Salesforce implements INodeType { // select them easily async getTaskRecurrenceInstances(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - //find a way to filter this object to get just the lead sources instead of the whole object + // TODO: find a way to filter this object to get just the lead sources instead of the whole object const { fields } = await salesforceApiRequest.call(this, 'GET', '/sobjects/task/describe'); for (const field of fields) { if (field.name === 'RecurrenceInstance') { @@ -533,13 +537,12 @@ export class Salesforce implements INodeType { async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); const returnData: IDataObject[] = []; - const length = items.length as unknown as number; let responseData; const qs: IDataObject = {}; const resource = this.getNodeParameter('resource', 0) as string; const operation = this.getNodeParameter('operation', 0) as string; - for (let i = 0; i < length; i++) { + for (let i = 0; i < items.length; i++) { if (resource === 'lead') { //https://developer.salesforce.com/docs/api-explorer/sobject/Lead/post-lead if (operation === 'create') { @@ -703,10 +706,10 @@ export class Salesforce implements INodeType { if (operation === 'getAll') { const returnAll = this.getNodeParameter('returnAll', i) as boolean; const options = this.getNodeParameter('options', i) as IDataObject; - const fields = ['id']; + const fields = ['id,company,firstname,lastname,street,postalCode,city,email,status']; if (options.fields) { // @ts-ignore - fields.push(...options.fields.split(',')) + fields.push(...options.fields.split(',')); } try { if (returnAll) { @@ -978,10 +981,10 @@ export class Salesforce implements INodeType { if (operation === 'getAll') { const returnAll = this.getNodeParameter('returnAll', i) as boolean; const options = this.getNodeParameter('options', i) as IDataObject; - const fields = ['id']; + const fields = ['id,firstname,lastname,email']; if (options.fields) { // @ts-ignore - fields.push(...options.fields.split(',')) + fields.push(...options.fields.split(',')); } try { if (returnAll) { @@ -1149,10 +1152,10 @@ export class Salesforce implements INodeType { if (operation === 'getAll') { const returnAll = this.getNodeParameter('returnAll', i) as boolean; const options = this.getNodeParameter('options', i) as IDataObject; - const fields = ['id']; + const fields = ['id,accountId,amount,probability,type']; if (options.fields) { // @ts-ignore - fields.push(...options.fields.split(',')) + fields.push(...options.fields.split(',')); } try { if (returnAll) { @@ -1374,10 +1377,10 @@ export class Salesforce implements INodeType { if (operation === 'getAll') { const returnAll = this.getNodeParameter('returnAll', i) as boolean; const options = this.getNodeParameter('options', i) as IDataObject; - const fields = ['id']; + const fields = ['id,name,type']; if (options.fields) { // @ts-ignore - fields.push(...options.fields.split(',')) + fields.push(...options.fields.split(',')); } try { if (returnAll) { @@ -1539,10 +1542,10 @@ export class Salesforce implements INodeType { if (operation === 'getAll') { const returnAll = this.getNodeParameter('returnAll', i) as boolean; const options = this.getNodeParameter('options', i) as IDataObject; - const fields = ['id']; + const fields = ['id,accountId,contactId,priority,status,subject,type']; if (options.fields) { // @ts-ignore - fields.push(...options.fields.split(',')) + fields.push(...options.fields.split(',')); } try { if (returnAll) { @@ -1753,10 +1756,10 @@ export class Salesforce implements INodeType { if (operation === 'getAll') { const returnAll = this.getNodeParameter('returnAll', i) as boolean; const options = this.getNodeParameter('options', i) as IDataObject; - const fields = ['id']; + const fields = ['id,subject,status,priority']; if (options.fields) { // @ts-ignore - fields.push(...options.fields.split(',')) + fields.push(...options.fields.split(',')); } try { if (returnAll) { @@ -1850,10 +1853,10 @@ export class Salesforce implements INodeType { if (operation === 'getAll') { const returnAll = this.getNodeParameter('returnAll', i) as boolean; const options = this.getNodeParameter('options', i) as IDataObject; - const fields = ['id']; + const fields = ['id,name']; if (options.fields) { // @ts-ignore - fields.push(...options.fields.split(',')) + fields.push(...options.fields.split(',')); } try { if (returnAll) { @@ -1885,6 +1888,14 @@ export class Salesforce implements INodeType { if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData as IDataObject[]); } else { + if (responseData === undefined) { + // Make sure that always valid JSON gets returned which also matches the + // Salesforce default response + responseData = { + errors: [], + success: true, + }; + } returnData.push(responseData as IDataObject); } } diff --git a/packages/nodes-base/nodes/Salesforce/TaskDescription.ts b/packages/nodes-base/nodes/Salesforce/TaskDescription.ts index 79e13ec34f..2278785044 100644 --- a/packages/nodes-base/nodes/Salesforce/TaskDescription.ts +++ b/packages/nodes-base/nodes/Salesforce/TaskDescription.ts @@ -19,29 +19,29 @@ export const taskOperations = [ description: 'Create a task', }, { - name: 'Update', - value: 'update', - description: 'Update a task', + name: 'Delete', + value: 'delete', + description: 'Delete a task', }, { name: 'Get', value: 'get', description: 'Get a task', }, - { - name: 'Get Summary', - value: 'getSummary', - description: `Returns an overview of task's metadata.`, - }, { name: 'Get All', value: 'getAll', description: 'Get all tasks', }, { - name: 'Delete', - value: 'delete', - description: 'Delete a task', + name: 'Get Summary', + value: 'getSummary', + description: `Returns an overview of task's metadata.`, + }, + { + name: 'Update', + value: 'update', + description: 'Update a task', }, ], default: 'create', @@ -92,85 +92,6 @@ export const taskFields = [ }, }, options: [ - { - displayName: 'Who Id', - name: 'whoId', - type: 'string', - default: '', - description: `The WhoId represents a human such as a lead or a contact.
- WhoIds are polymorphic. Polymorphic means a WhoId is equivalent to a contact’s ID or a lead’s ID.`, - }, - { - displayName: 'What Id', - name: 'whatId', - type: 'string', - default: '', - description: `The WhatId represents nonhuman objects such as accounts, opportunities,
- campaigns, cases, or custom objects. WhatIds are polymorphic. Polymorphic means a
- WhatId is equivalent to the ID of a related object.`, - }, - { - displayName: 'Owner', - name: 'owner', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getUsers', - }, - default: '', - description: 'ID of the User who owns the record.', - }, - { - displayName: 'Subject', - name: 'subject', - type: 'options', - default: '', - typeOptions: { - loadOptionsMethod: 'getTaskSubjects', - }, - description: 'The subject line of the task, such as “Call” or “Send Quote.” Limit: 255 characters.', - }, - { - displayName: 'Call Type', - name: 'callType', - type: 'options', - default: '', - typeOptions: { - loadOptionsMethod: 'getTaskCallTypes', - }, - description: 'The type of call being answered: Inbound, Internal, or Outbound.', - }, - { - displayName: 'Priority', - name: 'priority', - type: 'options', - default: '', - typeOptions: { - loadOptionsMethod: 'getTaskPriorities', - }, - description: `Indicates the importance or urgency of a task, such as high or low.`, - }, - { - displayName: 'Call Object', - name: 'callObject', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: `Name of a call center. Limit is 255 characters.
- Not subject to field-level security, available for any user in an
- organization with Salesforce CRM Call Center.`, - }, - { - displayName: 'Description', - name: 'description', - type: 'string', - default: '', - typeOptions: { - alwaysOpenEditWindow: true, - }, - description: 'Contains a text description of the task.', - }, { displayName: 'Activity Date', name: 'activityDate', @@ -180,23 +101,6 @@ export const taskFields = [ This field has a timestamp that is always set to midnight
in the Coordinated Universal Time (UTC) time zone.`, }, - { - displayName: 'Is ReminderSet', - name: 'isReminderSet', - type: 'boolean', - default: false, - description: 'Indicates whether a popup reminder has been set for the task (true) or not (false).', - }, - { - displayName: 'Recurrence Type', - name: 'recurrenceType', - type: 'options', - default: '', - typeOptions: { - loadOptionsMethod: 'getTaskRecurrenceTypes' - }, - description: 'Website for the task.', - }, { displayName: 'Call Disposition', name: 'callDisposition', @@ -210,14 +114,81 @@ export const taskFields = [ in an organization with Salesforce CRM Call Center.`, }, { - displayName: 'Reminder Date Time', - name: 'reminderDateTime', - type: 'dateTime', + displayName: 'Call Duration In Seconds', + name: 'callDurationInSeconds', + type: 'number', default: '', - description: `Represents the time when the reminder is scheduled to fire,
- if IsReminderSet is set to true. If IsReminderSet is set to false, then the
- user may have deselected the reminder checkbox in the Salesforce user interface,
- or the reminder has already fired at the time indicated by the value.`, + description: `Duration of the call in seconds. Not subject to field-level security,
+ available for any user in an organization with Salesforce CRM Call Cente`, + }, + { + displayName: 'Call Object', + name: 'callObject', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: `Name of a call center. Limit is 255 characters.
+ Not subject to field-level security, available for any user in an
+ organization with Salesforce CRM Call Center.`, + }, + { + displayName: 'Call Type', + name: 'callType', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getTaskCallTypes', + }, + description: 'The type of call being answered: Inbound, Internal, or Outbound.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + description: 'Contains a text description of the task.', + }, + { + displayName: 'Is ReminderSet', + name: 'isReminderSet', + type: 'boolean', + default: false, + description: 'Indicates whether a popup reminder has been set for the task (true) or not (false).', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'ID of the User who owns the record.', + }, + { + displayName: 'Priority', + name: 'priority', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getTaskPriorities', + }, + description: `Indicates the importance or urgency of a task, such as high or low.`, + }, + { + displayName: 'Recurrence Type', + name: 'recurrenceType', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getTaskRecurrenceTypes' + }, + description: 'Recurrence Type of the task.', }, { displayName: 'Recurrence Instance', @@ -244,12 +215,15 @@ export const taskFields = [ description: 'The day of the month in which the task repeats.', }, { - displayName: 'Call Duration In Seconds', - name: 'callDurationInSeconds', + displayName: 'Recurrence Day Of Week Mask', + name: 'recurrenceDayOfWeekMask', type: 'number', default: '', - description: `Duration of the call in seconds. Not subject to field-level security,
- available for any user in an organization with Salesforce CRM Call Cente`, + description: `The day or days of the week on which the task repeats.
+ This field contains a bitmask. The values are as follows: Sunday = 1 Monday = 2
+ Tuesday = 4 Wednesday = 8 Thursday = 16 Friday = 32 Saturday = 64
+ Multiple days are represented as the sum of their numerical values.
+ For example, Tuesday and Thursday = 4 + 16 = 20.`, }, { displayName: 'Recurrence End Date Only', @@ -316,33 +290,6 @@ export const taskFields = [ default: '', description: 'The month of the year in which the task repeats.', }, - { - displayName: 'Recurrence Day Of Week Mask', - name: 'recurrenceDayOfWeekMask', - type: 'number', - default: '', - description: `The day or days of the week on which the task repeats.
- This field contains a bitmask. The values are as follows: Sunday = 1 Monday = 2
- Tuesday = 4 Wednesday = 8 Thursday = 16 Friday = 32 Saturday = 64
- Multiple days are represented as the sum of their numerical values.
- For example, Tuesday and Thursday = 4 + 16 = 20.`, - }, - { - displayName: 'recurrence Start Date Only', - name: 'recurrenceEndDateOnly', - type: 'dateTime', - default: '', - description: `The date when the recurring task begins.
- Must be a date and time before RecurrenceEndDateOnly.`, - }, - { - displayName: 'Recurrence TimeZone SidKey', - name: 'recurrenceTimeZoneSidKey', - type: 'string', - default: '', - description: `The time zone associated with the recurring task.
- For example, “UTC-8:00” for Pacific Standard Time.`, - }, { displayName: 'Recurrence Regenerated Type', name: 'recurrenceRegeneratedType', @@ -367,6 +314,59 @@ export const taskFields = [ which determines the number of days between the triggering date (due date or close date)
and the due date of the next repeating task in the series.Label is Repeat This Task.`, }, + { + displayName: 'Recurrence Start Date Only', + name: 'recurrenceEndDateOnly', + type: 'dateTime', + default: '', + description: `The date when the recurring task begins.
+ Must be a date and time before RecurrenceEndDateOnly.`, + }, + { + displayName: 'Recurrence TimeZone SidKey', + name: 'recurrenceTimeZoneSidKey', + type: 'string', + default: '', + description: `The time zone associated with the recurring task.
+ For example, “UTC-8:00” for Pacific Standard Time.`, + }, + { + displayName: 'Reminder Date Time', + name: 'reminderDateTime', + type: 'dateTime', + default: '', + description: `Represents the time when the reminder is scheduled to fire,
+ if IsReminderSet is set to true. If IsReminderSet is set to false, then the
+ user may have deselected the reminder checkbox in the Salesforce user interface,
+ or the reminder has already fired at the time indicated by the value.`, + }, + { + displayName: 'Subject', + name: 'subject', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getTaskSubjects', + }, + description: 'The subject line of the task, such as “Call” or “Send Quote.” Limit: 255 characters.', + }, + { + displayName: 'What Id', + name: 'whatId', + type: 'string', + default: '', + description: `The WhatId represents nonhuman objects such as accounts, opportunities,
+ campaigns, cases, or custom objects. WhatIds are polymorphic. Polymorphic means a
+ WhatId is equivalent to the ID of a related object.`, + }, + { + displayName: 'Who Id', + name: 'whoId', + type: 'string', + default: '', + description: `The WhoId represents a human such as a lead or a contact.
+ WhoIds are polymorphic. Polymorphic means a WhoId is equivalent to a contact’s ID or a lead’s ID.`, + }, ] }, /* -------------------------------------------------------------------------- */ @@ -407,95 +407,6 @@ export const taskFields = [ }, }, options: [ - { - displayName: 'Who Id', - name: 'whoId', - type: 'string', - default: '', - description: `The WhoId represents a human such as a lead or a contact.
- WhoIds are polymorphic. Polymorphic means a WhoId is equivalent to a contact’s ID or a lead’s ID.`, - }, - { - displayName: 'Status', - name: 'status', - type: 'options', - default: '', - typeOptions: { - loadOptionsMethod: 'getTaskStatuses', - }, - description: 'The current status of the task, such as In Progress or Completed.', - }, - { - displayName: 'What Id', - name: 'whatId', - type: 'string', - default: '', - description: `The WhatId represents nonhuman objects such as accounts, opportunities,
- campaigns, cases, or custom objects. WhatIds are polymorphic. Polymorphic means a
- WhatId is equivalent to the ID of a related object.`, - }, - { - displayName: 'Owner', - name: 'owner', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getUsers', - }, - default: '', - description: 'ID of the User who owns the record.', - }, - { - displayName: 'Subject', - name: 'subject', - type: 'options', - default: '', - typeOptions: { - loadOptionsMethod: 'getTaskSubjects', - }, - description: 'The subject line of the task, such as “Call” or “Send Quote.” Limit: 255 characters.', - }, - { - displayName: 'Call Type', - name: 'callType', - type: 'options', - default: '', - typeOptions: { - loadOptionsMethod: 'getTaskCallTypes', - }, - description: 'The type of call being answered: Inbound, Internal, or Outbound.', - }, - { - displayName: 'Priority', - name: 'priority', - type: 'options', - default: '', - typeOptions: { - loadOptionsMethod: 'getTaskPriorities', - }, - description: `Indicates the importance or urgency of a task, such as high or low.`, - }, - { - displayName: 'Call Object', - name: 'callObject', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: `Name of a call center. Limit is 255 characters.
- Not subject to field-level security, available for any user in an
- organization with Salesforce CRM Call Center.`, - }, - { - displayName: 'Description', - name: 'description', - type: 'string', - default: '', - typeOptions: { - alwaysOpenEditWindow: true, - }, - description: 'Contains a text description of the task.', - }, { displayName: 'Activity Date', name: 'activityDate', @@ -505,23 +416,6 @@ export const taskFields = [ This field has a timestamp that is always set to midnight
in the Coordinated Universal Time (UTC) time zone.`, }, - { - displayName: 'Is ReminderSet', - name: 'isReminderSet', - type: 'boolean', - default: false, - description: 'Indicates whether a popup reminder has been set for the task (true) or not (false).', - }, - { - displayName: 'Recurrence Type', - name: 'recurrenceType', - type: 'options', - default: '', - typeOptions: { - loadOptionsMethod: 'getTaskRecurrenceTypes' - }, - description: 'Website for the task.', - }, { displayName: 'Call Disposition', name: 'callDisposition', @@ -535,14 +429,117 @@ export const taskFields = [ in an organization with Salesforce CRM Call Center.`, }, { - displayName: 'Reminder Date Time', - name: 'reminderDateTime', + displayName: 'Call Duration In Seconds', + name: 'callDurationInSeconds', + type: 'number', + default: '', + description: `Duration of the call in seconds. Not subject to field-level security,
+ available for any user in an organization with Salesforce CRM Call Cente`, + }, + { + displayName: 'Call Object', + name: 'callObject', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: `Name of a call center. Limit is 255 characters.
+ Not subject to field-level security, available for any user in an
+ organization with Salesforce CRM Call Center.`, + }, + { + displayName: 'Call Type', + name: 'callType', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getTaskCallTypes', + }, + description: 'The type of call being answered: Inbound, Internal, or Outbound.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + description: 'Contains a text description of the task.', + }, + { + displayName: 'Is ReminderSet', + name: 'isReminderSet', + type: 'boolean', + default: false, + description: 'Indicates whether a popup reminder has been set for the task (true) or not (false).', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'ID of the User who owns the record.', + }, + { + displayName: 'Priority', + name: 'priority', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getTaskPriorities', + }, + description: `Indicates the importance or urgency of a task, such as high or low.`, + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getTaskStatuses', + }, + description: 'The current status of the task, such as In Progress or Completed.', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getTaskSubjects', + }, + description: 'The subject line of the task, such as “Call” or “Send Quote.” Limit: 255 characters.', + }, + { + displayName: 'Recurrence Day Of Month', + name: 'recurrenceDayOfMonth', + type: 'number', + default: '', + description: 'The day of the month in which the task repeats.', + }, + { + displayName: 'Recurrence Day Of Week Mask', + name: 'recurrenceDayOfWeekMask', + type: 'number', + default: '', + description: `The day or days of the week on which the task repeats.
+ This field contains a bitmask. The values are as follows: Sunday = 1 Monday = 2
+ Tuesday = 4 Wednesday = 8 Thursday = 16 Friday = 32 Saturday = 64
+ Multiple days are represented as the sum of their numerical values.
+ For example, Tuesday and Thursday = 4 + 16 = 20.`, + }, + { + displayName: 'Recurrence End Date Only', + name: 'recurrenceEndDateOnly', type: 'dateTime', default: '', - description: `Represents the time when the reminder is scheduled to fire,
- if IsReminderSet is set to true. If IsReminderSet is set to false, then the
- user may have deselected the reminder checkbox in the Salesforce user interface,
- or the reminder has already fired at the time indicated by the value.`, + description: `The last date on which the task repeats. This field has a timestamp that
+ is always set to midnight in the Coordinated Universal Time (UTC) time zone.`, }, { displayName: 'Recurrence Instance', @@ -561,29 +558,6 @@ export const taskFields = [ default: '', description: 'The interval between recurring tasks.', }, - { - displayName: 'Recurrence Day Of Month', - name: 'recurrenceDayOfMonth', - type: 'number', - default: '', - description: 'The day of the month in which the task repeats.', - }, - { - displayName: 'Call Duration In Seconds', - name: 'callDurationInSeconds', - type: 'number', - default: '', - description: `Duration of the call in seconds. Not subject to field-level security,
- available for any user in an organization with Salesforce CRM Call Cente`, - }, - { - displayName: 'Recurrence End Date Only', - name: 'recurrenceEndDateOnly', - type: 'dateTime', - default: '', - description: `The last date on which the task repeats. This field has a timestamp that
- is always set to midnight in the Coordinated Universal Time (UTC) time zone.`, - }, { displayName: 'Recurrence Month Of Year', name: 'recurrenceMonthOfYear', @@ -642,32 +616,13 @@ export const taskFields = [ description: 'The month of the year in which the task repeats.', }, { - displayName: 'Recurrence Day Of Week Mask', - name: 'recurrenceDayOfWeekMask', - type: 'number', - default: '', - description: `The day or days of the week on which the task repeats.
- This field contains a bitmask. The values are as follows: Sunday = 1 Monday = 2
- Tuesday = 4 Wednesday = 8 Thursday = 16 Friday = 32 Saturday = 64
- Multiple days are represented as the sum of their numerical values.
- For example, Tuesday and Thursday = 4 + 16 = 20.`, - }, - { - displayName: 'recurrence Start Date Only', + displayName: 'Recurrence Start Date Only', name: 'recurrenceEndDateOnly', type: 'dateTime', default: '', description: `The date when the recurring task begins.
Must be a date and time before RecurrenceEndDateOnly.`, }, - { - displayName: 'Recurrence TimeZone SidKey', - name: 'recurrenceTimeZoneSidKey', - type: 'string', - default: '', - description: `The time zone associated with the recurring task.
- For example, “UTC-8:00” for Pacific Standard Time.`, - }, { displayName: 'Recurrence Regenerated Type', name: 'recurrenceRegeneratedType', @@ -692,6 +647,51 @@ export const taskFields = [ which determines the number of days between the triggering date (due date or close date)
and the due date of the next repeating task in the series.Label is Repeat This Task.`, }, + { + displayName: 'Recurrence Type', + name: 'recurrenceType', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getTaskRecurrenceTypes' + }, + description: 'Website for the task.', + }, + { + displayName: 'Recurrence TimeZone SidKey', + name: 'recurrenceTimeZoneSidKey', + type: 'string', + default: '', + description: `The time zone associated with the recurring task.
+ For example, “UTC-8:00” for Pacific Standard Time.`, + }, + { + displayName: 'Reminder Date Time', + name: 'reminderDateTime', + type: 'dateTime', + default: '', + description: `Represents the time when the reminder is scheduled to fire,
+ if IsReminderSet is set to true. If IsReminderSet is set to false, then the
+ user may have deselected the reminder checkbox in the Salesforce user interface,
+ or the reminder has already fired at the time indicated by the value.`, + }, + { + displayName: 'What Id', + name: 'whatId', + type: 'string', + default: '', + description: `The WhatId represents nonhuman objects such as accounts, opportunities,
+ campaigns, cases, or custom objects. WhatIds are polymorphic. Polymorphic means a
+ WhatId is equivalent to the ID of a related object.`, + }, + { + displayName: 'Who Id', + name: 'whoId', + type: 'string', + default: '', + description: `The WhoId represents a human such as a lead or a contact.
+ WhoIds are polymorphic. Polymorphic means a WhoId is equivalent to a contact’s ID or a lead’s ID.`, + }, ] }, diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 938424c6c9..ec149d9910 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -66,10 +66,10 @@ "dist/credentials/Redis.credentials.js", "dist/credentials/RocketchatApi.credentials.js", "dist/credentials/ShopifyApi.credentials.js", + "dist/credentials/SalesforceOAuth2Api.credentials.js", "dist/credentials/SlackApi.credentials.js", "dist/credentials/Smtp.credentials.js", "dist/credentials/StripeApi.credentials.js", - "dist/credentials/SalesforceOAuth2Api.credentials.js", "dist/credentials/TelegramApi.credentials.js", "dist/credentials/TodoistApi.credentials.js", "dist/credentials/TrelloApi.credentials.js", @@ -152,6 +152,7 @@ "dist/nodes/ReadPdf.node.js", "dist/nodes/RenameKeys.node.js", "dist/nodes/RssFeedRead.node.js", + "dist/nodes/Salesforce/Salesforce.node.js", "dist/nodes/Set.node.js", "dist/nodes/SseTrigger.node.js", "dist/nodes/SplitInBatches.node.js", @@ -161,7 +162,6 @@ "dist/nodes/Stripe/StripeTrigger.node.js", "dist/nodes/Shopify/ShopifyTrigger.node.js", "dist/nodes/Switch.node.js", - "dist/nodes/Salesforce/Salesforce.node.js", "dist/nodes/Telegram/Telegram.node.js", "dist/nodes/Telegram/TelegramTrigger.node.js", "dist/nodes/Todoist/Todoist.node.js", From 8bf5f592afb5377f20553ddd95451a3131e0c8f2 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 14 Feb 2020 17:30:45 -0800 Subject: [PATCH 013/165] :zap: Fix Salesforce OAuth missing refresh-token problem --- .../nodes-base/credentials/SalesforceOAuth2Api.credentials.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/credentials/SalesforceOAuth2Api.credentials.ts b/packages/nodes-base/credentials/SalesforceOAuth2Api.credentials.ts index 23739bf118..0883734e5a 100644 --- a/packages/nodes-base/credentials/SalesforceOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/SalesforceOAuth2Api.credentials.ts @@ -28,7 +28,7 @@ export class SalesforceOAuth2Api implements ICredentialType { displayName: 'Scope', name: 'scope', type: 'hidden' as NodePropertyTypes, - default: 'full', + default: 'full refresh_token', }, { displayName: 'Auth URI Query Parameters', From 6a08fc9da3940e96f0b95f1bac4d27d7e33c6804 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 14 Feb 2020 18:48:58 -0800 Subject: [PATCH 014/165] :zap: Small improvements to ZohoCRM-Node and OAuth fix --- packages/cli/src/Server.ts | 19 ++++++++----------- .../GithubOAuth2Api.credentials.ts | 6 ++++++ .../credentials/OAuth2Api.credentials.ts | 19 +++++++++++++++++++ .../credentials/ZohoOAuth2Api.credentials.ts | 8 +++++++- .../nodes-base/nodes/Zoho/GenericFunctions.ts | 3 --- .../nodes-base/nodes/Zoho/ZohoCrm.node.ts | 8 ++++++-- 6 files changed, 46 insertions(+), 17 deletions(-) diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index e2e17a9740..4fe1ef70d7 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -955,17 +955,14 @@ class App { let options = {}; - // Here we need a variable that can be set on the credentials - // so that base on that include the credentails on the body or - // leave it as default with woukd include the credentails on the header. - // if (thatvariableistrue) { - // options = { - // body: { - // client_id: _.get(oauthCredentials, 'clientId') as string, - // client_secret: _.get(oauthCredentials, 'clientSecret', '') as string, - // }, - // } - // } + if (_.get(oauthCredentials, 'authentication', 'header') as string === 'body') { + options = { + body: { + client_id: _.get(oauthCredentials, 'clientId') as string, + client_secret: _.get(oauthCredentials, 'clientSecret', '') as string, + }, + }; + } const oAuthObj = new clientOAuth2({ clientId: _.get(oauthCredentials, 'clientId') as string, diff --git a/packages/nodes-base/credentials/GithubOAuth2Api.credentials.ts b/packages/nodes-base/credentials/GithubOAuth2Api.credentials.ts index 9466ce447b..afca85f57b 100644 --- a/packages/nodes-base/credentials/GithubOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/GithubOAuth2Api.credentials.ts @@ -37,5 +37,11 @@ export class GithubOAuth2Api implements ICredentialType { type: 'hidden' as NodePropertyTypes, default: '', }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'header', + }, ]; } diff --git a/packages/nodes-base/credentials/OAuth2Api.credentials.ts b/packages/nodes-base/credentials/OAuth2Api.credentials.ts index efa5127e6e..ce2a248b62 100644 --- a/packages/nodes-base/credentials/OAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/OAuth2Api.credentials.ts @@ -53,5 +53,24 @@ export class OAuth2Api implements ICredentialType { description: 'For some services additional query parameters have to be set which can be defined here.', placeholder: 'access_type=offline', }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'options' as NodePropertyTypes, + options: [ + { + name: 'Body', + value: 'body', + description: 'Send credentials in body', + }, + { + name: 'Header', + value: 'header', + description: 'Send credentials as Basic Auth header', + }, + ], + default: 'header', + description: 'Resource to consume.', + }, ]; } diff --git a/packages/nodes-base/credentials/ZohoOAuth2Api.credentials.ts b/packages/nodes-base/credentials/ZohoOAuth2Api.credentials.ts index 3617458a94..801b164ffd 100644 --- a/packages/nodes-base/credentials/ZohoOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/ZohoOAuth2Api.credentials.ts @@ -68,7 +68,13 @@ export class ZohoOAuth2Api implements ICredentialType { displayName: 'Auth URI Query Parameters', name: 'authQueryParameters', type: 'hidden' as NodePropertyTypes, - default: 'access_type=online', + default: 'access_type=offline', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'body', }, ]; } diff --git a/packages/nodes-base/nodes/Zoho/GenericFunctions.ts b/packages/nodes-base/nodes/Zoho/GenericFunctions.ts index ab6f00146c..04fe204858 100644 --- a/packages/nodes-base/nodes/Zoho/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zoho/GenericFunctions.ts @@ -9,12 +9,9 @@ import { } from 'n8n-workflow'; export async function zohoApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any - const credentials = this.getCredentials('zohoOAuth2Api'); const options: OptionsWithUri = { headers: { 'Content-Type': 'application/json', - //@ts-ignore - Authorization: `Zoho-oauthtoken ${credentials!.oauthTokenData.access_token}` }, method, body: { diff --git a/packages/nodes-base/nodes/Zoho/ZohoCrm.node.ts b/packages/nodes-base/nodes/Zoho/ZohoCrm.node.ts index 2032a57921..f1d397c2dc 100644 --- a/packages/nodes-base/nodes/Zoho/ZohoCrm.node.ts +++ b/packages/nodes-base/nodes/Zoho/ZohoCrm.node.ts @@ -80,12 +80,16 @@ export class ZohoCrm implements INodeType { // } responseData = await zohoApiRequest.call(this, 'POST', '/leads', body); responseData = responseData.data; - + } else { + throw new Error(`The operation "${operation}" is not known!`); } + } else { + throw new Error(`The resource "${resource}" is not known!`); } + if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData as IDataObject[]); - } else { + } else if (responseData !== undefined) { returnData.push(responseData as IDataObject); } } From c630028531b90abdc4fb18854b2c1eccd16a63d5 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sat, 15 Feb 2020 19:23:22 -0500 Subject: [PATCH 015/165] :sparkles: Added lead resource --- .../credentials/ZohoOAuth2Api.credentials.ts | 2 +- .../nodes-base/nodes/Zoho/GenericFunctions.ts | 10 +- .../nodes-base/nodes/Zoho/LeadDescription.ts | 637 ++++++++++++++++-- .../nodes-base/nodes/Zoho/LeadInterface.ts | 37 + .../nodes-base/nodes/Zoho/ZohoCrm.node.ts | 363 +++++++++- 5 files changed, 984 insertions(+), 65 deletions(-) create mode 100644 packages/nodes-base/nodes/Zoho/LeadInterface.ts diff --git a/packages/nodes-base/credentials/ZohoOAuth2Api.credentials.ts b/packages/nodes-base/credentials/ZohoOAuth2Api.credentials.ts index 801b164ffd..12a83493a4 100644 --- a/packages/nodes-base/credentials/ZohoOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/ZohoOAuth2Api.credentials.ts @@ -62,7 +62,7 @@ export class ZohoOAuth2Api implements ICredentialType { displayName: 'Scope', name: 'scope', type: 'hidden' as NodePropertyTypes, - default: 'ZohoCRM.modules.ALL', + default: 'ZohoCRM.modules.ALL,ZohoCRM.settings.all,ZohoCRM.users.all', }, { displayName: 'Auth URI Query Parameters', diff --git a/packages/nodes-base/nodes/Zoho/GenericFunctions.ts b/packages/nodes-base/nodes/Zoho/GenericFunctions.ts index 04fe204858..3d20a3ec02 100644 --- a/packages/nodes-base/nodes/Zoho/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zoho/GenericFunctions.ts @@ -7,6 +7,7 @@ import { import { IDataObject } from 'n8n-workflow'; +import { queryResult } from 'pg-promise'; export async function zohoApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any const options: OptionsWithUri = { @@ -41,14 +42,17 @@ export async function zohoApiRequestAllItems(this: IExecuteFunctions | ILoadOpti let responseData; let uri: string | undefined; + query.per_page = 200; + query.page = 0; do { responseData = await zohoApiRequest.call(this, method, endpoint, body, query, uri); - uri = responseData.nextRecordsUrl; + uri = responseData.info.more_records; returnData.push.apply(returnData, responseData[propertyName]); + query.page++; } while ( - responseData.nextRecordsUrl !== undefined && - responseData.nextRecordsUrl !== null + responseData.info.more_records !== undefined && + responseData.info.more_records === true ); return returnData; diff --git a/packages/nodes-base/nodes/Zoho/LeadDescription.ts b/packages/nodes-base/nodes/Zoho/LeadDescription.ts index 198abbe472..577fee4aec 100644 --- a/packages/nodes-base/nodes/Zoho/LeadDescription.ts +++ b/packages/nodes-base/nodes/Zoho/LeadDescription.ts @@ -28,10 +28,15 @@ export const leadOperations = [ value: 'getAll', description: 'Get data of all leads', }, + { + name: 'Get Fields', + value: 'getFields', + description: `Get the fields' metadata`, + }, { name: 'Update', value: 'update', - description: 'Update new lead', + description: 'Update a lead', }, { name: 'Delete', @@ -49,7 +54,6 @@ export const leadFields = [ /* -------------------------------------------------------------------------- */ /* lead:create */ /* -------------------------------------------------------------------------- */ - { displayName: 'Last Name', name: 'lastName', @@ -86,86 +90,617 @@ export const leadFields = [ }, options: [ { - displayName: 'Avatar', - name: 'avatar', - type: 'string', - default: '', - description: 'An avatar image URL. note: the image url needs to be https.', + displayName: 'Annual Revenue', + name: 'annualRevenue', + type: 'number', + typeOptions: { + numberPrecision: 2, + }, + default: 0, }, { - displayName: 'Name', - name: 'name', + displayName: 'Company', + name: 'company', type: 'string', default: '', - description: 'Name of the user', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + }, + { + displayName: 'Email Opt Out', + name: 'emailOptOut', + type: 'boolean', + default: false, + }, + { + displayName: 'Fax', + name: 'fax', + type: 'string', + default: '', + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + }, + { + displayName: 'Industry', + name: 'industry', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getIndustries', + }, + default: '', + }, + { + displayName: 'Is Record Duplicate', + name: 'isRecordDuplicate', + type: 'boolean', + default: false, + }, + { + displayName: 'Lead Source', + name: 'leadSource', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLeadSources', + }, + default: '', + }, + { + displayName: 'Lead Status', + name: 'leadStatus', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLeadStatuses' + }, + default: '', + }, + { + displayName: 'Mobile', + name: 'mobile', + type: 'string', + default: '', + }, + { + displayName: 'No. of Employees', + name: 'numberOfEmployees', + type: 'number', + default: 1, + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, }, { displayName: 'Phone', name: 'phone', type: 'string', default: '', - description: 'The phone number of the user', }, { - displayName: 'Unsubscribed From Emails', - name: 'unsubscribedFromEmails', - type: 'boolean', - default: false, - description: 'Whether the Lead is unsubscribed from emails', - }, - { - displayName: 'Update Last Request At', - name: 'updateLastRequestAt', - type: 'boolean', - default: false, - description: 'A boolean value, which if true, instructs Intercom to update the
users last_request_at value to the current API service time in
UTC. default value if not sent is false.', - }, - { - displayName: 'Companies', - name: 'companies', - type: 'multiOptions', - typeOptions: { - loadOptionsMethod: 'getCompanies', - }, - default: [], - description: 'Identifies the companies this user belongs to.', - }, - { - displayName: 'UTM Source', - name: 'utmSource', + displayName: 'Salutation', + name: 'salutation', type: 'string', default: '', - description: 'An avatar image URL. note: the image url needs to be https.', }, { - displayName: 'UTM Medium', - name: 'utmMedium', + displayName: 'Secondary Email', + name: 'secondaryEmail', type: 'string', default: '', - description: 'Identifies what type of link was used', }, { - displayName: 'UTM Campaign', - name: 'utmCampaign', + displayName: 'Skype ID', + name: 'SkypeId', type: 'string', default: '', - description: 'Identifies a specific product promotion or strategic campaign', }, { - displayName: 'UTM Term', - name: 'utmTerm', + displayName: 'Title', + name: 'title', type: 'string', default: '', - description: 'Identifies search terms', }, { - displayName: 'UTM Content', - name: 'utmContent', + displayName: 'Twitter', + name: 'twitter', + type: 'string', + default: '', + }, + { + displayName: 'Website', + name: 'website', type: 'string', default: '', - description: 'Identifies what specifically was clicked to bring the user to the site', }, ] }, - + { + displayName: 'Address', + name: 'addressUi', + type: 'fixedCollection', + default: {}, + placeholder: 'Add Address', + typeOptions: { + multipleValues: false, + }, + required: false, + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + name: 'addressValues', + displayName: 'Address', + values: [ + { + displayName: 'Street', + name: 'street', + type: 'string', + default: '', + }, + { + displayName: 'City', + name: 'city', + type: 'string', + default: '', + }, + { + displayName: 'State', + name: 'state', + type: 'string', + default: '', + }, + { + displayName: 'Country', + name: 'country', + type: 'string', + default: '', + }, + { + displayName: 'Zip Code', + name: 'zipCode', + type: 'string', + default: '', + }, + ], + } + ], + }, +/* -------------------------------------------------------------------------- */ +/* lead:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Lead ID', + name: 'leadId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'lead', + ], + }, + }, + options: [ + { + displayName: 'Annual Revenue', + name: 'annualRevenue', + type: 'number', + typeOptions: { + numberPrecision: 2, + }, + default: 0, + }, + { + displayName: 'Company', + name: 'company', + type: 'string', + default: '', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + }, + { + displayName: 'Email Opt Out', + name: 'emailOptOut', + type: 'boolean', + default: false, + }, + { + displayName: 'Fax', + name: 'fax', + type: 'string', + default: '', + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + }, + { + displayName: 'Industry', + name: 'industry', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getIndustries', + }, + default: '', + }, + { + displayName: 'Is Record Duplicate', + name: 'isRecordDuplicate', + type: 'boolean', + default: false, + }, + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + default: '', + description: `User's last name`, + }, + { + displayName: 'Lead Source', + name: 'leadSource', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLeadSources', + }, + default: '', + }, + { + displayName: 'Lead Status', + name: 'leadStatus', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLeadStatuses' + }, + default: '', + }, + { + displayName: 'Mobile', + name: 'mobile', + type: 'string', + default: '', + }, + { + displayName: 'No. of Employees', + name: 'numberOfEmployees', + type: 'number', + default: 1, + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + }, + { + displayName: 'Salutation', + name: 'salutation', + type: 'string', + default: '', + }, + { + displayName: 'Secondary Email', + name: 'secondaryEmail', + type: 'string', + default: '', + }, + { + displayName: 'Skype ID', + name: 'SkypeId', + type: 'string', + default: '', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + }, + { + displayName: 'Twitter', + name: 'twitter', + type: 'string', + default: '', + }, + { + displayName: 'Website', + name: 'website', + type: 'string', + default: '', + }, + ] + }, + { + displayName: 'Address', + name: 'addressUi', + type: 'fixedCollection', + default: {}, + placeholder: 'Add Address', + typeOptions: { + multipleValues: false, + }, + required: false, + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + name: 'addressValues', + displayName: 'Address', + values: [ + { + displayName: 'Street', + name: 'street', + type: 'string', + default: '', + }, + { + displayName: 'City', + name: 'city', + type: 'string', + default: '', + }, + { + displayName: 'State', + name: 'state', + type: 'string', + default: '', + }, + { + displayName: 'Country', + name: 'country', + type: 'string', + default: '', + }, + { + displayName: 'Zip Code', + name: 'zipCode', + type: 'string', + default: '', + }, + ], + } + ], + }, +/* -------------------------------------------------------------------------- */ +/* lead:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Lead ID', + name: 'leadId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'get', + ], + }, + }, + }, +/* -------------------------------------------------------------------------- */ +/* lead:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 200, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Approved', + name: 'approved', + type: 'boolean', + default: true, + description: 'To get the list of approved records. Default value is true.', + }, + { + displayName: 'Converted', + name: 'converted', + type: 'boolean', + default: false, + description: 'To get the list of converted records. Default value is false', + }, + { + displayName: 'Fields', + name: 'fields', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getLeadFields', + }, + default: [], + }, + { + displayName: 'Include Child', + name: 'includeChild', + type: 'boolean', + default: false, + description: 'To include records from the child territories. True includes child territory records', + }, + { + displayName: 'Sort Order', + name: 'sortOrder', + type: 'options', + options: [ + { + name: 'ASC', + value: 'asc', + }, + { + name: 'DESC', + value: 'desc', + }, + ], + default: 'desc', + description: 'Order sort attribute ascending or descending.', + }, + { + displayName: 'Sort By', + name: 'sortBy', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getLeadFields', + }, + default: [], + }, + { + displayName: 'Territory ID', + name: 'territoryId', + type: 'string', + default: '', + description: 'To get the list of records based on the territory ', + }, + ] + }, +/* -------------------------------------------------------------------------- */ +/* lead:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Lead ID', + name: 'leadId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'delete', + ], + }, + }, + }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zoho/LeadInterface.ts b/packages/nodes-base/nodes/Zoho/LeadInterface.ts new file mode 100644 index 0000000000..957f816cb6 --- /dev/null +++ b/packages/nodes-base/nodes/Zoho/LeadInterface.ts @@ -0,0 +1,37 @@ +export interface ILead { + Annual_Revenue?: number; + City?: string; + Company?: string; + Country?: string; + Description?: string; + Designation?: string; + Email?: string; + Email_Opt_Out?: boolean; + Fax?: string; + First_Name?: string; + Industry?: string; + Is_Record_Duplicate?: boolean; + Last_Name?: string; + Lead_Owner?: string; + Lead_Source?: string; + Lead_Status?: string; + Mobile?: string; + No_of_Employees?: number; + Phone?: string; + Salutation?: string; + Secondary_Email?: string; + Skype_ID?: string; + State?: string; + Street?: string; + Twitter?: string; + Website?: string; + Zip_Code?: string; +} + +export interface IAddress { + street?: string; + city?: string; + state?: string; + country?: string; + zipCode?: string; +} diff --git a/packages/nodes-base/nodes/Zoho/ZohoCrm.node.ts b/packages/nodes-base/nodes/Zoho/ZohoCrm.node.ts index f1d397c2dc..20a04295e0 100644 --- a/packages/nodes-base/nodes/Zoho/ZohoCrm.node.ts +++ b/packages/nodes-base/nodes/Zoho/ZohoCrm.node.ts @@ -7,6 +7,8 @@ import { INodeExecutionData, INodeTypeDescription, INodeType, + ILoadOptionsFunctions, + INodePropertyOptions, } from 'n8n-workflow'; import { @@ -19,6 +21,11 @@ import { leadFields, } from './LeadDescription'; +import { + ILead, + IAddress, +} from './LeadInterface'; + export class ZohoCrm implements INodeType { description: INodeTypeDescription = { displayName: 'Zoho CRM', @@ -59,34 +66,370 @@ export class ZohoCrm implements INodeType { ], }; + methods = { + loadOptions: { + // Get all the available users to display them to user so that he can + // select them easily + async getUsers(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const { users } = await zohoApiRequest.call(this, 'GET', '/users', {}, { type: 'AllUsers' }); + for (const user of users) { + const userName = `${user.first_name} ${user.last_name}`; + const userId = user.profile.id; + returnData.push({ + name: userName, + value: userId, + }); + } + return returnData; + }, + // Get all the available accounts to display them to user so that he can + // select them easily + async getAccounts(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const qs: IDataObject = {}; + qs.sort_by = 'Created_Time'; + qs.sort_order = 'desc'; + const { data } = await zohoApiRequest.call(this, 'GET', '/accounts', {}, qs); + for (const account of data) { + const accountName = account.Account_Name + const accountId = account.id; + returnData.push({ + name: accountName, + value: accountId, + }); + } + return returnData; + }, + // Get all the available lead statuses to display them to user so that he can + // select them easily + async getLeadStatuses(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const qs: IDataObject = {}; + qs.module = 'leads'; + const { fields } = await zohoApiRequest.call(this, 'GET', '/settings/fields', {}, qs); + for (const field of fields) { + if (field.api_name === 'Lead_Status') { + for (const value of field.pick_list_values) { + const valueName = value.display_value + const valueId = value.actual_value; + returnData.push({ + name: valueName, + value: valueId, + }); + return returnData; + } + } + } + return returnData; + }, + // Get all the available lead sources to display them to user so that he can + // select them easily + async getLeadSources(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const qs: IDataObject = {}; + qs.module = 'leads'; + const { fields } = await zohoApiRequest.call(this, 'GET', '/settings/fields', {}, qs); + for (const field of fields) { + if (field.api_name === 'Lead_Source') { + for (const value of field.pick_list_values) { + const valueName = value.display_value + const valueId = value.actual_value; + returnData.push({ + name: valueName, + value: valueId, + }); + return returnData; + } + } + } + return returnData; + }, + // Get all the available industries to display them to user so that he can + // select them easily + async getIndustries(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const qs: IDataObject = {}; + qs.module = 'leads'; + const { fields } = await zohoApiRequest.call(this, 'GET', '/settings/fields', {}, qs); + for (const field of fields) { + if (field.api_name === 'Industry') { + for (const value of field.pick_list_values) { + const valueName = value.display_value + const valueId = value.actual_value; + returnData.push({ + name: valueName, + value: valueId, + }); + return returnData; + } + } + } + return returnData; + }, + // Get all the available lead fields to display them to user so that he can + // select them easily + async getLeadFields(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const qs: IDataObject = {}; + qs.module = 'leads'; + const { fields } = await zohoApiRequest.call(this, 'GET', '/settings/fields', {}, qs); + for (const field of fields) { + returnData.push({ + name: field.field_label, + value: field.api_name, + }); + } + return returnData; + }, + }, + }; + 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; for (let i = 0; i < length; i++) { const resource = this.getNodeParameter('resource', 0) as string; const operation = this.getNodeParameter('operation', 0) as string; if (resource === 'lead') { + //https://www.zoho.com/crm/developer/docs/api/insert-records.html if (operation === 'create') { const lastName = this.getNodeParameter('lastName', i) as string; const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; - const body = { + const body: ILead = { Last_Name: lastName, }; - // if (additionalFields.email) { - // // @ts-ignore - // body.email = additionalFields.email as string; - // } + if (additionalFields.owner) { + body.Lead_Owner = additionalFields.owner as string; + } + if (additionalFields.company) { + body.Company = additionalFields.company as string; + } + if (additionalFields.firstName) { + body.First_Name = additionalFields.firstName as string; + } + if (additionalFields.email) { + body.Email = additionalFields.email as string; + } + if (additionalFields.title) { + body.Designation = additionalFields.title as string; + } + if (additionalFields.phone) { + body.Phone = additionalFields.phone as string; + } + if (additionalFields.mobile) { + body.Mobile = additionalFields.mobile as string; + } + if (additionalFields.leadStatus) { + body.Lead_Status = additionalFields.leadStatus as string; + } + if (additionalFields.fax) { + body.Fax = additionalFields.fax as string; + } + if (additionalFields.website) { + body.Website = additionalFields.website as string; + } + if (additionalFields.leadSource) { + body.Lead_Source = additionalFields.leadSource as string; + } + if (additionalFields.industry) { + body.Industry = additionalFields.industry as string; + } + if (additionalFields.numberOfEmployees) { + body.No_of_Employees = additionalFields.numberOfEmployees as number; + } + if (additionalFields.annualRevenue) { + body.Annual_Revenue = additionalFields.annualRevenue as number; + } + if (additionalFields.emailOptOut) { + body.Email_Opt_Out = additionalFields.emailOptOut as boolean; + } + if (additionalFields.skypeId) { + body.Skype_ID = additionalFields.skypeId as string; + } + if (additionalFields.salutation) { + body.Salutation = additionalFields.salutation as string; + } + if (additionalFields.secondaryEmail) { + body.Secondary_Email = additionalFields.secondaryEmail as string; + } + if (additionalFields.twitter) { + body.Twitter = additionalFields.twitter as string; + } + if (additionalFields.isRecordDuplicate) { + body.Is_Record_Duplicate = additionalFields.isRecordDuplicate as boolean; + } + if (additionalFields.description) { + body.Description = additionalFields.description as string; + } + const address = (this.getNodeParameter('addressUi', i) as IDataObject).addressValues as IAddress; + if (address) { + if (address.country) { + body.Country = address.country as string; + } + if (address.city) { + body.City = address.city as string; + } + if (address.state) { + body.State = address.state as string; + } + if (address.street) { + body.Street = address.street as string; + } + if (address.zipCode) { + body.Zip_Code = address.zipCode as string; + } + } responseData = await zohoApiRequest.call(this, 'POST', '/leads', body); responseData = responseData.data; - } else { - throw new Error(`The operation "${operation}" is not known!`); } - } else { - throw new Error(`The resource "${resource}" is not known!`); + //https://www.zoho.com/crm/developer/docs/api/update-specific-record.html + if (operation === 'update') { + const leadId = this.getNodeParameter('leadId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: ILead = {}; + if (additionalFields.lastName) { + body.Last_Name = additionalFields.lastName as string; + } + if (additionalFields.owner) { + body.Lead_Owner = additionalFields.owner as string; + } + if (additionalFields.company) { + body.Company = additionalFields.company as string; + } + if (additionalFields.firstName) { + body.First_Name = additionalFields.firstName as string; + } + if (additionalFields.email) { + body.Email = additionalFields.email as string; + } + if (additionalFields.title) { + body.Designation = additionalFields.title as string; + } + if (additionalFields.phone) { + body.Phone = additionalFields.phone as string; + } + if (additionalFields.mobile) { + body.Mobile = additionalFields.mobile as string; + } + if (additionalFields.leadStatus) { + body.Lead_Status = additionalFields.leadStatus as string; + } + if (additionalFields.fax) { + body.Fax = additionalFields.fax as string; + } + if (additionalFields.website) { + body.Website = additionalFields.website as string; + } + if (additionalFields.leadSource) { + body.Lead_Source = additionalFields.leadSource as string; + } + if (additionalFields.industry) { + body.Industry = additionalFields.industry as string; + } + if (additionalFields.numberOfEmployees) { + body.No_of_Employees = additionalFields.numberOfEmployees as number; + } + if (additionalFields.annualRevenue) { + body.Annual_Revenue = additionalFields.annualRevenue as number; + } + if (additionalFields.emailOptOut) { + body.Email_Opt_Out = additionalFields.emailOptOut as boolean; + } + if (additionalFields.skypeId) { + body.Skype_ID = additionalFields.skypeId as string; + } + if (additionalFields.salutation) { + body.Salutation = additionalFields.salutation as string; + } + if (additionalFields.secondaryEmail) { + body.Secondary_Email = additionalFields.secondaryEmail as string; + } + if (additionalFields.twitter) { + body.Twitter = additionalFields.twitter as string; + } + if (additionalFields.isRecordDuplicate) { + body.Is_Record_Duplicate = additionalFields.isRecordDuplicate as boolean; + } + if (additionalFields.description) { + body.Description = additionalFields.description as string; + } + const address = (this.getNodeParameter('addressUi', i) as IDataObject).addressValues as IAddress; + if (address) { + if (address.country) { + body.Country = address.country as string; + } + if (address.city) { + body.City = address.city as string; + } + if (address.state) { + body.State = address.state as string; + } + if (address.street) { + body.Street = address.street as string; + } + if (address.zipCode) { + body.Zip_Code = address.zipCode as string; + } + } + responseData = await zohoApiRequest.call(this, 'PUT', `/leads/${leadId}`, body); + responseData = responseData.data; + } + //https://www.zoho.com/crm/developer/docs/api/update-specific-record.html + if (operation === 'get') { + const leadId = this.getNodeParameter('leadId', i) as string; + responseData = await zohoApiRequest.call(this, 'GET', `/leads/${leadId}`); + responseData = responseData.data; + } + //https://www.zoho.com/crm/developer/docs/api/get-records.html + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const options = this.getNodeParameter('options', i) as IDataObject; + if (options.fields) { + qs.fields = (options.fields as string[]).join(','); + } + if (options.approved) { + qs.approved = options.approved as boolean; + } + if (options.converted) { + qs.converted = options.converted as boolean; + } + if (options.includeChild) { + qs.include_child = options.includeChild as boolean; + } + if (options.sortOrder) { + qs.sort_order = options.sortOrder as string; + } + if (options.sortBy) { + qs.sort_by = options.sortBy as string; + } + if (options.territoryId) { + qs.territory_id = options.territoryId as string; + } + if (returnAll) { + responseData = await zohoApiRequestAllItems.call(this, 'data', 'GET', '/leads', {}, qs); + } else { + qs.per_page = this.getNodeParameter('limit', i) as number; + responseData = await zohoApiRequest.call(this, 'GET', '/leads', {}, qs); + responseData = responseData.data; + } + } + //https://www.zoho.com/crm/developer/docs/api/delete-specific-record.html + if (operation === 'delete') { + const leadId = this.getNodeParameter('leadId', i) as string; + responseData = await zohoApiRequest.call(this, 'DELETE', `/leads/${leadId}`); + responseData = responseData.data; + } + //https://www.zoho.com/crm/developer/docs/api/field-meta.html + if (operation === 'getFields') { + qs.module = 'leads'; + responseData = await zohoApiRequest.call(this, 'GET', '/settings/fields', {}, qs); + responseData = responseData.fields; + } } - if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData as IDataObject[]); } else if (responseData !== undefined) { From b3a9eb08c1aabace44d3beea46d53f9e1cc2a8a9 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sat, 15 Feb 2020 19:25:14 -0500 Subject: [PATCH 016/165] :zap: small fix --- packages/nodes-base/nodes/Zoho/GenericFunctions.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/nodes-base/nodes/Zoho/GenericFunctions.ts b/packages/nodes-base/nodes/Zoho/GenericFunctions.ts index 3d20a3ec02..4deea5f6f5 100644 --- a/packages/nodes-base/nodes/Zoho/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zoho/GenericFunctions.ts @@ -7,7 +7,6 @@ import { import { IDataObject } from 'n8n-workflow'; -import { queryResult } from 'pg-promise'; export async function zohoApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any const options: OptionsWithUri = { From fe36c9c76a40ecdeed27f47232a7a6782f0bbd9a Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Thu, 5 Mar 2020 18:25:18 -0500 Subject: [PATCH 017/165] :zap: setup --- packages/cli/src/Server.ts | 2 +- packages/core/src/NodeExecuteFunctions.ts | 32 +- .../credentials/SlackOAuth2Api.credentials.ts | 47 + .../nodes/Slack/ChannelDescription.ts | 209 +++++ .../nodes/Slack/ConversationDescription.ts | 846 ++++++++++++++++++ .../nodes/Slack/GenericFunctions.ts | 77 ++ .../nodes/Slack/MessageDescription.ts | 843 +++++++++++++++++ .../nodes/Slack/MessageInterface.ts | 7 + packages/nodes-base/nodes/Slack/Slack.node.ts | 714 +++------------ packages/nodes-base/package.json | 1 + 10 files changed, 2167 insertions(+), 611 deletions(-) create mode 100644 packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts create mode 100644 packages/nodes-base/nodes/Slack/ChannelDescription.ts create mode 100644 packages/nodes-base/nodes/Slack/ConversationDescription.ts create mode 100644 packages/nodes-base/nodes/Slack/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Slack/MessageDescription.ts create mode 100644 packages/nodes-base/nodes/Slack/MessageInterface.ts diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 4fe1ef70d7..bafe3120e2 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -203,7 +203,7 @@ class App { }); } - jwt.verify(token, getKey, {}, (err: Error, decoded: string) => { + jwt.verify(token, getKey, {}, (err: Error) => { if (err) return ResponseHelper.jwtAuthAuthorizationError(res, "Invalid token"); next(); diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 69dd2c92e1..f73eb2f068 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -115,7 +115,7 @@ export async function prepareBinaryData(binaryData: Buffer, filePath?: string, m * @param {IWorkflowExecuteAdditionalData} additionalData * @returns */ -export function requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, node: INode, additionalData: IWorkflowExecuteAdditionalData) { +export function requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, node: INode, additionalData: IWorkflowExecuteAdditionalData, tokenType?: string | undefined, property?: string) { const credentials = this.getCredentials(credentialsType) as ICredentialDataDecryptedObject; if (credentials === undefined) { @@ -133,8 +133,8 @@ export function requestOAuth(this: IAllExecuteFunctions, credentialsType: string }); const oauthTokenData = credentials.oauthTokenData as clientOAuth2.Data; - const token = oAuthClient.createToken(oauthTokenData); + const token = oAuthClient.createToken(get(oauthTokenData, property as string) || oauthTokenData.accessToken, oauthTokenData.refresToken, tokenType || oauthTokenData.tokenType, oauthTokenData); // Signs the request by adding authorization headers or query parameters depending // on the token-type used. const newRequestOptions = token.sign(requestOptions as clientOAuth2.RequestObject); @@ -412,8 +412,8 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any - return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType: string | undefined = undefined, property: string = ''): Promise { // tslint:disable-line:no-any + return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); }, returnJsonArray, }, @@ -466,8 +466,8 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any - return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType: string | undefined = undefined, property: string = ''): Promise { // tslint:disable-line:no-any + return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); }, returnJsonArray, }, @@ -547,8 +547,8 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any - return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType: string | undefined = undefined, property: string = ''): Promise { // tslint:disable-line:no-any + return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); }, returnJsonArray, }, @@ -629,8 +629,8 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData: helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any - return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType: string | undefined, property: string = ''): Promise { // tslint:disable-line:no-any + return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); }, }, }; @@ -679,8 +679,8 @@ export function getLoadOptionsFunctions(workflow: Workflow, node: INode, additio }, helpers: { request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any - return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType: string | undefined = undefined, property: string = ''): Promise { // tslint:disable-line:no-any + return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); }, }, }; @@ -737,8 +737,8 @@ export function getExecuteHookFunctions(workflow: Workflow, node: INode, additio }, helpers: { request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any - return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType: string | undefined = undefined, property: string = ''): Promise { // tslint:disable-line:no-any + return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); }, }, }; @@ -822,8 +822,8 @@ export function getExecuteWebhookFunctions(workflow: Workflow, node: INode, addi helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any - return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType: string | undefined = undefined, property: string = ''): Promise { // tslint:disable-line:no-any + return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); }, returnJsonArray, }, diff --git a/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts b/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts new file mode 100644 index 0000000000..45399a3d77 --- /dev/null +++ b/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts @@ -0,0 +1,47 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +//https://api.slack.com/authentication/oauth-v2 + +export class SlackOAuth2Api implements ICredentialType { + name = 'slackOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Slack OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://slack.com/oauth/v2/authorize', + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://slack.com/api/oauth.v2.access', + }, + //https://api.slack.com/scopes + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: 'chat:write', + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: 'user_scope=chat:write', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'body', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Slack/ChannelDescription.ts b/packages/nodes-base/nodes/Slack/ChannelDescription.ts new file mode 100644 index 0000000000..88565716d5 --- /dev/null +++ b/packages/nodes-base/nodes/Slack/ChannelDescription.ts @@ -0,0 +1,209 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const channelOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'channel', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Initiates a public or private channel-based conversation', + }, + { + name: 'Invite', + value: 'invite', + description: 'Invite a user to a channel', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const channelFields = [ + +/* -------------------------------------------------------------------------- */ +/* channel:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Name', + name: 'channel', + type: 'string', + default: '', + placeholder: 'Channel name', + displayOptions: { + show: { + operation: [ + 'create' + ], + resource: [ + 'channel', + ], + }, + }, + required: true, + description: 'The name of the channel to create.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'channel', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Is Private', + name: 'isPrivate', + type: 'boolean', + default: false, + }, + { + displayName: 'Users', + name: 'users', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: [], + }, + ] + }, +/* -------------------------------------------------------------------------- */ +/* channel:invite */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Channel ID', + name: 'channel', + type: 'string', + default: '', + placeholder: 'myChannel', + displayOptions: { + show: { + operation: [ + 'invite' + ], + resource: [ + 'channel', + ], + }, + }, + required: true, + description: 'The ID of the channel to invite user to.', + }, + { + displayName: 'User ID', + name: 'username', + type: 'string', + default: '', + placeholder: 'frank', + displayOptions: { + show: { + operation: [ + 'invite' + ], + resource: [ + 'channel', + ], + }, + }, + required: true, + description: 'The ID of the user to invite into channel.', + }, +/* -------------------------------------------------------------------------- */ +/* channel:get */ +/* -------------------------------------------------------------------------- */ + +/* -------------------------------------------------------------------------- */ +/* channel:delete */ +/* -------------------------------------------------------------------------- */ + +/* -------------------------------------------------------------------------- */ +/* channel:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'channel', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'channel', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'channel', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'Fields to include separated by ,', + }, + ] + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Slack/ConversationDescription.ts b/packages/nodes-base/nodes/Slack/ConversationDescription.ts new file mode 100644 index 0000000000..62628cc654 --- /dev/null +++ b/packages/nodes-base/nodes/Slack/ConversationDescription.ts @@ -0,0 +1,846 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const conversationOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + authentication: [ + 'accessToken', + ], + resource: [ + 'conversation', + ], + }, + }, + options: [ + { + name: 'Post', + value: 'post', + description: 'Post a conversation into a channel', + }, + ], + default: 'post', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const conversationFields = [ + +/* -------------------------------------------------------------------------- */ +/* conversation:post */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channel', + type: 'string', + default: '', + placeholder: 'Channel name', + displayOptions: { + show: { + operation: [ + 'post' + ], + resource: [ + 'conversation', + ], + }, + }, + required: true, + description: 'The channel to send the conversation to.', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + displayOptions: { + show: { + operation: [ + 'post' + ], + resource: [ + 'conversation', + ], + }, + }, + description: 'The text to send.', + }, + { + displayName: 'As User', + name: 'as_user', + type: 'boolean', + default: false, + displayOptions: { + show: { + operation: [ + 'post' + ], + resource: [ + 'conversation', + ], + }, + }, + description: 'Post the conversation as authenticated user instead of bot.', + }, + { + displayName: 'User Name', + name: 'username', + type: 'string', + default: '', + displayOptions: { + show: { + as_user: [ + false + ], + operation: [ + 'post' + ], + resource: [ + 'conversation', + ], + }, + }, + description: 'Set the bot\'s user name.', + }, + { + displayName: 'Attachments', + name: 'attachments', + type: 'collection', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add attachment', + }, + displayOptions: { + show: { + operation: [ + 'post' + ], + resource: [ + 'conversation', + ], + }, + }, + default: {}, // TODO: Remove comment: has to make default array for the main property, check where that happens in UI + description: 'The attachment to add', + placeholder: 'Add attachment item', + options: [ + { + displayName: 'Fallback Text', + name: 'fallback', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Required plain-text summary of the attachment.', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Text to send.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Title of the conversation.', + }, + { + displayName: 'Title Link', + name: 'title_link', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Link of the title.', + }, + { + displayName: 'Color', + name: 'color', + type: 'color', + default: '#ff0000', + description: 'Color of the line left of text.', + }, + { + displayName: 'Pretext', + name: 'pretext', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Text which appears before the conversation block.', + }, + { + displayName: 'Author Name', + name: 'author_name', + type: 'string', + default: '', + description: 'Name that should appear.', + }, + { + displayName: 'Author Link', + name: 'author_link', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Link for the author.', + }, + { + displayName: 'Author Icon', + name: 'author_icon', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Icon which should appear for the user.', + }, + { + displayName: 'Image URL', + name: 'image_url', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'URL of image.', + }, + { + displayName: 'Thumbnail URL', + name: 'thumb_url', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'URL of thumbnail.', + }, + { + displayName: 'Footer', + name: 'footer', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Text of footer to add.', + }, + { + displayName: 'Footer Icon', + name: 'footer_icon', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Icon which should appear next to footer.', + }, + { + displayName: 'Timestamp', + name: 'ts', + type: 'dateTime', + default: '', + description: 'Time conversation relates to.', + }, + { + displayName: 'Fields', + name: 'fields', + placeholder: 'Add Fields', + description: 'Fields to add to conversation.', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'item', + displayName: 'Item', + values: [ + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title of the item.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the item.', + }, + { + displayName: 'Short', + name: 'short', + type: 'boolean', + default: true, + description: 'If items can be displayed next to each other.', + }, + ] + }, + ], + } + ], + }, + { + displayName: 'Other Options', + name: 'otherOptions', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'post' + ], + resource: [ + 'conversation', + ], + }, + }, + default: {}, + description: 'Other options to set', + placeholder: 'Add options', + options: [ + { + displayName: 'Icon Emoji', + name: 'icon_emoji', + type: 'string', + displayOptions: { + show: { + '/as_user': [ + false + ], + '/operation': [ + 'post' + ], + '/resource': [ + 'conversation', + ], + }, + }, + default: '', + description: 'Emoji to use as the icon for this conversation. Overrides icon_url.', + }, + { + displayName: 'Icon URL', + name: 'icon_url', + type: 'string', + displayOptions: { + show: { + '/as_user': [ + false + ], + '/operation': [ + 'post' + ], + '/resource': [ + 'conversation', + ], + }, + }, + default: '', + description: 'URL to an image to use as the icon for this conversation.', + }, + { + displayName: 'Make Reply', + name: 'thread_ts', + type: 'string', + default: '', + description: 'Provide another conversation\'s ts value to make this conversation a reply.', + }, + { + displayName: 'Unfurl Links', + name: 'unfurl_links', + type: 'boolean', + default: false, + description: 'Pass true to enable unfurling of primarily text-based content.', + }, + { + displayName: 'Unfurl Media', + name: 'unfurl_media', + type: 'boolean', + default: true, + description: 'Pass false to disable unfurling of media content.', + }, + { + displayName: 'Markdown', + name: 'mrkdwn', + type: 'boolean', + default: true, + description: 'Use Slack Markdown parsing.', + }, + { + displayName: 'Reply Broadcast', + name: 'reply_broadcast', + type: 'boolean', + default: false, + description: 'Used in conjunction with thread_ts and indicates whether reply should be made visible to everyone in the channel or conversation.', + }, + { + displayName: 'Link Names', + name: 'link_names', + type: 'boolean', + default: false, + description: 'Find and link channel names and usernames.', + }, + ], + }, +/* ----------------------------------------------------------------------- */ +/* conversation:update */ +/* ----------------------------------------------------------------------- */ + { + displayName: 'conversation ID', + name: 'conversationId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'conversation', + ], + operation: [ + 'update', + ] + }, + }, + description: 'Id of conversation that needs to be fetched', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'conversation', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Activity Date', + name: 'activityDate', + type: 'dateTime', + default: '', + description: `Represents the due date of the conversation.
+ This field has a timestamp that is always set to midnight
+ in the Coordinated Universal Time (UTC) time zone.`, + }, + { + displayName: 'Call Disposition', + name: 'callDisposition', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: `Represents the result of a given call, for example, “we'll call back,” or “call
+ unsuccessful.” Limit is 255 characters. Not subject to field-level security, available for any user
+ in an organization with Salesforce CRM Call Center.`, + }, + { + displayName: 'Call Duration In Seconds', + name: 'callDurationInSeconds', + type: 'number', + default: '', + description: `Duration of the call in seconds. Not subject to field-level security,
+ available for any user in an organization with Salesforce CRM Call Cente`, + }, + { + displayName: 'Call Object', + name: 'callObject', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: `Name of a call center. Limit is 255 characters.
+ Not subject to field-level security, available for any user in an
+ organization with Salesforce CRM Call Center.`, + }, + { + displayName: 'Call Type', + name: 'callType', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getconversationCallTypes', + }, + description: 'The type of call being answered: Inbound, Internal, or Outbound.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + description: 'Contains a text description of the conversation.', + }, + { + displayName: 'Is ReminderSet', + name: 'isReminderSet', + type: 'boolean', + default: false, + description: 'Indicates whether a popup reminder has been set for the conversation (true) or not (false).', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'ID of the User who owns the record.', + }, + { + displayName: 'Priority', + name: 'priority', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getconversationPriorities', + }, + description: `Indicates the importance or urgency of a conversation, such as high or low.`, + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getconversationStatuses', + }, + description: 'The current status of the conversation, such as In Progress or Completed.', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getconversationSubjects', + }, + description: 'The subject line of the conversation, such as “Call” or “Send Quote.” Limit: 255 characters.', + }, + { + displayName: 'Recurrence Day Of Month', + name: 'recurrenceDayOfMonth', + type: 'number', + default: '', + description: 'The day of the month in which the conversation repeats.', + }, + { + displayName: 'Recurrence Day Of Week Mask', + name: 'recurrenceDayOfWeekMask', + type: 'number', + default: '', + description: `The day or days of the week on which the conversation repeats.
+ This field contains a bitmask. The values are as follows: Sunday = 1 Monday = 2
+ Tuesday = 4 Wednesday = 8 Thursday = 16 Friday = 32 Saturday = 64
+ Multiple days are represented as the sum of their numerical values.
+ For example, Tuesday and Thursday = 4 + 16 = 20.`, + }, + { + displayName: 'Recurrence End Date Only', + name: 'recurrenceEndDateOnly', + type: 'dateTime', + default: '', + description: `The last date on which the conversation repeats. This field has a timestamp that
+ is always set to midnight in the Coordinated Universal Time (UTC) time zone.`, + }, + { + displayName: 'Recurrence Instance', + name: 'recurrenceInstance', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getconversationRecurrenceInstances', + }, + default: '', + description: `The frequency of the recurring conversation. For example, “2nd” or “3rd.”`, + }, + { + displayName: 'Recurrence Interval', + name: 'recurrenceInterval', + type: 'number', + default: '', + description: 'The interval between recurring conversations.', + }, + { + displayName: 'Recurrence Month Of Year', + name: 'recurrenceMonthOfYear', + type: 'options', + options: [ + { + name: 'January', + value: 'January' + }, + { + name: 'February', + value: 'February' + }, + { + name: 'March', + value: 'March' + }, + { + name: 'April', + value: 'April' + }, + { + name: 'May', + value: 'May' + }, + { + name: 'June', + value: 'June' + }, + { + name: 'July', + value: 'July' + }, + { + name: 'August', + value: 'August' + }, + { + name: 'September', + value: 'September' + }, + { + name: 'October', + value: 'October' + }, + { + name: 'November', + value: 'November' + }, + { + name: 'December', + value: 'December' + } + ], + default: '', + description: 'The month of the year in which the conversation repeats.', + }, + { + displayName: 'Recurrence Start Date Only', + name: 'recurrenceEndDateOnly', + type: 'dateTime', + default: '', + description: `The date when the recurring conversation begins.
+ Must be a date and time before RecurrenceEndDateOnly.`, + }, + { + displayName: 'Recurrence Regenerated Type', + name: 'recurrenceRegeneratedType', + type: 'options', + default: '', + options: [ + { + name: 'After due date', + value: 'RecurrenceRegenerateAfterDueDate' + }, + { + name: 'After date completed', + value: 'RecurrenceRegenerateAfterToday' + }, + { + name: '(conversation Closed)', + value: 'RecurrenceRegenerated' + } + ], + description: `Represents what triggers a repeating conversation to repeat.
+ Add this field to a page layout together with the RecurrenceInterval field,
+ which determines the number of days between the triggering date (due date or close date)
+ and the due date of the next repeating conversation in the series.Label is Repeat This conversation.`, + }, + { + displayName: 'Recurrence Type', + name: 'recurrenceType', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getconversationRecurrenceTypes' + }, + description: 'Website for the conversation.', + }, + { + displayName: 'Recurrence TimeZone SidKey', + name: 'recurrenceTimeZoneSidKey', + type: 'string', + default: '', + description: `The time zone associated with the recurring conversation.
+ For example, “UTC-8:00” for Pacific Standard Time.`, + }, + { + displayName: 'Reminder Date Time', + name: 'reminderDateTime', + type: 'dateTime', + default: '', + description: `Represents the time when the reminder is scheduled to fire,
+ if IsReminderSet is set to true. If IsReminderSet is set to false, then the
+ user may have deselected the reminder checkbox in the Salesforce user interface,
+ or the reminder has already fired at the time indicated by the value.`, + }, + { + displayName: 'What Id', + name: 'whatId', + type: 'string', + default: '', + description: `The WhatId represents nonhuman objects such as accounts, opportunities,
+ campaigns, cases, or custom objects. WhatIds are polymorphic. Polymorphic means a
+ WhatId is equivalent to the ID of a related object.`, + }, + { + displayName: 'Who Id', + name: 'whoId', + type: 'string', + default: '', + description: `The WhoId represents a human such as a lead or a contact.
+ WhoIds are polymorphic. Polymorphic means a WhoId is equivalent to a contact’s ID or a lead’s ID.`, + }, + ] + }, + +/* -------------------------------------------------------------------------- */ +/* conversation:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'conversation ID', + name: 'conversationId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'conversation', + ], + operation: [ + 'get', + ] + }, + }, + description: 'Id of conversation that needs to be fetched', + }, +/* -------------------------------------------------------------------------- */ +/* conversation:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'conversation ID', + name: 'conversationId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'conversation', + ], + operation: [ + 'delete', + ] + }, + }, + description: 'Id of conversation that needs to be fetched', + }, +/* -------------------------------------------------------------------------- */ +/* conversation:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'conversation', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'conversation', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'conversation', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'Fields to include separated by ,', + }, + ] + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Slack/GenericFunctions.ts b/packages/nodes-base/nodes/Slack/GenericFunctions.ts new file mode 100644 index 0000000000..57b5b00c11 --- /dev/null +++ b/packages/nodes-base/nodes/Slack/GenericFunctions.ts @@ -0,0 +1,77 @@ +import { + OptionsWithUri, + } from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; +import { + IDataObject + } from 'n8n-workflow'; + import * as _ from 'lodash'; + +export async function slackApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: object = {}, query: object = {}): Promise { // tslint:disable-line:no-any + const authenticationMethod = this.getNodeParameter('authentication', 0, 'accessToken') as string; + const options: OptionsWithUri = { + method, + headers: { + 'Content-Type': 'application/json; charset=utf-8' + }, + body, + qs: query, + uri: `https://slack.com/api${resource}`, + json: true + }; + if (Object.keys(body).length === 0) { + delete options.body; + } + if (Object.keys(query).length === 0) { + delete options.qs; + } + try { + if (authenticationMethod === 'accessToken') { + const credentials = this.getCredentials('slackApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + options.headers!.Authorization = `Bearer ${credentials.accessToken}`; + //@ts-ignore + return await this.helpers.request(options); + } else { + //@ts-ignore + return await this.helpers.requestOAuth.call(this, 'slackOAuth2Api', options, 'bearer', 'authed_user.access_token'); + } + } catch (error) { + if (error.statusCode === 401) { + // Return a clear error + throw new Error('The Slack credentials are not valid!'); + } + + if (error.response && error.response.body && error.response.body.message) { + // Try to return the error prettier + throw new Error(`Slack error response [${error.statusCode}]: ${error.response.body.message}`); + } + + // If that data does not exist for some reason return the actual error + throw error; + } +} + +export async function salckApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + const returnData: IDataObject[] = []; + let responseData; + do { + responseData = await slackApiRequest.call(this, method, endpoint, body, query); + query.cursor = encodeURIComponent(_.get(responseData, 'response_metadata.next_cursor')); + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData.response_metadata !== undefined && + responseData.response_metadata.mext_cursor !== undefined && + responseData.response_metadata.next_cursor !== "" && + responseData.response_metadata.next_cursor !== null + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Slack/MessageDescription.ts b/packages/nodes-base/nodes/Slack/MessageDescription.ts new file mode 100644 index 0000000000..7f9dff3ac3 --- /dev/null +++ b/packages/nodes-base/nodes/Slack/MessageDescription.ts @@ -0,0 +1,843 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const messageOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'message', + ], + }, + }, + options: [ + { + name: 'Post', + value: 'post', + description: 'Post a message into a channel', + }, + ], + default: 'post', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const messageFields = [ + +/* -------------------------------------------------------------------------- */ +/* message:post */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channel', + type: 'string', + default: '', + placeholder: 'Channel name', + displayOptions: { + show: { + operation: [ + 'post' + ], + resource: [ + 'message', + ], + }, + }, + required: true, + description: 'The channel to send the message to.', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + displayOptions: { + show: { + operation: [ + 'post' + ], + resource: [ + 'message', + ], + }, + }, + description: 'The text to send.', + }, + { + displayName: 'As User', + name: 'as_user', + type: 'boolean', + default: false, + displayOptions: { + show: { + operation: [ + 'post' + ], + resource: [ + 'message', + ], + }, + }, + description: 'Post the message as authenticated user instead of bot.', + }, + { + displayName: 'User Name', + name: 'username', + type: 'string', + default: '', + displayOptions: { + show: { + as_user: [ + false + ], + operation: [ + 'post' + ], + resource: [ + 'message', + ], + }, + }, + description: 'Set the bot\'s user name.', + }, + { + displayName: 'Attachments', + name: 'attachments', + type: 'collection', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add attachment', + }, + displayOptions: { + show: { + operation: [ + 'post' + ], + resource: [ + 'message', + ], + }, + }, + default: {}, // TODO: Remove comment: has to make default array for the main property, check where that happens in UI + description: 'The attachment to add', + placeholder: 'Add attachment item', + options: [ + { + displayName: 'Fallback Text', + name: 'fallback', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Required plain-text summary of the attachment.', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Text to send.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Title of the message.', + }, + { + displayName: 'Title Link', + name: 'title_link', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Link of the title.', + }, + { + displayName: 'Color', + name: 'color', + type: 'color', + default: '#ff0000', + description: 'Color of the line left of text.', + }, + { + displayName: 'Pretext', + name: 'pretext', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Text which appears before the message block.', + }, + { + displayName: 'Author Name', + name: 'author_name', + type: 'string', + default: '', + description: 'Name that should appear.', + }, + { + displayName: 'Author Link', + name: 'author_link', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Link for the author.', + }, + { + displayName: 'Author Icon', + name: 'author_icon', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Icon which should appear for the user.', + }, + { + displayName: 'Image URL', + name: 'image_url', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'URL of image.', + }, + { + displayName: 'Thumbnail URL', + name: 'thumb_url', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'URL of thumbnail.', + }, + { + displayName: 'Footer', + name: 'footer', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Text of footer to add.', + }, + { + displayName: 'Footer Icon', + name: 'footer_icon', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Icon which should appear next to footer.', + }, + { + displayName: 'Timestamp', + name: 'ts', + type: 'dateTime', + default: '', + description: 'Time message relates to.', + }, + { + displayName: 'Fields', + name: 'fields', + placeholder: 'Add Fields', + description: 'Fields to add to message.', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'item', + displayName: 'Item', + values: [ + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title of the item.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the item.', + }, + { + displayName: 'Short', + name: 'short', + type: 'boolean', + default: true, + description: 'If items can be displayed next to each other.', + }, + ] + }, + ], + } + ], + }, + { + displayName: 'Other Options', + name: 'otherOptions', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'post' + ], + resource: [ + 'message', + ], + }, + }, + default: {}, + description: 'Other options to set', + placeholder: 'Add options', + options: [ + { + displayName: 'Icon Emoji', + name: 'icon_emoji', + type: 'string', + displayOptions: { + show: { + '/as_user': [ + false + ], + '/operation': [ + 'post' + ], + '/resource': [ + 'message', + ], + }, + }, + default: '', + description: 'Emoji to use as the icon for this message. Overrides icon_url.', + }, + { + displayName: 'Icon URL', + name: 'icon_url', + type: 'string', + displayOptions: { + show: { + '/as_user': [ + false + ], + '/operation': [ + 'post' + ], + '/resource': [ + 'message', + ], + }, + }, + default: '', + description: 'URL to an image to use as the icon for this message.', + }, + { + displayName: 'Make Reply', + name: 'thread_ts', + type: 'string', + default: '', + description: 'Provide another message\'s ts value to make this message a reply.', + }, + { + displayName: 'Unfurl Links', + name: 'unfurl_links', + type: 'boolean', + default: false, + description: 'Pass true to enable unfurling of primarily text-based content.', + }, + { + displayName: 'Unfurl Media', + name: 'unfurl_media', + type: 'boolean', + default: true, + description: 'Pass false to disable unfurling of media content.', + }, + { + displayName: 'Markdown', + name: 'mrkdwn', + type: 'boolean', + default: true, + description: 'Use Slack Markdown parsing.', + }, + { + displayName: 'Reply Broadcast', + name: 'reply_broadcast', + type: 'boolean', + default: false, + description: 'Used in conjunction with thread_ts and indicates whether reply should be made visible to everyone in the channel or conversation.', + }, + { + displayName: 'Link Names', + name: 'link_names', + type: 'boolean', + default: false, + description: 'Find and link channel names and usernames.', + }, + ], + }, +/* ----------------------------------------------------------------------- */ +/* message:update */ +/* ----------------------------------------------------------------------- */ + { + displayName: 'message ID', + name: 'messageId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'update', + ] + }, + }, + description: 'Id of message that needs to be fetched', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Activity Date', + name: 'activityDate', + type: 'dateTime', + default: '', + description: `Represents the due date of the message.
+ This field has a timestamp that is always set to midnight
+ in the Coordinated Universal Time (UTC) time zone.`, + }, + { + displayName: 'Call Disposition', + name: 'callDisposition', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: `Represents the result of a given call, for example, “we'll call back,” or “call
+ unsuccessful.” Limit is 255 characters. Not subject to field-level security, available for any user
+ in an organization with Salesforce CRM Call Center.`, + }, + { + displayName: 'Call Duration In Seconds', + name: 'callDurationInSeconds', + type: 'number', + default: '', + description: `Duration of the call in seconds. Not subject to field-level security,
+ available for any user in an organization with Salesforce CRM Call Cente`, + }, + { + displayName: 'Call Object', + name: 'callObject', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: `Name of a call center. Limit is 255 characters.
+ Not subject to field-level security, available for any user in an
+ organization with Salesforce CRM Call Center.`, + }, + { + displayName: 'Call Type', + name: 'callType', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getmessageCallTypes', + }, + description: 'The type of call being answered: Inbound, Internal, or Outbound.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + description: 'Contains a text description of the message.', + }, + { + displayName: 'Is ReminderSet', + name: 'isReminderSet', + type: 'boolean', + default: false, + description: 'Indicates whether a popup reminder has been set for the message (true) or not (false).', + }, + { + displayName: 'Owner', + name: 'owner', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'ID of the User who owns the record.', + }, + { + displayName: 'Priority', + name: 'priority', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getmessagePriorities', + }, + description: `Indicates the importance or urgency of a message, such as high or low.`, + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getmessageStatuses', + }, + description: 'The current status of the message, such as In Progress or Completed.', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getmessageSubjects', + }, + description: 'The subject line of the message, such as “Call” or “Send Quote.” Limit: 255 characters.', + }, + { + displayName: 'Recurrence Day Of Month', + name: 'recurrenceDayOfMonth', + type: 'number', + default: '', + description: 'The day of the month in which the message repeats.', + }, + { + displayName: 'Recurrence Day Of Week Mask', + name: 'recurrenceDayOfWeekMask', + type: 'number', + default: '', + description: `The day or days of the week on which the message repeats.
+ This field contains a bitmask. The values are as follows: Sunday = 1 Monday = 2
+ Tuesday = 4 Wednesday = 8 Thursday = 16 Friday = 32 Saturday = 64
+ Multiple days are represented as the sum of their numerical values.
+ For example, Tuesday and Thursday = 4 + 16 = 20.`, + }, + { + displayName: 'Recurrence End Date Only', + name: 'recurrenceEndDateOnly', + type: 'dateTime', + default: '', + description: `The last date on which the message repeats. This field has a timestamp that
+ is always set to midnight in the Coordinated Universal Time (UTC) time zone.`, + }, + { + displayName: 'Recurrence Instance', + name: 'recurrenceInstance', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getmessageRecurrenceInstances', + }, + default: '', + description: `The frequency of the recurring message. For example, “2nd” or “3rd.”`, + }, + { + displayName: 'Recurrence Interval', + name: 'recurrenceInterval', + type: 'number', + default: '', + description: 'The interval between recurring messages.', + }, + { + displayName: 'Recurrence Month Of Year', + name: 'recurrenceMonthOfYear', + type: 'options', + options: [ + { + name: 'January', + value: 'January' + }, + { + name: 'February', + value: 'February' + }, + { + name: 'March', + value: 'March' + }, + { + name: 'April', + value: 'April' + }, + { + name: 'May', + value: 'May' + }, + { + name: 'June', + value: 'June' + }, + { + name: 'July', + value: 'July' + }, + { + name: 'August', + value: 'August' + }, + { + name: 'September', + value: 'September' + }, + { + name: 'October', + value: 'October' + }, + { + name: 'November', + value: 'November' + }, + { + name: 'December', + value: 'December' + } + ], + default: '', + description: 'The month of the year in which the message repeats.', + }, + { + displayName: 'Recurrence Start Date Only', + name: 'recurrenceEndDateOnly', + type: 'dateTime', + default: '', + description: `The date when the recurring message begins.
+ Must be a date and time before RecurrenceEndDateOnly.`, + }, + { + displayName: 'Recurrence Regenerated Type', + name: 'recurrenceRegeneratedType', + type: 'options', + default: '', + options: [ + { + name: 'After due date', + value: 'RecurrenceRegenerateAfterDueDate' + }, + { + name: 'After date completed', + value: 'RecurrenceRegenerateAfterToday' + }, + { + name: '(message Closed)', + value: 'RecurrenceRegenerated' + } + ], + description: `Represents what triggers a repeating message to repeat.
+ Add this field to a page layout together with the RecurrenceInterval field,
+ which determines the number of days between the triggering date (due date or close date)
+ and the due date of the next repeating message in the series.Label is Repeat This message.`, + }, + { + displayName: 'Recurrence Type', + name: 'recurrenceType', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getmessageRecurrenceTypes' + }, + description: 'Website for the message.', + }, + { + displayName: 'Recurrence TimeZone SidKey', + name: 'recurrenceTimeZoneSidKey', + type: 'string', + default: '', + description: `The time zone associated with the recurring message.
+ For example, “UTC-8:00” for Pacific Standard Time.`, + }, + { + displayName: 'Reminder Date Time', + name: 'reminderDateTime', + type: 'dateTime', + default: '', + description: `Represents the time when the reminder is scheduled to fire,
+ if IsReminderSet is set to true. If IsReminderSet is set to false, then the
+ user may have deselected the reminder checkbox in the Salesforce user interface,
+ or the reminder has already fired at the time indicated by the value.`, + }, + { + displayName: 'What Id', + name: 'whatId', + type: 'string', + default: '', + description: `The WhatId represents nonhuman objects such as accounts, opportunities,
+ campaigns, cases, or custom objects. WhatIds are polymorphic. Polymorphic means a
+ WhatId is equivalent to the ID of a related object.`, + }, + { + displayName: 'Who Id', + name: 'whoId', + type: 'string', + default: '', + description: `The WhoId represents a human such as a lead or a contact.
+ WhoIds are polymorphic. Polymorphic means a WhoId is equivalent to a contact’s ID or a lead’s ID.`, + }, + ] + }, + +/* -------------------------------------------------------------------------- */ +/* message:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'message ID', + name: 'messageId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'get', + ] + }, + }, + description: 'Id of message that needs to be fetched', + }, +/* -------------------------------------------------------------------------- */ +/* message:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'message ID', + name: 'messageId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'delete', + ] + }, + }, + description: 'Id of message that needs to be fetched', + }, +/* -------------------------------------------------------------------------- */ +/* message:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'Fields to include separated by ,', + }, + ] + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Slack/MessageInterface.ts b/packages/nodes-base/nodes/Slack/MessageInterface.ts new file mode 100644 index 0000000000..caff7c0765 --- /dev/null +++ b/packages/nodes-base/nodes/Slack/MessageInterface.ts @@ -0,0 +1,7 @@ + +export interface IAttachment { + fields: { + item?: object[]; + }; +} + diff --git a/packages/nodes-base/nodes/Slack/Slack.node.ts b/packages/nodes-base/nodes/Slack/Slack.node.ts index c20f8ba12b..c3b074c1e1 100644 --- a/packages/nodes-base/nodes/Slack/Slack.node.ts +++ b/packages/nodes-base/nodes/Slack/Slack.node.ts @@ -1,16 +1,33 @@ -import { IExecuteFunctions } from 'n8n-core'; +import { + IExecuteFunctions, + } from 'n8n-core'; import { IDataObject, INodeTypeDescription, INodeExecutionData, INodeType, + ILoadOptionsFunctions, + INodePropertyOptions, } from 'n8n-workflow'; - -interface Attachment { - fields: { - item?: object[]; - }; -} +import { + channelOperations, + channelFields, +} from './ChannelDescription'; +import { + messageOperations, + messageFields, +} from './MessageDescription'; +import { + conversationOperations, + conversationFields, +} from './ConversationDescription'; +import { + slackApiRequest, + salckApiRequestAllItems, +} from './GenericFunctions'; +import { + IAttachment, +} from './MessageInterface'; export class Slack implements INodeType { description: INodeTypeDescription = { @@ -31,9 +48,44 @@ export class Slack implements INodeType { { name: 'slackApi', required: true, + displayOptions: { + show: { + authentication: [ + 'accessToken', + ], + }, + }, + }, + { + name: 'slackOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: [ + 'oauth2', + ], + }, + }, } ], properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'Access Token', + value: 'accessToken', + }, + { + name: 'OAuth2', + value: 'oauth2', + }, + ], + default: 'accessToken', + description: 'The resource to operate on.', + }, { displayName: 'Resource', name: 'resource', @@ -51,586 +103,77 @@ export class Slack implements INodeType { default: 'message', description: 'The resource to operate on.', }, - - - - // ---------------------------------- - // operations - // ---------------------------------- - { - displayName: 'Operation', - name: 'operation', - type: 'options', - displayOptions: { - show: { - resource: [ - 'channel', - ], - }, - }, - options: [ - { - name: 'Create', - value: 'create', - description: 'Create a new channel', - }, - { - name: 'Invite', - value: 'invite', - description: 'Invite a user to a channel', - }, - ], - default: 'create', - description: 'The operation to perform.', - }, - - { - displayName: 'Operation', - name: 'operation', - type: 'options', - displayOptions: { - show: { - resource: [ - 'message', - ], - }, - }, - options: [ - { - name: 'Post', - value: 'post', - description: 'Post a message into a channel', - }, - ], - default: 'post', - description: 'The operation to perform.', - }, - - - - // ---------------------------------- - // channel - // ---------------------------------- - - // ---------------------------------- - // channel:create - // ---------------------------------- - { - displayName: 'Name', - name: 'channel', - type: 'string', - default: '', - placeholder: 'Channel name', - displayOptions: { - show: { - operation: [ - 'create' - ], - resource: [ - 'channel', - ], - }, - }, - required: true, - description: 'The name of the channel to create.', - }, - - // ---------------------------------- - // channel:invite - // ---------------------------------- - { - displayName: 'Channel ID', - name: 'channel', - type: 'string', - default: '', - placeholder: 'myChannel', - displayOptions: { - show: { - operation: [ - 'invite' - ], - resource: [ - 'channel', - ], - }, - }, - required: true, - description: 'The ID of the channel to invite user to.', - }, - { - displayName: 'User ID', - name: 'username', - type: 'string', - default: '', - placeholder: 'frank', - displayOptions: { - show: { - operation: [ - 'invite' - ], - resource: [ - 'channel', - ], - }, - }, - required: true, - description: 'The ID of the user to invite into channel.', - }, - - - - // ---------------------------------- - // message - // ---------------------------------- - - // ---------------------------------- - // message:post - // ---------------------------------- - { - displayName: 'Channel', - name: 'channel', - type: 'string', - default: '', - placeholder: 'Channel name', - displayOptions: { - show: { - operation: [ - 'post' - ], - resource: [ - 'message', - ], - }, - }, - required: true, - description: 'The channel to send the message to.', - }, - { - displayName: 'Text', - name: 'text', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - displayOptions: { - show: { - operation: [ - 'post' - ], - resource: [ - 'message', - ], - }, - }, - description: 'The text to send.', - }, - { - displayName: 'As User', - name: 'as_user', - type: 'boolean', - default: false, - displayOptions: { - show: { - operation: [ - 'post' - ], - resource: [ - 'message', - ], - }, - }, - description: 'Post the message as authenticated user instead of bot.', - }, - { - displayName: 'User Name', - name: 'username', - type: 'string', - default: '', - displayOptions: { - show: { - as_user: [ - false - ], - operation: [ - 'post' - ], - resource: [ - 'message', - ], - }, - }, - description: 'Set the bot\'s user name.', - }, - { - displayName: 'Attachments', - name: 'attachments', - type: 'collection', - typeOptions: { - multipleValues: true, - multipleValueButtonText: 'Add attachment', - }, - displayOptions: { - show: { - operation: [ - 'post' - ], - resource: [ - 'message', - ], - }, - }, - default: {}, // TODO: Remove comment: has to make default array for the main property, check where that happens in UI - description: 'The attachment to add', - placeholder: 'Add attachment item', - options: [ - { - displayName: 'Fallback Text', - name: 'fallback', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'Required plain-text summary of the attachment.', - }, - { - displayName: 'Text', - name: 'text', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'Text to send.', - }, - { - displayName: 'Title', - name: 'title', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'Title of the message.', - }, - { - displayName: 'Title Link', - name: 'title_link', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'Link of the title.', - }, - { - displayName: 'Color', - name: 'color', - type: 'color', - default: '#ff0000', - description: 'Color of the line left of text.', - }, - { - displayName: 'Pretext', - name: 'pretext', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'Text which appears before the message block.', - }, - { - displayName: 'Author Name', - name: 'author_name', - type: 'string', - default: '', - description: 'Name that should appear.', - }, - { - displayName: 'Author Link', - name: 'author_link', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'Link for the author.', - }, - { - displayName: 'Author Icon', - name: 'author_icon', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'Icon which should appear for the user.', - }, - { - displayName: 'Image URL', - name: 'image_url', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'URL of image.', - }, - { - displayName: 'Thumbnail URL', - name: 'thumb_url', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'URL of thumbnail.', - }, - { - displayName: 'Footer', - name: 'footer', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'Text of footer to add.', - }, - { - displayName: 'Footer Icon', - name: 'footer_icon', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'Icon which should appear next to footer.', - }, - { - displayName: 'Timestamp', - name: 'ts', - type: 'dateTime', - default: '', - description: 'Time message relates to.', - }, - { - displayName: 'Fields', - name: 'fields', - placeholder: 'Add Fields', - description: 'Fields to add to message.', - type: 'fixedCollection', - typeOptions: { - multipleValues: true, - }, - default: {}, - options: [ - { - name: 'item', - displayName: 'Item', - values: [ - { - displayName: 'Title', - name: 'title', - type: 'string', - default: '', - description: 'Title of the item.', - }, - { - displayName: 'Value', - name: 'value', - type: 'string', - default: '', - description: 'Value of the item.', - }, - { - displayName: 'Short', - name: 'short', - type: 'boolean', - default: true, - description: 'If items can be displayed next to each other.', - }, - ] - }, - ], - } - ], - }, - { - displayName: 'Other Options', - name: 'otherOptions', - type: 'collection', - displayOptions: { - show: { - operation: [ - 'post' - ], - resource: [ - 'message', - ], - }, - }, - default: {}, - description: 'Other options to set', - placeholder: 'Add options', - options: [ - { - displayName: 'Icon Emoji', - name: 'icon_emoji', - type: 'string', - displayOptions: { - show: { - '/as_user': [ - false - ], - '/operation': [ - 'post' - ], - '/resource': [ - 'message', - ], - }, - }, - default: '', - description: 'Emoji to use as the icon for this message. Overrides icon_url.', - }, - { - displayName: 'Icon URL', - name: 'icon_url', - type: 'string', - displayOptions: { - show: { - '/as_user': [ - false - ], - '/operation': [ - 'post' - ], - '/resource': [ - 'message', - ], - }, - }, - default: '', - description: 'URL to an image to use as the icon for this message.', - }, - { - displayName: 'Make Reply', - name: 'thread_ts', - type: 'string', - default: '', - description: 'Provide another message\'s ts value to make this message a reply.', - }, - { - displayName: 'Unfurl Links', - name: 'unfurl_links', - type: 'boolean', - default: false, - description: 'Pass true to enable unfurling of primarily text-based content.', - }, - { - displayName: 'Unfurl Media', - name: 'unfurl_media', - type: 'boolean', - default: true, - description: 'Pass false to disable unfurling of media content.', - }, - { - displayName: 'Markdown', - name: 'mrkdwn', - type: 'boolean', - default: true, - description: 'Use Slack Markdown parsing.', - }, - { - displayName: 'Reply Broadcast', - name: 'reply_broadcast', - type: 'boolean', - default: false, - description: 'Used in conjunction with thread_ts and indicates whether reply should be made visible to everyone in the channel or conversation.', - }, - { - displayName: 'Link Names', - name: 'link_names', - type: 'boolean', - default: false, - description: 'Find and link channel names and usernames.', - }, - ], - }, + ...channelOperations, + ...channelFields, + ...messageOperations, + ...messageFields, ], }; - + methods = { + loadOptions: { + // Get all the users to display them to user so that he can + // select them easily + async getUsers(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const users = await salckApiRequestAllItems.call(this, 'members', 'GET', '/users.list'); + for (const user of users) { + const userName = user.name; + const userId = user.id; + returnData.push({ + name: userName, + value: userId, + }); + } + console.log(users) + return returnData; + }, + } + }; async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); const returnData: IDataObject[] = []; - - const credentials = this.getCredentials('slackApi'); - - if (credentials === undefined) { - throw new Error('No credentials got returned!'); - } - - const baseUrl = `https://slack.com/api/`; - let operation: string; - let resource: string; - let requestMethod = 'POST'; - - // For Post - let body: IDataObject; - // For Query string + const length = items.length as unknown as number; let qs: IDataObject; - - for (let i = 0; i < items.length; i++) { - let endpoint = ''; - body = {}; + let responseData; + for (let i = 0; i < length; i++) { qs = {}; - - resource = this.getNodeParameter('resource', i) as string; - operation = this.getNodeParameter('operation', i) as string; - + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; if (resource === 'channel') { + //https://api.slack.com/methods/conversations.create if (operation === 'create') { - // ---------------------------------- - // channel:create - // ---------------------------------- - - requestMethod = 'POST'; - endpoint = 'channels.create'; - - body.name = this.getNodeParameter('channel', i) as string; - } else if (operation === 'invite') { - // ---------------------------------- - // channel:invite - // ---------------------------------- - - requestMethod = 'POST'; - endpoint = 'channels.invite'; - - body.channel = this.getNodeParameter('channel', i) as string; - body.user = this.getNodeParameter('username', i) as string; + const channel = this.getNodeParameter('channel', i) as string; + const body: IDataObject = { + name: channel, + }; + responseData = await slackApiRequest.call(this, 'POST', '/channels.create', body, qs); } - } else if (resource === 'message') { + if (operation === 'invite') { + const channel = this.getNodeParameter('channel', i) as string; + const user = this.getNodeParameter('username', i) as string; + const body: IDataObject = { + channel, + user, + }; + responseData = await slackApiRequest.call(this, 'POST', '/channels.invite', body, qs); + } + } + if (resource === 'message') { if (operation === 'post') { - // ---------------------------------- - // message:post - // ---------------------------------- - - requestMethod = 'POST'; - endpoint = 'chat.postMessage'; - - body.channel = this.getNodeParameter('channel', i) as string; - body.text = this.getNodeParameter('text', i) as string; - body.as_user = this.getNodeParameter('as_user', i) as boolean; - if (body.as_user === false) { + const channel = this.getNodeParameter('channel', i) as string; + const text = this.getNodeParameter('text', i) as string; + const attachments = this.getNodeParameter('attachments', i, []) as unknown as IAttachment[]; + const as_user = this.getNodeParameter('as_user', i) as boolean; + const body: IDataObject = { + channel: channel, + text, + as_user, + }; + if (as_user === false) { body.username = this.getNodeParameter('username', i) as string; } - - const attachments = this.getNodeParameter('attachments', i, []) as unknown as Attachment[]; - // The node does save the fields data differently than the API // expects so fix the data befre we send the request for (const attachment of attachments) { @@ -650,32 +193,15 @@ export class Slack implements INodeType { // Add all the other options to the request const otherOptions = this.getNodeParameter('otherOptions', i) as IDataObject; Object.assign(body, otherOptions); + responseData = await slackApiRequest.call(this, 'POST', '/chat.postMessage', body, qs); } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); } else { - throw new Error(`The resource "${resource}" is not known!`); + returnData.push(responseData as IDataObject); } - - const options = { - method: requestMethod, - body, - qs, - uri: `${baseUrl}/${endpoint}`, - headers: { - Authorization: `Bearer ${credentials.accessToken }`, - 'content-type': 'application/json; charset=utf-8' - }, - json: true - }; - - const responseData = await this.helpers.request(options); - - if (!responseData.ok) { - throw new Error(`Request to Slack did fail with error: "${responseData.error}"`); - } - - returnData.push(responseData as IDataObject); } - return [this.helpers.returnJsonArray(returnData)]; } } diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 746f91f723..45d5012c8e 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -68,6 +68,7 @@ "dist/credentials/ShopifyApi.credentials.js", "dist/credentials/SalesforceOAuth2Api.credentials.js", "dist/credentials/SlackApi.credentials.js", + "dist/credentials/SlackOAuth2Api.credentials.js", "dist/credentials/Smtp.credentials.js", "dist/credentials/StripeApi.credentials.js", "dist/credentials/TelegramApi.credentials.js", From ba22ab02d8dda844cd571248ec207e28d705639e Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sun, 8 Mar 2020 18:22:33 -0400 Subject: [PATCH 018/165] :zap: Added missing resources --- .../credentials/SlackOAuth2Api.credentials.ts | 12 +- .../nodes/Slack/ChannelDescription.ts | 735 ++++++++++++++- .../nodes/Slack/ConversationDescription.ts | 846 ------------------ .../nodes-base/nodes/Slack/FileDescription.ts | 322 +++++++ .../nodes/Slack/GenericFunctions.ts | 19 +- .../nodes/Slack/MessageDescription.ts | 629 ++++++------- packages/nodes-base/nodes/Slack/Slack.node.ts | 408 ++++++++- .../nodes-base/nodes/Slack/StarDescription.ts | 185 ++++ 8 files changed, 1900 insertions(+), 1256 deletions(-) delete mode 100644 packages/nodes-base/nodes/Slack/ConversationDescription.ts create mode 100644 packages/nodes-base/nodes/Slack/FileDescription.ts create mode 100644 packages/nodes-base/nodes/Slack/StarDescription.ts diff --git a/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts b/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts index 45399a3d77..6c5284ee44 100644 --- a/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts @@ -4,6 +4,16 @@ import { } from 'n8n-workflow'; //https://api.slack.com/authentication/oauth-v2 +const userScopes = [ + 'chat:write', + 'conversations:history', + 'conversations:read', + 'files:read', + 'files:write', + 'stars:read', + 'stars:write', +] + export class SlackOAuth2Api implements ICredentialType { name = 'slackOAuth2Api'; @@ -35,7 +45,7 @@ export class SlackOAuth2Api implements ICredentialType { displayName: 'Auth URI Query Parameters', name: 'authQueryParameters', type: 'hidden' as NodePropertyTypes, - default: 'user_scope=chat:write', + default: `user_scope=${userScopes.join(' ')}`, }, { displayName: 'Authentication', diff --git a/packages/nodes-base/nodes/Slack/ChannelDescription.ts b/packages/nodes-base/nodes/Slack/ChannelDescription.ts index 88565716d5..cda5824105 100644 --- a/packages/nodes-base/nodes/Slack/ChannelDescription.ts +++ b/packages/nodes-base/nodes/Slack/ChannelDescription.ts @@ -13,16 +13,86 @@ export const channelOperations = [ }, }, options: [ + { + name: 'Archive', + value: 'archive', + description: 'Archives a conversation.', + }, + { + name: 'Close', + value: 'close', + description: 'Closes a direct message or multi-person direct message.', + }, { name: 'Create', value: 'create', description: 'Initiates a public or private channel-based conversation', }, + { + name: 'Get', + value: 'get', + description: 'Get information about a channel.', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all channels in a Slack team.', + }, + { + name: 'History', + value: 'history', + description: `Get a conversation's history of messages and events.`, + }, { name: 'Invite', value: 'invite', description: 'Invite a user to a channel', }, + { + name: 'Join', + value: 'join', + description: 'Joins an existing conversation.', + }, + { + name: 'Kick', + value: 'kick', + description: 'Removes a user from a channel.', + }, + { + name: 'Leave', + value: 'leave', + description: 'Leaves a conversation.', + }, + { + name: 'Open', + value: 'open', + description: 'Opens or resumes a direct message or multi-person direct message.', + }, + { + name: 'Rename', + value: 'rename', + description: 'Renames a conversation.', + }, + { + name: 'Replies', + value: 'replies', + description: 'Get a thread of messages posted to a channel', + }, + { + name: 'Set Purpose', + value: 'setPurpose', + description: 'Sets the purpose for a conversation.', + }, + { + name: 'Set Topic', + value: 'setTopic', + description: 'Sets the topic for a conversation.', + }, + { + name: 'Unarchive', + value: 'unarchive', + description: 'Unarchives a conversation.', + }, ], default: 'create', description: 'The operation to perform.', @@ -31,12 +101,60 @@ export const channelOperations = [ export const channelFields = [ +/* -------------------------------------------------------------------------- */ +/* channel:archive */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + displayOptions: { + show: { + operation: [ + 'archive' + ], + resource: [ + 'channel', + ], + }, + }, + default: '', + required: true, + description: 'The name of the channel to archive.', + }, +/* -------------------------------------------------------------------------- */ +/* channel:close */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + displayOptions: { + show: { + operation: [ + 'close' + ], + resource: [ + 'channel', + ], + }, + }, + default: '', + required: true, + description: 'The name of the channel to close.', + }, /* -------------------------------------------------------------------------- */ /* channel:create */ /* -------------------------------------------------------------------------- */ { - displayName: 'Name', - name: 'channel', + displayName: 'Channel', + name: 'channelId', type: 'string', default: '', placeholder: 'Channel name', @@ -75,6 +193,7 @@ export const channelFields = [ name: 'isPrivate', type: 'boolean', default: false, + description: 'Create a private channel instead of a public one', }, { displayName: 'Users', @@ -84,6 +203,7 @@ export const channelFields = [ loadOptionsMethod: 'getUsers', }, default: [], + description: `Required for workspace apps. A list of between 1 and 30 human users that will be added to the newly-created conversation`, }, ] }, @@ -91,11 +211,13 @@ export const channelFields = [ /* channel:invite */ /* -------------------------------------------------------------------------- */ { - displayName: 'Channel ID', - name: 'channel', - type: 'string', + displayName: 'Channel', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, default: '', - placeholder: 'myChannel', displayOptions: { show: { operation: [ @@ -111,10 +233,9 @@ export const channelFields = [ }, { displayName: 'User ID', - name: 'username', + name: 'userId', type: 'string', default: '', - placeholder: 'frank', displayOptions: { show: { operation: [ @@ -131,11 +252,114 @@ export const channelFields = [ /* -------------------------------------------------------------------------- */ /* channel:get */ /* -------------------------------------------------------------------------- */ - + { + displayName: 'Channel', + name: 'channelId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'get' + ], + resource: [ + 'channel', + ], + }, + }, + required: true, + description: 'Channel ID to learn more about', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'channel', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Include Num of Members', + name: 'includeNumMembers', + type: 'boolean', + default: false, + }, + ] + }, /* -------------------------------------------------------------------------- */ -/* channel:delete */ +/* channel:kick */ /* -------------------------------------------------------------------------- */ - + { + displayName: 'Channel', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + default: '', + placeholder: 'Channel name', + displayOptions: { + show: { + operation: [ + 'kick' + ], + resource: [ + 'channel', + ], + }, + }, + required: true, + description: 'The name of the channel to create.', + }, + { + displayName: 'User ID', + name: 'userId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'kick' + ], + resource: [ + 'channel', + ], + }, + }, + default: '', + }, +/* -------------------------------------------------------------------------- */ +/* channel:join */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + default: '', + placeholder: 'Channel name', + displayOptions: { + show: { + operation: [ + 'join' + ], + resource: [ + 'channel', + ], + }, + }, + required: true, + }, /* -------------------------------------------------------------------------- */ /* channel:getAll */ /* -------------------------------------------------------------------------- */ @@ -181,8 +405,8 @@ export const channelFields = [ description: 'How many results to return.', }, { - displayName: 'Options', - name: 'options', + displayName: 'Filters', + name: 'filters', type: 'collection', placeholder: 'Add Field', default: {}, @@ -198,12 +422,487 @@ export const channelFields = [ }, options: [ { - displayName: 'Fields', - name: 'fields', - type: 'string', - default: '', - description: 'Fields to include separated by ,', + displayName: 'Exclude Archived', + name: 'excludeArchived', + type: 'boolean', + default: false, + description: 'Set to true to exclude archived channels from the list', + }, + { + displayName: 'Types', + name: 'types', + type: 'multiOptions', + options: [ + { + name: 'Public Channel', + value: 'public_channel' + }, + { + name: 'Private Channel', + value: 'private_channel' + }, + { + name: 'mpim', + value: 'mpim' + }, + { + name: 'im', + value: 'im' + }, + ], + default: ['public_channel'], + description: 'Mix and match channel types', }, ] }, +/* -------------------------------------------------------------------------- */ +/* channel:history */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + default: '', + placeholder: 'Channel name', + displayOptions: { + show: { + operation: [ + 'history' + ], + resource: [ + 'channel', + ], + }, + }, + required: true, + description: 'The name of the channel to create.', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'channel', + ], + operation: [ + 'history', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'channel', + ], + operation: [ + 'history', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'channel', + ], + operation: [ + 'history', + ], + }, + }, + options: [ + { + displayName: 'Inclusive', + name: 'inclusive', + type: 'boolean', + default: false, + description: 'Include messages with latest or oldest timestamp in results only when either timestamp is specified.', + }, + { + displayName: 'Latest', + name: 'latest', + type: 'dateTime', + default: '', + description: 'End of time range of messages to include in results.', + }, + { + displayName: 'Oldest', + name: 'oldest', + type: 'dateTime', + default: '', + description: 'Start of time range of messages to include in results.', + }, + ] + }, +/* -------------------------------------------------------------------------- */ +/* channel:leave */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + displayOptions: { + show: { + operation: [ + 'leave' + ], + resource: [ + 'channel', + ], + }, + }, + default: '', + required: true, + description: 'The name of the channel to leave.', + }, +/* -------------------------------------------------------------------------- */ +/* channel:open */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'channel', + ], + operation: [ + 'open', + ], + }, + }, + options: [ + { + displayName: 'Channel ID', + name: 'channelId', + type: 'string', + default: '', + description: `Resume a conversation by supplying an im or mpim's ID. Or provide the users field instead`, + }, + { + displayName: 'Return IM', + name: 'returnIm', + type: 'boolean', + default: false, + description: 'Boolean, indicates you want the full IM channel definition in the response.', + }, + { + displayName: 'Users', + name: 'users', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: [], + description: `If only one user is included, this creates a 1:1 DM. The ordering of the users is preserved whenever a multi-person direct message is returned. Supply a channel when not supplying users.`, + }, + ] + }, +/* -------------------------------------------------------------------------- */ +/* channel:rename */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + displayOptions: { + show: { + operation: [ + 'rename' + ], + resource: [ + 'channel', + ], + }, + }, + default: '', + required: true, + description: 'The name of the channel to rename.', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + displayOptions: { + show: { + operation: [ + 'rename' + ], + resource: [ + 'channel', + ], + }, + }, + default: '', + required: true, + description: 'New name for conversation.', + }, +/* -------------------------------------------------------------------------- */ +/* channel:replies */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + default: '', + placeholder: 'Channel name', + displayOptions: { + show: { + operation: [ + 'replies' + ], + resource: [ + 'channel', + ], + }, + }, + required: true, + description: 'The name of the channel to create.', + }, + { + displayName: 'TS', + name: 'ts', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'replies' + ], + resource: [ + 'channel', + ], + }, + }, + required: true, + description: `Unique identifier of a thread's parent message.`, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'channel', + ], + operation: [ + 'replies', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'channel', + ], + operation: [ + 'replies', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'channel', + ], + operation: [ + 'replies', + ], + }, + }, + options: [ + { + displayName: 'Inclusive', + name: 'inclusive', + type: 'boolean', + default: false, + description: 'Include messages with latest or oldest timestamp in results only when either timestamp is specified.', + }, + { + displayName: 'Latest', + name: 'latest', + type: 'string', + default: '', + description: 'End of time range of messages to include in results.', + }, + { + displayName: 'Oldest', + name: 'oldest', + type: 'string', + default: '', + description: 'Start of time range of messages to include in results.', + }, + ] + }, +/* -------------------------------------------------------------------------- */ +/* channel:setPurpose */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + displayOptions: { + show: { + operation: [ + 'setPurpose' + ], + resource: [ + 'channel', + ], + }, + }, + default: '', + required: true, + description: 'Conversation to set the purpose of', + }, + { + displayName: 'Purpose', + name: 'purpose', + type: 'string', + displayOptions: { + show: { + operation: [ + 'setPurpose' + ], + resource: [ + 'channel', + ], + }, + }, + default: '', + required: true, + description: 'A new, specialer purpose', + }, +/* -------------------------------------------------------------------------- */ +/* channel:setTopic */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + displayOptions: { + show: { + operation: [ + 'setTopic' + ], + resource: [ + 'channel', + ], + }, + }, + default: '', + required: true, + description: 'Conversation to set the topic of', + }, + { + displayName: 'Topic', + name: 'topic', + type: 'string', + displayOptions: { + show: { + operation: [ + 'setTopic' + ], + resource: [ + 'channel', + ], + }, + }, + default: '', + required: true, + description: 'The new topic string. Does not support formatting or linkification.', + }, +/* -------------------------------------------------------------------------- */ +/* channel:unarchive */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + displayOptions: { + show: { + operation: [ + 'unarchive' + ], + resource: [ + 'channel', + ], + }, + }, + default: '', + required: true, + description: 'The ID of the channel to unarchive.', + }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Slack/ConversationDescription.ts b/packages/nodes-base/nodes/Slack/ConversationDescription.ts deleted file mode 100644 index 62628cc654..0000000000 --- a/packages/nodes-base/nodes/Slack/ConversationDescription.ts +++ /dev/null @@ -1,846 +0,0 @@ -import { INodeProperties } from 'n8n-workflow'; - -export const conversationOperations = [ - { - displayName: 'Operation', - name: 'operation', - type: 'options', - displayOptions: { - show: { - authentication: [ - 'accessToken', - ], - resource: [ - 'conversation', - ], - }, - }, - options: [ - { - name: 'Post', - value: 'post', - description: 'Post a conversation into a channel', - }, - ], - default: 'post', - description: 'The operation to perform.', - }, -] as INodeProperties[]; - -export const conversationFields = [ - -/* -------------------------------------------------------------------------- */ -/* conversation:post */ -/* -------------------------------------------------------------------------- */ - { - displayName: 'Channel', - name: 'channel', - type: 'string', - default: '', - placeholder: 'Channel name', - displayOptions: { - show: { - operation: [ - 'post' - ], - resource: [ - 'conversation', - ], - }, - }, - required: true, - description: 'The channel to send the conversation to.', - }, - { - displayName: 'Text', - name: 'text', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - displayOptions: { - show: { - operation: [ - 'post' - ], - resource: [ - 'conversation', - ], - }, - }, - description: 'The text to send.', - }, - { - displayName: 'As User', - name: 'as_user', - type: 'boolean', - default: false, - displayOptions: { - show: { - operation: [ - 'post' - ], - resource: [ - 'conversation', - ], - }, - }, - description: 'Post the conversation as authenticated user instead of bot.', - }, - { - displayName: 'User Name', - name: 'username', - type: 'string', - default: '', - displayOptions: { - show: { - as_user: [ - false - ], - operation: [ - 'post' - ], - resource: [ - 'conversation', - ], - }, - }, - description: 'Set the bot\'s user name.', - }, - { - displayName: 'Attachments', - name: 'attachments', - type: 'collection', - typeOptions: { - multipleValues: true, - multipleValueButtonText: 'Add attachment', - }, - displayOptions: { - show: { - operation: [ - 'post' - ], - resource: [ - 'conversation', - ], - }, - }, - default: {}, // TODO: Remove comment: has to make default array for the main property, check where that happens in UI - description: 'The attachment to add', - placeholder: 'Add attachment item', - options: [ - { - displayName: 'Fallback Text', - name: 'fallback', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'Required plain-text summary of the attachment.', - }, - { - displayName: 'Text', - name: 'text', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'Text to send.', - }, - { - displayName: 'Title', - name: 'title', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'Title of the conversation.', - }, - { - displayName: 'Title Link', - name: 'title_link', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'Link of the title.', - }, - { - displayName: 'Color', - name: 'color', - type: 'color', - default: '#ff0000', - description: 'Color of the line left of text.', - }, - { - displayName: 'Pretext', - name: 'pretext', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'Text which appears before the conversation block.', - }, - { - displayName: 'Author Name', - name: 'author_name', - type: 'string', - default: '', - description: 'Name that should appear.', - }, - { - displayName: 'Author Link', - name: 'author_link', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'Link for the author.', - }, - { - displayName: 'Author Icon', - name: 'author_icon', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'Icon which should appear for the user.', - }, - { - displayName: 'Image URL', - name: 'image_url', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'URL of image.', - }, - { - displayName: 'Thumbnail URL', - name: 'thumb_url', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'URL of thumbnail.', - }, - { - displayName: 'Footer', - name: 'footer', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'Text of footer to add.', - }, - { - displayName: 'Footer Icon', - name: 'footer_icon', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: 'Icon which should appear next to footer.', - }, - { - displayName: 'Timestamp', - name: 'ts', - type: 'dateTime', - default: '', - description: 'Time conversation relates to.', - }, - { - displayName: 'Fields', - name: 'fields', - placeholder: 'Add Fields', - description: 'Fields to add to conversation.', - type: 'fixedCollection', - typeOptions: { - multipleValues: true, - }, - default: {}, - options: [ - { - name: 'item', - displayName: 'Item', - values: [ - { - displayName: 'Title', - name: 'title', - type: 'string', - default: '', - description: 'Title of the item.', - }, - { - displayName: 'Value', - name: 'value', - type: 'string', - default: '', - description: 'Value of the item.', - }, - { - displayName: 'Short', - name: 'short', - type: 'boolean', - default: true, - description: 'If items can be displayed next to each other.', - }, - ] - }, - ], - } - ], - }, - { - displayName: 'Other Options', - name: 'otherOptions', - type: 'collection', - displayOptions: { - show: { - operation: [ - 'post' - ], - resource: [ - 'conversation', - ], - }, - }, - default: {}, - description: 'Other options to set', - placeholder: 'Add options', - options: [ - { - displayName: 'Icon Emoji', - name: 'icon_emoji', - type: 'string', - displayOptions: { - show: { - '/as_user': [ - false - ], - '/operation': [ - 'post' - ], - '/resource': [ - 'conversation', - ], - }, - }, - default: '', - description: 'Emoji to use as the icon for this conversation. Overrides icon_url.', - }, - { - displayName: 'Icon URL', - name: 'icon_url', - type: 'string', - displayOptions: { - show: { - '/as_user': [ - false - ], - '/operation': [ - 'post' - ], - '/resource': [ - 'conversation', - ], - }, - }, - default: '', - description: 'URL to an image to use as the icon for this conversation.', - }, - { - displayName: 'Make Reply', - name: 'thread_ts', - type: 'string', - default: '', - description: 'Provide another conversation\'s ts value to make this conversation a reply.', - }, - { - displayName: 'Unfurl Links', - name: 'unfurl_links', - type: 'boolean', - default: false, - description: 'Pass true to enable unfurling of primarily text-based content.', - }, - { - displayName: 'Unfurl Media', - name: 'unfurl_media', - type: 'boolean', - default: true, - description: 'Pass false to disable unfurling of media content.', - }, - { - displayName: 'Markdown', - name: 'mrkdwn', - type: 'boolean', - default: true, - description: 'Use Slack Markdown parsing.', - }, - { - displayName: 'Reply Broadcast', - name: 'reply_broadcast', - type: 'boolean', - default: false, - description: 'Used in conjunction with thread_ts and indicates whether reply should be made visible to everyone in the channel or conversation.', - }, - { - displayName: 'Link Names', - name: 'link_names', - type: 'boolean', - default: false, - description: 'Find and link channel names and usernames.', - }, - ], - }, -/* ----------------------------------------------------------------------- */ -/* conversation:update */ -/* ----------------------------------------------------------------------- */ - { - displayName: 'conversation ID', - name: 'conversationId', - type: 'string', - required: true, - default: '', - displayOptions: { - show: { - resource: [ - 'conversation', - ], - operation: [ - 'update', - ] - }, - }, - description: 'Id of conversation that needs to be fetched', - }, - { - displayName: 'Update Fields', - name: 'updateFields', - type: 'collection', - placeholder: 'Add Field', - default: {}, - displayOptions: { - show: { - resource: [ - 'conversation', - ], - operation: [ - 'update', - ], - }, - }, - options: [ - { - displayName: 'Activity Date', - name: 'activityDate', - type: 'dateTime', - default: '', - description: `Represents the due date of the conversation.
- This field has a timestamp that is always set to midnight
- in the Coordinated Universal Time (UTC) time zone.`, - }, - { - displayName: 'Call Disposition', - name: 'callDisposition', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: `Represents the result of a given call, for example, “we'll call back,” or “call
- unsuccessful.” Limit is 255 characters. Not subject to field-level security, available for any user
- in an organization with Salesforce CRM Call Center.`, - }, - { - displayName: 'Call Duration In Seconds', - name: 'callDurationInSeconds', - type: 'number', - default: '', - description: `Duration of the call in seconds. Not subject to field-level security,
- available for any user in an organization with Salesforce CRM Call Cente`, - }, - { - displayName: 'Call Object', - name: 'callObject', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: `Name of a call center. Limit is 255 characters.
- Not subject to field-level security, available for any user in an
- organization with Salesforce CRM Call Center.`, - }, - { - displayName: 'Call Type', - name: 'callType', - type: 'options', - default: '', - typeOptions: { - loadOptionsMethod: 'getconversationCallTypes', - }, - description: 'The type of call being answered: Inbound, Internal, or Outbound.', - }, - { - displayName: 'Description', - name: 'description', - type: 'string', - default: '', - typeOptions: { - alwaysOpenEditWindow: true, - }, - description: 'Contains a text description of the conversation.', - }, - { - displayName: 'Is ReminderSet', - name: 'isReminderSet', - type: 'boolean', - default: false, - description: 'Indicates whether a popup reminder has been set for the conversation (true) or not (false).', - }, - { - displayName: 'Owner', - name: 'owner', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getUsers', - }, - default: '', - description: 'ID of the User who owns the record.', - }, - { - displayName: 'Priority', - name: 'priority', - type: 'options', - default: '', - typeOptions: { - loadOptionsMethod: 'getconversationPriorities', - }, - description: `Indicates the importance or urgency of a conversation, such as high or low.`, - }, - { - displayName: 'Status', - name: 'status', - type: 'options', - default: '', - typeOptions: { - loadOptionsMethod: 'getconversationStatuses', - }, - description: 'The current status of the conversation, such as In Progress or Completed.', - }, - { - displayName: 'Subject', - name: 'subject', - type: 'options', - default: '', - typeOptions: { - loadOptionsMethod: 'getconversationSubjects', - }, - description: 'The subject line of the conversation, such as “Call” or “Send Quote.” Limit: 255 characters.', - }, - { - displayName: 'Recurrence Day Of Month', - name: 'recurrenceDayOfMonth', - type: 'number', - default: '', - description: 'The day of the month in which the conversation repeats.', - }, - { - displayName: 'Recurrence Day Of Week Mask', - name: 'recurrenceDayOfWeekMask', - type: 'number', - default: '', - description: `The day or days of the week on which the conversation repeats.
- This field contains a bitmask. The values are as follows: Sunday = 1 Monday = 2
- Tuesday = 4 Wednesday = 8 Thursday = 16 Friday = 32 Saturday = 64
- Multiple days are represented as the sum of their numerical values.
- For example, Tuesday and Thursday = 4 + 16 = 20.`, - }, - { - displayName: 'Recurrence End Date Only', - name: 'recurrenceEndDateOnly', - type: 'dateTime', - default: '', - description: `The last date on which the conversation repeats. This field has a timestamp that
- is always set to midnight in the Coordinated Universal Time (UTC) time zone.`, - }, - { - displayName: 'Recurrence Instance', - name: 'recurrenceInstance', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getconversationRecurrenceInstances', - }, - default: '', - description: `The frequency of the recurring conversation. For example, “2nd” or “3rd.”`, - }, - { - displayName: 'Recurrence Interval', - name: 'recurrenceInterval', - type: 'number', - default: '', - description: 'The interval between recurring conversations.', - }, - { - displayName: 'Recurrence Month Of Year', - name: 'recurrenceMonthOfYear', - type: 'options', - options: [ - { - name: 'January', - value: 'January' - }, - { - name: 'February', - value: 'February' - }, - { - name: 'March', - value: 'March' - }, - { - name: 'April', - value: 'April' - }, - { - name: 'May', - value: 'May' - }, - { - name: 'June', - value: 'June' - }, - { - name: 'July', - value: 'July' - }, - { - name: 'August', - value: 'August' - }, - { - name: 'September', - value: 'September' - }, - { - name: 'October', - value: 'October' - }, - { - name: 'November', - value: 'November' - }, - { - name: 'December', - value: 'December' - } - ], - default: '', - description: 'The month of the year in which the conversation repeats.', - }, - { - displayName: 'Recurrence Start Date Only', - name: 'recurrenceEndDateOnly', - type: 'dateTime', - default: '', - description: `The date when the recurring conversation begins.
- Must be a date and time before RecurrenceEndDateOnly.`, - }, - { - displayName: 'Recurrence Regenerated Type', - name: 'recurrenceRegeneratedType', - type: 'options', - default: '', - options: [ - { - name: 'After due date', - value: 'RecurrenceRegenerateAfterDueDate' - }, - { - name: 'After date completed', - value: 'RecurrenceRegenerateAfterToday' - }, - { - name: '(conversation Closed)', - value: 'RecurrenceRegenerated' - } - ], - description: `Represents what triggers a repeating conversation to repeat.
- Add this field to a page layout together with the RecurrenceInterval field,
- which determines the number of days between the triggering date (due date or close date)
- and the due date of the next repeating conversation in the series.Label is Repeat This conversation.`, - }, - { - displayName: 'Recurrence Type', - name: 'recurrenceType', - type: 'options', - default: '', - typeOptions: { - loadOptionsMethod: 'getconversationRecurrenceTypes' - }, - description: 'Website for the conversation.', - }, - { - displayName: 'Recurrence TimeZone SidKey', - name: 'recurrenceTimeZoneSidKey', - type: 'string', - default: '', - description: `The time zone associated with the recurring conversation.
- For example, “UTC-8:00” for Pacific Standard Time.`, - }, - { - displayName: 'Reminder Date Time', - name: 'reminderDateTime', - type: 'dateTime', - default: '', - description: `Represents the time when the reminder is scheduled to fire,
- if IsReminderSet is set to true. If IsReminderSet is set to false, then the
- user may have deselected the reminder checkbox in the Salesforce user interface,
- or the reminder has already fired at the time indicated by the value.`, - }, - { - displayName: 'What Id', - name: 'whatId', - type: 'string', - default: '', - description: `The WhatId represents nonhuman objects such as accounts, opportunities,
- campaigns, cases, or custom objects. WhatIds are polymorphic. Polymorphic means a
- WhatId is equivalent to the ID of a related object.`, - }, - { - displayName: 'Who Id', - name: 'whoId', - type: 'string', - default: '', - description: `The WhoId represents a human such as a lead or a contact.
- WhoIds are polymorphic. Polymorphic means a WhoId is equivalent to a contact’s ID or a lead’s ID.`, - }, - ] - }, - -/* -------------------------------------------------------------------------- */ -/* conversation:get */ -/* -------------------------------------------------------------------------- */ - { - displayName: 'conversation ID', - name: 'conversationId', - type: 'string', - required: true, - default: '', - displayOptions: { - show: { - resource: [ - 'conversation', - ], - operation: [ - 'get', - ] - }, - }, - description: 'Id of conversation that needs to be fetched', - }, -/* -------------------------------------------------------------------------- */ -/* conversation:delete */ -/* -------------------------------------------------------------------------- */ - { - displayName: 'conversation ID', - name: 'conversationId', - type: 'string', - required: true, - default: '', - displayOptions: { - show: { - resource: [ - 'conversation', - ], - operation: [ - 'delete', - ] - }, - }, - description: 'Id of conversation that needs to be fetched', - }, -/* -------------------------------------------------------------------------- */ -/* conversation:getAll */ -/* -------------------------------------------------------------------------- */ - { - displayName: 'Return All', - name: 'returnAll', - type: 'boolean', - displayOptions: { - show: { - resource: [ - 'conversation', - ], - operation: [ - 'getAll', - ], - }, - }, - default: false, - description: 'If all results should be returned or only up to a given limit.', - }, - { - displayName: 'Limit', - name: 'limit', - type: 'number', - displayOptions: { - show: { - resource: [ - 'conversation', - ], - operation: [ - 'getAll', - ], - returnAll: [ - false, - ], - }, - }, - typeOptions: { - minValue: 1, - maxValue: 100, - }, - default: 50, - description: 'How many results to return.', - }, - { - displayName: 'Options', - name: 'options', - type: 'collection', - placeholder: 'Add Field', - default: {}, - displayOptions: { - show: { - resource: [ - 'conversation', - ], - operation: [ - 'getAll', - ], - }, - }, - options: [ - { - displayName: 'Fields', - name: 'fields', - type: 'string', - default: '', - description: 'Fields to include separated by ,', - }, - ] - }, -] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Slack/FileDescription.ts b/packages/nodes-base/nodes/Slack/FileDescription.ts new file mode 100644 index 0000000000..fe1bbe8f8e --- /dev/null +++ b/packages/nodes-base/nodes/Slack/FileDescription.ts @@ -0,0 +1,322 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const fileOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'file', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get a file info', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get & filters team files.', + }, + { + name: 'Upload', + value: 'upload', + description: 'Create or upload an existing file.', + }, + ], + default: 'upload', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const fileFields = [ + +/* -------------------------------------------------------------------------- */ +/* file:upload */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Binary Data', + name: 'binaryData', + type: 'boolean', + default: false, + displayOptions: { + show: { + operation: [ + 'upload' + ], + resource: [ + 'file', + ], + }, + }, + description: 'If the data to upload should be taken from binary field.', + }, + { + displayName: 'File Content', + name: 'fileContent', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'upload' + ], + resource: [ + 'file', + ], + binaryData: [ + false + ], + }, + + }, + placeholder: '', + description: 'The text content of the file to upload.', + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + required: true, + displayOptions: { + show: { + operation: [ + 'upload' + ], + resource: [ + 'file', + ], + binaryData: [ + true + ], + }, + + }, + placeholder: '', + description: 'Name of the binary property which contains
the data for the file to be uploaded.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'upload' + ], + resource: [ + 'file', + ], + }, + }, + default: {}, + description: 'Other options to set', + placeholder: 'Add options', + options: [ + { + displayName: 'Channels', + name: 'channelIds', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + default: [], + description: 'The channels to send the file to.', + }, + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + default: '', + description: 'Filename of file.', + }, + { + displayName: 'Initial Comment', + name: 'initialComment', + type: 'string', + default: '', + description: 'The message text introducing the file in specified channels.', + }, + { + displayName: 'Thread TS', + name: 'threadTs', + type: 'string', + default: '', + description: `Provide another message's ts value to upload this file as a reply. Never use a reply's ts value; use its parent instead.`, + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title of file.', + }, + ], + }, +/* ----------------------------------------------------------------------- */ +/* file:getAll */ +/* ----------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'file', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'file', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'getAll' + ], + resource: [ + 'file', + ], + }, + }, + default: {}, + placeholder: 'Add Field', + options: [ + { + displayName: 'Channel', + name: 'channelId', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + description: 'Channel containing the file to be listed.', + }, + { + displayName: 'Show Files Hidden By Limit', + name: 'showFilesHidden', + type: 'boolean', + default: false, + description: 'Show truncated file info for files hidden due to being too old, and the team who owns the file being over the file limit.', + }, + { + displayName: 'TS From', + name: 'tsFrom', + type: 'string', + default: '', + description: 'Filter files created after this timestamp (inclusive).', + }, + { + displayName: 'TS To', + name: 'tsTo', + type: 'string', + default: '', + description: 'Filter files created before this timestamp (inclusive).', + }, + { + displayName: 'Types', + name: 'types', + type: 'multiOptions', + options: [ + { + name: 'All', + value: 'all', + }, + { + name: 'Spaces', + value: 'spaces', + }, + { + name: 'Snippets', + value: 'snippets', + }, + { + name: 'Images', + value: 'images', + }, + { + name: 'Google Docs', + value: 'gdocs', + }, + { + name: 'Zips', + value: 'zips', + }, + { + name: 'pdfs', + value: 'pdfs', + }, + ], + default: ['all'], + description: 'Filter files by type', + }, + { + displayName: 'User', + name: 'userId', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + description: 'Filter files created by a single user.', + }, + ], + }, +/* ----------------------------------------------------------------------- */ +/* file:get */ +/* ----------------------------------------------------------------------- */ + { + displayName: 'File ID', + name: 'fileId', + type: 'string', + displayOptions: { + show: { + resource: [ + 'file', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Slack/GenericFunctions.ts b/packages/nodes-base/nodes/Slack/GenericFunctions.ts index 57b5b00c11..b73de1cac5 100644 --- a/packages/nodes-base/nodes/Slack/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Slack/GenericFunctions.ts @@ -12,11 +12,11 @@ import { } from 'n8n-workflow'; import * as _ from 'lodash'; -export async function slackApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: object = {}, query: object = {}): Promise { // tslint:disable-line:no-any +export async function slackApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: object = {}, query: object = {}, headers: {} | undefined = undefined, option: {} = {}): Promise { // tslint:disable-line:no-any const authenticationMethod = this.getNodeParameter('authentication', 0, 'accessToken') as string; - const options: OptionsWithUri = { + let options: OptionsWithUri = { method, - headers: { + headers: headers || { 'Content-Type': 'application/json; charset=utf-8' }, body, @@ -24,6 +24,7 @@ export async function slackApiRequest(this: IExecuteFunctions | IExecuteSingleFu uri: `https://slack.com/api${resource}`, json: true }; + options = Object.assign({}, options, option); if (Object.keys(body).length === 0) { delete options.body; } @@ -62,15 +63,23 @@ export async function slackApiRequest(this: IExecuteFunctions | IExecuteSingleFu export async function salckApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any const returnData: IDataObject[] = []; let responseData; + query.page = 1; + query.count = 100; do { responseData = await slackApiRequest.call(this, method, endpoint, body, query); query.cursor = encodeURIComponent(_.get(responseData, 'response_metadata.next_cursor')); + query.page++; returnData.push.apply(returnData, responseData[propertyName]); } while ( - responseData.response_metadata !== undefined && + (responseData.response_metadata !== undefined && responseData.response_metadata.mext_cursor !== undefined && responseData.response_metadata.next_cursor !== "" && - responseData.response_metadata.next_cursor !== null + responseData.response_metadata.next_cursor !== null) || + (responseData.paging !== undefined && + responseData.paging.pages !== undefined && + responseData.paging.page !== undefined && + responseData.paging.page < responseData.paging.pages + ) ); return returnData; diff --git a/packages/nodes-base/nodes/Slack/MessageDescription.ts b/packages/nodes-base/nodes/Slack/MessageDescription.ts index 7f9dff3ac3..8c582503e5 100644 --- a/packages/nodes-base/nodes/Slack/MessageDescription.ts +++ b/packages/nodes-base/nodes/Slack/MessageDescription.ts @@ -18,6 +18,11 @@ export const messageOperations = [ value: 'post', description: 'Post a message into a channel', }, + { + name: 'Update', + value: 'update', + description: 'Updates a message.', + }, ], default: 'post', description: 'The operation to perform.', @@ -406,8 +411,8 @@ export const messageFields = [ /* message:update */ /* ----------------------------------------------------------------------- */ { - displayName: 'message ID', - name: 'messageId', + displayName: 'Channel ID', + name: 'channelId', type: 'string', required: true, default: '', @@ -421,7 +426,60 @@ export const messageFields = [ ] }, }, - description: 'Id of message that needs to be fetched', + description: 'Channel containing the message to be updated.', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'update', + ] + }, + }, + description: `New text for the message, using the default formatting rules. It's not required when presenting attachments.`, + }, + { + displayName: 'TS', + name: 'ts', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'update', + ] + }, + }, + description: `Timestamp of the message to be updated.`, + }, + { + displayName: 'As User', + name: 'as_user', + type: 'boolean', + default: false, + displayOptions: { + show: { + operation: [ + 'update' + ], + resource: [ + 'message', + ], + }, + }, + description: 'Pass true to update the message as the authed user. Bot users in this context are considered authed users.', }, { displayName: 'Update Fields', @@ -441,403 +499,228 @@ export const messageFields = [ }, options: [ { - displayName: 'Activity Date', - name: 'activityDate', - type: 'dateTime', - default: '', - description: `Represents the due date of the message.
- This field has a timestamp that is always set to midnight
- in the Coordinated Universal Time (UTC) time zone.`, - }, - { - displayName: 'Call Disposition', - name: 'callDisposition', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: `Represents the result of a given call, for example, “we'll call back,” or “call
- unsuccessful.” Limit is 255 characters. Not subject to field-level security, available for any user
- in an organization with Salesforce CRM Call Center.`, - }, - { - displayName: 'Call Duration In Seconds', - name: 'callDurationInSeconds', - type: 'number', - default: '', - description: `Duration of the call in seconds. Not subject to field-level security,
- available for any user in an organization with Salesforce CRM Call Cente`, - }, - { - displayName: 'Call Object', - name: 'callObject', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: `Name of a call center. Limit is 255 characters.
- Not subject to field-level security, available for any user in an
- organization with Salesforce CRM Call Center.`, - }, - { - displayName: 'Call Type', - name: 'callType', - type: 'options', - default: '', - typeOptions: { - loadOptionsMethod: 'getmessageCallTypes', - }, - description: 'The type of call being answered: Inbound, Internal, or Outbound.', - }, - { - displayName: 'Description', - name: 'description', - type: 'string', - default: '', - typeOptions: { - alwaysOpenEditWindow: true, - }, - description: 'Contains a text description of the message.', - }, - { - displayName: 'Is ReminderSet', - name: 'isReminderSet', + displayName: 'Link Names', + name: 'link_names', type: 'boolean', default: false, - description: 'Indicates whether a popup reminder has been set for the message (true) or not (false).', + description: 'Find and link channel names and usernames.', }, { - displayName: 'Owner', - name: 'owner', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getUsers', - }, - default: '', - description: 'ID of the User who owns the record.', - }, - { - displayName: 'Priority', - name: 'priority', - type: 'options', - default: '', - typeOptions: { - loadOptionsMethod: 'getmessagePriorities', - }, - description: `Indicates the importance or urgency of a message, such as high or low.`, - }, - { - displayName: 'Status', - name: 'status', - type: 'options', - default: '', - typeOptions: { - loadOptionsMethod: 'getmessageStatuses', - }, - description: 'The current status of the message, such as In Progress or Completed.', - }, - { - displayName: 'Subject', - name: 'subject', - type: 'options', - default: '', - typeOptions: { - loadOptionsMethod: 'getmessageSubjects', - }, - description: 'The subject line of the message, such as “Call” or “Send Quote.” Limit: 255 characters.', - }, - { - displayName: 'Recurrence Day Of Month', - name: 'recurrenceDayOfMonth', - type: 'number', - default: '', - description: 'The day of the month in which the message repeats.', - }, - { - displayName: 'Recurrence Day Of Week Mask', - name: 'recurrenceDayOfWeekMask', - type: 'number', - default: '', - description: `The day or days of the week on which the message repeats.
- This field contains a bitmask. The values are as follows: Sunday = 1 Monday = 2
- Tuesday = 4 Wednesday = 8 Thursday = 16 Friday = 32 Saturday = 64
- Multiple days are represented as the sum of their numerical values.
- For example, Tuesday and Thursday = 4 + 16 = 20.`, - }, - { - displayName: 'Recurrence End Date Only', - name: 'recurrenceEndDateOnly', - type: 'dateTime', - default: '', - description: `The last date on which the message repeats. This field has a timestamp that
- is always set to midnight in the Coordinated Universal Time (UTC) time zone.`, - }, - { - displayName: 'Recurrence Instance', - name: 'recurrenceInstance', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getmessageRecurrenceInstances', - }, - default: '', - description: `The frequency of the recurring message. For example, “2nd” or “3rd.”`, - }, - { - displayName: 'Recurrence Interval', - name: 'recurrenceInterval', - type: 'number', - default: '', - description: 'The interval between recurring messages.', - }, - { - displayName: 'Recurrence Month Of Year', - name: 'recurrenceMonthOfYear', + displayName: 'Parse', + name: 'parse', type: 'options', options: [ { - name: 'January', - value: 'January' + name: 'Client', + value: 'client', }, { - name: 'February', - value: 'February' + name: 'Full', + value: 'full', }, { - name: 'March', - value: 'March' + name: 'None', + value: 'none', }, - { - name: 'April', - value: 'April' - }, - { - name: 'May', - value: 'May' - }, - { - name: 'June', - value: 'June' - }, - { - name: 'July', - value: 'July' - }, - { - name: 'August', - value: 'August' - }, - { - name: 'September', - value: 'September' - }, - { - name: 'October', - value: 'October' - }, - { - name: 'November', - value: 'November' - }, - { - name: 'December', - value: 'December' - } ], - default: '', - description: 'The month of the year in which the message repeats.', + default: 'client', + description: 'Change how messages are treated', }, - { - displayName: 'Recurrence Start Date Only', - name: 'recurrenceEndDateOnly', - type: 'dateTime', - default: '', - description: `The date when the recurring message begins.
- Must be a date and time before RecurrenceEndDateOnly.`, - }, - { - displayName: 'Recurrence Regenerated Type', - name: 'recurrenceRegeneratedType', - type: 'options', - default: '', - options: [ - { - name: 'After due date', - value: 'RecurrenceRegenerateAfterDueDate' - }, - { - name: 'After date completed', - value: 'RecurrenceRegenerateAfterToday' - }, - { - name: '(message Closed)', - value: 'RecurrenceRegenerated' - } - ], - description: `Represents what triggers a repeating message to repeat.
- Add this field to a page layout together with the RecurrenceInterval field,
- which determines the number of days between the triggering date (due date or close date)
- and the due date of the next repeating message in the series.Label is Repeat This message.`, - }, - { - displayName: 'Recurrence Type', - name: 'recurrenceType', - type: 'options', - default: '', - typeOptions: { - loadOptionsMethod: 'getmessageRecurrenceTypes' - }, - description: 'Website for the message.', - }, - { - displayName: 'Recurrence TimeZone SidKey', - name: 'recurrenceTimeZoneSidKey', - type: 'string', - default: '', - description: `The time zone associated with the recurring message.
- For example, “UTC-8:00” for Pacific Standard Time.`, - }, - { - displayName: 'Reminder Date Time', - name: 'reminderDateTime', - type: 'dateTime', - default: '', - description: `Represents the time when the reminder is scheduled to fire,
- if IsReminderSet is set to true. If IsReminderSet is set to false, then the
- user may have deselected the reminder checkbox in the Salesforce user interface,
- or the reminder has already fired at the time indicated by the value.`, - }, - { - displayName: 'What Id', - name: 'whatId', - type: 'string', - default: '', - description: `The WhatId represents nonhuman objects such as accounts, opportunities,
- campaigns, cases, or custom objects. WhatIds are polymorphic. Polymorphic means a
- WhatId is equivalent to the ID of a related object.`, - }, - { - displayName: 'Who Id', - name: 'whoId', - type: 'string', - default: '', - description: `The WhoId represents a human such as a lead or a contact.
- WhoIds are polymorphic. Polymorphic means a WhoId is equivalent to a contact’s ID or a lead’s ID.`, - }, - ] - }, - -/* -------------------------------------------------------------------------- */ -/* message:get */ -/* -------------------------------------------------------------------------- */ - { - displayName: 'message ID', - name: 'messageId', - type: 'string', - required: true, - default: '', - displayOptions: { - show: { - resource: [ - 'message', - ], - operation: [ - 'get', - ] - }, - }, - description: 'Id of message that needs to be fetched', - }, -/* -------------------------------------------------------------------------- */ -/* message:delete */ -/* -------------------------------------------------------------------------- */ - { - displayName: 'message ID', - name: 'messageId', - type: 'string', - required: true, - default: '', - displayOptions: { - show: { - resource: [ - 'message', - ], - operation: [ - 'delete', - ] - }, - }, - description: 'Id of message that needs to be fetched', - }, -/* -------------------------------------------------------------------------- */ -/* message:getAll */ -/* -------------------------------------------------------------------------- */ - { - displayName: 'Return All', - name: 'returnAll', - type: 'boolean', - displayOptions: { - show: { - resource: [ - 'message', - ], - operation: [ - 'getAll', - ], - }, - }, - default: false, - description: 'If all results should be returned or only up to a given limit.', + ], }, { - displayName: 'Limit', - name: 'limit', - type: 'number', - displayOptions: { - show: { - resource: [ - 'message', - ], - operation: [ - 'getAll', - ], - returnAll: [ - false, - ], - }, - }, - typeOptions: { - minValue: 1, - maxValue: 100, - }, - default: 50, - description: 'How many results to return.', - }, - { - displayName: 'Options', - name: 'options', + displayName: 'Attachments', + name: 'attachments', type: 'collection', - placeholder: 'Add Field', - default: {}, + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add attachment', + }, displayOptions: { show: { + operation: [ + 'update' + ], resource: [ 'message', ], - operation: [ - 'getAll', - ], }, }, + default: {}, // TODO: Remove comment: has to make default array for the main property, check where that happens in UI + description: 'The attachment to add', + placeholder: 'Add attachment item', options: [ + { + displayName: 'Fallback Text', + name: 'fallback', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Required plain-text summary of the attachment.', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Text to send.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Title of the message.', + }, + { + displayName: 'Title Link', + name: 'title_link', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Link of the title.', + }, + { + displayName: 'Color', + name: 'color', + type: 'color', + default: '#ff0000', + description: 'Color of the line left of text.', + }, + { + displayName: 'Pretext', + name: 'pretext', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Text which appears before the message block.', + }, + { + displayName: 'Author Name', + name: 'author_name', + type: 'string', + default: '', + description: 'Name that should appear.', + }, + { + displayName: 'Author Link', + name: 'author_link', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Link for the author.', + }, + { + displayName: 'Author Icon', + name: 'author_icon', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Icon which should appear for the user.', + }, + { + displayName: 'Image URL', + name: 'image_url', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'URL of image.', + }, + { + displayName: 'Thumbnail URL', + name: 'thumb_url', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'URL of thumbnail.', + }, + { + displayName: 'Footer', + name: 'footer', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Text of footer to add.', + }, + { + displayName: 'Footer Icon', + name: 'footer_icon', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Icon which should appear next to footer.', + }, + { + displayName: 'Timestamp', + name: 'ts', + type: 'dateTime', + default: '', + description: 'Time message relates to.', + }, { displayName: 'Fields', name: 'fields', - type: 'string', - default: '', - description: 'Fields to include separated by ,', - }, - ] + placeholder: 'Add Fields', + description: 'Fields to add to message.', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'item', + displayName: 'Item', + values: [ + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title of the item.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the item.', + }, + { + displayName: 'Short', + name: 'short', + type: 'boolean', + default: true, + description: 'If items can be displayed next to each other.', + }, + ] + }, + ], + } + ], }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Slack/Slack.node.ts b/packages/nodes-base/nodes/Slack/Slack.node.ts index c3b074c1e1..1afeb4ebd0 100644 --- a/packages/nodes-base/nodes/Slack/Slack.node.ts +++ b/packages/nodes-base/nodes/Slack/Slack.node.ts @@ -1,5 +1,6 @@ import { IExecuteFunctions, + BINARY_ENCODING, } from 'n8n-core'; import { IDataObject, @@ -18,9 +19,13 @@ import { messageFields, } from './MessageDescription'; import { - conversationOperations, - conversationFields, -} from './ConversationDescription'; + starOperations, + starFields, +} from './StarDescription'; +import { + fileOperations, + fileFields, +} from './FileDescription'; import { slackApiRequest, salckApiRequestAllItems, @@ -37,7 +42,7 @@ export class Slack implements INodeType { group: ['output'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Sends data to Slack', + description: 'Consume Slack API', defaults: { name: 'Slack', color: '#BB2244', @@ -66,7 +71,7 @@ export class Slack implements INodeType { ], }, }, - } + }, ], properties: [ { @@ -95,10 +100,18 @@ export class Slack implements INodeType { name: 'Channel', value: 'channel', }, + { + name: 'File', + value: 'file', + }, { name: 'Message', value: 'message', }, + { + name: 'Star', + value: 'star', + }, ], default: 'message', description: 'The resource to operate on.', @@ -107,6 +120,10 @@ export class Slack implements INodeType { ...channelFields, ...messageOperations, ...messageFields, + ...starOperations, + ...starFields, + ...fileOperations, + ...fileFields, ], }; @@ -125,7 +142,21 @@ export class Slack implements INodeType { value: userId, }); } - console.log(users) + return returnData; + }, + // Get all the users to display them to user so that he can + // select them easily + async getChannels(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const channels = await salckApiRequestAllItems.call(this, 'channels', 'GET', '/conversations.list'); + for (const channel of channels) { + const channelName = channel.name; + const channelId = channel.id; + returnData.push({ + name: channelName, + value: channelId, + }); + } return returnData; }, } @@ -142,25 +173,208 @@ export class Slack implements INodeType { const resource = this.getNodeParameter('resource', 0) as string; const operation = this.getNodeParameter('operation', 0) as string; if (resource === 'channel') { + //https://api.slack.com/methods/conversations.archive + if (operation === 'archive') { + const channel = this.getNodeParameter('channelId', i) as string; + const body: IDataObject = { + channel, + }; + responseData = await slackApiRequest.call(this, 'POST', '/conversations.archive', body, qs); + } + //https://api.slack.com/methods/conversations.close + if (operation === 'close') { + const channel = this.getNodeParameter('channelId', i) as string; + const body: IDataObject = { + channel, + }; + responseData = await slackApiRequest.call(this, 'POST', '/conversations.close', body, qs); + } //https://api.slack.com/methods/conversations.create if (operation === 'create') { - const channel = this.getNodeParameter('channel', i) as string; + const channel = this.getNodeParameter('channelId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; const body: IDataObject = { name: channel, }; - responseData = await slackApiRequest.call(this, 'POST', '/channels.create', body, qs); + if (additionalFields.isPrivate) { + body.is_private = additionalFields.isPrivate as boolean; + } + if (additionalFields.users) { + body.user_ids = (additionalFields.users as string[]).join(','); + } + responseData = await slackApiRequest.call(this, 'POST', '/conversations.create', body, qs); } - if (operation === 'invite') { - const channel = this.getNodeParameter('channel', i) as string; - const user = this.getNodeParameter('username', i) as string; + //https://api.slack.com/methods/conversations.kick + if (operation === 'kick') { + const channel = this.getNodeParameter('channelId', i) as string; + const userId = this.getNodeParameter('userId', i) as string; + const body: IDataObject = { + name: channel, + user: userId, + }; + responseData = await slackApiRequest.call(this, 'POST', '/conversations.kick', body, qs); + } + //https://api.slack.com/methods/conversations.join + if (operation === 'join') { + const channel = this.getNodeParameter('channelId', i) as string; const body: IDataObject = { channel, - user, }; - responseData = await slackApiRequest.call(this, 'POST', '/channels.invite', body, qs); + responseData = await slackApiRequest.call(this, 'POST', '/conversations.join', body, qs); + responseData = responseData.channel; + } + //https://api.slack.com/methods/conversations.info + if (operation === 'get') { + const channel = this.getNodeParameter('channelId', i) as string; + qs.channel = channel, + responseData = await slackApiRequest.call(this, 'POST', '/conversations.info', {}, qs); + responseData = responseData.channel; + } + //https://api.slack.com/methods/conversations.list + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + if (filters.types) { + qs.types = (filters.types as string[]).join(','); + } + if (filters.excludeArchived) { + qs.exclude_archived = filters.excludeArchived as boolean; + } + if (returnAll === true) { + responseData = await salckApiRequestAllItems.call(this, 'channels', 'GET', '/conversations.list', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await slackApiRequest.call(this, 'GET', '/conversations.list', {}, qs); + responseData = responseData.channels; + } + } + //https://api.slack.com/methods/conversations.history + if (operation === 'history') { + const channel = this.getNodeParameter('channelId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + qs.channel = channel; + if (filters.inclusive) { + qs.inclusive = filters.inclusive as boolean; + } + if (filters.latest) { + qs.latest = filters.latest as string; + } + if (filters.oldest) { + qs.oldest = filters.oldest as string; + } + if (returnAll === true) { + responseData = await salckApiRequestAllItems.call(this, 'messages', 'GET', '/conversations.history', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await slackApiRequest.call(this, 'GET', '/conversations.history', {}, qs); + responseData = responseData.messages; + } + } + //https://api.slack.com/methods/conversations.invite + if (operation === 'invite') { + const channel = this.getNodeParameter('channelId', i) as string; + const userId = this.getNodeParameter('userId', i) as string; + const body: IDataObject = { + channel, + user: userId, + }; + responseData = await slackApiRequest.call(this, 'POST', '/conversations.invite', body, qs); + responseData = responseData.channel; + } + //https://api.slack.com/methods/conversations.leave + if (operation === 'leave') { + const channel = this.getNodeParameter('channelId', i) as string; + const body: IDataObject = { + channel, + }; + responseData = await slackApiRequest.call(this, 'POST', '/conversations.leave', body, qs); + } + //https://api.slack.com/methods/conversations.open + if (operation === 'open') { + const options = this.getNodeParameter('options', i) as IDataObject; + const body: IDataObject = {}; + if (options.channelId) { + body.channel = options.channelId as string; + } + if (options.returnIm) { + body.return_im = options.returnIm as boolean; + } + if (options.users) { + body.users = (options.users as string[]).join(','); + } + responseData = await slackApiRequest.call(this, 'POST', '/conversations.open', body, qs); + responseData = responseData.channel; + } + //https://api.slack.com/methods/conversations.rename + if (operation === 'rename') { + const channel = this.getNodeParameter('channelId', i) as IDataObject; + const name = this.getNodeParameter('name', i) as IDataObject; + const body: IDataObject = { + channel, + name, + }; + responseData = await slackApiRequest.call(this, 'POST', '/conversations.rename', body, qs); + responseData = responseData.channel; + } + //https://api.slack.com/methods/conversations.replies + if (operation === 'replies') { + const channel = this.getNodeParameter('channelId', i) as string; + const ts = this.getNodeParameter('ts', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + qs.channel = channel; + qs.ts = ts; + if (filters.inclusive) { + qs.inclusive = filters.inclusive as boolean; + } + if (filters.latest) { + qs.latest = filters.latest as string; + } + if (filters.oldest) { + qs.oldest = filters.oldest as string; + } + if (returnAll === true) { + responseData = await salckApiRequestAllItems.call(this, 'messages', 'GET', '/conversations.replies', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await slackApiRequest.call(this, 'GET', '/conversations.replies', {}, qs); + responseData = responseData.messages; + } + } + //https://api.slack.com/methods/conversations.setPurpose + if (operation === 'setPurpose') { + const channel = this.getNodeParameter('channelId', i) as IDataObject; + const purpose = this.getNodeParameter('purpose', i) as IDataObject; + const body: IDataObject = { + channel, + purpose, + }; + responseData = await slackApiRequest.call(this, 'POST', '/conversations.setPurpose', body, qs); + responseData = responseData.channel; + } + //https://api.slack.com/methods/conversations.setTopic + if (operation === 'setTopic') { + const channel = this.getNodeParameter('channelId', i) as IDataObject; + const topic = this.getNodeParameter('topic', i) as IDataObject; + const body: IDataObject = { + channel, + topic, + }; + responseData = await slackApiRequest.call(this, 'POST', '/conversations.setTopic', body, qs); + responseData = responseData.channel; + } + //https://api.slack.com/methods/conversations.unarchive + if (operation === 'unarchive') { + const channel = this.getNodeParameter('channelId', i) as string; + const body: IDataObject = { + channel, + }; + responseData = await slackApiRequest.call(this, 'POST', '/conversations.unarchive', body, qs); } } if (resource === 'message') { + //https://api.slack.com/methods/chat.postMessage if (operation === 'post') { const channel = this.getNodeParameter('channel', i) as string; const text = this.getNodeParameter('text', i) as string; @@ -195,6 +409,174 @@ export class Slack implements INodeType { Object.assign(body, otherOptions); responseData = await slackApiRequest.call(this, 'POST', '/chat.postMessage', body, qs); } + //https://api.slack.com/methods/chat.update + if (operation === 'update') { + const channel = this.getNodeParameter('channel', i) as string; + const text = this.getNodeParameter('text', i) as string; + const ts = this.getNodeParameter('ts', i) as string; + const as_user = this.getNodeParameter('as_user', i) as boolean; + const attachments = this.getNodeParameter('attachments', i, []) as unknown as IAttachment[]; + const body: IDataObject = { + channel, + text, + ts, + as_user, + }; + // The node does save the fields data differently than the API + // expects so fix the data befre we send the request + for (const attachment of attachments) { + if (attachment.fields !== undefined) { + if (attachment.fields.item !== undefined) { + // Move the field-content up + // @ts-ignore + attachment.fields = attachment.fields.item; + } else { + // If it does not have any items set remove it + delete attachment.fields; + } + } + } + body['attachments'] = attachments; + + // Add all the other options to the request + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + Object.assign(body, updateFields); + responseData = await slackApiRequest.call(this, 'POST', '/chat.update', body, qs); + } + } + if (resource === 'star') { + //https://api.slack.com/methods/stars.add + if (operation === 'add') { + const options = this.getNodeParameter('options', i) as IDataObject; + const body: IDataObject = {}; + if (options.channelId) { + body.channel = options.channelId as string; + } + if (options.fileId) { + body.file = options.fileId as string; + } + if (options.fileComment) { + body.file_comment = options.fileComment as string; + } + if (options.timestamp) { + body.timestamp = options.timestamp as string; + } + responseData = await slackApiRequest.call(this, 'POST', '/stars.add', body, qs); + } + //https://api.slack.com/methods/stars.remove + if (operation === 'delete') { + const options = this.getNodeParameter('options', i) as IDataObject; + const body: IDataObject = {}; + if (options.channelId) { + body.channel = options.channelId as string; + } + if (options.fileId) { + body.file = options.fileId as string; + } + if (options.fileComment) { + body.file_comment = options.fileComment as string; + } + if (options.timestamp) { + body.timestamp = options.timestamp as string; + } + responseData = await slackApiRequest.call(this, 'POST', '/stars.remove', body, qs); + } + //https://api.slack.com/methods/stars.list + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + if (returnAll === true) { + responseData = await salckApiRequestAllItems.call(this, 'items', 'GET', '/stars.list', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await slackApiRequest.call(this, 'GET', '/stars.list', {}, qs); + responseData = responseData.items; + } + } + } + if (resource === 'file') { + //https://api.slack.com/methods/files.upload + if (operation === 'upload') { + const options = this.getNodeParameter('options', i) as IDataObject; + const binaryData = this.getNodeParameter('binaryData', i) as boolean; + const body: IDataObject = {}; + if (options.channelIds) { + body.channels = (options.channelIds as string[]).join(','); + } + if (options.fileName) { + body.filename = options.fileName as string; + } + if (options.initialComment) { + body.initial_comment = options.initialComment as string; + } + if (options.threadTs) { + body.thread_ts = options.threadTs as string; + } + if (options.title) { + body.title = options.title as string; + } + if (binaryData) { + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string; + if (items[i].binary === undefined + //@ts-ignore + || items[i].binary[binaryPropertyName] === undefined) { + throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`); + } + body.file = { + //@ts-ignore + value: Buffer.from(items[i].binary[binaryPropertyName].data, BINARY_ENCODING), + options: { + //@ts-ignore + filename: items[i].binary[binaryPropertyName].fileName, + //@ts-ignore + contentType: items[i].binary[binaryPropertyName].mimeType, + } + } + responseData = await slackApiRequest.call(this, 'POST', '/files.upload', {}, qs, { 'Content-Type': 'multipart/form-data' }, { formData: body }); + responseData = responseData.file; + } else { + const fileContent = this.getNodeParameter('fileContent', i) as string; + body.content = fileContent; + responseData = await slackApiRequest.call(this, 'POST', '/files.upload', body, qs, { 'Content-Type': 'application/x-www-form-urlencoded' }, { form: body }); + responseData = responseData.file; + } + } + //https://api.slack.com/methods/files.list + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + if (filters.channelId) { + qs.channel = filters.channelId as string; + } + if (filters.showFilesHidden) { + qs.show_files_hidden_by_limit = filters.showFilesHidden as boolean; + } + if (filters.tsFrom) { + qs.ts_from = filters.tsFrom as string; + } + if (filters.tsTo) { + qs.ts_to = filters.tsTo as string; + } + if (filters.types) { + qs.types = (filters.types as string[]).join(',') as string; + } + if (filters.userId) { + qs.user = filters.userId as string; + } + if (returnAll === true) { + responseData = await salckApiRequestAllItems.call(this, 'files', 'GET', '/files.list', {}, qs); + } else { + qs.count = this.getNodeParameter('limit', i) as number; + responseData = await slackApiRequest.call(this, 'GET', '/files.list', {}, qs); + responseData = responseData.files; + } + } + //https://api.slack.com/methods/files.info + if (operation === 'get') { + const fileId = this.getNodeParameter('fileId', i) as string; + qs.file = fileId; + responseData = await slackApiRequest.call(this, 'GET', '/files.info', {}, qs); + responseData = responseData.file; + } } if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData as IDataObject[]); diff --git a/packages/nodes-base/nodes/Slack/StarDescription.ts b/packages/nodes-base/nodes/Slack/StarDescription.ts new file mode 100644 index 0000000000..39174b6a87 --- /dev/null +++ b/packages/nodes-base/nodes/Slack/StarDescription.ts @@ -0,0 +1,185 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const starOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'star', + ], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Add a star to an item.', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a star from an item.', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all stars for a user.', + }, + ], + default: 'add', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const starFields = [ + +/* -------------------------------------------------------------------------- */ +/* star:add */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Options', + name: 'options', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'add' + ], + resource: [ + 'star', + ], + }, + }, + default: {}, + description: 'Options to set', + placeholder: 'Add options', + options: [ + { + displayName: 'Channel ID', + name: 'channelId', + type: 'string', + default: '', + description: 'Channel to add star to, or channel where the message to add star to was posted (used with timestamp).', + }, + { + displayName: 'File ID', + name: 'fileId', + type: 'string', + default: '', + description: 'File to add star to.', + }, + { + displayName: 'File Comment', + name: 'fileComment', + type: 'string', + default: '', + description: 'File comment to add star to.', + }, + { + displayName: 'Timestamp', + name: 'timestamp', + type: 'string', + default: '', + description: 'Timestamp of the message to add star to.', + }, + ], + }, +/* ----------------------------------------------------------------------- */ +/* star:delete */ +/* ----------------------------------------------------------------------- */ + { + displayName: 'Options', + name: 'options', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'delete' + ], + resource: [ + 'star', + ], + }, + }, + default: {}, + description: 'Options to set', + placeholder: 'Add options', + options: [ + { + displayName: 'Channel ID', + name: 'channelId', + type: 'string', + default: '', + description: 'Channel to add star to, or channel where the message to add star to was posted (used with timestamp).', + }, + { + displayName: 'File ID', + name: 'fileId', + type: 'string', + default: '', + description: 'File to add star to.', + }, + { + displayName: 'File Comment', + name: 'fileComment', + type: 'string', + default: '', + description: 'File comment to add star to.', + }, + { + displayName: 'Timestamp', + name: 'timestamp', + type: 'string', + default: '', + description: 'Timestamp of the message to add star to.', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* star:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'star', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'star', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, +] as INodeProperties[]; From eb1640c67ecfc7e4ae12af952ed4d7d634371213 Mon Sep 17 00:00:00 2001 From: ricardo Date: Fri, 13 Mar 2020 21:22:36 -0400 Subject: [PATCH 019/165] :sparkles: Google Calendar Integration done :sparkles: Google Calendar Integration --- packages/cli/src/Server.ts | 4 +- .../GoogleOAuth2Api.credentials.ts | 49 + .../nodes/Google/EventDescription.ts | 1106 +++++++++++++++++ .../nodes-base/nodes/Google/EventInterface.ts | 26 + .../nodes/Google/GenericFunctions.ts | 57 + .../nodes/Google/GoogleCalendar.node.ts | 412 ++++++ .../nodes/Google/googleCalendar.png | Bin 0 -> 6937 bytes packages/nodes-base/package.json | 12 +- 8 files changed, 1661 insertions(+), 5 deletions(-) create mode 100644 packages/nodes-base/credentials/GoogleOAuth2Api.credentials.ts create mode 100644 packages/nodes-base/nodes/Google/EventDescription.ts create mode 100644 packages/nodes-base/nodes/Google/EventInterface.ts create mode 100644 packages/nodes-base/nodes/Google/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Google/GoogleCalendar.node.ts create mode 100644 packages/nodes-base/nodes/Google/googleCalendar.png diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 4fe1ef70d7..5a1c0703bf 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -203,7 +203,9 @@ class App { }); } - jwt.verify(token, getKey, {}, (err: Error, decoded: string) => { + jwt.verify(token, getKey, {}, (err: Error, + //decoded: string + ) => { if (err) return ResponseHelper.jwtAuthAuthorizationError(res, "Invalid token"); next(); diff --git a/packages/nodes-base/credentials/GoogleOAuth2Api.credentials.ts b/packages/nodes-base/credentials/GoogleOAuth2Api.credentials.ts new file mode 100644 index 0000000000..facd5e8fac --- /dev/null +++ b/packages/nodes-base/credentials/GoogleOAuth2Api.credentials.ts @@ -0,0 +1,49 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +const scopes = [ + 'https://www.googleapis.com/auth/calendar', + 'https://www.googleapis.com/auth/calendar.events', +]; + +export class GoogleOAuth2Api implements ICredentialType { + name = 'googleOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Google OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://accounts.google.com/o/oauth2/v2/auth', + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://oauth2.googleapis.com/token', + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: scopes.join(' '), + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: 'access_type=offline', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'body', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Google/EventDescription.ts b/packages/nodes-base/nodes/Google/EventDescription.ts new file mode 100644 index 0000000000..fb1ea6a90f --- /dev/null +++ b/packages/nodes-base/nodes/Google/EventDescription.ts @@ -0,0 +1,1106 @@ +import { INodeProperties } from "n8n-workflow"; + +export const eventOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'event', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Add a event to calendar', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an event', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve an event', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all events from a calendar', + }, + { + name: 'Update', + value: 'update', + description: 'Update an event', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const eventFields = [ + +/* -------------------------------------------------------------------------- */ +/* event:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Calendar', + name: 'calendar', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCalendars', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + }, + { + displayName: 'Start', + name: 'start', + type: 'dateTime', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + description: 'Start time of the event.', + }, + { + displayName: 'End', + name: 'end', + type: 'dateTime', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + description: 'End time of the event.', + }, + { + displayName: 'Use Default Reminders', + name: 'useDefaultReminders', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'event', + ], + }, + }, + default: true, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'event', + ], + }, + }, + options: [ + { + displayName: 'All Day', + name: 'allday', + type: 'boolean', + options: [ + { + name: 'Yes', + value: 'yes', + }, + { + name: 'No', + value: 'no', + }, + ], + default: 'no', + description: 'Wheater the event is all day or not', + }, + { + displayName: 'Attendees', + name: 'attendees', + type: 'string', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Attendee', + }, + default: '', + description: 'The attendees of the event', + }, + { + displayName: 'Color', + name: 'color', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getColors', + }, + default: '', + description: 'The color of the event.', + }, + { + displayName: 'Guests Can Invite Others', + name: 'guestsCanInviteOthers', + type: 'boolean', + default: true, + description: 'Whether attendees other than the organizer can invite others to the event', + }, + { + displayName: 'Guests Can Modify', + name: 'guestsCanModify', + type: 'boolean', + default: false, + description: 'Whether attendees other than the organizer can modify the event', + }, + { + displayName: 'Guests Can See Other Guests', + name: 'guestsCanSeeOtherGuests', + type: 'boolean', + default: true, + description: `Whether attendees other than the organizer can see who the event's attendees are.`, + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + default: '', + description: 'Opaque identifier of the event', + }, + { + displayName: 'Location', + name: 'location', + type: 'string', + default: '', + description: 'Geographic location of the event as free-form text.', + }, + { + displayName: 'Max Attendees', + name: 'maxAttendees', + type: 'number', + default: 0, + description: `The maximum number of attendees to include in the response.
+ If there are more than the specified number of attendees, only the participant is returned`, + }, + { + displayName: 'Repeat Frecuency', + name: 'repeatFrecuency', + type: 'options', + options: [ + { + name: 'Daily', + value: 'Daily', + }, + { + name: 'Weekly', + value: 'weekly', + }, + { + name: 'Monthly', + value: 'monthly', + }, + { + name: 'Yearly', + value: 'yearly', + }, + ], + default: '', + }, + { + displayName: 'Repeat Until', + name: 'repeatUntil', + type: 'dateTime', + default: '', + }, + { + displayName: 'Repeat How Many Times?', + name: 'repeatHowManyTimes', + type: 'number', + typeOptions: { + minValue: 1, + }, + default: 1, + }, + { + displayName: 'Send Updates', + name: 'sendUpdates', + type: 'options', + options: [ + { + name: 'All', + value: 'all', + description: ' Notifications are sent to all guests', + }, + { + name: 'External Only', + value: 'externalOnly', + description: 'Notifications are sent to non-Google Calendar guests only', + }, + { + name: 'None', + value: 'none', + description: ' No notifications are sent. This value should only be used for migration use case', + }, + ], + description: 'Whether to send notifications about the creation of the new event', + default: '', + }, + { + displayName: 'Summary', + name: 'summary', + type: 'string', + default: '', + description: 'Title of the event.', + }, + { + displayName: 'Show Me As', + name: 'showMeAs', + type: 'options', + options: [ + { + name: 'Available', + value: 'transparent', + description: 'The event does not block time on the calendar', + }, + { + name: 'Busy', + value: 'opaque', + description: ' The event does block time on the calendar.', + }, + ], + default: 'opaque', + description: 'Whether the event blocks time on the calendar', + }, + { + displayName: 'Timezone', + name: 'timezone', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTimezones', + }, + default: '', + description: 'The timezone the event will have set. By default events are schedule on n8n timezone ' + }, + { + displayName: 'Visibility', + name: 'visibility', + type: 'options', + options: [ + { + name: 'Confidential', + value: 'confidential', + description: 'The event is private. This value is provided for compatibility reasons.', + }, + { + name: 'Default', + value: 'default', + description: ' Uses the default visibility for events on the calendar.', + }, + { + name: 'Public', + value: 'public', + description: 'The event is public and event details are visible to all readers of the calendar.', + }, + { + name: 'Private', + value: 'private', + description: 'The event is private and only event attendees may view event details.', + }, + ], + default: 'default', + description: 'Visibility of the event.', + }, + ], + }, + { + displayName: 'Reminders', + name: 'remindersUi', + type: 'fixedCollection', + default: '', + placeholder: 'Add Reminder', + typeOptions: { + multipleValues: true, + }, + required: false, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'create', + ], + useDefaultReminders: [ + false, + ], + }, + }, + options: [ + { + name: 'remindersValues', + displayName: 'Reminder', + values: [ + { + displayName: 'Method', + name: 'method', + type: 'options', + options: [ + { + name: 'Email', + value: 'email', + }, + { + name: 'Popup', + value: 'popup', + }, + ], + default: '', + }, + { + displayName: 'Minutes Before', + name: 'minutes', + type: 'number', + typeOptions: { + minValue: 0, + maxValue: 40320, + }, + default: 0, + }, + ], + } + ], + description: `If the event doesn't use the default reminders, this lists the reminders specific to the event`, + }, +/* -------------------------------------------------------------------------- */ +/* event:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Calendar', + name: 'calendar', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCalendars', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + }, + { + displayName: 'Event ID', + name: 'eventId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Options', + default: {}, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'event', + ], + }, + }, + options: [ + { + displayName: 'Send Updates', + name: 'sendUpdates', + type: 'options', + options: [ + { + name: 'All', + value: 'all', + description: ' Notifications are sent to all guests', + }, + { + name: 'External Only', + value: 'externalOnly', + description: 'Notifications are sent to non-Google Calendar guests only', + }, + { + name: 'None', + value: 'none', + description: ' No notifications are sent. This value should only be used for migration use case', + }, + ], + description: 'Whether to send notifications about the creation of the new event', + default: '', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* event:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Calendar', + name: 'calendar', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCalendars', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + }, + { + displayName: 'Event ID', + name: 'eventId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Options', + default: {}, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'event', + ], + }, + }, + options: [ + { + displayName: 'Max Attendees', + name: 'maxAttendees', + type: 'number', + default: 0, + description: `The maximum number of attendees to include in the response.
+ If there are more than the specified number of attendees, only the participant is returned`, + }, + { + displayName: 'Timezone', + name: 'timeZone', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTimezones', + }, + default: '', + description: `Time zone used in the response. The default is the time zone of the calendar.`, + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* event:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Calendar', + name: 'calendar', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCalendars', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + }, + { + 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: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'event', + ], + }, + }, + options: [ + { + displayName: 'iCalUID', + name: 'iCalUID', + type: 'string', + default: '', + description: 'Specifies event ID in the iCalendar format to be included in the response', + }, + { + displayName: 'Max Attendees', + name: 'maxAttendees', + type: 'number', + default: 0, + description: `The maximum number of attendees to include in the response.
+ If there are more than the specified number of attendees, only the participant is returned`, + }, + { + displayName: 'Order By', + name: 'orderBy', + type: 'options', + options: [ + { + name: 'Start Time', + value: 'startTime', + description: 'Order by the start date/time (ascending). This is only available when querying single events (i.e. the parameter singleEvents is True)', + }, + { + name: 'Updated', + value: 'updated', + description: 'Order by last modification time (ascending).', + }, + ], + default: '', + description: 'The order of the events returned in the result.', + }, + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + description: 'Free text search terms to find events that match these terms in any field, except for extended properties.', + }, + { + displayName: 'Show Deleted', + name: 'showDeleted', + type: 'boolean', + default: false, + description: 'Whether to include deleted events (with status equals "cancelled") in the result.', + }, + { + displayName: 'Show Hidden Invitations', + name: 'showHiddenInvitations', + type: 'boolean', + default: false, + description: 'Whether to include hidden invitations in the result.', + }, + { + displayName: 'Single Events', + name: 'singleEvents', + type: 'boolean', + default: false, + description: `Whether to expand recurring events into instances and only return single one-off
+ events and instances of recurring events, but not the underlying recurring events themselves.`, + }, + { + displayName: 'Time Max', + name: 'timeMax', + type: 'dateTime', + default: '', + description: `Upper bound (exclusive) for an event's start time to filter by`, + }, + { + displayName: 'Time Min', + name: 'timeMin', + type: 'dateTime', + default: '', + description: `Lower bound (exclusive) for an event's end time to filter by`, + }, + { + displayName: 'Timezone', + name: 'timeZone', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTimezones', + }, + default: '', + description: `Time zone used in the response. The default is the time zone of the calendar.`, + }, + { + displayName: 'Updated Min', + name: 'updatedMin', + type: 'dateTime', + default: '', + description: `Lower bound for an event's last modification time (as a RFC3339 timestamp) to filter by. + When specified, entries deleted since this time will always be included regardless of showDeleted`, + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* event:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Calendar', + name: 'calendar', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCalendars', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + }, + { + displayName: 'Event ID', + name: 'eventId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + }, + { + displayName: 'Use Default Reminders', + name: 'useDefaultReminders', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'event', + ], + }, + }, + default: true, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'event', + ], + }, + }, + options: [ + { + displayName: 'All Day', + name: 'allday', + type: 'boolean', + options: [ + { + name: 'Yes', + value: 'yes', + }, + { + name: 'No', + value: 'no', + }, + ], + default: 'no', + description: 'Wheater the event is all day or not', + }, + { + displayName: 'Attendees', + name: 'attendees', + type: 'string', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Attendee', + }, + default: '', + description: 'The attendees of the event', + }, + { + displayName: 'Color', + name: 'color', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getColors', + }, + default: '', + description: 'The color of the event.', + }, + { + displayName: 'End', + name: 'end', + type: 'dateTime', + default: '', + description: 'End time of the event.', + }, + { + displayName: 'Guests Can Invite Others', + name: 'guestsCanInviteOthers', + type: 'boolean', + default: true, + description: 'Whether attendees other than the organizer can invite others to the event', + }, + { + displayName: 'Guests Can Modify', + name: 'guestsCanModify', + type: 'boolean', + default: false, + description: 'Whether attendees other than the organizer can modify the event', + }, + { + displayName: 'Guests Can See Other Guests', + name: 'guestsCanSeeOtherGuests', + type: 'boolean', + default: true, + description: `Whether attendees other than the organizer can see who the event's attendees are.`, + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + default: '', + description: 'Opaque identifier of the event', + }, + { + displayName: 'Location', + name: 'location', + type: 'string', + default: '', + description: 'Geographic location of the event as free-form text.', + }, + { + displayName: 'Max Attendees', + name: 'maxAttendees', + type: 'number', + default: 0, + description: `The maximum number of attendees to include in the response.
+ If there are more than the specified number of attendees, only the participant is returned`, + }, + { + displayName: 'Repeat Frecuency', + name: 'repeatFrecuency', + type: 'options', + options: [ + { + name: 'Daily', + value: 'Daily', + }, + { + name: 'Weekly', + value: 'weekly', + }, + { + name: 'Monthly', + value: 'monthly', + }, + { + name: 'Yearly', + value: 'yearly', + }, + ], + default: '', + }, + { + displayName: 'Repeat Until', + name: 'repeatUntil', + type: 'dateTime', + default: '', + }, + { + displayName: 'Repeat How Many Times?', + name: 'repeatHowManyTimes', + type: 'number', + typeOptions: { + minValue: 1, + }, + default: 1, + }, + { + displayName: 'Start', + name: 'start', + type: 'dateTime', + default: '', + description: 'Start time of the event.', + }, + { + displayName: 'Send Updates', + name: 'sendUpdates', + type: 'options', + options: [ + { + name: 'All', + value: 'all', + description: ' Notifications are sent to all guests', + }, + { + name: 'External Only', + value: 'externalOnly', + description: 'Notifications are sent to non-Google Calendar guests only', + }, + { + name: 'None', + value: 'none', + description: ' No notifications are sent. This value should only be used for migration use case', + }, + ], + description: 'Whether to send notifications about the creation of the new event', + default: '', + }, + { + displayName: 'Summary', + name: 'summary', + type: 'string', + default: '', + description: 'Title of the event.', + }, + { + displayName: 'Show Me As', + name: 'showMeAs', + type: 'options', + options: [ + { + name: 'Available', + value: 'transparent', + description: 'The event does not block time on the calendar', + }, + { + name: 'Busy', + value: 'opaque', + description: ' The event does block time on the calendar.', + }, + ], + default: 'opaque', + description: 'Whether the event blocks time on the calendar', + }, + { + displayName: 'Timezone', + name: 'timezone', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTimezones', + }, + default: '', + description: 'The timezone the event will have set. By default events are schedule on n8n timezone ' + }, + { + displayName: 'Visibility', + name: 'visibility', + type: 'options', + options: [ + { + name: 'Confidential', + value: 'confidential', + description: 'The event is private. This value is provided for compatibility reasons.', + }, + { + name: 'Default', + value: 'default', + description: ' Uses the default visibility for events on the calendar.', + }, + { + name: 'Public', + value: 'public', + description: 'The event is public and event details are visible to all readers of the calendar.', + }, + { + name: 'Private', + value: 'private', + description: 'The event is private and only event attendees may view event details.', + }, + ], + default: 'default', + description: 'Visibility of the event.', + }, + ], + }, + { + displayName: 'Reminders', + name: 'remindersUi', + type: 'fixedCollection', + default: '', + placeholder: 'Add Reminder', + typeOptions: { + multipleValues: true, + }, + required: false, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'update', + ], + useDefaultReminders: [ + false, + ], + }, + }, + options: [ + { + name: 'remindersValues', + displayName: 'Reminder', + values: [ + { + displayName: 'Method', + name: 'method', + type: 'options', + options: [ + { + name: 'Email', + value: 'email', + }, + { + name: 'Popup', + value: 'popup', + }, + ], + default: '', + }, + { + displayName: 'Minutes Before', + name: 'minutes', + type: 'number', + typeOptions: { + minValue: 0, + maxValue: 40320, + }, + default: 0, + }, + ], + } + ], + description: `If the event doesn't use the default reminders, this lists the reminders specific to the event`, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Google/EventInterface.ts b/packages/nodes-base/nodes/Google/EventInterface.ts new file mode 100644 index 0000000000..72bf96cc80 --- /dev/null +++ b/packages/nodes-base/nodes/Google/EventInterface.ts @@ -0,0 +1,26 @@ +import { IDataObject } from "n8n-workflow"; + +export interface IReminder { + useDefault?: boolean; + overrides?: IDataObject[]; +} + +export interface IEvent { + attendees?: IDataObject[]; + colorId?: string; + description?: string; + end?: IDataObject; + guestsCanInviteOthers?: boolean; + guestsCanModify?: boolean; + guestsCanSeeOtherGuests?: boolean; + id?: string; + location?: string; + maxAttendees?: number; + recurrence?: string[]; + reminders?: IReminder; + sendUpdates?: string; + start?: IDataObject; + summary?: string; + transparency?: string; + visibility?: string; +} diff --git a/packages/nodes-base/nodes/Google/GenericFunctions.ts b/packages/nodes-base/nodes/Google/GenericFunctions.ts new file mode 100644 index 0000000000..80edb44370 --- /dev/null +++ b/packages/nodes-base/nodes/Google/GenericFunctions.ts @@ -0,0 +1,57 @@ +import { OptionsWithUri } from 'request'; +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; +import { + IDataObject +} from 'n8n-workflow'; + +export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}): Promise { // tslint:disable-line:no-any + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || `https://www.googleapis.com${resource}`, + json: true + }; + try { + if (Object.keys(headers).length !== 0) { + options.headers = Object.assign({}, options.headers, headers); + } + if (Object.keys(body).length === 0) { + delete options.body; + } + //@ts-ignore + return await this.helpers.requestOAuth.call(this, 'googleOAuth2Api', options); + } catch (error) { + if (error.response && error.response.body && error.response.body.message) { + // Try to return the error prettier + throw new Error(`Google Calendar error response [${error.statusCode}]: ${error.response.body.message}`); + } + throw error; + } +} + +export async function googleApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + query.maxResults = 100; + + do { + responseData = await googleApiRequest.call(this, method, endpoint, body, query); + query.pageToken = responseData['nextPageToken']; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData['nextPageToken'] !== undefined && + responseData['nextPageToken'] !== '' + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Google/GoogleCalendar.node.ts b/packages/nodes-base/nodes/Google/GoogleCalendar.node.ts new file mode 100644 index 0000000000..fa6a8b28c7 --- /dev/null +++ b/packages/nodes-base/nodes/Google/GoogleCalendar.node.ts @@ -0,0 +1,412 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeTypeDescription, + INodeType, + ILoadOptionsFunctions, + INodePropertyOptions, +} from 'n8n-workflow'; + +import { + googleApiRequest, + googleApiRequestAllItems, +} from './GenericFunctions'; + +import { + eventOperations, + eventFields, +} from './EventDescription'; + +import { + IEvent, +} from './EventInterface'; + +import * as moment from 'moment-timezone'; + +export class GoogleCalendar implements INodeType { + description: INodeTypeDescription = { + displayName: 'Google Calendar', + name: 'googleCalendar', + icon: 'file:googleCalendar.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Google Calendar API.', + defaults: { + name: 'Google Calendar', + color: '#3E87E4', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'googleOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Event', + value: 'event', + }, + ], + default: 'event', + description: 'The resource to operate on.', + }, + ...eventOperations, + ...eventFields, + ], + }; + + methods = { + loadOptions: { + // Get all the calendars to display them to user so that he can + // select them easily + async getCalendars(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const calendars = await googleApiRequestAllItems.call(this, 'items', 'GET', '/calendar/v3/users/me/calendarList'); + for (const calendar of calendars) { + const calendarName = calendar.summary; + const calendarId = calendar.id; + returnData.push({ + name: calendarName, + value: calendarId, + }); + } + return returnData; + }, + // Get all the colors to display them to user so that he can + // select them easily + async getColors(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const { calendar } = await googleApiRequest.call(this, 'GET', '/calendar/v3/colors'); + for (let key of Object.keys(calendar)) { + const colorName = calendar[key].background; + const colorId = key; + returnData.push({ + name: `${colorName} - ${colorId}`, + value: colorId, + }); + } + return returnData; + }, + // Get all the timezones to display them to user so that he can + // select them easily + async getTimezones(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + for (const timezone of moment.tz.names()) { + const timezoneName = timezone; + const timezoneId = timezone; + returnData.push({ + name: timezoneName, + value: timezoneId, + }); + } + 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') { + //https://developers.google.com/calendar/v3/reference/events/insert + if (operation === 'create') { + const calendarId = this.getNodeParameter('calendar', i) as string; + const start = this.getNodeParameter('start', i) as string; + const end = this.getNodeParameter('end', i) as string; + const useDefaultReminders = this.getNodeParameter('useDefaultReminders', i) as boolean; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + if (additionalFields.maxAttendees) { + qs.maxAttendees = additionalFields.maxAttendees as number; + } + if (additionalFields.sendNotifications) { + qs.sendNotifications = additionalFields.sendNotifications as boolean; + } + if (additionalFields.sendUpdates) { + qs.sendUpdates = additionalFields.sendUpdates as string; + } + const body: IEvent = { + start: { + dateTime: start, + timeZone: additionalFields.timeZone || this.getTimezone(), + }, + end: { + dateTime: end, + timeZone: additionalFields.timeZone || this.getTimezone(), + } + }; + if (additionalFields.attendees) { + body.attendees = (additionalFields.attendees as string[]).map(attendee => { + return { email: attendee }; + }); + } + if (additionalFields.color) { + body.colorId = additionalFields.color as string; + } + if (additionalFields.description) { + body.description = additionalFields.description as string; + } + if (additionalFields.guestsCanInviteOthers) { + body.guestsCanInviteOthers = additionalFields.guestsCanInviteOthers as boolean; + } + if (additionalFields.guestsCanModify) { + body.guestsCanModify = additionalFields.guestsCanModify as boolean; + } + if (additionalFields.guestsCanSeeOtherGuests) { + body.guestsCanSeeOtherGuests = additionalFields.guestsCanSeeOtherGuests as boolean; + } + if (additionalFields.id) { + body.id = additionalFields.id as string; + } + if (additionalFields.location) { + body.location = additionalFields.location as string; + } + if (additionalFields.summary) { + body.summary = additionalFields.summary as string; + } + if (additionalFields.showMeAs) { + body.transparency = additionalFields.showMeAs as string; + } + if (additionalFields.visibility) { + body.visibility = additionalFields.visibility as string; + } + if (!useDefaultReminders) { + const reminders = (this.getNodeParameter('remindersUi', i) as IDataObject).remindersValues as IDataObject[]; + body.reminders = { + useDefault: false, + }; + if (reminders) { + body.reminders.overrides = reminders; + } + } + if (additionalFields.allday) { + body.start = { + date: moment(start).utc().format('YYYY-MM-DD'), + }; + body.end = { + date: moment(end).utc().format('YYYY-MM-DD'), + }; + } + //exampel: RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=10;UNTIL=20110701T170000Z + //https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html + body.recurrence = []; + if (additionalFields.repeatHowManyTimes + && additionalFields.repeatUntil) { + throw new Error(`You can set either 'Repeat How Many Times' or 'Repeat Until' but not both`); + } + if (additionalFields.repeatFrecuency) { + body.recurrence?.push(`FREQ=${(additionalFields.repeatFrecuency as string).toUpperCase()};`); + } + if (additionalFields.repeatHowManyTimes) { + body.recurrence?.push(`COUNT=${additionalFields.repeatHowManyTimes};`); + } + if (additionalFields.repeatUntil) { + body.recurrence?.push(`UNTIL=${moment(additionalFields.repeatUntil as string).utc().format('YYYYMMDDTHHmmss')}Z`); + } + if (body.recurrence.length !== 0) { + body.recurrence = [`RRULE:${body.recurrence.join('')}`]; + } + responseData = await googleApiRequest.call(this, 'POST', `/calendar/v3/calendars/${calendarId}/events`, body, qs); + } + //https://developers.google.com/calendar/v3/reference/events/delete + if (operation === 'delete') { + const calendarId = this.getNodeParameter('calendar', i) as string; + const eventId = this.getNodeParameter('eventId', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + if (options.sendUpdates) { + qs.sendUpdates = options.sendUpdates as number; + } + responseData = await googleApiRequest.call(this, 'DELETE', `/calendar/v3/calendars/${calendarId}/events/${eventId}`, {}); + responseData = { success: true }; + } + //https://developers.google.com/calendar/v3/reference/events/get + if (operation === 'get') { + const calendarId = this.getNodeParameter('calendar', i) as string; + const eventId = this.getNodeParameter('eventId', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + if (options.maxAttendees) { + qs.maxAttendees = options.maxAttendees as number; + } + if (options.timeZone) { + qs.timeZone = options.timeZone as string; + } + responseData = await googleApiRequest.call(this, 'GET', `/calendar/v3/calendars/${calendarId}/events/${eventId}`, {}, qs); + } + //https://developers.google.com/calendar/v3/reference/events/list + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const calendarId = this.getNodeParameter('calendar', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + if (options.iCalUID) { + qs.iCalUID = options.iCalUID as string; + } + if (options.maxAttendees) { + qs.maxAttendees = options.maxAttendees as number; + } + if (options.orderBy) { + qs.orderBy = options.orderBy as number; + } + if (options.query) { + qs.q = options.query as number; + } + if (options.showDeleted) { + qs.showDeleted = options.showDeleted as boolean; + } + if (options.showHiddenInvitations) { + qs.showHiddenInvitations = options.showHiddenInvitations as boolean; + } + if (options.singleEvents) { + qs.singleEvents = options.singleEvents as boolean; + } + if (options.timeMax) { + qs.timeMax = options.timeMax as string; + } + if (options.timeMin) { + qs.timeMin = options.timeMin as string; + } + if (options.timeZone) { + qs.timeZone = options.timeZone as string; + } + if (options.updatedMin) { + qs.updatedMin = options.updatedMin as string; + } + if (returnAll) { + responseData = await googleApiRequestAllItems.call(this, 'items', 'GET', `/calendar/v3/calendars/${calendarId}/events`, {}, qs); + } else { + qs.maxResults = this.getNodeParameter('limit', i) as number; + responseData = await googleApiRequest.call(this, 'GET', `/calendar/v3/calendars/${calendarId}/events`, {}, qs); + responseData = responseData.items; + } + } + //https://developers.google.com/calendar/v3/reference/events/patch + if (operation === 'update') { + const calendarId = this.getNodeParameter('calendar', i) as string; + const eventId = this.getNodeParameter('eventId', i) as string; + const useDefaultReminders = this.getNodeParameter('useDefaultReminders', i) as boolean; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + if (updateFields.maxAttendees) { + qs.maxAttendees = updateFields.maxAttendees as number; + } + if (updateFields.sendNotifications) { + qs.sendNotifications = updateFields.sendNotifications as boolean; + } + if (updateFields.sendUpdates) { + qs.sendUpdates = updateFields.sendUpdates as string; + } + const body: IEvent = {}; + if (updateFields.start) { + body.start = { + dateTime: updateFields.start, + timeZone: updateFields.timeZone || this.getTimezone(), + }; + } + if (updateFields.end) { + body.end = { + dateTime: updateFields.end, + timeZone: updateFields.timeZone || this.getTimezone(), + }; + } + if (updateFields.attendees) { + body.attendees = (updateFields.attendees as string[]).map(attendee => { + return { email: attendee }; + }); + } + if (updateFields.color) { + body.colorId = updateFields.color as string; + } + if (updateFields.description) { + body.description = updateFields.description as string; + } + if (updateFields.guestsCanInviteOthers) { + body.guestsCanInviteOthers = updateFields.guestsCanInviteOthers as boolean; + } + if (updateFields.guestsCanModify) { + body.guestsCanModify = updateFields.guestsCanModify as boolean; + } + if (updateFields.guestsCanSeeOtherGuests) { + body.guestsCanSeeOtherGuests = updateFields.guestsCanSeeOtherGuests as boolean; + } + if (updateFields.id) { + body.id = updateFields.id as string; + } + if (updateFields.location) { + body.location = updateFields.location as string; + } + if (updateFields.summary) { + body.summary = updateFields.summary as string; + } + if (updateFields.showMeAs) { + body.transparency = updateFields.showMeAs as string; + } + if (updateFields.visibility) { + body.visibility = updateFields.visibility as string; + } + if (!useDefaultReminders) { + const reminders = (this.getNodeParameter('remindersUi', i) as IDataObject).remindersValues as IDataObject[]; + body.reminders = { + useDefault: false, + }; + if (reminders) { + body.reminders.overrides = reminders; + } + } + if (updateFields.allday + && updateFields.start + && updateFields.end) { + body.start = { + date: moment(updateFields.start as string).utc().format('YYYY-MM-DD'), + }; + body.end = { + date: moment(updateFields.end as string).utc().format('YYYY-MM-DD'), + }; + } + //exampel: RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=10;UNTIL=20110701T170000Z + //https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html + body.recurrence = []; + if (updateFields.repeatHowManyTimes + && updateFields.repeatUntil) { + throw new Error(`You can set either 'Repeat How Many Times' or 'Repeat Until' but not both`); + } + if (updateFields.repeatFrecuency) { + body.recurrence?.push(`FREQ=${(updateFields.repeatFrecuency as string).toUpperCase()};`); + } + if (updateFields.repeatHowManyTimes) { + body.recurrence?.push(`COUNT=${updateFields.repeatHowManyTimes};`); + } + if (updateFields.repeatUntil) { + body.recurrence?.push(`UNTIL=${moment(updateFields.repeatUntil as string).utc().format('YYYYMMDDTHHmmss')}Z`); + } + if (body.recurrence.length !== 0) { + body.recurrence = [`RRULE:${body.recurrence.join('')}`]; + } else { + delete body.recurrence; + } + responseData = await googleApiRequest.call(this, 'PATCH', `/calendar/v3/calendars/${calendarId}/events/${eventId}`, body, qs); + } + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Google/googleCalendar.png b/packages/nodes-base/nodes/Google/googleCalendar.png new file mode 100644 index 0000000000000000000000000000000000000000..2a2bfa1c7b4754b9d4695b0beab270a62ab0a4da GIT binary patch literal 6937 zcmY*;1z1#F7w*iE(jbk%NaxTn;Ls)AFd*HXLrI6^AYDp_3eqVtNJ$H#bayH-fPf%g z{QmF$|GoP>`>c1b^R9R8bfD)-J;MAO0KGyYN{3-aPPpYU~35;6MD^ zfq++e)c0z}PWo^JTti*L#={L_W$R&W2MKWV{2K*;1xVaOH#>wCIKa)--A5uolIb6Y z#6AA&=3@f?LqWJmGQl;p!SWv7c3=^RFod5;3J(kh!@O({tdq$NisPi5S|iz zeE$Ca5Pu+Zw+ACdp7qhRM_k-MANw7tpX>2t<^s> zU%O(XIDFx$B3C5Y6AHH%CKaRsD#f2iAJjMcZLpayp8BB7kL~?YwQWMTp{uz<{zpq~ zm*efS3XaJk$9dwrL3iW+1w#Icl;4;@iMTvWb#dK)!R;z|Eg0Y|jZ4EAPGiK&)ag84 z7<<|sf$@T$V7X9vAU2})sY3R^XXg++R!7+jIJV5 zO)h1FjU}gK&1Q%ugz`&sAzzlJW@cJ8lf+i8u)?oCTnN;fwV~Fa^)9oJKPT&bL4cLU z($dm1ZMnNKuP+jcu@VvsM7*$~bgL`|+uAU&AgmlP&ChTa}62h3*=4%j+7 zt5z`_jC{p0k8#KC+87!d!dPr^TXYEdY$Jb!+Zk|L67vul9N_1d)$qW4%mb*#RjyaA zt72GfQvFF3J)S3CI^MuPT2Y2H3_NHqG(e%gPR>_Y);PJO7Z*QvYqG9?XWif$9ZIz= z6#|+gj`A$LXwGz>83YJ3$AI>{xH=gmpCcrhA8@|x#Exe^TcUVBkT9o@86 z6@=H%-W@ye?qn!j!aM65rynz|a^1*wn!2VYsN2bR>}8_w7(P#tB<9gbCN~}>UA)BL z;A(bTn|L*|MtWIScuZF_CNC$u5hfcANB){V*HF1{eVX4!Wq0qMIMnZ8D(LI`jOD9)a9 z*VP%DuFZ|lbL%3ZA-&{--khNITbkUQoG-%|johwVkJd|1SY>Ww7T|ozB$Q5iu=jlXQ?|v8$HxtK#o2z8-fCH@EIIj;7!n&QYq7J_^|5!2 z%;WgWVKb6p0=H)+bW*o>$P6yvZ_$pl=U9kwFijgLuq4`& zriofw$yadA8O=52y~*~(<lM4YMrp z?vRV12|+FS<;%Kti5OYh2W@kb!=&L%CR$PrDcL>{8vZctRu##oA1f3AMM-koIjy7D z-n=$k`hEWqa3+i4z|Mk%xJh9D{6XOXz!=ifLQ0GCs&dI1H!n+fs(k1{JrN%$rnKZcx-?Y!_ltD6>h+O~t@64yFdM^8+i0KO=qaG`zF^ zxy0LU5w?2KgUs_h>TgA5rR>8~>WIwREG}MYPClVpg7azdo?l(K7(0!+-yleUD9j4l zKc93)x6S^#Y-Wjv;=(SE$91cm-j$_RCs6mf;iU$;+?O@byc-=7 z#j2~V)xy!9^@;_B(SCoNjc)rs^rd{195k2=F(y-(hU9P{8>-1o%!8WAYCwpFfb(YE z2b$XBGSLy4xn1|+7C*UA$Kx-p^9Ai4c=~@7mAE$W@LJ z0L7xn)LT04SeYj*Q?lHSUxPS)YgG`A&X{%qsAB2ATcxJ?8H*FvVkQnO=IhhKDCYV* z*bFMubsUP`i@g5{DV%H<>2}6oAi8vY#At{`ik-YCNKV z28Rl#7_Aa+EwY2=XZD|x!p#mPErBv(#8UN z9J8SX8NxwlSn)b+(ma@-!IFRg^;nnD!Y}33Ie8_izZzh(xhPuR)hj&8AwuaQTS^ zJVc__F{7h%HbvIG(j!H&anlpDu^xz_RZb@OAOmZPGCg7qMR+3BI-Dxmk)Ycz+0*-q zO!)^A#$%{6lN5Ity_pd>SX~fYF`6lTogIjkY*+cnh>JcCZ@UGe+!fHl&WKvcya3cS zeunqSuh1y4(B=N*M@)&Dp0SjF(x$}@p4hizPlNAKqt=Yj%3zqM$8a<(L8Aum0K@psB^<*zTENILNeu>tmOieVS|;@7{P z1%xI>8nC9?!70r8qtUUem)q>ITme!{g7}uweo@>Vh7y7imS7G{7Z7fUEs9$$doAVx z73?6zUtk91T~;Ej(m&@zV-T43H6rWKG>#sJI9M?ucs4kyb_SsI(>*kdBr?iM;9IaO zZB#1nQ1Vf>SAIXB$tQPcV7g|^2DX`V+56bS93$>Y7TPH;o?3~JJ$EsPKJchHLA@xE ztcnORx>)PA&5Q6+Vx(`9pXZTz5-63jYw>ftwo8ni_^vOn^Zw@n;i}K_(TlO7gjBI` z|Hc%<`hiU_mc~|4X1mmE1HQZ}2Xtn2^;lo(?z-n&!R9hv`8lVMz^|C~t-(o(4*sr5 zfr4~KNxG3YM9S>Af@y_vP#sTiXv`^EaNs*@(c=kcP|}f_Oh}PGVaWxS!hT!5Oo5~w zHRrJeGF}pphjuMl+CuS8w3{eXgCGC`uCuq7$AkFy%gqxG=0tYmEDV`cDH=~45=_q> zmWdJM;Rz43SIYN@Mh5|%;LE|Yg*czMUVQj*GmzsjPb80t!|BnRjX=Gny#PTPc2O$! za*r%qGyNkrl1UxYx}e6MjcAEAlGTjC0HaoMi;*Xw5hpU-*_z>nuc zr@b~iW_eI;!SZidM_fnIgkxtyI9h=k?(?|nbUasDW8=i;yCWlZFCks>Ul>C9Qe5gu z2F{DDh8@=zAh~I+887^`nnu&vjI+1uufYi>LBt~P)eW7}%||xIv-LABGGDhrUut4u zVo-YQ95w;IllR6YA*SbS4@cR)GmJlDdNy1A!zjGJgeEeys__L5;tA>GU zG{=>LBk^3+Nw|0n;Wog1O@^68GHW}Vlvv+I$n6t%vL-x={rskZVvv9>9bcw{p*Ine zO0EYsWJ3cl?!(^tVww&mUkujoHzCl>D_)%3bvJPz5jW>OMr>26i$3!}m*FS(!=iUj zzeU_`e>Ac^BpX{h0O9H}Rpj3C?5w9xAA@vwXZdEpnto=N|H2qnuNnG!yEB2!Fur75DE;XZB zoD4@#dM+4D3S`E6lnL5wDGq$KF6dobWOO>jov0j{b3nOVWfFJC{a%|r&RMzcA5PRH zsUqwHN!WB{kBQGsSh3ezzt~;!mZvk`he3 z<5_!-%vtD}#9+PBYI`q$$#Y2Iu)Gkd22lBk@TIOQ#P8dN_n-0iC_FCLf0!s@>J=dp zahRnl%P)PKUR$+2seIG~2Gp1Ppfc%BB3GNP^h&Y5s|LU@>}EdLt}KM(M(Oa-e04Fox zZZU*cHnx4$)yf!35L9X~uXidkB|k*vYc~PxC}G=gs%$G`tgW1%BcHrj;m9CmL?nd zXce=qt{!VVp*hBF;cs&-+`-ktcU&O!gixZHe6hKjx^HJsAW$Tipy!)=tcym;7moR& zN}$%EE{x|JlcO)*r@&Sm99QUgaQ! zGCbw@yHon_j>WCx&^8V|>a6{iB!dtmk~C{DztXt3d9n=nDQhqyPwyhN9T)Zklx1;P z%E#JAbw@`+SeT#zboZrU?gp*OolfGUFcmUa5c1tW&pEYE*6dorDLh0vge4GAYAqNg z`D3V>dhlq~IP1ngW6S7xVDXH@Dgi^<{bXZtc zfy8MBQjz40KPK`3F370!iO}xYi_jDXL9E-9RN0AAg;OQxfc?2-+4Eg8a5p-Q1}615 zV(YR)!UR)>xg!u(<{b{D+_f89WjKvBKgN-QN>wjY0iNHQ?G5^W2?bP1!gkk4-|09q z8w^TO%V-nSC7A*kGq7+FV>6|9Ey7?qUX*rV2j1`F3vgoq1w&(r1 z0v0F_zO{nb(v+q2GJItYV>w;};Yc>W2?2E`X(%sRtg!cCGa>cam!xaq;}<>JGm7|D z`5XX0G8ozisv`CdL`U&zudK#7p~J0tXb173-#WOIcgW{6)>{7puIQNVOpE$@s&DyG zx!`8zR@R8=Id^|NE{lS@Jex7w;@tNMP@-mZe#lguCnNaRJk61$=`Ng`sassSu~WS zW+mzlP078wRw*!{%?28TRS`77`a!#|%F$EB0>-V@h72EYe25jQ2EDiIZxYc@_C~Ji z(-D+cWEY5%FNU3lq;|9!;si*z&or5k2O`)=O`C?nmhII_7QG79qkwM8 zdUGePvt^z`li3g1#th~@Ku4MrJ>d5UFQc$6Yn5}=kM_VJ)M55WDaNKZdINduXQ5Kl zQ%^xmVrYG67Zru?Aa#(}3p_ZhO}IThgZGBVY%;Za8K+WlNASYxdw!WNs4k5-bsy3S zuV6g0bmpl->7118boT~pxVfEmm;!#fi&jU-(g~zVuUGqDkF_%d?)>ly+!;c`I%$$w zy*%4#aoz@-D4z^?dP^dtG8j?3=w&NqzjJLVBH+p7{40QCnE<-7`;w^K z6TgfL-ejC?sfrnU!hgsY>kv#m&BGaRD;Qr&a2MPDaGto(O|j7h0=TJ;=I^fSs#7RD zhgYK1MBCEQI<6RA1@E*rp+L`xk!@V38cSvC_9|d9cwhGg%@>ZMv3eL6v(opYj=Hd- z!LqHB;!R<65}8wVXHE*wQ3@!0S6+6=GZ#v{ziKuef^J-C`_=Cjb8Ez&o5uMSv8ZGjT=Zfg#BYY}&P!^(?0{BMb`$ z3z&$JEL7!;d&X>)7!@8;)Dzaf<)q{=-N!S6Vj}2w141hbX^1ylrXfF+*=qNLXf1?jdUTIyuJ$ zF$_lT2|Amc!n2a-KIPsO>thFqMl=P9-F+Xvn@M%JrV3t01r;iTsm87zI}~Z1yA|6* zI6!*6J+I%Fmd!~aTk-n-P&r5;0elNh|Jf_}XVc&h2=oVIU0CPVvTr)$@BWaAqNYNP IoMq(y0lY1>SO5S3 literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 746f91f723..b8152aed6c 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -42,7 +42,8 @@ "dist/credentials/GithubApi.credentials.js", "dist/credentials/GithubOAuth2Api.credentials.js", "dist/credentials/GitlabApi.credentials.js", - "dist/credentials/GoogleApi.credentials.js", + "dist/credentials/GoogleApi.credentials.js", + "dist/credentials/GoogleOAuth2Api.credentials.js", "dist/credentials/HttpBasicAuth.credentials.js", "dist/credentials/HttpDigestAuth.credentials.js", "dist/credentials/HttpHeaderAuth.credentials.js", @@ -115,7 +116,8 @@ "dist/nodes/Github/Github.node.js", "dist/nodes/Github/GithubTrigger.node.js", "dist/nodes/Gitlab/Gitlab.node.js", - "dist/nodes/Gitlab/GitlabTrigger.node.js", + "dist/nodes/Gitlab/GitlabTrigger.node.js", + "dist/nodes/Google/GoogleCalendar.node.js", "dist/nodes/Google/GoogleDrive.node.js", "dist/nodes/Google/GoogleSheets.node.js", "dist/nodes/GraphQL/GraphQL.node.js", @@ -189,7 +191,8 @@ "@types/gm": "^1.18.2", "@types/imap-simple": "^4.2.0", "@types/jest": "^24.0.18", - "@types/lodash.set": "^4.3.6", + "@types/lodash.set": "^4.3.6", + "@types/moment-timezone": "^0.5.12", "@types/mongodb": "^3.3.6", "@types/node": "^10.10.1", "@types/nodemailer": "^4.6.5", @@ -215,7 +218,8 @@ "imap-simple": "^4.3.0", "lodash.get": "^4.4.2", "lodash.set": "^4.3.2", - "lodash.unset": "^4.5.2", + "lodash.unset": "^4.5.2", + "moment-timezone": "0.5.28", "mongodb": "^3.3.2", "mysql2": "^2.0.1", "n8n-core": "~0.20.0", From 150aa7daee102a6078fa0ea49d7c50b2ac1b1761 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 15 Mar 2020 13:00:57 +0100 Subject: [PATCH 020/165] :bug: Fix issue that did not use actual node parameters loading options --- packages/cli/src/Server.ts | 2 +- packages/core/src/LoadNodeParameterOptions.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index bafe3120e2..91c318086f 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -550,7 +550,7 @@ class App { const nodeTypes = NodeTypes(); - const loadDataInstance = new LoadNodeParameterOptions(nodeType, nodeTypes, credentials); + const loadDataInstance = new LoadNodeParameterOptions(nodeType, nodeTypes, JSON.parse('' + req.query.currentNodeParameters), credentials); const workflowData = loadDataInstance.getWorkflowData() as IWorkflowBase; const workflowCredentials = await WorkflowCredentials(workflowData.nodes); diff --git a/packages/core/src/LoadNodeParameterOptions.ts b/packages/core/src/LoadNodeParameterOptions.ts index 8ff78e41cd..fc9bf46b03 100644 --- a/packages/core/src/LoadNodeParameterOptions.ts +++ b/packages/core/src/LoadNodeParameterOptions.ts @@ -1,6 +1,7 @@ import { INode, INodeCredentials, + INodeParameters, INodePropertyOptions, INodeTypes, IWorkflowExecuteAdditionalData, @@ -20,7 +21,7 @@ export class LoadNodeParameterOptions { workflow: Workflow; - constructor(nodeTypeName: string, nodeTypes: INodeTypes, credentials?: INodeCredentials) { + constructor(nodeTypeName: string, nodeTypes: INodeTypes, currentNodeParameters: INodeParameters, credentials?: INodeCredentials) { const nodeType = nodeTypes.getByName(nodeTypeName); if (nodeType === undefined) { @@ -28,8 +29,7 @@ export class LoadNodeParameterOptions { } const nodeData: INode = { - parameters: { - }, + parameters: currentNodeParameters, name: TEMP_NODE_NAME, type: nodeTypeName, typeVersion: 1, From ff7f0a5de5370be475c718985d6da1c1b9c9e24a Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 15 Mar 2020 15:51:49 +0100 Subject: [PATCH 021/165] :zap: Fix some issues with Slack-Node --- packages/cli/BREAKING-CHANGES.md | 28 +++++++ packages/cli/src/Server.ts | 2 +- packages/core/src/NodeExecuteFunctions.ts | 18 ++--- .../credentials/SlackOAuth2Api.credentials.ts | 2 +- .../nodes/Slack/ChannelDescription.ts | 12 ++- .../nodes/Slack/GenericFunctions.ts | 4 +- .../nodes/Slack/MessageDescription.ts | 23 ++++-- packages/nodes-base/nodes/Slack/Slack.node.ts | 79 ++++++++++++------- .../nodes-base/nodes/Slack/StarDescription.ts | 12 ++- 9 files changed, 126 insertions(+), 54 deletions(-) diff --git a/packages/cli/BREAKING-CHANGES.md b/packages/cli/BREAKING-CHANGES.md index 3effaa08ee..33e5118859 100644 --- a/packages/cli/BREAKING-CHANGES.md +++ b/packages/cli/BREAKING-CHANGES.md @@ -2,6 +2,34 @@ This list shows all the versions which include breaking changes and how to upgrade +## ??? + +### What changed? + +To make it easier to use the data which the Slack-Node outputs we no longer return the whole +object the Slack-API returns if the only other property is `"ok": true`. In this case it returns +now directly the data under "channel". + +### When is action necessary? + +When you currently use the Slack-Node with Operations Channel -> Create and you use +any of the data the node outputs. + +### How to upgrade: + +All values that get referenced which were before under the property "channel" are now on the main level. +This means that these expressions have to get adjusted. + +Meaning if the expression used before was: +``` +{{ $node["Slack"].data["channel"]["id"] }} +``` +it has to get changed to: +``` +{{ $node["Slack"].data["id"] }} +``` + + ## 0.37.0 ### What changed? diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 91c318086f..9f4e9e77bc 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -203,7 +203,7 @@ class App { }); } - jwt.verify(token, getKey, {}, (err: Error) => { + jwt.verify(token, getKey, {}, (err: Error, decoded: object) => { if (err) return ResponseHelper.jwtAuthAuthorizationError(res, "Invalid token"); next(); diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index f73eb2f068..80d5d0d135 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -115,7 +115,7 @@ export async function prepareBinaryData(binaryData: Buffer, filePath?: string, m * @param {IWorkflowExecuteAdditionalData} additionalData * @returns */ -export function requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, node: INode, additionalData: IWorkflowExecuteAdditionalData, tokenType?: string | undefined, property?: string) { +export function requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, node: INode, additionalData: IWorkflowExecuteAdditionalData, tokenType?: string, property?: string) { const credentials = this.getCredentials(credentialsType) as ICredentialDataDecryptedObject; if (credentials === undefined) { @@ -134,7 +134,7 @@ export function requestOAuth(this: IAllExecuteFunctions, credentialsType: string const oauthTokenData = credentials.oauthTokenData as clientOAuth2.Data; - const token = oAuthClient.createToken(get(oauthTokenData, property as string) || oauthTokenData.accessToken, oauthTokenData.refresToken, tokenType || oauthTokenData.tokenType, oauthTokenData); + const token = oAuthClient.createToken(get(oauthTokenData, property as string) || oauthTokenData.accessToken, oauthTokenData.refreshToken, tokenType || oauthTokenData.tokenType, oauthTokenData); // Signs the request by adding authorization headers or query parameters depending // on the token-type used. const newRequestOptions = token.sign(requestOptions as clientOAuth2.RequestObject); @@ -412,7 +412,7 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType: string | undefined = undefined, property: string = ''): Promise { // tslint:disable-line:no-any + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); }, returnJsonArray, @@ -466,7 +466,7 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType: string | undefined = undefined, property: string = ''): Promise { // tslint:disable-line:no-any + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); }, returnJsonArray, @@ -547,7 +547,7 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType: string | undefined = undefined, property: string = ''): Promise { // tslint:disable-line:no-any + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); }, returnJsonArray, @@ -629,7 +629,7 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData: helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType: string | undefined, property: string = ''): Promise { // tslint:disable-line:no-any + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); }, }, @@ -679,7 +679,7 @@ export function getLoadOptionsFunctions(workflow: Workflow, node: INode, additio }, helpers: { request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType: string | undefined = undefined, property: string = ''): Promise { // tslint:disable-line:no-any + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); }, }, @@ -737,7 +737,7 @@ export function getExecuteHookFunctions(workflow: Workflow, node: INode, additio }, helpers: { request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType: string | undefined = undefined, property: string = ''): Promise { // tslint:disable-line:no-any + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); }, }, @@ -822,7 +822,7 @@ export function getExecuteWebhookFunctions(workflow: Workflow, node: INode, addi helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType: string | undefined = undefined, property: string = ''): Promise { // tslint:disable-line:no-any + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); }, returnJsonArray, diff --git a/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts b/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts index 6c5284ee44..b56699fe68 100644 --- a/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts @@ -12,7 +12,7 @@ const userScopes = [ 'files:write', 'stars:read', 'stars:write', -] +]; export class SlackOAuth2Api implements ICredentialType { diff --git a/packages/nodes-base/nodes/Slack/ChannelDescription.ts b/packages/nodes-base/nodes/Slack/ChannelDescription.ts index cda5824105..8e279bbeab 100644 --- a/packages/nodes-base/nodes/Slack/ChannelDescription.ts +++ b/packages/nodes-base/nodes/Slack/ChannelDescription.ts @@ -234,7 +234,10 @@ export const channelFields = [ { displayName: 'User ID', name: 'userId', - type: 'string', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, default: '', displayOptions: { show: { @@ -321,9 +324,12 @@ export const channelFields = [ description: 'The name of the channel to create.', }, { - displayName: 'User ID', + displayName: 'User', name: 'userId', - type: 'string', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, displayOptions: { show: { operation: [ diff --git a/packages/nodes-base/nodes/Slack/GenericFunctions.ts b/packages/nodes-base/nodes/Slack/GenericFunctions.ts index b73de1cac5..ff4769cb8e 100644 --- a/packages/nodes-base/nodes/Slack/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Slack/GenericFunctions.ts @@ -60,7 +60,7 @@ export async function slackApiRequest(this: IExecuteFunctions | IExecuteSingleFu } } -export async function salckApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any +export async function slackApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any const returnData: IDataObject[] = []; let responseData; query.page = 1; @@ -73,7 +73,7 @@ export async function salckApiRequestAllItems(this: IExecuteFunctions | ILoadOpt } while ( (responseData.response_metadata !== undefined && responseData.response_metadata.mext_cursor !== undefined && - responseData.response_metadata.next_cursor !== "" && + responseData.response_metadata.next_cursor !== '' && responseData.response_metadata.next_cursor !== null) || (responseData.paging !== undefined && responseData.paging.pages !== undefined && diff --git a/packages/nodes-base/nodes/Slack/MessageDescription.ts b/packages/nodes-base/nodes/Slack/MessageDescription.ts index 8c582503e5..8b077e6bab 100644 --- a/packages/nodes-base/nodes/Slack/MessageDescription.ts +++ b/packages/nodes-base/nodes/Slack/MessageDescription.ts @@ -43,7 +43,7 @@ export const messageFields = [ displayOptions: { show: { operation: [ - 'post' + 'post', ], resource: [ 'message', @@ -64,7 +64,7 @@ export const messageFields = [ displayOptions: { show: { operation: [ - 'post' + 'post', ], resource: [ 'message', @@ -80,6 +80,9 @@ export const messageFields = [ default: false, displayOptions: { show: { + authentication: [ + 'accessToken', + ], operation: [ 'post' ], @@ -98,10 +101,10 @@ export const messageFields = [ displayOptions: { show: { as_user: [ - false + false, ], operation: [ - 'post' + 'post', ], resource: [ 'message', @@ -121,7 +124,7 @@ export const messageFields = [ displayOptions: { show: { operation: [ - 'post' + 'post', ], resource: [ 'message', @@ -411,9 +414,12 @@ export const messageFields = [ /* message:update */ /* ----------------------------------------------------------------------- */ { - displayName: 'Channel ID', + displayName: 'Channel', name: 'channelId', - type: 'string', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, required: true, default: '', displayOptions: { @@ -471,6 +477,9 @@ export const messageFields = [ default: false, displayOptions: { show: { + authentication: [ + 'accessToken', + ], operation: [ 'update' ], diff --git a/packages/nodes-base/nodes/Slack/Slack.node.ts b/packages/nodes-base/nodes/Slack/Slack.node.ts index 1afeb4ebd0..2b2061d4e3 100644 --- a/packages/nodes-base/nodes/Slack/Slack.node.ts +++ b/packages/nodes-base/nodes/Slack/Slack.node.ts @@ -11,24 +11,24 @@ import { INodePropertyOptions, } from 'n8n-workflow'; import { - channelOperations, channelFields, + channelOperations, } from './ChannelDescription'; import { - messageOperations, messageFields, + messageOperations, } from './MessageDescription'; import { - starOperations, starFields, + starOperations, } from './StarDescription'; import { - fileOperations, fileFields, + fileOperations, } from './FileDescription'; import { slackApiRequest, - salckApiRequestAllItems, + slackApiRequestAllItems, } from './GenericFunctions'; import { IAttachment, @@ -133,7 +133,7 @@ export class Slack implements INodeType { // select them easily async getUsers(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - const users = await salckApiRequestAllItems.call(this, 'members', 'GET', '/users.list'); + const users = await slackApiRequestAllItems.call(this, 'members', 'GET', '/users.list'); for (const user of users) { const userName = user.name; const userId = user.id; @@ -142,13 +142,20 @@ export class Slack implements INodeType { value: userId, }); } + + returnData.sort((a, b) => { + if (a.name < b.name) { return -1; } + if (a.name > b.name) { return 1; } + return 0; + }); + return returnData; }, // Get all the users to display them to user so that he can // select them easily async getChannels(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - const channels = await salckApiRequestAllItems.call(this, 'channels', 'GET', '/conversations.list'); + const channels = await slackApiRequestAllItems.call(this, 'channels', 'GET', '/conversations.list'); for (const channel of channels) { const channelName = channel.name; const channelId = channel.id; @@ -157,6 +164,13 @@ export class Slack implements INodeType { value: channelId, }); } + + returnData.sort((a, b) => { + if (a.name < b.name) { return -1; } + if (a.name > b.name) { return 1; } + return 0; + }); + return returnData; }, } @@ -168,10 +182,12 @@ export class Slack implements INodeType { const length = items.length as unknown as number; let qs: IDataObject; let responseData; + const authentication = this.getNodeParameter('authentication', 0) as string; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { qs = {}; - const resource = this.getNodeParameter('resource', 0) as string; - const operation = this.getNodeParameter('operation', 0) as string; if (resource === 'channel') { //https://api.slack.com/methods/conversations.archive if (operation === 'archive') { @@ -203,6 +219,7 @@ export class Slack implements INodeType { body.user_ids = (additionalFields.users as string[]).join(','); } responseData = await slackApiRequest.call(this, 'POST', '/conversations.create', body, qs); + responseData = responseData.channel; } //https://api.slack.com/methods/conversations.kick if (operation === 'kick') { @@ -241,7 +258,7 @@ export class Slack implements INodeType { qs.exclude_archived = filters.excludeArchived as boolean; } if (returnAll === true) { - responseData = await salckApiRequestAllItems.call(this, 'channels', 'GET', '/conversations.list', {}, qs); + responseData = await slackApiRequestAllItems.call(this, 'channels', 'GET', '/conversations.list', {}, qs); } else { qs.limit = this.getNodeParameter('limit', i) as number; responseData = await slackApiRequest.call(this, 'GET', '/conversations.list', {}, qs); @@ -264,7 +281,7 @@ export class Slack implements INodeType { qs.oldest = filters.oldest as string; } if (returnAll === true) { - responseData = await salckApiRequestAllItems.call(this, 'messages', 'GET', '/conversations.history', {}, qs); + responseData = await slackApiRequestAllItems.call(this, 'messages', 'GET', '/conversations.history', {}, qs); } else { qs.limit = this.getNodeParameter('limit', i) as number; responseData = await slackApiRequest.call(this, 'GET', '/conversations.history', {}, qs); @@ -335,7 +352,7 @@ export class Slack implements INodeType { qs.oldest = filters.oldest as string; } if (returnAll === true) { - responseData = await salckApiRequestAllItems.call(this, 'messages', 'GET', '/conversations.replies', {}, qs); + responseData = await slackApiRequestAllItems.call(this, 'messages', 'GET', '/conversations.replies', {}, qs); } else { qs.limit = this.getNodeParameter('limit', i) as number; responseData = await slackApiRequest.call(this, 'GET', '/conversations.replies', {}, qs); @@ -379,15 +396,18 @@ export class Slack implements INodeType { const channel = this.getNodeParameter('channel', i) as string; const text = this.getNodeParameter('text', i) as string; const attachments = this.getNodeParameter('attachments', i, []) as unknown as IAttachment[]; - const as_user = this.getNodeParameter('as_user', i) as boolean; const body: IDataObject = { - channel: channel, + channel, text, - as_user, }; - if (as_user === false) { + + if (authentication === 'accessToken') { + body.as_user = this.getNodeParameter('as_user', i) as boolean; + } + if (body.as_user === false) { body.username = this.getNodeParameter('username', i) as string; } + // The node does save the fields data differently than the API // expects so fix the data befre we send the request for (const attachment of attachments) { @@ -411,17 +431,20 @@ export class Slack implements INodeType { } //https://api.slack.com/methods/chat.update if (operation === 'update') { - const channel = this.getNodeParameter('channel', i) as string; + const channel = this.getNodeParameter('channelId', i) as string; const text = this.getNodeParameter('text', i) as string; const ts = this.getNodeParameter('ts', i) as string; - const as_user = this.getNodeParameter('as_user', i) as boolean; const attachments = this.getNodeParameter('attachments', i, []) as unknown as IAttachment[]; const body: IDataObject = { channel, text, ts, - as_user, }; + + if (authentication === 'accessToken') { + body.as_user = this.getNodeParameter('as_user', i) as boolean; + } + // The node does save the fields data differently than the API // expects so fix the data befre we send the request for (const attachment of attachments) { @@ -485,7 +508,7 @@ export class Slack implements INodeType { if (operation === 'getAll') { const returnAll = this.getNodeParameter('returnAll', i) as boolean; if (returnAll === true) { - responseData = await salckApiRequestAllItems.call(this, 'items', 'GET', '/stars.list', {}, qs); + responseData = await slackApiRequestAllItems.call(this, 'items', 'GET', '/stars.list', {}, qs); } else { qs.limit = this.getNodeParameter('limit', i) as number; responseData = await slackApiRequest.call(this, 'GET', '/stars.list', {}, qs); @@ -522,15 +545,15 @@ export class Slack implements INodeType { throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`); } body.file = { + //@ts-ignore + value: Buffer.from(items[i].binary[binaryPropertyName].data, BINARY_ENCODING), + options: { //@ts-ignore - value: Buffer.from(items[i].binary[binaryPropertyName].data, BINARY_ENCODING), - options: { - //@ts-ignore - filename: items[i].binary[binaryPropertyName].fileName, - //@ts-ignore - contentType: items[i].binary[binaryPropertyName].mimeType, - } + filename: items[i].binary[binaryPropertyName].fileName, + //@ts-ignore + contentType: items[i].binary[binaryPropertyName].mimeType, } + }; responseData = await slackApiRequest.call(this, 'POST', '/files.upload', {}, qs, { 'Content-Type': 'multipart/form-data' }, { formData: body }); responseData = responseData.file; } else { @@ -563,7 +586,7 @@ export class Slack implements INodeType { qs.user = filters.userId as string; } if (returnAll === true) { - responseData = await salckApiRequestAllItems.call(this, 'files', 'GET', '/files.list', {}, qs); + responseData = await slackApiRequestAllItems.call(this, 'files', 'GET', '/files.list', {}, qs); } else { qs.count = this.getNodeParameter('limit', i) as number; responseData = await slackApiRequest.call(this, 'GET', '/files.list', {}, qs); diff --git a/packages/nodes-base/nodes/Slack/StarDescription.ts b/packages/nodes-base/nodes/Slack/StarDescription.ts index 39174b6a87..282025b1ea 100644 --- a/packages/nodes-base/nodes/Slack/StarDescription.ts +++ b/packages/nodes-base/nodes/Slack/StarDescription.ts @@ -26,7 +26,7 @@ export const starOperations = [ { name: 'Get All', value: 'getAll', - description: 'Get all stars for a user.', + description: 'Get all stars of autenticated user.', }, ], default: 'add', @@ -60,7 +60,10 @@ export const starFields = [ { displayName: 'Channel ID', name: 'channelId', - type: 'string', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, default: '', description: 'Channel to add star to, or channel where the message to add star to was posted (used with timestamp).', }, @@ -111,7 +114,10 @@ export const starFields = [ { displayName: 'Channel ID', name: 'channelId', - type: 'string', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, default: '', description: 'Channel to add star to, or channel where the message to add star to was posted (used with timestamp).', }, From 3fd4884667b167d4773001efb8cc2406328a6113 Mon Sep 17 00:00:00 2001 From: ricardo Date: Sun, 15 Mar 2020 19:51:31 -0400 Subject: [PATCH 022/165] :sparkles: Microsoft Excel node --- packages/cli/src/Server.ts | 4 +- .../MicrosoftOAuth2Api.credentials.ts | 45 ++ .../nodes/Microsoft/GenericFunctions.ts | 73 +++ .../nodes/Microsoft/MicrosoftExcel.node.ts | 335 +++++++++++++ .../nodes/Microsoft/TableDescription.ts | 447 ++++++++++++++++++ .../nodes/Microsoft/WorkbookDescription.ts | 154 ++++++ .../nodes/Microsoft/WorksheetDescription.ts | 283 +++++++++++ packages/nodes-base/nodes/Microsoft/excel.png | Bin 0 -> 5984 bytes packages/nodes-base/package.json | 6 +- 9 files changed, 1344 insertions(+), 3 deletions(-) create mode 100644 packages/nodes-base/credentials/MicrosoftOAuth2Api.credentials.ts create mode 100644 packages/nodes-base/nodes/Microsoft/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Microsoft/MicrosoftExcel.node.ts create mode 100644 packages/nodes-base/nodes/Microsoft/TableDescription.ts create mode 100644 packages/nodes-base/nodes/Microsoft/WorkbookDescription.ts create mode 100644 packages/nodes-base/nodes/Microsoft/WorksheetDescription.ts create mode 100644 packages/nodes-base/nodes/Microsoft/excel.png diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 4fe1ef70d7..5a1c0703bf 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -203,7 +203,9 @@ class App { }); } - jwt.verify(token, getKey, {}, (err: Error, decoded: string) => { + jwt.verify(token, getKey, {}, (err: Error, + //decoded: string + ) => { if (err) return ResponseHelper.jwtAuthAuthorizationError(res, "Invalid token"); next(); diff --git a/packages/nodes-base/credentials/MicrosoftOAuth2Api.credentials.ts b/packages/nodes-base/credentials/MicrosoftOAuth2Api.credentials.ts new file mode 100644 index 0000000000..2519ba583b --- /dev/null +++ b/packages/nodes-base/credentials/MicrosoftOAuth2Api.credentials.ts @@ -0,0 +1,45 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class MicrosoftOAuth2Api implements ICredentialType { + name = 'microsoftOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Microsoft OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'string' as NodePropertyTypes, + default: 'https://login.microsoftonline.com/{yourtenantid}/oauth2/v2.0/authorize', + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'string' as NodePropertyTypes, + default: 'https://login.microsoftonline.com/{yourtenantid}/oauth2/v2.0/token', + }, + //https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: 'openid offline_access Files.ReadWrite', + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: 'response_mode=query', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'body', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Microsoft/GenericFunctions.ts b/packages/nodes-base/nodes/Microsoft/GenericFunctions.ts new file mode 100644 index 0000000000..b4f68be343 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/GenericFunctions.ts @@ -0,0 +1,73 @@ +import { OptionsWithUri } from 'request'; +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; +import { + IDataObject +} from 'n8n-workflow'; + +export async function microsoftApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}): Promise { // tslint:disable-line:no-any + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || `https://graph.microsoft.com/v1.0/me${resource}`, + json: true + }; + try { + if (Object.keys(headers).length !== 0) { + options.headers = Object.assign({}, options.headers, headers); + } + //@ts-ignore + return await this.helpers.requestOAuth.call(this, 'microsoftOAuth2Api', options); + } catch (error) { + if (error.response && error.response.body && error.response.body.error && error.response.body.error.message) { + // Try to return the error prettier + throw new Error(`Microsoft error response [${error.statusCode}]: ${error.response.body.error.message}`); + } + throw error; + } +} + +export async function microsoftApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + let uri: string | undefined; + query['$top'] = 100; + + do { + responseData = await microsoftApiRequest.call(this, method, endpoint, body, query, uri); + uri = responseData['@odata.nextLink']; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData['@odata.nextLink'] !== undefined + ); + + return returnData; +} + +export async function microsoftApiRequestAllItemsSkip(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + query['$top'] = 100; + query['$skip'] = 0; + + do { + responseData = await microsoftApiRequest.call(this, method, endpoint, body, query); + query['$skip'] += query['$top']; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData['value'].length !== 0 + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Microsoft/MicrosoftExcel.node.ts b/packages/nodes-base/nodes/Microsoft/MicrosoftExcel.node.ts new file mode 100644 index 0000000000..c62730449d --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/MicrosoftExcel.node.ts @@ -0,0 +1,335 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeTypeDescription, + INodeType, + ILoadOptionsFunctions, + INodePropertyOptions, +} from 'n8n-workflow'; + +import { + microsoftApiRequest, + microsoftApiRequestAllItems, + microsoftApiRequestAllItemsSkip, +} from './GenericFunctions'; + +import { + workbookOperations, + workbookFields, +} from './WorkbookDescription'; + +import { + worksheetOperations, + worksheetFields, +} from './WorksheetDescription'; + +import { + tableOperations, + tableFields, +} from './TableDescription'; + +export class MicrosoftExcel implements INodeType { + description: INodeTypeDescription = { + displayName: 'Microsoft Excel', + name: 'microsoftExcel', + icon: 'file:excel.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Microsoft Excel API.', + defaults: { + name: 'Microsoft Excel', + color: '#1c6d40', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'microsoftOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Table', + value: 'table', + description: 'Represents an Excel table.', + }, + { + name: 'Workbook', + value: 'workbook', + description: 'Workbook is the top level object which contains related workbook objects such as worksheets, tables, ranges, etc.', + }, + { + name: 'Worksheet', + value: 'worksheet', + description: 'An Excel worksheet is a grid of cells. It can contain data, tables, charts, etc.', + }, + ], + default: 'workbook', + description: 'The resource to operate on.', + }, + ...workbookOperations, + ...workbookFields, + ...worksheetOperations, + ...worksheetFields, + ...tableOperations, + ...tableFields, + ], + }; + + methods = { + loadOptions: { + // Get all the workbooks to display them to user so that he can + // select them easily + async getWorkbooks(this: ILoadOptionsFunctions): Promise { + const qs: IDataObject = { + select: 'id,name', + }; + const returnData: INodePropertyOptions[] = []; + const workbooks = await microsoftApiRequestAllItems.call(this, 'value', 'GET', `/drive/root/search(q='.xlsx')`, {}, qs); + for (const workbook of workbooks) { + const workbookName = workbook.name; + const workbookId = workbook.id; + returnData.push({ + name: workbookName, + value: workbookId, + }); + } + return returnData; + }, + // Get all the worksheets to display them to user so that he can + // select them easily + async getworksheets(this: ILoadOptionsFunctions): Promise { + const workbookId = this.getCurrentNodeParameter('workbook'); + const qs: IDataObject = { + select: 'id,name', + }; + const returnData: INodePropertyOptions[] = []; + const worksheets = await microsoftApiRequestAllItems.call(this, 'value', 'GET', `/drive/items/${workbookId}/workbook/worksheets`, {}, qs); + for (const worksheet of worksheets) { + const worksheetName = worksheet.name; + const worksheetId = worksheet.id; + returnData.push({ + name: worksheetName, + value: worksheetId, + }); + } + return returnData; + }, + // Get all the tables to display them to user so that he can + // select them easily + async getTables(this: ILoadOptionsFunctions): Promise { + const workbookId = this.getCurrentNodeParameter('workbook'); + const worksheetId = this.getCurrentNodeParameter('worksheet'); + const qs: IDataObject = { + select: 'id,name', + }; + const returnData: INodePropertyOptions[] = []; + const tables = await microsoftApiRequestAllItems.call(this, 'value', 'GET', `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables`, {}, qs); + for (const table of tables) { + const tableName = table.name; + const tableId = table.id; + returnData.push({ + name: tableName, + value: tableId, + }); + } + 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 = {}; + const result: IDataObject[] = []; + const object: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + if (resource === 'table') { + //https://docs.microsoft.com/en-us/graph/api/table-post-rows?view=graph-rest-1.0&tabs=http + if (operation === 'addRow') { + const workbookId = this.getNodeParameter('workbook', 0) as string; + const worksheetId = this.getNodeParameter('worksheet', 0) as string; + const tableId = this.getNodeParameter('table', 0) as string; + const additionalFields = this.getNodeParameter('additionalFields', 0) as IDataObject; + const body: IDataObject = {}; + if (Object.keys(items[0].json).length === 0) { + throw new Error('Input cannot be empty'); + } + if (additionalFields.index) { + body.index = additionalFields.index as number; + } + const values: any[][] = []; + for (const item of items) { + values.push(Object.values(item.json)); + } + body.values = values; + const { id } = await microsoftApiRequest.call(this, 'POST', `/drive/items/${workbookId}/workbook/createSession`, { persistChanges: true }); + responseData = await microsoftApiRequest.call(this, 'POST', `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows/add`, body, {}, '', { 'workbook-session-id': id }); + await microsoftApiRequest.call(this, 'POST', `/drive/items/${workbookId}/workbook/closeSession`, {}, {}, '', { 'workbook-session-id': id }); + } + //https://docs.microsoft.com/en-us/graph/api/table-list-columns?view=graph-rest-1.0&tabs=http + if (operation === 'getColumns') { + for (let i = 0; i < length; i++) { + const workbookId = this.getNodeParameter('workbook', 0) as string; + const worksheetId = this.getNodeParameter('worksheet', 0) as string; + const tableId = this.getNodeParameter('table', 0) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const rawData = this.getNodeParameter('rawData', i) as boolean; + if (rawData) { + const filters = this.getNodeParameter('filters', i) as IDataObject; + if (filters.fields) { + qs['$select'] = filters.fields; + } + } + if (returnAll === true) { + responseData = await microsoftApiRequestAllItemsSkip.call(this, 'value', 'GET', `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, {}, qs); + } else { + qs['$top'] = this.getNodeParameter('limit', i) as number; + responseData = await microsoftApiRequest.call(this, 'GET', `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, {}, qs); + responseData = responseData.value; + } + if (!rawData) { + //@ts-ignore + responseData = responseData.map(column => ({ name: column.name })); + } + } + } + //https://docs.microsoft.com/en-us/graph/api/table-list-rows?view=graph-rest-1.0&tabs=http + if (operation === 'getRows') { + for (let i = 0; i < length; i++) { + const workbookId = this.getNodeParameter('workbook', 0) as string; + const worksheetId = this.getNodeParameter('worksheet', 0) as string; + const tableId = this.getNodeParameter('table', 0) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const rawData = this.getNodeParameter('rawData', i) as boolean; + if (rawData) { + const filters = this.getNodeParameter('filters', i) as IDataObject; + if (filters.fields) { + qs['$select'] = filters.fields; + } + } + if (returnAll === true) { + responseData = await microsoftApiRequestAllItemsSkip.call(this, 'value', 'GET', `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`, {}, qs); + } else { + qs['$top'] = this.getNodeParameter('limit', i) as number; + responseData = await microsoftApiRequest.call(this, 'GET', `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`, {}, qs); + responseData = responseData.value; + } + if (!rawData) { + qs['$select'] = 'name'; + let columns = await microsoftApiRequestAllItemsSkip.call(this, 'value', 'GET', `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, {}, qs); + //@ts-ignore + columns = columns.map(column => column.name); + for (let i = 0; i < responseData.length; i++) { + for (let y = 0; y < columns.length; y++) { + object[columns[y]] = responseData[i].values[0][y]; + } + result.push({ ...object }); + } + responseData = result; + } + } + } + } + if (resource === 'workbook') { + for (let i = 0; i < length; i++) { + //https://docs.microsoft.com/en-us/graph/api/worksheetcollection-add?view=graph-rest-1.0&tabs=http + if (operation === 'addWorksheet') { + const workbookId = this.getNodeParameter('workbook', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IDataObject = {}; + if (additionalFields.name) { + body.name = additionalFields.name; + } + const { id } = await microsoftApiRequest.call(this, 'POST', `/drive/items/${workbookId}/workbook/createSession`, { persistChanges: true }); + responseData = await microsoftApiRequest.call(this, 'POST', `/drive/items/${workbookId}/workbook/worksheets/add`, body, {}, '', { 'workbook-session-id': id }); + await microsoftApiRequest.call(this, 'POST', `/drive/items/${workbookId}/workbook/closeSession`, {}, {}, '', { 'workbook-session-id': id }); + } + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + if (filters.fields) { + qs['$select'] = filters.fields; + } + if (returnAll === true) { + responseData = await microsoftApiRequestAllItems.call(this, 'value', 'GET', `/drive/root/search(q='.xlsx')`, {}, qs); + } else { + qs['$top'] = this.getNodeParameter('limit', i) as number; + responseData = await microsoftApiRequest.call(this, 'GET', `/drive/root/search(q='.xlsx')`, {}, qs); + responseData = responseData.value; + } + } + } + } + if (resource === 'worksheet') { + for (let i = 0; i < length; i++) { + //https://docs.microsoft.com/en-us/graph/api/workbook-list-worksheets?view=graph-rest-1.0&tabs=http + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const workbookId = this.getNodeParameter('workbook', i) as string; + const filters = this.getNodeParameter('filters', i) as IDataObject; + if (filters.fields) { + qs['$select'] = filters.fields; + } + if (returnAll === true) { + responseData = await microsoftApiRequestAllItems.call(this, 'value', 'GET', `/drive/items/${workbookId}/workbook/worksheets`, {}, qs); + } else { + qs['$top'] = this.getNodeParameter('limit', i) as number; + responseData = await microsoftApiRequest.call(this, 'GET', `/drive/items/${workbookId}/workbook/worksheets`, {}, qs); + responseData = responseData.value; + } + } + //https://docs.microsoft.com/en-us/graph/api/worksheet-range?view=graph-rest-1.0&tabs=http + if (operation === 'getContent') { + const workbookId = this.getNodeParameter('workbook', i) as string; + const worksheetId = this.getNodeParameter('worksheet', i) as string; + const range = this.getNodeParameter('range', i) as string; + const rawData = this.getNodeParameter('rawData', i) as boolean; + if (rawData) { + const filters = this.getNodeParameter('filters', i) as IDataObject; + if (filters.fields) { + qs['$select'] = filters.fields; + } + } + responseData = await microsoftApiRequest.call(this, 'GET', `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`, {}, qs); + if (!rawData) { + const keyRow = this.getNodeParameter('keyRow', i) as number; + const dataStartRow = this.getNodeParameter('dataStartRow', i) as number; + if (responseData.values === null) { + throw new Error('Range did not return data'); + } + const keyValues = responseData.values[keyRow]; + for (let i = dataStartRow; i < responseData.values.length; i++) { + for (let y = 0; y < keyValues.length; y++) { + object[keyValues[y]] = responseData.values[i][y]; + } + result.push({ ...object }); + } + responseData = result; + } + } + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Microsoft/TableDescription.ts b/packages/nodes-base/nodes/Microsoft/TableDescription.ts new file mode 100644 index 0000000000..6dc6de780b --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/TableDescription.ts @@ -0,0 +1,447 @@ +import { INodeProperties } from "n8n-workflow"; + +export const tableOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'table', + ], + }, + }, + options: [ + { + name: 'Add Row', + value: 'addRow', + description: 'Adds rows to the end of the table' + }, + { + name: 'Get Columns', + value: 'getColumns', + description: 'Retrieve a list of tablecolumns', + }, + { + name: 'Get Rows', + value: 'getRows', + description: 'Retrieve a list of tablerows', + }, + ], + default: 'addRow', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const tableFields = [ + +/* -------------------------------------------------------------------------- */ +/* table:addRow */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Workbook', + name: 'workbook', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWorkbooks', + }, + displayOptions: { + show: { + operation: [ + 'addRow', + ], + resource: [ + 'table', + ], + }, + }, + default: '', + }, + { + displayName: 'Worksheet', + name: 'worksheet', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getworksheets', + loadOptionsDependsOn: [ + 'workbook', + ], + }, + displayOptions: { + show: { + operation: [ + 'addRow', + ], + resource: [ + 'table', + ], + }, + }, + default: '', + }, + { + displayName: 'Table', + name: 'table', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getTables', + loadOptionsDependsOn: [ + 'worksheet', + ], + }, + displayOptions: { + show: { + operation: [ + 'addRow', + ], + resource: [ + 'table', + ], + }, + }, + default: '', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'addRow', + ], + resource: [ + 'table', + ], + }, + }, + options: [ + { + displayName: 'Index', + name: 'index', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: `Specifies the relative position of the new row. If not defined,
+ the addition happens at the end. Any rows below the inserted row are shifted downwards. Zero-indexed`, + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* table:getRows */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Workbook', + name: 'workbook', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWorkbooks', + }, + displayOptions: { + show: { + operation: [ + 'getRows', + ], + resource: [ + 'table', + ], + }, + }, + default: '', + }, + { + displayName: 'Worksheet', + name: 'worksheet', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getworksheets', + loadOptionsDependsOn: [ + 'workbook', + ], + }, + displayOptions: { + show: { + operation: [ + 'getRows', + ], + resource: [ + 'table', + ], + }, + }, + default: '', + }, + { + displayName: 'Table', + name: 'table', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getTables', + loadOptionsDependsOn: [ + 'worksheet', + ], + }, + displayOptions: { + show: { + operation: [ + 'getRows', + ], + resource: [ + 'table', + ], + }, + }, + default: '', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getRows', + ], + resource: [ + 'table', + ], + }, + }, + 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: [ + 'getRows', + ], + resource: [ + 'table', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'RAW Data', + name: 'rawData', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getRows', + ], + resource: [ + 'table', + ], + }, + }, + default: false, + description: 'If the data should be returned RAW instead of parsed into keys according to their header.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + operation: [ + 'getRows', + ], + resource: [ + 'table', + ], + rawData: [ + true, + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: `Fields the response will containt. Multiple can be added separated by ,.`, + }, + ] + }, +/* -------------------------------------------------------------------------- */ +/* table:getColumns */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Workbook', + name: 'workbook', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWorkbooks', + }, + displayOptions: { + show: { + operation: [ + 'getColumns', + ], + resource: [ + 'table', + ], + }, + }, + default: '', + }, + { + displayName: 'Worksheet', + name: 'worksheet', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getworksheets', + loadOptionsDependsOn: [ + 'workbook', + ], + }, + displayOptions: { + show: { + operation: [ + 'getColumns', + ], + resource: [ + 'table', + ], + }, + }, + default: '', + }, + { + displayName: 'Table', + name: 'table', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getTables', + loadOptionsDependsOn: [ + 'worksheet', + ], + }, + displayOptions: { + show: { + operation: [ + 'getColumns', + ], + resource: [ + 'table', + ], + }, + }, + default: '', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getColumns', + ], + resource: [ + 'table', + ], + }, + }, + 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: [ + 'getColumns', + ], + resource: [ + 'table', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'RAW Data', + name: 'rawData', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getColumns', + ], + resource: [ + 'table', + ], + }, + }, + default: false, + description: 'If the data should be returned RAW instead of parsed into keys according to their header.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + operation: [ + 'getColumns', + ], + resource: [ + 'table', + ], + rawData: [ + true + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: `Fields the response will containt. Multiple can be added separated by ,.`, + }, + ] + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Microsoft/WorkbookDescription.ts b/packages/nodes-base/nodes/Microsoft/WorkbookDescription.ts new file mode 100644 index 0000000000..526a8ceb43 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/WorkbookDescription.ts @@ -0,0 +1,154 @@ +import { INodeProperties } from "n8n-workflow"; + +export const workbookOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'workbook', + ], + }, + }, + options: [ + { + name: 'Add Worksheet', + value: 'addWorksheet', + description: 'Adds a new worksheet to the workbook.', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get data of all workbooks', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const workbookFields = [ + +/* -------------------------------------------------------------------------- */ +/* workbook:addWorksheet */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Workbook', + name: 'workbook', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getWorkbooks', + }, + displayOptions: { + show: { + operation: [ + 'addWorksheet', + ], + resource: [ + 'workbook', + ], + }, + }, + default: '', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'addWorksheet', + ], + resource: [ + 'workbook', + ], + }, + }, + options: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: `The name of the worksheet to be added. If specified, name should be unqiue.
+ If not specified, Excel determines the name of the new worksheet.`, + }, + ] + }, +/* -------------------------------------------------------------------------- */ +/* workbook:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'workbook', + ], + }, + }, + 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: [ + 'workbook', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'workbook', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: `Fields the response will containt. Multiple can be added separated by ,.`, + }, + ] + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Microsoft/WorksheetDescription.ts b/packages/nodes-base/nodes/Microsoft/WorksheetDescription.ts new file mode 100644 index 0000000000..204e5f0e41 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/WorksheetDescription.ts @@ -0,0 +1,283 @@ +import { INodeProperties } from "n8n-workflow"; + +export const worksheetOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'worksheet', + ], + }, + }, + options: [ + { + name: 'Get All', + value: 'getAll', + description: 'Get all worksheets', + }, + { + name: 'Get Content', + value: 'getContent', + description: 'Get worksheet content', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const worksheetFields = [ + +/* -------------------------------------------------------------------------- */ +/* worksheet:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Workbook', + name: 'workbook', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWorkbooks', + }, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'worksheet', + ], + }, + }, + default: '', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'worksheet', + ], + }, + }, + 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: [ + 'worksheet', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'worksheet', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: `Fields the response will containt. Multiple can be added separated by ,.`, + }, + ] + }, +/* -------------------------------------------------------------------------- */ +/* worksheet:getContent */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Workbook', + name: 'workbook', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getWorkbooks', + }, + displayOptions: { + show: { + operation: [ + 'getContent', + ], + resource: [ + 'worksheet', + ], + }, + }, + default: '', + }, + { + displayName: 'Worksheet', + name: 'worksheet', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getworksheets', + loadOptionsDependsOn: [ + 'workbook', + ], + }, + displayOptions: { + show: { + operation: [ + 'getContent', + ], + resource: [ + 'worksheet', + ], + }, + }, + default: '', + }, + { + displayName: 'Range', + name: 'range', + type: 'string', + displayOptions: { + show: { + operation: [ + 'getContent', + ], + resource: [ + 'worksheet', + ], + }, + }, + default: 'A1:C3', + required: true, + description: 'The address or the name of the range. If not specified, the entire worksheet range is returned.', + }, + { + displayName: 'RAW Data', + name: 'rawData', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getContent', + ], + resource: [ + 'worksheet', + ], + }, + }, + default: false, + description: 'If the data should be returned RAW instead of parsed into keys according to their header.', + }, + { + displayName: 'Data Start Row', + name: 'dataStartRow', + type: 'number', + typeOptions: { + minValue: 1, + }, + default: 1, + displayOptions: { + show: { + operation: [ + 'getContent', + ], + resource: [ + 'worksheet', + ], + }, + hide: { + rawData: [ + true + ], + }, + }, + description: 'Index of the first row which contains
the actual data and not the keys. Starts with 0.', + }, + { + displayName: 'Key Row', + name: 'keyRow', + type: 'number', + typeOptions: { + minValue: 0, + }, + displayOptions: { + show: { + operation: [ + 'getContent', + ], + resource: [ + 'worksheet', + ], + }, + hide: { + rawData: [ + true + ], + }, + }, + default: 0, + description: 'Index of the row which contains the keys. Starts at 0.
The incoming node data is matched to the keys for assignment. The matching is case sensitve.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + operation: [ + 'getContent', + ], + resource: [ + 'worksheet', + ], + rawData: [ + true, + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: `Fields the response will containt. Multiple can be added separated by ,.`, + }, + ] + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Microsoft/excel.png b/packages/nodes-base/nodes/Microsoft/excel.png new file mode 100644 index 0000000000000000000000000000000000000000..40631413a5f656ad5819f9faada8eecc78509bd4 GIT binary patch literal 5984 zcmZ{Iby(ER*Z$HCDoE`DD@aO*OLwz0A`2q1q|~x3Qu+{rG!oLaNJ~mdNh+NSQj$wZ zNjCy7KHul}e1GpB@60uG?m2Ux`^=d?KG*e`a6KJ03Q|T=002Otp{{IjYxVvNV#3=w z=I%k+t-L0q|}S&TR+aFaikv#sGjO4)9-WfW!3<2NwVcbp+u5 z!!f0gV7#`&)z8uwqe9vbhz_)lD(gueZ)Awj6aJOBWGwm$>MJ74bQtrX1B z$OL7g4V8qux$s-rxLMosd%Gb1L;<9|C2ygNEy@b)?c(g}A?Yo{`Zq)J7XNVzu!8@l zpqyk_O|N ztbY>y+x~M-l%w5$Gr4;FQ`T*P0)KV{g!lyo{?)z>mHy+E)I~bl-e&%zFDoSdH}ik_ z{>dXP@F)5IcbNZF`nUJCR9RAKfq$<}mK55-2?GFVRy33qjJ$DvJtILqS7Fp~i^+^J`j%x_ ziq{Ed+|YvrsYC^FRlSw(NIT{`ojbp3x!mdR@N1TSQhq!f(-Mz7 zMw{Be^ca&F_}Z)-TJzR#$Gl0Cphocl%MrvG$KubJ z#-^=|K@5m~W}^7_l_w7Q7B{pZUm=%%0)C`zz!Kl3S}PVAtXxTDsIh#1V`wN#Sgl8h zIr$@0`^7hW6H|v8degWa_jUXTj3eW$qBWt?N^LBIaC?Yd1@pLW)B4Pz2B@vem(%mf zVa~w_KqiEQ&(L4{qwTi7tD>%{ew&+-LnChZtP!``B_QKDFaKP=1UQf}h59Vz0f3-Q z@fzHa093%@;W{WPv#pG)MEhB&HdMC=^>2UmW`$5o=M352&-a2C$1m6TbTFySIMck| zULpwjkiLu}(IyVe6*bI`O4V)aQFYW&V^kjD(Lwj=ETWw3Iy2@5&F^ z1ETPuq+negw zolVN{T(BTVldo=CzsFco_tVl)Gcji!s0q;rdzo>%U~7o4AZU{_A}gjn@ak*G0%r)< zr0iI%bEw&6FTsMBtvBW&z3{kF)DJ5;#1$CJm^!n8(6rZ0w)gpQ(N~8T`d3t4d9F~N zCJQAyIhc`Rj~hE2EmU*?ViCxIA{B64$x!{lN3VSLx-O&01sVh-#>Xb9&-&qB-jH*t z?qXVCdW?Iu_3?Yb003Ft;dsJvY>}+?#^JF9u_gG<9rMrRH1BHG_dFlsmjRukoI^jp zU!_1)DtU#m{3;?%TcaF?dO|W_9OR1#Mi~ZM8gS)zN~o-ejG^Zel462hbX!te?7kMr z8ulX|#~VOPcCZXVO#mDM4v|d!8{|j5Gi#pl38{PvZyu8w5;+(?dci)I&haZ$GH!uEa8w;AV2(s^_HE)`M z;emhZUj4+S`+na%M|nE8DQ|9pJtT{FU092*k$WOR)7Et#vf_Y-Sm(p!2A7h&GPb^L z?H8SAr;N;uz`7G~!JIR?bp;!(*4f3ec-599Ivm9OK$9_k$X?IA!8w<`g`hiQufA^<((kX~@1`4G+ts}wQ{n+dMnqnI=;bVZa60lBgCn?>B znQjA#y}B1bWv`@h=gva7nitQ=SyTVsqrKS?`%fHQNR)j!R}n2_6BG3Evj5>&0`sRX z{z5IyuIH?D_o46S(Nt_zfR%ZI>|G^9&gAR2NO1cWp}q4KJcU$8(AQni20CFP9)$n` zZ#|3mo0!}O8zH&S!<%HWa9Few-Sb9a zkGrH_@i|pJnP_pdL7jA!G;8A~Hh`(D;-1H|YIF4Uk5A0z{^2?H8q4ysR&w>FQ<)FO zs5ZngOH*VkLuSkgqEKlOJ|*|LR^Hdol6Dt( z?`MI&Pqeiw;wGj@wtJp8oMVxaZ1Pj9XNZjYtZ+W&yFKt;>;$}`yX9VM;gl!QoPw)n z3+aibifUjpVi#@@pn+60ni_8Y-OOw1UNOMzS&LPaIE$P8YimrnaHm5ZnB8$~(sIaG z4APHYKbh*)ms@L(mbkYbm$+rDrK6gJ-{DFwvC%vu+1vJJ%FD7<)W1$YoI7(fa25*b zA}TNAFNv+vhY#~Y-co0f5BWGv_E*7ouM9Nh9!Upk*|n9KkZ63vm))M>^x%9h;Q4)w z#`~>RQP|=_@(nF^$MB*W@4=6@@M>^8aXba2U~Yl!>6bIA`Vk@s2a78-R9!V^`|4bk zU@_)P(@HwkAYCyt8|L$vap1%o|%BECCCghOq)H>rKg}ly1*eIt6vSd{bBExy=@_uBoR@ayRf6Wh`eMT&vwaIT$q!F z(&G;B`I3#H4^PJ^3A}dKkOW^JBFR)D^&vU(nKom9^Jw>ChNJv0m(O|TD$;fEC@eAK ziHMS?@9ubieoYT)ZjTs?|8=-@?M@+bXT8LRr9K=)8%>N?CSJBL#_owFgpB&^;3V7D zkFmzo5P`(YHc3;{S!83+Rf%*iBhDk$lAf(-Kox)Aucu8FNz?)T~1Y?^*%ufbfG2L}$~*dbIzqd|fv|zIA!3j>5R}}hH*wt%J4yko%E)u>}2^5W)92eYK99m~lbx28B*UNtIUW}mMTkc)Qq zMekqc6*O!1Y4s2BLb|d<70~W_N#ol|H@;rAzb%i<`$CrRQ@%cQnLP-XzFw-x^Ibu0 zgye)jX!ywMl>}Ulzv}6FcJ;$eNPrSTXa=c+#!VE?&;&@$yzGB+YQZl|&^R9I(8KXk z{^dE;<~-pMFQ9$YJ_-nbm!S3*B~uxntCqn@b>mqq`s`Ic4INGU1wrEar-8oHg<$B3 zx@44sK*wkAujfBhS3Z22znfM zdI~qYAF3g7)gV1{WPj!2zE%Z^f}(WP>2%Wh$VzsSl3%tw`o0n3-VpoCAZo15)9cA| z{^x~e#RL|JtccGHcnK+qEf+yjHqNe}&WO(ZjJUV`b$iJZ2koXs;_)PUrR~$4#hX=H zdEd#aS7qbdK~LFD zurv8AeXATcqEg0y{XKM65%{+uYSFZ`1tp7^YJ}CMKKpCdwqiEN|`%<%owxszI%UP|K=# zDwDwyimI~ApUpzz$vH2F%btPfGDI-ZIe+n}KD)5G3_WNIk&7!%Okk6A+Zu8?=-9MS z?>t{12fh!`RGX__YF{D=cS(G$aTV>v}H8Y>03PGQq`P$ljM-_9WBT9krPi5w>OuF+nFI-tkWy4$139YS}qAt4)*LOctD;%R(Wra z@hvax@}v5-55m_kPxMsLXMOG_Hsr9SEWUi)%5gEwk8YN?Li(zi{tny@_4XoUg*HNH z(y#@xL0W6mCcFMoFs=b} zy0ve={KGftPMU<+tInk4DDC06n(xm6E_ch7tFYf#WRwOvL}@s-j;ZFMZ-EIklzH_+ zS!K%QzeXvvROVkzMYtDriXihP-ReLpt}!Hd{K4;}?6Y`%w+LVp&6=d{+5Pe7sSTAK z<{#KJ`o0zC1f76;8B0@?NU{S(h-^`EGzx_v;FKNFu*S{WFkr4R7MylC zoLwR;+B9?0lXkqzrN85U`t60bTGgk*il;**#R)tJo(BI)wv2jXC@B&0qh=JqfRiP&F0d(;!f0gz3vC!| zd3>ba)}tS4<-%75SZiU~Y_H{AEQ3%F3AB)YC@uL+-4l2IWYYqzR+i1%(t|9hEi#o-Cn z52OVHHkY)pRr$U?s#?pYK9qgNbWc}JQLWYEtok-)k-UV2A<4S*3jYwx!>cstdOK0U z!ov-I0YJcU==?EQBsWQ9l%lgiicT7=B5}SsFCS9?tZuc&;q}g9)B`CnOrUii5m7n? zFDVX;zq-GjB(dRhkYL8FcM&5wK&#(W#$y9d;`PN``^@J4>Qa_}JlOttwZ672{r2Z( z0#TM&2mVI$RqQSSuB}i|P@`CBrsfWo4QvW$A5qHg5;T=)0^oO#GF2HKyre|XA4Zwt zvzDQC)sBK7C4mo~Zc*n3fKAKnj3yLw@NKzy_EEN2j<{ z){cP0Z;^tgy$_8q^9~)*KaQ3|ly()KG~p!KGa%_Y(ZM*u8239ecj#zSC_e1%sb9I{ z1_hgTf-ge7duN>wy&R8Q&;3d%P$pkqGa2@c#xsEk9Bt1i6E^`#L^eU$3M2l=srLxzW8Y&R^hFq~@`1I-94BShr=81M-q`u-Uv z?&3lrDsb-6kwh()pvnobG_361%&N{2kC?hi3$~Epc|q#p=G^fpPcd`AC8ROReERB` z4LT>GL-w{y=^Uy^cD7={q3D#@LD4@cP1!6Ld}`yYcdkIeC$|r`2%eJslrCM?{S{}M zr93Uhn$)poh{&JyB#b5VX9f zqAX^^1Qa)hIvWs*wRx%^>vAf}vW=lm{N-K55Emy_F%l|xXmb8?uW|Lj9jQClpE(WQ z+^mci9W9RNL+f{i-^JM_F(~BHFy0AFDqdR9#%Jf;Yr!Jw?0efrJ{E4D783~0)DU;n zQuxYz&AyRlJ$UDZy}=`nnZy2e_?7j+sgR{@ow~M5#oPHxvhvw01W}SsX5s(woOr{4 bdqb4Qmz%rb(0TCZ=~zQWN4Z?lGU$H*2Ppz* literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 746f91f723..47f21e8396 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -54,7 +54,8 @@ "dist/credentials/MailchimpApi.credentials.js", "dist/credentials/MailgunApi.credentials.js", "dist/credentials/MandrillApi.credentials.js", - "dist/credentials/MattermostApi.credentials.js", + "dist/credentials/MattermostApi.credentials.js", + "dist/credentials/MicrosoftOAuth2Api.credentials.js", "dist/credentials/MongoDb.credentials.js", "dist/credentials/MySql.credentials.js", "dist/credentials/NextCloudApi.credentials.js", @@ -132,7 +133,8 @@ "dist/nodes/Mailgun/Mailgun.node.js", "dist/nodes/Mandrill/Mandrill.node.js", "dist/nodes/Mattermost/Mattermost.node.js", - "dist/nodes/Merge.node.js", + "dist/nodes/Merge.node.js", + "dist/nodes/Microsoft/MicrosoftExcel.node.js", "dist/nodes/MoveBinaryData.node.js", "dist/nodes/MongoDb/MongoDb.node.js", "dist/nodes/MySql/MySql.node.js", From f45a7cb031009bf0ba6d1ccd57cac9118b57f66b Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 16 Mar 2020 14:34:04 +0100 Subject: [PATCH 023/165] :zap: Minor change to Google Calendar-Node --- packages/nodes-base/nodes/Google/EventDescription.ts | 12 ++++++------ .../nodes-base/nodes/Google/GoogleCalendar.node.ts | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/nodes-base/nodes/Google/EventDescription.ts b/packages/nodes-base/nodes/Google/EventDescription.ts index fb1ea6a90f..e92582a847 100644 --- a/packages/nodes-base/nodes/Google/EventDescription.ts +++ b/packages/nodes-base/nodes/Google/EventDescription.ts @@ -316,7 +316,7 @@ export const eventFields = [ loadOptionsMethod: 'getTimezones', }, default: '', - description: 'The timezone the event will have set. By default events are schedule on n8n timezone ' + description: 'The timezone the event will have set. By default events are schedule on timezone set in n8n.' }, { displayName: 'Visibility', @@ -333,16 +333,16 @@ export const eventFields = [ value: 'default', description: ' Uses the default visibility for events on the calendar.', }, - { - name: 'Public', - value: 'public', - description: 'The event is public and event details are visible to all readers of the calendar.', - }, { name: 'Private', value: 'private', description: 'The event is private and only event attendees may view event details.', }, + { + name: 'Public', + value: 'public', + description: 'The event is public and event details are visible to all readers of the calendar.', + }, ], default: 'default', description: 'Visibility of the event.', diff --git a/packages/nodes-base/nodes/Google/GoogleCalendar.node.ts b/packages/nodes-base/nodes/Google/GoogleCalendar.node.ts index fa6a8b28c7..c7eb35794e 100644 --- a/packages/nodes-base/nodes/Google/GoogleCalendar.node.ts +++ b/packages/nodes-base/nodes/Google/GoogleCalendar.node.ts @@ -89,7 +89,7 @@ export class GoogleCalendar implements INodeType { async getColors(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; const { calendar } = await googleApiRequest.call(this, 'GET', '/calendar/v3/colors'); - for (let key of Object.keys(calendar)) { + for (const key of Object.keys(calendar)) { const colorName = calendar[key].background; const colorId = key; returnData.push({ From 5ff4f836096c77df4549bd231f1c0afd0e687641 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 16 Mar 2020 14:55:50 +0100 Subject: [PATCH 024/165] :zap: Optimize Google Calendar icon --- .../nodes/Google/googleCalendar.png | Bin 6937 -> 1843 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/nodes-base/nodes/Google/googleCalendar.png b/packages/nodes-base/nodes/Google/googleCalendar.png index 2a2bfa1c7b4754b9d4695b0beab270a62ab0a4da..31e365fdbd6944d92c8d335d8715f99f67258e70 100644 GIT binary patch literal 1843 zcmV-32h8}1P)S(8I&SMuqW2d*e!S&@W)nOmxvOVbCjG&@o@mNs9Hz*6&4u>qC9#M1ABt zZrU+m(8k5Xy1c-Pu<$~G?LU9*M1SaZk3q^X}N-?&<36t;6h?wd{DI>xZlANrLJ&b?EKq-00-n zGH2G@+0(+u(y_eKkfhRWi_vF?(OG@bJ!#N8Xwc8k&nQ{X%E`#KxV=b;^}*8bz|ifs z%I$@#?P8VfgsAIPjO$H?>XESNJ9+0mdgk=)-P_ySyTseI!P|y zp5?{E84~*T?d;v#(#yrLrHFiSYGq|dJTE9B{P^?l>D=4b%*x2Zz`D1sq@9|Qkcfze zgm!aiU|?2GL^>4Z+Z~|*00YxWL_t(Y$L-T)lhZ%|fZ>C~-QC^Y-5Q!Bw7KgYZIW^! z;b=<>Z7EP_p&W899PaM!?(XjHF8_heMrg@qhEIIq-pRB{W}eyX&TdqG|6e7m7b&1s zks|Q7dPxmCsA3F35HSq6z)g4zF^vEh!!Sfe4c(210TFr24qpRz;StmFmMzO;2+*{y zXj};rW=-<$HFN&N;N|mO@FR+A;24Ml^OLXr{1XR~lk?`lSrtR$;=^FX4cDAqjB8j} zE*B%dbh(CU;UDp^oj1pT;v*X)yxh4nJ|2y0l}V7BDkMLQkV+IQQww2xU#0Y@D2k$K zn&UX9(>WUy($`SqW=5S>Mx#*=8!R-kyc(C){UweotHu@n#zI^{jVu2(uB^sYHSM3c zsv7qR2TqoDvXp}LEKe&ouCE%``Az977dLJEvL%5NEzMI2n{!zO*HPmVr4@Vm^OPy; zvx9*xoKg6QZ(Gj?f?N5)5-wF=jibv;R-gEB-OSvC%NN#XUu1R8?HhA~>jG!T@$$e$ z3r4AHE6;aR@VqWWar}jK8;p8>^ZT5QTQ_8nn%?UfLmSRcb^|U^{E%c#fjOaZeN;pez^%w>h7C_R|h{SVRmh zs6PtVFRfFeLLq*~hT!IqUWH55L*ar&jdF2Qw{81+Zv7^GocOIHzh{rTK2i)UHma6u z*RGumJM-$vI>9Ha{Fm{1xvBo7(xJ08QpuxB+Rsd`Fdi{%1(jYcZ z5JtwV5I01wcHIUzp5(JQxF!ns!ezbm*qwcQ4=+l~kSs~wJ}8fDFjPa~?zq$~)AXf}9xirI zcgLCqY{9TSdwjAbyP$972csybS;La$qEUEmq$bS~)*x9!bJVe_HdoY27Nj-wKpx2|R#QY6X#tUu hxvNA+<(J>T=@;apTA+xJPu~Cl002ovPDHLkV1fao=4t={ literal 6937 zcmY*;1z1#F7w*iE(jbk%NaxTn;Ls)AFd*HXLrI6^AYDp_3eqVtNJ$H#bayH-fPf%g z{QmF$|GoP>`>c1b^R9R8bfD)-J;MAO0KGyYN{3-aPPpYU~35;6MD^ zfq++e)c0z}PWo^JTti*L#={L_W$R&W2MKWV{2K*;1xVaOH#>wCIKa)--A5uolIb6Y z#6AA&=3@f?LqWJmGQl;p!SWv7c3=^RFod5;3J(kh!@O({tdq$NisPi5S|iz zeE$Ca5Pu+Zw+ACdp7qhRM_k-MANw7tpX>2t<^s> zU%O(XIDFx$B3C5Y6AHH%CKaRsD#f2iAJjMcZLpayp8BB7kL~?YwQWMTp{uz<{zpq~ zm*efS3XaJk$9dwrL3iW+1w#Icl;4;@iMTvWb#dK)!R;z|Eg0Y|jZ4EAPGiK&)ag84 z7<<|sf$@T$V7X9vAU2})sY3R^XXg++R!7+jIJV5 zO)h1FjU}gK&1Q%ugz`&sAzzlJW@cJ8lf+i8u)?oCTnN;fwV~Fa^)9oJKPT&bL4cLU z($dm1ZMnNKuP+jcu@VvsM7*$~bgL`|+uAU&AgmlP&ChTa}62h3*=4%j+7 zt5z`_jC{p0k8#KC+87!d!dPr^TXYEdY$Jb!+Zk|L67vul9N_1d)$qW4%mb*#RjyaA zt72GfQvFF3J)S3CI^MuPT2Y2H3_NHqG(e%gPR>_Y);PJO7Z*QvYqG9?XWif$9ZIz= z6#|+gj`A$LXwGz>83YJ3$AI>{xH=gmpCcrhA8@|x#Exe^TcUVBkT9o@86 z6@=H%-W@ye?qn!j!aM65rynz|a^1*wn!2VYsN2bR>}8_w7(P#tB<9gbCN~}>UA)BL z;A(bTn|L*|MtWIScuZF_CNC$u5hfcANB){V*HF1{eVX4!Wq0qMIMnZ8D(LI`jOD9)a9 z*VP%DuFZ|lbL%3ZA-&{--khNITbkUQoG-%|johwVkJd|1SY>Ww7T|ozB$Q5iu=jlXQ?|v8$HxtK#o2z8-fCH@EIIj;7!n&QYq7J_^|5!2 z%;WgWVKb6p0=H)+bW*o>$P6yvZ_$pl=U9kwFijgLuq4`& zriofw$yadA8O=52y~*~(<lM4YMrp z?vRV12|+FS<;%Kti5OYh2W@kb!=&L%CR$PrDcL>{8vZctRu##oA1f3AMM-koIjy7D z-n=$k`hEWqa3+i4z|Mk%xJh9D{6XOXz!=ifLQ0GCs&dI1H!n+fs(k1{JrN%$rnKZcx-?Y!_ltD6>h+O~t@64yFdM^8+i0KO=qaG`zF^ zxy0LU5w?2KgUs_h>TgA5rR>8~>WIwREG}MYPClVpg7azdo?l(K7(0!+-yleUD9j4l zKc93)x6S^#Y-Wjv;=(SE$91cm-j$_RCs6mf;iU$;+?O@byc-=7 z#j2~V)xy!9^@;_B(SCoNjc)rs^rd{195k2=F(y-(hU9P{8>-1o%!8WAYCwpFfb(YE z2b$XBGSLy4xn1|+7C*UA$Kx-p^9Ai4c=~@7mAE$W@LJ z0L7xn)LT04SeYj*Q?lHSUxPS)YgG`A&X{%qsAB2ATcxJ?8H*FvVkQnO=IhhKDCYV* z*bFMubsUP`i@g5{DV%H<>2}6oAi8vY#At{`ik-YCNKV z28Rl#7_Aa+EwY2=XZD|x!p#mPErBv(#8UN z9J8SX8NxwlSn)b+(ma@-!IFRg^;nnD!Y}33Ie8_izZzh(xhPuR)hj&8AwuaQTS^ zJVc__F{7h%HbvIG(j!H&anlpDu^xz_RZb@OAOmZPGCg7qMR+3BI-Dxmk)Ycz+0*-q zO!)^A#$%{6lN5Ity_pd>SX~fYF`6lTogIjkY*+cnh>JcCZ@UGe+!fHl&WKvcya3cS zeunqSuh1y4(B=N*M@)&Dp0SjF(x$}@p4hizPlNAKqt=Yj%3zqM$8a<(L8Aum0K@psB^<*zTENILNeu>tmOieVS|;@7{P z1%xI>8nC9?!70r8qtUUem)q>ITme!{g7}uweo@>Vh7y7imS7G{7Z7fUEs9$$doAVx z73?6zUtk91T~;Ej(m&@zV-T43H6rWKG>#sJI9M?ucs4kyb_SsI(>*kdBr?iM;9IaO zZB#1nQ1Vf>SAIXB$tQPcV7g|^2DX`V+56bS93$>Y7TPH;o?3~JJ$EsPKJchHLA@xE ztcnORx>)PA&5Q6+Vx(`9pXZTz5-63jYw>ftwo8ni_^vOn^Zw@n;i}K_(TlO7gjBI` z|Hc%<`hiU_mc~|4X1mmE1HQZ}2Xtn2^;lo(?z-n&!R9hv`8lVMz^|C~t-(o(4*sr5 zfr4~KNxG3YM9S>Af@y_vP#sTiXv`^EaNs*@(c=kcP|}f_Oh}PGVaWxS!hT!5Oo5~w zHRrJeGF}pphjuMl+CuS8w3{eXgCGC`uCuq7$AkFy%gqxG=0tYmEDV`cDH=~45=_q> zmWdJM;Rz43SIYN@Mh5|%;LE|Yg*czMUVQj*GmzsjPb80t!|BnRjX=Gny#PTPc2O$! za*r%qGyNkrl1UxYx}e6MjcAEAlGTjC0HaoMi;*Xw5hpU-*_z>nuc zr@b~iW_eI;!SZidM_fnIgkxtyI9h=k?(?|nbUasDW8=i;yCWlZFCks>Ul>C9Qe5gu z2F{DDh8@=zAh~I+887^`nnu&vjI+1uufYi>LBt~P)eW7}%||xIv-LABGGDhrUut4u zVo-YQ95w;IllR6YA*SbS4@cR)GmJlDdNy1A!zjGJgeEeys__L5;tA>GU zG{=>LBk^3+Nw|0n;Wog1O@^68GHW}Vlvv+I$n6t%vL-x={rskZVvv9>9bcw{p*Ine zO0EYsWJ3cl?!(^tVww&mUkujoHzCl>D_)%3bvJPz5jW>OMr>26i$3!}m*FS(!=iUj zzeU_`e>Ac^BpX{h0O9H}Rpj3C?5w9xAA@vwXZdEpnto=N|H2qnuNnG!yEB2!Fur75DE;XZB zoD4@#dM+4D3S`E6lnL5wDGq$KF6dobWOO>jov0j{b3nOVWfFJC{a%|r&RMzcA5PRH zsUqwHN!WB{kBQGsSh3ezzt~;!mZvk`he3 z<5_!-%vtD}#9+PBYI`q$$#Y2Iu)Gkd22lBk@TIOQ#P8dN_n-0iC_FCLf0!s@>J=dp zahRnl%P)PKUR$+2seIG~2Gp1Ppfc%BB3GNP^h&Y5s|LU@>}EdLt}KM(M(Oa-e04Fox zZZU*cHnx4$)yf!35L9X~uXidkB|k*vYc~PxC}G=gs%$G`tgW1%BcHrj;m9CmL?nd zXce=qt{!VVp*hBF;cs&-+`-ktcU&O!gixZHe6hKjx^HJsAW$Tipy!)=tcym;7moR& zN}$%EE{x|JlcO)*r@&Sm99QUgaQ! zGCbw@yHon_j>WCx&^8V|>a6{iB!dtmk~C{DztXt3d9n=nDQhqyPwyhN9T)Zklx1;P z%E#JAbw@`+SeT#zboZrU?gp*OolfGUFcmUa5c1tW&pEYE*6dorDLh0vge4GAYAqNg z`D3V>dhlq~IP1ngW6S7xVDXH@Dgi^<{bXZtc zfy8MBQjz40KPK`3F370!iO}xYi_jDXL9E-9RN0AAg;OQxfc?2-+4Eg8a5p-Q1}615 zV(YR)!UR)>xg!u(<{b{D+_f89WjKvBKgN-QN>wjY0iNHQ?G5^W2?bP1!gkk4-|09q z8w^TO%V-nSC7A*kGq7+FV>6|9Ey7?qUX*rV2j1`F3vgoq1w&(r1 z0v0F_zO{nb(v+q2GJItYV>w;};Yc>W2?2E`X(%sRtg!cCGa>cam!xaq;}<>JGm7|D z`5XX0G8ozisv`CdL`U&zudK#7p~J0tXb173-#WOIcgW{6)>{7puIQNVOpE$@s&DyG zx!`8zR@R8=Id^|NE{lS@Jex7w;@tNMP@-mZe#lguCnNaRJk61$=`Ng`sassSu~WS zW+mzlP078wRw*!{%?28TRS`77`a!#|%F$EB0>-V@h72EYe25jQ2EDiIZxYc@_C~Ji z(-D+cWEY5%FNU3lq;|9!;si*z&or5k2O`)=O`C?nmhII_7QG79qkwM8 zdUGePvt^z`li3g1#th~@Ku4MrJ>d5UFQc$6Yn5}=kM_VJ)M55WDaNKZdINduXQ5Kl zQ%^xmVrYG67Zru?Aa#(}3p_ZhO}IThgZGBVY%;Za8K+WlNASYxdw!WNs4k5-bsy3S zuV6g0bmpl->7118boT~pxVfEmm;!#fi&jU-(g~zVuUGqDkF_%d?)>ly+!;c`I%$$w zy*%4#aoz@-D4z^?dP^dtG8j?3=w&NqzjJLVBH+p7{40QCnE<-7`;w^K z6TgfL-ejC?sfrnU!hgsY>kv#m&BGaRD;Qr&a2MPDaGto(O|j7h0=TJ;=I^fSs#7RD zhgYK1MBCEQI<6RA1@E*rp+L`xk!@V38cSvC_9|d9cwhGg%@>ZMv3eL6v(opYj=Hd- z!LqHB;!R<65}8wVXHE*wQ3@!0S6+6=GZ#v{ziKuef^J-C`_=Cjb8Ez&o5uMSv8ZGjT=Zfg#BYY}&P!^(?0{BMb`$ z3z&$JEL7!;d&X>)7!@8;)Dzaf<)q{=-N!S6Vj}2w141hbX^1ylrXfF+*=qNLXf1?jdUTIyuJ$ zF$_lT2|Amc!n2a-KIPsO>thFqMl=P9-F+Xvn@M%JrV3t01r;iTsm87zI}~Z1yA|6* zI6!*6J+I%Fmd!~aTk-n-P&r5;0elNh|Jf_}XVc&h2=oVIU0CPVvTr)$@BWaAqNYNP IoMq(y0lY1>SO5S3 From 07246a0b156806083c2876b4508ddbfe457265c5 Mon Sep 17 00:00:00 2001 From: ricardo Date: Mon, 16 Mar 2020 22:02:48 -0400 Subject: [PATCH 025/165] :sparkles: HelpScout Integration --- packages/cli/src/Server.ts | 2 +- .../HelpScoutOAuth2Api.credentials.ts | 46 + .../HelpScout/ConversationDescription.ts | 598 +++++++ .../nodes/HelpScout/ConversationInterface.ts | 18 + .../nodes/HelpScout/CountriesCodes.ts | 1579 +++++++++++++++++ .../nodes/HelpScout/CustomerDescription.ts | 811 +++++++++ .../nodes/HelpScout/CustomerInterface.ts | 20 + .../nodes/HelpScout/GenericFunctions.ts | 69 + .../nodes/HelpScout/HelpScout.node.ts | 410 +++++ .../nodes/HelpScout/HelpScoutTrigger.node.ts | 202 +++ .../nodes/HelpScout/MailboxDescription.ts | 54 + .../nodes/HelpScout/ThreadDescription.ts | 257 +++ .../nodes/HelpScout/ThreadInterface.ts | 15 + .../nodes-base/nodes/HelpScout/helpScout.png | Bin 0 -> 4862 bytes packages/nodes-base/package.json | 7 +- 15 files changed, 4085 insertions(+), 3 deletions(-) create mode 100644 packages/nodes-base/credentials/HelpScoutOAuth2Api.credentials.ts create mode 100644 packages/nodes-base/nodes/HelpScout/ConversationDescription.ts create mode 100644 packages/nodes-base/nodes/HelpScout/ConversationInterface.ts create mode 100644 packages/nodes-base/nodes/HelpScout/CountriesCodes.ts create mode 100644 packages/nodes-base/nodes/HelpScout/CustomerDescription.ts create mode 100644 packages/nodes-base/nodes/HelpScout/CustomerInterface.ts create mode 100644 packages/nodes-base/nodes/HelpScout/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/HelpScout/HelpScout.node.ts create mode 100644 packages/nodes-base/nodes/HelpScout/HelpScoutTrigger.node.ts create mode 100644 packages/nodes-base/nodes/HelpScout/MailboxDescription.ts create mode 100644 packages/nodes-base/nodes/HelpScout/ThreadDescription.ts create mode 100644 packages/nodes-base/nodes/HelpScout/ThreadInterface.ts create mode 100644 packages/nodes-base/nodes/HelpScout/helpScout.png diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 4fe1ef70d7..99cda09412 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -203,7 +203,7 @@ class App { }); } - jwt.verify(token, getKey, {}, (err: Error, decoded: string) => { + jwt.verify(token, getKey, {}, (err: Error, decoded: object) => { if (err) return ResponseHelper.jwtAuthAuthorizationError(res, "Invalid token"); next(); diff --git a/packages/nodes-base/credentials/HelpScoutOAuth2Api.credentials.ts b/packages/nodes-base/credentials/HelpScoutOAuth2Api.credentials.ts new file mode 100644 index 0000000000..0301bf2c51 --- /dev/null +++ b/packages/nodes-base/credentials/HelpScoutOAuth2Api.credentials.ts @@ -0,0 +1,46 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class HelpScoutOAuth2Api implements ICredentialType { + name = 'helpScoutOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'HelpScout OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://secure.helpscout.net/authentication/authorizeClientApplication', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://api.helpscout.net/v2/oauth2/token', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'body', + }, + ]; +} diff --git a/packages/nodes-base/nodes/HelpScout/ConversationDescription.ts b/packages/nodes-base/nodes/HelpScout/ConversationDescription.ts new file mode 100644 index 0000000000..cce35b096a --- /dev/null +++ b/packages/nodes-base/nodes/HelpScout/ConversationDescription.ts @@ -0,0 +1,598 @@ +import { INodeProperties } from "n8n-workflow"; + +export const conversationOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'conversation', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new conversation', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a conversation', + }, + { + name: 'Get', + value: 'get', + description: 'Get a conversation', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all conversations', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const conversationFields = [ +/* -------------------------------------------------------------------------- */ +/* conversation:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Mailbox', + name: 'mailboxId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getMailboxes', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'conversation', + ], + }, + }, + default: '', + description: 'ID of a mailbox where the conversation is being created', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + required: true, + options: [ + { + name: 'Active', + value: 'active', + }, + { + name: 'Closed', + value: 'closed', + }, + { + name: 'Pending', + value: 'pending', + }, + ], + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'conversation', + ], + }, + }, + default: '', + description: 'Conversation status', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'string', + required: true, + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'conversation', + ], + }, + }, + default: '', + description: `Conversation’s subject`, + }, + { + displayName: 'Type', + name: 'type', + required: true, + type: 'options', + options: [ + { + name: 'Chat', + value: 'chat', + }, + { + name: 'Email', + value: 'email', + }, + { + name: 'Phone', + value: 'phone', + }, + ], + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'conversation', + ], + }, + }, + default: '', + description: 'Conversation type', + }, + { + displayName: 'Resolve Data', + name: 'resolveData', + type: 'boolean', + default: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'conversation', + ], + }, + }, + description: 'By default the response only contain the ID to resource
. If this option gets activated it
will resolve the data automatically.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'conversation', + ], + }, + }, + options: [ + { + displayName: 'Assign To', + name: 'assignTo', + type: 'number', + default: 0, + description: 'The Help Scout user assigned to the conversation.', + }, + { + displayName: 'Auto Reply', + name: 'autoReply', + type: 'boolean', + default: false, + description: `When autoReply is set to true, an auto reply will be sent
+ as long as there is at least one customer thread in the conversation.`, + }, + { + displayName: 'Closed At', + name: 'closedAt', + type: 'dateTime', + default: '', + description: `When the conversation was closed, only applicable for imported conversations`, + }, + { + displayName: 'Created At', + name: 'createdAt', + type: 'dateTime', + default: '', + description: `When this conversation was created - ISO 8601 date time`, + }, + { + displayName: 'Customer Email', + name: 'customerEmail', + type: 'string', + default: '', + }, + { + displayName: 'Customer ID', + name: 'customerId', + type: 'number', + default: 0, + }, + { + displayName: 'Imported', + name: 'imported', + type: 'boolean', + default: false, + description: `When imported is set to true, no outgoing emails or notifications will be generated.`, + }, + { + displayName: 'Tags', + name: 'tags', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + default: [], + description: 'List of tags to be be added to the conversation', + }, + { + displayName: 'User ID', + name: 'user', + type: 'number', + default: 0, + description: 'ID of the user who is adding the conversation and threads.', + }, + ] + }, + { + displayName: 'Threads', + name: 'threadsUi', + placeholder: 'Add Thread', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'conversation', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Thread', + name: 'threadsValues', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Chat', + value: 'chat' + }, + { + name: 'Customer', + value: 'customer' + }, + { + name: 'Note', + value: 'note' + }, + { + name: 'Phone', + value: 'phone' + }, + { + name: 'Reply', + value: 'reply' + }, + ], + default: '', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true + }, + default: '', + description: 'The message text, ' + }, + { + displayName: 'Bcc', + name: 'bcc', + displayOptions: { + show: { + type: [ + 'customer' + ], + }, + }, + type: 'string', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Email', + }, + default: [], + description: 'Email addresses.' + }, + { + displayName: 'Cc', + name: 'cc', + displayOptions: { + show: { + type: [ + 'customer' + ], + }, + }, + type: 'string', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Email', + }, + default: [], + description: 'Email addresses.' + }, + { + displayName: 'Draft', + name: 'draft', + displayOptions: { + show: { + type: [ + 'reply' + ], + }, + }, + type: 'boolean', + default: false, + description: 'If set to true, a draft reply is created', + }, + ], + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* conversation:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Conversation ID', + name: 'conversationId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'conversation', + ], + operation: [ + 'get', + ], + }, + }, + description: 'conversation ID', + }, +/* -------------------------------------------------------------------------- */ +/* conversation:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Conversation ID', + name: 'conversationId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'conversation', + ], + operation: [ + 'delete', + ], + }, + }, + description: 'conversation ID', + }, +/* -------------------------------------------------------------------------- */ +/* conversation:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'conversation', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Embed', + name: 'embed', + type: 'options', + options: [ + { + name: 'Threads', + value: 'threads', + }, + ], + default: '', + description: 'Allows embedding/loading of sub-entities', + }, + { + displayName: 'Mailbox ID', + name: 'mailbox', + type: 'string', + default: '', + description: 'Filters conversations from a specific mailbox', + }, + { + displayName: 'Folder ID', + name: 'folder', + type: 'string', + default: '', + description: 'Filters conversations from a specific folder id', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Active', + value: 'active', + }, + { + name: 'All', + value: 'all', + }, + { + name: 'Closed', + value: 'closed', + }, + { + name: 'Open', + value: 'open', + }, + { + name: 'Pending', + value: 'pending', + }, + { + name: 'Spam', + value: 'spam', + }, + ], + default: 'active', + description: 'Filter conversation by status', + }, + { + displayName: 'Tags', + name: 'tags', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + default: [], + description: 'Filter conversation by tags', + }, + { + displayName: 'Assign To', + name: 'assignTo', + type: 'number', + default: 0, + description: 'Filters conversations by assignee id', + }, + { + displayName: 'Modified Since', + name: 'modifiedSince', + type: 'dateTime', + default: '', + description: 'Returns only conversations that were modified after this date', + }, + { + displayName: 'Number', + name: 'number', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Looks up conversation by conversation number', + }, + { + displayName: 'Sort Field', + name: 'sortField', + type: 'options', + options: [ + { + name: 'Created At', + value: 'createdAt', + }, + { + name: 'customer Email', + value: 'customerEmail', + }, + { + name: 'customer Name', + value: 'customerName', + }, + { + name: 'Mailbox ID', + value: 'mailboxid', + }, + { + name: 'Modified At', + value: 'modifiedAt', + }, + { + name: 'Number', + value: 'number', + }, + { + name: 'Score', + value: 'score', + }, + { + name: 'Status', + value: 'status', + }, + { + name: 'Subject', + value: 'subject', + }, + ], + default: '', + description: 'Sorts the result by specified field', + }, + { + displayName: 'Sort Order', + name: 'sortOrder', + type: 'options', + options: [ + { + name: 'ASC', + value: 'asc', + }, + { + name: 'Desc', + value: 'desc', + }, + ], + default: 'desc', + }, + { + displayName: 'Query', + name: 'query', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Advanced search Examples' + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/HelpScout/ConversationInterface.ts b/packages/nodes-base/nodes/HelpScout/ConversationInterface.ts new file mode 100644 index 0000000000..4140fda218 --- /dev/null +++ b/packages/nodes-base/nodes/HelpScout/ConversationInterface.ts @@ -0,0 +1,18 @@ +import { IDataObject } from "n8n-workflow"; + +export interface IConversation { + assignTo?: number; + autoReply?: boolean; + closedAt?: string; + createdAt?: string; + customer?: IDataObject; + fields?: IDataObject[]; + imported?: boolean; + mailboxId?: number; // + status?: string; // + subject?: string; // + tags?: IDataObject[]; + threads?: IDataObject[]; + type?: string; // + user?: number; +} diff --git a/packages/nodes-base/nodes/HelpScout/CountriesCodes.ts b/packages/nodes-base/nodes/HelpScout/CountriesCodes.ts new file mode 100644 index 0000000000..653e876beb --- /dev/null +++ b/packages/nodes-base/nodes/HelpScout/CountriesCodes.ts @@ -0,0 +1,1579 @@ +export const countriesCodes = [ + { + "name": "Afghanistan", + "alpha2": "AF", + "alpha3": "AFG", + "numeric": "004" + }, + { + "name": "Åland Islands", + "alpha2": "AX", + "alpha3": "ALA", + "numeric": "248", + "altName": "Aland Islands" + }, + { + "name": "Albania", + "alpha2": "AL", + "alpha3": "ALB", + "numeric": "008" + }, + { + "name": "Algeria", + "alpha2": "DZ", + "alpha3": "DZA", + "numeric": "012" + }, + { + "name": "American Samoa", + "alpha2": "AS", + "alpha3": "ASM", + "numeric": "016" + }, + { + "name": "Andorra", + "alpha2": "AD", + "alpha3": "AND", + "numeric": "020" + }, + { + "name": "Angola", + "alpha2": "AO", + "alpha3": "AGO", + "numeric": "024" + }, + { + "name": "Anguilla", + "alpha2": "AI", + "alpha3": "AIA", + "numeric": "660" + }, + { + "name": "Antarctica", + "alpha2": "AQ", + "alpha3": "ATA", + "numeric": "010" + }, + { + "name": "Antigua and Barbuda", + "alpha2": "AG", + "alpha3": "ATG", + "numeric": "028" + }, + { + "name": "Argentina", + "alpha2": "AR", + "alpha3": "ARG", + "numeric": "032" + }, + { + "name": "Armenia", + "alpha2": "AM", + "alpha3": "ARM", + "numeric": "051" + }, + { + "name": "Aruba", + "alpha2": "AW", + "alpha3": "ABW", + "numeric": "533" + }, + { + "name": "Australia", + "alpha2": "AU", + "alpha3": "AUS", + "numeric": "036" + }, + { + "name": "Austria", + "alpha2": "AT", + "alpha3": "AUT", + "numeric": "040" + }, + { + "name": "Azerbaijan", + "alpha2": "AZ", + "alpha3": "AZE", + "numeric": "031" + }, + { + "name": "Bahamas (the)", + "alpha2": "BS", + "alpha3": "BHS", + "numeric": "044", + "altName": "Bahamas" + }, + { + "name": "Bahrain", + "alpha2": "BH", + "alpha3": "BHR", + "numeric": "048" + }, + { + "name": "Bangladesh", + "alpha2": "BD", + "alpha3": "BGD", + "numeric": "050" + }, + { + "name": "Barbados", + "alpha2": "BB", + "alpha3": "BRB", + "numeric": "052" + }, + { + "name": "Belarus", + "alpha2": "BY", + "alpha3": "BLR", + "numeric": "112" + }, + { + "name": "Belgium", + "alpha2": "BE", + "alpha3": "BEL", + "numeric": "056" + }, + { + "name": "Belize", + "alpha2": "BZ", + "alpha3": "BLZ", + "numeric": "084" + }, + { + "name": "Benin", + "alpha2": "BJ", + "alpha3": "BEN", + "numeric": "204" + }, + { + "name": "Bermuda", + "alpha2": "BM", + "alpha3": "BMU", + "numeric": "060" + }, + { + "name": "Bhutan", + "alpha2": "BT", + "alpha3": "BTN", + "numeric": "064" + }, + { + "name": "Bolivia (Plurinational State of)", + "alpha2": "BO", + "alpha3": "BOL", + "numeric": "068", + "altName": "Bolivia" + }, + { + "name": "Bonaire, Sint Eustatius and Saba", + "alpha2": "BQ", + "alpha3": "BES", + "numeric": "535" + }, + { + "name": "Bosnia and Herzegovina", + "alpha2": "BA", + "alpha3": "BIH", + "numeric": "070" + }, + { + "name": "Botswana", + "alpha2": "BW", + "alpha3": "BWA", + "numeric": "072" + }, + { + "name": "Bouvet Island", + "alpha2": "BV", + "alpha3": "BVT", + "numeric": "074" + }, + { + "name": "Brazil", + "alpha2": "BR", + "alpha3": "BRA", + "numeric": "076" + }, + { + "name": "British Indian Ocean Territory (the)", + "alpha2": "IO", + "alpha3": "IOT", + "numeric": "086", + "altName": "British Indian Ocean Territory" + }, + { + "name": "Brunei Darussalam", + "alpha2": "BN", + "alpha3": "BRN", + "numeric": "096", + "shortName": "Brunei" + }, + { + "name": "Bulgaria", + "alpha2": "BG", + "alpha3": "BGR", + "numeric": "100" + }, + { + "name": "Burkina Faso", + "alpha2": "BF", + "alpha3": "BFA", + "numeric": "854" + }, + { + "name": "Burundi", + "alpha2": "BI", + "alpha3": "BDI", + "numeric": "108" + }, + { + "name": "Cabo Verde", + "alpha2": "CV", + "alpha3": "CPV", + "numeric": "132", + "altName": "Cape Verde" + }, + { + "name": "Cambodia", + "alpha2": "KH", + "alpha3": "KHM", + "numeric": "116" + }, + { + "name": "Cameroon", + "alpha2": "CM", + "alpha3": "CMR", + "numeric": "120" + }, + { + "name": "Canada", + "alpha2": "CA", + "alpha3": "CAN", + "numeric": "124" + }, + { + "name": "Cayman Islands (the)", + "alpha2": "KY", + "alpha3": "CYM", + "numeric": "136", + "altName": "Cayman Islands" + }, + { + "name": "Central African Republic (the)", + "alpha2": "CF", + "alpha3": "CAF", + "numeric": "140", + "altName": "Central African Republic" + }, + { + "name": "Chad", + "alpha2": "TD", + "alpha3": "TCD", + "numeric": "148" + }, + { + "name": "Chile", + "alpha2": "CL", + "alpha3": "CHL", + "numeric": "152" + }, + { + "name": "China", + "alpha2": "CN", + "alpha3": "CHN", + "numeric": "156" + }, + { + "name": "Christmas Island", + "alpha2": "CX", + "alpha3": "CXR", + "numeric": "162" + }, + { + "name": "Cocos (Keeling) Islands (the)", + "alpha2": "CC", + "alpha3": "CCK", + "numeric": "166", + "altName": "Cocos (Keeling) Islands", + "shortName": "Cocos Islands" + }, + { + "name": "Colombia", + "alpha2": "CO", + "alpha3": "COL", + "numeric": "170" + }, + { + "name": "Comoros (the)", + "alpha2": "KM", + "alpha3": "COM", + "numeric": "174", + "altName": "Comoros" + }, + { + "name": "Congo (the Democratic Republic of the)", + "alpha2": "CD", + "alpha3": "COD", + "numeric": "180", + "altName": "Congo, (Kinshasa)", + "shortName": "Democratic Republic of the Congo" + }, + { + "name": "Congo (the)", + "alpha2": "CG", + "alpha3": "COG", + "numeric": "178", + "altName": "Congo (Brazzaville)", + "shortName": "Republic of the Congo" + }, + { + "name": "Cook Islands (the)", + "alpha2": "CK", + "alpha3": "COK", + "numeric": "184", + "altName": "Cook Islands" + }, + { + "name": "Costa Rica", + "alpha2": "CR", + "alpha3": "CRI", + "numeric": "188" + }, + { + "name": "Côte d'Ivoire", + "alpha2": "CI", + "alpha3": "CIV", + "numeric": "384", + "shortName": "Ivory Coast" + }, + { + "name": "Croatia", + "alpha2": "HR", + "alpha3": "HRV", + "numeric": "191" + }, + { + "name": "Cuba", + "alpha2": "CU", + "alpha3": "CUB", + "numeric": "192" + }, + { + "name": "Curaçao", + "alpha2": "CW", + "alpha3": "CUW", + "numeric": "531", + "shortName": "Curacao" + }, + { + "name": "Cyprus", + "alpha2": "CY", + "alpha3": "CYP", + "numeric": "196" + }, + { + "name": "Czechia", + "alpha2": "CZ", + "alpha3": "CZE", + "numeric": "203", + "altName": "Czech Republic" + }, + { + "name": "Denmark", + "alpha2": "DK", + "alpha3": "DNK", + "numeric": "208" + }, + { + "name": "Djibouti", + "alpha2": "DJ", + "alpha3": "DJI", + "numeric": "262" + }, + { + "name": "Dominica", + "alpha2": "DM", + "alpha3": "DMA", + "numeric": "212" + }, + { + "name": "Dominican Republic (the)", + "alpha2": "DO", + "alpha3": "DOM", + "numeric": "214", + "altName": "Dominican Republic" + }, + { + "name": "Ecuador", + "alpha2": "EC", + "alpha3": "ECU", + "numeric": "218" + }, + { + "name": "Egypt", + "alpha2": "EG", + "alpha3": "EGY", + "numeric": "818" + }, + { + "name": "El Salvador", + "alpha2": "SV", + "alpha3": "SLV", + "numeric": "222" + }, + { + "name": "Equatorial Guinea", + "alpha2": "GQ", + "alpha3": "GNQ", + "numeric": "226" + }, + { + "name": "Eritrea", + "alpha2": "ER", + "alpha3": "ERI", + "numeric": "232" + }, + { + "name": "Estonia", + "alpha2": "EE", + "alpha3": "EST", + "numeric": "233" + }, + { + "name": "Ethiopia", + "alpha2": "ET", + "alpha3": "ETH", + "numeric": "231" + }, + { + "name": "Falkland Islands (the) [Malvinas]", + "alpha2": "FK", + "alpha3": "FLK", + "numeric": "238", + "altName": "Falkland Islands (Malvinas)", + "shortName": "Falkland Islands" + }, + { + "name": "Faroe Islands (the)", + "alpha2": "FO", + "alpha3": "FRO", + "numeric": "234", + "altName": "Faroe Islands" + }, + { + "name": "Fiji", + "alpha2": "FJ", + "alpha3": "FJI", + "numeric": "242" + }, + { + "name": "Finland", + "alpha2": "FI", + "alpha3": "FIN", + "numeric": "246" + }, + { + "name": "France", + "alpha2": "FR", + "alpha3": "FRA", + "numeric": "250" + }, + { + "name": "French Guiana", + "alpha2": "GF", + "alpha3": "GUF", + "numeric": "254" + }, + { + "name": "French Polynesia", + "alpha2": "PF", + "alpha3": "PYF", + "numeric": "258" + }, + { + "name": "French Southern Territories (the)", + "alpha2": "TF", + "alpha3": "ATF", + "numeric": "260", + "altName": "French Southern Territories" + }, + { + "name": "Gabon", + "alpha2": "GA", + "alpha3": "GAB", + "numeric": "266" + }, + { + "name": "Gambia (the)", + "alpha2": "GM", + "alpha3": "GMB", + "numeric": "270", + "altName": "Gambia" + }, + { + "name": "Georgia", + "alpha2": "GE", + "alpha3": "GEO", + "numeric": "268" + }, + { + "name": "Germany", + "alpha2": "DE", + "alpha3": "DEU", + "numeric": "276" + }, + { + "name": "Ghana", + "alpha2": "GH", + "alpha3": "GHA", + "numeric": "288" + }, + { + "name": "Gibraltar", + "alpha2": "GI", + "alpha3": "GIB", + "numeric": "292" + }, + { + "name": "Greece", + "alpha2": "GR", + "alpha3": "GRC", + "numeric": "300" + }, + { + "name": "Greenland", + "alpha2": "GL", + "alpha3": "GRL", + "numeric": "304" + }, + { + "name": "Grenada", + "alpha2": "GD", + "alpha3": "GRD", + "numeric": "308" + }, + { + "name": "Guadeloupe", + "alpha2": "GP", + "alpha3": "GLP", + "numeric": "312" + }, + { + "name": "Guam", + "alpha2": "GU", + "alpha3": "GUM", + "numeric": "316" + }, + { + "name": "Guatemala", + "alpha2": "GT", + "alpha3": "GTM", + "numeric": "320" + }, + { + "name": "Guernsey", + "alpha2": "GG", + "alpha3": "GGY", + "numeric": "831" + }, + { + "name": "Guinea", + "alpha2": "GN", + "alpha3": "GIN", + "numeric": "324" + }, + { + "name": "Guinea-Bissau", + "alpha2": "GW", + "alpha3": "GNB", + "numeric": "624" + }, + { + "name": "Guyana", + "alpha2": "GY", + "alpha3": "GUY", + "numeric": "328" + }, + { + "name": "Haiti", + "alpha2": "HT", + "alpha3": "HTI", + "numeric": "332" + }, + { + "name": "Heard Island and McDonald Islands", + "alpha2": "HM", + "alpha3": "HMD", + "numeric": "334", + "altName": "Heard and Mcdonald Islands" + }, + { + "name": "Holy See (the)", + "alpha2": "VA", + "alpha3": "VAT", + "numeric": "336", + "altName": "Holy See (Vatican City State)", + "shortName": "Vatican" + }, + { + "name": "Honduras", + "alpha2": "HN", + "alpha3": "HND", + "numeric": "340" + }, + { + "name": "Hong Kong", + "alpha2": "HK", + "alpha3": "HKG", + "numeric": "344", + "altName": "Hong Kong, SAR China" + }, + { + "name": "Hungary", + "alpha2": "HU", + "alpha3": "HUN", + "numeric": "348" + }, + { + "name": "Iceland", + "alpha2": "IS", + "alpha3": "ISL", + "numeric": "352" + }, + { + "name": "India", + "alpha2": "IN", + "alpha3": "IND", + "numeric": "356" + }, + { + "name": "Indonesia", + "alpha2": "ID", + "alpha3": "IDN", + "numeric": "360" + }, + { + "name": "Iran (Islamic Republic of)", + "alpha2": "IR", + "alpha3": "IRN", + "numeric": "364", + "altName": "Iran, Islamic Republic of", + "shortName": "Iran" + }, + { + "name": "Iraq", + "alpha2": "IQ", + "alpha3": "IRQ", + "numeric": "368" + }, + { + "name": "Ireland", + "alpha2": "IE", + "alpha3": "IRL", + "numeric": "372" + }, + { + "name": "Isle of Man", + "alpha2": "IM", + "alpha3": "IMN", + "numeric": "833" + }, + { + "name": "Israel", + "alpha2": "IL", + "alpha3": "ISR", + "numeric": "376" + }, + { + "name": "Italy", + "alpha2": "IT", + "alpha3": "ITA", + "numeric": "380" + }, + { + "name": "Jamaica", + "alpha2": "JM", + "alpha3": "JAM", + "numeric": "388" + }, + { + "name": "Japan", + "alpha2": "JP", + "alpha3": "JPN", + "numeric": "392" + }, + { + "name": "Jersey", + "alpha2": "JE", + "alpha3": "JEY", + "numeric": "832" + }, + { + "name": "Jordan", + "alpha2": "JO", + "alpha3": "JOR", + "numeric": "400" + }, + { + "name": "Kazakhstan", + "alpha2": "KZ", + "alpha3": "KAZ", + "numeric": "398" + }, + { + "name": "Kenya", + "alpha2": "KE", + "alpha3": "KEN", + "numeric": "404" + }, + { + "name": "Kiribati", + "alpha2": "KI", + "alpha3": "KIR", + "numeric": "296" + }, + { + "name": "Korea (the Democratic People's Republic of)", + "alpha2": "KP", + "alpha3": "PRK", + "numeric": "408", + "altName": "Korea (North)", + "shortName": "North Korea" + }, + { + "name": "Korea (the Republic of)", + "alpha2": "KR", + "alpha3": "KOR", + "numeric": "410", + "altName": "Korea (South)", + "shortName": "South Korea" + }, + { + "name": "Kuwait", + "alpha2": "KW", + "alpha3": "KWT", + "numeric": "414" + }, + { + "name": "Kyrgyzstan", + "alpha2": "KG", + "alpha3": "KGZ", + "numeric": "417" + }, + { + "name": "Lao People's Democratic Republic (the)", + "alpha2": "LA", + "alpha3": "LAO", + "numeric": "418", + "altName": "Lao PDR", + "shortName": "Laos" + }, + { + "name": "Latvia", + "alpha2": "LV", + "alpha3": "LVA", + "numeric": "428" + }, + { + "name": "Lebanon", + "alpha2": "LB", + "alpha3": "LBN", + "numeric": "422" + }, + { + "name": "Lesotho", + "alpha2": "LS", + "alpha3": "LSO", + "numeric": "426" + }, + { + "name": "Liberia", + "alpha2": "LR", + "alpha3": "LBR", + "numeric": "430" + }, + { + "name": "Libya", + "alpha2": "LY", + "alpha3": "LBY", + "numeric": "434" + }, + { + "name": "Liechtenstein", + "alpha2": "LI", + "alpha3": "LIE", + "numeric": "438" + }, + { + "name": "Lithuania", + "alpha2": "LT", + "alpha3": "LTU", + "numeric": "440" + }, + { + "name": "Luxembourg", + "alpha2": "LU", + "alpha3": "LUX", + "numeric": "442" + }, + { + "name": "Macao", + "alpha2": "MO", + "alpha3": "MAC", + "numeric": "446", + "altName": "Macao, SAR China", + "shortName": "Macau" + }, + { + "name": "Macedonia (the former Yugoslav Republic of)", + "alpha2": "MK", + "alpha3": "MKD", + "numeric": "807", + "altName": "Macedonia, Republic of", + "shortName": "Macedonia" + }, + { + "name": "Madagascar", + "alpha2": "MG", + "alpha3": "MDG", + "numeric": "450" + }, + { + "name": "Malawi", + "alpha2": "MW", + "alpha3": "MWI", + "numeric": "454" + }, + { + "name": "Malaysia", + "alpha2": "MY", + "alpha3": "MYS", + "numeric": "458" + }, + { + "name": "Maldives", + "alpha2": "MV", + "alpha3": "MDV", + "numeric": "462" + }, + { + "name": "Mali", + "alpha2": "ML", + "alpha3": "MLI", + "numeric": "466" + }, + { + "name": "Malta", + "alpha2": "MT", + "alpha3": "MLT", + "numeric": "470" + }, + { + "name": "Marshall Islands (the)", + "alpha2": "MH", + "alpha3": "MHL", + "numeric": "584", + "altName": "Marshall Islands" + }, + { + "name": "Martinique", + "alpha2": "MQ", + "alpha3": "MTQ", + "numeric": "474" + }, + { + "name": "Mauritania", + "alpha2": "MR", + "alpha3": "MRT", + "numeric": "478" + }, + { + "name": "Mauritius", + "alpha2": "MU", + "alpha3": "MUS", + "numeric": "480" + }, + { + "name": "Mayotte", + "alpha2": "YT", + "alpha3": "MYT", + "numeric": "175" + }, + { + "name": "Mexico", + "alpha2": "MX", + "alpha3": "MEX", + "numeric": "484" + }, + { + "name": "Micronesia (Federated States of)", + "alpha2": "FM", + "alpha3": "FSM", + "numeric": "583", + "altName": "Micronesia, Federated States of", + "shortName": "Micronesia" + }, + { + "name": "Moldova (the Republic of)", + "alpha2": "MD", + "alpha3": "MDA", + "numeric": "498", + "altName": "Moldova" + }, + { + "name": "Monaco", + "alpha2": "MC", + "alpha3": "MCO", + "numeric": "492" + }, + { + "name": "Mongolia", + "alpha2": "MN", + "alpha3": "MNG", + "numeric": "496" + }, + { + "name": "Montenegro", + "alpha2": "ME", + "alpha3": "MNE", + "numeric": "499" + }, + { + "name": "Montserrat", + "alpha2": "MS", + "alpha3": "MSR", + "numeric": "500" + }, + { + "name": "Morocco", + "alpha2": "MA", + "alpha3": "MAR", + "numeric": "504" + }, + { + "name": "Mozambique", + "alpha2": "MZ", + "alpha3": "MOZ", + "numeric": "508" + }, + { + "name": "Myanmar", + "alpha2": "MM", + "alpha3": "MMR", + "numeric": "104" + }, + { + "name": "Namibia", + "alpha2": "NA", + "alpha3": "NAM", + "numeric": "516" + }, + { + "name": "Nauru", + "alpha2": "NR", + "alpha3": "NRU", + "numeric": "520" + }, + { + "name": "Nepal", + "alpha2": "NP", + "alpha3": "NPL", + "numeric": "524" + }, + { + "name": "Netherlands (the)", + "alpha2": "NL", + "alpha3": "NLD", + "numeric": "528", + "altName": "Netherlands" + }, + { + "name": "New Caledonia", + "alpha2": "NC", + "alpha3": "NCL", + "numeric": "540" + }, + { + "name": "New Zealand", + "alpha2": "NZ", + "alpha3": "NZL", + "numeric": "554" + }, + { + "name": "Nicaragua", + "alpha2": "NI", + "alpha3": "NIC", + "numeric": "558" + }, + { + "name": "Niger (the)", + "alpha2": "NE", + "alpha3": "NER", + "numeric": "562", + "altName": "Niger" + }, + { + "name": "Nigeria", + "alpha2": "NG", + "alpha3": "NGA", + "numeric": "566" + }, + { + "name": "Niue", + "alpha2": "NU", + "alpha3": "NIU", + "numeric": "570" + }, + { + "name": "Norfolk Island", + "alpha2": "NF", + "alpha3": "NFK", + "numeric": "574" + }, + { + "name": "Northern Mariana Islands (the)", + "alpha2": "MP", + "alpha3": "MNP", + "numeric": "580", + "altName": "Northern Mariana Islands" + }, + { + "name": "Norway", + "alpha2": "NO", + "alpha3": "NOR", + "numeric": "578" + }, + { + "name": "Oman", + "alpha2": "OM", + "alpha3": "OMN", + "numeric": "512" + }, + { + "name": "Pakistan", + "alpha2": "PK", + "alpha3": "PAK", + "numeric": "586" + }, + { + "name": "Palau", + "alpha2": "PW", + "alpha3": "PLW", + "numeric": "585" + }, + { + "name": "Palestine, State of", + "alpha2": "PS", + "alpha3": "PSE", + "numeric": "275", + "altName": "Palestinian Territory", + "shortName": "Palestine" + }, + { + "name": "Panama", + "alpha2": "PA", + "alpha3": "PAN", + "numeric": "591" + }, + { + "name": "Papua New Guinea", + "alpha2": "PG", + "alpha3": "PNG", + "numeric": "598" + }, + { + "name": "Paraguay", + "alpha2": "PY", + "alpha3": "PRY", + "numeric": "600" + }, + { + "name": "Peru", + "alpha2": "PE", + "alpha3": "PER", + "numeric": "604" + }, + { + "name": "Philippines (the)", + "alpha2": "PH", + "alpha3": "PHL", + "numeric": "608", + "altName": "Philippines" + }, + { + "name": "Pitcairn", + "alpha2": "PN", + "alpha3": "PCN", + "numeric": "612" + }, + { + "name": "Poland", + "alpha2": "PL", + "alpha3": "POL", + "numeric": "616" + }, + { + "name": "Portugal", + "alpha2": "PT", + "alpha3": "PRT", + "numeric": "620" + }, + { + "name": "Puerto Rico", + "alpha2": "PR", + "alpha3": "PRI", + "numeric": "630" + }, + { + "name": "Qatar", + "alpha2": "QA", + "alpha3": "QAT", + "numeric": "634" + }, + { + "name": "Réunion", + "alpha2": "RE", + "alpha3": "REU", + "numeric": "638", + "shortName": "Reunion" + }, + { + "name": "Romania", + "alpha2": "RO", + "alpha3": "ROU", + "numeric": "642" + }, + { + "name": "Russian Federation (the)", + "alpha2": "RU", + "alpha3": "RUS", + "numeric": "643", + "altName": "Russian Federation", + "shortName": "Russia" + }, + { + "name": "Rwanda", + "alpha2": "RW", + "alpha3": "RWA", + "numeric": "646" + }, + { + "name": "Saint Barthélemy", + "alpha2": "BL", + "alpha3": "BLM", + "numeric": "652", + "altName": "Saint-Barthélemy", + "shortName": "Saint Barthelemy" + }, + { + "name": "Saint Helena, Ascension and Tristan da Cunha", + "alpha2": "SH", + "alpha3": "SHN", + "numeric": "654", + "altName": "Saint Helena" + }, + { + "name": "Saint Kitts and Nevis", + "alpha2": "KN", + "alpha3": "KNA", + "numeric": "659" + }, + { + "name": "Saint Lucia", + "alpha2": "LC", + "alpha3": "LCA", + "numeric": "662" + }, + { + "name": "Saint Martin (French part)", + "alpha2": "MF", + "alpha3": "MAF", + "numeric": "663", + "altName": "Saint-Martin (French part)", + "shortName": "Saint Martin" + }, + { + "name": "Saint Pierre and Miquelon", + "alpha2": "PM", + "alpha3": "SPM", + "numeric": "666" + }, + { + "name": "Saint Vincent and the Grenadines", + "alpha2": "VC", + "alpha3": "VCT", + "numeric": "670", + "altName": "Saint Vincent and Grenadines" + }, + { + "name": "Samoa", + "alpha2": "WS", + "alpha3": "WSM", + "numeric": "882" + }, + { + "name": "San Marino", + "alpha2": "SM", + "alpha3": "SMR", + "numeric": "674" + }, + { + "name": "Sao Tome and Principe", + "alpha2": "ST", + "alpha3": "STP", + "numeric": "678" + }, + { + "name": "Saudi Arabia", + "alpha2": "SA", + "alpha3": "SAU", + "numeric": "682" + }, + { + "name": "Senegal", + "alpha2": "SN", + "alpha3": "SEN", + "numeric": "686" + }, + { + "name": "Serbia", + "alpha2": "RS", + "alpha3": "SRB", + "numeric": "688" + }, + { + "name": "Seychelles", + "alpha2": "SC", + "alpha3": "SYC", + "numeric": "690" + }, + { + "name": "Sierra Leone", + "alpha2": "SL", + "alpha3": "SLE", + "numeric": "694" + }, + { + "name": "Singapore", + "alpha2": "SG", + "alpha3": "SGP", + "numeric": "702" + }, + { + "name": "Sint Maarten (Dutch part)", + "alpha2": "SX", + "alpha3": "SXM", + "numeric": "534", + "shortName": "Sint Maarten" + }, + { + "name": "Slovakia", + "alpha2": "SK", + "alpha3": "SVK", + "numeric": "703" + }, + { + "name": "Slovenia", + "alpha2": "SI", + "alpha3": "SVN", + "numeric": "705" + }, + { + "name": "Solomon Islands", + "alpha2": "SB", + "alpha3": "SLB", + "numeric": "090" + }, + { + "name": "Somalia", + "alpha2": "SO", + "alpha3": "SOM", + "numeric": "706" + }, + { + "name": "South Africa", + "alpha2": "ZA", + "alpha3": "ZAF", + "numeric": "710" + }, + { + "name": "South Georgia and the South Sandwich Islands", + "alpha2": "GS", + "alpha3": "SGS", + "numeric": "239" + }, + { + "name": "South Sudan", + "alpha2": "SS", + "alpha3": "SSD", + "numeric": "728" + }, + { + "name": "Spain", + "alpha2": "ES", + "alpha3": "ESP", + "numeric": "724" + }, + { + "name": "Sri Lanka", + "alpha2": "LK", + "alpha3": "LKA", + "numeric": "144" + }, + { + "name": "Sudan (the)", + "alpha2": "SD", + "alpha3": "SDN", + "numeric": "729", + "altName": "Sudan" + }, + { + "name": "Suriname", + "alpha2": "SR", + "alpha3": "SUR", + "numeric": "740" + }, + { + "name": "Svalbard and Jan Mayen", + "alpha2": "SJ", + "alpha3": "SJM", + "numeric": "744", + "altName": "Svalbard and Jan Mayen Islands" + }, + { + "name": "Swaziland", + "alpha2": "SZ", + "alpha3": "SWZ", + "numeric": "748" + }, + { + "name": "Sweden", + "alpha2": "SE", + "alpha3": "SWE", + "numeric": "752" + }, + { + "name": "Switzerland", + "alpha2": "CH", + "alpha3": "CHE", + "numeric": "756" + }, + { + "name": "Syrian Arab Republic", + "alpha2": "SY", + "alpha3": "SYR", + "numeric": "760", + "altName": "Syrian Arab Republic (Syria)", + "shortName": "Syria" + }, + { + "name": "Taiwan (Province of China)", + "alpha2": "TW", + "alpha3": "TWN", + "numeric": "158", + "altName": "Taiwan, Republic of China", + "shortName": "Taiwan" + }, + { + "name": "Tajikistan", + "alpha2": "TJ", + "alpha3": "TJK", + "numeric": "762" + }, + { + "name": "Tanzania, United Republic of", + "alpha2": "TZ", + "alpha3": "TZA", + "numeric": "834", + "shortName": "Tanzania" + }, + { + "name": "Thailand", + "alpha2": "TH", + "alpha3": "THA", + "numeric": "764" + }, + { + "name": "Timor-Leste", + "alpha2": "TL", + "alpha3": "TLS", + "numeric": "626", + "shortName": "East Timor" + }, + { + "name": "Togo", + "alpha2": "TG", + "alpha3": "TGO", + "numeric": "768" + }, + { + "name": "Tokelau", + "alpha2": "TK", + "alpha3": "TKL", + "numeric": "772" + }, + { + "name": "Tonga", + "alpha2": "TO", + "alpha3": "TON", + "numeric": "776" + }, + { + "name": "Trinidad and Tobago", + "alpha2": "TT", + "alpha3": "TTO", + "numeric": "780" + }, + { + "name": "Tunisia", + "alpha2": "TN", + "alpha3": "TUN", + "numeric": "788" + }, + { + "name": "Turkey", + "alpha2": "TR", + "alpha3": "TUR", + "numeric": "792" + }, + { + "name": "Turkmenistan", + "alpha2": "TM", + "alpha3": "TKM", + "numeric": "795" + }, + { + "name": "Turks and Caicos Islands (the)", + "alpha2": "TC", + "alpha3": "TCA", + "numeric": "796", + "altName": "Turks and Caicos Islands" + }, + { + "name": "Tuvalu", + "alpha2": "TV", + "alpha3": "TUV", + "numeric": "798" + }, + { + "name": "Uganda", + "alpha2": "UG", + "alpha3": "UGA", + "numeric": "800" + }, + { + "name": "Ukraine", + "alpha2": "UA", + "alpha3": "UKR", + "numeric": "804" + }, + { + "name": "United Arab Emirates (the)", + "alpha2": "AE", + "alpha3": "ARE", + "numeric": "784", + "altName": "United Arab Emirates" + }, + { + "name": "United Kingdom of Great Britain and Northern Ireland (the)", + "alpha2": "GB", + "alpha3": "GBR", + "numeric": "826", + "altName": "United Kingdom" + }, + { + "name": "United States Minor Outlying Islands (the)", + "alpha2": "UM", + "alpha3": "UMI", + "numeric": "581", + "altName": "US Minor Outlying Islands" + }, + { + "name": "United States of America (the)", + "alpha2": "US", + "alpha3": "USA", + "numeric": "840", + "altName": "United States of America", + "shortName": "United States" + }, + { + "name": "Uruguay", + "alpha2": "UY", + "alpha3": "URY", + "numeric": "858" + }, + { + "name": "Uzbekistan", + "alpha2": "UZ", + "alpha3": "UZB", + "numeric": "860" + }, + { + "name": "Vanuatu", + "alpha2": "VU", + "alpha3": "VUT", + "numeric": "548" + }, + { + "name": "Venezuela (Bolivarian Republic of)", + "alpha2": "VE", + "alpha3": "VEN", + "numeric": "862", + "altName": "Venezuela (Bolivarian Republic)", + "shortName": "Venezuela" + }, + { + "name": "Viet Nam", + "alpha2": "VN", + "alpha3": "VNM", + "numeric": "704", + "shortName": "Vietnam" + }, + { + "name": "Virgin Islands (British)", + "alpha2": "VG", + "alpha3": "VGB", + "numeric": "092", + "altName": "British Virgin Islands" + }, + { + "name": "Virgin Islands (U.S.)", + "alpha2": "VI", + "alpha3": "VIR", + "numeric": "850", + "altName": "Virgin Islands, US", + "shortName": "U.S. Virgin Islands" + }, + { + "name": "Wallis and Futuna", + "alpha2": "WF", + "alpha3": "WLF", + "numeric": "876", + "altName": "Wallis and Futuna Islands" + }, + { + "name": "Western Sahara*", + "alpha2": "EH", + "alpha3": "ESH", + "numeric": "732", + "altName": "Western Sahara" + }, + { + "name": "Yemen", + "alpha2": "YE", + "alpha3": "YEM", + "numeric": "887" + }, + { + "name": "Zambia", + "alpha2": "ZM", + "alpha3": "ZMB", + "numeric": "894" + }, + { + "name": "Zimbabwe", + "alpha2": "ZW", + "alpha3": "ZWE", + "numeric": "716" + } + ]; diff --git a/packages/nodes-base/nodes/HelpScout/CustomerDescription.ts b/packages/nodes-base/nodes/HelpScout/CustomerDescription.ts new file mode 100644 index 0000000000..9f909a0b6b --- /dev/null +++ b/packages/nodes-base/nodes/HelpScout/CustomerDescription.ts @@ -0,0 +1,811 @@ +import { INodeProperties } from "n8n-workflow"; + +export const customerOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'customer', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new customer', + }, + { + name: 'Properties', + value: 'properties', + description: 'Get customer property definitions', + }, + { + name: 'Get', + value: 'get', + description: 'Get a customer', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all customers', + }, + { + name: 'Update', + value: 'update', + description: 'Update a customer', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const customerFields = [ +/* -------------------------------------------------------------------------- */ +/* customer:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Resolve Data', + name: 'resolveData', + type: 'boolean', + default: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'customer', + ], + }, + }, + description: 'By default the response only contain the ID to resource
. If this option gets activated it
will resolve the data automatically.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'customer', + ], + }, + }, + options: [ + { + displayName: 'Age', + name: 'age', + type: 'number', + typeOptions: { + minValue: 1, + }, + default: 1, + description: `Customer’s age`, + }, + { + displayName: 'Notes', + name: 'background', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: `Notes`, + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: `First name of the customer. When defined it must be between 1 and 40 characters.`, + }, + { + displayName: 'Gender', + name: 'gender', + type: 'options', + options: [ + { + name: 'Female', + value: 'female', + }, + { + name: 'Male', + value: 'male', + }, + { + name: 'Unknown', + value: 'unknown', + }, + ], + default: '', + description: 'Gender of this customer.', + }, + { + displayName: 'Job Title', + name: 'jobTitle', + type: 'string', + default: '', + description: 'Job title. Max length 60 characters.', + }, + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + default: '', + description: 'Last name of the customer', + }, + { + displayName: 'Location', + name: 'location', + type: 'string', + default: '', + description: 'Location of the customer.', + }, + { + displayName: 'Organization', + name: 'organization', + type: 'string', + default: '', + description: 'Organization', + }, + { + displayName: 'Photo Url', + name: 'photoUrl', + type: 'string', + default: '', + description: 'URL of the customer’s photo', + }, + ] + }, + { + displayName: 'Address', + name: 'addressUi', + placeholder: 'Add Address', + type: 'fixedCollection', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'customer', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Address', + name: 'addressValue', + values: [ + { + displayName: 'Line 1', + name: 'line1', + type: 'string', + default: '', + description: 'line1', + }, + { + displayName: 'Line 2', + name: 'line2', + type: 'string', + default: '', + description: 'line2', + }, + { + displayName: 'City', + name: 'city', + type: 'string', + default: '', + description: 'City', + }, + { + displayName: 'State', + name: 'state', + type: 'string', + default: '', + description: 'State', + }, + { + displayName: 'Country', + name: 'country', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCountriesCodes', + }, + default: '', + description: 'Country', + }, + { + displayName: 'Postal Code', + name: 'postalCode', + type: 'string', + default: '', + description: 'Postal code', + }, + ], + }, + ], + }, + { + displayName: 'Chat Handles', + name: 'chatsUi', + placeholder: 'Add Chat Handle', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'customer', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Chat Handle', + name: 'chatsValues', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'aim', + value: 'aim', + }, + { + name: 'gtalk', + value: 'gtalk', + }, + { + name: 'icq', + value: 'icq', + }, + { + name: 'msn', + value: 'msn', + }, + { + name: 'other', + value: 'other', + }, + { + name: 'qq', + value: 'qq', + }, + { + name: 'skype', + value: 'skype', + }, + { + name: 'xmpp', + value: 'xmpp', + }, + { + name: 'yahoo', + value: 'yahoo', + }, + ], + description: 'Chat type', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Chat handle', + }, + ], + }, + ], + }, + { + displayName: 'Emails', + name: 'emailsUi', + placeholder: 'Add Email', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'customer', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Email', + name: 'emailsValues', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Home', + value: 'home', + }, + { + name: 'Other', + value: 'other', + }, + { + name: 'Work', + value: 'work', + }, + ], + description: 'Location for this email address', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Email', + }, + ], + }, + ], + }, + { + displayName: 'Phones', + name: 'phonesUi', + placeholder: 'Add Phone', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'customer', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Email', + name: 'phonesValues', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Fax', + value: 'fax', + }, + { + name: 'Home', + value: 'home', + }, + { + name: 'Other', + value: 'other', + }, + { + name: 'Pager', + value: 'pager', + }, + { + name: 'Work', + value: 'work', + }, + ], + description: 'Location for this phone', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Phone', + }, + ], + }, + ], + }, + { + displayName: 'Social Profiles', + name: 'socialProfilesUi', + placeholder: 'Add Social Profile', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'customer', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Social Profile', + name: 'socialProfilesValues', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'About Me', + value: 'aboutMe', + }, + { + name: 'Facebook', + value: 'facebook', + }, + { + name: 'Flickr', + value: 'flickr', + }, + { + name: 'Forsquare', + value: 'forsquare', + }, + { + name: 'Google', + value: 'google', + }, + { + name: 'Google Plus', + value: 'googleplus', + }, + { + name: 'Linkedin', + value: 'linkedin', + }, + { + name: 'Other', + value: 'other', + }, + { + name: 'Quora', + value: 'quora', + }, + { + name: 'Tungleme', + value: 'tungleme', + }, + { + name: 'Twitter', + value: 'twitter', + }, + { + name: 'Youtube', + value: 'youtube', + }, + ], + description: 'Type of social profile', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Social Profile handle (url for example)', + }, + ], + }, + ], + }, + { + displayName: 'Websites', + name: 'websitesUi', + placeholder: 'Add Website', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'customer', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Website', + name: 'websitesValues', + values: [ + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Website URL', + }, + ], + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* customer:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Mailbox ID', + name: 'mailbox', + type: 'string', + default: '', + description: 'Filters customers from a specific mailbox', + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: 'Filters customers by first name', + }, + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + default: '', + description: 'Filters customers by last name', + }, + { + displayName: 'Modified Since', + name: 'modifiedSince', + type: 'dateTime', + default: '', + description: 'Returns only customers that were modified after this date', + }, + { + displayName: 'Sort Field', + name: 'sortField', + type: 'options', + options: [ + { + name: 'Score', + value: 'score', + }, + { + name: 'First Name', + value: 'firstName', + }, + { + name: 'Last Name', + value: 'lastName', + }, + { + name: 'Modified At', + value: 'modifiedAt', + }, + ], + default: 'score', + description: 'Sorts the result by specified field', + }, + { + displayName: 'Sort Order', + name: 'sortOrder', + type: 'options', + options: [ + { + name: 'ASC', + value: 'asc', + }, + { + name: 'Desc', + value: 'desc', + }, + ], + default: 'desc', + }, + { + displayName: 'Query', + name: 'query', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Advanced search Examples' + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* customer:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Customer ID', + name: 'customerId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'get', + ], + }, + }, + description: 'Customer ID', + }, +/* -------------------------------------------------------------------------- */ +/* customer:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Customer ID', + name: 'customerId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'customer', + ], + }, + }, + description: 'Customer ID', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'customer', + ], + }, + }, + options: [ + { + displayName: 'Age', + name: 'age', + type: 'number', + typeOptions: { + minValue: 1, + }, + default: 1, + description: `Customer’s age`, + }, + { + displayName: 'Notes', + name: 'background', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: `Notes`, + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: `First name of the customer. When defined it must be between 1 and 40 characters.`, + }, + { + displayName: 'Gender', + name: 'gender', + type: 'options', + options: [ + { + name: 'Female', + value: 'female', + }, + { + name: 'Male', + value: 'male', + }, + { + name: 'Unknown', + value: 'unknown', + }, + ], + default: '', + description: 'Gender of this customer.', + }, + { + displayName: 'Job Title', + name: 'jobTitle', + type: 'string', + default: '', + description: 'Job title. Max length 60 characters.', + }, + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + default: '', + description: 'Last name of the customer', + }, + { + displayName: 'Location', + name: 'location', + type: 'string', + default: '', + description: 'Location of the customer.', + }, + { + displayName: 'Organization', + name: 'organization', + type: 'string', + default: '', + description: 'Organization', + }, + { + displayName: 'Photo Url', + name: 'photoUrl', + type: 'string', + default: '', + description: 'URL of the customer’s photo', + }, + ] + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/HelpScout/CustomerInterface.ts b/packages/nodes-base/nodes/HelpScout/CustomerInterface.ts new file mode 100644 index 0000000000..569a92c9aa --- /dev/null +++ b/packages/nodes-base/nodes/HelpScout/CustomerInterface.ts @@ -0,0 +1,20 @@ +import { IDataObject } from "n8n-workflow"; + +export interface ICustomer { + address?: IDataObject; + age?: string; + background?: string; + chats?: IDataObject[]; + emails?: IDataObject[]; + firstName?: string; + gender?: string; + jobTitle?: string; + lastName?: string; + location?: string; + organization?: string; + phones?: IDataObject[]; + photoUrl?: string; + properties?: IDataObject; + socialProfiles?: IDataObject[]; + websites?: IDataObject[]; +} diff --git a/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts b/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts new file mode 100644 index 0000000000..c7cda4eac6 --- /dev/null +++ b/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts @@ -0,0 +1,69 @@ +import { OptionsWithUri } from 'request'; +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, + IHookFunctions, +} from 'n8n-core'; +import { + IDataObject, +} from 'n8n-workflow'; + +import { + get, +} from 'lodash'; + +export async function helpscoutApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IHookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + let options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || `https://api.helpscout.net${resource}`, + json: true + }; + try { + if (Object.keys(option).length !== 0) { + options = Object.assign({}, options, option); + } + if (Object.keys(body).length === 0) { + delete options.body; + } + //@ts-ignore + return await this.helpers.requestOAuth.call(this, 'helpScoutOAuth2Api', options); + } catch (error) { + if (error.response && error.response.body + && error.response.body._embedded + && error.response.body._embedded.errors) { + // Try to return the error prettier + //@ts-ignore + throw new Error(`HelpScout error response [${error.statusCode}]: ${error.response.body.message} - ${error.response.body._embedded.errors.map(error => { + return `${error.path} ${error.message}`; + }).join('-')}`); + } + throw error; + } +} + +export async function helpscoutApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + query.size = 50; + let uri; + + do { + responseData = await helpscoutApiRequest.call(this, method, endpoint, body, query, uri); + uri = get(responseData, '_links.next.href'); + returnData.push.apply(returnData, get(responseData, propertyName)); + } while ( + responseData['_links'] !== undefined && + responseData['_links'].next !== undefined && + responseData['_links'].next.href !== undefined + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/HelpScout/HelpScout.node.ts b/packages/nodes-base/nodes/HelpScout/HelpScout.node.ts new file mode 100644 index 0000000000..27534b2426 --- /dev/null +++ b/packages/nodes-base/nodes/HelpScout/HelpScout.node.ts @@ -0,0 +1,410 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeTypeDescription, + INodeType, + ILoadOptionsFunctions, + INodePropertyOptions, + IBinaryKeyData, +} from 'n8n-workflow'; + +import { + helpscoutApiRequest, + helpscoutApiRequestAllItems, +} from './GenericFunctions'; + +import { + conversationOperations, + conversationFields, +} from './ConversationDescription'; + +import { + customerOperations, + customerFields, +} from './CustomerDescription'; + +import { + mailboxOperations, + mailboxFields, +} from './MailboxDescription'; + +import { + threadOperations, + threadFields, +} from './ThreadDescription'; + +import { + ICustomer, +} from './CustomerInterface'; + +import { + IConversation, + } from './ConversationInterface'; + + import { + IThread, + IAttachment, + } from './ThreadInterface'; + + import { + countriesCodes +} from './CountriesCodes'; + +export class HelpScout implements INodeType { + description: INodeTypeDescription = { + displayName: 'HelpScout', + name: 'helpScout', + icon: 'file:helpScout.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Help Scout API.', + defaults: { + name: 'HelpScout', + color: '#1392ee', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'helpScoutOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Conversation', + value: 'conversation', + }, + { + name: 'Customer', + value: 'customer', + }, + { + name: 'Mailbox', + value: 'mailbox', + }, + { + name: 'Thread', + value: 'thread', + }, + ], + default: 'conversation', + description: 'The resource to operate on.', + }, + ...conversationOperations, + ...conversationFields, + ...customerOperations, + ...customerFields, + ...mailboxOperations, + ...mailboxFields, + ...threadOperations, + ...threadFields, + ], + }; + + methods = { + loadOptions: { + // Get all the countries codes to display them to user so that he can + // select them easily + async getCountriesCodes(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + for (const countryCode of countriesCodes) { + const countryCodeName = `${countryCode.name} - ${countryCode.alpha2}`; + const countryCodeId = countryCode.alpha2; + returnData.push({ + name: countryCodeName, + value: countryCodeId, + }); + } + return returnData; + }, + // Get all the tags to display them to user so that he can + // select them easily + async getTags(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const tags = await helpscoutApiRequestAllItems.call(this, '_embedded.tags', 'GET', '/v2/tags'); + for (const tag of tags) { + const tagName = tag.name; + const tagId = tag.id; + returnData.push({ + name: tagName, + value: tagId, + }); + } + return returnData; + }, + // Get all the mailboxes to display them to user so that he can + // select them easily + async getMailboxes(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const mailboxes = await helpscoutApiRequestAllItems.call(this, '_embedded.mailboxes', 'GET', '/v2/mailboxes'); + for (const mailbox of mailboxes) { + const mailboxName = mailbox.name; + const mailboxId = mailbox.id; + returnData.push({ + name: mailboxName, + value: mailboxId, + }); + } + 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 === 'conversation') { + //https://developer.helpscout.com/mailbox-api/endpoints/conversations/create + if (operation === 'create') { + const mailboxId = this.getNodeParameter('mailboxId', i) as number; + const status = this.getNodeParameter('status', i) as string; + const subject = this.getNodeParameter('subject', i) as string; + const type = this.getNodeParameter('type', i) as string; + const resolveData = this.getNodeParameter('resolveData', i) as boolean; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const threads = (this.getNodeParameter('threadsUi', i) as IDataObject).threadsValues as IDataObject[]; + const body: IConversation = { + mailboxId, + status, + subject, + type, + }; + Object.assign(body, additionalFields); + if (additionalFields.customerId) { + body.customer = { + id: additionalFields.customerId, + }; + //@ts-ignore + delete body.customerId; + } + if (additionalFields.customerEmail) { + body.customer = { + email: additionalFields.customerEmail, + }; + //@ts-ignore + delete body.customerEmail; + } + if (body.customer === undefined) { + throw new Error('Either customer email or customer ID must be set'); + } + if (threads) { + for (let i = 0; i < threads.length; i++) { + if (threads[i].type === '' || threads[i].text === '') { + throw new Error('Chat Threads cannot be empty'); + } + if (threads[i].type !== 'note') { + threads[i].customer = body.customer; + } + } + body.threads = threads; + } + responseData = await helpscoutApiRequest.call(this, 'POST', '/v2/conversations', body, qs, undefined, { resolveWithFullResponse: true }); + const id = responseData.headers['resource-id']; + const uri = responseData.headers.location; + if (resolveData) { + responseData = await helpscoutApiRequest.call(this, 'GET', '', {}, {}, uri); + } else { + responseData = { + id, + uri, + }; + } + } + //https://developer.helpscout.com/mailbox-api/endpoints/conversations/delete + if (operation === 'delete') { + const conversationId = this.getNodeParameter('conversationId', i) as string; + responseData = await helpscoutApiRequest.call(this, 'DELETE', `/v2/conversations/${conversationId}`); + responseData = { success: true }; + } + //https://developer.helpscout.com/mailbox-api/endpoints/conversations/get + if (operation === 'get') { + const conversationId = this.getNodeParameter('conversationId', i) as string; + responseData = await helpscoutApiRequest.call(this, 'GET', `/v2/conversations/${conversationId}`); + } + //https://developer.helpscout.com/mailbox-api/endpoints/conversations/list + if (operation === 'getAll') { + const options = this.getNodeParameter('options', i) as IDataObject; + Object.assign(qs, options); + responseData = await helpscoutApiRequestAllItems.call(this, '_embedded.conversations', 'GET', '/v2/conversations', {}, qs); + } + } + if (resource === 'customer') { + //https://developer.helpscout.com/mailbox-api/endpoints/customers/create + if (operation === 'create') { + const resolveData = this.getNodeParameter('resolveData', i) as boolean; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const chats = (this.getNodeParameter('chatsUi', i) as IDataObject).chatsValues as IDataObject[]; + const address = (this.getNodeParameter('addressUi', i) as IDataObject).addressValue as IDataObject; + const emails = (this.getNodeParameter('emailsUi', i) as IDataObject).emailsValues as IDataObject[]; + const phones = (this.getNodeParameter('phonesUi', i) as IDataObject).phonesValues as IDataObject[]; + const socialProfiles = (this.getNodeParameter('socialProfilesUi', i) as IDataObject).socialProfilesValues as IDataObject[]; + const websites = (this.getNodeParameter('websitesUi', i) as IDataObject).websitesValues as IDataObject[]; + let body: ICustomer = {}; + body = Object.assign({}, additionalFields); + if (body.age) { + body.age = body.age.toString(); + } + if (chats) { + body.chats = chats; + } + if (address) { + body.address = address; + body.address.lines = [address.line1, address.line2]; + } + if (emails) { + body.emails = emails; + } + if (phones) { + body.phones = phones; + } + if (socialProfiles) { + body.socialProfiles = socialProfiles; + } + if (websites) { + body.websites = websites; + } + if (Object.keys(body).length === 0) { + throw new Error('You have to set at least one field'); + } + responseData = await helpscoutApiRequest.call(this, 'POST', '/v2/customers', body, qs, undefined, { resolveWithFullResponse: true }); + const id = responseData.headers['resource-id']; + const uri = responseData.headers.location; + if (resolveData) { + responseData = await helpscoutApiRequest.call(this, 'GET', '', {}, {}, uri); + } else { + responseData = { + id, + uri, + }; + } + } + //https://developer.helpscout.com/mailbox-api/endpoints/customer_properties/list + if (operation === 'properties') { + responseData = await helpscoutApiRequestAllItems.call(this, '_embedded.customer-properties', 'GET', '/v2/customer-properties', {}, qs); + } + //https://developer.helpscout.com/mailbox-api/endpoints/customers/get + if (operation === 'get') { + const customerId = this.getNodeParameter('customerId', i) as string; + responseData = await helpscoutApiRequest.call(this, 'GET', `/v2/customers/${customerId}`); + } + //https://developer.helpscout.com/mailbox-api/endpoints/customers/list + if (operation === 'getAll') { + const options = this.getNodeParameter('options', i) as IDataObject; + Object.assign(qs, options); + responseData = await helpscoutApiRequestAllItems.call(this, '_embedded.customers', 'GET', '/v2/customers', {}, qs); + } + //https://developer.helpscout.com/mailbox-api/endpoints/customers/overwrite/ + if (operation === 'update') { + const customerId = this.getNodeParameter('customerId', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + let body: ICustomer = {}; + body = Object.assign({}, updateFields); + if (body.age) { + body.age = body.age.toString(); + } + if (Object.keys(body).length === 0) { + throw new Error('You have to set at least one field'); + } + responseData = await helpscoutApiRequest.call(this, 'PUT', `/v2/customers/${customerId}`, body, qs, undefined, { resolveWithFullResponse: true }); + responseData = { success: true }; + } + } + if (resource === 'mailbox') { + //https://developer.helpscout.com/mailbox-api/endpoints/mailboxes/get + if (operation === 'get') { + const mailboxId = this.getNodeParameter('mailboxId', i) as string; + responseData = await helpscoutApiRequest.call(this, 'GET', `/v2/mailboxes/${mailboxId}`, {}, qs); + } + //https://developer.helpscout.com/mailbox-api/endpoints/mailboxes/list + if (operation === 'getAll') { + responseData = await helpscoutApiRequestAllItems.call(this, '_embedded.mailboxes', 'GET', '/v2/mailboxes', {}, qs); + } + } + if (resource === 'thread') { + //https://developer.helpscout.com/mailbox-api/endpoints/conversations/threads/chat + if (operation === 'create') { + const conversationId = this.getNodeParameter('conversationId', i) as string; + const type = this.getNodeParameter('type', i) as string; + const text = this.getNodeParameter('text', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const attachments = this.getNodeParameter('attachmentsUi', i) as IDataObject; + const body: IThread = { + text, + attachments: [], + }; + Object.assign(body, additionalFields); + if (additionalFields.customerId) { + body.customer = { + id: additionalFields.customerId, + }; + //@ts-ignore + delete body.customerId; + } + if (additionalFields.customerEmail) { + body.customer = { + email: additionalFields.customerEmail, + }; + //@ts-ignore + delete body.customerEmail; + } + if (body.customer === undefined) { + throw new Error('Either customer email or customer ID must be set'); + } + if (attachments) { + if (attachments.attachmentsValues + && (attachments.attachmentsValues as IDataObject[]).length !== 0) { + body.attachments?.push.apply(body.attachments, attachments.attachmentsValues as IAttachment[]); + } + if (attachments.attachmentsBinary + && (attachments.attachmentsBinary as IDataObject[]).length !== 0 + && items[i].binary) { + const mapFunction = (value: IDataObject): IAttachment => { + const binaryProperty = (items[i].binary as IBinaryKeyData)[value.property as string]; + if (binaryProperty) { + return { + fileName: binaryProperty.fileName || 'unknown', + data: binaryProperty.data, + mimeType: binaryProperty.mimeType, + }; + } else { + throw new Error(`Binary property ${value.property} does not exist on input`); + } + }; + body.attachments?.push.apply(body.attachments, (attachments.attachmentsBinary as IDataObject[]).map(mapFunction) as IAttachment[]); + } + } + responseData = await helpscoutApiRequest.call(this, 'POST', `/v2/conversations/${conversationId}/chats`, body); + responseData = { success: true }; + } + //https://developer.helpscout.com/mailbox-api/endpoints/conversations/threads/list + if (operation === 'getAll') { + const conversationId = this.getNodeParameter('conversationId', i) as string; + responseData = await helpscoutApiRequestAllItems.call(this, '_embedded.threads', 'GET', `/v2/conversations/${conversationId}/threads`); + } + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/HelpScout/HelpScoutTrigger.node.ts b/packages/nodes-base/nodes/HelpScout/HelpScoutTrigger.node.ts new file mode 100644 index 0000000000..ecb7e2a12a --- /dev/null +++ b/packages/nodes-base/nodes/HelpScout/HelpScoutTrigger.node.ts @@ -0,0 +1,202 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + INodeTypeDescription, + INodeType, + IWebhookResponseData, + IDataObject, +} from 'n8n-workflow'; + +import { + helpscoutApiRequest, + helpscoutApiRequestAllItems, +} from './GenericFunctions'; + +import { createHmac } from 'crypto'; + +export class HelpScoutTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'HelpScout Trigger', + name: 'helpScoutTrigger', + icon: 'file:helpScout.png', + group: ['trigger'], + version: 1, + description: 'Starts the workflow when HelpScout events occure.', + defaults: { + name: 'HelpScout Trigger', + color: '#1392ee', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'helpScoutOAuth2Api', + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Events', + name: 'events', + type: 'multiOptions', + options: [ + { + name: 'convo.agent.reply.created', + value: 'convo.agent.reply.created', + }, + { + name: 'convo.assigned', + value: 'convo.assigned', + }, + { + name: 'convo.created', + value: 'convo.created', + }, + { + name: 'convo.customer.reply.created', + value: 'convo.customer.reply.created', + }, + { + name: 'convo.deleted', + value: 'convo.deleted', + }, + { + name: 'convo.merged', + value: 'convo.merged', + }, + { + name: 'convo.moved', + value: 'convo.moved', + }, + { + name: 'convo.note.created', + value: 'convo.note.created', + }, + { + name: 'convo.status', + value: 'convo.status', + }, + { + name: 'convo.tags', + value: 'convo.tags', + }, + { + name: 'customer.created', + value: 'customer.created', + }, + { + name: 'satisfaction.ratings', + value: 'satisfaction.ratings', + }, + ], + default: [], + required: true, + }, + ], + + }; + + // @ts-ignore (because of request) + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + const events = this.getNodeParameter('events') as string; + + // Check all the webhooks which exist already if it is identical to the + // one that is supposed to get created. + const endpoint = '/v2/webhooks'; + const data = await helpscoutApiRequestAllItems.call(this, '_embedded.webhooks', 'GET', endpoint, {}); + + for (const webhook of data) { + if (webhook.url === webhookUrl) { + for (const event of events) { + if (!webhook.events.includes(event) + && webhook.state === 'enabled') { + return false; + } + } + } + // Set webhook-id to be sure that it can be deleted + webhookData.webhookId = webhook.id as string; + return true; + } + return false; + }, + async create(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default'); + const events = this.getNodeParameter('events') as string; + + const endpoint = '/v2/webhooks'; + + const body = { + url: webhookUrl, + events, + secret: Math.random().toString(36).substring(2, 15), + }; + + const responseData = await helpscoutApiRequest.call(this, 'POST', endpoint, body, {}, undefined, { resolveWithFullResponse: true }); + + if (responseData.headers['resource-id'] === undefined) { + // Required data is missing so was not successful + return false; + } + + webhookData.webhookId = responseData.headers['resource-id'] as string; + webhookData.secret = body.secret; + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId !== undefined) { + + const endpoint = `/v2/webhooks/${webhookData.webhookId}`; + try { + await helpscoutApiRequest.call(this, 'DELETE', endpoint); + } catch (e) { + return false; + } + + // Remove from the static workflow data so that it is clear + // that no webhooks are registred anymore + delete webhookData.webhookId; + delete webhookData.secret; + } + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const req = this.getRequestObject(); + const bodyData = this.getBodyData(); + const headerData = this.getHeaderData() as IDataObject; + const webhookData = this.getWorkflowStaticData('node'); + if (headerData['x-helpscout-signature'] === undefined) { + return {}; + } + //@ts-ignore + const computedSignature = createHmac('sha1', webhookData.secret as string).update(req.rawBody).digest('base64'); + if (headerData['x-helpscout-signature'] !== computedSignature) { + return {}; + } + return { + workflowData: [ + this.helpers.returnJsonArray(bodyData), + ], + }; + } +} diff --git a/packages/nodes-base/nodes/HelpScout/MailboxDescription.ts b/packages/nodes-base/nodes/HelpScout/MailboxDescription.ts new file mode 100644 index 0000000000..923b01677f --- /dev/null +++ b/packages/nodes-base/nodes/HelpScout/MailboxDescription.ts @@ -0,0 +1,54 @@ +import { INodeProperties } from "n8n-workflow"; + +export const mailboxOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'mailbox', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get data of a mailbox', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all mailboxes', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const mailboxFields = [ + +/* -------------------------------------------------------------------------- */ +/* mailbox:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Mailbox ID', + name: 'mailboxId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'mailbox', + ], + operation: [ + 'get', + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/HelpScout/ThreadDescription.ts b/packages/nodes-base/nodes/HelpScout/ThreadDescription.ts new file mode 100644 index 0000000000..230b99ce56 --- /dev/null +++ b/packages/nodes-base/nodes/HelpScout/ThreadDescription.ts @@ -0,0 +1,257 @@ +import { INodeProperties } from "n8n-workflow"; + +export const threadOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'thread', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new chat thread', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all chat threads', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const threadFields = [ +/* -------------------------------------------------------------------------- */ +/* thread:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Conversation ID', + name: 'conversationId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'thread', + ], + operation: [ + 'create', + ], + }, + }, + description: 'conversation ID', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + required: true, + displayOptions: { + show: { + resource: [ + 'thread', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + name: 'Chat', + value: 'chat' + }, + { + name: 'Customer', + value: 'customer' + }, + { + name: 'Note', + value: 'note' + }, + { + name: 'Phone', + value: 'phone' + }, + { + name: 'Reply', + value: 'reply' + }, + ], + default: '', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + required: true, + displayOptions: { + show: { + resource: [ + 'thread', + ], + operation: [ + 'create', + ], + }, + }, + description: 'The chat text', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'thread', + ], + }, + }, + options: [ + { + displayName: 'Customer Email', + name: 'customerEmail', + type: 'string', + default: '', + }, + { + displayName: 'Customer ID', + name: 'customerId', + type: 'number', + default: 0, + }, + { + displayName: 'Draft', + name: 'draft', + type: 'boolean', + default: false, + displayOptions: { + show: { + '/type': [ + 'note', + ], + }, + }, + description: 'If set to true, a draft reply is created', + }, + { + displayName: 'Imported', + name: 'imported', + type: 'boolean', + default: false, + description: 'When imported is set to true, no outgoing emails or notifications will be generated.', + }, + { + displayName: 'Created At', + name: 'createdAt', + type: 'dateTime', + default: '', + }, + ] + }, + { + displayName: 'Attachments', + name: 'attachmentsUi', + placeholder: 'Add Attachments', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'thread', + ], + }, + }, + options: [ + { + name: 'attachmentsValues', + displayName: 'Attachments Values', + values: [ + { + displayName: 'FileName', + name: 'fileName', + type: 'string', + default: '', + description: 'Attachment’s file name', + }, + { + displayName: 'Mime Type', + name: 'mimeType', + type: 'string', + default: '', + description: 'Attachment’s mime type', + }, + { + displayName: 'Data', + name: 'data', + type: 'string', + default: '', + placeholder: 'ZXhhbXBsZSBmaWxl', + description: 'Base64-encoded stream of data.', + }, + ], + }, + { + name: 'attachmentsBinary', + displayName: 'Attachments Binary', + values: [ + { + displayName: 'Property', + name: 'property', + type: 'string', + default: 'data', + description: 'Name of the binary properties which contain data which should be added to email as attachment', + }, + ], + }, + ], + default: '', + description: 'Array of supported attachments to add to the message.', + }, +/* -------------------------------------------------------------------------- */ +/* thread:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Conversation ID', + name: 'conversationId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'thread', + ], + operation: [ + 'getAll', + ], + }, + }, + description: 'conversation ID', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/HelpScout/ThreadInterface.ts b/packages/nodes-base/nodes/HelpScout/ThreadInterface.ts new file mode 100644 index 0000000000..b6cbbda213 --- /dev/null +++ b/packages/nodes-base/nodes/HelpScout/ThreadInterface.ts @@ -0,0 +1,15 @@ +import { IDataObject } from "n8n-workflow"; + +export interface IAttachment { + fileName?: string; + mimeType?: string; + data?: string; +} + +export interface IThread { + createdAt?: string; + customer?: IDataObject; + imported?: boolean; + text?: string; + attachments?: IAttachment[]; +} diff --git a/packages/nodes-base/nodes/HelpScout/helpScout.png b/packages/nodes-base/nodes/HelpScout/helpScout.png new file mode 100644 index 0000000000000000000000000000000000000000..a2be95ba7f734e4f120cc2b6624a660b4ddec7b2 GIT binary patch literal 4862 zcmY*cc|4Tw*PfvfW6xMKVNk=^Mr9om8rxuuB_!JzjO=77M8>|a5i&z#ON*r>`@R*G zL?U|#*_E}I@2~g!d*A!{ocmn&xvz7b`#gUrI`f-CRO@`4ghl#| zT`xuWmRw;LWll4@M5%srZ+u+NJyS<+6t>{7^Rqvzr(fY>-66&B8a4>|BL~vnR1+!) z-)8-)S}mOc79IU02v@EC9gbayD_Y7{zL-sXQ}K=PGiWn!_0ip*wN9&`h1xRn%oGTa z>)|s=ZrN#{sHA8WL^3Ig%uPqRZUjZ#EXB~s88Z66yfLBCde`R5(D+gQ(;76CQ_xGm zzUq4kV{{U5t=P2B4|r!UMc~R0%X2&S3pGf!cIi8>r|`pVKB^%JM8{c)>A`4_sj*A+ z1Gc2M9ly~``^XocZOHmAOJxgl1@<|&$H^6*t9D2EZY|Ccn{I8z1KIhiq*XhseZ*za z`6;7zy|UDN^a1_!Ew|uT{F7fZb5hu^&?S$3d-%MFeO1${{Ojqm#{9=mU&acB%#-9c zC8i~#2ju5_jax>Zl%;j#w)nNzT(;cUP(IZh^XA6G^)ByyH{(Ye^K(BgKkU03sClqg zAE><(?47T;vKX|mNy?NweL#JX9?sbw9(|93zPd9%eN-`~%SOxV!Z)XLPb#&L<%aak zO}Cg`DWCHQg!ybmd4ifqwa2- z@ZHz_)#46Os`t4n`3@4xxdsXAPa>@F99Kdlh^10E?aIJmF z?uhAtZhEcHUS~yzWcFmwKi|5Evha{%L6<)O?YZol@fsv(mT!e`MO8~QTnL6Ab~d~V z?hSqy+#lR>lO{#EFK?!CO3G8f$EI_~Ws}cx;x1PpPmR-J=B;vFAz@d}b6no7a#Kcj zvKtqe=<(`CBVn0Om-e~xr5~w4S?~hii}$S>rObda`xP`>`bVawCX1fwVo);=eTBxW z9PuI=4)N)7Ccj=^-=x4Y4Cin}He+RDj?#)xS6QQ~iu}lKH2Vk8p;Grs+>ej=Z^1Mc zgG%m8%F~-Vq86bRs;~tbsY8e0piD)9smNK0ni>J z;PC|j@&Q2qU;scDc=lh61H%7uXaImHBH+|tj^(jG8QRBooc-6+kZJy9pvkoV#V6E{ z!*iv_j=@vg!Uq6gxQZg`Au%MtI%G<$F z38#tvn|{1ffjRs7dMZgv`}_M#`CpXs@OF|$Dk>^U%g9R0%1RzHBz*$ieeD7y-F<}r zBl5pGngk!bH__9V=;1DSqHAaGaobk~20JPA@A%I-eTk0$t>o_Wx2@v_rB8OGky0|! z|7stnqE4bphTcTNapejI*jI#6@Z%=+uW!E+F^9*6A zHyg=OA{nz+y5m-|M4t8HGECxUKvAz{o+YOP@n#G{OsUPG?J$O?!a@m-=O_H@R^MO5 z=v^Eaj)|(3d-#F6zopW2^s{#Ukvl5mN)t2%b2l{fSJ)#=6f7Bt7bY=fG4Y(GksIgj zaA+Q3d$HILKTq2k8LID(MzRX?Pf`oMix<)fm|Mm|bx zic48ZL1@?!bLZ1ECZySAQm-mR{CvEsFttZ{bqfy@O&*)NDY{`5Vdy><2RC=0dJE@a zo*SHc;hMda_P{^0cW`NAu4VwD3kZ%7JSAr=r!Lk}PZWsL_RH)JyBV_EIvTXQeLXha z$~k;+P(KGLLy|q6`Wiyt_ucz>{mWr##=a_%`$sGJh-5dt%ExY(V1elaA_;!I04i;7 z^Ww+)8jOoTUy9(7y~gcl2lmF#tj@-3BR`Qepf$X}LW&h#I&;YL`i5f6_x&mnNTN26 zgZJbDObep)~SAAM$Dl?XAHa6%WP1a&p`xQn_-Fh&sGhtW#%euLIf32c?X`Nlf zh61@hP8roCp{F=gRWh?qRTTH86Qi8c|4NK1E8A4rU?)r5uvP21uY@|MLG1fbQ`(BS z9?t&KQdB0xWVrhNhkmTsHZ_!nDj`F8&nx$Q5lfx8aJj>Ju|v0mFg)s9%%Ar_tF^M= zy>Jwe>hdiZjxx)~SxsgP`LNk}o{>)BsT#U7y*fzO2_`_v{Gm7=WB6U>4yDId3aZ?< zB<(#h)`ct8KwW5aFaVzPPT#gmq%BGPy!H#cpM`YV)-NKkFn0?sOVNQI_znNs&0iXhnX8>fZm(bacY{$nQ3L?dK)Kh86{BeVsW->@@ ztY;JhA@LnFF>!?F@PgY642xm0XPI_15i^7l?=t$mj`iZ(dpiPA89z9K4EGxx@*=J< z^ua=ftcoSwY!&D^1u5yKq3TqW5(rWjff$5`)`v_#>Rr`(ugGnHWEca(_t{?{^nzbi z#3ZK0SWj7BQHs^iLMAL{9y-`kK}?k2?8c)2YSDYdDs8Nr#EW=w*Z#u8E}D(01cuW7 z%@Tc4JUC;+@eI~EahL6@@&)v)A@}`CCOl6&Ik%I0)SEfdKf=OMndVzBguF1c=-QRF z93(Bc$(Gb?CN>u%lA(UO8frl4JZzI~A|Y~=;nhoErd19TX^nd2v8sb^={OEaYJDn| z=%l*B)SGsX9~;AXT|g!HDEl$4I*)OrKctjTn^a;e?=pqG87`(4zW|NxZfY{) zPm8fsXb&3dubyD)HEvo;1DtuIPe9^T>b_7ot+HTMO$?I))7uh6U_BW4O6TD6otfo${ST|ZgPsZg0yqluZ*ubzgnX{>_~@)) zhQ}FpWs3%`VH)m~tLeW#%=FHG%;)?R`vVLB?Z~MG3aqa8$;DGSo(T8lSxJsx(z4~T zNmugRJ~Kvj-WGH}hisaROj!&OCg(26XWX}KIHNOpG=3zOCa+XHWl1T&+U(Ak*lpil z($ui<;9Z~3+=Bti{ip;kk2K+~0A!V-QvOj@b9-4+*iRdg5{azj(j|uFMU=P&M^p1|^O~*60s9B!zW17j&bQj~CL$Xl zAxbob{9Q@15=jDfZJ*n8<@hWrh}12Xh}j(T)TAk%pOp*|A9m@}&)66u`{NIJO?wme zpRi_pun^kl6!0GB7`x^*EJfd#Q z1K69QEMyUP2g4!m;i`|-eOIXXs?OmyTZksbQzxXO+t@{)x)#ZQIinzHkQ0@uB;TLc z0X{cw^2|Q@-k(!v<&Ri;*m?o6y3)%_X>IZvmFI4!JF2(*nFmB5vK~5GeRh55_nFU_ zehvkBkaT-Xfh=M3JK1b)fvbjQh`!*SGehs`d9*2cr3-hUqNW%C)di+k<*L$uB;jgA zKveUh0T+ybI;GJNzina1)r=PpJMR<%QDNe&H~Q)7C2w=b-@MXTB5;D)i?>P{ZG%a5s|nVsrJpFLIE~!+_XG?kFnf83Aefd%rNMd{Z&vnh}Ojw3;!kh2L*qC61(#Lf2D9kIa2ujwIBpjI1Y`~G; zdLdy?Vd?g^zk$FZ&-Z~YG`3UbQ~V&braIJ)E1X9HntPp8ke-@zkjj6Wnqe9;m}uQ` z-k5*$yt@`l9yDY4VYtiHtZnc)D>l?j>bd30p;TA(DX4+V1J|(Do8Au?e!czq2{Igl zfG2n`=_u~#yIl1w=bX_Tj-6^G87qdW`D{d$D&q50^(ypjF+9x>rP%8lp9_V*ym@x z+!r=5qtl{mVvDCB)9V~WFs^@@#jO%-4FS_|;ClsEW%?=5#yz9(%JzEMHUrp8XSl?j zU{0(-VPhfLmT56k-G)ERE@<6*qq#@1+pjROef;vu+iQAgJveRAGP5!)ItRy6vG%S^ zQ~#^ohd8VOpcgD+&@gx^0`GY$@Gbu+8`$O75rgY>WK0_qkWYH^Egq81uc>+Q`d z&dA2CwQN=X1P?x4dGazKeWpRE9yOcs=(ty;0*yvgR^1xXfssy$)Z9}v{;?kp->Xf+ z#g5|3@Y}54BZ7L?&X{qD-%ogP{01#H&K5*lU{-Z{BDyDDN`>)d91 zP0oGR7w2fI%bDIf)a5Sr&e8M!hJnADTv-dtg6G-hjRkqYTvN%Zeh7r617CAyrMDQlIt9oo-N^2;F&Izt0?bAx@4mlTh1$#iDr6BekG)8N#A{ye zvQ@~-B1oY_f4_@CDu&l^RkEXQJWJ4%EKZ6QLqD=V-N8?59oSWwBp9;xgH#_Fr1{i6 zS9Ky1$1)*H_BHgue87LmPrPWuhh+~Vh5ou;GI_Wa#~!#YpmC=4?KGk|Oi!Jag6`TC)-L Vf)A~m9w$G3I@b&|pQ+p4{~u?LqWk~= literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 746f91f723..282af6371b 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -42,7 +42,8 @@ "dist/credentials/GithubApi.credentials.js", "dist/credentials/GithubOAuth2Api.credentials.js", "dist/credentials/GitlabApi.credentials.js", - "dist/credentials/GoogleApi.credentials.js", + "dist/credentials/GoogleApi.credentials.js", + "dist/credentials/HelpScoutOAuth2Api.credentials.js", "dist/credentials/HttpBasicAuth.credentials.js", "dist/credentials/HttpDigestAuth.credentials.js", "dist/credentials/HttpHeaderAuth.credentials.js", @@ -118,7 +119,9 @@ "dist/nodes/Gitlab/GitlabTrigger.node.js", "dist/nodes/Google/GoogleDrive.node.js", "dist/nodes/Google/GoogleSheets.node.js", - "dist/nodes/GraphQL/GraphQL.node.js", + "dist/nodes/GraphQL/GraphQL.node.js", + "dist/nodes/HelpScout/HelpScout.node.js", + "dist/nodes/HelpScout/HelpScoutTrigger.node.js", "dist/nodes/HtmlExtract/HtmlExtract.node.js", "dist/nodes/HttpRequest.node.js", "dist/nodes/Hubspot/Hubspot.node.js", From 8fcd8839dd6cc8d3bc890ed620591b7b19806446 Mon Sep 17 00:00:00 2001 From: ricardo Date: Mon, 16 Mar 2020 22:07:15 -0400 Subject: [PATCH 026/165] :zap: small fix --- packages/nodes-base/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 207acd0351..20e30fe8c1 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -42,9 +42,9 @@ "dist/credentials/GithubApi.credentials.js", "dist/credentials/GithubOAuth2Api.credentials.js", "dist/credentials/GitlabApi.credentials.js", - "dist/credentials/GoogleApi.credentials.js", - "dist/credentials/HelpScoutOAuth2Api.credentials.js", - "dist/credentials/GoogleOAuth2Api.credentials.js", + "dist/credentials/GoogleApi.credentials.js", + "dist/credentials/GoogleOAuth2Api.credentials.js", + "dist/credentials/HelpScoutOAuth2Api.credentials.js", "dist/credentials/HttpBasicAuth.credentials.js", "dist/credentials/HttpDigestAuth.credentials.js", "dist/credentials/HttpHeaderAuth.credentials.js", From 79ba4dd43d5d992ec966ffaeda4a98913868a435 Mon Sep 17 00:00:00 2001 From: ricardo Date: Tue, 17 Mar 2020 14:39:20 -0400 Subject: [PATCH 027/165] :zap: small improvements --- .../nodes/Microsoft/MicrosoftExcel.node.ts | 9 +++++ .../nodes/Microsoft/TableDescription.ts | 40 +++++++++++++++++++ .../nodes/Microsoft/WorksheetDescription.ts | 20 ++++++++++ 3 files changed, 69 insertions(+) diff --git a/packages/nodes-base/nodes/Microsoft/MicrosoftExcel.node.ts b/packages/nodes-base/nodes/Microsoft/MicrosoftExcel.node.ts index c62730449d..fd3de90659 100644 --- a/packages/nodes-base/nodes/Microsoft/MicrosoftExcel.node.ts +++ b/packages/nodes-base/nodes/Microsoft/MicrosoftExcel.node.ts @@ -206,6 +206,9 @@ export class MicrosoftExcel implements INodeType { if (!rawData) { //@ts-ignore responseData = responseData.map(column => ({ name: column.name })); + } else { + const dataProperty = this.getNodeParameter('dataProperty', i) as string; + responseData = { [dataProperty] : responseData }; } } } @@ -242,6 +245,9 @@ export class MicrosoftExcel implements INodeType { result.push({ ...object }); } responseData = result; + } else { + const dataProperty = this.getNodeParameter('dataProperty', i) as string; + responseData = { [dataProperty] : responseData }; } } } @@ -321,6 +327,9 @@ export class MicrosoftExcel implements INodeType { result.push({ ...object }); } responseData = result; + } else { + const dataProperty = this.getNodeParameter('dataProperty', i) as string; + responseData = { [dataProperty] : responseData }; } } } diff --git a/packages/nodes-base/nodes/Microsoft/TableDescription.ts b/packages/nodes-base/nodes/Microsoft/TableDescription.ts index 6dc6de780b..8c491a16e1 100644 --- a/packages/nodes-base/nodes/Microsoft/TableDescription.ts +++ b/packages/nodes-base/nodes/Microsoft/TableDescription.ts @@ -260,6 +260,26 @@ export const tableFields = [ default: false, description: 'If the data should be returned RAW instead of parsed into keys according to their header.', }, + { + displayName: 'Data Property', + name: 'dataProperty', + type: 'string', + default: 'data', + displayOptions: { + show: { + operation: [ + 'getRows' + ], + resource: [ + 'table', + ], + rawData: [ + true, + ], + }, + }, + description: 'The name of the property into which to write the RAW data.', + }, { displayName: 'Filters', name: 'filters', @@ -415,6 +435,26 @@ export const tableFields = [ default: false, description: 'If the data should be returned RAW instead of parsed into keys according to their header.', }, + { + displayName: 'Data Property', + name: 'dataProperty', + type: 'string', + default: 'data', + displayOptions: { + show: { + operation: [ + 'getColumns' + ], + resource: [ + 'table', + ], + rawData: [ + true, + ], + }, + }, + description: 'The name of the property into which to write the RAW data.', + }, { displayName: 'Filters', name: 'filters', diff --git a/packages/nodes-base/nodes/Microsoft/WorksheetDescription.ts b/packages/nodes-base/nodes/Microsoft/WorksheetDescription.ts index 204e5f0e41..f50fbb3ece 100644 --- a/packages/nodes-base/nodes/Microsoft/WorksheetDescription.ts +++ b/packages/nodes-base/nodes/Microsoft/WorksheetDescription.ts @@ -201,6 +201,26 @@ export const worksheetFields = [ default: false, description: 'If the data should be returned RAW instead of parsed into keys according to their header.', }, + { + displayName: 'Data Property', + name: 'dataProperty', + type: 'string', + default: 'data', + displayOptions: { + show: { + operation: [ + 'getContent' + ], + resource: [ + 'worksheet', + ], + rawData: [ + true, + ], + }, + }, + description: 'The name of the property into which to write the RAW data.', + }, { displayName: 'Data Start Row', name: 'dataStartRow', From b60c9be282c8ea603acb8e63da961c38e8bdb510 Mon Sep 17 00:00:00 2001 From: ricardo Date: Sun, 22 Mar 2020 13:03:56 -0400 Subject: [PATCH 028/165] :zap: added folder and changed name to credentials --- ...ls.ts => MicrosoftExcelOAuth2Api.credentials.ts} | 4 ++-- .../nodes/Microsoft/{ => Excel}/GenericFunctions.ts | 2 +- .../Microsoft/{ => Excel}/MicrosoftExcel.node.ts | 2 +- .../nodes/Microsoft/{ => Excel}/TableDescription.ts | 0 .../Microsoft/{ => Excel}/WorkbookDescription.ts | 0 .../Microsoft/{ => Excel}/WorksheetDescription.ts | 0 .../nodes/Microsoft/{ => Excel}/excel.png | Bin packages/nodes-base/package.json | 4 ++-- 8 files changed, 6 insertions(+), 6 deletions(-) rename packages/nodes-base/credentials/{MicrosoftOAuth2Api.credentials.ts => MicrosoftExcelOAuth2Api.credentials.ts} (91%) rename packages/nodes-base/nodes/Microsoft/{ => Excel}/GenericFunctions.ts (96%) rename packages/nodes-base/nodes/Microsoft/{ => Excel}/MicrosoftExcel.node.ts (99%) rename packages/nodes-base/nodes/Microsoft/{ => Excel}/TableDescription.ts (100%) rename packages/nodes-base/nodes/Microsoft/{ => Excel}/WorkbookDescription.ts (100%) rename packages/nodes-base/nodes/Microsoft/{ => Excel}/WorksheetDescription.ts (100%) rename packages/nodes-base/nodes/Microsoft/{ => Excel}/excel.png (100%) diff --git a/packages/nodes-base/credentials/MicrosoftOAuth2Api.credentials.ts b/packages/nodes-base/credentials/MicrosoftExcelOAuth2Api.credentials.ts similarity index 91% rename from packages/nodes-base/credentials/MicrosoftOAuth2Api.credentials.ts rename to packages/nodes-base/credentials/MicrosoftExcelOAuth2Api.credentials.ts index 2519ba583b..d6e8e0f452 100644 --- a/packages/nodes-base/credentials/MicrosoftOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/MicrosoftExcelOAuth2Api.credentials.ts @@ -3,8 +3,8 @@ import { NodePropertyTypes, } from 'n8n-workflow'; -export class MicrosoftOAuth2Api implements ICredentialType { - name = 'microsoftOAuth2Api'; +export class MicrosoftExcelOAuth2Api implements ICredentialType { + name = 'microsoftExcelOAuth2Api'; extends = [ 'oAuth2Api', ]; diff --git a/packages/nodes-base/nodes/Microsoft/GenericFunctions.ts b/packages/nodes-base/nodes/Microsoft/Excel/GenericFunctions.ts similarity index 96% rename from packages/nodes-base/nodes/Microsoft/GenericFunctions.ts rename to packages/nodes-base/nodes/Microsoft/Excel/GenericFunctions.ts index b4f68be343..33090090b3 100644 --- a/packages/nodes-base/nodes/Microsoft/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Microsoft/Excel/GenericFunctions.ts @@ -24,7 +24,7 @@ export async function microsoftApiRequest(this: IExecuteFunctions | IExecuteSing options.headers = Object.assign({}, options.headers, headers); } //@ts-ignore - return await this.helpers.requestOAuth.call(this, 'microsoftOAuth2Api', options); + return await this.helpers.requestOAuth.call(this, 'microsoftExcelOAuth2Api', options); } catch (error) { if (error.response && error.response.body && error.response.body.error && error.response.body.error.message) { // Try to return the error prettier diff --git a/packages/nodes-base/nodes/Microsoft/MicrosoftExcel.node.ts b/packages/nodes-base/nodes/Microsoft/Excel/MicrosoftExcel.node.ts similarity index 99% rename from packages/nodes-base/nodes/Microsoft/MicrosoftExcel.node.ts rename to packages/nodes-base/nodes/Microsoft/Excel/MicrosoftExcel.node.ts index fd3de90659..d7c372c2db 100644 --- a/packages/nodes-base/nodes/Microsoft/MicrosoftExcel.node.ts +++ b/packages/nodes-base/nodes/Microsoft/Excel/MicrosoftExcel.node.ts @@ -49,7 +49,7 @@ export class MicrosoftExcel implements INodeType { outputs: ['main'], credentials: [ { - name: 'microsoftOAuth2Api', + name: 'microsoftExcelOAuth2Api', required: true, }, ], diff --git a/packages/nodes-base/nodes/Microsoft/TableDescription.ts b/packages/nodes-base/nodes/Microsoft/Excel/TableDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Microsoft/TableDescription.ts rename to packages/nodes-base/nodes/Microsoft/Excel/TableDescription.ts diff --git a/packages/nodes-base/nodes/Microsoft/WorkbookDescription.ts b/packages/nodes-base/nodes/Microsoft/Excel/WorkbookDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Microsoft/WorkbookDescription.ts rename to packages/nodes-base/nodes/Microsoft/Excel/WorkbookDescription.ts diff --git a/packages/nodes-base/nodes/Microsoft/WorksheetDescription.ts b/packages/nodes-base/nodes/Microsoft/Excel/WorksheetDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Microsoft/WorksheetDescription.ts rename to packages/nodes-base/nodes/Microsoft/Excel/WorksheetDescription.ts diff --git a/packages/nodes-base/nodes/Microsoft/excel.png b/packages/nodes-base/nodes/Microsoft/Excel/excel.png similarity index 100% rename from packages/nodes-base/nodes/Microsoft/excel.png rename to packages/nodes-base/nodes/Microsoft/Excel/excel.png diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 3bd0b4a6fd..b8ef81cded 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -55,7 +55,7 @@ "dist/credentials/MailgunApi.credentials.js", "dist/credentials/MandrillApi.credentials.js", "dist/credentials/MattermostApi.credentials.js", - "dist/credentials/MicrosoftOAuth2Api.credentials.js", + "dist/credentials/MicrosoftExcelOAuth2Api.credentials.js", "dist/credentials/MongoDb.credentials.js", "dist/credentials/MySql.credentials.js", "dist/credentials/NextCloudApi.credentials.js", @@ -135,7 +135,7 @@ "dist/nodes/Mandrill/Mandrill.node.js", "dist/nodes/Mattermost/Mattermost.node.js", "dist/nodes/Merge.node.js", - "dist/nodes/Microsoft/MicrosoftExcel.node.js", + "dist/nodes/Microsoft/Excel/MicrosoftExcel.node.js", "dist/nodes/MoveBinaryData.node.js", "dist/nodes/MongoDb/MongoDb.node.js", "dist/nodes/MySql/MySql.node.js", From 31ca6be3a55f2d996fc5d80d6e04d99194d96230 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 22 Mar 2020 18:26:42 +0100 Subject: [PATCH 029/165] :zap: Fix extend for grandchild credential --- .../src/components/CredentialsInput.vue | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/editor-ui/src/components/CredentialsInput.vue b/packages/editor-ui/src/components/CredentialsInput.vue index 76aa1c6672..8a846b4055 100644 --- a/packages/editor-ui/src/components/CredentialsInput.vue +++ b/packages/editor-ui/src/components/CredentialsInput.vue @@ -194,7 +194,8 @@ export default mixins( return this.credentialDataTemp; }, isOAuthType (): boolean { - return this.credentialTypeData.name === 'oAuth2Api' || (this.credentialTypeData.extends !== undefined && this.credentialTypeData.extends.includes('oAuth2Api')); + const types = this.parentTypes(this.credentialTypeData.name); + return types.includes('oAuth2Api'); }, isOAuthConnected (): boolean { if (this.isOAuthType === false) { @@ -205,6 +206,21 @@ export default mixins( }, }, methods: { + parentTypes (name: string): string[] { + const credentialType = this.$store.getters.credentialType(name); + + if (credentialType === undefined || credentialType.extends === undefined) { + return []; + } + + const types: string[] = []; + for (const typeName of credentialType.extends) { + types.push(typeName); + types.push.apply(types, this.parentTypes(typeName)); + } + + return types; + }, valueChanged (parameterData: IUpdateInformation) { const name = parameterData.name.split('.').pop() as string; // For a currently for me unknown reason can In not simply just From eaa84827c786fb17ce6a1e63dc8e1c427e33c08b Mon Sep 17 00:00:00 2001 From: ricardo Date: Sun, 22 Mar 2020 15:14:45 -0400 Subject: [PATCH 030/165] :zap: Small improvements --- .../MicrosoftExcelOAuth2Api.credentials.ts | 26 +------------ .../MicrosoftOAuth2Api.credentials.ts | 38 +++++++++++++++++++ packages/nodes-base/package.json | 1 + 3 files changed, 40 insertions(+), 25 deletions(-) create mode 100644 packages/nodes-base/credentials/MicrosoftOAuth2Api.credentials.ts diff --git a/packages/nodes-base/credentials/MicrosoftExcelOAuth2Api.credentials.ts b/packages/nodes-base/credentials/MicrosoftExcelOAuth2Api.credentials.ts index d6e8e0f452..3dc1f4b960 100644 --- a/packages/nodes-base/credentials/MicrosoftExcelOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/MicrosoftExcelOAuth2Api.credentials.ts @@ -6,22 +6,10 @@ import { export class MicrosoftExcelOAuth2Api implements ICredentialType { name = 'microsoftExcelOAuth2Api'; extends = [ - 'oAuth2Api', + 'microsoftOAuth2Api', ]; displayName = 'Microsoft OAuth2 API'; properties = [ - { - displayName: 'Authorization URL', - name: 'authUrl', - type: 'string' as NodePropertyTypes, - default: 'https://login.microsoftonline.com/{yourtenantid}/oauth2/v2.0/authorize', - }, - { - displayName: 'Access Token URL', - name: 'accessTokenUrl', - type: 'string' as NodePropertyTypes, - default: 'https://login.microsoftonline.com/{yourtenantid}/oauth2/v2.0/token', - }, //https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent { displayName: 'Scope', @@ -29,17 +17,5 @@ export class MicrosoftExcelOAuth2Api implements ICredentialType { type: 'hidden' as NodePropertyTypes, default: 'openid offline_access Files.ReadWrite', }, - { - displayName: 'Auth URI Query Parameters', - name: 'authQueryParameters', - type: 'hidden' as NodePropertyTypes, - default: 'response_mode=query', - }, - { - displayName: 'Authentication', - name: 'authentication', - type: 'hidden' as NodePropertyTypes, - default: 'body', - }, ]; } diff --git a/packages/nodes-base/credentials/MicrosoftOAuth2Api.credentials.ts b/packages/nodes-base/credentials/MicrosoftOAuth2Api.credentials.ts new file mode 100644 index 0000000000..aa98141e3f --- /dev/null +++ b/packages/nodes-base/credentials/MicrosoftOAuth2Api.credentials.ts @@ -0,0 +1,38 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class MicrosoftOAuth2Api implements ICredentialType { + name = 'microsoftOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Microsoft OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'string' as NodePropertyTypes, + default: 'https://login.microsoftonline.com/{yourtenantid}/oauth2/v2.0/authorize', + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'string' as NodePropertyTypes, + default: 'https://login.microsoftonline.com/{yourtenantid}/oauth2/v2.0/token', + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: 'response_mode=query', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'body', + }, + ]; +} diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 462c97bd71..c5fa0e2e47 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -56,6 +56,7 @@ "dist/credentials/MailgunApi.credentials.js", "dist/credentials/MandrillApi.credentials.js", "dist/credentials/MattermostApi.credentials.js", + "dist/credentials/MicrosoftOAuth2Api.credentials.js", "dist/credentials/MicrosoftExcelOAuth2Api.credentials.js", "dist/credentials/MongoDb.credentials.js", "dist/credentials/MySql.credentials.js", From 882afed0585ae3917a306eb8b2cdb8de8e11fa9a Mon Sep 17 00:00:00 2001 From: ricardo Date: Sun, 22 Mar 2020 15:43:35 -0400 Subject: [PATCH 031/165] :sparkles: Microsoft One Drive --- .../MicrosoftOneDriveOAuth2Api.credentials.ts | 21 + .../Microsoft/OneDrive/FileDescription.ts | 377 ++++++++++++++++++ .../Microsoft/OneDrive/FolderDescriptiont.ts | 75 ++++ .../Microsoft/OneDrive/GenericFunctions.ts | 83 ++++ .../OneDrive/MicrosoftOneDrive.node.ts | 215 ++++++++++ .../nodes/Microsoft/OneDrive/oneDrive.png | Bin 0 -> 4123 bytes packages/nodes-base/package.json | 2 + 7 files changed, 773 insertions(+) create mode 100644 packages/nodes-base/credentials/MicrosoftOneDriveOAuth2Api.credentials.ts create mode 100644 packages/nodes-base/nodes/Microsoft/OneDrive/FileDescription.ts create mode 100644 packages/nodes-base/nodes/Microsoft/OneDrive/FolderDescriptiont.ts create mode 100644 packages/nodes-base/nodes/Microsoft/OneDrive/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.ts create mode 100644 packages/nodes-base/nodes/Microsoft/OneDrive/oneDrive.png diff --git a/packages/nodes-base/credentials/MicrosoftOneDriveOAuth2Api.credentials.ts b/packages/nodes-base/credentials/MicrosoftOneDriveOAuth2Api.credentials.ts new file mode 100644 index 0000000000..adeeb79670 --- /dev/null +++ b/packages/nodes-base/credentials/MicrosoftOneDriveOAuth2Api.credentials.ts @@ -0,0 +1,21 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class MicrosoftOneDriveOAuth2Api implements ICredentialType { + name = 'microsoftOneDriveOAuth2Api'; + extends = [ + 'microsoftOAuth2Api', + ]; + displayName = 'Microsoft OAuth2 API'; + properties = [ + //https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: 'openid offline_access Files.ReadWrite.All', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Microsoft/OneDrive/FileDescription.ts b/packages/nodes-base/nodes/Microsoft/OneDrive/FileDescription.ts new file mode 100644 index 0000000000..32fe70a948 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/OneDrive/FileDescription.ts @@ -0,0 +1,377 @@ +import { INodeProperties } from "n8n-workflow"; + +export const fileOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'file', + ], + }, + }, + options: [ + { + name: 'Copy', + value: 'copy', + description: 'Copy a file', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a file', + }, + { + name: 'Download', + value: 'download', + description: 'Download a file', + }, + { + name: 'Get', + value: 'get', + description: 'Get a file', + }, + { + name: 'Search', + value: 'search', + description: 'Search a file', + }, + { + name: 'Upload', + value: 'upload', + description: 'Upload a file up to 4MB in size', + }, + ], + default: 'upload', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const fileFields = [ + +/* -------------------------------------------------------------------------- */ +/* file:copy */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'File ID', + name: 'fileId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'copy', + ], + resource: [ + 'file', + ], + }, + }, + default: '', + description: 'File ID', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'copy', + ], + resource: [ + 'file', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: `The new name for the copy. If this isn't provided, the same name will be used as the original.`, + }, + ], + }, + { + displayName: 'Parent Reference', + name: 'parentReference', + type: 'collection', + placeholder: 'Add Parent Reference', + description: 'Reference to the parent item the copy will be created in Details ', + displayOptions: { + show: { + operation: [ + 'copy', + ], + resource: [ + 'file', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Drive ID', + name: 'driveId', + type: 'string', + default: '', + description: 'Identifier of the drive instance that contains the item.', + }, + { + displayName: 'Drive Type', + name: 'driveType', + type: 'string', + default: '', + description: 'Identifies the type of drive.', + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + default: '', + description: 'Identifier of the item in the drive.', + }, + { + displayName: 'List ID', + name: 'listId', + type: 'string', + default: '', + description: 'Identifier of the list.', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'The name of the item being referenced', + }, + { + displayName: 'Path', + name: 'path', + type: 'string', + default: '', + description: 'Path that can be used to navigate to the item', + }, + { + displayName: 'Share ID', + name: 'shareId', + type: 'string', + default: '', + description: 'Identifier for a shared resource that can be accessed via the Shares API.', + }, + { + displayName: 'Site ID', + name: 'siteId', + type: 'string', + default: '', + description: 'Identifier of the site.', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* file:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'File ID', + name: 'fileId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'file', + ], + }, + }, + default: '', + description: 'Field ID', + }, +/* -------------------------------------------------------------------------- */ +/* file:download */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'File ID', + name: 'fileId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'download', + ], + resource: [ + 'file', + ], + }, + }, + default: '', + description: 'File ID', + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + required: true, + default: 'data', + displayOptions: { + show: { + operation: [ + 'download' + ], + resource: [ + 'file', + ], + }, + }, + description: 'Name of the binary property to which to
write the data of the read file.', + }, +/* -------------------------------------------------------------------------- */ +/* file:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'File ID', + name: 'fileId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'file', + ], + }, + }, + default: '', + description: 'Field ID', + }, +/* -------------------------------------------------------------------------- */ +/* file:search */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Query', + name: 'query', + type: 'string', + displayOptions: { + show: { + operation: [ + 'search', + ], + resource: [ + 'file', + ], + }, + }, + default: '', + description: `The query text used to search for items. Values may be matched + across several fields including filename, metadata, and file content.`, + }, +/* -------------------------------------------------------------------------- */ +/* file:upload */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + displayOptions: { + show: { + operation: [ + 'upload', + ], + resource: [ + 'file', + ], + }, + }, + default: '', + description: 'The name the file should be saved as.', + }, + { + displayName: 'Parent ID', + name: 'parentId', + required: true, + type: 'string', + displayOptions: { + show: { + operation: [ + 'upload', + ], + resource: [ + 'file', + ], + }, + }, + default: '', + description: 'ID of the parent folder that will contain the file.', + }, + { + displayName: 'Binary Data', + name: 'binaryData', + type: 'boolean', + default: false, + required: true, + displayOptions: { + show: { + operation: [ + 'upload', + ], + resource: [ + 'file', + ], + }, + }, + description: 'If the data to upload should be taken from binary field.', + }, + { + displayName: 'File Content', + name: 'fileContent', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + binaryData: [ + false, + ], + operation: [ + 'upload', + ], + resource: [ + 'file', + ], + }, + + }, + placeholder: '', + description: 'The text content of the file.', + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + required: true, + displayOptions: { + show: { + binaryData: [ + true, + ], + operation: [ + 'upload', + ], + resource: [ + 'file', + ], + }, + + }, + placeholder: '', + description: 'Name of the binary property which contains
the data for the file.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Microsoft/OneDrive/FolderDescriptiont.ts b/packages/nodes-base/nodes/Microsoft/OneDrive/FolderDescriptiont.ts new file mode 100644 index 0000000000..83bd871143 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/OneDrive/FolderDescriptiont.ts @@ -0,0 +1,75 @@ +import { INodeProperties } from "n8n-workflow"; + +export const folderOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'folder', + ], + }, + }, + options: [ + { + name: 'Get Children', + value: 'getChildren', + description: 'Get items inside a folder', + }, + { + name: 'Search', + value: 'search', + description: 'Search a folder', + }, + ], + default: 'getChildren', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const folderFields = [ + +/* -------------------------------------------------------------------------- */ +/* folder:getChildren */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Folder ID', + name: 'folderId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'getChildren', + ], + resource: [ + 'folder', + ], + }, + }, + default: '', + description: 'Folder ID', + }, +/* -------------------------------------------------------------------------- */ +/* folder:search */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Query', + name: 'query', + type: 'string', + displayOptions: { + show: { + operation: [ + 'search', + ], + resource: [ + 'folder', + ], + }, + }, + default: '', + description: `The query text used to search for items. Values may be matched + across several fields including filename, metadata, and file content.`, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Microsoft/OneDrive/GenericFunctions.ts b/packages/nodes-base/nodes/Microsoft/OneDrive/GenericFunctions.ts new file mode 100644 index 0000000000..9330437f77 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/OneDrive/GenericFunctions.ts @@ -0,0 +1,83 @@ +import { + OptionsWithUri + } from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject +} from 'n8n-workflow'; + +export async function microsoftApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}, option: IDataObject = { json: true }): Promise { // tslint:disable-line:no-any + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || `https://graph.microsoft.com/v1.0/me${resource}`, + }; + try { + Object.assign(options, option); + if (Object.keys(headers).length !== 0) { + options.headers = Object.assign({}, options.headers, headers); + } + if (Object.keys(qs).length === 0) { + delete options.qs; + } + if (Object.keys(body).length === 0) { + delete options.body; + } + //@ts-ignore + return await this.helpers.requestOAuth.call(this, 'microsoftOneDriveOAuth2Api', options); + } catch (error) { + if (error.response && error.response.body && error.response.body.error && error.response.body.error.message) { + // Try to return the error prettier + throw new Error(`Microsoft OneDrive response [${error.statusCode}]: ${error.response.body.error.message}`); + } + throw error; + } +} + +export async function microsoftApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + let uri: string | undefined; + query['$top'] = 100; + + do { + responseData = await microsoftApiRequest.call(this, method, endpoint, body, query, uri); + uri = responseData['@odata.nextLink']; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData['@odata.nextLink'] !== undefined + ); + + return returnData; +} + +export async function microsoftApiRequestAllItemsSkip(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + query['$top'] = 100; + query['$skip'] = 0; + + do { + responseData = await microsoftApiRequest.call(this, method, endpoint, body, query); + query['$skip'] += query['$top']; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData['value'].length !== 0 + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.ts b/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.ts new file mode 100644 index 0000000000..cd263d854f --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.ts @@ -0,0 +1,215 @@ +import { + IExecuteFunctions, + BINARY_ENCODING, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeTypeDescription, + INodeType, + ILoadOptionsFunctions, + INodePropertyOptions, + IBinaryKeyData, +} from 'n8n-workflow'; + +import { + microsoftApiRequest, + microsoftApiRequestAllItems, +} from './GenericFunctions'; + +import { + fileOperations, + fileFields, +} from './FileDescription'; + +import { + folderOperations, + folderFields +} from './FolderDescriptiont'; + +export class MicrosoftOneDrive implements INodeType { + description: INodeTypeDescription = { + displayName: 'Microsoft OneDrive', + name: 'microsoftOneDrive', + icon: 'file:oneDrive.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Microsoft OneDrive API.', + defaults: { + name: 'Microsoft OneDrive', + color: '#1d4bab', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'microsoftOneDriveOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'File', + value: 'file', + }, + { + name: 'Folder', + value: 'folder', + }, + ], + default: 'file', + description: 'The resource to operate on.', + }, + ...fileOperations, + ...fileFields, + ...folderOperations, + ...folderFields, + ], + }; + + 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 === 'file') { + //https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_copy?view=odsp-graph-online + if (operation === 'copy') { + const fileId = this.getNodeParameter('fileId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const parentReference = this.getNodeParameter('parentReference', i) as IDataObject; + const body: IDataObject = {}; + if (parentReference) { + body.parentReference = { ...parentReference }; + } + if (additionalFields.name) { + body.name = additionalFields.name as string; + } + responseData = await microsoftApiRequest.call(this, 'POST', `/drive/items/${fileId}/copy`, body, {}, undefined, {}, { json: true, resolveWithFullResponse: true }); + responseData = { location : responseData.headers.location }; + } + //https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_delete?view=odsp-graph-online + if (operation === 'delete') { + const fileId = this.getNodeParameter('fileId', i) as string; + responseData = await microsoftApiRequest.call(this, 'DELETE', `/drive/items/${fileId}`); + responseData = { success: true }; + } + //https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_list_children?view=odsp-graph-online + if (operation === 'download') { + const fileId = this.getNodeParameter('fileId', i) as string; + const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i) as string; + responseData = await microsoftApiRequest.call(this, 'GET', `/drive/items/${fileId}`); + + if (responseData.file === undefined) { + throw new Error('The ID you provided does not belong to a file.'); + } + + responseData = await microsoftApiRequest.call(this, 'GET', `/drive/items/${fileId}/content`, {}, {}, undefined, {}, { encoding: null, resolveWithFullResponse: true }); + + const newItem: INodeExecutionData = { + json: items[i].json, + binary: {}, + }; + + let mimeType: string | undefined; + if (responseData.headers['content-type']) { + mimeType = responseData.headers['content-type']; + } + + if (items[i].binary !== undefined) { + // Create a shallow copy of the binary data so that the old + // data references which do not get changed still stay behind + // but the incoming data does not get changed. + Object.assign(newItem.binary, items[i].binary); + } + + items[i] = newItem; + + const data = Buffer.from(responseData.body); + + items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(data as unknown as Buffer, undefined, mimeType); + } + //https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_get?view=odsp-graph-online + if (operation === 'get') { + const fileId = this.getNodeParameter('fileId', i) as string; + responseData = await microsoftApiRequest.call(this, 'GET', `/drive/items/${fileId}`); + } + //https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_search?view=odsp-graph-online + if (operation === 'search') { + const query = this.getNodeParameter('query', i) as string; + responseData = await microsoftApiRequestAllItems.call(this, 'value', 'GET', `/drive/root/search(q='{${query}}')`); + responseData = responseData.filter((item: IDataObject) => item.file); + } + //https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_put_content?view=odsp-graph-online#example-upload-a-new-file + if (operation === 'upload') { + const parentId = this.getNodeParameter('parentId', i) as string; + const binaryData = this.getNodeParameter('binaryData', 0) as boolean; + let fileName = this.getNodeParameter('fileName', 0) as string; + + if (binaryData) { + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0) as string; + + if (items[i].binary === undefined) { + throw new Error('No binary data exists on item!'); + } + //@ts-ignore + if (items[i].binary[binaryPropertyName] === undefined) { + throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`); + } + + const binaryData = (items[i].binary as IBinaryKeyData)[binaryPropertyName]; + + if (fileName !== '') { + fileName = `${fileName}.${binaryData.fileExtension}`; + } + + const body = Buffer.from(binaryData.data, BINARY_ENCODING); + responseData = await microsoftApiRequest.call(this, 'PUT', `/drive/items/${parentId}:/${fileName || binaryData.fileName}:/content`, body , {}, undefined, { 'Content-Type': binaryData.mimeType, 'Content-length': body.length } ); + + } else { + const body = Buffer.from(this.getNodeParameter('fileContent', i) as string, 'utf8'); + if (fileName === '') { + throw new Error('File name must be defined'); + } + responseData = await microsoftApiRequest.call(this, 'PUT', `/drive/items/${parentId}:/${fileName}.txt:/content`, body , {}, undefined, { 'Content-Type': 'text/plain' } ); + } + } + } + if (resource === 'folder') { + //https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_list_children?view=odsp-graph-online + if (operation === 'getChildren') { + const folderId = this.getNodeParameter('folderId', i) as string; + responseData = await microsoftApiRequestAllItems.call(this, 'value', 'GET', `/drive/items/${folderId}/children`); + } + //https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_search?view=odsp-graph-online + if (operation === 'search') { + const query = this.getNodeParameter('query', i) as string; + responseData = await microsoftApiRequestAllItems.call(this, 'value', 'GET', `/drive/root/search(q='{${query}}')`); + responseData = responseData.filter((item: IDataObject) => item.folder); + } + } + } + if (resource === 'file' && operation === 'download') { + // For file downloads the files get attached to the existing items + return this.prepareOutputData(items); + } else { + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + return [this.helpers.returnJsonArray(returnData)]; + } + } +} diff --git a/packages/nodes-base/nodes/Microsoft/OneDrive/oneDrive.png b/packages/nodes-base/nodes/Microsoft/OneDrive/oneDrive.png new file mode 100644 index 0000000000000000000000000000000000000000..b33d98a8bb25e5b516df50c7769df1e617a38509 GIT binary patch literal 4123 zcma)9XIN8PvrdfkUZe*?RcdIFUJN~i-U5OY0S&$PE*L=slqS6i7z7lhiF86ox)C&HyrI4p6*(I@005xU(N;IUh=!L=N__E5 zqT8>!2tZ$BEjXZhm}BE&6YGT1an{oV2wh-O01=QL0J@}H7y!ryAo?2v000?eEB~?$3qk+Emt~!^+{lYS=A~`v3jk0s zTsjbtoy&5eW{EaM`JwcX@^?Mm#q1qD9h}61+`TTV0E$8K7tr0w&mI!w?&jeuAEd3AJ%`{c-sadXildbJz z4t@P&7y0q|k%)h80U+QSeMp!Z-efJ?s~c}G7*`z1J^gYFvRj&x<->M% zG&qrn8NofSz^9jwtT`yWP_m@kbd~coapHGiv9ro|*75Q3+&%-er>BJYjGkaX;j?1j z*JOEX_5L*ZSnEAszz-)@-QM`e#>x?1_x(u%N^y}dHFMJ+U#p&<9$UY6WVj>c1~pf7 zh@bxUJjKpN2BUJFRW+*ovFS#2SSI~upk%(B+Ix){&B>ri!4z3EOQ$wKx3Ab_sF%w3 z`wDmqLv*-WlIl`O9c_ut=LGH#o%QrCravSCE=5|b-&l;@8 zo8K}#Vqsq|SyaV>TrnwoL}e9;oV)1_Ws0?XOV`P9c7}(jTpgu*kw2rB44DZ&4`)Jc zI0oy}hSp@*&DY2XiF7o~Dm7WnOyO>5C*#8J%6*mf7>{Gs(Hpm5Ln(Jm7My8RW8Owu zRyvdRh4H|AlI(k!gbli>_!9=6k$kFYy$nVWQ6YkH%3yab6z7(6wa(lX&jXWN)x$OI>38Gl*^MQ zG#~WD%AQgKi{{|8bv4){q9ERSWNw7nemWtrq$8XM!=$+OlB4Y^eRBoxAmK)OlR?ki z$mgguEe>%za)w*tQSoG4tSF%ZPKwQNW-2}b^4v2N!Pjhumk zfU;a(b&N5bYz0`*WzzY*aCOj)23rJ$`@w=_DHsN!@q{lcH|(4P$@rI|UYt)SboWr& zhUjPAIJisY?}JF zm+Qun-xxT}=y~JFY^Eq_suSzF(-h8IjrUzki@t@|>GvPbDR9?Kcj6UDyc+KQ%s2I+ z^xfyK{(Z-fAr$v5rw89E`oaA@G_eYGqFb}gsDC?fk2&dIoPnyCP9P9%3tDch&G}S9!C*GcjhrL8Rl|<8EqHG4gu8KGo^_>WCSx zc>iPD8HFY;7N}boNaFjad^YxH{Yis$ywqkzx!&vyDFQZp;Fk#J7NpN7@(q{9$qcad zRW)&87WPkt^U4!(oni!dwi3IpqF107@y8MY z%})88*_kxV50-%wq3e+M0+H2kCKtpVJ%Lq9s~(%GdWf1tkc_Z)oY9wIL>6n*r*@UR zk1Ar7^`fYojxR0I)ojc&rnZ$L-?k&1?04$p2nqB24EHKc2?lmn{-5pSLvp1p1egR0 zLziNsZKd*ec$KUQXkGU4o3$16sJXEM zdd2!17DFU82j)VjsS>2@Vb2Y8tXa4b&1Ac7tSVnABAon4jl)ta)52st79gdOf=s{Z zwLPwwz`h9|ipfS|oB#3+rMsS~LHuo+ky&J$=;LIaSTvHI`?%cma-9?<# zXY4O|p8$6El||Fc#C@yE9`KHtX>#~A=pp|Oo^Eqs<{qt8Y?*bg#g?DQFCli!(lG%J z3-+Y#zNLI%SI&nhFMq_eO>(%B{?Y;(Yi~7tUqh~h`Wb~22Y+}#&-&-~Eu;-u!$#I* zQwK&m*qsvgq--(3tz-?jJw!CvvMEnGVFfI>HY{Yu)+_EVXb;MwGBwQ$Cn zcDtiRvymmzFb$Ev+@%rJmGXS7^jNT8^c$^PjKRcx&jnHIlCvGQ>pyQg>|tPe^DR>0 z_oNcIK%XgmgP!!= zwqoDl5XPo5&b)Z03G31LY76Zgczf4_nwWCG*#=1b>9)0j%E5}dW;18>n-erkne@7V zb`Z5&`-120I6vid3IK2=f_LEy{Ii3fb5ZEar2q=2v&G?=VBBzRZC}q{OAq;=8aTtc zA$yTw%RJo{L8a3whoV5sT_&YFN!X3?ATpBTD#+&2wY?)nzg~Lqm+>9ftNAyXyoaRU zil^_D>y?VVqflbsE}|MiMl}T3@j5Rn#*#Y*WXHROC{{_oSj6S_KoJ3iwigNkRNS=p ztFDHKR?<|Nc%)|Dk(m*7$g=H1yuHSUHLAVEk&=+WznU`Ih1y)g-RXX}J>%U)L4oi- zC3Y*b-*^m#%Uhu5+4Qa|3|o#(-PY-nOu;k~zdl|?-Z9g~tl zGA8MCDYPGgW~N`Kd(V1#b_tB&M~z_pt}ni)U?}LxyqIpzjd%uSA;)b}%ufV4GOwVx zuod(#vgDdmi+(5*Jt?L#?Xe6KZ@xES|MY{_#;-JkI?C6iByXn7k1e(=$MJ@uK~G+E zYp6%<4h6qOt#k6ln@%tP@?)kIo=9o;y3cmt6)YVQ_yi-Tq;}SxleQVMe(a9juD{)$ zIj#^gv?y)VN#bpXN@hGKnc%*`_~RiFlT1cb@|P31az_T%AN1;J^f@G?j$TtH*ZjJ8P`8( zeZpth00i$Ao&BmipmTxRFH*O|BD6qj`pdl94sU#kWS%I5f**PvA{^#vlsBu}cRTW? zixeQB#E-<`c-622UfHldGF{$Bo_jSkJn2Jc*P7m7$IP=H=&5~`9e25%hf?rL^}sj4 zIIb&Jo<@Pg2|ENQ zsA&*c0~20yv>qK2n&(8ablrDLpZY6Hil~mMiqR0+a)^^KOHaefr;=l4i!w`b@3Z%Y z^~F|4>8T+^71`GxFh-PSAjBsw_+Z}a_PCW zRU8Fz=9dbm-sAfST4HYG Date: Thu, 26 Mar 2020 15:44:48 -0400 Subject: [PATCH 032/165] :zap: added lookup and fixed issue with add columns operation --- .../Microsoft/Excel/MicrosoftExcel.node.ts | 72 ++++++++- .../nodes/Microsoft/Excel/TableDescription.ts | 138 ++++++++++++++++++ 2 files changed, 206 insertions(+), 4 deletions(-) diff --git a/packages/nodes-base/nodes/Microsoft/Excel/MicrosoftExcel.node.ts b/packages/nodes-base/nodes/Microsoft/Excel/MicrosoftExcel.node.ts index d7c372c2db..4edab92d35 100644 --- a/packages/nodes-base/nodes/Microsoft/Excel/MicrosoftExcel.node.ts +++ b/packages/nodes-base/nodes/Microsoft/Excel/MicrosoftExcel.node.ts @@ -173,10 +173,35 @@ export class MicrosoftExcel implements INodeType { if (additionalFields.index) { body.index = additionalFields.index as number; } - const values: any[][] = []; + + // Get table columns to eliminate any columns not needed on the input + responseData = await microsoftApiRequest.call(this, 'GET', `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, {}, qs); + const columns = responseData.value.map((column: IDataObject) => (column.name)); + + const cleanedItems: IDataObject[] = []; + + // Delete columns the excel table does not have for (const item of items) { - values.push(Object.values(item.json)); + for (const key of Object.keys(item.json)) { + if (!columns.includes(key)) { + const property = { ...item.json }; + delete property[key]; + cleanedItems.push(property); + } + } } + + // Map the keys to the column index + const values: any[][] = []; + let value = []; + for (const item of cleanedItems) { + for (const column of columns) { + value.push(item[column]); + } + values.push(value); + value = []; + } + body.values = values; const { id } = await microsoftApiRequest.call(this, 'POST', `/drive/items/${workbookId}/workbook/createSession`, { persistChanges: true }); responseData = await microsoftApiRequest.call(this, 'POST', `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows/add`, body, {}, '', { 'workbook-session-id': id }); @@ -204,8 +229,7 @@ export class MicrosoftExcel implements INodeType { responseData = responseData.value; } if (!rawData) { - //@ts-ignore - responseData = responseData.map(column => ({ name: column.name })); + responseData = responseData.map((column: IDataObject) => ({ name: column.name })); } else { const dataProperty = this.getNodeParameter('dataProperty', i) as string; responseData = { [dataProperty] : responseData }; @@ -251,6 +275,46 @@ export class MicrosoftExcel implements INodeType { } } } + if (operation === 'lookup') { + for (let i = 0; i < length; i++) { + const workbookId = this.getNodeParameter('workbook', 0) as string; + const worksheetId = this.getNodeParameter('worksheet', 0) as string; + const tableId = this.getNodeParameter('table', 0) as string; + const lookupColumn = this.getNodeParameter('lookupColumn', 0) as string; + const lookupValue = this.getNodeParameter('lookupValue', 0) as string; + const options = this.getNodeParameter('options', 0) as IDataObject; + + responseData = await microsoftApiRequestAllItemsSkip.call(this, 'value', 'GET', `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`, {}, qs); + + qs['$select'] = 'name'; + let columns = await microsoftApiRequestAllItemsSkip.call(this, 'value', 'GET', `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, {}, qs); + columns = columns.map((column: IDataObject) => column.name); + for (let i = 0; i < responseData.length; i++) { + for (let y = 0; y < columns.length; y++) { + object[columns[y]] = responseData[i].values[0][y]; + } + result.push({ ...object }); + } + responseData = result; + + if (!columns.includes(lookupColumn)) { + throw new Error(`Column ${lookupColumn} does not exist on the table selected`); + } + + if (options.returnAllMatches) { + + responseData = responseData.filter((data: IDataObject) => { + return (data[lookupColumn]?.toString() === lookupValue ); + }); + + } else { + + responseData = responseData.find((data: IDataObject) => { + return (data[lookupColumn]?.toString() === lookupValue ); + }); + } + } + } } if (resource === 'workbook') { for (let i = 0; i < length; i++) { diff --git a/packages/nodes-base/nodes/Microsoft/Excel/TableDescription.ts b/packages/nodes-base/nodes/Microsoft/Excel/TableDescription.ts index 8c491a16e1..ea8b78b9d6 100644 --- a/packages/nodes-base/nodes/Microsoft/Excel/TableDescription.ts +++ b/packages/nodes-base/nodes/Microsoft/Excel/TableDescription.ts @@ -28,6 +28,11 @@ export const tableOperations = [ value: 'getRows', description: 'Retrieve a list of tablerows', }, + { + name: 'Lookup', + value: 'lookup', + description: 'Looks for a specific column value and then returns the matching row' + }, ], default: 'addRow', description: 'The operation to perform.', @@ -484,4 +489,137 @@ export const tableFields = [ }, ] }, +/* -------------------------------------------------------------------------- */ +/* table:lookup */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Workbook', + name: 'workbook', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getWorkbooks', + }, + displayOptions: { + show: { + operation: [ + 'lookup', + ], + resource: [ + 'table', + ], + }, + }, + default: '', + }, + { + displayName: 'Worksheet', + name: 'worksheet', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getworksheets', + loadOptionsDependsOn: [ + 'workbook', + ], + }, + displayOptions: { + show: { + operation: [ + 'lookup', + ], + resource: [ + 'table', + ], + }, + }, + default: '', + }, + { + displayName: 'Table', + name: 'table', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getTables', + loadOptionsDependsOn: [ + 'worksheet', + ], + }, + displayOptions: { + show: { + operation: [ + 'lookup', + ], + resource: [ + 'table', + ], + }, + }, + default: '', + }, + { + displayName: 'Lookup Column', + name: 'lookupColumn', + type: 'string', + default: '', + placeholder: 'Email', + required: true, + displayOptions: { + show: { + resource: [ + 'table', + ], + operation: [ + 'lookup' + ], + }, + }, + description: 'The name of the column in which to look for value.', + }, + { + displayName: 'Lookup Value', + name: 'lookupValue', + type: 'string', + default: '', + placeholder: 'frank@example.com', + required: true, + displayOptions: { + show: { + resource: [ + 'table', + ], + operation: [ + 'lookup' + ], + }, + }, + description: 'The value to look for in column.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'table', + ], + operation: [ + 'lookup', + ], + }, + }, + options: [ + { + displayName: 'Return All Matches', + name: 'returnAllMatches', + type: 'boolean', + default: false, + description: 'By default only the first result gets returned. If options gets set all found matches get returned.', + }, + ], + } ] as INodeProperties[]; From 4a2828a2f367d24c1c35ac1679b71a4b3cff67af Mon Sep 17 00:00:00 2001 From: Subhash Chandra Date: Fri, 27 Mar 2020 17:32:13 +0530 Subject: [PATCH 033/165] Fix oauth2 connect UI --- packages/editor-ui/src/components/CredentialsInput.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/editor-ui/src/components/CredentialsInput.vue b/packages/editor-ui/src/components/CredentialsInput.vue index 8a846b4055..5b5e5492ff 100644 --- a/packages/editor-ui/src/components/CredentialsInput.vue +++ b/packages/editor-ui/src/components/CredentialsInput.vue @@ -194,6 +194,9 @@ export default mixins( return this.credentialDataTemp; }, isOAuthType (): boolean { + if (this.credentialTypeData.name === 'oAuth2Api') { + return true; + } const types = this.parentTypes(this.credentialTypeData.name); return types.includes('oAuth2Api'); }, From bba6a8494d5b4a616d1171bb1d3c7430b6d70460 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sat, 28 Mar 2020 19:08:39 +0100 Subject: [PATCH 034/165] :zap: Fixed some issues with Excel-Node --- .../credentials/TestOAuth2Api.credentials.ts | 26 ++ .../nodes/Google/GoogleDriveTrigger.node.ts | 293 ++++++++++++++++++ .../Microsoft/Excel/MicrosoftExcel.node.ts | 141 +++++---- .../nodes/Microsoft/Excel/TableDescription.ts | 2 +- .../Microsoft/Excel/WorkbookDescription.ts | 2 +- .../Microsoft/Excel/WorksheetDescription.ts | 2 +- .../nodes/Microsoft/Excel/excel.png | Bin 5984 -> 1964 bytes packages/nodes-base/package.json | 26 +- 8 files changed, 412 insertions(+), 80 deletions(-) create mode 100644 packages/nodes-base/credentials/TestOAuth2Api.credentials.ts create mode 100644 packages/nodes-base/nodes/Google/GoogleDriveTrigger.node.ts diff --git a/packages/nodes-base/credentials/TestOAuth2Api.credentials.ts b/packages/nodes-base/credentials/TestOAuth2Api.credentials.ts new file mode 100644 index 0000000000..2a350faecf --- /dev/null +++ b/packages/nodes-base/credentials/TestOAuth2Api.credentials.ts @@ -0,0 +1,26 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +const scopes = [ + 'https://www.googleapis.com/auth/calendar', + 'https://www.googleapis.com/auth/calendar.events', +]; + +export class TestOAuth2Api implements ICredentialType { + name = 'testOAuth2Api'; + extends = [ + 'googleOAuth2Api', + ]; + displayName = 'Test OAuth2 API'; + properties = [ + { + displayName: 'Scope', + name: 'scope', + type: 'string' as NodePropertyTypes, + default: '', + placeholder: 'asdf', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Google/GoogleDriveTrigger.node.ts b/packages/nodes-base/nodes/Google/GoogleDriveTrigger.node.ts new file mode 100644 index 0000000000..e347222e7e --- /dev/null +++ b/packages/nodes-base/nodes/Google/GoogleDriveTrigger.node.ts @@ -0,0 +1,293 @@ +// import { google } from 'googleapis'; + +// import { +// IHookFunctions, +// IWebhookFunctions, +// } from 'n8n-core'; + +// import { +// IDataObject, +// INodeTypeDescription, +// INodeType, +// IWebhookResponseData, +// } from 'n8n-workflow'; + +// import { getAuthenticationClient } from './GoogleApi'; + + +// export class GoogleDriveTrigger implements INodeType { +// description: INodeTypeDescription = { +// displayName: 'Google Drive Trigger', +// name: 'googleDriveTrigger', +// icon: 'file:googleDrive.png', +// group: ['trigger'], +// version: 1, +// subtitle: '={{$parameter["owner"] + "/" + $parameter["repository"] + ": " + $parameter["events"].join(", ")}}', +// description: 'Starts the workflow when a file on Google Drive got changed.', +// defaults: { +// name: 'Google Drive Trigger', +// color: '#3f87f2', +// }, +// inputs: [], +// outputs: ['main'], +// credentials: [ +// { +// name: 'googleApi', +// required: true, +// } +// ], +// webhooks: [ +// { +// name: 'default', +// httpMethod: 'POST', +// responseMode: 'onReceived', +// path: 'webhook', +// }, +// ], +// properties: [ +// { +// displayName: 'Resource Id', +// name: 'resourceId', +// type: 'string', +// default: '', +// required: true, +// placeholder: '', +// description: 'ID of the resource to watch, for example a file ID.', +// }, +// ], +// }; + +// // @ts-ignore (because of request) +// webhookMethods = { +// default: { +// async checkExists(this: IHookFunctions): Promise { +// // const webhookData = this.getWorkflowStaticData('node'); + +// // if (webhookData.webhookId === undefined) { +// // // No webhook id is set so no webhook can exist +// // return false; +// // } + +// // // Webhook got created before so check if it still exists +// // const owner = this.getNodeParameter('owner') as string; +// // const repository = this.getNodeParameter('repository') as string; +// // const endpoint = `/repos/${owner}/${repository}/hooks/${webhookData.webhookId}`; + +// // try { +// // await githubApiRequest.call(this, 'GET', endpoint, {}); +// // } catch (e) { +// // if (e.message.includes('[404]:')) { +// // // Webhook does not exist +// // delete webhookData.webhookId; +// // delete webhookData.webhookEvents; + +// // return false; +// // } + +// // // Some error occured +// // throw e; +// // } + +// // If it did not error then the webhook exists +// // return true; +// return false; +// }, +// async create(this: IHookFunctions): Promise { +// const webhookUrl = this.getNodeWebhookUrl('default'); + +// const resourceId = this.getNodeParameter('resourceId') as string; + +// const credentials = this.getCredentials('googleApi'); + +// if (credentials === undefined) { +// throw new Error('No credentials got returned!'); +// } + +// const scopes = [ +// 'https://www.googleapis.com/auth/drive', +// 'https://www.googleapis.com/auth/drive.appdata', +// 'https://www.googleapis.com/auth/drive.photos.readonly', +// ]; + +// const client = await getAuthenticationClient(credentials.email as string, credentials.privateKey as string, scopes); + +// const drive = google.drive({ +// version: 'v3', +// auth: client, +// }); + + +// const accessToken = await client.getAccessToken(); +// console.log('accessToken: '); +// console.log(accessToken); + +// const asdf = await drive.changes.getStartPageToken(); +// // console.log('asdf: '); +// // console.log(asdf); + + + + +// const response = await drive.changes.watch({ +// // +// pageToken: asdf.data.startPageToken, +// requestBody: { +// id: 'asdf-test-2', +// address: webhookUrl, +// resourceId, +// type: 'web_hook', +// // page_token: '', +// } +// }); + +// console.log('...response...CREATE'); +// console.log(JSON.stringify(response, null, 2)); + + + + + +// // const endpoint = `/repos/${owner}/${repository}/hooks`; + +// // const body = { +// // name: 'web', +// // config: { +// // url: webhookUrl, +// // content_type: 'json', +// // // secret: '...later...', +// // insecure_ssl: '1', // '0' -> not allow inscure ssl | '1' -> allow insercure SSL +// // }, +// // events, +// // active: true, +// // }; + + +// // let responseData; +// // try { +// // responseData = await githubApiRequest.call(this, 'POST', endpoint, body); +// // } catch (e) { +// // if (e.message.includes('[422]:')) { +// // throw new Error('A webhook with the identical URL exists already. Please delete it manually on Github!'); +// // } + +// // throw e; +// // } + +// // if (responseData.id === undefined || responseData.active !== true) { +// // // Required data is missing so was not successful +// // throw new Error('Github webhook creation response did not contain the expected data.'); +// // } + +// // const webhookData = this.getWorkflowStaticData('node'); +// // webhookData.webhookId = responseData.id as string; +// // webhookData.webhookEvents = responseData.events as string[]; + +// return true; +// }, +// async delete(this: IHookFunctions): Promise { +// const webhookUrl = this.getNodeWebhookUrl('default'); + +// const resourceId = this.getNodeParameter('resourceId') as string; + +// const credentials = this.getCredentials('googleApi'); + +// if (credentials === undefined) { +// throw new Error('No credentials got returned!'); +// } + +// const scopes = [ +// 'https://www.googleapis.com/auth/drive', +// 'https://www.googleapis.com/auth/drive.appdata', +// 'https://www.googleapis.com/auth/drive.photos.readonly', +// ]; + +// const client = await getAuthenticationClient(credentials.email as string, credentials.privateKey as string, scopes); + +// const drive = google.drive({ +// version: 'v3', +// auth: client, +// }); + +// // Remove channel +// const response = await drive.channels.stop({ +// requestBody: { +// id: 'asdf-test-2', +// address: webhookUrl, +// resourceId, +// type: 'web_hook', +// } +// }); + + +// console.log('...response...DELETE'); +// console.log(JSON.stringify(response, null, 2)); + + + +// // const webhookData = this.getWorkflowStaticData('node'); + +// // if (webhookData.webhookId !== undefined) { +// // const owner = this.getNodeParameter('owner') as string; +// // const repository = this.getNodeParameter('repository') as string; +// // const endpoint = `/repos/${owner}/${repository}/hooks/${webhookData.webhookId}`; +// // const body = {}; + +// // try { +// // await githubApiRequest.call(this, 'DELETE', endpoint, body); +// // } catch (e) { +// // return false; +// // } + +// // // Remove from the static workflow data so that it is clear +// // // that no webhooks are registred anymore +// // delete webhookData.webhookId; +// // delete webhookData.webhookEvents; +// // } + +// return true; +// }, +// }, +// }; + + + +// async webhook(this: IWebhookFunctions): Promise { +// const bodyData = this.getBodyData(); + +// console.log(''); +// console.log(''); +// console.log('GOT WEBHOOK CALL'); +// console.log(JSON.stringify(bodyData, null, 2)); + + + +// // Check if the webhook is only the ping from Github to confirm if it workshook_id +// if (bodyData.hook_id !== undefined && bodyData.action === undefined) { +// // Is only the ping and not an actual webhook call. So return 'OK' +// // but do not start the workflow. + +// return { +// webhookResponse: 'OK' +// }; +// } + +// // Is a regular webhoook call + +// // TODO: Add headers & requestPath +// const returnData: IDataObject[] = []; + +// returnData.push( +// { +// body: bodyData, +// headers: this.getHeaderData(), +// query: this.getQueryData(), +// } +// ); + +// return { +// workflowData: [ +// this.helpers.returnJsonArray(returnData) +// ], +// }; +// } +// } diff --git a/packages/nodes-base/nodes/Microsoft/Excel/MicrosoftExcel.node.ts b/packages/nodes-base/nodes/Microsoft/Excel/MicrosoftExcel.node.ts index 4edab92d35..26e58ef9ad 100644 --- a/packages/nodes-base/nodes/Microsoft/Excel/MicrosoftExcel.node.ts +++ b/packages/nodes-base/nodes/Microsoft/Excel/MicrosoftExcel.node.ts @@ -153,23 +153,23 @@ export class MicrosoftExcel implements INodeType { const items = this.getInputData(); const returnData: IDataObject[] = []; const length = items.length as unknown as number; - const qs: IDataObject = {}; + let qs: IDataObject = {}; const result: IDataObject[] = []; - const object: IDataObject = {}; let responseData; const resource = this.getNodeParameter('resource', 0) as string; const operation = this.getNodeParameter('operation', 0) as string; + if (resource === 'table') { //https://docs.microsoft.com/en-us/graph/api/table-post-rows?view=graph-rest-1.0&tabs=http if (operation === 'addRow') { + // TODO: At some point it should be possible to use item dependent parameters. + // Is however important to then not make one separate request each. const workbookId = this.getNodeParameter('workbook', 0) as string; const worksheetId = this.getNodeParameter('worksheet', 0) as string; const tableId = this.getNodeParameter('table', 0) as string; const additionalFields = this.getNodeParameter('additionalFields', 0) as IDataObject; const body: IDataObject = {}; - if (Object.keys(items[0].json).length === 0) { - throw new Error('Input cannot be empty'); - } + if (additionalFields.index) { body.index = additionalFields.index as number; } @@ -178,41 +178,35 @@ export class MicrosoftExcel implements INodeType { responseData = await microsoftApiRequest.call(this, 'GET', `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, {}, qs); const columns = responseData.value.map((column: IDataObject) => (column.name)); - const cleanedItems: IDataObject[] = []; + const rows: any[][] = []; // tslint:disable-line:no-any - // Delete columns the excel table does not have + // Bring the items into the correct format for (const item of items) { - for (const key of Object.keys(item.json)) { - if (!columns.includes(key)) { - const property = { ...item.json }; - delete property[key]; - cleanedItems.push(property); - } - } - } - - // Map the keys to the column index - const values: any[][] = []; - let value = []; - for (const item of cleanedItems) { + const row = []; for (const column of columns) { - value.push(item[column]); + row.push(item.json[column]); } - values.push(value); - value = []; + rows.push(row); } - body.values = values; + body.values = rows; const { id } = await microsoftApiRequest.call(this, 'POST', `/drive/items/${workbookId}/workbook/createSession`, { persistChanges: true }); responseData = await microsoftApiRequest.call(this, 'POST', `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows/add`, body, {}, '', { 'workbook-session-id': id }); await microsoftApiRequest.call(this, 'POST', `/drive/items/${workbookId}/workbook/closeSession`, {}, {}, '', { 'workbook-session-id': id }); + + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } } //https://docs.microsoft.com/en-us/graph/api/table-list-columns?view=graph-rest-1.0&tabs=http if (operation === 'getColumns') { for (let i = 0; i < length; i++) { - const workbookId = this.getNodeParameter('workbook', 0) as string; - const worksheetId = this.getNodeParameter('worksheet', 0) as string; - const tableId = this.getNodeParameter('table', 0) as string; + qs = {}; + const workbookId = this.getNodeParameter('workbook', i) as string; + const worksheetId = this.getNodeParameter('worksheet', i) as string; + const tableId = this.getNodeParameter('table', i) as string; const returnAll = this.getNodeParameter('returnAll', i) as boolean; const rawData = this.getNodeParameter('rawData', i) as boolean; if (rawData) { @@ -234,14 +228,21 @@ export class MicrosoftExcel implements INodeType { const dataProperty = this.getNodeParameter('dataProperty', i) as string; responseData = { [dataProperty] : responseData }; } + + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } } } //https://docs.microsoft.com/en-us/graph/api/table-list-rows?view=graph-rest-1.0&tabs=http if (operation === 'getRows') { for (let i = 0; i < length; i++) { - const workbookId = this.getNodeParameter('workbook', 0) as string; - const worksheetId = this.getNodeParameter('worksheet', 0) as string; - const tableId = this.getNodeParameter('table', 0) as string; + qs = {}; + const workbookId = this.getNodeParameter('workbook', i) as string; + const worksheetId = this.getNodeParameter('worksheet', i) as string; + const tableId = this.getNodeParameter('table', i) as string; const returnAll = this.getNodeParameter('returnAll', i) as boolean; const rawData = this.getNodeParameter('rawData', i) as boolean; if (rawData) { @@ -253,71 +254,78 @@ export class MicrosoftExcel implements INodeType { if (returnAll === true) { responseData = await microsoftApiRequestAllItemsSkip.call(this, 'value', 'GET', `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`, {}, qs); } else { - qs['$top'] = this.getNodeParameter('limit', i) as number; - responseData = await microsoftApiRequest.call(this, 'GET', `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`, {}, qs); + const rowsQs = { ...qs }; + rowsQs['$top'] = this.getNodeParameter('limit', i) as number; + responseData = await microsoftApiRequest.call(this, 'GET', `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`, {}, rowsQs); responseData = responseData.value; } if (!rawData) { - qs['$select'] = 'name'; - let columns = await microsoftApiRequestAllItemsSkip.call(this, 'value', 'GET', `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, {}, qs); + const columnsQs = { ...qs }; + columnsQs['$select'] = 'name'; + // TODO: That should probably be cached in the future + let columns = await microsoftApiRequestAllItemsSkip.call(this, 'value', 'GET', `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, {}, columnsQs); //@ts-ignore columns = columns.map(column => column.name); for (let i = 0; i < responseData.length; i++) { + const object: IDataObject = {}; for (let y = 0; y < columns.length; y++) { object[columns[y]] = responseData[i].values[0][y]; } - result.push({ ...object }); + returnData.push({ ...object }); } - responseData = result; } else { const dataProperty = this.getNodeParameter('dataProperty', i) as string; - responseData = { [dataProperty] : responseData }; + returnData.push({ [dataProperty]: responseData }); } } } if (operation === 'lookup') { for (let i = 0; i < length; i++) { - const workbookId = this.getNodeParameter('workbook', 0) as string; - const worksheetId = this.getNodeParameter('worksheet', 0) as string; - const tableId = this.getNodeParameter('table', 0) as string; - const lookupColumn = this.getNodeParameter('lookupColumn', 0) as string; - const lookupValue = this.getNodeParameter('lookupValue', 0) as string; - const options = this.getNodeParameter('options', 0) as IDataObject; + qs = {}; + const workbookId = this.getNodeParameter('workbook', i) as string; + const worksheetId = this.getNodeParameter('worksheet', i) as string; + const tableId = this.getNodeParameter('table', i) as string; + const lookupColumn = this.getNodeParameter('lookupColumn', i) as string; + const lookupValue = this.getNodeParameter('lookupValue', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; - responseData = await microsoftApiRequestAllItemsSkip.call(this, 'value', 'GET', `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`, {}, qs); + responseData = await microsoftApiRequestAllItemsSkip.call(this, 'value', 'GET', `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`, {}, {}); qs['$select'] = 'name'; + // TODO: That should probably be cached in the future let columns = await microsoftApiRequestAllItemsSkip.call(this, 'value', 'GET', `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, {}, qs); columns = columns.map((column: IDataObject) => column.name); - for (let i = 0; i < responseData.length; i++) { - for (let y = 0; y < columns.length; y++) { - object[columns[y]] = responseData[i].values[0][y]; - } - result.push({ ...object }); - } - responseData = result; if (!columns.includes(lookupColumn)) { throw new Error(`Column ${lookupColumn} does not exist on the table selected`); } + result.length = 0; + for (let i = 0; i < responseData.length; i++) { + const object: IDataObject = {}; + for (let y = 0; y < columns.length; y++) { + object[columns[y]] = responseData[i].values[0][y]; + } + result.push({ ...object }); + } + if (options.returnAllMatches) { - - responseData = responseData.filter((data: IDataObject) => { + responseData = result.filter((data: IDataObject) => { return (data[lookupColumn]?.toString() === lookupValue ); }); - + returnData.push.apply(returnData, responseData as IDataObject[]); } else { - - responseData = responseData.find((data: IDataObject) => { + responseData = result.find((data: IDataObject) => { return (data[lookupColumn]?.toString() === lookupValue ); }); + returnData.push(responseData as IDataObject); } } } } if (resource === 'workbook') { for (let i = 0; i < length; i++) { + qs = {}; //https://docs.microsoft.com/en-us/graph/api/worksheetcollection-add?view=graph-rest-1.0&tabs=http if (operation === 'addWorksheet') { const workbookId = this.getNodeParameter('workbook', i) as string; @@ -344,10 +352,17 @@ export class MicrosoftExcel implements INodeType { responseData = responseData.value; } } + + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } } } if (resource === 'worksheet') { for (let i = 0; i < length; i++) { + qs = {}; //https://docs.microsoft.com/en-us/graph/api/workbook-list-worksheets?view=graph-rest-1.0&tabs=http if (operation === 'getAll') { const returnAll = this.getNodeParameter('returnAll', i) as boolean; @@ -376,7 +391,9 @@ export class MicrosoftExcel implements INodeType { qs['$select'] = filters.fields; } } + responseData = await microsoftApiRequest.call(this, 'GET', `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`, {}, qs); + if (!rawData) { const keyRow = this.getNodeParameter('keyRow', i) as number; const dataStartRow = this.getNodeParameter('dataStartRow', i) as number; @@ -385,24 +402,20 @@ export class MicrosoftExcel implements INodeType { } const keyValues = responseData.values[keyRow]; for (let i = dataStartRow; i < responseData.values.length; i++) { + const object: IDataObject = {}; for (let y = 0; y < keyValues.length; y++) { object[keyValues[y]] = responseData.values[i][y]; } - result.push({ ...object }); + returnData.push({ ...object }); } - responseData = result; } else { const dataProperty = this.getNodeParameter('dataProperty', i) as string; - responseData = { [dataProperty] : responseData }; + returnData.push({ [dataProperty]: responseData }); } } } } - if (Array.isArray(responseData)) { - returnData.push.apply(returnData, responseData as IDataObject[]); - } else if (responseData !== undefined) { - returnData.push(responseData as IDataObject); - } + return [this.helpers.returnJsonArray(returnData)]; } } diff --git a/packages/nodes-base/nodes/Microsoft/Excel/TableDescription.ts b/packages/nodes-base/nodes/Microsoft/Excel/TableDescription.ts index ea8b78b9d6..8c399330d5 100644 --- a/packages/nodes-base/nodes/Microsoft/Excel/TableDescription.ts +++ b/packages/nodes-base/nodes/Microsoft/Excel/TableDescription.ts @@ -1,4 +1,4 @@ -import { INodeProperties } from "n8n-workflow"; +import { INodeProperties } from 'n8n-workflow'; export const tableOperations = [ { diff --git a/packages/nodes-base/nodes/Microsoft/Excel/WorkbookDescription.ts b/packages/nodes-base/nodes/Microsoft/Excel/WorkbookDescription.ts index 526a8ceb43..61f7b6f501 100644 --- a/packages/nodes-base/nodes/Microsoft/Excel/WorkbookDescription.ts +++ b/packages/nodes-base/nodes/Microsoft/Excel/WorkbookDescription.ts @@ -1,4 +1,4 @@ -import { INodeProperties } from "n8n-workflow"; +import { INodeProperties } from 'n8n-workflow'; export const workbookOperations = [ { diff --git a/packages/nodes-base/nodes/Microsoft/Excel/WorksheetDescription.ts b/packages/nodes-base/nodes/Microsoft/Excel/WorksheetDescription.ts index f50fbb3ece..e5811e6c68 100644 --- a/packages/nodes-base/nodes/Microsoft/Excel/WorksheetDescription.ts +++ b/packages/nodes-base/nodes/Microsoft/Excel/WorksheetDescription.ts @@ -1,4 +1,4 @@ -import { INodeProperties } from "n8n-workflow"; +import { INodeProperties } from 'n8n-workflow'; export const worksheetOperations = [ { diff --git a/packages/nodes-base/nodes/Microsoft/Excel/excel.png b/packages/nodes-base/nodes/Microsoft/Excel/excel.png index 40631413a5f656ad5819f9faada8eecc78509bd4..ba2b10c06305af8b06f424a16a69ed42fa1a584f 100644 GIT binary patch delta 1961 zcmV;a2UhstF02ob8Gi!+006nq0-pc?0n1QKR7C&)03dQjFKs^`az!9-RPprjAaX?@ zazy}ZH2?qqV2g0g*3bZ93m|et0BSTMeM&odQaXB6A8|$>aYZ3;L?CiSJ9<MIUoS5pFjhazy}ZH6Laz!6=Mt>r7N5Rd-AaO(>azr0; zMj&xTAaO-%jdlQI8USlG0BSS|Tmbs}`yz5i8*xJ*bVdMeJN5SUBy&dqWdQs9`}g?wGIvh-`uV)cz$SA@0BSRa zqmMy+ST%T1Fn@MUB6>;yXDjFI>BZ2<0An2hSOC!2(yYC)K6_R?dQ>fSOS#9su)wt< zcu6UANpzNfaFl#Fc~Ssl7wYco*528`%)~@}S^#Dx0A2ye)68p;cvpsKPl94ee_a4< zHvnld0AdsJ_4DoU?y0)2UW#oebVvYYBLG$a%GAxY!+*J3h-)K!OXKL|c$tG2a6tfJ z4FF&U08#+b+SH!1q<@`>e4B<-gk%wLKmcYZ07wAj>E)!hs%DOKWQ}qPZ$1Ds0N&%@ zkg1r7q>+T7jQ~sl06YKyIsi6*R4s#2cDML-0000rbW%=J0Q&{h1N{B@&-^L;`SZ>R zg@Hml9)Hk=X!_r&n=u*?=+LxVPAMY!$G?kzb|CZ8(Yun0ficYhgmitF%Bz__Zq6xWeMCoUY=-D|a` zw7WFAZU(FMipEnXFCIL5RG*skr>un5+WWUcC`$Qtc*ov@=k^;+DSygj)fx;2y(B!X zF+%A}Sg}>>4(~j1a|*fR!hj}t-VJye?r}O>cmZHu>6yLlcdt>3B8_Z zV}I?vb!k%vm)5BpdnWypDXSh{5L|zN3T@f9MPVO&r0>f;hg6L{re1InDllw%_S5Hk zOZoh1?#BVuR_!){O_jU0*x1HS*5BT$QKM%i;eF&HNi#A#Nf{@JNOb@9v95(47m_fe z7G@Hy?^@`1;q4m?27`9gUku6=AEVU>p?@o7dTL@cfCj&XXkhef5?MgFp-Umad*kU? zKZ=ArULe-!!xd5#rBvl`INCX^3hx%)1<>YatWd31Up#w+DP$5pAMc>Ra5s@ov#jEC zjLZ*LhWpWTc6bDsC){{K=BZFyC{YafZ1hyVHcl;eIsVEuX19~b0^bs2E=1x8S$|(; zIW-U&BCs1O=={E7{qEV|upI|@8< z%RKskRUwlo2ce2cIcM{%N2Kdbo~H;2hMJ~FC-Q$C8;UPAIWywww)|`41hG%)Gn0#Z zd;<`h(m*aAj}M5LO>yw_Aef@`Q-Am;m@|2AtIdK~4cP+d;g@$}tyZ(ivB8)eSZ^o` zqy38h3_}zNd#P4z=9~5TI>g3T9|r1t5l;v&O+N=$JqTZ!hL0tb*aR$gjw6xNzK8{p zSVGp9pM^(*_YrztR(!;`%51Z;5R8dDB!VOn%9IkV+2$#qboceS$50^ICx4UtfG~wr zV(KF@OCy+1Gr!F?^aTB^46%7%4g>W2B;FVT4(i=2co^NA;)ym)%u~r#AU3s%2dD^c zY++h>%OQB>;&g&(3n?*Fn-(B8FRP7GsU&=Pgezou`XM6Wr7cY%-S*vmlg+-#_M^f` zb_mGM2XSMQSQvs<7A$5|qh&Pkwwh>vOuO8#1T^C!jojQZ$?b8 zUV4VJo4GnBku%?~t#I&BmFlVdolf%py?^S$NxxtHXr?C&*SGK4 z4s4Uz18m#2eftiLDu?!-Ki*?7!GX_2Xgdy%9j8$|bh{Q$%6>SKS_vo@#EKBk%eqd~ zW$oJv<7xRUPk>fB%gciR!fvk!+P4iS;kMzZH6lPRTFgx#WR vU$Yob?Es8jNFr2kN&erfsx{VFL!RV)@{00000NkvXXu0mjf0^f~! literal 5984 zcmZ{Iby(ER*Z$HCDoE`DD@aO*OLwz0A`2q1q|~x3Qu+{rG!oLaNJ~mdNh+NSQj$wZ zNjCy7KHul}e1GpB@60uG?m2Ux`^=d?KG*e`a6KJ03Q|T=002Otp{{IjYxVvNV#3=w z=I%k+t-L0q|}S&TR+aFaikv#sGjO4)9-WfW!3<2NwVcbp+u5 z!!f0gV7#`&)z8uwqe9vbhz_)lD(gueZ)Awj6aJOBWGwm$>MJ74bQtrX1B z$OL7g4V8qux$s-rxLMosd%Gb1L;<9|C2ygNEy@b)?c(g}A?Yo{`Zq)J7XNVzu!8@l zpqyk_O|N ztbY>y+x~M-l%w5$Gr4;FQ`T*P0)KV{g!lyo{?)z>mHy+E)I~bl-e&%zFDoSdH}ik_ z{>dXP@F)5IcbNZF`nUJCR9RAKfq$<}mK55-2?GFVRy33qjJ$DvJtILqS7Fp~i^+^J`j%x_ ziq{Ed+|YvrsYC^FRlSw(NIT{`ojbp3x!mdR@N1TSQhq!f(-Mz7 zMw{Be^ca&F_}Z)-TJzR#$Gl0Cphocl%MrvG$KubJ z#-^=|K@5m~W}^7_l_w7Q7B{pZUm=%%0)C`zz!Kl3S}PVAtXxTDsIh#1V`wN#Sgl8h zIr$@0`^7hW6H|v8degWa_jUXTj3eW$qBWt?N^LBIaC?Yd1@pLW)B4Pz2B@vem(%mf zVa~w_KqiEQ&(L4{qwTi7tD>%{ew&+-LnChZtP!``B_QKDFaKP=1UQf}h59Vz0f3-Q z@fzHa093%@;W{WPv#pG)MEhB&HdMC=^>2UmW`$5o=M352&-a2C$1m6TbTFySIMck| zULpwjkiLu}(IyVe6*bI`O4V)aQFYW&V^kjD(Lwj=ETWw3Iy2@5&F^ z1ETPuq+negw zolVN{T(BTVldo=CzsFco_tVl)Gcji!s0q;rdzo>%U~7o4AZU{_A}gjn@ak*G0%r)< zr0iI%bEw&6FTsMBtvBW&z3{kF)DJ5;#1$CJm^!n8(6rZ0w)gpQ(N~8T`d3t4d9F~N zCJQAyIhc`Rj~hE2EmU*?ViCxIA{B64$x!{lN3VSLx-O&01sVh-#>Xb9&-&qB-jH*t z?qXVCdW?Iu_3?Yb003Ft;dsJvY>}+?#^JF9u_gG<9rMrRH1BHG_dFlsmjRukoI^jp zU!_1)DtU#m{3;?%TcaF?dO|W_9OR1#Mi~ZM8gS)zN~o-ejG^Zel462hbX!te?7kMr z8ulX|#~VOPcCZXVO#mDM4v|d!8{|j5Gi#pl38{PvZyu8w5;+(?dci)I&haZ$GH!uEa8w;AV2(s^_HE)`M z;emhZUj4+S`+na%M|nE8DQ|9pJtT{FU092*k$WOR)7Et#vf_Y-Sm(p!2A7h&GPb^L z?H8SAr;N;uz`7G~!JIR?bp;!(*4f3ec-599Ivm9OK$9_k$X?IA!8w<`g`hiQufA^<((kX~@1`4G+ts}wQ{n+dMnqnI=;bVZa60lBgCn?>B znQjA#y}B1bWv`@h=gva7nitQ=SyTVsqrKS?`%fHQNR)j!R}n2_6BG3Evj5>&0`sRX z{z5IyuIH?D_o46S(Nt_zfR%ZI>|G^9&gAR2NO1cWp}q4KJcU$8(AQni20CFP9)$n` zZ#|3mo0!}O8zH&S!<%HWa9Few-Sb9a zkGrH_@i|pJnP_pdL7jA!G;8A~Hh`(D;-1H|YIF4Uk5A0z{^2?H8q4ysR&w>FQ<)FO zs5ZngOH*VkLuSkgqEKlOJ|*|LR^Hdol6Dt( z?`MI&Pqeiw;wGj@wtJp8oMVxaZ1Pj9XNZjYtZ+W&yFKt;>;$}`yX9VM;gl!QoPw)n z3+aibifUjpVi#@@pn+60ni_8Y-OOw1UNOMzS&LPaIE$P8YimrnaHm5ZnB8$~(sIaG z4APHYKbh*)ms@L(mbkYbm$+rDrK6gJ-{DFwvC%vu+1vJJ%FD7<)W1$YoI7(fa25*b zA}TNAFNv+vhY#~Y-co0f5BWGv_E*7ouM9Nh9!Upk*|n9KkZ63vm))M>^x%9h;Q4)w z#`~>RQP|=_@(nF^$MB*W@4=6@@M>^8aXba2U~Yl!>6bIA`Vk@s2a78-R9!V^`|4bk zU@_)P(@HwkAYCyt8|L$vap1%o|%BECCCghOq)H>rKg}ly1*eIt6vSd{bBExy=@_uBoR@ayRf6Wh`eMT&vwaIT$q!F z(&G;B`I3#H4^PJ^3A}dKkOW^JBFR)D^&vU(nKom9^Jw>ChNJv0m(O|TD$;fEC@eAK ziHMS?@9ubieoYT)ZjTs?|8=-@?M@+bXT8LRr9K=)8%>N?CSJBL#_owFgpB&^;3V7D zkFmzo5P`(YHc3;{S!83+Rf%*iBhDk$lAf(-Kox)Aucu8FNz?)T~1Y?^*%ufbfG2L}$~*dbIzqd|fv|zIA!3j>5R}}hH*wt%J4yko%E)u>}2^5W)92eYK99m~lbx28B*UNtIUW}mMTkc)Qq zMekqc6*O!1Y4s2BLb|d<70~W_N#ol|H@;rAzb%i<`$CrRQ@%cQnLP-XzFw-x^Ibu0 zgye)jX!ywMl>}Ulzv}6FcJ;$eNPrSTXa=c+#!VE?&;&@$yzGB+YQZl|&^R9I(8KXk z{^dE;<~-pMFQ9$YJ_-nbm!S3*B~uxntCqn@b>mqq`s`Ic4INGU1wrEar-8oHg<$B3 zx@44sK*wkAujfBhS3Z22znfM zdI~qYAF3g7)gV1{WPj!2zE%Z^f}(WP>2%Wh$VzsSl3%tw`o0n3-VpoCAZo15)9cA| z{^x~e#RL|JtccGHcnK+qEf+yjHqNe}&WO(ZjJUV`b$iJZ2koXs;_)PUrR~$4#hX=H zdEd#aS7qbdK~LFD zurv8AeXATcqEg0y{XKM65%{+uYSFZ`1tp7^YJ}CMKKpCdwqiEN|`%<%owxszI%UP|K=# zDwDwyimI~ApUpzz$vH2F%btPfGDI-ZIe+n}KD)5G3_WNIk&7!%Okk6A+Zu8?=-9MS z?>t{12fh!`RGX__YF{D=cS(G$aTV>v}H8Y>03PGQq`P$ljM-_9WBT9krPi5w>OuF+nFI-tkWy4$139YS}qAt4)*LOctD;%R(Wra z@hvax@}v5-55m_kPxMsLXMOG_Hsr9SEWUi)%5gEwk8YN?Li(zi{tny@_4XoUg*HNH z(y#@xL0W6mCcFMoFs=b} zy0ve={KGftPMU<+tInk4DDC06n(xm6E_ch7tFYf#WRwOvL}@s-j;ZFMZ-EIklzH_+ zS!K%QzeXvvROVkzMYtDriXihP-ReLpt}!Hd{K4;}?6Y`%w+LVp&6=d{+5Pe7sSTAK z<{#KJ`o0zC1f76;8B0@?NU{S(h-^`EGzx_v;FKNFu*S{WFkr4R7MylC zoLwR;+B9?0lXkqzrN85U`t60bTGgk*il;**#R)tJo(BI)wv2jXC@B&0qh=JqfRiP&F0d(;!f0gz3vC!| zd3>ba)}tS4<-%75SZiU~Y_H{AEQ3%F3AB)YC@uL+-4l2IWYYqzR+i1%(t|9hEi#o-Cn z52OVHHkY)pRr$U?s#?pYK9qgNbWc}JQLWYEtok-)k-UV2A<4S*3jYwx!>cstdOK0U z!ov-I0YJcU==?EQBsWQ9l%lgiicT7=B5}SsFCS9?tZuc&;q}g9)B`CnOrUii5m7n? zFDVX;zq-GjB(dRhkYL8FcM&5wK&#(W#$y9d;`PN``^@J4>Qa_}JlOttwZ672{r2Z( z0#TM&2mVI$RqQSSuB}i|P@`CBrsfWo4QvW$A5qHg5;T=)0^oO#GF2HKyre|XA4Zwt zvzDQC)sBK7C4mo~Zc*n3fKAKnj3yLw@NKzy_EEN2j<{ z){cP0Z;^tgy$_8q^9~)*KaQ3|ly()KG~p!KGa%_Y(ZM*u8239ecj#zSC_e1%sb9I{ z1_hgTf-ge7duN>wy&R8Q&;3d%P$pkqGa2@c#xsEk9Bt1i6E^`#L^eU$3M2l=srLxzW8Y&R^hFq~@`1I-94BShr=81M-q`u-Uv z?&3lrDsb-6kwh()pvnobG_361%&N{2kC?hi3$~Epc|q#p=G^fpPcd`AC8ROReERB` z4LT>GL-w{y=^Uy^cD7={q3D#@LD4@cP1!6Ld}`yYcdkIeC$|r`2%eJslrCM?{S{}M zr93Uhn$)poh{&JyB#b5VX9f zqAX^^1Qa)hIvWs*wRx%^>vAf}vW=lm{N-K55Emy_F%l|xXmb8?uW|Lj9jQClpE(WQ z+^mci9W9RNL+f{i-^JM_F(~BHFy0AFDqdR9#%Jf;Yr!Jw?0efrJ{E4D783~0)DU;n zQuxYz&AyRlJ$UDZy}=`nnZy2e_?7j+sgR{@ow~M5#oPHxvhvw01W}SsX5s(woOr{4 bdqb4Qmz%rb(0TCZ=~zQWN4Z?lGU$H*2Ppz* diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index c5fa0e2e47..25f148aa7e 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -42,8 +42,8 @@ "dist/credentials/GithubApi.credentials.js", "dist/credentials/GithubOAuth2Api.credentials.js", "dist/credentials/GitlabApi.credentials.js", - "dist/credentials/GoogleApi.credentials.js", - "dist/credentials/GoogleOAuth2Api.credentials.js", + "dist/credentials/GoogleApi.credentials.js", + "dist/credentials/GoogleOAuth2Api.credentials.js", "dist/credentials/HttpBasicAuth.credentials.js", "dist/credentials/HttpDigestAuth.credentials.js", "dist/credentials/HttpHeaderAuth.credentials.js", @@ -55,9 +55,9 @@ "dist/credentials/MailchimpApi.credentials.js", "dist/credentials/MailgunApi.credentials.js", "dist/credentials/MandrillApi.credentials.js", - "dist/credentials/MattermostApi.credentials.js", - "dist/credentials/MicrosoftOAuth2Api.credentials.js", - "dist/credentials/MicrosoftExcelOAuth2Api.credentials.js", + "dist/credentials/MattermostApi.credentials.js", + "dist/credentials/MicrosoftExcelOAuth2Api.credentials.js", + "dist/credentials/MicrosoftOAuth2Api.credentials.js", "dist/credentials/MongoDb.credentials.js", "dist/credentials/MySql.credentials.js", "dist/credentials/NextCloudApi.credentials.js", @@ -84,8 +84,8 @@ "dist/credentials/TypeformApi.credentials.js", "dist/credentials/TogglApi.credentials.js", "dist/credentials/VeroApi.credentials.js", - "dist/credentials/WordpressApi.credentials.js", - "dist/credentials/ZohoOAuth2Api.credentials.js" + "dist/credentials/WordpressApi.credentials.js", + "dist/credentials/ZohoOAuth2Api.credentials.js" ], "nodes": [ "dist/nodes/ActiveCampaign/ActiveCampaign.node.js", @@ -119,8 +119,8 @@ "dist/nodes/Github/Github.node.js", "dist/nodes/Github/GithubTrigger.node.js", "dist/nodes/Gitlab/Gitlab.node.js", - "dist/nodes/Gitlab/GitlabTrigger.node.js", - "dist/nodes/Google/GoogleCalendar.node.js", + "dist/nodes/Gitlab/GitlabTrigger.node.js", + "dist/nodes/Google/GoogleCalendar.node.js", "dist/nodes/Google/GoogleDrive.node.js", "dist/nodes/Google/GoogleSheets.node.js", "dist/nodes/GraphQL/GraphQL.node.js", @@ -137,8 +137,8 @@ "dist/nodes/Mailgun/Mailgun.node.js", "dist/nodes/Mandrill/Mandrill.node.js", "dist/nodes/Mattermost/Mattermost.node.js", - "dist/nodes/Merge.node.js", - "dist/nodes/Microsoft/Excel/MicrosoftExcel.node.js", + "dist/nodes/Merge.node.js", + "dist/nodes/Microsoft/Excel/MicrosoftExcel.node.js", "dist/nodes/MoveBinaryData.node.js", "dist/nodes/MongoDb/MongoDb.node.js", "dist/nodes/MySql/MySql.node.js", @@ -181,8 +181,8 @@ "dist/nodes/WriteBinaryFile.node.js", "dist/nodes/Webhook.node.js", "dist/nodes/Wordpress/Wordpress.node.js", - "dist/nodes/Xml.node.js", - "dist/nodes/Zoho/ZohoCrm.node.js" + "dist/nodes/Xml.node.js", + "dist/nodes/Zoho/ZohoCrm.node.js" ] }, "devDependencies": { From 11fab3c702886da74173862c1708e3753fcaf630 Mon Sep 17 00:00:00 2001 From: ricardo Date: Sat, 28 Mar 2020 19:52:51 -0400 Subject: [PATCH 035/165] :zap: small fix --- packages/nodes-base/nodes/HelpScout/HelpScout.node.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/nodes-base/nodes/HelpScout/HelpScout.node.ts b/packages/nodes-base/nodes/HelpScout/HelpScout.node.ts index 27534b2426..a339e6cfb4 100644 --- a/packages/nodes-base/nodes/HelpScout/HelpScout.node.ts +++ b/packages/nodes-base/nodes/HelpScout/HelpScout.node.ts @@ -399,11 +399,11 @@ export class HelpScout implements INodeType { responseData = await helpscoutApiRequestAllItems.call(this, '_embedded.threads', 'GET', `/v2/conversations/${conversationId}/threads`); } } - } - if (Array.isArray(responseData)) { - returnData.push.apply(returnData, responseData as IDataObject[]); - } else if (responseData !== undefined) { - returnData.push(responseData as IDataObject); + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } } return [this.helpers.returnJsonArray(returnData)]; } From 5cbe76a3ceffeba8e0c052d4694d913c8dd93ccb Mon Sep 17 00:00:00 2001 From: ricardo Date: Sat, 28 Mar 2020 20:13:56 -0400 Subject: [PATCH 036/165] :zap: small fix --- .../nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.ts b/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.ts index cd263d854f..cee6733bd5 100644 --- a/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.ts +++ b/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.ts @@ -98,12 +98,14 @@ export class MicrosoftOneDrive implements INodeType { } responseData = await microsoftApiRequest.call(this, 'POST', `/drive/items/${fileId}/copy`, body, {}, undefined, {}, { json: true, resolveWithFullResponse: true }); responseData = { location : responseData.headers.location }; + returnData.push(responseData as IDataObject); } //https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_delete?view=odsp-graph-online if (operation === 'delete') { const fileId = this.getNodeParameter('fileId', i) as string; responseData = await microsoftApiRequest.call(this, 'DELETE', `/drive/items/${fileId}`); responseData = { success: true }; + returnData.push(responseData as IDataObject); } //https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_list_children?view=odsp-graph-online if (operation === 'download') { @@ -144,12 +146,14 @@ export class MicrosoftOneDrive implements INodeType { if (operation === 'get') { const fileId = this.getNodeParameter('fileId', i) as string; responseData = await microsoftApiRequest.call(this, 'GET', `/drive/items/${fileId}`); + returnData.push(responseData as IDataObject); } //https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_search?view=odsp-graph-online if (operation === 'search') { const query = this.getNodeParameter('query', i) as string; responseData = await microsoftApiRequestAllItems.call(this, 'value', 'GET', `/drive/root/search(q='{${query}}')`); responseData = responseData.filter((item: IDataObject) => item.file); + returnData.push(responseData as IDataObject); } //https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_put_content?view=odsp-graph-online#example-upload-a-new-file if (operation === 'upload') { @@ -176,6 +180,7 @@ export class MicrosoftOneDrive implements INodeType { const body = Buffer.from(binaryData.data, BINARY_ENCODING); responseData = await microsoftApiRequest.call(this, 'PUT', `/drive/items/${parentId}:/${fileName || binaryData.fileName}:/content`, body , {}, undefined, { 'Content-Type': binaryData.mimeType, 'Content-length': body.length } ); + returnData.push(responseData as IDataObject); } else { const body = Buffer.from(this.getNodeParameter('fileContent', i) as string, 'utf8'); @@ -183,6 +188,7 @@ export class MicrosoftOneDrive implements INodeType { throw new Error('File name must be defined'); } responseData = await microsoftApiRequest.call(this, 'PUT', `/drive/items/${parentId}:/${fileName}.txt:/content`, body , {}, undefined, { 'Content-Type': 'text/plain' } ); + returnData.push(responseData as IDataObject); } } } @@ -191,12 +197,14 @@ export class MicrosoftOneDrive implements INodeType { if (operation === 'getChildren') { const folderId = this.getNodeParameter('folderId', i) as string; responseData = await microsoftApiRequestAllItems.call(this, 'value', 'GET', `/drive/items/${folderId}/children`); + returnData.push(responseData as IDataObject); } //https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_search?view=odsp-graph-online if (operation === 'search') { const query = this.getNodeParameter('query', i) as string; responseData = await microsoftApiRequestAllItems.call(this, 'value', 'GET', `/drive/root/search(q='{${query}}')`); responseData = responseData.filter((item: IDataObject) => item.folder); + returnData.push(responseData as IDataObject); } } } From 83e6e8bf114399dc2dce70df330b02bc53f0cc28 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 29 Mar 2020 19:10:54 +0200 Subject: [PATCH 037/165] :zap: Improved HelpScout-Nodes --- .../HelpScout/ConversationDescription.ts | 126 +- .../nodes/HelpScout/ConversationInterface.ts | 10 +- .../nodes/HelpScout/CountriesCodes.ts | 2160 ++++++++--------- .../nodes/HelpScout/CustomerDescription.ts | 86 +- .../nodes/HelpScout/CustomerInterface.ts | 2 +- .../nodes/HelpScout/GenericFunctions.ts | 5 +- .../nodes/HelpScout/HelpScout.node.ts | 51 +- .../nodes/HelpScout/HelpScoutTrigger.node.ts | 49 +- .../nodes/HelpScout/MailboxDescription.ts | 2 +- .../nodes/HelpScout/ThreadDescription.ts | 14 +- .../nodes/HelpScout/ThreadInterface.ts | 2 +- .../nodes-base/nodes/HelpScout/helpScout.png | Bin 4862 -> 870 bytes packages/nodes-base/package.json | 8 +- 13 files changed, 1259 insertions(+), 1256 deletions(-) diff --git a/packages/nodes-base/nodes/HelpScout/ConversationDescription.ts b/packages/nodes-base/nodes/HelpScout/ConversationDescription.ts index cce35b096a..5d882ac80f 100644 --- a/packages/nodes-base/nodes/HelpScout/ConversationDescription.ts +++ b/packages/nodes-base/nodes/HelpScout/ConversationDescription.ts @@ -1,4 +1,4 @@ -import { INodeProperties } from "n8n-workflow"; +import { INodeProperties } from 'n8n-workflow'; export const conversationOperations = [ { @@ -310,7 +310,7 @@ export const conversationFields = [ alwaysOpenEditWindow: true }, default: '', - description: 'The message text, ' + description: 'The message text.' }, { displayName: 'Bcc', @@ -428,6 +428,13 @@ export const conversationFields = [ }, }, options: [ + { + displayName: 'Assign To', + name: 'assignTo', + type: 'number', + default: 0, + description: 'Filters conversations by assignee id', + }, { displayName: 'Embed', name: 'embed', @@ -441,13 +448,6 @@ export const conversationFields = [ default: '', description: 'Allows embedding/loading of sub-entities', }, - { - displayName: 'Mailbox ID', - name: 'mailbox', - type: 'string', - default: '', - description: 'Filters conversations from a specific mailbox', - }, { displayName: 'Folder ID', name: 'folder', @@ -456,54 +456,11 @@ export const conversationFields = [ description: 'Filters conversations from a specific folder id', }, { - displayName: 'Status', - name: 'status', - type: 'options', - options: [ - { - name: 'Active', - value: 'active', - }, - { - name: 'All', - value: 'all', - }, - { - name: 'Closed', - value: 'closed', - }, - { - name: 'Open', - value: 'open', - }, - { - name: 'Pending', - value: 'pending', - }, - { - name: 'Spam', - value: 'spam', - }, - ], - default: 'active', - description: 'Filter conversation by status', - }, - { - displayName: 'Tags', - name: 'tags', - type: 'multiOptions', - typeOptions: { - loadOptionsMethod: 'getTags', - }, - default: [], - description: 'Filter conversation by tags', - }, - { - displayName: 'Assign To', - name: 'assignTo', - type: 'number', - default: 0, - description: 'Filters conversations by assignee id', + displayName: 'Mailbox ID', + name: 'mailbox', + type: 'string', + default: '', + description: 'Filters conversations from a specific mailbox', }, { displayName: 'Modified Since', @@ -522,6 +479,16 @@ export const conversationFields = [ }, description: 'Looks up conversation by conversation number', }, + { + displayName: 'Query', + name: 'query', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'Advanced search Examples' + }, { displayName: 'Sort Field', name: 'sortField', @@ -584,14 +551,47 @@ export const conversationFields = [ default: 'desc', }, { - displayName: 'Query', - name: 'query', - type: 'string', + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Active', + value: 'active', + }, + { + name: 'All', + value: 'all', + }, + { + name: 'Closed', + value: 'closed', + }, + { + name: 'Open', + value: 'open', + }, + { + name: 'Pending', + value: 'pending', + }, + { + name: 'Spam', + value: 'spam', + }, + ], + default: 'active', + description: 'Filter conversation by status', + }, + { + displayName: 'Tags', + name: 'tags', + type: 'multiOptions', typeOptions: { - alwaysOpenEditWindow: true, + loadOptionsMethod: 'getTags', }, - default: '', - description: 'Advanced search Examples' + default: [], + description: 'Filter conversation by tags', }, ], }, diff --git a/packages/nodes-base/nodes/HelpScout/ConversationInterface.ts b/packages/nodes-base/nodes/HelpScout/ConversationInterface.ts index 4140fda218..ea617f5aa3 100644 --- a/packages/nodes-base/nodes/HelpScout/ConversationInterface.ts +++ b/packages/nodes-base/nodes/HelpScout/ConversationInterface.ts @@ -1,4 +1,4 @@ -import { IDataObject } from "n8n-workflow"; +import { IDataObject } from 'n8n-workflow'; export interface IConversation { assignTo?: number; @@ -8,11 +8,11 @@ export interface IConversation { customer?: IDataObject; fields?: IDataObject[]; imported?: boolean; - mailboxId?: number; // - status?: string; // - subject?: string; // + mailboxId?: number; + status?: string; + subject?: string; tags?: IDataObject[]; threads?: IDataObject[]; - type?: string; // + type?: string; user?: number; } diff --git a/packages/nodes-base/nodes/HelpScout/CountriesCodes.ts b/packages/nodes-base/nodes/HelpScout/CountriesCodes.ts index 653e876beb..3c41a76c7c 100644 --- a/packages/nodes-base/nodes/HelpScout/CountriesCodes.ts +++ b/packages/nodes-base/nodes/HelpScout/CountriesCodes.ts @@ -1,1579 +1,1579 @@ export const countriesCodes = [ { - "name": "Afghanistan", - "alpha2": "AF", - "alpha3": "AFG", - "numeric": "004" + 'name': 'Afghanistan', + 'alpha2': 'AF', + 'alpha3': 'AFG', + 'numeric': '004' }, { - "name": "Åland Islands", - "alpha2": "AX", - "alpha3": "ALA", - "numeric": "248", - "altName": "Aland Islands" + 'name': 'Åland Islands', + 'alpha2': 'AX', + 'alpha3': 'ALA', + 'numeric': '248', + 'altName': 'Aland Islands' }, { - "name": "Albania", - "alpha2": "AL", - "alpha3": "ALB", - "numeric": "008" + 'name': 'Albania', + 'alpha2': 'AL', + 'alpha3': 'ALB', + 'numeric': '008' }, { - "name": "Algeria", - "alpha2": "DZ", - "alpha3": "DZA", - "numeric": "012" + 'name': 'Algeria', + 'alpha2': 'DZ', + 'alpha3': 'DZA', + 'numeric': '012' }, { - "name": "American Samoa", - "alpha2": "AS", - "alpha3": "ASM", - "numeric": "016" + 'name': 'American Samoa', + 'alpha2': 'AS', + 'alpha3': 'ASM', + 'numeric': '016' }, { - "name": "Andorra", - "alpha2": "AD", - "alpha3": "AND", - "numeric": "020" + 'name': 'Andorra', + 'alpha2': 'AD', + 'alpha3': 'AND', + 'numeric': '020' }, { - "name": "Angola", - "alpha2": "AO", - "alpha3": "AGO", - "numeric": "024" + 'name': 'Angola', + 'alpha2': 'AO', + 'alpha3': 'AGO', + 'numeric': '024' }, { - "name": "Anguilla", - "alpha2": "AI", - "alpha3": "AIA", - "numeric": "660" + 'name': 'Anguilla', + 'alpha2': 'AI', + 'alpha3': 'AIA', + 'numeric': '660' }, { - "name": "Antarctica", - "alpha2": "AQ", - "alpha3": "ATA", - "numeric": "010" + 'name': 'Antarctica', + 'alpha2': 'AQ', + 'alpha3': 'ATA', + 'numeric': '010' }, { - "name": "Antigua and Barbuda", - "alpha2": "AG", - "alpha3": "ATG", - "numeric": "028" + 'name': 'Antigua and Barbuda', + 'alpha2': 'AG', + 'alpha3': 'ATG', + 'numeric': '028' }, { - "name": "Argentina", - "alpha2": "AR", - "alpha3": "ARG", - "numeric": "032" + 'name': 'Argentina', + 'alpha2': 'AR', + 'alpha3': 'ARG', + 'numeric': '032' }, { - "name": "Armenia", - "alpha2": "AM", - "alpha3": "ARM", - "numeric": "051" + 'name': 'Armenia', + 'alpha2': 'AM', + 'alpha3': 'ARM', + 'numeric': '051' }, { - "name": "Aruba", - "alpha2": "AW", - "alpha3": "ABW", - "numeric": "533" + 'name': 'Aruba', + 'alpha2': 'AW', + 'alpha3': 'ABW', + 'numeric': '533' }, { - "name": "Australia", - "alpha2": "AU", - "alpha3": "AUS", - "numeric": "036" + 'name': 'Australia', + 'alpha2': 'AU', + 'alpha3': 'AUS', + 'numeric': '036' }, { - "name": "Austria", - "alpha2": "AT", - "alpha3": "AUT", - "numeric": "040" + 'name': 'Austria', + 'alpha2': 'AT', + 'alpha3': 'AUT', + 'numeric': '040' }, { - "name": "Azerbaijan", - "alpha2": "AZ", - "alpha3": "AZE", - "numeric": "031" + 'name': 'Azerbaijan', + 'alpha2': 'AZ', + 'alpha3': 'AZE', + 'numeric': '031' }, { - "name": "Bahamas (the)", - "alpha2": "BS", - "alpha3": "BHS", - "numeric": "044", - "altName": "Bahamas" + 'name': 'Bahamas (the)', + 'alpha2': 'BS', + 'alpha3': 'BHS', + 'numeric': '044', + 'altName': 'Bahamas' }, { - "name": "Bahrain", - "alpha2": "BH", - "alpha3": "BHR", - "numeric": "048" + 'name': 'Bahrain', + 'alpha2': 'BH', + 'alpha3': 'BHR', + 'numeric': '048' }, { - "name": "Bangladesh", - "alpha2": "BD", - "alpha3": "BGD", - "numeric": "050" + 'name': 'Bangladesh', + 'alpha2': 'BD', + 'alpha3': 'BGD', + 'numeric': '050' }, { - "name": "Barbados", - "alpha2": "BB", - "alpha3": "BRB", - "numeric": "052" + 'name': 'Barbados', + 'alpha2': 'BB', + 'alpha3': 'BRB', + 'numeric': '052' }, { - "name": "Belarus", - "alpha2": "BY", - "alpha3": "BLR", - "numeric": "112" + 'name': 'Belarus', + 'alpha2': 'BY', + 'alpha3': 'BLR', + 'numeric': '112' }, { - "name": "Belgium", - "alpha2": "BE", - "alpha3": "BEL", - "numeric": "056" + 'name': 'Belgium', + 'alpha2': 'BE', + 'alpha3': 'BEL', + 'numeric': '056' }, { - "name": "Belize", - "alpha2": "BZ", - "alpha3": "BLZ", - "numeric": "084" + 'name': 'Belize', + 'alpha2': 'BZ', + 'alpha3': 'BLZ', + 'numeric': '084' }, { - "name": "Benin", - "alpha2": "BJ", - "alpha3": "BEN", - "numeric": "204" + 'name': 'Benin', + 'alpha2': 'BJ', + 'alpha3': 'BEN', + 'numeric': '204' }, { - "name": "Bermuda", - "alpha2": "BM", - "alpha3": "BMU", - "numeric": "060" + 'name': 'Bermuda', + 'alpha2': 'BM', + 'alpha3': 'BMU', + 'numeric': '060' }, { - "name": "Bhutan", - "alpha2": "BT", - "alpha3": "BTN", - "numeric": "064" + 'name': 'Bhutan', + 'alpha2': 'BT', + 'alpha3': 'BTN', + 'numeric': '064' }, { - "name": "Bolivia (Plurinational State of)", - "alpha2": "BO", - "alpha3": "BOL", - "numeric": "068", - "altName": "Bolivia" + 'name': 'Bolivia (Plurinational State of)', + 'alpha2': 'BO', + 'alpha3': 'BOL', + 'numeric': '068', + 'altName': 'Bolivia' }, { - "name": "Bonaire, Sint Eustatius and Saba", - "alpha2": "BQ", - "alpha3": "BES", - "numeric": "535" + 'name': 'Bonaire, Sint Eustatius and Saba', + 'alpha2': 'BQ', + 'alpha3': 'BES', + 'numeric': '535' }, { - "name": "Bosnia and Herzegovina", - "alpha2": "BA", - "alpha3": "BIH", - "numeric": "070" + 'name': 'Bosnia and Herzegovina', + 'alpha2': 'BA', + 'alpha3': 'BIH', + 'numeric': '070' }, { - "name": "Botswana", - "alpha2": "BW", - "alpha3": "BWA", - "numeric": "072" + 'name': 'Botswana', + 'alpha2': 'BW', + 'alpha3': 'BWA', + 'numeric': '072' }, { - "name": "Bouvet Island", - "alpha2": "BV", - "alpha3": "BVT", - "numeric": "074" + 'name': 'Bouvet Island', + 'alpha2': 'BV', + 'alpha3': 'BVT', + 'numeric': '074' }, { - "name": "Brazil", - "alpha2": "BR", - "alpha3": "BRA", - "numeric": "076" + 'name': 'Brazil', + 'alpha2': 'BR', + 'alpha3': 'BRA', + 'numeric': '076' }, { - "name": "British Indian Ocean Territory (the)", - "alpha2": "IO", - "alpha3": "IOT", - "numeric": "086", - "altName": "British Indian Ocean Territory" + 'name': 'British Indian Ocean Territory (the)', + 'alpha2': 'IO', + 'alpha3': 'IOT', + 'numeric': '086', + 'altName': 'British Indian Ocean Territory' }, { - "name": "Brunei Darussalam", - "alpha2": "BN", - "alpha3": "BRN", - "numeric": "096", - "shortName": "Brunei" + 'name': 'Brunei Darussalam', + 'alpha2': 'BN', + 'alpha3': 'BRN', + 'numeric': '096', + 'shortName': 'Brunei' }, { - "name": "Bulgaria", - "alpha2": "BG", - "alpha3": "BGR", - "numeric": "100" + 'name': 'Bulgaria', + 'alpha2': 'BG', + 'alpha3': 'BGR', + 'numeric': '100' }, { - "name": "Burkina Faso", - "alpha2": "BF", - "alpha3": "BFA", - "numeric": "854" + 'name': 'Burkina Faso', + 'alpha2': 'BF', + 'alpha3': 'BFA', + 'numeric': '854' }, { - "name": "Burundi", - "alpha2": "BI", - "alpha3": "BDI", - "numeric": "108" + 'name': 'Burundi', + 'alpha2': 'BI', + 'alpha3': 'BDI', + 'numeric': '108' }, { - "name": "Cabo Verde", - "alpha2": "CV", - "alpha3": "CPV", - "numeric": "132", - "altName": "Cape Verde" + 'name': 'Cabo Verde', + 'alpha2': 'CV', + 'alpha3': 'CPV', + 'numeric': '132', + 'altName': 'Cape Verde' }, { - "name": "Cambodia", - "alpha2": "KH", - "alpha3": "KHM", - "numeric": "116" + 'name': 'Cambodia', + 'alpha2': 'KH', + 'alpha3': 'KHM', + 'numeric': '116' }, { - "name": "Cameroon", - "alpha2": "CM", - "alpha3": "CMR", - "numeric": "120" + 'name': 'Cameroon', + 'alpha2': 'CM', + 'alpha3': 'CMR', + 'numeric': '120' }, { - "name": "Canada", - "alpha2": "CA", - "alpha3": "CAN", - "numeric": "124" + 'name': 'Canada', + 'alpha2': 'CA', + 'alpha3': 'CAN', + 'numeric': '124' }, { - "name": "Cayman Islands (the)", - "alpha2": "KY", - "alpha3": "CYM", - "numeric": "136", - "altName": "Cayman Islands" + 'name': 'Cayman Islands (the)', + 'alpha2': 'KY', + 'alpha3': 'CYM', + 'numeric': '136', + 'altName': 'Cayman Islands' }, { - "name": "Central African Republic (the)", - "alpha2": "CF", - "alpha3": "CAF", - "numeric": "140", - "altName": "Central African Republic" + 'name': 'Central African Republic (the)', + 'alpha2': 'CF', + 'alpha3': 'CAF', + 'numeric': '140', + 'altName': 'Central African Republic' }, { - "name": "Chad", - "alpha2": "TD", - "alpha3": "TCD", - "numeric": "148" + 'name': 'Chad', + 'alpha2': 'TD', + 'alpha3': 'TCD', + 'numeric': '148' }, { - "name": "Chile", - "alpha2": "CL", - "alpha3": "CHL", - "numeric": "152" + 'name': 'Chile', + 'alpha2': 'CL', + 'alpha3': 'CHL', + 'numeric': '152' }, { - "name": "China", - "alpha2": "CN", - "alpha3": "CHN", - "numeric": "156" + 'name': 'China', + 'alpha2': 'CN', + 'alpha3': 'CHN', + 'numeric': '156' }, { - "name": "Christmas Island", - "alpha2": "CX", - "alpha3": "CXR", - "numeric": "162" + 'name': 'Christmas Island', + 'alpha2': 'CX', + 'alpha3': 'CXR', + 'numeric': '162' }, { - "name": "Cocos (Keeling) Islands (the)", - "alpha2": "CC", - "alpha3": "CCK", - "numeric": "166", - "altName": "Cocos (Keeling) Islands", - "shortName": "Cocos Islands" + 'name': 'Cocos (Keeling) Islands (the)', + 'alpha2': 'CC', + 'alpha3': 'CCK', + 'numeric': '166', + 'altName': 'Cocos (Keeling) Islands', + 'shortName': 'Cocos Islands' }, { - "name": "Colombia", - "alpha2": "CO", - "alpha3": "COL", - "numeric": "170" + 'name': 'Colombia', + 'alpha2': 'CO', + 'alpha3': 'COL', + 'numeric': '170' }, { - "name": "Comoros (the)", - "alpha2": "KM", - "alpha3": "COM", - "numeric": "174", - "altName": "Comoros" + 'name': 'Comoros (the)', + 'alpha2': 'KM', + 'alpha3': 'COM', + 'numeric': '174', + 'altName': 'Comoros' }, { - "name": "Congo (the Democratic Republic of the)", - "alpha2": "CD", - "alpha3": "COD", - "numeric": "180", - "altName": "Congo, (Kinshasa)", - "shortName": "Democratic Republic of the Congo" + 'name': 'Congo (the Democratic Republic of the)', + 'alpha2': 'CD', + 'alpha3': 'COD', + 'numeric': '180', + 'altName': 'Congo, (Kinshasa)', + 'shortName': 'Democratic Republic of the Congo' }, { - "name": "Congo (the)", - "alpha2": "CG", - "alpha3": "COG", - "numeric": "178", - "altName": "Congo (Brazzaville)", - "shortName": "Republic of the Congo" + 'name': 'Congo (the)', + 'alpha2': 'CG', + 'alpha3': 'COG', + 'numeric': '178', + 'altName': 'Congo (Brazzaville)', + 'shortName': 'Republic of the Congo' }, { - "name": "Cook Islands (the)", - "alpha2": "CK", - "alpha3": "COK", - "numeric": "184", - "altName": "Cook Islands" + 'name': 'Cook Islands (the)', + 'alpha2': 'CK', + 'alpha3': 'COK', + 'numeric': '184', + 'altName': 'Cook Islands' }, { - "name": "Costa Rica", - "alpha2": "CR", - "alpha3": "CRI", - "numeric": "188" + 'name': 'Costa Rica', + 'alpha2': 'CR', + 'alpha3': 'CRI', + 'numeric': '188' }, { - "name": "Côte d'Ivoire", - "alpha2": "CI", - "alpha3": "CIV", - "numeric": "384", - "shortName": "Ivory Coast" + 'name': 'Côte d\'Ivoire', + 'alpha2': 'CI', + 'alpha3': 'CIV', + 'numeric': '384', + 'shortName': 'Ivory Coast' }, { - "name": "Croatia", - "alpha2": "HR", - "alpha3": "HRV", - "numeric": "191" + 'name': 'Croatia', + 'alpha2': 'HR', + 'alpha3': 'HRV', + 'numeric': '191' }, { - "name": "Cuba", - "alpha2": "CU", - "alpha3": "CUB", - "numeric": "192" + 'name': 'Cuba', + 'alpha2': 'CU', + 'alpha3': 'CUB', + 'numeric': '192' }, { - "name": "Curaçao", - "alpha2": "CW", - "alpha3": "CUW", - "numeric": "531", - "shortName": "Curacao" + 'name': 'Curaçao', + 'alpha2': 'CW', + 'alpha3': 'CUW', + 'numeric': '531', + 'shortName': 'Curacao' }, { - "name": "Cyprus", - "alpha2": "CY", - "alpha3": "CYP", - "numeric": "196" + 'name': 'Cyprus', + 'alpha2': 'CY', + 'alpha3': 'CYP', + 'numeric': '196' }, { - "name": "Czechia", - "alpha2": "CZ", - "alpha3": "CZE", - "numeric": "203", - "altName": "Czech Republic" + 'name': 'Czechia', + 'alpha2': 'CZ', + 'alpha3': 'CZE', + 'numeric': '203', + 'altName': 'Czech Republic' }, { - "name": "Denmark", - "alpha2": "DK", - "alpha3": "DNK", - "numeric": "208" + 'name': 'Denmark', + 'alpha2': 'DK', + 'alpha3': 'DNK', + 'numeric': '208' }, { - "name": "Djibouti", - "alpha2": "DJ", - "alpha3": "DJI", - "numeric": "262" + 'name': 'Djibouti', + 'alpha2': 'DJ', + 'alpha3': 'DJI', + 'numeric': '262' }, { - "name": "Dominica", - "alpha2": "DM", - "alpha3": "DMA", - "numeric": "212" + 'name': 'Dominica', + 'alpha2': 'DM', + 'alpha3': 'DMA', + 'numeric': '212' }, { - "name": "Dominican Republic (the)", - "alpha2": "DO", - "alpha3": "DOM", - "numeric": "214", - "altName": "Dominican Republic" + 'name': 'Dominican Republic (the)', + 'alpha2': 'DO', + 'alpha3': 'DOM', + 'numeric': '214', + 'altName': 'Dominican Republic' }, { - "name": "Ecuador", - "alpha2": "EC", - "alpha3": "ECU", - "numeric": "218" + 'name': 'Ecuador', + 'alpha2': 'EC', + 'alpha3': 'ECU', + 'numeric': '218' }, { - "name": "Egypt", - "alpha2": "EG", - "alpha3": "EGY", - "numeric": "818" + 'name': 'Egypt', + 'alpha2': 'EG', + 'alpha3': 'EGY', + 'numeric': '818' }, { - "name": "El Salvador", - "alpha2": "SV", - "alpha3": "SLV", - "numeric": "222" + 'name': 'El Salvador', + 'alpha2': 'SV', + 'alpha3': 'SLV', + 'numeric': '222' }, { - "name": "Equatorial Guinea", - "alpha2": "GQ", - "alpha3": "GNQ", - "numeric": "226" + 'name': 'Equatorial Guinea', + 'alpha2': 'GQ', + 'alpha3': 'GNQ', + 'numeric': '226' }, { - "name": "Eritrea", - "alpha2": "ER", - "alpha3": "ERI", - "numeric": "232" + 'name': 'Eritrea', + 'alpha2': 'ER', + 'alpha3': 'ERI', + 'numeric': '232' }, { - "name": "Estonia", - "alpha2": "EE", - "alpha3": "EST", - "numeric": "233" + 'name': 'Estonia', + 'alpha2': 'EE', + 'alpha3': 'EST', + 'numeric': '233' }, { - "name": "Ethiopia", - "alpha2": "ET", - "alpha3": "ETH", - "numeric": "231" + 'name': 'Ethiopia', + 'alpha2': 'ET', + 'alpha3': 'ETH', + 'numeric': '231' }, { - "name": "Falkland Islands (the) [Malvinas]", - "alpha2": "FK", - "alpha3": "FLK", - "numeric": "238", - "altName": "Falkland Islands (Malvinas)", - "shortName": "Falkland Islands" + 'name': 'Falkland Islands (the) [Malvinas]', + 'alpha2': 'FK', + 'alpha3': 'FLK', + 'numeric': '238', + 'altName': 'Falkland Islands (Malvinas)', + 'shortName': 'Falkland Islands' }, { - "name": "Faroe Islands (the)", - "alpha2": "FO", - "alpha3": "FRO", - "numeric": "234", - "altName": "Faroe Islands" + 'name': 'Faroe Islands (the)', + 'alpha2': 'FO', + 'alpha3': 'FRO', + 'numeric': '234', + 'altName': 'Faroe Islands' }, { - "name": "Fiji", - "alpha2": "FJ", - "alpha3": "FJI", - "numeric": "242" + 'name': 'Fiji', + 'alpha2': 'FJ', + 'alpha3': 'FJI', + 'numeric': '242' }, { - "name": "Finland", - "alpha2": "FI", - "alpha3": "FIN", - "numeric": "246" + 'name': 'Finland', + 'alpha2': 'FI', + 'alpha3': 'FIN', + 'numeric': '246' }, { - "name": "France", - "alpha2": "FR", - "alpha3": "FRA", - "numeric": "250" + 'name': 'France', + 'alpha2': 'FR', + 'alpha3': 'FRA', + 'numeric': '250' }, { - "name": "French Guiana", - "alpha2": "GF", - "alpha3": "GUF", - "numeric": "254" + 'name': 'French Guiana', + 'alpha2': 'GF', + 'alpha3': 'GUF', + 'numeric': '254' }, { - "name": "French Polynesia", - "alpha2": "PF", - "alpha3": "PYF", - "numeric": "258" + 'name': 'French Polynesia', + 'alpha2': 'PF', + 'alpha3': 'PYF', + 'numeric': '258' }, { - "name": "French Southern Territories (the)", - "alpha2": "TF", - "alpha3": "ATF", - "numeric": "260", - "altName": "French Southern Territories" + 'name': 'French Southern Territories (the)', + 'alpha2': 'TF', + 'alpha3': 'ATF', + 'numeric': '260', + 'altName': 'French Southern Territories' }, { - "name": "Gabon", - "alpha2": "GA", - "alpha3": "GAB", - "numeric": "266" + 'name': 'Gabon', + 'alpha2': 'GA', + 'alpha3': 'GAB', + 'numeric': '266' }, { - "name": "Gambia (the)", - "alpha2": "GM", - "alpha3": "GMB", - "numeric": "270", - "altName": "Gambia" + 'name': 'Gambia (the)', + 'alpha2': 'GM', + 'alpha3': 'GMB', + 'numeric': '270', + 'altName': 'Gambia' }, { - "name": "Georgia", - "alpha2": "GE", - "alpha3": "GEO", - "numeric": "268" + 'name': 'Georgia', + 'alpha2': 'GE', + 'alpha3': 'GEO', + 'numeric': '268' }, { - "name": "Germany", - "alpha2": "DE", - "alpha3": "DEU", - "numeric": "276" + 'name': 'Germany', + 'alpha2': 'DE', + 'alpha3': 'DEU', + 'numeric': '276' }, { - "name": "Ghana", - "alpha2": "GH", - "alpha3": "GHA", - "numeric": "288" + 'name': 'Ghana', + 'alpha2': 'GH', + 'alpha3': 'GHA', + 'numeric': '288' }, { - "name": "Gibraltar", - "alpha2": "GI", - "alpha3": "GIB", - "numeric": "292" + 'name': 'Gibraltar', + 'alpha2': 'GI', + 'alpha3': 'GIB', + 'numeric': '292' }, { - "name": "Greece", - "alpha2": "GR", - "alpha3": "GRC", - "numeric": "300" + 'name': 'Greece', + 'alpha2': 'GR', + 'alpha3': 'GRC', + 'numeric': '300' }, { - "name": "Greenland", - "alpha2": "GL", - "alpha3": "GRL", - "numeric": "304" + 'name': 'Greenland', + 'alpha2': 'GL', + 'alpha3': 'GRL', + 'numeric': '304' }, { - "name": "Grenada", - "alpha2": "GD", - "alpha3": "GRD", - "numeric": "308" + 'name': 'Grenada', + 'alpha2': 'GD', + 'alpha3': 'GRD', + 'numeric': '308' }, { - "name": "Guadeloupe", - "alpha2": "GP", - "alpha3": "GLP", - "numeric": "312" + 'name': 'Guadeloupe', + 'alpha2': 'GP', + 'alpha3': 'GLP', + 'numeric': '312' }, { - "name": "Guam", - "alpha2": "GU", - "alpha3": "GUM", - "numeric": "316" + 'name': 'Guam', + 'alpha2': 'GU', + 'alpha3': 'GUM', + 'numeric': '316' }, { - "name": "Guatemala", - "alpha2": "GT", - "alpha3": "GTM", - "numeric": "320" + 'name': 'Guatemala', + 'alpha2': 'GT', + 'alpha3': 'GTM', + 'numeric': '320' }, { - "name": "Guernsey", - "alpha2": "GG", - "alpha3": "GGY", - "numeric": "831" + 'name': 'Guernsey', + 'alpha2': 'GG', + 'alpha3': 'GGY', + 'numeric': '831' }, { - "name": "Guinea", - "alpha2": "GN", - "alpha3": "GIN", - "numeric": "324" + 'name': 'Guinea', + 'alpha2': 'GN', + 'alpha3': 'GIN', + 'numeric': '324' }, { - "name": "Guinea-Bissau", - "alpha2": "GW", - "alpha3": "GNB", - "numeric": "624" + 'name': 'Guinea-Bissau', + 'alpha2': 'GW', + 'alpha3': 'GNB', + 'numeric': '624' }, { - "name": "Guyana", - "alpha2": "GY", - "alpha3": "GUY", - "numeric": "328" + 'name': 'Guyana', + 'alpha2': 'GY', + 'alpha3': 'GUY', + 'numeric': '328' }, { - "name": "Haiti", - "alpha2": "HT", - "alpha3": "HTI", - "numeric": "332" + 'name': 'Haiti', + 'alpha2': 'HT', + 'alpha3': 'HTI', + 'numeric': '332' }, { - "name": "Heard Island and McDonald Islands", - "alpha2": "HM", - "alpha3": "HMD", - "numeric": "334", - "altName": "Heard and Mcdonald Islands" + 'name': 'Heard Island and McDonald Islands', + 'alpha2': 'HM', + 'alpha3': 'HMD', + 'numeric': '334', + 'altName': 'Heard and Mcdonald Islands' }, { - "name": "Holy See (the)", - "alpha2": "VA", - "alpha3": "VAT", - "numeric": "336", - "altName": "Holy See (Vatican City State)", - "shortName": "Vatican" + 'name': 'Holy See (the)', + 'alpha2': 'VA', + 'alpha3': 'VAT', + 'numeric': '336', + 'altName': 'Holy See (Vatican City State)', + 'shortName': 'Vatican' }, { - "name": "Honduras", - "alpha2": "HN", - "alpha3": "HND", - "numeric": "340" + 'name': 'Honduras', + 'alpha2': 'HN', + 'alpha3': 'HND', + 'numeric': '340' }, { - "name": "Hong Kong", - "alpha2": "HK", - "alpha3": "HKG", - "numeric": "344", - "altName": "Hong Kong, SAR China" + 'name': 'Hong Kong', + 'alpha2': 'HK', + 'alpha3': 'HKG', + 'numeric': '344', + 'altName': 'Hong Kong, SAR China' }, { - "name": "Hungary", - "alpha2": "HU", - "alpha3": "HUN", - "numeric": "348" + 'name': 'Hungary', + 'alpha2': 'HU', + 'alpha3': 'HUN', + 'numeric': '348' }, { - "name": "Iceland", - "alpha2": "IS", - "alpha3": "ISL", - "numeric": "352" + 'name': 'Iceland', + 'alpha2': 'IS', + 'alpha3': 'ISL', + 'numeric': '352' }, { - "name": "India", - "alpha2": "IN", - "alpha3": "IND", - "numeric": "356" + 'name': 'India', + 'alpha2': 'IN', + 'alpha3': 'IND', + 'numeric': '356' }, { - "name": "Indonesia", - "alpha2": "ID", - "alpha3": "IDN", - "numeric": "360" + 'name': 'Indonesia', + 'alpha2': 'ID', + 'alpha3': 'IDN', + 'numeric': '360' }, { - "name": "Iran (Islamic Republic of)", - "alpha2": "IR", - "alpha3": "IRN", - "numeric": "364", - "altName": "Iran, Islamic Republic of", - "shortName": "Iran" + 'name': 'Iran (Islamic Republic of)', + 'alpha2': 'IR', + 'alpha3': 'IRN', + 'numeric': '364', + 'altName': 'Iran, Islamic Republic of', + 'shortName': 'Iran' }, { - "name": "Iraq", - "alpha2": "IQ", - "alpha3": "IRQ", - "numeric": "368" + 'name': 'Iraq', + 'alpha2': 'IQ', + 'alpha3': 'IRQ', + 'numeric': '368' }, { - "name": "Ireland", - "alpha2": "IE", - "alpha3": "IRL", - "numeric": "372" + 'name': 'Ireland', + 'alpha2': 'IE', + 'alpha3': 'IRL', + 'numeric': '372' }, { - "name": "Isle of Man", - "alpha2": "IM", - "alpha3": "IMN", - "numeric": "833" + 'name': 'Isle of Man', + 'alpha2': 'IM', + 'alpha3': 'IMN', + 'numeric': '833' }, { - "name": "Israel", - "alpha2": "IL", - "alpha3": "ISR", - "numeric": "376" + 'name': 'Israel', + 'alpha2': 'IL', + 'alpha3': 'ISR', + 'numeric': '376' }, { - "name": "Italy", - "alpha2": "IT", - "alpha3": "ITA", - "numeric": "380" + 'name': 'Italy', + 'alpha2': 'IT', + 'alpha3': 'ITA', + 'numeric': '380' }, { - "name": "Jamaica", - "alpha2": "JM", - "alpha3": "JAM", - "numeric": "388" + 'name': 'Jamaica', + 'alpha2': 'JM', + 'alpha3': 'JAM', + 'numeric': '388' }, { - "name": "Japan", - "alpha2": "JP", - "alpha3": "JPN", - "numeric": "392" + 'name': 'Japan', + 'alpha2': 'JP', + 'alpha3': 'JPN', + 'numeric': '392' }, { - "name": "Jersey", - "alpha2": "JE", - "alpha3": "JEY", - "numeric": "832" + 'name': 'Jersey', + 'alpha2': 'JE', + 'alpha3': 'JEY', + 'numeric': '832' }, { - "name": "Jordan", - "alpha2": "JO", - "alpha3": "JOR", - "numeric": "400" + 'name': 'Jordan', + 'alpha2': 'JO', + 'alpha3': 'JOR', + 'numeric': '400' }, { - "name": "Kazakhstan", - "alpha2": "KZ", - "alpha3": "KAZ", - "numeric": "398" + 'name': 'Kazakhstan', + 'alpha2': 'KZ', + 'alpha3': 'KAZ', + 'numeric': '398' }, { - "name": "Kenya", - "alpha2": "KE", - "alpha3": "KEN", - "numeric": "404" + 'name': 'Kenya', + 'alpha2': 'KE', + 'alpha3': 'KEN', + 'numeric': '404' }, { - "name": "Kiribati", - "alpha2": "KI", - "alpha3": "KIR", - "numeric": "296" + 'name': 'Kiribati', + 'alpha2': 'KI', + 'alpha3': 'KIR', + 'numeric': '296' }, { - "name": "Korea (the Democratic People's Republic of)", - "alpha2": "KP", - "alpha3": "PRK", - "numeric": "408", - "altName": "Korea (North)", - "shortName": "North Korea" + 'name': 'Korea (the Democratic People\'s Republic of)', + 'alpha2': 'KP', + 'alpha3': 'PRK', + 'numeric': '408', + 'altName': 'Korea (North)', + 'shortName': 'North Korea' }, { - "name": "Korea (the Republic of)", - "alpha2": "KR", - "alpha3": "KOR", - "numeric": "410", - "altName": "Korea (South)", - "shortName": "South Korea" + 'name': 'Korea (the Republic of)', + 'alpha2': 'KR', + 'alpha3': 'KOR', + 'numeric': '410', + 'altName': 'Korea (South)', + 'shortName': 'South Korea' }, { - "name": "Kuwait", - "alpha2": "KW", - "alpha3": "KWT", - "numeric": "414" + 'name': 'Kuwait', + 'alpha2': 'KW', + 'alpha3': 'KWT', + 'numeric': '414' }, { - "name": "Kyrgyzstan", - "alpha2": "KG", - "alpha3": "KGZ", - "numeric": "417" + 'name': 'Kyrgyzstan', + 'alpha2': 'KG', + 'alpha3': 'KGZ', + 'numeric': '417' }, { - "name": "Lao People's Democratic Republic (the)", - "alpha2": "LA", - "alpha3": "LAO", - "numeric": "418", - "altName": "Lao PDR", - "shortName": "Laos" + 'name': 'Lao People\'s Democratic Republic (the)', + 'alpha2': 'LA', + 'alpha3': 'LAO', + 'numeric': '418', + 'altName': 'Lao PDR', + 'shortName': 'Laos' }, { - "name": "Latvia", - "alpha2": "LV", - "alpha3": "LVA", - "numeric": "428" + 'name': 'Latvia', + 'alpha2': 'LV', + 'alpha3': 'LVA', + 'numeric': '428' }, { - "name": "Lebanon", - "alpha2": "LB", - "alpha3": "LBN", - "numeric": "422" + 'name': 'Lebanon', + 'alpha2': 'LB', + 'alpha3': 'LBN', + 'numeric': '422' }, { - "name": "Lesotho", - "alpha2": "LS", - "alpha3": "LSO", - "numeric": "426" + 'name': 'Lesotho', + 'alpha2': 'LS', + 'alpha3': 'LSO', + 'numeric': '426' }, { - "name": "Liberia", - "alpha2": "LR", - "alpha3": "LBR", - "numeric": "430" + 'name': 'Liberia', + 'alpha2': 'LR', + 'alpha3': 'LBR', + 'numeric': '430' }, { - "name": "Libya", - "alpha2": "LY", - "alpha3": "LBY", - "numeric": "434" + 'name': 'Libya', + 'alpha2': 'LY', + 'alpha3': 'LBY', + 'numeric': '434' }, { - "name": "Liechtenstein", - "alpha2": "LI", - "alpha3": "LIE", - "numeric": "438" + 'name': 'Liechtenstein', + 'alpha2': 'LI', + 'alpha3': 'LIE', + 'numeric': '438' }, { - "name": "Lithuania", - "alpha2": "LT", - "alpha3": "LTU", - "numeric": "440" + 'name': 'Lithuania', + 'alpha2': 'LT', + 'alpha3': 'LTU', + 'numeric': '440' }, { - "name": "Luxembourg", - "alpha2": "LU", - "alpha3": "LUX", - "numeric": "442" + 'name': 'Luxembourg', + 'alpha2': 'LU', + 'alpha3': 'LUX', + 'numeric': '442' }, { - "name": "Macao", - "alpha2": "MO", - "alpha3": "MAC", - "numeric": "446", - "altName": "Macao, SAR China", - "shortName": "Macau" + 'name': 'Macao', + 'alpha2': 'MO', + 'alpha3': 'MAC', + 'numeric': '446', + 'altName': 'Macao, SAR China', + 'shortName': 'Macau' }, { - "name": "Macedonia (the former Yugoslav Republic of)", - "alpha2": "MK", - "alpha3": "MKD", - "numeric": "807", - "altName": "Macedonia, Republic of", - "shortName": "Macedonia" + 'name': 'Macedonia (the former Yugoslav Republic of)', + 'alpha2': 'MK', + 'alpha3': 'MKD', + 'numeric': '807', + 'altName': 'Macedonia, Republic of', + 'shortName': 'Macedonia' }, { - "name": "Madagascar", - "alpha2": "MG", - "alpha3": "MDG", - "numeric": "450" + 'name': 'Madagascar', + 'alpha2': 'MG', + 'alpha3': 'MDG', + 'numeric': '450' }, { - "name": "Malawi", - "alpha2": "MW", - "alpha3": "MWI", - "numeric": "454" + 'name': 'Malawi', + 'alpha2': 'MW', + 'alpha3': 'MWI', + 'numeric': '454' }, { - "name": "Malaysia", - "alpha2": "MY", - "alpha3": "MYS", - "numeric": "458" + 'name': 'Malaysia', + 'alpha2': 'MY', + 'alpha3': 'MYS', + 'numeric': '458' }, { - "name": "Maldives", - "alpha2": "MV", - "alpha3": "MDV", - "numeric": "462" + 'name': 'Maldives', + 'alpha2': 'MV', + 'alpha3': 'MDV', + 'numeric': '462' }, { - "name": "Mali", - "alpha2": "ML", - "alpha3": "MLI", - "numeric": "466" + 'name': 'Mali', + 'alpha2': 'ML', + 'alpha3': 'MLI', + 'numeric': '466' }, { - "name": "Malta", - "alpha2": "MT", - "alpha3": "MLT", - "numeric": "470" + 'name': 'Malta', + 'alpha2': 'MT', + 'alpha3': 'MLT', + 'numeric': '470' }, { - "name": "Marshall Islands (the)", - "alpha2": "MH", - "alpha3": "MHL", - "numeric": "584", - "altName": "Marshall Islands" + 'name': 'Marshall Islands (the)', + 'alpha2': 'MH', + 'alpha3': 'MHL', + 'numeric': '584', + 'altName': 'Marshall Islands' }, { - "name": "Martinique", - "alpha2": "MQ", - "alpha3": "MTQ", - "numeric": "474" + 'name': 'Martinique', + 'alpha2': 'MQ', + 'alpha3': 'MTQ', + 'numeric': '474' }, { - "name": "Mauritania", - "alpha2": "MR", - "alpha3": "MRT", - "numeric": "478" + 'name': 'Mauritania', + 'alpha2': 'MR', + 'alpha3': 'MRT', + 'numeric': '478' }, { - "name": "Mauritius", - "alpha2": "MU", - "alpha3": "MUS", - "numeric": "480" + 'name': 'Mauritius', + 'alpha2': 'MU', + 'alpha3': 'MUS', + 'numeric': '480' }, { - "name": "Mayotte", - "alpha2": "YT", - "alpha3": "MYT", - "numeric": "175" + 'name': 'Mayotte', + 'alpha2': 'YT', + 'alpha3': 'MYT', + 'numeric': '175' }, { - "name": "Mexico", - "alpha2": "MX", - "alpha3": "MEX", - "numeric": "484" + 'name': 'Mexico', + 'alpha2': 'MX', + 'alpha3': 'MEX', + 'numeric': '484' }, { - "name": "Micronesia (Federated States of)", - "alpha2": "FM", - "alpha3": "FSM", - "numeric": "583", - "altName": "Micronesia, Federated States of", - "shortName": "Micronesia" + 'name': 'Micronesia (Federated States of)', + 'alpha2': 'FM', + 'alpha3': 'FSM', + 'numeric': '583', + 'altName': 'Micronesia, Federated States of', + 'shortName': 'Micronesia' }, { - "name": "Moldova (the Republic of)", - "alpha2": "MD", - "alpha3": "MDA", - "numeric": "498", - "altName": "Moldova" + 'name': 'Moldova (the Republic of)', + 'alpha2': 'MD', + 'alpha3': 'MDA', + 'numeric': '498', + 'altName': 'Moldova' }, { - "name": "Monaco", - "alpha2": "MC", - "alpha3": "MCO", - "numeric": "492" + 'name': 'Monaco', + 'alpha2': 'MC', + 'alpha3': 'MCO', + 'numeric': '492' }, { - "name": "Mongolia", - "alpha2": "MN", - "alpha3": "MNG", - "numeric": "496" + 'name': 'Mongolia', + 'alpha2': 'MN', + 'alpha3': 'MNG', + 'numeric': '496' }, { - "name": "Montenegro", - "alpha2": "ME", - "alpha3": "MNE", - "numeric": "499" + 'name': 'Montenegro', + 'alpha2': 'ME', + 'alpha3': 'MNE', + 'numeric': '499' }, { - "name": "Montserrat", - "alpha2": "MS", - "alpha3": "MSR", - "numeric": "500" + 'name': 'Montserrat', + 'alpha2': 'MS', + 'alpha3': 'MSR', + 'numeric': '500' }, { - "name": "Morocco", - "alpha2": "MA", - "alpha3": "MAR", - "numeric": "504" + 'name': 'Morocco', + 'alpha2': 'MA', + 'alpha3': 'MAR', + 'numeric': '504' }, { - "name": "Mozambique", - "alpha2": "MZ", - "alpha3": "MOZ", - "numeric": "508" + 'name': 'Mozambique', + 'alpha2': 'MZ', + 'alpha3': 'MOZ', + 'numeric': '508' }, { - "name": "Myanmar", - "alpha2": "MM", - "alpha3": "MMR", - "numeric": "104" + 'name': 'Myanmar', + 'alpha2': 'MM', + 'alpha3': 'MMR', + 'numeric': '104' }, { - "name": "Namibia", - "alpha2": "NA", - "alpha3": "NAM", - "numeric": "516" + 'name': 'Namibia', + 'alpha2': 'NA', + 'alpha3': 'NAM', + 'numeric': '516' }, { - "name": "Nauru", - "alpha2": "NR", - "alpha3": "NRU", - "numeric": "520" + 'name': 'Nauru', + 'alpha2': 'NR', + 'alpha3': 'NRU', + 'numeric': '520' }, { - "name": "Nepal", - "alpha2": "NP", - "alpha3": "NPL", - "numeric": "524" + 'name': 'Nepal', + 'alpha2': 'NP', + 'alpha3': 'NPL', + 'numeric': '524' }, { - "name": "Netherlands (the)", - "alpha2": "NL", - "alpha3": "NLD", - "numeric": "528", - "altName": "Netherlands" + 'name': 'Netherlands (the)', + 'alpha2': 'NL', + 'alpha3': 'NLD', + 'numeric': '528', + 'altName': 'Netherlands' }, { - "name": "New Caledonia", - "alpha2": "NC", - "alpha3": "NCL", - "numeric": "540" + 'name': 'New Caledonia', + 'alpha2': 'NC', + 'alpha3': 'NCL', + 'numeric': '540' }, { - "name": "New Zealand", - "alpha2": "NZ", - "alpha3": "NZL", - "numeric": "554" + 'name': 'New Zealand', + 'alpha2': 'NZ', + 'alpha3': 'NZL', + 'numeric': '554' }, { - "name": "Nicaragua", - "alpha2": "NI", - "alpha3": "NIC", - "numeric": "558" + 'name': 'Nicaragua', + 'alpha2': 'NI', + 'alpha3': 'NIC', + 'numeric': '558' }, { - "name": "Niger (the)", - "alpha2": "NE", - "alpha3": "NER", - "numeric": "562", - "altName": "Niger" + 'name': 'Niger (the)', + 'alpha2': 'NE', + 'alpha3': 'NER', + 'numeric': '562', + 'altName': 'Niger' }, { - "name": "Nigeria", - "alpha2": "NG", - "alpha3": "NGA", - "numeric": "566" + 'name': 'Nigeria', + 'alpha2': 'NG', + 'alpha3': 'NGA', + 'numeric': '566' }, { - "name": "Niue", - "alpha2": "NU", - "alpha3": "NIU", - "numeric": "570" + 'name': 'Niue', + 'alpha2': 'NU', + 'alpha3': 'NIU', + 'numeric': '570' }, { - "name": "Norfolk Island", - "alpha2": "NF", - "alpha3": "NFK", - "numeric": "574" + 'name': 'Norfolk Island', + 'alpha2': 'NF', + 'alpha3': 'NFK', + 'numeric': '574' }, { - "name": "Northern Mariana Islands (the)", - "alpha2": "MP", - "alpha3": "MNP", - "numeric": "580", - "altName": "Northern Mariana Islands" + 'name': 'Northern Mariana Islands (the)', + 'alpha2': 'MP', + 'alpha3': 'MNP', + 'numeric': '580', + 'altName': 'Northern Mariana Islands' }, { - "name": "Norway", - "alpha2": "NO", - "alpha3": "NOR", - "numeric": "578" + 'name': 'Norway', + 'alpha2': 'NO', + 'alpha3': 'NOR', + 'numeric': '578' }, { - "name": "Oman", - "alpha2": "OM", - "alpha3": "OMN", - "numeric": "512" + 'name': 'Oman', + 'alpha2': 'OM', + 'alpha3': 'OMN', + 'numeric': '512' }, { - "name": "Pakistan", - "alpha2": "PK", - "alpha3": "PAK", - "numeric": "586" + 'name': 'Pakistan', + 'alpha2': 'PK', + 'alpha3': 'PAK', + 'numeric': '586' }, { - "name": "Palau", - "alpha2": "PW", - "alpha3": "PLW", - "numeric": "585" + 'name': 'Palau', + 'alpha2': 'PW', + 'alpha3': 'PLW', + 'numeric': '585' }, { - "name": "Palestine, State of", - "alpha2": "PS", - "alpha3": "PSE", - "numeric": "275", - "altName": "Palestinian Territory", - "shortName": "Palestine" + 'name': 'Palestine, State of', + 'alpha2': 'PS', + 'alpha3': 'PSE', + 'numeric': '275', + 'altName': 'Palestinian Territory', + 'shortName': 'Palestine' }, { - "name": "Panama", - "alpha2": "PA", - "alpha3": "PAN", - "numeric": "591" + 'name': 'Panama', + 'alpha2': 'PA', + 'alpha3': 'PAN', + 'numeric': '591' }, { - "name": "Papua New Guinea", - "alpha2": "PG", - "alpha3": "PNG", - "numeric": "598" + 'name': 'Papua New Guinea', + 'alpha2': 'PG', + 'alpha3': 'PNG', + 'numeric': '598' }, { - "name": "Paraguay", - "alpha2": "PY", - "alpha3": "PRY", - "numeric": "600" + 'name': 'Paraguay', + 'alpha2': 'PY', + 'alpha3': 'PRY', + 'numeric': '600' }, { - "name": "Peru", - "alpha2": "PE", - "alpha3": "PER", - "numeric": "604" + 'name': 'Peru', + 'alpha2': 'PE', + 'alpha3': 'PER', + 'numeric': '604' }, { - "name": "Philippines (the)", - "alpha2": "PH", - "alpha3": "PHL", - "numeric": "608", - "altName": "Philippines" + 'name': 'Philippines (the)', + 'alpha2': 'PH', + 'alpha3': 'PHL', + 'numeric': '608', + 'altName': 'Philippines' }, { - "name": "Pitcairn", - "alpha2": "PN", - "alpha3": "PCN", - "numeric": "612" + 'name': 'Pitcairn', + 'alpha2': 'PN', + 'alpha3': 'PCN', + 'numeric': '612' }, { - "name": "Poland", - "alpha2": "PL", - "alpha3": "POL", - "numeric": "616" + 'name': 'Poland', + 'alpha2': 'PL', + 'alpha3': 'POL', + 'numeric': '616' }, { - "name": "Portugal", - "alpha2": "PT", - "alpha3": "PRT", - "numeric": "620" + 'name': 'Portugal', + 'alpha2': 'PT', + 'alpha3': 'PRT', + 'numeric': '620' }, { - "name": "Puerto Rico", - "alpha2": "PR", - "alpha3": "PRI", - "numeric": "630" + 'name': 'Puerto Rico', + 'alpha2': 'PR', + 'alpha3': 'PRI', + 'numeric': '630' }, { - "name": "Qatar", - "alpha2": "QA", - "alpha3": "QAT", - "numeric": "634" + 'name': 'Qatar', + 'alpha2': 'QA', + 'alpha3': 'QAT', + 'numeric': '634' }, { - "name": "Réunion", - "alpha2": "RE", - "alpha3": "REU", - "numeric": "638", - "shortName": "Reunion" + 'name': 'Réunion', + 'alpha2': 'RE', + 'alpha3': 'REU', + 'numeric': '638', + 'shortName': 'Reunion' }, { - "name": "Romania", - "alpha2": "RO", - "alpha3": "ROU", - "numeric": "642" + 'name': 'Romania', + 'alpha2': 'RO', + 'alpha3': 'ROU', + 'numeric': '642' }, { - "name": "Russian Federation (the)", - "alpha2": "RU", - "alpha3": "RUS", - "numeric": "643", - "altName": "Russian Federation", - "shortName": "Russia" + 'name': 'Russian Federation (the)', + 'alpha2': 'RU', + 'alpha3': 'RUS', + 'numeric': '643', + 'altName': 'Russian Federation', + 'shortName': 'Russia' }, { - "name": "Rwanda", - "alpha2": "RW", - "alpha3": "RWA", - "numeric": "646" + 'name': 'Rwanda', + 'alpha2': 'RW', + 'alpha3': 'RWA', + 'numeric': '646' }, { - "name": "Saint Barthélemy", - "alpha2": "BL", - "alpha3": "BLM", - "numeric": "652", - "altName": "Saint-Barthélemy", - "shortName": "Saint Barthelemy" + 'name': 'Saint Barthélemy', + 'alpha2': 'BL', + 'alpha3': 'BLM', + 'numeric': '652', + 'altName': 'Saint-Barthélemy', + 'shortName': 'Saint Barthelemy' }, { - "name": "Saint Helena, Ascension and Tristan da Cunha", - "alpha2": "SH", - "alpha3": "SHN", - "numeric": "654", - "altName": "Saint Helena" + 'name': 'Saint Helena, Ascension and Tristan da Cunha', + 'alpha2': 'SH', + 'alpha3': 'SHN', + 'numeric': '654', + 'altName': 'Saint Helena' }, { - "name": "Saint Kitts and Nevis", - "alpha2": "KN", - "alpha3": "KNA", - "numeric": "659" + 'name': 'Saint Kitts and Nevis', + 'alpha2': 'KN', + 'alpha3': 'KNA', + 'numeric': '659' }, { - "name": "Saint Lucia", - "alpha2": "LC", - "alpha3": "LCA", - "numeric": "662" + 'name': 'Saint Lucia', + 'alpha2': 'LC', + 'alpha3': 'LCA', + 'numeric': '662' }, { - "name": "Saint Martin (French part)", - "alpha2": "MF", - "alpha3": "MAF", - "numeric": "663", - "altName": "Saint-Martin (French part)", - "shortName": "Saint Martin" + 'name': 'Saint Martin (French part)', + 'alpha2': 'MF', + 'alpha3': 'MAF', + 'numeric': '663', + 'altName': 'Saint-Martin (French part)', + 'shortName': 'Saint Martin' }, { - "name": "Saint Pierre and Miquelon", - "alpha2": "PM", - "alpha3": "SPM", - "numeric": "666" + 'name': 'Saint Pierre and Miquelon', + 'alpha2': 'PM', + 'alpha3': 'SPM', + 'numeric': '666' }, { - "name": "Saint Vincent and the Grenadines", - "alpha2": "VC", - "alpha3": "VCT", - "numeric": "670", - "altName": "Saint Vincent and Grenadines" + 'name': 'Saint Vincent and the Grenadines', + 'alpha2': 'VC', + 'alpha3': 'VCT', + 'numeric': '670', + 'altName': 'Saint Vincent and Grenadines' }, { - "name": "Samoa", - "alpha2": "WS", - "alpha3": "WSM", - "numeric": "882" + 'name': 'Samoa', + 'alpha2': 'WS', + 'alpha3': 'WSM', + 'numeric': '882' }, { - "name": "San Marino", - "alpha2": "SM", - "alpha3": "SMR", - "numeric": "674" + 'name': 'San Marino', + 'alpha2': 'SM', + 'alpha3': 'SMR', + 'numeric': '674' }, { - "name": "Sao Tome and Principe", - "alpha2": "ST", - "alpha3": "STP", - "numeric": "678" + 'name': 'Sao Tome and Principe', + 'alpha2': 'ST', + 'alpha3': 'STP', + 'numeric': '678' }, { - "name": "Saudi Arabia", - "alpha2": "SA", - "alpha3": "SAU", - "numeric": "682" + 'name': 'Saudi Arabia', + 'alpha2': 'SA', + 'alpha3': 'SAU', + 'numeric': '682' }, { - "name": "Senegal", - "alpha2": "SN", - "alpha3": "SEN", - "numeric": "686" + 'name': 'Senegal', + 'alpha2': 'SN', + 'alpha3': 'SEN', + 'numeric': '686' }, { - "name": "Serbia", - "alpha2": "RS", - "alpha3": "SRB", - "numeric": "688" + 'name': 'Serbia', + 'alpha2': 'RS', + 'alpha3': 'SRB', + 'numeric': '688' }, { - "name": "Seychelles", - "alpha2": "SC", - "alpha3": "SYC", - "numeric": "690" + 'name': 'Seychelles', + 'alpha2': 'SC', + 'alpha3': 'SYC', + 'numeric': '690' }, { - "name": "Sierra Leone", - "alpha2": "SL", - "alpha3": "SLE", - "numeric": "694" + 'name': 'Sierra Leone', + 'alpha2': 'SL', + 'alpha3': 'SLE', + 'numeric': '694' }, { - "name": "Singapore", - "alpha2": "SG", - "alpha3": "SGP", - "numeric": "702" + 'name': 'Singapore', + 'alpha2': 'SG', + 'alpha3': 'SGP', + 'numeric': '702' }, { - "name": "Sint Maarten (Dutch part)", - "alpha2": "SX", - "alpha3": "SXM", - "numeric": "534", - "shortName": "Sint Maarten" + 'name': 'Sint Maarten (Dutch part)', + 'alpha2': 'SX', + 'alpha3': 'SXM', + 'numeric': '534', + 'shortName': 'Sint Maarten' }, { - "name": "Slovakia", - "alpha2": "SK", - "alpha3": "SVK", - "numeric": "703" + 'name': 'Slovakia', + 'alpha2': 'SK', + 'alpha3': 'SVK', + 'numeric': '703' }, { - "name": "Slovenia", - "alpha2": "SI", - "alpha3": "SVN", - "numeric": "705" + 'name': 'Slovenia', + 'alpha2': 'SI', + 'alpha3': 'SVN', + 'numeric': '705' }, { - "name": "Solomon Islands", - "alpha2": "SB", - "alpha3": "SLB", - "numeric": "090" + 'name': 'Solomon Islands', + 'alpha2': 'SB', + 'alpha3': 'SLB', + 'numeric': '090' }, { - "name": "Somalia", - "alpha2": "SO", - "alpha3": "SOM", - "numeric": "706" + 'name': 'Somalia', + 'alpha2': 'SO', + 'alpha3': 'SOM', + 'numeric': '706' }, { - "name": "South Africa", - "alpha2": "ZA", - "alpha3": "ZAF", - "numeric": "710" + 'name': 'South Africa', + 'alpha2': 'ZA', + 'alpha3': 'ZAF', + 'numeric': '710' }, { - "name": "South Georgia and the South Sandwich Islands", - "alpha2": "GS", - "alpha3": "SGS", - "numeric": "239" + 'name': 'South Georgia and the South Sandwich Islands', + 'alpha2': 'GS', + 'alpha3': 'SGS', + 'numeric': '239' }, { - "name": "South Sudan", - "alpha2": "SS", - "alpha3": "SSD", - "numeric": "728" + 'name': 'South Sudan', + 'alpha2': 'SS', + 'alpha3': 'SSD', + 'numeric': '728' }, { - "name": "Spain", - "alpha2": "ES", - "alpha3": "ESP", - "numeric": "724" + 'name': 'Spain', + 'alpha2': 'ES', + 'alpha3': 'ESP', + 'numeric': '724' }, { - "name": "Sri Lanka", - "alpha2": "LK", - "alpha3": "LKA", - "numeric": "144" + 'name': 'Sri Lanka', + 'alpha2': 'LK', + 'alpha3': 'LKA', + 'numeric': '144' }, { - "name": "Sudan (the)", - "alpha2": "SD", - "alpha3": "SDN", - "numeric": "729", - "altName": "Sudan" + 'name': 'Sudan (the)', + 'alpha2': 'SD', + 'alpha3': 'SDN', + 'numeric': '729', + 'altName': 'Sudan' }, { - "name": "Suriname", - "alpha2": "SR", - "alpha3": "SUR", - "numeric": "740" + 'name': 'Suriname', + 'alpha2': 'SR', + 'alpha3': 'SUR', + 'numeric': '740' }, { - "name": "Svalbard and Jan Mayen", - "alpha2": "SJ", - "alpha3": "SJM", - "numeric": "744", - "altName": "Svalbard and Jan Mayen Islands" + 'name': 'Svalbard and Jan Mayen', + 'alpha2': 'SJ', + 'alpha3': 'SJM', + 'numeric': '744', + 'altName': 'Svalbard and Jan Mayen Islands' }, { - "name": "Swaziland", - "alpha2": "SZ", - "alpha3": "SWZ", - "numeric": "748" + 'name': 'Swaziland', + 'alpha2': 'SZ', + 'alpha3': 'SWZ', + 'numeric': '748' }, { - "name": "Sweden", - "alpha2": "SE", - "alpha3": "SWE", - "numeric": "752" + 'name': 'Sweden', + 'alpha2': 'SE', + 'alpha3': 'SWE', + 'numeric': '752' }, { - "name": "Switzerland", - "alpha2": "CH", - "alpha3": "CHE", - "numeric": "756" + 'name': 'Switzerland', + 'alpha2': 'CH', + 'alpha3': 'CHE', + 'numeric': '756' }, { - "name": "Syrian Arab Republic", - "alpha2": "SY", - "alpha3": "SYR", - "numeric": "760", - "altName": "Syrian Arab Republic (Syria)", - "shortName": "Syria" + 'name': 'Syrian Arab Republic', + 'alpha2': 'SY', + 'alpha3': 'SYR', + 'numeric': '760', + 'altName': 'Syrian Arab Republic (Syria)', + 'shortName': 'Syria' }, { - "name": "Taiwan (Province of China)", - "alpha2": "TW", - "alpha3": "TWN", - "numeric": "158", - "altName": "Taiwan, Republic of China", - "shortName": "Taiwan" + 'name': 'Taiwan (Province of China)', + 'alpha2': 'TW', + 'alpha3': 'TWN', + 'numeric': '158', + 'altName': 'Taiwan, Republic of China', + 'shortName': 'Taiwan' }, { - "name": "Tajikistan", - "alpha2": "TJ", - "alpha3": "TJK", - "numeric": "762" + 'name': 'Tajikistan', + 'alpha2': 'TJ', + 'alpha3': 'TJK', + 'numeric': '762' }, { - "name": "Tanzania, United Republic of", - "alpha2": "TZ", - "alpha3": "TZA", - "numeric": "834", - "shortName": "Tanzania" + 'name': 'Tanzania, United Republic of', + 'alpha2': 'TZ', + 'alpha3': 'TZA', + 'numeric': '834', + 'shortName': 'Tanzania' }, { - "name": "Thailand", - "alpha2": "TH", - "alpha3": "THA", - "numeric": "764" + 'name': 'Thailand', + 'alpha2': 'TH', + 'alpha3': 'THA', + 'numeric': '764' }, { - "name": "Timor-Leste", - "alpha2": "TL", - "alpha3": "TLS", - "numeric": "626", - "shortName": "East Timor" + 'name': 'Timor-Leste', + 'alpha2': 'TL', + 'alpha3': 'TLS', + 'numeric': '626', + 'shortName': 'East Timor' }, { - "name": "Togo", - "alpha2": "TG", - "alpha3": "TGO", - "numeric": "768" + 'name': 'Togo', + 'alpha2': 'TG', + 'alpha3': 'TGO', + 'numeric': '768' }, { - "name": "Tokelau", - "alpha2": "TK", - "alpha3": "TKL", - "numeric": "772" + 'name': 'Tokelau', + 'alpha2': 'TK', + 'alpha3': 'TKL', + 'numeric': '772' }, { - "name": "Tonga", - "alpha2": "TO", - "alpha3": "TON", - "numeric": "776" + 'name': 'Tonga', + 'alpha2': 'TO', + 'alpha3': 'TON', + 'numeric': '776' }, { - "name": "Trinidad and Tobago", - "alpha2": "TT", - "alpha3": "TTO", - "numeric": "780" + 'name': 'Trinidad and Tobago', + 'alpha2': 'TT', + 'alpha3': 'TTO', + 'numeric': '780' }, { - "name": "Tunisia", - "alpha2": "TN", - "alpha3": "TUN", - "numeric": "788" + 'name': 'Tunisia', + 'alpha2': 'TN', + 'alpha3': 'TUN', + 'numeric': '788' }, { - "name": "Turkey", - "alpha2": "TR", - "alpha3": "TUR", - "numeric": "792" + 'name': 'Turkey', + 'alpha2': 'TR', + 'alpha3': 'TUR', + 'numeric': '792' }, { - "name": "Turkmenistan", - "alpha2": "TM", - "alpha3": "TKM", - "numeric": "795" + 'name': 'Turkmenistan', + 'alpha2': 'TM', + 'alpha3': 'TKM', + 'numeric': '795' }, { - "name": "Turks and Caicos Islands (the)", - "alpha2": "TC", - "alpha3": "TCA", - "numeric": "796", - "altName": "Turks and Caicos Islands" + 'name': 'Turks and Caicos Islands (the)', + 'alpha2': 'TC', + 'alpha3': 'TCA', + 'numeric': '796', + 'altName': 'Turks and Caicos Islands' }, { - "name": "Tuvalu", - "alpha2": "TV", - "alpha3": "TUV", - "numeric": "798" + 'name': 'Tuvalu', + 'alpha2': 'TV', + 'alpha3': 'TUV', + 'numeric': '798' }, { - "name": "Uganda", - "alpha2": "UG", - "alpha3": "UGA", - "numeric": "800" + 'name': 'Uganda', + 'alpha2': 'UG', + 'alpha3': 'UGA', + 'numeric': '800' }, { - "name": "Ukraine", - "alpha2": "UA", - "alpha3": "UKR", - "numeric": "804" + 'name': 'Ukraine', + 'alpha2': 'UA', + 'alpha3': 'UKR', + 'numeric': '804' }, { - "name": "United Arab Emirates (the)", - "alpha2": "AE", - "alpha3": "ARE", - "numeric": "784", - "altName": "United Arab Emirates" + 'name': 'United Arab Emirates (the)', + 'alpha2': 'AE', + 'alpha3': 'ARE', + 'numeric': '784', + 'altName': 'United Arab Emirates' }, { - "name": "United Kingdom of Great Britain and Northern Ireland (the)", - "alpha2": "GB", - "alpha3": "GBR", - "numeric": "826", - "altName": "United Kingdom" + 'name': 'United Kingdom of Great Britain and Northern Ireland (the)', + 'alpha2': 'GB', + 'alpha3': 'GBR', + 'numeric': '826', + 'altName': 'United Kingdom' }, { - "name": "United States Minor Outlying Islands (the)", - "alpha2": "UM", - "alpha3": "UMI", - "numeric": "581", - "altName": "US Minor Outlying Islands" + 'name': 'United States Minor Outlying Islands (the)', + 'alpha2': 'UM', + 'alpha3': 'UMI', + 'numeric': '581', + 'altName': 'US Minor Outlying Islands' }, { - "name": "United States of America (the)", - "alpha2": "US", - "alpha3": "USA", - "numeric": "840", - "altName": "United States of America", - "shortName": "United States" + 'name': 'United States of America (the)', + 'alpha2': 'US', + 'alpha3': 'USA', + 'numeric': '840', + 'altName': 'United States of America', + 'shortName': 'United States' }, { - "name": "Uruguay", - "alpha2": "UY", - "alpha3": "URY", - "numeric": "858" + 'name': 'Uruguay', + 'alpha2': 'UY', + 'alpha3': 'URY', + 'numeric': '858' }, { - "name": "Uzbekistan", - "alpha2": "UZ", - "alpha3": "UZB", - "numeric": "860" + 'name': 'Uzbekistan', + 'alpha2': 'UZ', + 'alpha3': 'UZB', + 'numeric': '860' }, { - "name": "Vanuatu", - "alpha2": "VU", - "alpha3": "VUT", - "numeric": "548" + 'name': 'Vanuatu', + 'alpha2': 'VU', + 'alpha3': 'VUT', + 'numeric': '548' }, { - "name": "Venezuela (Bolivarian Republic of)", - "alpha2": "VE", - "alpha3": "VEN", - "numeric": "862", - "altName": "Venezuela (Bolivarian Republic)", - "shortName": "Venezuela" + 'name': 'Venezuela (Bolivarian Republic of)', + 'alpha2': 'VE', + 'alpha3': 'VEN', + 'numeric': '862', + 'altName': 'Venezuela (Bolivarian Republic)', + 'shortName': 'Venezuela' }, { - "name": "Viet Nam", - "alpha2": "VN", - "alpha3": "VNM", - "numeric": "704", - "shortName": "Vietnam" + 'name': 'Viet Nam', + 'alpha2': 'VN', + 'alpha3': 'VNM', + 'numeric': '704', + 'shortName': 'Vietnam' }, { - "name": "Virgin Islands (British)", - "alpha2": "VG", - "alpha3": "VGB", - "numeric": "092", - "altName": "British Virgin Islands" + 'name': 'Virgin Islands (British)', + 'alpha2': 'VG', + 'alpha3': 'VGB', + 'numeric': '092', + 'altName': 'British Virgin Islands' }, { - "name": "Virgin Islands (U.S.)", - "alpha2": "VI", - "alpha3": "VIR", - "numeric": "850", - "altName": "Virgin Islands, US", - "shortName": "U.S. Virgin Islands" + 'name': 'Virgin Islands (U.S.)', + 'alpha2': 'VI', + 'alpha3': 'VIR', + 'numeric': '850', + 'altName': 'Virgin Islands, US', + 'shortName': 'U.S. Virgin Islands' }, { - "name": "Wallis and Futuna", - "alpha2": "WF", - "alpha3": "WLF", - "numeric": "876", - "altName": "Wallis and Futuna Islands" + 'name': 'Wallis and Futuna', + 'alpha2': 'WF', + 'alpha3': 'WLF', + 'numeric': '876', + 'altName': 'Wallis and Futuna Islands' }, { - "name": "Western Sahara*", - "alpha2": "EH", - "alpha3": "ESH", - "numeric": "732", - "altName": "Western Sahara" + 'name': 'Western Sahara*', + 'alpha2': 'EH', + 'alpha3': 'ESH', + 'numeric': '732', + 'altName': 'Western Sahara' }, { - "name": "Yemen", - "alpha2": "YE", - "alpha3": "YEM", - "numeric": "887" + 'name': 'Yemen', + 'alpha2': 'YE', + 'alpha3': 'YEM', + 'numeric': '887' }, { - "name": "Zambia", - "alpha2": "ZM", - "alpha3": "ZMB", - "numeric": "894" + 'name': 'Zambia', + 'alpha2': 'ZM', + 'alpha3': 'ZMB', + 'numeric': '894' }, { - "name": "Zimbabwe", - "alpha2": "ZW", - "alpha3": "ZWE", - "numeric": "716" + 'name': 'Zimbabwe', + 'alpha2': 'ZW', + 'alpha3': 'ZWE', + 'numeric': '716' } - ]; +]; diff --git a/packages/nodes-base/nodes/HelpScout/CustomerDescription.ts b/packages/nodes-base/nodes/HelpScout/CustomerDescription.ts index 9f909a0b6b..584108cf77 100644 --- a/packages/nodes-base/nodes/HelpScout/CustomerDescription.ts +++ b/packages/nodes-base/nodes/HelpScout/CustomerDescription.ts @@ -1,4 +1,4 @@ -import { INodeProperties } from "n8n-workflow"; +import { INodeProperties } from 'n8n-workflow'; export const customerOperations = [ { @@ -18,11 +18,6 @@ export const customerOperations = [ value: 'create', description: 'Create a new customer', }, - { - name: 'Properties', - value: 'properties', - description: 'Get customer property definitions', - }, { name: 'Get', value: 'get', @@ -33,6 +28,11 @@ export const customerOperations = [ value: 'getAll', description: 'Get all customers', }, + { + name: 'Properties', + value: 'properties', + description: 'Get customer property definitions', + }, { name: 'Update', value: 'update', @@ -92,16 +92,6 @@ export const customerFields = [ default: 1, description: `Customer’s age`, }, - { - displayName: 'Notes', - name: 'background', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: `Notes`, - }, { displayName: 'First Name', name: 'firstName', @@ -151,6 +141,16 @@ export const customerFields = [ default: '', description: 'Location of the customer.', }, + { + displayName: 'Notes', + name: 'background', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: `Notes`, + }, { displayName: 'Organization', name: 'organization', @@ -267,39 +267,39 @@ export const customerFields = [ type: 'options', options: [ { - name: 'aim', + name: 'AIM', value: 'aim', }, { - name: 'gtalk', + name: 'Google Talk', value: 'gtalk', }, { - name: 'icq', + name: 'ICQ', value: 'icq', }, { - name: 'msn', + name: 'MSN', value: 'msn', }, { - name: 'other', + name: 'Other', value: 'other', }, { - name: 'qq', + name: 'QQ', value: 'qq', }, { - name: 'skype', + name: 'Skype', value: 'skype', }, { - name: 'xmpp', + name: 'XMPP', value: 'xmpp', }, { - name: 'yahoo', + name: 'Yahoo', value: 'yahoo', }, ], @@ -584,13 +584,6 @@ export const customerFields = [ }, }, options: [ - { - displayName: 'Mailbox ID', - name: 'mailbox', - type: 'string', - default: '', - description: 'Filters customers from a specific mailbox', - }, { displayName: 'First Name', name: 'firstName', @@ -605,6 +598,13 @@ export const customerFields = [ default: '', description: 'Filters customers by last name', }, + { + displayName: 'Mailbox ID', + name: 'mailbox', + type: 'string', + default: '', + description: 'Filters customers from a specific mailbox', + }, { displayName: 'Modified Since', name: 'modifiedSince', @@ -733,16 +733,6 @@ export const customerFields = [ default: 1, description: `Customer’s age`, }, - { - displayName: 'Notes', - name: 'background', - type: 'string', - typeOptions: { - alwaysOpenEditWindow: true, - }, - default: '', - description: `Notes`, - }, { displayName: 'First Name', name: 'firstName', @@ -792,6 +782,16 @@ export const customerFields = [ default: '', description: 'Location of the customer.', }, + { + displayName: 'Notes', + name: 'background', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: `Notes`, + }, { displayName: 'Organization', name: 'organization', @@ -806,6 +806,6 @@ export const customerFields = [ default: '', description: 'URL of the customer’s photo', }, - ] + ], }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/HelpScout/CustomerInterface.ts b/packages/nodes-base/nodes/HelpScout/CustomerInterface.ts index 569a92c9aa..fe50a7cb3c 100644 --- a/packages/nodes-base/nodes/HelpScout/CustomerInterface.ts +++ b/packages/nodes-base/nodes/HelpScout/CustomerInterface.ts @@ -1,4 +1,4 @@ -import { IDataObject } from "n8n-workflow"; +import { IDataObject } from 'n8n-workflow'; export interface ICustomer { address?: IDataObject; diff --git a/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts b/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts index c7cda4eac6..fb04b669e6 100644 --- a/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts +++ b/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts @@ -1,9 +1,9 @@ import { OptionsWithUri } from 'request'; import { + IHookFunctions, IExecuteFunctions, IExecuteSingleFunctions, ILoadOptionsFunctions, - IHookFunctions, } from 'n8n-core'; import { IDataObject, @@ -43,7 +43,8 @@ export async function helpscoutApiRequest(this: IExecuteFunctions | IExecuteSing return `${error.path} ${error.message}`; }).join('-')}`); } - throw error; + + throw new Error(`HelpScout error response [${error.statusCode}]: ${error.message}`); } } diff --git a/packages/nodes-base/nodes/HelpScout/HelpScout.node.ts b/packages/nodes-base/nodes/HelpScout/HelpScout.node.ts index a339e6cfb4..190b276cb8 100644 --- a/packages/nodes-base/nodes/HelpScout/HelpScout.node.ts +++ b/packages/nodes-base/nodes/HelpScout/HelpScout.node.ts @@ -3,56 +3,57 @@ import { } from 'n8n-core'; import { + IBinaryKeyData, IDataObject, + ILoadOptionsFunctions, INodeExecutionData, + INodePropertyOptions, INodeTypeDescription, INodeType, - ILoadOptionsFunctions, - INodePropertyOptions, - IBinaryKeyData, } from 'n8n-workflow'; import { - helpscoutApiRequest, - helpscoutApiRequestAllItems, -} from './GenericFunctions'; + countriesCodes +} from './CountriesCodes'; import { - conversationOperations, conversationFields, + conversationOperations, } from './ConversationDescription'; import { - customerOperations, customerFields, + customerOperations, } from './CustomerDescription'; -import { - mailboxOperations, - mailboxFields, -} from './MailboxDescription'; - -import { - threadOperations, - threadFields, -} from './ThreadDescription'; - import { ICustomer, } from './CustomerInterface'; import { IConversation, - } from './ConversationInterface'; +} from './ConversationInterface'; - import { - IThread, +import { + helpscoutApiRequest, + helpscoutApiRequestAllItems, +} from './GenericFunctions'; + +import { + mailboxFields, + mailboxOperations, +} from './MailboxDescription'; + +import { + threadFields, + threadOperations, +} from './ThreadDescription'; + +import { IAttachment, - } from './ThreadInterface'; + IThread, +} from './ThreadInterface'; - import { - countriesCodes -} from './CountriesCodes'; export class HelpScout implements INodeType { description: INodeTypeDescription = { diff --git a/packages/nodes-base/nodes/HelpScout/HelpScoutTrigger.node.ts b/packages/nodes-base/nodes/HelpScout/HelpScoutTrigger.node.ts index ecb7e2a12a..63592aa14e 100644 --- a/packages/nodes-base/nodes/HelpScout/HelpScoutTrigger.node.ts +++ b/packages/nodes-base/nodes/HelpScout/HelpScoutTrigger.node.ts @@ -4,10 +4,10 @@ import { } from 'n8n-core'; import { - INodeTypeDescription, - INodeType, - IWebhookResponseData, IDataObject, + INodeType, + INodeTypeDescription, + IWebhookResponseData, } from 'n8n-workflow'; import { @@ -51,52 +51,53 @@ export class HelpScoutTrigger implements INodeType { name: 'events', type: 'multiOptions', options: [ + { - name: 'convo.agent.reply.created', - value: 'convo.agent.reply.created', - }, - { - name: 'convo.assigned', + name: 'Conversation - Assigned', value: 'convo.assigned', }, { - name: 'convo.created', + name: 'Conversation - Created', value: 'convo.created', }, { - name: 'convo.customer.reply.created', - value: 'convo.customer.reply.created', - }, - { - name: 'convo.deleted', + name: 'Conversation - Deleted', value: 'convo.deleted', }, { - name: 'convo.merged', + name: 'Conversation - Merged', value: 'convo.merged', }, { - name: 'convo.moved', + name: 'Conversation - Moved', value: 'convo.moved', }, { - name: 'convo.note.created', - value: 'convo.note.created', - }, - { - name: 'convo.status', + name: 'Conversation - Status', value: 'convo.status', }, { - name: 'convo.tags', + name: 'Conversation - Tags', value: 'convo.tags', }, { - name: 'customer.created', + name: 'Conversation Agent Reply - Created', + value: 'convo.agent.reply.created', + }, + { + name: 'Conversation Customer Reply - Created', + value: 'convo.customer.reply.created', + }, + { + name: 'Conversation Note - Created', + value: 'convo.note.created', + }, + { + name: 'Customer - Created', value: 'customer.created', }, { - name: 'satisfaction.ratings', + name: 'Rating - Received', value: 'satisfaction.ratings', }, ], diff --git a/packages/nodes-base/nodes/HelpScout/MailboxDescription.ts b/packages/nodes-base/nodes/HelpScout/MailboxDescription.ts index 923b01677f..27063c05ed 100644 --- a/packages/nodes-base/nodes/HelpScout/MailboxDescription.ts +++ b/packages/nodes-base/nodes/HelpScout/MailboxDescription.ts @@ -1,4 +1,4 @@ -import { INodeProperties } from "n8n-workflow"; +import { INodeProperties } from 'n8n-workflow'; export const mailboxOperations = [ { diff --git a/packages/nodes-base/nodes/HelpScout/ThreadDescription.ts b/packages/nodes-base/nodes/HelpScout/ThreadDescription.ts index 230b99ce56..4374e68453 100644 --- a/packages/nodes-base/nodes/HelpScout/ThreadDescription.ts +++ b/packages/nodes-base/nodes/HelpScout/ThreadDescription.ts @@ -1,4 +1,4 @@ -import { INodeProperties } from "n8n-workflow"; +import { INodeProperties } from 'n8n-workflow'; export const threadOperations = [ { @@ -128,6 +128,12 @@ export const threadFields = [ }, }, options: [ + { + displayName: 'Created At', + name: 'createdAt', + type: 'dateTime', + default: '', + }, { displayName: 'Customer Email', name: 'customerEmail', @@ -161,12 +167,6 @@ export const threadFields = [ default: false, description: 'When imported is set to true, no outgoing emails or notifications will be generated.', }, - { - displayName: 'Created At', - name: 'createdAt', - type: 'dateTime', - default: '', - }, ] }, { diff --git a/packages/nodes-base/nodes/HelpScout/ThreadInterface.ts b/packages/nodes-base/nodes/HelpScout/ThreadInterface.ts index b6cbbda213..e47a0a6747 100644 --- a/packages/nodes-base/nodes/HelpScout/ThreadInterface.ts +++ b/packages/nodes-base/nodes/HelpScout/ThreadInterface.ts @@ -1,4 +1,4 @@ -import { IDataObject } from "n8n-workflow"; +import { IDataObject } from 'n8n-workflow'; export interface IAttachment { fileName?: string; diff --git a/packages/nodes-base/nodes/HelpScout/helpScout.png b/packages/nodes-base/nodes/HelpScout/helpScout.png index a2be95ba7f734e4f120cc2b6624a660b4ddec7b2..1ebdaa8e5e17325e79cec03c48aa5321c6e5e2a9 100644 GIT binary patch delta 858 zcmV-g1Eu`_CFTZ@8Gi!+002ht61D&U0D(|UR7De#?*IS){r&#{j_wbV?kkn=3X$#r zknRAC?cM15CYA3PlkOpu?*NJI0EX@D^ZhWE@9FUT*yZ{;nC~^0?;Mlv@%H^no$o%H z@8j(I$lLfuobOzt@b&rraI5jr;rid|`%$0ov(5H>u<~oE@qdlD@^-E9w$Jvg%J#n0 z_qx&caupn$0007@NklceO@hvM9nLXkUCmgpvq zQc@4TUvUL=YNXIIXHXJ%Qo_pp+AB@YqbRmVIlV!}JAVr8aK@r3-i-6&mERH5$1)gD zwsK){U-}9t#X=O7in`Zx6;KYntOV$=8J;)2o|-{n#*{1#FUfo+iKkQ(6poZoWt|!P z*kn@_9ZoGJ?2fvSVr3zZBZ>tRPCqTrIO{|yCMbnGT!QJxwE1}UtPirFv-gw|be&v+ zaigI{H-CJ)tJIu`d#vn9XB>ec{3%zc1%gHUHXOR z1}Uv6CMe<>mwq(GNGYc%tLXG&3X1JHFD5Bn`rU%*$CQv@|DjmgZy&2cPQ@p@_*0{k z3jhC4c<~CJ{2-=7hM=fC=t z5_m-V$s2xo;02B+uV8u*CH$KjBqo_5${~uP1a~>*)0=7I=WZRQQ2N~{{fN@VDrHa< zg=Lg}9fc{_~9Pr-H-6b;aO*DlnGdO(YF1beZaGn+IThqWQx9>NA$M~)YU0_a kB4r+F;@Rv~ulnir7wLQ}BpcsNu>b%707*qoM6N<$f;s235&!@I literal 4862 zcmY*cc|4Tw*PfvfW6xMKVNk=^Mr9om8rxuuB_!JzjO=77M8>|a5i&z#ON*r>`@R*G zL?U|#*_E}I@2~g!d*A!{ocmn&xvz7b`#gUrI`f-CRO@`4ghl#| zT`xuWmRw;LWll4@M5%srZ+u+NJyS<+6t>{7^Rqvzr(fY>-66&B8a4>|BL~vnR1+!) z-)8-)S}mOc79IU02v@EC9gbayD_Y7{zL-sXQ}K=PGiWn!_0ip*wN9&`h1xRn%oGTa z>)|s=ZrN#{sHA8WL^3Ig%uPqRZUjZ#EXB~s88Z66yfLBCde`R5(D+gQ(;76CQ_xGm zzUq4kV{{U5t=P2B4|r!UMc~R0%X2&S3pGf!cIi8>r|`pVKB^%JM8{c)>A`4_sj*A+ z1Gc2M9ly~``^XocZOHmAOJxgl1@<|&$H^6*t9D2EZY|Ccn{I8z1KIhiq*XhseZ*za z`6;7zy|UDN^a1_!Ew|uT{F7fZb5hu^&?S$3d-%MFeO1${{Ojqm#{9=mU&acB%#-9c zC8i~#2ju5_jax>Zl%;j#w)nNzT(;cUP(IZh^XA6G^)ByyH{(Ye^K(BgKkU03sClqg zAE><(?47T;vKX|mNy?NweL#JX9?sbw9(|93zPd9%eN-`~%SOxV!Z)XLPb#&L<%aak zO}Cg`DWCHQg!ybmd4ifqwa2- z@ZHz_)#46Os`t4n`3@4xxdsXAPa>@F99Kdlh^10E?aIJmF z?uhAtZhEcHUS~yzWcFmwKi|5Evha{%L6<)O?YZol@fsv(mT!e`MO8~QTnL6Ab~d~V z?hSqy+#lR>lO{#EFK?!CO3G8f$EI_~Ws}cx;x1PpPmR-J=B;vFAz@d}b6no7a#Kcj zvKtqe=<(`CBVn0Om-e~xr5~w4S?~hii}$S>rObda`xP`>`bVawCX1fwVo);=eTBxW z9PuI=4)N)7Ccj=^-=x4Y4Cin}He+RDj?#)xS6QQ~iu}lKH2Vk8p;Grs+>ej=Z^1Mc zgG%m8%F~-Vq86bRs;~tbsY8e0piD)9smNK0ni>J z;PC|j@&Q2qU;scDc=lh61H%7uXaImHBH+|tj^(jG8QRBooc-6+kZJy9pvkoV#V6E{ z!*iv_j=@vg!Uq6gxQZg`Au%MtI%G<$F z38#tvn|{1ffjRs7dMZgv`}_M#`CpXs@OF|$Dk>^U%g9R0%1RzHBz*$ieeD7y-F<}r zBl5pGngk!bH__9V=;1DSqHAaGaobk~20JPA@A%I-eTk0$t>o_Wx2@v_rB8OGky0|! z|7stnqE4bphTcTNapejI*jI#6@Z%=+uW!E+F^9*6A zHyg=OA{nz+y5m-|M4t8HGECxUKvAz{o+YOP@n#G{OsUPG?J$O?!a@m-=O_H@R^MO5 z=v^Eaj)|(3d-#F6zopW2^s{#Ukvl5mN)t2%b2l{fSJ)#=6f7Bt7bY=fG4Y(GksIgj zaA+Q3d$HILKTq2k8LID(MzRX?Pf`oMix<)fm|Mm|bx zic48ZL1@?!bLZ1ECZySAQm-mR{CvEsFttZ{bqfy@O&*)NDY{`5Vdy><2RC=0dJE@a zo*SHc;hMda_P{^0cW`NAu4VwD3kZ%7JSAr=r!Lk}PZWsL_RH)JyBV_EIvTXQeLXha z$~k;+P(KGLLy|q6`Wiyt_ucz>{mWr##=a_%`$sGJh-5dt%ExY(V1elaA_;!I04i;7 z^Ww+)8jOoTUy9(7y~gcl2lmF#tj@-3BR`Qepf$X}LW&h#I&;YL`i5f6_x&mnNTN26 zgZJbDObep)~SAAM$Dl?XAHa6%WP1a&p`xQn_-Fh&sGhtW#%euLIf32c?X`Nlf zh61@hP8roCp{F=gRWh?qRTTH86Qi8c|4NK1E8A4rU?)r5uvP21uY@|MLG1fbQ`(BS z9?t&KQdB0xWVrhNhkmTsHZ_!nDj`F8&nx$Q5lfx8aJj>Ju|v0mFg)s9%%Ar_tF^M= zy>Jwe>hdiZjxx)~SxsgP`LNk}o{>)BsT#U7y*fzO2_`_v{Gm7=WB6U>4yDId3aZ?< zB<(#h)`ct8KwW5aFaVzPPT#gmq%BGPy!H#cpM`YV)-NKkFn0?sOVNQI_znNs&0iXhnX8>fZm(bacY{$nQ3L?dK)Kh86{BeVsW->@@ ztY;JhA@LnFF>!?F@PgY642xm0XPI_15i^7l?=t$mj`iZ(dpiPA89z9K4EGxx@*=J< z^ua=ftcoSwY!&D^1u5yKq3TqW5(rWjff$5`)`v_#>Rr`(ugGnHWEca(_t{?{^nzbi z#3ZK0SWj7BQHs^iLMAL{9y-`kK}?k2?8c)2YSDYdDs8Nr#EW=w*Z#u8E}D(01cuW7 z%@Tc4JUC;+@eI~EahL6@@&)v)A@}`CCOl6&Ik%I0)SEfdKf=OMndVzBguF1c=-QRF z93(Bc$(Gb?CN>u%lA(UO8frl4JZzI~A|Y~=;nhoErd19TX^nd2v8sb^={OEaYJDn| z=%l*B)SGsX9~;AXT|g!HDEl$4I*)OrKctjTn^a;e?=pqG87`(4zW|NxZfY{) zPm8fsXb&3dubyD)HEvo;1DtuIPe9^T>b_7ot+HTMO$?I))7uh6U_BW4O6TD6otfo${ST|ZgPsZg0yqluZ*ubzgnX{>_~@)) zhQ}FpWs3%`VH)m~tLeW#%=FHG%;)?R`vVLB?Z~MG3aqa8$;DGSo(T8lSxJsx(z4~T zNmugRJ~Kvj-WGH}hisaROj!&OCg(26XWX}KIHNOpG=3zOCa+XHWl1T&+U(Ak*lpil z($ui<;9Z~3+=Bti{ip;kk2K+~0A!V-QvOj@b9-4+*iRdg5{azj(j|uFMU=P&M^p1|^O~*60s9B!zW17j&bQj~CL$Xl zAxbob{9Q@15=jDfZJ*n8<@hWrh}12Xh}j(T)TAk%pOp*|A9m@}&)66u`{NIJO?wme zpRi_pun^kl6!0GB7`x^*EJfd#Q z1K69QEMyUP2g4!m;i`|-eOIXXs?OmyTZksbQzxXO+t@{)x)#ZQIinzHkQ0@uB;TLc z0X{cw^2|Q@-k(!v<&Ri;*m?o6y3)%_X>IZvmFI4!JF2(*nFmB5vK~5GeRh55_nFU_ zehvkBkaT-Xfh=M3JK1b)fvbjQh`!*SGehs`d9*2cr3-hUqNW%C)di+k<*L$uB;jgA zKveUh0T+ybI;GJNzina1)r=PpJMR<%QDNe&H~Q)7C2w=b-@MXTB5;D)i?>P{ZG%a5s|nVsrJpFLIE~!+_XG?kFnf83Aefd%rNMd{Z&vnh}Ojw3;!kh2L*qC61(#Lf2D9kIa2ujwIBpjI1Y`~G; zdLdy?Vd?g^zk$FZ&-Z~YG`3UbQ~V&braIJ)E1X9HntPp8ke-@zkjj6Wnqe9;m}uQ` z-k5*$yt@`l9yDY4VYtiHtZnc)D>l?j>bd30p;TA(DX4+V1J|(Do8Au?e!czq2{Igl zfG2n`=_u~#yIl1w=bX_Tj-6^G87qdW`D{d$D&q50^(ypjF+9x>rP%8lp9_V*ym@x z+!r=5qtl{mVvDCB)9V~WFs^@@#jO%-4FS_|;ClsEW%?=5#yz9(%JzEMHUrp8XSl?j zU{0(-VPhfLmT56k-G)ERE@<6*qq#@1+pjROef;vu+iQAgJveRAGP5!)ItRy6vG%S^ zQ~#^ohd8VOpcgD+&@gx^0`GY$@Gbu+8`$O75rgY>WK0_qkWYH^Egq81uc>+Q`d z&dA2CwQN=X1P?x4dGazKeWpRE9yOcs=(ty;0*yvgR^1xXfssy$)Z9}v{;?kp->Xf+ z#g5|3@Y}54BZ7L?&X{qD-%ogP{01#H&K5*lU{-Z{BDyDDN`>)d91 zP0oGR7w2fI%bDIf)a5Sr&e8M!hJnADTv-dtg6G-hjRkqYTvN%Zeh7r617CAyrMDQlIt9oo-N^2;F&Izt0?bAx@4mlTh1$#iDr6BekG)8N#A{ye zvQ@~-B1oY_f4_@CDu&l^RkEXQJWJ4%EKZ6QLqD=V-N8?59oSWwBp9;xgH#_Fr1{i6 zS9Ky1$1)*H_BHgue87LmPrPWuhh+~Vh5ou;GI_Wa#~!#YpmC=4?KGk|Oi!Jag6`TC)-L Vf)A~m9w$G3I@b&|pQ+p4{~u?LqWk~= diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index ecef7e0f5e..67490dd1cc 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -198,8 +198,8 @@ "@types/gm": "^1.18.2", "@types/imap-simple": "^4.2.0", "@types/jest": "^24.0.18", - "@types/lodash.set": "^4.3.6", - "@types/moment-timezone": "^0.5.12", + "@types/lodash.set": "^4.3.6", + "@types/moment-timezone": "^0.5.12", "@types/mongodb": "^3.3.6", "@types/node": "^10.10.1", "@types/nodemailer": "^4.6.5", @@ -225,8 +225,8 @@ "imap-simple": "^4.3.0", "lodash.get": "^4.4.2", "lodash.set": "^4.3.2", - "lodash.unset": "^4.5.2", - "moment-timezone": "0.5.28", + "lodash.unset": "^4.5.2", + "moment-timezone": "0.5.28", "mongodb": "^3.3.2", "mysql2": "^2.0.1", "n8n-core": "~0.20.0", From bc7dfd97de43814e8600331c0e6c27b7b55afc40 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 30 Mar 2020 00:43:26 +0200 Subject: [PATCH 038/165] :zap: Fix OneDrive-Node --- .../Microsoft/OneDrive/FileDescription.ts | 2 +- .../Microsoft/OneDrive/FolderDescriptiont.ts | 75 ------------------ .../OneDrive/MicrosoftOneDrive.node.ts | 61 +++++++------- .../nodes/Microsoft/OneDrive/oneDrive.png | Bin 4123 -> 1088 bytes 4 files changed, 29 insertions(+), 109 deletions(-) delete mode 100644 packages/nodes-base/nodes/Microsoft/OneDrive/FolderDescriptiont.ts diff --git a/packages/nodes-base/nodes/Microsoft/OneDrive/FileDescription.ts b/packages/nodes-base/nodes/Microsoft/OneDrive/FileDescription.ts index 32fe70a948..0e9bc26cba 100644 --- a/packages/nodes-base/nodes/Microsoft/OneDrive/FileDescription.ts +++ b/packages/nodes-base/nodes/Microsoft/OneDrive/FileDescription.ts @@ -1,4 +1,4 @@ -import { INodeProperties } from "n8n-workflow"; +import { INodeProperties } from 'n8n-workflow'; export const fileOperations = [ { diff --git a/packages/nodes-base/nodes/Microsoft/OneDrive/FolderDescriptiont.ts b/packages/nodes-base/nodes/Microsoft/OneDrive/FolderDescriptiont.ts deleted file mode 100644 index 83bd871143..0000000000 --- a/packages/nodes-base/nodes/Microsoft/OneDrive/FolderDescriptiont.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { INodeProperties } from "n8n-workflow"; - -export const folderOperations = [ - { - displayName: 'Operation', - name: 'operation', - type: 'options', - displayOptions: { - show: { - resource: [ - 'folder', - ], - }, - }, - options: [ - { - name: 'Get Children', - value: 'getChildren', - description: 'Get items inside a folder', - }, - { - name: 'Search', - value: 'search', - description: 'Search a folder', - }, - ], - default: 'getChildren', - description: 'The operation to perform.', - }, -] as INodeProperties[]; - -export const folderFields = [ - -/* -------------------------------------------------------------------------- */ -/* folder:getChildren */ -/* -------------------------------------------------------------------------- */ - { - displayName: 'Folder ID', - name: 'folderId', - type: 'string', - displayOptions: { - show: { - operation: [ - 'getChildren', - ], - resource: [ - 'folder', - ], - }, - }, - default: '', - description: 'Folder ID', - }, -/* -------------------------------------------------------------------------- */ -/* folder:search */ -/* -------------------------------------------------------------------------- */ - { - displayName: 'Query', - name: 'query', - type: 'string', - displayOptions: { - show: { - operation: [ - 'search', - ], - resource: [ - 'folder', - ], - }, - }, - default: '', - description: `The query text used to search for items. Values may be matched - across several fields including filename, metadata, and file content.`, - }, -] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.ts b/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.ts index cee6733bd5..199dd48798 100644 --- a/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.ts +++ b/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.ts @@ -1,16 +1,14 @@ import { - IExecuteFunctions, BINARY_ENCODING, + IExecuteFunctions, } from 'n8n-core'; import { + IBinaryKeyData, IDataObject, INodeExecutionData, - INodeTypeDescription, INodeType, - ILoadOptionsFunctions, - INodePropertyOptions, - IBinaryKeyData, + INodeTypeDescription, } from 'n8n-workflow'; import { @@ -19,14 +17,14 @@ import { } from './GenericFunctions'; import { - fileOperations, fileFields, + fileOperations, } from './FileDescription'; import { + folderFields, folderOperations, - folderFields -} from './FolderDescriptiont'; +} from './FolderDescription'; export class MicrosoftOneDrive implements INodeType { description: INodeTypeDescription = { @@ -113,10 +111,17 @@ export class MicrosoftOneDrive implements INodeType { const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i) as string; responseData = await microsoftApiRequest.call(this, 'GET', `/drive/items/${fileId}`); + const fileName = responseData.name; + if (responseData.file === undefined) { throw new Error('The ID you provided does not belong to a file.'); } + let mimeType: string | undefined; + if (responseData.file.mimeType) { + mimeType = responseData.file.mimeType; + } + responseData = await microsoftApiRequest.call(this, 'GET', `/drive/items/${fileId}/content`, {}, {}, undefined, {}, { encoding: null, resolveWithFullResponse: true }); const newItem: INodeExecutionData = { @@ -124,8 +129,7 @@ export class MicrosoftOneDrive implements INodeType { binary: {}, }; - let mimeType: string | undefined; - if (responseData.headers['content-type']) { + if (mimeType === undefined && responseData.headers['content-type']) { mimeType = responseData.headers['content-type']; } @@ -140,7 +144,7 @@ export class MicrosoftOneDrive implements INodeType { const data = Buffer.from(responseData.body); - items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(data as unknown as Buffer, undefined, mimeType); + items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(data as unknown as Buffer, fileName, mimeType); } //https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_get?view=odsp-graph-online if (operation === 'get') { @@ -151,17 +155,17 @@ export class MicrosoftOneDrive implements INodeType { //https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_search?view=odsp-graph-online if (operation === 'search') { const query = this.getNodeParameter('query', i) as string; - responseData = await microsoftApiRequestAllItems.call(this, 'value', 'GET', `/drive/root/search(q='{${query}}')`); + responseData = await microsoftApiRequestAllItems.call(this, 'value', 'GET', `/drive/root/search(q='${query}')`); responseData = responseData.filter((item: IDataObject) => item.file); - returnData.push(responseData as IDataObject); + returnData.push.apply(returnData, responseData as IDataObject[]); } //https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_put_content?view=odsp-graph-online#example-upload-a-new-file if (operation === 'upload') { const parentId = this.getNodeParameter('parentId', i) as string; - const binaryData = this.getNodeParameter('binaryData', 0) as boolean; - let fileName = this.getNodeParameter('fileName', 0) as string; + const isBinaryData = this.getNodeParameter('binaryData', i) as boolean; + const fileName = this.getNodeParameter('fileName', i) as string; - if (binaryData) { + if (isBinaryData) { const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0) as string; if (items[i].binary === undefined) { @@ -174,20 +178,16 @@ export class MicrosoftOneDrive implements INodeType { const binaryData = (items[i].binary as IBinaryKeyData)[binaryPropertyName]; - if (fileName !== '') { - fileName = `${fileName}.${binaryData.fileExtension}`; - } - const body = Buffer.from(binaryData.data, BINARY_ENCODING); - responseData = await microsoftApiRequest.call(this, 'PUT', `/drive/items/${parentId}:/${fileName || binaryData.fileName}:/content`, body , {}, undefined, { 'Content-Type': binaryData.mimeType, 'Content-length': body.length } ); - returnData.push(responseData as IDataObject); + responseData = await microsoftApiRequest.call(this, 'PUT', `/drive/items/${parentId}:/${fileName || binaryData.fileName}:/content`, body, {}, undefined, { 'Content-Type': binaryData.mimeType, 'Content-length': body.length }, {} ); + returnData.push(JSON.parse(responseData) as IDataObject); } else { - const body = Buffer.from(this.getNodeParameter('fileContent', i) as string, 'utf8'); + const body = this.getNodeParameter('fileContent', i) as string; if (fileName === '') { - throw new Error('File name must be defined'); + throw new Error('File name must be set!'); } - responseData = await microsoftApiRequest.call(this, 'PUT', `/drive/items/${parentId}:/${fileName}.txt:/content`, body , {}, undefined, { 'Content-Type': 'text/plain' } ); + responseData = await microsoftApiRequest.call(this, 'PUT', `/drive/items/${parentId}:/${fileName}:/content`, body , {}, undefined, { 'Content-Type': 'text/plain' } ); returnData.push(responseData as IDataObject); } } @@ -197,14 +197,14 @@ export class MicrosoftOneDrive implements INodeType { if (operation === 'getChildren') { const folderId = this.getNodeParameter('folderId', i) as string; responseData = await microsoftApiRequestAllItems.call(this, 'value', 'GET', `/drive/items/${folderId}/children`); - returnData.push(responseData as IDataObject); + returnData.push.apply(returnData, responseData as IDataObject[]); } //https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_search?view=odsp-graph-online if (operation === 'search') { const query = this.getNodeParameter('query', i) as string; - responseData = await microsoftApiRequestAllItems.call(this, 'value', 'GET', `/drive/root/search(q='{${query}}')`); + responseData = await microsoftApiRequestAllItems.call(this, 'value', 'GET', `/drive/root/search(q='${query}')`); responseData = responseData.filter((item: IDataObject) => item.folder); - returnData.push(responseData as IDataObject); + returnData.push.apply(returnData, responseData as IDataObject[]); } } } @@ -212,11 +212,6 @@ export class MicrosoftOneDrive implements INodeType { // For file downloads the files get attached to the existing items return this.prepareOutputData(items); } else { - if (Array.isArray(responseData)) { - returnData.push.apply(returnData, responseData as IDataObject[]); - } else if (responseData !== undefined) { - returnData.push(responseData as IDataObject); - } return [this.helpers.returnJsonArray(returnData)]; } } diff --git a/packages/nodes-base/nodes/Microsoft/OneDrive/oneDrive.png b/packages/nodes-base/nodes/Microsoft/OneDrive/oneDrive.png index b33d98a8bb25e5b516df50c7769df1e617a38509..44f3f594ffe7233c976100cd21fb6b472b0497b0 100644 GIT binary patch delta 1078 zcmbQOaDZciWIZzj1H;bqO!I-%q5z)|S0K&cw~5nhBLp#cYz8veeKv7=ZUiC@pH1A} zn>oBT0uiUj28bFE8z=%)02b%)+5lFtiQfxh4!bv;y@?BAxaS6L&kgkqZrdQHaC*X3 zY~=La$>qP7({DGY_ZCi{?OcI}xB?Dv1?=bY-^Uen+^)KC8PJv5B|(0{46MKY{9u2j z_~n_L=*3W}N2_M}yPw=#lV~q+r=v;z{n179CR^!o-`HzbloPgW({=_1W+hJ-$B>A_ zZ>PO1TjU^6?`|LLOwz5u?((vs`Tt8ubyfx?A8ILwznZduJ zi}lL3dncYLE}tPZfp{`V|M}*RRStmC$gVeQ&?pV$00j4_9(nQ+@b8xxMlD_2hHRosj-|SKAD~}`ETZwlb$A1pPcS7c8cn^Yqh>H=|weLZo$vfhxB`A z|2wy@U-M}FTeBsNu2uX8PS)>#5mqqil~fbwt*;w9lzHE97dW4^?sE#>G1Xaj*~xZ> z(+>P`X8ny<_sA!nN{`sTaQ_>FsSlYgEAQ6#hAnuxH#Y6}lRd6Yj?)56PJReI@Ne(k zhLvK9-kJ?T2{So&pZpy!W4UAdeRjU16K)>(lu%wPvZSSV_JxpV4QoWitmM63{hT84 zzBS~G`4Y{dAMegu%&wdI;mVPfCmNr!?`MneUt6tQ(b{rG#3KKGh~$hnqGFdS=G;1} zyI<<4#PoU{Znc~}Gye9(eJgPESiEAx>~}|!*Ssm7YVBO4p|(Uq$+qN|P8x%QXsqDz zumBec*?<{Iw-(ItURj@b-^a;#zhJy&t?kCSj=P`!{>L@H)R`^KPs8oRvXx7o?7VRC z+^IdqM~|<1+3V)WyK>3OyBt{uZ=5KUo*2=-F1lQ1`K)7>!TRnp=FfBYq>AimJ$GzJ j!cs#yMO6^^udtu7^YYvI>)HK}gNg)CS3j3^P6)C&HyrI4p6*(I@005xU(N;IUh=!L=N__E5 zqT8>!2tZ$BEjXZhm}BE&6YGT1an{oV2wh-O01=QL0J@}H7y!ryAo?2v000?eEB~?$3qk+Emt~!^+{lYS=A~`v3jk0s zTsjbtoy&5eW{EaM`JwcX@^?Mm#q1qD9h}61+`TTV0E$8K7tr0w&mI!w?&jeuAEd3AJ%`{c-sadXildbJz z4t@P&7y0q|k%)h80U+QSeMp!Z-efJ?s~c}G7*`z1J^gYFvRj&x<->M% zG&qrn8NofSz^9jwtT`yWP_m@kbd~coapHGiv9ro|*75Q3+&%-er>BJYjGkaX;j?1j z*JOEX_5L*ZSnEAszz-)@-QM`e#>x?1_x(u%N^y}dHFMJ+U#p&<9$UY6WVj>c1~pf7 zh@bxUJjKpN2BUJFRW+*ovFS#2SSI~upk%(B+Ix){&B>ri!4z3EOQ$wKx3Ab_sF%w3 z`wDmqLv*-WlIl`O9c_ut=LGH#o%QrCravSCE=5|b-&l;@8 zo8K}#Vqsq|SyaV>TrnwoL}e9;oV)1_Ws0?XOV`P9c7}(jTpgu*kw2rB44DZ&4`)Jc zI0oy}hSp@*&DY2XiF7o~Dm7WnOyO>5C*#8J%6*mf7>{Gs(Hpm5Ln(Jm7My8RW8Owu zRyvdRh4H|AlI(k!gbli>_!9=6k$kFYy$nVWQ6YkH%3yab6z7(6wa(lX&jXWN)x$OI>38Gl*^MQ zG#~WD%AQgKi{{|8bv4){q9ERSWNw7nemWtrq$8XM!=$+OlB4Y^eRBoxAmK)OlR?ki z$mgguEe>%za)w*tQSoG4tSF%ZPKwQNW-2}b^4v2N!Pjhumk zfU;a(b&N5bYz0`*WzzY*aCOj)23rJ$`@w=_DHsN!@q{lcH|(4P$@rI|UYt)SboWr& zhUjPAIJisY?}JF zm+Qun-xxT}=y~JFY^Eq_suSzF(-h8IjrUzki@t@|>GvPbDR9?Kcj6UDyc+KQ%s2I+ z^xfyK{(Z-fAr$v5rw89E`oaA@G_eYGqFb}gsDC?fk2&dIoPnyCP9P9%3tDch&G}S9!C*GcjhrL8Rl|<8EqHG4gu8KGo^_>WCSx zc>iPD8HFY;7N}boNaFjad^YxH{Yis$ywqkzx!&vyDFQZp;Fk#J7NpN7@(q{9$qcad zRW)&87WPkt^U4!(oni!dwi3IpqF107@y8MY z%})88*_kxV50-%wq3e+M0+H2kCKtpVJ%Lq9s~(%GdWf1tkc_Z)oY9wIL>6n*r*@UR zk1Ar7^`fYojxR0I)ojc&rnZ$L-?k&1?04$p2nqB24EHKc2?lmn{-5pSLvp1p1egR0 zLziNsZKd*ec$KUQXkGU4o3$16sJXEM zdd2!17DFU82j)VjsS>2@Vb2Y8tXa4b&1Ac7tSVnABAon4jl)ta)52st79gdOf=s{Z zwLPwwz`h9|ipfS|oB#3+rMsS~LHuo+ky&J$=;LIaSTvHI`?%cma-9?<# zXY4O|p8$6El||Fc#C@yE9`KHtX>#~A=pp|Oo^Eqs<{qt8Y?*bg#g?DQFCli!(lG%J z3-+Y#zNLI%SI&nhFMq_eO>(%B{?Y;(Yi~7tUqh~h`Wb~22Y+}#&-&-~Eu;-u!$#I* zQwK&m*qsvgq--(3tz-?jJw!CvvMEnGVFfI>HY{Yu)+_EVXb;MwGBwQ$Cn zcDtiRvymmzFb$Ev+@%rJmGXS7^jNT8^c$^PjKRcx&jnHIlCvGQ>pyQg>|tPe^DR>0 z_oNcIK%XgmgP!!= zwqoDl5XPo5&b)Z03G31LY76Zgczf4_nwWCG*#=1b>9)0j%E5}dW;18>n-erkne@7V zb`Z5&`-120I6vid3IK2=f_LEy{Ii3fb5ZEar2q=2v&G?=VBBzRZC}q{OAq;=8aTtc zA$yTw%RJo{L8a3whoV5sT_&YFN!X3?ATpBTD#+&2wY?)nzg~Lqm+>9ftNAyXyoaRU zil^_D>y?VVqflbsE}|MiMl}T3@j5Rn#*#Y*WXHROC{{_oSj6S_KoJ3iwigNkRNS=p ztFDHKR?<|Nc%)|Dk(m*7$g=H1yuHSUHLAVEk&=+WznU`Ih1y)g-RXX}J>%U)L4oi- zC3Y*b-*^m#%Uhu5+4Qa|3|o#(-PY-nOu;k~zdl|?-Z9g~tl zGA8MCDYPGgW~N`Kd(V1#b_tB&M~z_pt}ni)U?}LxyqIpzjd%uSA;)b}%ufV4GOwVx zuod(#vgDdmi+(5*Jt?L#?Xe6KZ@xES|MY{_#;-JkI?C6iByXn7k1e(=$MJ@uK~G+E zYp6%<4h6qOt#k6ln@%tP@?)kIo=9o;y3cmt6)YVQ_yi-Tq;}SxleQVMe(a9juD{)$ zIj#^gv?y)VN#bpXN@hGKnc%*`_~RiFlT1cb@|P31az_T%AN1;J^f@G?j$TtH*ZjJ8P`8( zeZpth00i$Ao&BmipmTxRFH*O|BD6qj`pdl94sU#kWS%I5f**PvA{^#vlsBu}cRTW? zixeQB#E-<`c-622UfHldGF{$Bo_jSkJn2Jc*P7m7$IP=H=&5~`9e25%hf?rL^}sj4 zIIb&Jo<@Pg2|ENQ zsA&*c0~20yv>qK2n&(8ablrDLpZY6Hil~mMiqR0+a)^^KOHaefr;=l4i!w`b@3Z%Y z^~F|4>8T+^71`GxFh-PSAjBsw_+Z}a_PCW zRU8Fz=9dbm-sAfST4HYG Date: Sun, 29 Mar 2020 22:08:00 -0400 Subject: [PATCH 039/165] :zap: Improved getall operations --- .../HelpScout/ConversationDescription.ts | 40 +++++++++++++++++ .../nodes/HelpScout/CustomerDescription.ts | 40 +++++++++++++++++ .../nodes/HelpScout/GenericFunctions.ts | 5 ++- .../nodes/HelpScout/HelpScout.node.ts | 36 ++++++++++++++-- .../nodes/HelpScout/MailboxDescription.ts | 43 +++++++++++++++++++ .../nodes/HelpScout/ThreadDescription.ts | 40 +++++++++++++++++ 6 files changed, 199 insertions(+), 5 deletions(-) diff --git a/packages/nodes-base/nodes/HelpScout/ConversationDescription.ts b/packages/nodes-base/nodes/HelpScout/ConversationDescription.ts index 5d882ac80f..30b46368a7 100644 --- a/packages/nodes-base/nodes/HelpScout/ConversationDescription.ts +++ b/packages/nodes-base/nodes/HelpScout/ConversationDescription.ts @@ -411,6 +411,46 @@ export const conversationFields = [ /* -------------------------------------------------------------------------- */ /* conversation:getAll */ /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'conversation', + ], + }, + }, + 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: [ + 'conversation', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + }, + default: 50, + description: 'How many results to return.', + }, { displayName: 'Options', name: 'options', diff --git a/packages/nodes-base/nodes/HelpScout/CustomerDescription.ts b/packages/nodes-base/nodes/HelpScout/CustomerDescription.ts index 584108cf77..5f353d0bd5 100644 --- a/packages/nodes-base/nodes/HelpScout/CustomerDescription.ts +++ b/packages/nodes-base/nodes/HelpScout/CustomerDescription.ts @@ -567,6 +567,46 @@ export const customerFields = [ /* -------------------------------------------------------------------------- */ /* customer:getAll */ /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'customer', + ], + }, + }, + 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: [ + 'customer', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + }, + default: 50, + description: 'How many results to return.', + }, { displayName: 'Options', name: 'options', diff --git a/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts b/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts index fb04b669e6..ab933856c9 100644 --- a/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts +++ b/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts @@ -12,6 +12,7 @@ import { import { get, } from 'lodash'; +import { queryResult } from 'pg-promise'; export async function helpscoutApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IHookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any let options: OptionsWithUri = { @@ -53,13 +54,15 @@ export async function helpscoutApiRequestAllItems(this: IExecuteFunctions | ILoa const returnData: IDataObject[] = []; let responseData; - query.size = 50; let uri; do { responseData = await helpscoutApiRequest.call(this, method, endpoint, body, query, uri); uri = get(responseData, '_links.next.href'); returnData.push.apply(returnData, get(responseData, propertyName)); + if (query.limit && query.limit <= returnData.length) { + return returnData; + } } while ( responseData['_links'] !== undefined && responseData['_links'].next !== undefined && diff --git a/packages/nodes-base/nodes/HelpScout/HelpScout.node.ts b/packages/nodes-base/nodes/HelpScout/HelpScout.node.ts index 190b276cb8..ce1f02d648 100644 --- a/packages/nodes-base/nodes/HelpScout/HelpScout.node.ts +++ b/packages/nodes-base/nodes/HelpScout/HelpScout.node.ts @@ -241,9 +241,16 @@ export class HelpScout implements INodeType { } //https://developer.helpscout.com/mailbox-api/endpoints/conversations/list if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; const options = this.getNodeParameter('options', i) as IDataObject; Object.assign(qs, options); - responseData = await helpscoutApiRequestAllItems.call(this, '_embedded.conversations', 'GET', '/v2/conversations', {}, qs); + if (returnAll) { + responseData = await helpscoutApiRequestAllItems.call(this, '_embedded.conversations', 'GET', '/v2/conversations', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await helpscoutApiRequestAllItems.call(this, '_embedded.conversations', 'GET', '/v2/conversations', {}, qs); + responseData = responseData.splice(0, qs.limit); + } } } if (resource === 'customer') { @@ -307,9 +314,16 @@ export class HelpScout implements INodeType { } //https://developer.helpscout.com/mailbox-api/endpoints/customers/list if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; const options = this.getNodeParameter('options', i) as IDataObject; Object.assign(qs, options); - responseData = await helpscoutApiRequestAllItems.call(this, '_embedded.customers', 'GET', '/v2/customers', {}, qs); + if (returnAll) { + responseData = await helpscoutApiRequestAllItems.call(this, '_embedded.customers', 'GET', '/v2/customers', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await helpscoutApiRequestAllItems.call(this, '_embedded.customers', 'GET', '/v2/customers', {}, qs); + responseData = responseData.splice(0, qs.limit); + } } //https://developer.helpscout.com/mailbox-api/endpoints/customers/overwrite/ if (operation === 'update') { @@ -335,7 +349,14 @@ export class HelpScout implements INodeType { } //https://developer.helpscout.com/mailbox-api/endpoints/mailboxes/list if (operation === 'getAll') { - responseData = await helpscoutApiRequestAllItems.call(this, '_embedded.mailboxes', 'GET', '/v2/mailboxes', {}, qs); + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + if (returnAll) { + responseData = await helpscoutApiRequestAllItems.call(this, '_embedded.mailboxes', 'GET', '/v2/mailboxes', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await helpscoutApiRequestAllItems.call(this, '_embedded.mailboxes', 'GET', '/v2/mailboxes', {}, qs); + responseData = responseData.splice(0, qs.limit); + } } } if (resource === 'thread') { @@ -396,8 +417,15 @@ export class HelpScout implements INodeType { } //https://developer.helpscout.com/mailbox-api/endpoints/conversations/threads/list if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; const conversationId = this.getNodeParameter('conversationId', i) as string; - responseData = await helpscoutApiRequestAllItems.call(this, '_embedded.threads', 'GET', `/v2/conversations/${conversationId}/threads`); + if (returnAll) { + responseData = await helpscoutApiRequestAllItems.call(this, '_embedded.threads', 'GET', `/v2/conversations/${conversationId}/threads`); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await helpscoutApiRequestAllItems.call(this, '_embedded.threads', 'GET', `/v2/conversations/${conversationId}/threads`, {}, qs); + responseData = responseData.splice(0, qs.limit); + } } } if (Array.isArray(responseData)) { diff --git a/packages/nodes-base/nodes/HelpScout/MailboxDescription.ts b/packages/nodes-base/nodes/HelpScout/MailboxDescription.ts index 27063c05ed..d9aff61100 100644 --- a/packages/nodes-base/nodes/HelpScout/MailboxDescription.ts +++ b/packages/nodes-base/nodes/HelpScout/MailboxDescription.ts @@ -51,4 +51,47 @@ export const mailboxFields = [ }, }, }, +/* -------------------------------------------------------------------------- */ +/* mailbox:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'mailbox', + ], + }, + }, + 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: [ + 'mailbox', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + }, + default: 50, + description: 'How many results to return.', + }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/HelpScout/ThreadDescription.ts b/packages/nodes-base/nodes/HelpScout/ThreadDescription.ts index 4374e68453..eb2039745c 100644 --- a/packages/nodes-base/nodes/HelpScout/ThreadDescription.ts +++ b/packages/nodes-base/nodes/HelpScout/ThreadDescription.ts @@ -254,4 +254,44 @@ export const threadFields = [ }, description: 'conversation ID', }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'thread', + ], + }, + }, + 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: [ + 'thread', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + }, + default: 50, + description: 'How many results to return.', + }, ] as INodeProperties[]; From 79fda65e7ed56c3bc3997aff5bc0c3777d4fb844 Mon Sep 17 00:00:00 2001 From: ricardo Date: Sun, 29 Mar 2020 22:39:43 -0400 Subject: [PATCH 040/165] :zap: Added create folder operation --- .../Microsoft/OneDrive/FolderDescription.ts | 101 ++++++++++++++++++ .../Microsoft/OneDrive/GenericFunctions.ts | 1 + .../OneDrive/MicrosoftOneDrive.node.ts | 10 ++ 3 files changed, 112 insertions(+) create mode 100644 packages/nodes-base/nodes/Microsoft/OneDrive/FolderDescription.ts diff --git a/packages/nodes-base/nodes/Microsoft/OneDrive/FolderDescription.ts b/packages/nodes-base/nodes/Microsoft/OneDrive/FolderDescription.ts new file mode 100644 index 0000000000..8f31be22a5 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/OneDrive/FolderDescription.ts @@ -0,0 +1,101 @@ +import { INodeProperties } from "n8n-workflow"; + +export const folderOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'folder', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a folder', + }, + { + name: 'Get Children', + value: 'getChildren', + description: 'Get items inside a folder', + }, + { + name: 'Search', + value: 'search', + description: 'Search a folder', + }, + ], + default: 'getChildren', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const folderFields = [ + +/* -------------------------------------------------------------------------- */ +/* folder:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Name', + name: 'name', + required: true, + type: 'string', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'folder', + ], + }, + }, + default: '', + description: `Folder's name`, + }, +/* -------------------------------------------------------------------------- */ +/* folder:getChildren */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Folder ID', + name: 'folderId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'getChildren', + ], + resource: [ + 'folder', + ], + }, + }, + default: '', + description: 'Folder ID', + }, +/* -------------------------------------------------------------------------- */ +/* folder:search */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Query', + name: 'query', + type: 'string', + displayOptions: { + show: { + operation: [ + 'search', + ], + resource: [ + 'folder', + ], + }, + }, + default: '', + description: `The query text used to search for items. Values may be matched + across several fields including filename, metadata, and file content.`, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Microsoft/OneDrive/GenericFunctions.ts b/packages/nodes-base/nodes/Microsoft/OneDrive/GenericFunctions.ts index 9330437f77..1bd1bee202 100644 --- a/packages/nodes-base/nodes/Microsoft/OneDrive/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Microsoft/OneDrive/GenericFunctions.ts @@ -33,6 +33,7 @@ export async function microsoftApiRequest(this: IExecuteFunctions | IExecuteSing if (Object.keys(body).length === 0) { delete options.body; } + //@ts-ignore return await this.helpers.requestOAuth.call(this, 'microsoftOneDriveOAuth2Api', options); } catch (error) { diff --git a/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.ts b/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.ts index 199dd48798..2af523cbc0 100644 --- a/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.ts +++ b/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.ts @@ -193,6 +193,16 @@ export class MicrosoftOneDrive implements INodeType { } } if (resource === 'folder') { + //https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_post_children?view=odsp-graph-online + if (operation === 'create') { + const name = this.getNodeParameter('name', i) as string; + const body: IDataObject = { + name, + folder: {}, + }; + responseData = await microsoftApiRequest.call(this, 'POST', '/drive/root/children', body); + returnData.push(responseData); + } //https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_list_children?view=odsp-graph-online if (operation === 'getChildren') { const folderId = this.getNodeParameter('folderId', i) as string; From e9f71f1b8e1713ff09ed5d36768d5d24d6917104 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 30 Mar 2020 08:19:59 +0200 Subject: [PATCH 041/165] :zap: Add back missing file --- .../Microsoft/OneDrive/FolderDescription.ts | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 packages/nodes-base/nodes/Microsoft/OneDrive/FolderDescription.ts diff --git a/packages/nodes-base/nodes/Microsoft/OneDrive/FolderDescription.ts b/packages/nodes-base/nodes/Microsoft/OneDrive/FolderDescription.ts new file mode 100644 index 0000000000..cead016ab4 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/OneDrive/FolderDescription.ts @@ -0,0 +1,75 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const folderOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'folder', + ], + }, + }, + options: [ + { + name: 'Get Children', + value: 'getChildren', + description: 'Get items inside a folder', + }, + { + name: 'Search', + value: 'search', + description: 'Search a folder', + }, + ], + default: 'getChildren', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const folderFields = [ + +/* -------------------------------------------------------------------------- */ +/* folder:getChildren */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Folder ID', + name: 'folderId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'getChildren', + ], + resource: [ + 'folder', + ], + }, + }, + default: '', + description: 'Folder ID', + }, +/* -------------------------------------------------------------------------- */ +/* folder:search */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Query', + name: 'query', + type: 'string', + displayOptions: { + show: { + operation: [ + 'search', + ], + resource: [ + 'folder', + ], + }, + }, + default: '', + description: `The query text used to search for items. Values may be matched + across several fields including filename, metadata, and file content.`, + }, +] as INodeProperties[]; From a45c9acb671e0266a65779bb1c8a6e4db1adf121 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 30 Mar 2020 09:20:40 +0200 Subject: [PATCH 042/165] :zap: Minor cleanup HelpScout-Node --- packages/nodes-base/nodes/HelpScout/ConversationDescription.ts | 2 +- packages/nodes-base/nodes/HelpScout/CustomerDescription.ts | 2 +- packages/nodes-base/nodes/HelpScout/GenericFunctions.ts | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/nodes-base/nodes/HelpScout/ConversationDescription.ts b/packages/nodes-base/nodes/HelpScout/ConversationDescription.ts index 30b46368a7..1ab9dd88ea 100644 --- a/packages/nodes-base/nodes/HelpScout/ConversationDescription.ts +++ b/packages/nodes-base/nodes/HelpScout/ConversationDescription.ts @@ -584,7 +584,7 @@ export const conversationFields = [ value: 'asc', }, { - name: 'Desc', + name: 'DESC', value: 'desc', }, ], diff --git a/packages/nodes-base/nodes/HelpScout/CustomerDescription.ts b/packages/nodes-base/nodes/HelpScout/CustomerDescription.ts index 5f353d0bd5..1b3466d078 100644 --- a/packages/nodes-base/nodes/HelpScout/CustomerDescription.ts +++ b/packages/nodes-base/nodes/HelpScout/CustomerDescription.ts @@ -687,7 +687,7 @@ export const customerFields = [ value: 'asc', }, { - name: 'Desc', + name: 'DESC', value: 'desc', }, ], diff --git a/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts b/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts index ab933856c9..76b64ed3e7 100644 --- a/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts +++ b/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts @@ -12,7 +12,6 @@ import { import { get, } from 'lodash'; -import { queryResult } from 'pg-promise'; export async function helpscoutApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IHookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any let options: OptionsWithUri = { From d207396edf41f8e9edc3693da7de4d50f63d1cbc Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 31 Mar 2020 18:54:44 +0200 Subject: [PATCH 043/165] :zap: Small improvements to Zoho CRM Node --- .../nodes-base/nodes/Zoho/LeadDescription.ts | 30 +++++++++--------- .../nodes-base/nodes/Zoho/ZohoCrm.node.ts | 31 +++++++++++++------ 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/packages/nodes-base/nodes/Zoho/LeadDescription.ts b/packages/nodes-base/nodes/Zoho/LeadDescription.ts index 577fee4aec..3b7777befd 100644 --- a/packages/nodes-base/nodes/Zoho/LeadDescription.ts +++ b/packages/nodes-base/nodes/Zoho/LeadDescription.ts @@ -1,4 +1,4 @@ -import { INodeProperties } from "n8n-workflow"; +import { INodeProperties } from 'n8n-workflow'; export const leadOperations = [ { @@ -18,6 +18,11 @@ export const leadOperations = [ value: 'create', description: 'Create a new lead', }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a lead', + }, { name: 'Get', value: 'get', @@ -38,11 +43,6 @@ export const leadOperations = [ value: 'update', description: 'Update a lead', }, - { - name: 'Delete', - value: 'delete', - description: 'Delete a lead', - } ], default: 'create', description: 'The operation to perform.', @@ -648,6 +648,15 @@ export const leadFields = [ default: false, description: 'To include records from the child territories. True includes child territory records', }, + { + displayName: 'Sort By', + name: 'sortBy', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getLeadFields', + }, + default: [], + }, { displayName: 'Sort Order', name: 'sortOrder', @@ -665,15 +674,6 @@ export const leadFields = [ default: 'desc', description: 'Order sort attribute ascending or descending.', }, - { - displayName: 'Sort By', - name: 'sortBy', - type: 'multiOptions', - typeOptions: { - loadOptionsMethod: 'getLeadFields', - }, - default: [], - }, { displayName: 'Territory ID', name: 'territoryId', diff --git a/packages/nodes-base/nodes/Zoho/ZohoCrm.node.ts b/packages/nodes-base/nodes/Zoho/ZohoCrm.node.ts index 20a04295e0..2e2a36bee6 100644 --- a/packages/nodes-base/nodes/Zoho/ZohoCrm.node.ts +++ b/packages/nodes-base/nodes/Zoho/ZohoCrm.node.ts @@ -4,11 +4,11 @@ import { import { IDataObject, - INodeExecutionData, - INodeTypeDescription, - INodeType, ILoadOptionsFunctions, + INodeExecutionData, INodePropertyOptions, + INodeType, + INodeTypeDescription, } from 'n8n-workflow'; import { @@ -17,13 +17,13 @@ import { } from './GenericFunctions'; import { - leadOperations, leadFields, + leadOperations, } from './LeadDescription'; import { - ILead, IAddress, + ILead, } from './LeadInterface'; export class ZohoCrm implements INodeType { @@ -92,7 +92,7 @@ export class ZohoCrm implements INodeType { qs.sort_order = 'desc'; const { data } = await zohoApiRequest.call(this, 'GET', '/accounts', {}, qs); for (const account of data) { - const accountName = account.Account_Name + const accountName = account.Account_Name; const accountId = account.id; returnData.push({ name: accountName, @@ -111,7 +111,7 @@ export class ZohoCrm implements INodeType { for (const field of fields) { if (field.api_name === 'Lead_Status') { for (const value of field.pick_list_values) { - const valueName = value.display_value + const valueName = value.display_value; const valueId = value.actual_value; returnData.push({ name: valueName, @@ -133,7 +133,7 @@ export class ZohoCrm implements INodeType { for (const field of fields) { if (field.api_name === 'Lead_Source') { for (const value of field.pick_list_values) { - const valueName = value.display_value + const valueName = value.display_value; const valueId = value.actual_value; returnData.push({ name: valueName, @@ -155,7 +155,7 @@ export class ZohoCrm implements INodeType { for (const field of fields) { if (field.api_name === 'Industry') { for (const value of field.pick_list_values) { - const valueName = value.display_value + const valueName = value.display_value; const valueId = value.actual_value; returnData.push({ name: valueName, @@ -285,6 +285,10 @@ export class ZohoCrm implements INodeType { } responseData = await zohoApiRequest.call(this, 'POST', '/leads', body); responseData = responseData.data; + + if (responseData.length) { + responseData = responseData[0].details; + } } //https://www.zoho.com/crm/developer/docs/api/update-specific-record.html if (operation === 'update') { @@ -377,12 +381,19 @@ export class ZohoCrm implements INodeType { } responseData = await zohoApiRequest.call(this, 'PUT', `/leads/${leadId}`, body); responseData = responseData.data; + + if (responseData.length) { + responseData = responseData[0].details; + } } //https://www.zoho.com/crm/developer/docs/api/update-specific-record.html if (operation === 'get') { const leadId = this.getNodeParameter('leadId', i) as string; responseData = await zohoApiRequest.call(this, 'GET', `/leads/${leadId}`); - responseData = responseData.data; + if (responseData !== undefined) { + responseData = responseData.data; + } + } //https://www.zoho.com/crm/developer/docs/api/get-records.html if (operation === 'getAll') { From 4fc66c9e1c40975c4d5834f4c628f5d1f9b4b7a2 Mon Sep 17 00:00:00 2001 From: ricardo Date: Wed, 1 Apr 2020 18:10:41 -0400 Subject: [PATCH 044/165] :sparkles: Infusionsoft node --- .../InfusionsoftOAuth2Api.credentials.ts | 48 ++ .../nodes/Infusionsoft/CompanyDescription.ts | 374 ++++++++ .../nodes/Infusionsoft/CompanyInterface.ts | 12 + .../nodes/Infusionsoft/ConctactInterface.ts | 72 ++ .../nodes/Infusionsoft/ContactDescription.ts | 760 ++++++++++++++++ .../Infusionsoft/ContactNoteDescription.ts | 382 +++++++++ .../Infusionsoft/ContactNoteInterface.ts | 8 + .../Infusionsoft/ContactTagDescription.ts | 179 ++++ .../Infusionsoft/EcommerceOrderDescripion.ts | 486 +++++++++++ .../Infusionsoft/EcommerceOrderInterface.ts | 35 + .../EcommerceProductDescription.ts | 236 +++++ .../Infusionsoft/EcommerceProductInterface.ts | 10 + .../nodes/Infusionsoft/EmaiIInterface.ts | 15 + .../nodes/Infusionsoft/EmailDescription.ts | 464 ++++++++++ .../nodes/Infusionsoft/FileDescription.ts | 404 +++++++++ .../nodes/Infusionsoft/FileInterface.ts | 8 + .../nodes/Infusionsoft/GenericFunctions.ts | 82 ++ .../nodes/Infusionsoft/Infusionsoft.node.ts | 811 ++++++++++++++++++ .../Infusionsoft/InfusionsoftTrigger.node.ts | 196 +++++ .../nodes/Infusionsoft/infusionsoft.png | Bin 0 -> 4307 bytes packages/nodes-base/package.json | 7 +- 21 files changed, 4587 insertions(+), 2 deletions(-) create mode 100644 packages/nodes-base/credentials/InfusionsoftOAuth2Api.credentials.ts create mode 100644 packages/nodes-base/nodes/Infusionsoft/CompanyDescription.ts create mode 100644 packages/nodes-base/nodes/Infusionsoft/CompanyInterface.ts create mode 100644 packages/nodes-base/nodes/Infusionsoft/ConctactInterface.ts create mode 100644 packages/nodes-base/nodes/Infusionsoft/ContactDescription.ts create mode 100644 packages/nodes-base/nodes/Infusionsoft/ContactNoteDescription.ts create mode 100644 packages/nodes-base/nodes/Infusionsoft/ContactNoteInterface.ts create mode 100644 packages/nodes-base/nodes/Infusionsoft/ContactTagDescription.ts create mode 100644 packages/nodes-base/nodes/Infusionsoft/EcommerceOrderDescripion.ts create mode 100644 packages/nodes-base/nodes/Infusionsoft/EcommerceOrderInterface.ts create mode 100644 packages/nodes-base/nodes/Infusionsoft/EcommerceProductDescription.ts create mode 100644 packages/nodes-base/nodes/Infusionsoft/EcommerceProductInterface.ts create mode 100644 packages/nodes-base/nodes/Infusionsoft/EmaiIInterface.ts create mode 100644 packages/nodes-base/nodes/Infusionsoft/EmailDescription.ts create mode 100644 packages/nodes-base/nodes/Infusionsoft/FileDescription.ts create mode 100644 packages/nodes-base/nodes/Infusionsoft/FileInterface.ts create mode 100644 packages/nodes-base/nodes/Infusionsoft/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Infusionsoft/Infusionsoft.node.ts create mode 100644 packages/nodes-base/nodes/Infusionsoft/InfusionsoftTrigger.node.ts create mode 100644 packages/nodes-base/nodes/Infusionsoft/infusionsoft.png diff --git a/packages/nodes-base/credentials/InfusionsoftOAuth2Api.credentials.ts b/packages/nodes-base/credentials/InfusionsoftOAuth2Api.credentials.ts new file mode 100644 index 0000000000..63bd890382 --- /dev/null +++ b/packages/nodes-base/credentials/InfusionsoftOAuth2Api.credentials.ts @@ -0,0 +1,48 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +const scopes = [ + 'full', +]; + +export class InfusionsoftOAuth2Api implements ICredentialType { + name = 'infusionsoftOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Infusionsoft OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://signin.infusionsoft.com/app/oauth/authorize', + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://api.infusionsoft.com/token', + }, + { + 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: 'body', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Infusionsoft/CompanyDescription.ts b/packages/nodes-base/nodes/Infusionsoft/CompanyDescription.ts new file mode 100644 index 0000000000..b0278d856c --- /dev/null +++ b/packages/nodes-base/nodes/Infusionsoft/CompanyDescription.ts @@ -0,0 +1,374 @@ +import { + INodeProperties, + } from "n8n-workflow"; + +export const companyOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'company', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a company', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all companies', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const companyFields = [ + +/* -------------------------------------------------------------------------- */ +/* company:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Company Name', + name: 'companyName', + required: true, + type: 'string', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'company', + ], + }, + }, + default: '', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'company', + ], + }, + }, + options: [ + { + displayName: 'Email', + name: 'emailAddress', + type: 'string', + default: '', + }, + { + displayName: 'Notes', + name: 'notes', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + }, + { + displayName: 'Opt In Reason', + name: 'optInReason', + type: 'string', + default: '', + }, + { + displayName: 'Website', + name: 'website', + type: 'string', + default: '', + }, + ], + }, + { + displayName: 'Addresses', + name: 'addressesUi', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + default: '', + placeholder: 'Add Address', + displayOptions: { + show: { + resource: [ + 'company', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + name: 'addressesValues', + displayName: 'Address', + values: [ + { + displayName: 'Country Code', + name: 'countryCode', + type: 'string', + default: '', + description: 'ISO Alpha-3 Code' + }, + { + displayName: 'Line 1', + name: 'line1', + type: 'string', + default: '', + }, + { + displayName: 'Line 2', + name: 'line2', + type: 'string', + default: '', + }, + { + displayName: 'Locality', + name: 'locality', + type: 'string', + default: '', + }, + { + displayName: 'Postal Code', + name: 'postalCode', + type: 'string', + default: '', + }, + { + displayName: 'Region', + name: 'region', + type: 'string', + default: '', + }, + { + displayName: 'Zip Code', + name: 'zipCode', + type: 'string', + default: '', + }, + { + displayName: 'Zip Four', + name: 'zipFour', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Faxes', + name: 'faxesUi', + type: 'fixedCollection', + default: {}, + typeOptions: { + multipleValues: false, + }, + placeholder: 'Add Fax', + displayOptions: { + show: { + resource: [ + 'company', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + name: 'faxesValues', + displayName: 'Fax', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'string', + default: '', + }, + { + displayName: 'Number', + name: 'number', + type: 'string', + default: '', + }, + ], + } + ], + }, + { + displayName: 'Phones', + name: 'phonesUi', + type: 'fixedCollection', + default: {}, + typeOptions: { + multipleValues: true, + }, + placeholder: 'Add Phone', + displayOptions: { + show: { + resource: [ + 'company', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + name: 'phonesValues', + displayName: 'Phones', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'string', + default: '', + }, + { + displayName: 'Number', + name: 'number', + type: 'string', + default: '', + }, + ], + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* company:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'company', + ], + }, + }, + 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: [ + 'company', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 200, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'company', + ], + }, + }, + options: [ + { + displayName: 'Company Name', + name: 'companyName', + type: 'string', + default: '', + description: 'Company name to query on', + }, + { + displayName: 'Order', + name: 'order', + type: 'options', + options: [ + { + name: 'Date Created', + value: 'datecreated', + }, + { + name: 'ID', + value: 'id', + }, + { + name: 'Name', + value: 'name', + }, + ], + default: '', + description: 'Attribute to order items by', + }, + { + displayName: 'Order Direction', + name: 'orderDirection', + type: 'options', + options: [ + { + name: 'ASC', + value: 'ascending', + }, + { + name: 'DES', + value: 'descending', + }, + ], + default: '', + }, + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: `Comma-delimited list of Company properties to include in the response.
+ (Fields such as notes, fax_number and custom_fields aren't included, by default.)`, + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Infusionsoft/CompanyInterface.ts b/packages/nodes-base/nodes/Infusionsoft/CompanyInterface.ts new file mode 100644 index 0000000000..c8006e9f41 --- /dev/null +++ b/packages/nodes-base/nodes/Infusionsoft/CompanyInterface.ts @@ -0,0 +1,12 @@ +import { IDataObject } from "n8n-workflow"; + +export interface ICompany { + address?: IDataObject; + company_name?: string; + email_address?: string; + fax_number?: IDataObject; + notes?: string; + opt_in_reason?: string; + phone_number?: IDataObject; + website?: string; +} diff --git a/packages/nodes-base/nodes/Infusionsoft/ConctactInterface.ts b/packages/nodes-base/nodes/Infusionsoft/ConctactInterface.ts new file mode 100644 index 0000000000..370888a17b --- /dev/null +++ b/packages/nodes-base/nodes/Infusionsoft/ConctactInterface.ts @@ -0,0 +1,72 @@ +import { + IDataObject, + } from "n8n-workflow"; + +export interface IAddress { + country_code?: string; + field?: string; + line1?: string; + line2?: string; + locality?: string; + postal_code?: string; + region?: string; + zip_code?: string; + zip_four?: string; +} + +export interface ICustomField { + content: IDataObject; + id: number; +} + +export interface IEmailContact { + email?: string; + field?: string; +} + +export interface IFax { + field?: string; + number?: string; + type?: string; +} + +export interface IPhone { + extension?: string; + field?: string; + number?: string; + type?: string; +} + +export interface ISocialAccount { + name?: string; + type?: string; +} + +export interface IContact { + addresses?: IAddress[]; + anniversary?: string; + company?: IDataObject; + contact_type?: string; + custom_fields?: ICustomField[]; + duplicate_option?: string; + email_addresses?: IEmailContact[]; + family_name?: string; + fax_numbers?: IFax[]; + given_name?: string; + job_title?: string; + lead_source_id?: number; + middle_name?: string; + opt_in_reason?: string; + origin?: IDataObject; + owner_id?: number; + phone_numbers?: IPhone[]; + preferred_locale?: string; + preferred_name?: string; + prefix?: string; + social_accounts?: ISocialAccount[]; + source_type?: string; + spouse_name?: string; + suffix?: string; + time_zone?: string; + website?: string; +} diff --git a/packages/nodes-base/nodes/Infusionsoft/ContactDescription.ts b/packages/nodes-base/nodes/Infusionsoft/ContactDescription.ts new file mode 100644 index 0000000000..198224fff6 --- /dev/null +++ b/packages/nodes-base/nodes/Infusionsoft/ContactDescription.ts @@ -0,0 +1,760 @@ +import { + INodeProperties, + } from "n8n-workflow"; + +export const contactOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'contact', + ], + }, + }, + options: [ + { + name: 'Create/Update', + value: 'create/update', + description: 'Create/update a contact', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an contact', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve an contact', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all contacts', + }, + ], + default: 'create/update', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const contactFields = [ + +/* -------------------------------------------------------------------------- */ +/* contact:create/update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Duplicate Option', + name: 'duplicateOption', + required: true, + type: 'options', + options: [ + { + name: 'Email', + value: 'email', + }, + { + name: 'Email And Name', + value: 'emailAndName', + }, + ], + displayOptions: { + show: { + operation: [ + 'create/update', + ], + resource: [ + 'contact', + ], + }, + }, + default: 'email', + description: `Performs duplicate checking by one of the following options: Email, EmailAndName,
+ if a match is found using the option provided, the existing contact will be updated` + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'create/update', + ], + resource: [ + 'contact', + ], + }, + }, + options: [ + { + displayName: 'Anniversary', + name: 'anniversary', + type: 'dateTime', + default: '', + }, + { + displayName: 'Company ID', + name: 'companyId', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + }, + { + displayName: 'Contact Type', + name: 'contactType', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getContactTypes', + }, + default: '', + }, + { + displayName: 'Family Name', + name: 'familyName', + type: 'string', + default: '', + }, + { + displayName: 'Given Name', + name: 'givenName', + type: 'string', + default: '', + }, + { + displayName: 'IP Address', + name: 'ipAddress', + type: 'string', + default: '', + }, + { + displayName: 'Job Title', + name: 'jobTitle', + type: 'string', + default: '', + }, + { + displayName: 'Lead Source ID', + name: 'leadSourceId', + type: 'number', + default: 0, + }, + { + displayName: 'Middle Name', + name: 'middleName', + type: 'string', + default: '', + }, + { + displayName: 'Opt In Reason', + name: 'optInReason', + type: 'string', + default: '', + }, + { + displayName: 'Owner ID', + name: 'ownerId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + }, + { + displayName: 'Preferred Locale', + name: 'preferredLocale', + type: 'string', + placeholder: 'en', + default: '', + }, + { + displayName: 'Preferred Name', + name: 'preferredName', + type: 'string', + default: '', + }, + { + displayName: 'Source Type', + name: 'sourceType', + type: 'options', + options: [ + { + name: 'API', + value: 'API', + }, + { + name: 'Import', + value: 'IMPORT', + }, + { + name: 'Landing Page', + value: 'LANDINGPAGE', + }, + { + name: 'Manual', + value: 'MANUAL', + }, + { + name: 'Other', + value: 'OTHER', + }, + { + name: 'Unknown', + value: 'UNKNOWN', + }, + ], + default: '', + }, + { + displayName: 'Spouse Name', + name: 'spouseName', + type: 'string', + default: '', + }, + { + displayName: 'Timezone', + name: 'timezone', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTimezones', + }, + default: '', + }, + { + displayName: 'Website', + name: 'website', + type: 'string', + default: '', + }, + ], + }, + { + displayName: 'Addresses', + name: 'addressesUi', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: '', + placeholder: 'Add Address', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'create/update', + ], + }, + }, + options: [ + { + name: 'addressesValues', + displayName: 'Address', + values: [ + { + displayName: 'Field', + name: 'field', + type: 'options', + options: [ + { + name: 'Billing', + value: 'BILLING', + }, + { + name: 'Shipping', + value: 'SHIPPING', + }, + { + name: 'Other', + value: 'OTHER', + }, + ], + default: '', + }, + { + displayName: 'Country Code', + name: 'countryCode', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCountries', + }, + default: '', + }, + { + displayName: 'Line 1', + name: 'line1', + type: 'string', + default: '', + }, + { + displayName: 'Line 2', + name: 'line2', + type: 'string', + default: '', + }, + { + displayName: 'Locality', + name: 'locality', + type: 'string', + default: '', + }, + { + displayName: 'Postal Code', + name: 'postalCode', + type: 'string', + default: '', + }, + { + displayName: 'Region', + name: 'region', + type: 'string', + default: '', + }, + { + displayName: 'Zip Code', + name: 'zipCode', + type: 'string', + default: '', + }, + { + displayName: 'Zip Four', + name: 'zipFour', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Emails', + name: 'emailsUi', + type: 'fixedCollection', + default: {}, + typeOptions: { + multipleValues: true, + }, + placeholder: 'Add Email', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'create/update', + ], + }, + }, + options: [ + { + name: 'emailsValues', + displayName: 'Email', + values: [ + { + displayName: 'Field', + name: 'field', + type: 'options', + options: [ + { + name: 'Email 1', + value: 'EMAIL1', + }, + { + name: 'Email 2', + value: 'EMAIL2', + }, + { + name: 'Email 3', + value: 'EMAIL3', + }, + ], + default: '', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + }, + ], + } + ], + }, + { + displayName: 'Faxes', + name: 'faxesUi', + type: 'fixedCollection', + default: {}, + typeOptions: { + multipleValues: true, + }, + placeholder: 'Add Fax', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'create/update', + ], + }, + }, + options: [ + { + name: 'faxesValues', + displayName: 'Fax', + values: [ + { + displayName: 'Field', + name: 'field', + type: 'options', + options: [ + { + name: 'Fax 1', + value: 'FAX1', + }, + { + name: 'Fax 2', + value: 'FAX2', + }, + ], + default: '', + }, + { + displayName: 'Number', + name: 'number', + type: 'string', + default: '', + }, + ], + } + ], + }, + { + displayName: 'Phones', + name: 'phonesUi', + type: 'fixedCollection', + default: {}, + typeOptions: { + multipleValues: true, + }, + placeholder: 'Add Phone', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'create/update', + ], + }, + }, + options: [ + { + name: 'phonesValues', + displayName: 'Phones', + values: [ + { + displayName: 'Field', + name: 'field', + type: 'options', + options: [ + { + name: 'Phone 1', + value: 'PHONE1', + }, + { + name: 'Phone 2', + value: 'PHONE2', + }, + { + name: 'Phone 3', + value: 'PHONE3', + }, + { + name: 'Phone 4', + value: 'PHONE4', + }, + { + name: 'Phone 5', + value: 'PHONE5', + }, + ], + default: '', + }, + { + displayName: 'Number', + name: 'number', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Social Accounts', + name: 'socialAccountsUi', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: '', + placeholder: 'Add Social Account', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'create/update', + ], + }, + }, + options: [ + { + name: 'socialAccountsValues', + displayName: 'Social Account', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Facebook', + value: 'Facebook', + }, + { + name: 'Twitter', + value: 'Twitter', + }, + { + name: 'LinkedIn', + value: 'LinkedIn', + }, + ], + default: '', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + }, + ], + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* contact:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'contact', + ], + }, + }, + default: '', + }, +/* -------------------------------------------------------------------------- */ +/* contact:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'contact', + ], + }, + }, + default: '', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Options', + default: {}, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'contact', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: `Comma-delimited list of Contact properties to include in the response.
+ (Some fields such as lead_source_id, custom_fields, and job_title aren't included, by default.)`, + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* contact:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'contact', + ], + }, + }, + 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: [ + 'contact', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 200, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'contact', + ], + }, + }, + options: [ + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + }, + { + displayName: 'Given Name', + name: 'givenName', + type: 'string', + default: '', + }, + { + displayName: 'Family Name', + name: 'familyName', + type: 'string', + default: '', + }, + { + displayName: 'Order', + name: 'order', + type: 'options', + options: [ + { + name: 'Date', + value: 'date', + }, + { + name: 'Email', + value: 'email', + }, + { + name: 'ID', + value: 'id', + }, + { + name: 'Name', + value: 'name', + }, + ], + default: '', + description: 'Attribute to order items by', + }, + { + displayName: 'Order Direction', + name: 'orderDirection', + type: 'options', + options: [ + { + name: 'ASC', + value: 'ascending', + }, + { + name: 'DES', + value: 'descending', + }, + ], + default: '', + }, + { + displayName: 'Since', + name: 'since', + type: 'dateTime', + default: '', + description: 'Date to start searching from on LastUpdated', + }, + { + displayName: 'Until', + name: 'until', + type: 'dateTime', + default: '', + description: 'Date to search to on LastUpdated', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Infusionsoft/ContactNoteDescription.ts b/packages/nodes-base/nodes/Infusionsoft/ContactNoteDescription.ts new file mode 100644 index 0000000000..d916702b20 --- /dev/null +++ b/packages/nodes-base/nodes/Infusionsoft/ContactNoteDescription.ts @@ -0,0 +1,382 @@ +import { + INodeProperties, + } from "n8n-workflow"; + +export const contactNoteOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'contactNote', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a note', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a note', + }, + { + name: 'Get', + value: 'get', + description: 'Get a notes', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all notes', + }, + { + name: 'Update', + value: 'update', + description: 'Update a note', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const contactNoteFields = [ + +/* -------------------------------------------------------------------------- */ +/* contactNote:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'User ID', + name: 'userId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'contactNote', + ], + }, + }, + default: '', + description: 'The infusionsoft user to create the note on behalf of', + }, + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'contactNote', + ], + }, + }, + default: '', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'contactNote', + ], + }, + }, + options: [ + { + displayName: 'Body', + name: 'body', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Appointment', + value: 'appointment', + }, + { + name: 'Call', + value: 'call', + }, + { + name: 'Email', + value: 'email', + }, + { + name: 'Fax', + value: 'fax', + }, + { + name: 'Letter', + value: 'letter', + }, + { + name: 'Other', + value: 'other', + }, + ], + default: '', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* contactNote:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Note ID', + name: 'noteId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'contactNote', + ], + }, + }, + default: '', + }, +/* -------------------------------------------------------------------------- */ +/* contactNote:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Note ID', + name: 'noteId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'contactNote', + ], + }, + }, + default: '', + }, +/* -------------------------------------------------------------------------- */ +/* contactNote:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'contactNote', + ], + }, + }, + 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: [ + 'contactNote', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 200, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'contactNote', + ], + }, + }, + options: [ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + }, + { + displayName: 'User ID', + name: 'userId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* contactNote:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Note ID', + name: 'noteId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'contactNote', + ], + }, + }, + default: '', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'contactNote', + ], + }, + }, + options: [ + { + displayName: 'Body', + name: 'body', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + }, + { + displayName: 'Contact ID', + name: 'contactId', + type: 'number', + typeOptions: { + minValue: 0 + }, + default: 0, + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Appointment', + value: 'appointment', + }, + { + name: 'Call', + value: 'call', + }, + { + name: 'Email', + value: 'email', + }, + { + name: 'Fax', + value: 'fax', + }, + { + name: 'Letter', + value: 'letter', + }, + { + name: 'Other', + value: 'other', + }, + ], + default: '', + }, + { + displayName: 'User ID', + name: 'userId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'The infusionsoft user to create the note on behalf of', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Infusionsoft/ContactNoteInterface.ts b/packages/nodes-base/nodes/Infusionsoft/ContactNoteInterface.ts new file mode 100644 index 0000000000..221bd73e35 --- /dev/null +++ b/packages/nodes-base/nodes/Infusionsoft/ContactNoteInterface.ts @@ -0,0 +1,8 @@ + +export interface INote { + body?: string; + contact_id?: number; + title?: string; + type?: string; + user_id?: number; +} diff --git a/packages/nodes-base/nodes/Infusionsoft/ContactTagDescription.ts b/packages/nodes-base/nodes/Infusionsoft/ContactTagDescription.ts new file mode 100644 index 0000000000..9643530ad4 --- /dev/null +++ b/packages/nodes-base/nodes/Infusionsoft/ContactTagDescription.ts @@ -0,0 +1,179 @@ +import { + INodeProperties, + } from "n8n-workflow"; + +export const contactTagOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'contactTag', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Add a list of tags to a contact', + }, + { + name: 'Delete', + value: 'delete', + description: `Delete a contact's tag`, + }, + { + name: 'Get All', + value: 'getAll', + description: `Retrieve all contact's tags`, + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const contactTagFields = [ + +/* -------------------------------------------------------------------------- */ +/* contactTag:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'contactTag', + ], + }, + }, + default: '', + }, + { + displayName: 'Tag IDs', + name: 'tagIds', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'contactTag', + ], + }, + }, + default: [], + }, +/* -------------------------------------------------------------------------- */ +/* contactTag:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'contactTag', + ], + }, + }, + default: '', + }, + { + displayName: 'Tag IDs', + name: 'tagIds', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'contactTag', + ], + }, + }, + default: 'Tag IDs, multiple ids can be set separated by comma.', + }, +/* -------------------------------------------------------------------------- */ +/* contactTag:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'contactTag', + ], + }, + }, + default: '', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'contactTag', + ], + }, + }, + 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: [ + 'contactTag', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 200, + }, + default: 100, + description: 'How many results to return.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Infusionsoft/EcommerceOrderDescripion.ts b/packages/nodes-base/nodes/Infusionsoft/EcommerceOrderDescripion.ts new file mode 100644 index 0000000000..df601111fb --- /dev/null +++ b/packages/nodes-base/nodes/Infusionsoft/EcommerceOrderDescripion.ts @@ -0,0 +1,486 @@ +import { + INodeProperties, + } from "n8n-workflow"; + +export const ecommerceOrderOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'ecommerceOrder', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create an ecommerce order', + }, + { + name: 'Get', + value: 'get', + description: 'Get an ecommerce order', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an ecommerce order', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all ecommerce orders', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const ecommerceOrderFields = [ + +/* -------------------------------------------------------------------------- */ +/* ecommerceOrder:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'ecommerceOrder', + ], + }, + }, + default: '', + }, + { + displayName: 'Order Date', + name: 'orderDate', + type: 'dateTime', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'ecommerceOrder', + ], + }, + }, + default: '', + }, + { + displayName: 'Order Title', + name: 'orderTitle', + type: 'dateTime', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'ecommerceOrder', + ], + }, + }, + default: '', + }, + { + displayName: 'Order Type', + name: 'orderType', + type: 'options', + options: [ + { + name: 'Offline', + value: 'offline', + }, + { + name: 'Online', + value: 'online', + }, + ], + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'ecommerceOrder', + ], + }, + }, + default: '', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'ecommerceOrder', + ], + }, + }, + options: [ + { + displayName: 'Lead Affiliate ID', + name: 'leadAffiliateId', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + }, + { + displayName: 'Promo Codes', + name: 'promoCodes', + type: 'string', + default: '', + description: `Uses multiple strings separated by comma as promo codes.
+ The corresponding discount will be applied to the order.` + }, + { + displayName: 'Sales Affiliate ID', + name: 'salesAffiliateId', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + }, + ], + }, + { + displayName: 'Shipping Address', + name: 'addressUi', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + default: '', + placeholder: 'Add Address', + displayOptions: { + show: { + resource: [ + 'ecommerceOrder', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + name: 'addressValues', + displayName: 'Address', + values: [ + { + displayName: 'Company', + name: 'company', + type: 'string', + default: '', + }, + { + displayName: 'Country Code', + name: 'countryCode', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCountries', + }, + default: '', + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + }, + { + displayName: 'Middle Name', + name: 'middleName', + type: 'string', + default: '', + }, + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + default: '', + }, + { + displayName: 'Line 1', + name: 'line1', + type: 'string', + default: '', + }, + { + displayName: 'Line 2', + name: 'line2', + type: 'string', + default: '', + }, + { + displayName: 'Locality', + name: 'locality', + type: 'string', + default: '', + }, + { + displayName: 'Region', + name: 'region', + type: 'string', + default: '', + }, + { + displayName: 'Zip Code', + name: 'zipCode', + type: 'string', + default: '', + }, + { + displayName: 'Zip Four', + name: 'zipFour', + type: 'string', + default: '', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Order Items', + name: 'orderItemsUi', + type: 'fixedCollection', + placeholder: 'Add Order Item', + typeOptions: { + multipleValues: true, + }, + default: {}, + displayOptions: { + show: { + resource: [ + 'ecommerceOrder', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + name: 'orderItemsValues', + displayName: 'Order Item', + values: [ + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + }, + { + displayName: 'Price', + name: 'price', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + description: `Overridable price of the product, if not specified,
+ the default will be used.`, + }, + { + displayName: 'Product ID', + name: 'product ID', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + }, + { + displayName: 'Quantity', + name: 'quantity', + type: 'number', + typeOptions: { + minValue: 1, + }, + default: 1, + }, + ], + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* ecommerceOrder:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Order ID', + name: 'orderId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'ecommerceOrder', + ], + }, + }, + default: '', + }, +/* -------------------------------------------------------------------------- */ +/* ecommerceOrder:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Order ID', + name: 'orderId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'ecommerceOrder', + ], + }, + }, + default: '', + }, +/* -------------------------------------------------------------------------- */ +/* ecommerceOrder:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'ecommerceOrder', + ], + }, + }, + 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: [ + 'ecommerceOrder', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 200, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'ecommerceOrder', + ], + }, + }, + options: [ + { + displayName: 'Since', + name: 'since', + type: 'dateTime', + default: '', + description: 'Date to start searching from', + }, + { + displayName: 'Until', + name: 'until', + type: 'dateTime', + default: '', + description: 'Date to search to', + }, + { + displayName: 'Paid', + name: 'paid', + type: 'boolean', + default: false, + }, + { + displayName: 'Order', + name: 'order', + type: 'string', + default: '', + description: 'Attribute to order items by', + }, + { + displayName: 'Contact ID', + name: 'contactId', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + }, + { + displayName: 'Product ID', + name: 'productId', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Infusionsoft/EcommerceOrderInterface.ts b/packages/nodes-base/nodes/Infusionsoft/EcommerceOrderInterface.ts new file mode 100644 index 0000000000..ba7eb8264b --- /dev/null +++ b/packages/nodes-base/nodes/Infusionsoft/EcommerceOrderInterface.ts @@ -0,0 +1,35 @@ + + +export interface IItem { + description?: string; + price?: number; + product_id?: number; + quantity?: number; +} + +export interface IShippingAddress { + company?: string; + country_code?: string; + first_name?: string; + last_name?: string; + line1?: string; + line2?: string; + locality?: string; + middle_name?: string; + postal_code?: string; + region?: string; + zip_code?: string; + zip_four?: string; +} + +export interface IEcommerceOrder { + contact_id: number; + lead_affiliate_id?: string; + order_date: string; + order_items?: IItem[]; + order_title: string; + order_type?: string; + promo_codes?: string[]; + sales_affiliate_id?: number; + shipping_address?: IShippingAddress; +} diff --git a/packages/nodes-base/nodes/Infusionsoft/EcommerceProductDescription.ts b/packages/nodes-base/nodes/Infusionsoft/EcommerceProductDescription.ts new file mode 100644 index 0000000000..f0b3ef3643 --- /dev/null +++ b/packages/nodes-base/nodes/Infusionsoft/EcommerceProductDescription.ts @@ -0,0 +1,236 @@ +import { + INodeProperties, + } from "n8n-workflow"; + +export const ecommerceProductOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'ecommerceProduct', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create an ecommerce product', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an ecommerce product', + }, + { + name: 'Get', + value: 'get', + description: 'Get an ecommerce product', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all ecommerce product', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const ecommerceProductFields = [ + +/* -------------------------------------------------------------------------- */ +/* ecommerceProduct:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Product Name', + name: 'productName', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'ecommerceProduct', + ], + }, + }, + default: '', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'ecommerceProduct', + ], + }, + }, + options: [ + { + displayName: 'Active', + name: 'active', + type: 'boolean', + default: false, + }, + { + displayName: 'Product Description', + name: 'productDesc', + typeOptions: { + alwaysOpenEditWindow: true, + }, + type: 'string', + default: '', + }, + { + displayName: 'Product Price', + name: 'productPrice', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + }, + { + displayName: 'Product Short Desc', + name: 'productShortDesc', + type: 'string', + default: '', + }, + { + displayName: 'SKU', + name: 'sku', + type: 'string', + default: '', + }, + { + displayName: 'Subscription Only', + name: 'subscriptionOnly', + type: 'boolean', + default: false, + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* ecommerceProduct:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Product ID', + name: 'productId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'ecommerceProduct', + ], + }, + }, + default: '', + }, +/* -------------------------------------------------------------------------- */ +/* ecommerceProduct:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Product ID', + name: 'productId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'ecommerceProduct', + ], + }, + }, + default: '', + }, +/* -------------------------------------------------------------------------- */ +/* ecommerceProduct:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'ecommerceProduct', + ], + }, + }, + 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: [ + 'ecommerceProduct', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 200, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'ecommerceProduct', + ], + }, + }, + options: [ + { + displayName: 'Active', + name: 'active', + type: 'boolean', + default: false, + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Infusionsoft/EcommerceProductInterface.ts b/packages/nodes-base/nodes/Infusionsoft/EcommerceProductInterface.ts new file mode 100644 index 0000000000..23c2fbb493 --- /dev/null +++ b/packages/nodes-base/nodes/Infusionsoft/EcommerceProductInterface.ts @@ -0,0 +1,10 @@ + +export interface IEcommerceProduct { + active?: string; + product_name?: string; + product_desc?: string; + product_price?: number; + product_short_desc?: string; + sku?: string; + subscription_only?: boolean; +} diff --git a/packages/nodes-base/nodes/Infusionsoft/EmaiIInterface.ts b/packages/nodes-base/nodes/Infusionsoft/EmaiIInterface.ts new file mode 100644 index 0000000000..5a8fdf5128 --- /dev/null +++ b/packages/nodes-base/nodes/Infusionsoft/EmaiIInterface.ts @@ -0,0 +1,15 @@ +export interface IAttachment { + file_data?: string; + file_name?: string; +} + + +export interface IEmail { + address_field?: string; + attachments?: IAttachment[]; + contacts: number[]; + html_content?: string; + plain_content?: string; + subject?: string; + user_id: number; +} diff --git a/packages/nodes-base/nodes/Infusionsoft/EmailDescription.ts b/packages/nodes-base/nodes/Infusionsoft/EmailDescription.ts new file mode 100644 index 0000000000..f7875c13f6 --- /dev/null +++ b/packages/nodes-base/nodes/Infusionsoft/EmailDescription.ts @@ -0,0 +1,464 @@ +import { + INodeProperties, + } from "n8n-workflow"; + +export const emailOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'email', + ], + }, + }, + options: [ + { + name: 'Create Record', + value: 'createRecord', + description: 'Create a record of an email sent to a contact', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all sent emails', + }, + { + name: 'Send', + value: 'send', + description: 'Send Email', + }, + ], + default: 'createRecord', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const emailFields = [ + +/* -------------------------------------------------------------------------- */ +/* email:createRecord */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Sent To Address', + name: 'sentToAddress', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'createRecord', + ], + resource: [ + 'email', + ], + }, + }, + default: '', + }, + { + displayName: 'Sent From Address', + name: 'sentFromAddress', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'createRecord', + ], + resource: [ + 'email', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'createRecord', + ], + resource: [ + 'email', + ], + }, + }, + options: [ + { + displayName: 'Clicked Date', + name: 'clickedDate', + type: 'dateTime', + default: '', + }, + { + displayName: 'Contact ID', + name: 'contactId', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + }, + { + displayName: 'Headers', + name: 'headers', + type: 'string', + default: '', + }, + { + displayName: 'HTML content', + name: 'htmlContent', + type: 'string', + default: '', + description: 'Base64 encoded HTML', + }, + { + displayName: 'Opened Date', + name: 'openedDate', + type: 'dateTime', + default: '', + }, + { + displayName: 'Original Provider', + name: 'originalProvider', + type: 'options', + options: [ + { + name: 'Unknown', + value: 'UNKNOWN', + }, + { + name: 'Infusionsoft', + value: 'INFUSIONSOFT', + }, + { + name: 'Microsoft', + value: 'MICROSOFT', + }, + { + name: 'Google', + value: 'GOOGLE', + }, + ], + default: 'UNKNOWN', + description: 'Provider that sent the email case insensitive, must be in list', + }, + { + displayName: 'Original Provider ID', + name: 'originalProviderId', + type: 'string', + default: '', + description: `Provider id that sent the email, must be unique when combined with provider.
+ If omitted a UUID without dashes is autogenerated for the record.` + }, + { + displayName: 'Plain Content', + name: 'plainContent', + type: 'string', + default: '', + description: 'Base64 encoded text', + }, + { + displayName: 'Provider Source ID', + name: 'providerSourceId', + type: 'string', + default: 'The email address of the synced email account that generated this message.', + }, + { + displayName: 'Received Date', + name: 'receivedDate', + type: 'dateTime', + default: '', + }, + { + displayName: 'Sent Date', + name: 'sentDate', + type: 'dateTime', + default: '', + }, + { + displayName: 'Sent From Reply Address', + name: 'sentFromReplyAddress', + type: 'string', + default: '', + }, + { + displayName: 'Sent To Bcc Addresses', + name: 'sentToBccAddresses', + type: 'string', + default: '', + }, + { + displayName: 'Sent To CC Addresses', + name: 'sentToCCAddresses', + type: 'string', + default: '', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'string', + default: '', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* email:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'email', + ], + }, + }, + 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: [ + 'email', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 200, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'email', + ], + }, + }, + options: [ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + }, + { + displayName: 'Since Sent Date', + name: 'sinceSentDate', + type: 'dateTime', + default: '', + description: 'Emails sent since the provided date, must be present if untilDate is provided', + + }, + { + displayName: 'Until Sent Date', + name: 'untilSentDate', + type: 'dateTime', + default: '', + description: 'Email sent until the provided date', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* email:send */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'User ID', + name: 'userId', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'email', + ], + }, + }, + default: '', + description: 'The infusionsoft user to send the email on behalf of', + }, + { + displayName: 'Contact IDs', + name: 'contactIds', + type: 'string', + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'email', + ], + }, + }, + default: '', + description: 'Contact Ids to receive the email. Multiple can be added seperated by comma', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'string', + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'email', + ], + }, + }, + default: '', + description: 'The subject line of the email', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'email', + ], + }, + }, + options: [ + { + displayName: 'Address field', + name: 'addressField', + type: 'string', + default: '', + description: `Email field of each Contact record to address the email to, such as
+ 'EmailAddress1', 'EmailAddress2', 'EmailAddress3', defaulting to the contact's primary email`, + }, + { + displayName: 'HTML Content', + name: 'htmlContent', + type: 'string', + default: '', + description: 'The HTML-formatted content of the email, encoded in Base64', + }, + { + displayName: 'Plain Content', + name: 'plainContent', + type: 'string', + default: '', + description: 'The plain-text content of the email, encoded in Base64', + }, + ], + }, + { + displayName: 'Attachments', + name: 'attachmentsUi', + placeholder: 'Add Attachments', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'email', + ], + }, + }, + options: [ + { + name: 'attachmentsValues', + displayName: 'Attachments Values', + values: [ + { + displayName: 'File Data', + name: 'fileData', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'The content of the attachment, encoded in Base64', + }, + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + default: '', + description: 'The filename of the attached file, including extension', + }, + ], + }, + { + name: 'attachmentsBinary', + displayName: 'Attachments Binary', + values: [ + { + displayName: 'Property', + name: 'property', + type: 'string', + default: '', + description: 'Name of the binary properties which contain data which should be added to email as attachment', + }, + ], + }, + ], + default: '', + description: 'Attachments to be sent with each copy of the email, maximum of 10 with size of 1MB each', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Infusionsoft/FileDescription.ts b/packages/nodes-base/nodes/Infusionsoft/FileDescription.ts new file mode 100644 index 0000000000..9d90f4411c --- /dev/null +++ b/packages/nodes-base/nodes/Infusionsoft/FileDescription.ts @@ -0,0 +1,404 @@ +import { + INodeProperties, + } from "n8n-workflow"; + +export const fileOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'file', + ], + }, + }, + options: [ + { + name: 'Delete', + value: 'delete', + description: 'Delete a file', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all files', + }, + { + name: 'Upload', + value: 'upload', + description: 'Upload a file', + }, + ], + default: 'delete', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const fileFields = [ +/* -------------------------------------------------------------------------- */ +/* file:upload */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Binary Data', + name: 'binaryData', + type: 'boolean', + default: false, + displayOptions: { + show: { + operation: [ + 'upload' + ], + resource: [ + 'file', + ], + }, + }, + description: 'If the data to upload should be taken from binary field.', + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + required: true, + displayOptions: { + show: { + operation: [ + 'upload' + ], + resource: [ + 'file', + ], + binaryData: [ + true, + ], + }, + }, + description: 'Name of the binary property which contains
the data for the file to be uploaded.', + }, + { + displayName: 'File Association', + name: 'fileAssociation', + type: 'options', + options: [ + { + name: 'Company', + value: 'company', + }, + { + name: 'Contact', + value: 'contact', + }, + { + name: 'User', + value: 'user', + }, + ], + required: true, + displayOptions: { + show: { + operation: [ + 'upload', + ], + resource: [ + 'file', + ], + }, + }, + default: '', + }, + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'upload', + ], + resource: [ + 'file', + ], + fileAssociation: [ + 'contact', + ], + }, + }, + default: '', + }, + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'upload', + ], + resource: [ + 'file', + ], + binaryData: [ + false, + ], + }, + }, + default: '', + description: 'The filename of the attached file, including extension', + }, + { + displayName: 'File Data', + name: 'fileData', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + required: true, + displayOptions: { + show: { + operation: [ + 'upload', + ], + resource: [ + 'file', + ], + binaryData: [ + false, + ], + }, + }, + default: '', + description: 'The content of the attachment, encoded in Base64', + }, + { + displayName: 'Is Public', + name: 'isPublic', + type: 'boolean', + default: false, + displayOptions: { + show: { + operation: [ + 'upload' + ], + resource: [ + 'file', + ], + }, + }, + }, +/* -------------------------------------------------------------------------- */ +/* file:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'File ID', + name: 'fileId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'file', + ], + }, + }, + default: '', + }, +/* -------------------------------------------------------------------------- */ +/* file:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'file', + ], + }, + }, + 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: [ + 'file', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 200, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'file', + ], + }, + }, + options: [ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'number', + typeOptions: { + minValue: 0 + }, + default: 0, + description: 'Filter based on Contact Id, if user has permission to see Contact files.', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: `Filter files based on name, with '*' preceding or following to indicate LIKE queries.`, + }, + { + displayName: 'Permission', + name: 'permission', + type: 'options', + options: [ + { + name: 'User', + value: 'user', + }, + { + name: 'Company', + value: 'company', + }, + { + name: 'Both', + value: 'both', + }, + ], + default: 'both', + description: 'Filter based on the permission of files', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Application', + value: 'application', + }, + { + name: 'Image', + value: 'image', + }, + { + name: 'Fax', + value: 'fax', + }, + { + name: 'Attachment', + value: 'attachment', + }, + { + name: 'Ticket', + value: 'ticket', + }, + { + name: 'Contact', + value: 'contact', + }, + { + name: 'Digital Product', + value: 'digitalProduct', + }, + { + name: 'Import', + value: 'import', + }, + { + name: 'Hidden', + value: 'hidden', + }, + { + name: 'Webform', + value: 'webform', + }, + { + name: 'Style Cart', + value: 'styleCart', + }, + { + name: 'Re Sampled Image', + value: 'reSampledImage', + }, + { + name: 'Template Thumnail', + value: 'templateThumnail', + }, + { + name: 'Funnel', + value: 'funnel', + }, + { + name: 'Logo Thumnail', + value: 'logoThumnail', + }, + ], + default: '', + description: 'Filter based on the type of file.', + }, + { + displayName: 'Viewable', + name: 'viewable', + type: 'options', + options: [ + { + name: 'Public', + value: 'public', + }, + { + name: 'Private', + value: 'private', + }, + { + name: 'Both', + value: 'both', + }, + ], + default: 'both', + description: 'Include public or private files in response', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Infusionsoft/FileInterface.ts b/packages/nodes-base/nodes/Infusionsoft/FileInterface.ts new file mode 100644 index 0000000000..e924826d26 --- /dev/null +++ b/packages/nodes-base/nodes/Infusionsoft/FileInterface.ts @@ -0,0 +1,8 @@ + +export interface IFile { + file_name?: string; + file_data?: string; + contact_id?: number; + is_public?: boolean; + file_association?: string; +} diff --git a/packages/nodes-base/nodes/Infusionsoft/GenericFunctions.ts b/packages/nodes-base/nodes/Infusionsoft/GenericFunctions.ts new file mode 100644 index 0000000000..4d942ccd64 --- /dev/null +++ b/packages/nodes-base/nodes/Infusionsoft/GenericFunctions.ts @@ -0,0 +1,82 @@ +import { + OptionsWithUri, + } from 'request'; + +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject +} from 'n8n-workflow'; + +import { + snakeCase, + } from 'change-case'; + +export async function infusionsoftApiRequest(this: IWebhookFunctions | IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + let options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || `https://api.infusionsoft.com/crm/rest/v1${resource}`, + json: true + }; + try { + options = Object.assign({}, options, option); + if (Object.keys(headers).length !== 0) { + options.headers = Object.assign({}, options.headers, headers); + } + if (Object.keys(body).length === 0) { + delete options.body; + } + //@ts-ignore + return await this.helpers.requestOAuth.call(this, 'infusionsoftOAuth2Api', options); + } catch (error) { + if (error.response && error.response.body && error.response.body.message) { + // Try to return the error prettier + throw new Error(`Infusionsoft error response [${error.statusCode}]: ${error.response.body.message}`); + } + throw error; + } +} + +export async function infusionsoftApiRequestAllItems(this: IHookFunctions| IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + let uri: string | undefined; + query.limit = 50; + + do { + responseData = await infusionsoftApiRequest.call(this, method, endpoint, body, query, uri); + uri = responseData.next; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + returnData.length < responseData.count + ); + + return returnData; +} + +export function keysToSnakeCase(elements: IDataObject[] | IDataObject) : IDataObject[] { + if (!Array.isArray(elements)) { + elements = [elements]; + } + for (const element of elements) { + for (const key of Object.keys(element)) { + if (key !== snakeCase(key)) { + element[snakeCase(key)] = element[key]; + delete element[key]; + } + } + } + return elements; +} diff --git a/packages/nodes-base/nodes/Infusionsoft/Infusionsoft.node.ts b/packages/nodes-base/nodes/Infusionsoft/Infusionsoft.node.ts new file mode 100644 index 0000000000..a2740c0f42 --- /dev/null +++ b/packages/nodes-base/nodes/Infusionsoft/Infusionsoft.node.ts @@ -0,0 +1,811 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeTypeDescription, + INodeType, + ILoadOptionsFunctions, + INodePropertyOptions, + IBinaryKeyData, +} from 'n8n-workflow'; + +import { + infusionsoftApiRequest, + infusionsoftApiRequestAllItems, + keysToSnakeCase, +} from './GenericFunctions'; + +import { + contactOperations, + contactFields, +} from './ContactDescription'; + +import { + contactNoteOperations, + contactNoteFields, +} from './ContactNoteDescription'; + +import { + contactTagOperations, + contactTagFields, +} from './ContactTagDescription'; + +import { + ecommerceOrderOperations, + ecommerceOrderFields, +} from './EcommerceOrderDescripion'; + +import { + ecommerceProductOperations, + ecommerceProductFields, +} from './EcommerceProductDescription'; + +import { + emailOperations, + emailFields, +} from './EmailDescription'; + +import { + fileOperations, + fileFields, +} from './FileDescription'; + +import { + companyOperations, + companyFields, + } from './CompanyDescription'; + +import { + IContact, + IAddress, + IFax, + IEmailContact, + ISocialAccount, + IPhone, +} from './ConctactInterface'; + +import { + IEmail, + IAttachment, +} from './EmaiIInterface'; + +import { + INote, +} from './ContactNoteInterface'; + +import { + IEcommerceOrder, + IItem, + IShippingAddress, +} from './EcommerceOrderInterface'; + +import { + IEcommerceProduct, +} from './EcommerceProductInterface'; + +import { + IFile, +} from './FileInterface'; + +import { + ICompany, +} from './CompanyInterface'; + +import { + pascalCase, + titleCase, +} from 'change-case'; + +import * as moment from 'moment-timezone'; + +export class Infusionsoft implements INodeType { + description: INodeTypeDescription = { + displayName: 'Infusionsoft', + name: ' infusionsoft', + icon: 'file:infusionsoft.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Infusionsoft API.', + defaults: { + name: 'Infusionsoft', + color: '#79af53', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'infusionsoftOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Company', + value: 'company', + }, + { + name: 'Contact', + value: 'contact', + }, + { + name: 'Contact Note', + value: 'contactNote', + }, + { + name: 'Contact Tag', + value: 'contactTag', + }, + { + name: 'Ecommerce Order', + value: 'ecommerceOrder', + }, + { + name: 'Ecommerce Product', + value: 'ecommerceProduct', + }, + { + name: 'Email', + value: 'email', + }, + { + name: 'File', + value: 'file', + }, + ], + default: 'company', + description: 'The resource to operate on.', + }, + // COMPANY + ...companyOperations, + ...companyFields, + // CONTACT + ...contactOperations, + ...contactFields, + // CONTACT NOTE + ...contactNoteOperations, + ...contactNoteFields, + // CONTACT TAG + ...contactTagOperations, + ...contactTagFields, + // ECOMMERCE ORDER + ...ecommerceOrderOperations, + ...ecommerceOrderFields, + // ECOMMERCE PRODUCT + ...ecommerceProductOperations, + ...ecommerceProductFields, + // EMAIL + ...emailOperations, + ...emailFields, + // FILE + ...fileOperations, + ...fileFields, + ], + }; + + methods = { + loadOptions: { + // Get all the tags to display them to user so that he can + // select them easily + async getTags(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const tags = await infusionsoftApiRequestAllItems.call(this, 'tags', 'GET', '/tags'); + for (const tag of tags) { + const tagName = tag.name; + const tagId = tag.id; + returnData.push({ + name: tagName as string, + value: tagId as string, + }); + } + return returnData; + }, + // Get all the users to display them to user so that he can + // select them easily + async getUsers(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const users = await infusionsoftApiRequestAllItems.call(this, 'users', 'GET', '/users'); + for (const user of users) { + const userName = user.given_name; + const userId = user.id; + returnData.push({ + name: userName as string, + value: userId as string, + }); + } + return returnData; + }, + // Get all the countries to display them to user so that he can + // select them easily + async getCountries(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const { countries } = await infusionsoftApiRequest.call(this, 'GET', '/locales/countries'); + for (const key of Object.keys(countries)) { + const countryName = countries[key]; + const countryId = key; + returnData.push({ + name: countryName as string, + value: countryId as string, + }); + } + return returnData; + }, + // Get all the provinces to display them to user so that he can + // select them easily + async getProvinces(this: ILoadOptionsFunctions): Promise { + const countryCode = this.getCurrentNodeParameter('countryCode') as string; + const returnData: INodePropertyOptions[] = []; + const { provinces } = await infusionsoftApiRequest.call(this, 'GET', `/locales/countries/${countryCode}/provinces`); + for (const key of Object.keys(provinces)) { + const provinceName = provinces[key]; + const provinceId = key; + returnData.push({ + name: provinceName as string, + value: provinceId as string, + }); + } + return returnData; + }, + // Get all the contact types to display them to user so that he can + // select them easily + async getContactTypes(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const types = await infusionsoftApiRequest.call(this, 'GET', '/setting/contact/optionTypes'); + for (const type of types.value.split(',')) { + const typeName = type; + const typeId = type; + returnData.push({ + name: typeName, + value: typeId, + }); + } + return returnData; + }, + // Get all the timezones to display them to user so that he can + // select them easily + async getTimezones(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + for (const timezone of moment.tz.names()) { + const timezoneName = timezone; + const timezoneId = timezone; + returnData.push({ + name: timezoneName, + value: timezoneId, + }); + } + 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 === 'company') { + //https://developer.infusionsoft.com/docs/rest/#!/Company/createCompanyUsingPOST + if (operation === 'create') { + const addresses = (this.getNodeParameter('addressesUi', i) as IDataObject).addressesValues as IDataObject[]; + const faxes = (this.getNodeParameter('faxesUi', i) as IDataObject).faxesValues as IDataObject[]; + const phones = (this.getNodeParameter('phonesUi', i) as IDataObject).phonesValues as IDataObject[]; + const companyName = this.getNodeParameter('companyName', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: ICompany = { + company_name: companyName, + }; + keysToSnakeCase(additionalFields); + Object.assign(body, additionalFields); + if (addresses) { + body.address = keysToSnakeCase(addresses)[0] ; + } + if (faxes) { + body.fax_number = faxes[0]; + } + if (phones) { + body.phone_number = phones[0]; + } + responseData = await infusionsoftApiRequest.call(this, 'POST', '/companies', body); + } + //https://developer.infusionsoft.com/docs/rest/#!/Company/listCompaniesUsingGET + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const options = this.getNodeParameter('options', i) as IDataObject; + keysToSnakeCase(options); + Object.assign(qs, options); + if (qs.fields) { + qs.optional_properties = qs.fields; + delete qs.fields; + } + if (returnAll) { + responseData = await infusionsoftApiRequestAllItems.call(this, 'companies', 'GET', '/companies', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await infusionsoftApiRequest.call(this, 'GET', '/companies', {}, qs); + responseData = responseData.companies; + } + } + } + if (resource === 'contact') { + //https://developer.infusionsoft.com/docs/rest/#!/Contact/createOrUpdateContactUsingPUT + if (operation === 'create/update') { + const duplicateOption = this.getNodeParameter('duplicateOption', i) as string; + const addresses = (this.getNodeParameter('addressesUi', i) as IDataObject).addressesValues as IDataObject[]; + const emails = (this.getNodeParameter('emailsUi', i) as IDataObject).emailsValues as IDataObject[]; + const faxes = (this.getNodeParameter('faxesUi', i) as IDataObject).faxesValues as IDataObject[]; + const socialAccounts = (this.getNodeParameter('socialAccountsUi', i) as IDataObject).socialAccountsValues as IDataObject[]; + const phones = (this.getNodeParameter('phonesUi', i) as IDataObject).phonesValues as IDataObject[]; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IContact = { + duplicate_option: pascalCase(duplicateOption), + }; + + if (additionalFields.anniversary) { + body.anniversary = additionalFields.anniversary as string; + } + if (additionalFields.contactType) { + body.contact_type = additionalFields.contactType as string; + } + if (additionalFields.familyName) { + body.family_name = additionalFields.familyName as string; + } + if (additionalFields.givenName) { + body.given_name = additionalFields.givenName as string; + } + if (additionalFields.jobTitle) { + body.job_title = additionalFields.jobTitle as string; + } + if (additionalFields.leadSourceId) { + body.lead_source_id = additionalFields.leadSourceId as number; + } + if (additionalFields.middleName) { + body.middle_name = additionalFields.middleName as string; + } + if (additionalFields.middleName) { + body.middle_name = additionalFields.middleName as string; + } + if (additionalFields.OptInReason) { + body.opt_in_reason = additionalFields.OptInReason as string; + } + if (additionalFields.ownerId) { + body.owner_id = additionalFields.ownerId as number; + } + if (additionalFields.preferredLocale) { + body.preferred_locale = additionalFields.preferredLocale as string; + } + if (additionalFields.preferredName) { + body.preferred_name = additionalFields.preferredName as string; + } + if (additionalFields.sourceType) { + body.source_type = additionalFields.sourceType as string; + } + if (additionalFields.spouseName) { + body.spouse_name = additionalFields.spouseName as string; + } + if (additionalFields.timezone) { + body.time_zone = additionalFields.timezone as string; + } + if (additionalFields.website) { + body.website = additionalFields.website as string; + } + if (additionalFields.ipAddress) { + body.origin = { ip_address: additionalFields.ipAddress as string }; + } + if (additionalFields.companyId) { + body.company = { id: additionalFields.companyId as number }; + } + if (addresses) { + body.addresses = keysToSnakeCase(addresses) as IAddress[]; + } + if (emails) { + body.email_addresses = emails as IEmailContact[]; + } + if (faxes) { + body.fax_numbers = faxes as IFax[]; + } + if (socialAccounts) { + body.social_accounts = socialAccounts as ISocialAccount[]; + } + if (phones) { + body.phone_numbers = phones as IPhone[]; + } + responseData = await infusionsoftApiRequest.call(this, 'PUT', '/contacts', body); + } + //https://developer.infusionsoft.com/docs/rest/#!/Contact/deleteContactUsingDELETE + if (operation === 'delete') { + const contactId = parseInt(this.getNodeParameter('contactId', i) as string, 10); + responseData = await infusionsoftApiRequest.call(this, 'DELETE', `/contacts/${contactId}`); + responseData = { success: true }; + } + //https://developer.infusionsoft.com/docs/rest/#!/Contact/getContactUsingGET + if (operation === 'get') { + const contactId = parseInt(this.getNodeParameter('contactId', i) as string, 10); + const options = this.getNodeParameter('options', i) as IDataObject; + if (options.fields) { + qs.optional_properties = options.fields as string; + } + responseData = await infusionsoftApiRequest.call(this, 'GET', `/contacts/${contactId}`, {}, qs); + } + //https://developer.infusionsoft.com/docs/rest/#!/Contact/listContactsUsingGET + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const options = this.getNodeParameter('options', i) as IDataObject; + if (options.email) { + qs.email = options.email as boolean; + } + if (options.givenName) { + qs.given_name = options.givenName as string; + } + if (options.familyName) { + qs.family_name = options.familyName as boolean; + } + if (options.order) { + qs.order = options.order as string; + } + if (options.orderDirection) { + qs.order_direction = options.orderDirection as string; + } + if (options.since) { + qs.since = options.since as string; + } + if (options.until) { + qs.until = options.until as string; + } + if (returnAll) { + responseData = await infusionsoftApiRequestAllItems.call(this, 'contacts', 'GET', '/contacts', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await infusionsoftApiRequest.call(this, 'GET', '/contacts', {}, qs); + responseData = responseData.contacts; + } + } + } + if (resource === 'contactNote') { + //https://developer.infusionsoft.com/docs/rest/#!/Note/createNoteUsingPOST + if (operation === 'create') { + const userId = this.getNodeParameter('userId', i) as number; + const contactId = parseInt(this.getNodeParameter('contactId', i) as string, 10); + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: INote = { + user_id: userId, + contact_id: contactId, + }; + keysToSnakeCase(additionalFields); + if (additionalFields.type) { + additionalFields.type = pascalCase(additionalFields.type as string); + } + Object.assign(body, additionalFields); + responseData = await infusionsoftApiRequest.call(this, 'POST', '/notes', body); + } + //https://developer.infusionsoft.com/docs/rest/#!/Note/deleteNoteUsingDELETE + if (operation === 'delete') { + const noteId = this.getNodeParameter('noteId', i) as string; + responseData = await infusionsoftApiRequest.call(this, 'DELETE', `/notes/${noteId}`); + responseData = { success: true }; + } + //https://developer.infusionsoft.com/docs/rest/#!/Note/getNoteUsingGET + if (operation === 'get') { + const noteId = this.getNodeParameter('noteId', i) as string; + responseData = await infusionsoftApiRequest.call(this, 'GET', `/notes/${noteId}`); + } + //https://developer.infusionsoft.com/docs/rest/#!/Note/listNotesUsingGET + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + keysToSnakeCase(filters); + Object.assign(qs, filters); + if (returnAll) { + responseData = await infusionsoftApiRequestAllItems.call(this, 'notes', 'GET', '/notes', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await infusionsoftApiRequest.call(this, 'GET', '/notes', {}, qs); + responseData = responseData.notes; + } + } + //https://developer.infusionsoft.com/docs/rest/#!/Note/updatePropertiesOnNoteUsingPATCH + if (operation === 'update') { + const noteId = this.getNodeParameter('noteId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: INote = {}; + keysToSnakeCase(additionalFields); + if (additionalFields.type) { + additionalFields.type = pascalCase(additionalFields.type as string); + } + Object.assign(body, additionalFields); + responseData = await infusionsoftApiRequest.call(this, 'PATCH', `/notes/${noteId}`, body); + } + } + if (resource === 'contactTag') { + //https://developer.infusionsoft.com/docs/rest/#!/Contact/applyTagsToContactIdUsingPOST + if (operation === 'create') { + const contactId = parseInt(this.getNodeParameter('contactId', i) as string, 10); + const tagIds = this.getNodeParameter('tagIds', i) as number[]; + const body: IDataObject = { + tagIds, + }; + responseData = await infusionsoftApiRequest.call(this, 'POST', `/contacts/${contactId}/tags`, body); + } + //https://developer.infusionsoft.com/docs/rest/#!/Contact/removeTagsFromContactUsingDELETE_1 + if (operation === 'delete') { + const contactId = parseInt(this.getNodeParameter('contactId', i) as string, 10); + const tagIds = this.getNodeParameter('tagIds', i) as string; + qs.ids = tagIds; + responseData = await infusionsoftApiRequest.call(this, 'DELETE', `/contacts/${contactId}/tags`, {}, qs); + responseData = { success: true }; + } + //https://developer.infusionsoft.com/docs/rest/#!/Contact/listAppliedTagsUsingGET + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const contactId = parseInt(this.getNodeParameter('contactId', i) as string, 10); + if (returnAll) { + responseData = await infusionsoftApiRequestAllItems.call(this, 'tags', 'GET', `/contacts/${contactId}/tags`, {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await infusionsoftApiRequest.call(this, 'GET', `/contacts/${contactId}/tags`, {}, qs); + responseData = responseData.tags; + } + } + } + if (resource === 'ecommerceOrder') { + //https://developer.infusionsoft.com/docs/rest/#!/E-Commerce/createOrderUsingPOST + if (operation === 'create') { + const contactId = parseInt(this.getNodeParameter('contactId', i) as string, 10); + const orderDate = this.getNodeParameter('orderDate', i) as string; + const orderTitle = this.getNodeParameter('orderTitle', i) as string; + const orderType = this.getNodeParameter('orderType', i) as string; + const orderItems = (this.getNodeParameter('orderItemsUi', i) as IDataObject).orderItemsValues as IDataObject[]; + const shippingAddress = (this.getNodeParameter('addressUi', i) as IDataObject).addressValues as IDataObject; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IEcommerceOrder = { + contact_id: contactId, + order_date: orderDate, + order_title: orderTitle, + order_type: pascalCase(orderType), + }; + if (additionalFields.promoCodes) { + additionalFields.promoCodes = (additionalFields.promoCodes as string).split(',') as string[]; + } + keysToSnakeCase(additionalFields); + Object.assign(body, additionalFields); + body.order_items = keysToSnakeCase(orderItems) as IItem[]; + if (shippingAddress) { + body.shipping_address = keysToSnakeCase(shippingAddress)[0] as IShippingAddress; + } + responseData = await infusionsoftApiRequest.call(this, 'POST', '/orders', body); + } + //https://developer.infusionsoft.com/docs/rest/#!/E-Commerce/deleteOrderUsingDELETE + if (operation === 'delete') { + const orderId = parseInt(this.getNodeParameter('orderId', i) as string, 10); + responseData = await infusionsoftApiRequest.call(this, 'DELETE', `/orders/${orderId}`); + responseData = { success: true }; + } + //https://developer.infusionsoft.com/docs/rest/#!/E-Commerce/getOrderUsingGET + if (operation === 'get') { + const orderId = parseInt(this.getNodeParameter('orderId', i) as string, 10); + responseData = await infusionsoftApiRequest.call(this, 'get', `/orders/${orderId}`); + } + //https://developer.infusionsoft.com/docs/rest/#!/E-Commerce/listOrdersUsingGET + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const options = this.getNodeParameter('options', i) as IDataObject; + keysToSnakeCase(options); + Object.assign(qs, options); + if (returnAll) { + responseData = await infusionsoftApiRequestAllItems.call(this, 'orders', 'GET', '/orders', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await infusionsoftApiRequest.call(this, 'GET', '/orders', {}, qs); + responseData = responseData.orders; + } + } + } + if (resource === 'ecommerceProduct') { + //https://developer.infusionsoft.com/docs/rest/#!/Product/createProductUsingPOST + if (operation === 'create') { + const productName = this.getNodeParameter('productName', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IEcommerceProduct = { + product_name: productName, + }; + keysToSnakeCase(additionalFields); + Object.assign(body, additionalFields); + responseData = await infusionsoftApiRequest.call(this, 'POST', '/products', body); + } + //https://developer.infusionsoft.com/docs/rest/#!/Product/deleteProductUsingDELETE + if (operation === 'delete') { + const productId = this.getNodeParameter('productId', i) as string; + responseData = await infusionsoftApiRequest.call(this, 'DELETE', `/products/${productId}`); + responseData = { success: true }; + } + //https://developer.infusionsoft.com/docs/rest/#!/Product/retrieveProductUsingGET + if (operation === 'get') { + const productId = this.getNodeParameter('productId', i) as string; + responseData = await infusionsoftApiRequest.call(this, 'get', `/products/${productId}`); + } + //https://developer.infusionsoft.com/docs/rest/#!/Product/listProductsUsingGET + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + keysToSnakeCase(filters); + Object.assign(qs, filters); + if (returnAll) { + responseData = await infusionsoftApiRequestAllItems.call(this, 'products', 'GET', '/products', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await infusionsoftApiRequest.call(this, 'GET', '/products', {}, qs); + responseData = responseData.products; + } + } + } + if (resource === 'email') { + //https://developer.infusionsoft.com/docs/rest/#!/Email/createEmailUsingPOST + if (operation === 'createRecord') { + const sentFromAddress = this.getNodeParameter('sentFromAddress', i) as string; + const sendToAddress = this.getNodeParameter('sentToAddress', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IDataObject = { + sent_to_address: sendToAddress, + sent_from_address: sentFromAddress, + }; + Object.assign(body, additionalFields); + keysToSnakeCase(body as IDataObject); + responseData = await infusionsoftApiRequest.call(this, 'POST', '/emails', body); + } + //https://developer.infusionsoft.com/docs/rest/#!/Email/deleteEmailUsingDELETE + if (operation === 'deleteRecord') { + const emailRecordId = parseInt(this.getNodeParameter('emailRecordId', i) as string, 10); + responseData = await infusionsoftApiRequest.call(this, 'DELETE', `/emails/${emailRecordId}`); + responseData = { success: true }; + } + //https://developer.infusionsoft.com/docs/rest/#!/Email/listEmailsUsingGET + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + keysToSnakeCase(filters); + Object.assign(qs, filters); + if (returnAll) { + responseData = await infusionsoftApiRequestAllItems.call(this, 'emails', 'GET', '/emails', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await infusionsoftApiRequest.call(this, 'GET', '/emails', {}, qs); + responseData = responseData.emails; + } + } + //https://developer.infusionsoft.com/docs/rest/#!/Email/deleteEmailUsingDELETE + if (operation === 'send') { + const userId = this.getNodeParameter('userId', i) as number; + const contactIds = ((this.getNodeParameter('contactIds', i) as string).split(',') as string[]).map((e) => (parseInt(e, 10))); + const subject = this.getNodeParameter('subject', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IEmail = { + user_id: userId, + contacts: contactIds, + subject, + }; + keysToSnakeCase(additionalFields); + Object.assign(body, additionalFields); + + const attachmentsUi = this.getNodeParameter('attachmentsUi', i) as IDataObject; + let attachments: IAttachment[] = []; + if (attachmentsUi) { + if (attachmentsUi.attachmentsValues) { + keysToSnakeCase(attachmentsUi.attachmentsValues as IDataObject); + attachments = attachmentsUi.attachmentsValues as IAttachment[]; + } + if (attachmentsUi.attachmentsBinary + && (attachmentsUi.attachmentsBinary as IDataObject).length) { + + if (items[i].binary === undefined) { + throw new Error('No binary data exists on item!'); + } + + for (const { property } of attachmentsUi.attachmentsBinary as IDataObject[]) { + + const item = items[i].binary as IBinaryKeyData; + + if (item[property as string] === undefined) { + throw new Error(`Binary data property "${property}" does not exists on item!`); + } + + attachments.push({ + file_data: item[property as string].data, + file_name: item[property as string].fileName, + }); + } + } + body.attachments = attachments; + } + + responseData = await infusionsoftApiRequest.call(this, 'POST', '/emails/queue', body); + responseData = { success: true }; + } + } + if (resource === 'file') { + //https://developer.infusionsoft.com/docs/rest/#!/File/deleteFileUsingDELETE + if (operation === 'delete') { + const fileId = parseInt(this.getNodeParameter('fileId', i) as string, 10); + responseData = await infusionsoftApiRequest.call(this, 'DELETE', `/files/${fileId}`); + responseData = { success: true }; + } + //https://developer.infusionsoft.com/docs/rest/#!/File/listFilesUsingGET + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + keysToSnakeCase(filters); + Object.assign(qs, filters); + if (qs.permission) { + qs.permission = (qs.permission as string).toUpperCase(); + } + if (qs.type) { + qs.type = titleCase(qs.type as string); + } + if (qs.viewable) { + qs.viewable = (qs.viewable as string).toUpperCase(); + } + if (returnAll) { + responseData = await infusionsoftApiRequestAllItems.call(this, 'files', 'GET', '/files', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await infusionsoftApiRequest.call(this, 'GET', '/files', {}, qs); + responseData = responseData.files; + } + } + //https://developer.infusionsoft.com/docs/rest/#!/File/createFileUsingPOST + if (operation === 'upload') { + const binaryData = this.getNodeParameter('binaryData', i) as boolean; + const fileAssociation = this.getNodeParameter('fileAssociation', i) as string; + const isPublic = this.getNodeParameter('isPublic', i) as boolean; + const body: IFile = { + is_public: isPublic, + file_association: fileAssociation.toUpperCase(), + }; + if (fileAssociation === 'contact') { + const contactId = parseInt(this.getNodeParameter('contactId', i) as string, 10); + body.contact_id = contactId; + } + if (binaryData) { + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string; + + if (items[i].binary === undefined) { + throw new Error('No binary data exists on item!'); + } + + const item = items[i].binary as IBinaryKeyData; + + if (item[binaryPropertyName as string] === undefined) { + throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`); + } + + body.file_data = item[binaryPropertyName as string].data; + body.file_name = item[binaryPropertyName as string].fileName; + + } else { + const fileName = this.getNodeParameter('fileName', i) as string; + const fileData = this.getNodeParameter('fileData', i) as string; + body.file_name = fileName; + body.file_data = fileData; + } + responseData = await infusionsoftApiRequest.call(this, 'POST', '/files', body); + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Infusionsoft/InfusionsoftTrigger.node.ts b/packages/nodes-base/nodes/Infusionsoft/InfusionsoftTrigger.node.ts new file mode 100644 index 0000000000..756b170d63 --- /dev/null +++ b/packages/nodes-base/nodes/Infusionsoft/InfusionsoftTrigger.node.ts @@ -0,0 +1,196 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeTypeDescription, + INodeType, + IWebhookResponseData, + ILoadOptionsFunctions, + INodePropertyOptions, +} from 'n8n-workflow'; + +import { + infusionsoftApiRequest, +} from './GenericFunctions'; + +import { + titleCase, + } from 'change-case'; + +export class InfusionsoftTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Infusionsoft Trigger', + name: 'infusionsoftTrigger', + icon: 'file:infusionsoft.png', + group: ['trigger'], + version: 1, + subtitle: '={{$parameter["eventId"]}}', + description: 'Starts the workflow when Infusionsoft events occure.', + defaults: { + name: 'Infusionsoft Trigger', + color: '#79af53', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'infusionsoftOAuth2Api', + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Event', + name: 'eventId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getEvents', + }, + default: '', + required: true, + }, + { + displayName: 'RAW Data', + name: 'rawData', + type: 'boolean', + default: false, + description: `Returns the data exactly in the way it got received from the API.`, + }, + ], + }; + + methods = { + loadOptions: { + // Get all the event types to display them to user so that he can + // select them easily + async getEvents(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const hooks = await infusionsoftApiRequest.call(this, 'GET', '/hooks/event_keys'); + for (const hook of hooks) { + const hookName = hook; + const hookId = hook; + returnData.push({ + name: titleCase((hookName as string).replace('.', ' ')), + value: hookId as string, + }); + } + return returnData; + }, + }, + }; + + // @ts-ignore (because of request) + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const eventId = this.getNodeParameter('eventId') as string; + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + + const responseData = await infusionsoftApiRequest.call(this, 'GET', '/hooks', {}); + + for (const existingData of responseData) { + if (existingData.hookUrl === webhookUrl + && existingData.eventKey === eventId + && existingData.status === 'Verified') { + // The webhook exists already + webhookData.webhookId = existingData.key; + return true; + } + } + + return false; + }, + async create(this: IHookFunctions): Promise { + const eventId = this.getNodeParameter('eventId') as string; + const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default'); + + const body = { + eventKey: eventId, + hookUrl: webhookUrl, + }; + + const responseData = await infusionsoftApiRequest.call(this, 'POST', '/hooks', body); + + if (responseData.key === undefined) { + // Required data is missing so was not successful + return false; + } + + webhookData.webhookId = responseData.key as string; + + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + + if (webhookData.webhookId !== undefined) { + + try { + await infusionsoftApiRequest.call(this, 'DELETE', `/hooks/${webhookData.webhookId}`); + } catch (e) { + return false; + } + + // Remove from the static workflow data so that it is clear + // that no webhooks are registred anymore + delete webhookData.webhookId; + } + + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const rawData = this.getNodeParameter('rawData') as boolean; + const headers = this.getHeaderData() as IDataObject; + const bodyData = this.getBodyData() as IDataObject; + + if (headers['x-hook-secret']) { + // Is a create webhook confirmation request + const res = this.getResponseObject(); + res.set('x-hook-secret', headers['x-hook-secret'] as string); + res.status(200).end(); + return { + noWebhookResponse: true, + }; + } + + if (rawData) { + return { + workflowData: [ + this.helpers.returnJsonArray(bodyData), + ], + }; + } + + const responseData: IDataObject[] = []; + for (const data of bodyData.object_keys as IDataObject[]) { + responseData.push({ + eventKey: bodyData.event_key, + objectType: bodyData.object_type, + id: data.id, + timestamp: data.timestamp, + apiUrl: data.apiUrl, + }); + } + return { + workflowData: [ + this.helpers.returnJsonArray(responseData), + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Infusionsoft/infusionsoft.png b/packages/nodes-base/nodes/Infusionsoft/infusionsoft.png new file mode 100644 index 0000000000000000000000000000000000000000..4cf546680258f9fd6efd9b203d52103e1864b1ea GIT binary patch literal 4307 zcmY*dc|25Y*gj)7iAt8NL&=sgMk-^?zLRZilQEVt+09^tC@LW)vMZ!y9cvWHR>WAc zFG)nUF!m+XM{nQze&2I`_qng%mW4h;JB`ihUsB8ISM9* z!+Y$>wc^7B!kcQV0pu>e`NK_wvz4xkp&=k~h?xL-&?$iSh;n!VARd7J4+a1|5bwX( z6eRYSLjwT!aRA+4j?G~{S~`busQ#O2vq1kjX3_l1Hp!y>4pfKrwy$_{|M^%KeFV6X}^O3)@ zjN%{W|EvA&qbPmU{Qn&0pGp5z4@Xr7D@yhJO=49S_>h z=>GL?HsDr0{!+}t#inGH3OjuIPXA!yD?@6reCBmVC)L-pW+N}Z3+0=m^OjN7d=6gX>u&b?vap{48k)m$}Tri}PWmAk(Y672|Y zT7*UaTnka|N_7lm^cS4SZ_3%zj)`TPux5;A%HsEX69w(h`BoZEZSM<0n2HjPx5M9! zdtwZ=@Oy~g>Gv;@_n%QKYTZl7X+pe~=dVoBL8X+x*{06M(D%MxQthMAbwB!X(0SRu zesWz!scEgV0>e4YV+q9NC^ge|iKMdw{M7cnx*uk?q1K;!DzF}LA+Fg_3`x?C4tcuG zNozEI;QEzMd=RlNG_iSnR&P0E>8EIb;BJ=GGXYy}W-Te4_c5h(2I3hv?H1+X#0GRw zycM|FVLgLQ{v>C!g5W34g}!P$$P)Dz9vcrq0Oe&I)g3;_ZJ5HxphRsjzv8T8j7L# z!O~JIRiCH16dJB({chW~v|Ib$qvTdw9D>h*w_9bZs)+U*1P(Q#kNZQd?TwRCsY(K^Tec%nYyo$^HxY2Y~iSNUH5Ap_q~R zx(kmNKFUcyS(X#iTnk99O@&UPv7$ zF5RH;$|LXFwbcqM_T{nh6)-sce4|VI{Ion+Gq+{^^N_**&K#rLyO`zc1!w0(-w0c) z$=4Jy|GW<-_yYVy15d>rwZ_kxo;9_v@arK}+S@!=DOy!@RK55gCIj+?PtzY)E?5r8 z-##e(y)(<+XwqGfa?+wW#Tk+f1&n=$LoV-#T$V&yn$1kij|lQC&9A|8AJ3J4>L->t zlq#z044ex4nEKFUkUn+unuqVbCm9h_nIei&(6{mPLi3Xj7L%wnsWta>t~5?Gd`1?P zVkHTKJLJ-l5&{xaG^a~t7dBUTb`=`cm|zGcR)cF8>$fJey$VN^6*n!aUgAxQwR)cv zOC4vs+s-r1@Mr`5)3mvaFphMmr|U_n%3%*KGKQbQsBPg4_#&J+t*XQ>3HgO@Hfvjcj^E$5vpi^U-Xc)}he>W8(VC@j7Xn>R_j>!VH727;5P+j(b~iQpArk?)vc!n#kg zKht~+irp=xcl8skLDAD=2@F@E9RdxCGmq@nbtbEYe9YkW&=ePth?5bl4UzwxF2T0S z!|~Ol#KAX-lk~%JlcDOoH<$VWm)j0;eg-TJ=3tNLp9V8=%=xMP6<`Vh2!JhrEXmL1i2*#XnWn2;OaYLS`Ny+1fC%4K}u(;2l} zA0)7<;0MM@j?lx(teIirCmF29E!BmW3Q?MTL z>1kzL;Ro+icZCF2{R@;Szq9RK>uo}I?fKp)5Am&V5C*s@NmBTvmLA$(3tw6^#;u;A zPir{GJNMX&mpRDQo53w(;+JjuDA=XE4a3n2S1Qy##4ltc_a_ja5F>Z_av=(ZD$mYt zjD7j-RAxrM&oaosHTRBuejbNQ4S!Of1Qd=nw-eI~#xi+E{ZP1*dA&Ps^^0w_`$785 zz@WW%BRzFsXh>nbXCo5(G*F9I?#AQeg&Lu$!EL%>V1$Bx+M-oag2ib=_^f$n^7kAG zoZ-cxVzIvFu{(E}<67~t&&0PQnkR!Ud5mtn%f(4g?%U6HJ^4I1JeGWD&O}>H)ooD6CV=u@*I4_;5`?h%#D;5Pmdm3C!*Rd3~ z`>cC5D8H)8A1UG6#T@o=8+h6-?821v@^c?Wy2{1euHjgSNE(j!f|d$yj!}DMObv>K zEJS`NjYn|i2*of^GH6}#&wA$0+E%#-GR;~F?`BVo;VAMyK=F3WrkUfN645VHDhg-X z*5YNKe7*JFphH%{+ZcCI3&u~OyF!!C#li8tQLWIs!^XyjX$O~FL_h3x?c9*3<%Coj zr_s(MC7(s}Y-SHta=6u(e85}EO#QLikb=NZb5Fj-v?AI=GI*LtL!hw~8=HwWEJ^7+ z7#Hp=H4+?2qPppaD(n+$ z>=UF+G#3%f|7IaP2wA;sUDnO58ee$(Lt+TYZ8LAZV}$)(Ys(JB!bz#Zk{ll5*vv{| zXs*0*d)g)g>4LhsB^=Z=*Ty|n(8SMzEwqybf7&EF`@U;)wdzfKx+{#eaHpQyX zNN%A8;zQpN!_^t_^rPIdkk@LaD2J;bEzA_RMA*rtxsQ#x0&VK;#8>6xFDw%y#X;sU zj@&6K$Hp1$@BKX$57#B7XRtdIbJ@n)SSKQ#taunop*Ca8`r~4o&1ztUFTEEr1A`1 z5piSl5qHHlC5x$6O~(t6qtpPq?f5EmK07s#-@y(ib9B1SY^as4(^**D!9 zdi_$pQ9I2Sh7Y2S+FYNu983@z)&hFlQ4O4JK zh_#t14R>_|=mAZVT7s;Ps_mzCsVf1}=<8r2#rv(8w6qt^b0|8WB@Fi~i6Nd4*RUr) zggyL%5oG~7Mi;(_$mZv_N>i)C8QC}&zt)Eepf63NNrJ)P_DRCW-E&RnC>>`=IQlu7 zhqeQNL6|u#wX@|RuXd=?n#E{MZEf_{;t2lCgcN-TWg@tx_rNJNMGBvcCcUq)nNg=9 zslFCr?E|TWKE*fQp=Txa_L`n!7>TZYRb8EVl7)dQJM=9lr*(2>rb<*UUmKhq?efBz zRbG|r+!BbSSSEW$A1+FHK499HYp8H%Jv7&YI$k#g+0>j9NFZ3D1+TcFMgVr;p zG;~?F<6Tn$^@A6Xl!1W(MxUPb%B8V46$RDR+Y)^jxtifz4e+`v{ukJ{CrZ}!ZqJgO zU5cDo(3ohDLjzoQ28r@ZTx*vsv67RMv!AGWi3{%#1Ug4fa%UuHr8ALBrdi@(3T6W8~c3m!qIl%wBrQoczWM4s6RUs}2yvSI z7v9+@DfPsGl`OW)z1i90Npfiu(wqT&4Owk8dVy^%RiKvSIUS(Z}WIOdGh2}Y-}t& zw;8nAH1e($+S+GlWm32XIs(T4=V>CF_q8@;T{==A Date: Wed, 1 Apr 2020 18:55:40 -0400 Subject: [PATCH 045/165] :zap: small fix --- .../nodes/Infusionsoft/CompanyDescription.ts | 2 +- .../nodes/Infusionsoft/CompanyInterface.ts | 2 +- .../nodes/Infusionsoft/ConctactInterface.ts | 2 +- .../nodes/Infusionsoft/ContactDescription.ts | 2 +- .../Infusionsoft/ContactNoteDescription.ts | 2 +- .../Infusionsoft/ContactTagDescription.ts | 2 +- .../Infusionsoft/EcommerceOrderDescripion.ts | 2 +- .../EcommerceProductDescription.ts | 2 +- .../nodes/Infusionsoft/EmailDescription.ts | 2 +- .../nodes/Infusionsoft/FileDescription.ts | 2 +- packages/nodes-base/package.json | 34 +++++++++---------- 11 files changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/nodes-base/nodes/Infusionsoft/CompanyDescription.ts b/packages/nodes-base/nodes/Infusionsoft/CompanyDescription.ts index b0278d856c..edb73d1ce1 100644 --- a/packages/nodes-base/nodes/Infusionsoft/CompanyDescription.ts +++ b/packages/nodes-base/nodes/Infusionsoft/CompanyDescription.ts @@ -1,6 +1,6 @@ import { INodeProperties, - } from "n8n-workflow"; + } from 'n8n-workflow'; export const companyOperations = [ { diff --git a/packages/nodes-base/nodes/Infusionsoft/CompanyInterface.ts b/packages/nodes-base/nodes/Infusionsoft/CompanyInterface.ts index c8006e9f41..7bf32c78e5 100644 --- a/packages/nodes-base/nodes/Infusionsoft/CompanyInterface.ts +++ b/packages/nodes-base/nodes/Infusionsoft/CompanyInterface.ts @@ -1,4 +1,4 @@ -import { IDataObject } from "n8n-workflow"; +import { IDataObject } from 'n8n-workflow'; export interface ICompany { address?: IDataObject; diff --git a/packages/nodes-base/nodes/Infusionsoft/ConctactInterface.ts b/packages/nodes-base/nodes/Infusionsoft/ConctactInterface.ts index 370888a17b..1bbdb6d6f1 100644 --- a/packages/nodes-base/nodes/Infusionsoft/ConctactInterface.ts +++ b/packages/nodes-base/nodes/Infusionsoft/ConctactInterface.ts @@ -1,6 +1,6 @@ import { IDataObject, - } from "n8n-workflow"; + } from 'n8n-workflow'; export interface IAddress { country_code?: string; diff --git a/packages/nodes-base/nodes/Infusionsoft/ContactDescription.ts b/packages/nodes-base/nodes/Infusionsoft/ContactDescription.ts index 198224fff6..a70e417762 100644 --- a/packages/nodes-base/nodes/Infusionsoft/ContactDescription.ts +++ b/packages/nodes-base/nodes/Infusionsoft/ContactDescription.ts @@ -1,6 +1,6 @@ import { INodeProperties, - } from "n8n-workflow"; + } from 'n8n-workflow'; export const contactOperations = [ { diff --git a/packages/nodes-base/nodes/Infusionsoft/ContactNoteDescription.ts b/packages/nodes-base/nodes/Infusionsoft/ContactNoteDescription.ts index d916702b20..aa625c3b63 100644 --- a/packages/nodes-base/nodes/Infusionsoft/ContactNoteDescription.ts +++ b/packages/nodes-base/nodes/Infusionsoft/ContactNoteDescription.ts @@ -1,6 +1,6 @@ import { INodeProperties, - } from "n8n-workflow"; + } from 'n8n-workflow'; export const contactNoteOperations = [ { diff --git a/packages/nodes-base/nodes/Infusionsoft/ContactTagDescription.ts b/packages/nodes-base/nodes/Infusionsoft/ContactTagDescription.ts index 9643530ad4..a212c112ae 100644 --- a/packages/nodes-base/nodes/Infusionsoft/ContactTagDescription.ts +++ b/packages/nodes-base/nodes/Infusionsoft/ContactTagDescription.ts @@ -1,6 +1,6 @@ import { INodeProperties, - } from "n8n-workflow"; + } from 'n8n-workflow'; export const contactTagOperations = [ { diff --git a/packages/nodes-base/nodes/Infusionsoft/EcommerceOrderDescripion.ts b/packages/nodes-base/nodes/Infusionsoft/EcommerceOrderDescripion.ts index df601111fb..2d43239fb7 100644 --- a/packages/nodes-base/nodes/Infusionsoft/EcommerceOrderDescripion.ts +++ b/packages/nodes-base/nodes/Infusionsoft/EcommerceOrderDescripion.ts @@ -1,6 +1,6 @@ import { INodeProperties, - } from "n8n-workflow"; + } from 'n8n-workflow'; export const ecommerceOrderOperations = [ { diff --git a/packages/nodes-base/nodes/Infusionsoft/EcommerceProductDescription.ts b/packages/nodes-base/nodes/Infusionsoft/EcommerceProductDescription.ts index f0b3ef3643..09ec6ea09a 100644 --- a/packages/nodes-base/nodes/Infusionsoft/EcommerceProductDescription.ts +++ b/packages/nodes-base/nodes/Infusionsoft/EcommerceProductDescription.ts @@ -1,6 +1,6 @@ import { INodeProperties, - } from "n8n-workflow"; + } from 'n8n-workflow'; export const ecommerceProductOperations = [ { diff --git a/packages/nodes-base/nodes/Infusionsoft/EmailDescription.ts b/packages/nodes-base/nodes/Infusionsoft/EmailDescription.ts index f7875c13f6..0f70152500 100644 --- a/packages/nodes-base/nodes/Infusionsoft/EmailDescription.ts +++ b/packages/nodes-base/nodes/Infusionsoft/EmailDescription.ts @@ -1,6 +1,6 @@ import { INodeProperties, - } from "n8n-workflow"; + } from 'n8n-workflow'; export const emailOperations = [ { diff --git a/packages/nodes-base/nodes/Infusionsoft/FileDescription.ts b/packages/nodes-base/nodes/Infusionsoft/FileDescription.ts index 9d90f4411c..863590a611 100644 --- a/packages/nodes-base/nodes/Infusionsoft/FileDescription.ts +++ b/packages/nodes-base/nodes/Infusionsoft/FileDescription.ts @@ -1,6 +1,6 @@ import { INodeProperties, - } from "n8n-workflow"; + } from 'n8n-workflow'; export const fileOperations = [ { diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index e30a66b893..9cc9d2bc6d 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -42,15 +42,15 @@ "dist/credentials/GithubApi.credentials.js", "dist/credentials/GithubOAuth2Api.credentials.js", "dist/credentials/GitlabApi.credentials.js", - "dist/credentials/GoogleApi.credentials.js", - "dist/credentials/GoogleOAuth2Api.credentials.js", + "dist/credentials/GoogleApi.credentials.js", + "dist/credentials/GoogleOAuth2Api.credentials.js", "dist/credentials/HttpBasicAuth.credentials.js", "dist/credentials/HttpDigestAuth.credentials.js", "dist/credentials/HttpHeaderAuth.credentials.js", "dist/credentials/HubspotApi.credentials.js", "dist/credentials/Imap.credentials.js", - "dist/credentials/IntercomApi.credentials.js", - "dist/credentials/InfusionsoftOAuth2Api.credentials.js", + "dist/credentials/IntercomApi.credentials.js", + "dist/credentials/InfusionsoftOAuth2Api.credentials.js", "dist/credentials/JiraSoftwareCloudApi.credentials.js", "dist/credentials/LinkFishApi.credentials.js", "dist/credentials/MailchimpApi.credentials.js", @@ -83,8 +83,8 @@ "dist/credentials/TypeformApi.credentials.js", "dist/credentials/TogglApi.credentials.js", "dist/credentials/VeroApi.credentials.js", - "dist/credentials/WordpressApi.credentials.js", - "dist/credentials/ZohoOAuth2Api.credentials.js" + "dist/credentials/WordpressApi.credentials.js", + "dist/credentials/ZohoOAuth2Api.credentials.js" ], "nodes": [ "dist/nodes/ActiveCampaign/ActiveCampaign.node.js", @@ -118,8 +118,8 @@ "dist/nodes/Github/Github.node.js", "dist/nodes/Github/GithubTrigger.node.js", "dist/nodes/Gitlab/Gitlab.node.js", - "dist/nodes/Gitlab/GitlabTrigger.node.js", - "dist/nodes/Google/GoogleCalendar.node.js", + "dist/nodes/Gitlab/GitlabTrigger.node.js", + "dist/nodes/Google/GoogleCalendar.node.js", "dist/nodes/Google/GoogleDrive.node.js", "dist/nodes/Google/GoogleSheets.node.js", "dist/nodes/GraphQL/GraphQL.node.js", @@ -128,9 +128,9 @@ "dist/nodes/Hubspot/Hubspot.node.js", "dist/nodes/If.node.js", "dist/nodes/Interval.node.js", - "dist/nodes/Intercom/Intercom.node.js", - "dist/nodes/Infusionsoft/Infusionsoft.node.js", - "dist/nodes/Infusionsoft/InfusionsoftTrigger.node.js", + "dist/nodes/Intercom/Intercom.node.js", + "dist/nodes/Infusionsoft/Infusionsoft.node.js", + "dist/nodes/Infusionsoft/InfusionsoftTrigger.node.js", "dist/nodes/Jira/JiraSoftwareCloud.node.js", "dist/nodes/LinkFish/LinkFish.node.js", "dist/nodes/Mailchimp/Mailchimp.node.js", @@ -181,8 +181,8 @@ "dist/nodes/WriteBinaryFile.node.js", "dist/nodes/Webhook.node.js", "dist/nodes/Wordpress/Wordpress.node.js", - "dist/nodes/Xml.node.js", - "dist/nodes/Zoho/ZohoCrm.node.js" + "dist/nodes/Xml.node.js", + "dist/nodes/Zoho/ZohoCrm.node.js" ] }, "devDependencies": { @@ -195,8 +195,8 @@ "@types/gm": "^1.18.2", "@types/imap-simple": "^4.2.0", "@types/jest": "^24.0.18", - "@types/lodash.set": "^4.3.6", - "@types/moment-timezone": "^0.5.12", + "@types/lodash.set": "^4.3.6", + "@types/moment-timezone": "^0.5.12", "@types/mongodb": "^3.3.6", "@types/node": "^10.10.1", "@types/nodemailer": "^4.6.5", @@ -222,8 +222,8 @@ "imap-simple": "^4.3.0", "lodash.get": "^4.4.2", "lodash.set": "^4.3.2", - "lodash.unset": "^4.5.2", - "moment-timezone": "0.5.28", + "lodash.unset": "^4.5.2", + "moment-timezone": "0.5.28", "mongodb": "^3.3.2", "mysql2": "^2.0.1", "n8n-core": "~0.20.0", From 3a71e2c978f2c69fb10d12dbc66762fb6d9a1186 Mon Sep 17 00:00:00 2001 From: ricardo Date: Thu, 2 Apr 2020 19:37:40 -0400 Subject: [PATCH 046/165] :zap: Renamed to Keap. --- ...ntials.ts => KeapOAuth2Api.credentials.ts} | 6 +- .../CompanyDescription.ts | 0 .../CompanyInterface.ts | 0 .../ConctactInterface.ts | 0 .../ContactDescription.ts | 20 ++-- .../ContactNoteDescription.ts | 0 .../ContactNoteInterface.ts | 0 .../ContactTagDescription.ts | 0 .../EcommerceOrderDescripion.ts | 0 .../EcommerceOrderInterface.ts | 0 .../EcommerceProductDescription.ts | 0 .../EcommerceProductInterface.ts | 0 .../{Infusionsoft => Keap}/EmaiIInterface.ts | 0 .../EmailDescription.ts | 0 .../{Infusionsoft => Keap}/FileDescription.ts | 0 .../{Infusionsoft => Keap}/FileInterface.ts | 0 .../GenericFunctions.ts | 8 +- .../Keap.node.ts} | 106 +++++++++--------- .../KeapTrigger.node.ts} | 22 ++-- .../infusionsoft.png => Keap/keap.png} | Bin packages/nodes-base/package.json | 6 +- 21 files changed, 84 insertions(+), 84 deletions(-) rename packages/nodes-base/credentials/{InfusionsoftOAuth2Api.credentials.ts => KeapOAuth2Api.credentials.ts} (86%) rename packages/nodes-base/nodes/{Infusionsoft => Keap}/CompanyDescription.ts (100%) rename packages/nodes-base/nodes/{Infusionsoft => Keap}/CompanyInterface.ts (100%) rename packages/nodes-base/nodes/{Infusionsoft => Keap}/ConctactInterface.ts (100%) rename packages/nodes-base/nodes/{Infusionsoft => Keap}/ContactDescription.ts (97%) rename packages/nodes-base/nodes/{Infusionsoft => Keap}/ContactNoteDescription.ts (100%) rename packages/nodes-base/nodes/{Infusionsoft => Keap}/ContactNoteInterface.ts (100%) rename packages/nodes-base/nodes/{Infusionsoft => Keap}/ContactTagDescription.ts (100%) rename packages/nodes-base/nodes/{Infusionsoft => Keap}/EcommerceOrderDescripion.ts (100%) rename packages/nodes-base/nodes/{Infusionsoft => Keap}/EcommerceOrderInterface.ts (100%) rename packages/nodes-base/nodes/{Infusionsoft => Keap}/EcommerceProductDescription.ts (100%) rename packages/nodes-base/nodes/{Infusionsoft => Keap}/EcommerceProductInterface.ts (100%) rename packages/nodes-base/nodes/{Infusionsoft => Keap}/EmaiIInterface.ts (100%) rename packages/nodes-base/nodes/{Infusionsoft => Keap}/EmailDescription.ts (100%) rename packages/nodes-base/nodes/{Infusionsoft => Keap}/FileDescription.ts (100%) rename packages/nodes-base/nodes/{Infusionsoft => Keap}/FileInterface.ts (100%) rename packages/nodes-base/nodes/{Infusionsoft => Keap}/GenericFunctions.ts (68%) rename packages/nodes-base/nodes/{Infusionsoft/Infusionsoft.node.ts => Keap/Keap.node.ts} (85%) rename packages/nodes-base/nodes/{Infusionsoft/InfusionsoftTrigger.node.ts => Keap/KeapTrigger.node.ts} (87%) rename packages/nodes-base/nodes/{Infusionsoft/infusionsoft.png => Keap/keap.png} (100%) diff --git a/packages/nodes-base/credentials/InfusionsoftOAuth2Api.credentials.ts b/packages/nodes-base/credentials/KeapOAuth2Api.credentials.ts similarity index 86% rename from packages/nodes-base/credentials/InfusionsoftOAuth2Api.credentials.ts rename to packages/nodes-base/credentials/KeapOAuth2Api.credentials.ts index 63bd890382..8bbe09d6fe 100644 --- a/packages/nodes-base/credentials/InfusionsoftOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/KeapOAuth2Api.credentials.ts @@ -7,12 +7,12 @@ const scopes = [ 'full', ]; -export class InfusionsoftOAuth2Api implements ICredentialType { - name = 'infusionsoftOAuth2Api'; +export class KeapOAuth2Api implements ICredentialType { + name = 'keapOAuth2Api'; extends = [ 'oAuth2Api', ]; - displayName = 'Infusionsoft OAuth2 API'; + displayName = 'Keap OAuth2 API'; properties = [ { displayName: 'Authorization URL', diff --git a/packages/nodes-base/nodes/Infusionsoft/CompanyDescription.ts b/packages/nodes-base/nodes/Keap/CompanyDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Infusionsoft/CompanyDescription.ts rename to packages/nodes-base/nodes/Keap/CompanyDescription.ts diff --git a/packages/nodes-base/nodes/Infusionsoft/CompanyInterface.ts b/packages/nodes-base/nodes/Keap/CompanyInterface.ts similarity index 100% rename from packages/nodes-base/nodes/Infusionsoft/CompanyInterface.ts rename to packages/nodes-base/nodes/Keap/CompanyInterface.ts diff --git a/packages/nodes-base/nodes/Infusionsoft/ConctactInterface.ts b/packages/nodes-base/nodes/Keap/ConctactInterface.ts similarity index 100% rename from packages/nodes-base/nodes/Infusionsoft/ConctactInterface.ts rename to packages/nodes-base/nodes/Keap/ConctactInterface.ts diff --git a/packages/nodes-base/nodes/Infusionsoft/ContactDescription.ts b/packages/nodes-base/nodes/Keap/ContactDescription.ts similarity index 97% rename from packages/nodes-base/nodes/Infusionsoft/ContactDescription.ts rename to packages/nodes-base/nodes/Keap/ContactDescription.ts index a70e417762..b1611c5182 100644 --- a/packages/nodes-base/nodes/Infusionsoft/ContactDescription.ts +++ b/packages/nodes-base/nodes/Keap/ContactDescription.ts @@ -17,7 +17,7 @@ export const contactOperations = [ options: [ { name: 'Create/Update', - value: 'create/update', + value: 'upsert', description: 'Create/update a contact', }, { @@ -36,7 +36,7 @@ export const contactOperations = [ description: 'Retrieve all contacts', }, ], - default: 'create/update', + default: 'upsert', description: 'The operation to perform.', }, ] as INodeProperties[]; @@ -44,7 +44,7 @@ export const contactOperations = [ export const contactFields = [ /* -------------------------------------------------------------------------- */ -/* contact:create/update */ +/* contact:upsert */ /* -------------------------------------------------------------------------- */ { displayName: 'Duplicate Option', @@ -64,7 +64,7 @@ export const contactFields = [ displayOptions: { show: { operation: [ - 'create/update', + 'upsert', ], resource: [ 'contact', @@ -84,7 +84,7 @@ export const contactFields = [ displayOptions: { show: { operation: [ - 'create/update', + 'upsert', ], resource: [ 'contact', @@ -250,7 +250,7 @@ export const contactFields = [ 'contact', ], operation: [ - 'create/update', + 'upsert', ], }, }, @@ -349,7 +349,7 @@ export const contactFields = [ 'contact', ], operation: [ - 'create/update', + 'upsert', ], }, }, @@ -403,7 +403,7 @@ export const contactFields = [ 'contact', ], operation: [ - 'create/update', + 'upsert', ], }, }, @@ -453,7 +453,7 @@ export const contactFields = [ 'contact', ], operation: [ - 'create/update', + 'upsert', ], }, }, @@ -515,7 +515,7 @@ export const contactFields = [ 'contact', ], operation: [ - 'create/update', + 'upsert', ], }, }, diff --git a/packages/nodes-base/nodes/Infusionsoft/ContactNoteDescription.ts b/packages/nodes-base/nodes/Keap/ContactNoteDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Infusionsoft/ContactNoteDescription.ts rename to packages/nodes-base/nodes/Keap/ContactNoteDescription.ts diff --git a/packages/nodes-base/nodes/Infusionsoft/ContactNoteInterface.ts b/packages/nodes-base/nodes/Keap/ContactNoteInterface.ts similarity index 100% rename from packages/nodes-base/nodes/Infusionsoft/ContactNoteInterface.ts rename to packages/nodes-base/nodes/Keap/ContactNoteInterface.ts diff --git a/packages/nodes-base/nodes/Infusionsoft/ContactTagDescription.ts b/packages/nodes-base/nodes/Keap/ContactTagDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Infusionsoft/ContactTagDescription.ts rename to packages/nodes-base/nodes/Keap/ContactTagDescription.ts diff --git a/packages/nodes-base/nodes/Infusionsoft/EcommerceOrderDescripion.ts b/packages/nodes-base/nodes/Keap/EcommerceOrderDescripion.ts similarity index 100% rename from packages/nodes-base/nodes/Infusionsoft/EcommerceOrderDescripion.ts rename to packages/nodes-base/nodes/Keap/EcommerceOrderDescripion.ts diff --git a/packages/nodes-base/nodes/Infusionsoft/EcommerceOrderInterface.ts b/packages/nodes-base/nodes/Keap/EcommerceOrderInterface.ts similarity index 100% rename from packages/nodes-base/nodes/Infusionsoft/EcommerceOrderInterface.ts rename to packages/nodes-base/nodes/Keap/EcommerceOrderInterface.ts diff --git a/packages/nodes-base/nodes/Infusionsoft/EcommerceProductDescription.ts b/packages/nodes-base/nodes/Keap/EcommerceProductDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Infusionsoft/EcommerceProductDescription.ts rename to packages/nodes-base/nodes/Keap/EcommerceProductDescription.ts diff --git a/packages/nodes-base/nodes/Infusionsoft/EcommerceProductInterface.ts b/packages/nodes-base/nodes/Keap/EcommerceProductInterface.ts similarity index 100% rename from packages/nodes-base/nodes/Infusionsoft/EcommerceProductInterface.ts rename to packages/nodes-base/nodes/Keap/EcommerceProductInterface.ts diff --git a/packages/nodes-base/nodes/Infusionsoft/EmaiIInterface.ts b/packages/nodes-base/nodes/Keap/EmaiIInterface.ts similarity index 100% rename from packages/nodes-base/nodes/Infusionsoft/EmaiIInterface.ts rename to packages/nodes-base/nodes/Keap/EmaiIInterface.ts diff --git a/packages/nodes-base/nodes/Infusionsoft/EmailDescription.ts b/packages/nodes-base/nodes/Keap/EmailDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Infusionsoft/EmailDescription.ts rename to packages/nodes-base/nodes/Keap/EmailDescription.ts diff --git a/packages/nodes-base/nodes/Infusionsoft/FileDescription.ts b/packages/nodes-base/nodes/Keap/FileDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Infusionsoft/FileDescription.ts rename to packages/nodes-base/nodes/Keap/FileDescription.ts diff --git a/packages/nodes-base/nodes/Infusionsoft/FileInterface.ts b/packages/nodes-base/nodes/Keap/FileInterface.ts similarity index 100% rename from packages/nodes-base/nodes/Infusionsoft/FileInterface.ts rename to packages/nodes-base/nodes/Keap/FileInterface.ts diff --git a/packages/nodes-base/nodes/Infusionsoft/GenericFunctions.ts b/packages/nodes-base/nodes/Keap/GenericFunctions.ts similarity index 68% rename from packages/nodes-base/nodes/Infusionsoft/GenericFunctions.ts rename to packages/nodes-base/nodes/Keap/GenericFunctions.ts index 4d942ccd64..c04fb057d5 100644 --- a/packages/nodes-base/nodes/Infusionsoft/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Keap/GenericFunctions.ts @@ -17,7 +17,7 @@ import { snakeCase, } from 'change-case'; -export async function infusionsoftApiRequest(this: IWebhookFunctions | IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}, option: IDataObject = {}): Promise { // tslint:disable-line:no-any +export async function keapApiRequest(this: IWebhookFunctions | IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}, option: IDataObject = {}): Promise { // tslint:disable-line:no-any let options: OptionsWithUri = { headers: { 'Content-Type': 'application/json', @@ -37,7 +37,7 @@ export async function infusionsoftApiRequest(this: IWebhookFunctions | IHookFunc delete options.body; } //@ts-ignore - return await this.helpers.requestOAuth.call(this, 'infusionsoftOAuth2Api', options); + return await this.helpers.requestOAuth.call(this, 'keapOAuth2Api', options); } catch (error) { if (error.response && error.response.body && error.response.body.message) { // Try to return the error prettier @@ -47,7 +47,7 @@ export async function infusionsoftApiRequest(this: IWebhookFunctions | IHookFunc } } -export async function infusionsoftApiRequestAllItems(this: IHookFunctions| IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any +export async function keapApiRequestAllItems(this: IHookFunctions| IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any const returnData: IDataObject[] = []; @@ -56,7 +56,7 @@ export async function infusionsoftApiRequestAllItems(this: IHookFunctions| IExec query.limit = 50; do { - responseData = await infusionsoftApiRequest.call(this, method, endpoint, body, query, uri); + responseData = await keapApiRequest.call(this, method, endpoint, body, query, uri); uri = responseData.next; returnData.push.apply(returnData, responseData[propertyName]); } while ( diff --git a/packages/nodes-base/nodes/Infusionsoft/Infusionsoft.node.ts b/packages/nodes-base/nodes/Keap/Keap.node.ts similarity index 85% rename from packages/nodes-base/nodes/Infusionsoft/Infusionsoft.node.ts rename to packages/nodes-base/nodes/Keap/Keap.node.ts index a2740c0f42..45c755ffd2 100644 --- a/packages/nodes-base/nodes/Infusionsoft/Infusionsoft.node.ts +++ b/packages/nodes-base/nodes/Keap/Keap.node.ts @@ -13,8 +13,8 @@ import { } from 'n8n-workflow'; import { - infusionsoftApiRequest, - infusionsoftApiRequestAllItems, + keapApiRequest, + keapApiRequestAllItems, keysToSnakeCase, } from './GenericFunctions'; @@ -101,24 +101,24 @@ import { import * as moment from 'moment-timezone'; -export class Infusionsoft implements INodeType { +export class Keap implements INodeType { description: INodeTypeDescription = { - displayName: 'Infusionsoft', - name: ' infusionsoft', - icon: 'file:infusionsoft.png', + displayName: 'Keap', + name: ' keap', + icon: 'file:keap.png', group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Infusionsoft API.', + description: 'Consume Keap API.', defaults: { - name: 'Infusionsoft', + name: 'Keap', color: '#79af53', }, inputs: ['main'], outputs: ['main'], credentials: [ { - name: 'infusionsoftOAuth2Api', + name: 'keapOAuth2Api', required: true, }, ], @@ -197,7 +197,7 @@ export class Infusionsoft implements INodeType { // select them easily async getTags(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - const tags = await infusionsoftApiRequestAllItems.call(this, 'tags', 'GET', '/tags'); + const tags = await keapApiRequestAllItems.call(this, 'tags', 'GET', '/tags'); for (const tag of tags) { const tagName = tag.name; const tagId = tag.id; @@ -212,7 +212,7 @@ export class Infusionsoft implements INodeType { // select them easily async getUsers(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - const users = await infusionsoftApiRequestAllItems.call(this, 'users', 'GET', '/users'); + const users = await keapApiRequestAllItems.call(this, 'users', 'GET', '/users'); for (const user of users) { const userName = user.given_name; const userId = user.id; @@ -227,7 +227,7 @@ export class Infusionsoft implements INodeType { // select them easily async getCountries(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - const { countries } = await infusionsoftApiRequest.call(this, 'GET', '/locales/countries'); + const { countries } = await keapApiRequest.call(this, 'GET', '/locales/countries'); for (const key of Object.keys(countries)) { const countryName = countries[key]; const countryId = key; @@ -243,7 +243,7 @@ export class Infusionsoft implements INodeType { async getProvinces(this: ILoadOptionsFunctions): Promise { const countryCode = this.getCurrentNodeParameter('countryCode') as string; const returnData: INodePropertyOptions[] = []; - const { provinces } = await infusionsoftApiRequest.call(this, 'GET', `/locales/countries/${countryCode}/provinces`); + const { provinces } = await keapApiRequest.call(this, 'GET', `/locales/countries/${countryCode}/provinces`); for (const key of Object.keys(provinces)) { const provinceName = provinces[key]; const provinceId = key; @@ -258,7 +258,7 @@ export class Infusionsoft implements INodeType { // select them easily async getContactTypes(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - const types = await infusionsoftApiRequest.call(this, 'GET', '/setting/contact/optionTypes'); + const types = await keapApiRequest.call(this, 'GET', '/setting/contact/optionTypes'); for (const type of types.value.split(',')) { const typeName = type; const typeId = type; @@ -296,7 +296,7 @@ export class Infusionsoft implements INodeType { const operation = this.getNodeParameter('operation', 0) as string; for (let i = 0; i < length; i++) { if (resource === 'company') { - //https://developer.infusionsoft.com/docs/rest/#!/Company/createCompanyUsingPOST + //https://developer.keap.com/docs/rest/#!/Company/createCompanyUsingPOST if (operation === 'create') { const addresses = (this.getNodeParameter('addressesUi', i) as IDataObject).addressesValues as IDataObject[]; const faxes = (this.getNodeParameter('faxesUi', i) as IDataObject).faxesValues as IDataObject[]; @@ -317,7 +317,7 @@ export class Infusionsoft implements INodeType { if (phones) { body.phone_number = phones[0]; } - responseData = await infusionsoftApiRequest.call(this, 'POST', '/companies', body); + responseData = await keapApiRequest.call(this, 'POST', '/companies', body); } //https://developer.infusionsoft.com/docs/rest/#!/Company/listCompaniesUsingGET if (operation === 'getAll') { @@ -330,17 +330,17 @@ export class Infusionsoft implements INodeType { delete qs.fields; } if (returnAll) { - responseData = await infusionsoftApiRequestAllItems.call(this, 'companies', 'GET', '/companies', {}, qs); + responseData = await keapApiRequestAllItems.call(this, 'companies', 'GET', '/companies', {}, qs); } else { qs.limit = this.getNodeParameter('limit', i) as number; - responseData = await infusionsoftApiRequest.call(this, 'GET', '/companies', {}, qs); + responseData = await keapApiRequest.call(this, 'GET', '/companies', {}, qs); responseData = responseData.companies; } } } if (resource === 'contact') { //https://developer.infusionsoft.com/docs/rest/#!/Contact/createOrUpdateContactUsingPUT - if (operation === 'create/update') { + if (operation === 'upsert') { const duplicateOption = this.getNodeParameter('duplicateOption', i) as string; const addresses = (this.getNodeParameter('addressesUi', i) as IDataObject).addressesValues as IDataObject[]; const emails = (this.getNodeParameter('emailsUi', i) as IDataObject).emailsValues as IDataObject[]; @@ -421,12 +421,12 @@ export class Infusionsoft implements INodeType { if (phones) { body.phone_numbers = phones as IPhone[]; } - responseData = await infusionsoftApiRequest.call(this, 'PUT', '/contacts', body); + responseData = await keapApiRequest.call(this, 'PUT', '/contacts', body); } //https://developer.infusionsoft.com/docs/rest/#!/Contact/deleteContactUsingDELETE if (operation === 'delete') { const contactId = parseInt(this.getNodeParameter('contactId', i) as string, 10); - responseData = await infusionsoftApiRequest.call(this, 'DELETE', `/contacts/${contactId}`); + responseData = await keapApiRequest.call(this, 'DELETE', `/contacts/${contactId}`); responseData = { success: true }; } //https://developer.infusionsoft.com/docs/rest/#!/Contact/getContactUsingGET @@ -436,7 +436,7 @@ export class Infusionsoft implements INodeType { if (options.fields) { qs.optional_properties = options.fields as string; } - responseData = await infusionsoftApiRequest.call(this, 'GET', `/contacts/${contactId}`, {}, qs); + responseData = await keapApiRequest.call(this, 'GET', `/contacts/${contactId}`, {}, qs); } //https://developer.infusionsoft.com/docs/rest/#!/Contact/listContactsUsingGET if (operation === 'getAll') { @@ -464,10 +464,10 @@ export class Infusionsoft implements INodeType { qs.until = options.until as string; } if (returnAll) { - responseData = await infusionsoftApiRequestAllItems.call(this, 'contacts', 'GET', '/contacts', {}, qs); + responseData = await keapApiRequestAllItems.call(this, 'contacts', 'GET', '/contacts', {}, qs); } else { qs.limit = this.getNodeParameter('limit', i) as number; - responseData = await infusionsoftApiRequest.call(this, 'GET', '/contacts', {}, qs); + responseData = await keapApiRequest.call(this, 'GET', '/contacts', {}, qs); responseData = responseData.contacts; } } @@ -487,18 +487,18 @@ export class Infusionsoft implements INodeType { additionalFields.type = pascalCase(additionalFields.type as string); } Object.assign(body, additionalFields); - responseData = await infusionsoftApiRequest.call(this, 'POST', '/notes', body); + responseData = await keapApiRequest.call(this, 'POST', '/notes', body); } //https://developer.infusionsoft.com/docs/rest/#!/Note/deleteNoteUsingDELETE if (operation === 'delete') { const noteId = this.getNodeParameter('noteId', i) as string; - responseData = await infusionsoftApiRequest.call(this, 'DELETE', `/notes/${noteId}`); + responseData = await keapApiRequest.call(this, 'DELETE', `/notes/${noteId}`); responseData = { success: true }; } //https://developer.infusionsoft.com/docs/rest/#!/Note/getNoteUsingGET if (operation === 'get') { const noteId = this.getNodeParameter('noteId', i) as string; - responseData = await infusionsoftApiRequest.call(this, 'GET', `/notes/${noteId}`); + responseData = await keapApiRequest.call(this, 'GET', `/notes/${noteId}`); } //https://developer.infusionsoft.com/docs/rest/#!/Note/listNotesUsingGET if (operation === 'getAll') { @@ -507,10 +507,10 @@ export class Infusionsoft implements INodeType { keysToSnakeCase(filters); Object.assign(qs, filters); if (returnAll) { - responseData = await infusionsoftApiRequestAllItems.call(this, 'notes', 'GET', '/notes', {}, qs); + responseData = await keapApiRequestAllItems.call(this, 'notes', 'GET', '/notes', {}, qs); } else { qs.limit = this.getNodeParameter('limit', i) as number; - responseData = await infusionsoftApiRequest.call(this, 'GET', '/notes', {}, qs); + responseData = await keapApiRequest.call(this, 'GET', '/notes', {}, qs); responseData = responseData.notes; } } @@ -524,7 +524,7 @@ export class Infusionsoft implements INodeType { additionalFields.type = pascalCase(additionalFields.type as string); } Object.assign(body, additionalFields); - responseData = await infusionsoftApiRequest.call(this, 'PATCH', `/notes/${noteId}`, body); + responseData = await keapApiRequest.call(this, 'PATCH', `/notes/${noteId}`, body); } } if (resource === 'contactTag') { @@ -535,14 +535,14 @@ export class Infusionsoft implements INodeType { const body: IDataObject = { tagIds, }; - responseData = await infusionsoftApiRequest.call(this, 'POST', `/contacts/${contactId}/tags`, body); + responseData = await keapApiRequest.call(this, 'POST', `/contacts/${contactId}/tags`, body); } //https://developer.infusionsoft.com/docs/rest/#!/Contact/removeTagsFromContactUsingDELETE_1 if (operation === 'delete') { const contactId = parseInt(this.getNodeParameter('contactId', i) as string, 10); const tagIds = this.getNodeParameter('tagIds', i) as string; qs.ids = tagIds; - responseData = await infusionsoftApiRequest.call(this, 'DELETE', `/contacts/${contactId}/tags`, {}, qs); + responseData = await keapApiRequest.call(this, 'DELETE', `/contacts/${contactId}/tags`, {}, qs); responseData = { success: true }; } //https://developer.infusionsoft.com/docs/rest/#!/Contact/listAppliedTagsUsingGET @@ -550,10 +550,10 @@ export class Infusionsoft implements INodeType { const returnAll = this.getNodeParameter('returnAll', i) as boolean; const contactId = parseInt(this.getNodeParameter('contactId', i) as string, 10); if (returnAll) { - responseData = await infusionsoftApiRequestAllItems.call(this, 'tags', 'GET', `/contacts/${contactId}/tags`, {}, qs); + responseData = await keapApiRequestAllItems.call(this, 'tags', 'GET', `/contacts/${contactId}/tags`, {}, qs); } else { qs.limit = this.getNodeParameter('limit', i) as number; - responseData = await infusionsoftApiRequest.call(this, 'GET', `/contacts/${contactId}/tags`, {}, qs); + responseData = await keapApiRequest.call(this, 'GET', `/contacts/${contactId}/tags`, {}, qs); responseData = responseData.tags; } } @@ -583,18 +583,18 @@ export class Infusionsoft implements INodeType { if (shippingAddress) { body.shipping_address = keysToSnakeCase(shippingAddress)[0] as IShippingAddress; } - responseData = await infusionsoftApiRequest.call(this, 'POST', '/orders', body); + responseData = await keapApiRequest.call(this, 'POST', '/orders', body); } //https://developer.infusionsoft.com/docs/rest/#!/E-Commerce/deleteOrderUsingDELETE if (operation === 'delete') { const orderId = parseInt(this.getNodeParameter('orderId', i) as string, 10); - responseData = await infusionsoftApiRequest.call(this, 'DELETE', `/orders/${orderId}`); + responseData = await keapApiRequest.call(this, 'DELETE', `/orders/${orderId}`); responseData = { success: true }; } //https://developer.infusionsoft.com/docs/rest/#!/E-Commerce/getOrderUsingGET if (operation === 'get') { const orderId = parseInt(this.getNodeParameter('orderId', i) as string, 10); - responseData = await infusionsoftApiRequest.call(this, 'get', `/orders/${orderId}`); + responseData = await keapApiRequest.call(this, 'get', `/orders/${orderId}`); } //https://developer.infusionsoft.com/docs/rest/#!/E-Commerce/listOrdersUsingGET if (operation === 'getAll') { @@ -603,10 +603,10 @@ export class Infusionsoft implements INodeType { keysToSnakeCase(options); Object.assign(qs, options); if (returnAll) { - responseData = await infusionsoftApiRequestAllItems.call(this, 'orders', 'GET', '/orders', {}, qs); + responseData = await keapApiRequestAllItems.call(this, 'orders', 'GET', '/orders', {}, qs); } else { qs.limit = this.getNodeParameter('limit', i) as number; - responseData = await infusionsoftApiRequest.call(this, 'GET', '/orders', {}, qs); + responseData = await keapApiRequest.call(this, 'GET', '/orders', {}, qs); responseData = responseData.orders; } } @@ -621,18 +621,18 @@ export class Infusionsoft implements INodeType { }; keysToSnakeCase(additionalFields); Object.assign(body, additionalFields); - responseData = await infusionsoftApiRequest.call(this, 'POST', '/products', body); + responseData = await keapApiRequest.call(this, 'POST', '/products', body); } //https://developer.infusionsoft.com/docs/rest/#!/Product/deleteProductUsingDELETE if (operation === 'delete') { const productId = this.getNodeParameter('productId', i) as string; - responseData = await infusionsoftApiRequest.call(this, 'DELETE', `/products/${productId}`); + responseData = await keapApiRequest.call(this, 'DELETE', `/products/${productId}`); responseData = { success: true }; } //https://developer.infusionsoft.com/docs/rest/#!/Product/retrieveProductUsingGET if (operation === 'get') { const productId = this.getNodeParameter('productId', i) as string; - responseData = await infusionsoftApiRequest.call(this, 'get', `/products/${productId}`); + responseData = await keapApiRequest.call(this, 'get', `/products/${productId}`); } //https://developer.infusionsoft.com/docs/rest/#!/Product/listProductsUsingGET if (operation === 'getAll') { @@ -641,10 +641,10 @@ export class Infusionsoft implements INodeType { keysToSnakeCase(filters); Object.assign(qs, filters); if (returnAll) { - responseData = await infusionsoftApiRequestAllItems.call(this, 'products', 'GET', '/products', {}, qs); + responseData = await keapApiRequestAllItems.call(this, 'products', 'GET', '/products', {}, qs); } else { qs.limit = this.getNodeParameter('limit', i) as number; - responseData = await infusionsoftApiRequest.call(this, 'GET', '/products', {}, qs); + responseData = await keapApiRequest.call(this, 'GET', '/products', {}, qs); responseData = responseData.products; } } @@ -661,12 +661,12 @@ export class Infusionsoft implements INodeType { }; Object.assign(body, additionalFields); keysToSnakeCase(body as IDataObject); - responseData = await infusionsoftApiRequest.call(this, 'POST', '/emails', body); + responseData = await keapApiRequest.call(this, 'POST', '/emails', body); } //https://developer.infusionsoft.com/docs/rest/#!/Email/deleteEmailUsingDELETE if (operation === 'deleteRecord') { const emailRecordId = parseInt(this.getNodeParameter('emailRecordId', i) as string, 10); - responseData = await infusionsoftApiRequest.call(this, 'DELETE', `/emails/${emailRecordId}`); + responseData = await keapApiRequest.call(this, 'DELETE', `/emails/${emailRecordId}`); responseData = { success: true }; } //https://developer.infusionsoft.com/docs/rest/#!/Email/listEmailsUsingGET @@ -676,10 +676,10 @@ export class Infusionsoft implements INodeType { keysToSnakeCase(filters); Object.assign(qs, filters); if (returnAll) { - responseData = await infusionsoftApiRequestAllItems.call(this, 'emails', 'GET', '/emails', {}, qs); + responseData = await keapApiRequestAllItems.call(this, 'emails', 'GET', '/emails', {}, qs); } else { qs.limit = this.getNodeParameter('limit', i) as number; - responseData = await infusionsoftApiRequest.call(this, 'GET', '/emails', {}, qs); + responseData = await keapApiRequest.call(this, 'GET', '/emails', {}, qs); responseData = responseData.emails; } } @@ -728,7 +728,7 @@ export class Infusionsoft implements INodeType { body.attachments = attachments; } - responseData = await infusionsoftApiRequest.call(this, 'POST', '/emails/queue', body); + responseData = await keapApiRequest.call(this, 'POST', '/emails/queue', body); responseData = { success: true }; } } @@ -736,7 +736,7 @@ export class Infusionsoft implements INodeType { //https://developer.infusionsoft.com/docs/rest/#!/File/deleteFileUsingDELETE if (operation === 'delete') { const fileId = parseInt(this.getNodeParameter('fileId', i) as string, 10); - responseData = await infusionsoftApiRequest.call(this, 'DELETE', `/files/${fileId}`); + responseData = await keapApiRequest.call(this, 'DELETE', `/files/${fileId}`); responseData = { success: true }; } //https://developer.infusionsoft.com/docs/rest/#!/File/listFilesUsingGET @@ -755,10 +755,10 @@ export class Infusionsoft implements INodeType { qs.viewable = (qs.viewable as string).toUpperCase(); } if (returnAll) { - responseData = await infusionsoftApiRequestAllItems.call(this, 'files', 'GET', '/files', {}, qs); + responseData = await keapApiRequestAllItems.call(this, 'files', 'GET', '/files', {}, qs); } else { qs.limit = this.getNodeParameter('limit', i) as number; - responseData = await infusionsoftApiRequest.call(this, 'GET', '/files', {}, qs); + responseData = await keapApiRequest.call(this, 'GET', '/files', {}, qs); responseData = responseData.files; } } @@ -797,7 +797,7 @@ export class Infusionsoft implements INodeType { body.file_name = fileName; body.file_data = fileData; } - responseData = await infusionsoftApiRequest.call(this, 'POST', '/files', body); + responseData = await keapApiRequest.call(this, 'POST', '/files', body); } } if (Array.isArray(responseData)) { diff --git a/packages/nodes-base/nodes/Infusionsoft/InfusionsoftTrigger.node.ts b/packages/nodes-base/nodes/Keap/KeapTrigger.node.ts similarity index 87% rename from packages/nodes-base/nodes/Infusionsoft/InfusionsoftTrigger.node.ts rename to packages/nodes-base/nodes/Keap/KeapTrigger.node.ts index 756b170d63..d7bb614d6a 100644 --- a/packages/nodes-base/nodes/Infusionsoft/InfusionsoftTrigger.node.ts +++ b/packages/nodes-base/nodes/Keap/KeapTrigger.node.ts @@ -13,31 +13,31 @@ import { } from 'n8n-workflow'; import { - infusionsoftApiRequest, + keapApiRequest, } from './GenericFunctions'; import { titleCase, } from 'change-case'; -export class InfusionsoftTrigger implements INodeType { +export class KeapTrigger implements INodeType { description: INodeTypeDescription = { - displayName: 'Infusionsoft Trigger', - name: 'infusionsoftTrigger', - icon: 'file:infusionsoft.png', + displayName: 'Keap Trigger', + name: 'keapTrigger', + icon: 'file:keap.png', group: ['trigger'], version: 1, subtitle: '={{$parameter["eventId"]}}', description: 'Starts the workflow when Infusionsoft events occure.', defaults: { - name: 'Infusionsoft Trigger', + name: 'Keap Trigger', color: '#79af53', }, inputs: [], outputs: ['main'], credentials: [ { - name: 'infusionsoftOAuth2Api', + name: 'keapOAuth2Api', required: true, }, ], @@ -76,7 +76,7 @@ export class InfusionsoftTrigger implements INodeType { // select them easily async getEvents(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - const hooks = await infusionsoftApiRequest.call(this, 'GET', '/hooks/event_keys'); + const hooks = await keapApiRequest.call(this, 'GET', '/hooks/event_keys'); for (const hook of hooks) { const hookName = hook; const hookId = hook; @@ -98,7 +98,7 @@ export class InfusionsoftTrigger implements INodeType { const webhookUrl = this.getNodeWebhookUrl('default'); const webhookData = this.getWorkflowStaticData('node'); - const responseData = await infusionsoftApiRequest.call(this, 'GET', '/hooks', {}); + const responseData = await keapApiRequest.call(this, 'GET', '/hooks', {}); for (const existingData of responseData) { if (existingData.hookUrl === webhookUrl @@ -122,7 +122,7 @@ export class InfusionsoftTrigger implements INodeType { hookUrl: webhookUrl, }; - const responseData = await infusionsoftApiRequest.call(this, 'POST', '/hooks', body); + const responseData = await keapApiRequest.call(this, 'POST', '/hooks', body); if (responseData.key === undefined) { // Required data is missing so was not successful @@ -139,7 +139,7 @@ export class InfusionsoftTrigger implements INodeType { if (webhookData.webhookId !== undefined) { try { - await infusionsoftApiRequest.call(this, 'DELETE', `/hooks/${webhookData.webhookId}`); + await keapApiRequest.call(this, 'DELETE', `/hooks/${webhookData.webhookId}`); } catch (e) { return false; } diff --git a/packages/nodes-base/nodes/Infusionsoft/infusionsoft.png b/packages/nodes-base/nodes/Keap/keap.png similarity index 100% rename from packages/nodes-base/nodes/Infusionsoft/infusionsoft.png rename to packages/nodes-base/nodes/Keap/keap.png diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 9cc9d2bc6d..1242479bfd 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -50,8 +50,8 @@ "dist/credentials/HubspotApi.credentials.js", "dist/credentials/Imap.credentials.js", "dist/credentials/IntercomApi.credentials.js", - "dist/credentials/InfusionsoftOAuth2Api.credentials.js", "dist/credentials/JiraSoftwareCloudApi.credentials.js", + "dist/credentials/KeapOAuth2Api.credentials.js", "dist/credentials/LinkFishApi.credentials.js", "dist/credentials/MailchimpApi.credentials.js", "dist/credentials/MailgunApi.credentials.js", @@ -129,9 +129,9 @@ "dist/nodes/If.node.js", "dist/nodes/Interval.node.js", "dist/nodes/Intercom/Intercom.node.js", - "dist/nodes/Infusionsoft/Infusionsoft.node.js", - "dist/nodes/Infusionsoft/InfusionsoftTrigger.node.js", "dist/nodes/Jira/JiraSoftwareCloud.node.js", + "dist/nodes/Keap/Keap.node.js", + "dist/nodes/Keap/KeapTrigger.node.js", "dist/nodes/LinkFish/LinkFish.node.js", "dist/nodes/Mailchimp/Mailchimp.node.js", "dist/nodes/Mailchimp/MailchimpTrigger.node.js", From 230e3f8209217aea3fc02c8ac2d4bdf626d2a758 Mon Sep 17 00:00:00 2001 From: ricardo Date: Fri, 3 Apr 2020 11:19:35 -0400 Subject: [PATCH 047/165] :zap: Added subfolders --- .../Microsoft/OneDrive/FolderDescription.ts | 26 +++++++++++++++++++ .../OneDrive/MicrosoftOneDrive.node.ts | 7 ++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/Microsoft/OneDrive/FolderDescription.ts b/packages/nodes-base/nodes/Microsoft/OneDrive/FolderDescription.ts index 62f488ae0c..55401bee5f 100644 --- a/packages/nodes-base/nodes/Microsoft/OneDrive/FolderDescription.ts +++ b/packages/nodes-base/nodes/Microsoft/OneDrive/FolderDescription.ts @@ -57,6 +57,32 @@ export const folderFields = [ default: '', description: `Folder's name`, }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'folder', + ], + }, + }, + default: {}, + placeholder: 'Add Field', + options: [ + { + displayName: 'Parent Folder ID', + name: 'parentFolderId', + type: 'string', + default: '', + description: 'ID of the folder you want to crate the new folder in', + }, + ], + }, /* -------------------------------------------------------------------------- */ /* folder:getChildren */ /* -------------------------------------------------------------------------- */ diff --git a/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.ts b/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.ts index 2af523cbc0..8b65b6b99f 100644 --- a/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.ts +++ b/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.ts @@ -196,11 +196,16 @@ export class MicrosoftOneDrive implements INodeType { //https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_post_children?view=odsp-graph-online if (operation === 'create') { const name = this.getNodeParameter('name', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; const body: IDataObject = { name, folder: {}, }; - responseData = await microsoftApiRequest.call(this, 'POST', '/drive/root/children', body); + let endpoint = '/drive/root/children'; + if (options.parentFolderId) { + endpoint = `/drive/items/${options.parentFolderId}/children`; + } + responseData = await microsoftApiRequest.call(this, 'POST', endpoint, body); returnData.push(responseData); } //https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_list_children?view=odsp-graph-online From 9816a3a711a928abe69c35ee84c0fc893e28e6e1 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 14 Apr 2020 21:07:53 +0200 Subject: [PATCH 048/165] :zap: Fix titleCase -> capitalCase --- packages/nodes-base/nodes/Keap/Keap.node.ts | 4 ++-- packages/nodes-base/nodes/Keap/KeapTrigger.node.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/nodes-base/nodes/Keap/Keap.node.ts b/packages/nodes-base/nodes/Keap/Keap.node.ts index 45c755ffd2..f67c07bbc4 100644 --- a/packages/nodes-base/nodes/Keap/Keap.node.ts +++ b/packages/nodes-base/nodes/Keap/Keap.node.ts @@ -95,8 +95,8 @@ import { } from './CompanyInterface'; import { + capitalCase, pascalCase, - titleCase, } from 'change-case'; import * as moment from 'moment-timezone'; @@ -749,7 +749,7 @@ export class Keap implements INodeType { qs.permission = (qs.permission as string).toUpperCase(); } if (qs.type) { - qs.type = titleCase(qs.type as string); + qs.type = capitalCase(qs.type as string); } if (qs.viewable) { qs.viewable = (qs.viewable as string).toUpperCase(); diff --git a/packages/nodes-base/nodes/Keap/KeapTrigger.node.ts b/packages/nodes-base/nodes/Keap/KeapTrigger.node.ts index d7bb614d6a..c5818165aa 100644 --- a/packages/nodes-base/nodes/Keap/KeapTrigger.node.ts +++ b/packages/nodes-base/nodes/Keap/KeapTrigger.node.ts @@ -17,7 +17,7 @@ import { } from './GenericFunctions'; import { - titleCase, + capitalCase, } from 'change-case'; export class KeapTrigger implements INodeType { @@ -81,7 +81,7 @@ export class KeapTrigger implements INodeType { const hookName = hook; const hookId = hook; returnData.push({ - name: titleCase((hookName as string).replace('.', ' ')), + name: capitalCase((hookName as string).replace('.', ' ')), value: hookId as string, }); } From 63505b4be06dad095284d4f48bd50f8d09ae4e4b Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 17 Apr 2020 23:18:03 +0200 Subject: [PATCH 049/165] :arrow_up: Set n8n-core@0.31.0 and n8n-workflow@0.28.0 on n8n-node-dev --- packages/node-dev/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/node-dev/package.json b/packages/node-dev/package.json index 9c1f053e99..3d17f2a328 100644 --- a/packages/node-dev/package.json +++ b/packages/node-dev/package.json @@ -58,8 +58,8 @@ "change-case": "^3.1.0", "copyfiles": "^2.1.1", "inquirer": "^7.0.0", - "n8n-core": "^0.21.0", - "n8n-workflow": "^0.20.0", + "n8n-core": "^0.31.0", + "n8n-workflow": "^0.28.0", "replace-in-file": "^4.1.0", "request": "^2.88.0", "tmp-promise": "^2.0.2", From 3d39aba93966c8d699cee7a0ec339b01afd2e271 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 17 Apr 2020 23:18:32 +0200 Subject: [PATCH 050/165] :bookmark: Release n8n-node-dev@0.7.0 --- packages/node-dev/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node-dev/package.json b/packages/node-dev/package.json index 3d17f2a328..82cd4911e2 100644 --- a/packages/node-dev/package.json +++ b/packages/node-dev/package.json @@ -1,6 +1,6 @@ { "name": "n8n-node-dev", - "version": "0.6.0", + "version": "0.7.0", "description": "CLI to simplify n8n credentials/node development", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From a380a9a3941e9059668aaf478555fee045827b1a Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 4 May 2020 08:56:01 +0200 Subject: [PATCH 051/165] :zap: Add first basic code for external hooks --- packages/cli/commands/start.ts | 7 ++ packages/cli/config/index.ts | 7 ++ packages/cli/src/ExternalHooks.ts | 89 +++++++++++++++++++ packages/cli/src/Interfaces.ts | 5 ++ packages/cli/src/Server.ts | 7 ++ .../cli/src/externalHooksTemp/test-hooks.ts | 10 +++ packages/cli/src/index.ts | 1 + 7 files changed, 126 insertions(+) create mode 100644 packages/cli/src/ExternalHooks.ts create mode 100644 packages/cli/src/externalHooksTemp/test-hooks.ts diff --git a/packages/cli/commands/start.ts b/packages/cli/commands/start.ts index 4e2efaacfd..65edec601d 100644 --- a/packages/cli/commands/start.ts +++ b/packages/cli/commands/start.ts @@ -12,6 +12,7 @@ import { ActiveWorkflowRunner, CredentialTypes, Db, + ExternalHooks, GenericHelpers, LoadNodesAndCredentials, NodeTypes, @@ -108,6 +109,12 @@ export class Start extends Command { const loadNodesAndCredentials = LoadNodesAndCredentials(); await loadNodesAndCredentials.init(); + // Load all external hooks + const externalHooks = ExternalHooks(); + await externalHooks.init(); + + // await externalHooks.run('credentials.new'); + // Add the found types to an instance other parts of the application can use const nodeTypes = NodeTypes(); await nodeTypes.init(loadNodesAndCredentials.nodeTypes); diff --git a/packages/cli/config/index.ts b/packages/cli/config/index.ts index 69b3154064..5705a5fc7d 100644 --- a/packages/cli/config/index.ts +++ b/packages/cli/config/index.ts @@ -252,6 +252,13 @@ const config = convict({ }, }, + externalHookFiles: { + doc: 'Files containing external hooks', + format: String, + default: '', + env: 'EXTERNAL_HOOK_FILES' + }, + nodes: { exclude: { doc: 'Nodes not to load', diff --git a/packages/cli/src/ExternalHooks.ts b/packages/cli/src/ExternalHooks.ts new file mode 100644 index 0000000000..2c00a525c6 --- /dev/null +++ b/packages/cli/src/ExternalHooks.ts @@ -0,0 +1,89 @@ +import { + Db, + IDatabaseCollections, + IExternalHooks, +} from "./"; + +import * as config from '../config'; +// import { +// access as fsAccess, +// readdir as fsReaddir, +// readFile as fsReadFile, +// stat as fsStat, +// } from 'fs'; + +// TODO: Give different name +interface IHookData { + DbCollections: IDatabaseCollections; +} + +// export EXTERNAL_HOOK_FILES=/data/packages/cli/dist/src/externalHooksTemp/test-hooks.js + +class ExternalHooksClass implements IExternalHooks { + + externalHooks: { + [key: string]: Array<() => {}> + } = {}; + + + async init(): Promise { + console.log('ExternalHooks.init'); + + const externalHookFiles = config.get('externalHookFiles').split(','); + + console.log('externalHookFiles'); + console.log(externalHookFiles); + + for (let hookFilePath of externalHookFiles) { + hookFilePath = hookFilePath.trim(); + if (hookFilePath !== '') { + console.log(' --- load: ' + hookFilePath); + const hookFile = require(hookFilePath); + + for (const resource of Object.keys(hookFile)) { + // if (this.externalHooks[resource] === undefined) { + // this.externalHooks[resource] = {}; + // } + + for (const operation of Object.keys(hookFile[resource])) { + const hookString = `${resource}.${operation}`; + if (this.externalHooks[hookString] === undefined) { + this.externalHooks[hookString] = []; + } + + this.externalHooks[hookString].push.apply(this.externalHooks[hookString], hookFile[resource][operation]); + } + } + } + } + } + + async run(hookName: string): Promise { + console.log('RUN NOW: ' + hookName); + + const hookData: IHookData = { + DbCollections: Db.collections, + }; + + if (this.externalHooks[hookName] === undefined) { + return; + } + + for(const externalHookFunction of this.externalHooks[hookName]) { + externalHookFunction.call(hookData); + } + } + +} + + + +let externalHooksInstance: ExternalHooksClass | undefined; + +export function ExternalHooks(): ExternalHooksClass { + if (externalHooksInstance === undefined) { + externalHooksInstance = new ExternalHooksClass(); + } + + return externalHooksInstance; +} diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index 225e02885f..b54e1eb299 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -188,6 +188,11 @@ export interface IExecutingWorkflowData { workflowExecution?: PCancelable; } +export interface IExternalHooks { + init(): Promise; + run(hookName: string): Promise; +} + export interface IN8nConfig { database: IN8nConfigDatabase; endpoints: IN8nConfigEndpoints; diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index ad2a3c1de5..baf4f1aa58 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -19,6 +19,7 @@ import { ActiveWorkflowRunner, CredentialTypes, Db, + ExternalHooks, IActivationError, ICustomRequest, ICredentialsDb, @@ -33,6 +34,7 @@ import { IExecutionsListResponse, IExecutionsStopData, IExecutionsSummary, + IExternalHooks, IN8nUISettings, IPackageVersions, IWorkflowBase, @@ -93,6 +95,7 @@ class App { testWebhooks: TestWebhooks.TestWebhooks; endpointWebhook: string; endpointWebhookTest: string; + externalHooks: IExternalHooks; saveDataErrorExecution: string; saveDataSuccessExecution: string; saveManualExecutions: boolean; @@ -124,6 +127,8 @@ class App { this.protocol = config.get('protocol'); this.sslKey = config.get('ssl_key'); this.sslCert = config.get('ssl_cert'); + + this.externalHooks = ExternalHooks(); } @@ -689,6 +694,8 @@ class App { throw new ResponseHelper.ResponseError(`Credentials with the same type and name exist already.`, undefined, 400); } + await this.externalHooks.run('credentials.new'); + // Encrypt the data const credentials = new Credentials(incomingData.name, incomingData.type, incomingData.nodesAccess); credentials.setData(incomingData.data, encryptionKey); diff --git a/packages/cli/src/externalHooksTemp/test-hooks.ts b/packages/cli/src/externalHooksTemp/test-hooks.ts new file mode 100644 index 0000000000..2cb60decfc --- /dev/null +++ b/packages/cli/src/externalHooksTemp/test-hooks.ts @@ -0,0 +1,10 @@ +export = { + credentials: { + new: [ + () => { + // Here any additional code can run or the creation blocked + throw new Error('No additional credentials can be created.'); + }, + ], + }, +}; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 0b7ae6ad0a..fba554c04e 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,4 +1,5 @@ export * from './CredentialTypes'; +export * from './ExternalHooks'; export * from './Interfaces'; export * from './LoadNodesAndCredentials'; export * from './NodeTypes'; From 0387671cae3084235619a20b76395f144df36f88 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 5 May 2020 01:23:54 +0200 Subject: [PATCH 052/165] :zap: Add additional external hooks and provide additional data --- packages/cli/config/index.ts | 2 +- packages/cli/src/ExternalHooks.ts | 48 ++++++++----------- packages/cli/src/Interfaces.ts | 6 ++- packages/cli/src/Server.ts | 18 +++++-- .../cli/src/externalHooksTemp/test-hooks.ts | 42 ++++++++++++++-- 5 files changed, 79 insertions(+), 37 deletions(-) diff --git a/packages/cli/config/index.ts b/packages/cli/config/index.ts index 5705a5fc7d..7ef5155a67 100644 --- a/packages/cli/config/index.ts +++ b/packages/cli/config/index.ts @@ -253,7 +253,7 @@ const config = convict({ }, externalHookFiles: { - doc: 'Files containing external hooks', + doc: 'Files containing external hooks. Multiple files can be separated by colon (":")', format: String, default: '', env: 'EXTERNAL_HOOK_FILES' diff --git a/packages/cli/src/ExternalHooks.ts b/packages/cli/src/ExternalHooks.ts index 2c00a525c6..c4f12301b4 100644 --- a/packages/cli/src/ExternalHooks.ts +++ b/packages/cli/src/ExternalHooks.ts @@ -1,21 +1,10 @@ import { Db, - IDatabaseCollections, + IExternalHookFunctions, IExternalHooks, -} from "./"; +} from './'; import * as config from '../config'; -// import { -// access as fsAccess, -// readdir as fsReaddir, -// readFile as fsReadFile, -// stat as fsStat, -// } from 'fs'; - -// TODO: Give different name -interface IHookData { - DbCollections: IDatabaseCollections; -} // export EXTERNAL_HOOK_FILES=/data/packages/cli/dist/src/externalHooksTemp/test-hooks.js @@ -29,39 +18,42 @@ class ExternalHooksClass implements IExternalHooks { async init(): Promise { console.log('ExternalHooks.init'); - const externalHookFiles = config.get('externalHookFiles').split(','); + const externalHookFiles = config.get('externalHookFiles').split(':'); console.log('externalHookFiles'); console.log(externalHookFiles); + // Load all the provided hook-files for (let hookFilePath of externalHookFiles) { hookFilePath = hookFilePath.trim(); if (hookFilePath !== '') { console.log(' --- load: ' + hookFilePath); - const hookFile = require(hookFilePath); + try { + const hookFile = require(hookFilePath); - for (const resource of Object.keys(hookFile)) { - // if (this.externalHooks[resource] === undefined) { - // this.externalHooks[resource] = {}; - // } + for (const resource of Object.keys(hookFile)) { + for (const operation of Object.keys(hookFile[resource])) { + // Save all the hook functions directly under their string + // format in an array + const hookString = `${resource}.${operation}`; + if (this.externalHooks[hookString] === undefined) { + this.externalHooks[hookString] = []; + } - for (const operation of Object.keys(hookFile[resource])) { - const hookString = `${resource}.${operation}`; - if (this.externalHooks[hookString] === undefined) { - this.externalHooks[hookString] = []; + this.externalHooks[hookString].push.apply(this.externalHooks[hookString], hookFile[resource][operation]); } - - this.externalHooks[hookString].push.apply(this.externalHooks[hookString], hookFile[resource][operation]); } + } catch (error) { + throw new Error(`Problem loading external hook file "${hookFilePath}": ${error.message}`); } } } } - async run(hookName: string): Promise { + async run(hookName: string, hookParameters?: any[]): Promise { // tslint:disable-line:no-any console.log('RUN NOW: ' + hookName); - const hookData: IHookData = { + const externalHookFunctions: IExternalHookFunctions = { DbCollections: Db.collections, }; @@ -70,7 +62,7 @@ class ExternalHooksClass implements IExternalHooks { } for(const externalHookFunction of this.externalHooks[hookName]) { - externalHookFunction.call(hookData); + await externalHookFunction.apply(externalHookFunctions, hookParameters); } } diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index b54e1eb299..0b9b9eca69 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -188,9 +188,13 @@ export interface IExecutingWorkflowData { workflowExecution?: PCancelable; } +export interface IExternalHookFunctions { + DbCollections: IDatabaseCollections; +} + export interface IExternalHooks { init(): Promise; - run(hookName: string): Promise; + run(hookName: string, hookParameters?: any[]): Promise; // tslint:disable-line:no-any } export interface IN8nConfig { diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index baf4f1aa58..a592c642c2 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -346,7 +346,7 @@ class App { // Creates a new workflow this.app.post('/rest/workflows', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - const newWorkflowData = req.body; + const newWorkflowData = req.body as IWorkflowBase; newWorkflowData.name = newWorkflowData.name.trim(); newWorkflowData.createdAt = this.getCurrentDate(); @@ -354,6 +354,8 @@ class App { newWorkflowData.id = undefined; + await this.externalHooks.run('workflow.create', [newWorkflowData]); + // Save the workflow in DB const result = await Db.collections.Workflow!.save(newWorkflowData); @@ -429,9 +431,11 @@ class App { // Updates an existing workflow this.app.patch('/rest/workflows/:id', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - const newWorkflowData = req.body; + const newWorkflowData = req.body as IWorkflowBase; const id = req.params.id; + await this.externalHooks.run('workflow.update', [newWorkflowData]); + if (this.activeWorkflowRunner.isActive(id)) { // When workflow gets saved always remove it as the triggers could have been // changed and so the changes would not take effect @@ -497,6 +501,8 @@ class App { this.app.delete('/rest/workflows/:id', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const id = req.params.id; + await this.externalHooks.run('workflow.delete', [id]); + if (this.activeWorkflowRunner.isActive(id)) { // Before deleting a workflow deactivate it await this.activeWorkflowRunner.remove(id); @@ -658,6 +664,8 @@ class App { this.app.delete('/rest/credentials/:id', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const id = req.params.id; + await this.externalHooks.run('credentials.delete', [id]); + await Db.collections.Credentials!.delete({ id }); return true; @@ -672,6 +680,8 @@ class App { nodeAccess.date = this.getCurrentDate(); } + await this.externalHooks.run('credentials.create'); + const encryptionKey = await UserSettings.getEncryptionKey(); if (encryptionKey === undefined) { throw new Error('No encryption key got found to encrypt the credentials!'); @@ -694,8 +704,6 @@ class App { throw new ResponseHelper.ResponseError(`Credentials with the same type and name exist already.`, undefined, 400); } - await this.externalHooks.run('credentials.new'); - // Encrypt the data const credentials = new Credentials(incomingData.name, incomingData.type, incomingData.nodesAccess); credentials.setData(incomingData.data, encryptionKey); @@ -722,6 +730,8 @@ class App { const id = req.params.id; + await this.externalHooks.run('credentials.update', [id]); + if (incomingData.name === '') { throw new Error('Credentials have to have a name set!'); } diff --git a/packages/cli/src/externalHooksTemp/test-hooks.ts b/packages/cli/src/externalHooksTemp/test-hooks.ts index 2cb60decfc..f10000f1f0 100644 --- a/packages/cli/src/externalHooksTemp/test-hooks.ts +++ b/packages/cli/src/externalHooksTemp/test-hooks.ts @@ -1,10 +1,46 @@ +import { + IExternalHookFunctions, + IWorkflowBase, +} from '../'; + +// TODO: Move that to interfaces +interface IExternalHooks { + credentials?: { + create?: Array<{ (this: IExternalHookFunctions): Promise; }> + delete?: Array<{ (this: IExternalHookFunctions, credentialId: string): Promise; }> + update?: Array<{ (this: IExternalHookFunctions, credentialId: string): Promise; }> + }; + workflow?: { + create?: Array<{ (this: IExternalHookFunctions, workflowData: IWorkflowBase): Promise; }> + delete?: Array<{ (this: IExternalHookFunctions, workflowId: string): Promise; }> + update?: Array<{ (this: IExternalHookFunctions, workflowData: IWorkflowBase): Promise; }> + }; +} + export = { credentials: { - new: [ - () => { + create: [ + async function (this: IExternalHookFunctions) { + // console.log(this.DbCollections.Workflow); + // Here any additional code can run or the creation blocked throw new Error('No additional credentials can be created.'); }, ], }, -}; + workflow: { + update: [ + async function (this: IExternalHookFunctions, workflowData: IWorkflowBase) { + console.log('update workflow hook'); + + // const responseData = await this.DbCollections.Workflow!.findOne(workflowData.id); + // console.log('workflowData'); + // console.log(responseData); + // console.log(workflowData); + + // Here any additional code can run or the creation blocked + throw new Error('Workflow can not be updated.'); + }, + ], + }, +} as IExternalHooks; From 6e1254fd54a43e6a2126b1192336ad0ccb97db69 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 6 May 2020 00:59:58 +0200 Subject: [PATCH 053/165] :zap: Add additional external workflow hooks --- packages/cli/src/ExternalHooks.ts | 17 +++++--- packages/cli/src/Interfaces.ts | 21 ++++++++-- packages/cli/src/Server.ts | 11 ++--- .../cli/src/WorkflowExecuteAdditionalData.ts | 5 +++ packages/cli/src/WorkflowRunner.ts | 4 ++ .../cli/src/externalHooksTemp/test-hooks.ts | 41 +++++++++---------- 6 files changed, 65 insertions(+), 34 deletions(-) diff --git a/packages/cli/src/ExternalHooks.ts b/packages/cli/src/ExternalHooks.ts index c4f12301b4..9d195ccbad 100644 --- a/packages/cli/src/ExternalHooks.ts +++ b/packages/cli/src/ExternalHooks.ts @@ -1,23 +1,28 @@ import { Db, - IExternalHookFunctions, - IExternalHooks, + IExternalHooksFunctions, + IExternalHooksClass, } from './'; import * as config from '../config'; // export EXTERNAL_HOOK_FILES=/data/packages/cli/dist/src/externalHooksTemp/test-hooks.js -class ExternalHooksClass implements IExternalHooks { +class ExternalHooksClass implements IExternalHooksClass { externalHooks: { [key: string]: Array<() => {}> } = {}; + initDidRun = false; async init(): Promise { console.log('ExternalHooks.init'); + if (this.initDidRun === true) { + return; + } + const externalHookFiles = config.get('externalHookFiles').split(':'); console.log('externalHookFiles'); @@ -48,13 +53,15 @@ class ExternalHooksClass implements IExternalHooks { } } } + + this.initDidRun = true; } async run(hookName: string, hookParameters?: any[]): Promise { // tslint:disable-line:no-any console.log('RUN NOW: ' + hookName); - const externalHookFunctions: IExternalHookFunctions = { - DbCollections: Db.collections, + const externalHookFunctions: IExternalHooksFunctions = { + dbCollections: Db.collections, }; if (this.externalHooks[hookName] === undefined) { diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index 0b9b9eca69..5e76f2c4e2 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -188,11 +188,26 @@ export interface IExecutingWorkflowData { workflowExecution?: PCancelable; } -export interface IExternalHookFunctions { - DbCollections: IDatabaseCollections; +export interface IExternalHooks { + credentials?: { + create?: Array<{ (this: IExternalHooksFunctions, credentialsData: ICredentialsEncrypted): Promise; }> + delete?: Array<{ (this: IExternalHooksFunctions, credentialId: string): Promise; }> + update?: Array<{ (this: IExternalHooksFunctions, credentialsData: ICredentialsDb): Promise; }> + }; + workflow?: { + activate?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb): Promise; }> + create?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowBase): Promise; }> + delete?: Array<{ (this: IExternalHooksFunctions, workflowId: string): Promise; }> + execute?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb, mode: WorkflowExecuteMode): Promise; }> + update?: Array<{ (this: IExternalHooksFunctions, workflowData: IWorkflowDb): Promise; }> + }; } -export interface IExternalHooks { +export interface IExternalHooksFunctions { + dbCollections: IDatabaseCollections; +} + +export interface IExternalHooksClass { init(): Promise; run(hookName: string, hookParameters?: any[]): Promise; // tslint:disable-line:no-any } diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index a592c642c2..a36a2fd81e 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -48,7 +48,6 @@ import { WorkflowCredentials, WebhookHelpers, WorkflowExecuteAdditionalData, - WorkflowHelpers, WorkflowRunner, GenericHelpers, } from './'; @@ -477,6 +476,8 @@ class App { if (responseData.active === true) { // When the workflow is supposed to be active add it again try { + await this.externalHooks.run('workflow.activate', [responseData]); + await this.activeWorkflowRunner.add(id); } catch (error) { // If workflow could not be activated set it again to inactive @@ -680,8 +681,6 @@ class App { nodeAccess.date = this.getCurrentDate(); } - await this.externalHooks.run('credentials.create'); - const encryptionKey = await UserSettings.getEncryptionKey(); if (encryptionKey === undefined) { throw new Error('No encryption key got found to encrypt the credentials!'); @@ -709,6 +708,8 @@ class App { credentials.setData(incomingData.data, encryptionKey); const newCredentialsData = credentials.getDataToSave() as ICredentialsDb; + await this.externalHooks.run('credentials.create', [newCredentialsData]); + // Add special database related data newCredentialsData.createdAt = this.getCurrentDate(); newCredentialsData.updatedAt = this.getCurrentDate(); @@ -730,8 +731,6 @@ class App { const id = req.params.id; - await this.externalHooks.run('credentials.update', [id]); - if (incomingData.name === '') { throw new Error('Credentials have to have a name set!'); } @@ -770,6 +769,8 @@ class App { // Add special database related data newCredentialsData.updatedAt = this.getCurrentDate(); + await this.externalHooks.run('credentials.update', [newCredentialsData]); + // Update the credentials in DB await Db.collections.Credentials!.update(id, newCredentialsData); diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index 9665080ebd..4b86357852 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -1,5 +1,6 @@ import { Db, + ExternalHooks, IExecutionDb, IExecutionFlattedDb, IPushDataExecutionFinished, @@ -302,6 +303,10 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi workflowData = workflowInfo.code; } + const externalHooks = ExternalHooks(); + await externalHooks.init(); + await externalHooks.run('workflow.execute', [workflowData, mode]); + const nodeTypes = NodeTypes(); const workflowName = workflowData ? workflowData.name : undefined; diff --git a/packages/cli/src/WorkflowRunner.ts b/packages/cli/src/WorkflowRunner.ts index 3a09606550..c3ce61cedc 100644 --- a/packages/cli/src/WorkflowRunner.ts +++ b/packages/cli/src/WorkflowRunner.ts @@ -1,5 +1,6 @@ import { ActiveExecutions, + ExternalHooks, IProcessMessageDataHook, ITransferNodeTypes, IWorkflowExecutionDataProcess, @@ -94,6 +95,9 @@ export class WorkflowRunner { * @memberof WorkflowRunner */ async run(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean): Promise { + const externalHooks = ExternalHooks(); + await externalHooks.run('workflow.execute', [data.workflowData, data.executionMode]); + const executionsProcess = config.get('executions.process') as string; if (executionsProcess === 'main') { return this.runMainProcess(data, loadStaticData); diff --git a/packages/cli/src/externalHooksTemp/test-hooks.ts b/packages/cli/src/externalHooksTemp/test-hooks.ts index f10000f1f0..b49c0600b2 100644 --- a/packages/cli/src/externalHooksTemp/test-hooks.ts +++ b/packages/cli/src/externalHooksTemp/test-hooks.ts @@ -1,45 +1,44 @@ import { - IExternalHookFunctions, + WorkflowExecuteMode, +} from 'n8n-workflow'; + +import { + IExternalHooks, + IExternalHooksFunctions, IWorkflowBase, + IWorkflowDb, } from '../'; -// TODO: Move that to interfaces -interface IExternalHooks { - credentials?: { - create?: Array<{ (this: IExternalHookFunctions): Promise; }> - delete?: Array<{ (this: IExternalHookFunctions, credentialId: string): Promise; }> - update?: Array<{ (this: IExternalHookFunctions, credentialId: string): Promise; }> - }; - workflow?: { - create?: Array<{ (this: IExternalHookFunctions, workflowData: IWorkflowBase): Promise; }> - delete?: Array<{ (this: IExternalHookFunctions, workflowId: string): Promise; }> - update?: Array<{ (this: IExternalHookFunctions, workflowData: IWorkflowBase): Promise; }> - }; -} export = { credentials: { create: [ - async function (this: IExternalHookFunctions) { - // console.log(this.DbCollections.Workflow); - + async function (this: IExternalHooksFunctions) { // Here any additional code can run or the creation blocked - throw new Error('No additional credentials can be created.'); + // throw new Error('No additional credentials can be created.'); }, ], }, workflow: { + execute: [ + async function (this: IExternalHooksFunctions, workflowData: IWorkflowDb, mode: WorkflowExecuteMode) { + console.log('execute: ' + mode); + // if (mode === 'integrated') { + // throw new Error('Workflow can not be executed.'); + // } + } + ], update: [ - async function (this: IExternalHookFunctions, workflowData: IWorkflowBase) { + async function (this: IExternalHooksFunctions, workflowData: IWorkflowBase) { console.log('update workflow hook'); - // const responseData = await this.DbCollections.Workflow!.findOne(workflowData.id); + // const responseData = await this.dbCollections.Workflow!.findOne(workflowData.id); // console.log('workflowData'); // console.log(responseData); // console.log(workflowData); // Here any additional code can run or the creation blocked - throw new Error('Workflow can not be updated.'); + // throw new Error('Workflow can not be updated.'); }, ], }, From 4e4c1c8a9f5168c916121df7baad0f563792e787 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 12 May 2020 11:27:07 +0200 Subject: [PATCH 054/165] :zap: Display OAuth callback URL in UI #PROD-23 --- .../src/components/CredentialsInput.vue | 53 +++++++++++++++++++ packages/editor-ui/src/store.ts | 3 ++ 2 files changed, 56 insertions(+) diff --git a/packages/editor-ui/src/components/CredentialsInput.vue b/packages/editor-ui/src/components/CredentialsInput.vue index 779e4a6b5b..16404d531d 100644 --- a/packages/editor-ui/src/components/CredentialsInput.vue +++ b/packages/editor-ui/src/components/CredentialsInput.vue @@ -30,6 +30,19 @@ Is NOT connected + +
+
+ + OAuth Callback URL +
+ +
+ {{oAuthCallbackUrl}} +
+
+
+
@@ -95,6 +108,7 @@