first commit
This commit is contained in:
94
calcom/packages/lib/sync/ISyncService.ts
Normal file
94
calcom/packages/lib/sync/ISyncService.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import type logger from "@calcom/lib/logger";
|
||||
import { default as webPrisma } from "@calcom/prisma";
|
||||
|
||||
export type UserInfo = {
|
||||
email: string;
|
||||
name: string | null;
|
||||
id: number;
|
||||
username: string | null;
|
||||
createdDate: Date;
|
||||
};
|
||||
|
||||
export type TeamInfoType = {
|
||||
name: string | undefined | null;
|
||||
};
|
||||
|
||||
export type WebUserInfoType = UserInfo & {
|
||||
/** All users are PRO now */
|
||||
plan?: "PRO";
|
||||
};
|
||||
|
||||
export type ConsoleUserInfoType = UserInfo & {
|
||||
plan: "CLOUD" | "SELFHOSTED"; // DeploymentType;
|
||||
};
|
||||
|
||||
export interface IUserDeletion<T> {
|
||||
delete(info: T): Promise<WebUserInfoType>;
|
||||
}
|
||||
|
||||
export interface IUserCreation<T> {
|
||||
create(info: T): Promise<WebUserInfoType>;
|
||||
update(info: T): Promise<WebUserInfoType>;
|
||||
upsert?: never;
|
||||
}
|
||||
|
||||
export interface IUserUpsertion<T> {
|
||||
create?: never;
|
||||
update?: never;
|
||||
upsert(info: T): Promise<WebUserInfoType>;
|
||||
}
|
||||
|
||||
export interface ISyncService {
|
||||
ready(): boolean;
|
||||
web: {
|
||||
user: (IUserCreation<WebUserInfoType> | IUserUpsertion<WebUserInfoType>) & IUserDeletion<WebUserInfoType>;
|
||||
};
|
||||
console: {
|
||||
user: IUserCreation<ConsoleUserInfoType> | IUserUpsertion<ConsoleUserInfoType>;
|
||||
};
|
||||
}
|
||||
|
||||
export default class SyncServiceCore {
|
||||
protected serviceName: string;
|
||||
protected service: unknown;
|
||||
protected log: typeof logger;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
constructor(serviceName: string, service: any, log: typeof logger) {
|
||||
this.serviceName = serviceName;
|
||||
this.log = log;
|
||||
try {
|
||||
this.service = new service();
|
||||
} catch (e) {
|
||||
this.log.warn("Couldn't instantiate sync service:", (e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
ready() {
|
||||
return this.service !== undefined;
|
||||
}
|
||||
|
||||
async getUserLastBooking(user: { email: string }): Promise<{ booking: { createdAt: Date } | null } | null> {
|
||||
return await webPrisma.attendee.findFirst({
|
||||
where: {
|
||||
email: user.email,
|
||||
},
|
||||
select: {
|
||||
booking: {
|
||||
select: {
|
||||
createdAt: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
booking: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export interface ISyncServices {
|
||||
new (): ISyncService;
|
||||
}
|
||||
168
calcom/packages/lib/sync/SyncServiceManager.ts
Normal file
168
calcom/packages/lib/sync/SyncServiceManager.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import logger from "@calcom/lib/logger";
|
||||
import type { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { safeStringify } from "../safeStringify";
|
||||
import type { ConsoleUserInfoType, TeamInfoType, WebUserInfoType } from "./ISyncService";
|
||||
import services from "./services";
|
||||
import CloseComService from "./services/CloseComService";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: [`[[SyncServiceManager] `] });
|
||||
|
||||
export const createConsoleUser = async (user: ConsoleUserInfoType | null | undefined) => {
|
||||
if (user) {
|
||||
log.debug("createConsoleUser", safeStringify({ user }));
|
||||
try {
|
||||
Promise.all(
|
||||
services.map(async (serviceClass) => {
|
||||
const service = new serviceClass();
|
||||
if (service.ready()) {
|
||||
if (service.console.user.upsert) {
|
||||
await service.console.user.upsert(user);
|
||||
} else {
|
||||
await service.console.user.create(user);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
log.warn("createConsoleUser", safeStringify({ error: e }));
|
||||
}
|
||||
} else {
|
||||
log.warn("createConsoleUser:noUser", safeStringify({ user }));
|
||||
}
|
||||
};
|
||||
|
||||
export const createWebUser = async (user: WebUserInfoType | null | undefined) => {
|
||||
if (user) {
|
||||
log.debug("createWebUser", safeStringify({ user }));
|
||||
try {
|
||||
Promise.all(
|
||||
services.map(async (serviceClass) => {
|
||||
const service = new serviceClass();
|
||||
if (service.ready()) {
|
||||
if (service.web.user.upsert) {
|
||||
await service.web.user.upsert(user);
|
||||
} else {
|
||||
await service.web.user.create(user);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
log.warn("createWebUser", safeStringify({ error: e }));
|
||||
}
|
||||
} else {
|
||||
log.warn("createWebUser:noUser", safeStringify({ user }));
|
||||
}
|
||||
};
|
||||
|
||||
export const updateWebUser = async (user: WebUserInfoType | null | undefined) => {
|
||||
if (user) {
|
||||
log.debug("updateWebUser", safeStringify({ user }));
|
||||
try {
|
||||
Promise.all(
|
||||
services.map(async (serviceClass) => {
|
||||
const service = new serviceClass();
|
||||
if (service.ready()) {
|
||||
if (service.web.user.upsert) {
|
||||
await service.web.user.upsert(user);
|
||||
} else {
|
||||
await service.web.user.update(user);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
log.warn("updateWebUser", safeStringify({ error: e }));
|
||||
}
|
||||
} else {
|
||||
log.warn("updateWebUser:noUser", safeStringify({ user }));
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteWebUser = async (user: WebUserInfoType | null | undefined) => {
|
||||
if (user) {
|
||||
log.debug("deleteWebUser", { user });
|
||||
try {
|
||||
Promise.all(
|
||||
services.map(async (serviceClass) => {
|
||||
const service = new serviceClass();
|
||||
if (service.ready()) {
|
||||
await service.web.user.delete(user);
|
||||
}
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
log.warn("deleteWebUser", e);
|
||||
}
|
||||
} else {
|
||||
log.warn("deleteWebUser:noUser", { user });
|
||||
}
|
||||
};
|
||||
|
||||
export const closeComUpsertTeamUser = async (
|
||||
team: TeamInfoType,
|
||||
user: WebUserInfoType | null | undefined,
|
||||
role: MembershipRole
|
||||
) => {
|
||||
if (user && team && role) {
|
||||
log.debug("closeComUpsertTeamUser", { team, user, role });
|
||||
try {
|
||||
const closeComService = new CloseComService();
|
||||
if (closeComService.ready()) {
|
||||
await closeComService.web.team.create(team, user, role);
|
||||
}
|
||||
} catch (e) {
|
||||
log.warn("closeComUpsertTeamUser", e);
|
||||
}
|
||||
} else {
|
||||
log.warn("closeComUpsertTeamUser:noTeamOrUserOrRole", { team, user, role });
|
||||
}
|
||||
};
|
||||
|
||||
export const closeComDeleteTeam = async (team: TeamInfoType) => {
|
||||
if (team) {
|
||||
log.debug("closeComDeleteTeamUser", { team });
|
||||
try {
|
||||
const closeComService = new CloseComService();
|
||||
if (closeComService.ready()) {
|
||||
await closeComService.web.team.delete(team);
|
||||
}
|
||||
} catch (e) {
|
||||
log.warn("closeComDeleteTeamUser", e);
|
||||
}
|
||||
} else {
|
||||
log.warn("closeComDeleteTeamUser:noTeam");
|
||||
}
|
||||
};
|
||||
|
||||
export const closeComDeleteTeamMembership = async (user: WebUserInfoType | null | undefined) => {
|
||||
if (user) {
|
||||
log.debug("closeComDeleteTeamMembership", { user });
|
||||
try {
|
||||
const closeComService = new CloseComService();
|
||||
if (closeComService.ready()) {
|
||||
await closeComService.web.membership.delete(user);
|
||||
}
|
||||
} catch (e) {
|
||||
log.warn("closeComDeleteTeamMembership", e);
|
||||
}
|
||||
} else {
|
||||
log.warn("closeComDeleteTeamMembership:noUser");
|
||||
}
|
||||
};
|
||||
|
||||
export const closeComUpdateTeam = async (prevTeam: TeamInfoType, updatedTeam: TeamInfoType) => {
|
||||
if (prevTeam && updatedTeam) {
|
||||
try {
|
||||
const closeComService = new CloseComService();
|
||||
if (closeComService.ready()) {
|
||||
await closeComService.web.team.update(prevTeam, updatedTeam);
|
||||
}
|
||||
} catch (e) {
|
||||
log.warn("closeComUpdateTeam", e);
|
||||
}
|
||||
} else {
|
||||
log.warn("closeComUpdateTeam:noPrevTeamOrUpdatedTeam");
|
||||
}
|
||||
};
|
||||
143
calcom/packages/lib/sync/services/CloseComService.ts
Normal file
143
calcom/packages/lib/sync/services/CloseComService.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import type { CloseComFieldOptions, CloseComLead } from "@calcom/lib/CloseCom";
|
||||
import CloseCom from "@calcom/lib/CloseCom";
|
||||
import { getCloseComContactIds, getCloseComLeadId, getCustomFieldsIds } from "@calcom/lib/CloseComeUtils";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import type { TeamInfoType } from "@calcom/lib/sync/ISyncService";
|
||||
import SyncServiceCore from "@calcom/lib/sync/ISyncService";
|
||||
import type { ConsoleUserInfoType, WebUserInfoType } from "@calcom/lib/sync/ISyncService";
|
||||
import type ISyncService from "@calcom/lib/sync/ISyncService";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
// Cal.com Custom Contact Fields
|
||||
const calComCustomContactFields: CloseComFieldOptions = [
|
||||
// Field name, field type, required?, multiple values?
|
||||
["Username", "text", false, false],
|
||||
["Plan", "text", true, false],
|
||||
["Last booking", "date", false, false],
|
||||
["Created at", "date", true, false],
|
||||
];
|
||||
|
||||
const calComSharedFields: CloseComFieldOptions = [["Contact Role", "text", false, false]];
|
||||
|
||||
const serviceName = "closecom_service";
|
||||
|
||||
export default class CloseComService extends SyncServiceCore implements ISyncService {
|
||||
protected declare service: CloseCom;
|
||||
|
||||
constructor() {
|
||||
super(serviceName, CloseCom, logger.getSubLogger({ prefix: [`[[sync] ${serviceName}`] }));
|
||||
}
|
||||
|
||||
upsertAnyUser = async (
|
||||
user: WebUserInfoType | ConsoleUserInfoType,
|
||||
leadInfo?: CloseComLead,
|
||||
role?: string
|
||||
) => {
|
||||
this.log.debug("sync:closecom:user", { user });
|
||||
// Get Cal.com Lead
|
||||
const leadId = await getCloseComLeadId(this.service, leadInfo);
|
||||
this.log.debug("sync:closecom:user:leadId", { leadId });
|
||||
// Get Contacts ids: already creates contacts
|
||||
const [contactId] = await getCloseComContactIds([user], this.service, leadId);
|
||||
this.log.debug("sync:closecom:user:contactsIds", { contactId });
|
||||
// Get Custom Contact fields ids
|
||||
const customFieldsIds = await getCustomFieldsIds("contact", calComCustomContactFields, this.service);
|
||||
this.log.debug("sync:closecom:user:customFieldsIds", { customFieldsIds });
|
||||
// Get shared fields ids
|
||||
const sharedFieldsIds = await getCustomFieldsIds("shared", calComSharedFields, this.service);
|
||||
this.log.debug("sync:closecom:user:sharedFieldsIds", { sharedFieldsIds });
|
||||
const allFields = customFieldsIds.concat(sharedFieldsIds);
|
||||
this.log.debug("sync:closecom:user:allFields", { allFields });
|
||||
const lastBooking = "email" in user ? await this.getUserLastBooking(user) : null;
|
||||
this.log.debug("sync:closecom:user:lastBooking", { lastBooking });
|
||||
const username = "username" in user ? user.username : null;
|
||||
// Prepare values for each Custom Contact Fields
|
||||
const allFieldsValues = [
|
||||
username, // Username
|
||||
user.plan, // Plan
|
||||
lastBooking && lastBooking.booking
|
||||
? new Date(lastBooking.booking.createdAt).toLocaleDateString("en-US")
|
||||
: null, // Last Booking
|
||||
user.createdDate,
|
||||
role === MembershipRole.OWNER ? "Point of Contact" : "",
|
||||
];
|
||||
this.log.debug("sync:closecom:contact:allFieldsValues", { allFieldsValues });
|
||||
// Preparing Custom Activity Instance data for Close.com
|
||||
const person = Object.assign(
|
||||
{},
|
||||
{
|
||||
person: user,
|
||||
lead_id: leadId,
|
||||
contactId,
|
||||
},
|
||||
...allFields.map((fieldId: string, index: number) => {
|
||||
return {
|
||||
[`custom.${fieldId}`]: allFieldsValues[index],
|
||||
};
|
||||
})
|
||||
);
|
||||
// Create Custom Activity type instance
|
||||
return await this.service.contact.update(person);
|
||||
};
|
||||
|
||||
public console = {
|
||||
user: {
|
||||
upsert: async (consoleUser: ConsoleUserInfoType) => {
|
||||
return this.upsertAnyUser(consoleUser);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
public web = {
|
||||
user: {
|
||||
upsert: async (webUser: WebUserInfoType) => {
|
||||
return this.upsertAnyUser(webUser);
|
||||
},
|
||||
delete: async (webUser: WebUserInfoType) => {
|
||||
this.log.debug("sync:closecom:web:user:delete", { webUser });
|
||||
const [contactId] = await getCloseComContactIds([webUser], this.service);
|
||||
this.log.debug("sync:closecom:web:user:delete:contactId", { contactId });
|
||||
if (contactId) {
|
||||
return this.service.contact.delete(contactId);
|
||||
} else {
|
||||
throw Error("Web user not found in service");
|
||||
}
|
||||
},
|
||||
},
|
||||
team: {
|
||||
create: async (team: TeamInfoType, webUser: WebUserInfoType, role: MembershipRole) => {
|
||||
return this.upsertAnyUser(
|
||||
webUser,
|
||||
{
|
||||
companyName: team.name,
|
||||
},
|
||||
role
|
||||
);
|
||||
},
|
||||
delete: async (team: TeamInfoType) => {
|
||||
this.log.debug("sync:closecom:web:team:delete", { team });
|
||||
const leadId = await getCloseComLeadId(this.service, { companyName: team.name });
|
||||
this.log.debug("sync:closecom:web:team:delete:leadId", { leadId });
|
||||
this.service.lead.delete(leadId);
|
||||
},
|
||||
update: async (prevTeam: TeamInfoType, updatedTeam: TeamInfoType) => {
|
||||
this.log.debug("sync:closecom:web:team:update", { prevTeam, updatedTeam });
|
||||
const leadId = await getCloseComLeadId(this.service, { companyName: prevTeam.name });
|
||||
this.log.debug("sync:closecom:web:team:update:leadId", { leadId });
|
||||
this.service.lead.update(leadId, { companyName: updatedTeam.name });
|
||||
},
|
||||
},
|
||||
membership: {
|
||||
delete: async (webUser: WebUserInfoType) => {
|
||||
this.log.debug("sync:closecom:web:membership:delete", { webUser });
|
||||
const [contactId] = await getCloseComContactIds([webUser], this.service);
|
||||
this.log.debug("sync:closecom:web:membership:delete:contactId", { contactId });
|
||||
if (contactId) {
|
||||
return this.service.contact.delete(contactId);
|
||||
} else {
|
||||
throw Error("Web user not found in service");
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
101
calcom/packages/lib/sync/services/SendgridService.ts
Normal file
101
calcom/packages/lib/sync/services/SendgridService.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import logger from "@calcom/lib/logger";
|
||||
|
||||
import type { SendgridFieldOptions, SendgridNewContact } from "../../Sendgrid";
|
||||
import Sendgrid from "../../Sendgrid";
|
||||
import type { ConsoleUserInfoType, WebUserInfoType } from "../ISyncService";
|
||||
import type ISyncService from "../ISyncService";
|
||||
import SyncServiceCore from "../ISyncService";
|
||||
|
||||
// Cal.com Custom Contact Fields
|
||||
const calComCustomContactFields: SendgridFieldOptions = [
|
||||
// Field name, field type
|
||||
["username", "Text"],
|
||||
["plan", "Text"],
|
||||
["last_booking", "Date"], // Sendgrid custom fields only allow alphanumeric characters (letters A-Z, numbers 0-9) and underscores.
|
||||
["createdAt", "Date"],
|
||||
];
|
||||
|
||||
const serviceName = "sendgrid_service";
|
||||
|
||||
export default class SendgridService extends SyncServiceCore implements ISyncService {
|
||||
protected declare service: Sendgrid;
|
||||
constructor() {
|
||||
super(serviceName, Sendgrid, logger.getSubLogger({ prefix: [`[[sync] ${serviceName}`] }));
|
||||
}
|
||||
|
||||
upsert = async (user: WebUserInfoType | ConsoleUserInfoType) => {
|
||||
this.log.debug("sync:sendgrid:user", user);
|
||||
// Get Custom Contact fields ids
|
||||
const customFieldsIds = await this.service.getSendgridCustomFieldsIds(calComCustomContactFields);
|
||||
this.log.debug("sync:sendgrid:user:customFieldsIds", customFieldsIds);
|
||||
const lastBooking = "email" in user ? await this.getUserLastBooking(user) : null;
|
||||
this.log.debug("sync:sendgrid:user:lastBooking", lastBooking);
|
||||
const username = "username" in user ? user.username : null;
|
||||
// Prepare values for each Custom Contact Fields
|
||||
const customContactFieldsValues = [
|
||||
username, // Username
|
||||
user.plan, // Plan
|
||||
lastBooking && lastBooking.booking
|
||||
? new Date(lastBooking.booking.createdAt).toLocaleDateString("en-US")
|
||||
: null, // Last Booking
|
||||
user.createdDate,
|
||||
];
|
||||
this.log.debug("sync:sendgrid:contact:customContactFieldsValues", customContactFieldsValues);
|
||||
// Preparing Custom Activity Instance data for Sendgrid
|
||||
const contactData = {
|
||||
first_name: user.name,
|
||||
email: user.email,
|
||||
custom_fields: Object.assign(
|
||||
{},
|
||||
...customFieldsIds.map((fieldId: string, index: number) => {
|
||||
if (customContactFieldsValues[index] !== null) {
|
||||
return {
|
||||
[fieldId]: customContactFieldsValues[index],
|
||||
};
|
||||
}
|
||||
})
|
||||
),
|
||||
};
|
||||
this.log.debug("sync:sendgrid:contact:contactData", contactData);
|
||||
const newContact = await this.service.sendgridRequest<SendgridNewContact>({
|
||||
url: `/v3/marketing/contacts`,
|
||||
method: "PUT",
|
||||
body: {
|
||||
contacts: [contactData],
|
||||
},
|
||||
});
|
||||
// Create contact
|
||||
this.log.debug("sync:sendgrid:contact:newContact", newContact);
|
||||
return newContact;
|
||||
};
|
||||
|
||||
public console = {
|
||||
user: {
|
||||
upsert: async (consoleUser: ConsoleUserInfoType) => {
|
||||
return this.upsert(consoleUser);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
public web = {
|
||||
user: {
|
||||
upsert: async (webUser: WebUserInfoType) => {
|
||||
return this.upsert(webUser);
|
||||
},
|
||||
delete: async (webUser: WebUserInfoType) => {
|
||||
const [contactId] = await this.service.getSendgridContactId(webUser.email);
|
||||
if (contactId) {
|
||||
return this.service.sendgridRequest({
|
||||
url: `/v3/marketing/contacts`,
|
||||
method: "DELETE",
|
||||
qs: {
|
||||
ids: contactId.id,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
throw Error("Web user not found in service");
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
10
calcom/packages/lib/sync/services/index.ts
Normal file
10
calcom/packages/lib/sync/services/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { ISyncServices } from "../ISyncService";
|
||||
import SendgridService from "./SendgridService";
|
||||
|
||||
const services: ISyncServices[] = [
|
||||
//CloseComService, This service gets a special treatment after deciding it shouldn't get the same treatment as Sendgrid
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
SendgridService as any,
|
||||
];
|
||||
|
||||
export default services;
|
||||
Reference in New Issue
Block a user