From bbef8b7f71dac4d3df121b916ec2f76fffb1fcad Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Tue, 10 Nov 2020 12:28:03 -0500 Subject: [PATCH] :sparkles: Add GetResponse-Node (#1139) --- .../credentials/GetResponseApi.credentials.ts | 17 + .../GetResponseOAuth2Api.credentials.ts | 47 ++ .../nodes/GetResponse/ContactDescription.ts | 646 ++++++++++++++++++ .../nodes/GetResponse/GenericFunctions.ts | 71 ++ .../nodes/GetResponse/GetResponse.node.ts | 320 +++++++++ .../nodes/GetResponse/getResponse.png | Bin 0 -> 2851 bytes packages/nodes-base/package.json | 3 + 7 files changed, 1104 insertions(+) create mode 100644 packages/nodes-base/credentials/GetResponseApi.credentials.ts create mode 100644 packages/nodes-base/credentials/GetResponseOAuth2Api.credentials.ts create mode 100644 packages/nodes-base/nodes/GetResponse/ContactDescription.ts create mode 100644 packages/nodes-base/nodes/GetResponse/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/GetResponse/GetResponse.node.ts create mode 100644 packages/nodes-base/nodes/GetResponse/getResponse.png diff --git a/packages/nodes-base/credentials/GetResponseApi.credentials.ts b/packages/nodes-base/credentials/GetResponseApi.credentials.ts new file mode 100644 index 0000000000..1494a01930 --- /dev/null +++ b/packages/nodes-base/credentials/GetResponseApi.credentials.ts @@ -0,0 +1,17 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class GetResponseApi implements ICredentialType { + name = 'getResponseApi'; + displayName = 'GetResponse API'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/credentials/GetResponseOAuth2Api.credentials.ts b/packages/nodes-base/credentials/GetResponseOAuth2Api.credentials.ts new file mode 100644 index 0000000000..76ce3acb0f --- /dev/null +++ b/packages/nodes-base/credentials/GetResponseOAuth2Api.credentials.ts @@ -0,0 +1,47 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class GetResponseOAuth2Api implements ICredentialType { + name = 'getResponseOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'GetResponse OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://app.getresponse.com/oauth2_authorize.html', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://api.getresponse.com/v3/token', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'header', + description: 'Resource to consume.', + }, + ]; +} diff --git a/packages/nodes-base/nodes/GetResponse/ContactDescription.ts b/packages/nodes-base/nodes/GetResponse/ContactDescription.ts new file mode 100644 index 0000000000..ba173edde4 --- /dev/null +++ b/packages/nodes-base/nodes/GetResponse/ContactDescription.ts @@ -0,0 +1,646 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const contactOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'contact', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new contact', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a contact', + }, + { + name: 'Get', + value: 'get', + description: 'Get a contact', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all contacts', + }, + { + name: 'Update', + value: 'update', + description: 'Update contact properties', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const contactFields = [ + /* -------------------------------------------------------------------------- */ + /* contact:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Email', + name: 'email', + type: 'string', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: '', + }, + { + displayName: 'Campaign ID', + name: 'campaignId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: '', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Custom Fields', + name: 'customFieldsUi', + type: 'fixedCollection', + default: '', + placeholder: 'Add Custom Field', + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'customFieldValues', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field ID', + name: 'customFieldId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCustomFields', + }, + description: 'The end user specified key of the user defined data.', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + description: 'The end user specified value of the user defined data.', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Day Of Cycle', + name: 'dayOfCycle', + type: 'string', + description: `The day on which the contact is in the Autoresponder cycle. null indicates the contacts is not in the cycle.`, + default: '', + }, + { + displayName: 'IP Address', + name: 'ipAddress', + type: 'string', + description: `The contact's IP address. IPv4 and IPv6 formats are accepted.`, + default: '', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + }, + { + displayName: 'Note', + name: 'note', + type: 'string', + default: '', + }, + { + displayName: 'Scoring', + name: 'scoring', + type: 'number', + default: '', + description: 'Contact scoring, pass null to remove the score from a contact', + typeOptions: { + minValue: 0, + }, + }, + { + displayName: 'Tag IDs', + name: 'tags', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + default: '', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* contact:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'delete', + ], + }, + }, + default: '', + description: 'Id of contact to delete.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'delete', + ], + }, + }, + options: [ + { + displayName: 'IP Address', + name: 'ipAddress', + type: 'string', + description: `This makes it possible to pass the IP from which the contact unsubscribed. Used only if the messageId was send.`, + default: '', + }, + { + displayName: 'Message ID', + name: 'messageId', + type: 'string', + description: `The ID of a message (such as a newsletter, an autoresponder, or an RSS-newsletter). When passed, this method will simulate the unsubscribe process, as if the contact clicked the unsubscribe link in a given message.`, + default: '', + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* contact:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + description: 'Unique identifier for a particular contact', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + description: `List of fields that should be returned. Id is always returned. Fields should be separated by comma`, + default: '', + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* contact:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 20, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Campaign ID', + name: 'campaignId', + type: 'string', + description: `Search contacts by campaign ID`, + default: '', + }, + { + displayName: 'Change On From', + name: 'changeOnFrom', + type: 'dateTime', + default: '', + description: `Search contacts edited from this date`, + }, + { + displayName: 'Change On To', + name: 'changeOnTo', + type: 'dateTime', + default: '', + description: `Search contacts edited to this date`, + }, + { + displayName: 'Created On From', + name: 'createdOnFrom', + type: 'dateTime', + default: '', + description: `Count data from this date`, + }, + { + displayName: 'Created On To', + name: 'createdOnTo', + type: 'dateTime', + default: '', + description: `Count data from this date`, + }, + { + displayName: 'Exact Match', + name: 'exactMatch', + type: 'boolean', + default: false, + description: `When set to true it will search for contacts with the exact value
+ of the email and name provided in the query string. Without this flag, matching is done via a standard 'like' comparison,
+ which may sometimes be slow.`, + }, + { + displayName: 'Fields', + name: 'fields', + type: 'string', + description: `List of fields that should be returned. Id is always returned. Fields should be separated by comma`, + default: '', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + description: `Search contacts by name`, + default: '', + }, + { + displayName: 'Origin', + name: 'origin', + type: 'options', + options: [ + { + name: 'API', + value: 'api', + }, + { + name: 'Copy', + value: 'copy', + }, + { + name: 'Email', + value: 'email', + }, + { + name: 'Forward', + value: 'forward', + }, + { + name: 'import', + value: 'import', + }, + { + name: 'Iphone', + value: 'iphone', + }, + { + name: 'Landing Page', + value: 'landing_page', + }, + { + name: 'Leads', + value: 'leads', + }, + { + name: 'Panel', + value: 'panel', + }, + { + name: 'Sale', + value: 'sale', + }, + { + name: 'Survey', + value: 'survey', + }, + { + name: 'Webinar', + value: 'webinar', + }, + { + name: 'WWW', + value: 'www', + }, + ], + description: `Search contacts by origin`, + default: '', + }, + { + displayName: 'Sort By', + name: 'sortBy', + type: 'options', + options: [ + { + name: 'Campaign ID', + value: 'campaignId', + }, + { + name: 'Changed On', + value: 'changedOn', + }, + { + name: 'Created On', + value: 'createdOn', + }, + { + name: 'Email', + value: 'email', + }, + ], + default: '', + }, + { + displayName: 'Sort Order', + name: 'sortOrder', + type: 'options', + options: [ + { + name: 'ASC', + value: 'ASC', + }, + { + name: 'DESC', + value: 'DESC', + }, + ], + default: '', + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* contact:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + description: 'Unique identifier for a particular contact', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Campaign ID', + name: 'campaignId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + default: '', + description: '', + }, + { + displayName: 'Custom Fields', + name: 'customFieldsUi', + type: 'fixedCollection', + default: '', + placeholder: 'Add Custom Field', + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'customFieldValues', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field ID', + name: 'customFieldId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCustomFields', + }, + description: 'The end user specified key of the user defined data.', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + description: 'The end user specified value of the user defined data.', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Day Of Cycle', + name: 'dayOfCycle', + type: 'string', + description: `The day on which the contact is in the Autoresponder cycle. null indicates the contacts is not in the cycle.`, + default: '', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: '', + }, + { + displayName: 'IP Address', + name: 'ipAddress', + type: 'string', + description: `The contact's IP address. IPv4 and IPv6 formats are accepted.`, + default: '', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + }, + { + displayName: 'Note', + name: 'note', + type: 'string', + default: '', + }, + { + displayName: 'Scoring', + name: 'scoring', + type: 'number', + default: '', + description: 'Contact scoring, pass null to remove the score from a contact', + typeOptions: { + minValue: 0, + }, + }, + { + displayName: 'Tag IDs', + name: 'tags', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + default: '', + }, + ], + }, + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/GetResponse/GenericFunctions.ts b/packages/nodes-base/nodes/GetResponse/GenericFunctions.ts new file mode 100644 index 0000000000..eaae8d4df1 --- /dev/null +++ b/packages/nodes-base/nodes/GetResponse/GenericFunctions.ts @@ -0,0 +1,71 @@ +import { + OptionsWithUri, + } from 'request'; + +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject +} from 'n8n-workflow'; + +export async function getresponseApiRequest(this: IWebhookFunctions | IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const authentication = this.getNodeParameter('authentication', 0, 'apiKey') as string; + + let options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || `https://api.getresponse.com/v3${resource}`, + json: true, + }; + try { + options = Object.assign({}, options, option); + if (Object.keys(body).length === 0) { + delete options.body; + } + + if (authentication === 'apiKey') { + const credentials = this.getCredentials('getResponseApi') as IDataObject; + options!.headers!['X-Auth-Token'] = `api-key ${credentials.apiKey}`; + //@ts-ignore + return await this.helpers.request.call(this, options); + } else { + //@ts-ignore + return await this.helpers.requestOAuth2.call(this, 'getResponseOAuth2Api', options); + } + } catch (error) { + if (error.response && error.response.body && error.response.body.message) { + // Try to return the error prettier + throw new Error(`GetResponse error response [${error.statusCode}]: ${error.response.body.message}`); + } + throw error; + } +} + +export async function getResponseApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + query.page = 1; + + do { + responseData = await getresponseApiRequest.call(this, method, endpoint, body, query, undefined, { resolveWithFullResponse: true }); + query.page++; + returnData.push.apply(returnData, responseData.body); + } while ( + responseData.headers.TotalPages !== responseData.headers.CurrentPage + ); + + return returnData; +} + diff --git a/packages/nodes-base/nodes/GetResponse/GetResponse.node.ts b/packages/nodes-base/nodes/GetResponse/GetResponse.node.ts new file mode 100644 index 0000000000..01b069b79c --- /dev/null +++ b/packages/nodes-base/nodes/GetResponse/GetResponse.node.ts @@ -0,0 +1,320 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + getresponseApiRequest, + getResponseApiRequestAllItems, +} from './GenericFunctions'; + +import { + contactFields, + contactOperations, +} from './ContactDescription'; + +import * as moment from 'moment-timezone'; + +export class GetResponse implements INodeType { + description: INodeTypeDescription = { + displayName: 'GetResponse', + name: 'getResponse', + icon: 'file:getResponse.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume GetResponse API.', + defaults: { + name: 'GetResponse', + color: '#00afec', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'getResponseApi', + required: true, + displayOptions: { + show: { + authentication: [ + 'apiKey', + ], + }, + }, + }, + { + name: 'getResponseOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, + }, + ], + properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'API Key', + value: 'apiKey', + }, + { + name: 'OAuth2', + value: 'oAuth2', + }, + ], + default: 'apiKey', + description: 'The resource to operate on.', + }, + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Contact', + value: 'contact', + }, + ], + default: 'contact', + description: 'The resource to operate on.', + }, + ...contactOperations, + ...contactFields, + ], + }; + + methods = { + loadOptions: { + // Get all the campaigns to display them to user so that he can + // select them easily + async getCampaigns( + this: ILoadOptionsFunctions, + ): Promise { + const returnData: INodePropertyOptions[] = []; + const campaigns = await getresponseApiRequest.call( + this, + 'GET', + `/campaigns`, + ); + for (const campaign of campaigns) { + returnData.push({ + name: campaign.name as string, + value: campaign.campaignId, + }); + } + return returnData; + }, + // Get all the tagd to display them to user so that he can + // select them easily + async getTags( + this: ILoadOptionsFunctions, + ): Promise { + const returnData: INodePropertyOptions[] = []; + const tags = await getresponseApiRequest.call( + this, + 'GET', + `/tags`, + ); + for (const tag of tags) { + returnData.push({ + name: tag.name as string, + value: tag.tagId, + }); + } + return returnData; + }, + // Get all the custom fields to display them to user so that he can + // select them easily + async getCustomFields( + this: ILoadOptionsFunctions, + ): Promise { + const returnData: INodePropertyOptions[] = []; + const customFields = await getresponseApiRequest.call( + this, + 'GET', + `/custom-fields`, + ); + for (const customField of customFields) { + returnData.push({ + name: customField.name as string, + value: customField.customFieldId, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = (items.length as unknown) as number; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { + + if (resource === 'contact') { + //https://apireference.getresponse.com/#operation/createContact + if (operation === 'create') { + const email = this.getNodeParameter('email', i) as string; + + const campaignId = this.getNodeParameter('campaignId', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + email, + campaign: { + campaignId, + }, + }; + + Object.assign(body, additionalFields); + + if (additionalFields.customFieldsUi) { + const customFieldValues = (additionalFields.customFieldsUi as IDataObject).customFieldValues as IDataObject[]; + if (customFieldValues) { + body.customFieldValues = customFieldValues; + for (let i = 0; i < customFieldValues.length; i++) { + if (!Array.isArray(customFieldValues[i].value)) { + customFieldValues[i].value = [customFieldValues[i].value]; + } + } + delete body.customFieldsUi; + } + } + + responseData = await getresponseApiRequest.call(this, 'POST', '/contacts', body); + + responseData = { success: true }; + } + //https://apireference.getresponse.com/?_ga=2.160836350.2102802044.1604719933-1897033509.1604598019#operation/deleteContact + if (operation === 'delete') { + const contactId = this.getNodeParameter('contactId', i) as string; + + const options = this.getNodeParameter('options', i) as IDataObject; + + Object.assign(qs, options); + + responseData = await getresponseApiRequest.call(this, 'DELETE', `/contacts/${contactId}`, {}, qs); + + responseData = { success: true }; + } + //https://apireference.getresponse.com/?_ga=2.160836350.2102802044.1604719933-1897033509.1604598019#operation/getContactById + if (operation === 'get') { + const contactId = this.getNodeParameter('contactId', i) as string; + + const options = this.getNodeParameter('options', i) as IDataObject; + + Object.assign(qs, options); + + responseData = await getresponseApiRequest.call(this, 'GET', `/contacts/${contactId}`, {}, qs); + } + //https://apireference.getresponse.com/?_ga=2.160836350.2102802044.1604719933-1897033509.1604598019#operation/getContactList + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const options = this.getNodeParameter('options', i) as IDataObject; + + const timezone = this.getTimezone(); + + Object.assign(qs, options); + + const isNotQuery = [ + 'sortBy', + 'sortOrder', + 'additionalFlags', + 'fields', + 'exactMatch', + ]; + + const isDate = [ + 'createdOnFrom', + 'createdOnTo', + 'changeOnFrom', + 'changeOnTo', + ]; + + const dateMapToKey: { [key: string]: string; } = { + 'createdOnFrom': '[createdOn][from]', + 'createdOnTo': '[createdOn][to]', + 'changeOnFrom': '[changeOn][from]', + 'changeOnTo': '[changeOn][to]', + }; + + for (const key of Object.keys(qs)) { + if (!isNotQuery.includes(key)) { + if (isDate.includes(key)) { + qs[`query${dateMapToKey[key]}`] = moment.tz(qs[key], timezone).format('YYYY-MM-DDTHH:mm:ssZZ'); + } else { + qs[`query[${key}]`] = qs[key]; + } + delete qs[key]; + } + } + + if (qs.sortBy) { + qs[`sort[${qs.sortBy}]`] = qs.sortOrder || 'ASC'; + } + + if (qs.exactMatch === true) { + qs['additionalFlags'] = 'exactMatch'; + delete qs.exactMatch; + } + + if (returnAll) { + responseData = await getResponseApiRequestAllItems.call(this, 'GET', `/contacts`, {}, qs); + } else { + qs.perPage = this.getNodeParameter('limit', i) as number; + responseData = await getresponseApiRequest.call(this, 'GET', `/contacts`, {}, qs); + } + } + //https://apireference.getresponse.com/?_ga=2.160836350.2102802044.1604719933-1897033509.1604598019#operation/updateContact + if (operation === 'update') { + + const contactId = this.getNodeParameter('contactId', i) as string; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + const body: IDataObject = {}; + + Object.assign(body, updateFields); + + if (updateFields.customFieldsUi) { + const customFieldValues = (updateFields.customFieldsUi as IDataObject).customFieldValues as IDataObject[]; + if (customFieldValues) { + body.customFieldValues = customFieldValues; + delete body.customFieldsUi; + } + } + + responseData = await getresponseApiRequest.call(this, 'POST', `/contacts/${contactId}`, body); + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/GetResponse/getResponse.png b/packages/nodes-base/nodes/GetResponse/getResponse.png new file mode 100644 index 0000000000000000000000000000000000000000..78a1717949755d09a00ea979daf7b76bbb9ea65b GIT binary patch literal 2851 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