From d57d45714836a5688a3fc9f1f28b3e90a401d264 Mon Sep 17 00:00:00 2001 From: ricardo Date: Sun, 26 Apr 2020 12:51:20 -0500 Subject: [PATCH 1/4] :sparkles: SurveyMonkey trigger --- packages/cli/src/Server.ts | 43 +- .../SurveyMonkeyApi.credentials.ts | 35 ++ .../nodes/SurveyMonkey/GenericFunctions.ts | 82 ++++ .../SurveyMonkey/SurveyMonkeyTrigger.node.ts | 433 ++++++++++++++++++ .../nodes/SurveyMonkey/surveyMonkey.png | Bin 0 -> 3327 bytes packages/nodes-base/package.json | 2 + packages/workflow/src/Interfaces.ts | 2 +- 7 files changed, 593 insertions(+), 4 deletions(-) create mode 100644 packages/nodes-base/credentials/SurveyMonkeyApi.credentials.ts create mode 100644 packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts create mode 100644 packages/nodes-base/nodes/SurveyMonkey/surveyMonkey.png diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index a98def2cbe..b6a07aab44 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -1083,7 +1083,6 @@ class App { return returnData; })); - // Forces the execution to stop this.app.post('/rest/executions-current/:id/stop', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const executionId = req.params.id; @@ -1151,6 +1150,26 @@ class App { // Webhooks // ---------------------------------------- + // HEAD webhook requests + this.app.head(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => { + // Cut away the "/webhook/" to get the registred part of the url + const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhook.length + 2); + + let response; + try { + response = await this.activeWorkflowRunner.executeWebhook('HEAD', requestUrl, req, res); + } catch (error) { + ResponseHelper.sendErrorResponse(res, error); + return; + } + + if (response.noWebhookResponse === true) { + // Nothing else to do as the response got already sent + return; + } + + ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); + }); // GET webhook requests this.app.get(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => { @@ -1173,7 +1192,6 @@ class App { ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); }); - // POST webhook requests this.app.post(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => { // Cut away the "/webhook/" to get the registred part of the url @@ -1195,6 +1213,26 @@ class App { ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); }); + // HEAD webhook requests (test for UI) + this.app.head(`/${this.endpointWebhookTest}/*`, async (req: express.Request, res: express.Response) => { + // Cut away the "/webhook-test/" to get the registred part of the url + const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhookTest.length + 2); + + let response; + try { + response = await this.testWebhooks.callTestWebhook('HEAD', requestUrl, req, res); + } catch (error) { + ResponseHelper.sendErrorResponse(res, error); + return; + } + + if (response.noWebhookResponse === true) { + // Nothing else to do as the response got already sent + return; + } + + ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); + }); // GET webhook requests (test for UI) this.app.get(`/${this.endpointWebhookTest}/*`, async (req: express.Request, res: express.Response) => { @@ -1217,7 +1255,6 @@ class App { ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode); }); - // POST webhook requests (test for UI) this.app.post(`/${this.endpointWebhookTest}/*`, async (req: express.Request, res: express.Response) => { // Cut away the "/webhook-test/" to get the registred part of the url diff --git a/packages/nodes-base/credentials/SurveyMonkeyApi.credentials.ts b/packages/nodes-base/credentials/SurveyMonkeyApi.credentials.ts new file mode 100644 index 0000000000..130690d423 --- /dev/null +++ b/packages/nodes-base/credentials/SurveyMonkeyApi.credentials.ts @@ -0,0 +1,35 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class SurveyMonkeyApi implements ICredentialType { + name = 'surveyMonkeyApi'; + displayName = 'SurveyMonkey API'; + properties = [ + { + displayName: 'Access Token', + name: 'accessToken', + type: 'string' as NodePropertyTypes, + default: '', + description: `The access token must have the following scopes:
+ - Create/modify webhooks
+ - View webhooks
+ - View surveys
+ - View collectors
+ - View response details`, + }, + { + displayName: 'Client ID', + name: 'clientId', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Client Secret', + name: 'clientSecret', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts b/packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts new file mode 100644 index 0000000000..2d2cf7995d --- /dev/null +++ b/packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts @@ -0,0 +1,82 @@ +import { + OptionsWithUri, + } from 'request'; + +import { + IExecuteFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, + IHookFunctions, + IWebhookFunctions +} from 'n8n-workflow'; + +export async function surveyMonkeyApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const credentials = this.getCredentials('surveyMonkeyApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const endpoint = 'https://api.surveymonkey.com/v3'; + + let options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `bearer ${credentials.accessToken}`, + }, + method, + body, + qs: query, + uri: uri || `${endpoint}${resource}`, + json: true + }; + if (!Object.keys(body).length) { + delete options.body; + } + if (!Object.keys(query).length) { + delete options.qs; + } + options = Object.assign({}, options, option); + try { + return await this.helpers.request!(options); + } catch (error) { + const errorMessage = error.response.body.error.message; + if (errorMessage !== undefined) { + throw new Error(`SurveyMonkey error response [${error.statusCode}]: ${errorMessage}`); + } + throw error; + } +} + +export async function surveyMonkeyRequestAllItems(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + query.page = 1; + query.per_page = 100; + let uri: string | undefined; + + do { + responseData = await surveyMonkeyApiRequest.call(this, method, endpoint, body, query, uri); + uri = responseData.links.next; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData.links.next + ); + + return returnData; +} + +export function idsExist(ids: string[], surveyIds: string[]) { + for (const surveyId of surveyIds) { + if (!ids.includes(surveyId)) { + return false; + } + } + return true; +} diff --git a/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts b/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts new file mode 100644 index 0000000000..51d101697a --- /dev/null +++ b/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts @@ -0,0 +1,433 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + idsExist, + surveyMonkeyApiRequest, + surveyMonkeyRequestAllItems, +} from './GenericFunctions'; + +import { + createHmac, + } from 'crypto'; + +export class SurveyMonkeyTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'SurveyMonkey Trigger', + name: 'surveyMonkeyTrigger', + icon: 'file:surveyMonkey.png', + group: ['trigger'], + version: 1, + description: 'Starts the workflow when Survey Monkey events occure.', + defaults: { + name: 'SurveyMonkey Trigger', + color: '#53b675', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'surveyMonkeyApi', + required: true, + }, + ], + webhooks: [ + { + name: 'setup', + httpMethod: 'HEAD', + responseMode: 'onReceived', + path: 'webhook', + }, + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Event', + name: 'event', + type: 'options', + options: [ + { + name: 'Collector Created', + value: 'collector_created', + description: 'A collector is created', + }, + { + name: 'Collector Updated', + value: 'collector_updated', + description: 'A collector is updated', + }, + { + name: 'Collector Deleted', + value: 'collector_deleted', + description: 'A collector is deleted', + }, + { + name: 'Response Completed', + value: 'response_completed', + description: 'A survey response is completed', + }, + { + name: 'Response Created', + value: 'response_created', + description: 'A respondent begins a survey', + }, + { + name: 'Response Deleted', + value: 'response_deleted', + description: 'A response is deleted', + }, + { + name: 'Response Desqualified', + value: 'response_desqualified', + description: 'A survey response is disqualified ', + }, + { + name: 'Response Overquota', + value: 'response_overquota', + description: `A response is over a survey’s quota`, + }, + { + name: 'Response Updated', + value: 'response_updated', + description: 'A survey response is updated', + }, + { + name: 'Survey Created', + value: 'survey_created', + description: 'A survey is created', + }, + { + name: 'Survey Deleted', + value: 'survey_deleted', + description: 'A survey is deleted', + }, + { + name: 'Survey Updated', + value: 'survey_updated', + description: 'A survey is updated', + }, + ], + default: '', + required: true, + }, + { + displayName: 'Type', + name: 'objectType', + type: 'options', + options: [ + { + name: 'Collector', + value: 'collector', + }, + { + name: 'Survey', + value: 'survey', + }, + ], + default: '', + required: true, + }, + { + displayName: 'Survey IDs', + name: 'surveyIds', + type: 'multiOptions', + displayOptions: { + show: { + objectType: [ + 'survey', + ], + }, + hide: { + event: [ + 'survey_created', + ], + }, + }, + typeOptions: { + loadOptionsMethod: 'getSurveys', + }, + options: [], + default: [], + required: true, + }, + { + displayName: 'Survey ID', + name: 'surveyId', + type: 'options', + displayOptions: { + show: { + objectType: [ + 'collector', + ], + }, + }, + typeOptions: { + loadOptionsMethod: 'getSurveys', + }, + default: [], + required: true, + }, + { + displayName: 'Collector IDs', + name: 'collectorIds', + type: 'multiOptions', + displayOptions: { + show: { + objectType: [ + 'collector', + ], + }, + }, + typeOptions: { + loadOptionsMethod: 'getCollectors', + loadOptionsDependsOn: [ + 'surveyId', + ], + }, + options: [], + default: [], + required: true, + }, + { + displayName: 'Resolve Data', + name: 'resolveData', + type: 'boolean', + displayOptions: { + show: { + event: [ + 'response_completed', + ], + }, + }, + default: true, + description: 'By default the webhook-data only contain the IDs. If this option gets activated it
will resolve the data automatically.', + }, + ], + }; + + methods = { + loadOptions: { + // Get all the survey's collectors to display them to user so that he can + // select them easily + async getCollectors(this: ILoadOptionsFunctions): Promise { + const surveyId = this.getCurrentNodeParameter('surveyId'); + const returnData: INodePropertyOptions[] = []; + const collectors = await surveyMonkeyRequestAllItems.call(this, 'data', 'GET', `/surveys/${surveyId}/collectors`); + for (const collector of collectors) { + const collectorName = collector.name; + const collectorId = collector.id; + returnData.push({ + name: collectorName, + value: collectorId, + }); + } + return returnData; + }, + + // Get all the surveys to display them to user so that he can + // select them easily + async getSurveys(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const surveys = await surveyMonkeyRequestAllItems.call(this, 'data', 'GET', '/surveys'); + for (const survey of surveys) { + const surveyName = survey.title; + const surveyId = survey.id; + returnData.push({ + name: surveyName, + value: surveyId, + }); + } + return returnData; + }, + }, + }; + + // @ts-ignore (because of request) + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const objectType = this.getNodeParameter('objectType') as string; + const event = this.getNodeParameter('event') as string; + // Check all the webhooks which exist already if it is identical to the + // one that is supposed to get created. + const endpoint = '/webhooks'; + + const responseData = await surveyMonkeyRequestAllItems.call(this, 'data', 'GET', endpoint, {}); + + const webhookUrl = this.getNodeWebhookUrl('default'); + + const ids: string[] = []; + + if (objectType === 'survey') { + const surveyIds = this.getNodeParameter('surveyIds') as string[]; + ids.push.apply(ids, surveyIds); + } else if (objectType === 'collector') { + const collectorIds = this.getNodeParameter('collectorIds') as string[]; + ids.push.apply(ids, collectorIds); + } + + for (const webhook of responseData) { + const webhookDetails = await surveyMonkeyApiRequest.call(this, 'GET', `/webhooks/${webhook.id}`); + if (webhookDetails.subscription_url === webhookUrl + && idsExist(webhookDetails.object_ids as string[], ids as string[]) + && webhookDetails.event_type === event) { + // Set webhook-id to be sure that it can be deleted + const webhookData = this.getWorkflowStaticData('node'); + webhookData.webhookId = webhook.id as string; + return true; + } + } + + return false; + }, + + async create(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + const event = this.getNodeParameter('event') as string; + const objectType = this.getNodeParameter('objectType') as string; + const endpoint = '/webhooks'; + const ids: string[] = []; + + if (objectType === 'survey') { + const surveyIds = this.getNodeParameter('surveyIds') as string[]; + ids.push.apply(ids, surveyIds); + } else if (objectType === 'collector') { + const collectorIds = this.getNodeParameter('collectorIds') as string[]; + ids.push.apply(ids, collectorIds); + } + + const body: IDataObject = { + name: `n8n - Webhook [${event}]`, + object_type: objectType, + object_ids: ids, + subscription_url: webhookUrl, + event_type: event, + }; + + if (objectType === 'collector' && event === 'collector_created') { + throw new Error('Type collector cannot be used with collector created event'); + } + + if (objectType === 'survey' && event === 'survey_created') { + delete body.object_type; + } + + let responseData: IDataObject = {}; + + responseData = await surveyMonkeyApiRequest.call(this, 'POST', endpoint, body); + + if (responseData.id === undefined) { + // Required data is missing so was not successful + return false; + } + + const webhookData = this.getWorkflowStaticData('node'); + webhookData.webhookId = responseData.id as string; + + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId !== undefined) { + + const endpoint = `/webhooks/${webhookData.webhookId}`; + + try { + await surveyMonkeyApiRequest.call(this, 'DELETE', endpoint); + } catch (e) { + return false; + } + + // Remove from the static workflow data so that it is clear + // that no webhooks are registred anymore + delete webhookData.webhookId; + } + + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const event = this.getNodeParameter('event') as string; + const objectType = this.getNodeParameter('objectType') as string; + const credentials = this.getCredentials('surveyMonkeyApi') as IDataObject; + const headerData = this.getHeaderData() as IDataObject; + const req = this.getRequestObject(); + const webhookName = this.getWebhookName(); + + if (webhookName === 'setup') { + // It is a create webhook confirmation request + return {}; + } + + if (headerData['sm-signature'] === undefined) { + return {}; + } + + return new Promise((resolve, reject) => { + const data: Buffer[] = []; + + req.on('data', (chunk) => { + data.push(chunk); + }); + + req.on('end', async () => { + + const computedSignature = createHmac('sha1', `${credentials.clientId}&${credentials.clientSecret}`).update(data.join('')).digest('base64'); + if (headerData['sm-signature'] !== computedSignature) { + // Signature is not valid so ignore call + return {}; + } + + let responseData = JSON.parse(data.join('')); + let endpoint = ''; + + if (event === 'response_completed') { + const resolveData = this.getNodeParameter('resolveData') as boolean; + if (resolveData) { + if (objectType === 'survey') { + endpoint = `/surveys/${responseData.resources.survey_id}/responses/${responseData.object_id}/details`; + } else { + endpoint = `/collectors/${responseData.resources.collector_id}/responses/${responseData.object_id}/details`; + } + responseData = await surveyMonkeyApiRequest.call(this, 'GET', endpoint); + } + } + + const returnItem: INodeExecutionData = { + json: responseData, + }; + + return resolve({ + workflowData: [ + [ + returnItem, + ], + ], + }); + }); + + req.on('error', (err) => { + throw new Error(err.message); + }); + }); + } +} diff --git a/packages/nodes-base/nodes/SurveyMonkey/surveyMonkey.png b/packages/nodes-base/nodes/SurveyMonkey/surveyMonkey.png new file mode 100644 index 0000000000000000000000000000000000000000..80e09c3afeba2230fbd4ce7364820bb5811c7cfd GIT binary patch literal 3327 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 ow4Hb{6MGU;`FwUW=l;^s9G6arP3(`x Date: Mon, 27 Apr 2020 01:03:01 +0200 Subject: [PATCH 2/4] :zap: Improvements to SurveyMonkey --- .../SurveyMonkeyApi.credentials.ts | 11 ++++++----- .../nodes/SurveyMonkey/GenericFunctions.ts | 2 +- .../SurveyMonkey/SurveyMonkeyTrigger.node.ts | 2 +- .../nodes/SurveyMonkey/surveyMonkey.png | Bin 3327 -> 1052 bytes 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/nodes-base/credentials/SurveyMonkeyApi.credentials.ts b/packages/nodes-base/credentials/SurveyMonkeyApi.credentials.ts index 130690d423..66614595eb 100644 --- a/packages/nodes-base/credentials/SurveyMonkeyApi.credentials.ts +++ b/packages/nodes-base/credentials/SurveyMonkeyApi.credentials.ts @@ -12,11 +12,12 @@ export class SurveyMonkeyApi implements ICredentialType { name: 'accessToken', type: 'string' as NodePropertyTypes, default: '', - description: `The access token must have the following scopes:
- - Create/modify webhooks
- - View webhooks
- - View surveys
- - View collectors
+ description: `The access token must have the following scopes:
+ - Create/modify webhooks
+ - View webhooks
+ - View surveys
+ - View collectors
+ - View responses
- View response details`, }, { diff --git a/packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts b/packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts index 2d2cf7995d..49ecef2912 100644 --- a/packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts +++ b/packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts @@ -1,6 +1,6 @@ import { OptionsWithUri, - } from 'request'; +} from 'request'; import { IExecuteFunctions, diff --git a/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts b/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts index 51d101697a..ee31ff95c3 100644 --- a/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts +++ b/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts @@ -21,7 +21,7 @@ import { import { createHmac, - } from 'crypto'; +} from 'crypto'; export class SurveyMonkeyTrigger implements INodeType { description: INodeTypeDescription = { diff --git a/packages/nodes-base/nodes/SurveyMonkey/surveyMonkey.png b/packages/nodes-base/nodes/SurveyMonkey/surveyMonkey.png index 80e09c3afeba2230fbd4ce7364820bb5811c7cfd..e5337accd626afbe0bfe893e301bf769a61e7ec0 100644 GIT binary patch literal 1052 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKw3=&b&bO2Ik0(?STfi%O)B8EK$4Eu{1_U19{ z&SRJ!z_16%C}7xIz_30YB(*1>VP`Ig+zUhnAT>bto?M1)*$f+#!Gb_;K1j=Updnyo zAO%1Xuz|aBKvEzsP!Om$k6}kHNEyf$uu(hnKvr+h1u27yWP?QZf|UVnJW$ARqKx5K zDZ`;+h9e~mr^*=)7BQTvU^rXJaHfLcRwKjtDu&B7+nO&W1HEHf666=m!2Fl>_g}Uj zKfiHZn&|N1me1D+WqFa$uTQ!NJYK%IB`MI_`0SQ>oq9XhPRoilm3(>cfcc&B`EM6+ zFfcHk@^oGpV;QqU7&&)Fx z)8@@r@lGx|Ci!2zi{q%c{R7di4-21tDE?<0%Wk^pyW*U}ReD_RtWRR<+!&A48M(a= zd|#L7Br->CWAmZ!L7JhaAsQCTMQ^4>UUmwwt=e?z_+@T~xh+e@7Edt@)V(`(^WMb_ zd3VDVLr>{ctP;A|sW|KLuSMn|lY)JWjnrC5ENb#t)I+|y;Z~alH#-r$V z{@vrKA9DgY4!N}NWR#L_lbpKU|HPs{i$eC#)}77VsVE%je9*3Ln|VXe-X~(K-dXrD ze)ZBno5O2v<71%yNv-&7F!$x6?duIMzEu}Vspfn2^-WL8RgPPlX^SrD+DF}&ednw) z!a#;fFWhQ`SucczC%3bK{WU(sK1cH8P(lYTiJ zIvKL#`tOsx;Q|E{4lQ5ZtvlWOsr2^8#cribH`X)F%I#fvUN3stDzBrxS6;DnzhdDH z%X2H^o49h$!sA@epHwy5h`xIyy6|GVcT2g#n_cP}zV8Hf1*+)gYZ?dYE?&)QvdJ#Q zAo+KCpW1>R$4ce&m$R~#r=}+vZfdjn`TbPf)ccv4@wet&pY~B`x093QPIc?w^E>*s zm~&n&`{=iIAuG?f9SJ?1W3zZ}12sW1EeY0-wVyBn?j8l=`b z=D&ZFVdQ&MBb@0FIyC AF#rGn literal 3327 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 ow4Hb{6MGU;`FwUW=l;^s9G6arP3(`x Date: Fri, 1 May 2020 19:46:02 -0400 Subject: [PATCH 3/4] :zap: Improvements --- .../SurveyMonkeyApi.credentials.ts | 11 +- .../nodes/Asana/GenericFunctions.ts | 2 +- .../nodes/SurveyMonkey/GenericFunctions.ts | 4 +- .../nodes/SurveyMonkey/Interfaces.ts | 47 +++ .../SurveyMonkey/SurveyMonkeyTrigger.node.ts | 316 ++++++++++++++++-- 5 files changed, 349 insertions(+), 31 deletions(-) create mode 100644 packages/nodes-base/nodes/SurveyMonkey/Interfaces.ts diff --git a/packages/nodes-base/credentials/SurveyMonkeyApi.credentials.ts b/packages/nodes-base/credentials/SurveyMonkeyApi.credentials.ts index 130690d423..66614595eb 100644 --- a/packages/nodes-base/credentials/SurveyMonkeyApi.credentials.ts +++ b/packages/nodes-base/credentials/SurveyMonkeyApi.credentials.ts @@ -12,11 +12,12 @@ export class SurveyMonkeyApi implements ICredentialType { name: 'accessToken', type: 'string' as NodePropertyTypes, default: '', - description: `The access token must have the following scopes:
- - Create/modify webhooks
- - View webhooks
- - View surveys
- - View collectors
+ description: `The access token must have the following scopes:
+ - Create/modify webhooks
+ - View webhooks
+ - View surveys
+ - View collectors
+ - View responses
- View response details`, }, { diff --git a/packages/nodes-base/nodes/Asana/GenericFunctions.ts b/packages/nodes-base/nodes/Asana/GenericFunctions.ts index 83bdfe01a2..1f6a1a5252 100644 --- a/packages/nodes-base/nodes/Asana/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Asana/GenericFunctions.ts @@ -41,7 +41,7 @@ export async function asanaApiRequest(this: IHookFunctions | IExecuteFunctions | // Return a clear error throw new Error('The Asana credentials are not valid!'); } - + console.log(error); if (error.response && error.response.body && error.response.body.errors) { // Try to return the error prettier const errorMessages = error.response.body.errors.map((errorData: { message: string }) => { diff --git a/packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts b/packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts index 2d2cf7995d..86f999b578 100644 --- a/packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts +++ b/packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts @@ -1,6 +1,6 @@ import { OptionsWithUri, - } from 'request'; +} from 'request'; import { IExecuteFunctions, @@ -52,7 +52,7 @@ export async function surveyMonkeyApiRequest(this: IExecuteFunctions | IWebhookF } } -export async function surveyMonkeyRequestAllItems(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any +export async function surveyMonkeyRequestAllItems(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any const returnData: IDataObject[] = []; diff --git a/packages/nodes-base/nodes/SurveyMonkey/Interfaces.ts b/packages/nodes-base/nodes/SurveyMonkey/Interfaces.ts new file mode 100644 index 0000000000..56c4e03a29 --- /dev/null +++ b/packages/nodes-base/nodes/SurveyMonkey/Interfaces.ts @@ -0,0 +1,47 @@ +import { + IDataObject, + } from 'n8n-workflow'; + +export interface IImage { + url: string; +} + +export interface IChoice { + position: number; + visible: boolean; + text: string; + id: string; + weight: number; + description: string; + image?: IImage; +} + +export interface IRow { + position: number; + visible: boolean; + text: string; + id: string; +} + +export interface IOther { + text: string; + visible: boolean; + is_answer_choice: boolean; + id: string; +} + +export interface IQuestion { + id: string; + family?: string; + subtype?: string; + headings?: IDataObject[]; + answers: IDataObject; + rows?: IDataObject; +} + +export interface IAnswer { + choice_id: string; + row_id?: string; + text?: string; + other_id?: string; +} diff --git a/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts b/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts index 51d101697a..49c44c9ac0 100644 --- a/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts +++ b/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts @@ -19,9 +19,17 @@ import { surveyMonkeyRequestAllItems, } from './GenericFunctions'; +import { + IAnswer, + IChoice, + IQuestion, + IRow, + IOther, +} from './Interfaces'; + import { createHmac, - } from 'crypto'; +} from 'crypto'; export class SurveyMonkeyTrigger implements INodeType { description: INodeTypeDescription = { @@ -58,9 +66,33 @@ export class SurveyMonkeyTrigger implements INodeType { }, ], properties: [ + { + displayName: 'Type', + name: 'objectType', + type: 'options', + options: [ + { + name: 'Collector', + value: 'collector', + }, + { + name: 'Survey', + value: 'survey', + }, + ], + default: '', + required: true, + }, { displayName: 'Event', name: 'event', + displayOptions: { + show: { + objectType: [ + 'survey' + ], + }, + }, type: 'options', options: [ { @@ -94,8 +126,8 @@ export class SurveyMonkeyTrigger implements INodeType { description: 'A response is deleted', }, { - name: 'Response Desqualified', - value: 'response_desqualified', + name: 'Response Disqualified', + value: 'response_disqualified', description: 'A survey response is disqualified ', }, { @@ -128,17 +160,56 @@ export class SurveyMonkeyTrigger implements INodeType { required: true, }, { - displayName: 'Type', - name: 'objectType', + displayName: 'Event', + name: 'event', type: 'options', + displayOptions: { + show: { + objectType: [ + 'collector', + ], + }, + }, options: [ { - name: 'Collector', - value: 'collector', + name: 'Collector Updated', + value: 'collector_updated', + description: 'A collector is updated', }, { - name: 'Survey', - value: 'survey', + name: 'Collector Deleted', + value: 'collector_deleted', + description: 'A collector is deleted', + }, + { + name: 'Response Completed', + value: 'response_completed', + description: 'A survey response is completed', + }, + { + name: 'Response Created', + value: 'response_created', + description: 'A respondent begins a survey', + }, + { + name: 'Response Deleted', + value: 'response_deleted', + description: 'A response is deleted', + }, + { + name: 'Response Disqualified', + value: 'response_disqualified', + description: 'A survey response is disqualified ', + }, + { + name: 'Response Overquota', + value: 'response_overquota', + description: `A response is over a survey’s quota`, + }, + { + name: 'Response Updated', + value: 'response_updated', + description: 'A survey response is updated', }, ], default: '', @@ -219,6 +290,23 @@ export class SurveyMonkeyTrigger implements INodeType { default: true, description: 'By default the webhook-data only contain the IDs. If this option gets activated it
will resolve the data automatically.', }, + { + displayName: 'Only Answers', + name: 'onlyAnswers', + displayOptions: { + show: { + resolveData: [ + true, + ], + event: [ + 'response_completed', + ], + }, + }, + type: 'boolean', + default: true, + description: 'Returns only the answers of the form and not any of the other data.', + }, ], }; @@ -275,7 +363,7 @@ export class SurveyMonkeyTrigger implements INodeType { const ids: string[] = []; - if (objectType === 'survey') { + if (objectType === 'survey' && event !== 'survey_created') { const surveyIds = this.getNodeParameter('surveyIds') as string[]; ids.push.apply(ids, surveyIds); } else if (objectType === 'collector') { @@ -305,7 +393,7 @@ export class SurveyMonkeyTrigger implements INodeType { const endpoint = '/webhooks'; const ids: string[] = []; - if (objectType === 'survey') { + if (objectType === 'survey' && event !== 'survey_created') { const surveyIds = this.getNodeParameter('surveyIds') as string[]; ids.push.apply(ids, surveyIds); } else if (objectType === 'collector') { @@ -321,12 +409,9 @@ export class SurveyMonkeyTrigger implements INodeType { event_type: event, }; - if (objectType === 'collector' && event === 'collector_created') { - throw new Error('Type collector cannot be used with collector created event'); - } - if (objectType === 'survey' && event === 'survey_created') { delete body.object_type; + delete body.object_ids; } let responseData: IDataObject = {}; @@ -390,7 +475,6 @@ export class SurveyMonkeyTrigger implements INodeType { }); req.on('end', async () => { - const computedSignature = createHmac('sha1', `${credentials.clientId}&${credentials.clientSecret}`).update(data.join('')).digest('base64'); if (headerData['sm-signature'] !== computedSignature) { // Signature is not valid so ignore call @@ -400,6 +484,12 @@ export class SurveyMonkeyTrigger implements INodeType { let responseData = JSON.parse(data.join('')); let endpoint = ''; + let returnItem: INodeExecutionData[] = [ + { + json: responseData, + } + ]; + if (event === 'response_completed') { const resolveData = this.getNodeParameter('resolveData') as boolean; if (resolveData) { @@ -409,18 +499,198 @@ export class SurveyMonkeyTrigger implements INodeType { endpoint = `/collectors/${responseData.resources.collector_id}/responses/${responseData.object_id}/details`; } responseData = await surveyMonkeyApiRequest.call(this, 'GET', endpoint); + const surveyId = responseData.survey_id; + + const questions: IQuestion[] = []; + const answers = new Map(); + + const { pages } = await surveyMonkeyApiRequest.call(this, 'GET', `/surveys/${surveyId}/details`); + + for (const page of pages) { + questions.push.apply(questions, page.questions); + } + + for (const page of responseData.pages as IDataObject[]) { + for (const question of page.questions as IDataObject[]) { + answers.set(question.id as string, question.answers as IAnswer[]); + } + } + + const responseQuestions = new Map(); + + for (const question of questions) { + + /* + TODO add support for premiun companents + - File Upload + - Matrix of dropdowm menus + */ + + // if question does not have an answer ignore it + if (!answers.get(question.id)) { + continue; + } + + const heading = question.headings![0].heading as string; + + if (question.family === 'open_ended' || question.family === 'datetime') { + if (question.subtype !== 'multi') { + responseQuestions.set(heading, answers.get(question.id)![0].text as string); + } else { + + const results: IDataObject = {}; + const keys = (question.answers.rows as IRow[]).map(e => e.text) as string[]; + const values = answers.get(question.id)?.map(e => e.text) as string[]; + for (let i = 0; i < keys.length; i++) { + // if for some reason there are questions texts repeted add the index to the key + if (results[keys[i]] !== undefined) { + results[`${keys[i]}(${i})`] = values[i] || ''; + } else { + results[keys[i]] = values[i] || ''; + } + } + responseQuestions.set(heading, results); + } + } + + if (question.family === 'single_choice') { + const other = question.answers.other as IOther; + if (other && other.visible && other.is_answer_choice && answers.get(question.id)![0].other_id) { + responseQuestions.set(heading, answers.get(question.id)![0].text as string); + + } else if (other && other.visible && !other.is_answer_choice){ + const choiceId = answers.get(question.id)![0].choice_id; + + const choice = (question.answers.choices as IChoice[]) + .filter(e => e.id === choiceId)[0]; + + const comment = answers.get(question.id) + ?.find(e => e.other_id === other.id)?.text as string; + responseQuestions.set(heading, { value: choice.text, comment }); + + } else { + const choiceId = answers.get(question.id)![0].choice_id; + const choice = (question.answers.choices as IChoice[]) + .filter(e => e.id === choiceId)[0]; + responseQuestions.set(heading, choice.text); + } + } + + if (question.family === 'multiple_choice') { + const other = question.answers.other as IOther; + const choiceIds = answers.get(question.id)?.map((e) => e.choice_id); + const value = (question.answers.choices as IChoice[]) + .filter(e => choiceIds?.includes(e.id)) + .map(e => e.text) as string[]; + // if "Add an "Other" Answer Option for Comments" is active and was selected + if (other && other.is_answer_choice && other.visible) { + const text = answers.get(question.id) + ?.find(e => e.other_id === other.id)?.text as string; + value.push(text); + } + responseQuestions.set(heading, value); + } + + if (question.family === 'matrix') { + // if more than one row it's a matrix/rating-scale + const rows = question.answers.rows as IRow[]; + + if (rows.length > 1) { + + const results: IDataObject = {}; + const choiceIds = answers.get(question.id)?.map(e => e.choice_id) as string[]; + const rowIds = answers.get(question.id)?.map(e => e.row_id) as string[]; + + const rowsValues = (question.answers.rows as IRow[]) + .filter(e => rowIds!.includes(e.id as string)) + .map(e => e.text); + + const choicesValues = (question.answers.choices as IChoice[]) + .filter(e => choiceIds!.includes(e.id as string)) + .map(e => e.text); + + for (let i = 0; i < rowsValues.length; i++) { + results[rowsValues[i]] = choicesValues[i] || ''; + } + + // add the rows that were not answered + for (const row of question.answers.rows as IDataObject[]) { + if (!rowIds.includes(row.id as string)) { + results[row.text as string] = ''; + } + } + // the comment then add the comment + const other = question.answers.other as IOther; + if (other !== undefined && other.visible) { + results.comment = answers.get(question.id)?.filter((e) => e.other_id)[0].text; + } + + responseQuestions.set(heading, results); + + } else { + const choiceIds = answers.get(question.id)?.map((e) => e.choice_id); + const value = (question.answers.choices as IChoice[]) + .filter(e => choiceIds!.includes(e.id as string)) + .map(e => (e.text === '') ? e.weight : e.text)[0]; + responseQuestions.set(heading, value); + + // if "Add an Other Answer Option for Comments" is active then add comment to the answer + const other = question.answers.other as IOther; + if (other !== undefined && other.visible) { + const response: IDataObject = {}; + //const questionName = (question.answers.other as IOther).text as string; + const text = answers.get(question.id)?.filter((e) => e.other_id)[0].text; + response.value = value; + response.comment = text; + responseQuestions.set(heading, response); + } + } + } + + if (question.family === 'demographic') { + const rows: IDataObject = {}; + for (const row of answers.get(question.id) as IAnswer[]) { + rows[row.row_id as string] = row.text; + } + const addressInfo: IDataObject = {}; + for (const answer of question.answers.rows as IDataObject[]) { + addressInfo[answer.type as string] = rows[answer.id as string] || ''; + } + responseQuestions.set(heading, addressInfo); + } + + if (question.family === 'presentation') { + if (question.subtype === 'image') { + const { url } = question.headings![0].image as IDataObject; + responseQuestions.set(heading, url as string); + } + } + } + delete responseData.pages; + responseData.questions = {}; + + // Map the "Map" to JSON + const tuples = JSON.parse(JSON.stringify([...responseQuestions])); + for (const [key, value] of tuples) { + responseData.questions[key] = value; + } + + const onlyAnswers = this.getNodeParameter('onlyAnswers') as boolean; + if (onlyAnswers) { + responseData = responseData.questions; + } + + returnItem = [ + { + json: responseData, + } + ]; } } - const returnItem: INodeExecutionData = { - json: responseData, - }; - return resolve({ workflowData: [ - [ - returnItem, - ], + returnItem, ], }); }); From 0b438e27388595c9d901d67bae262754780f8380 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sat, 2 May 2020 12:30:23 +0200 Subject: [PATCH 4/4] :zap: Small fixes --- packages/nodes-base/nodes/Asana/GenericFunctions.ts | 2 +- .../nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nodes-base/nodes/Asana/GenericFunctions.ts b/packages/nodes-base/nodes/Asana/GenericFunctions.ts index 1f6a1a5252..83bdfe01a2 100644 --- a/packages/nodes-base/nodes/Asana/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Asana/GenericFunctions.ts @@ -41,7 +41,7 @@ export async function asanaApiRequest(this: IHookFunctions | IExecuteFunctions | // Return a clear error throw new Error('The Asana credentials are not valid!'); } - console.log(error); + if (error.response && error.response.body && error.response.body.errors) { // Try to return the error prettier const errorMessages = error.response.body.errors.map((errorData: { message: string }) => { diff --git a/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts b/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts index 49c44c9ac0..efdc8dba5a 100644 --- a/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts +++ b/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts @@ -521,7 +521,7 @@ export class SurveyMonkeyTrigger implements INodeType { for (const question of questions) { /* - TODO add support for premiun companents + TODO: add support for premium components - File Upload - Matrix of dropdowm menus */