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,
],
});
});