From e4eefedcbfa5d764e2bb0edb661de1ef84b66b98 Mon Sep 17 00:00:00 2001 From: Priyanka P Date: Fri, 13 Dec 2019 16:59:33 +0530 Subject: [PATCH 01/26] Msg91 Integration(node) --- .../credentials/Msg91Api.credentials.ts | 19 ++ .../nodes/Msg91/GenericFunctions.ts | 64 ++++++ packages/nodes-base/nodes/Msg91/Msg91.node.ts | 186 ++++++++++++++++++ packages/nodes-base/nodes/Msg91/msg91.png | Bin 0 -> 2982 bytes packages/nodes-base/package.json | 2 + 5 files changed, 271 insertions(+) create mode 100644 packages/nodes-base/credentials/Msg91Api.credentials.ts create mode 100644 packages/nodes-base/nodes/Msg91/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Msg91/Msg91.node.ts create mode 100644 packages/nodes-base/nodes/Msg91/msg91.png diff --git a/packages/nodes-base/credentials/Msg91Api.credentials.ts b/packages/nodes-base/credentials/Msg91Api.credentials.ts new file mode 100644 index 000000000..d4f96a1ca --- /dev/null +++ b/packages/nodes-base/credentials/Msg91Api.credentials.ts @@ -0,0 +1,19 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class Msg91Api implements ICredentialType { + name = 'msg91Api'; + displayName = 'Msg91 Api'; + properties = [ + // User authentication key + { + displayName: 'Authentication Key', + name: 'authkey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Msg91/GenericFunctions.ts b/packages/nodes-base/nodes/Msg91/GenericFunctions.ts new file mode 100644 index 000000000..59a5831bf --- /dev/null +++ b/packages/nodes-base/nodes/Msg91/GenericFunctions.ts @@ -0,0 +1,64 @@ +import { + IExecuteFunctions, + IHookFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +/** + * Make an API request to MSG91 + * + * @param {IHookFunctions} this + * @param {string} method + * @param {string} url + * @param {object} body + * @returns {Promise} + */ +export async function msg91ApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: IDataObject, query?: IDataObject): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('msg91Api'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + if (query === undefined) { + query = {}; + } + + query.authkey = credentials.authkey as string; + + const options = { + method, + form: body, + qs: query, + uri: `https://api.msg91.com/api/sendhttp.php`, + auth: { + user: '', + pass: '', + }, + json: true + }; + + try { + return await this.helpers.request(options); + } catch (error) { + if (error.statusCode === 401) { + // Return a clear error + throw new Error('The MSG91 credentials are not valid!'); + } + + if (error.response && error.response.body && error.response.body.message) { + // Try to return the error prettier + let errorMessage = `MSG91 error response [${error.statusCode}]: ${error.response.body.message}`; + if (error.response.body.more_info) { + errorMessage = `errorMessage (${error.response.body.more_info})`; + } + + throw new Error(errorMessage); + } + + // If that data does not exist for some reason return the actual error + throw error; + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Msg91/Msg91.node.ts b/packages/nodes-base/nodes/Msg91/Msg91.node.ts new file mode 100644 index 000000000..f59dd49ec --- /dev/null +++ b/packages/nodes-base/nodes/Msg91/Msg91.node.ts @@ -0,0 +1,186 @@ +import { IExecuteFunctions } from 'n8n-core'; +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + msg91ApiRequest, +} from './GenericFunctions'; + + +export class Msg91 implements INodeType { + description: INodeTypeDescription = { + displayName: 'Msg91', + name: 'msg91', + icon: 'file:msg91.png', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Send Transactional SMS', + defaults: { + name: 'Msg91', + color: '#0000ff', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'msg91Api', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'SMS', + value: 'sms', + }, + ], + default: 'sms', + description: 'The resource to operate on.', + }, + + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'sms', + ], + }, + }, + options: [ + { + name: 'Send', + value: 'send', + description: 'Send SMS', + }, + ], + default: 'send', + description: 'The operation to perform.', + }, + { + displayName: 'Sender', + name: 'sender', + type: 'string', + default: '', + placeholder: '+14155238886', + required: true, + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'sms', + ], + }, + }, + description: 'The number from which to send the message', + }, + { + displayName: 'To', + name: 'mobiles', + type: 'string', + default: '', + placeholder: 'Mobile Number With Country Code', + required: true, + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'sms', + ], + }, + }, + description: 'The number to which to send the message', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'sms', + ], + }, + }, + description: 'The message to send', + }, + ] + }; + + + async execute(this: IExecuteFunctions): Promise { + + const items = this.getInputData(); + const returnData: IDataObject[] = []; + + let operation: string; + let resource: string; + + // For Post + let body: IDataObject; + // For Query string + let qs: IDataObject; + + let requestMethod: string; + let endpoint: string; + + for (let i = 0; i < items.length; i++) { + requestMethod = 'GET'; + endpoint = ''; + body = {}; + qs = {}; + + resource = this.getNodeParameter('resource', i) as string; + operation = this.getNodeParameter('operation', i) as string; + + if (resource === 'sms') { + if (operation === 'send') { + // ---------------------------------- + // sms:send + // ---------------------------------- + + requestMethod = 'GET'; + endpoint = 'https://api.msg91.com/api/sendhttp.php'; + + qs.route = 4; + qs.country = 0; + qs.sender = this.getNodeParameter('sender', i) as string; + qs.mobiles = this.getNodeParameter('mobiles', i) as string; + qs.message = this.getNodeParameter('message', i) as string; + + } else { + throw new Error(`The operation "${operation}" is not known!`); + } + } else { + throw new Error(`The resource "${resource}" is not known!`); + } + + const responseData = await msg91ApiRequest.call(this, requestMethod, endpoint, body, qs); + + returnData.push(responseData as IDataObject); + } + + return [this.helpers.returnJsonArray(returnData)]; + + } +} diff --git a/packages/nodes-base/nodes/Msg91/msg91.png b/packages/nodes-base/nodes/Msg91/msg91.png new file mode 100644 index 0000000000000000000000000000000000000000..dc10c1f269c2c7d9c03c0976959d4ccc25b9ac67 GIT binary patch literal 2982 zcmb_e`9BkmAKqAVPO*<+d>ms5b0*hNA1t>v#7K@Wik54V95F{TA32hv9Jz9Y7`dBs zj000nM8!P8On(+tr zg5W=VX7ido03Z@#YxR%o4R3CVqqD3njPzc^*84mA-a^!{zNmQ;2iiu{d5IKUh2l#F zh@uY9nl?lKqd4>vM26zUDq1t87^(oLN$DQtOY^>l*FZM0(B}G9#xHiR@bvnG=c^MZ z8+aV|4Ihii!U9qy2pdr%UxNg|VUVO&kUnana*F>Hst6b;6ok*iR0~|e>+?@YOj1u| z*d3jr27&XC<$gF=zI>hE3(*tyXXuqEbu%ozQU+T;l zA3z^i4o7N3n6ob5Yi|Wswx8VWoZ97+e)%uI6)c-v?MoFr!ytkA3|g=H&~q`KRhb7a zK?UR0(IO=JCATwemt?q;)}>J)R3j3WUkFap(ftf=OF=iS&a5T6;b~YY>U{x4&nI;X z*W{I(k=Pm@<+Tpe~i2OH^|2{u{zP zZ#VA|hr*8vMQtqj`GtTJ1@3{8kbVW5RWIn>!&Y@sIp$@4lW*cp#YZ_c*}xxrS6_k0c^?`%z+53@zcu zy|M}`>Rs3s0;^K}-HWkXzPnx(p>LIs0v{Z2G@e zH6nF8gtL#CGr2=XUSzCf`Qn4#Vn*+?v%c2;&MaChGrFYOdKrW6TCs`U@17SH5f4fD z{60wc#A#G-gUlK2X}MQjngo`fxoEJ4mjWvFoY?&O)mOY=yi-t0Fqv!+;LYpiplbTbq}|-^HV0?-z+|w8ai+M z$e1uEMCR{3IlAk}SvJUq3=UO%*^tW54+|@q_YI0}oCPc?sEgEQDgD$r6Lcf%>h3ZU zG2^_ZCW=i83=M5Mn~MHV_u%5P+f)*>GjP(bdV;+iMprV8Kj`u2*Y>Fib_3ecw43jn z8j%IPJ=|0~pj0=4L;UvnLtRAE!?4nwr|%x;WM38#=4_QAl{lLpyO1})A3ska0T)4|awCXcJuWl#(o zo@$zeiG?Sy5ToDT8bR~hRHMxaDMDc0*TxywZNh5QMTHTA!qo3N&u1JoLlmfSO8pGe zG0o<-G!n~aZhhNJ)4wcJOXnJn#T$7Qr*9XV zLM+1Ly9~B2mUxY&BI}InGD*E#Q;X-Z?=~5vgJ<&z_9kvsYgY!w7&6(6o`UfH;!0JOoAHG|g=OpweDpnS&xTq*9 zCPSH@9c&V^kHTJw0tYU2zQH*O+^Z4T6kxvocNVNpEb@H)o3$g5{y@Hob^NRgB2HoL zBkzDXi)xWo3}5~!*Y}|U@vxEl?N6E6T;L*C#pTcCkuyWxGO{Ei{7)Ij$OZC?9Vv=u zeN0L0%>}J)D-2#eu5j!Ir&yG#lC&M;JKn?89n0p+FuJj0+4Ihc5gPe$b9+)8zS|fE9?d+Ket4iit}+e}_JR(x)$Uig|M}JQ_n%#X4OA+{5HDxv3Gy%6avW<` zVw*B{3qm`gp9P3>`UZIai#&y+nO$jIbbyV-Xzv9ge!s=1Te9#`zS3k7(xQkDwu^O$ zTnC67r;1`Qx!OxNkE5$_RLUDY5MVgwZ?!$g-sx$crSa6AHd&SC%a$LJM>t&Zr5YRF zc$Nn6s@&t}C@{t^ZT?`YX$_%ruW^X*zd)DNk>7PECO|Ed=l#-C$W_{;LCZUDaG9ye z)xxJyze}?7@>V?mn$MXND*dW05urc7e7lE{0TJ3DIw9>YNJ_xEJ66(0Y`ex2(!Vs4 z9k9m=P?zXFTIJrIPooUGyxqz$Ml-mq7Ey;|eCrYNh$hP9E=|8nGJof1{ENwZVuZO* z!D6AgXDNCZ-0`4&dej7jmc`&r9J`!J&h=f2Gx{(ZB=Hi%5(2%HRqQaXUEVgVRoM`J z-q{#Gp4JknODs;#VhS;85Onz;^GKNi+mkF|*_ zsT1`52^v_O+E@M^?J7bKlz_liJR!?~T#0D!g8ATy`mfz^ReR*yBie7Z#G?~y_lbQ> zu2L`(2(d4(QBGBt!zQc%7Y#a}-8*xFZYm*sDk?`rYJz0Udg)wUB<10s@a2mv+k2j#uD4W7v__7;Q zUOd)JkK)RIMr#wS>=!*A(LzK>W5#bj8O^B~A1IoaN3?g;BuN-p z+J|u`RFufEelIF?+M!OI8h5kfn%CT7M364mh}YeUvc9OL^Uxra?Os5p1&CF*aPf58 zHmuX<*OUHD4S}VtYF>44WxomQJ0qGGT+o=t_!MvrJu>38?rXiQwZmj)aW{EVRPH&- z@yec0t>%_9H;&+q4$N6*EfKfOI`{g=d$nw{8j2^@nr^-~s?7=drk(gy`@y?c>gY1cl?3I%ch0{ti}Y-Zus}p?i`U& z^1sutbf|H-dRATl-cEk3$2`DB_W%Nx!D8`0`x{`3ak6T%@J;?N D8CIAm literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index dad780ab2..6d44a32fa 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -68,6 +68,7 @@ "dist/credentials/TodoistApi.credentials.js", "dist/credentials/TrelloApi.credentials.js", "dist/credentials/TwilioApi.credentials.js", + "dist/credentials/Msg91Api.credentials.js", "dist/credentials/TypeformApi.credentials.js", "dist/credentials/MandrillApi.credentials.js", "dist/credentials/TodoistApi.credentials.js", @@ -149,6 +150,7 @@ "dist/nodes/Trello/Trello.node.js", "dist/nodes/Trello/TrelloTrigger.node.js", "dist/nodes/Twilio/Twilio.node.js", + "dist/nodes/Msg91/Msg91.node.js", "dist/nodes/Typeform/TypeformTrigger.node.js", "dist/nodes/WriteBinaryFile.node.js", "dist/nodes/Webhook.node.js", From ed93611f43c26d9a99418262f59a0213524e3451 Mon Sep 17 00:00:00 2001 From: Ram Yalamanchili Date: Wed, 1 Jan 2020 22:44:36 -0800 Subject: [PATCH 02/26] Fix issue with tracking changes to rest api code in dev mode --- packages/cli/commands/start.ts | 2 +- packages/cli/nodemon.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cli/commands/start.ts b/packages/cli/commands/start.ts index 5494b5c5c..e8f1694b9 100644 --- a/packages/cli/commands/start.ts +++ b/packages/cli/commands/start.ts @@ -176,7 +176,7 @@ export class Start extends Command { Start.openBrowser(); } this.log(`\nPress "o" to open in Browser.`); - process.stdin.on("data", (key) => { + process.stdin.on("data", (key: string) => { if (key === 'o') { Start.openBrowser(); inputText = ''; diff --git a/packages/cli/nodemon.json b/packages/cli/nodemon.json index efb39c666..5bdb290fb 100644 --- a/packages/cli/nodemon.json +++ b/packages/cli/nodemon.json @@ -9,6 +9,6 @@ "index.ts", "src" ], - "exec": "npm start", + "exec": "npm run build && npm start", "ext": "ts" -} \ No newline at end of file +} From 3b450b4372acfa4b56f025a1d55b6fa05f1409f8 Mon Sep 17 00:00:00 2001 From: Ram Yalamanchili Date: Wed, 1 Jan 2020 22:46:16 -0800 Subject: [PATCH 03/26] Add debugger support for vuejs --- packages/editor-ui/vue.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/editor-ui/vue.config.js b/packages/editor-ui/vue.config.js index f70f41c5b..b47e0c4b2 100644 --- a/packages/editor-ui/vue.config.js +++ b/packages/editor-ui/vue.config.js @@ -12,6 +12,7 @@ module.exports = { }, }, configureWebpack: { + devtool: 'source-map', plugins: [ new GoogleFontsPlugin({ fonts: [ From d2ea3ce877b4233e9c1cb20f8e2882e42ad964e0 Mon Sep 17 00:00:00 2001 From: Ram Yalamanchili Date: Wed, 1 Jan 2020 22:47:11 -0800 Subject: [PATCH 04/26] Add OAuth2 Authorization and Callback rest endpoints URL generation for OAuth2 authorization and the subsequent login callback are handled at the backend API. While this can be done client side, the credentials are better managed entirely on the server side. --- packages/cli/package.json | 2 + packages/cli/src/Server.ts | 135 +++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/packages/cli/package.json b/packages/cli/package.json index 48bbacb41..df55d5a6b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -78,9 +78,11 @@ "basic-auth": "^2.0.1", "body-parser": "^1.18.3", "body-parser-xml": "^1.1.0", + "client-oauth2": "^4.2.5", "compression": "^1.7.4", "connect-history-api-fallback": "^1.6.0", "convict": "^5.0.0", + "csrf": "^3.1.0", "dotenv": "^8.0.0", "express": "^4.16.4", "flatted": "^2.0.0", diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 41fda8037..f5a608331 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -10,6 +10,9 @@ import * as bodyParser from 'body-parser'; require('body-parser-xml')(bodyParser); import * as history from 'connect-history-api-fallback'; import * as requestPromise from 'request-promise-native'; +import * as _ from 'lodash'; +import * as clientOAuth2 from 'client-oauth2'; +import * as csrf from 'csrf'; import { ActiveExecutions, @@ -721,6 +724,8 @@ 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; @@ -840,8 +845,138 @@ class App { return returnData; })); + // ---------------------------------------- + // OAuth2-Credential/Auth + // ---------------------------------------- + // Returns all the credential types which are defined in the loaded n8n-modules + 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!'); + } + + const result = await Db.collections.Credentials!.findOne(req.query.id); + if (result === undefined) { + res.status(404).send('The credential is not known.'); + return ''; + } + + let encryptionKey = undefined; + encryptionKey = await UserSettings.getEncryptionKey(); + if (encryptionKey === undefined) { + throw new Error('No encryption key got found to decrypt the credentials!'); + } + + 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 oauthCredentials = (result as ICredentialsDecryptedDb).data; + if (oauthCredentials === undefined) { + throw new Error('Unable to read OAuth credentials'); + } + + let 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 + } + const stateEncodedStr = Buffer.from(JSON.stringify(state)).toString('base64') as string; + + const oAuthObj = new clientOAuth2({ + clientId: _.get(oauthCredentials, 'clientId') as string, + 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, + scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ','), + state: stateEncodedStr + }); + + credentials.setData(oauthCredentials, encryptionKey); + const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb; + + // Add special database related data + newCredentialsData.updatedAt = this.getCurrentDate(); + + // Update the credentials in DB + await Db.collections.Credentials!.update(req.query.id, newCredentialsData); + + return oAuthObj.code.getUri(); + })); + + // ---------------------------------------- + // OAuth2-Credential/Callback + // ---------------------------------------- + + // 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 => { + const {code, state: stateEncoded} = req.query; + if (code === undefined || stateEncoded === undefined) { + 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 result = await Db.collections.Credentials!.findOne(state.cid); + if (result === undefined) { + res.status(404).send('The credential is not known.'); + return ''; + } + + let encryptionKey = undefined; + encryptionKey = await UserSettings.getEncryptionKey(); + if (encryptionKey === undefined) { + throw new Error('No encryption key got found to decrypt the credentials!'); + } + + 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'); + } + + let 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 oAuthObj = new clientOAuth2({ + clientId: _.get(oauthCredentials, 'clientId') as string, + 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, + 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'); + } + + oauthCredentials.oauthTokenData = JSON.stringify(oauthToken.data); + _.unset(oauthCredentials, 'csrfSecret'); + credentials.setData(oauthCredentials, encryptionKey); + const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb; + // Add special database related data + newCredentialsData.updatedAt = this.getCurrentDate(); + // Save the credentials in DB + await Db.collections.Credentials!.update(state.cid, newCredentialsData); + + return 'Success!'; + })); + // ---------------------------------------- // Executions // ---------------------------------------- From cb73853680449dbfae5b8ec141ed4e6464a0ce8d Mon Sep 17 00:00:00 2001 From: Ram Yalamanchili Date: Wed, 1 Jan 2020 22:49:18 -0800 Subject: [PATCH 05/26] Add UI logic to support OAuth authentication flow Add support in credentialsList to kickoff an OAuth2 authorization flow. This enables users to authenticate and allow n8n to store the resulting keys in the backend. --- packages/editor-ui/src/Interface.ts | 2 ++ .../src/components/CredentialsList.vue | 20 +++++++++++++++++-- .../src/components/mixins/restApi.ts | 15 ++++++++++++++ packages/editor-ui/src/router.ts | 6 ++++++ 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index e6e34fb9d..a79ebdd8f 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -145,6 +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; } export interface IBinaryDisplayData { diff --git a/packages/editor-ui/src/components/CredentialsList.vue b/packages/editor-ui/src/components/CredentialsList.vue index 758adf1a4..eeece990b 100644 --- a/packages/editor-ui/src/components/CredentialsList.vue +++ b/packages/editor-ui/src/components/CredentialsList.vue @@ -25,10 +25,12 @@ + width="180"> @@ -91,6 +93,20 @@ 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, @@ -124,7 +140,7 @@ export default mixins( try { this.credentials = JSON.parse(JSON.stringify(this.$store.getters.allCredentials)); } catch (error) { - this.$showError(error, 'Proble loading credentials', 'There was a problem loading the credentials:'); + this.$showError(error, 'Problem loading credentials', 'There was a problem loading the credentials:'); this.isDataLoading = false; return; } diff --git a/packages/editor-ui/src/components/mixins/restApi.ts b/packages/editor-ui/src/components/mixins/restApi.ts index a72520718..a2cdbd658 100644 --- a/packages/editor-ui/src/components/mixins/restApi.ts +++ b/packages/editor-ui/src/components/mixins/restApi.ts @@ -252,6 +252,21 @@ export const restApi = Vue.extend({ return self.restApi().makeRestApiRequest('GET', `/credential-types`); }, + // Get OAuth2 Authorization URL using the stored credentials + 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 => { + const sendData = { + 'code': code, + 'state': state + }; + + return self.restApi().makeRestApiRequest('POST', `/oauth2-credential/callback`, sendData); + }, + // Returns the execution with the given name getExecution: async (id: string): Promise => { const response = await self.restApi().makeRestApiRequest('GET', `/executions/${id}`); diff --git a/packages/editor-ui/src/router.ts b/packages/editor-ui/src/router.ts index 14a31c7e8..f33d028f9 100644 --- a/packages/editor-ui/src/router.ts +++ b/packages/editor-ui/src/router.ts @@ -19,6 +19,12 @@ export default new Router({ sidebar: MainSidebar, }, }, + { + path: '/oauth2/callback', + name: 'OAuth2Callback', + components: { + }, + }, { path: '/workflow', name: 'NodeViewNew', From c44cfffdd91df5e60013a310a5d048399b57a192 Mon Sep 17 00:00:00 2001 From: Ram Yalamanchili Date: Wed, 1 Jan 2020 22:51:23 -0800 Subject: [PATCH 06/26] Add OAuth2 credential type --- .../credentials/OAuth2Api.credentials.ts | 56 +++++++++++++++++++ packages/nodes-base/package.json | 1 + 2 files changed, 57 insertions(+) create mode 100644 packages/nodes-base/credentials/OAuth2Api.credentials.ts diff --git a/packages/nodes-base/credentials/OAuth2Api.credentials.ts b/packages/nodes-base/credentials/OAuth2Api.credentials.ts new file mode 100644 index 000000000..452fdb8f5 --- /dev/null +++ b/packages/nodes-base/credentials/OAuth2Api.credentials.ts @@ -0,0 +1,56 @@ +import { + 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: '', + }, + ]; +} diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index caf67cc06..3074251ef 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -56,6 +56,7 @@ "dist/credentials/MySql.credentials.js", "dist/credentials/NextCloudApi.credentials.js", "dist/credentials/OpenWeatherMapApi.credentials.js", + "dist/credentials/OAuth2Api.credentials.js", "dist/credentials/PipedriveApi.credentials.js", "dist/credentials/Postgres.credentials.js", "dist/credentials/PayPalApi.credentials.js", From bd2713d83adfa1e7b019faad34b45da5d9d2abbb Mon Sep 17 00:00:00 2001 From: Ram Yalamanchili Date: Wed, 1 Jan 2020 22:51:41 -0800 Subject: [PATCH 07/26] OAuth2 testing node --- packages/nodes-base/nodes/OAuth.node.ts | 104 ++++++++++++++++++++++++ packages/nodes-base/package.json | 3 +- 2 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 packages/nodes-base/nodes/OAuth.node.ts diff --git a/packages/nodes-base/nodes/OAuth.node.ts b/packages/nodes-base/nodes/OAuth.node.ts new file mode 100644 index 000000000..189ae9e40 --- /dev/null +++ b/packages/nodes-base/nodes/OAuth.node.ts @@ -0,0 +1,104 @@ +import { IExecuteFunctions } from 'n8n-core'; +import { + GenericValue, + IDataObject, + 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', + 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.', + }, + + // ---------------------------------- + // 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!'); + } + + 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[] = []; + + 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'); + } + } +} diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 3074251ef..28c220136 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -166,7 +166,8 @@ "dist/nodes/Xml.node.js", "dist/nodes/Mandrill/Mandrill.node.js", "dist/nodes/Todoist/Todoist.node.js", - "dist/nodes/Xml.node.js" + "dist/nodes/Xml.node.js", + "dist/nodes/OAuth.node.js" ] }, "devDependencies": { From 7d2e8576137b07587c15d86e750b8af2d00e8b84 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sun, 5 Jan 2020 13:34:09 -0500 Subject: [PATCH 08/26] done --- .../credentials/ZendeskApi.credentials.ts | 29 ++++ .../nodes/Zendesk/GenericFunctions.ts | 64 ++++++++ .../nodes/Zendesk/ZendeskTrigger.node.ts | 149 ++++++++++++++++++ packages/nodes-base/nodes/Zendesk/zendesk.png | Bin 0 -> 3433 bytes packages/nodes-base/package.json | 16 +- 5 files changed, 251 insertions(+), 7 deletions(-) create mode 100644 packages/nodes-base/credentials/ZendeskApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Zendesk/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts create mode 100644 packages/nodes-base/nodes/Zendesk/zendesk.png diff --git a/packages/nodes-base/credentials/ZendeskApi.credentials.ts b/packages/nodes-base/credentials/ZendeskApi.credentials.ts new file mode 100644 index 000000000..29048c117 --- /dev/null +++ b/packages/nodes-base/credentials/ZendeskApi.credentials.ts @@ -0,0 +1,29 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class ZendeskApi implements ICredentialType { + name = 'zendeskApi'; + displayName = 'Zendesk API'; + properties = [ + { + displayName: 'URL', + name: 'url', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Email', + name: 'email', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'API Token', + name: 'apiToken', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts new file mode 100644 index 000000000..8221cb402 --- /dev/null +++ b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts @@ -0,0 +1,64 @@ +import { OptionsWithUri } from 'request'; +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IExecuteSingleFunctions, +} from 'n8n-core'; +import { IDataObject } from 'n8n-workflow'; + +export async function zendeskApiRequest(this: IHookFunctions | 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('zendeskApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + const base64Key = Buffer.from(`${credentials.email}/token:${credentials.apiToken}`).toString('base64') + let options: OptionsWithUri = { + headers: { 'Authorization': `Basic ${base64Key}`}, + method, + qs, + body, + uri: uri ||`${credentials.domain}/api/v2${resource}`, + json: true + }; + options = Object.assign({}, options, option); + if (Object.keys(options.body).length === 0) { + delete options.body; + } + + try { + return await this.helpers.request!(options); + } catch (error) { + let errorMessage = error.message; + if (error.response.body) { + errorMessage = error.response.body.message || error.response.body.Message || error.message; + } + + throw new Error(errorMessage); + } +} + +/** + * Make an API request to paginated flow endpoint + * and return all results + */ +export async function zendeskApiRequestAllItems(this: IHookFunctions | IExecuteFunctions| ILoadOptionsFunctions, propertyName: string, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + let uri: string | undefined; + + do { + responseData = await zendeskApiRequest.call(this, method, resource, body, query, uri); + query.continuation = responseData.pagination.continuation; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData.pagination !== undefined && + responseData.pagination.has_more_items !== undefined && + responseData.pagination.has_more_items !== false + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts new file mode 100644 index 000000000..479ef88f2 --- /dev/null +++ b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts @@ -0,0 +1,149 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + INodeTypeDescription, + INodeType, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + zendeskApiRequest, +} from './GenericFunctions'; + +export class ZendeskTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Zendesk Trigger', + name: 'zendesk', + icon: 'file:zendesk.png', + group: ['trigger'], + version: 1, + description: 'Handle Zendesk events via webhooks', + defaults: { + name: 'Zendesk Trigger', + color: '#559922', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'zendeskApi', + required: true, + } + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Service', + name: 'service', + type: 'options', + required: true, + options: [ + { + name: 'Support', + value: 'support', + } + ], + default: 'support', + description: '', + }, + { + displayName: 'Events', + name: 'events', + type: 'multiOptions', + displayOptions: { + show: { + service: [ + 'support' + ] + } + }, + options: [ + { + name: 'ticket.status.open', + value: 'ticket.status.open' + }, + ], + required: true, + default: [], + description: '', + }, + ], + + }; + // @ts-ignore + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + let webhooks; + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId === undefined) { + return false; + } + const endpoint = `/webhooks/${webhookData.webhookId}/`; + try { + webhooks = await zendeskApiRequest.call(this, 'GET', endpoint); + } catch (e) { + return false; + } + return true; + }, + async create(this: IHookFunctions): Promise { + let body, responseData; + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + const event = this.getNodeParameter('event') as string; + const actions = this.getNodeParameter('actions') as string[]; + const endpoint = `/webhooks/`; + // @ts-ignore + body = { + endpoint_url: webhookUrl, + actions: actions.join(','), + event_id: event, + }; + try { + responseData = await zendeskApiRequest.call(this, 'POST', endpoint, body); + } catch(error) { + console.log(error) + return false; + } + // @ts-ignore + webhookData.webhookId = responseData.id; + return true; + }, + async delete(this: IHookFunctions): Promise { + let responseData; + const webhookData = this.getWorkflowStaticData('node'); + const endpoint = `/webhooks/${webhookData.webhookId}/`; + try { + responseData = await zendeskApiRequest.call(this, 'DELETE', endpoint); + } catch(error) { + return false; + } + if (!responseData.success) { + return false; + } + delete webhookData.webhookId; + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const req = this.getRequestObject(); + return { + workflowData: [ + this.helpers.returnJsonArray(req.body) + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Zendesk/zendesk.png b/packages/nodes-base/nodes/Zendesk/zendesk.png new file mode 100644 index 0000000000000000000000000000000000000000..f8c5d2e744ec9975375e33aa85634d0c30530b5a GIT binary patch literal 3433 zcmY*cXEYqz79PathUn!*OY|{Hl$q#V41%jhnbCV0y+!XLhUh|wkf@U(qD4f^Ac7#G zjT&7dA&j0p?!E86_s&}ToW1w?&iCzoew?*V>K#KJI%-a8002Ozr>kjlu_|5~CGcW~ zolT=I7LouH9d$s%F!$y~K;@@v6#xLxFkKo6ps0lX0$}C-hh?Cp0UYM&>n(wF@^x^Q z2=(^6KwmJ0!Y*8I=RhQHsJE9-04!9A|4#($!oN&I_<8??1bQm*TN>Qq)$m0*^U6xx zl#t|Crsn14RYWZDe=1n2KvDukdTlNi4YkHUz96E3JQfnB&8wJ(%=gO zI3Ua?5E%;g2@v>;P@ob&|E17BUS?rND0k-z1s{P?c{>|Du#a4%-6dJJey5ksQKbu7d7J1I{SQEUGyd7stx1wGB#48#@^Vo0#5s?A|slZV)WbCeUu|4$q*o z%t@1T$3~1m1MusG0XuU6Bl8i5_sETd7k7>e3J)K-u02|hT2~V5^{{#rJHcW3&vfKF z6ayE|t9Y|upcwKy_&i*II`Q#szv$?yt+rxNdb)|%Tn%oe(lFhC)M)ZtMbYAi9(trB z`1AMnh@8E12$hbLrf?*m|71OXV(f=&BDe+8sDl_`)+n*?+0vIaA-hz_4?!Q-pO`r# z9z9PIbibW7NiiW5KfT%K7&gYj2C+t1y=gYQ!- zE8m#~ZF*~cc3kN`Li^9i?rHbF*OKF+HtO<;{Oyu4O!lybWT$kk5@STCWi(DM1LK1ibXs@V)5M8^BU?8%qCL1 zQ1&2%Ez{8mV&NY76t|R$5oYzIpg_W=+bw}oeOIPDUOtwa3-O$B8zxLbWMW8uL$j87 z?FEXOyui1Heca8niW3syw2&P*S4VjURf^bGn{|jR{SIzHJ#ZVzl5N`$Fm0^B9adsz zbRplBun^WuQn5xTN52-7YRS)M;FC5_yHZ z*HX>ttH>mr$9(~!vZHv4!>&g~a_?>JrRB3uUm+nLyJjl@GNP^1E_?RMF1}&lm6(WE5_kjF=Vn_NxfZ>vi=T( zhu^;5*Kc$9R})s~LD}oMSnMa3sP6vjA`21HCB?ra>N5LgHn_7q2pcb0y7i!Zs)A)X zhy5j;k9##oLx9@XfKwweFZ8EEpbYj{Td_lgQ`SQw$kCa{FfhYPW!EKf)QU6S!DiSCvm%6k3xXr3$m@{VQm%mR}pQ%cPWMBlK&Iy6uwO{fX{_Gn>b;=%W_u%{X323&u(@h5P)mQq~sW14h3EefF>h5Rq3# zdU$S<*i9CL>pF-|VL$c^)pn_xk`}05nduw0DCpv-tpz3@#@bxN@rK21BKQq@`(vYN zDUOrB3ZI@9X1Nujy%#=}4FeqCjLCQ0(oRZ18zDzbI(?)y;dHoC0)w2j_OFP?jCiaj z^m&V3OEtV!SM}?)ZR~aVWX@(bTQK%A1#^|a~>(_%*xDOwJyuhf1zz# zV2?!(-G9%_Q|DCn_X{_C=hnP~iESVw6B2l39rJmQC6it*m*^ku(#|f0x`FjHbD1N4 z0|a{sQ=vT&D}6mf`vQrkJ^Z^D|5 zXhZIjf6ySM8fc9duvawtPH{%;3~F z!ZPn#HhCORP{l<79;-KgHRG#}Bk40bp9RxL(YBTK`2(t-nG1RAy_QP3wF*|$+MJ#P zQA&ZoZq39~kQNRM2upwu``06Sd(>I|+eQdnb`ab?ZDd6}T;p&;5h0CQ`sm=&w?Vv9 zX{65Ugl~xU7nuw`+1eE>iLlT@zx#=?z!*mKdp|4vme=hv`q}M z-hV==LX=uCuy}T0pN0Qzs1UGUZ0HyG60a4{w3?3xWS)cBA zY6FdwAa5pKS^lAI0X5PXNf~1C7?L~luj8g^)Et)DP2=Xen*4#D{18as;rPQ?R`yo4F- z;{-cmvI6%^!W3lZgZ%>>4dy+oCnu&kcOy1huCP~p4ybO~T$P$Ew#%{n3XrYw#OikRoGg=M^*1&Jp2*lpg@O+p;r9qro1( zA0(murSsKsUFn}+Y2f7U&1x;lZ08TriQN@vm0Vq3Q`#6zd>LcK7?qnmZV@lQLt?7z zy1<7;Zu>9;byJ3qeD&F0Qp@4|F)1o_PZztIi{`}CZ*r;Q7@L!L-w_v;f4*lrIm!V;e83`C!SVrCR9z> y8ik~Ng?4C?HLOD<+U)ZdZQXG97OhPO#}R1Q6fYSH$&R`F|LAEMYBs1N;{FZy6;1B| literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index dc5747eb3..4ddc3b715 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -71,11 +71,12 @@ "dist/credentials/TwilioApi.credentials.js", "dist/credentials/TypeformApi.credentials.js", "dist/credentials/MandrillApi.credentials.js", - "dist/credentials/TodoistApi.credentials.js", - "dist/credentials/TypeformApi.credentials.js", - "dist/credentials/TogglApi.credentials.js", + "dist/credentials/TodoistApi.credentials.js", + "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/ZendeskApi.credentials.js" ], "nodes": [ "dist/nodes/ActiveCampaign/ActiveCampaign.node.js", @@ -163,9 +164,10 @@ "dist/nodes/Toggl/TogglTrigger.node.js", "dist/nodes/Vero/Vero.node.js", "dist/nodes/WriteBinaryFile.node.js", - "dist/nodes/Webhook.node.js", - "dist/nodes/Wordpress/Wordpress.node.js", - "dist/nodes/Xml.node.js" + "dist/nodes/Webhook.node.js", + "dist/nodes/Wordpress/Wordpress.node.js", + "dist/nodes/Xml.node.js", + "dist/nodes/Zendesk/ZendeskTrigger.node.js" ] }, "devDependencies": { From f92a42dfe11fac49f75c5330ae501fcd12341098 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sun, 5 Jan 2020 21:32:22 -0500 Subject: [PATCH 09/26] done --- .../nodes/Zendesk/GenericFunctions.ts | 37 ++-------- .../nodes/Zendesk/ZendeskTrigger.node.ts | 74 ++++++++++++------- 2 files changed, 51 insertions(+), 60 deletions(-) diff --git a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts index 8221cb402..9677f5652 100644 --- a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts @@ -18,47 +18,20 @@ export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions method, qs, body, - uri: uri ||`${credentials.domain}/api/v2${resource}`, + uri: uri ||`${credentials.domain}/api/v2${resource}.json`, json: true }; options = Object.assign({}, options, option); if (Object.keys(options.body).length === 0) { delete options.body; } - try { return await this.helpers.request!(options); - } catch (error) { - let errorMessage = error.message; - if (error.response.body) { - errorMessage = error.response.body.message || error.response.body.Message || error.message; + } catch (err) { + let errorMessage = ''; + if (err.error && err.description) { + errorMessage = err.description; } - throw new Error(errorMessage); } } - -/** - * Make an API request to paginated flow endpoint - * and return all results - */ -export async function zendeskApiRequestAllItems(this: IHookFunctions | IExecuteFunctions| ILoadOptionsFunctions, propertyName: string, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any - - const returnData: IDataObject[] = []; - - let responseData; - - let uri: string | undefined; - - do { - responseData = await zendeskApiRequest.call(this, method, resource, body, query, uri); - query.continuation = responseData.pagination.continuation; - returnData.push.apply(returnData, responseData[propertyName]); - } while ( - responseData.pagination !== undefined && - responseData.pagination.has_more_items !== undefined && - responseData.pagination.has_more_items !== false - ); - - return returnData; -} diff --git a/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts index 479ef88f2..0859dd9db 100644 --- a/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts +++ b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts @@ -7,6 +7,7 @@ import { INodeTypeDescription, INodeType, IWebhookResponseData, + IDataObject, } from 'n8n-workflow'; import { @@ -59,7 +60,7 @@ export class ZendeskTrigger implements INodeType { { displayName: 'Events', name: 'events', - type: 'multiOptions', + type: 'options', displayOptions: { show: { service: [ @@ -69,12 +70,12 @@ export class ZendeskTrigger implements INodeType { }, options: [ { - name: 'ticket.status.open', - value: 'ticket.status.open' + name: 'ticket.created', + value: 'ticket.created', }, ], required: true, - default: [], + default: '', description: '', }, ], @@ -84,55 +85,72 @@ export class ZendeskTrigger implements INodeType { webhookMethods = { default: { async checkExists(this: IHookFunctions): Promise { - let webhooks; const webhookData = this.getWorkflowStaticData('node'); if (webhookData.webhookId === undefined) { return false; } - const endpoint = `/webhooks/${webhookData.webhookId}/`; + const endpoint = `/triggers/${webhookData.webhookId}`; try { - webhooks = await zendeskApiRequest.call(this, 'GET', endpoint); + await zendeskApiRequest.call(this, 'GET', endpoint); } catch (e) { return false; } return true; }, async create(this: IHookFunctions): Promise { - let body, responseData; + let condition: IDataObject = {}; const webhookUrl = this.getNodeWebhookUrl('default'); const webhookData = this.getWorkflowStaticData('node'); const event = this.getNodeParameter('event') as string; - const actions = this.getNodeParameter('actions') as string[]; - const endpoint = `/webhooks/`; - // @ts-ignore - body = { - endpoint_url: webhookUrl, - actions: actions.join(','), - event_id: event, - }; - try { - responseData = await zendeskApiRequest.call(this, 'POST', endpoint, body); - } catch(error) { - console.log(error) - return false; + if (event === 'ticket.created') { + condition = { + all: [ + { + field: 'status', + value: 'open', + }, + ], + } } + const bodyTrigger: IDataObject = { + trigger: { + conditions: { ...condition }, + actions: [ + { + field: 'notification_target', + value: [], + } + ] + }, + } + const bodyTarget: IDataObject = { + target: { + title: 'N8N webhook', + type: 'http_target', + target_url: webhookUrl, + method: 'POST', + active: true, + content_type: 'application/json', + }, + } + const { target } = await zendeskApiRequest.call(this, 'POST', '/targets', bodyTarget); // @ts-ignore - webhookData.webhookId = responseData.id; + bodyTrigger.trigger.actions[0].value = [target.id, '']; + const { trigger } = await zendeskApiRequest.call(this, 'POST', '/triggers', bodyTrigger); + webhookData.webhookId = trigger.id; + webhookData.targetId = target.id; return true; }, async delete(this: IHookFunctions): Promise { - let responseData; const webhookData = this.getWorkflowStaticData('node'); - const endpoint = `/webhooks/${webhookData.webhookId}/`; try { - responseData = await zendeskApiRequest.call(this, 'DELETE', endpoint); + await zendeskApiRequest.call(this, 'DELETE', `/triggers/${webhookData.webhookId}`); + await zendeskApiRequest.call(this, 'DELETE', `/targets/${webhookData.targetId}`); } catch(error) { return false; } - if (!responseData.success) { - return false; - } delete webhookData.webhookId; + delete webhookData.targetId return true; }, }, From 0d4a7f54085b526e6ae70c093ec4246039b78863 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Mon, 6 Jan 2020 19:30:40 -0500 Subject: [PATCH 10/26] done --- .../nodes/Zendesk/GenericFunctions.ts | 30 +- .../nodes/Zendesk/ZendeskTrigger.node.ts | 773 ++++++++++++++++-- 2 files changed, 753 insertions(+), 50 deletions(-) diff --git a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts index 9677f5652..392c1426e 100644 --- a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts @@ -18,7 +18,7 @@ export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions method, qs, body, - uri: uri ||`${credentials.domain}/api/v2${resource}.json`, + uri: uri ||`${credentials.url}/api/v2${resource}.json`, json: true }; options = Object.assign({}, options, option); @@ -29,9 +29,33 @@ export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions return await this.helpers.request!(options); } catch (err) { let errorMessage = ''; - if (err.error && err.description) { - errorMessage = err.description; + if (err.message && err.error) { + errorMessage = err.message; } throw new Error(errorMessage); } } + +/** + * Make an API request to paginated flow endpoint + * and return all results + */ +export async function zendeskApiRequestAllItems(this: IHookFunctions | IExecuteFunctions| ILoadOptionsFunctions, propertyName: string, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + let uri: string | undefined; + + do { + responseData = await zendeskApiRequest.call(this, method, resource, body, query, uri); + uri = responseData.next_page + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData.next_page !== undefined && + responseData.next_page !== null + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts index 0859dd9db..e86a6ddd7 100644 --- a/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts +++ b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts @@ -8,10 +8,13 @@ import { INodeType, IWebhookResponseData, IDataObject, + INodePropertyOptions, + ILoadOptionsFunctions, } from 'n8n-workflow'; import { zendeskApiRequest, + zendeskApiRequestAllItems, } from './GenericFunctions'; export class ZendeskTrigger implements INodeType { @@ -24,7 +27,7 @@ export class ZendeskTrigger implements INodeType { description: 'Handle Zendesk events via webhooks', defaults: { name: 'Zendesk Trigger', - color: '#559922', + color: '#13353c', }, inputs: [], outputs: ['main'], @@ -58,9 +61,9 @@ export class ZendeskTrigger implements INodeType { description: '', }, { - displayName: 'Events', - name: 'events', - type: 'options', + displayName: 'Title', + name: 'title', + type: 'string', displayOptions: { show: { service: [ @@ -68,19 +71,669 @@ export class ZendeskTrigger implements INodeType { ] } }, - options: [ - { - name: 'ticket.created', - value: 'ticket.created', - }, - ], required: true, default: '', description: '', }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + displayOptions: { + show: { + service: [ + 'support' + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'multiOptions', + default: [], + options: [ + { + name: 'Title', + value: 'ticket.title', + description: `Ticket's subject`, + }, + { + name: 'Description', + value: 'ticket.description', + description: `Ticket's description`, + }, + { + name: 'URL', + value: 'ticket.url', + description: `Ticket's URL`, + }, + { + name: 'ID', + value: 'ticket.id', + description: `Ticket's ID`, + }, + { + name: 'External ID', + value: 'ticket.external_id', + description: `Ticket's external ID`, + }, + { + name: 'Via', + value: 'ticket.via', + description: `Ticket's source` + }, + { + name: 'Status', + value: 'ticket.status', + description: `Ticket's status`, + }, + { + name: 'Priority', + value: 'ticket.priority', + description: `Ticket's priority`, + }, + { + name: 'Type', + value: 'ticket.ticket_type', + description: `Ticket's type`, + }, + { + name: 'Group Name', + value: 'ticket.group.name', + description: `Ticket's assigned group`, + }, + { + name: 'Brand Name', + value: 'ticket.brand.name', + description: `Ticket's brand`, + }, + { + name: 'Due Date', + value: 'ticket.due_date', + description: `Ticket's due date (relevant for tickets of type Task)`, + }, + { + name: 'Account', + value: 'ticket.account', + description: `This Zendesk Support's account name`, + }, + { + name: 'Assignee Email', + value: 'ticket.assignee.email', + description: `Ticket assignee email (if any)`, + }, + { + name: 'Assignee Name', + value: 'ticket.assignee.name', + description: `Assignee's full name`, + }, + { + name: 'Assignee First Name', + value: 'ticket.assignee.first_name', + description: `Assignee's first name`, + }, + { + name: 'Assignee Last Name', + value: 'ticket.assignee.last_name', + description: `Assignee's last name`, + }, + { + name: 'Requester Full Name', + value: 'ticket.requester.name', + description: `Requester's full name`, + }, + { + name: 'Requester First Name', + value: 'ticket.requester.first_name', + description: `Requester's first name`, + }, + { + name: 'Requester Last Name', + value: 'ticket.requester.last_name', + description: `Requester's last name`, + }, + { + name: 'Requester Email', + value: 'ticket.requester.email', + description: `Requester's email`, + }, + { + name: 'Requester Language', + value: 'ticket.requester.language', + description: `Requester's language`, + }, + { + name: 'Requester Phone', + value: 'ticket.requester.phone', + description: `Requester's phone number`, + }, + { + name: 'Requester External ID', + value: 'ticket.requester.external_id', + description: `Requester's external ID`, + }, + { + name: 'Requester Field', + value: 'ticket.requester.requester_field', + description: `Name or email`, + }, + { + name: 'Requester Details', + value: 'ticket.requester.details', + description: `Detailed information about the ticket's requester`, + }, + { + name: 'Requester Organization', + value: 'ticket.organization.name', + description: `Requester's organization`, + }, + { + name: `Ticket's Organization External ID`, + value: 'ticket.organization.external_id', + description: `Ticket's organization external ID`, + }, + { + name: `Organization details`, + value: 'ticket.organization.details', + description: `The details about the organization of the ticket's requester`, + }, + { + name: `Organization Note`, + value: 'ticket.organization.notes', + description: `The notes about the organization of the ticket's requester`, + }, + { + name: `Ticket's CCs`, + value: 'ticket.ccs', + description: `Ticket's CCs`, + }, + { + name: `Ticket's CCs names`, + value: 'ticket.cc_names', + description: `Ticket's CCs names`, + }, + { + name: `Ticket's tags`, + value: 'ticket.tags', + description: `Ticket's tags`, + }, + { + name: `Current Holiday Name`, + value: 'ticket.current_holiday_name', + description: `Displays the name of the current holiday on the ticket's schedule`, + }, + { + name: `Current User Name `, + value: 'current_user.name', + description: `Your full name`, + }, + { + name: `Current User First Name `, + value: 'current_user.first_name', + description: 'Your first name', + }, + { + name: `Current User Email `, + value: 'current_user.email', + description: 'Your primary email', + }, + { + name: `Current User Organization Name `, + value: 'current_user.organization.name', + description: 'Your default organization', + }, + { + name: `Current User Organization Details `, + value: 'current_user.organization.details', + description: `Your default organization's details`, + }, + { + name: `Current User Organization Notes `, + value: 'current_user.organization.notes', + description: `Your default organization's note`, + }, + { + name: `Current User Language `, + value: 'current_user.language', + description: `Your chosen language`, + }, + { + name: `Current User External ID `, + value: 'current_user.external_id', + description: 'Your external ID', + }, + { + name: `Current User Notes `, + value: 'current_user.notes', + description: 'Your notes, stored in your profile', + }, + { + name: `Satisfation Current Rating `, + value: 'satisfaction.current_rating', + description: 'The text of the current satisfaction rating', + }, + { + name: `Satisfation Current Comment `, + value: 'satisfaction.current_comment', + description: 'The text of the current satisfaction rating comment``', + }, + ], + }, + ], + placeholder: 'Add Option', + }, + { + displayName: 'Conditions', + name: 'conditions', + placeholder: 'Add Condition', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + service: [ + 'support' + ], + } + }, + description: 'The condition to set.', + default: {}, + options: [ + { + name: 'all', + displayName: 'All', + values: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Ticket', + value: 'ticket', + }, + ], + default: 'ticket', + description: '', + }, + { + displayName: 'Field', + name: 'field', + type: 'options', + displayOptions: { + show: { + 'resource': [ + 'ticket' + ] + } + }, + options: [ + { + name: 'Status', + value: 'status', + }, + { + name: 'Type', + value: 'type', + }, + { + name: 'Priority', + value: 'priority', + }, + { + name: 'Group', + value: 'group', + }, + { + name: 'Assignee', + value: 'assignee', + }, + ], + default: 'status', + description: '', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Is', + value: 'is', + }, + { + name: 'Is Not', + value: 'is_not', + }, + { + name: 'Less Than', + value: 'less_than', + }, + { + name: 'Greater Than', + value: 'greater_than', + }, + { + name: 'Changed', + value: 'changed', + }, + { + name: 'Changed To', + value: 'value', + }, + { + name: 'Changed From', + value: 'value_previous', + }, + { + name: 'Not Changed', + value: 'not_changed', + }, + { + name: 'Not Changed To', + value: 'not_value', + }, + { + name: 'Not Changed From', + value: 'not_value_previous', + }, + ], + displayOptions: { + hide: { + field: [ + 'assignee', + ] + } + }, + default: 'is', + description: '', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Is', + value: 'is', + }, + { + name: 'Is Not', + value: 'is_not', + }, + { + name: 'Changed', + value: 'changed', + }, + { + name: 'Changed To', + value: 'value', + }, + { + name: 'Changed From', + value: 'value_previous', + }, + { + name: 'Not Changed', + value: 'not_changed', + }, + { + name: 'Not Changed To', + value: 'not_value', + }, + { + name: 'Not Changed From', + value: 'not_value_previous', + }, + ], + displayOptions: { + show: { + field: [ + 'assignee', + ] + } + }, + default: 'is', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + displayOptions: { + show: { + field: [ + 'status' + ], + }, + hide: { + operation:[ + 'changed', + 'not_changed', + ], + field: [ + 'assignee', + 'group', + 'priority', + 'type', + ], + } + }, + options: [ + { + name: 'Open', + value: 'open', + }, + { + name: 'New', + value: 'new', + }, + { + name: 'Pending', + value: 'pending', + }, + { + name: 'Solved', + value: 'solved', + }, + { + name: 'Closed', + value: 'closed', + }, + ], + default: 'open', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + displayOptions: { + show: { + field: [ + 'type' + ], + }, + hide: { + operation:[ + 'changed', + 'not_changed', + ], + field: [ + 'assignee', + 'group', + 'priority', + 'status', + ], + } + }, + options: [ + { + name: 'Question', + value: 'question', + }, + { + name: 'Incident', + value: 'incident', + }, + { + name: 'Problem', + value: 'problem', + }, + { + name: 'Task', + value: 'task', + }, + ], + default: 'question', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + displayOptions: { + show: { + field: [ + 'priority' + ], + }, + hide: { + operation:[ + 'changed', + 'not_changed', + ], + field: [ + 'assignee', + 'group', + 'type', + 'status', + ], + } + }, + options: [ + { + name: 'Low', + value: 'low', + }, + { + name: 'Normal', + value: 'normal', + }, + { + name: 'High', + value: 'high', + }, + { + name: 'Urgent', + value: 'urgent', + }, + ], + default: 'low', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getGroups', + }, + displayOptions: { + show: { + field: [ + 'group' + ], + }, + hide: { + field: [ + 'assignee', + 'priority', + 'type', + 'status', + ], + }, + }, + default: '', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + displayOptions: { + show: { + field: [ + 'assignee' + ], + }, + hide: { + field: [ + 'group', + 'priority', + 'type', + 'status', + ], + }, + }, + default: '', + description: '', + }, + ] + } + ], + }, ], }; + methods = { + loadOptions: { + // Get all the groups to display them to user so that he can + // select them easily + async getGroups(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const groups = await zendeskApiRequestAllItems.call(this, 'groups', 'GET', '/groups'); + for (const group of groups) { + const groupName = group.name; + const groupId = group.id; + returnData.push({ + name: groupName, + value: groupId, + }); + } + 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 zendeskApiRequestAllItems.call(this, 'users', 'GET', '/users'); + for (const user of users) { + const userName = user.name; + const userId = user.id; + returnData.push({ + name: userName, + value: userId, + }); + } + returnData.push({ + name: 'Current User', + value: 'current_user', + }) + returnData.push({ + name: 'Requester', + value: 'requester_id', + }) + return returnData; + }, + } + }; // @ts-ignore webhookMethods = { default: { @@ -98,47 +751,73 @@ export class ZendeskTrigger implements INodeType { return true; }, async create(this: IHookFunctions): Promise { - let condition: IDataObject = {}; const webhookUrl = this.getNodeWebhookUrl('default'); const webhookData = this.getWorkflowStaticData('node'); - const event = this.getNodeParameter('event') as string; - if (event === 'ticket.created') { - condition = { - all: [ - { - field: 'status', - value: 'open', - }, - ], + const service = this.getNodeParameter('service') as string; + if (service === 'support') { + const aux: IDataObject = {}; + const message: IDataObject = {}; + const resultAll = []; + const title = this.getNodeParameter('title') as string; + const conditions = this.getNodeParameter('conditions') as IDataObject; + const options = this.getNodeParameter('options') as IDataObject; + if (Object.keys(conditions).length === 0) { + throw new Error('You must have at least one condition'); } + console.log(options) + if (options.fields) { + // @ts-ignore + for (let field of options.fields) { + // @ts-ignore + message[field] = `{{${field}}}`; + } + } else { + message['ticket.id'] = '{{ticket.id}}' + } + const conditionsAll = conditions.all as [IDataObject]; + for (let conditionAll of conditionsAll) { + aux.field = conditionAll.field; + aux.operator = conditionAll.operation; + if (conditionAll.operation !== 'changed' + && conditionAll.operation !== 'not_changed') { + aux.value = conditionAll.value; + } else { + aux.value = null; + } + resultAll.push(aux) + } + const bodyTrigger: IDataObject = { + trigger: { + title, + conditions: { + all: resultAll, + any: [], + }, + actions: [ + { + field: 'notification_target', + value: [], + } + ] + }, + } + const bodyTarget: IDataObject = { + target: { + title: 'N8N webhook', + type: 'http_target', + target_url: webhookUrl, + method: 'POST', + active: true, + content_type: 'application/json', + }, + }; + const { target } = await zendeskApiRequest.call(this, 'POST', '/targets', bodyTarget); + // @ts-ignore + bodyTrigger.trigger.actions[0].value = [target.id, JSON.stringify(message)]; + const { trigger } = await zendeskApiRequest.call(this, 'POST', '/triggers', bodyTrigger); + webhookData.webhookId = trigger.id; + webhookData.targetId = target.id; } - const bodyTrigger: IDataObject = { - trigger: { - conditions: { ...condition }, - actions: [ - { - field: 'notification_target', - value: [], - } - ] - }, - } - const bodyTarget: IDataObject = { - target: { - title: 'N8N webhook', - type: 'http_target', - target_url: webhookUrl, - method: 'POST', - active: true, - content_type: 'application/json', - }, - } - const { target } = await zendeskApiRequest.call(this, 'POST', '/targets', bodyTarget); - // @ts-ignore - bodyTrigger.trigger.actions[0].value = [target.id, '']; - const { trigger } = await zendeskApiRequest.call(this, 'POST', '/triggers', bodyTrigger); - webhookData.webhookId = trigger.id; - webhookData.targetId = target.id; return true; }, async delete(this: IHookFunctions): Promise { From 0cb7965101f6b3ac2790de95e89077e2a3e9b591 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Mon, 6 Jan 2020 19:52:37 -0500 Subject: [PATCH 11/26] :sparkles: zendesk trigger --- .../nodes/Zendesk/ConditionDescription.ts | 343 +++++++++++++++ .../nodes/Zendesk/ZendeskTrigger.node.ts | 391 ++---------------- 2 files changed, 382 insertions(+), 352 deletions(-) create mode 100644 packages/nodes-base/nodes/Zendesk/ConditionDescription.ts diff --git a/packages/nodes-base/nodes/Zendesk/ConditionDescription.ts b/packages/nodes-base/nodes/Zendesk/ConditionDescription.ts new file mode 100644 index 000000000..802ba004f --- /dev/null +++ b/packages/nodes-base/nodes/Zendesk/ConditionDescription.ts @@ -0,0 +1,343 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const conditionFields = [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Ticket', + value: 'ticket', + }, + ], + default: 'ticket', + description: '', + }, + { + displayName: 'Field', + name: 'field', + type: 'options', + displayOptions: { + show: { + 'resource': [ + 'ticket' + ] + } + }, + options: [ + { + name: 'Status', + value: 'status', + }, + { + name: 'Type', + value: 'type', + }, + { + name: 'Priority', + value: 'priority', + }, + { + name: 'Group', + value: 'group', + }, + { + name: 'Assignee', + value: 'assignee', + }, + ], + default: 'status', + description: '', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Is', + value: 'is', + }, + { + name: 'Is Not', + value: 'is_not', + }, + { + name: 'Less Than', + value: 'less_than', + }, + { + name: 'Greater Than', + value: 'greater_than', + }, + { + name: 'Changed', + value: 'changed', + }, + { + name: 'Changed To', + value: 'value', + }, + { + name: 'Changed From', + value: 'value_previous', + }, + { + name: 'Not Changed', + value: 'not_changed', + }, + { + name: 'Not Changed To', + value: 'not_value', + }, + { + name: 'Not Changed From', + value: 'not_value_previous', + }, + ], + displayOptions: { + hide: { + field: [ + 'assignee', + ] + } + }, + default: 'is', + description: '', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Is', + value: 'is', + }, + { + name: 'Is Not', + value: 'is_not', + }, + { + name: 'Changed', + value: 'changed', + }, + { + name: 'Changed To', + value: 'value', + }, + { + name: 'Changed From', + value: 'value_previous', + }, + { + name: 'Not Changed', + value: 'not_changed', + }, + { + name: 'Not Changed To', + value: 'not_value', + }, + { + name: 'Not Changed From', + value: 'not_value_previous', + }, + ], + displayOptions: { + show: { + field: [ + 'assignee', + ] + } + }, + default: 'is', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + displayOptions: { + show: { + field: [ + 'status' + ], + }, + hide: { + operation:[ + 'changed', + 'not_changed', + ], + field: [ + 'assignee', + 'group', + 'priority', + 'type', + ], + } + }, + options: [ + { + name: 'Open', + value: 'open', + }, + { + name: 'New', + value: 'new', + }, + { + name: 'Pending', + value: 'pending', + }, + { + name: 'Solved', + value: 'solved', + }, + { + name: 'Closed', + value: 'closed', + }, + ], + default: 'open', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + displayOptions: { + show: { + field: [ + 'type' + ], + }, + hide: { + operation:[ + 'changed', + 'not_changed', + ], + field: [ + 'assignee', + 'group', + 'priority', + 'status', + ], + } + }, + options: [ + { + name: 'Question', + value: 'question', + }, + { + name: 'Incident', + value: 'incident', + }, + { + name: 'Problem', + value: 'problem', + }, + { + name: 'Task', + value: 'task', + }, + ], + default: 'question', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + displayOptions: { + show: { + field: [ + 'priority' + ], + }, + hide: { + operation:[ + 'changed', + 'not_changed', + ], + field: [ + 'assignee', + 'group', + 'type', + 'status', + ], + } + }, + options: [ + { + name: 'Low', + value: 'low', + }, + { + name: 'Normal', + value: 'normal', + }, + { + name: 'High', + value: 'high', + }, + { + name: 'Urgent', + value: 'urgent', + }, + ], + default: 'low', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getGroups', + }, + displayOptions: { + show: { + field: [ + 'group' + ], + }, + hide: { + field: [ + 'assignee', + 'priority', + 'type', + 'status', + ], + }, + }, + default: '', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + displayOptions: { + show: { + field: [ + 'assignee' + ], + }, + hide: { + field: [ + 'group', + 'priority', + 'type', + 'status', + ], + }, + }, + default: '', + description: '', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts index e86a6ddd7..a380fa07b 100644 --- a/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts +++ b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts @@ -16,6 +16,9 @@ import { zendeskApiRequest, zendeskApiRequestAllItems, } from './GenericFunctions'; +import { + conditionFields + } from './ConditionDescription'; export class ZendeskTrigger implements INodeType { description: INodeTypeDescription = { @@ -346,347 +349,16 @@ export class ZendeskTrigger implements INodeType { name: 'all', displayName: 'All', values: [ - { - displayName: 'Resource', - name: 'resource', - type: 'options', - options: [ - { - name: 'Ticket', - value: 'ticket', - }, - ], - default: 'ticket', - description: '', - }, - { - displayName: 'Field', - name: 'field', - type: 'options', - displayOptions: { - show: { - 'resource': [ - 'ticket' - ] - } - }, - options: [ - { - name: 'Status', - value: 'status', - }, - { - name: 'Type', - value: 'type', - }, - { - name: 'Priority', - value: 'priority', - }, - { - name: 'Group', - value: 'group', - }, - { - name: 'Assignee', - value: 'assignee', - }, - ], - default: 'status', - description: '', - }, - { - displayName: 'Operation', - name: 'operation', - type: 'options', - options: [ - { - name: 'Is', - value: 'is', - }, - { - name: 'Is Not', - value: 'is_not', - }, - { - name: 'Less Than', - value: 'less_than', - }, - { - name: 'Greater Than', - value: 'greater_than', - }, - { - name: 'Changed', - value: 'changed', - }, - { - name: 'Changed To', - value: 'value', - }, - { - name: 'Changed From', - value: 'value_previous', - }, - { - name: 'Not Changed', - value: 'not_changed', - }, - { - name: 'Not Changed To', - value: 'not_value', - }, - { - name: 'Not Changed From', - value: 'not_value_previous', - }, - ], - displayOptions: { - hide: { - field: [ - 'assignee', - ] - } - }, - default: 'is', - description: '', - }, - { - displayName: 'Operation', - name: 'operation', - type: 'options', - options: [ - { - name: 'Is', - value: 'is', - }, - { - name: 'Is Not', - value: 'is_not', - }, - { - name: 'Changed', - value: 'changed', - }, - { - name: 'Changed To', - value: 'value', - }, - { - name: 'Changed From', - value: 'value_previous', - }, - { - name: 'Not Changed', - value: 'not_changed', - }, - { - name: 'Not Changed To', - value: 'not_value', - }, - { - name: 'Not Changed From', - value: 'not_value_previous', - }, - ], - displayOptions: { - show: { - field: [ - 'assignee', - ] - } - }, - default: 'is', - description: '', - }, - { - displayName: 'Value', - name: 'value', - type: 'options', - displayOptions: { - show: { - field: [ - 'status' - ], - }, - hide: { - operation:[ - 'changed', - 'not_changed', - ], - field: [ - 'assignee', - 'group', - 'priority', - 'type', - ], - } - }, - options: [ - { - name: 'Open', - value: 'open', - }, - { - name: 'New', - value: 'new', - }, - { - name: 'Pending', - value: 'pending', - }, - { - name: 'Solved', - value: 'solved', - }, - { - name: 'Closed', - value: 'closed', - }, - ], - default: 'open', - description: '', - }, - { - displayName: 'Value', - name: 'value', - type: 'options', - displayOptions: { - show: { - field: [ - 'type' - ], - }, - hide: { - operation:[ - 'changed', - 'not_changed', - ], - field: [ - 'assignee', - 'group', - 'priority', - 'status', - ], - } - }, - options: [ - { - name: 'Question', - value: 'question', - }, - { - name: 'Incident', - value: 'incident', - }, - { - name: 'Problem', - value: 'problem', - }, - { - name: 'Task', - value: 'task', - }, - ], - default: 'question', - description: '', - }, - { - displayName: 'Value', - name: 'value', - type: 'options', - displayOptions: { - show: { - field: [ - 'priority' - ], - }, - hide: { - operation:[ - 'changed', - 'not_changed', - ], - field: [ - 'assignee', - 'group', - 'type', - 'status', - ], - } - }, - options: [ - { - name: 'Low', - value: 'low', - }, - { - name: 'Normal', - value: 'normal', - }, - { - name: 'High', - value: 'high', - }, - { - name: 'Urgent', - value: 'urgent', - }, - ], - default: 'low', - description: '', - }, - { - displayName: 'Value', - name: 'value', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getGroups', - }, - displayOptions: { - show: { - field: [ - 'group' - ], - }, - hide: { - field: [ - 'assignee', - 'priority', - 'type', - 'status', - ], - }, - }, - default: '', - description: '', - }, - { - displayName: 'Value', - name: 'value', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getUsers', - }, - displayOptions: { - show: { - field: [ - 'assignee' - ], - }, - hide: { - field: [ - 'group', - 'priority', - 'type', - 'status', - ], - }, - }, - default: '', - description: '', - }, + ...conditionFields, ] - } + }, + { + name: 'any', + displayName: 'Any', + values: [ + ...conditionFields, + ] + }, ], }, ], @@ -757,14 +429,13 @@ export class ZendeskTrigger implements INodeType { if (service === 'support') { const aux: IDataObject = {}; const message: IDataObject = {}; - const resultAll = []; + const resultAll = [], resultAny = []; const title = this.getNodeParameter('title') as string; const conditions = this.getNodeParameter('conditions') as IDataObject; const options = this.getNodeParameter('options') as IDataObject; if (Object.keys(conditions).length === 0) { throw new Error('You must have at least one condition'); } - console.log(options) if (options.fields) { // @ts-ignore for (let field of options.fields) { @@ -775,23 +446,39 @@ export class ZendeskTrigger implements INodeType { message['ticket.id'] = '{{ticket.id}}' } const conditionsAll = conditions.all as [IDataObject]; - for (let conditionAll of conditionsAll) { - aux.field = conditionAll.field; - aux.operator = conditionAll.operation; - if (conditionAll.operation !== 'changed' - && conditionAll.operation !== 'not_changed') { - aux.value = conditionAll.value; - } else { - aux.value = null; + if (conditionsAll) { + for (let conditionAll of conditionsAll) { + aux.field = conditionAll.field; + aux.operator = conditionAll.operation; + if (conditionAll.operation !== 'changed' + && conditionAll.operation !== 'not_changed') { + aux.value = conditionAll.value; + } else { + aux.value = null; + } + resultAll.push(aux) + } + } + const conditionsAny = conditions.any as [IDataObject]; + if (conditionsAny) { + for (let conditionAny of conditionsAny) { + aux.field = conditionAny.field; + aux.operator = conditionAny.operation; + if (conditionAny.operation !== 'changed' + && conditionAny.operation !== 'not_changed') { + aux.value = conditionAny.value; + } else { + aux.value = null; + } + resultAny.push(aux) } - resultAll.push(aux) } const bodyTrigger: IDataObject = { trigger: { title, conditions: { all: resultAll, - any: [], + any: resultAny, }, actions: [ { From 3448575a206dc0c98f3d00e8ce7302d75cdb0d5e Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Tue, 7 Jan 2020 14:56:42 -0500 Subject: [PATCH 12/26] :sparkles: added zendesk node --- .../nodes/Zendesk/GenericFunctions.ts | 6 +- .../nodes/Zendesk/TicketDescription.ts | 503 ++++++++++++++++++ .../nodes/Zendesk/TicketInterface.ts | 16 + .../nodes-base/nodes/Zendesk/Zendesk.node.ts | 238 +++++++++ packages/nodes-base/package.json | 3 +- 5 files changed, 760 insertions(+), 6 deletions(-) create mode 100644 packages/nodes-base/nodes/Zendesk/TicketDescription.ts create mode 100644 packages/nodes-base/nodes/Zendesk/TicketInterface.ts create mode 100644 packages/nodes-base/nodes/Zendesk/Zendesk.node.ts diff --git a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts index 392c1426e..0c87d8773 100644 --- a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts @@ -28,11 +28,7 @@ export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions try { return await this.helpers.request!(options); } catch (err) { - let errorMessage = ''; - if (err.message && err.error) { - errorMessage = err.message; - } - throw new Error(errorMessage); + throw new Error(err); } } diff --git a/packages/nodes-base/nodes/Zendesk/TicketDescription.ts b/packages/nodes-base/nodes/Zendesk/TicketDescription.ts new file mode 100644 index 000000000..601beb71f --- /dev/null +++ b/packages/nodes-base/nodes/Zendesk/TicketDescription.ts @@ -0,0 +1,503 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const ticketOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'ticket', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a ticket', + }, + { + name: 'Update', + value: 'update', + description: 'Update a ticket' + }, + { + name: 'Get', + value: 'get', + description: 'Get a ticket' + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all tickets' + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a ticket' + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const ticketFields = [ + +/* -------------------------------------------------------------------------- */ +/* ticket:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Description', + name: 'description', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'create', + ], + }, + }, + required: true, + description: 'The first comment on the ticket', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'External ID', + name: 'externalId', + type: 'string', + default: '', + description: 'An id you can use to link Zendesk Support tickets to local records', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'string', + default: '', + description: 'The value of the subject field for this ticket', + }, + { + displayName: 'Recipient', + name: 'recipient', + type: 'string', + default: '', + description: 'The original recipient e-mail address of the ticket', + }, + { + displayName: 'Group', + name: 'group', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getGroups', + }, + default: '', + description: 'The group this ticket is assigned to', + }, + { + displayName: 'Tags', + name: 'tags', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + default: [], + description: 'The array of tags applied to this ticket', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Question', + value: 'question', + }, + { + name: 'Incident', + value: 'incident', + }, + { + name: 'Problem', + value: 'problem', + }, + { + name: 'Task', + value: 'task', + }, + ], + default: '', + description: 'The type of this ticket', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Open', + value: 'open', + }, + { + name: 'New', + value: 'new', + }, + { + name: 'Pending', + value: 'pending', + }, + { + name: 'Solved', + value: 'solved', + }, + { + name: 'Closed', + value: 'closed', + }, + ], + default: '', + description: 'The state of the ticket', + } + ], + }, +/* -------------------------------------------------------------------------- */ +/* ticket:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'update', + ], + }, + }, + description: 'Ticket ID', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'External ID', + name: 'externalId', + type: 'string', + default: '', + description: 'An id you can use to link Zendesk Support tickets to local records', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'string', + default: '', + description: 'The value of the subject field for this ticket', + }, + { + displayName: 'Recipient', + name: 'recipient', + type: 'string', + default: '', + description: 'The original recipient e-mail address of the ticket', + }, + { + displayName: 'Group', + name: 'group', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getGroups', + }, + default: '', + description: 'The group this ticket is assigned to', + }, + { + displayName: 'Tags', + name: 'tags', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + default: [], + description: 'The array of tags applied to this ticket', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Question', + value: 'question', + }, + { + name: 'Incident', + value: 'incident', + }, + { + name: 'Problem', + value: 'problem', + }, + { + name: 'Task', + value: 'task', + }, + ], + default: '', + description: 'The type of this ticket', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Open', + value: 'open', + }, + { + name: 'New', + value: 'new', + }, + { + name: 'Pending', + value: 'pending', + }, + { + name: 'Solved', + value: 'solved', + }, + { + name: 'Closed', + value: 'closed', + }, + ], + default: '', + description: 'The state of the ticket', + } + ], + }, +/* -------------------------------------------------------------------------- */ +/* ticket:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'get', + ], + }, + }, + description: 'Ticket ID', + }, +/* -------------------------------------------------------------------------- */ +/* ticket:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'ticket', + ], + 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: [ + 'ticket', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Open', + value: 'open', + }, + { + name: 'New', + value: 'new', + }, + { + name: 'Pending', + value: 'pending', + }, + { + name: 'Solved', + value: 'solved', + }, + { + name: 'Closed', + value: 'closed', + }, + ], + default: '', + description: 'The state of the ticket', + }, + { + displayName: 'Sort By', + name: 'sortBy', + type: 'options', + options: [ + { + name: 'Updated At', + value: 'updated_at', + }, + { + name: 'Created At', + value: 'created_at', + }, + { + name: 'Priority', + value: 'priority', + }, + { + name: 'Status', + value: 'status', + }, + { + name: 'Ticket Type', + value: 'ticket_type', + }, + ], + default: 'updated_at', + description: 'Defaults to sorting by relevance', + }, + { + displayName: 'Sort Order', + name: 'sortOrder', + type: 'options', + options: [ + { + name: 'Asc', + value: 'asc', + }, + { + name: 'Desc', + value: 'desc', + }, + ], + default: 'desc', + description: 'Sort order', + } + ], + }, + +/* -------------------------------------------------------------------------- */ +/* ticket:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'delete', + ], + }, + }, + description: 'Ticket ID', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zendesk/TicketInterface.ts b/packages/nodes-base/nodes/Zendesk/TicketInterface.ts new file mode 100644 index 000000000..fc4eb7569 --- /dev/null +++ b/packages/nodes-base/nodes/Zendesk/TicketInterface.ts @@ -0,0 +1,16 @@ +import { IDataObject } from "n8n-workflow"; + +export interface ITicket { + subject?: string; + comment?: IComment; + type?: string; + group?: string; + external_id?: string; + tags?: string[]; + status?: string; + recipient?: string; +} + +export interface IComment { + body?: string; +} diff --git a/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts b/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts new file mode 100644 index 000000000..49f91ceb5 --- /dev/null +++ b/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts @@ -0,0 +1,238 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; +import { + IDataObject, + INodeTypeDescription, + INodeExecutionData, + INodeType, + ILoadOptionsFunctions, + INodePropertyOptions, +} from 'n8n-workflow'; +import { + zendeskApiRequest, + zendeskApiRequestAllItems, +} from './GenericFunctions'; +import { + ticketFields, + ticketOperations +} from './TicketDescription'; +import { + ITicket, + IComment, + } from './TicketInterface'; + +export class Zendesk implements INodeType { + description: INodeTypeDescription = { + displayName: 'Zendesk', + name: 'zendesk', + icon: 'file:zendesk.png', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Zendesk API', + defaults: { + name: 'Zendesk', + color: '#13353c', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'zendeskApi', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Ticket', + value: 'ticket', + description: 'Tickets are the means through which your end users (customers) communicate with agents in Zendesk Support.', + }, + ], + default: 'ticket', + description: 'Resource to consume.', + }, + ...ticketOperations, + ...ticketFields, + ], + }; + + methods = { + loadOptions: { + // Get all the groups to display them to user so that he can + // select them easily + async getGroups(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const groups = await zendeskApiRequestAllItems.call(this, 'groups', 'GET', '/groups'); + for (const group of groups) { + const groupName = group.name; + const groupId = group.id; + returnData.push({ + name: groupName, + value: groupId, + }); + } + 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 zendeskApiRequestAllItems.call(this, 'tags', 'GET', '/tags'); + for (const tag of tags) { + const tagName = tag.name; + const tagId = tag.name; + returnData.push({ + name: tagName, + value: tagId, + }); + } + return returnData; + }, + } + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + let 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; + //https://developer.zendesk.com/rest_api/docs/support/introduction + if (resource === 'ticket') { + //https://developer.zendesk.com/rest_api/docs/support/tickets + if (operation === 'create') { + const description = this.getNodeParameter('description', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const comment: IComment = { + body: description, + }; + const body: ITicket = { + comment, + }; + if (additionalFields.type) { + body.type = additionalFields.type as string; + } + if (additionalFields.externalId) { + body.external_id = additionalFields.externalId as string; + } + if (additionalFields.subject) { + body.subject = additionalFields.subject as string; + } + if (additionalFields.status) { + body.status = additionalFields.status as string; + } + if (additionalFields.recipient) { + body.recipient = additionalFields.recipient as string; + } + if (additionalFields.group) { + body.group = additionalFields.group as string; + } + if (additionalFields.tags) { + body.tags = additionalFields.tags as string[]; + } + try { + responseData = await zendeskApiRequest.call(this, 'POST', '/tickets', { ticket: body }); + responseData = responseData.ticket; + } catch (err) { + throw new Error(`Zendesk Error: ${err}`); + } + } + //https://developer.zendesk.com/rest_api/docs/support/tickets#update-ticket + if (operation === 'update') { + const ticketId = this.getNodeParameter('id', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + const body: ITicket = {}; + if (updateFields.type) { + body.type = updateFields.type as string; + } + if (updateFields.externalId) { + body.external_id = updateFields.externalId as string; + } + if (updateFields.subject) { + body.subject = updateFields.subject as string; + } + if (updateFields.status) { + body.status = updateFields.status as string; + } + if (updateFields.recipient) { + body.recipient = updateFields.recipient as string; + } + if (updateFields.group) { + body.group = updateFields.group as string; + } + if (updateFields.tags) { + body.tags = updateFields.tags as string[]; + } + try { + responseData = await zendeskApiRequest.call(this, 'PUT', `/tickets/${ticketId}`, { ticket: body }); + responseData = responseData.ticket; + } catch (err) { + throw new Error(`Zendesk Error: ${err}`); + } + } + //https://developer.zendesk.com/rest_api/docs/support/tickets#show-ticket + if (operation === 'get') { + const ticketId = this.getNodeParameter('id', i) as string; + try { + responseData = await zendeskApiRequest.call(this, 'GET', `/tickets/${ticketId}`, {}); + responseData = responseData.ticket; + } catch (err) { + throw new Error(`Zendesk Error: ${err}`); + } + } + //https://developer.zendesk.com/rest_api/docs/support/search#list-search-results + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const options = this.getNodeParameter('options', i) as IDataObject; + qs.query = 'type:ticket' + if (options.status) { + qs.query += ` status:${options.status}` + } + if (options.sortBy) { + qs.sort_by = options.sortBy; + } + if (options.sortOrder) { + qs.sort_order = options.sortOrder; + } + try { + if (returnAll) { + responseData = await zendeskApiRequestAllItems.call(this, 'results', 'GET', `/search`, {}, qs); + } else { + const limit = this.getNodeParameter('limit', i) as number; + qs.per_page = limit; + responseData = await zendeskApiRequest.call(this, 'GET', `/search`, {}, qs); + responseData = responseData.results; + } + } catch (err) { + throw new Error(`Zendesk Error: ${err}`); + } + } + //https://developer.zendesk.com/rest_api/docs/support/tickets#delete-ticket + if (operation === 'delete') { + const ticketId = this.getNodeParameter('id', i) as string; + try { + responseData = await zendeskApiRequest.call(this, 'DELETE', `/tickets/${ticketId}`, {}); + } catch (err) { + throw new Error(`Zendesk Error: ${err}`); + } + } + } + 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/package.json b/packages/nodes-base/package.json index ade1417d8..a4d423612 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -171,7 +171,8 @@ "dist/nodes/Webhook.node.js", "dist/nodes/Wordpress/Wordpress.node.js", "dist/nodes/Xml.node.js", - "dist/nodes/Zendesk/ZendeskTrigger.node.js" + "dist/nodes/Zendesk/ZendeskTrigger.node.js", + "dist/nodes/Zendesk/Zendesk.node.js" ] }, "devDependencies": { From d169a5617fcef2b5ea5834c04d0193578201b8bf Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 20 Dec 2019 16:35:00 -0600 Subject: [PATCH 13/26] :zap: Small improvements to Msg91-Node --- .../nodes/Msg91/GenericFunctions.ts | 14 ++--- packages/nodes-base/nodes/Msg91/Msg91.node.ts | 54 +++++++++---------- 2 files changed, 31 insertions(+), 37 deletions(-) diff --git a/packages/nodes-base/nodes/Msg91/GenericFunctions.ts b/packages/nodes-base/nodes/Msg91/GenericFunctions.ts index 59a5831bf..893996fc8 100644 --- a/packages/nodes-base/nodes/Msg91/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Msg91/GenericFunctions.ts @@ -24,19 +24,15 @@ export async function msg91ApiRequest(this: IHookFunctions | IExecuteFunctions, if (query === undefined) { query = {}; - } - - query.authkey = credentials.authkey as string; + } + + query.authkey = credentials.authkey as string; const options = { method, form: body, qs: query, - uri: `https://api.msg91.com/api/sendhttp.php`, - auth: { - user: '', - pass: '', - }, + uri: `https://api.msg91.com/api${endpoint}`, json: true }; @@ -61,4 +57,4 @@ export async function msg91ApiRequest(this: IHookFunctions | IExecuteFunctions, // If that data does not exist for some reason return the actual error throw error; } -} \ No newline at end of file +} diff --git a/packages/nodes-base/nodes/Msg91/Msg91.node.ts b/packages/nodes-base/nodes/Msg91/Msg91.node.ts index f59dd49ec..240a3623e 100644 --- a/packages/nodes-base/nodes/Msg91/Msg91.node.ts +++ b/packages/nodes-base/nodes/Msg91/Msg91.node.ts @@ -46,7 +46,6 @@ export class Msg91 implements INodeType { default: 'sms', description: 'The resource to operate on.', }, - { displayName: 'Operation', name: 'operation', @@ -69,8 +68,27 @@ export class Msg91 implements INodeType { description: 'The operation to perform.', }, { - displayName: 'Sender', - name: 'sender', + displayName: 'From', + name: 'from', + type: 'string', + default: '', + placeholder: '4155238886', + required: true, + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'sms', + ], + }, + }, + description: 'The number from which to send the message.', + }, + { + displayName: 'To', + name: 'to', type: 'string', default: '', placeholder: '+14155238886', @@ -85,26 +103,7 @@ export class Msg91 implements INodeType { ], }, }, - description: 'The number from which to send the message', - }, - { - displayName: 'To', - name: 'mobiles', - type: 'string', - default: '', - placeholder: 'Mobile Number With Country Code', - required: true, - displayOptions: { - show: { - operation: [ - 'send', - ], - resource: [ - 'sms', - ], - }, - }, - description: 'The number to which to send the message', + description: 'The number, with coutry code, to which to send the message.', }, { displayName: 'Message', @@ -145,7 +144,6 @@ export class Msg91 implements INodeType { let endpoint: string; for (let i = 0; i < items.length; i++) { - requestMethod = 'GET'; endpoint = ''; body = {}; qs = {}; @@ -160,12 +158,12 @@ export class Msg91 implements INodeType { // ---------------------------------- requestMethod = 'GET'; - endpoint = 'https://api.msg91.com/api/sendhttp.php'; + endpoint = '/sendhttp.php'; qs.route = 4; qs.country = 0; - qs.sender = this.getNodeParameter('sender', i) as string; - qs.mobiles = this.getNodeParameter('mobiles', i) as string; + qs.sender = this.getNodeParameter('from', i) as string; + qs.mobiles = this.getNodeParameter('to', i) as string; qs.message = this.getNodeParameter('message', i) as string; } else { @@ -177,7 +175,7 @@ export class Msg91 implements INodeType { const responseData = await msg91ApiRequest.call(this, requestMethod, endpoint, body, qs); - returnData.push(responseData as IDataObject); + returnData.push({ requestId: responseData }); } return [this.helpers.returnJsonArray(returnData)]; From 97cc3af4c320c8a74df83b25971a8f428c41c2ee Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 7 Jan 2020 21:15:37 -0600 Subject: [PATCH 14/26] :zap: Small fixes and improvements on Zendesk-Node --- .../nodes/Zendesk/ConditionDescription.ts | 9 ---- .../nodes/Zendesk/GenericFunctions.ts | 6 +-- .../nodes/Zendesk/TicketDescription.ts | 8 +-- .../nodes/Zendesk/TicketInterface.ts | 2 - .../nodes-base/nodes/Zendesk/Zendesk.node.ts | 6 +-- .../nodes/Zendesk/ZendeskTrigger.node.ts | 50 ++++++++----------- 6 files changed, 30 insertions(+), 51 deletions(-) diff --git a/packages/nodes-base/nodes/Zendesk/ConditionDescription.ts b/packages/nodes-base/nodes/Zendesk/ConditionDescription.ts index 802ba004f..99a1429b0 100644 --- a/packages/nodes-base/nodes/Zendesk/ConditionDescription.ts +++ b/packages/nodes-base/nodes/Zendesk/ConditionDescription.ts @@ -12,7 +12,6 @@ export const conditionFields = [ }, ], default: 'ticket', - description: '', }, { displayName: 'Field', @@ -48,7 +47,6 @@ export const conditionFields = [ }, ], default: 'status', - description: '', }, { displayName: 'Operation', @@ -104,7 +102,6 @@ export const conditionFields = [ } }, default: 'is', - description: '', }, { displayName: 'Operation', @@ -152,7 +149,6 @@ export const conditionFields = [ } }, default: 'is', - description: '', }, { displayName: 'Value', @@ -200,7 +196,6 @@ export const conditionFields = [ }, ], default: 'open', - description: '', }, { displayName: 'Value', @@ -244,7 +239,6 @@ export const conditionFields = [ }, ], default: 'question', - description: '', }, { displayName: 'Value', @@ -288,7 +282,6 @@ export const conditionFields = [ }, ], default: 'low', - description: '', }, { displayName: 'Value', @@ -313,7 +306,6 @@ export const conditionFields = [ }, }, default: '', - description: '', }, { displayName: 'Value', @@ -338,6 +330,5 @@ export const conditionFields = [ }, }, default: '', - description: '', }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts index 0c87d8773..5126c6949 100644 --- a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts @@ -1,9 +1,9 @@ import { OptionsWithUri } from 'request'; import { IExecuteFunctions, + IExecuteSingleFunctions, IHookFunctions, ILoadOptionsFunctions, - IExecuteSingleFunctions, } from 'n8n-core'; import { IDataObject } from 'n8n-workflow'; @@ -12,7 +12,7 @@ export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions if (credentials === undefined) { throw new Error('No credentials got returned!'); } - const base64Key = Buffer.from(`${credentials.email}/token:${credentials.apiToken}`).toString('base64') + const base64Key = Buffer.from(`${credentials.email}/token:${credentials.apiToken}`).toString('base64'); let options: OptionsWithUri = { headers: { 'Authorization': `Basic ${base64Key}`}, method, @@ -46,7 +46,7 @@ export async function zendeskApiRequestAllItems(this: IHookFunctions | IExecuteF do { responseData = await zendeskApiRequest.call(this, method, resource, body, query, uri); - uri = responseData.next_page + uri = responseData.next_page; returnData.push.apply(returnData, responseData[propertyName]); } while ( responseData.next_page !== undefined && diff --git a/packages/nodes-base/nodes/Zendesk/TicketDescription.ts b/packages/nodes-base/nodes/Zendesk/TicketDescription.ts index 601beb71f..477a8074e 100644 --- a/packages/nodes-base/nodes/Zendesk/TicketDescription.ts +++ b/packages/nodes-base/nodes/Zendesk/TicketDescription.ts @@ -21,22 +21,22 @@ export const ticketOperations = [ { name: 'Update', value: 'update', - description: 'Update a ticket' + description: 'Update a ticket', }, { name: 'Get', value: 'get', - description: 'Get a ticket' + description: 'Get a ticket', }, { name: 'Get All', value: 'getAll', - description: 'Get all tickets' + description: 'Get all tickets', }, { name: 'Delete', value: 'delete', - description: 'Delete a ticket' + description: 'Delete a ticket', }, ], default: 'create', diff --git a/packages/nodes-base/nodes/Zendesk/TicketInterface.ts b/packages/nodes-base/nodes/Zendesk/TicketInterface.ts index fc4eb7569..5ef968381 100644 --- a/packages/nodes-base/nodes/Zendesk/TicketInterface.ts +++ b/packages/nodes-base/nodes/Zendesk/TicketInterface.ts @@ -1,5 +1,3 @@ -import { IDataObject } from "n8n-workflow"; - export interface ITicket { subject?: string; comment?: IComment; diff --git a/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts b/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts index 49f91ceb5..91d055dae 100644 --- a/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts +++ b/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts @@ -102,7 +102,7 @@ export class Zendesk implements INodeType { const items = this.getInputData(); const returnData: IDataObject[] = []; const length = items.length as unknown as number; - let qs: IDataObject = {}; + const qs: IDataObject = {}; let responseData; for (let i = 0; i < length; i++) { const resource = this.getNodeParameter('resource', 0) as string; @@ -194,9 +194,9 @@ export class Zendesk implements INodeType { if (operation === 'getAll') { const returnAll = this.getNodeParameter('returnAll', i) as boolean; const options = this.getNodeParameter('options', i) as IDataObject; - qs.query = 'type:ticket' + qs.query = 'type:ticket'; if (options.status) { - qs.query += ` status:${options.status}` + qs.query += ` status:${options.status}`; } if (options.sortBy) { qs.sort_by = options.sortBy; diff --git a/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts index a380fa07b..e242141d9 100644 --- a/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts +++ b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts @@ -1,3 +1,7 @@ +import { + parse as urlParse, +} from 'url'; + import { IHookFunctions, IWebhookFunctions, @@ -23,7 +27,7 @@ import { export class ZendeskTrigger implements INodeType { description: INodeTypeDescription = { displayName: 'Zendesk Trigger', - name: 'zendesk', + name: 'zendeskTrigger', icon: 'file:zendesk.png', group: ['trigger'], version: 1, @@ -63,21 +67,6 @@ export class ZendeskTrigger implements INodeType { default: 'support', description: '', }, - { - displayName: 'Title', - name: 'title', - type: 'string', - displayOptions: { - show: { - service: [ - 'support' - ] - } - }, - required: true, - default: '', - description: '', - }, { displayName: 'Options', name: 'options', @@ -94,6 +83,7 @@ export class ZendeskTrigger implements INodeType { { displayName: 'Fields', name: 'fields', + description: 'The fields to return the values of.', type: 'multiOptions', default: [], options: [ @@ -397,11 +387,11 @@ export class ZendeskTrigger implements INodeType { returnData.push({ name: 'Current User', value: 'current_user', - }) + }); returnData.push({ name: 'Requester', value: 'requester_id', - }) + }); return returnData; }, } @@ -423,14 +413,13 @@ export class ZendeskTrigger implements INodeType { return true; }, async create(this: IHookFunctions): Promise { - const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookUrl = this.getNodeWebhookUrl('default') as string; const webhookData = this.getWorkflowStaticData('node'); const service = this.getNodeParameter('service') as string; if (service === 'support') { const aux: IDataObject = {}; const message: IDataObject = {}; const resultAll = [], resultAny = []; - const title = this.getNodeParameter('title') as string; const conditions = this.getNodeParameter('conditions') as IDataObject; const options = this.getNodeParameter('options') as IDataObject; if (Object.keys(conditions).length === 0) { @@ -438,16 +427,16 @@ export class ZendeskTrigger implements INodeType { } if (options.fields) { // @ts-ignore - for (let field of options.fields) { + for (const field of options.fields) { // @ts-ignore message[field] = `{{${field}}}`; } } else { - message['ticket.id'] = '{{ticket.id}}' + message['ticket.id'] = '{{ticket.id}}'; } const conditionsAll = conditions.all as [IDataObject]; if (conditionsAll) { - for (let conditionAll of conditionsAll) { + for (const conditionAll of conditionsAll) { aux.field = conditionAll.field; aux.operator = conditionAll.operation; if (conditionAll.operation !== 'changed' @@ -456,12 +445,12 @@ export class ZendeskTrigger implements INodeType { } else { aux.value = null; } - resultAll.push(aux) + resultAll.push(aux); } } const conditionsAny = conditions.any as [IDataObject]; if (conditionsAny) { - for (let conditionAny of conditionsAny) { + for (const conditionAny of conditionsAny) { aux.field = conditionAny.field; aux.operator = conditionAny.operation; if (conditionAny.operation !== 'changed' @@ -470,12 +459,13 @@ export class ZendeskTrigger implements INodeType { } else { aux.value = null; } - resultAny.push(aux) + resultAny.push(aux); } } + const urlParts = urlParse(webhookUrl); const bodyTrigger: IDataObject = { trigger: { - title, + title: `n8n-webhook:${urlParts.path}`, conditions: { all: resultAll, any: resultAny, @@ -487,10 +477,10 @@ export class ZendeskTrigger implements INodeType { } ] }, - } + }; const bodyTarget: IDataObject = { target: { - title: 'N8N webhook', + title: 'n8n webhook', type: 'http_target', target_url: webhookUrl, method: 'POST', @@ -516,7 +506,7 @@ export class ZendeskTrigger implements INodeType { return false; } delete webhookData.webhookId; - delete webhookData.targetId + delete webhookData.targetId; return true; }, }, From dfc3cb962ce2b1aa2e98cce59a1cdbe935091f30 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 7 Jan 2020 23:23:43 -0600 Subject: [PATCH 15/26] :shirt: Fix lint issue --- packages/editor-ui/src/components/mixins/restApi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor-ui/src/components/mixins/restApi.ts b/packages/editor-ui/src/components/mixins/restApi.ts index a2cdbd658..72616e83d 100644 --- a/packages/editor-ui/src/components/mixins/restApi.ts +++ b/packages/editor-ui/src/components/mixins/restApi.ts @@ -261,7 +261,7 @@ export const restApi = Vue.extend({ OAuth2Callback: (code: string, state: string): Promise => { const sendData = { 'code': code, - 'state': state + 'state': state, }; return self.restApi().makeRestApiRequest('POST', `/oauth2-credential/callback`, sendData); From e9a9b58afbb24ccf187e076cdfe5cb65ee2743f4 Mon Sep 17 00:00:00 2001 From: Florian GAULTIER Date: Wed, 8 Jan 2020 11:27:39 +0100 Subject: [PATCH 16/26] Add expire option to redis set --- packages/nodes-base/nodes/Redis/Redis.node.ts | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/packages/nodes-base/nodes/Redis/Redis.node.ts b/packages/nodes-base/nodes/Redis/Redis.node.ts index f3a574a52..beb3bc42d 100644 --- a/packages/nodes-base/nodes/Redis/Redis.node.ts +++ b/packages/nodes-base/nodes/Redis/Redis.node.ts @@ -250,6 +250,35 @@ export class Redis implements INodeType { default: 'automatic', description: 'The type of the key to set.', }, + + { + displayName: 'Expire', + name: 'expire', + type: 'boolean', + default: false, + description: 'Set a timeout on key ?', + }, + + { + displayName: 'TTL', + name: 'ttl', + type: 'number', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + operation: [ + 'set' + ], + expire: [ + true, + ], + }, + }, + default: 60, + description: 'Number of seconds before key expiration.', + } ] }; @@ -319,7 +348,7 @@ export class Redis implements INodeType { } - async function setValue(client: redis.RedisClient, keyName: string, value: string | number | object | string[] | number[], type?: string) { + async function setValue(client: redis.RedisClient, keyName: string, value: string | number | object | string[] | number[], expire: boolean, ttl: number, type?: string) { if (type === undefined || type === 'automatic') { // Request the type first if (typeof value === 'string') { @@ -335,20 +364,24 @@ export class Redis implements INodeType { if (type === 'string') { const clientSet = util.promisify(client.set).bind(client); - return await clientSet(keyName, value.toString()); + await clientSet(keyName, value.toString()); } else if (type === 'hash') { const clientHset = util.promisify(client.hset).bind(client); for (const key of Object.keys(value)) { await clientHset(keyName, key, (value as IDataObject)[key]!.toString()); } - return; } else if (type === 'list') { const clientLset = util.promisify(client.lset).bind(client); for (let index = 0; index < (value as string[]).length; index++) { await clientLset(keyName, index, (value as IDataObject)[index]!.toString()); } - return; } + + if (expire === true) { + const clientExpire = util.promisify(client.expire).bind(client); + await clientExpire(keyName, ttl); + } + return; } @@ -434,8 +467,10 @@ export class Redis implements INodeType { const keySet = this.getNodeParameter('key', itemIndex) as string; const value = this.getNodeParameter('value', itemIndex) as string; const keyType = this.getNodeParameter('keyType', itemIndex) as string; + const expire = this.getNodeParameter('expire', itemIndex, false) as boolean; + const ttl = this.getNodeParameter('ttl', itemIndex, -1) as number; - await setValue(client, keySet, value, keyType); + await setValue(client, keySet, value, expire, ttl, keyType); returnItems.push(items[itemIndex]); } } From e6b11c9dcccf6130781d15bb49c41019e1961116 Mon Sep 17 00:00:00 2001 From: Florian GAULTIER Date: Wed, 8 Jan 2020 12:22:18 +0100 Subject: [PATCH 17/26] Print expire only when set --- packages/nodes-base/nodes/Redis/Redis.node.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/nodes-base/nodes/Redis/Redis.node.ts b/packages/nodes-base/nodes/Redis/Redis.node.ts index beb3bc42d..337e0c553 100644 --- a/packages/nodes-base/nodes/Redis/Redis.node.ts +++ b/packages/nodes-base/nodes/Redis/Redis.node.ts @@ -255,6 +255,13 @@ export class Redis implements INodeType { displayName: 'Expire', name: 'expire', type: 'boolean', + displayOptions: { + show: { + operation: [ + 'set' + ], + }, + }, default: false, description: 'Set a timeout on key ?', }, From 5ecc9553386351d984b030d90b9663bee08be452 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Wed, 8 Jan 2020 11:06:28 -0500 Subject: [PATCH 18/26] fixed issue with url --- packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts b/packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts index b9d0763f4..ef3fc8be7 100644 --- a/packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts @@ -20,14 +20,12 @@ export async function rocketchatApiRequest(this: IHookFunctions | IExecuteFuncti headers: headerWithAuthentication, method, body, - uri: `${credentials.domain}${resource}.${operation}`, + uri: `${credentials.domain}/api/v1${resource}.${operation}`, json: true }; - if (Object.keys(options.body).length === 0) { delete options.body; } - try { return await this.helpers.request!(options); } catch (error) { From 7f21f3eee7041b79d1f639f53d5ad6fc2b27ad57 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 8 Jan 2020 13:25:35 -0600 Subject: [PATCH 19/26] :bug: Fix bug that TextEdit did not display existing values --- packages/editor-ui/src/components/TextEdit.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/editor-ui/src/components/TextEdit.vue b/packages/editor-ui/src/components/TextEdit.vue index 11fa9059b..5e6fb7de6 100644 --- a/packages/editor-ui/src/components/TextEdit.vue +++ b/packages/editor-ui/src/components/TextEdit.vue @@ -47,6 +47,9 @@ export default Vue.extend({ return false; }, }, + mounted () { + this.tempValue = this.value as string; + }, watch: { dialogVisible () { if (this.dialogVisible === true) { From 1d1b580200f68a637b0516a99f90272daabda8f7 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 8 Jan 2020 13:38:20 -0600 Subject: [PATCH 20/26] :bookmark: Release n8n-nodes-base@0.40.0 --- packages/nodes-base/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index d90cf8362..a91be93fb 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -1,6 +1,6 @@ { "name": "n8n-nodes-base", - "version": "0.39.0", + "version": "0.40.0", "description": "Base nodes of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From cba55d4f868f2f038c23d86e06dddbd0bceb2d1c Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 8 Jan 2020 13:39:50 -0600 Subject: [PATCH 21/26] :bookmark: Release n8n-editor-ui@0.32.0 --- packages/editor-ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index abbdab835..ea5898388 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -1,6 +1,6 @@ { "name": "n8n-editor-ui", - "version": "0.31.0", + "version": "0.32.0", "description": "Workflow Editor UI for n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From a91e66ad169ed051aa00b44efd9a213133124d2d Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 8 Jan 2020 13:41:39 -0600 Subject: [PATCH 22/26] :arrow_up: Set n8n-editor-ui@0.32.0 and n8n-nodes-base@0.40.0 on n8n --- packages/cli/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 220cf6a24..8a39473ad 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -95,8 +95,8 @@ "lodash.get": "^4.4.2", "mongodb": "^3.2.3", "n8n-core": "~0.20.0", - "n8n-editor-ui": "~0.31.0", - "n8n-nodes-base": "~0.39.0", + "n8n-editor-ui": "~0.32.0", + "n8n-nodes-base": "~0.40.0", "n8n-workflow": "~0.20.0", "open": "^7.0.0", "pg": "^7.11.0", From c78916af5ff06b41b9ff7b83310c3b8511e9be3c Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 8 Jan 2020 13:42:25 -0600 Subject: [PATCH 23/26] :bookmark: Release n8n@0.45.0 --- packages/cli/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 8a39473ad..f3fdb87bc 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "0.44.0", + "version": "0.45.0", "description": "n8n Workflow Automation Tool", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From c174f6cc70b9fd5a352a6a1037b30535e6197945 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 9 Jan 2020 20:05:57 -0600 Subject: [PATCH 24/26] :zap: Make it possible to soft-delete and restore Mattermost-Channels --- .../nodes/Mattermost/Mattermost.node.ts | 83 ++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/packages/nodes-base/nodes/Mattermost/Mattermost.node.ts b/packages/nodes-base/nodes/Mattermost/Mattermost.node.ts index 1a8262d8d..fd936e927 100644 --- a/packages/nodes-base/nodes/Mattermost/Mattermost.node.ts +++ b/packages/nodes-base/nodes/Mattermost/Mattermost.node.ts @@ -85,6 +85,16 @@ export class Mattermost implements INodeType { value: 'create', description: 'Create a new channel', }, + { + name: 'Delete', + value: 'delete', + description: 'Soft-deletes a channel', + }, + { + name: 'Restore', + value: 'restore', + description: 'Restores a soft-deleted channel', + }, { name: 'Statistics', value: 'statistics', @@ -219,6 +229,56 @@ export class Mattermost implements INodeType { }, + // ---------------------------------- + // channel:delete + // ---------------------------------- + { + displayName: 'Channel ID', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + options: [], + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'delete' + ], + resource: [ + 'channel', + ], + }, + }, + description: 'The ID of the channel to soft-delete.', + }, + + + // ---------------------------------- + // channel:restore + // ---------------------------------- + { + displayName: 'Channel ID', + name: 'channelId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'restore' + ], + resource: [ + 'channel', + ], + }, + }, + description: 'The ID of the channel to restore.', + }, + + // ---------------------------------- // channel:addUser // ---------------------------------- @@ -266,6 +326,8 @@ export class Mattermost implements INodeType { }, description: 'The ID of the user to invite into channel.', }, + + // ---------------------------------- // channel:statistics // ---------------------------------- @@ -629,8 +691,7 @@ export class Mattermost implements INodeType { methods = { loadOptions: { - // Get all the available workspaces to display them to user so that he can - // select them easily + // Get all the available channels async getChannels(this: ILoadOptionsFunctions): Promise { const endpoint = 'channels'; const responseData = await apiRequest.call(this, 'GET', endpoint, {}); @@ -754,6 +815,24 @@ export class Mattermost implements INodeType { const type = this.getNodeParameter('type', i) as string; body.type = type === 'public' ? 'O' : 'P'; + } else if (operation === 'delete') { + // ---------------------------------- + // channel:delete + // ---------------------------------- + + requestMethod = 'DELETE'; + const channelId = this.getNodeParameter('channelId', i) as string; + endpoint = `channels/${channelId}`; + + } else if (operation === 'restore') { + // ---------------------------------- + // channel:restore + // ---------------------------------- + + requestMethod = 'POST'; + const channelId = this.getNodeParameter('channelId', i) as string; + endpoint = `channels/${channelId}/restore`; + } else if (operation === 'addUser') { // ---------------------------------- // channel:addUser From 9a2188f22fdcca8076d893cddc2e95833fef4b4d Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 9 Jan 2020 21:23:47 -0600 Subject: [PATCH 25/26] :zip: Do not allow credentials without name --- packages/cli/src/Server.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index f5a608331..9eecf6706 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -657,6 +657,10 @@ class App { throw new Error('No encryption key got found to encrypt the credentials!'); } + if (incomingData.name === '') { + throw new Error('Credentials have to have a name set!'); + } + // Check if credentials with the same name and type exist already const findQuery = { where: { @@ -696,6 +700,10 @@ class App { const id = req.params.id; + if (incomingData.name === '') { + throw new Error('Credentials have to have a name set!'); + } + // Add the date for newly added node access permissions for (const nodeAccess of incomingData.nodesAccess) { if (!nodeAccess.date) { From 29633cfd1f3baecbb156e61d46fa6004bfd15e36 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 9 Jan 2020 21:53:26 -0600 Subject: [PATCH 26/26] :zap: Add "About n8n" to Help in sidebar --- packages/editor-ui/src/components/About.vue | 88 +++++++++++++++++++ .../editor-ui/src/components/MainSidebar.vue | 37 +++++--- 2 files changed, 113 insertions(+), 12 deletions(-) create mode 100644 packages/editor-ui/src/components/About.vue diff --git a/packages/editor-ui/src/components/About.vue b/packages/editor-ui/src/components/About.vue new file mode 100644 index 000000000..62563a1c2 --- /dev/null +++ b/packages/editor-ui/src/components/About.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/packages/editor-ui/src/components/MainSidebar.vue b/packages/editor-ui/src/components/MainSidebar.vue index cbf0664e7..71388c2cd 100644 --- a/packages/editor-ui/src/components/MainSidebar.vue +++ b/packages/editor-ui/src/components/MainSidebar.vue @@ -1,5 +1,6 @@ + + + @@ -168,6 +169,7 @@ import { IWorkflowDataUpdate, } from '../Interface'; +import About from '@/components/About.vue'; import CredentialsEdit from '@/components/CredentialsEdit.vue'; import CredentialsList from '@/components/CredentialsList.vue'; import ExecutionsList from '@/components/ExecutionsList.vue'; @@ -196,6 +198,7 @@ export default mixins( .extend({ name: 'MainHeader', components: { + About, CredentialsEdit, CredentialsList, ExecutionsList, @@ -204,6 +207,7 @@ export default mixins( }, data () { return { + aboutDialogVisible: false, isCollapsed: true, credentialNewDialogVisible: false, credentialOpenDialogVisible: false, @@ -251,9 +255,6 @@ export default mixins( currentWorkflow (): string { return this.$route.params.name; }, - versionCli (): string { - return this.$store.getters.versionCli; - }, workflowExecution (): IExecutionResponse | null { return this.$store.getters.getWorkflowExecution; }, @@ -269,6 +270,9 @@ export default mixins( this.$store.commit('setWorkflowExecutionData', null); this.updateNodesExecutionIssues(); }, + closeAboutDialog () { + this.aboutDialogVisible = false; + }, closeWorkflowOpenDialog () { this.workflowOpenDialogVisible = false; }, @@ -434,6 +438,8 @@ export default mixins( this.saveCurrentWorkflow(); } else if (key === 'workflow-save-as') { this.saveCurrentWorkflow(true); + } else if (key === 'help-about') { + this.aboutDialogVisible = true; } else if (key === 'workflow-settings') { this.workflowSettingsDialogVisible = true; } else if (key === 'workflow-new') { @@ -466,6 +472,9 @@ export default mixins(