refactor: replace shouldServeCache with mode parameter for calendar cache control (#26539)
* refactor: replace shouldServeCache with mode parameter for calendar cache control Replace the boolean shouldServeCache parameter with a new CalendarFetchMode type that can have values 'slots', 'overlay', and 'booking'. This provides better control over when to serve cache vs relay on calendar providers. - 'slots' mode: Check feature flags and use cache when available (for getting actual calendar availability) - 'overlay' mode: Don't use cache (for overlay calendar availability) - 'booking' mode: Don't use cache (for booking confirmation) - undefined: Same as 'slots' for backwards compatibility The cache decision logic is now centralized in getCalendar.ts based on the mode parameter. Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: update CalendarService.test.ts to use mode parameter Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * refactor: use shared GetAvailabilityParams type across all calendar services - Import GetAvailabilityParams and GetAvailabilityWithTimeZonesParams from @calcom/types/Calendar - Replace inline type definitions with shared types in all calendar service implementations - Update BaseCalendarService, CalendarCacheWrapper, and CalendarTelemetryWrapper - Ensures consistent typing and follows DRY principles Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: add missing IntegrationCalendar import and update mock getAvailability signature - Add IntegrationCalendar back to sendgrid CalendarService imports - Update bookingScenario mock getAvailability to use typed params object - Add listCalendars method to mock Calendar object Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: update test files to use typed params object for getAvailability methods - Update CalendarCacheWrapper.test.ts to call getAvailability/getAvailabilityWithTimeZones with params object - Update getCalendarsEvents.test.ts toHaveBeenCalledWith assertions to expect params object - All 32 tests now pass Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * refactor: consolidate to single GetAvailabilityParams type for both getAvailability methods - Remove GetAvailabilityWithTimeZonesParams, use GetAvailabilityParams for both methods - Add mode parameter to getAvailabilityWithTimeZones calls - Update wrapper classes to pass mode through to underlying calendar - Update test files to include mode in getAvailabilityWithTimeZones calls and assertions Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * refactor: use EventBusyDate with optional timeZone for getAvailabilityWithTimeZones - Add optional timeZone field to EventBusyDate type - Update getAvailabilityWithTimeZones return type to use EventBusyDate[] - Update Google Calendar service and wrapper classes to use the new type Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: type errors and remove unused calendar watching methods - Fix type errors in CalendarCacheWrapper.ts (convert null to undefined for timeZone) - Fix type errors in getCalendarsEvents.ts (ensure timeZone is always present) - Remove unused watchCalendar/unwatchCalendar from Calendar interface - Remove unused startWatchingCalendarsInGoogle/stopWatchingCalendarsInGoogle from GoogleCalendarService - Remove unused imports (uuid, uniqueBy, GOOGLE_WEBHOOK_URL, ONE_MONTH_IN_MS) Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: remove watchCalendar/unwatchCalendar from CalendarTelemetryWrapper Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * refactor: remove verbose JSDoc param comments from wrapper classes Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * refactor: make mode parameter required in getCalendar Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: add required mode parameter to all getCalendar callers Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: provide default mode value in getBusyCalendarTimes Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: add mode parameter to remaining getCalendar callers Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: add mode parameter to vital and wipemycalother reschedule Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: add mode parameter to credential-sync API endpoint Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * refactor: add 'none' mode to CalendarFetchMode and use as default Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * test: add mode parameter to getCalendarsEvents test calls Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * Update packages/app-store/delegationCredential.ts Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Volnei Munhoz
cubic-dev-ai[bot]
Devin AI
parent
64828abad2
commit
a62eb2bdac
@@ -109,7 +109,7 @@ async function handler(req: NextApiRequest) {
|
||||
// ^ Workaround for the select in `create` not working
|
||||
|
||||
if (createCalendarResources) {
|
||||
const calendar = await getCalendar({ ...credential, delegatedTo: null });
|
||||
const calendar = await getCalendar({ ...credential, delegatedTo: null }, "slots");
|
||||
if (!calendar) throw new HttpError({ message: "Calendar missing for credential", statusCode: 500 });
|
||||
const calendars = await calendar.listCalendars();
|
||||
const calendarToCreate = calendars.find((calendar) => calendar.primary) || calendars[0];
|
||||
|
||||
@@ -7,7 +7,7 @@ import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { isTelemetryEnabled } from "@calcom/lib/sentryWrapper";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { Calendar } from "@calcom/types/Calendar";
|
||||
import type { Calendar, CalendarFetchMode } from "@calcom/types/Calendar";
|
||||
import type { CredentialForCalendarService } from "@calcom/types/Credential";
|
||||
|
||||
import { CalendarServiceMap } from "../calendar.services.generated";
|
||||
@@ -16,7 +16,7 @@ const log = logger.getSubLogger({ prefix: ["CalendarManager"] });
|
||||
|
||||
export const getCalendar = async (
|
||||
credential: CredentialForCalendarService | null,
|
||||
shouldServeCache?: boolean
|
||||
mode: CalendarFetchMode = "none"
|
||||
): Promise<Calendar | null> => {
|
||||
if (!credential || !credential.key) return null;
|
||||
let { type: calendarType } = credential;
|
||||
@@ -44,8 +44,14 @@ export const getCalendar = async (
|
||||
log.warn(`calendar of type ${calendarType} is not implemented`);
|
||||
return null;
|
||||
}
|
||||
// if shouldServeCache is not supplied, determine on the fly.
|
||||
if (typeof shouldServeCache === "undefined") {
|
||||
|
||||
// Determine if we should use cache based on mode:
|
||||
// - "slots": Check feature flags and use cache when available (for getting actual calendar availability)
|
||||
// - "overlay": Don't use cache (for overlay calendar availability)
|
||||
// - "booking": Don't use cache (for booking confirmation)
|
||||
// - "none": Don't use cache (for operations that don't use getAvailability, e.g., deleteEvent, listCalendars)
|
||||
let shouldServeCache = false;
|
||||
if (mode === "slots") {
|
||||
const featuresRepository = new FeaturesRepository(prisma);
|
||||
const [isCalendarSubscriptionCacheEnabled, isCalendarSubscriptionCacheEnabledForUser] = await Promise.all(
|
||||
[
|
||||
@@ -62,11 +68,19 @@ export const getCalendar = async (
|
||||
log.debug("Cache feature flag check", {
|
||||
credentialId: credential.id,
|
||||
userId: credential.userId,
|
||||
mode,
|
||||
isCalendarSubscriptionCacheEnabled,
|
||||
isCalendarSubscriptionCacheEnabledForUser,
|
||||
shouldServeCache,
|
||||
});
|
||||
} else {
|
||||
log.debug("Cache disabled for mode", {
|
||||
credentialId: credential.id,
|
||||
userId: credential.userId,
|
||||
mode,
|
||||
});
|
||||
}
|
||||
|
||||
const isCacheSupported = CalendarCacheEventService.isCalendarTypeSupported(calendarType);
|
||||
|
||||
const originalCalendar = new CalendarService(credential as any);
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
Calendar,
|
||||
CalendarEvent,
|
||||
EventBusyDate,
|
||||
GetAvailabilityParams,
|
||||
IntegrationCalendar,
|
||||
NewCalendarEventType,
|
||||
} from "@calcom/types/Calendar";
|
||||
@@ -247,11 +248,7 @@ export default class BasecampCalendarService implements Calendar {
|
||||
return true;
|
||||
};
|
||||
|
||||
async getAvailability(
|
||||
_dateFrom: string,
|
||||
_dateTo: string,
|
||||
_selectedCalendars: IntegrationCalendar[]
|
||||
): Promise<EventBusyDate[]> {
|
||||
async getAvailability(_params: GetAvailabilityParams): Promise<EventBusyDate[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
|
||||
@@ -321,7 +321,7 @@ export async function checkIfSuccessfullyConfiguredInWorkspace({
|
||||
user,
|
||||
});
|
||||
|
||||
const calendar = await getCalendar(credential);
|
||||
const calendar = await getCalendar(credential, "none");
|
||||
|
||||
if (!calendar) {
|
||||
throw new Error("Google Calendar App not found");
|
||||
|
||||
@@ -28,6 +28,7 @@ import type {
|
||||
Calendar,
|
||||
CalendarEvent,
|
||||
EventBusyDate,
|
||||
GetAvailabilityParams,
|
||||
IntegrationCalendar,
|
||||
NewCalendarEventType,
|
||||
} from "@calcom/types/Calendar";
|
||||
@@ -142,7 +143,8 @@ export default class ExchangeCalendarService implements Calendar {
|
||||
}
|
||||
}
|
||||
|
||||
async getAvailability(dateFrom: string, dateTo: string, selectedCalendars: IntegrationCalendar[]) {
|
||||
async getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
|
||||
const { dateFrom, dateTo, selectedCalendars } = params;
|
||||
try {
|
||||
const externalCalendars: IntegrationCalendar[] = await this.listCalendars();
|
||||
const calendarsToGetAppointmentsFrom = [];
|
||||
|
||||
@@ -28,6 +28,7 @@ import type {
|
||||
Calendar,
|
||||
CalendarEvent,
|
||||
EventBusyDate,
|
||||
GetAvailabilityParams,
|
||||
IntegrationCalendar,
|
||||
NewCalendarEventType,
|
||||
} from "@calcom/types/Calendar";
|
||||
@@ -143,11 +144,8 @@ export default class ExchangeCalendarService implements Calendar {
|
||||
}
|
||||
}
|
||||
|
||||
async getAvailability(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[]
|
||||
): Promise<EventBusyDate[]> {
|
||||
async getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
|
||||
const { dateFrom, dateTo, selectedCalendars } = params;
|
||||
try {
|
||||
const externalCalendars = await this.listCalendars();
|
||||
const calendarsToGetAppointmentsFrom = [];
|
||||
|
||||
@@ -32,6 +32,7 @@ import type {
|
||||
Calendar,
|
||||
CalendarEvent,
|
||||
EventBusyDate,
|
||||
GetAvailabilityParams,
|
||||
IntegrationCalendar,
|
||||
NewCalendarEventType,
|
||||
Person,
|
||||
@@ -133,11 +134,8 @@ export default class ExchangeCalendarService implements Calendar {
|
||||
});
|
||||
}
|
||||
|
||||
async getAvailability(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[]
|
||||
): Promise<EventBusyDate[]> {
|
||||
async getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
|
||||
const { dateFrom, dateTo, selectedCalendars } = params;
|
||||
const calendars: IntegrationCalendar[] = await this.listCalendars();
|
||||
const promises: Promise<EventBusyDate[]>[] = calendars
|
||||
.filter((lcal) => selectedCalendars.some((rcal) => lcal.externalId == rcal.externalId))
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
CalendarServiceEvent,
|
||||
CalendarEvent,
|
||||
EventBusyDate,
|
||||
GetAvailabilityParams,
|
||||
IntegrationCalendar,
|
||||
NewCalendarEventType,
|
||||
} from "@calcom/types/Calendar";
|
||||
@@ -270,11 +271,8 @@ export default class FeishuCalendarService implements Calendar {
|
||||
}
|
||||
}
|
||||
|
||||
async getAvailability(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[]
|
||||
): Promise<EventBusyDate[]> {
|
||||
async getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
|
||||
const { dateFrom, dateTo, selectedCalendars } = params;
|
||||
const selectedCalendarIds = selectedCalendars
|
||||
.filter((e) => e.integration === this.integrationName)
|
||||
.map((e) => e.externalId)
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
import type { calendar_v3 } from "@googleapis/calendar";
|
||||
import type { GaxiosResponse } from "googleapis-common";
|
||||
import { RRule } from "rrule";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
import { MeetLocationType } from "@calcom/app-store/constants";
|
||||
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
|
||||
import { uniqueBy } from "@calcom/lib/array";
|
||||
import { ORGANIZER_EMAIL_EXEMPT_DOMAINS } from "@calcom/lib/constants";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
@@ -17,10 +14,11 @@ import type {
|
||||
CalendarServiceEvent,
|
||||
CalendarEvent,
|
||||
EventBusyDate,
|
||||
GetAvailabilityParams,
|
||||
IntegrationCalendar,
|
||||
NewCalendarEventType,
|
||||
SelectedCalendarEventTypeIds,
|
||||
} from "@calcom/types/Calendar";
|
||||
import type { SelectedCalendarEventTypeIds } from "@calcom/types/Calendar";
|
||||
import type { CredentialForCalendarServiceWithEmail } from "@calcom/types/Credential";
|
||||
|
||||
import { AxiosLikeResponseToFetchResponse } from "../../_utils/oauth/AxiosLikeResponseToFetchResponse";
|
||||
@@ -34,13 +32,7 @@ interface GoogleCalError extends Error {
|
||||
code?: number;
|
||||
}
|
||||
|
||||
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
||||
const ONE_MONTH_IN_MS = 30 * MS_PER_DAY;
|
||||
|
||||
const GOOGLE_WEBHOOK_URL_BASE = process.env.GOOGLE_WEBHOOK_URL || process.env.NEXT_PUBLIC_WEBAPP_URL;
|
||||
const GOOGLE_WEBHOOK_URL = `${GOOGLE_WEBHOOK_URL_BASE}/api/integrations/googlecalendar/webhook`;
|
||||
|
||||
const isGaxiosResponse = (error: unknown): error is GaxiosResponse<calendar_v3.Schema$Event> =>
|
||||
const isGaxiosResponse= (error: unknown): error is GaxiosResponse<calendar_v3.Schema$Event> =>
|
||||
typeof error === "object" && !!error && Object.prototype.hasOwnProperty.call(error, "config");
|
||||
|
||||
export default class GoogleCalendarService implements Calendar {
|
||||
@@ -120,50 +112,6 @@ export default class GoogleCalendarService implements Calendar {
|
||||
return attendees;
|
||||
};
|
||||
|
||||
private async stopWatchingCalendarsInGoogle(
|
||||
channels: { googleChannelResourceId: string | null; googleChannelId: string | null }[]
|
||||
) {
|
||||
const calendar = await this.authedCalendar();
|
||||
logger.debug(`Unsubscribing from calendars ${channels.map((c) => c.googleChannelId).join(", ")}`);
|
||||
const uniqueChannels = uniqueBy(channels, ["googleChannelId", "googleChannelResourceId"]);
|
||||
await Promise.allSettled(
|
||||
uniqueChannels.map(({ googleChannelResourceId, googleChannelId }) =>
|
||||
calendar.channels
|
||||
.stop({
|
||||
requestBody: {
|
||||
resourceId: googleChannelResourceId,
|
||||
id: googleChannelId,
|
||||
},
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn(JSON.stringify(err));
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private async startWatchingCalendarsInGoogle({ calendarId }: { calendarId: string }) {
|
||||
const calendar = await this.authedCalendar();
|
||||
logger.debug(`Subscribing to calendar ${calendarId}`, safeStringify({ GOOGLE_WEBHOOK_URL }));
|
||||
|
||||
const res = await calendar.events.watch({
|
||||
// Calendar identifier. To retrieve calendar IDs call the calendarList.list method. If you want to access the primary calendar of the currently logged in user, use the "primary" keyword.
|
||||
calendarId,
|
||||
requestBody: {
|
||||
// A UUID or similar unique string that identifies this channel.
|
||||
id: uuid(),
|
||||
type: "web_hook",
|
||||
address: GOOGLE_WEBHOOK_URL,
|
||||
token: process.env.GOOGLE_WEBHOOK_TOKEN,
|
||||
params: {
|
||||
// The time-to-live in seconds for the notification channel. Default is 604800 seconds.
|
||||
ttl: `${Math.round(ONE_MONTH_IN_MS / 1000)}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async createEvent(
|
||||
calEvent: CalendarServiceEvent,
|
||||
credentialId: number,
|
||||
@@ -491,15 +439,8 @@ export default class GoogleCalendarService implements Calendar {
|
||||
return result;
|
||||
}
|
||||
|
||||
async getAvailabilityWithTimeZones(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[],
|
||||
/**
|
||||
* If true, we will fallback to the primary calendar if no valid selected calendars are found
|
||||
*/
|
||||
fallbackToPrimary?: boolean
|
||||
): Promise<{ start: Date | string; end: Date | string; timeZone: string }[]> {
|
||||
async getAvailabilityWithTimeZones(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
|
||||
const { dateFrom, dateTo, selectedCalendars, fallbackToPrimary } = params;
|
||||
const calendar = await this.authedCalendar();
|
||||
const selectedCalendarIds = selectedCalendars
|
||||
.filter((e) => e.integration === this.integrationName)
|
||||
@@ -665,15 +606,8 @@ export default class GoogleCalendarService implements Calendar {
|
||||
return busyData;
|
||||
}
|
||||
|
||||
async getAvailability(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[],
|
||||
/**
|
||||
* If true, we will fallback to the primary calendar if no valid selected calendars are found
|
||||
*/
|
||||
fallbackToPrimary?: boolean
|
||||
): Promise<EventBusyDate[]> {
|
||||
async getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
|
||||
const { dateFrom, dateTo, selectedCalendars, fallbackToPrimary } = params;
|
||||
this.log.debug("Getting availability", safeStringify({ dateFrom, dateTo, selectedCalendars }));
|
||||
|
||||
const selectedCalendarIds = selectedCalendars
|
||||
|
||||
@@ -105,21 +105,23 @@ describe("getAvailability", () => {
|
||||
};
|
||||
});
|
||||
|
||||
const availabilityWithPrimaryAsFallback = await calendarService.getAvailability(
|
||||
"2024-01-01",
|
||||
"2024-01-02",
|
||||
[],
|
||||
true
|
||||
);
|
||||
const availabilityWithPrimaryAsFallback = await calendarService.getAvailability({
|
||||
dateFrom: "2024-01-01",
|
||||
dateTo: "2024-01-02",
|
||||
selectedCalendars: [],
|
||||
mode: "slots",
|
||||
fallbackToPrimary: true,
|
||||
});
|
||||
|
||||
expect(availabilityWithPrimaryAsFallback).toEqual(mockedBusyTimes1);
|
||||
|
||||
const availabilityWithAllCalendarsAsFallback = await calendarService.getAvailability(
|
||||
"2024-01-01",
|
||||
"2024-01-02",
|
||||
[],
|
||||
false
|
||||
);
|
||||
const availabilityWithAllCalendarsAsFallback = await calendarService.getAvailability({
|
||||
dateFrom: "2024-01-01",
|
||||
dateTo: "2024-01-02",
|
||||
selectedCalendars: [],
|
||||
mode: "slots",
|
||||
fallbackToPrimary: false,
|
||||
});
|
||||
|
||||
expect(availabilityWithAllCalendarsAsFallback).toEqual([...mockedBusyTimes1, ...mockedBusyTimes2]);
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
IntegrationCalendar,
|
||||
EventBusyDate,
|
||||
CalendarEvent,
|
||||
GetAvailabilityParams,
|
||||
NewCalendarEventType,
|
||||
} from "@calcom/types/Calendar";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
@@ -134,11 +135,8 @@ export default class ICSFeedCalendarService implements Calendar {
|
||||
return selectedCalendars[0].userId || null;
|
||||
};
|
||||
|
||||
async getAvailability(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[]
|
||||
): Promise<EventBusyDate[]> {
|
||||
async getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
|
||||
const { dateFrom, dateTo, selectedCalendars } = params;
|
||||
const startISOString = new Date(dateFrom).toISOString();
|
||||
|
||||
const calendars = await this.fetchCalendars();
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
CalendarServiceEvent,
|
||||
CalendarEvent,
|
||||
EventBusyDate,
|
||||
GetAvailabilityParams,
|
||||
IntegrationCalendar,
|
||||
NewCalendarEventType,
|
||||
} from "@calcom/types/Calendar";
|
||||
@@ -270,11 +271,8 @@ export default class LarkCalendarService implements Calendar {
|
||||
}
|
||||
}
|
||||
|
||||
async getAvailability(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[]
|
||||
): Promise<EventBusyDate[]> {
|
||||
async getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
|
||||
const { dateFrom, dateTo, selectedCalendars } = params;
|
||||
const selectedCalendarIds = selectedCalendars
|
||||
.filter((e) => e.integration === this.integrationName)
|
||||
.map((e) => e.externalId)
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
Calendar,
|
||||
CalendarServiceEvent,
|
||||
EventBusyDate,
|
||||
GetAvailabilityParams,
|
||||
IntegrationCalendar,
|
||||
NewCalendarEventType,
|
||||
} from "@calcom/types/Calendar";
|
||||
@@ -359,11 +360,8 @@ export default class Office365CalendarService implements Calendar {
|
||||
}
|
||||
}
|
||||
|
||||
async getAvailability(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[]
|
||||
): Promise<EventBusyDate[]> {
|
||||
async getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
|
||||
const { dateFrom, dateTo, selectedCalendars } = params;
|
||||
const dateFromParsed = new Date(dateFrom);
|
||||
const dateToParsed = new Date(dateTo);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
Calendar,
|
||||
CalendarEvent,
|
||||
EventBusyDate,
|
||||
GetAvailabilityParams,
|
||||
IntegrationCalendar,
|
||||
NewCalendarEventType,
|
||||
} from "@calcom/types/Calendar";
|
||||
@@ -87,11 +88,7 @@ export default class CloseComCalendarService implements Calendar {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async getAvailability(
|
||||
_dateFrom: string,
|
||||
_dateTo: string,
|
||||
_selectedCalendars: IntegrationCalendar[]
|
||||
): Promise<EventBusyDate[]> {
|
||||
async getAvailability(_params: GetAvailabilityParams): Promise<EventBusyDate[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ const Reschedule = async (bookingUid: string, cancellationReason: string) => {
|
||||
if (!bookingRef.uid) return;
|
||||
|
||||
if (bookingRef.type.endsWith("_calendar")) {
|
||||
const calendar = await getCalendar(credentialsMap.get(bookingRef.type));
|
||||
const calendar = await getCalendar(credentialsMap.get(bookingRef.type), "booking");
|
||||
return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent);
|
||||
} else if (bookingRef.type.endsWith("_video")) {
|
||||
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
|
||||
|
||||
@@ -149,7 +149,7 @@ const Reschedule = async (bookingUid: string, cancellationReason: string) => {
|
||||
if (!bookingRef.uid) return;
|
||||
|
||||
if (bookingRef.type.endsWith("_calendar")) {
|
||||
const calendar = await getCalendar(credentialsMap.get(bookingRef.type));
|
||||
const calendar = await getCalendar(credentialsMap.get(bookingRef.type), "booking");
|
||||
return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent);
|
||||
} else if (bookingRef.type.endsWith("_video")) {
|
||||
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
CalendarServiceEvent,
|
||||
CalendarEvent,
|
||||
EventBusyDate,
|
||||
GetAvailabilityParams,
|
||||
IntegrationCalendar,
|
||||
NewCalendarEventType,
|
||||
} from "@calcom/types/Calendar";
|
||||
@@ -302,11 +303,8 @@ export default class ZohoCalendarService implements Calendar {
|
||||
}
|
||||
}
|
||||
|
||||
async getAvailability(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[]
|
||||
): Promise<EventBusyDate[]> {
|
||||
async getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
|
||||
const { dateFrom, dateTo, selectedCalendars } = params;
|
||||
const selectedCalendarIds = selectedCalendars
|
||||
.filter((e) => e.integration === this.integrationName)
|
||||
.map((e) => e.externalId);
|
||||
|
||||
@@ -38,7 +38,7 @@ import type {
|
||||
} from "@calcom/prisma/client";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { EventBusyDetails, IntervalLimitUnit } from "@calcom/types/Calendar";
|
||||
import type { CalendarFetchMode, EventBusyDetails, IntervalLimitUnit } from "@calcom/types/Calendar";
|
||||
import type { TimeRange } from "@calcom/types/schedule";
|
||||
|
||||
import { findUsersForAvailabilityCheck } from "./findUsersForAvailabilityCheck";
|
||||
@@ -58,7 +58,7 @@ const availabilitySchema = z
|
||||
returnDateOverrides: z.boolean(),
|
||||
bypassBusyCalendarTimes: z.boolean().optional(),
|
||||
silentlyHandleCalendarFailures: z.boolean().optional(),
|
||||
shouldServeCache: z.boolean().optional(),
|
||||
mode: z.enum(["slots", "overlay", "booking"]).optional(),
|
||||
})
|
||||
.refine((data) => !!data.username || !!data.userId, "Either username or userId should be filled in.");
|
||||
|
||||
@@ -115,7 +115,7 @@ type GetUserAvailabilityQuery = {
|
||||
returnDateOverrides: boolean;
|
||||
bypassBusyCalendarTimes: boolean;
|
||||
silentlyHandleCalendarFailures?: boolean;
|
||||
shouldServeCache?: boolean;
|
||||
mode?: CalendarFetchMode;
|
||||
};
|
||||
|
||||
export type CurrentSeats = Awaited<
|
||||
@@ -205,7 +205,7 @@ export class UserAvailabilityService {
|
||||
|
||||
for (const credential of delegatedCredentials) {
|
||||
try {
|
||||
const calendar = await getCalendar(credential);
|
||||
const calendar = await getCalendar(credential, "slots");
|
||||
if (calendar && "getMainTimeZone" in calendar && typeof calendar.getMainTimeZone === "function") {
|
||||
const timezone = await calendar.getMainTimeZone();
|
||||
if (timezone && timezone !== "UTC") {
|
||||
@@ -304,7 +304,7 @@ export class UserAvailabilityService {
|
||||
returnDateOverrides,
|
||||
bypassBusyCalendarTimes = false,
|
||||
silentlyHandleCalendarFailures = false,
|
||||
shouldServeCache,
|
||||
mode,
|
||||
} = availabilitySchema.parse(query);
|
||||
|
||||
log.debug(
|
||||
@@ -467,7 +467,7 @@ export class UserAvailabilityService {
|
||||
currentBookings: initialData?.currentBookings,
|
||||
bypassBusyCalendarTimes,
|
||||
silentlyHandleCalendarFailures,
|
||||
shouldServeCache,
|
||||
mode,
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error fetching busy times for user ${username}:`, error);
|
||||
|
||||
@@ -1160,7 +1160,7 @@ export default class EventManager {
|
||||
const calendarCredential = await CredentialRepository.findCredentialForCalendarServiceById({
|
||||
id: oldCalendarEvent.credentialId,
|
||||
});
|
||||
const calendar = await getCalendar(calendarCredential);
|
||||
const calendar = await getCalendar(calendarCredential, "booking");
|
||||
await calendar?.deleteEvent(oldCalendarEvent.uid, event, oldCalendarEvent.externalCalendarId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -687,7 +687,7 @@ export class LuckyUserService implements ILuckyUserService {
|
||||
getIntervalStartDate({ interval, rrTimestampBasis, meetingStartTime }).toISOString(),
|
||||
getIntervalEndDate({ interval, rrTimestampBasis, meetingStartTime }).toISOString(),
|
||||
user.userLevelSelectedCalendars,
|
||||
true,
|
||||
"slots",
|
||||
true
|
||||
)
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ import { getPiiFreeUser } from "@calcom/lib/piiFreeData";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { withReporting } from "@calcom/lib/sentryWrapper";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { CalendarFetchMode } from "@calcom/types/Calendar";
|
||||
|
||||
import type { getEventTypeResponse } from "./getEventTypesFromDB";
|
||||
import type { BookingType } from "./originalRescheduledBookingUtils";
|
||||
@@ -59,7 +60,7 @@ const _ensureAvailableUsers = async (
|
||||
},
|
||||
input: { dateFrom: string; dateTo: string; timeZone: string; originalRescheduledBooking?: BookingType },
|
||||
loggerWithEventDetails: Logger<unknown>,
|
||||
shouldServeCache?: boolean
|
||||
mode?: CalendarFetchMode
|
||||
// ReturnType hint of at least one IsFixedAwareUser, as it's made sure at least one entry exists
|
||||
): Promise<[IsFixedAwareUser, ...IsFixedAwareUser[]]> => {
|
||||
const userAvailabilityService = getUserAvailabilityService();
|
||||
@@ -102,7 +103,7 @@ const _ensureAvailableUsers = async (
|
||||
beforeEventBuffer: eventType.beforeEventBuffer,
|
||||
afterEventBuffer: eventType.afterEventBuffer,
|
||||
bypassBusyCalendarTimes: false,
|
||||
shouldServeCache,
|
||||
mode,
|
||||
withSource: true,
|
||||
},
|
||||
initialData: {
|
||||
|
||||
@@ -117,7 +117,7 @@ async function cancelAttendeeSeat(
|
||||
integrationsToUpdate.push(updateMeeting(credential, updatedEvt, reference));
|
||||
}
|
||||
if (reference.type.includes("_calendar")) {
|
||||
const calendar = await getCalendar(credential);
|
||||
const calendar = await getCalendar(credential, "booking");
|
||||
if (calendar) {
|
||||
integrationsToUpdate.push(
|
||||
calendar?.updateEvent(reference.uid, updatedEvt, reference.externalCalendarId)
|
||||
|
||||
@@ -43,7 +43,7 @@ const lastAttendeeDeleteBooking = async (
|
||||
integrationsToDelete.push(deleteMeeting(credential, reference.uid));
|
||||
}
|
||||
if (reference.type.includes("_calendar") && originalBookingEvt) {
|
||||
const calendar = await getCalendar(credential);
|
||||
const calendar = await getCalendar(credential, "booking");
|
||||
if (calendar) {
|
||||
integrationsToDelete.push(
|
||||
calendar?.deleteEvent(reference.uid, originalBookingEvt, reference.externalCalendarId)
|
||||
|
||||
@@ -795,7 +795,8 @@ async function handler(
|
||||
const isTeamEventType =
|
||||
!!eventType.schedulingType && ["COLLECTIVE", "ROUND_ROBIN"].includes(eventType.schedulingType);
|
||||
|
||||
const shouldServeCache = false;
|
||||
// Use "booking" mode to bypass cache for booking confirmation
|
||||
const calendarFetchMode = "booking" as const;
|
||||
|
||||
tracingLogger.info(
|
||||
`Booking eventType ${eventTypeId} started`,
|
||||
@@ -1002,7 +1003,7 @@ async function handler(
|
||||
originalRescheduledBooking: originalRescheduledBooking ?? null,
|
||||
},
|
||||
tracingLogger,
|
||||
shouldServeCache
|
||||
calendarFetchMode
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1017,7 +1018,7 @@ async function handler(
|
||||
originalRescheduledBooking,
|
||||
},
|
||||
tracingLogger,
|
||||
shouldServeCache
|
||||
calendarFetchMode
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1036,7 +1037,7 @@ async function handler(
|
||||
originalRescheduledBooking,
|
||||
},
|
||||
tracingLogger,
|
||||
shouldServeCache
|
||||
calendarFetchMode
|
||||
);
|
||||
} else {
|
||||
availableUsers = [...qualifiedRRUsers, ...fixedUsers] as IsFixedAwareUser[];
|
||||
@@ -1065,7 +1066,7 @@ async function handler(
|
||||
originalRescheduledBooking,
|
||||
},
|
||||
tracingLogger,
|
||||
shouldServeCache
|
||||
calendarFetchMode
|
||||
);
|
||||
} else {
|
||||
availableUsers = [...additionalFallbackRRUsers, ...fixedUsers] as IsFixedAwareUser[];
|
||||
@@ -1178,7 +1179,7 @@ async function handler(
|
||||
originalRescheduledBooking,
|
||||
},
|
||||
tracingLogger,
|
||||
shouldServeCache
|
||||
calendarFetchMode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import type { Booking, EventType } from "@calcom/prisma/client";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import type { SelectedCalendar } from "@calcom/prisma/client";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import type { EventBusyDetails } from "@calcom/types/Calendar";
|
||||
import type { CalendarFetchMode, EventBusyDetails } from "@calcom/types/Calendar";
|
||||
import type { CredentialForCalendarService } from "@calcom/types/Credential";
|
||||
|
||||
export interface IBusyTimesService {
|
||||
@@ -52,7 +52,7 @@ export class BusyTimesService {
|
||||
| null;
|
||||
bypassBusyCalendarTimes: boolean;
|
||||
silentlyHandleCalendarFailures?: boolean;
|
||||
shouldServeCache?: boolean;
|
||||
mode?: CalendarFetchMode;
|
||||
}) {
|
||||
const {
|
||||
credentials,
|
||||
@@ -70,7 +70,7 @@ export class BusyTimesService {
|
||||
duration,
|
||||
bypassBusyCalendarTimes = false,
|
||||
silentlyHandleCalendarFailures = false,
|
||||
shouldServeCache,
|
||||
mode,
|
||||
} = params;
|
||||
|
||||
logger.silly(
|
||||
@@ -194,7 +194,7 @@ export class BusyTimesService {
|
||||
startTime,
|
||||
endTime,
|
||||
selectedCalendars,
|
||||
shouldServeCache
|
||||
mode
|
||||
);
|
||||
|
||||
if (!calendarBusyTimesQuery.success) {
|
||||
|
||||
+24
-41
@@ -6,9 +6,9 @@ import type {
|
||||
CalendarEvent,
|
||||
CalendarServiceEvent,
|
||||
EventBusyDate,
|
||||
GetAvailabilityParams,
|
||||
IntegrationCalendar,
|
||||
NewCalendarEventType,
|
||||
SelectedCalendarEventTypeIds,
|
||||
} from "@calcom/types/Calendar";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["CachedCalendarWrapper"] });
|
||||
@@ -56,17 +56,9 @@ export class CalendarCacheWrapper implements Calendar {
|
||||
* - Calendars **with** both `syncToken` and `syncSubscribedAt` → fetched from cache.
|
||||
* - Calendars **without** one of them → fetched directly from the original calendar.
|
||||
* - Results are merged into a single array.
|
||||
*
|
||||
* @param dateFrom - Start date (ISO string)
|
||||
* @param dateTo - End date (ISO string)
|
||||
* @param selectedCalendars - List of calendars to retrieve availability from
|
||||
* @returns Combined array of busy date ranges
|
||||
*/
|
||||
async getAvailability(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[]
|
||||
): Promise<EventBusyDate[]> {
|
||||
async getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
|
||||
const { dateFrom, dateTo, selectedCalendars } = params;
|
||||
return withSpan(
|
||||
{
|
||||
name: "CalendarCacheWrapper.getAvailability",
|
||||
@@ -100,7 +92,12 @@ export class CalendarCacheWrapper implements Calendar {
|
||||
new Date(dateTo)
|
||||
);
|
||||
const cacheDurationMs = performance.now() - cacheStartTime;
|
||||
results.push(...cached);
|
||||
results.push(
|
||||
...cached.map((event) => ({
|
||||
...event,
|
||||
timeZone: event.timeZone ?? undefined,
|
||||
}))
|
||||
);
|
||||
|
||||
span.setAttribute("cachedCalendarCount", withSync.length);
|
||||
span.setAttribute("cacheFetchDurationMs", cacheDurationMs);
|
||||
@@ -115,7 +112,13 @@ export class CalendarCacheWrapper implements Calendar {
|
||||
// Fetch from original calendar for unsynced ones
|
||||
if (withoutSync.length) {
|
||||
const originalStartTime = performance.now();
|
||||
const original = await this.deps.originalCalendar.getAvailability(dateFrom, dateTo, withoutSync);
|
||||
const original = await this.deps.originalCalendar.getAvailability({
|
||||
dateFrom,
|
||||
dateTo,
|
||||
selectedCalendars: withoutSync,
|
||||
mode: params.mode,
|
||||
fallbackToPrimary: params.fallbackToPrimary,
|
||||
});
|
||||
const originalDurationMs = performance.now() - originalStartTime;
|
||||
results.push(...original);
|
||||
|
||||
@@ -143,17 +146,9 @@ export class CalendarCacheWrapper implements Calendar {
|
||||
* - Calendars **with** both `syncToken` and `syncSubscribedAt` → fetched from cache.
|
||||
* - Calendars **without** one of them → fetched directly from the original calendar.
|
||||
* - Results are merged into a single array with `{ start, end, timeZone }` format.
|
||||
*
|
||||
* @param dateFrom - Start date (ISO string)
|
||||
* @param dateTo - End date (ISO string)
|
||||
* @param selectedCalendars - List of calendars to retrieve availability from
|
||||
* @returns Combined array of time-zone-aware availability ranges
|
||||
*/
|
||||
async getAvailabilityWithTimeZones(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[]
|
||||
): Promise<{ start: Date | string; end: Date | string; timeZone: string }[]> {
|
||||
async getAvailabilityWithTimeZones(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
|
||||
const { dateFrom, dateTo, selectedCalendars } = params;
|
||||
return withSpan(
|
||||
{
|
||||
name: "CalendarCacheWrapper.getAvailabilityWithTimeZones",
|
||||
@@ -175,7 +170,7 @@ export class CalendarCacheWrapper implements Calendar {
|
||||
const withSync = selectedCalendars.filter((c) => c.syncToken && c.syncSubscribedAt);
|
||||
const withoutSync = selectedCalendars.filter((c) => !c.syncToken || !c.syncSubscribedAt);
|
||||
|
||||
const results: { start: Date | string; end: Date | string; timeZone: string }[] = [];
|
||||
const results: EventBusyDate[] = [];
|
||||
|
||||
// Fetch from cache for synced calendars
|
||||
if (withSync.length) {
|
||||
@@ -208,11 +203,13 @@ export class CalendarCacheWrapper implements Calendar {
|
||||
// Fetch from original calendar for unsynced ones
|
||||
if (withoutSync.length) {
|
||||
const originalStartTime = performance.now();
|
||||
const original = await this.deps.originalCalendar.getAvailabilityWithTimeZones?.(
|
||||
const original = await this.deps.originalCalendar.getAvailabilityWithTimeZones?.({
|
||||
dateFrom,
|
||||
dateTo,
|
||||
withoutSync
|
||||
);
|
||||
selectedCalendars: withoutSync,
|
||||
mode: params.mode,
|
||||
fallbackToPrimary: params.fallbackToPrimary,
|
||||
});
|
||||
const originalDurationMs = performance.now() - originalStartTime;
|
||||
if (original?.length) results.push(...original);
|
||||
|
||||
@@ -245,18 +242,4 @@ export class CalendarCacheWrapper implements Calendar {
|
||||
testDelegationCredentialSetup?(): Promise<boolean> {
|
||||
return this.deps.originalCalendar.testDelegationCredentialSetup?.() || Promise.resolve(false);
|
||||
}
|
||||
|
||||
watchCalendar?(options: {
|
||||
calendarId: string;
|
||||
eventTypeIds: SelectedCalendarEventTypeIds;
|
||||
}): Promise<unknown> {
|
||||
return this.deps.originalCalendar.watchCalendar?.(options) || Promise.resolve();
|
||||
}
|
||||
|
||||
unwatchCalendar?(options: {
|
||||
calendarId: string;
|
||||
eventTypeIds: SelectedCalendarEventTypeIds;
|
||||
}): Promise<void> {
|
||||
return this.deps.originalCalendar.unwatchCalendar?.(options) || Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
+87
-24
@@ -41,7 +41,12 @@ describe("CalendarCacheWrapper", () => {
|
||||
|
||||
describe("getAvailability", () => {
|
||||
it("should return empty array when no calendars provided", async () => {
|
||||
const result = await wrapper.getAvailability("2025-01-01", "2025-01-02", []);
|
||||
const result = await wrapper.getAvailability({
|
||||
dateFrom: "2025-01-01",
|
||||
dateTo: "2025-01-02",
|
||||
selectedCalendars: [],
|
||||
mode: "slots",
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(mockRepository.findAllBySelectedCalendarIdsBetween).not.toHaveBeenCalled();
|
||||
@@ -73,7 +78,12 @@ describe("CalendarCacheWrapper", () => {
|
||||
|
||||
vi.mocked(mockRepository.findAllBySelectedCalendarIdsBetween).mockResolvedValue(cachedEvents);
|
||||
|
||||
const result = await wrapper.getAvailability("2025-01-01", "2025-01-02", selectedCalendars);
|
||||
const result = await wrapper.getAvailability({
|
||||
dateFrom: "2025-01-01",
|
||||
dateTo: "2025-01-02",
|
||||
selectedCalendars,
|
||||
mode: "slots",
|
||||
});
|
||||
|
||||
expect(result).toEqual(cachedEvents);
|
||||
expect(mockRepository.findAllBySelectedCalendarIdsBetween).toHaveBeenCalledWith(
|
||||
@@ -108,15 +118,22 @@ describe("CalendarCacheWrapper", () => {
|
||||
|
||||
vi.mocked(mockOriginalCalendar.getAvailability).mockResolvedValue(originalEvents);
|
||||
|
||||
const result = await wrapper.getAvailability("2025-01-01", "2025-01-02", selectedCalendars);
|
||||
const result = await wrapper.getAvailability({
|
||||
dateFrom: "2025-01-01",
|
||||
dateTo: "2025-01-02",
|
||||
selectedCalendars,
|
||||
mode: "slots",
|
||||
});
|
||||
|
||||
expect(result).toEqual(originalEvents);
|
||||
expect(mockRepository.findAllBySelectedCalendarIdsBetween).not.toHaveBeenCalled();
|
||||
expect(mockOriginalCalendar.getAvailability).toHaveBeenCalledWith(
|
||||
"2025-01-01",
|
||||
"2025-01-02",
|
||||
selectedCalendars
|
||||
);
|
||||
expect(mockOriginalCalendar.getAvailability).toHaveBeenCalledWith({
|
||||
dateFrom: "2025-01-01",
|
||||
dateTo: "2025-01-02",
|
||||
selectedCalendars,
|
||||
mode: "slots",
|
||||
fallbackToPrimary: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch from both cache and original calendar when calendars are mixed", async () => {
|
||||
@@ -148,7 +165,12 @@ describe("CalendarCacheWrapper", () => {
|
||||
vi.mocked(mockRepository.findAllBySelectedCalendarIdsBetween).mockResolvedValue(cachedEvents);
|
||||
vi.mocked(mockOriginalCalendar.getAvailability).mockResolvedValue(originalEvents);
|
||||
|
||||
const result = await wrapper.getAvailability("2025-01-01", "2025-01-02", selectedCalendars);
|
||||
const result = await wrapper.getAvailability({
|
||||
dateFrom: "2025-01-01",
|
||||
dateTo: "2025-01-02",
|
||||
selectedCalendars,
|
||||
mode: "slots",
|
||||
});
|
||||
|
||||
expect(result).toEqual([...cachedEvents, ...originalEvents]);
|
||||
expect(mockRepository.findAllBySelectedCalendarIdsBetween).toHaveBeenCalledWith(
|
||||
@@ -156,9 +178,13 @@ describe("CalendarCacheWrapper", () => {
|
||||
new Date("2025-01-01"),
|
||||
new Date("2025-01-02")
|
||||
);
|
||||
expect(mockOriginalCalendar.getAvailability).toHaveBeenCalledWith("2025-01-01", "2025-01-02", [
|
||||
selectedCalendars[1],
|
||||
]);
|
||||
expect(mockOriginalCalendar.getAvailability).toHaveBeenCalledWith({
|
||||
dateFrom: "2025-01-01",
|
||||
dateTo: "2025-01-02",
|
||||
selectedCalendars: [selectedCalendars[1]],
|
||||
mode: "slots",
|
||||
fallbackToPrimary: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("should filter out calendars without id when fetching from cache", async () => {
|
||||
@@ -179,7 +205,12 @@ describe("CalendarCacheWrapper", () => {
|
||||
},
|
||||
];
|
||||
|
||||
await wrapper.getAvailability("2025-01-01", "2025-01-02", selectedCalendars);
|
||||
await wrapper.getAvailability({
|
||||
dateFrom: "2025-01-01",
|
||||
dateTo: "2025-01-02",
|
||||
selectedCalendars,
|
||||
mode: "slots",
|
||||
});
|
||||
|
||||
expect(mockRepository.findAllBySelectedCalendarIdsBetween).toHaveBeenCalledWith(
|
||||
["cal-1"], // Only cal-1 has a valid id
|
||||
@@ -191,7 +222,12 @@ describe("CalendarCacheWrapper", () => {
|
||||
|
||||
describe("getAvailabilityWithTimeZones", () => {
|
||||
it("should return empty array when no calendars provided", async () => {
|
||||
const result = await wrapper.getAvailabilityWithTimeZones("2025-01-01", "2025-01-02", []);
|
||||
const result = await wrapper.getAvailabilityWithTimeZones({
|
||||
dateFrom: "2025-01-01",
|
||||
dateTo: "2025-01-02",
|
||||
selectedCalendars: [],
|
||||
mode: "slots",
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(mockRepository.findAllBySelectedCalendarIdsBetween).not.toHaveBeenCalled();
|
||||
@@ -224,7 +260,12 @@ describe("CalendarCacheWrapper", () => {
|
||||
|
||||
vi.mocked(mockRepository.findAllBySelectedCalendarIdsBetween).mockResolvedValue(cachedEvents);
|
||||
|
||||
const result = await wrapper.getAvailabilityWithTimeZones("2025-01-01", "2025-01-02", selectedCalendars);
|
||||
const result = await wrapper.getAvailabilityWithTimeZones({
|
||||
dateFrom: "2025-01-01",
|
||||
dateTo: "2025-01-02",
|
||||
selectedCalendars,
|
||||
mode: "slots",
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
@@ -262,15 +303,22 @@ describe("CalendarCacheWrapper", () => {
|
||||
|
||||
vi.mocked(mockOriginalCalendar.getAvailabilityWithTimeZones).mockResolvedValue(originalEvents);
|
||||
|
||||
const result = await wrapper.getAvailabilityWithTimeZones("2025-01-01", "2025-01-02", selectedCalendars);
|
||||
const result = await wrapper.getAvailabilityWithTimeZones({
|
||||
dateFrom: "2025-01-01",
|
||||
dateTo: "2025-01-02",
|
||||
selectedCalendars,
|
||||
mode: "slots",
|
||||
});
|
||||
|
||||
expect(result).toEqual(originalEvents);
|
||||
expect(mockRepository.findAllBySelectedCalendarIdsBetween).not.toHaveBeenCalled();
|
||||
expect(mockOriginalCalendar.getAvailabilityWithTimeZones).toHaveBeenCalledWith(
|
||||
"2025-01-01",
|
||||
"2025-01-02",
|
||||
selectedCalendars
|
||||
);
|
||||
expect(mockOriginalCalendar.getAvailabilityWithTimeZones).toHaveBeenCalledWith({
|
||||
dateFrom: "2025-01-01",
|
||||
dateTo: "2025-01-02",
|
||||
selectedCalendars,
|
||||
mode: "slots",
|
||||
fallbackToPrimary: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch from both cache and original calendar when calendars are mixed", async () => {
|
||||
@@ -310,7 +358,12 @@ describe("CalendarCacheWrapper", () => {
|
||||
vi.mocked(mockRepository.findAllBySelectedCalendarIdsBetween).mockResolvedValue(cachedEvents);
|
||||
vi.mocked(mockOriginalCalendar.getAvailabilityWithTimeZones).mockResolvedValue(originalEvents);
|
||||
|
||||
const result = await wrapper.getAvailabilityWithTimeZones("2025-01-01", "2025-01-02", selectedCalendars);
|
||||
const result = await wrapper.getAvailabilityWithTimeZones({
|
||||
dateFrom: "2025-01-01",
|
||||
dateTo: "2025-01-02",
|
||||
selectedCalendars,
|
||||
mode: "slots",
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
@@ -351,7 +404,12 @@ describe("CalendarCacheWrapper", () => {
|
||||
vi.mocked(mockRepository.findAllBySelectedCalendarIdsBetween).mockResolvedValue(cachedEvents);
|
||||
mockOriginalCalendar.getAvailabilityWithTimeZones = undefined;
|
||||
|
||||
const result = await wrapper.getAvailabilityWithTimeZones("2025-01-01", "2025-01-02", selectedCalendars);
|
||||
const result = await wrapper.getAvailabilityWithTimeZones({
|
||||
dateFrom: "2025-01-01",
|
||||
dateTo: "2025-01-02",
|
||||
selectedCalendars,
|
||||
mode: "slots",
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
@@ -375,7 +433,12 @@ describe("CalendarCacheWrapper", () => {
|
||||
|
||||
vi.mocked(mockOriginalCalendar.getAvailabilityWithTimeZones).mockResolvedValue([]);
|
||||
|
||||
const result = await wrapper.getAvailabilityWithTimeZones("2025-01-01", "2025-01-02", selectedCalendars);
|
||||
const result = await wrapper.getAvailabilityWithTimeZones({
|
||||
dateFrom: "2025-01-01",
|
||||
dateTo: "2025-01-02",
|
||||
selectedCalendars,
|
||||
mode: "slots",
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -5,9 +5,9 @@ import type {
|
||||
CalendarEvent,
|
||||
CalendarServiceEvent,
|
||||
EventBusyDate,
|
||||
GetAvailabilityParams,
|
||||
IntegrationCalendar,
|
||||
NewCalendarEventType,
|
||||
SelectedCalendarEventTypeIds,
|
||||
} from "@calcom/types/Calendar";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["CalendarTelemetryWrapper"] });
|
||||
@@ -53,19 +53,8 @@ export class CalendarTelemetryWrapper implements Calendar {
|
||||
return this.deps.originalCalendar.deleteEvent(uid, event, externalCalendarId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves availability with telemetry tracking when cache is disabled.
|
||||
*
|
||||
* @param dateFrom - Start date (ISO string)
|
||||
* @param dateTo - End date (ISO string)
|
||||
* @param selectedCalendars - List of calendars to retrieve availability from
|
||||
* @returns Array of busy date ranges
|
||||
*/
|
||||
async getAvailability(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[]
|
||||
): Promise<EventBusyDate[]> {
|
||||
async getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
|
||||
const { dateFrom, dateTo, selectedCalendars } = params;
|
||||
return withSpan(
|
||||
{
|
||||
name: "CalendarTelemetryWrapper.getAvailability",
|
||||
@@ -90,7 +79,13 @@ export class CalendarTelemetryWrapper implements Calendar {
|
||||
if (!selectedCalendars?.length) return [];
|
||||
|
||||
const startTime = performance.now();
|
||||
const results = await this.deps.originalCalendar.getAvailability(dateFrom, dateTo, selectedCalendars);
|
||||
const results = await this.deps.originalCalendar.getAvailability({
|
||||
dateFrom,
|
||||
dateTo,
|
||||
selectedCalendars,
|
||||
mode: params.mode,
|
||||
fallbackToPrimary: params.fallbackToPrimary,
|
||||
});
|
||||
const totalFetchDurationMs = performance.now() - startTime;
|
||||
|
||||
span.setAttribute("totalFetchDurationMs", totalFetchDurationMs);
|
||||
@@ -109,19 +104,8 @@ export class CalendarTelemetryWrapper implements Calendar {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves availability with time zones and telemetry tracking when cache is disabled.
|
||||
*
|
||||
* @param dateFrom - Start date (ISO string)
|
||||
* @param dateTo - End date (ISO string)
|
||||
* @param selectedCalendars - List of calendars to retrieve availability from
|
||||
* @returns Array of time-zone-aware availability ranges
|
||||
*/
|
||||
async getAvailabilityWithTimeZones(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[]
|
||||
): Promise<{ start: Date | string; end: Date | string; timeZone: string }[]> {
|
||||
async getAvailabilityWithTimeZones(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
|
||||
const { dateFrom, dateTo, selectedCalendars } = params;
|
||||
// Check if the original calendar supports this method
|
||||
if (!this.deps.originalCalendar.getAvailabilityWithTimeZones) {
|
||||
return [];
|
||||
@@ -151,11 +135,13 @@ export class CalendarTelemetryWrapper implements Calendar {
|
||||
if (!selectedCalendars?.length) return [];
|
||||
|
||||
const startTime = performance.now();
|
||||
const results = await this.deps.originalCalendar.getAvailabilityWithTimeZones?.(
|
||||
const results = await this.deps.originalCalendar.getAvailabilityWithTimeZones?.({
|
||||
dateFrom,
|
||||
dateTo,
|
||||
selectedCalendars
|
||||
);
|
||||
selectedCalendars,
|
||||
mode: params.mode,
|
||||
fallbackToPrimary: params.fallbackToPrimary,
|
||||
});
|
||||
const totalFetchDurationMs = performance.now() - startTime;
|
||||
|
||||
span.setAttribute("totalFetchDurationMs", totalFetchDurationMs);
|
||||
@@ -185,18 +171,4 @@ export class CalendarTelemetryWrapper implements Calendar {
|
||||
testDelegationCredentialSetup?(): Promise<boolean> {
|
||||
return this.deps.originalCalendar.testDelegationCredentialSetup?.() || Promise.resolve(false);
|
||||
}
|
||||
|
||||
watchCalendar?(options: {
|
||||
calendarId: string;
|
||||
eventTypeIds: SelectedCalendarEventTypeIds;
|
||||
}): Promise<unknown> {
|
||||
return this.deps.originalCalendar.watchCalendar?.(options) || Promise.resolve();
|
||||
}
|
||||
|
||||
unwatchCalendar?(options: {
|
||||
calendarId: string;
|
||||
eventTypeIds: SelectedCalendarEventTypeIds;
|
||||
}): Promise<void> {
|
||||
return this.deps.originalCalendar.unwatchCalendar?.(options) || Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { getPiiFreeCalendarEvent, getPiiFreeCredential } from "@calcom/lib/piiFr
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import type {
|
||||
CalendarEvent,
|
||||
CalendarFetchMode,
|
||||
CalendarServiceEvent,
|
||||
EventBusyDate,
|
||||
IntegrationCalendar,
|
||||
@@ -34,7 +35,7 @@ export const getCalendarCredentials = (credentials: Array<CredentialForCalendarS
|
||||
.filter((app) => app.type.endsWith("_calendar"))
|
||||
.flatMap((app) => {
|
||||
const credentials = app.credentials.flatMap((credential) => {
|
||||
const calendar = () => getCalendar(credential);
|
||||
const calendar = () => getCalendar(credential, "slots");
|
||||
return app.variant === "calendar" ? [{ integration: app, credential, calendar }] : [];
|
||||
});
|
||||
|
||||
@@ -236,7 +237,7 @@ export const getBusyCalendarTimes = async (
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: SelectedCalendar[],
|
||||
shouldServeCache?: boolean,
|
||||
mode?: CalendarFetchMode,
|
||||
includeTimeZone?: boolean
|
||||
) => {
|
||||
let results: (EventBusyDate & { timeZone?: string })[][] = [];
|
||||
@@ -274,7 +275,7 @@ export const getBusyCalendarTimes = async (
|
||||
startDate,
|
||||
endDate,
|
||||
selectedCalendars,
|
||||
shouldServeCache
|
||||
mode ?? "slots"
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -295,7 +296,7 @@ export const createEvent = async (
|
||||
// Some calendar libraries may edit the original event so let's clone it
|
||||
const formattedEvent = formatCalEvent(originalEvent);
|
||||
const uid: string = getUid(formattedEvent);
|
||||
const calendar = await getCalendar(credential);
|
||||
const calendar = await getCalendar(credential, "booking");
|
||||
let success = true;
|
||||
let calError: string | undefined = undefined;
|
||||
|
||||
@@ -389,7 +390,7 @@ export const updateEvent = async (
|
||||
const formattedEvent = formatCalEvent(rawCalEvent);
|
||||
const calEvent = processEvent(formattedEvent);
|
||||
const uid = getUid(calEvent);
|
||||
const calendar = await getCalendar(credential);
|
||||
const calendar = await getCalendar(credential, "booking");
|
||||
let success = false;
|
||||
let calError: string | undefined = undefined;
|
||||
let calWarnings: string[] | undefined = [];
|
||||
@@ -472,7 +473,7 @@ export const deleteEvent = async ({
|
||||
event: CalendarEvent;
|
||||
externalCalendarId?: string | null;
|
||||
}): Promise<unknown> => {
|
||||
const calendar = await getCalendar(credential);
|
||||
const calendar = await getCalendar(credential, "booking");
|
||||
log.debug(
|
||||
"Deleting calendar event",
|
||||
safeStringify({
|
||||
|
||||
@@ -190,7 +190,8 @@ describe("getCalendarsEvents", () => {
|
||||
],
|
||||
"2010-12-01",
|
||||
"2010-12-02",
|
||||
[]
|
||||
[],
|
||||
"slots"
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
@@ -206,7 +207,8 @@ describe("getCalendarsEvents", () => {
|
||||
],
|
||||
"2010-12-01",
|
||||
"2010-12-02",
|
||||
[]
|
||||
[],
|
||||
"slots"
|
||||
);
|
||||
|
||||
expect(result).toEqual([[]]);
|
||||
@@ -229,7 +231,8 @@ describe("getCalendarsEvents", () => {
|
||||
],
|
||||
"2010-12-01",
|
||||
"2010-12-02",
|
||||
[selectedCalendar]
|
||||
[selectedCalendar],
|
||||
"slots"
|
||||
);
|
||||
|
||||
expect(result).toEqual([[]]);
|
||||
@@ -267,15 +270,17 @@ describe("getCalendarsEvents", () => {
|
||||
],
|
||||
"2010-12-01",
|
||||
"2010-12-04",
|
||||
[selectedCalendar]
|
||||
[selectedCalendar],
|
||||
"slots"
|
||||
);
|
||||
|
||||
expect(getAvailabilitySpy).toHaveBeenCalledWith(
|
||||
"2010-12-01",
|
||||
"2010-12-04",
|
||||
[selectedCalendar],
|
||||
false
|
||||
);
|
||||
expect(getAvailabilitySpy).toHaveBeenCalledWith({
|
||||
dateFrom: "2010-12-01",
|
||||
dateTo: "2010-12-04",
|
||||
selectedCalendars: [selectedCalendar],
|
||||
mode: "slots",
|
||||
fallbackToPrimary: false,
|
||||
});
|
||||
expect(result).toEqual([
|
||||
availability.map((av) => ({
|
||||
...av,
|
||||
@@ -337,21 +342,24 @@ describe("getCalendarsEvents", () => {
|
||||
],
|
||||
"2010-12-01",
|
||||
"2010-12-04",
|
||||
[selectedGoogleCalendar, selectedOfficeCalendar]
|
||||
[selectedGoogleCalendar, selectedOfficeCalendar],
|
||||
"slots"
|
||||
);
|
||||
|
||||
expect(getGoogleAvailabilitySpy).toHaveBeenCalledWith(
|
||||
"2010-12-01",
|
||||
"2010-12-04",
|
||||
[selectedGoogleCalendar],
|
||||
false
|
||||
);
|
||||
expect(getOfficeAvailabilitySpy).toHaveBeenCalledWith(
|
||||
"2010-12-01",
|
||||
"2010-12-04",
|
||||
[selectedOfficeCalendar],
|
||||
false
|
||||
);
|
||||
expect(getGoogleAvailabilitySpy).toHaveBeenCalledWith({
|
||||
dateFrom: "2010-12-01",
|
||||
dateTo: "2010-12-04",
|
||||
selectedCalendars: [selectedGoogleCalendar],
|
||||
mode: "slots",
|
||||
fallbackToPrimary: false,
|
||||
});
|
||||
expect(getOfficeAvailabilitySpy).toHaveBeenCalledWith({
|
||||
dateFrom: "2010-12-01",
|
||||
dateTo: "2010-12-04",
|
||||
selectedCalendars: [selectedOfficeCalendar],
|
||||
mode: "slots",
|
||||
fallbackToPrimary: false,
|
||||
});
|
||||
expect(result).toEqual([
|
||||
googleAvailability.map((av) => ({
|
||||
...av,
|
||||
@@ -373,7 +381,8 @@ describe("getCalendarsEvents", () => {
|
||||
[buildRegularCredential(credential)],
|
||||
"2010-12-01",
|
||||
"2010-12-02",
|
||||
[]
|
||||
[],
|
||||
"slots"
|
||||
);
|
||||
|
||||
expect(getAvailabilitySpy).not.toHaveBeenCalled();
|
||||
@@ -391,9 +400,15 @@ describe("getCalendarsEvents", () => {
|
||||
.spyOn(GoogleCalendarService.prototype, "getAvailability")
|
||||
.mockReturnValue(Promise.resolve([]));
|
||||
|
||||
const result = await getCalendarsEvents(credentials, startDate, endDate, []);
|
||||
const result = await getCalendarsEvents(credentials, startDate, endDate, [], "slots");
|
||||
|
||||
expect(getAvailabilitySpy).toHaveBeenCalledWith(startDate, endDate, [], true);
|
||||
expect(getAvailabilitySpy).toHaveBeenCalledWith({
|
||||
dateFrom: startDate,
|
||||
dateTo: endDate,
|
||||
selectedCalendars: [],
|
||||
mode: "slots",
|
||||
fallbackToPrimary: true,
|
||||
});
|
||||
expect(result).toEqual([[]]);
|
||||
});
|
||||
});
|
||||
@@ -523,12 +538,13 @@ describe("getCalendarsEventsWithTimezones", () => {
|
||||
[selectedCalendar]
|
||||
);
|
||||
|
||||
expect(getAvailabilityWithTimezonesSpy).toHaveBeenCalledWith(
|
||||
"2010-12-01",
|
||||
"2010-12-04",
|
||||
[selectedCalendar],
|
||||
false
|
||||
);
|
||||
expect(getAvailabilityWithTimezonesSpy).toHaveBeenCalledWith({
|
||||
dateFrom: "2010-12-01",
|
||||
dateTo: "2010-12-04",
|
||||
selectedCalendars: [selectedCalendar],
|
||||
mode: "slots",
|
||||
fallbackToPrimary: false,
|
||||
});
|
||||
expect(result).toEqual([
|
||||
availability.map((av) => ({
|
||||
...av,
|
||||
@@ -565,7 +581,13 @@ describe("getCalendarsEventsWithTimezones", () => {
|
||||
|
||||
const result = await getCalendarsEventsWithTimezones(credentials, startDate, endDate, []);
|
||||
|
||||
expect(getAvailabilityWithTimezonesSpy).toHaveBeenCalledWith(startDate, endDate, [], true);
|
||||
expect(getAvailabilityWithTimezonesSpy).toHaveBeenCalledWith({
|
||||
dateFrom: startDate,
|
||||
dateTo: endDate,
|
||||
selectedCalendars: [],
|
||||
mode: "slots",
|
||||
fallbackToPrimary: true,
|
||||
});
|
||||
expect(result).toEqual([[]]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import logger from "@calcom/lib/logger";
|
||||
import { getPiiFreeSelectedCalendar, getPiiFreeCredential } from "@calcom/lib/piiFreeData";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { performance } from "@calcom/lib/server/perfObserver";
|
||||
import type { EventBusyDate, SelectedCalendar } from "@calcom/types/Calendar";
|
||||
import type { CalendarFetchMode, EventBusyDate, SelectedCalendar } from "@calcom/types/Calendar";
|
||||
import type { CredentialForCalendarService } from "@calcom/types/Credential";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["getCalendarsEvents"] });
|
||||
@@ -25,7 +25,7 @@ export const getCalendarsEventsWithTimezones = async (
|
||||
|
||||
const calendarAndCredentialPairs = await Promise.all(
|
||||
calendarCredentials.map(async (credential) => {
|
||||
const calendar = await getCalendar(credential);
|
||||
const calendar = await getCalendar(credential, "slots");
|
||||
return [calendar, credential] as const;
|
||||
})
|
||||
);
|
||||
@@ -67,14 +67,18 @@ export const getCalendarsEventsWithTimezones = async (
|
||||
}
|
||||
/** We extract external Ids so we don't cache too much */
|
||||
const eventBusyDates =
|
||||
(await c.getAvailabilityWithTimeZones?.(
|
||||
(await c.getAvailabilityWithTimeZones?.({
|
||||
dateFrom,
|
||||
dateTo,
|
||||
passedSelectedCalendars,
|
||||
allowFallbackToPrimary
|
||||
)) || [];
|
||||
selectedCalendars: passedSelectedCalendars,
|
||||
mode: "slots",
|
||||
fallbackToPrimary: allowFallbackToPrimary,
|
||||
})) || [];
|
||||
|
||||
return eventBusyDates;
|
||||
return eventBusyDates.map((event) => ({
|
||||
...event,
|
||||
timeZone: event.timeZone || "UTC",
|
||||
}));
|
||||
});
|
||||
const awaitedResults = await Promise.all(results);
|
||||
return awaitedResults;
|
||||
@@ -85,7 +89,7 @@ const getCalendarsEvents = async (
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: SelectedCalendar[],
|
||||
shouldServeCache?: boolean
|
||||
mode: CalendarFetchMode
|
||||
): Promise<EventBusyDate[][]> => {
|
||||
const calendarCredentials = withCredentials
|
||||
.filter((credential) => credential.type.endsWith("_calendar"))
|
||||
@@ -94,7 +98,7 @@ const getCalendarsEvents = async (
|
||||
|
||||
const calendarAndCredentialPairs = await Promise.all(
|
||||
calendarCredentials.map(async (credential) => {
|
||||
const calendar = await getCalendar(credential, shouldServeCache);
|
||||
const calendar = await getCalendar(credential, mode);
|
||||
return [calendar, credential] as const;
|
||||
})
|
||||
);
|
||||
@@ -147,12 +151,13 @@ const getCalendarsEvents = async (
|
||||
selectedCalendars: passedSelectedCalendars.map(getPiiFreeSelectedCalendar),
|
||||
})
|
||||
);
|
||||
const eventBusyDates = await calendarService.getAvailability(
|
||||
const eventBusyDates = await calendarService.getAvailability({
|
||||
dateFrom,
|
||||
dateTo,
|
||||
passedSelectedCalendars,
|
||||
allowFallbackToPrimary
|
||||
);
|
||||
selectedCalendars: passedSelectedCalendars,
|
||||
mode,
|
||||
fallbackToPrimary: allowFallbackToPrimary,
|
||||
});
|
||||
performance.mark("eventBusyDatesEnd");
|
||||
performance.measure(
|
||||
`[getAvailability for ${selectedCalendarIds.join(", ")}][$1]'`,
|
||||
|
||||
@@ -455,7 +455,7 @@ const handleDeleteCredential = async ({
|
||||
// If it's a calendar remove it from the SelectedCalendars
|
||||
if (credential.app?.categories.includes(AppCategories.calendar)) {
|
||||
try {
|
||||
const calendar = await getCalendar(buildNonDelegationCredential(credential));
|
||||
const calendar = await getCalendar(buildNonDelegationCredential(credential), "slots");
|
||||
|
||||
const calendars = await calendar?.listCalendars();
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import type {
|
||||
CalendarEvent,
|
||||
CalendarEventType,
|
||||
EventBusyDate,
|
||||
GetAvailabilityParams,
|
||||
IntegrationCalendar,
|
||||
NewCalendarEventType,
|
||||
TeamMember,
|
||||
@@ -358,11 +359,8 @@ export default abstract class BaseCalendarService implements Calendar {
|
||||
return true;
|
||||
};
|
||||
|
||||
async getAvailability(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[]
|
||||
): Promise<EventBusyDate[]> {
|
||||
async getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
|
||||
const { dateFrom, dateTo, selectedCalendars } = params;
|
||||
const startISOString = new Date(dateFrom).toISOString();
|
||||
|
||||
const objects = await this.fetchObjectsWithOptionalExpand({
|
||||
|
||||
@@ -1740,7 +1740,7 @@ type GetAvailabilityMethodMockCall = CalendarServiceMethodMockCallBase & {
|
||||
dateFrom: string;
|
||||
dateTo: string;
|
||||
selectedCalendars: IntegrationCalendar[];
|
||||
shouldServeCache?: boolean;
|
||||
mode: "slots" | "overlay" | "booking";
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1941,12 +1941,14 @@ export async function mockCalendar(
|
||||
},
|
||||
});
|
||||
},
|
||||
getAvailability: async (
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[],
|
||||
shouldServeCache?: boolean
|
||||
): Promise<EventBusyDate[]> => {
|
||||
getAvailability: async (params: {
|
||||
dateFrom: string;
|
||||
dateTo: string;
|
||||
selectedCalendars: IntegrationCalendar[];
|
||||
mode: "slots" | "overlay" | "booking";
|
||||
fallbackToPrimary?: boolean;
|
||||
}): Promise<EventBusyDate[]> => {
|
||||
const { dateFrom, dateTo, selectedCalendars, mode } = params;
|
||||
if (calendarData?.getAvailabilityCrash) {
|
||||
throw new Error("MockCalendarService.getAvailability fake error");
|
||||
}
|
||||
@@ -1955,7 +1957,7 @@ export async function mockCalendar(
|
||||
dateFrom,
|
||||
dateTo,
|
||||
selectedCalendars,
|
||||
shouldServeCache,
|
||||
mode,
|
||||
},
|
||||
calendarServiceConstructorArgs: {
|
||||
credential,
|
||||
@@ -1965,6 +1967,9 @@ export async function mockCalendar(
|
||||
resolve(calendarData?.busySlots || []);
|
||||
});
|
||||
},
|
||||
listCalendars: async function (): Promise<IntegrationCalendar[]> {
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
} as Calendar;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -83,12 +83,14 @@ export const calendarOverlayHandler = async ({ ctx, input }: ListOptions) => {
|
||||
};
|
||||
});
|
||||
|
||||
// get all clanedar services
|
||||
// get all calendar services
|
||||
// Use "overlay" mode to bypass cache for overlay calendar availability
|
||||
const calendarBusyTimesQuery = await getBusyCalendarTimes(
|
||||
credentials,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
composedSelectedCalendars
|
||||
composedSelectedCalendars,
|
||||
"overlay"
|
||||
);
|
||||
|
||||
if (!calendarBusyTimesQuery.success) {
|
||||
|
||||
@@ -52,7 +52,7 @@ import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { withReporting } from "@calcom/lib/sentryWrapper";
|
||||
import type { RoutingFormResponseRepository } from "@calcom/lib/server/repository/formResponse";
|
||||
import { PeriodType, SchedulingType } from "@calcom/prisma/enums";
|
||||
import type { EventBusyDate, EventBusyDetails } from "@calcom/types/Calendar";
|
||||
import type { CalendarFetchMode, EventBusyDate, EventBusyDetails } from "@calcom/types/Calendar";
|
||||
import type { CredentialForCalendarService } from "@calcom/types/Credential";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import type { Logger } from "tslog";
|
||||
@@ -786,7 +786,7 @@ export class AvailableSlotsService {
|
||||
endTime,
|
||||
bypassBusyCalendarTimes,
|
||||
silentCalendarFailures,
|
||||
shouldServeCache,
|
||||
mode,
|
||||
}: {
|
||||
input: TGetScheduleInputSchema;
|
||||
eventType: Exclude<
|
||||
@@ -803,7 +803,7 @@ export class AvailableSlotsService {
|
||||
endTime: Dayjs;
|
||||
bypassBusyCalendarTimes: boolean;
|
||||
silentCalendarFailures: boolean;
|
||||
shouldServeCache?: boolean;
|
||||
mode?: CalendarFetchMode;
|
||||
}) {
|
||||
const usersWithCredentials = this.getUsersWithCredentials({
|
||||
hosts,
|
||||
@@ -940,7 +940,7 @@ export class AvailableSlotsService {
|
||||
returnDateOverrides: false,
|
||||
bypassBusyCalendarTimes,
|
||||
silentlyHandleCalendarFailures: silentCalendarFailures,
|
||||
shouldServeCache,
|
||||
mode,
|
||||
},
|
||||
initialData: {
|
||||
eventType,
|
||||
@@ -1032,7 +1032,8 @@ export class AvailableSlotsService {
|
||||
throw new TRPCError({ code: "NOT_FOUND" });
|
||||
}
|
||||
|
||||
const shouldServeCache = false;
|
||||
// Use "slots" mode to enable cache when available for getting calendar availability
|
||||
const mode: CalendarFetchMode = "slots";
|
||||
if (isEventTypeLoggingEnabled({ eventTypeId: eventType.id })) {
|
||||
logger.settings.minLevel = 2;
|
||||
}
|
||||
@@ -1141,7 +1142,7 @@ export class AvailableSlotsService {
|
||||
: endTime,
|
||||
bypassBusyCalendarTimes,
|
||||
silentCalendarFailures,
|
||||
shouldServeCache,
|
||||
mode,
|
||||
});
|
||||
|
||||
let aggregatedAvailability = getAggregatedAvailability(allUsersAvailability, eventType.schedulingType);
|
||||
@@ -1168,7 +1169,7 @@ export class AvailableSlotsService {
|
||||
endTime: twoWeeksFromNow,
|
||||
bypassBusyCalendarTimes,
|
||||
silentCalendarFailures,
|
||||
shouldServeCache,
|
||||
mode,
|
||||
});
|
||||
if (
|
||||
!getAggregatedAvailability(
|
||||
@@ -1204,7 +1205,7 @@ export class AvailableSlotsService {
|
||||
endTime,
|
||||
bypassBusyCalendarTimes,
|
||||
silentCalendarFailures,
|
||||
shouldServeCache,
|
||||
mode,
|
||||
}));
|
||||
aggregatedAvailability = getAggregatedAvailability(allUsersAvailability, eventType.schedulingType);
|
||||
}
|
||||
|
||||
Vendored
+23
-22
@@ -57,6 +57,7 @@ export type EventBusyDate = {
|
||||
start: Date | string;
|
||||
end: Date | string;
|
||||
source?: string | null;
|
||||
timeZone?: string;
|
||||
};
|
||||
|
||||
export type EventBusyDetails = EventBusyDate & {
|
||||
@@ -255,6 +256,26 @@ export interface IntegrationCalendar extends Ensure<Partial<_SelectedCalendar>,
|
||||
integrationTitle?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mode for calendar fetch operations to control caching behavior:
|
||||
* - "slots": For getting actual calendar availability (uses cache when available)
|
||||
* - "overlay": For getting overlay calendar availability (does not use cache)
|
||||
* - "booking": For booking confirmation (does not use cache)
|
||||
* - "none": For operations that don't use getAvailability (e.g., deleteEvent, listCalendars)
|
||||
*/
|
||||
export type CalendarFetchMode = "slots" | "overlay" | "booking" | "none";
|
||||
|
||||
/**
|
||||
* Parameters for getAvailability and getAvailabilityWithTimeZones methods
|
||||
*/
|
||||
export interface GetAvailabilityParams {
|
||||
dateFrom: string;
|
||||
dateTo: string;
|
||||
selectedCalendars: IntegrationCalendar[];
|
||||
mode: CalendarFetchMode;
|
||||
fallbackToPrimary?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* null is to refer to user-level SelectedCalendar
|
||||
*/
|
||||
@@ -280,36 +301,16 @@ export interface Calendar {
|
||||
|
||||
deleteEvent(uid: string, event: CalendarEvent, externalCalendarId?: string | null): Promise<unknown>;
|
||||
|
||||
getAvailability(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[],
|
||||
shouldServeCache?: boolean,
|
||||
fallbackToPrimary?: boolean
|
||||
): Promise<EventBusyDate[]>;
|
||||
getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]>;
|
||||
|
||||
// for OOO calibration (only google calendar for now)
|
||||
getAvailabilityWithTimeZones?(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[],
|
||||
fallbackToPrimary?: boolean
|
||||
): Promise<{ start: Date | string; end: Date | string; timeZone: string }[]>;
|
||||
getAvailabilityWithTimeZones?(params: GetAvailabilityParams): Promise<EventBusyDate[]>;
|
||||
|
||||
fetchAvailabilityAndSetCache?(selectedCalendars: IntegrationCalendar[]): Promise<unknown>;
|
||||
|
||||
listCalendars(event?: CalendarEvent): Promise<IntegrationCalendar[]>;
|
||||
|
||||
testDelegationCredentialSetup?(): Promise<boolean>;
|
||||
|
||||
watchCalendar?(options: {
|
||||
calendarId: string;
|
||||
eventTypeIds: SelectedCalendarEventTypeIds;
|
||||
}): Promise<unknown>;
|
||||
unwatchCalendar?(options: {
|
||||
calendarId: string;
|
||||
eventTypeIds: SelectedCalendarEventTypeIds;
|
||||
}): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user