Skip to content
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
4 changes: 2 additions & 2 deletions packages/app-store/_utils/getCalendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import appStore from "..";

const log = logger.getChildLogger({ prefix: ["CalendarManager"] });

export const getCalendar = (credential: CredentialPayload | null): Calendar | null => {
export const getCalendar = async (credential: CredentialPayload | null): Promise<Calendar | null> => {
if (!credential || !credential.key) return null;
let { type: calendarType } = credential;
if (calendarType?.endsWith("_other_calendar")) {
calendarType = calendarType.split("_other_calendar")[0];
}
const calendarApp = appStore[calendarType.split("_").join("") as keyof typeof appStore];
const calendarApp = await appStore[calendarType.split("_").join("") as keyof typeof appStore];
if (!(calendarApp && "lib" in calendarApp && "CalendarService" in calendarApp.lib)) {
log.warn(`calendar of type ${calendarType} is not implemented`);
return null;
Expand Down
88 changes: 29 additions & 59 deletions packages/app-store/index.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,33 @@
// import * as example from "./_example";
import * as applecalendar from "./applecalendar";
import * as caldavcalendar from "./caldavcalendar";
import * as closecom from "./closecom";
import * as dailyvideo from "./dailyvideo";
import * as exchange2013calendar from "./exchange2013calendar";
import * as exchange2016calendar from "./exchange2016calendar";
import * as exchangecalendar from "./exchangecalendar";
import * as facetime from "./facetime";
import * as giphy from "./giphy";
import * as googlecalendar from "./googlecalendar";
import * as googlevideo from "./googlevideo";
import * as hubspot from "./hubspot";
import * as huddle01video from "./huddle01video";
import * as jitsivideo from "./jitsivideo";
import * as larkcalendar from "./larkcalendar";
import * as office365calendar from "./office365calendar";
import * as office365video from "./office365video";
import * as plausible from "./plausible";
import * as salesforce from "./salesforce";
import * as sendgrid from "./sendgrid";
import * as stripepayment from "./stripepayment";
import * as sylapsvideo from "./sylapsvideo";
import * as tandemvideo from "./tandemvideo";
import * as vital from "./vital";
import * as wipemycalother from "./wipemycalother";
import * as zapier from "./zapier";
import * as zohocrm from "./zohocrm";
import * as zoomvideo from "./zoomvideo";

const appStore = {
// example,
applecalendar,
caldavcalendar,
closecom,
dailyvideo,
googlecalendar,
googlevideo,
hubspot,
huddle01video,
jitsivideo,
sylapsvideo,
larkcalendar,
office365calendar,
office365video,
plausible,
salesforce,
zohocrm,
sendgrid,
stripepayment,
tandemvideo,
vital,
zoomvideo,
wipemycalother,
giphy,
zapier,
exchange2013calendar,
exchange2016calendar,
exchangecalendar,
facetime,
// example: import("./example"),
applecalendar: import("./applecalendar"),
caldavcalendar: import("./caldavcalendar"),
closecom: import("./closecom"),
dailyvideo: import("./dailyvideo"),
googlecalendar: import("./googlecalendar"),
googlevideo: import("./googlevideo"),
hubspot: import("./hubspot"),
huddle01video: import("./huddle01video"),
jitsivideo: import("./jitsivideo"),
larkcalendar: import("./larkcalendar"),
office365calendar: import("./office365calendar"),
office365video: import("./office365video"),
plausible: import("./plausible"),
salesforce: import("./salesforce"),
zohocrm: import("./zohocrm"),
sendgrid: import("./sendgrid"),
stripepayment: import("./stripepayment"),
tandemvideo: import("./tandemvideo"),
vital: import("./vital"),
zoomvideo: import("./zoomvideo"),
wipemycalother: import("./wipemycalother"),
giphy: import("./giphy"),
zapier: import("./zapier"),
exchange2013calendar: import("./exchange2013calendar"),
exchange2016calendar: import("./exchange2016calendar"),
exchangecalendar: import("./exchangecalendar"),
facetime: import("./facetime"),
sylapsvideo: import("./sylapsvideo"),
};

export default appStore;
4 changes: 2 additions & 2 deletions packages/app-store/vital/lib/reschedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,10 @@ const Reschedule = async (bookingUid: string, cancellationReason: string) => {
(ref) => !!credentialsMap.get(ref.type)
);
try {
bookingRefsFiltered.forEach((bookingRef) => {
bookingRefsFiltered.forEach(async (bookingRef) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ High: Async forEach is not awaited, so calendar/video deletions run in the background and errors won't be caught by the surrounding try/catch. This can leave orphaned events or miss deletions under load.

Suggested change
bookingRefsFiltered.forEach(async (bookingRef) => {
```suggestion
const tasks = bookingRefsFiltered.map(async (bookingRef) => {
if (!bookingRef.uid) return;
if (bookingRef.type.endsWith("_calendar")) {
const calendar = await getCalendar(credentialsMap.get(bookingRef.type));
return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent);
}
if (bookingRef.type.endsWith("_video")) {
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
}
});
await Promise.all(tasks);

if (bookingRef.uid) {
if (bookingRef.type.endsWith("_calendar")) {
const calendar = getCalendar(credentialsMap.get(bookingRef.type));
const calendar = await getCalendar(credentialsMap.get(bookingRef.type));
return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent);
} else if (bookingRef.type.endsWith("_video")) {
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
Expand Down
4 changes: 2 additions & 2 deletions packages/app-store/wipemycalother/lib/reschedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,10 @@ const Reschedule = async (bookingUid: string, cancellationReason: string) => {
(ref) => !!credentialsMap.get(ref.type)
);
try {
bookingRefsFiltered.forEach((bookingRef) => {
bookingRefsFiltered.forEach(async (bookingRef) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ High: Same issue: async forEach is not awaited, so deleteEvent/deleteMeeting operations can be dropped or finish after the handler completes, and exceptions won't be caught.

Suggested change
bookingRefsFiltered.forEach(async (bookingRef) => {
```suggestion
const tasks = bookingRefsFiltered.map(async (bookingRef) => {
if (!bookingRef.uid) return;
if (bookingRef.type.endsWith("_calendar")) {
const calendar = await getCalendar(credentialsMap.get(bookingRef.type));
return calendar?.deleteEvent(
bookingRef.uid,
builder.calendarEvent
);
}
if (bookingRef.type.endsWith("_video")) {
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
}
});
await Promise.all(tasks);

if (bookingRef.uid) {
if (bookingRef.type.endsWith("_calendar")) {
const calendar = getCalendar(credentialsMap.get(bookingRef.type));
const calendar = await getCalendar(credentialsMap.get(bookingRef.type));
return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent);
} else if (bookingRef.type.endsWith("_video")) {
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
Expand Down
15 changes: 8 additions & 7 deletions packages/core/CalendarManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const getCalendarCredentials = (credentials: Array<CredentialPayload>) =>
const calendar = getCalendar(credential);
return app.variant === "calendar" ? [{ integration: app, credential, calendar }] : [];
});

return credentials.length ? credentials : [];
});

Expand All @@ -43,8 +44,8 @@ export const getConnectedCalendars = async (
const connectedCalendars = await Promise.all(
calendarCredentials.map(async (item) => {
try {
const { calendar, integration, credential } = item;

const { integration, credential } = item;
const calendar = await item.calendar;
// Don't leak credentials to the client
const credentialId = credential.id;
if (!calendar) {
Expand Down Expand Up @@ -138,7 +139,7 @@ export const getCachedResults = async (
selectedCalendars: SelectedCalendar[]
): Promise<EventBusyDate[][]> => {
const calendarCredentials = withCredentials.filter((credential) => credential.type.endsWith("_calendar"));
const calendars = calendarCredentials.map((credential) => getCalendar(credential));
const calendars = await Promise.all(calendarCredentials.map((credential) => getCalendar(credential)));
performance.mark("getBusyCalendarTimesStart");
const results = calendars.map(async (c, i) => {
/** Filter out nulls */
Expand Down Expand Up @@ -229,7 +230,7 @@ export const createEvent = async (
calEvent: CalendarEvent
): Promise<EventResult<NewCalendarEventType>> => {
const uid: string = getUid(calEvent);
const calendar = getCalendar(credential);
const calendar = await getCalendar(credential);
let success = true;
let calError: string | undefined = undefined;

Expand Down Expand Up @@ -280,7 +281,7 @@ export const updateEvent = async (
externalCalendarId: string | null
): Promise<EventResult<NewCalendarEventType>> => {
const uid = getUid(calEvent);
const calendar = getCalendar(credential);
const calendar = await getCalendar(credential);
let success = false;
let calError: string | undefined = undefined;
let calWarnings: string[] | undefined = [];
Expand Down Expand Up @@ -326,12 +327,12 @@ export const updateEvent = async (
};
};

export const deleteEvent = (
export const deleteEvent = async (
credential: CredentialPayload,
uid: string,
event: CalendarEvent
): Promise<unknown> => {
const calendar = getCalendar(credential);
const calendar = await getCalendar(credential);
if (calendar) {
return calendar.deleteEvent(uid, event);
}
Expand Down
3 changes: 1 addition & 2 deletions packages/core/EventManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,8 +485,7 @@ export default class EventManager {
id: oldCalendarEvent.credentialId,
},
});
const calendar = getCalendar(calendarCredential);

const calendar = await getCalendar(calendarCredential);
await calendar?.deleteEvent(oldCalendarEvent.uid, event, oldCalendarEvent.externalCalendarId);
}
}
Expand Down
36 changes: 20 additions & 16 deletions packages/core/videoClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,25 @@ const log = logger.getChildLogger({ prefix: ["[lib] videoClient"] });
const translator = short();

// factory
const getVideoAdapters = (withCredentials: CredentialPayload[]): VideoApiAdapter[] =>
withCredentials.reduce<VideoApiAdapter[]>((acc, cred) => {
const getVideoAdapters = async (withCredentials: CredentialPayload[]): Promise<VideoApiAdapter[]> => {
const videoAdapters: VideoApiAdapter[] = [];

for (const cred of withCredentials) {
const appName = cred.type.split("_").join(""); // Transform `zoom_video` to `zoomvideo`;
const app = appStore[appName as keyof typeof appStore];
const app = await appStore[appName as keyof typeof appStore];

if (app && "lib" in app && "VideoApiAdapter" in app.lib) {
const makeVideoApiAdapter = app.lib.VideoApiAdapter as VideoApiAdapterFactory;
const videoAdapter = makeVideoApiAdapter(cred);
acc.push(videoAdapter);
return acc;
videoAdapters.push(videoAdapter);
}
return acc;
}, []);
}

return videoAdapters;
};

const getBusyVideoTimes = (withCredentials: CredentialPayload[]) =>
Promise.all(getVideoAdapters(withCredentials).map((c) => c?.getAvailability())).then((results) =>
const getBusyVideoTimes = async (withCredentials: CredentialPayload[]) =>
Promise.all((await getVideoAdapters(withCredentials)).map((c) => c?.getAvailability())).then((results) =>
results.reduce((acc, availability) => acc.concat(availability), [] as (EventBusyDate | undefined)[])
);

Expand All @@ -45,7 +49,7 @@ const createMeeting = async (credential: CredentialWithAppName, calEvent: Calend
);
}

const videoAdapters = getVideoAdapters([credential]);
const videoAdapters = await getVideoAdapters([credential]);
const [firstVideoAdapter] = videoAdapters;
let createdMeeting;
let returnObject: {
Expand Down Expand Up @@ -104,7 +108,7 @@ const updateMeeting = async (

let success = true;

const [firstVideoAdapter] = getVideoAdapters([credential]);
const [firstVideoAdapter] = await getVideoAdapters([credential]);
const updatedMeeting =
credential && bookingRef
? await firstVideoAdapter?.updateMeeting(bookingRef, calEvent).catch(async (e) => {
Expand Down Expand Up @@ -135,9 +139,9 @@ const updateMeeting = async (
};
};

const deleteMeeting = (credential: CredentialPayload, uid: string): Promise<unknown> => {
const deleteMeeting = async (credential: CredentialPayload, uid: string): Promise<unknown> => {
if (credential) {
const videoAdapter = getVideoAdapters([credential])[0];
const videoAdapter = (await getVideoAdapters([credential]))[0];
// There are certain video apps with no video adapter defined. e.g. riverby,whereby
if (videoAdapter) {
return videoAdapter.deleteMeeting(uid);
Expand All @@ -155,7 +159,7 @@ const createMeetingWithCalVideo = async (calEvent: CalendarEvent) => {
} catch (e) {
return;
}
const [videoAdapter] = getVideoAdapters([
const [videoAdapter] = await getVideoAdapters([
{
id: 0,
appId: "daily-video",
Expand All @@ -178,7 +182,7 @@ const getRecordingsOfCalVideoByRoomName = async (
console.error("Error: Cal video provider is not installed.");
return;
}
const [videoAdapter] = getVideoAdapters([
const [videoAdapter] = await getVideoAdapters([
{
id: 0,
appId: "daily-video",
Expand All @@ -199,7 +203,7 @@ const getDownloadLinkOfCalVideoByRecordingId = async (recordingId: string) => {
console.error("Error: Cal video provider is not installed.");
return;
}
const [videoAdapter] = getVideoAdapters([
const [videoAdapter] = await getVideoAdapters([
{
id: 0,
appId: "daily-video",
Expand Down
23 changes: 12 additions & 11 deletions packages/features/bookings/lib/handleCancelBooking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ async function handler(req: CustomRequest) {
integrationsToDelete.push(deleteMeeting(credential, reference.uid));
}
if (reference.type.includes("_calendar")) {
const calendar = getCalendar(credential);
const calendar = await getCalendar(credential);
if (calendar) {
integrationsToDelete.push(
calendar?.deleteEvent(reference.uid, evt, reference.externalCalendarId)
Expand All @@ -262,7 +262,7 @@ async function handler(req: CustomRequest) {
);
}
if (reference.type.includes("_calendar")) {
const calendar = getCalendar(credential);
const calendar = await getCalendar(credential);
if (calendar) {
integrationsToDelete.push(
calendar?.updateEvent(reference.uid, updatedEvt, reference.externalCalendarId)
Expand Down Expand Up @@ -449,7 +449,7 @@ async function handler(req: CustomRequest) {
(credential) => credential.id === credentialId
);
if (calendarCredential) {
const calendar = getCalendar(calendarCredential);
const calendar = await getCalendar(calendarCredential);
if (
bookingToDelete.eventType?.recurringEvent &&
bookingToDelete.recurringEventId &&
Expand All @@ -458,7 +458,7 @@ async function handler(req: CustomRequest) {
bookingToDelete.user.credentials
.filter((credential) => credential.type.endsWith("_calendar"))
.forEach(async (credential) => {
const calendar = getCalendar(credential);
const calendar = await getCalendar(credential);
for (const updBooking of updatedBookings) {
const bookingRef = updBooking.references.find((ref) => ref.type.includes("_calendar"));
if (bookingRef) {
Expand All @@ -474,12 +474,13 @@ async function handler(req: CustomRequest) {
}
} else {
// For bookings made before the refactor we go through the old behaviour of running through each calendar credential
bookingToDelete.user.credentials
.filter((credential) => credential.type.endsWith("_calendar"))
.forEach((credential) => {
const calendar = getCalendar(credential);
apiDeletes.push(calendar?.deleteEvent(uid, evt, externalCalendarId) as Promise<unknown>);
});
const calendarCredentials = bookingToDelete.user.credentials.filter((credential) =>
credential.type.endsWith("_calendar")
);
for (const credential of calendarCredentials) {
const calendar = await getCalendar(credential);
apiDeletes.push(calendar?.deleteEvent(uid, evt, externalCalendarId) as Promise<unknown>);
}
}
}

Expand Down Expand Up @@ -585,7 +586,7 @@ async function handler(req: CustomRequest) {
}

// Posible to refactor TODO:
const paymentApp = appStore[paymentAppCredential?.app?.dirName as keyof typeof appStore];
const paymentApp = await appStore[paymentAppCredential?.app?.dirName as keyof typeof appStore];
if (!(paymentApp && "lib" in paymentApp && "PaymentService" in paymentApp.lib)) {
console.warn(`payment App service of type ${paymentApp} is not implemented`);
return null;
Expand Down
2 changes: 1 addition & 1 deletion packages/features/bookings/lib/handleNewBooking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -885,7 +885,7 @@ async function handler(
integrationsToDelete.push(deleteMeeting(credential, reference.uid));
}
if (reference.type.includes("_calendar") && originalBookingEvt) {
const calendar = getCalendar(credential);
const calendar = await getCalendar(credential);
if (calendar) {
integrationsToDelete.push(
calendar?.deleteEvent(reference.uid, originalBookingEvt, reference.externalCalendarId)
Expand Down
2 changes: 1 addition & 1 deletion packages/lib/payment/deletePayment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const deletePayment = async (
} | null;
}
): Promise<boolean> => {
const paymentApp = appStore[paymentAppCredentials?.app?.dirName as keyof typeof appStore];
const paymentApp = await appStore[paymentAppCredentials?.app?.dirName as keyof typeof appStore];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ High: If the async appStore loader rejects (missing package, load error), this await will throw and bypass your existing guard, causing an unhandled error in a critical payment flow. Wrap the load in try/catch and return false to preserve the function's established error-handling model.

Suggested change
const paymentApp = await appStore[paymentAppCredentials?.app?.dirName as keyof typeof appStore];
```suggestion
let paymentApp;
try {
paymentApp = await appStore[paymentAppCredentials?.app?.dirName as keyof typeof appStore];
} catch (err) {
console.warn(`Failed to load payment app: ${paymentAppCredentials?.app?.dirName}`, err);
return false;
}

if (!(paymentApp && "lib" in paymentApp && "PaymentService" in paymentApp.lib)) {
console.warn(`payment App service of type ${paymentApp} is not implemented`);
return false;
Expand Down
2 changes: 1 addition & 1 deletion packages/lib/payment/handlePayment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const handlePayment = async (
uid: string;
}
) => {
const paymentApp = appStore[paymentAppCredentials?.app?.dirName as keyof typeof appStore];
const paymentApp = await appStore[paymentAppCredentials?.app?.dirName as keyof typeof appStore];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ High: A rejected async appStore load here will throw and skip your not-implemented guard, potentially surfacing as a 500 during booking payments. Catch loader failures and return null to align with the function's current fallback behavior.

Suggested change
const paymentApp = await appStore[paymentAppCredentials?.app?.dirName as keyof typeof appStore];
```suggestion
let paymentApp;
try {
paymentApp = await appStore[paymentAppCredentials?.app?.dirName as keyof typeof appStore];
} catch (err) {
console.warn(`Failed to load payment app: ${paymentAppCredentials?.app?.dirName}`, err);
return null;
}

if (!(paymentApp && "lib" in paymentApp && "PaymentService" in paymentApp.lib)) {
console.warn(`payment App service of type ${paymentApp} is not implemented`);
return null;
Expand Down
Loading