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:
Omar López
2024-12-17 16:54:55 +00:00
committed by GitHub
co-authored by Keith Williams
parent a0f986f949
commit 2eb385af48
24 changed files with 217 additions and 125 deletions
@@ -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);
+9 -48
View File
@@ -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));
}
+4 -1
View File
@@ -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(
+18 -7
View File
@@ -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,
+8 -2
View File
@@ -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]'`,
+4
View File
@@ -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");
}
+1
View File
@@ -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;
}
}
}
+1
View File
@@ -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;
+2
View File
@@ -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,
+2 -1
View File
@@ -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>;