Skip to content

feat: outlook cache #21072

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 87 commits into
base: main
Choose a base branch
from

Conversation

vijayraghav-io
Copy link
Contributor

@vijayraghav-io vijayraghav-io commented May 2, 2025

What does this PR do?

This PR implements the functionality to -

  • Subscribe for notifications of - create, update or delete events in Microsoft Outlook Calendars (These calendars are pre-connected by team members with their Cal.com accounts, and are mapped to the cal.com events)
  • Receive notifications through a web-hook
  • Validates & filters these notifications and computes the set of outlook calendars (having unique externalIDs) for which the calendar-cache needs invalidation( or update)
  • fetchAvailabilityAndSetCache() is called for above outlook calendars to update the calendar-cache.
  • watch and unwatch functionality is implemented through Microsoft Graph APIs /subscribe , so that the existing cron job can start to subscribe or unsubscribe based on conditions (to avoid duplicate subscriptions).
  • With the calendar-cache available for outlook calendars ,the latest availabilities are shown in real time on public event page with low latency as Graph APIs are not called every time.

Key points:
Visit the team public page with cal.cache query parameter set to true, to see the Cache effect or see [Cache Hit] in logs for office365_calendar.
Example : http://localhost:3000/team/devs/teamevent?overlayCalendar=true&layout=month_view&cal.cache=true

In this PR, the startWatchingCalendarInMicrosoft() calls /subscription endpoint targeting specific calendars. This avoids subscribing to overly broad resources (e.g., me/events for all calendar events). Instead, target specific calendars or folders (e.g., me/calendars/{calendar_id}/events) to reduce notification volume.
Screenshot 2025-05-04 at 11 02 29 PM

Next Steps or Improvements for scaling
Given the volume of usage, if we have very high number of teams, team members, team events, simultaneously updating events in outlook - the volume of subscriptions received by the web-hook can be overwhelming even with load balancers.

We can consider Asynchronous Processing: Process notifications asynchronously to avoid blocking the webhook endpoint. For example, queue incoming notifications (using a cloud message queuing system like AWS SQS or RabbitMQ or Azure Queue Storage) and process them in the background with worker nodes. This ensures the endpoint remains responsive under high load.

Also we can consider using Azure Event Hubs or Azure Event Grid as alternative delivery channels. These services are designed for high-throughput event streaming and can buffer notifications, reducing the load on our application.

Video Demo

This demo shows - on creation of an event in outlook calendar the CalendarCache is updated through the webhook
Cron job was triggered manually through api - http://localhost:3000/api/calendar-cache/cron?apiKey=

https://www.loom.com/share/468bf851a93d49849b2dab27fe751efd?sid=458f9244-e4fe-4632-897d-b93835535ff6

The second demo shows the effect of caching on public team event page-

https://www.loom.com/share/567c3d5af03e478e97a44299e9bcc8da?sid=9254a7e4-a28b-4a49-b17e-2890489fcf84

Mandatory Tasks (DO NOT REMOVE)

  • I have self-reviewed the code (A decent size PR without self-review might be rejected).
  • I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. If N/A, write N/A here and check the checkbox.
  • I confirm automated tests are in place that prove my fix is effective or that my feature works.

How should this be tested?

  • Are there environment variables that should be set?
    MICROSOFT_WEBHOOK_TOKEN - This token is sent with subscription requests through /subscriptions Graph API.
    The notifications received by webhook are verified with this token
  • What are the minimal test data to have?
    Enable calendar-cache feature flag for the team.
    Create a team event with hosts having their outlook calendars connected
  • What is expected (happy path) to have (input and output)?
    At any given point of time Calendar-Cache in cal.com has the latest busy slots in sync with changes in outlook calendar busy times or events.
    Update or create or delete event in Microsoft outlook calendar , there should be instant update in cal.com calendar-cache
    On visiting the team event public page with &cal.cahce=true, latest available slots are displayed with low latency
  • Any other important info that could help to test that PR
    To test locally use port forwarding providers

Summary by mrge

Added caching and webhook support for Outlook (Office365) calendar availability to improve performance and enable real-time updates.

  • New Features
    • Implemented webhook endpoint to receive Outlook calendar change notifications and refresh cache.
    • Added cache layer for Outlook calendar availability queries.
    • Updated database schema to store Outlook subscription info.

@vijayraghav-io vijayraghav-io requested a review from a team as a code owner May 2, 2025 05:56
Copy link

vercel bot commented May 2, 2025

@vijayraghav-io is attempting to deploy a commit to the cal Team on Vercel.

A member of the Team first needs to authorize it.

@graphite-app graphite-app bot added the community Created by Linear-GitHub Sync label May 2, 2025
@graphite-app graphite-app bot requested a review from a team May 2, 2025 05:56
@github-actions github-actions bot added $500 calendar-apps area: calendar, google calendar, outlook, lark, microsoft 365, apple calendar teams area: teams, round robin, collective, managed event-types ✨ feature New feature or request 💎 Bounty A bounty on Algora.io ❗️ migrations contains migration files ❗️ .env changes contains changes to env variables labels May 2, 2025
Copy link

graphite-app bot commented May 2, 2025

Graphite Automations

"Add consumer team as reviewer" took an action on this PR • (05/02/25)

1 reviewer was added to this PR based on Keith Williams's automation.

"Add community label" took an action on this PR • (05/02/25)

1 label was added to this PR based on Keith Williams's automation.

"Add foundation team as reviewer" took an action on this PR • (05/16/25)

1 reviewer was added to this PR based on Keith Williams's automation.

@vijayraghav-io vijayraghav-io requested a review from a team as a code owner May 2, 2025 06:08
Comment on lines +168 to +176
if (!isDelegated) {
const user = await this.fetcher("/me");
const userResponseBody = await handleErrorsJson<User>(user);
this.azureUserId = userResponseBody.userPrincipalName ?? undefined;
if (!this.azureUserId) {
throw new Error("UserPrincipalName is missing for non-delegated user");
}
return this.azureUserId;
}
Copy link
Contributor Author

@vijayraghav-io vijayraghav-io May 2, 2025

Choose a reason for hiding this comment

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

Thought of handling non-delegatedTo scenarios.

Comment on lines +220 to +221
}
this.azureUserId = parsedBody.value[0].userPrincipalName ?? undefined;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using UPN instead of GUID

The Microsoft Graph documentation (Outlook change notifications overview) specifies that the {id} in /users/{id}/events can be either:

  • A user principal name (UPN, e.g., user@domain.com).
  • A user’s Azure AD ID (GUID).

Reason for using UPN :

  • It’s more readable and aligns with Cal’s use of email-based identifiers.
  • It’s supported for all Graph API operations, including subscriptions.
  • It avoids the need for additional queries to map GUIDs to users in delegated scenarios.

@@ -52,6 +57,9 @@ interface BodyValue {
start: { dateTime: string };
}

const MICROSOFT_WEBHOOK_URL = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/integrations/office365calendar/webhook`;
const MICROSOFT_SUBSCRIPTION_TTL = 3 * 24 * 60 * 60 * 1000; // 3 days in milliseconds
Copy link
Contributor Author

@vijayraghav-io vijayraghav-io May 2, 2025

Choose a reason for hiding this comment

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

recommended TTL for calendar event subscriptions is 4320 mins (~ 3 Days)
Ref - https://learn.microsoft.com/en-us/answers/questions/2184864/wrong-max-subscription-expiration-on-meetingcallev

hbjORbj
hbjORbj previously requested changes May 2, 2025
Copy link
Contributor

@hbjORbj hbjORbj left a comment

Choose a reason for hiding this comment

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

That was quick! Can you add some unit tests please?

@vijayraghav-io
Copy link
Contributor Author

vijayraghav-io commented May 3, 2025

That was quick! Can you add some unit tests please?

Sure will add 🙏

@coderabbitai coderabbitai bot mentioned this pull request Aug 6, 2025
Copy link
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

🔭 Outside diff range comments (1)
packages/app-store/office365calendar/lib/CalendarService.ts (1)

348-390: Add input validation for date range and calendar IDs.

The method should validate inputs before processing to prevent unnecessary API calls and potential errors.

Add validation at the beginning of the method:

 async fetchAvailability(
   dateFrom: string,
   dateTo: string,
   calendarIds: string[],
   getCreatedDateTime = false
 ): Promise<EventBusyDate[]> {
+  if (!calendarIds || calendarIds.length === 0) {
+    return [];
+  }
+  
   const dateFromParsed = new Date(dateFrom);
   const dateToParsed = new Date(dateTo);
+  
+  if (isNaN(dateFromParsed.getTime()) || isNaN(dateToParsed.getTime())) {
+    throw new Error("Invalid date format provided");
+  }
+  
+  if (dateFromParsed >= dateToParsed) {
+    throw new Error("dateFrom must be before dateTo");
+  }
♻️ Duplicate comments (2)
packages/app-store/office365calendar/lib/CalendarService.ts (2)

697-699: LGTM! Well-documented subscription scoping.

The implementation correctly scopes subscriptions to specific calendars using /calendars/{calendar_id}/events to reduce notification volume, as documented in the Microsoft Graph API guidelines.


700-700: LGTM! Using recommended TTL for calendar subscriptions.

The 3-day TTL aligns with Microsoft's recommended duration for calendar event subscriptions as per the documentation.

🧹 Nitpick comments (3)
packages/app-store/office365calendar/lib/CalendarService.ts (3)

405-436: Consider implementing cache warming strategy.

The cache miss path fetches data but doesn't automatically populate the cache. Consider warming the cache after a miss to benefit subsequent requests.

Consider updating the cache after a miss:

 this.log.debug("[Cache Miss] Fetching availability", { dateFrom, dateTo, calendarIds });
 const data = await this.fetchAvailability(dateFrom, dateTo, calendarIds);
+
+// Optionally warm the cache for future requests
+await this.setAvailabilityInCache(cacheArgs, data).catch((err) => {
+  this.log.warn("Failed to warm cache after miss", err);
+});
+
 return data;

712-726: Add error handling for failed subscription deletions.

While the method logs warnings for failed deletions, consider tracking which deletions failed and potentially retrying or escalating critical failures.

Consider tracking failed deletions:

 private async stopWatchingCalendarsInMicrosoft(subscriptions: { subscriptionId: string | null }[]) {
   const uniqueSubscriptions = subscriptions.filter(
     (s, i, arr) => s.subscriptionId && arr.findIndex((x) => x.subscriptionId === s.subscriptionId) === i
   );

-  await Promise.allSettled(
+  const results = await Promise.allSettled(
     uniqueSubscriptions.map(({ subscriptionId }) =>
       subscriptionId
         ? this.fetcher(`/subscriptions/${subscriptionId}`, { method: "DELETE" }).catch((err) => {
             this.log.warn(`Failed to delete subscription ${subscriptionId}`, err);
+            throw err; // Re-throw to capture in allSettled
           })
         : Promise.resolve()
     )
   );
+  
+  const failedDeletions = results.filter(r => r.status === 'rejected');
+  if (failedDeletions.length > 0) {
+    this.log.error(`Failed to delete ${failedDeletions.length} subscriptions`);
+  }
 }

728-756: Consider batching cache operations for performance.

The method processes calendars per event type sequentially. For better performance with many event types, consider batching the cache operations.

Consider parallel processing with batching:

-for (const [_eventTypeId, selectedCalendars] of Array.from(selectedCalendarsPerEventType.entries())) {
-  const parsedArgs = {
-    timeMin: getTimeMin(),
-    timeMax: getTimeMax(),
-    items: selectedCalendars.map((sc) => ({ id: sc.externalId })),
-  };
-  const data = await this.fetchAvailability(
-    parsedArgs.timeMin,
-    parsedArgs.timeMax,
-    parsedArgs.items.map((i) => i.id)
-  );
-  await this.setAvailabilityInCache(parsedArgs, data);
-}
+const cacheOperations = Array.from(selectedCalendarsPerEventType.entries()).map(
+  async ([_eventTypeId, selectedCalendars]) => {
+    const parsedArgs = {
+      timeMin: getTimeMin(),
+      timeMax: getTimeMax(),
+      items: selectedCalendars.map((sc) => ({ id: sc.externalId })),
+    };
+    const data = await this.fetchAvailability(
+      parsedArgs.timeMin,
+      parsedArgs.timeMax,
+      parsedArgs.items.map((i) => i.id)
+    );
+    return this.setAvailabilityInCache(parsedArgs, data);
+  }
+);
+
+await Promise.all(cacheOperations);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8c201de and e2aadb2.

📒 Files selected for processing (1)
  • packages/app-store/office365calendar/lib/CalendarService.ts (9 hunks)
🧰 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/app-store/office365calendar/lib/CalendarService.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/app-store/office365calendar/lib/CalendarService.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/app-store/office365calendar/lib/CalendarService.ts
🧠 Learnings (5)
📓 Common learnings
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.
📚 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/app-store/office365calendar/lib/CalendarService.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/app-store/office365calendar/lib/CalendarService.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/app-store/office365calendar/lib/CalendarService.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/app-store/office365calendar/lib/CalendarService.ts
🧬 Code Graph Analysis (1)
packages/app-store/office365calendar/lib/CalendarService.ts (4)
packages/types/Calendar.d.ts (3)
  • EventBusyDate (56-62)
  • IntegrationCalendar (247-256)
  • SelectedCalendarEventTypeIds (261-261)
packages/features/calendar-cache/calendar-cache.ts (1)
  • CalendarCache (15-31)
packages/features/calendar-cache/lib/datesForCache.ts (2)
  • getTimeMin (2-8)
  • getTimeMax (21-59)
packages/lib/server/repository/selectedCalendar.ts (1)
  • SelectedCalendarRepository (48-502)
⏰ 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: Install dependencies / Yarn install & cache
🔇 Additional comments (3)
packages/app-store/office365calendar/lib/CalendarService.ts (3)

177-185: LGTM! Proper handling of non-delegated scenarios.

The implementation correctly fetches the UserPrincipalName (UPN) for non-delegated users using the /me endpoint and includes appropriate error handling when the UPN is missing.


208-230: LGTM! Well-documented implementation using UPN.

The implementation correctly uses UserPrincipalName (UPN) instead of GUID, which aligns with Microsoft Graph documentation and improves readability. The regex pattern for removing OAuth client ID aliases is appropriate.


1003-1021: LGTM! Clean implementation of busy time processing.

The method correctly filters out free/workingElsewhere events and properly enriches the response with optional id and createdDateTime fields when available.

Copy link
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 (4)
packages/app-store/office365calendar/lib/CalendarService.ts (4)

177-185: LGTM! Non-delegated user handling is correct.

The implementation properly extracts userPrincipalName from the /me endpoint for non-delegated scenarios and includes appropriate error handling for missing values. This aligns with the past review comment addressing non-delegatedTo scenarios.


208-236: LGTM! Delegated user credential handling is well-implemented.

The implementation correctly:

  • Uses UPN instead of GUID as noted in past comments
  • Handles OAuth client alias regex pattern removal from email
  • Provides proper error handling for missing users and UserPrincipalName
  • Uses appropriate Microsoft Graph API filtering

The approach follows Microsoft's recommended practices for delegated scenarios.


566-569: Consider returning a structured error response for consistency.

When MICROSOFT_WEBHOOK_TOKEN is not set, the method returns undefined. For better error handling consistency, consider returning a structured error response.

Apply this diff to improve error handling consistency:

 if (!process.env.MICROSOFT_WEBHOOK_TOKEN) {
   this.log.warn("MICROSOFT_WEBHOOK_TOKEN is not set, skipping watching calendar");
-  return;
+  return { error: "MICROSOFT_WEBHOOK_TOKEN is not configured" };
 }

692-710: LGTM! Microsoft Graph subscription creation is properly implemented.

The private method correctly:

  • Uses appropriate change types (created, updated, deleted)
  • Sets proper webhook URL and client state for validation
  • Respects the recommended TTL for calendar subscriptions (3 days)
  • Returns structured subscription data

The implementation follows Microsoft Graph API best practices as noted in past comments.

🧹 Nitpick comments (1)
packages/app-store/office365calendar/lib/CalendarService.ts (1)

2-2: Consider the necessity of the Prisma type import.

The Prisma import is only used for the Prisma.SelectedCalendarUncheckedCreateInput type in the upsertSelectedCalendarsForEventTypeIds method. If this is the sole usage, consider whether a more specific import or inline type definition would be more appropriate.

📜 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 b346ffa and ae32304.

📒 Files selected for processing (2)
  • packages/app-store/office365calendar/lib/CalendarService.ts (9 hunks)
  • turbo.json (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • turbo.json
🧰 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/app-store/office365calendar/lib/CalendarService.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/app-store/office365calendar/lib/CalendarService.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/app-store/office365calendar/lib/CalendarService.ts
🧠 Learnings (5)
📓 Common learnings
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.
📚 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/app-store/office365calendar/lib/CalendarService.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/app-store/office365calendar/lib/CalendarService.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/app-store/office365calendar/lib/CalendarService.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/app-store/office365calendar/lib/CalendarService.ts
🧬 Code Graph Analysis (1)
packages/app-store/office365calendar/lib/CalendarService.ts (4)
packages/types/Calendar.d.ts (2)
  • EventBusyDate (56-62)
  • SelectedCalendarEventTypeIds (261-261)
packages/features/calendar-cache/calendar-cache.ts (1)
  • CalendarCache (15-31)
packages/features/calendar-cache/lib/datesForCache.ts (2)
  • getTimeMin (2-8)
  • getTimeMax (21-59)
packages/lib/server/repository/selectedCalendar.ts (1)
  • SelectedCalendarRepository (48-502)
🔇 Additional comments (14)
packages/app-store/office365calendar/lib/CalendarService.ts (14)

64-67: LGTM! Well-defined interface for enriched busy time data.

The EnrichedBufferedBusyTime interface properly extends BufferedBusyTime with optional id and createdDateTime fields, providing type safety for the enhanced availability data processing pipeline.


69-71: LGTM! Constants are appropriately exported and documented.

The webhook URL construction and subscription TTL constant (3 days) align with Microsoft Graph API recommendations and provide clear configuration values for the webhook integration.


267-268: LGTM! Proper user endpoint construction.

The method correctly uses the resolved Azure User ID when available, falling back to /me for non-delegated scenarios.


348-390: LGTM! Enhanced availability fetching with proper batching.

The implementation correctly:

  • Supports optional createdDateTime retrieval through the getCreatedDateTime parameter
  • Uses appropriate Microsoft Graph API endpoints (calendarView vs events)
  • Implements proper batch request handling with retry logic
  • Processes responses through the updated processBusyTimes method

The batching approach efficiently handles multiple calendar queries while respecting API limits.


392-403: LGTM! Cache integration is properly implemented.

The setAvailabilityInCache method correctly uses the CalendarCache feature with proper credential context and JSON serialization of the availability data.


405-436: LGTM! Cache-or-fetch strategy is well-designed.

The implementation provides:

  • Proper cache bypass when shouldServeCache is false
  • Appropriate cache key generation using getTimeMin/getTimeMax utilities
  • Clear debug logging for cache hits and misses
  • Fallback to direct fetching when cache misses occur

The caching strategy will significantly improve performance for frequently accessed calendar data.


438-510: LGTM! Comprehensive availability orchestration with 90-day chunking.

The implementation correctly handles:

  • Calendar ID resolution from selected calendars or fallback to primary
  • Date range chunking for periods > 90 days to respect API limits
  • Proper loop calculation and boundary handling
  • Cache integration through getCacheOrFetchAvailability
  • Error handling with logging

The 90-day chunking strategy efficiently manages large date ranges while maintaining performance through caching.


557-620: LGTM! Subscription management with proper reuse logic.

The watchCalendar method correctly:

  • Validates webhook token availability
  • Implements subscription reuse across event types to avoid duplicates
  • Handles subscription creation only when necessary
  • Provides proper error handling and logging
  • Updates SelectedCalendar records with subscription details

This approach optimizes subscription usage while maintaining proper lifecycle management.


622-690: LGTM! Comprehensive unwatch logic with proper cleanup.

The unwatchCalendar method correctly handles:

  • Subscription sharing validation across event types
  • Selective removal when other event types still need the subscription
  • Complete subscription deletion when no longer needed
  • Cache cleanup and availability refresh for remaining calendars
  • Proper SelectedCalendar record updates

The logic ensures efficient subscription management while maintaining data consistency.


712-726: LGTM! Subscription cleanup with proper deduplication.

The method correctly handles:

  • Deduplication of subscription IDs to avoid redundant API calls
  • Error handling for failed deletions with appropriate logging
  • Promise settlement to ensure all cleanup attempts complete

This ensures reliable subscription lifecycle management.


728-756: LGTM! Cache refresh orchestration is well-structured.

The fetchAvailabilityAndSetCache method properly:

  • Groups selected calendars by event type for efficient processing
  • Uses appropriate cache date ranges with getTimeMin/getTimeMax
  • Fetches fresh availability data and updates the cache
  • Handles multiple event type scenarios

This method serves as the core webhook-triggered cache refresh mechanism.


758-782: LGTM! Batch upsert with proper validation.

The method correctly:

  • Validates required userId presence
  • Uses the SelectedCalendarRepository for consistent data operations
  • Includes proper credential context and delegation handling
  • Provides debug logging for troubleshooting

The implementation ensures consistent SelectedCalendar record management across event types.


1003-1021: LGTM! Enhanced busy time processing with optional fields.

The updated processBusyTimes method correctly:

  • Returns EnrichedBufferedBusyTime[] with optional id and createdDateTime fields
  • Conditionally includes the new fields only when present in the source data
  • Maintains backward compatibility for existing functionality
  • Preserves the existing filtering logic for free/workingElsewhere events

The implementation properly supports the enriched data flow while maintaining existing behavior.


60-62: LGTM! Enhanced BodyValue interface supports enriched data.

The addition of optional id and createdDateTime fields to the BodyValue interface properly supports the enriched availability data processing pipeline while maintaining backward compatibility.

@vijayraghav-io
Copy link
Contributor Author

Reminding...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🙋 Bounty claim 💎 Bounty A bounty on Algora.io calendar-apps area: calendar, google calendar, outlook, lark, microsoft 365, apple calendar community Created by Linear-GitHub Sync ❗️ .env changes contains changes to env variables ✨ feature New feature or request High priority Created by Linear-GitHub Sync high-risk Requires approval by Foundation team ❗️ migrations contains migration files teams area: teams, round robin, collective, managed event-types webhooks area: webhooks, callback, webhook payload $500
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Outlook Cache – Bounty-to-Hire
5 participants