Skip to content

Conversation

@2witstudios
Copy link
Owner

@2witstudios 2witstudios commented Feb 2, 2026

Summary

This PR introduces a complete calendar event management system with REST API endpoints and React UI components. Users can create, read, update, and delete calendar events with support for attendees, RSVP management, recurrence rules, and real-time WebSocket broadcasting.

Key Changes

API Endpoints

  • POST /api/calendar/events - Create new calendar events with optional attendees and recurrence rules
  • GET /api/calendar/events - List events with filtering by date range and context (user/drive)
  • GET /api/calendar/events/[eventId] - Fetch individual event details with full relations
  • PATCH /api/calendar/events/[eventId] - Update event details (creator only)
  • DELETE /api/calendar/events/[eventId] - Soft delete events (move to trash)
  • GET /api/calendar/events/[eventId]/attendees - List event attendees
  • POST /api/calendar/events/[eventId]/attendees - Add attendees to event (creator only)
  • PATCH /api/calendar/events/[eventId]/attendees - Update RSVP status for current user
  • DELETE /api/calendar/events/[eventId]/attendees - Remove attendees from event

UI Components

  • CalendarView - Main calendar component supporting multiple view modes (month, week, day, agenda)
  • MonthView - Traditional month grid with event indicators
  • WeekView - Week-based timeline view with hourly slots
  • DayView - Detailed day view with time-based event placement
  • AgendaView - List-based view grouped by date
  • EventModal - Create/edit event dialog with form validation
  • EventDetailsModal - View event details and manage RSVP

Pages

  • /dashboard/calendar - User's personal calendar view
  • /dashboard/[driveId]/calendar - Drive-specific calendar view

Notable Implementation Details

  • Access Control: Events support three visibility levels (DRIVE, ATTENDEES_ONLY, PRIVATE) with granular permission checks
  • RSVP Management: Attendees can respond with status (PENDING, ACCEPTED, DECLINED, TENTATIVE) and optional notes
  • Recurrence Support: Full iCalendar-compatible recurrence rules (DAILY, WEEKLY, MONTHLY, YEARLY)
  • Real-time Updates: WebSocket broadcasting for event changes to all attendees
  • Soft Deletes: Events are marked as trashed rather than permanently deleted
  • Date Filtering: Efficient date-range queries for calendar views
  • Duplicate Prevention: Prevents adding the same user as attendee multiple times
  • Creator Privileges: Only event creators can add/remove attendees and edit event details
  • Organizer Protection: Cannot remove the event organizer/creator from attendee list

https://claude.ai/code/session_014UwUdTDf7hSk3nuC1rTnBU

Summary by CodeRabbit

Release Notes

  • New Features
    • Comprehensive calendar interface with month, week, day, and agenda views.
    • Full event lifecycle management: create, edit, and delete calendar events with customizable details.
    • Attendee management and RSVP tracking with response status visibility.
    • Real-time event synchronization for seamless collaborative scheduling.
    • Integrated task display within calendar views.
    • Support for both personal and drive-scoped calendars.

Add a comprehensive calendar system to PageSpace with the following features:

Database Schema:
- calendarEvents table with support for drive-scoped and personal events
- eventAttendees table for tracking invitations and RSVP status
- Recurrence rules support (DAILY, WEEKLY, MONTHLY, YEARLY)
- Event visibility levels (DRIVE, ATTENDEES_ONLY, PRIVATE)

API Routes:
- GET/POST /api/calendar/events - list and create events
- GET/PATCH/DELETE /api/calendar/events/[eventId] - event CRUD
- GET/POST/PATCH/DELETE /api/calendar/events/[eventId]/attendees - attendee management

UI Components:
- CalendarView with Month, Week, Day, and Agenda views
- EventModal for creating and editing events
- Task overlay integration (tasks appear muted behind events)
- Color-coded events with multiple category options
- Responsive design (agenda view on mobile)

Page Routes:
- /dashboard/calendar - personal calendar (aggregates all events)
- /dashboard/[driveId]/calendar - drive-specific calendar

Real-time Updates:
- Socket.IO broadcasts for calendar events
- Broadcasts to drive channel and individual attendee channels

https://claude.ai/code/session_014UwUdTDf7hSk3nuC1rTnBU
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 2, 2026

📝 Walkthrough

Walkthrough

This PR introduces a complete calendar feature including database schema for events and attendees, RESTful API endpoints for calendar CRUD operations with attendee management, frontend pages and UI components for multiple calendar views (month, week, day, agenda), an event modal for creation/editing, and real-time WebSocket broadcasting for calendar updates.

Changes

Cohort / File(s) Summary
Calendar API Routes
apps/web/src/app/api/calendar/events/route.ts, apps/web/src/app/api/calendar/events/[eventId]/route.ts, apps/web/src/app/api/calendar/events/[eventId]/attendees/route.ts
Full CRUD endpoints for events with GET (list/fetch with date filtering), POST (create with attendee assignment), PATCH (update with validation), DELETE (soft-delete). Attendee management includes GET (list with user info), POST (add attendees), PATCH (RSVP status), DELETE (remove attendee). All handlers include authentication, authorization checks, validation, and broadcasting integration.
Calendar UI Views
apps/web/src/components/calendar/MonthView.tsx, apps/web/src/components/calendar/WeekView.tsx, apps/web/src/components/calendar/DayView.tsx, apps/web/src/components/calendar/AgendaView.tsx
Four distinct calendar view components rendering events and tasks with day/week/month/agenda layouts, time grids, event positioning, all-day sections, interactive date/view navigation, and click handlers for event/task interactions.
Calendar Container & Pages
apps/web/src/components/calendar/CalendarView.tsx, apps/web/src/app/dashboard/calendar/page.tsx, apps/web/src/app/dashboard/[driveId]/calendar/page.tsx
Main CalendarView orchestrates state, data fetching, view switching, and event creation/editing flows with handlers for all views. User and drive-specific calendar pages provide context-aware wrappers with Suspense boundaries.
Event Modal & Infrastructure
apps/web/src/components/calendar/EventModal.tsx, apps/web/src/components/calendar/calendar-types.ts, apps/web/src/components/calendar/useCalendarData.ts, apps/web/src/components/calendar/index.ts
EventModal handles event creation/editing with form validation, time composition, and all-day handling. calendar-types defines enums, interfaces, constants, and utility helpers for colors, date comparisons, and event filtering. useCalendarData hook aggregates events/tasks, fetches via SWR, and provides mutation helpers for CRUD and RSVP updates.
Real-time Broadcasting
apps/web/src/lib/websocket/calendar-events.ts, apps/web/src/lib/websocket/index.ts
Broadcasting utilities for publishing calendar events (create/update/delete/rsvp_updated) to drive and attendee channels via Socket.IO, with payload construction and environment-safe handling.
Database Schema
packages/db/src/schema/calendar.ts, packages/db/src/schema.ts
New calendar schema with calendar_events table (timestamps, recurrence, visibility, soft-delete), event_attendees table (status, RSVP tracking, organizer flag), enums for visibility and attendee status, and relational mappings. Schema integration via export and composition.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client<br/>(Browser)
    participant Server as API Server<br/>/api/calendar/events
    participant DB as Database
    participant WS as WebSocket<br/>Broadcaster

    rect rgba(100, 150, 200, 0.5)
    Note over Client,WS: Event Creation Flow
    Client->>Server: POST /api/calendar/events<br/>(title, startAt, endAt,<br/>attendeeIds, driveId)
    Server->>Server: Validate auth & body
    Server->>DB: Insert calendar_events<br/>with createdById
    Server->>DB: Insert event_attendees<br/>(creator as organizer,<br/>others as PENDING)
    DB-->>Server: Return created event<br/>with relations
    Server->>WS: Broadcast 'created' event<br/>to drive & attendees
    WS-->>WS: Publish to drive:${driveId}:calendar<br/>and user:${attendeeId}:calendar
    Server-->>Client: Return 201 + event
    Client->>Client: Update local state via SWR
    end
Loading
sequenceDiagram
    participant Client as Client<br/>(Browser)
    participant Hook as useCalendarData<br/>Hook
    participant Server as API Server<br/>/api/calendar/events
    participant DB as Database

    rect rgba(100, 150, 200, 0.5)
    Note over Client,DB: Event Fetching & Display Flow
    Client->>Hook: useCalendarData(context,<br/>driveId, dateRange)
    Hook->>Hook: Compute buffered date range<br/>& build query URLs
    Hook->>Server: SWR fetch GET /api/calendar/events?<br/>context, driveId, startDate, endDate
    Server->>Server: Validate context & auth
    alt User Context
        Server->>DB: Query personal events +<br/>drive-scoped events +<br/>attendee events
    else Drive Context
        Server->>DB: Query drive-scoped events<br/>within date range
    end
    DB-->>Server: Return events with relations<br/>(createdBy, attendees, page, drive)
    Server-->>Hook: Return JSON events
    Hook-->>Client: Return {events, tasks,<br/>loading, error, ...mutations}
    Client->>Client: Render CalendarView<br/>with current view mode
    Note over Client: Display events in<br/>MonthView/WeekView/DayView/AgendaView
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • PR #206: Shares the getDriveIdsForUser permission helper used across calendar and task API routes for drive membership validation and event filtering by user-accessible drives.

Poem

🐰 Hop through calendars now with glee,
Events dancing in harmony!
Weeks and days and months align,
Real-time broadcasts—events so fine!
From schema to views, a feature complete,
Making calendars perfectly neat! 📅✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 79.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly describes the main feature being added: a calendar event management system with both API endpoints and UI components.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/add-pageSpace-calendars-ucezA

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

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bb927cff4a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +48 to +52
const event = await db.query.calendarEvents.findFirst({
where: and(
eq(calendarEvents.id, eventId),
eq(calendarEvents.isTrashed, false)
),

Choose a reason for hiding this comment

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

P1 Badge Enforce access checks before returning attendee list

The attendees endpoint returns the full attendee list (including emails) for any existing event without verifying that the caller can access the event. Because only isTrashed and id are checked here, any authenticated user who can guess or obtain an eventId can enumerate attendees for private or attendees-only events. This is a privacy leak; the handler should apply the same access rules used by the event GET (creator/attendee or drive member with DRIVE visibility) before returning attendee details.

Useful? React with 👍 / 👎.

Comment on lines +110 to +114
where: and(
eq(calendarEvents.driveId, params.driveId),
eq(calendarEvents.isTrashed, false),
lte(calendarEvents.startAt, params.endDate),
gte(calendarEvents.endAt, params.startDate)

Choose a reason for hiding this comment

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

P1 Badge Filter drive calendar results by event visibility

In drive context, the listing query returns all events in the drive regardless of visibility. That means events marked ATTENDEES_ONLY or PRIVATE become visible to every drive member who hits the drive calendar view. This bypasses the visibility model described elsewhere in the API (where only attendees/creator can see those events). The drive listing should restrict to visibility = DRIVE or include only events where the user is an attendee/creator.

Useful? React with 👍 / 👎.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/db/src/schema.ts (1)

25-79: ⚠️ Potential issue | 🔴 Critical

Generate and commit migrations for the new calendar schema.

The calendar schema defines two tables (calendarEvents and eventAttendees) with enums, indexes, and relations, but no corresponding migration exists in packages/db/drizzle/. Run pnpm db:generate to auto-generate the required migration files before merging.

🤖 Fix all issues with AI agents
In `@apps/web/src/app/api/calendar/events/`[eventId]/attendees/route.ts:
- Around line 44-69: The GET handler currently reads auth.userId into _userId
but never enforces access controls; after fetching the event (calendarEvents)
check its visibility field and enforce that if event.visibility === 'PRIVATE' or
'ATTENDEES_ONLY' the requester is either the event.creatorId or is listed as an
attendee: query eventAttendees.findFirst({ where: and(eq(eventAttendees.eventId,
eventId), eq(eventAttendees.userId, _userId)) }) and if neither condition holds
return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); only then
proceed to fetch and return the attendees.
- Around line 145-154: Before calling db.insert on eventAttendees with
newUserIds, validate that each userId exists in the users table: query the users
table (e.g., select id from users where id in newUserIds) to get existing IDs,
compute missingIds = newUserIds - existingIds, and if missingIds is non-empty
return a 400 response listing the invalid user IDs; only call
db.insert(eventAttendees).values(...) with the verified existing IDs (or abort
with the clear error) so the handler avoids a DB foreign-key error and provides
a precise client-facing message.

In `@apps/web/src/app/api/calendar/events/route.ts`:
- Around line 92-135: The drive-context branch (when params.context === 'drive')
returns all drive events and thus exposes ATTENDEES_ONLY/PRIVATE events; update
the db.query.calendarEvents.findMany where clause to enforce visibility: only
include events with calendarEvents.visibility === 'DRIVE' unless the requester
is the event creator (createdBy.id === userId) or is listed as an
attendee/organizer. Implement this by adding an OR group to the existing
and(...) predicate that permits events where visibility is DRIVE OR
(createdBy.id == userId) OR (exists an attendee record for userId with role
organizer/attendee), using the calendarEvents.attendees relation (or an
exists/subquery) to test membership; keep the with: attendees/createdBy
projections and preserve other filters (driveId, isTrashed, date range) and
ordering.
- Around line 1-15: The conditions array is currently untyped (const conditions
= []) causing an implicit any[]; import the SQL type from '@pagespace/db' and
declare the array as const conditions: SQL[] = [] so TypeScript knows the
element type. Update the import list to include SQL (alongside db,
calendarEvents, etc.) and replace the untyped declaration (conditions) with the
explicitly typed one to satisfy no-any rules.
- Around line 264-336: The event creation should deduplicate attendeeIds and
perform all attendee inserts atomically to avoid unique constraint failures and
partial state: before inserting into eventAttendees, create a deduped array
(remove duplicates and the creating userId) from data.attendeeIds, then run the
creator insert and the other attendee inserts inside a single transaction (use
the existing db.transaction / db.transaction(async (tx) => ...) or the project's
transactional API) using tx.insert on eventAttendees and tx.insert on
calendarEvents (or insert calendarEvents inside the same transaction), commit
before fetching completeEvent (db.query.calendarEvents.findFirst) and before
calling broadcastCalendarEvent so that broadcasting only happens after a
successful transaction; ensure you still set the creator as organizer
(isOrganizer: true) and add other attendees with status 'PENDING'.

In `@apps/web/src/components/calendar/EventModal.tsx`:
- Around line 161-166: The code mutates React state by calling
startDate.setHours(...) and endDate.setHours(...); instead, clone the Date
objects before modifying them so state isn't mutated. Update the startAt/endAt
logic to create new Date instances from startDate and endDate (e.g., new
Date(startDate) / new Date(endDate)) and call setHours on those clones when
allDay is true, or otherwise continue using buildDateTime(startDate, startTime)
and buildDateTime(endDate, endTime); ensure references in the startAt and endAt
expressions (allDay, startDate, endDate, startTime, endTime, buildDateTime) are
used without mutating the original Date objects.

In `@packages/db/src/schema/calendar.ts`:
- Around line 1-5: The new tables calendarEvents and eventAttendees are defined
in the schema (symbols: calendarEvents, eventAttendees in
packages/db/src/schema/calendar.ts) but no SQL migrations were generated; run
the migration generator (pnpm db:generate) from the repo root so Drizzle outputs
the corresponding migration SQL files into packages/db/drizzle/, verify the
generated migration includes the table definitions and any indexes/enums used,
and add/commit those migration files to the PR so the DB schema and migrations
remain in sync.
🧹 Nitpick comments (8)
apps/web/src/components/calendar/useCalendarData.ts (2)

17-23: Make driveId required when context is "drive".
This prevents accidental drive events being created with a null driveId and improves call-site safety.

♻️ Suggested typing upgrade
-interface UseCalendarDataOptions {
-  context: 'user' | 'drive';
-  driveId?: string;
-  currentDate: Date;
-  includePersonal?: boolean;
-  includeTasks?: boolean;
-}
+type UseCalendarDataOptions =
+  | {
+      context: 'user';
+      currentDate: Date;
+      includePersonal?: boolean;
+      includeTasks?: boolean;
+      driveId?: never;
+    }
+  | {
+      context: 'drive';
+      driveId: string;
+      currentDate: Date;
+      includePersonal?: boolean;
+      includeTasks?: boolean;
+    };

47-110: Align isPaused with the standard editing-protection pattern.
Using isEditingActive() keeps pause behavior consistent and avoids stale values if the store implementation changes.
As per coding guidelines, for SWR data fetching with editing protection, use isPaused: () => hasLoadedRef.current && isEditingActive() to allow initial fetch and only pause after, with revalidateOnFocus: false.

♻️ Suggested adjustment
-  const isAnyActive = useEditingStore((state) => state.isAnyActive());
+  const isEditingActive = useEditingStore((state) => state.isEditingActive);
@@
-    isPaused: () => hasLoadedRef.current && isAnyActive,
+    isPaused: () => hasLoadedRef.current && isEditingActive(),
@@
-      isPaused: () => hasLoadedRef.current && isAnyActive,
+      isPaused: () => hasLoadedRef.current && isEditingActive(),
apps/web/src/components/calendar/MonthView.tsx (1)

201-227: TaskPill completion indicator inconsistent with AgendaView.

The TaskPill always displays regardless of completion status, while AgendaView's TaskCard shows for completed tasks (line 271 in AgendaView.tsx). Consider aligning the visual indicators for consistency across views.

♻️ Proposed fix for consistency
   return (
     <button
       className={cn(
         'w-full text-left px-1.5 py-0.5 rounded text-xs truncate border-l-2',
         TASK_OVERLAY_STYLE.bg,
         TASK_OVERLAY_STYLE.border,
         TASK_OVERLAY_STYLE.opacity,
         isCompleted && 'line-through',
         'hover:opacity-100 transition-opacity'
       )}
       onClick={onClick}
     >
-      <span className="mr-1">☐</span>
+      <span className="mr-1">{isCompleted ? '✓' : '☐'}</span>
       {task.title}
     </button>
   );
apps/web/src/components/calendar/DayView.tsx (2)

185-202: CurrentTimeIndicator does not update in real-time.

The indicator calculates position using new Date() at render time but won't update as time passes. Users keeping the view open for extended periods will see a stale position.

♻️ Proposed fix using interval
+import { useState, useEffect, useMemo, useRef } from 'react';

 // Current time indicator
 function CurrentTimeIndicator() {
-  const now = new Date();
-  const minutes = now.getHours() * 60 + now.getMinutes();
+  const [now, setNow] = useState(new Date());
+
+  useEffect(() => {
+    const interval = setInterval(() => setNow(new Date()), 60000);
+    return () => clearInterval(interval);
+  }, []);
+
+  const minutes = now.getHours() * 60 + now.getMinutes();
   const top = (minutes / 60) * HOUR_HEIGHT;

298-326: TaskCard completion indicator inconsistent.

Same issue as MonthView - the checkbox always shows regardless of completion status while line-through is applied. Consider showing for completed tasks to match AgendaView.

apps/web/src/components/calendar/EventModal.tsx (1)

355-367: Color picker buttons should have explicit type="button".

Without type="button", these buttons default to type="submit" inside a form context, which could cause unintended form submission.

♻️ Proposed fix
                 <button
                   key={colorKey}
+                  type="button"
                   className={cn(
                     'w-8 h-8 rounded-full transition-all',
apps/web/src/app/api/calendar/events/[eventId]/route.ts (1)

44-76: Consider using centralized permission functions.

The coding guidelines recommend using centralized permission functions from @pagespace/lib/permissions. The local canAccessEvent and canEditEvent helpers implement custom logic that could potentially be consolidated with existing permission patterns for consistency. As per coding guidelines: "Use centralized permission functions from @pagespace/lib/permissions".

apps/web/src/lib/websocket/calendar-events.ts (1)

43-98: Verify Socket.IO alignment and avoid sequential fan‑out.
This module posts to /api/broadcast via fetch. Please confirm this is the sanctioned Socket.IO gateway; otherwise, align with the Socket.IO client from the realtime service. Also, attendee broadcasts are sequential and can block request paths for large attendee lists—consider deduping and running requests in parallel.

⚡ Optional fan‑out optimization
-    // Broadcast to individual attendee channels (for personal calendar views)
-    for (const attendeeId of payload.attendeeIds) {
-      const userRequestBody = JSON.stringify({
-        channelId: `user:${attendeeId}:calendar`,
-        event: `calendar:${payload.operation}`,
-        payload,
-      });
-
-      await fetch(`${realtimeUrl}/api/broadcast`, {
-        method: 'POST',
-        headers: createSignedBroadcastHeaders(userRequestBody),
-        body: userRequestBody,
-      });
-    }
+    const uniqueAttendeeIds = [...new Set(payload.attendeeIds)];
+
+    // Broadcast to individual attendee channels (for personal calendar views)
+    await Promise.all(
+      uniqueAttendeeIds.map(async (attendeeId) => {
+        const userRequestBody = JSON.stringify({
+          channelId: `user:${attendeeId}:calendar`,
+          event: `calendar:${payload.operation}`,
+          payload,
+        });
+
+        await fetch(`${realtimeUrl}/api/broadcast`, {
+          method: 'POST',
+          headers: createSignedBroadcastHeaders(userRequestBody),
+          body: userRequestBody,
+        });
+      })
+    );
@@
-        attendeeCount: payload.attendeeIds.length,
+        attendeeCount: uniqueAttendeeIds.length,

As per coding guidelines, Use Socket.IO for real-time collaboration features - imported from the realtime service at port 3001.

Comment on lines +44 to +69
const _userId = auth.userId;

try {
// Verify event exists and user has access
const event = await db.query.calendarEvents.findFirst({
where: and(
eq(calendarEvents.id, eventId),
eq(calendarEvents.isTrashed, false)
),
});

if (!event) {
return NextResponse.json({ error: 'Event not found' }, { status: 404 });
}

// Get attendees with user info
const attendees = await db.query.eventAttendees.findMany({
where: eq(eventAttendees.eventId, eventId),
with: {
user: {
columns: { id: true, name: true, email: true, image: true },
},
},
});

return NextResponse.json({ attendees });
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

GET handler missing access control check.

The handler fetches _userId (line 44) but doesn't verify if the authenticated user has permission to view this event's attendees. For events with PRIVATE or ATTENDEES_ONLY visibility, unauthorized users could enumerate attendee lists.

🛡️ Proposed fix to add access verification
-  const _userId = auth.userId;
+  const userId = auth.userId;

   try {
     // Verify event exists and user has access
     const event = await db.query.calendarEvents.findFirst({
       where: and(
         eq(calendarEvents.id, eventId),
         eq(calendarEvents.isTrashed, false)
       ),
     });

     if (!event) {
       return NextResponse.json({ error: 'Event not found' }, { status: 404 });
     }

+    // Verify user has access to this event
+    const isCreator = event.createdById === userId;
+    const isAttendee = await db.query.eventAttendees.findFirst({
+      where: and(
+        eq(eventAttendees.eventId, eventId),
+        eq(eventAttendees.userId, userId)
+      ),
+    });
+
+    if (!isCreator && !isAttendee) {
+      return NextResponse.json({ error: 'Access denied' }, { status: 403 });
+    }
🤖 Prompt for AI Agents
In `@apps/web/src/app/api/calendar/events/`[eventId]/attendees/route.ts around
lines 44 - 69, The GET handler currently reads auth.userId into _userId but
never enforces access controls; after fetching the event (calendarEvents) check
its visibility field and enforce that if event.visibility === 'PRIVATE' or
'ATTENDEES_ONLY' the requester is either the event.creatorId or is listed as an
attendee: query eventAttendees.findFirst({ where: and(eq(eventAttendees.eventId,
eventId), eq(eventAttendees.userId, _userId)) }) and if neither condition holds
return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); only then
proceed to fetch and return the attendees.

Comment on lines +145 to +154
// Add new attendees
await db.insert(eventAttendees).values(
newUserIds.map(attendeeId => ({
eventId,
userId: attendeeId,
status: 'PENDING' as const,
isOrganizer: false,
isOptional,
}))
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n apps/web/src/app/api/calendar/events/\[eventId\]/attendees/route.ts

Repository: 2witstudios/PageSpace

Length of output: 12551


🏁 Script executed:

find . -type f -name "*.ts" -path "*db*" | grep -E "(schema|migration)" | head -20

Repository: 2witstudios/PageSpace

Length of output: 832


🏁 Script executed:

cat packages/db/src/schema/calendar.ts

Repository: 2witstudios/PageSpace

Length of output: 6417


🏁 Script executed:

rg -A 5 "references.*users\.id" packages/db/src/schema/ | head -40

Repository: 2witstudios/PageSpace

Length of output: 2750


Add application-level validation for user IDs before inserting attendees.

While the database foreign key constraint on eventAttendees.userId prevents orphaned records, the POST handler should validate that the provided user IDs exist before attempting insertion. Currently, invalid user IDs will cause a database constraint error caught by the generic error handler, returning a 500 response with "Failed to add event attendees"—leaving the client without clarity on which users don't exist. Validate user existence before the insert at line 145 to provide clearer error feedback.

🤖 Prompt for AI Agents
In `@apps/web/src/app/api/calendar/events/`[eventId]/attendees/route.ts around
lines 145 - 154, Before calling db.insert on eventAttendees with newUserIds,
validate that each userId exists in the users table: query the users table
(e.g., select id from users where id in newUserIds) to get existing IDs, compute
missingIds = newUserIds - existingIds, and if missingIds is non-empty return a
400 response listing the invalid user IDs; only call
db.insert(eventAttendees).values(...) with the verified existing IDs (or abort
with the clear error) so the handler avoids a DB foreign-key error and provides
a precise client-facing message.

Comment on lines +1 to +15
import { NextResponse } from 'next/server';
import { z } from 'zod';
import {
db,
calendarEvents,
eventAttendees,
eq,
and,
or,
gte,
lte,
inArray,
isNull,
desc,
} from '@pagespace/db';
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

wc -l apps/web/src/app/api/calendar/events/route.ts

Repository: 2witstudios/PageSpace

Length of output: 114


🏁 Script executed:

head -20 apps/web/src/app/api/calendar/events/route.ts

Repository: 2witstudios/PageSpace

Length of output: 540


🏁 Script executed:

sed -n '140,180p' apps/web/src/app/api/calendar/events/route.ts

Repository: 2witstudios/PageSpace

Length of output: 1216


🏁 Script executed:

sed -n '175,210p' apps/web/src/app/api/calendar/events/route.ts

Repository: 2witstudios/PageSpace

Length of output: 986


🏁 Script executed:

rg "or\(" apps/web/src/app/api/calendar/events/route.ts -A 2 -B 2

Repository: 2witstudios/PageSpace

Length of output: 1098


🏁 Script executed:

rg "import.*SQL" apps/web/src --type ts --type tsx

Repository: 2witstudios/PageSpace

Length of output: 92


🏁 Script executed:

rg "SQL\[" apps/web/src --type ts --type tsx -B 2 -A 2

Repository: 2witstudios/PageSpace

Length of output: 92


🏁 Script executed:

rg "conditions.*:" apps/web/src/app/api --type ts -B 2 -A 2

Repository: 2witstudios/PageSpace

Length of output: 6235


🏁 Script executed:

sed -n '1,50p' apps/web/src/app/api/search/route.ts

Repository: 2witstudios/PageSpace

Length of output: 1659


🏁 Script executed:

rg "const conditions" apps/web/src/app/api --type ts -B 2 -A 8

Repository: 2witstudios/PageSpace

Length of output: 8667


🏁 Script executed:

grep -n "SQL" apps/web/src/app/api/calendar/events/route.ts

Repository: 2witstudios/PageSpace

Length of output: 47


🏁 Script executed:

head -50 apps/web/src/app/api/admin/audit-logs/route.ts

Repository: 2witstudios/PageSpace

Length of output: 1395


🏁 Script executed:

grep -n "export.*SQL" packages/db/src/index.ts

Repository: 2witstudios/PageSpace

Length of output: 143


🏁 Script executed:

cat packages/db/src/index.ts | head -100

Repository: 2witstudios/PageSpace

Length of output: 2373


🏁 Script executed:

sed -n '1,20p' apps/web/src/app/api/calendar/events/route.ts | head -20

Repository: 2witstudios/PageSpace

Length of output: 540


Add explicit SQL[] type to conditions array to avoid implicit any[].
const conditions = []; infers any[] under typical TypeScript settings, which violates the no-any rule. Import SQL from @pagespace/db and add explicit typing.

🛠️ Suggested type fix
 import {
   db,
   calendarEvents,
   eventAttendees,
   eq,
   and,
   or,
   gte,
   lte,
   inArray,
   isNull,
   desc,
+  type SQL,
 } from '@pagespace/db';

...

-    const conditions = [];
+    const conditions: SQL[] = [];
🤖 Prompt for AI Agents
In `@apps/web/src/app/api/calendar/events/route.ts` around lines 1 - 15, The
conditions array is currently untyped (const conditions = []) causing an
implicit any[]; import the SQL type from '@pagespace/db' and declare the array
as const conditions: SQL[] = [] so TypeScript knows the element type. Update the
import list to include SQL (alongside db, calendarEvents, etc.) and replace the
untyped declaration (conditions) with the explicitly typed one to satisfy no-any
rules.

Comment on lines +92 to +135
if (params.context === 'drive') {
if (!params.driveId) {
return NextResponse.json(
{ error: 'driveId is required for drive context' },
{ status: 400 }
);
}

const canView = await isUserDriveMember(userId, params.driveId);
if (!canView) {
return NextResponse.json(
{ error: 'Unauthorized - you do not have access to this drive' },
{ status: 403 }
);
}

// Fetch drive events within date range
const events = await db.query.calendarEvents.findMany({
where: and(
eq(calendarEvents.driveId, params.driveId),
eq(calendarEvents.isTrashed, false),
lte(calendarEvents.startAt, params.endDate),
gte(calendarEvents.endAt, params.startDate)
),
with: {
createdBy: {
columns: { id: true, name: true, image: true },
},
attendees: {
with: {
user: {
columns: { id: true, name: true, image: true },
},
},
},
page: {
columns: { id: true, title: true, type: true },
},
},
orderBy: [desc(calendarEvents.startAt)],
});

return NextResponse.json({ events });
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Enforce visibility rules in drive-context listings.
Drive-context queries currently return every event in the drive, which exposes ATTENDEES_ONLY/PRIVATE events to non-attendees. Filter to DRIVE visibility unless the requester is an attendee/organizer (or creator), similar to the user-context logic.

🔒 Suggested filtering approach
+      const attendeeEvents = await db
+        .select({ eventId: eventAttendees.eventId })
+        .from(eventAttendees)
+        .where(eq(eventAttendees.userId, userId));
+      const attendeeEventIds = attendeeEvents.map(e => e.eventId);
+      const visibilityFilter =
+        attendeeEventIds.length > 0
+          ? or(
+              eq(calendarEvents.visibility, 'DRIVE'),
+              inArray(calendarEvents.id, attendeeEventIds)
+            )
+          : eq(calendarEvents.visibility, 'DRIVE');
+
       const events = await db.query.calendarEvents.findMany({
         where: and(
           eq(calendarEvents.driveId, params.driveId),
           eq(calendarEvents.isTrashed, false),
+          visibilityFilter,
           lte(calendarEvents.startAt, params.endDate),
           gte(calendarEvents.endAt, params.startDate)
         ),
🤖 Prompt for AI Agents
In `@apps/web/src/app/api/calendar/events/route.ts` around lines 92 - 135, The
drive-context branch (when params.context === 'drive') returns all drive events
and thus exposes ATTENDEES_ONLY/PRIVATE events; update the
db.query.calendarEvents.findMany where clause to enforce visibility: only
include events with calendarEvents.visibility === 'DRIVE' unless the requester
is the event creator (createdBy.id === userId) or is listed as an
attendee/organizer. Implement this by adding an OR group to the existing
and(...) predicate that permits events where visibility is DRIVE OR
(createdBy.id == userId) OR (exists an attendee record for userId with role
organizer/attendee), using the calendarEvents.attendees relation (or an
exists/subquery) to test membership; keep the with: attendees/createdBy
projections and preserve other filters (driveId, isTrashed, date range) and
ordering.

Comment on lines +264 to +336
// Create the event
const [event] = await db
.insert(calendarEvents)
.values({
driveId: data.driveId ?? null,
createdById: userId,
pageId: data.pageId ?? null,
title: data.title,
description: data.description ?? null,
location: data.location ?? null,
startAt: data.startAt,
endAt: data.endAt,
allDay: data.allDay,
timezone: data.timezone,
recurrenceRule: data.recurrenceRule ?? null,
visibility: data.visibility,
color: data.color,
updatedAt: new Date(),
})
.returning();

// Add creator as organizer attendee
await db.insert(eventAttendees).values({
eventId: event.id,
userId: userId,
status: 'ACCEPTED',
isOrganizer: true,
respondedAt: new Date(),
});

// Add other attendees if provided
if (data.attendeeIds && data.attendeeIds.length > 0) {
const otherAttendees = data.attendeeIds.filter(id => id !== userId);
if (otherAttendees.length > 0) {
await db.insert(eventAttendees).values(
otherAttendees.map(attendeeId => ({
eventId: event.id,
userId: attendeeId,
status: 'PENDING' as const,
isOrganizer: false,
}))
);
}
}

// Fetch the complete event with relations
const completeEvent = await db.query.calendarEvents.findFirst({
where: eq(calendarEvents.id, event.id),
with: {
createdBy: {
columns: { id: true, name: true, image: true },
},
attendees: {
with: {
user: {
columns: { id: true, name: true, image: true },
},
},
},
page: {
columns: { id: true, title: true, type: true },
},
},
});

// Broadcast event creation
await broadcastCalendarEvent({
eventId: event.id,
driveId: data.driveId ?? null,
operation: 'created',
userId,
attendeeIds: [userId, ...(data.attendeeIds ?? [])],
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find database schema files
fd -e sql -e ts | grep -E "(schema|drizzle|event)" | head -20

Repository: 2witstudios/PageSpace

Length of output: 1000


🏁 Script executed:

# Search for eventAttendees table definition
rg "eventAttendees" --type ts --type tsx -A 5 -B 2 | head -50

Repository: 2witstudios/PageSpace

Length of output: 92


🏁 Script executed:

# Look for unique constraints on eventAttendees
rg "unique|constraint" --type ts --type tsx | grep -i event

Repository: 2witstudios/PageSpace

Length of output: 92


🏁 Script executed:

# Check how transactions are used in the codebase
rg "db\.transaction|transaction\(" --type ts --type tsx | head -20

Repository: 2witstudios/PageSpace

Length of output: 92


🏁 Script executed:

# Find schema definition files in packages
fd -type f -name "*.ts" | grep -E "schema|db" | grep packages/db | head -20

Repository: 2witstudios/PageSpace

Length of output: 235


🏁 Script executed:

# Look for eventAttendees definition
rg "eventAttendees" -A 10 | head -100

Repository: 2witstudios/PageSpace

Length of output: 7190


🏁 Script executed:

# Check the schema in packages/db
ls -la packages/db/

Repository: 2witstudios/PageSpace

Length of output: 670


🏁 Script executed:

# Look for table definitions
fd -type f -name "*.ts" packages/db/ | xargs grep -l "eventAttendees\|createTable" | head -10

Repository: 2witstudios/PageSpace

Length of output: 235


🏁 Script executed:

cat -n packages/db/src/schema/calendar.ts | grep -A 30 "export const eventAttendees = pgTable"

Repository: 2witstudios/PageSpace

Length of output: 1631


🏁 Script executed:

# Check for unique constraints in calendar schema
grep -n "unique\|constraint" packages/db/src/schema/calendar.ts

Repository: 2witstudios/PageSpace

Length of output: 265


🏁 Script executed:

# Check how transactions are used in the route handler file
grep -n "transaction\|db\." apps/web/src/app/api/calendar/events/route.ts | head -40

Repository: 2witstudios/PageSpace

Length of output: 367


🏁 Script executed:

# Get more context around the code - see full POST handler
sed -n '264,340p' apps/web/src/app/api/calendar/events/route.ts

Repository: 2witstudios/PageSpace

Length of output: 2307


🏁 Script executed:

# Check if there are imports for transaction
grep -n "transaction\|import" apps/web/src/app/api/calendar/events/route.ts | head -20

Repository: 2witstudios/PageSpace

Length of output: 429


🏁 Script executed:

# Look for transaction usage patterns in the codebase
rg "\.transaction\(" apps/web/src --A 5 | head -50

Repository: 2witstudios/PageSpace

Length of output: 470


🏁 Script executed:

# Check the database export to see what APIs are available
grep -n "export\|transaction" packages/db/src/index.ts | head -30

Repository: 2witstudios/PageSpace

Length of output: 924


🏁 Script executed:

# Check if db.transaction is available and how it's used
grep -rn "\.transaction" packages/db/src --include="*.ts"

Repository: 2witstudios/PageSpace

Length of output: 251


🏁 Script executed:

# Check the drizzle config and db setup
cat packages/db/src/index.ts

Repository: 2witstudios/PageSpace

Length of output: 2373


🏁 Script executed:

# Look for any transaction functions exported
fd transaction packages/db

Repository: 2witstudios/PageSpace

Length of output: 209


🏁 Script executed:

# Let me verify how the duplicate scenario would fail by checking the broadcast call
sed -n '330,340p' apps/web/src/app/api/calendar/events/route.ts

Repository: 2witstudios/PageSpace

Length of output: 425


🏁 Script executed:

# Check if there are any other similar patterns in the codebase that handle this
rg "attendeeIds" apps/web/src/app/api/calendar/events/route.ts

Repository: 2witstudios/PageSpace

Length of output: 303


🏁 Script executed:

# Verify the complete POST handler to see error handling
sed -n '240,350p' apps/web/src/app/api/calendar/events/route.ts

Repository: 2witstudios/PageSpace

Length of output: 3020


🏁 Script executed:

# Check if there are similar multi-insert patterns elsewhere to see if they handle deduplication
rg "\.insert.*\.values\(" apps/web/src/app/api/calendar -A 3 | head -50

Repository: 2witstudios/PageSpace

Length of output: 1104


Deduplicate attendee IDs and wrap attendee inserts in a transaction to ensure atomicity.

The eventAttendees table enforces a unique constraint on (eventId, userId). If data.attendeeIds contains duplicates, the code filters out userId but doesn't deduplicate the array itself. The second insert would then fail with a constraint violation after the event has already been created, leaving the database in an inconsistent state and returning a 500 error with partial data.

Wrapping the inserts in a transaction with deduplication prevents both the constraint violation and partial data insertion.

🧱 Suggested transaction + dedupe
-    // Create the event
-    const [event] = await db
-      .insert(calendarEvents)
-      .values({
-        driveId: data.driveId ?? null,
-        createdById: userId,
-        pageId: data.pageId ?? null,
-        title: data.title,
-        description: data.description ?? null,
-        location: data.location ?? null,
-        startAt: data.startAt,
-        endAt: data.endAt,
-        allDay: data.allDay,
-        timezone: data.timezone,
-        recurrenceRule: data.recurrenceRule ?? null,
-        visibility: data.visibility,
-        color: data.color,
-        updatedAt: new Date(),
-      })
-      .returning();
-
-    // Add creator as organizer attendee
-    await db.insert(eventAttendees).values({
-      eventId: event.id,
-      userId: userId,
-      status: 'ACCEPTED',
-      isOrganizer: true,
-      respondedAt: new Date(),
-    });
-
-    // Add other attendees if provided
-    if (data.attendeeIds && data.attendeeIds.length > 0) {
-      const otherAttendees = data.attendeeIds.filter(id => id !== userId);
-      if (otherAttendees.length > 0) {
-        await db.insert(eventAttendees).values(
-          otherAttendees.map(attendeeId => ({
-            eventId: event.id,
-            userId: attendeeId,
-            status: 'PENDING' as const,
-            isOrganizer: false,
-          }))
-        );
-      }
-    }
+    const uniqueAttendeeIds = [...new Set(data.attendeeIds ?? [])].filter(
+      attendeeId => attendeeId !== userId
+    );
+
+    const event = await db.transaction(async (tx) => {
+      const [newEvent] = await tx
+        .insert(calendarEvents)
+        .values({
+          driveId: data.driveId ?? null,
+          createdById: userId,
+          pageId: data.pageId ?? null,
+          title: data.title,
+          description: data.description ?? null,
+          location: data.location ?? null,
+          startAt: data.startAt,
+          endAt: data.endAt,
+          allDay: data.allDay,
+          timezone: data.timezone,
+          recurrenceRule: data.recurrenceRule ?? null,
+          visibility: data.visibility,
+          color: data.color,
+          updatedAt: new Date(),
+        })
+        .returning();
+
+      await tx.insert(eventAttendees).values({
+        eventId: newEvent.id,
+        userId,
+        status: 'ACCEPTED',
+        isOrganizer: true,
+        respondedAt: new Date(),
+      });
+
+      if (uniqueAttendeeIds.length > 0) {
+        await tx.insert(eventAttendees).values(
+          uniqueAttendeeIds.map(attendeeId => ({
+            eventId: newEvent.id,
+            userId: attendeeId,
+            status: 'PENDING' as const,
+            isOrganizer: false,
+          }))
+        );
+      }
+
+      return newEvent;
+    });
@@
-      attendeeIds: [userId, ...(data.attendeeIds ?? [])],
+      attendeeIds: [userId, ...uniqueAttendeeIds],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Create the event
const [event] = await db
.insert(calendarEvents)
.values({
driveId: data.driveId ?? null,
createdById: userId,
pageId: data.pageId ?? null,
title: data.title,
description: data.description ?? null,
location: data.location ?? null,
startAt: data.startAt,
endAt: data.endAt,
allDay: data.allDay,
timezone: data.timezone,
recurrenceRule: data.recurrenceRule ?? null,
visibility: data.visibility,
color: data.color,
updatedAt: new Date(),
})
.returning();
// Add creator as organizer attendee
await db.insert(eventAttendees).values({
eventId: event.id,
userId: userId,
status: 'ACCEPTED',
isOrganizer: true,
respondedAt: new Date(),
});
// Add other attendees if provided
if (data.attendeeIds && data.attendeeIds.length > 0) {
const otherAttendees = data.attendeeIds.filter(id => id !== userId);
if (otherAttendees.length > 0) {
await db.insert(eventAttendees).values(
otherAttendees.map(attendeeId => ({
eventId: event.id,
userId: attendeeId,
status: 'PENDING' as const,
isOrganizer: false,
}))
);
}
}
// Fetch the complete event with relations
const completeEvent = await db.query.calendarEvents.findFirst({
where: eq(calendarEvents.id, event.id),
with: {
createdBy: {
columns: { id: true, name: true, image: true },
},
attendees: {
with: {
user: {
columns: { id: true, name: true, image: true },
},
},
},
page: {
columns: { id: true, title: true, type: true },
},
},
});
// Broadcast event creation
await broadcastCalendarEvent({
eventId: event.id,
driveId: data.driveId ?? null,
operation: 'created',
userId,
attendeeIds: [userId, ...(data.attendeeIds ?? [])],
});
const uniqueAttendeeIds = [...new Set(data.attendeeIds ?? [])].filter(
attendeeId => attendeeId !== userId
);
const event = await db.transaction(async (tx) => {
const [newEvent] = await tx
.insert(calendarEvents)
.values({
driveId: data.driveId ?? null,
createdById: userId,
pageId: data.pageId ?? null,
title: data.title,
description: data.description ?? null,
location: data.location ?? null,
startAt: data.startAt,
endAt: data.endAt,
allDay: data.allDay,
timezone: data.timezone,
recurrenceRule: data.recurrenceRule ?? null,
visibility: data.visibility,
color: data.color,
updatedAt: new Date(),
})
.returning();
await tx.insert(eventAttendees).values({
eventId: newEvent.id,
userId,
status: 'ACCEPTED',
isOrganizer: true,
respondedAt: new Date(),
});
if (uniqueAttendeeIds.length > 0) {
await tx.insert(eventAttendees).values(
uniqueAttendeeIds.map(attendeeId => ({
eventId: newEvent.id,
userId: attendeeId,
status: 'PENDING' as const,
isOrganizer: false,
}))
);
}
return newEvent;
});
// Fetch the complete event with relations
const completeEvent = await db.query.calendarEvents.findFirst({
where: eq(calendarEvents.id, event.id),
with: {
createdBy: {
columns: { id: true, name: true, image: true },
},
attendees: {
with: {
user: {
columns: { id: true, name: true, image: true },
},
},
},
page: {
columns: { id: true, title: true, type: true },
},
},
});
// Broadcast event creation
await broadcastCalendarEvent({
eventId: event.id,
driveId: data.driveId ?? null,
operation: 'created',
userId,
attendeeIds: [userId, ...uniqueAttendeeIds],
});
🤖 Prompt for AI Agents
In `@apps/web/src/app/api/calendar/events/route.ts` around lines 264 - 336, The
event creation should deduplicate attendeeIds and perform all attendee inserts
atomically to avoid unique constraint failures and partial state: before
inserting into eventAttendees, create a deduped array (remove duplicates and the
creating userId) from data.attendeeIds, then run the creator insert and the
other attendee inserts inside a single transaction (use the existing
db.transaction / db.transaction(async (tx) => ...) or the project's
transactional API) using tx.insert on eventAttendees and tx.insert on
calendarEvents (or insert calendarEvents inside the same transaction), commit
before fetching completeEvent (db.query.calendarEvents.findFirst) and before
calling broadcastCalendarEvent so that broadcasting only happens after a
successful transaction; ensure you still set the creator as organizer
(isOrganizer: true) and add other attendees with status 'PENDING'.

Comment on lines +161 to +166
const startAt = allDay
? new Date(startDate.setHours(0, 0, 0, 0))
: buildDateTime(startDate, startTime);
const endAt = allDay
? new Date(endDate.setHours(23, 59, 59, 999))
: buildDateTime(endDate, endTime);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

State mutation bug in all-day date handling.

startDate.setHours() and endDate.setHours() mutate the Date objects stored in React state directly instead of creating new instances. This violates React's immutability principle and can cause stale closure bugs or missed re-renders.

🐛 Proposed fix to avoid state mutation
     const startAt = allDay
-      ? new Date(startDate.setHours(0, 0, 0, 0))
+      ? new Date(new Date(startDate).setHours(0, 0, 0, 0))
       : buildDateTime(startDate, startTime);
     const endAt = allDay
-      ? new Date(endDate.setHours(23, 59, 59, 999))
+      ? new Date(new Date(endDate).setHours(23, 59, 59, 999))
       : buildDateTime(endDate, endTime);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const startAt = allDay
? new Date(startDate.setHours(0, 0, 0, 0))
: buildDateTime(startDate, startTime);
const endAt = allDay
? new Date(endDate.setHours(23, 59, 59, 999))
: buildDateTime(endDate, endTime);
const startAt = allDay
? new Date(new Date(startDate).setHours(0, 0, 0, 0))
: buildDateTime(startDate, startTime);
const endAt = allDay
? new Date(new Date(endDate).setHours(23, 59, 59, 999))
: buildDateTime(endDate, endTime);
🤖 Prompt for AI Agents
In `@apps/web/src/components/calendar/EventModal.tsx` around lines 161 - 166, The
code mutates React state by calling startDate.setHours(...) and
endDate.setHours(...); instead, clone the Date objects before modifying them so
state isn't mutated. Update the startAt/endAt logic to create new Date instances
from startDate and endDate (e.g., new Date(startDate) / new Date(endDate)) and
call setHours on those clones when allDay is true, or otherwise continue using
buildDateTime(startDate, startTime) and buildDateTime(endDate, endTime); ensure
references in the startAt and endAt expressions (allDay, startDate, endDate,
startTime, endTime, buildDateTime) are used without mutating the original Date
objects.

Comment on lines +1 to +5
import { pgTable, text, timestamp, boolean, pgEnum, index, unique, jsonb } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
import { users } from './auth';
import { drives, pages } from './core';
import { createId } from '@paralleldrive/cuid2';
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if migration files exist and reference the new calendar tables
echo "=== Listing migration files in packages/db/drizzle/ ==="
ls -la packages/db/drizzle/ 2>/dev/null | head -20

echo -e "\n=== Searching for calendar_events and event_attendees references ==="
rg -l "calendar_events|event_attendees" packages/db/drizzle/ 2>/dev/null || echo "No matches found"

echo -e "\n=== Show content of any recent migration files mentioning calendar ==="
rg -A 5 "calendar_events|event_attendees" packages/db/drizzle/ 2>/dev/null || echo "Pattern not found in migrations"

Repository: 2witstudios/PageSpace

Length of output: 1718


🏁 Script executed:

#!/bin/bash
# Check the content of calendar.ts to confirm table definitions
echo "=== Checking calendar.ts for table definitions ==="
rg -n "calendar_events|event_attendees|pgTable" packages/db/src/schema/calendar.ts | head -20

Repository: 2witstudios/PageSpace

Length of output: 1424


Generate migrations for the new calendar tables using pnpm db:generate.
The schema defines calendarEvents and eventAttendees tables, but corresponding migrations are missing from packages/db/drizzle/. Run the migration generation command to create the SQL migrations for these tables.

🤖 Prompt for AI Agents
In `@packages/db/src/schema/calendar.ts` around lines 1 - 5, The new tables
calendarEvents and eventAttendees are defined in the schema (symbols:
calendarEvents, eventAttendees in packages/db/src/schema/calendar.ts) but no SQL
migrations were generated; run the migration generator (pnpm db:generate) from
the repo root so Drizzle outputs the corresponding migration SQL files into
packages/db/drizzle/, verify the generated migration includes the table
definitions and any indexes/enums used, and add/commit those migration files to
the PR so the DB schema and migrations remain in sync.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants