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 0000000000..ef6ac995a3 Binary files /dev/null and b/packages/nodes-base/nodes/AgileCrm/agilecrm.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index fcdbde4675..d147d860d0 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -27,6 +27,7 @@ "n8n": { "credentials": [ "dist/credentials/ActiveCampaignApi.credentials.js", + "dist/credentials/AgileCrm.credentials.js", "dist/credentials/AcuitySchedulingApi.credentials.js", "dist/credentials/AirtableApi.credentials.js", "dist/credentials/Amqp.credentials.js", @@ -113,6 +114,7 @@ "nodes": [ "dist/nodes/ActiveCampaign/ActiveCampaign.node.js", "dist/nodes/ActiveCampaign/ActiveCampaignTrigger.node.js", + "dist/nodes/AgileCrm/AgileCrm.node.js", "dist/nodes/Airtable/Airtable.node.js", "dist/nodes/AcuityScheduling/AcuitySchedulingTrigger.node.js", "dist/nodes/Amqp/Amqp.node.js",