Skip to content

feat: implement calendar cache denormalized SQL system#22708

Closed
zomars wants to merge 229 commits intomainfrom
devin/1753300938-calendar-cache-sql-system
Closed

feat: implement calendar cache denormalized SQL system#22708
zomars wants to merge 229 commits intomainfrom
devin/1753300938-calendar-cache-sql-system

Conversation

@zomars
Copy link
Copy Markdown
Contributor

@zomars zomars commented Jul 23, 2025

🚀 Calendar Cache SQL Implementation

  refs #22508

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:

  • Database Schema: Added CalendarSubscription and CalendarEvent tables with proper indexes for performance
  • Repository Pattern: Implemented repository interfaces and classes with dependency injection following existing patterns
  • Feature Flags: Added calendar-cache-sql-read and calendar-cache-sql-write for manual team-based rollout
  • Webhook Processing: New Google Calendar webhook endpoint based on existing webhook implementation
  • Subscription Management: Cron job for managing calendar subscriptions and Google watch setup
  • Comprehensive Testing: Unit tests for repositories and service layer (12 tests passing)

The system is designed for gradual rollout using manual team-based feature flags via the TeamFeature table, 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

  • Global Flags: calendar-cache-sql-read, calendar-cache-sql-write, calendar-cache-sql-cleanup
  • Team-Level Flags: Granular control for individual teams
  • Kill Switch Capability: Global flags act as emergency kill switches

Subscription Management

  • Automated Subscription: Cron jobs automatically subscribe to relevant calendars
  • Webhook Integration: Real-time updates via Google Calendar webhooks
  • Sync Token Management: Efficient incremental sync using Google's sync tokens
  • Error Handling: Robust backoff mechanism for failed syncs

Intelligent Cache Reading

  • Conditional Caching: Only Google Calendar with active subscriptions use cache
  • Fallback Strategy: Service selection logic falls back to direct CalendarService when cache is not applicable (non-Google calendars, disabled flags, no subscriptions, or setup errors)"
  • API Call Reduction: Subscribed calendars no longer make direct Google API calls for availability

🏗️ Architecture

Database Schema

  • CalendarSubscription: Manages webhook subscriptions and sync state
  • CalendarEvent: Stores cached calendar events
  • Enhanced feature flag system for granular control

Services

  • CalendarCacheService: Main caching service implementing Calendar interface
  • CalendarSubscriptionService: Handles subscription lifecycle
  • GoogleCalendarWebhookService: Processes webhook notifications
  • CalendarCacheSqlCleanupService: Manages old event cleanup

Repository Pattern

  • CalendarEventRepository: Event data access layer
  • CalendarSubscriptionRepository: Subscription data access layer
  • Clean separation of concerns with interface-based design

✅ Comprehensive Testing Results

Subscription System

  • Global feature flags operational for all three cache flags
  • Team feature flags working correctly
  • Cron jobs successfully subscribe to calendars
  • Subscriptions properly created and persisted
  • Webhook endpoints receiving calendar change notifications
  • Initial subscription correctly populates future CalendarEvents
  • Sync tokens properly saved and managed
  • Incremental updates processing only changed events

Cache Reading

  • Subscribed calendars fetch from CalendarCacheService
  • API call elimination - cached calendars bypass Google API for availability
  • Proper fallback to direct API calls when cache unavailable

🔧 Implementation Highlights

Smart Service Selection

// Automatically chooses between cached and direct service
const calendarService = await getCalendarService(credential);

Feature Flag Integration

  • Global kill switches for emergency situations
  • Team-level enablement for gradual rollout
  • Graceful fallback when flags disabled

Webhook Processing

  • Real-time event updates via Google Calendar notifications
  • Efficient incremental sync using sync tokens
  • Robust error handling and retry mechanisms

🎯 Performance Impact

  • Reduced API Calls: Subscribed calendars eliminate redundant Google API requests
  • Real-time Updates: Webhook-based synchronization ensures data freshness
  • Scalable Architecture: Repository pattern supports future enhancements

🔮 Future Enhancements (Wishlist)

  • Troubleshooter integration for cache diagnostics
  • Cache bypass mechanism for debugging/testing

🧪 Testing

All core functionality has been thoroughly tested including:

  • Feature flag behavior (global and team level)
  • Subscription lifecycle management
  • Webhook event processing
  • Cache reading and API call reduction
  • Error handling and fallback scenarios

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

⚠️ HIGH RISK ITEMS - CRITICAL TO VERIFY:

  • Database Migration Safety: Run the migration in a test environment and verify schema changes are reversible. Check that indexes don't cause performance issues on large datasets.
  • Webhook Security: Test the webhook endpoint (/api/webhook/google-calendar-sql) with valid/invalid tokens to ensure proper authentication. Verify GOOGLE_WEBHOOK_TOKEN validation works correctly.
  • End-to-End Google Calendar Integration: Set up a test Google Calendar webhook and verify events are properly synced to the new CalendarEvent table with correct etag handling.
  • Feature Flag Team Rollout: Create test TeamFeature entries for calendar-cache-sql-write and verify the cron job only processes calendars for enabled teams.
  • Error Handling & Recovery: Test webhook processing with malformed payloads, API failures, and database errors to ensure graceful degradation.

Recommended Test Plan:

  1. Enable feature flags for a test team
  2. Set up Google Calendar webhook subscription via cron job
  3. Create/modify/delete calendar events and verify they sync correctly
  4. Test with invalid webhook tokens and verify rejection
  5. Disable feature flags and verify system falls back gracefully

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:#FFFFFF
Loading

Notes

  • Migration Strategy: This is a standalone system - no migration of existing CalendarCache data is required
  • Webhook Reliability: Implements etag tracking and deduplication to handle out-of-order Google webhooks
  • Performance Considerations: Added strategic indexes on CalendarEvent for availability queries and CalendarSubscription for webhook management
  • Security: Webhook endpoint validates GOOGLE_WEBHOOK_TOKEN and includes comprehensive error handling
  • Rollout Strategy: Manual team-based feature flags allow controlled rollout without percentage-based logic

Session Info: Implemented by @zomars via Devin session: https://app.devin.ai/sessions/2b24599eb86543928cf058a0ce35477e

Link to Devin run: https://app.devin.ai/sessions/2b24599eb86543928cf058a0ce35477e

devin-ai-integration bot and others added 2 commits July 23, 2025 20:26
- 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-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jul 23, 2025

Walkthrough

This 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 details

Configuration used: .coderabbit.yaml
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.

📥 Commits

Reviewing files that changed from the base of the PR and between 0927d12 and 577006c.

📒 Files selected for processing (1)
  • apps/web/vercel.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/vercel.json
⏰ 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)
  • GitHub Check: Install dependencies / Yarn install & cache
  • GitHub Check: Atoms E2E Tests
✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch devin/1753300938-calendar-cache-sql-system

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions github-actions bot added the ❗️ migrations contains migration files label Jul 23, 2025
@keithwillcode keithwillcode added core area: core, team members only foundation labels Jul 23, 2025
…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>
@vercel
Copy link
Copy Markdown

vercel bot commented Jul 23, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Preview Comments Updated (UTC)
cal Ignored Ignored Aug 21, 2025 11:58am
cal-eu Ignored Ignored Aug 21, 2025 11:58am

@zomars zomars changed the title feat: implement calendar cache SQL system feat: implement calendar cache denormalized SQL system Jul 24, 2025
- 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>
@devin-ai-integration
Copy link
Copy Markdown
Contributor

Thanks for the feedback! I'm a bit confused about the query issue you mentioned.

My current query has:

    • which should find selected calendars that don't have an entry in the new CalendarSubscriptions table
  • Team-based filtering for feature
  • Only integration
  • Only user-level calendars ()

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.)?

@zomars
Copy link
Copy Markdown
Contributor Author

zomars commented Jul 24, 2025

Thanks for the feedback! I'm a bit confused about the query issue you mentioned.

My current query has:

* * which should find selected calendars that don't have an entry in the new CalendarSubscriptions table

* Team-based filtering for  feature

* Only  integration

* Only user-level calendars ()

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.)?

DevinAI, you're right I got confused reviewing.

devin-ai-integration bot and others added 2 commits July 24, 2025 22:13
…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>
zomars added 7 commits August 18, 2025 16:51
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.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 debugging

If one upsert fails, Promise.all aborts the transaction without context. Wrapping the per-item call with try/catch and logging the failing externalEventId will 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 feedback

The 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: Remove any and validate externalEventId before upsert

The key is read via (data as any).externalEventId. This loses type-safety and risks undefined lookups. Extract and validate the id from scalarFields instead.

-    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 token

You 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 explosion

If 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: Use select to avoid returning all fields

This 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 observability

You 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 leakage

Add 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 fields

The 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 fields

You 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 all

Using 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 path

Service 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 from vi.doMock to a combination of vi.resetModules() and vi.mock() to ensure each dynamic import of CalendarService is 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.mock with dynamic imports.
If not, consider one of:

  • A top-level vi.mock() with a mutable factory you adjust per test
  • vi.importActual() + vi.unmock() patterns
packages/features/calendar-cache-sql/README.md (5)

346-351: Clarify manual tmole installation guidance

Explicitly 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 validation

The 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 fallback

You 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 enabled

You 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.

📥 Commits

Reviewing files that changed from the base of the PR and between b77e66a and 8ceb975.

⛔ Files ignored due to path filters (1)
  • yarn.lock is 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 Repository suffix, prefix with technology if applicable (e.g., PrismaAppRepository.ts), and use PascalCase matching the exported class

Files:

  • packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts
  • packages/features/calendar-cache-sql/CalendarEventRepository.ts
**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts
  • packages/features/calendar-cache-sql/CalendarSubscriptionService.ts
  • packages/features/calendar-cache-sql/CalendarEventRepository.ts
  • packages/features/calendar-cache-sql/CalendarCacheSqlService.ts
  • packages/features/calendar-cache-sql/CalendarCacheSqlService.test.ts
  • 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/CalendarSubscriptionRepository.ts
  • packages/features/calendar-cache-sql/CalendarSubscriptionService.ts
  • packages/features/calendar-cache-sql/CalendarEventRepository.ts
  • packages/features/calendar-cache-sql/CalendarCacheSqlService.ts
  • packages/features/calendar-cache-sql/CalendarCacheSqlService.test.ts
  • packages/features/calendar-cache-sql/CalendarCacheService.ts
**/*Service.ts

📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)

Service files must include Service suffix, use PascalCase matching exported class, and avoid generic names (e.g., MembershipService.ts)

Files:

  • packages/features/calendar-cache-sql/CalendarSubscriptionService.ts
  • packages/features/calendar-cache-sql/CalendarCacheSqlService.ts
  • packages/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.ts
  • packages/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.ts
  • packages/features/calendar-cache-sql/CalendarCacheSqlService.ts
  • packages/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.ts
  • packages/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.ts
  • packages/features/calendar-cache-sql/CalendarEventRepository.ts
  • packages/features/calendar-cache-sql/CalendarCacheSqlService.ts
  • packages/features/calendar-cache-sql/CalendarCacheSqlService.test.ts
  • 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/CalendarSubscriptionService.ts
  • packages/features/calendar-cache-sql/CalendarCacheSqlService.ts
  • packages/features/calendar-cache-sql/CalendarCacheSqlService.test.ts
  • 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 **/*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.example and 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 correct

Unique 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 strings

Given 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 and select usage are on point

  • Connect by subscription id, update scalar fields, bump updatedAt.
  • Returned fields match the UpsertResult contract.

52-81: Efficient availability query using select and 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 correctly

Same overlap logic and minimal selection for multiple subscriptionIds. Good.


134-151: Cleanup policy will purge past events; confirm this aligns with downstream needs

Deleting 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 safely

Conditional 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 correctness

For 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 downstream

Resolves selectedCalendarIds -> subscriptionIds -> batch events with a single query. Clear and efficient.


109-125: Availability response normalization is clean and type-safe

ISO 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: good

Avoids 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 isolation

Constructing the service with repository interfaces and a SelectedCalendar repo makes the unit tests clean and focused.


71-94: Availability mapping looks correct

Mocks and expectations validate ISO serialization and the “calendar-cache-sql” source consistently.


96-106: Error path is precise and useful

Throwing “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 schema

The Prisma schema defines a TeamFeatures model (plural) and the feature relation uses Feature.slug (a String @id), exactly matching the README’s use of the TeamFeatures table and string slugs for featureId. 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.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 backoffUntil

The 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 sane

Defensive 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 index

A 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 index

If 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 8ceb975 and e683e06.

📒 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 pattern

One 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/end

You’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 right

Uniqueness on (calendarSubscriptionId, externalEventId) prevents duplicates while allowing the same externalEventId across different subscriptions. LGTM.


16-17: Ensure Prisma schemas use @updatedat on updatedAt fields

The migration adds an updatedAt column as NOT NULL without 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.prisma

Please update each model’s updatedAt field from:

- updatedAt DateTime
+ updatedAt DateTime @updatedAt

This ensures Prisma sets a default timestamp on create and automatically updates it on save.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
packages/features/calendar-cache-sql/CalendarCacheService.ts (5)

92-100: Early return when no valid selected calendar IDs

If selectedCalendars is non-empty but none have an id, selectedCalendarIds becomes 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 parsing

Currently 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 objects

Logging full selectedCalendars can 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 IDs

If IntegrationCalendar["id"] can be string | undefined | null in some call sites, prefer a type-guard to ensure string[] 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.id is always string. If it can be number, we should adjust the return type and consumers accordingly.


20-20: Minor: extract source string into a constant

Avoid 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 SOURCE in 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.

📥 Commits

Reviewing files that changed from the base of the PR and between e683e06 and 840cae1.

📒 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 Service suffix, 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 use include, always use select
Ensure the credential.key field 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 conventions

Class 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.

zomars and others added 8 commits August 18, 2025 18:51
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
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 responses

Do 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 variant

Same 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 explicit select to comply with Prisma “select-only” guideline

This read currently returns all columns. Add a select projection 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 bursts

Using 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 context

The _error arg 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 payload

Logging 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 840cae1 and 235c274.

⛔ Files ignored due to path filters (1)
  • yarn.lock is 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 Service suffix, 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 use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • packages/features/calendar-cache-sql/CalendarCacheService.ts
  • packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts
  • packages/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.ts
  • packages/features/calendar-cache-sql/CalendarSubscriptionRepository.ts
  • packages/features/calendar-cache-sql/CalendarSubscriptionRepository.interface.ts
**/*Repository.ts

📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)

Repository files must include Repository suffix, 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 runtime

The 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 fields

Explicit select with a narrow projection is aligned with the “select-only” guideline and prevents returning credential.key.


122-134: Upsert by selectedCalendarId: concise and correct

Transactionally safe and minimal. Works with a unique index on selectedCalendarId.


267-276: Clear watch error: straightforward reset

Resets counters and backoff as expected.

packages/features/calendar-cache-sql/CalendarSubscriptionRepository.interface.ts (1)

1-14: SafeCredential and SubscriptionWithSelectedCalendar types look solid

Good 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 correct

Matches the Calendar interface’s optional method and returns the id from the injected credential.

zomars added 2 commits August 19, 2025 06:17
Updated the channelExpiration field from string to Date in CalendarSubscriptionRepository to ensure proper handling of expiration values.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.maxSyncErrors is not a valid Prisma construct. Prisma does not support column-to-column comparisons in where. Fetch a superset, filter syncErrors < maxSyncErrors in memory, then slice to the limit. Also add an orderBy for 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 selectedCalendarIds is 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).

updateSyncToken writes syncCursor. Consider renaming to updateSyncCursor (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 error parameter 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 checked

In 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) to findByCredentialId
  • Alternatively, renaming it to something like existsByCredentialId and returning a boolean

If you ever need full subscription data downstream, update this method’s select accordingly.

/// 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 235c274 and 0927d12.

📒 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 Repository suffix, 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 use include, always use select
Ensure the credential.key field 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.CredentialSelect and explicit omission of sensitive fields like credential.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-app
Copy link
Copy Markdown

graphite-app bot commented Aug 20, 2025

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.

@keithwillcode keithwillcode mentioned this pull request Aug 27, 2025
3 tasks
@keithwillcode
Copy link
Copy Markdown
Contributor

Closing as work continues in #23675

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

calendar-apps area: calendar, google calendar, outlook, lark, microsoft 365, apple calendar core area: core, team members only ✨ feature New feature or request foundation High priority Created by Linear-GitHub Sync ❗️ migrations contains migration files ready-for-e2e

Projects

None yet

Development

Successfully merging this pull request may close these issues.