fix: double end-date parsing when saving availability (#20963)

* fix: double end-date parsing when saving availablity

Signed-off-by: Omar López <zomars@me.com>

* fix: cache hits in current and next month

* Update CalendarService.test.ts

---------

Signed-off-by: Omar López <zomars@me.com>
This commit is contained in:
Omar López authored and GitHub committed 2025-04-24 19:33:55 -03:00
1 parent ce2d3a4b00
commit ca8ee749a2
6 files changed
+54 -35

No files matched your search

@@ -7,6 +7,7 @@ import { expect, test, beforeEach, vi, describe } from "vitest";
import "vitest-fetch-mock";
import { CalendarCache } from "@calcom/features/calendar-cache/calendar-cache";
import { getTimeMin, getTimeMax } from "@calcom/features/calendar-cache/lib/datesForCache";
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
import type { CredentialForCalendarServiceWithEmail } from "@calcom/types/Credential";
@@ -187,8 +188,8 @@ test("Calendar Cache is being read on cache HIT", async () => {
await calendarCache.upsertCachedAvailability(
credentialInDb1.id,
{
timeMin: dateFrom1,
timeMax: dateTo1,
timeMin: getTimeMin(dateFrom1),
timeMax: getTimeMax(dateTo1),
items: [{ id: testSelectedCalendar.externalId }],
},
JSON.parse(
@@ -663,7 +663,13 @@ export default class GoogleCalendarService implements Calendar {
): 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);
const cached = await calendarCache.getCachedAvailability(this.credential.id, {
// Expand the start date to the start of the month to increase cache hits
timeMin: getTimeMin(args.timeMin),
// Expand the end date to the end of the month to increase cache hits
timeMax: getTimeMax(args.timeMax),
items: args.items,
});
if (cached) return cached.value as unknown as calendar_v3.Schema$FreeBusyResponse;
return await this.fetchAvailability(args);
}
@@ -1055,9 +1061,9 @@ export default class GoogleCalendarService implements Calendar {
for (const [_eventTypeId, selectedCalendars] of Array.from(selectedCalendarsPerEventType.entries())) {
const parsedArgs = {
/** Expand the start date to the start of the month */
/** Expand the start date to the start of the month to increase cache hits */
timeMin: getTimeMin(),
/** Expand the end date to the end of the month */
/** Expand the end date to the end of the month to increase cache hits */
timeMax: getTimeMax(),
// Dont use eventTypeId in key because it can be used by any eventType
// The only reason we are building it per eventType is because there can be different groups of calendars to lookup the availability for
@@ -7,7 +7,6 @@ import prisma from "@calcom/prisma";
import type { Calendar, SelectedCalendarEventTypeIds } from "@calcom/types/Calendar";
import type { ICalendarCacheRepository } from "./calendar-cache.repository.interface";
import { getTimeMax, getTimeMin } from "./lib/datesForCache";
const log = logger.getSubLogger({ prefix: ["CalendarCacheRepository"] });
@@ -19,8 +18,8 @@ function parseKeyForCache(args: FreeBusyArgs): string {
// Ensure that calendarIds are unique
const uniqueItems = uniqueBy(args.items, ["id"]);
const key = JSON.stringify({
timeMin: getTimeMin(args.timeMin),
timeMax: getTimeMax(args.timeMax),
timeMin: args.timeMin,
timeMax: args.timeMax,
items: uniqueItems,
});
return key;
@@ -1,10 +1,11 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { getTimeMin, getTimeMax } from "./datesForCache";
describe("getTimeMin", () => {
// Tested on multiple dates
// vi.setSystemTime("2030-04-21T00:00:13Z");
vi.setSystemTime("2025-04-24T00:00:13Z");
it("should return start of current month when no date is passed", () => {
const result = getTimeMin();
const expected = new Date();
@@ -74,4 +75,23 @@ describe("getTimeMax", () => {
const result = getTimeMax(testDate);
expect(result).toMatchInlineSnapshot(`"2024-11-01T00:00:00.000Z"`);
});
it("should handle year changes", () => {
const testDate = "2024-12-24T23:59:59Z";
const result = getTimeMax(testDate);
expect(result).toMatchInlineSnapshot(`"2025-02-01T00:00:00.000Z"`);
});
it("should handle next month", () => {
const testDate = "2025-05-01T02:59:59.999Z";
const result = getTimeMax(testDate);
expect(result).toMatchInlineSnapshot(`"2025-06-01T00:00:00.000Z"`);
});
it("should handle special case where timeMax is more than 2 months from now but less than 3 months", () => {
vi.setSystemTime("2025-04-24T00:00:13Z");
const testDate = "2025-06-02T23:59:59.999Z";
const result = getTimeMax(testDate);
expect(result).toMatchInlineSnapshot(`"2025-06-01T00:00:00.000Z"`);
});
});
@@ -23,44 +23,37 @@ export function getTimeMax(timeMax?: string) {
const currentMonth = now.getUTCMonth();
const currentYear = now.getUTCFullYear();
// If no date is passed, return start of the month two months from *now*.
if (!timeMax) {
// If no date passed, return start of the overnext month from current date
const result = new Date(Date.UTC(currentYear, currentMonth + 2, 1));
result.setUTCHours(0, 0, 0, 0);
return result.toISOString();
}
// If a date is passed, determine the base month/year for calculation.
const date = new Date(timeMax);
const dateMonth = date.getUTCMonth();
const dateYear = date.getUTCFullYear();
// If the date is within current month or next month, return start of overnext month
// Otherwise, return start of overnext month from the date
let targetYear = dateYear;
let targetMonth = dateMonth;
let baseYear = currentYear;
let baseMonth = currentMonth;
// Check if date is within current month or next month
const isWithinCurrentOrNextMonth =
(dateYear === currentYear && dateMonth <= currentMonth + 1) ||
(dateYear === currentYear + 1 && currentMonth === 11 && dateMonth === 0);
// Check if date is within the current month or the next two months relative to *now*.
const isWithinCurrentOrNextTwoMonths =
(dateYear === currentYear && dateMonth <= currentMonth + 2) ||
(dateYear === currentYear + 1 &&
((currentMonth === 10 && dateMonth === 0) || (currentMonth === 11 && dateMonth <= 1)));
if (isWithinCurrentOrNextMonth) {
// For dates within current month or next month, return start of overnext month
targetMonth = dateMonth + 2;
if (targetMonth > 11) {
targetMonth = 0;
targetYear++;
}
} else {
// For dates beyond overnext month, return start of overnext month from the date
targetMonth = dateMonth + 2;
if (targetMonth > 11) {
targetMonth = 0;
targetYear++;
}
// If the input date is beyond the next two months relative to *now*,
// use the *input date's* month/year as the base.
if (!isWithinCurrentOrNextTwoMonths) {
baseYear = dateYear;
baseMonth = dateMonth;
}
// Otherwise, the base remains the current year/month.
const result = new Date(Date.UTC(targetYear, targetMonth, 1));
// Calculate the start of the month two months after the determined base date.
const result = new Date(Date.UTC(baseYear, baseMonth + 2, 1));
result.setUTCHours(0, 0, 0, 0);
return result.toISOString();
}
+1 -1
View File
@@ -169,7 +169,7 @@ export const getBusyCalendarTimes = async (
// Subtract 11 hours from the start date to avoid problems in UTC- time zones.
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();
const endDate = dayjs(dateTo).add(14, "hours").format();
if (includeTimeZone) {
results = await getCalendarsEventsWithTimezones(withCredentials, startDate, endDate, selectedCalendars);