Skip to content

Conversation

@alishaz-polymath
Copy link
Member

What does this PR do?

Refactors watchlist blocking to use batched checks and graceful filtering instead of the previous N+1 query pattern that hard-failed if any user was blocked.

Key Changes:

1. Batch Watchlist Methods (N+1 → O(1))

  • Added areBlocked(emails[]) to GlobalBlockingService and OrganizationBlockingService
  • Added findBlockingEntriesForEmailsAndDomains() to repository interfaces
  • Single DB query for N users instead of N queries

2. Slot Display: Filter Blocked Hosts

  • New filterBlockedHosts() controller filters hosts BEFORE calculating availability
  • Blocked users' schedules no longer affect team aggregate availability
  • Returns empty slots only if ALL hosts are blocked

3. Booking: Graceful Filtering

  • New filterBlockedUsers() controller replaces checkIfUsersAreBlocked()
  • Team events proceed with eligible users instead of 404'ing
  • Only throws 404 if ALL users are blocked

Before: Team of 100 users, 1 blocked → ❌ 404 error, entire event unavailable
After: Team of 100 users, 1 blocked → ✅ 99 users available, booking succeeds

  • Fixes CAL-XXXX

Visual Demo (For contributors especially)

Video Demo (if applicable):

  • Before: Add a user to watchlist → Their team events return 404, all slots disappear
  • After: Add a user to watchlist → Their slots are filtered out, other team members remain bookable

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. N/A - internal behavior change, no API changes.
  • I confirm automated tests are in place that prove my fix is effective or that my feature works.

How should this be tested?

Prerequisites:

  • A team with multiple members (Round Robin or Collective event)
  • Access to admin watchlist functionality

Test Steps:

  1. Setup:

    • Create a Round Robin event with 3+ team members
    • Note one member's email (e.g., blocked@example.com)
  2. Before blocking - verify baseline:

    • Visit the booking page → All team members' slots should appear
    • Book a slot → Should succeed
  3. Block a user:

    • Add blocked@example.com to the global watchlist (BLOCK action)
  4. After blocking - verify graceful filtering:

    • Refresh the booking page → Slots should still appear (from non-blocked members)
    • The blocked user's specific availability should NOT appear
    • Book a slot → Should succeed and assign to a non-blocked member
  5. Edge case - all users blocked:

    • Block all team members
    • Visit booking page → Should show no availability
    • Attempt to book → Should return 404

Expected Behavior:

Scenario Before After
1 of 3 members blocked ❌ 404 on booking page ✅ 2 members' slots shown
Booking attempt ❌ 404 error ✅ Books with eligible member
All members blocked ❌ 404 ✅ 404 (correct behavior)

Performance Verification:

  • Check DB queries in dev tools - should see single SELECT for watchlist check regardless of team size

Checklist

  • I have read the contributing guide
  • My code follows the style guidelines of this project
  • I have commented my code, particularly in hard-to-understand areas
  • I have checked that my changes generate no new warnings

@alishaz-polymath alishaz-polymath requested a review from a team as a code owner January 5, 2026 06:37
@alishaz-polymath alishaz-polymath marked this pull request as draft January 5, 2026 06:37
@graphite-app graphite-app bot added enterprise area: enterprise, audit log, organisation, SAML, SSO core area: core, team members only labels Jan 5, 2026
@graphite-app graphite-app bot requested a review from a team January 5, 2026 06:37
@vercel
Copy link

vercel bot commented Jan 5, 2026

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

4 Skipped Deployments
Project Deployment Review Updated (UTC)
api-v2 Ignored Ignored Preview Jan 7, 2026 8:33pm
cal Ignored Ignored Jan 7, 2026 8:33pm
cal-companion Ignored Ignored Preview Jan 7, 2026 8:33pm
cal-eu Ignored Ignored Jan 7, 2026 8:33pm

cubic-dev-ai[bot]

This comment was marked as resolved.

@github-actions github-actions bot added the ❗️ migrations contains migration files label Jan 5, 2026
@@unique([type, value, organizationId])
@@index([type, value, organizationId, action])
@@index([isGlobal, action, organizationId, type, value])
Copy link
Member Author

Choose a reason for hiding this comment

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

This ordering is optimal because:

  1. isGlobal (boolean) - filters to global vs org entries immediately
  2. action (enum: REPORT/BLOCK/ALERT) - narrows to blocking entries
  3. organizationId - filters by specific org (or null for global)
  4. type (EMAIL/DOMAIN) - before the range scan
  5. value - at the end for the IN clause range lookup

This matches both query patterns:

  • Global: isGlobal=true, action=BLOCK, organizationId=null, type IN (EMAIL,DOMAIN), value IN (...)
  • Org: isGlobal=false, action=BLOCK, organizationId=X, type IN (EMAIL,DOMAIN), value IN (...)

@pull-request-size pull-request-size bot added size/XL and removed size/L labels Jan 5, 2026
@alishaz-polymath alishaz-polymath requested a review from a team as a code owner January 5, 2026 19:00
cubic-dev-ai[bot]

This comment was marked as resolved.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 7, 2026

E2E results are ready!

cubic-dev-ai[bot]

This comment was marked as resolved.

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
organizationId?: number | null
): Promise<CheckUserBlockingResult> {
// Filter out users with empty/invalid emails
const validUsers = users.filter((u) => u.email && u.email.trim().length > 0);
Copy link
Member

Choose a reason for hiding this comment

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

nit: Instead of doing emai.trim twice at 103 and here, we could do map first here and then filter. Tha would ensure validUsers have email trimmed already

Comment on lines +1100 to +1101
const organizationId = eventType.parent?.team?.parentId ?? eventType.team?.parentId ?? null;

Copy link
Member

Choose a reason for hiding this comment

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

This isn't consistent with what we are doing in loadAndValidateUsers. It is missing Organization members' personal events' handling

Copy link
Contributor

@volnei volnei left a comment

Choose a reason for hiding this comment

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

Great work!

* Bulk check multiple emails in a single query.
* Returns Map<email (lowercase), BlockingResult> for efficient lookup.
*/
async areBlocked(emails: string[]): Promise<BulkBlockingResult> {
Copy link
Contributor

Choose a reason for hiding this comment

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

❤️

@volnei volnei merged commit 59fca85 into main Jan 8, 2026
50 checks passed
@volnei volnei deleted the fix/team-events-not-working-blocklist branch January 8, 2026 11:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core area: core, team members only enterprise area: enterprise, audit log, organisation, SAML, SSO ❗️ migrations contains migration files ready-for-e2e size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants