Skip to content

Commit 1eb6ff3

Browse files
chore: calendar cache optmize (calcom#24533)
* wip * fix: regenerate Prisma types and fix GoogleCalendarSubscriptionAdapter tests - Run yarn prisma generate to regenerate Prisma client with missing types (WorkflowType enum) - Fix addMonthsFromNow helper to properly return the calculated date - Update GoogleCalendarSubscriptionAdapter to use startOf/endOf day for time ranges - Update test expectations to use Date objects instead of dayjs objects - Fix all-day event test to use future date instead of today - All tests now passing (3599 passed) Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix tests * parametrize months ahead * improve names * Improve based on reviews --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent fc4ecf5 commit 1eb6ff3

7 files changed

Lines changed: 102 additions & 64 deletions

File tree

packages/features/calendar-subscription/adapters/GoogleCalendarSubscription.adapter.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { v4 as uuid } from "uuid";
33

44
import { CalendarAuth } from "@calcom/app-store/googlecalendar/lib/CalendarAuth";
55
import dayjs from "@calcom/dayjs";
6+
import { CalendarCacheEventService } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventService";
67
import logger from "@calcom/lib/logger";
78
import type { SelectedCalendar } from "@calcom/prisma/client";
89

@@ -117,12 +118,12 @@ export class GoogleCalendarSubscriptionAdapter implements ICalendarSubscriptionP
117118
};
118119

119120
if (!syncToken) {
120-
const now = dayjs();
121+
const now = dayjs().startOf("day");
121122
// first sync or unsync (3 months)
122-
const threeMonths = now.add(3, "month");
123+
const monthsAhead = now.add(CalendarCacheEventService.MONTHS_AHEAD, "month").endOf("day");
123124

124125
const timeMinISO = now.toISOString();
125-
const timeMaxISO = threeMonths.toISOString();
126+
const timeMaxISO = monthsAhead.toISOString();
126127
params.timeMin = timeMinISO;
127128
params.timeMax = timeMaxISO;
128129
} else {
@@ -151,8 +152,21 @@ export class GoogleCalendarSubscriptionAdapter implements ICalendarSubscriptionP
151152
}
152153

153154
private parseEvents(events: calendar_v3.Schema$Event[]): CalendarSubscriptionEventItem[] {
155+
const now = dayjs().startOf("day");
156+
const monthsAhead = now.add(CalendarCacheEventService.MONTHS_AHEAD, "month").endOf("day");
157+
158+
function filterEventsWithoutId(event: calendar_v3.Schema$Event) {
159+
return typeof event.id === "string" && !!event.id;
160+
}
161+
162+
function filterEventsByDateRange(event: calendar_v3.Schema$Event) {
163+
const start = dayjs(event.start?.dateTime || event.start?.date);
164+
return !start.isBefore(now) && !start.isAfter(monthsAhead);
165+
}
166+
154167
return events
155-
.filter((event) => typeof event.id === "string" && !!event.id)
168+
.filter(filterEventsWithoutId)
169+
.filter(filterEventsByDateRange)
156170
.map((event) => {
157171
// empty or opaque is busy
158172
const busy = !event.transparency || event.transparency === "opaque";

packages/features/calendar-subscription/adapters/__tests__/GoogleCalendarSubscriptionAdapter.test.ts

Lines changed: 74 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,25 @@ import "../__mocks__/CalendarAuth";
22

33
import { beforeEach, describe, expect, test, vi } from "vitest";
44

5+
import dayjs from "@calcom/dayjs";
56
import type { SelectedCalendar } from "@calcom/prisma/client";
67
import type { CredentialForCalendarServiceWithEmail } from "@calcom/types/Credential";
78

89
import { GoogleCalendarSubscriptionAdapter } from "../GoogleCalendarSubscription.adapter";
910

11+
const addMonthsFromNow = (months: number) => {
12+
const date = dayjs();
13+
return date.add(months, "months").startOf("day");
14+
};
15+
16+
const today = dayjs().startOf("day");
17+
18+
const oneWeekFromNow = today.add(7, "days");
19+
20+
const eventEndTime = oneWeekFromNow.add(1, "hours");
21+
22+
const channelExpirationDate = new Date(Date.now() + 86400000);
23+
1024
vi.mock("uuid", () => ({
1125
v4: vi.fn().mockReturnValue("test-uuid"),
1226
}));
@@ -30,16 +44,16 @@ const mockSelectedCalendar: SelectedCalendar = {
3044
watchAttempts: 0,
3145
maxAttempts: 3,
3246
unwatchAttempts: 0,
33-
createdAt: new Date(),
34-
updatedAt: new Date(),
47+
createdAt: today.toDate(),
48+
updatedAt: today.toDate(),
3549
channelId: "test-channel-id",
3650
channelKind: "web_hook",
3751
channelResourceId: "test-resource-id",
3852
channelResourceUri: "test-resource-uri",
39-
channelExpiration: new Date(Date.now() + 86400000),
40-
syncSubscribedAt: new Date(),
53+
channelExpiration: channelExpirationDate,
54+
syncSubscribedAt: today.toDate(),
4155
syncToken: "test-sync-token",
42-
syncedAt: new Date(),
56+
syncedAt: today.toDate(),
4357
syncErrorAt: null,
4458
syncErrorCount: 0,
4559
};
@@ -176,7 +190,7 @@ describe("GoogleCalendarSubscriptionAdapter", () => {
176190
id: "test-channel-id",
177191
resourceId: "test-resource-id",
178192
resourceUri: "test-resource-uri",
179-
expiration: "1640995200000",
193+
expiration: String(channelExpirationDate.getTime()),
180194
},
181195
};
182196

@@ -202,25 +216,26 @@ describe("GoogleCalendarSubscriptionAdapter", () => {
202216
id: "test-channel-id",
203217
resourceId: "test-resource-id",
204218
resourceUri: "test-resource-uri",
205-
expiration: new Date(1640995200000),
219+
expiration: channelExpirationDate,
206220
});
207221
});
208222

209223
test("should handle expiration as ISO string", async () => {
224+
const mockExpirationDate = new Date(Date.now() + 10000000);
210225
const mockWatchResponse = {
211226
data: {
212227
id: "test-channel-id",
213228
resourceId: "test-resource-id",
214229
resourceUri: "test-resource-uri",
215-
expiration: "2023-12-01T10:00:00Z",
230+
expiration: mockExpirationDate.toISOString(),
216231
},
217232
};
218233

219234
mockClient.events.watch.mockResolvedValue(mockWatchResponse);
220235

221236
const result = await adapter.subscribe(mockSelectedCalendar, mockCredential);
222237

223-
expect(result.expiration).toEqual(new Date("2023-12-01T10:00:00Z"));
238+
expect(result.expiration).toEqual(mockExpirationDate);
224239
});
225240

226241
test("should handle missing expiration", async () => {
@@ -265,30 +280,34 @@ describe("GoogleCalendarSubscriptionAdapter", () => {
265280
});
266281

267282
describe("fetchEvents", () => {
283+
const commonEventData = {
284+
iCalUID: "event-1@cal.com",
285+
summary: "Test Event",
286+
description: "Test Description",
287+
location: "Test Location",
288+
status: "confirmed",
289+
transparency: "opaque",
290+
kind: "calendar#event",
291+
etag: "test-etag",
292+
created: today.toISOString(),
293+
updated: today.toISOString(),
294+
};
295+
268296
test("should fetch events with sync token", async () => {
269297
const mockEventsResponse = {
270298
data: {
271299
nextSyncToken: "new-sync-token",
272300
items: [
273301
{
274302
id: "event-1",
275-
iCalUID: "event-1@cal.com",
276-
summary: "Test Event",
277-
description: "Test Description",
278-
location: "Test Location",
303+
...commonEventData,
279304
start: {
280-
dateTime: "2023-12-01T10:00:00Z",
305+
dateTime: oneWeekFromNow.toISOString(),
281306
timeZone: "UTC",
282307
},
283308
end: {
284-
dateTime: "2023-12-01T11:00:00Z",
309+
dateTime: eventEndTime.toISOString(),
285310
},
286-
status: "confirmed",
287-
transparency: "opaque",
288-
kind: "calendar#event",
289-
etag: "test-etag",
290-
created: "2023-12-01T09:00:00Z",
291-
updated: "2023-12-01T09:30:00Z",
292311
},
293312
],
294313
},
@@ -312,8 +331,8 @@ describe("GoogleCalendarSubscriptionAdapter", () => {
312331
{
313332
id: "event-1",
314333
iCalUID: "event-1@cal.com",
315-
start: new Date("2023-12-01T10:00:00Z"),
316-
end: new Date("2023-12-01T11:00:00Z"),
334+
start: oneWeekFromNow.toDate(),
335+
end: eventEndTime.toDate(),
317336
busy: true,
318337
summary: "Test Event",
319338
description: "Test Description",
@@ -325,14 +344,14 @@ describe("GoogleCalendarSubscriptionAdapter", () => {
325344
timeZone: "UTC",
326345
recurringEventId: null,
327346
originalStartDate: null,
328-
createdAt: new Date("2023-12-01T09:00:00Z"),
329-
updatedAt: new Date("2023-12-01T09:30:00Z"),
347+
createdAt: today.toDate(),
348+
updatedAt: today.toDate(),
330349
},
331350
],
332351
});
333352
});
334353

335-
test("should fetch events without sync token (initial sync)", async () => {
354+
test("should fetch events without sync token (initial sync) with a 3-month range", async () => {
336355
const calendarWithoutSyncToken = {
337356
...mockSelectedCalendar,
338357
syncToken: null,
@@ -349,34 +368,37 @@ describe("GoogleCalendarSubscriptionAdapter", () => {
349368

350369
const result = await adapter.fetchEvents(calendarWithoutSyncToken, mockCredential);
351370

371+
const expectedTimeMin = today.startOf("day").toISOString();
372+
const expectedTimeMax = addMonthsFromNow(3).endOf("day").toISOString();
373+
352374
expect(mockClient.events.list).toHaveBeenCalledWith({
353375
calendarId: "test@example.com",
354376
pageToken: undefined,
355377
singleEvents: true,
356-
timeMin: expect.any(String),
357-
timeMax: expect.any(String),
378+
timeMin: expectedTimeMin,
379+
timeMax: expectedTimeMax,
358380
});
359381

360382
expect(result.syncToken).toBe("initial-sync-token");
361383
});
362384

363385
test("should handle all-day events", async () => {
386+
const allDayStart = oneWeekFromNow.startOf("day");
387+
const allDayEnd = oneWeekFromNow.endOf("day");
388+
364389
const mockEventsResponse = {
365390
data: {
366391
nextSyncToken: "new-sync-token",
367392
items: [
368393
{
369394
id: "event-1",
370-
iCalUID: "event-1@cal.com",
371-
summary: "All Day Event",
395+
...commonEventData,
372396
start: {
373-
date: "2023-12-01",
397+
date: allDayStart.toISOString().split("T")[0],
374398
},
375399
end: {
376-
date: "2023-12-02",
400+
date: allDayEnd.toISOString().split("T")[0],
377401
},
378-
status: "confirmed",
379-
transparency: "opaque",
380402
},
381403
],
382404
},
@@ -387,8 +409,8 @@ describe("GoogleCalendarSubscriptionAdapter", () => {
387409
const result = await adapter.fetchEvents(mockSelectedCalendar, mockCredential);
388410

389411
expect(result.items[0].isAllDay).toBe(true);
390-
expect(result.items[0].start).toEqual(new Date("2023-12-01"));
391-
expect(result.items[0].end).toEqual(new Date("2023-12-02"));
412+
expect(result.items[0].start).toEqual(new Date(allDayStart.toISOString().split("T")[0]));
413+
expect(result.items[0].end).toEqual(new Date(allDayEnd.toISOString().split("T")[0]));
392414
});
393415

394416
test("should handle free events (transparent)", async () => {
@@ -398,15 +420,14 @@ describe("GoogleCalendarSubscriptionAdapter", () => {
398420
items: [
399421
{
400422
id: "event-1",
401-
iCalUID: "event-1@cal.com",
423+
...commonEventData,
402424
summary: "Free Event",
403425
start: {
404-
dateTime: "2023-12-01T10:00:00Z",
426+
dateTime: oneWeekFromNow.toISOString(),
405427
},
406428
end: {
407-
dateTime: "2023-12-01T11:00:00Z",
429+
dateTime: eventEndTime.toISOString(),
408430
},
409-
status: "confirmed",
410431
transparency: "transparent",
411432
},
412433
],
@@ -421,15 +442,18 @@ describe("GoogleCalendarSubscriptionAdapter", () => {
421442
});
422443

423444
test("should handle pagination", async () => {
445+
const date2 = new Date(oneWeekFromNow.toDate());
446+
date2.setDate(date2.getDate() + 1);
447+
424448
const mockEventsResponse1 = {
425449
data: {
426450
nextPageToken: "page-2",
427451
items: [
428452
{
429453
id: "event-1",
430454
summary: "Event 1",
431-
start: { dateTime: "2023-12-01T10:00:00Z" },
432-
end: { dateTime: "2023-12-01T11:00:00Z" },
455+
start: { dateTime: oneWeekFromNow.toISOString() },
456+
end: { dateTime: eventEndTime.toISOString() },
433457
},
434458
],
435459
},
@@ -442,8 +466,8 @@ describe("GoogleCalendarSubscriptionAdapter", () => {
442466
{
443467
id: "event-2",
444468
summary: "Event 2",
445-
start: { dateTime: "2023-12-01T12:00:00Z" },
446-
end: { dateTime: "2023-12-01T13:00:00Z" },
469+
start: { dateTime: date2.toISOString() },
470+
end: { dateTime: new Date(date2.getTime() + 3600000).toISOString() },
447471
},
448472
],
449473
},
@@ -468,19 +492,19 @@ describe("GoogleCalendarSubscriptionAdapter", () => {
468492
{
469493
id: "event-1",
470494
summary: "Valid Event",
471-
start: { dateTime: "2023-12-01T10:00:00Z" },
472-
end: { dateTime: "2023-12-01T11:00:00Z" },
495+
start: { dateTime: oneWeekFromNow.toISOString() },
496+
end: { dateTime: eventEndTime.toISOString() },
473497
},
474498
{
475499
summary: "Invalid Event",
476-
start: { dateTime: "2023-12-01T12:00:00Z" },
477-
end: { dateTime: "2023-12-01T13:00:00Z" },
500+
start: { dateTime: oneWeekFromNow.toISOString() },
501+
end: { dateTime: eventEndTime.toISOString() },
478502
},
479503
{
480504
id: "",
481505
summary: "Empty ID Event",
482-
start: { dateTime: "2023-12-01T14:00:00Z" },
483-
end: { dateTime: "2023-12-01T15:00:00Z" },
506+
start: { dateTime: oneWeekFromNow.toISOString() },
507+
end: { dateTime: eventEndTime.toISOString() },
484508
},
485509
],
486510
},

packages/features/calendar-subscription/lib/cache/CalendarCacheEventRepository.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,7 @@ export class CalendarCacheEventRepository implements ICalendarCacheEventReposito
8383
async deleteStale() {
8484
return this.prismaClient.calendarCacheEvent.deleteMany({
8585
where: {
86-
end: {
87-
lte: new Date(),
88-
},
86+
end: { lte: new Date() },
8987
},
9088
});
9189
}

packages/features/calendar-subscription/lib/cache/CalendarCacheEventService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const log = logger.getSubLogger({ prefix: ["CalendarCacheEventService"] });
99
* Service to handle calendar cache
1010
*/
1111
export class CalendarCacheEventService {
12+
static MONTHS_AHEAD: number = 3;
1213
constructor(
1314
private deps: {
1415
calendarCacheEventRepository: ICalendarCacheEventRepository;

packages/features/calendar-subscription/lib/cache/__tests__/CalendarCacheEventRepository.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -239,9 +239,7 @@ describe("CalendarCacheEventRepository", () => {
239239

240240
expect(mockPrismaClient.calendarCacheEvent.deleteMany).toHaveBeenCalledWith({
241241
where: {
242-
end: {
243-
lte: expect.any(Date),
244-
},
242+
end: { lte: expect.any(Date) },
245243
},
246244
});
247245
});

0 commit comments

Comments
 (0)