feat: implement calendar cache denormalized SQL system#22708
feat: implement calendar cache denormalized SQL system#22708
Conversation
- Add CalendarSubscription and CalendarEvent database tables with proper indexes - Implement repository pattern with dependency injection following selectedSlots pattern - Add calendar-cache-sql-read and calendar-cache-sql-write feature flags for manual team rollout - Create Google Calendar webhook endpoint based on existing webhook implementation - Add cron job for subscription management and watch setup - Include comprehensive unit and integration tests (12 tests passing) - Follow existing Cal.com patterns for error handling, logging, and DI - Support etag-based webhook reliability and race condition handling - Enable gradual rollout via TeamFeature table entries This implements the complete calendar cache SQL system as described in the PRD, replacing the JSON-based CalendarCache with proper SQL tables for better performance and queryability. Co-Authored-By: zomars@cal.com <zomars@me.com>
Co-Authored-By: zomars@cal.com <zomars@me.com>
🤖 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:
|
WalkthroughThis PR adds a SQL-backed calendar cache system with Prisma models (CalendarSubscription, CalendarEvent), repositories, and services for availability, webhook processing, subscription management, and cleanup. It introduces API routes for webhook handling and cron jobs, Vercel cron schedules, and a cron tester update. Feature flags (read/write/cleanup) are added and wired into handlers. A getCalendarService selector enables conditional use of the SQL cache. UI and types are updated to surface event source and SQL cache metadata. Legacy JSONB calendar-cache code is marked deprecated. Multiple unit tests and migrations are included. Possibly related PRs
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. 📜 Recent review detailsConfiguration used: .coderabbit.yaml 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
✨ Finishing Touches🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
Status, Documentation and Community
|
…ionship - Replace inefficient per-user feature flag checking with database-level team filtering - Update CalendarSubscription to have one-to-one relationship with SelectedCalendar - Fix webhook to use CalendarCacheService instead of old fetchAvailabilityAndSetCache - Add new getNextBatchForSqlCache method for efficient team-based filtering - Update repository interfaces and implementations for new schema - Update tests to reflect new schema relationship Co-Authored-By: zomars@cal.com <zomars@me.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
- Add calendar-cache-sql-read feature for reading from SQL cache - Add calendar-cache-sql-write feature for writing to SQL cache - Both features disabled by default for manual team enablement - Use OPERATIONAL type following existing pattern Co-Authored-By: zomars@cal.com <zomars@me.com>
|
Thanks for the feedback! I'm a bit confused about the query issue you mentioned. My current query has:
Could you clarify what specific part of the query logic is wrong? The condition seems to match what you're asking for - finding calendars that don't have entries in the new CalendarSubscriptions table yet. Is there additional logic I should be including from the existing method (like error handling, expiration checks, etc.)? |
.../20250723210325_update_calendar_subscription_to_selected_calendar_relationship/migration.sql
Outdated
Show resolved
Hide resolved
DevinAI, you're right I got confused reviewing. |
…lendar interface - Remove destructive migration file that would cause data loss - Update webhook to use fetchAvailabilityAndSetCache instead of invalid listEvents method - Fix TypeScript errors in webhook implementation Co-Authored-By: zomars@cal.com <zomars@me.com>
- Add type assertion for watchCalendar response to handle Calendar interface returning unknown - Ensure cron job properly calls Google Calendar watch API and saves webhook fields - Addresses final GitHub feedback about cron job not populating googleChannelId, googleChannelToken, googleChannelExpiration Co-Authored-By: zomars@cal.com <zomars@me.com>
…acheService - Remove destructive migration file that would cause data loss - Update webhook to use CalendarCacheService.processWebhookEvents instead of fetchAvailabilityAndSetCache - Fix CalendarSubscriptionRepository interface to include required selectedCalendar properties - All type checks and tests passing Co-Authored-By: zomars@cal.com <zomars@me.com>
Sync error threshold now uses the maxSyncErrors field from the database. The setWatchError method updates syncErrors and backoffUntil within a transaction, applying exponential backoff capped at 60 minutes and respecting the maximum allowed sync errors.
Eliminates the lastFullSync property from the CalendarSubscription model, repository, tests, and documentation. This field is no longer required for sync state management.
Removes participant-related assertions from CalendarCacheSqlService tests to reflect that participant data is no longer persisted. Adds setup for subscription existence in CalendarEventRepository tests to ensure foreign key relations. Updates CalendarSubscriptionRepository test to check 'syncCursor' instead of 'nextSyncToken'.
Signed-off-by: Omar López <zomars@me.com> # Conflicts: # apps/api/v2/package.json # yarn.lock
Replaces the previous migration for the calendar subscription system with a new migration. The new schema refactors table and column names, removes the CalendarEventParticipant table, and updates indexes and foreign keys for improved clarity and maintainability.
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (4)
packages/features/calendar-cache-sql/CalendarSubscriptionService.ts (2)
62-66: Avoid class-name shadowing; alias the dynamically imported class.This destructuring shadows the local class name, hurting readability and IDE navigation. Alias the imported class.
- const { CalendarSubscriptionService } = await import( + const { CalendarSubscriptionService: GoogleCalendarSubscriptionService } = await import( "@calcom/app-store/googlecalendar/lib/CalendarSubscriptionService" ); - const subscriptionService = new CalendarSubscriptionService(credentialWithEmail); + const subscriptionService = new GoogleCalendarSubscriptionService(credentialWithEmail);
85-89: Repeat of shadowing issue in unwatchCalendar; alias here as well.Same problem as above; please alias the imported class.
- const { CalendarSubscriptionService } = await import( + const { CalendarSubscriptionService: GoogleCalendarSubscriptionService } = await import( "@calcom/app-store/googlecalendar/lib/CalendarSubscriptionService" ); - const subscriptionService = new CalendarSubscriptionService(credentialWithEmail); + const subscriptionService = new GoogleCalendarSubscriptionService(credentialWithEmail);packages/features/calendar-cache-sql/CalendarEventRepository.ts (1)
125-132: Consider logging per-event failure inside the transaction for easier debuggingIf one upsert fails, Promise.all aborts the transaction without context. Wrapping the per-item call with try/catch and logging the failing
externalEventIdwill help triage.- return await this.prismaClient.$transaction(async (tx) => { - const operations = events.map((event) => this.upsertEvent(event, subscriptionId, tx)); - await Promise.all(operations); - }); + return await this.prismaClient.$transaction(async (tx) => { + const operations = events.map(async (event) => { + try { + await this.upsertEvent(event, subscriptionId, tx); + } catch (error) { + // Best-effort identifier (avoid accessing nested relation) + const id = (event as any)?.externalEventId; + console.error(`Failed to upsert event ${id ?? "<unknown>"} for subscription ${subscriptionId}`, error); + throw error; + } + }); + await Promise.all(operations); + });packages/features/calendar-cache-sql/CalendarCacheService.ts (1)
127-142: Timezone-aware availability path addresses prior feedbackThe with-timezones variant returns {start, end, timeZone, title, source} as requested earlier in review threads.
🧹 Nitpick comments (25)
packages/features/calendar-cache-sql/CalendarSubscriptionService.ts (6)
15-21: De-duplicate GoogleChannelProps; import the type from the app-store module.The type already exists in the Google app-store service. Importing it avoids drift and keeps a single source of truth.
+import type { GoogleChannelProps } from "@calcom/app-store/googlecalendar/lib/CalendarSubscriptionService"; @@ -export type GoogleChannelProps = { - kind?: string | null; - id?: string | null; - resourceId?: string | null; - resourceUri?: string | null; - expiration?: string | null; -};Also applies to: 1-1
23-31: Rename the orchestrator service to avoid name collisions with the app-store class.There are two CalendarSubscriptionService classes in the codebase (this orchestrator and the Google app-store one). Renaming improves clarity and avoids confusion/import collisions. Also update the logger prefix.
-const log = logger.getSubLogger({ prefix: ["CalendarSubscriptionService"] }); +const log = logger.getSubLogger({ prefix: ["SqlCalendarSubscriptionService"] }); -export class CalendarSubscriptionService { +export class SqlCalendarSubscriptionService {Follow-up: rename file and update imports/usages in cron route/tests if applicable. I can generate a codemod or PR-wide diff on request.
53-56: Tighten the return type: this method should not return undefined on success.Errors are thrown; callers shouldn’t handle undefined.
- ): Promise<GoogleChannelProps | undefined> { + ): Promise<GoogleChannelProps> {
98-105: Improve missing-dependency error message to pinpoint what’s absent.This makes troubleshooting DI issues faster.
- if (!subscriptionRepo || !selectedCalendarRepo) { - throw new Error("Missing repositories in CalendarSubscriptionService.deps"); - } + if (!subscriptionRepo || !selectedCalendarRepo) { + const missing = [ + !subscriptionRepo ? "subscriptionRepo" : null, + !selectedCalendarRepo ? "selectedCalendarRepo" : null, + ] + .filter(Boolean) + .join(", "); + throw new Error(`Missing repositories in CalendarSubscriptionService.deps: ${missing}`); + }
112-123: createdCount may be misleading; consider renaming or deriving from repo result.upsertManyBySelectedCalendarId can noop existing rows. Counting selectedCalendars length as createdCount suggests new rows were created. Consider renaming to upsertedCount or use a return value that tells how many were actually inserted.
53-72: Longer-term: decouple from the app-store service to make this orchestrator standalone.Per the earlier discussion, this class still depends on the app-store Google subscription service. Consider inlining the minimal googleapis flow (events.watch/channels.stop) here to:
- remove cross-package coupling,
- localize webhook URL decisions,
- simplify testing and ownership.
I can draft a minimal, typed googleapis client wrapper for watch/unwatch aligned with your credential shapes.
Also applies to: 74-96
packages/prisma/schema.prisma (1)
1597-1627: Avoid redundant index on selectedCalendarId (unique already creates one)You declare selectedCalendarId as unique and also create an explicit index on the same field. The extra index is redundant and wastes storage/maintenance.
Apply this diff:
model CalendarSubscription { @@ @@index([channelExpiration, syncErrors, backoffUntil]) @@index([syncCursor]) - @@index([selectedCalendarId]) }packages/features/calendar-cache-sql/CalendarEventRepository.ts (1)
20-27: Removeanyand validateexternalEventIdbefore upsertThe key is read via
(data as any).externalEventId. This loses type-safety and risks undefined lookups. Extract and validate the id fromscalarFieldsinstead.- const { calendarSubscription: _ignored, ...scalarFields } = data; + const { calendarSubscription: _ignored, ...scalarFields } = data; + const { externalEventId } = scalarFields as { externalEventId?: string }; + if (!externalEventId) { + throw new Error("externalEventId is required for calendar event upsert"); + } @@ - externalEventId: (data as any).externalEventId, + externalEventId,packages/features/calendar-cache-sql/CalendarCacheSqlService.ts (3)
39-46: Avoid logging entire subscription objects
console.info("Got subscription", subscription)may log user and credential metadata. Prefer structured logging with key identifiers only.Example:
- subscriptionId
- selectedCalendarId
- selectedCalendar.externalId
- userId
202-208: Minimize payload when fetching a fresh sync tokenYou can avoid returning any items by adding maxResults: 1; this still returns nextSyncToken.
- const resp = await calendar.events.list({ calendarId, singleEvents: true }); + const resp = await calendar.events.list({ calendarId, singleEvents: true, maxResults: 1 });
313-341: Deduplicate calendar lookups before querying to reduce OR explosionIf calendars contain duplicates, the OR clause can grow unnecessarily. Build a distinct set of pairs.
- const calendarLookups = calendars.flatMap((cal) => - (cal.calendars || []).map((calendar) => ({ - externalId: calendar.externalId, - credentialId: cal.credentialId, - })) - ); + const pairSet = new Set<string>(); + const calendarLookups = calendars.flatMap((cal) => + (cal.calendars || []).reduce((acc, calendar) => { + const key = `${calendar.externalId}_${cal.credentialId}`; + if (!pairSet.has(key)) { + pairSet.add(key); + acc.push({ externalId: calendar.externalId, credentialId: cal.credentialId }); + } + return acc; + }, [] as Array<{ externalId: string; credentialId: number }>) + );packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts (2)
61-69: Useselectto avoid returning all fieldsThis read returns the full entity; project only the fields you need (e.g., id) to keep payloads small and consistent with the “always use select” guideline.
- async findByCredentialId(credentialId: number) { - return await this.prismaClient.calendarSubscription.findFirst({ + async findByCredentialId(credentialId: number) { + return await this.prismaClient.calendarSubscription.findFirst({ where: { selectedCalendar: { credentialId, }, }, - }); + select: { + id: true, + selectedCalendarId: true, + updatedAt: true, + }, + }); }
211-235: Progressive backoff logic is good; consider capturing the error message for observabilityYou already cap exponential backoff and respect maxSyncErrors. Optionally store the latest error message if you add an error field later; for now, app-level logging should capture it.
packages/features/calendar-cache-sql/CalendarCacheSqlService.test.ts (7)
12-26: Reset mocks between tests to avoid cross-test leakageAdd a reset call in beforeEach so module/function mocks don’t bleed across tests.
beforeEach(() => { + vi.resetAllMocks(); + vi.clearAllMocks(); mockSubscriptionRepo = { findBySelectedCalendar: vi.fn(), findByChannelId: vi.fn(), findByCredentialId: vi.fn(), findBySelectedCalendarIds: vi.fn(), upsert: vi.fn(), upsertMany: vi.fn(), updateSyncToken: vi.fn(), updateWatchDetails: vi.fn(), getSubscriptionsToWatch: vi.fn(), setWatchError: vi.fn(), clearWatchError: vi.fn(), };
170-189: Consider asserting exact payload structure to ensure no PII sneaks in via extra fieldsThe current assertions use objectContaining, which won’t fail if PII fields are accidentally included. Strengthen the check to ensure only the intended fields are persisted.
Example follow-up assertions to add after your current expect:
const [eventsArg] = vi.mocked(mockEventRepo.bulkUpsertEvents).mock.calls[0]; const cancelled = eventsArg.find((e: any) => e.externalEventId === "cancelled-event-id"); const active = eventsArg.find((e: any) => e.externalEventId === "active-event-id"); // Ensure no participants or other PII-ish structures slip in expect(cancelled).not.toHaveProperty("attendees"); expect(cancelled).not.toHaveProperty("creator"); expect(cancelled).not.toHaveProperty("organizer"); expect(active).not.toHaveProperty("attendees"); expect(active).not.toHaveProperty("creator"); expect(active).not.toHaveProperty("organizer");
264-277: PII filtering test should also assert absence of participant-related fieldsYou confirm summary/description/location are nulled, but if attendees/organizer/creator are accidentally persisted, this test still passes. Add explicit negative assertions.
Example additions:
const [eventsArg] = vi.mocked(mockEventRepo.bulkUpsertEvents).mock.calls[0]; const external = eventsArg.find((e: any) => e.externalEventId === "external-event-id"); expect(external).not.toHaveProperty("attendees"); expect(external).not.toHaveProperty("creator"); expect(external).not.toHaveProperty("organizer");
357-369: Strengthen the participant data test: ensure participants aren’t persisted at allUsing objectContaining doesn’t preclude extra fields from being present. Add negative assertions to guarantee no participants/organizer/creator fields are saved.
Example additions:
const [eventsArg] = vi.mocked(mockEventRepo.bulkUpsertEvents).mock.calls[0]; const withParticipants = eventsArg.find((e: any) => e.externalEventId === "event-with-participants"); expect(withParticipants).not.toHaveProperty("attendees"); expect(withParticipants).not.toHaveProperty("creator"); expect(withParticipants).not.toHaveProperty("organizer");
109-189: Add coverage for stale sync token (410) fallback pathService logic (per PR summary) falls back to a full time-ranged sync on 410. Add a test where events.list first rejects with a 410-like error, then a subsequent full sync succeeds, and verify updateSyncToken is called with a fresh token.
Happy to draft this additional test with mock sequencing for the Google API list calls.
109-189: Add pagination test for incremental fetch (nextPageToken)Cover a case where events are split across multiple pages to validate that bulkUpsertEvents receives the combined list and sync token is updated once.
I can provide a test snippet that chains multiple list responses with nextPageToken.
159-163: Isolate dynamic import mocks with vi.mock and resetModules
Switch fromvi.doMockto a combination ofvi.resetModules()andvi.mock()to ensure each dynamic import ofCalendarServiceis intercepted cleanly and not served from cache. For example, update at lines 159–163 (and similarly at 253–257, 347–349):- // Mock the import - vi.doMock("@calcom/app-store/googlecalendar/lib/CalendarService", () => ({ - default: vi.fn().mockImplementation(() => mockGoogleCalendarService), - })); + // Ensure no cached modules, then mock + vi.resetModules(); + vi.mock("@calcom/app-store/googlecalendar/lib/CalendarService", () => ({ + default: vi.fn().mockImplementation(() => mockGoogleCalendarService), + }));Please confirm that Vitest (v^2.1.1) in your setup supports local-scoped
vi.mockwith dynamic imports.
If not, consider one of:
- A top-level
vi.mock()with a mutable factory you adjust per testvi.importActual()+vi.unmock()patternspackages/features/calendar-cache-sql/README.md (5)
346-351: Clarify manual tmole installation guidanceExplicitly reference the tap for clarity and consistency with the install step.
-# Start tmole manually -tmole 3000 +# Start tmole manually (installed via tunnelmole/tap) +tmole 3000
439-452: Document X-Goog-Channel-Token requirement and webhook token validationThe webhook route validates GOOGLE_WEBHOOK_TOKEN per PR summary. Add the token header to the doc for completeness.
**Headers:** - `X-Goog-Channel-ID`: Google webhook channel ID - `X-Goog-Resource-ID`: Google resource ID +- `X-Goog-Channel-Token`: Verification token (must match GOOGLE_WEBHOOK_TOKEN)
241-253: Small usage snippet enhancement: show feature flag check and fallbackYou already show this below, but adding one line here helps prevent copy/paste without guardrails.
-import { CalendarCacheService } from "@calcom/features/calendar-cache-sql/CalendarCacheService"; +import { CalendarCacheService } from "@calcom/features/calendar-cache-sql/CalendarCacheService"; +// Ensure "calendar-cache-sql-read" is enabled for the user's team before using this service.
516-531: Minor wording tweaks for bullets (optional)Grammar flags are cosmetic; content is clear. Feel free to ignore, but tightening phrasing reduces lint noise in docs.
Examples:
- “Batch queries” → “Indexes for batch queries”
- “Transaction-based operations for data consistency” → “Transactions for data consistency”
- “Configurable cleanup intervals via cron jobs” → “Cleanup intervals configured via cron jobs”
283-287: Clarify that the cron only acts when write flag is enabledYou mention this elsewhere; repeating it here reduces operational mistakes.
-# The cron job at /api/cron/calendar-subscriptions will automatically -# set up webhooks for users with the calendar-cache-sql-write feature enabled +# The cron job at /api/cron/calendar-subscriptions will automatically +# set up webhooks for users with the calendar-cache-sql-write feature enabled. +# If the feature is disabled globally or for the user's team, the cron is a no-op.Also applies to: 317-323
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
yarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (13)
packages/features/calendar-cache-sql/CalendarCacheService.test.ts(1 hunks)packages/features/calendar-cache-sql/CalendarCacheService.ts(1 hunks)packages/features/calendar-cache-sql/CalendarCacheSqlService.test.ts(1 hunks)packages/features/calendar-cache-sql/CalendarCacheSqlService.ts(1 hunks)packages/features/calendar-cache-sql/CalendarEventRepository.interface.ts(1 hunks)packages/features/calendar-cache-sql/CalendarEventRepository.test.ts(1 hunks)packages/features/calendar-cache-sql/CalendarEventRepository.ts(1 hunks)packages/features/calendar-cache-sql/CalendarSubscriptionRepository.interface.ts(1 hunks)packages/features/calendar-cache-sql/CalendarSubscriptionRepository.test.ts(1 hunks)packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts(1 hunks)packages/features/calendar-cache-sql/CalendarSubscriptionService.ts(1 hunks)packages/features/calendar-cache-sql/README.md(1 hunks)packages/prisma/schema.prisma(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
- packages/features/calendar-cache-sql/CalendarEventRepository.test.ts
- packages/features/calendar-cache-sql/CalendarCacheService.test.ts
- packages/features/calendar-cache-sql/CalendarSubscriptionRepository.test.ts
- packages/features/calendar-cache-sql/CalendarSubscriptionRepository.interface.ts
- packages/features/calendar-cache-sql/CalendarEventRepository.interface.ts
🧰 Additional context used
📓 Path-based instructions (4)
**/*Repository.ts
📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)
Repository files must include
Repositorysuffix, prefix with technology if applicable (e.g.,PrismaAppRepository.ts), and use PascalCase matching the exported class
Files:
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.tspackages/features/calendar-cache-sql/CalendarEventRepository.ts
**/*.ts
📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)
**/*.ts: For Prisma queries, only select data you need; never useinclude, always useselect
Ensure thecredential.keyfield is never returned from tRPC endpoints or APIs
Files:
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.tspackages/features/calendar-cache-sql/CalendarSubscriptionService.tspackages/features/calendar-cache-sql/CalendarEventRepository.tspackages/features/calendar-cache-sql/CalendarCacheSqlService.tspackages/features/calendar-cache-sql/CalendarCacheSqlService.test.tspackages/features/calendar-cache-sql/CalendarCacheService.ts
**/*.{ts,tsx}
📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)
Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js
.utc()in hot paths like loops
Files:
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.tspackages/features/calendar-cache-sql/CalendarSubscriptionService.tspackages/features/calendar-cache-sql/CalendarEventRepository.tspackages/features/calendar-cache-sql/CalendarCacheSqlService.tspackages/features/calendar-cache-sql/CalendarCacheSqlService.test.tspackages/features/calendar-cache-sql/CalendarCacheService.ts
**/*Service.ts
📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)
Service files must include
Servicesuffix, use PascalCase matching exported class, and avoid generic names (e.g.,MembershipService.ts)
Files:
packages/features/calendar-cache-sql/CalendarSubscriptionService.tspackages/features/calendar-cache-sql/CalendarCacheSqlService.tspackages/features/calendar-cache-sql/CalendarCacheService.ts
🧠 Learnings (7)
📚 Learning: 2025-07-28T11:50:23.946Z
Learnt from: CR
PR: calcom/cal.com#0
File: .cursor/rules/review.mdc:0-0
Timestamp: 2025-07-28T11:50:23.946Z
Learning: Applies to **/*.ts : For Prisma queries, only select data you need; never use `include`, always use `select`
Applied to files:
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.tspackages/features/calendar-cache-sql/CalendarEventRepository.ts
📚 Learning: 2025-07-18T08:47:01.264Z
Learnt from: vijayraghav-io
PR: calcom/cal.com#21072
File: packages/prisma/schema.prisma:891-891
Timestamp: 2025-07-18T08:47:01.264Z
Learning: The Outlook Calendar integration in Cal.com intentionally reuses subscription IDs across multiple event types for efficiency. The `upsertSelectedCalendarsForEventTypeIds` method creates separate SelectedCalendar records for each eventTypeId, all sharing the same outlookSubscriptionId. This subscription sharing pattern means that unique constraints like `@unique([outlookSubscriptionId, eventTypeId])` should not be applied as they would prevent this intended functionality.
Applied to files:
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.tspackages/features/calendar-cache-sql/CalendarCacheSqlService.tspackages/prisma/schema.prisma
📚 Learning: 2025-08-07T18:42:34.081Z
Learnt from: Udit-takkar
PR: calcom/cal.com#22919
File: packages/lib/server/repository/PrismaPhoneNumberRepository.ts:412-417
Timestamp: 2025-08-07T18:42:34.081Z
Learning: In Cal.com codebase, the coding guideline requiring explicit `select` clauses instead of `include` for Prisma queries applies to read operations but not to update operations. Update operations don't need explicit select clauses.
Applied to files:
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.tspackages/features/calendar-cache-sql/CalendarEventRepository.ts
📚 Learning: 2025-08-05T12:04:29.037Z
Learnt from: din-prajapati
PR: calcom/cal.com#21854
File: packages/app-store/office365calendar/__tests__/unit_tests/SubscriptionManager.test.ts:0-0
Timestamp: 2025-08-05T12:04:29.037Z
Learning: In packages/app-store/office365calendar/lib/CalendarService.ts, the fetcher method in Office365CalendarService class is public, not private. It was specifically changed from private to public in this PR to support proper testing and external access patterns.
Applied to files:
packages/features/calendar-cache-sql/CalendarSubscriptionService.tspackages/features/calendar-cache-sql/CalendarEventRepository.tspackages/features/calendar-cache-sql/CalendarCacheSqlService.tspackages/features/calendar-cache-sql/CalendarCacheSqlService.test.tspackages/features/calendar-cache-sql/CalendarCacheService.ts
📚 Learning: 2025-07-18T17:57:16.395Z
Learnt from: vijayraghav-io
PR: calcom/cal.com#21072
File: packages/app-store/office365calendar/api/webhook.ts:120-123
Timestamp: 2025-07-18T17:57:16.395Z
Learning: The office365calendar webhook handler in packages/app-store/office365calendar/api/webhook.ts is specifically designed for Office365 calendar integration, not as a generic webhook handler. Therefore, it's safe to assume that fetchAvailabilityAndSetCache method will be implemented in the Office365CalendarService, making explicit validation checks unnecessary.
Applied to files:
packages/features/calendar-cache-sql/CalendarSubscriptionService.tspackages/features/calendar-cache-sql/CalendarCacheSqlService.tspackages/features/calendar-cache-sql/CalendarCacheSqlService.test.tspackages/features/calendar-cache-sql/CalendarCacheService.ts
📚 Learning: 2025-07-28T11:50:23.946Z
Learnt from: CR
PR: calcom/cal.com#0
File: .cursor/rules/review.mdc:0-0
Timestamp: 2025-07-28T11:50:23.946Z
Learning: Applies to **/*Service.ts : Service files must include `Service` suffix, use PascalCase matching exported class, and avoid generic names (e.g., `MembershipService.ts`)
Applied to files:
packages/features/calendar-cache-sql/CalendarSubscriptionService.ts
📚 Learning: 2025-07-18T08:47:01.264Z
Learnt from: vijayraghav-io
PR: calcom/cal.com#21072
File: packages/prisma/schema.prisma:891-891
Timestamp: 2025-07-18T08:47:01.264Z
Learning: In Cal.com's calendar integration, both Google Calendar and Outlook Calendar are designed to allow multiple eventTypeIds to share the same subscription ID (googleChannelId or outlookSubscriptionId). This is an intentional design pattern to reuse existing subscriptions for efficiency rather than creating separate subscriptions for each event type. Therefore, unique constraints like `@unique([outlookSubscriptionId, eventTypeId])` should not be added as they would break this subscription sharing functionality.
Applied to files:
packages/prisma/schema.prisma
🧬 Code Graph Analysis (5)
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts (2)
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.interface.ts (1)
ICalendarSubscriptionRepository(16-49)packages/prisma/index.ts (1)
PrismaClient(83-83)
packages/features/calendar-cache-sql/CalendarSubscriptionService.ts (4)
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.interface.ts (2)
ICalendarSubscriptionRepository(16-49)SubscriptionWithSelectedCalendar(6-14)packages/lib/server/repository/SelectedCalendarRepository.ts (1)
SelectedCalendarRepository(4-47)apps/api/v2/src/lib/logger.bridge.ts (1)
error(77-79)packages/lib/delegationCredential/server.ts (1)
getCredentialForCalendarCache(623-652)
packages/features/calendar-cache-sql/CalendarEventRepository.ts (2)
packages/features/calendar-cache-sql/CalendarEventRepository.interface.ts (2)
ICalendarEventRepository(15-47)CalendarEventUpsertResult(10-13)packages/prisma/index.ts (2)
PrismaClient(83-83)PrismaTransaction(91-91)
packages/features/calendar-cache-sql/CalendarCacheSqlService.ts (4)
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.interface.ts (1)
ICalendarSubscriptionRepository(16-49)packages/features/calendar-cache-sql/CalendarEventRepository.interface.ts (1)
ICalendarEventRepository(15-47)packages/lib/server/repository/SelectedCalendarRepository.ts (1)
SelectedCalendarRepository(4-47)packages/app-store/googlecalendar/lib/CalendarService.ts (1)
GoogleCalendarService(55-1131)
packages/features/calendar-cache-sql/CalendarCacheService.ts (3)
packages/features/calendar-cache-sql/CalendarEventRepository.interface.ts (2)
CalendarEventForAvailability(4-7)ICalendarEventRepository(15-47)packages/types/Calendar.d.ts (6)
EventBusyDate(56-61)IntegrationCalendar(246-258)Calendar(269-315)CalendarServiceEvent(265-267)NewCalendarEventType(72-85)CalendarEvent(164-227)packages/features/calendar-cache-sql/CalendarSubscriptionRepository.interface.ts (1)
ICalendarSubscriptionRepository(16-49)
🪛 LanguageTool
packages/features/calendar-cache-sql/README.md
[grammar] ~33-~33: There might be a mistake here.
Context: ...tion management with sync state tracking - Better performance through proper inde...
(QB_NEW_EN)
[grammar] ~34-~34: There might be a mistake here.
Context: ...h proper indexing and structured queries - Real-time sync via Google Calendar web...
(QB_NEW_EN)
[grammar] ~67-~67: There might be a mistake here.
Context: ...sses webhook events from Google Calendar - Manages incremental and full sync operat...
(QB_NEW_EN)
[grammar] ~68-~68: There might be a mistake here.
Context: ...ges incremental and full sync operations - Handles event creation, updates, and del...
(QB_NEW_EN)
[grammar] ~81-~81: There might be a mistake here.
Context: ...y**: CRUD operations for calendar events - CalendarSubscriptionRepository: CRUD o...
(QB_NEW_EN)
[grammar] ~82-~82: There might be a mistake here.
Context: ...ory**: CRUD operations for subscriptions - Interface-based design for easy testin...
(QB_NEW_EN)
[grammar] ~319-~319: There might be a mistake here.
Context: ...records** for eligible SelectedCalendars 2. Sets up Google Calendar watch channels...
(QB_NEW_EN)
[grammar] ~320-~320: There might be a mistake here.
Context: ...ls** for real-time webhook notifications 3. Updates watch details (channel ID, exp...
(QB_NEW_EN)
[grammar] ~321-~321: There might be a mistake here.
Context: ...el ID, expiration, etc.) in the database 4. Handles errors and implements backoff ...
(QB_NEW_EN)
[grammar] ~338-~338: There might be a mistake here.
Context: ...n port 3000 (or reuses existing session) 2. Extracts the public URL from tunnelmol...
(QB_NEW_EN)
[grammar] ~339-~339: There might be a mistake here.
Context: ...ts the public URL** from tunnelmole logs 3. Updates GOOGLE_WEBHOOK_URL in your `.e...
(QB_NEW_EN)
[grammar] ~340-~340: There might be a mistake here.
Context: ..._URL** in your .env file automatically 4. Keeps the tunnel active until you stop...
(QB_NEW_EN)
[grammar] ~445-~445: There might be a mistake here.
Context: ...g-Channel-ID: Google webhook channel ID - X-Goog-Resource-ID`: Google resource ID Response: ```...
(QB_NEW_EN)
[grammar] ~516-~516: There might be a mistake here.
Context: ...rformance: - Availability queries: [calendarSubscriptionId, start, end] - Batch queries: [start, end, status] ...
(QB_NEW_EN)
[grammar] ~517-~517: There might be a mistake here.
Context: ...onId, start, end]- **Batch queries**:[start, end, status]- **Unique constraints**:[calendarSubscrip...
(QB_NEW_EN)
[grammar] ~522-~522: There might be a mistake here.
Context: ...upserts** for processing multiple events - Transaction-based operations for data ...
(QB_NEW_EN)
[grammar] ~523-~523: There might be a mistake here.
Context: ...-based operations** for data consistency - Batch subscription queries for multipl...
(QB_NEW_EN)
[grammar] ~528-~528: There might be a mistake here.
Context: ...c cleanup** of cancelled events >24h old - Past event removal to maintain databas...
(QB_NEW_EN)
[grammar] ~529-~529: There might be a mistake here.
Context: ...vent removal** to maintain database size - Configurable cleanup intervals via cro...
(QB_NEW_EN)
[grammar] ~609-~609: There might be a mistake here.
Context: ...Re-enable feature flags** for the team (see feature flag setup section) 2. **Run su...
(QB_NEW_EN)
[grammar] ~673-~673: There might be a mistake here.
Context: ...ies, subscription lookups, result counts - CalendarSubscriptionService: Watch/unw...
(QB_NEW_EN)
[grammar] ~674-~674: There might be a mistake here.
Context: ...: Watch/unwatch operations, calendar IDs - GoogleCalendarWebhookService: Webhook ...
(QB_NEW_EN)
[grammar] ~675-~675: There might be a mistake here.
Context: ...Webhook processing, subscription lookups - Cron jobs: Subscription creation, erro...
(QB_NEW_EN)
🪛 Gitleaks (8.27.2)
packages/features/calendar-cache-sql/README.md
294-298: Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.
(curl-auth-header)
301-302: Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.
(curl-auth-header)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Install dependencies / Yarn install & cache
- GitHub Check: Atoms E2E Tests
🔇 Additional comments (20)
packages/features/calendar-cache-sql/CalendarSubscriptionService.ts (2)
32-51: Good extraction of credential validation/normalization.This removes duplication between watch and unwatch and centralizes validation. Nice.
62-67: Webhook endpoint correctly wired to SQL-backed route
- CalendarSubscriptionService constructs the callback URL as
${GOOGLE_WEBHOOK_URL_BASE}/api/webhook/google-calendar-sql..env.exampleand the feature README both point to/api/webhook/google-calendar-sql.- No references remain to any legacy “googlecalendar” webhook path.
No further action required.
packages/prisma/schema.prisma (3)
909-910: 1:1 relation wiring between SelectedCalendar and CalendarSubscription looks correctUnique on selectedCalendarId on the target model enforces a single active subscription per selected calendar, matching the intended design.
1597-1627: Channel expiration is DateTime: ensure upstream code uses Date objects, not stringsGiven channelExpiration is DateTime?, repository/service code should pass a Date (or an ISO 8601 string) consistently. I noticed repository code comparing against a stringified epoch and accepting string inputs for channelExpiration. That will likely break comparisons and migrations.
I’ve suggested concrete fixes in CalendarSubscriptionRepository.ts comments to move to Date objects for writes and comparisons.
1629-1672: Event model and indexes look solid for availability queries
- Composite unique on (calendarSubscriptionId, externalEventId) prevents dupes.
- Covering indexes on (calendarSubscriptionId, start, end) and (start, end, status) are appropriate.
packages/features/calendar-cache-sql/CalendarEventRepository.ts (4)
22-47: Upsert shape andselectusage are on point
- Connect by subscription id, update scalar fields, bump updatedAt.
- Returned fields match the UpsertResult contract.
52-81: Efficient availability query usingselectand overlap logic
- Proper window intersection (start/end) and filtering out cancelled/transparent events.
- Minimal field projection. LGTM.
83-112: Batch availability query mirrors single-id path correctlySame overlap logic and minimal selection for multiple subscriptionIds. Good.
134-151: Cleanup policy will purge past events; confirm this aligns with downstream needsDeleting any event with end < now keeps the cache lean, but if any downstream jobs rely on recent past events for reconciliation/metrics, consider a grace period (e.g., 24–48h) for non-cancelled events as well.
packages/features/calendar-cache-sql/CalendarCacheSqlService.ts (4)
47-59: Credential normalization for delegated accounts is handled safelyConditional extraction of client_email prevents undefined access. Good.
72-103: Initial sync: pagination + token bootstrap is correct
- Time-ranged fetch with paging.
- Then fetch and persist a fresh sync token for future incrementals.
105-156: Incremental sync with 410 (“Gone”) recovery is robust
- Paged incremental fetch.
- On 410, fall back to time-ranged full sync, then fetch and persist a fresh token. Well done.
224-289: All-day event time parsing may be timezone-sensitive; verify correctnessFor all-day events, Google provides date (without time) in the calendar’s timezone. Creating a Date from a bare YYYY-MM-DD string uses the server’s local timezone, which can shift the effective window. Consider normalizing with the event’s timeZone (if present) to start/end midnight in that zone, or add tests to ensure availability windows aren’t off by timezone/DST.
Would you like a small test harness to assert all-day busy coverage across timezones and DST boundaries?
packages/features/calendar-cache-sql/CalendarCacheService.ts (2)
81-107: Good: DRY helper to load events, minimal projections downstreamResolves selectedCalendarIds -> subscriptionIds -> batch events with a single query. Clear and efficient.
109-125: Availability response normalization is clean and type-safeISO strings, source tagging, and default title are consistent with the Calendar types.
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts (1)
25-59: Select-only projection with safe credential fields: goodAvoids include, and excludes sensitive credential fields (key, etc.). Matches guidelines.
packages/features/calendar-cache-sql/CalendarCacheSqlService.test.ts (3)
44-45: Good DI pattern and isolationConstructing the service with repository interfaces and a SelectedCalendar repo makes the unit tests clean and focused.
71-94: Availability mapping looks correctMocks and expectations validate ISO serialization and the “calendar-cache-sql” source consistently.
96-106: Error path is precise and usefulThrowing “Calendar subscription not found” is explicit and test coverage looks good.
packages/features/calendar-cache-sql/README.md (1)
210-222: No Action Required: README SQL example aligns with Prisma schemaThe Prisma schema defines a
TeamFeaturesmodel (plural) and thefeaturerelation usesFeature.slug(aString @id), exactly matching the README’s use of theTeamFeaturestable and string slugs forfeatureId. No changes to the SQL examples are needed.
Introduces an optional timeZone property to the EventBusyDate type and updates CalendarCacheService to use Ensure<EventBusyDate, "timeZone"> for availability data. This change improves timezone handling for calendar event availability.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (4)
packages/prisma/migrations/20250819013055_add_calendar_subscription_system/migration.sql (4)
54-55: Consider query patterns: add standalone index on backoffUntilThe composite index (channelExpiration, syncErrors, backoffUntil) won’t help queries that filter primarily on backoffUntil (the leftmost column rule applies). If your scheduler picks “all subs where backoffUntil <= now” regardless of channelExpiration, you’ll get a seq scan.
Add:
+-- CreateIndex +CREATE INDEX "CalendarSubscription_backoffUntil_idx" ON "CalendarSubscription"("backoffUntil");
13-15: Add CHECK constraints to keep error counters saneDefensive constraints prevent accidental negatives and make invariants explicit.
You can add:
+ALTER TABLE "CalendarSubscription" + ADD CONSTRAINT "CalendarSubscription_syncErrors_nonneg_chk" CHECK ("syncErrors" >= 0), + ADD CONSTRAINT "CalendarSubscription_maxSyncErrors_nonneg_chk" CHECK ("maxSyncErrors" >= 0);
69-70: Reassess global start/end/status index; consider a partial indexA global index on (start, end, status) may be low-selectivity and large. If your common scans target confirmed events, a partial index can reduce bloat and improve performance.
Keep existing indexes for now, but consider adding:
+-- CreateIndex (Postgres partial index example) +CREATE INDEX "CalendarEvent_start_end_confirmed_idx" + ON "CalendarEvent"("start", "end") + WHERE "status" = 'confirmed';You can then measure usage and decide whether to drop "CalendarEvent_start_end_status_idx".
40-41: Optional: add recurrence-friendly indexIf you query instances by series, an index helps. You already have (calendarSubscriptionId, iCalUID). If queries use recurringEventId or pair iCalUID+originalStartTime, add one accordingly.
For series lookups by recurringEventId:
+-- CreateIndex +CREATE INDEX "CalendarEvent_calendarSubscriptionId_recurringEventId_idx" + ON "CalendarEvent"("calendarSubscriptionId", "recurringEventId");Also applies to: 62-67
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
packages/prisma/migrations/20250819013055_add_calendar_subscription_system/migration.sql(1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-07-18T08:47:01.264Z
Learnt from: vijayraghav-io
PR: calcom/cal.com#21072
File: packages/prisma/schema.prisma:891-891
Timestamp: 2025-07-18T08:47:01.264Z
Learning: The Outlook Calendar integration in Cal.com intentionally reuses subscription IDs across multiple event types for efficiency. The `upsertSelectedCalendarsForEventTypeIds` method creates separate SelectedCalendar records for each eventTypeId, all sharing the same outlookSubscriptionId. This subscription sharing pattern means that unique constraints like `@unique([outlookSubscriptionId, eventTypeId])` should not be applied as they would prevent this intended functionality.
Applied to files:
packages/prisma/migrations/20250819013055_add_calendar_subscription_system/migration.sql
📚 Learning: 2025-07-18T08:47:01.264Z
Learnt from: vijayraghav-io
PR: calcom/cal.com#21072
File: packages/prisma/schema.prisma:891-891
Timestamp: 2025-07-18T08:47:01.264Z
Learning: In Cal.com's calendar integration, both Google Calendar and Outlook Calendar are designed to allow multiple eventTypeIds to share the same subscription ID (googleChannelId or outlookSubscriptionId). This is an intentional design pattern to reuse existing subscriptions for efficiency rather than creating separate subscriptions for each event type. Therefore, unique constraints like `@unique([outlookSubscriptionId, eventTypeId])` should not be added as they would break this subscription sharing functionality.
Applied to files:
packages/prisma/migrations/20250819013055_add_calendar_subscription_system/migration.sql
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Atoms E2E Tests
🔇 Additional comments (4)
packages/prisma/migrations/20250819013055_add_calendar_subscription_system/migration.sql (4)
51-52: Unique per selectedCalendarId is correct and aligns with shared-subscription patternOne subscription row per SelectedCalendar prevents duplicates while still allowing multiple SelectedCalendars to point to the same channelId, which preserves the “shared subscription ID across event types” design (per our prior learning). LGTM.
33-37: Confirm intent to use timestamp without time zone for event start/endYou’re storing start/end as TIMESTAMP(3) (no tz) and carrying timeZone separately. That’s fine if all reads/writes normalize accordingly. If you plan cross-timezone queries at SQL level, timestamptz may be safer.
Please confirm the service layer consistently normalizes to UTC and uses timeZone to reconstruct local times where needed. If not, consider switching to timestamptz for start/end.
75-76: Event uniqueness per subscription looks rightUniqueness on (calendarSubscriptionId, externalEventId) prevents duplicates while allowing the same externalEventId across different subscriptions. LGTM.
16-17: Ensure Prisma schemas use @updatedat on updatedAt fieldsThe migration adds an updatedAt column as
NOT NULLwithout a default, so inserts will fail unless Prisma auto-manages this field. Both schema files are missing the @updatedat attribute on these models:• packages/platform/examples/base/prisma/schema.prisma
• packages/prisma/schema.prismaPlease update each model’s updatedAt field from:
- updatedAt DateTime + updatedAt DateTime @updatedAtThis ensures Prisma sets a default timestamp on create and automatically updates it on save.
packages/prisma/migrations/20250819013055_add_calendar_subscription_system/migration.sql
Show resolved
Hide resolved
packages/prisma/migrations/20250819013055_add_calendar_subscription_system/migration.sql
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (5)
packages/features/calendar-cache-sql/CalendarCacheService.ts (5)
92-100: Early return when no valid selected calendar IDsIf
selectedCalendarsis non-empty but none have anid,selectedCalendarIdsbecomes empty and the repo call might inadvertently query broadly. Short-circuit to avoid unnecessary/expensive queries.const selectedCalendarIds = CalendarCachePresenter.presentCalendarIds(selectedCalendars); + if (selectedCalendarIds.length === 0) { + log.debug("No valid selectedCalendar IDs, returning empty results"); + return []; + } + const subscriptions = await this.subscriptionRepo.findBySelectedCalendarIds(selectedCalendarIds);
83-109: Validate and reuse parsed dates to avoid invalid ranges and double parsingCurrently
new Date()is called twice and invalid inputs aren’t handled. Validate once, bail early on invalid ranges, and reuse the parsed dates.private async loadEventsForSelectedCalendars( dateFrom: string, dateTo: string, selectedCalendars: IntegrationCalendar[] ): Promise<CalendarEventForAvailability[]> { if (selectedCalendars.length === 0) { return []; } + const from = new Date(dateFrom); + const to = new Date(dateTo); + if (isNaN(from.getTime()) || isNaN(to.getTime())) { + log.warn( + "Invalid date range provided to loadEventsForSelectedCalendars", + safeStringify({ dateFrom, dateTo }) + ); + return []; + } + const selectedCalendarIds = CalendarCachePresenter.presentCalendarIds(selectedCalendars); const subscriptions = await this.subscriptionRepo.findBySelectedCalendarIds(selectedCalendarIds); const subscriptionIds = CalendarCachePresenter.presentSubscriptionIds(subscriptions); if (subscriptionIds.length === 0) { log.debug("No subscriptions found, returning empty results"); return []; } const allEvents = await this.eventRepo.getEventsForAvailabilityBatch( subscriptionIds, - new Date(dateFrom), - new Date(dateTo) + from, + to ); return allEvents; }
118-119: Trim debug logs to avoid logging potentially large objectsLogging full
selectedCalendarscan be verbose. Log counts and identifiers only.- log.debug("Getting availability from cache", safeStringify({ dateFrom, dateTo, selectedCalendars })); + log.debug( + "Getting availability from cache", + safeStringify({ dateFrom, dateTo, selectedCount: selectedCalendars.length }) + );- log.debug( - "Getting availability with timezones from cache", - safeStringify({ dateFrom, dateTo, selectedCalendars }) - ); + log.debug( + "Getting availability with timezones from cache", + safeStringify({ dateFrom, dateTo, selectedCount: selectedCalendars.length }) + );Also applies to: 136-139
43-50: Type guard for calendar IDsIf
IntegrationCalendar["id"]can bestring | undefined | nullin some call sites, prefer a type-guard to ensurestring[]at compile-time.- static presentCalendarIds(selectedCalendars: IntegrationCalendar[]): string[] { - return selectedCalendars.reduce<string[]>((acc, sc) => { - if (sc.id !== undefined) { - acc.push(sc.id); - } - return acc; - }, []); - } + static presentCalendarIds(selectedCalendars: IntegrationCalendar[]): string[] { + return selectedCalendars + .map((sc) => sc.id) + .filter((id): id is string => typeof id === "string" && id.length > 0); + }Please confirm
IntegrationCalendar.idis alwaysstring. If it can be number, we should adjust the return type and consumers accordingly.
20-20: Minor: extract source string into a constantAvoid string duplication and typos by centralizing the source value.
-const log = logger.getSubLogger({ prefix: ["CalendarCacheService"] }); +const log = logger.getSubLogger({ prefix: ["CalendarCacheService"] }); +const SOURCE = "calendar-cache-sql";Then use
SOURCEin presenters:- source: "calendar-cache-sql", + source: SOURCE,
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
packages/features/calendar-cache-sql/CalendarCacheService.ts(1 hunks)packages/types/Calendar.d.ts(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/types/Calendar.d.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*Service.ts
📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)
Service files must include
Servicesuffix, use PascalCase matching exported class, and avoid generic names (e.g.,MembershipService.ts)
Files:
packages/features/calendar-cache-sql/CalendarCacheService.ts
**/*.ts
📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)
**/*.ts: For Prisma queries, only select data you need; never useinclude, always useselect
Ensure thecredential.keyfield is never returned from tRPC endpoints or APIs
Files:
packages/features/calendar-cache-sql/CalendarCacheService.ts
**/*.{ts,tsx}
📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)
Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js
.utc()in hot paths like loops
Files:
packages/features/calendar-cache-sql/CalendarCacheService.ts
🧠 Learnings (2)
📚 Learning: 2025-08-05T12:04:29.037Z
Learnt from: din-prajapati
PR: calcom/cal.com#21854
File: packages/app-store/office365calendar/__tests__/unit_tests/SubscriptionManager.test.ts:0-0
Timestamp: 2025-08-05T12:04:29.037Z
Learning: In packages/app-store/office365calendar/lib/CalendarService.ts, the fetcher method in Office365CalendarService class is public, not private. It was specifically changed from private to public in this PR to support proper testing and external access patterns.
Applied to files:
packages/features/calendar-cache-sql/CalendarCacheService.ts
📚 Learning: 2025-07-18T17:57:16.395Z
Learnt from: vijayraghav-io
PR: calcom/cal.com#21072
File: packages/app-store/office365calendar/api/webhook.ts:120-123
Timestamp: 2025-07-18T17:57:16.395Z
Learning: The office365calendar webhook handler in packages/app-store/office365calendar/api/webhook.ts is specifically designed for Office365 calendar integration, not as a generic webhook handler. Therefore, it's safe to assume that fetchAvailabilityAndSetCache method will be implemented in the Office365CalendarService, making explicit validation checks unnecessary.
Applied to files:
packages/features/calendar-cache-sql/CalendarCacheService.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Atoms E2E Tests
🔇 Additional comments (1)
packages/features/calendar-cache-sql/CalendarCacheService.ts (1)
72-79: Good separation of concerns and adherence to Service conventionsClass name/file suffix match the guidelines, repositories are injected (DI-friendly), and a Presenter isolates response shaping. This will be easy to swap/extend during rollout.
Bump @calcom/platform-libraries dependency from version 0.0.314 to 0.0.316 in package.json and yarn.lock to use the latest features and fixes.
…itory.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…itory.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Refactors channelExpiration from string to Date type across repository interfaces, implementations, and service logic. Updates tests to use Date objects for channelExpiration, and adds parsing logic to handle both timestamp and ISO string formats for expiration values.
Throws an error if the watch response is missing channel id or expiration, improving robustness and error reporting in CalendarSubscriptionService.
Replaces thrown error with setting a watch error on the subscription when the watch response is missing channel id or expiration. Increments error count and continues processing, improving error handling and robustness.
Signed-off-by: Omar López <zomars@me.com> # Conflicts: # apps/api/v2/package.json # yarn.lock
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (3)
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts (1)
162-171: Type mismatch: channelExpiration should be Date (matches schema/interface)The interface and Prisma model use DateTime. Using string here risks invalid values and breaks comparisons.
Apply:
async updateWatchDetails( id: string, details: { channelId: string; channelKind?: string; channelResourceId?: string; channelResourceUri?: string; - channelExpiration: string; + channelExpiration: Date; } ) {packages/features/calendar-cache-sql/CalendarCacheService.ts (2)
31-38: Mask event titles to avoid PII leakage in availability responsesDo not expose event.summary in public/free-busy outputs. Always return “Busy”.
Apply:
static presentAvailabilityData(events: CalendarEventForAvailability[]): EventBusyDate[] { return events.map((event) => ({ start: event.start.toISOString(), end: event.end.toISOString(), source: "calendar-cache-sql", - title: event.summary || "Busy", + title: "Busy", })); }
59-69: Also mask titles in timezone variantSame rationale as above; avoid leaking summaries.
Apply:
static presentAvailabilityDataWithTimezones( events: CalendarEventForAvailability[] ): Ensure<EventBusyDate, "timeZone">[] { return events.map((event) => ({ start: event.start.toISOString(), end: event.end.toISOString(), timeZone: event.timeZone || "", source: "calendar-cache-sql", - title: event.summary || "Busy", + title: "Busy", })); }
🧹 Nitpick comments (4)
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts (3)
61-69: Use explicitselectto comply with Prisma “select-only” guidelineThis read currently returns all columns. Add a
selectprojection consistent with other methods to keep the contract tight.Apply:
async findByCredentialId(credentialId: number) { return await this.prismaClient.calendarSubscription.findFirst({ where: { selectedCalendar: { credentialId, }, }, + select: { + id: true, + selectedCalendarId: true, + channelId: true, + channelKind: true, + channelResourceId: true, + channelResourceUri: true, + channelResource: true, + clientState: true, + channelExpiration: true, + syncCursor: true, + syncErrors: true, + maxSyncErrors: true, + backoffUntil: true, + createdAt: true, + updatedAt: true, + }, }); }
136-150: Batch upsert: consider chunking to avoid large concurrent burstsUsing Promise.all is fine for small/medium sets. If this ever handles hundreds/thousands of IDs, chunk requests to keep DB and connection pool healthy.
I can provide a chunking helper if you expect large batches.
241-265: Optional: Use the error parameter for observability and progressive backoff contextThe
_errorarg is ignored. Consider logging it at debug/warn level to aid troubleshooting, without storing sensitive payloads.- async setWatchError(id: string, _error: string) { + async setWatchError(id: string, error: string) { await this.prismaClient.$transaction(async (tx) => { + // Optionally log error context (avoid logging tokens/PII) + // log.warn({ id, error }, "Set watch error");packages/features/calendar-cache-sql/CalendarCacheService.ts (1)
118-121: Reduce PII in logs: avoid logging full selectedCalendars payloadLogging the entire selectedCalendars can include emails and other PII. Log counts and IDs instead.
Apply:
- log.debug("Getting availability from cache", safeStringify({ dateFrom, dateTo, selectedCalendars })); + const selectedCalendarIds = CalendarCachePresenter.presentCalendarIds(selectedCalendars); + log.debug( + "Getting availability from cache", + safeStringify({ dateFrom, dateTo, selectedCalendarCount: selectedCalendars.length, selectedCalendarIds }) + );
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
yarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (6)
apps/api/v2/package.json(1 hunks)packages/features/calendar-cache-sql/CalendarCacheService.test.ts(1 hunks)packages/features/calendar-cache-sql/CalendarCacheService.ts(1 hunks)packages/features/calendar-cache-sql/CalendarSubscriptionRepository.interface.ts(1 hunks)packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts(1 hunks)packages/features/calendar-cache-sql/CalendarSubscriptionService.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/features/calendar-cache-sql/CalendarCacheService.test.ts
- packages/features/calendar-cache-sql/CalendarSubscriptionService.ts
🧰 Additional context used
📓 Path-based instructions (4)
**/*Service.ts
📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)
Service files must include
Servicesuffix, use PascalCase matching exported class, and avoid generic names (e.g.,MembershipService.ts)
Files:
packages/features/calendar-cache-sql/CalendarCacheService.ts
**/*.ts
📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)
**/*.ts: For Prisma queries, only select data you need; never useinclude, always useselect
Ensure thecredential.keyfield is never returned from tRPC endpoints or APIs
Files:
packages/features/calendar-cache-sql/CalendarCacheService.tspackages/features/calendar-cache-sql/CalendarSubscriptionRepository.tspackages/features/calendar-cache-sql/CalendarSubscriptionRepository.interface.ts
**/*.{ts,tsx}
📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)
Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js
.utc()in hot paths like loops
Files:
packages/features/calendar-cache-sql/CalendarCacheService.tspackages/features/calendar-cache-sql/CalendarSubscriptionRepository.tspackages/features/calendar-cache-sql/CalendarSubscriptionRepository.interface.ts
**/*Repository.ts
📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)
Repository files must include
Repositorysuffix, prefix with technology if applicable (e.g.,PrismaAppRepository.ts), and use PascalCase matching the exported class
Files:
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts
🧠 Learnings (7)
📚 Learning: 2025-08-05T12:04:29.037Z
Learnt from: din-prajapati
PR: calcom/cal.com#21854
File: packages/app-store/office365calendar/__tests__/unit_tests/SubscriptionManager.test.ts:0-0
Timestamp: 2025-08-05T12:04:29.037Z
Learning: In packages/app-store/office365calendar/lib/CalendarService.ts, the fetcher method in Office365CalendarService class is public, not private. It was specifically changed from private to public in this PR to support proper testing and external access patterns.
Applied to files:
packages/features/calendar-cache-sql/CalendarCacheService.ts
📚 Learning: 2025-07-18T17:57:16.395Z
Learnt from: vijayraghav-io
PR: calcom/cal.com#21072
File: packages/app-store/office365calendar/api/webhook.ts:120-123
Timestamp: 2025-07-18T17:57:16.395Z
Learning: The office365calendar webhook handler in packages/app-store/office365calendar/api/webhook.ts is specifically designed for Office365 calendar integration, not as a generic webhook handler. Therefore, it's safe to assume that fetchAvailabilityAndSetCache method will be implemented in the Office365CalendarService, making explicit validation checks unnecessary.
Applied to files:
packages/features/calendar-cache-sql/CalendarCacheService.ts
📚 Learning: 2025-08-08T09:29:11.681Z
Learnt from: Udit-takkar
PR: calcom/cal.com#22919
File: packages/features/calAIPhone/interfaces/AIPhoneService.interface.ts:118-143
Timestamp: 2025-08-08T09:29:11.681Z
Learning: In calcom/cal.com PR #22919, packages/features/calAIPhone/interfaces/AIPhoneService.interface.ts (TypeScript), the AIPhoneServiceAgentListItem is required to include user.email in listAgents responses (per maintainer Udit-takkar). Future reviews should not flag this as unnecessary PII unless requirements change.
Applied to files:
packages/features/calendar-cache-sql/CalendarCacheService.ts
📚 Learning: 2025-08-08T10:26:13.362Z
Learnt from: Udit-takkar
PR: calcom/cal.com#22919
File: packages/features/calAIPhone/providers/retellAI/services/PhoneNumberService.ts:212-220
Timestamp: 2025-08-08T10:26:13.362Z
Learning: In calcom/cal.com PR #22919, packages/features/calAIPhone/providers/retellAI/services/PhoneNumberService.ts should include the phone number in client-facing HttpError messages (e.g., in updatePhoneNumber/getPhoneNumber catch blocks). Do not suggest redacting the phone number from these errors unless requirements change (per maintainer: Udit-takkar).
Applied to files:
packages/features/calendar-cache-sql/CalendarCacheService.ts
📚 Learning: 2025-07-28T11:50:23.946Z
Learnt from: CR
PR: calcom/cal.com#0
File: .cursor/rules/review.mdc:0-0
Timestamp: 2025-07-28T11:50:23.946Z
Learning: Applies to **/*.ts : For Prisma queries, only select data you need; never use `include`, always use `select`
Applied to files:
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts
📚 Learning: 2025-07-18T08:47:01.264Z
Learnt from: vijayraghav-io
PR: calcom/cal.com#21072
File: packages/prisma/schema.prisma:891-891
Timestamp: 2025-07-18T08:47:01.264Z
Learning: The Outlook Calendar integration in Cal.com intentionally reuses subscription IDs across multiple event types for efficiency. The `upsertSelectedCalendarsForEventTypeIds` method creates separate SelectedCalendar records for each eventTypeId, all sharing the same outlookSubscriptionId. This subscription sharing pattern means that unique constraints like `@unique([outlookSubscriptionId, eventTypeId])` should not be applied as they would prevent this intended functionality.
Applied to files:
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts
📚 Learning: 2025-08-07T18:42:34.081Z
Learnt from: Udit-takkar
PR: calcom/cal.com#22919
File: packages/lib/server/repository/PrismaPhoneNumberRepository.ts:412-417
Timestamp: 2025-08-07T18:42:34.081Z
Learning: In Cal.com codebase, the coding guideline requiring explicit `select` clauses instead of `include` for Prisma queries applies to read operations but not to update operations. Update operations don't need explicit select clauses.
Applied to files:
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts
🧬 Code Graph Analysis (2)
packages/features/calendar-cache-sql/CalendarCacheService.ts (3)
packages/features/calendar-cache-sql/CalendarEventRepository.interface.ts (2)
CalendarEventForAvailability(4-7)ICalendarEventRepository(15-47)packages/types/Calendar.d.ts (6)
EventBusyDate(56-62)IntegrationCalendar(247-259)Calendar(270-316)CalendarServiceEvent(266-268)NewCalendarEventType(73-86)CalendarEvent(165-228)packages/features/calendar-cache-sql/CalendarSubscriptionRepository.interface.ts (1)
ICalendarSubscriptionRepository(16-49)
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts (2)
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.interface.ts (1)
ICalendarSubscriptionRepository(16-49)packages/prisma/index.ts (1)
PrismaClient(83-83)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Install dependencies / Yarn install & cache
- GitHub Check: Atoms E2E Tests
🔇 Additional comments (6)
apps/api/v2/package.json (1)
41-41: Version bump LGTM; confirm no breaking changes in 0.0.316 for API v2 runtimeThe scoped bump to @calcom/platform-libraries looks fine. If this package affects DI tokens or shared types used by the new SQL cache routes, just sanity-check locally for regressions.
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts (3)
10-18: Good: safe credential projection excludes sensitive fieldsExplicit select with a narrow projection is aligned with the “select-only” guideline and prevents returning credential.key.
122-134: Upsert by selectedCalendarId: concise and correctTransactionally safe and minimal. Works with a unique index on selectedCalendarId.
267-276: Clear watch error: straightforward resetResets counters and backoff as expected.
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.interface.ts (1)
1-14: SafeCredential and SubscriptionWithSelectedCalendar types look solidGood explicit narrowing of credential fields and helpful composite type for subscription + selectedCalendar.
packages/features/calendar-cache-sql/CalendarCacheService.ts (1)
171-178: getCredentialId: simple and correctMatches the Calendar interface’s optional method and returns the id from the injected credential.
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.interface.ts
Show resolved
Hide resolved
Updated the channelExpiration field from string to Date in CalendarSubscriptionRepository to ensure proper handling of expiration values.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts (1)
181-239: Fix invalid field-to-field comparison in Prisma filter; filter in application layer and add deterministic ordering.
this.prismaClient.calendarSubscription.fields.maxSyncErrorsis not a valid Prisma construct. Prisma does not support column-to-column comparisons inwhere. Fetch a superset, filtersyncErrors < maxSyncErrorsin memory, then slice to the limit. Also add anorderByfor determinism.Apply:
async getSubscriptionsToWatch(limit = 100) { const oneDayInMS = 24 * 60 * 60 * 1000; const tomorrow = new Date(Date.now() + oneDayInMS); - return await this.prismaClient.calendarSubscription.findMany({ - take: limit, + const rows = await this.prismaClient.calendarSubscription.findMany({ + // Over-fetch to compensate for post-filtering (syncErrors < maxSyncErrors) + take: limit * 3, where: { selectedCalendar: { user: { teams: { some: { team: { features: { some: { featureId: CALENDAR_CACHE_SQL_WRITE_FEATURE, }, }, }, }, }, }, integration: "google_calendar", }, - syncErrors: { - lt: this.prismaClient.calendarSubscription.fields.maxSyncErrors, - }, OR: [{ channelExpiration: null }, { channelExpiration: { lt: tomorrow } }], AND: [{ OR: [{ backoffUntil: null }, { backoffUntil: { lte: new Date() } }] }], }, + orderBy: [{ channelExpiration: "asc" }, { updatedAt: "asc" }], select: { id: true, selectedCalendarId: true, channelId: true, channelKind: true, channelResourceId: true, channelResourceUri: true, channelResource: true, clientState: true, channelExpiration: true, syncCursor: true, syncErrors: true, maxSyncErrors: true, backoffUntil: true, createdAt: true, updatedAt: true, selectedCalendar: { select: { id: true, externalId: true, integration: true, userId: true, credential: { select: safeCredentialSelectForCalendarCache, }, }, }, }, }); + return rows.filter((s) => s.syncErrors < s.maxSyncErrors).slice(0, limit); }
🧹 Nitpick comments (5)
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts (5)
71-84: Early-return on empty array to skip unnecessary DB call.Minor optimization: if
selectedCalendarIdsis empty, return[]immediately.Apply:
async findBySelectedCalendarIds(selectedCalendarIds: string[]) { - return await this.prismaClient.calendarSubscription.findMany({ + if (selectedCalendarIds.length === 0) return []; + return await this.prismaClient.calendarSubscription.findMany({ where: { selectedCalendarId: { in: selectedCalendarIds, }, }, select: { id: true, selectedCalendarId: true, updatedAt: true, }, }); }
136-150: Avoid unbounded Promise.all inside a transaction; chunk for scalability.Large arrays can create many concurrent operations within a single transaction. Chunking reduces DB pressure and memory spikes.
Apply:
async upsertManyBySelectedCalendarId(selectedCalendarIds: string[]) { // Use transactional callback for better compatibility with Prismock tests return await this.prismaClient.$transaction(async (tx) => { - const results = await Promise.all( - selectedCalendarIds.map((selectedCalendarId) => - tx.calendarSubscription.upsert({ - where: { selectedCalendarId }, - create: { selectedCalendar: { connect: { id: selectedCalendarId } } }, - update: { updatedAt: new Date() }, - }) - ) - ); - return results; + const results: Prisma.CalendarSubscription[] = []; + const BATCH_SIZE = 50; + for (let i = 0; i < selectedCalendarIds.length; i += BATCH_SIZE) { + const slice = selectedCalendarIds.slice(i, i + BATCH_SIZE); + const batch = await Promise.all( + slice.map((selectedCalendarId) => + tx.calendarSubscription.upsert({ + where: { selectedCalendarId }, + create: { selectedCalendar: { connect: { id: selectedCalendarId } } }, + update: { updatedAt: new Date() }, + }) + ) + ); + results.push(...batch); + } + return results; }); }
152-160: Nit: Method name vs field name mismatch (token vs cursor).
updateSyncTokenwritessyncCursor. Consider renaming toupdateSyncCursor(or documenting the mapping) to avoid confusion.
241-265: setWatchError: Progressive backoff is good; consider using the error param or documenting why it’s ignored.If the schema has a place to store the last error (message/timestamp), consider persisting it. Otherwise, add a short comment indicating the error string is handled upstream to avoid confusion.
Where is the
errorparameter consumed or logged today (service layer? monitoring)? If not used, should we remove it from the interface or store it for troubleshooting?
61-69: Consider narrowing selected fields since only existence is checkedIn the only call site (getCalendar.ts), the returned subscription is used solely to test for existence (truthiness), not to access any of its properties. You can reduce query payload and improve clarity by:
- Adding a
select: { id: true }(or minimal required fields) tofindByCredentialId- Alternatively, renaming it to something like
existsByCredentialIdand returning a booleanIf you ever need full subscription data downstream, update this method’s
selectaccordingly./// packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts async findByCredentialId(credentialId: number) { - return await this.prismaClient.calendarSubscription.findFirst({ - where: { selectedCalendar: { credentialId } }, - }); + return await this.prismaClient.calendarSubscription.findFirst({ + where: { selectedCalendar: { credentialId } }, + select: { id: true }, // only fetch the primary key since we only check existence + }); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.test.ts(1 hunks)packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/features/calendar-cache-sql/CalendarSubscriptionRepository.test.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*Repository.ts
📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)
Repository files must include
Repositorysuffix, prefix with technology if applicable (e.g.,PrismaAppRepository.ts), and use PascalCase matching the exported class
Files:
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts
**/*.ts
📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)
**/*.ts: For Prisma queries, only select data you need; never useinclude, always useselect
Ensure thecredential.keyfield is never returned from tRPC endpoints or APIs
Files:
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts
**/*.{ts,tsx}
📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)
Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js
.utc()in hot paths like loops
Files:
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts
🧠 Learnings (3)
📚 Learning: 2025-07-28T11:50:23.946Z
Learnt from: CR
PR: calcom/cal.com#0
File: .cursor/rules/review.mdc:0-0
Timestamp: 2025-07-28T11:50:23.946Z
Learning: Applies to **/*.ts : For Prisma queries, only select data you need; never use `include`, always use `select`
Applied to files:
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts
📚 Learning: 2025-07-18T08:47:01.264Z
Learnt from: vijayraghav-io
PR: calcom/cal.com#21072
File: packages/prisma/schema.prisma:891-891
Timestamp: 2025-07-18T08:47:01.264Z
Learning: The Outlook Calendar integration in Cal.com intentionally reuses subscription IDs across multiple event types for efficiency. The `upsertSelectedCalendarsForEventTypeIds` method creates separate SelectedCalendar records for each eventTypeId, all sharing the same outlookSubscriptionId. This subscription sharing pattern means that unique constraints like `@unique([outlookSubscriptionId, eventTypeId])` should not be applied as they would prevent this intended functionality.
Applied to files:
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts
📚 Learning: 2025-08-07T18:42:34.081Z
Learnt from: Udit-takkar
PR: calcom/cal.com#22919
File: packages/lib/server/repository/PrismaPhoneNumberRepository.ts:412-417
Timestamp: 2025-08-07T18:42:34.081Z
Learning: In Cal.com codebase, the coding guideline requiring explicit `select` clauses instead of `include` for Prisma queries applies to read operations but not to update operations. Update operations don't need explicit select clauses.
Applied to files:
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts
🧬 Code Graph Analysis (1)
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts (2)
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.interface.ts (1)
ICalendarSubscriptionRepository(16-49)packages/prisma/index.ts (1)
PrismaClient(83-83)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Install dependencies / Yarn install & cache
- GitHub Check: Atoms E2E Tests
🔇 Additional comments (5)
packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts (5)
7-18: Safe credential projection looks solid (no key exposure).Good use of
satisfies Prisma.CredentialSelectand explicit omission of sensitive fields likecredential.key.
25-59: findBySelectedCalendar: adheres to select-only rule and safe credential projection.The query is precise, returns only required fields, and keeps credentials safe.
86-120: findByChannelId: correct switch to select-only and safe credential projection.This addresses prior feedback (include→select) and prevents credential leaks.
162-179: updateWatchDetails: correct Date typing for channelExpiration.Matches schema and enables native Date comparisons.
267-276: clearWatchError: straightforward and correct.Resets counters and backoff atomically.
Graphite Automations"Add community label" took an action on this PR • (08/20/25)1 label was added to this PR based on Keith Williams's automation. |
|
Closing as work continues in #23675 |
🚀 Calendar Cache SQL Implementation
calendar-cache-sql-demo - Watch Video
Summary
This PR implements a complete calendar cache SQL system to replace the current JSON-based CalendarCache with proper SQL tables for better performance and queryability. The implementation includes:
CalendarSubscriptionandCalendarEventtables with proper indexes for performancecalendar-cache-sql-readandcalendar-cache-sql-writefor manual team-based rolloutThe system is designed for gradual rollout using manual team-based feature flags via the
TeamFeaturetable, with comprehensive error handling and monitoring capabilities.Overview
This PR introduces a comprehensive calendar caching system for Google Calendar integration, significantly reducing API calls to third-party providers while maintaining real-time synchronization through webhook subscriptions.
🎯 Key Features
Feature Flag System
calendar-cache-sql-read,calendar-cache-sql-write,calendar-cache-sql-cleanupSubscription Management
Intelligent Cache Reading
🏗️ Architecture
Database Schema
CalendarSubscription: Manages webhook subscriptions and sync stateCalendarEvent: Stores cached calendar eventsServices
CalendarCacheService: Main caching service implementing Calendar interfaceCalendarSubscriptionService: Handles subscription lifecycleGoogleCalendarWebhookService: Processes webhook notificationsCalendarCacheSqlCleanupService: Manages old event cleanupRepository Pattern
CalendarEventRepository: Event data access layerCalendarSubscriptionRepository: Subscription data access layer✅ Comprehensive Testing Results
Subscription System
Cache Reading
🔧 Implementation Highlights
Smart Service Selection
Feature Flag Integration
Webhook Processing
🎯 Performance Impact
🔮 Future Enhancements (Wishlist)
🧪 Testing
All core functionality has been thoroughly tested including:
Breaking Changes: None - feature is opt-in via feature flags
Migration: Requires database migration for new CalendarSubscription and CalendarEvent tables
Review & Testing Checklist for Human
/api/webhook/google-calendar-sql) with valid/invalid tokens to ensure proper authentication. VerifyGOOGLE_WEBHOOK_TOKENvalidation works correctly.CalendarEventtable with correctetaghandling.TeamFeatureentries forcalendar-cache-sql-writeand verify the cron job only processes calendars for enabled teams.Recommended Test Plan:
Diagram
%%{ init : { "theme" : "default" }}%% graph TB subgraph "Database Schema" Schema["packages/prisma/schema.prisma<br/>CalendarSubscription + CalendarEvent models"]:::major-edit Migration["packages/prisma/migrations/<br/>20250723201504_add_calendar_subscription_and_event_tables/"]:::major-edit end subgraph "Feature Flags" FlagConfig["packages/features/flags/config.ts<br/>calendar-cache-sql-* flags"]:::minor-edit FlagHooks["packages/features/flags/hooks/index.ts<br/>initialData updates"]:::minor-edit end subgraph "Repository Layer" SubRepo["packages/features/calendar-cache-sql/<br/>calendar-subscription.repository.ts"]:::major-edit EventRepo["packages/features/calendar-cache-sql/<br/>calendar-event.repository.ts"]:::major-edit Service["packages/features/calendar-cache-sql/<br/>calendar-cache-sql.service.ts"]:::major-edit end subgraph "API Endpoints" Webhook["apps/web/app/api/webhook/<br/>google-calendar-sql/route.ts"]:::major-edit Cron["apps/web/app/api/cron/<br/>calendar-subscriptions-sql/route.ts"]:::major-edit end subgraph "DI System" DITokens["packages/lib/di/tokens.ts<br/>new DI tokens"]:::minor-edit DIModules["packages/lib/di/modules/<br/>calendar*.ts modules"]:::major-edit end subgraph "Tests" Tests["packages/features/calendar-cache-sql/<br/>__tests__/*.test.ts"]:::major-edit end Schema --> SubRepo Schema --> EventRepo SubRepo --> Service EventRepo --> Service Service --> Webhook Service --> Cron FlagConfig --> Webhook FlagConfig --> Cron DITokens --> DIModules DIModules --> Service subgraph Legend L1[Major Edit]:::major-edit L2[Minor Edit]:::minor-edit L3[Context/No Edit]:::context end classDef major-edit fill:#90EE90 classDef minor-edit fill:#87CEEB classDef context fill:#FFFFFFNotes
CalendarCachedata is requiredetagtracking and deduplication to handle out-of-order Google webhooksCalendarEventfor availability queries andCalendarSubscriptionfor webhook managementGOOGLE_WEBHOOK_TOKENand includes comprehensive error handlingSession Info: Implemented by @zomars via Devin session: https://app.devin.ai/sessions/2b24599eb86543928cf058a0ce35477e
Link to Devin run: https://app.devin.ai/sessions/2b24599eb86543928cf058a0ce35477e