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 00000000000..d4f96a1ca95 --- /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 00000000000..59a5831bf40 --- /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 00000000000..f59dd49ece6 --- /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 dad780ab23e..6d44a32faf0 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 5494b5c5c30..e8f1694b9e1 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 efb39c66671..5bdb290fb27 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 f70f41c5b23..b47e0c4b23a 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 48bbacb418c..df55d5a6b01 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 41fda803724..f5a608331af 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 e6e34fb9d51..a79ebdd8fde 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 758adf1a4c5..eeece990bcf 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 a72520718e4..a2cdbd65841 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 14a31c7e800..f33d028f944 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 00000000000..452fdb8f57e --- /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 caf67cc069a..3074251ef78 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 00000000000..189ae9e408d --- /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 3074251ef78..28c220136da 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 00000000000..29048c1172e --- /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 00000000000..8221cb402fa --- /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 00000000000..479ef88f2d9 --- /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 dc5747eb365..4ddc3b71508 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 8221cb402fa..9677f5652fa 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 479ef88f2d9..0859dd9db1c 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 9677f5652fa..392c1426e39 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 0859dd9db1c..e86a6ddd727 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 00000000000..802ba004f9d --- /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 e86a6ddd727..a380fa07be9 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 392c1426e39..0c87d87738c 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 00000000000..601beb71fe8 --- /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 00000000000..fc4eb75697f --- /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 00000000000..49f91ceb568 --- /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 ade1417d8a3..a4d4236129e 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 59a5831bf40..893996fc8a1 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 f59dd49ece6..240a3623e01 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 802ba004f9d..99a1429b0cc 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 0c87d87738c..5126c69495e 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 601beb71fe8..477a8074e46 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 fc4eb75697f..5ef968381b2 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 49f91ceb568..91d055dae8d 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 a380fa07be9..e242141d927 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 a2cdbd65841..72616e83d4b 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 f3a574a5224..beb3bc42dc0 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 beb3bc42dc0..337e0c55375 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 b9d0763f4b9..ef3fc8be7b3 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 11fa9059bb2..5e6fb7de6b7 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 d90cf836276..a91be93fb69 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 abbdab83531..ea589838834 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 220cf6a24b7..8a39473ad92 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 8a39473ad92..f3fdb87bcde 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 1a8262d8d87..fd936e9276e 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 f5a608331af..9eecf670622 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 00000000000..62563a1c25b --- /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 cbf0664e78f..71388c2cd33 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(