feat(unified-cal): connection-based unified calendar API with CRUD, freebusy, and list connections#28387
Conversation
…reebusy, and list connections
- New GET /v2/calendars/connections endpoint returning all calendar connections with connectionId
- Connection-scoped CRUD: GET/POST/PATCH/DELETE /v2/calendars/connections/{connectionId}/events/*
- Connection-scoped free/busy: GET /v2/calendars/connections/{connectionId}/freebusy
- Legacy calendar-type endpoints: GET/POST/DELETE /v2/calendars/{calendar}/events, GET /{calendar}/freebusy
- Backward compat: dual @patch decorators for singular /event/ (deprecated) and plural /events/
- ConnectedCalendarEntry interface to eliminate inline type annotations
- DRY service layer with shared private helpers (listEventsWithClient, createEventWithClient, etc.)
- Input validation: @isdefined() on start/end, @IsTimeZone() on timezone fields, cross-field to >= from validation
- All-day event support: Google Calendar date-only events converted to midnight UTC
- New findCredentialByIdAndUserId method in CredentialsRepository for connection-scoped lookups
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Devin AI is addressing Cubic AI's review feedbackNew feedback has been sent to the existing Devin session. ✅ Pushed commit |
- Comment 3: getCalendarClientForUser and getCalendarClientByCredentialId now use getAuthorizedCalendarInstance with delegated-auth fallback instead of requiring credential.key directly. Added findCredentialWithDelegationByTypeAndUserId and expanded findCredentialByIdAndUserId to include delegationCredentialId. - Comment 5: Extracted freebusy and connections logic from controller into UnifiedCalendarsFreebusyService, keeping the controller thin (HTTP-only). Moved ConnectedCalendarEntry type and INTEGRATION_TYPE_TO_API mapping into the service layer. - Biome auto-formatting applied to touched files.
- GoogleCalendarService: 30 tests covering delegation auth, client creation, CRUD - UnifiedCalendarsFreebusyService: 21 tests covering connections, busy times, filtering - CalUnifiedCalendarsController: 31 tests covering all endpoints (connection-scoped + legacy) - Pipe specs: 37 existing tests continue to pass Total: 98 tests across 5 suites
- Fix incorrect JSDoc on listEventsForUser (all-day events ARE included, not skipped) - Fix IsAfterFrom validator to return false instead of throwing BadRequestException (preserves standard ValidationPipe error format)
Devin AI is addressing Cubic AI's review feedbackNew feedback has been sent to the existing Devin session. |
Cubic AI (confidence 9/10, team feedback): validators should throw BadRequestException to preserve the API's standard bad-request response structure, per team convention.
Devin AI is addressing Cubic AI's review feedbackNew feedback has been sent to the existing Devin session. |
…unified-calendars.controller.ts Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
…ers/cal-unified-calendars.controller.ts" This reverts commit e18e462.
Devin AI is addressing Cubic AI's review feedbackNew feedback has been sent to the existing Devin session. ✅ No changes pushed |
…mprove API documentation
Devin AI is addressing Cubic AI's review feedbackNew feedback has been sent to the existing Devin session. ✅ Pushed commit |
… leak tests - Item 3: Add 7 comprehensive delegation auth integration tests covering JWT creation params, email cleaning, fallback scenarios, and error handling - Item 7: Document why virtual mocks are necessary in all test files (workspace packages with DB dependencies cannot resolve in Jest) - Cubic #1: Document getCalendarsForConnection caching and upstream limitation - Cubic #2+#3: Make credential key leak tests non-vacuous by including actual key fields in mocks and verifying they don't leak - Remove unused BadRequestException import from freebusy service
Devin AI is addressing Cubic AI's review feedbackNew feedback has been sent to the existing Devin session. ✅ Pushed commit |
Controller now destructures only { connectionId, type, email } from each
connection before returning, so credential.key can never leak even if the
service layer has a future regression. Test updated to verify stripping.
… from response.data
Devin AI is addressing Cubic AI's review feedbackNew feedback has been sent to the existing Devin session. ✅ Pushed commit |
Ryukemeister
left a comment
There was a problem hiding this comment.
Review: nice work on this @sahitya-chandra, had a few suggestions. since this PR is so big can you do a self review of this. also it would be good if the PR descriptioin would explain what files to look at first, what things to keep in mind while reviewing this PR, is
apps/api/v2/src/modules/cal-unified-calendars/controllers/cal-unified-calendars.controller.ts
Outdated
Show resolved
Hide resolved
apps/api/v2/src/modules/cal-unified-calendars/controllers/cal-unified-calendars.controller.ts
Outdated
Show resolved
Hide resolved
apps/api/v2/src/modules/cal-unified-calendars/controllers/cal-unified-calendars.controller.ts
Outdated
Show resolved
Hide resolved
apps/api/v2/src/modules/cal-unified-calendars/controllers/cal-unified-calendars.controller.ts
Outdated
Show resolved
Hide resolved
apps/api/v2/src/modules/cal-unified-calendars/controllers/cal-unified-calendars.controller.ts
Outdated
Show resolved
Hide resolved
apps/api/v2/src/modules/cal-unified-calendars/controllers/cal-unified-calendars.controller.ts
Outdated
Show resolved
Hide resolved
apps/api/v2/src/modules/cal-unified-calendars/controllers/cal-unified-calendars.controller.ts
Outdated
Show resolved
Hide resolved
apps/api/v2/src/modules/cal-unified-calendars/controllers/cal-unified-calendars.controller.ts
Outdated
Show resolved
Hide resolved
apps/api/v2/src/modules/cal-unified-calendars/controllers/cal-unified-calendars.controller.ts
Outdated
Show resolved
Hide resolved
apps/api/v2/src/modules/cal-unified-calendars/controllers/cal-unified-calendars.controller.ts
Outdated
Show resolved
Hide resolved
apps/api/v2/src/modules/cal-unified-calendars/controllers/cal-unified-calendars.controller.ts
Outdated
Show resolved
Hide resolved
apps/api/v2/src/modules/cal-unified-calendars/controllers/cal-unified-calendars.controller.ts
Outdated
Show resolved
Hide resolved
apps/api/v2/src/modules/cal-unified-calendars/controllers/cal-unified-calendars.controller.ts
Outdated
Show resolved
Hide resolved
apps/api/v2/src/modules/cal-unified-calendars/services/unified-calendars-freebusy.service.ts
Outdated
Show resolved
Hide resolved
…ectionIdPipe, thin controller - Comment 70 (Ryukemeister): Remove 'what' JSDoc from calendars.service.ts - Comment 71 (Ryukemeister): Use array syntax for dual paths instead of separate methods - Comments 73-78 (ThyMinimalDev): Create ParseConnectionIdPipe for connectionId validation - Comments 79-84 (ThyMinimalDev): Create UnifiedCalendarService with strategy pattern - Comment 85 (ThyMinimalDev): Move getConnections from freebusy to UnifiedCalendarService - Controller now only handles HTTP concerns, delegates all logic to UnifiedCalendarService - Updated all test specs to match refactored architecture
ThyMinimalDev
left a comment
There was a problem hiding this comment.
unified calendar service will need some refactors when we start having multiple calendars, ok for now
What does this PR do?
Adds a unified calendar API to the
cal-unified-calendarsmodule in API v2. Extracted from #28297 as a standalone PR.Two sets of endpoints are introduced:
/v2/calendars/connections/...) — CRUD + freebusy scoped to a specific calendar connection byconnectionId/v2/calendars/:calendar/...) — operates on the user's default calendar credentialCurrently only Google Calendar is supported for CRUD operations. Non-Google connections are returned by
listConnectionsbut will receive a 400 if used for event operations.Recommended review order
Start with the service layer (core logic), then the controller (thin HTTP layer), then supporting files:
services/unified-calendar.service.ts— Orchestration service with strategy pattern. Delegates toGoogleCalendarServicefor Google-specific operations, validates calendar type, transforms responses viaGoogleCalendarEventOutputPipe. All business logic lives here — the controller calls this service exclusively.services/google-calendar.service.ts— Core Google Calendar service. Contains auth logic (getAuthorizedCalendarInstancetries delegated auth first, falls back to OAuth), DRY CRUD helpers (listEventsWithClient,createEventWithClient, etc.), andmapGoogleApiError()for proper HTTP status mapping of GaxiosError responses.services/unified-calendars-freebusy.service.ts— Freebusy service. ContainsgetBusyTimesForConnection()andgetBusyTimesForGoogleCalendars(). (Connection listing was moved toUnifiedCalendarService.)controllers/cal-unified-calendars.controller.ts— Thin controller. Handles HTTP concerns only: request parsing, callingUnifiedCalendarService, wrapping responses. UsesParseConnectionIdPipefor connectionId validation. Deprecated singular/event/and preferred plural/events/paths use array syntax on the same method.pipes/parse-connection-id.pipe.ts— Validates and parsesconnectionIdpath param to a number, throwingBadRequestExceptionfor invalid values. Used on all connection-scoped endpoints.inputs/— Input DTOs with validation:create-unified-calendar-event.input.ts(event creation with@IsTimeZone(),@IsDefined(),@IsISO8601()),freebusy-unified.input.ts(cross-fieldto >= fromvia@Validate(IsAfterFrom)),list-unified-calendar-events.input.ts.outputs/— Response DTOs:list-connections.output.ts,get-unified-calendar-event.output.ts.credentials.repository.ts— Two new repository methods:findCredentialByIdAndUserIdandfindCredentialWithDelegationByTypeAndUserId. Both use the read replica and includedelegationCredentialId+user.emailfor delegation-aware lookups.pipes/— UpdatedGoogleCalendarEventOutputPipeto handle all-day events (start.dateformat → midnight UTC).docs/api-reference/v2/openapi.json— Auto-generated from controller decorators but must be committed because CI's oasdiff compares the committed file. Regenerate locally withNODE_ENV=development yarn generate-swaggerif decorators change.Things to keep in mind while reviewing
Thin controller pattern: The controller delegates all business logic to
UnifiedCalendarService. Calendar type validation, event transformation, and CRUD calls all happen in the service layer — the controller only handles HTTP concerns (request parsing, response wrapping).Auth flow:
getAuthorizedCalendarInstance()is the single entry point for all auth. It checks for delegation credentials first (service account impersonation via JWT), then falls back to direct OAuth. BothgetCalendarClientForUserandgetCalendarClientByCredentialIddelegate to it.Error handling: All CRUD helpers wrap Google Calendar API calls in try/catch with
mapGoogleApiError(). GaxiosError stores HTTP status as a string inerror.code(e.g."404") and error reasons aterror.response.data.error.errors— the mapper handles both correctly withtypeofchecks andNumber.isNaN()guards.dailyLimitExceededstays 403 (non-retriable), onlyrateLimitExceeded/userRateLimitExceededare remapped to 429.Credential key stripping:
credential.key(OAuth tokens) is never exposed in API responses. The service layer returns only{ connectionId, type, email }, and the controller also destructures as defense-in-depth. Tests verify this at both layers.Deprecated paths:
/:calendar/event/:eventUid(singular) is kept for backward compat alongside/:calendar/events/:eventUid(plural). They use NestJS array syntax on the same method:@Get(["/:calendar/events/:eventUid", "/:calendar/event/:eventUid"]).ParseConnectionIdPipe: All connection-scoped endpoints use
@Param("connectionId", ParseConnectionIdPipe) credentialId: numberinstead of inline parseInt + isNaN checks.Virtual mocks in tests:
@calcom/platform-librariesis mocked with{ virtual: true }because its transitive dependencies (prisma, DB) can't resolve in Jest. An integration spec imports the realConnectedDestinationCalendarstype to catch shape changes at compile time.Important items for review
getCalendarsForConnectionfetches all calendars then filters: The upstreamplatform-librariesAPI only supports fetching all credentials at once. The method incalendars.service.tscallsgetCalendars()(which benefits fromCalendarsCacheService) and filters to the target credential. A targeted single-credential query would need upstream changes.UnifiedCalendarService.ensureGoogleCalendar()throwsBadRequestExceptionif a non-Google calendar type is used. When Office 365 / Apple support is added, new strategy branches should be added here./event/path still works at runtime but only the/events/path appears in the generated OpenAPI spec.New endpoints
GET/v2/calendars/connectionsGET/v2/calendars/connections/:id/eventsPOST/v2/calendars/connections/:id/eventsGET/v2/calendars/connections/:id/events/:eventIdPATCH/v2/calendars/connections/:id/events/:eventIdDELETE/v2/calendars/connections/:id/events/:eventIdGET/v2/calendars/connections/:id/freebusyGET/v2/calendars/:calendar/eventsPOST/v2/calendars/:calendar/eventsGET/v2/calendars/:calendar/events/:eventUidPATCH/v2/calendars/:calendar/events/:eventUidDELETE/v2/calendars/:calendar/events/:eventUidGET/v2/calendars/:calendar/freebusyTests
95 tests across 6 suites, all passing:
GoogleCalendarService— delegation auth, client creation, credential validation, error mappingUnifiedCalendarsFreebusyService— busy times, connection filteringCalUnifiedCalendarsController— all endpoints, validation, credential leak guardsConnectedDestinationCalendarstype shapeHow should this be tested?
$TOKENis an API key (prefixedcal_) or managed user access token{connId}is theconnectionIdfrom the list connections endpointMandatory Tasks (DO NOT REMOVE)
Link to Devin Session: https://app.devin.ai/sessions/42d4c8d802d44f09bb7349c07614024e
Requested by: @sahitya-chandra