Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Update "@hubspot/api-client" from 6.0.1 to 12.0.1 CAL-[5029] #18670

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -2926,5 +2926,6 @@
"managed_users": "Managed Users",
"managed_users_description": "See all the managed users created by your OAuth client",
"select_oAuth_client": "Select Oauth Client",
"hubspot_create_contact_under_company": "Create new contacts under a company where the website matches the contact email domain",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
13 changes: 9 additions & 4 deletions packages/app-store/hubspot/api/add.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import * as hubspot from "@hubspot/api-client";
import { Client } from "@hubspot/api-client";
import type { NextApiRequest, NextApiResponse } from "next";

import { WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";

import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";

const scopes = ["crm.objects.contacts.read", "crm.objects.contacts.write"];
const scopes = [
"crm.objects.contacts.read",
"crm.objects.contacts.write",
"crm.schemas.companies.read",
"crm.objects.companies.read",
];

let client_id = "";
const hubspotClient = new hubspot.Client();
const oauth = new Client().oauth;

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "GET") return res.status(405).json({ message: "Method not allowed" });
Expand All @@ -19,7 +24,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (!client_id) return res.status(400).json({ message: "HubSpot client id missing." });

const redirectUri = `${WEBAPP_URL_FOR_OAUTH}/api/integrations/hubspot/callback`;
const url = hubspotClient.oauth.getAuthorizationUrl(
const url = oauth.getAuthorizationUrl(
client_id,
redirectUri,
scopes.join(" "),
Expand Down
7 changes: 4 additions & 3 deletions packages/app-store/hubspot/api/callback.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as hubspot from "@hubspot/api-client";
import { Client } from "@hubspot/api-client";
import type { TokenResponseIF } from "@hubspot/api-client/lib/codegen/oauth/models/TokenResponseIF";
import type { NextApiRequest, NextApiResponse } from "next";

Expand All @@ -13,7 +13,6 @@ import metadata from "../_metadata";

let client_id = "";
let client_secret = "";
const hubspotClient = new hubspot.Client();

export interface HubspotToken extends TokenResponseIF {
expiryDate?: number;
Expand All @@ -23,6 +22,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { code } = req.query;
const state = decodeOAuthState(req);

const oauth = new Client().oauth;

if (code && typeof code !== "string") {
res.status(400).json({ message: "`code` must be a string" });
return;
Expand All @@ -38,7 +39,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (!client_id) return res.status(400).json({ message: "HubSpot client id missing." });
if (!client_secret) return res.status(400).json({ message: "HubSpot client secret missing." });

const hubspotToken: HubspotToken = await hubspotClient.oauth.tokensApi.createToken(
const hubspotToken: HubspotToken = await oauth.tokensApi.create(
"authorization_code",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change in method from .createToken() to create()

code,
`${WEBAPP_URL_FOR_OAUTH}/api/integrations/hubspot/callback`,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import { usePathname } from "next/navigation";

import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
import AppCard from "@calcom/app-store/_components/AppCard";
import useIsAppEnabled from "@calcom/app-store/_utils/useIsAppEnabled";
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Switch } from "@calcom/ui";

import type { appDataSchema } from "../zod";

const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
const pathname = usePathname();

const { t } = useLocale();
const { enabled, updateEnabled } = useIsAppEnabled(app);
const { getAppData, setAppData, disabled } = useAppContextWithSchema<typeof appDataSchema>();

const createContactUnderCompany = getAppData("createContactUnderCompany");

return (
<AppCard
Expand All @@ -19,8 +28,20 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
updateEnabled(e);
}}
switchChecked={enabled}
hideAppCardOptions
/>
hideSettingsIcon>
<>
<div>
<Switch
label={t("hubspot_create_contact_under_company")}
labelOnLeading
checked={createContactUnderCompany}
onCheckedChange={(checked) => {
setAppData("createContactUnderCompany", checked);
}}
/>
</div>
</>
</AppCard>
);
};

Expand Down
177 changes: 115 additions & 62 deletions packages/app-store/hubspot/lib/CrmService.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as hubspot from "@hubspot/api-client";
import type { BatchInputPublicAssociation } from "@hubspot/api-client/lib/codegen/crm/associations";
import type { PublicObjectSearchRequest } from "@hubspot/api-client/lib/codegen/crm/contacts";
import { Client, AssociationTypes } from "@hubspot/api-client";
import { FilterOperatorEnum as CompanyFilterOperatorEnum } from "@hubspot/api-client/lib/codegen/crm/companies";
import type {
PublicObjectSearchRequest as ContactSearchInput,
SimplePublicObjectInputForCreate as HubspotContactCreateInput,
PublicObjectSearchRequest as CompanySearchInput,
} from "@hubspot/api-client/lib/codegen/crm/contacts";
import { FilterOperatorEnum as ContactFilterOperatorEnum } from "@hubspot/api-client/lib/codegen/crm/contacts";
import { AssociationSpecAssociationCategoryEnum as ContactAssociationCategoryEnum } from "@hubspot/api-client/lib/codegen/crm/contacts";
import type {
SimplePublicObject,
SimplePublicObjectInput,
SimplePublicObjectInputForCreate as MeetingCreateInput,
PublicAssociationsForObject,
} from "@hubspot/api-client/lib/codegen/crm/objects/meetings";
import { AssociationSpecAssociationCategoryEnum as MeetingAssociationCategoryEnum } from "@hubspot/api-client/lib/codegen/crm/objects/meetings";
import type z from "zod";

import { getLocation } from "@calcom/lib/CalEventParser";
import { WEBAPP_URL } from "@calcom/lib/constants";
Expand All @@ -19,27 +28,25 @@ import type { CRM, ContactCreateInput, Contact, CrmEvent } from "@calcom/types/C
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import refreshOAuthTokens from "../../_utils/oauth/refreshOAuthTokens";
import type { HubspotToken } from "../api/callback";
import type { appDataSchema } from "../zod";

const hubspotClient = new hubspot.Client();

interface CustomPublicObjectInput extends SimplePublicObjectInput {
id?: string;
}
const hubspotClient = new Client();

export default class HubspotCalendarService implements CRM {
private url = "";
private integrationName = "";
private auth: Promise<{ getToken: () => Promise<HubspotToken | void | never[]> }>;
private log: typeof logger;
private client_id = "";
private client_secret = "";
private appOptions: z.infer<typeof appDataSchema>;

constructor(credential: CredentialPayload) {
constructor(credential: CredentialPayload, appOptions: any) {
this.integrationName = "hubspot_other_calendar";

this.auth = this.hubspotAuth(credential).then((r) => r);

this.log = logger.getSubLogger({ prefix: [`[[lib] ${this.integrationName}`] });
this.appOptions = appOptions;
}

private getHubspotMeetingBody = (event: CalendarEvent): string => {
Expand All @@ -50,35 +57,40 @@ export default class HubspotCalendarService implements CRM {
}`;
};

private hubspotCreateMeeting = async (event: CalendarEvent) => {
const simplePublicObjectInput: SimplePublicObjectInput = {
properties: {
hs_timestamp: Date.now().toString(),
hs_meeting_title: event.title,
hs_meeting_body: this.getHubspotMeetingBody(event),
hs_meeting_location: getLocation(event),
hs_meeting_start_time: new Date(event.startTime).toISOString(),
hs_meeting_end_time: new Date(event.endTime).toISOString(),
hs_meeting_outcome: "SCHEDULED",
},
};

return hubspotClient.crm.objects.meetings.basicApi.create(simplePublicObjectInput);
};
private hubspotCreateMeeting = async (event: CalendarEvent, contacts: Contact[]) => {
try {
const simplePublicObjectInput: MeetingCreateInput = {
properties: {
hs_timestamp: Date.now().toString(),
hs_meeting_title: event.title,
hs_meeting_body: this.getHubspotMeetingBody(event),
hs_meeting_location: getLocation(event),
hs_meeting_start_time: new Date(event.startTime).toISOString(),
hs_meeting_end_time: new Date(event.endTime).toISOString(),
hs_meeting_outcome: "SCHEDULED",
},
associations: contacts.reduce((associations, contact) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of making a separate call to associate a meeting with a contact, we can do it in one call. It'll actually throw a type error if we don't.

if (contact.id) {
associations.push({
to: {
id: contact.id,
},
types: [
{
associationCategory: MeetingAssociationCategoryEnum.HubspotDefined,
associationTypeId: AssociationTypes.meetingToContact,
},
],
});
}
return associations;
}, [] as PublicAssociationsForObject[]),
};

private hubspotAssociate = async (meeting: SimplePublicObject, contacts: Array<{ id: string }>) => {
const batchInputPublicAssociation: BatchInputPublicAssociation = {
inputs: contacts.map((contact: { id: string }) => ({
_from: { id: meeting.id },
to: { id: contact.id },
type: "meeting_event_to_contact",
})),
};
return hubspotClient.crm.associations.batchApi.create(
"meetings",
"contacts",
batchInputPublicAssociation
);
return await hubspotClient.crm.objects.meetings.basicApi.create(simplePublicObjectInput);
} catch (e) {
this.log.warn(`error creating event for bookingUid ${event.uid}, ${e}`);
}
};

private hubspotUpdateMeeting = async (uid: string, event: CalendarEvent) => {
Expand Down Expand Up @@ -120,7 +132,7 @@ export default class HubspotCalendarService implements CRM {
try {
const hubspotRefreshToken: HubspotToken = await refreshOAuthTokens(
async () =>
await hubspotClient.oauth.tokensApi.createToken(
await hubspotClient.oauth.tokensApi.create(
"refresh_token",
undefined,
`${WEBAPP_URL}/api/integrations/hubspot/callback`,
Expand Down Expand Up @@ -156,23 +168,18 @@ export default class HubspotCalendarService implements CRM {
};

async handleMeetingCreation(event: CalendarEvent, contacts: Contact[]) {
const contactIds: { id?: string }[] = contacts.map((contact) => ({ id: contact.id }));
const meetingEvent = await this.hubspotCreateMeeting(event);
const meetingEvent = await this.hubspotCreateMeeting(event, contacts);

if (meetingEvent) {
this.log.debug("meeting:creation:ok", { meetingEvent });
const associatedMeeting = await this.hubspotAssociate(meetingEvent, contactIds as any);
if (associatedMeeting) {
this.log.debug("association:creation:ok", { associatedMeeting });
return Promise.resolve({
uid: meetingEvent.id,
id: meetingEvent.id,
type: "hubspot_other_calendar",
password: "",
url: "",
additionalInfo: { contacts, associatedMeeting },
});
}
return Promise.reject("Something went wrong when associating the meeting and attendees in HubSpot");
return Promise.resolve({
uid: meetingEvent.id,
id: meetingEvent.id,
type: "hubspot_other_calendar",
password: "",
url: "",
additionalInfo: { contacts, meetingEvent },
});
}
this.log.debug("meeting:creation:notOk", { meetingEvent, event, contacts });
return Promise.reject("Something went wrong when creating a meeting in HubSpot");
Expand Down Expand Up @@ -204,20 +211,19 @@ export default class HubspotCalendarService implements CRM {

const emailArray = Array.isArray(emails) ? emails : [emails];

const publicObjectSearchRequest: PublicObjectSearchRequest = {
const publicObjectSearchRequest: ContactSearchInput = {
filterGroups: emailArray.map((attendeeEmail) => ({
filters: [
{
value: attendeeEmail,
propertyName: "email",
operator: "EQ",
operator: ContactFilterOperatorEnum.Eq,
},
],
})),
sorts: ["hs_object_id"],
properties: ["hs_object_id", "email"],
limit: 10,
after: 0,
};

const contacts = await hubspotClient.crm.contacts.searchApi
Expand All @@ -227,7 +233,7 @@ export default class HubspotCalendarService implements CRM {
return contacts.map((contact) => {
return {
id: contact.id,
email: contact.properties.email,
email: contact.properties.email || "",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Emails are required for contacts so not sure why it's throwing a string | null type here.

};
});
}
Expand All @@ -236,14 +242,61 @@ export default class HubspotCalendarService implements CRM {
const auth = await this.auth;
await auth.getToken();

const simplePublicObjectInputs = contactsToCreate.map((attendee) => {
const appOptions = this.getAppOptions();
let companyId: string;

// Check for a company to associate the contact with
if (appOptions?.createContactUnderCompany) {
const emailDomain = contactsToCreate[0].email.split("@")[1];

const companySearchInput: CompanySearchInput = {
filterGroups: [
{
filters: [
{
propertyName: "website",
operator: CompanyFilterOperatorEnum.ContainsToken,
value: emailDomain,
},
],
},
],
properties: ["id"],
limit: 1,
};

const companyQuery = await hubspotClient.crm.companies.searchApi
.doSearch(companySearchInput)
.then((apiResponse) => apiResponse.results);
if (companyQuery.length > 0) {
const company = companyQuery[0];
companyId = company.id;
}
}

const simplePublicObjectInputs: HubspotContactCreateInput[] = contactsToCreate.map((attendee) => {
const [firstname, lastname] = attendee.name ? attendee.name.split(" ") : [attendee.email, ""];
return {
properties: {
firstname,
lastname,
email: attendee.email,
},
...(companyId && {
associations: [
{
to: {
id: companyId,
},
types: [
{
associationCategory: ContactAssociationCategoryEnum.HubspotDefined,
associationTypeId: AssociationTypes.contactToCompany,
},
],
},
],
}),
};
});
const createdContacts = await Promise.all(
Expand All @@ -265,12 +318,12 @@ export default class HubspotCalendarService implements CRM {
return createdContacts.map((contact) => {
return {
id: contact.id,
email: contact.properties.email,
email: contact.properties.email || "",
};
});
}

getAppOptions() {
console.log("No options implemented");
return this.appOptions;
}
}
Loading
Loading