Skip to content

Commit

Permalink
Merge pull request #595 from manish-singh-bisht/assoc
Browse files Browse the repository at this point in the history
feat:  Support for Hubspot associations API
  • Loading branch information
Nabhag8848 authored Jul 30, 2024
2 parents bdd26bf + 0ce366f commit d4cdc8c
Show file tree
Hide file tree
Showing 19 changed files with 680 additions and 93 deletions.
2 changes: 2 additions & 0 deletions fern/definition/crm/company.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ service:
name: GetCompanyRequest
query-parameters:
fields: optional<string>
associations: optional<string>
response: GetCompanyResponse
errors:
- errors.UnAuthorizedError
Expand All @@ -74,6 +75,7 @@ service:
name: GetCompaniesRequest
query-parameters:
fields: optional<string>
associations: optional<string>
pageSize: optional<string>
cursor: optional<string>
response: GetCompaniesResponse
Expand Down
2 changes: 2 additions & 0 deletions fern/definition/crm/contact.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ service:
name: GetContactRequest
query-parameters:
fields: optional<string>
associations: optional<string>
response: GetContactResponse
errors:
- errors.UnAuthorizedError
Expand All @@ -75,6 +76,7 @@ service:
fields: optional<string>
pageSize: optional<string>
cursor: optional<string>
associations: optional<string>
response: GetContactsResponse
errors:
- errors.UnAuthorizedError
Expand Down
2 changes: 2 additions & 0 deletions fern/definition/crm/deal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ service:
name: GetDealRequest
query-parameters:
fields: optional<string>
associations: optional<string>
response: GetDealResponse
errors:
- errors.UnAuthorizedError
Expand All @@ -75,6 +76,7 @@ service:
fields: optional<string>
pageSize: optional<string>
cursor: optional<string>
associations: optional<string>
response: GetDealsResponse
errors:
- errors.UnAuthorizedError
Expand Down
2 changes: 2 additions & 0 deletions fern/definition/crm/event.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ service:
name: GetEventRequest
query-parameters:
fields: optional<string>
associations: optional<string>
response: GetEventResponse
errors:
- errors.UnAuthorizedError
Expand All @@ -80,6 +81,7 @@ service:
fields: optional<string>
pageSize: optional<string>
cursor: optional<string>
associations: optional<string>
response: GetEventsResponse
errors:
- errors.UnAuthorizedError
Expand Down
2 changes: 2 additions & 0 deletions fern/definition/crm/lead.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ service:
name: GetLeadRequest
query-parameters:
fields: optional<string>
associations: optional<string>
response: GetLeadResponse
errors:
- errors.UnAuthorizedError
Expand All @@ -75,6 +76,7 @@ service:
fields: optional<string>
pageSize: optional<string>
cursor: optional<string>
associations: optional<string>
response: GetLeadsResponse
errors:
- errors.UnAuthorizedError
Expand Down
2 changes: 2 additions & 0 deletions fern/definition/crm/note.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ service:
name: GetNoteRequest
query-parameters:
fields: optional<string>
associations: optional<string>
response: GetNoteResponse
errors:
- errors.UnAuthorizedError
Expand All @@ -75,6 +76,7 @@ service:
fields: optional<string>
pageSize: optional<string>
cursor: optional<string>
associations: optional<string>
response: GetNotesResponse
errors:
- errors.UnAuthorizedError
Expand Down
2 changes: 2 additions & 0 deletions fern/definition/crm/task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ service:
name: GetTaskRequest
query-parameters:
fields: optional<string>
associations: optional<string>
response: GetTaskResponse
errors:
- errors.UnAuthorizedError
Expand All @@ -75,6 +76,7 @@ service:
fields: optional<string>
pageSize: optional<string>
cursor: optional<string>
associations: optional<string>
response: GetTasksResponse
errors:
- errors.UnAuthorizedError
Expand Down
2 changes: 2 additions & 0 deletions fern/definition/crm/user.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ service:
name: GetUserRequest
query-parameters:
fields: optional<string>
associations: optional<string>
response: GetUserResponse
errors:
- errors.UnAuthorizedError
Expand All @@ -73,6 +74,7 @@ service:
fields: optional<string>
pageSize: optional<string>
cursor: optional<string>
associations: optional<string>
response: GetUsersResponse
errors:
- errors.UnAuthorizedError
Expand Down
159 changes: 159 additions & 0 deletions packages/backend/helpers/crm/hubspot.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import axios from 'axios';
import { AllAssociation } from '../../constants/associations';
import { StandardObjects } from '../../constants/common';
import { unifyObject } from './transform';

export const getHubspotAssociationObj = (key: AllAssociation, associateObj: StandardObjects) => {
const associationTypeMapping: {
Expand Down Expand Up @@ -72,6 +74,163 @@ export const getHubspotAssociationObj = (key: AllAssociation, associateObj: Stan
return null;
};

export type PluralObjectType = 'notes' | 'deals' | 'contacts' | 'leads' | 'companies' | 'events' | 'tasks' | 'users';
export const getStandardObjects = (obj: PluralObjectType) => {
const correctStandardObj = {
notes: StandardObjects.note,
deals: StandardObjects.deal,
contacts: StandardObjects.contact,
leads: StandardObjects.lead,
companies: StandardObjects.company,
events: StandardObjects.event,
tasks: StandardObjects.task,
users: StandardObjects.user,
};

const object = correctStandardObj[obj];
return object ? object : null;
};

export const getAssociationObjects = async (
originalAssociations: any,
thirdPartyToken: any,
thirdPartyId: any,
connection: any,
account: any,
invalidAssociations: any
) => {
const associatedData: any = {};

if (originalAssociations) {
//looping over multiple associations if any.
for (const [objectType, associatedDataResult] of Object.entries(originalAssociations)) {
associatedData[objectType] = [];

if (associatedDataResult) {
associatedData[objectType] = [...associatedData[objectType], ...(associatedDataResult as any).results];

let hasMoreLink = (associatedDataResult as any).paging?.next?.link;

if (hasMoreLink) {
//the data is paginated,thus running a loop to get the whole data.
while (hasMoreLink) {
const nextPageAssociatedData = await axios({
method: 'get',
url: hasMoreLink,
headers: {
authorization: `Bearer ${thirdPartyToken}`,
'Content-Type': 'application/json',
},
});
associatedData[objectType] = [
...associatedData[objectType],
...nextPageAssociatedData.data.results,
];

//the following if-else condition can mess up,in my test request via postman ,it gets all remaining in the second request call.i tested with around 400 deals associated to a single company.Writing it still to be on the safer side.
if (nextPageAssociatedData.data.paging?.next?.link) {
hasMoreLink = nextPageAssociatedData.data.paging?.next.link;
} else {
hasMoreLink = undefined;
}

if (nextPageAssociatedData.headers['x-hubspot-ratelimit-secondly-remaining'] === 0) {
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
}

//collecting all ids for batch request.
const ids = (associatedData[objectType] as any[]).map((item) => item.id);

if (ids.length > 0) {
let index = 0;
let fullBatchData: any[] = [];

while (index < ids.length) {
const batchIds = ids.slice(index, index + 100); //the batch api only takes 100 at a time

//bacth request for that object type to get full data for that object.
const responseData = await axios({
method: 'post',
url: `https://api.hubapi.com/crm/v3/objects/${objectType}/batch/read`,
headers: {
authorization: `Bearer ${thirdPartyToken}`,
'content-type': 'application/json',
},
data: JSON.stringify({
inputs: batchIds,
}),
});
index += 100;

fullBatchData = [...fullBatchData, ...responseData.data.results];

if (responseData.headers['x-hubspot-ratelimit-secondly-remaining'] === 0) {
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}

//converting the objectType into the correct objType which is needed for unification.
const associatedObjectType = getStandardObjects(objectType as PluralObjectType);

associatedData[objectType] =
associatedObjectType &&
associatedObjectType !== null &&
(await Promise.all(
fullBatchData.map(
async (item: any) =>
await unifyObject<any, any>({
obj: {
...item,
...item?.properties,
},
tpId: thirdPartyId,
objType: associatedObjectType,
tenantSchemaMappingId: connection.schema_mapping_id,
accountFieldMappingConfig: account.accountFieldMappingConfig,
})
)
));
}
}
}
}
if (invalidAssociations && invalidAssociations.length > 0) {
invalidAssociations.map((item: string) => {
associatedData[item] = [
{
error: 'No such association object or if its exists we currently do not support, please contact us for more information.',
},
];
});
}

return associatedData;
};

export const isValidAssociationTypeRequestedByUser = (str: string) => {
const validStrings = [
'notes',
'deals',
'contacts',
'leads',
'companies',
'events',
'tasks',
'users',
'note',
'deal',
'contact',
'lead',
'company',
'event',
'task',
'user',
];
return validStrings.includes(str);
};

export function handleHubspotDisunify<T extends Record<string, any>, AssociationType extends AllAssociation>({
obj,
objType,
Expand Down
7 changes: 6 additions & 1 deletion packages/backend/helpers/crm/transform/unify.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { accountFieldMappingConfig } from '@prisma/client';
import { TP_ID, accountFieldMappingConfig } from '@prisma/client';
import { CRM_TP_ID, ChatStandardObjects, StandardObjects, TicketStandardObjects } from '../../../constants/common';
import { transformFieldMappingToModel } from '.';
import { preprocessUnifyObject } from './preprocess';
Expand Down Expand Up @@ -44,6 +44,11 @@ export async function unifyObject<T extends Record<string, any>, K>({
}
}
});
if (tpId === TP_ID.hubspot) {
if (obj.associations) {
unifiedObject.associations = obj.associations;
}
}

// Check if associations object is empty and set it to undefined
if (Object.keys(unifiedObject.associations || {}).length === 0) {
Expand Down
Loading

0 comments on commit d4cdc8c

Please sign in to comment.