From 9ac9ff3557a545d549634fe44f04925a9f71e9ec Mon Sep 17 00:00:00 2001 From: Rupenieks Date: Thu, 30 Apr 2020 14:03:36 +0200 Subject: [PATCH] node setup --- .../credentials/AgileCrmApi.credentials.ts | 23 + .../nodes/AgileCrm/AgileCrm.node.ts | 148 ++++++ .../nodes/AgileCrm/ContactDescription.ts | 473 ++++++++++++++++++ .../nodes/AgileCrm/ContactInterface.ts | 18 + .../nodes/AgileCrm/GenericFunctions.ts | 59 +++ .../nodes-base/nodes/AgileCrm/agilecrm.png | Bin 0 -> 20236 bytes packages/nodes-base/package.json | 2 + 7 files changed, 723 insertions(+) create mode 100644 packages/nodes-base/credentials/AgileCrmApi.credentials.ts create mode 100644 packages/nodes-base/nodes/AgileCrm/AgileCrm.node.ts create mode 100644 packages/nodes-base/nodes/AgileCrm/ContactDescription.ts create mode 100644 packages/nodes-base/nodes/AgileCrm/ContactInterface.ts create mode 100644 packages/nodes-base/nodes/AgileCrm/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/AgileCrm/agilecrm.png diff --git a/packages/nodes-base/credentials/AgileCrmApi.credentials.ts b/packages/nodes-base/credentials/AgileCrmApi.credentials.ts new file mode 100644 index 0000000000..d189270932 --- /dev/null +++ b/packages/nodes-base/credentials/AgileCrmApi.credentials.ts @@ -0,0 +1,23 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class AgileCrmApi implements ICredentialType { + name = 'agileCrmApi'; + displayName = 'AgileCRM API'; + properties = [ + { + displayName: 'Email', + name: 'email', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/AgileCrm/AgileCrm.node.ts b/packages/nodes-base/nodes/AgileCrm/AgileCrm.node.ts new file mode 100644 index 0000000000..84be0ddcfc --- /dev/null +++ b/packages/nodes-base/nodes/AgileCrm/AgileCrm.node.ts @@ -0,0 +1,148 @@ +import { IExecuteFunctions } from 'n8n-core'; +import { + INodeExecutionData, + INodeType, + INodeTypeDescription, + IDataObject +} from 'n8n-workflow'; + +import { + contactOperations, + contactFields +} from './ContactDescription'; +import { agileCrmApiRequest, validateJSON} from './GenericFunctions'; +import { IContact, IProperty } from './ContactInterface'; + + +export class AgileCrm implements INodeType { + description: INodeTypeDescription = { + displayName: 'AgileCRM', + name: 'agileCrm', + icon: 'file:agilecrm.png', + group: ['transform'], + version: 1, + description: 'Consume AgileCRM API', + defaults: { + name: 'AgileCRM', + color: '#772244', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'agileCrmApi', + required: true, + } + ], + properties: [ + // Node properties which the user gets displayed and + // can change on the node. + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Contact', + value: 'contact' + } + ], + default: 'contact', + description: 'Resource to consume.', + }, + // CONTACT + ...contactOperations, + ...contactFields, + ], + + }; + + + async execute(this: IExecuteFunctions): Promise { + + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + let responseData; + const qs: IDataObject = {}; + 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'){ + + if(operation === 'get'){ + const contactId = this.getNodeParameter('contactId', i) as string; + + const endpoint = `api/contacts/${contactId}`; + responseData = await agileCrmApiRequest.call(this, 'GET', endpoint); + + } + + if(operation === 'getAll'){ + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + if (returnAll) { + const endpoint = `api/contacts`; + responseData = await agileCrmApiRequest.call(this, 'GET', endpoint); + } else { + const limit = this.getNodeParameter('limit', i) as number; + const endpoint = `api/contacts?page_size=${limit}`; + responseData = await agileCrmApiRequest.call(this, 'GET', endpoint); + } + } + + if(operation === 'create'){ + const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; + const body: IContact = {}; + + if (jsonParameters) { + const additionalFieldsJson = this.getNodeParameter('additionalFieldsJson', i) as string; + + if (additionalFieldsJson !== '' ) { + + if (validateJSON(additionalFieldsJson) !== undefined) { + + Object.assign(body, JSON.parse(additionalFieldsJson)); + + } else { + throw new Error('Additional fields must be a valid JSON'); + } + } + + } else { + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.starValue) { + body.star_value = additionalFields.starValue as string; + } + if (additionalFields.leadScore) { + body.lead_score = additionalFields.leadScore as string; + } + if (additionalFields.tags) { + body.tags = additionalFields.tags as string[]; + } + if (additionalFields.properties) { + body.properties = (additionalFields.properties as IDataObject).property as IDataObject[]; + } + } + const endpoint = 'api/contacts'; + console.log(body); + responseData = await agileCrmApiRequest.call(this, 'POST', endpoint, body); + } + + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else { + returnData.push(responseData as IDataObject); + } + } + + } + + return [this.helpers.returnJsonArray(returnData)]; + } + +} diff --git a/packages/nodes-base/nodes/AgileCrm/ContactDescription.ts b/packages/nodes-base/nodes/AgileCrm/ContactDescription.ts new file mode 100644 index 0000000000..b660cff6b2 --- /dev/null +++ b/packages/nodes-base/nodes/AgileCrm/ContactDescription.ts @@ -0,0 +1,473 @@ +import { + INodeProperties, + } from 'n8n-workflow'; + +export const contactOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'contact', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get a contact', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all contacts', + }, + { + name: 'Create', + value: 'create', + description: 'Create a new contact', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const contactFields = [ +/* -------------------------------------------------------------------------- */ +/* 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', + }, + + +/* -------------------------------------------------------------------------- */ +/* contact:get all */ +/* -------------------------------------------------------------------------- */ + { + 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', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + } + }, + +/* -------------------------------------------------------------------------- */ +/* ticket:create */ +/* -------------------------------------------------------------------------- */ + +{ + displayName: 'JSON Parameters', + name: 'jsonParameters', + type: 'boolean', + default: false, + description: '', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'create', + ], + }, + }, +}, +{ + displayName: ' Additional Fields', + name: 'additionalFieldsJson', + type: 'json', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'create', + ], + jsonParameters: [ + true, + ], + }, + }, + + description: `Object of values to set as described here.`, +}, + +{ + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'create', + ], + jsonParameters: [ + false, + ], + }, + }, + options: [ + { + displayName: 'Star Value', + name: 'starValue', + type: 'options', + default: '', + required: false, + description: 'Rating of contact (Max value 5). This is not applicable for companies.', + options: [ + { + name: '0', + value: 0 + }, + { + name: '1', + value: 1 + }, + { + name: '2', + value: 2 + }, + { + name: '3', + value: 3 + }, + { + name: '4', + value: 4 + }, + { + name: '5', + value: 5 + }, + ] + }, + { + displayName: 'Lead Score', + name: 'leadScore', + type: 'number', + default: '', + description: 'Score of contact. This is not applicable for companies.', + required: false, + typeOptions: { + minValue: 0 + } + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Tag', + }, + default: [], + placeholder: 'Tag', + description: 'Unique identifiers added to contact, for easy management of contacts. This is not applicable for companies.', + }, + { + displayName: 'Properties', + name: 'properties', + type: 'fixedCollection', + default: {}, + description: 'Contact properties are represented by list of JSON objects, each JSON object should follow the prototype shown. Custom fields will have type as CUSTOM and others will have type as SYSTEM.', + required: true, + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Property', + name: 'property', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + default: 'SYSTEM', + required: true, + description: 'Type of the field.', + options: [ + { + name: 'SYSTEM', + value: 'SYSTEM', + }, + { + name: 'CUSTOM', + value: 'CUSTOM' + } + ] + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + required: true, + description: 'Name of the field.' + }, + { + displayName: 'Sub Type', + name: 'subType', + default: '', + required: false, + type: 'options', + description: 'Name of the field.', + displayOptions: { + show: { + type: [ + 'SYSTEM' + ], + name: [ + 'email' + ] + } + }, + options: [ + { + name: 'Work', + value: 'work', + + }, + { + name: 'Personal', + value: 'personal', + + } + ] + }, + { + displayName: 'Sub Type', + name: 'subType', + default: '', + required: false, + type: 'options', + description: 'Name of the field.', + displayOptions: { + show: { + type: [ + 'SYSTEM' + ], + name: [ + 'phone' + ] + } + }, + options: [ + { + name: 'Work', + value: 'work', + + }, + { + name: 'Home', + value: 'home', + }, + { + name: 'Mobile', + value: 'mobile', + }, + { + name: 'Main', + value: 'main', + }, + { + name: 'Home Fax', + value: 'homeFax', + }, + { + name: 'Work Fax', + value: 'workFax', + }, + { + name: 'Other', + value: 'other', + }, + ] + }, + { + displayName: 'Sub Type', + name: 'subType', + default: '', + required: false, + type: 'options', + description: 'Name of the field.', + displayOptions: { + show: { + type: [ + 'SYSTEM' + ], + name: [ + 'address' + ] + } + }, + options: [ + { + name: 'Home', + value: 'home', + }, + { + name: 'Postal', + value: 'postal', + }, + { + name: 'Office', + value: 'office', + }, + ] + }, + { + displayName: 'Sub Type', + name: 'subType', + default: '', + required: false, + type: 'options', + description: 'Name of the field.', + displayOptions: { + show: { + type: [ + 'SYSTEM' + ], + name: [ + 'website' + ] + } + }, + options: [ + { + name: 'URL', + value: 'url', + }, + { + name: 'SKYPE', + value: 'skype', + }, + { + name: 'TWITTER', + value: 'twitter', + }, + { + name: 'LINKEDIN', + value: 'linkedin', + }, + { + name: 'FACEBOOK', + value: 'facebook', + }, + { + name: 'XING', + value: 'xing', + }, + { + name: 'FEED', + value: 'feed', + }, + { + name: 'GOOGLE_PLUS', + value: 'googlePlus', + }, + { + name: 'FLICKR', + value: 'flickr', + }, + { + name: 'GITHUB', + value: 'github', + }, + { + name: 'YOUTUBE', + value: 'youtube', + }, + ] + }, + { + displayName: 'Sub Type', + name: 'subType', + default: '', + required: false, + type: 'string', + description: 'Name of the field.', + displayOptions: { + show: { + type: [ + 'CUSTOM' + ], + } + } + }, + { + displayName: 'Value', + name: 'value', + default: '', + required: false, + type: 'string', + description: 'Value of the property.' + }, + ] + } + + ] + + }, + ], +}, + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/AgileCrm/ContactInterface.ts b/packages/nodes-base/nodes/AgileCrm/ContactInterface.ts new file mode 100644 index 0000000000..661a4c4742 --- /dev/null +++ b/packages/nodes-base/nodes/AgileCrm/ContactInterface.ts @@ -0,0 +1,18 @@ +import { + IDataObject, + } from 'n8n-workflow'; + + export interface IProperty { + type: string; + name: string; + subtype?: string; + value?: string; +} + + export interface IContact { + star_value?: string; + lead_score?: string; + tags?: string[]; + properties?: IDataObject[]; + } + diff --git a/packages/nodes-base/nodes/AgileCrm/GenericFunctions.ts b/packages/nodes-base/nodes/AgileCrm/GenericFunctions.ts new file mode 100644 index 0000000000..d363443b50 --- /dev/null +++ b/packages/nodes-base/nodes/AgileCrm/GenericFunctions.ts @@ -0,0 +1,59 @@ +import { + OptionsWithUri, + } from 'request'; + +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IExecuteSingleFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + + +export async function agileCrmApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, query: IDataObject = {}, uri?: string): Promise { + + const node = this.getNodeParameter('credentials', 1); + const credentials = this.getCredentials('agileCrmApi'); + + const options: OptionsWithUri = { + method, + headers: { + 'Accept': 'application/json', + }, + body: body! || {}, + auth: { + username: credentials!.email as string, + password: credentials!.apiKey as string + }, + uri: uri || `https://n8nio.agilecrm.com/dev/${endpoint}`, + json: true + }; + + + try { + return await this.helpers.request!(options); + } catch (error) { + + if (error.response && error.response.body && error.response.body.errors) { + const errorMessages = error.response.body.errors.map((e: IDataObject) => e.message); + throw new Error(`AgileCRM error response [${error.statusCode}]: ${errorMessages.join(' | ')}`); + } + + throw error; + } +} + +export function validateJSON(json: string | undefined): any { // tslint:disable-line:no-any + let result; + try { + result = JSON.parse(json!); + } catch (exception) { + result = undefined; + } + return result; +} + diff --git a/packages/nodes-base/nodes/AgileCrm/agilecrm.png b/packages/nodes-base/nodes/AgileCrm/agilecrm.png new file mode 100644 index 0000000000000000000000000000000000000000..ef6ac995a32b776d569b203cbdd1969d99aa977b GIT binary patch literal 20236 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 UrKTU<{!