Files
calendar/packages/features/calendar-subscription/lib/cache/CalendarCacheWrapper.ts
T
Volnei MunhozGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Alex van AndelKeith Williams
e6b2116a2b feat: Calendar Cache and Sync (#23876)
* feat: calendar cache and sync - wip

* Add env.example

* refactor on CalendarCacheEventService

* remove test console.log

* Fix type checks errors

* chore: remove pt comment

* add route.ts

* chore: fix tests

* Improve cache impl

* chore: update recurring event id

* chore: small improvements

* calendar cache improvements

* Fix remove dynamic imports

* Add cleanup stale cache

* Fix tests

* add event update

* type fixes

* feat: add comprehensive tests for new calendar subscription API routes

- Add tests for /api/cron/calendar-subscriptions-cleanup route (9 tests)
- Add tests for /api/cron/calendar-subscriptions route (10 tests)
- Add tests for /api/webhooks/calendar-subscription/[provider] route (11 tests)
- Add missing feature flags for calendar-subscription-cache and calendar-subscription-sync
- All 30 tests pass with comprehensive coverage of authentication, feature flags, error handling, and service instantiation

Tests cover:
- Authentication scenarios (API key validation, Bearer tokens, query parameters)
- Feature flag combinations (cache/sync enabled/disabled states)
- Success and error handling (including non-Error exceptions)
- Service instantiation with proper dependency injection
- Provider validation for webhook endpoints

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* feat: add comprehensive tests for calendar subscription services, repositories, and adapters

- Add unit tests for CalendarSubscriptionService with subscription, webhook, and event processing
- Add unit tests for CalendarCacheEventService with cache operations and cleanup
- Add unit tests for CalendarSyncService with Cal.com event filtering and booking operations
- Add unit tests for CalendarCacheEventRepository with CRUD operations
- Add unit tests for SelectedCalendarRepository with calendar selection management
- Add unit tests for GoogleCalendarSubscriptionAdapter with subscription and event fetching
- Add unit tests for Office365CalendarSubscriptionAdapter with placeholder implementation
- Add unit tests for AdaptersFactory with provider management and adapter creation
- Fix lint issues by removing explicit 'any' type casting and unused variables
- All tests follow Cal.com conventions using Vitest framework with proper mocking

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix: improve calendar-subscriptions-cleanup test performance by adding missing mocks

- Add comprehensive mocks for defaultResponderForAppDir, logger, performance monitoring, and Sentry
- Fix slow test execution (933ms -> <100ms) caused by missing dependency mocks
- Ensure consistent test performance across different environments

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* Fix tests

* Fix tests

* type fix

* Fix coderabbit comments

* Fix types

* Fix test

* Update apps/web/app/api/cron/calendar-subscriptions/route.ts

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* Fixes by first review

* feat: add database migrations for calendar cache and sync fields

- Add CalendarCacheEventStatus enum with confirmed, tentative, cancelled values
- Add new fields to SelectedCalendar: channelId, channelKind, channelResourceId, channelResourceUri, channelExpiration, syncSubscribedAt, syncToken, syncedAt, syncErrorAt, syncErrorCount
- Create CalendarCacheEvent table with foreign key to SelectedCalendar
- Add necessary indexes and constraints for performance and data integrity

Fixes database schema issues causing e2e test failures with 'column does not exist' errors.

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* only google-calendar for now

* docs: add Calendar Cache and Sync feature documentation

- Add comprehensive feature overview and motivation
- Document feature flags with SQL examples
- Include SQL examples for enabling features for users and teams
- Reference technical documentation files

Addresses PR #23876 documentation requirements

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* docs: update calendar subscription README with comprehensive documentation

- Undo incorrect changes to main README.md
- Update packages/features/calendar-subscription/README.md with:
  - Feature overview and motivation
  - Environment variables section
  - Complete feature flags documentation with SQL examples
  - SQL examples for enabling features for users and teams
  - Detailed architecture documentation

Addresses PR #23876 documentation requirements

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix docs

* Fix test to available calendars

* Fix test to available calendars

* add migration and sync boilerplate

* fix typo

* remove double log

* sync boilerplate

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-09-29 14:26:14 +00:00

133 lines
4.2 KiB
TypeScript

import type { ICalendarCacheEventRepository } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventRepository.interface";
import logger from "@calcom/lib/logger";
import type {
Calendar,
CalendarEvent,
CalendarServiceEvent,
EventBusyDate,
IntegrationCalendar,
NewCalendarEventType,
SelectedCalendarEventTypeIds,
} from "@calcom/types/Calendar";
const log = logger.getSubLogger({ prefix: ["CachedCalendarWrapper"] });
/**
* A wrapper to load cache from database and cache it.
*
* @see Calendar
*/
export class CalendarCacheWrapper implements Calendar {
constructor(
private deps: {
originalCalendar: Calendar;
calendarCacheEventRepository: ICalendarCacheEventRepository;
}
) {}
getCredentialId?(): number {
return this.deps.originalCalendar.getCredentialId ? this.deps.originalCalendar.getCredentialId() : -1;
}
createEvent(
event: CalendarServiceEvent,
credentialId: number,
externalCalendarId?: string
): Promise<NewCalendarEventType> {
return this.deps.originalCalendar.createEvent(event, credentialId, externalCalendarId);
}
updateEvent(
uid: string,
event: CalendarServiceEvent,
externalCalendarId?: string | null
): Promise<NewCalendarEventType | NewCalendarEventType[]> {
return this.deps.originalCalendar.updateEvent(uid, event, externalCalendarId);
}
deleteEvent(uid: string, event: CalendarEvent, externalCalendarId?: string | null): Promise<unknown> {
return this.deps.originalCalendar.deleteEvent(uid, event, externalCalendarId);
}
/**
* Override this method to use cache
*
* @param dateFrom
* @param dateTo
* @param selectedCalendars
* @param shouldServeCache
*/
async getAvailability(
dateFrom: string,
dateTo: string,
selectedCalendars: IntegrationCalendar[],
shouldServeCache?: boolean
// _fallbackToPrimary?: boolean
): Promise<EventBusyDate[]> {
if (!shouldServeCache) {
return this.deps.originalCalendar.getAvailability(dateFrom, dateTo, selectedCalendars);
}
log.debug("getAvailability from cache", { dateFrom, dateTo, selectedCalendars });
const selectedCalendarIds = selectedCalendars.map((e) => e.id).filter((id): id is string => Boolean(id));
if (!selectedCalendarIds.length) {
return Promise.resolve([]);
}
return this.deps.calendarCacheEventRepository.findAllBySelectedCalendarIdsBetween(
selectedCalendarIds,
new Date(dateFrom),
new Date(dateTo)
);
}
/**
* Override this method to use cache
*
* @param dateFrom
* @param dateTo
* @param selectedCalendars
* @returns
*/
async getAvailabilityWithTimeZones?(
dateFrom: string,
dateTo: string,
selectedCalendars: IntegrationCalendar[]
// _fallbackToPrimary?: boolean
): Promise<{ start: Date | string; end: Date | string; timeZone: string }[]> {
log.debug("getAvailabilityWithTimeZones from cache", { dateFrom, dateTo, selectedCalendars });
const selectedCalendarIds = selectedCalendars.map((e) => e.id).filter((id): id is string => Boolean(id));
const result = await this.deps.calendarCacheEventRepository.findAllBySelectedCalendarIdsBetween(
selectedCalendarIds,
new Date(dateFrom),
new Date(dateTo)
);
return result.map(({ start, end, timeZone }) => ({ start, end, timeZone: timeZone || "UTC" }));
}
fetchAvailabilityAndSetCache?(selectedCalendars: IntegrationCalendar[]): Promise<unknown> {
return this.deps.originalCalendar.fetchAvailabilityAndSetCache?.(selectedCalendars) || Promise.resolve();
}
listCalendars(event?: CalendarEvent): Promise<IntegrationCalendar[]> {
return this.deps.originalCalendar.listCalendars(event);
}
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();
}
}