feat: allows forcing/skipping calendar cache serving (#18224)
* feat: allows forcing/skipping calendar cache serving Signed-off-by: Omar López <zomars@me.com> * Update features.repository.ts * Added to HNB * type fixes * Update packages/prisma/migrations/20241216000000_add_calendar_cache_serve/migration.sql Co-authored-by: Keith Williams <keithwillcode@gmail.com> * Update packages/prisma/zod-utils.ts Co-authored-by: Keith Williams <keithwillcode@gmail.com> * Update selectedCalendar.ts --------- Signed-off-by: Omar López <zomars@me.com> Co-authored-by: Keith Williams <keithwillcode@gmail.com>
This commit is contained in:
co-authored by
Keith Williams
parent
a0f986f949
commit
2eb385af48
@@ -10,6 +10,7 @@ import { MeetLocationType } from "@calcom/app-store/locations";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { CalendarCache } from "@calcom/features/calendar-cache/calendar-cache";
|
||||
import type { FreeBusyArgs } from "@calcom/features/calendar-cache/calendar-cache.repository.interface";
|
||||
import { getTimeMax, getTimeMin } from "@calcom/features/calendar-cache/lib/datesForCache";
|
||||
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
|
||||
import type CalendarService from "@calcom/lib/CalendarService";
|
||||
import {
|
||||
@@ -510,16 +511,22 @@ export default class GoogleCalendarService implements Calendar {
|
||||
return apiResponse.json;
|
||||
}
|
||||
|
||||
async getCacheOrFetchAvailability(args: FreeBusyArgs): Promise<EventBusyDate[] | null> {
|
||||
const { timeMin, timeMax, items } = args;
|
||||
let freeBusyResult: calendar_v3.Schema$FreeBusyResponse = {};
|
||||
async getFreeBusyResult(
|
||||
args: FreeBusyArgs,
|
||||
shouldServeCache?: boolean
|
||||
): Promise<calendar_v3.Schema$FreeBusyResponse> {
|
||||
if (shouldServeCache === false) return await this.fetchAvailability(args);
|
||||
const calendarCache = await CalendarCache.init(null);
|
||||
const cached = await calendarCache.getCachedAvailability(this.credential.id, args);
|
||||
if (cached) {
|
||||
freeBusyResult = cached.value as unknown as calendar_v3.Schema$FreeBusyResponse;
|
||||
} else {
|
||||
freeBusyResult = await this.fetchAvailability({ timeMin, timeMax, items });
|
||||
}
|
||||
if (cached) return cached.value as unknown as calendar_v3.Schema$FreeBusyResponse;
|
||||
return await this.fetchAvailability(args);
|
||||
}
|
||||
|
||||
async getCacheOrFetchAvailability(
|
||||
args: FreeBusyArgs,
|
||||
shouldServeCache?: boolean
|
||||
): Promise<EventBusyDate[] | null> {
|
||||
const freeBusyResult = await this.getFreeBusyResult(args, shouldServeCache);
|
||||
if (!freeBusyResult.calendars) return null;
|
||||
|
||||
const result = Object.values(freeBusyResult.calendars).reduce((c, i) => {
|
||||
@@ -537,7 +544,8 @@ export default class GoogleCalendarService implements Calendar {
|
||||
async getAvailability(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[]
|
||||
selectedCalendars: IntegrationCalendar[],
|
||||
shouldServeCache?: boolean
|
||||
): Promise<EventBusyDate[]> {
|
||||
this.log.debug("Getting availability", safeStringify({ dateFrom, dateTo, selectedCalendars }));
|
||||
const calendar = await this.authedCalendar();
|
||||
@@ -563,11 +571,14 @@ export default class GoogleCalendarService implements Calendar {
|
||||
|
||||
// /freebusy from google api only allows a date range of 90 days
|
||||
if (diff <= 90) {
|
||||
const freeBusyData = await this.getCacheOrFetchAvailability({
|
||||
timeMin: dateFrom,
|
||||
timeMax: dateTo,
|
||||
items: calsIds.map((id) => ({ id })),
|
||||
});
|
||||
const freeBusyData = await this.getCacheOrFetchAvailability(
|
||||
{
|
||||
timeMin: dateFrom,
|
||||
timeMax: dateTo,
|
||||
items: calsIds.map((id) => ({ id })),
|
||||
},
|
||||
shouldServeCache
|
||||
);
|
||||
if (!freeBusyData) throw new Error("No response from google calendar");
|
||||
|
||||
return freeBusyData;
|
||||
@@ -583,11 +594,14 @@ export default class GoogleCalendarService implements Calendar {
|
||||
if (endDate.isAfter(originalEndDate)) endDate = originalEndDate;
|
||||
|
||||
busyData.push(
|
||||
...((await this.getCacheOrFetchAvailability({
|
||||
timeMin: startDate.format(),
|
||||
timeMax: endDate.format(),
|
||||
items: calsIds.map((id) => ({ id })),
|
||||
})) || [])
|
||||
...((await this.getCacheOrFetchAvailability(
|
||||
{
|
||||
timeMin: startDate.format(),
|
||||
timeMax: endDate.format(),
|
||||
items: calsIds.map((id) => ({ id })),
|
||||
},
|
||||
shouldServeCache
|
||||
)) || [])
|
||||
);
|
||||
|
||||
startDate = endDate.add(1, "minutes");
|
||||
@@ -672,12 +686,7 @@ export default class GoogleCalendarService implements Calendar {
|
||||
}
|
||||
async unwatchCalendar({ calendarId }: { calendarId: string }) {
|
||||
const credentialId = this.credential.id;
|
||||
const sc = await prisma.selectedCalendar.findFirst({
|
||||
where: {
|
||||
credentialId,
|
||||
externalId: calendarId,
|
||||
},
|
||||
});
|
||||
const sc = await SelectedCalendarRepository.findByExternalId(credentialId, calendarId);
|
||||
// Delete the calendar cache to force a fresh cache
|
||||
await prisma.calendarCache.deleteMany({ where: { credentialId } });
|
||||
const calendar = await this.authedCalendar();
|
||||
@@ -699,6 +708,12 @@ export default class GoogleCalendarService implements Calendar {
|
||||
googleChannelResourceUri: null,
|
||||
googleChannelExpiration: null,
|
||||
});
|
||||
// Populate the cache back for the remaining calendars, if any
|
||||
const remainingCalendars =
|
||||
sc?.credential?.selectedCalendars.filter((sc) => sc.externalId !== calendarId) || [];
|
||||
if (remainingCalendars.length > 0) {
|
||||
await this.fetchAvailabilityAndSetCache(remainingCalendars);
|
||||
}
|
||||
}
|
||||
|
||||
async setAvailabilityInCache(args: FreeBusyArgs, data: calendar_v3.Schema$FreeBusyResponse): Promise<void> {
|
||||
@@ -707,12 +722,11 @@ export default class GoogleCalendarService implements Calendar {
|
||||
}
|
||||
|
||||
async fetchAvailabilityAndSetCache(selectedCalendars: IntegrationCalendar[]) {
|
||||
const date = new Date();
|
||||
const parsedArgs = {
|
||||
/** Expand the start date to the start of the month */
|
||||
timeMin: new Date(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0).toISOString(),
|
||||
timeMin: getTimeMax(),
|
||||
/** Expand the end date to the end of the month */
|
||||
timeMax: new Date(date.getFullYear(), date.getMonth() + 1, 0, 0, 0, 0, 0).toISOString(),
|
||||
timeMax: getTimeMin(),
|
||||
items: selectedCalendars.map((sc) => ({ id: sc.externalId })),
|
||||
};
|
||||
const data = await this.fetchAvailability(parsedArgs);
|
||||
|
||||
@@ -8,7 +8,6 @@ import { getUid } from "@calcom/lib/CalEventParser";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { getPiiFreeCalendarEvent, getPiiFreeCredential } from "@calcom/lib/piiFreeData";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { performance } from "@calcom/lib/server/perfObserver";
|
||||
import type {
|
||||
CalendarEvent,
|
||||
EventBusyDate,
|
||||
@@ -136,51 +135,6 @@ const cleanIntegrationKeys = (
|
||||
return rest;
|
||||
};
|
||||
|
||||
// here I will fetch the page json file.
|
||||
export const getCachedResults = async (
|
||||
withCredentials: CredentialPayload[],
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: SelectedCalendar[]
|
||||
): Promise<EventBusyDate[][]> => {
|
||||
const calendarCredentials = withCredentials.filter((credential) => credential.type.endsWith("_calendar"));
|
||||
const calendars = await Promise.all(calendarCredentials.map((credential) => getCalendar(credential)));
|
||||
performance.mark("getBusyCalendarTimesStart");
|
||||
const results = calendars.map(async (c, i) => {
|
||||
/** Filter out nulls */
|
||||
if (!c) return [];
|
||||
/** We rely on the index so we can match credentials with calendars */
|
||||
const { type, appId } = calendarCredentials[i];
|
||||
/** We just pass the calendars that matched the credential type,
|
||||
* TODO: Migrate credential type or appId
|
||||
*/
|
||||
const passedSelectedCalendars = selectedCalendars.filter((sc) => sc.integration === type);
|
||||
if (!passedSelectedCalendars.length) return [];
|
||||
/** We extract external Ids so we don't cache too much */
|
||||
const selectedCalendarIds = passedSelectedCalendars.map((sc) => sc.externalId);
|
||||
/** If we don't then we actually fetch external calendars (which can be very slow) */
|
||||
performance.mark("eventBusyDatesStart");
|
||||
const eventBusyDates = await c.getAvailability(dateFrom, dateTo, passedSelectedCalendars);
|
||||
performance.mark("eventBusyDatesEnd");
|
||||
performance.measure(
|
||||
`[getAvailability for ${selectedCalendarIds.join(", ")}][$1]'`,
|
||||
"eventBusyDatesStart",
|
||||
"eventBusyDatesEnd"
|
||||
);
|
||||
|
||||
return eventBusyDates.map((a: object) => ({ ...a, source: `${appId}` }));
|
||||
});
|
||||
const awaitedResults = await Promise.all(results);
|
||||
performance.mark("getBusyCalendarTimesEnd");
|
||||
performance.measure(
|
||||
`getBusyCalendarTimes took $1 for creds ${calendarCredentials.map((cred) => cred.id)}`,
|
||||
"getBusyCalendarTimesStart",
|
||||
"getBusyCalendarTimesEnd"
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return awaitedResults as any;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get months between given dates
|
||||
* @returns ["2023-04", "2024-05"]
|
||||
@@ -203,7 +157,8 @@ export const getBusyCalendarTimes = async (
|
||||
withCredentials: CredentialPayload[],
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: SelectedCalendar[]
|
||||
selectedCalendars: SelectedCalendar[],
|
||||
shouldServeCache?: boolean
|
||||
) => {
|
||||
let results: EventBusyDate[][] = [];
|
||||
// const months = getMonths(dateFrom, dateTo);
|
||||
@@ -212,7 +167,13 @@ export const getBusyCalendarTimes = async (
|
||||
const startDate = dayjs(dateFrom).subtract(11, "hours").format();
|
||||
// Add 14 hours from the start date to avoid problems in UTC+ time zones.
|
||||
const endDate = dayjs(dateTo).endOf("month").add(14, "hours").format();
|
||||
results = await getCalendarsEvents(withCredentials, startDate, endDate, selectedCalendars);
|
||||
results = await getCalendarsEvents(
|
||||
withCredentials,
|
||||
startDate,
|
||||
endDate,
|
||||
selectedCalendars,
|
||||
shouldServeCache
|
||||
);
|
||||
} catch (e) {
|
||||
log.warn(safeStringify(e));
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ export async function getBusyTimes(params: {
|
||||
})[]
|
||||
| null;
|
||||
bypassBusyCalendarTimes: boolean;
|
||||
shouldServeCache?: boolean;
|
||||
}) {
|
||||
const {
|
||||
credentials,
|
||||
@@ -60,6 +61,7 @@ export async function getBusyTimes(params: {
|
||||
rescheduleUid,
|
||||
duration,
|
||||
bypassBusyCalendarTimes = false,
|
||||
shouldServeCache,
|
||||
} = params;
|
||||
|
||||
logger.silly(
|
||||
@@ -243,7 +245,8 @@ export async function getBusyTimes(params: {
|
||||
credentials,
|
||||
startTime,
|
||||
endTime,
|
||||
selectedCalendars
|
||||
selectedCalendars,
|
||||
shouldServeCache
|
||||
);
|
||||
const endConnectedCalendarsGet = performance.now();
|
||||
logger.debug(
|
||||
|
||||
@@ -127,7 +127,12 @@ describe("getCalendarsEvents", () => {
|
||||
[selectedCalendar]
|
||||
);
|
||||
|
||||
expect(getAvailabilitySpy).toHaveBeenCalledWith("2010-12-01", "2010-12-04", [selectedCalendar]);
|
||||
expect(getAvailabilitySpy).toHaveBeenCalledWith(
|
||||
"2010-12-01",
|
||||
"2010-12-04",
|
||||
[selectedCalendar],
|
||||
undefined
|
||||
);
|
||||
expect(result).toEqual([
|
||||
availability.map((av) => ({
|
||||
...av,
|
||||
@@ -190,12 +195,18 @@ describe("getCalendarsEvents", () => {
|
||||
[selectedGoogleCalendar, selectedOfficeCalendar]
|
||||
);
|
||||
|
||||
expect(getGoogleAvailabilitySpy).toHaveBeenCalledWith("2010-12-01", "2010-12-04", [
|
||||
selectedGoogleCalendar,
|
||||
]);
|
||||
expect(getOfficeAvailabilitySpy).toHaveBeenCalledWith("2010-12-01", "2010-12-04", [
|
||||
selectedOfficeCalendar,
|
||||
]);
|
||||
expect(getGoogleAvailabilitySpy).toHaveBeenCalledWith(
|
||||
"2010-12-01",
|
||||
"2010-12-04",
|
||||
[selectedGoogleCalendar],
|
||||
undefined
|
||||
);
|
||||
expect(getOfficeAvailabilitySpy).toHaveBeenCalledWith(
|
||||
"2010-12-01",
|
||||
"2010-12-04",
|
||||
[selectedOfficeCalendar],
|
||||
undefined
|
||||
);
|
||||
expect(result).toEqual([
|
||||
googleAvailability.map((av) => ({
|
||||
...av,
|
||||
|
||||
@@ -11,7 +11,8 @@ const getCalendarsEvents = async (
|
||||
withCredentials: CredentialPayload[],
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: SelectedCalendar[]
|
||||
selectedCalendars: SelectedCalendar[],
|
||||
shouldServeCache?: boolean
|
||||
): Promise<EventBusyDate[][]> => {
|
||||
const calendarCredentials = withCredentials
|
||||
.filter((credential) => credential.type.endsWith("_calendar"))
|
||||
@@ -45,7 +46,12 @@ const getCalendarsEvents = async (
|
||||
selectedCalendars: passedSelectedCalendars.map(getPiiFreeSelectedCalendar),
|
||||
})
|
||||
);
|
||||
const eventBusyDates = await c.getAvailability(dateFrom, dateTo, passedSelectedCalendars);
|
||||
const eventBusyDates = await c.getAvailability(
|
||||
dateFrom,
|
||||
dateTo,
|
||||
passedSelectedCalendars,
|
||||
shouldServeCache
|
||||
);
|
||||
performance.mark("eventBusyDatesEnd");
|
||||
performance.measure(
|
||||
`[getAvailability for ${selectedCalendarIds.join(", ")}][$1]'`,
|
||||
|
||||
@@ -45,6 +45,7 @@ const availabilitySchema = z
|
||||
withSource: z.boolean().optional(),
|
||||
returnDateOverrides: z.boolean(),
|
||||
bypassBusyCalendarTimes: z.boolean().optional(),
|
||||
shouldServeCache: z.boolean().optional(),
|
||||
})
|
||||
.refine((data) => !!data.username || !!data.userId, "Either username or userId should be filled in.");
|
||||
|
||||
@@ -245,6 +246,7 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
|
||||
duration?: number;
|
||||
returnDateOverrides: boolean;
|
||||
bypassBusyCalendarTimes: boolean;
|
||||
shouldServeCache?: boolean;
|
||||
},
|
||||
initialData?: {
|
||||
user?: GetUser;
|
||||
@@ -279,6 +281,7 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
|
||||
duration,
|
||||
returnDateOverrides,
|
||||
bypassBusyCalendarTimes = false,
|
||||
shouldServeCache,
|
||||
} = availabilitySchema.parse(query);
|
||||
|
||||
if (!dateFrom.isValid() || !dateTo.isValid()) {
|
||||
@@ -393,6 +396,7 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
|
||||
duration,
|
||||
currentBookings: initialData?.currentBookings,
|
||||
bypassBusyCalendarTimes,
|
||||
shouldServeCache,
|
||||
});
|
||||
|
||||
const detailedBusyTimes: EventBusyDetails[] = [
|
||||
|
||||
@@ -53,6 +53,8 @@ export const mapBookingToMutationInput = ({
|
||||
const skipContactOwner = searchParams.get("cal.skipContactOwner") === "true";
|
||||
const reroutingFormResponses = searchParams.get("cal.reroutingFormResponses");
|
||||
const isBookingDryRun = searchParams.get("cal.isBookingDryRun") === "true";
|
||||
const _cacheParam = searchParams?.get("cal.cache");
|
||||
const _shouldServeCache = _cacheParam ? _cacheParam === "true" : undefined;
|
||||
|
||||
return {
|
||||
...values,
|
||||
@@ -83,6 +85,7 @@ export const mapBookingToMutationInput = ({
|
||||
// In case of rerouting, the form responses are actually the responses that we need to update.
|
||||
reroutingFormResponses: reroutingFormResponses ? JSON.parse(reroutingFormResponses) : undefined,
|
||||
_isDryRun: isBookingDryRun,
|
||||
_shouldServeCache,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import getICalUID from "@calcom/emails/lib/getICalUID";
|
||||
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
||||
import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger";
|
||||
import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled";
|
||||
import { getShouldServeCache } from "@calcom/features/calendar-cache/lib/getShouldServeCache";
|
||||
import AssignmentReasonRecorder from "@calcom/features/ee/round-robin/assignmentReason/AssignmentReasonRecorder";
|
||||
import {
|
||||
allowDisablingAttendeeConfirmationEmails,
|
||||
@@ -393,6 +394,7 @@ async function handler(
|
||||
reroutingFormResponses,
|
||||
routingFormResponseId,
|
||||
_isDryRun: isDryRun = false,
|
||||
_shouldServeCache,
|
||||
...reqBody
|
||||
} = bookingData;
|
||||
|
||||
@@ -414,6 +416,7 @@ async function handler(
|
||||
|
||||
const dynamicUserList = Array.isArray(reqBody.user) ? reqBody.user : getUsernameList(reqBody.user);
|
||||
if (!eventType) throw new HttpError({ statusCode: 404, message: "event_type_not_found" });
|
||||
const shouldServeCache = await getShouldServeCache(_shouldServeCache, eventType.team?.id);
|
||||
|
||||
const isTeamEventType =
|
||||
!!eventType.schedulingType && ["COLLECTIVE", "ROUND_ROBIN"].includes(eventType.schedulingType);
|
||||
@@ -569,7 +572,8 @@ async function handler(
|
||||
timeZone: reqBody.timeZone,
|
||||
originalRescheduledBooking,
|
||||
},
|
||||
loggerWithEventDetails
|
||||
loggerWithEventDetails,
|
||||
shouldServeCache
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -581,7 +585,8 @@ async function handler(
|
||||
timeZone: reqBody.timeZone,
|
||||
originalRescheduledBooking,
|
||||
},
|
||||
loggerWithEventDetails
|
||||
loggerWithEventDetails,
|
||||
shouldServeCache
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -596,7 +601,8 @@ async function handler(
|
||||
timeZone: reqBody.timeZone,
|
||||
originalRescheduledBooking,
|
||||
},
|
||||
loggerWithEventDetails
|
||||
loggerWithEventDetails,
|
||||
shouldServeCache
|
||||
);
|
||||
const luckyUserPool: IsFixedAwareUser[] = [];
|
||||
const fixedUserPool: IsFixedAwareUser[] = [];
|
||||
@@ -696,7 +702,8 @@ async function handler(
|
||||
timeZone: reqBody.timeZone,
|
||||
originalRescheduledBooking,
|
||||
},
|
||||
loggerWithEventDetails
|
||||
loggerWithEventDetails,
|
||||
shouldServeCache
|
||||
);
|
||||
}
|
||||
// if no error, then lucky user is available for the next slots
|
||||
|
||||
@@ -52,7 +52,8 @@ export async function ensureAvailableUsers(
|
||||
users: IsFixedAwareUser[];
|
||||
},
|
||||
input: { dateFrom: string; dateTo: string; timeZone: string; originalRescheduledBooking?: BookingType },
|
||||
loggerWithEventDetails: Logger<unknown>
|
||||
loggerWithEventDetails: Logger<unknown>,
|
||||
shouldServeCache?: boolean
|
||||
// ReturnType hint of at least one IsFixedAwareUser, as it's made sure at least one entry exists
|
||||
): Promise<[IsFixedAwareUser, ...IsFixedAwareUser[]]> {
|
||||
const availableUsers: IsFixedAwareUser[] = [];
|
||||
@@ -91,6 +92,7 @@ export async function ensureAvailableUsers(
|
||||
beforeEventBuffer: eventType.beforeEventBuffer,
|
||||
afterEventBuffer: eventType.afterEventBuffer,
|
||||
bypassBusyCalendarTimes: false,
|
||||
shouldServeCache,
|
||||
},
|
||||
initialData: {
|
||||
eventType,
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { Calendar } from "@calcom/types/Calendar";
|
||||
|
||||
import type { ICalendarCacheRepository } from "./calendar-cache.repository.interface";
|
||||
import { watchCalendarSchema } from "./calendar-cache.repository.schema";
|
||||
import { getTimeMax, getTimeMin } from "./lib/datesForCache";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["CalendarCacheRepository"] });
|
||||
|
||||
@@ -15,18 +16,6 @@ const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
||||
const ONE_MONTH_IN_MS = 30 * MS_PER_DAY;
|
||||
const CACHING_TIME = ONE_MONTH_IN_MS;
|
||||
|
||||
/** Expand the start date to the start of the month */
|
||||
function getTimeMin(timeMin: string) {
|
||||
const dateMin = new Date(timeMin);
|
||||
return new Date(dateMin.getFullYear(), dateMin.getMonth(), 1, 0, 0, 0, 0).toISOString();
|
||||
}
|
||||
|
||||
/** Expand the end date to the end of the month */
|
||||
function getTimeMax(timeMax: string) {
|
||||
const dateMax = new Date(timeMax);
|
||||
return new Date(dateMax.getFullYear(), dateMax.getMonth() + 1, 0, 0, 0, 0, 0).toISOString();
|
||||
}
|
||||
|
||||
export function parseKeyForCache(args: FreeBusyArgs): string {
|
||||
const { timeMin: _timeMin, timeMax: _timeMax, items } = args;
|
||||
const { timeMin, timeMax } = handleMinMax(_timeMin, _timeMax);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/** Expand the start date to the beginning of the current month */
|
||||
export const getTimeMin = (timeMin?: string) => {
|
||||
const date = timeMin ? new Date(timeMin) : new Date();
|
||||
date.setDate(1);
|
||||
date.setHours(0, 0, 0, 0);
|
||||
return date.toISOString();
|
||||
};
|
||||
|
||||
/** Expand the end date to the end of the next month */
|
||||
export function getTimeMax(timeMax?: string) {
|
||||
const date = timeMax ? new Date(timeMax) : new Date();
|
||||
date.setMonth(date.getMonth() + 2);
|
||||
date.setDate(0);
|
||||
date.setHours(0, 0, 0, 0);
|
||||
return date.toISOString();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
|
||||
export async function getShouldServeCache(shouldServeCache?: boolean | undefined, teamId?: number) {
|
||||
if (typeof shouldServeCache === "boolean") return shouldServeCache;
|
||||
if (!teamId) return undefined;
|
||||
const featureRepo = new FeaturesRepository();
|
||||
return await featureRepo.checkIfTeamHasFeature(teamId, "calendar-cache-serve");
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
**/
|
||||
export type AppFlags = {
|
||||
"calendar-cache": boolean;
|
||||
"calendar-cache-serve": boolean;
|
||||
emails: boolean;
|
||||
insights: boolean;
|
||||
teams: boolean;
|
||||
|
||||
@@ -3,4 +3,5 @@ import type { AppFlags } from "./config";
|
||||
export interface IFeaturesRepository {
|
||||
checkIfFeatureIsEnabledGlobally(slug: keyof AppFlags): Promise<boolean>;
|
||||
checkIfUserHasFeature(userId: number, slug: string): Promise<boolean>;
|
||||
checkIfTeamHasFeature(teamId: number, slug: keyof AppFlags): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ export class MockFeaturesRepository implements IFeaturesRepository {
|
||||
async checkIfUserHasFeature(userId: number, slug: string) {
|
||||
return slug === "mock-feature";
|
||||
}
|
||||
async checkIfTeamHasFeature(team: number, slug: string) {
|
||||
return slug === "mock-feature";
|
||||
}
|
||||
async checkIfFeatureIsEnabledGlobally() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -65,4 +65,15 @@ export class FeaturesRepository implements IFeaturesRepository {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
async checkIfTeamHasFeature(teamId: number, featureId: keyof AppFlags) {
|
||||
try {
|
||||
const teamFeature = await db.teamFeatures.findUnique({
|
||||
where: { teamId_featureId: { teamId, featureId } },
|
||||
});
|
||||
return !!teamFeature;
|
||||
} catch (err) {
|
||||
captureException(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ const initialData: AppFlags = {
|
||||
organizations: false,
|
||||
teams: false,
|
||||
"calendar-cache": false,
|
||||
"calendar-cache-serve": false,
|
||||
emails: false,
|
||||
insights: false,
|
||||
webhooks: false,
|
||||
|
||||
@@ -48,6 +48,8 @@ export const useSchedule = ({
|
||||
const searchParams = useSearchParams();
|
||||
const routedTeamMemberIds = searchParams ? getRoutedTeamMemberIdsFromSearchParams(searchParams) : null;
|
||||
const skipContactOwner = searchParams ? searchParams.get("cal.skipContactOwner") === "true" : false;
|
||||
const _cacheParam = searchParams?.get("cal.cache");
|
||||
const shouldServeCache = _cacheParam ? _cacheParam === "true" : undefined;
|
||||
|
||||
const input = {
|
||||
isTeamEvent,
|
||||
@@ -68,6 +70,7 @@ export const useSchedule = ({
|
||||
teamMemberEmail,
|
||||
routedTeamMemberIds,
|
||||
skipContactOwner,
|
||||
shouldServeCache,
|
||||
};
|
||||
|
||||
const options = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
|
||||
@@ -59,26 +60,32 @@ export class SelectedCalendarRepository {
|
||||
}
|
||||
/** Retrieve calendars that are being watched but shouldn't be anymore */
|
||||
static async getNextBatchToUnwatch(limit = 100) {
|
||||
const nextBatch = await prisma.selectedCalendar.findMany({
|
||||
take: limit,
|
||||
where: {
|
||||
user: {
|
||||
teams: {
|
||||
every: {
|
||||
team: {
|
||||
features: {
|
||||
none: {
|
||||
featureId: "calendar-cache",
|
||||
},
|
||||
const where: Prisma.SelectedCalendarWhereInput = {
|
||||
// RN we only support google calendar subscriptions for now
|
||||
integration: "google_calendar",
|
||||
googleChannelExpiration: { not: null },
|
||||
};
|
||||
const featureRepo = new FeaturesRepository();
|
||||
const calendarCache = await featureRepo.checkIfFeatureIsEnabledGlobally("calendar-cache");
|
||||
// If calendar cache is disabled globally, we skip team features and unwatch all subscriptions
|
||||
if (!calendarCache) {
|
||||
where.user = {
|
||||
teams: {
|
||||
every: {
|
||||
team: {
|
||||
features: {
|
||||
none: {
|
||||
featureId: "calendar-cache",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// RN we only support google calendar subscriptions for now
|
||||
integration: "google_calendar",
|
||||
googleChannelExpiration: { not: null },
|
||||
},
|
||||
};
|
||||
}
|
||||
const nextBatch = await prisma.selectedCalendar.findMany({
|
||||
take: limit,
|
||||
where,
|
||||
});
|
||||
return nextBatch;
|
||||
}
|
||||
@@ -115,4 +122,25 @@ export class SelectedCalendarRepository {
|
||||
},
|
||||
});
|
||||
}
|
||||
static async findByExternalId(credentialId: number, externalId: string) {
|
||||
return await prisma.selectedCalendar.findFirst({
|
||||
where: {
|
||||
credentialId,
|
||||
externalId,
|
||||
},
|
||||
select: {
|
||||
googleChannelResourceId: true,
|
||||
googleChannelId: true,
|
||||
credential: {
|
||||
select: {
|
||||
selectedCalendars: {
|
||||
orderBy: {
|
||||
externalId: "asc",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
INSERT INTO
|
||||
"Feature" (slug, enabled, description, "type")
|
||||
VALUES
|
||||
(
|
||||
'calendar-cache-serve',
|
||||
false,
|
||||
'Whether to serve calendar cache by default or not on a team/user basis.',
|
||||
'OPERATIONAL'
|
||||
) ON CONFLICT (slug) DO NOTHING;
|
||||
@@ -265,6 +265,8 @@ export const bookingCreateBodySchema = z.object({
|
||||
* Used to identify if the booking is a dry run.
|
||||
*/
|
||||
_isDryRun: z.boolean().optional(),
|
||||
/** Whether to override the cache */
|
||||
_shouldServeCache: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const requiredCustomInputSchema = z.union([
|
||||
|
||||
@@ -29,6 +29,7 @@ export const getScheduleSchema = z
|
||||
skipContactOwner: z.boolean().nullish(),
|
||||
_enableTroubleshooter: z.boolean().optional(),
|
||||
_bypassCalendarBusyTimes: z.boolean().optional(),
|
||||
_shouldServeCache: z.boolean().optional(),
|
||||
})
|
||||
.transform((val) => {
|
||||
// Need this so we can pass a single username in the query string form public API
|
||||
|
||||
@@ -5,26 +5,27 @@ import { v4 as uuid } from "uuid";
|
||||
|
||||
import { getAggregatedAvailability } from "@calcom/core/getAggregatedAvailability";
|
||||
import { getBusyTimesForLimitChecks } from "@calcom/core/getBusyTimes";
|
||||
import type { CurrentSeats, IFromUser, IToUser, GetAvailabilityUser } from "@calcom/core/getUserAvailability";
|
||||
import type { CurrentSeats, GetAvailabilityUser, IFromUser, IToUser } from "@calcom/core/getUserAvailability";
|
||||
import { getUsersAvailability } from "@calcom/core/getUserAvailability";
|
||||
import monitorCallbackAsync, { monitorCallbackSync } from "@calcom/core/sentryWrapper";
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { getSlugOrRequestedSlug, orgDomainConfig } from "@calcom/ee/organizations/lib/orgDomains";
|
||||
import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled";
|
||||
import { getShouldServeCache } from "@calcom/features/calendar-cache/lib/getShouldServeCache";
|
||||
import { parseBookingLimit, parseDurationLimit } from "@calcom/lib";
|
||||
import { findQualifiedHosts } from "@calcom/lib/bookings/findQualifiedHosts";
|
||||
import {
|
||||
getRoutedHostsWithContactOwnerAndFixedHosts,
|
||||
findMatchingHostsWithEventSegment,
|
||||
getRoutedHostsWithContactOwnerAndFixedHosts,
|
||||
} from "@calcom/lib/bookings/getRoutedUsers";
|
||||
import { shouldIgnoreContactOwner, isRerouting } from "@calcom/lib/bookings/routing/utils";
|
||||
import { isRerouting, shouldIgnoreContactOwner } from "@calcom/lib/bookings/routing/utils";
|
||||
import { RESERVED_SUBDOMAINS } from "@calcom/lib/constants";
|
||||
import { getUTCOffsetByTimezone } from "@calcom/lib/date-fns";
|
||||
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
|
||||
import {
|
||||
isTimeOutOfBounds,
|
||||
calculatePeriodLimits,
|
||||
isTimeOutOfBounds,
|
||||
isTimeViolatingFutureLimit,
|
||||
} from "@calcom/lib/isOutOfBounds";
|
||||
import logger from "@calcom/lib/logger";
|
||||
@@ -33,8 +34,7 @@ import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import getSlots from "@calcom/lib/slots";
|
||||
import prisma, { availabilityUserSelect } from "@calcom/prisma";
|
||||
import { PeriodType, Prisma } from "@calcom/prisma/client";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { BookingStatus, SchedulingType } from "@calcom/prisma/enums";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import { EventTypeMetaDataSchema, rrSegmentQueryValueSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { EventBusyDate } from "@calcom/types/Calendar";
|
||||
@@ -430,6 +430,7 @@ async function _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise<I
|
||||
const {
|
||||
_enableTroubleshooter: enableTroubleshooter = false,
|
||||
_bypassCalendarBusyTimes: bypassBusyCalendarTimes = false,
|
||||
_shouldServeCache,
|
||||
} = input;
|
||||
const orgDetails = input?.orgSlug
|
||||
? {
|
||||
@@ -448,6 +449,7 @@ async function _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise<I
|
||||
throw new TRPCError({ code: "NOT_FOUND" });
|
||||
}
|
||||
|
||||
const shouldServeCache = await getShouldServeCache(_shouldServeCache, eventType.team?.id);
|
||||
if (isEventTypeLoggingEnabled({ eventTypeId: eventType.id })) {
|
||||
logger.settings.minLevel = 2;
|
||||
}
|
||||
@@ -515,6 +517,7 @@ async function _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise<I
|
||||
startTime,
|
||||
endTime,
|
||||
bypassBusyCalendarTimes,
|
||||
shouldServeCache,
|
||||
});
|
||||
|
||||
// If contact skipping, determine if there's availability within two weeks
|
||||
@@ -539,6 +542,7 @@ async function _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise<I
|
||||
startTime,
|
||||
endTime,
|
||||
bypassBusyCalendarTimes,
|
||||
shouldServeCache,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -1024,6 +1028,7 @@ const calculateHostsAndAvailabilities = async ({
|
||||
startTime,
|
||||
endTime,
|
||||
bypassBusyCalendarTimes,
|
||||
shouldServeCache,
|
||||
}: {
|
||||
input: TGetScheduleInputSchema;
|
||||
eventType: Exclude<Awaited<ReturnType<typeof getRegularOrDynamicEventType>>, null>;
|
||||
@@ -1036,6 +1041,7 @@ const calculateHostsAndAvailabilities = async ({
|
||||
startTime: ReturnType<typeof getStartTime>;
|
||||
endTime: Dayjs;
|
||||
bypassBusyCalendarTimes: boolean;
|
||||
shouldServeCache?: boolean;
|
||||
}) => {
|
||||
const routedTeamMemberIds = input.routedTeamMemberIds ?? null;
|
||||
if (
|
||||
@@ -1150,6 +1156,7 @@ const calculateHostsAndAvailabilities = async ({
|
||||
duration: input.duration || 0,
|
||||
returnDateOverrides: false,
|
||||
bypassBusyCalendarTimes,
|
||||
shouldServeCache,
|
||||
},
|
||||
initialData: {
|
||||
eventType,
|
||||
|
||||
Vendored
+2
-1
@@ -262,7 +262,8 @@ export interface Calendar {
|
||||
getAvailability(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[]
|
||||
selectedCalendars: IntegrationCalendar[],
|
||||
shouldServeCache?: boolean
|
||||
): Promise<EventBusyDate[]>;
|
||||
|
||||
fetchAvailabilityAndSetCache?(selectedCalendars: IntegrationCalendar[]): Promise<unknown>;
|
||||
|
||||
Reference in New Issue
Block a user