UNPKG

n8n-nodes-gohighlevel

Version:
555 lines (433 loc) 16.8 kB
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; } }