Commit 72acf09
feat(unified-cal): connection-based unified calendar API with CRUD, freebusy, and list connections (calcom#28387)
* feat(unified-cal): connection-based unified calendar API with CRUD, freebusy, 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
* style: apply biome formatting to unified calendar API files
* fix: use @IsTimeZone() validator for timeZone field in CreateEventDateTimeWithZone
* fix: add delegation auth support, extract freebusy service layer
- 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.
* test: add unit and integration tests for unified calendar API
- 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: address Devin Review feedback - fix JSDoc and validator pattern
- 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)
* fix: revert IsAfterFrom to throw BadRequestException per team convention
Cubic AI (confidence 9/10, team feedback): validators should throw
BadRequestException to preserve the API's standard bad-request response
structure, per team convention.
* fix: add calendarId query param to createConnectionEvent for API consistency
All other connection-scoped endpoints accept calendarId; this was the
only one hardcoding 'primary'. Added @apiquery decorator and @query
parameter with ?? 'primary' fallback, plus a test for custom calendarId.
* Update apps/api/v2/src/modules/cal-unified-calendars/controllers/cal-unified-calendars.controller.ts
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
* Revert "Update apps/api/v2/src/modules/cal-unified-calendars/controllers/cal-unified-calendars.controller.ts"
This reverts commit e18e462.
* feat: enhance calendar service with connection-specific methods and improve API documentation
* test: complete delegation auth tests, document virtual mocks, fix key 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
* fix: add defense-in-depth key stripping in listConnections controller
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.
* feat: add unified calendar API endpoints for connections and events management
* fix: add try/catch error handling to CRUD helper methods
Wrap Google Calendar API calls in listEventsWithClient, createEventWithClient,
getEventWithClient, updateEventWithClient, and deleteEventWithClient with
try/catch blocks matching the legacy getEventDetails/updateEventDetails pattern.
This ensures proper NestJS exceptions (NotFoundException, BadRequestException)
are returned instead of raw 500 errors when the Google API throws.
* fix: map Google API errors to correct HTTP status codes
Replace blanket NotFoundException/BadRequestException in CRUD catch blocks
with mapGoogleApiError() that inspects the GaxiosError status code and
returns the appropriate NestJS exception (404→NotFoundException,
401/403→UnauthorizedException, 400→BadRequestException, else→500).
* fix: preserve upstream Google API status codes in error mapping
Separate 403 (ForbiddenException) from 401 (UnauthorizedException) and
add 429 rate-limit handling. This ensures permission-denied and throttling
errors are not misreported to API clients.
* fix: distinguish Google quota/rate-limit 403 from permission 403
Check GaxiosError reason field for rateLimitExceeded, userRateLimitExceeded,
and dailyLimitExceeded before mapping 403 to ForbiddenException. Quota
errors are now correctly mapped to 429 (retriable) instead.
* fix: keep dailyLimitExceeded as 403 (non-retriable quota exhaustion)
dailyLimitExceeded is a daily quota cap, not transient throttling.
Only rateLimitExceeded and userRateLimitExceeded are remapped to 429.
* fix: add missing @apiquery decorators for calendarId on get/update/delete endpoints
getConnectionEvent, updateConnectionEvent, and deleteConnectionEvent were
missing @apiquery({ name: 'calendarId', required: false }) which caused
OpenAPI spec to incorrectly mark calendarId as required.
* ci: retry flaky vitest worker test
* fix: update calendarId query parameter to be optional in OpenAPI specification
* fix: swap dual decorator order so plural /events/ path appears in OpenAPI spec
NestJS Swagger only picks up the first HTTP method decorator. Swapping
the order ensures the preferred plural path (/events/:eventUid) is
generated in the OpenAPI spec, while the deprecated singular path
(/event/:eventUid) still works at runtime.
* fix: split dual decorators into separate methods so both paths appear in OpenAPI spec
NestJS Swagger only picks up the first HTTP method decorator per handler.
Split getCalendarEventDetails and updateCalendarEvent into separate
methods for the singular /event/ (deprecated) and plural /events/ paths,
each delegating to a shared private helper. Both routes now appear in
the generated OpenAPI spec.
* fix: update openapi.json with split dual-decorator paths for GET/PATCH event endpoints
* fix: mapGoogleApiError - coerce string code to number and read errors from response.data
* fix: mapGoogleApiError - guard against NaN from non-numeric error codes
* fix: use read replica for findCredentialWithDelegationByTypeAndUserId query
* refactor: address review comments - UnifiedCalendarService, ParseConnectionIdPipe, 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
* chore: regenerate openapi.json after controller refactor to array syntax paths
---------
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>1 parent fa20f19 commit 72acf09
23 files changed
Lines changed: 3522 additions & 95 deletions
File tree
- apps/api/v2/src
- ee/calendars/services
- modules
- cal-unified-calendars
- controllers
- inputs
- outputs
- pipes
- services
- credentials
- docs/api-reference/v2
Lines changed: 25 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
| 2 | + | |
2 | 3 | | |
3 | 4 | | |
4 | 5 | | |
| |||
51 | 52 | | |
52 | 53 | | |
53 | 54 | | |
54 | | - | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
55 | 59 | | |
56 | 60 | | |
57 | 61 | | |
| |||
79 | 83 | | |
80 | 84 | | |
81 | 85 | | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
82 | 106 | | |
83 | 107 | | |
84 | 108 | | |
| |||
Lines changed: 6 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
1 | 2 | | |
2 | 3 | | |
3 | | - | |
4 | 4 | | |
| 5 | + | |
5 | 6 | | |
6 | 7 | | |
7 | 8 | | |
8 | 9 | | |
| 10 | + | |
| 11 | + | |
9 | 12 | | |
10 | 13 | | |
11 | 14 | | |
12 | 15 | | |
13 | 16 | | |
14 | 17 | | |
15 | 18 | | |
16 | | - | |
17 | 19 | | |
18 | 20 | | |
19 | 21 | | |
20 | 22 | | |
21 | 23 | | |
22 | 24 | | |
| 25 | + | |
| 26 | + | |
23 | 27 | | |
24 | 28 | | |
25 | 29 | | |
| |||
0 commit comments