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..66614595eb --- /dev/null +++ b/packages/nodes-base/credentials/SurveyMonkeyApi.credentials.ts @@ -0,0 +1,36 @@ +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 responses
+ - 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..86f999b578 --- /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 | IWebhookFunctions, 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/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 new file mode 100644 index 0000000000..efdc8dba5a --- /dev/null +++ b/packages/nodes-base/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.ts @@ -0,0 +1,703 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + idsExist, + surveyMonkeyApiRequest, + surveyMonkeyRequestAllItems, +} from './GenericFunctions'; + +import { + IAnswer, + IChoice, + IQuestion, + IRow, + IOther, +} from './Interfaces'; + +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: '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: [ + { + 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 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', + }, + { + 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: 'Event', + name: 'event', + type: 'options', + displayOptions: { + show: { + objectType: [ + 'collector', + ], + }, + }, + options: [ + { + 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 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: '', + 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.', + }, + { + 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.', + }, + ], + }; + + 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' && event !== 'survey_created') { + 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' && event !== 'survey_created') { + 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 === 'survey' && event === 'survey_created') { + delete body.object_type; + delete body.object_ids; + } + + 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 = ''; + + let returnItem: INodeExecutionData[] = [ + { + json: responseData, + } + ]; + + 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 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 premium components + - 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, + } + ]; + } + } + + 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 0000000000..e5337accd6 Binary files /dev/null and b/packages/nodes-base/nodes/SurveyMonkey/surveyMonkey.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 51062fbe25..cb48917d6c 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -96,6 +96,7 @@ "dist/credentials/StripeApi.credentials.js", "dist/credentials/SalesmateApi.credentials.js", "dist/credentials/SegmentApi.credentials.js", + "dist/credentials/SurveyMonkeyApi.credentials.js", "dist/credentials/TelegramApi.credentials.js", "dist/credentials/TodoistApi.credentials.js", "dist/credentials/TrelloApi.credentials.js", @@ -226,6 +227,7 @@ "dist/nodes/Switch.node.js", "dist/nodes/Salesmate/Salesmate.node.js", "dist/nodes/Segment/Segment.node.js", + "dist/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.js", "dist/nodes/Telegram/Telegram.node.js", "dist/nodes/Telegram/TelegramTrigger.node.js", "dist/nodes/Todoist/Todoist.node.js", diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index ad64dc160b..6a1500e0d7 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -548,7 +548,7 @@ export interface IWorkflowMetadata { active: boolean; } -export type WebhookHttpMethod = 'GET' | 'POST'; +export type WebhookHttpMethod = 'GET' | 'POST' | 'HEAD'; export interface IWebhookResponseData { workflowData?: INodeExecutionData[][];