n8n-nodes-gohighlevel
Version:
n8n node for GoHighLevel API v2
555 lines (433 loc) • 16.8 kB
text/typescript
import fetch, { BodyInit } from 'node-fetch';
import {
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
IDataObject,
IHttpRequestMethods,
GenericValue,
IN8nHttpResponse,
IHttpRequestOptions,
} from 'n8n-workflow';
import { ICompany, ICustomField, IOpportunity } from '@src/resources';
import { IContact } from '@src/resources/contact/contact.types';
import { THttpFullResponse } from '@src/types/httpRequest';
import { prettyLog } from './pretty-log';
interface ContactSearchParams {
locationId: string;
page: number;
pageLimit: number;
searchAfter?: Array<string | number>;
filters?: Array<{
group?: 'AND' | 'OR';
filters?: Array<{
field: string;
operator: string;
value: any;
}>;
field?: string;
operator?: string;
value?: any;
}>;
sort?: Array<{
field: string;
direction: 'asc' | 'desc';
}>;
}
interface GoHighLevelCredentials {
grantType: string;
authUrl: string;
accessTokenUrl: string;
clientId: string;
clientSecret: string;
scope: string;
authQueryParameters: string;
authentication: string;
oauthTokenData: {
access_token: string;
token_type: string;
expires_in: number;
refresh_token: string;
scope: string;
userType: string;
companyId: string;
locationId: string;
userId: string;
};
}
type TBody = FormData | GenericValue | GenericValue[] | Buffer | URLSearchParams;
type TQueryString = IDataObject;
interface OpportunitySearchParams {
assigned_to?: string;
campaignId?: string;
contact_id?: string;
country?: string;
date?: string;
endDate?: string;
getCalendarEvents?: boolean;
getNotes?: boolean;
getTasks?: boolean;
id?: string;
limit?: number;
order?: string;
page?: number;
pipeline_id?: string;
pipeline_stage_id?: string;
q?: string;
startAfter?: string;
startAfterId?: string;
status?: 'open' | 'won' | 'lost' | 'abandoned' | 'all';
location_id?: string; // required
}
export class GoHighLevelManager {
static instance: GoHighLevelManager;
private baseURL = 'https://services.leadconnectorhq.com';
private n8n: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions;
private locationId: string;
constructor(n8n: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions, locationId: string) {
this.n8n = n8n;
this.locationId = locationId;
}
static async getInstance(n8n: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions) {
const credentials = await n8n.getCredentials('goHighLevelOAuth2Api') as unknown as GoHighLevelCredentials;
prettyLog({ credentials });
const instance = new GoHighLevelManager(n8n, String(credentials.oauthTokenData.locationId));
return instance;
// if (!this.instance) {
// const credentials = await n8n.getCredentials('goHighLevelOAuth2Api') as unknown as GoHighLevelCredentials;
// prettyLog({ credentials });
// this.instance = new GoHighLevelManager(n8n, String(credentials.oauthTokenData.locationId));
// }
// const credentialsInstance = await this.instance.n8n.getCredentials('goHighLevelOAuth2Api') as unknown as GoHighLevelCredentials
// prettyLog({ credentialsInstance });
// return this.instance;
}
async request<T extends IN8nHttpResponse>({ method, path, body, params }: {
method: IHttpRequestMethods, path: string, body?: TBody, params?: TQueryString;
}) {
const credentials = (await this.n8n.getCredentials('goHighLevelOAuth2Api')) as unknown as GoHighLevelCredentials;
const accessToken = credentials.oauthTokenData.access_token;
// console.log({ accessToken });
const url = path.startsWith('/') ? `${this.baseURL}${path}` : path;
const urlWithParams = new URL(url);
if (params) {
Object.entries(params).forEach(([key, value]) => {
urlWithParams.searchParams.set(key, String(value));
});
}
const formattedBody = body ? (body instanceof FormData || body instanceof Buffer ? body : JSON.stringify(body)) : undefined;
// curl log
// console.log(`curl -X ${method} ${urlWithParams.toString()}\n -H "Authorization: Bearer ${accessToken}"\n -H "Version: 2021-07-28"\n${formattedBody ? ` -d '${JSON.stringify(formattedBody, null, 2)}'` : ''}`);
console.log(`curl -X ${method} ${urlWithParams.toString()}\n${body ? ` -d '${JSON.stringify(body, null, 2)}'` : ''}`);
const response = await fetch(urlWithParams.toString(), {
method,
body: formattedBody as BodyInit,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
'Version': '2021-07-28',
},
});
console.log('response infos', response.status, response.statusText);
const data = await response.json() as T;
console.log('response', data);
return data;
}
async httpRequestWithAuthentication<T extends IN8nHttpResponse>({ method, path, body, params, verbose = false }: {
method: IHttpRequestMethods, path: string, body?: TBody, params?: TQueryString, verbose?: boolean;
}) {
const url = path.startsWith('/') ? `${this.baseURL}${path}` : path;
const options: IHttpRequestOptions = {
method,
body,
headers: {
'Content-Type': 'application/json',
'Version': '2021-07-28',
},
qs: params,
url: url,
returnFullResponse: true
};
if (verbose) {
console.log(`curl -X ${method} ${url}\n${body ? ` -d '${JSON.stringify(body, null, 2)}'` : ''}`);
prettyLog(options);
}
const response = (await this.n8n.helpers.httpRequestWithAuthentication.call(this.n8n, 'goHighLevelOAuth2Api', options)) as THttpFullResponse<T>;
if (verbose) {
prettyLog({
infos: {
statusCode: response.statusCode,
statusMessage: response.statusMessage,
},
body: response.body,
});
}
return response;
}
async get<T extends IN8nHttpResponse>(path: string, options?: { params?: TQueryString, verbose?: boolean }) {
const response = await this.httpRequestWithAuthentication<T>({
method: 'GET',
path,
...options,
verbose: false
});
return response.body;
}
async post<T extends IN8nHttpResponse>(path: string, options?: { body: TBody, verbose?: boolean }) {
const response = await this.httpRequestWithAuthentication<T>({
method: 'POST',
path,
...options,
verbose: true
});
return response.body;
}
async put<T extends IN8nHttpResponse>(path: string, options?: { body: TBody, verbose?: boolean }) {
const response = await this.httpRequestWithAuthentication<T>({
method: 'PUT',
path,
...options,
verbose: true,
});
return response.body;
}
async delete<T extends IN8nHttpResponse>(path: string, options?: { body: TBody, verbose?: boolean }) {
const response = await this.httpRequestWithAuthentication<T>({
method: 'DELETE',
path,
...options,
});
return response.body;
}
async findContact(params: ContactSearchParams) {
const { contacts } = await this.post<{ contacts: IContact[] }>('/contacts/search', {
body: params,
});
return contacts[0];
}
async findContactByEmail(email: string) {
const contact = await this.findContact({
locationId: this.locationId,
page: 1,
pageLimit: 50,
filters: [{
field: 'email',
operator: 'eq',
value: email,
}],
});
return contact;
}
#enrichContact(contact: IContact, customFields: ICustomField[]) {
const customFieldsMap = new Map(customFields.map((customField) => [customField.id, customField.name]));
return {
...contact,
customFields: contact.customFields?.map((customField) => ({
name: customFieldsMap.get(customField.id),
...customField,
})),
};
}
#enrichOpportunity(opportunity: IOpportunity, customFields: ICustomField[]) {
const customFieldsMap = new Map(customFields.map((customField) => [customField.id, customField.name]));
return {
...opportunity,
customFields: opportunity.customFields?.map((customField) => ({
name: customFieldsMap.get(customField.id),
...customField,
})),
};
}
async createContact(contact: IContact) {
const { contact: createdContact } = await this.post<{ contact: IContact }>('/contacts', {
body: { locationId: this.locationId, ...contact },
});
const customFields = await this.getCustomFields('contact');
const enrichedContact = this.#enrichContact(createdContact, customFields);
return enrichedContact;
}
async getContacts(options?: { params?: TQueryString }) {
const locationId = this.locationId;
const { contacts } = await this.get<{ contacts: IContact[] }>('/contacts', {
params: {
locationId,
...options?.params,
},
});
const customFields = await this.getCustomFields('contact');
const enrichedContacts = contacts.map((contact) => this.#enrichContact(contact, customFields));
return enrichedContacts;
}
async getContact(contactId: string) {
const { contact } = await this.get<{ contact: IContact }>(`/contacts/${contactId}`);
const customFields = await this.getCustomFields('contact');
return this.#enrichContact(contact, customFields);
}
async updateContact(contactId: string, contact: IContact) {
const customFields = await this.getCustomFields('contact');
const { contact: updatedContact } = await this.put<{ contact: IContact }>(`/contacts/${contactId}`, {
body: contact,
});
const enrichedContact = this.#enrichContact(updatedContact, customFields);
return enrichedContact;
}
async upsertContact(contact: IContact) {
const { contact: upsertedContact } = await this.post<{ contact: IContact }>('/contacts/upsert', {
body: contact,
});
const customFields = await this.getCustomFields('contact');
const enrichedContact = this.#enrichContact(upsertedContact, customFields);
return enrichedContact;
}
async deleteContact(contactId: string) {
const { contact } = await this.delete<{ contact: IContact }>(`/contacts/${contactId}`);
return contact;
}
async addTagToContact(contactId: string, tags: string[]) {
const { contact: updatedContact } = await this.post<{ contact: IContact }>(`/contacts/${contactId}/tags`, {
body: { tags },
});
return updatedContact;
}
async removeTagFromContact(contactId: string, tags: string[]) {
const { contact: updatedContact } = await this.delete<{ contact: IContact }>(`/contacts/${contactId}/tags`, {
body: { tags },
});
return updatedContact;
}
async findDuplicateContacts(contact: IContact) {
const { contacts } = await this.post<{ contacts: IContact[] }>('/contacts/search/duplicate', {
body: contact,
});
return contacts;
}
// CRUD for company
async createCompany(company: ICompany) {
const { company: createdCompany } = await this.post<{ company: ICompany }>('/companies', {
body: company,
});
return createdCompany;
}
async getCompany(companyId: string) {
const { company } = await this.get<{ company: ICompany }>(`/companies/${companyId}`);
return company;
}
async getCompanies(options?: { params?: TQueryString }) {
const { companies } = await this.get<{ companies: ICompany[] }>('/companies', options);
return companies;
}
async updateCompany(companyId: string, company: ICompany) {
const { company: updatedCompany } = await this.put<{ company: ICompany }>(`/companies/${companyId}`, {
body: company,
});
return updatedCompany;
}
async deleteCompany(companyId: string) {
const { company } = await this.delete<{ company: ICompany }>(`/companies/${companyId}`);
return company;
}
async addTagsToCompany(companyId: string, tags: string[]) {
const { company: updatedCompany } = await this.post<{ company: ICompany }>(`/companies/${companyId}/tags`, {
body: { tags },
});
return updatedCompany;
}
async removeTagsFromCompany(companyId: string, tags: string[]) {
const { company: updatedCompany } = await this.delete<{ company: ICompany }>(`/companies/${companyId}/tags`, {
body: { tags },
});
return updatedCompany;
}
// CRUD for opportunity
async findOpportunity(params: OpportunitySearchParams) {
const locationId = this.locationId;
const { opportunities } = await this.post<{ opportunities: IOpportunity[] }>('/opportunities/search', {
body: { location_id: locationId, ...params },
});
const opportunity = opportunities[0];
const customFields = await this.getCustomFields('opportunity')
const enrichedOpportunity = this.#enrichOpportunity(opportunity, customFields)
return enrichedOpportunity;
}
async createOpportunity(opportunity: IOpportunity) {
const { opportunity: createdOpportunity } = await this.post<{ opportunity: IOpportunity }>('/opportunities/', {
body: {
locationId: this.locationId,
...opportunity
},
});
const customFields = await this.getCustomFields('opportunity');
const enrichedOpportunity = this.#enrichOpportunity(createdOpportunity, customFields);
return enrichedOpportunity;
}
async getOpportunity(opportunityId: string) {
const { opportunity } = await this.get<{ opportunity: IOpportunity }>(`/opportunities/${opportunityId}`);
const customFields = await this.getCustomFields('opportunity');
return this.#enrichOpportunity(opportunity, customFields);
}
async getOpportunities(options?: { params?: TQueryString }) {
const response = await this.get<{ opportunities: IOpportunity[] }>('/opportunities/search', {
params: {
location_id: this.locationId,
...options?.params,
},
});
const customFields = await this.getCustomFields('opportunity');
console.log('response', response);
const opportunities = response.opportunities || [];
return opportunities.map((opportunity) => this.#enrichOpportunity(opportunity, customFields));
}
async updateOpportunity(opportunityId: string, opportunity: Partial<IOpportunity>) {
const customFields = await this.getCustomFields('opportunity');
const { opportunity: updatedOpportunity } = await this.put<{ opportunity: IOpportunity }>(`/opportunities/${opportunityId}`, {
body: opportunity,
});
const enrichedOpportunity = this.#enrichOpportunity(updatedOpportunity, customFields);
return enrichedOpportunity;
}
async deleteOpportunity(opportunityId: string) {
const { opportunity } = await this.delete<{ opportunity: IOpportunity }>(`/opportunities/${opportunityId}`);
return opportunity;
}
async updateOpportunityStage(opportunityId: string, stageId: string) {
const { opportunity: updatedOpportunity } = await this.put<{ opportunity: IOpportunity }>(`/opportunities/${opportunityId}/stage`, {
body: { stageId },
});
return updatedOpportunity;
}
async updateOpportunityStatus(opportunityId: string, status: string) {
const { opportunity: updatedOpportunity } = await this.put<{ opportunity: IOpportunity }>(`/opportunities/${opportunityId}/status`, {
body: { status },
});
return updatedOpportunity;
}
// CRUD for custom field
async getCustomFields(model: 'contact' | 'opportunity' | 'all' = 'all') {
const { customFields } = await this.get<{ customFields: ICustomField[] }>(`/locations/${this.locationId}/customFields`, {
params: {
model,
},
});
return customFields;
}
async createCustomField(model: 'contact' | 'opportunity' | 'company', customField: ICustomField) {
const { customField: createdCustomField } = await this.post<{ customField: ICustomField }>(`/locations/${this.locationId}/customFields`, {
body: { ...customField, model },
});
return createdCustomField;
}
async getCustomField(customFieldId: string) {
const { customField } = await this.get<{ customField: ICustomField }>(`/locations/${this.locationId}/customFields/${customFieldId}`);
return customField;
}
async updateCustomField(customFieldId: string, customField: Partial<ICustomField>) {
const { customField: updatedCustomField } = await this.put<{ customField: ICustomField }>(`/locations/${this.locationId}/customFields/${customFieldId}`, {
body: customField,
});
return updatedCustomField;
}
async deleteCustomField(customFieldId: string) {
const { customField } = await this.delete<{ customField: ICustomField }>(`/locations/${this.locationId}/customFields/${customFieldId}`);
return customField;
}
}