* 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>
133 lines
4.2 KiB
TypeScript
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();
|
|
}
|
|
}
|