Skip to content

feat(graphql): implement cursor-based pagination for events query (#1354) - #1594

Merged
krushit1307 merged 12 commits into
krushit1307:mainfrom
nayanraj864-cmyk:feat/graphql-cursor-pagination-1354
Jul 28, 2026
Merged

feat(graphql): implement cursor-based pagination for events query (#1354)#1594
krushit1307 merged 12 commits into
krushit1307:mainfrom
nayanraj864-cmyk:feat/graphql-cursor-pagination-1354

Conversation

@nayanraj864-cmyk

@nayanraj864-cmyk nayanraj864-cmyk commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Pull Request

Description

This pull request implements Relay-style cursor-based pagination for the GraphQL events query (events(first: $first, after: $after)):

  • GraphQL Schema Updates (graphql/resolvers/index.ts): Updated typeDefs schema with Relay connection types (Event, PageInfo, EventEdge, EventConnection). Added events(first, after) query definition.
  • Cursor Encoding & Keyset Pagination (graphql/resolvers/index.ts): Created encodeCursor and decodeCursor helpers using base64 timestamps and record IDs (created_at::id). Implemented keyset database queries (created_at.lt.X,and(...)) to prevent record skipping or duplication during concurrent inserts/deletes. Added Event.club and Event.organizer field resolvers with batch DataLoaders to prevent N+1 queries.
  • Frontend React Query Hook (useCursorEventsQuery.ts): Created useCursorEventsQuery hook leveraging React Query's useInfiniteQuery to consume the new cursor-paginated API.

Type of Change

  • New Feature
  • Bug Fix
  • Documentation Update
  • Refactor
  • Performance Improvement
  • Security Improvement
  • Other

Related Issue

Closes #1354

Testing

Describe the testing performed.

  • Tested locally

  • Existing functionality verified

  • No new warnings or errors

  • Added GraphQL Yoga unit test suite (src/graphql/server.test.ts) testing Relay connection structure, edges, nodes, pageInfo, and DataLoaders.

  • Added React Query hook unit test suite (src/hooks/useCursorEventsQuery.test.ts) testing fetcher request format, parameters, and error handling.

Screenshots

N/A (Backend GraphQL Relay Connection API & React Query Hooks)

Checklist

  • Code follows project conventions
  • Documentation updated where required
  • No unnecessary files included
  • Changes have been tested
  • Ready for review

Summary by CodeRabbit

  • New Features

    • Added cursor-based event browsing with event details, club, and organizer information.
    • Added rich-text editing with formatting, club mentions, and embedded event cards.
    • Added read-only rich-text content display.
    • Added realtime comment updates for expanded posts.
    • Added private group data access controls.
    • Added attendance-rate reporting and automated storage cleanup.
  • Bug Fixes

    • Improved comment loading and duplicate handling during realtime updates.
  • Tests

    • Added coverage for event pagination, editor features, realtime comments, security controls, and backend utilities.

@github-actions github-actions Bot added backend ECSoC26 Elite Coders Summer Of Code'26 - Open Source Program optimization labels Jul 27, 2026
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@nayanraj864-cmyk, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c664f24b-c79d-40ab-b14f-40de67c56737

📥 Commits

Reviewing files that changed from the base of the PR and between a8f248c and 5c58387.

📒 Files selected for processing (1)
  • src/components/Feed/CommentSection.tsx
📝 Walkthrough

Walkthrough

This PR adds cursor-paginated GraphQL events, TipTap club and event embeds, realtime comment subscriptions, storage cleanup, attendance reporting, immutable audit logs, private-group RLS, and related tests and migrations.

Changes

GraphQL events and pagination

Layer / File(s) Summary
Loader and cursor contracts
graphql/resolvers/index.ts
Replaces external DataLoader usage with local single-key caching and adds cursor encoding and decoding helpers.
Events schema and resolvers
graphql/resolvers/index.ts
Adds Relay-style event connections, keyset pagination, single-event lookup, and club/organizer relations.
GraphQL pagination validation
src/graphql/server.test.ts
Tests cursor handling, connection fields, pagination metadata, and nested event data.

TipTap editor and embedded content

Layer / File(s) Summary
Embedded content extensions
src/components/Editor/extensions/*
Adds clubMention and eventCard nodes with HTML parsing, serialization, and React views.
Rich text editor flow
src/components/Editor/TiptapRichTextEditor.tsx
Adds formatting controls, Supabase search, custom-node insertion, event URL pasting, and modal workflows.
Viewer and editor validation
src/components/Editor/TiptapReadOnlyViewer.tsx, src/components/Editor/TiptapExtensions.test.tsx
Adds a synchronized read-only viewer and tests for extensions and toolbar rendering.
Event card download formatting
src/components/EventCard.tsx, src/components/ui/EventCard/EventCardActions.tsx
Reformats existing imports and ICS filename sanitization expressions without behavior changes.

Realtime comment delivery

Layer / File(s) Summary
Realtime comment hook
src/hooks/useRealtimeComments.ts, src/hooks/useRealtimeComments.test.ts
Subscribes to post-filtered inserts, enriches authors, invokes callbacks, and cleans up channels.
Comment section integration
src/components/Feed/CommentSection.tsx
Adds an optional callback for newly received comments.
Expanded feed comment subscriptions
src/routes/feed.tsx, supabase/migrations/20260726040000_realtime_comments_broadcast.sql
Scopes subscriptions to expanded posts, deduplicates inserted comments, and publishes the comments table to realtime.

Supabase operations and policies

Layer / File(s) Summary
Storage cleanup endpoint
supabase/functions/cleanup-orphaned-storage/*
Adds authenticated orphaned-file scanning and deletion with CORS, grace-period, batching, summaries, and tests.
Scheduled storage cleanup
supabase/migrations/20260726020000_cron_cleanup_orphaned_storage.sql
Schedules weekly authenticated cleanup through pg_cron.
Attendance reporting RPC
supabase/migrations/20260726010000_get_user_attendance_rate.sql, supabase/tests/get_user_attendance_rate.test.sql
Adds and tests a past-event checked-in attendance percentage function.
Immutable audit logs
supabase/migrations/20260726030000_admin_audit_logs_immutability.sql, supabase/tests/audit_logs.test.sql
Adds audit fields, club triggers, read policy, immutability enforcement, and pgTAP coverage.
Private groups RLS
supabase/migrations/20260726050000_private_groups_rls.sql, supabase/tests/groups_rls.test.sql
Adds group tables, membership helpers, RLS policies, and authorization tests.

Link preview formatting

Layer / File(s) Summary
Link preview reformatting
supabase/functions/link-preview/index.ts
Reformats existing regex, headers, and error response declarations without changing behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant GraphQLYoga
  participant EventsResolver
  participant Supabase
  Client->>GraphQLYoga: Request events(first, after)
  GraphQLYoga->>EventsResolver: Resolve EventConnection
  EventsResolver->>Supabase: Apply cursor filter and fetch rows
  Supabase-->>EventsResolver: Ordered event records
  EventsResolver-->>GraphQLYoga: Build edges and pageInfo
  GraphQLYoga-->>Client: Return paginated events
Loading

Possibly related issues

  • None; issue #1445 concerns social feed pagination and IntersectionObserver, which are not implemented here.

Possibly related PRs

Suggested labels: enhancement, frontend, advanced, ECSoC26-L3

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes many unrelated editor, realtime comments, storage cleanup, audit-log, and groups migration changes beyond #1354. Move the editor, storage, audit-log, and groups work into separate PRs so this one only covers events cursor pagination.
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: cursor-based pagination for the GraphQL events query.
Linked Issues check ✅ Passed The PR adds Relay-style first/after pagination, cursor decoding, connection types, and a React Query hook as required by #1354.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 15

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (1)
src/routes/feed.tsx (1)

415-437: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Comment counts on collapsed posts no longer update live.

The previous blanket comments listener that called refetchPosts() on any comment change was removed, and the new targeted effect (Lines 439-511) only subscribes for postIds in expandedPostIds. A collapsed post's Comments ({postComments.length}) count (Line 1320) is now only as fresh as the last unrelated refetchPosts() — a new comment on a collapsed post won't be reflected until something else triggers a full refetch.

If live counts on collapsed posts matter, consider a lightweight listener that only increments a per-post counter (no profile fetch) instead of restoring the old blanket refetch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routes/feed.tsx` around lines 415 - 437, Update the realtime
subscriptions in the feed effect around supabase.channel("realtime_feed") to
handle comment INSERT events for collapsed posts as well as expanded posts. Add
a lightweight per-post comment-count update that increments the affected post’s
count without fetching profiles or triggering a blanket refetchPosts(); preserve
the existing expanded-post comment subscription behavior and avoid
double-counting events.
🟡 Minor comments (6)
supabase/functions/cleanup-orphaned-storage/index.ts-137-155 (1)

137-155: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not report failed deletions as deleted.

deletedFilesLog is populated before remove(). A failed batch still returns those names under deletedFiles and a top-level success: true. Append only after a successful removal and surface partial failures explicitly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/functions/cleanup-orphaned-storage/index.ts` around lines 137 - 155,
The cleanup flow currently records files in deletedFilesLog before confirming
removal, so failed batches are reported as deleted. Move the deletedFilesLog
updates from the orphan-detection loop into the successful branch after
supabase.storage.remove completes, and ensure partial batch failures are
explicitly surfaced rather than returning those files as deleted.
graphql/resolvers/index.ts-294-302 (1)

294-302: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

hasPreviousPage is true even when the cursor was invalid and silently ignored.

If after is provided but decodeCursor(after) fails (malformed/forged cursor), the code skips applying the .or() filter and effectively returns page 1 unpaginated — yet pageInfo.hasPreviousPage: !!after still reports true. Clients following this hint would believe there's a prior page when the response is actually the first page. Consider tracking whether the filter was actually applied (e.g., hasPreviousPage: !!after && !!decoded), or throwing a GraphQL error for invalid cursors instead of silently falling back to page 1.

Also applies to: 325-336

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@graphql/resolvers/index.ts` around lines 294 - 302, Update the pagination
flow around decodeCursor and pageInfo.hasPreviousPage so an invalid after cursor
does not report a previous page when its filter was skipped. Track whether
decodeCursor(after) succeeded and use that state when computing hasPreviousPage,
while preserving the existing keyset filter for valid cursors; alternatively,
reject invalid cursors with the established GraphQL error mechanism.
src/hooks/useCursorEventsQuery.ts-96-113 (1)

96-113: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

fetchGraphQL doesn't check HTTP status before parsing the response.

If the endpoint returns a non-2xx response without a GraphQL-shaped JSON body (e.g., an auth gateway error page or plain-text 500), res.json() can throw an unhelpful parse error, or json.data can be silently undefined, instead of a clear error surfaced to the caller.

🛡️ Proposed fix
   const res = await fetch("/api/graphql", {
     method: "POST",
     headers: {
       "Content-Type": "application/json",
     },
     body: JSON.stringify({ query, variables }),
   });

+  if (!res.ok) {
+    throw new Error(`GraphQL request failed with status ${res.status}`);
+  }
   const json = await res.json();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useCursorEventsQuery.ts` around lines 96 - 113, Update fetchGraphQL
to validate the fetch response’s HTTP status before parsing or returning data.
For non-2xx responses, throw a clear error that includes the response status,
and preserve the existing GraphQL error handling and successful json.data return
path for valid responses.
src/components/Editor/TiptapRichTextEditor.tsx-90-94 (1)

90-94: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Drop the id.ilike fallback.

events.id is defined as UUID, so PostgRESTBuilder.or(\id.eq.${eventId},id.ilike.${eventId}`)includes an invalid UUID ILIKE branch; valid UUID pastes should match withid.eq.${eventId}` only, while non-UUID input should be rejected rather than appended into the PostgREST filter.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Editor/TiptapRichTextEditor.tsx` around lines 90 - 94, Update
the events query in the TiptapRichTextEditor flow to use only the id.eq filter,
removing the id.ilike fallback. Validate eventId as a UUID before constructing
the PostgREST filter and reject non-UUID input instead of issuing a query with
an invalid identifier.
src/components/Editor/extensions/ClubMentionExtension.tsx-18-23 (1)

18-23: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Avoid full-page reload on club mention clicks.

The app uses React Router with /clubs/:slug routing, but this link forces a browser reload via window.location.href. Node views rendered by ReactNodeViewRenderer don’t inherit the app’s router context, so pass a navigation callback into ClubMentionView/the extension options instead of setting window.location directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Editor/extensions/ClubMentionExtension.tsx` around lines 18 -
23, Update handleClick in ClubMentionView and the ClubMentionExtension options
to accept and invoke a navigation callback for `/clubs/${clubSlug}` instead of
assigning window.location.href. Thread this callback from the React Router-aware
parent through the extension and ClubMentionView, preserving preventDefault and
the existing clubSlug guard.
src/components/Editor/extensions/EventCardExtension.tsx-108-118 (1)

108-118: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Mark EventCard attributes as internal before re-merging them.

Tiptap renders declared attributes as HTML attributes by default, so renderHTML here adds the intended data-* attrs and then HTMLAttributes also outputs raw attrs like eventid, title, bannerurl, and link. That also adds a native title tooltip for the card content. Add rendered: false to these attributes, or build the final props without merging the raw attrs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Editor/extensions/EventCardExtension.tsx` around lines 108 -
118, Update EventCardExtension’s addAttributes() so all internal event-card
attributes are marked rendered: false before renderHTML re-merges them into
data-* props. Ensure raw attributes such as eventId, title, bannerUrl, and url
are not emitted or treated as native HTML attributes, while preserving the
intended data-* output.
🧹 Nitpick comments (5)
supabase/functions/cleanup-orphaned-storage/index.test.ts (1)

5-44: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Cover the destructive cleanup path.

Add mocked-client tests for failed reference queries, exact bucket/path matching, nested/paginated listings, grace-period handling, and failed deletes. The current tests cannot prevent regressions that delete active files or misreport removals.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/functions/cleanup-orphaned-storage/index.test.ts` around lines 5 -
44, Add mocked Supabase-client tests for the destructive cleanup flow exercised
by the handler, covering failed reference queries, exact bucket/path matching,
nested and paginated storage listings, grace-period filtering, and failed delete
operations. Assert that active or recently modified files are preserved and that
removal results and errors are reported accurately.
src/graphql/server.test.ts (1)

82-154: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No test coverage for second-page (after) pagination or resolver-level invalid-cursor handling.

Given this PR's core objective is keyset pagination robustness, only the first page (first: 2, no after) is exercised end-to-end. Consider adding a case that requests a second page using the endCursor from page 1 and asserts the .or() filter narrows results correctly, plus a case passing a malformed after value to verify resolver-level behavior (see related comment on the hasPreviousPage/invalid-cursor handling in graphql/resolvers/index.ts).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/graphql/server.test.ts` around lines 82 - 154, Extend the GraphQL
pagination tests in “GraphQL Cursor-Based Events Pagination” with a second-page
request using the first response’s pageInfo.endCursor as after, asserting the
returned events reflect the narrowed keyset results. Add a resolver-level test
that sends a malformed after cursor and verifies the expected invalid-cursor
behavior, including the related hasPreviousPage handling in the events resolver.
supabase/migrations/20260726030000_admin_audit_logs_immutability.sql (1)

24-27: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Adding FK constraint in-place locks audit_logs and profiles.

Static analysis flags this: adding admin_id UUID REFERENCES public.profiles(id) directly requires a table scan and SHARE ROW EXCLUSIVE lock on both tables, blocking writes to each while validating. If audit_logs already has rows, split into NOT VALID + a separate VALIDATE CONSTRAINT transaction.

♻️ Proposed fix
 ALTER TABLE public.audit_logs
-  ADD COLUMN IF NOT EXISTS admin_id UUID REFERENCES public.profiles(id) ON DELETE SET NULL,
+  ADD COLUMN IF NOT EXISTS admin_id UUID,
   ADD COLUMN IF NOT EXISTS old_value JSONB,
   ADD COLUMN IF NOT EXISTS new_value JSONB;
+
+ALTER TABLE public.audit_logs
+  ADD CONSTRAINT audit_logs_admin_id_fkey FOREIGN KEY (admin_id)
+  REFERENCES public.profiles(id) ON DELETE SET NULL NOT VALID;
+-- run in a follow-up migration/transaction:
+-- ALTER TABLE public.audit_logs VALIDATE CONSTRAINT audit_logs_admin_id_fkey;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260726030000_admin_audit_logs_immutability.sql` around
lines 24 - 27, Update the audit_logs migration’s admin_id foreign-key creation
to add the constraint as NOT VALID, avoiding an immediate existing-row scan and
long write-blocking lock. Add a separate validation step using the named
foreign-key constraint, keeping the old_value and new_value column additions
unchanged.

Source: Linters/SAST tools

src/hooks/useRealtimeComments.ts (1)

96-96: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No status handling on .subscribe().

If the channel fails to join (CHANNEL_ERROR/TIMED_OUT), there's no visibility into the failure — comments simply stop arriving in real time with no signal to diagnose why.

🔧 Suggested fix
-      .subscribe();
+      .subscribe((status) => {
+        if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") {
+          console.error(`Realtime comments subscription failed for post ${postId}:`, status);
+        }
+      });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useRealtimeComments.ts` at line 96, Update the realtime channel
subscription in useRealtimeComments so the .subscribe() callback receives and
handles the subscription status, explicitly logging or reporting CHANNEL_ERROR
and TIMED_OUT failures while preserving the existing successful subscription
behavior.
src/hooks/useRealtimeComments.test.ts (1)

34-63: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test coverage gap: async insert-handling path and enabled: false case are untested.

Only the channel-name-on-subscribe path is covered. The core logic — the on("postgres_changes", ...) callback that fetches the author profile, builds formattedComment, and calls onNewComment — along with the enabled: false branch and cleanup (removeChannel on unmount) are not exercised.

Consider capturing the handler passed to mockOn and invoking it with a mock payload to assert onNewComment receives the expected shape, plus a dedicated test for enabled: false.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useRealtimeComments.test.ts` around lines 34 - 63, Extend the
useRealtimeComments tests to cover the postgres_changes handler: capture the
callback passed through mockOn, invoke it with a mock insert payload, mock the
author-profile fetch, and assert onNewComment receives the expected formatted
comment. Add a dedicated enabled: false test asserting no subscription occurs,
and verify unmount cleanup calls removeChannel.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@graphql/resolvers/index.ts`:
- Around line 290-292: Update the events resolver to compute totalCount with a
separate count-only Supabase query using head: true, applying the base event
filters but excluding the after cursor condition. Keep the existing paginated
query for fetching nodes, and return the full matching event count consistently
across pages.
- Around line 7-24: Update SimpleDataLoader.load to coalesce concurrent cache
misses within the same microtask into one batchFn call, rather than invoking
batchFn([key]) per key. Queue pending keys and their resolvers, schedule a
single flush, then map each returned value back to the corresponding key while
preserving caching and null results.
- Around line 290-303: Validate the result of decodeCursor in the events
resolver before using decoded.createdAt or decoded.id in query.or. Require a
valid ISO date and a safe, expected ID format/shape, returning null or ignoring
the cursor when validation fails; only construct the keyset filter from
validated values and preserve the existing pagination behavior for valid
cursors.
- Around line 337-342: Update the event resolver to use Supabase’s maybeSingle
query method instead of single, while retaining the existing error handling.
Ensure missing or deleted IDs return null through the nullable event field,
while genuine query errors still throw.

In `@src/hooks/useRealtimeComments.ts`:
- Around line 44-101: The useRealtimeComments subscription effect depends on the
unstable onNewComment callback, causing channel teardown and recreation on each
render. In src/hooks/useRealtimeComments.ts lines 44-101, store onNewComment in
a ref, update that ref in a separate effect, and have the subscription callback
invoke the current ref while removing onNewComment from the subscription effect
dependencies; retain postId and enabled as subscription dependencies. In
src/components/Feed/CommentSection.tsx lines 28-37, make no direct change
because the inline callback is safe once the hook is fixed.

In `@src/routes/feed.tsx`:
- Around line 439-511: Replace the manual realtime subscription useEffect in the
feed component with a per-post PostCommentsRealtimeSubscriber that delegates to
useRealtimeComments. Render one keyed subscriber for each expandedPostIds entry,
forwarding new comments into the existing setLazyComments update and preserving
duplicate prevention; remove the duplicated author enrichment, formatting,
channel array, and teardown logic.
- Around line 439-511: Prevent the realtime INSERT handler in the expanded-post
subscription from replacing a cleared comment cache with only the newly inserted
comment. Update the related commentMutation success flow to stop deleting
lazyComments[postId] and let the subscription append the insert to the existing
thread; if the cache-bust is retained, fetch the complete thread when the entry
is missing instead of initializing it from the realtime payload.

In `@supabase/functions/cleanup-orphaned-storage/index.ts`:
- Around line 93-135: Replace the substring-based check in the orphan detection
loop with exact object matching: parse active database storage URLs into
canonical bucket-and-path keys, decode URL-encoded object names, and compare
each listed object’s full path together with bucket.name. Update the active URL
key construction and the isReferenced logic while preserving the existing
grace-period and deletion flow.
- Around line 108-111: The bucket scan around the storage list call must
paginate through all root entries and recursively traverse folder-like prefixes,
rather than processing only the first 1,000 results. Update the cleanup flow
using the existing bucket iteration and list/delete logic to maintain a
prefix/page queue, continue until each listing is exhausted, and evaluate nested
files for orphan removal.
- Around line 48-94: Update the mandatory Supabase selects in the
active-reference loading flow for profiles, clubs, events, and certificates to
inspect each returned error and abort cleanup immediately when any query fails,
before processing incomplete data. Preserve the existing event_photos try/catch
as the only failure path treated as an unavailable optional table, and ensure
cleanup does not proceed after a mandatory query failure.

In `@supabase/migrations/20260726010000_get_user_attendance_rate.sql`:
- Around line 10-38: Update public.get_user_attendance_rate to authorize access
before querying attendance data: allow requests for auth.uid() and permitted
admin users, and reject all other target_user_id values. Preserve the existing
attendance calculation for authorized callers, using the project’s established
admin-role check and an appropriate authorization error for unauthorized
requests.

In `@supabase/migrations/20260726020000_cron_cleanup_orphaned_storage.sql`:
- Around line 25-30: Update the net.http_post call in the scheduled cleanup job
to read SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY from the provisioned
vault.decrypted_secrets view instead of secrets.decrypted_secrets, preserving
the existing secret-name filters and request construction.

In `@supabase/migrations/20260726030000_admin_audit_logs_immutability.sql`:
- Around line 32-43: The "System admins can view audit logs" policy currently
grants all club_admin users access; update its profiles role condition to
require system_admin instead. Preserve the policy’s authenticated SELECT
behavior and existing auth.uid() profile matching.

In `@supabase/migrations/20260726050000_private_groups_rls.sql`:
- Around line 21-29: The group_members INSERT policy must prevent self-joining
private groups as approved. Update the policy governing group_members inserts to
require public groups for self-service membership and restrict user-supplied
status so private-group requests cannot bypass approval; preserve the
pending/approved workflow for administrator-approved joins. Add a pgTAP
regression case in groups_rls.test.sql that verifies an authenticated user
cannot obtain approved membership by inserting into a private group.
- Around line 166-180: Add a WITH CHECK clause to the "Authors or group admins
can update posts" policy requiring the updated row’s group_id to belong to a
group where auth.uid() is a member, while preserving author/admin authorization
in the existing USING clause. Ensure post authors cannot reassign posts to
groups they do not belong to.

---

Outside diff comments:
In `@src/routes/feed.tsx`:
- Around line 415-437: Update the realtime subscriptions in the feed effect
around supabase.channel("realtime_feed") to handle comment INSERT events for
collapsed posts as well as expanded posts. Add a lightweight per-post
comment-count update that increments the affected post’s count without fetching
profiles or triggering a blanket refetchPosts(); preserve the existing
expanded-post comment subscription behavior and avoid double-counting events.

---

Minor comments:
In `@graphql/resolvers/index.ts`:
- Around line 294-302: Update the pagination flow around decodeCursor and
pageInfo.hasPreviousPage so an invalid after cursor does not report a previous
page when its filter was skipped. Track whether decodeCursor(after) succeeded
and use that state when computing hasPreviousPage, while preserving the existing
keyset filter for valid cursors; alternatively, reject invalid cursors with the
established GraphQL error mechanism.

In `@src/components/Editor/extensions/ClubMentionExtension.tsx`:
- Around line 18-23: Update handleClick in ClubMentionView and the
ClubMentionExtension options to accept and invoke a navigation callback for
`/clubs/${clubSlug}` instead of assigning window.location.href. Thread this
callback from the React Router-aware parent through the extension and
ClubMentionView, preserving preventDefault and the existing clubSlug guard.

In `@src/components/Editor/extensions/EventCardExtension.tsx`:
- Around line 108-118: Update EventCardExtension’s addAttributes() so all
internal event-card attributes are marked rendered: false before renderHTML
re-merges them into data-* props. Ensure raw attributes such as eventId, title,
bannerUrl, and url are not emitted or treated as native HTML attributes, while
preserving the intended data-* output.

In `@src/components/Editor/TiptapRichTextEditor.tsx`:
- Around line 90-94: Update the events query in the TiptapRichTextEditor flow to
use only the id.eq filter, removing the id.ilike fallback. Validate eventId as a
UUID before constructing the PostgREST filter and reject non-UUID input instead
of issuing a query with an invalid identifier.

In `@src/hooks/useCursorEventsQuery.ts`:
- Around line 96-113: Update fetchGraphQL to validate the fetch response’s HTTP
status before parsing or returning data. For non-2xx responses, throw a clear
error that includes the response status, and preserve the existing GraphQL error
handling and successful json.data return path for valid responses.

In `@supabase/functions/cleanup-orphaned-storage/index.ts`:
- Around line 137-155: The cleanup flow currently records files in
deletedFilesLog before confirming removal, so failed batches are reported as
deleted. Move the deletedFilesLog updates from the orphan-detection loop into
the successful branch after supabase.storage.remove completes, and ensure
partial batch failures are explicitly surfaced rather than returning those files
as deleted.

---

Nitpick comments:
In `@src/graphql/server.test.ts`:
- Around line 82-154: Extend the GraphQL pagination tests in “GraphQL
Cursor-Based Events Pagination” with a second-page request using the first
response’s pageInfo.endCursor as after, asserting the returned events reflect
the narrowed keyset results. Add a resolver-level test that sends a malformed
after cursor and verifies the expected invalid-cursor behavior, including the
related hasPreviousPage handling in the events resolver.

In `@src/hooks/useRealtimeComments.test.ts`:
- Around line 34-63: Extend the useRealtimeComments tests to cover the
postgres_changes handler: capture the callback passed through mockOn, invoke it
with a mock insert payload, mock the author-profile fetch, and assert
onNewComment receives the expected formatted comment. Add a dedicated enabled:
false test asserting no subscription occurs, and verify unmount cleanup calls
removeChannel.

In `@src/hooks/useRealtimeComments.ts`:
- Line 96: Update the realtime channel subscription in useRealtimeComments so
the .subscribe() callback receives and handles the subscription status,
explicitly logging or reporting CHANNEL_ERROR and TIMED_OUT failures while
preserving the existing successful subscription behavior.

In `@supabase/functions/cleanup-orphaned-storage/index.test.ts`:
- Around line 5-44: Add mocked Supabase-client tests for the destructive cleanup
flow exercised by the handler, covering failed reference queries, exact
bucket/path matching, nested and paginated storage listings, grace-period
filtering, and failed delete operations. Assert that active or recently modified
files are preserved and that removal results and errors are reported accurately.

In `@supabase/migrations/20260726030000_admin_audit_logs_immutability.sql`:
- Around line 24-27: Update the audit_logs migration’s admin_id foreign-key
creation to add the constraint as NOT VALID, avoiding an immediate existing-row
scan and long write-blocking lock. Add a separate validation step using the
named foreign-key constraint, keeping the old_value and new_value column
additions unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 01c57ee8-cf5b-46c2-970b-06ea006c50c3

📥 Commits

Reviewing files that changed from the base of the PR and between b70209c and a8f248c.

📒 Files selected for processing (26)
  • graphql/resolvers/index.ts
  • src/components/Editor/TiptapExtensions.test.tsx
  • src/components/Editor/TiptapReadOnlyViewer.tsx
  • src/components/Editor/TiptapRichTextEditor.tsx
  • src/components/Editor/extensions/ClubMentionExtension.tsx
  • src/components/Editor/extensions/EventCardExtension.tsx
  • src/components/EventCard.tsx
  • src/components/Feed/CommentSection.tsx
  • src/components/ui/EventCard/EventCardActions.tsx
  • src/graphql/server.test.ts
  • src/hooks/useCursorEventsQuery.test.ts
  • src/hooks/useCursorEventsQuery.ts
  • src/hooks/useRealtimeComments.test.ts
  • src/hooks/useRealtimeComments.ts
  • src/routes/feed.tsx
  • supabase/functions/cleanup-orphaned-storage/index.test.ts
  • supabase/functions/cleanup-orphaned-storage/index.ts
  • supabase/functions/link-preview/index.ts
  • supabase/migrations/20260726010000_get_user_attendance_rate.sql
  • supabase/migrations/20260726020000_cron_cleanup_orphaned_storage.sql
  • supabase/migrations/20260726030000_admin_audit_logs_immutability.sql
  • supabase/migrations/20260726040000_realtime_comments_broadcast.sql
  • supabase/migrations/20260726050000_private_groups_rls.sql
  • supabase/tests/audit_logs.test.sql
  • supabase/tests/get_user_attendance_rate.test.sql
  • supabase/tests/groups_rls.test.sql

Comment thread graphql/resolvers/index.ts
Comment thread graphql/resolvers/index.ts
Comment thread graphql/resolvers/index.ts
Comment thread graphql/resolvers/index.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 15

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (1)
src/routes/feed.tsx (1)

415-437: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Comment counts on collapsed posts no longer update live.

The previous blanket comments listener that called refetchPosts() on any comment change was removed, and the new targeted effect (Lines 439-511) only subscribes for postIds in expandedPostIds. A collapsed post's Comments ({postComments.length}) count (Line 1320) is now only as fresh as the last unrelated refetchPosts() — a new comment on a collapsed post won't be reflected until something else triggers a full refetch.

If live counts on collapsed posts matter, consider a lightweight listener that only increments a per-post counter (no profile fetch) instead of restoring the old blanket refetch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routes/feed.tsx` around lines 415 - 437, Update the realtime
subscriptions in the feed effect around supabase.channel("realtime_feed") to
handle comment INSERT events for collapsed posts as well as expanded posts. Add
a lightweight per-post comment-count update that increments the affected post’s
count without fetching profiles or triggering a blanket refetchPosts(); preserve
the existing expanded-post comment subscription behavior and avoid
double-counting events.
🟡 Minor comments (6)
supabase/functions/cleanup-orphaned-storage/index.ts-137-155 (1)

137-155: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not report failed deletions as deleted.

deletedFilesLog is populated before remove(). A failed batch still returns those names under deletedFiles and a top-level success: true. Append only after a successful removal and surface partial failures explicitly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/functions/cleanup-orphaned-storage/index.ts` around lines 137 - 155,
The cleanup flow currently records files in deletedFilesLog before confirming
removal, so failed batches are reported as deleted. Move the deletedFilesLog
updates from the orphan-detection loop into the successful branch after
supabase.storage.remove completes, and ensure partial batch failures are
explicitly surfaced rather than returning those files as deleted.
graphql/resolvers/index.ts-294-302 (1)

294-302: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

hasPreviousPage is true even when the cursor was invalid and silently ignored.

If after is provided but decodeCursor(after) fails (malformed/forged cursor), the code skips applying the .or() filter and effectively returns page 1 unpaginated — yet pageInfo.hasPreviousPage: !!after still reports true. Clients following this hint would believe there's a prior page when the response is actually the first page. Consider tracking whether the filter was actually applied (e.g., hasPreviousPage: !!after && !!decoded), or throwing a GraphQL error for invalid cursors instead of silently falling back to page 1.

Also applies to: 325-336

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@graphql/resolvers/index.ts` around lines 294 - 302, Update the pagination
flow around decodeCursor and pageInfo.hasPreviousPage so an invalid after cursor
does not report a previous page when its filter was skipped. Track whether
decodeCursor(after) succeeded and use that state when computing hasPreviousPage,
while preserving the existing keyset filter for valid cursors; alternatively,
reject invalid cursors with the established GraphQL error mechanism.
src/hooks/useCursorEventsQuery.ts-96-113 (1)

96-113: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

fetchGraphQL doesn't check HTTP status before parsing the response.

If the endpoint returns a non-2xx response without a GraphQL-shaped JSON body (e.g., an auth gateway error page or plain-text 500), res.json() can throw an unhelpful parse error, or json.data can be silently undefined, instead of a clear error surfaced to the caller.

🛡️ Proposed fix
   const res = await fetch("/api/graphql", {
     method: "POST",
     headers: {
       "Content-Type": "application/json",
     },
     body: JSON.stringify({ query, variables }),
   });

+  if (!res.ok) {
+    throw new Error(`GraphQL request failed with status ${res.status}`);
+  }
   const json = await res.json();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useCursorEventsQuery.ts` around lines 96 - 113, Update fetchGraphQL
to validate the fetch response’s HTTP status before parsing or returning data.
For non-2xx responses, throw a clear error that includes the response status,
and preserve the existing GraphQL error handling and successful json.data return
path for valid responses.
src/components/Editor/TiptapRichTextEditor.tsx-90-94 (1)

90-94: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Drop the id.ilike fallback.

events.id is defined as UUID, so PostgRESTBuilder.or(\id.eq.${eventId},id.ilike.${eventId}`)includes an invalid UUID ILIKE branch; valid UUID pastes should match withid.eq.${eventId}` only, while non-UUID input should be rejected rather than appended into the PostgREST filter.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Editor/TiptapRichTextEditor.tsx` around lines 90 - 94, Update
the events query in the TiptapRichTextEditor flow to use only the id.eq filter,
removing the id.ilike fallback. Validate eventId as a UUID before constructing
the PostgREST filter and reject non-UUID input instead of issuing a query with
an invalid identifier.
src/components/Editor/extensions/ClubMentionExtension.tsx-18-23 (1)

18-23: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Avoid full-page reload on club mention clicks.

The app uses React Router with /clubs/:slug routing, but this link forces a browser reload via window.location.href. Node views rendered by ReactNodeViewRenderer don’t inherit the app’s router context, so pass a navigation callback into ClubMentionView/the extension options instead of setting window.location directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Editor/extensions/ClubMentionExtension.tsx` around lines 18 -
23, Update handleClick in ClubMentionView and the ClubMentionExtension options
to accept and invoke a navigation callback for `/clubs/${clubSlug}` instead of
assigning window.location.href. Thread this callback from the React Router-aware
parent through the extension and ClubMentionView, preserving preventDefault and
the existing clubSlug guard.
src/components/Editor/extensions/EventCardExtension.tsx-108-118 (1)

108-118: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Mark EventCard attributes as internal before re-merging them.

Tiptap renders declared attributes as HTML attributes by default, so renderHTML here adds the intended data-* attrs and then HTMLAttributes also outputs raw attrs like eventid, title, bannerurl, and link. That also adds a native title tooltip for the card content. Add rendered: false to these attributes, or build the final props without merging the raw attrs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Editor/extensions/EventCardExtension.tsx` around lines 108 -
118, Update EventCardExtension’s addAttributes() so all internal event-card
attributes are marked rendered: false before renderHTML re-merges them into
data-* props. Ensure raw attributes such as eventId, title, bannerUrl, and url
are not emitted or treated as native HTML attributes, while preserving the
intended data-* output.
🧹 Nitpick comments (5)
supabase/functions/cleanup-orphaned-storage/index.test.ts (1)

5-44: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Cover the destructive cleanup path.

Add mocked-client tests for failed reference queries, exact bucket/path matching, nested/paginated listings, grace-period handling, and failed deletes. The current tests cannot prevent regressions that delete active files or misreport removals.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/functions/cleanup-orphaned-storage/index.test.ts` around lines 5 -
44, Add mocked Supabase-client tests for the destructive cleanup flow exercised
by the handler, covering failed reference queries, exact bucket/path matching,
nested and paginated storage listings, grace-period filtering, and failed delete
operations. Assert that active or recently modified files are preserved and that
removal results and errors are reported accurately.
src/graphql/server.test.ts (1)

82-154: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No test coverage for second-page (after) pagination or resolver-level invalid-cursor handling.

Given this PR's core objective is keyset pagination robustness, only the first page (first: 2, no after) is exercised end-to-end. Consider adding a case that requests a second page using the endCursor from page 1 and asserts the .or() filter narrows results correctly, plus a case passing a malformed after value to verify resolver-level behavior (see related comment on the hasPreviousPage/invalid-cursor handling in graphql/resolvers/index.ts).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/graphql/server.test.ts` around lines 82 - 154, Extend the GraphQL
pagination tests in “GraphQL Cursor-Based Events Pagination” with a second-page
request using the first response’s pageInfo.endCursor as after, asserting the
returned events reflect the narrowed keyset results. Add a resolver-level test
that sends a malformed after cursor and verifies the expected invalid-cursor
behavior, including the related hasPreviousPage handling in the events resolver.
supabase/migrations/20260726030000_admin_audit_logs_immutability.sql (1)

24-27: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Adding FK constraint in-place locks audit_logs and profiles.

Static analysis flags this: adding admin_id UUID REFERENCES public.profiles(id) directly requires a table scan and SHARE ROW EXCLUSIVE lock on both tables, blocking writes to each while validating. If audit_logs already has rows, split into NOT VALID + a separate VALIDATE CONSTRAINT transaction.

♻️ Proposed fix
 ALTER TABLE public.audit_logs
-  ADD COLUMN IF NOT EXISTS admin_id UUID REFERENCES public.profiles(id) ON DELETE SET NULL,
+  ADD COLUMN IF NOT EXISTS admin_id UUID,
   ADD COLUMN IF NOT EXISTS old_value JSONB,
   ADD COLUMN IF NOT EXISTS new_value JSONB;
+
+ALTER TABLE public.audit_logs
+  ADD CONSTRAINT audit_logs_admin_id_fkey FOREIGN KEY (admin_id)
+  REFERENCES public.profiles(id) ON DELETE SET NULL NOT VALID;
+-- run in a follow-up migration/transaction:
+-- ALTER TABLE public.audit_logs VALIDATE CONSTRAINT audit_logs_admin_id_fkey;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260726030000_admin_audit_logs_immutability.sql` around
lines 24 - 27, Update the audit_logs migration’s admin_id foreign-key creation
to add the constraint as NOT VALID, avoiding an immediate existing-row scan and
long write-blocking lock. Add a separate validation step using the named
foreign-key constraint, keeping the old_value and new_value column additions
unchanged.

Source: Linters/SAST tools

src/hooks/useRealtimeComments.ts (1)

96-96: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No status handling on .subscribe().

If the channel fails to join (CHANNEL_ERROR/TIMED_OUT), there's no visibility into the failure — comments simply stop arriving in real time with no signal to diagnose why.

🔧 Suggested fix
-      .subscribe();
+      .subscribe((status) => {
+        if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") {
+          console.error(`Realtime comments subscription failed for post ${postId}:`, status);
+        }
+      });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useRealtimeComments.ts` at line 96, Update the realtime channel
subscription in useRealtimeComments so the .subscribe() callback receives and
handles the subscription status, explicitly logging or reporting CHANNEL_ERROR
and TIMED_OUT failures while preserving the existing successful subscription
behavior.
src/hooks/useRealtimeComments.test.ts (1)

34-63: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test coverage gap: async insert-handling path and enabled: false case are untested.

Only the channel-name-on-subscribe path is covered. The core logic — the on("postgres_changes", ...) callback that fetches the author profile, builds formattedComment, and calls onNewComment — along with the enabled: false branch and cleanup (removeChannel on unmount) are not exercised.

Consider capturing the handler passed to mockOn and invoking it with a mock payload to assert onNewComment receives the expected shape, plus a dedicated test for enabled: false.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useRealtimeComments.test.ts` around lines 34 - 63, Extend the
useRealtimeComments tests to cover the postgres_changes handler: capture the
callback passed through mockOn, invoke it with a mock insert payload, mock the
author-profile fetch, and assert onNewComment receives the expected formatted
comment. Add a dedicated enabled: false test asserting no subscription occurs,
and verify unmount cleanup calls removeChannel.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@graphql/resolvers/index.ts`:
- Around line 290-292: Update the events resolver to compute totalCount with a
separate count-only Supabase query using head: true, applying the base event
filters but excluding the after cursor condition. Keep the existing paginated
query for fetching nodes, and return the full matching event count consistently
across pages.
- Around line 7-24: Update SimpleDataLoader.load to coalesce concurrent cache
misses within the same microtask into one batchFn call, rather than invoking
batchFn([key]) per key. Queue pending keys and their resolvers, schedule a
single flush, then map each returned value back to the corresponding key while
preserving caching and null results.
- Around line 290-303: Validate the result of decodeCursor in the events
resolver before using decoded.createdAt or decoded.id in query.or. Require a
valid ISO date and a safe, expected ID format/shape, returning null or ignoring
the cursor when validation fails; only construct the keyset filter from
validated values and preserve the existing pagination behavior for valid
cursors.
- Around line 337-342: Update the event resolver to use Supabase’s maybeSingle
query method instead of single, while retaining the existing error handling.
Ensure missing or deleted IDs return null through the nullable event field,
while genuine query errors still throw.

In `@src/hooks/useRealtimeComments.ts`:
- Around line 44-101: The useRealtimeComments subscription effect depends on the
unstable onNewComment callback, causing channel teardown and recreation on each
render. In src/hooks/useRealtimeComments.ts lines 44-101, store onNewComment in
a ref, update that ref in a separate effect, and have the subscription callback
invoke the current ref while removing onNewComment from the subscription effect
dependencies; retain postId and enabled as subscription dependencies. In
src/components/Feed/CommentSection.tsx lines 28-37, make no direct change
because the inline callback is safe once the hook is fixed.

In `@src/routes/feed.tsx`:
- Around line 439-511: Replace the manual realtime subscription useEffect in the
feed component with a per-post PostCommentsRealtimeSubscriber that delegates to
useRealtimeComments. Render one keyed subscriber for each expandedPostIds entry,
forwarding new comments into the existing setLazyComments update and preserving
duplicate prevention; remove the duplicated author enrichment, formatting,
channel array, and teardown logic.
- Around line 439-511: Prevent the realtime INSERT handler in the expanded-post
subscription from replacing a cleared comment cache with only the newly inserted
comment. Update the related commentMutation success flow to stop deleting
lazyComments[postId] and let the subscription append the insert to the existing
thread; if the cache-bust is retained, fetch the complete thread when the entry
is missing instead of initializing it from the realtime payload.

In `@supabase/functions/cleanup-orphaned-storage/index.ts`:
- Around line 93-135: Replace the substring-based check in the orphan detection
loop with exact object matching: parse active database storage URLs into
canonical bucket-and-path keys, decode URL-encoded object names, and compare
each listed object’s full path together with bucket.name. Update the active URL
key construction and the isReferenced logic while preserving the existing
grace-period and deletion flow.
- Around line 108-111: The bucket scan around the storage list call must
paginate through all root entries and recursively traverse folder-like prefixes,
rather than processing only the first 1,000 results. Update the cleanup flow
using the existing bucket iteration and list/delete logic to maintain a
prefix/page queue, continue until each listing is exhausted, and evaluate nested
files for orphan removal.
- Around line 48-94: Update the mandatory Supabase selects in the
active-reference loading flow for profiles, clubs, events, and certificates to
inspect each returned error and abort cleanup immediately when any query fails,
before processing incomplete data. Preserve the existing event_photos try/catch
as the only failure path treated as an unavailable optional table, and ensure
cleanup does not proceed after a mandatory query failure.

In `@supabase/migrations/20260726010000_get_user_attendance_rate.sql`:
- Around line 10-38: Update public.get_user_attendance_rate to authorize access
before querying attendance data: allow requests for auth.uid() and permitted
admin users, and reject all other target_user_id values. Preserve the existing
attendance calculation for authorized callers, using the project’s established
admin-role check and an appropriate authorization error for unauthorized
requests.

In `@supabase/migrations/20260726020000_cron_cleanup_orphaned_storage.sql`:
- Around line 25-30: Update the net.http_post call in the scheduled cleanup job
to read SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY from the provisioned
vault.decrypted_secrets view instead of secrets.decrypted_secrets, preserving
the existing secret-name filters and request construction.

In `@supabase/migrations/20260726030000_admin_audit_logs_immutability.sql`:
- Around line 32-43: The "System admins can view audit logs" policy currently
grants all club_admin users access; update its profiles role condition to
require system_admin instead. Preserve the policy’s authenticated SELECT
behavior and existing auth.uid() profile matching.

In `@supabase/migrations/20260726050000_private_groups_rls.sql`:
- Around line 21-29: The group_members INSERT policy must prevent self-joining
private groups as approved. Update the policy governing group_members inserts to
require public groups for self-service membership and restrict user-supplied
status so private-group requests cannot bypass approval; preserve the
pending/approved workflow for administrator-approved joins. Add a pgTAP
regression case in groups_rls.test.sql that verifies an authenticated user
cannot obtain approved membership by inserting into a private group.
- Around line 166-180: Add a WITH CHECK clause to the "Authors or group admins
can update posts" policy requiring the updated row’s group_id to belong to a
group where auth.uid() is a member, while preserving author/admin authorization
in the existing USING clause. Ensure post authors cannot reassign posts to
groups they do not belong to.

---

Outside diff comments:
In `@src/routes/feed.tsx`:
- Around line 415-437: Update the realtime subscriptions in the feed effect
around supabase.channel("realtime_feed") to handle comment INSERT events for
collapsed posts as well as expanded posts. Add a lightweight per-post
comment-count update that increments the affected post’s count without fetching
profiles or triggering a blanket refetchPosts(); preserve the existing
expanded-post comment subscription behavior and avoid double-counting events.

---

Minor comments:
In `@graphql/resolvers/index.ts`:
- Around line 294-302: Update the pagination flow around decodeCursor and
pageInfo.hasPreviousPage so an invalid after cursor does not report a previous
page when its filter was skipped. Track whether decodeCursor(after) succeeded
and use that state when computing hasPreviousPage, while preserving the existing
keyset filter for valid cursors; alternatively, reject invalid cursors with the
established GraphQL error mechanism.

In `@src/components/Editor/extensions/ClubMentionExtension.tsx`:
- Around line 18-23: Update handleClick in ClubMentionView and the
ClubMentionExtension options to accept and invoke a navigation callback for
`/clubs/${clubSlug}` instead of assigning window.location.href. Thread this
callback from the React Router-aware parent through the extension and
ClubMentionView, preserving preventDefault and the existing clubSlug guard.

In `@src/components/Editor/extensions/EventCardExtension.tsx`:
- Around line 108-118: Update EventCardExtension’s addAttributes() so all
internal event-card attributes are marked rendered: false before renderHTML
re-merges them into data-* props. Ensure raw attributes such as eventId, title,
bannerUrl, and url are not emitted or treated as native HTML attributes, while
preserving the intended data-* output.

In `@src/components/Editor/TiptapRichTextEditor.tsx`:
- Around line 90-94: Update the events query in the TiptapRichTextEditor flow to
use only the id.eq filter, removing the id.ilike fallback. Validate eventId as a
UUID before constructing the PostgREST filter and reject non-UUID input instead
of issuing a query with an invalid identifier.

In `@src/hooks/useCursorEventsQuery.ts`:
- Around line 96-113: Update fetchGraphQL to validate the fetch response’s HTTP
status before parsing or returning data. For non-2xx responses, throw a clear
error that includes the response status, and preserve the existing GraphQL error
handling and successful json.data return path for valid responses.

In `@supabase/functions/cleanup-orphaned-storage/index.ts`:
- Around line 137-155: The cleanup flow currently records files in
deletedFilesLog before confirming removal, so failed batches are reported as
deleted. Move the deletedFilesLog updates from the orphan-detection loop into
the successful branch after supabase.storage.remove completes, and ensure
partial batch failures are explicitly surfaced rather than returning those files
as deleted.

---

Nitpick comments:
In `@src/graphql/server.test.ts`:
- Around line 82-154: Extend the GraphQL pagination tests in “GraphQL
Cursor-Based Events Pagination” with a second-page request using the first
response’s pageInfo.endCursor as after, asserting the returned events reflect
the narrowed keyset results. Add a resolver-level test that sends a malformed
after cursor and verifies the expected invalid-cursor behavior, including the
related hasPreviousPage handling in the events resolver.

In `@src/hooks/useRealtimeComments.test.ts`:
- Around line 34-63: Extend the useRealtimeComments tests to cover the
postgres_changes handler: capture the callback passed through mockOn, invoke it
with a mock insert payload, mock the author-profile fetch, and assert
onNewComment receives the expected formatted comment. Add a dedicated enabled:
false test asserting no subscription occurs, and verify unmount cleanup calls
removeChannel.

In `@src/hooks/useRealtimeComments.ts`:
- Line 96: Update the realtime channel subscription in useRealtimeComments so
the .subscribe() callback receives and handles the subscription status,
explicitly logging or reporting CHANNEL_ERROR and TIMED_OUT failures while
preserving the existing successful subscription behavior.

In `@supabase/functions/cleanup-orphaned-storage/index.test.ts`:
- Around line 5-44: Add mocked Supabase-client tests for the destructive cleanup
flow exercised by the handler, covering failed reference queries, exact
bucket/path matching, nested and paginated storage listings, grace-period
filtering, and failed delete operations. Assert that active or recently modified
files are preserved and that removal results and errors are reported accurately.

In `@supabase/migrations/20260726030000_admin_audit_logs_immutability.sql`:
- Around line 24-27: Update the audit_logs migration’s admin_id foreign-key
creation to add the constraint as NOT VALID, avoiding an immediate existing-row
scan and long write-blocking lock. Add a separate validation step using the
named foreign-key constraint, keeping the old_value and new_value column
additions unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 01c57ee8-cf5b-46c2-970b-06ea006c50c3

📥 Commits

Reviewing files that changed from the base of the PR and between b70209c and a8f248c.

📒 Files selected for processing (26)
  • graphql/resolvers/index.ts
  • src/components/Editor/TiptapExtensions.test.tsx
  • src/components/Editor/TiptapReadOnlyViewer.tsx
  • src/components/Editor/TiptapRichTextEditor.tsx
  • src/components/Editor/extensions/ClubMentionExtension.tsx
  • src/components/Editor/extensions/EventCardExtension.tsx
  • src/components/EventCard.tsx
  • src/components/Feed/CommentSection.tsx
  • src/components/ui/EventCard/EventCardActions.tsx
  • src/graphql/server.test.ts
  • src/hooks/useCursorEventsQuery.test.ts
  • src/hooks/useCursorEventsQuery.ts
  • src/hooks/useRealtimeComments.test.ts
  • src/hooks/useRealtimeComments.ts
  • src/routes/feed.tsx
  • supabase/functions/cleanup-orphaned-storage/index.test.ts
  • supabase/functions/cleanup-orphaned-storage/index.ts
  • supabase/functions/link-preview/index.ts
  • supabase/migrations/20260726010000_get_user_attendance_rate.sql
  • supabase/migrations/20260726020000_cron_cleanup_orphaned_storage.sql
  • supabase/migrations/20260726030000_admin_audit_logs_immutability.sql
  • supabase/migrations/20260726040000_realtime_comments_broadcast.sql
  • supabase/migrations/20260726050000_private_groups_rls.sql
  • supabase/tests/audit_logs.test.sql
  • supabase/tests/get_user_attendance_rate.test.sql
  • supabase/tests/groups_rls.test.sql
🛑 Comments failed to post (10)
src/hooks/useRealtimeComments.ts (1)

44-101: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Realtime comment channel is torn down/rebuilt on nearly every keystroke. useRealtimeComments's effect depends on the raw onNewComment callback, and its only current caller passes a fresh inline arrow function every render; since CommentSection re-renders on every keystroke while composing a comment, the WebSocket subscription churns constantly, risking missed inserts during each resubscribe gap.

  • src/hooks/useRealtimeComments.ts#L44-L101: store onNewComment in a useRef, update it in a separate effect, and drop it from the subscription effect's dependency array so the channel only re-subscribes on postId/enabled changes.
  • src/components/Feed/CommentSection.tsx#L28-L37: no change needed once the hook is fixed — the inline callback becomes safe to pass as-is.
📍 Affects 2 files
  • src/hooks/useRealtimeComments.ts#L44-L101 (this comment)
  • src/components/Feed/CommentSection.tsx#L28-L37
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useRealtimeComments.ts` around lines 44 - 101, The
useRealtimeComments subscription effect depends on the unstable onNewComment
callback, causing channel teardown and recreation on each render. In
src/hooks/useRealtimeComments.ts lines 44-101, store onNewComment in a ref,
update that ref in a separate effect, and have the subscription callback invoke
the current ref while removing onNewComment from the subscription effect
dependencies; retain postId and enabled as subscription dependencies. In
src/components/Feed/CommentSection.tsx lines 28-37, make no direct change
because the inline callback is safe once the hook is fixed.
src/routes/feed.tsx (1)

439-511: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Duplicates useRealtimeComments and rebuilds every channel on any toggle.

This effect reimplements the same author-enrichment/format logic as src/hooks/useRealtimeComments.ts (compare Lines 456-489 here with that hook), and on every expandedPostIds change it tears down and recreates all per-post channels (Lines 441-511), not just the delta — expanding a 6th post resubscribes the 5 already-open ones too, with a brief gap where an insert on an already-open post could be missed.

Consider extracting a tiny per-post subscriber component that wraps useRealtimeComments, and rendering one per expandedPostIds entry so React's mount/unmount lifecycle naturally handles diffing instead of a manual channels array:

function PostCommentsRealtimeSubscriber({
  postId,
  onNewComment,
}: {
  postId: string;
  onNewComment: (c: Comment) => void;
}) {
  useRealtimeComments({ postId, enabled: true, onNewComment });
  return null;
}

Then render Array.from(expandedPostIds).map((id) => <PostCommentsRealtimeSubscriber key={id} postId={id} onNewComment={...} />). This also eliminates the duplicated type/logic in Lines 456-489 in favor of the shared hook (once the ref-based fix suggested in useRealtimeComments.ts lands, so per-post callback identity no longer matters).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routes/feed.tsx` around lines 439 - 511, Replace the manual realtime
subscription useEffect in the feed component with a per-post
PostCommentsRealtimeSubscriber that delegates to useRealtimeComments. Render one
keyed subscriber for each expandedPostIds entry, forwarding new comments into
the existing setLazyComments update and preserving duplicate prevention; remove
the duplicated author enrichment, formatting, channel array, and teardown logic.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Race with commentMutation's own cache-bust can wipe an expanded post's comment thread.

commentMutation.onSuccess (Lines 651-659, unchanged) deletes lazyComments[postId] after a successful insert, then calls refetchPosts(). The realtime insert handler added here (Lines 491-500) treats a missing cache entry as an empty list (prev[postId] || []), so when the realtime broadcast for that same insert arrives after the cache-bust, lazyComments[postId] gets overwritten with an array containing only the just-posted comment — the rest of the previously-loaded thread disappears from the UI until the post is collapsed/re-expanded.

This will reproduce on essentially every comment a user posts to an expanded thread, since the client's own insert always races the corresponding realtime broadcast for it.

🔧 Suggested fix (stop busting the cache; let realtime append organically)
     onSuccess: (_data, variables) => {
-      // Bust the lazy cache for this post so the new comment appears on re-fetch
-      setLazyComments((prev) => {
-        const next = { ...prev };
-        delete next[variables.postId];
-        return next;
-      });
       refetchPosts();
     },

Alternatively, if the cache-bust must stay, guard the realtime handler so a missing/just-cleared cache entry triggers fetchCommentsForPost(postId) instead of seeding a single-item array.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routes/feed.tsx` around lines 439 - 511, Prevent the realtime INSERT
handler in the expanded-post subscription from replacing a cleared comment cache
with only the newly inserted comment. Update the related commentMutation success
flow to stop deleting lazyComments[postId] and let the subscription append the
insert to the existing thread; if the cache-bust is retained, fetch the complete
thread when the entry is missing instead of initializing it from the realtime
payload.
supabase/functions/cleanup-orphaned-storage/index.ts (3)

48-94: 🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

For @supabase/supabase-jsv2.110.0, does a failedsupabase.from(...).select(...)return anerror result by default rather than throw, and how should optional-table failures be handled safely?

💡 Result:

For @supabase/supabase-js v2.110.0, a failed supabase.from(...).select(...) query returns a { data, error } object by default rather than throwing an error [1][2][3]. The error property will contain details if the request is unsuccessful [1][4]. To handle failures, you should check for the presence of the error object [1][2]: const { data, error } = await supabase.from('table').select; if (error) { console.error(error); // Logs message, code, details, and hint [1][2] return; } Regarding optional-table failures or cases where data may not exist, handling depends on the modifiers used: 1. Use.maybeSingle instead of.single: If you are querying for a single row that might not exist, using .single() will throw a PGRST116 error when no rows are found [5][6]. Using .maybeSingle() instead will safely return null for the data and null for the error [5][6]. 2. Check for null/empty results: Even if no error is returned, always verify if data is null or empty if you expect the record to exist, especially when using modifiers like .maybeSingle() or .limit(1) [6]. 3. Programmatic branching: Use error.code for programmatic logic (e.g., handling specific Postgres errors like RLS violations), as error codes are stable across versions, whereas error messages may change [1]. If you specifically require a call to throw an error, you can append .throwOnError() to your query builder chain, which will cause the promise to reject if the operation fails [4][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the cleanup function around the reported lines without executing repo code.
fd -a 'index.ts' . | rg 'supabase/functions/cleanup-orphaned-storage/index\.ts$'
echo '--- outline ---'
ast-grep outline supabase/functions/cleanup-orphaned-storage/index.ts || true
echo '--- relevant lines ---'
sed -n '1,180p' supabase/functions/cleanup-orphaned-storage/index.ts | cat -n

echo '--- error handling patterns in Supabase JS docs from installed/package metadata if present ---'
if [ -f package.json ]; then
  jq -r '.dependencies["`@supabase/supabase-js`"], .devDependencies["`@supabase/supabase-js`"]' package.json 2>/dev/null || true
fi
rg -n "throwOnError|\.select\(|\.from\(|event_photos|Supabase client" supabase/functions/cleanup-orphaned-storage/index.ts package.json 2>/dev/null || true

Repository: krushit1307/CampusConnect

Length of output: 9648


Fail closed when loading active references.

With Supabase JS, failed select() calls resolve with an error; the mandatory selects for profiles, clubs, events, and certificates ignore it and can populate activeUrlStrings incompletely. A file referenced only in a failed query will be treated as orphaned and deleted. Check the mandatory query error and abort cleanup on failure; only handle the optional event_photos failure as “table unavailable”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/functions/cleanup-orphaned-storage/index.ts` around lines 48 - 94,
Update the mandatory Supabase selects in the active-reference loading flow for
profiles, clubs, events, and certificates to inspect each returned error and
abort cleanup immediately when any query fails, before processing incomplete
data. Preserve the existing event_photos try/catch as the only failure path
treated as an unavailable optional table, and ensure cleanup does not proceed
after a mandatory query failure.

93-135: 🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Match referenced objects by canonical bucket and path.

includes(file.name) is not an object identity check: an active bucket-a/foo.png protects an orphaned bucket-b/foo.png, while URL-encoded object names can fail to match and be deleted. Parse stored URLs into exact { bucket, path } keys and compare against the listed object's full path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/functions/cleanup-orphaned-storage/index.ts` around lines 93 - 135,
Replace the substring-based check in the orphan detection loop with exact object
matching: parse active database storage URLs into canonical bucket-and-path
keys, decode URL-encoded object names, and compare each listed object’s full
path together with bucket.name. Update the active URL key construction and the
isReferenced logic while preserving the existing grace-period and deletion flow.

108-111: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

For @supabase/supabase-jsv2.110.0 Storagelist, confirm pagination semantics and whether listing a bucket prefix recursively returns nested objects.

💡 Result:

In @supabase/supabase-js v2.110.0, the standard list method (also referred to as the legacy list API) has specific behavior regarding recursion and pagination: Recursive Listing The list method does not recursively return nested objects [1]. It returns only the immediate children—both files and subfolders—within the specified prefix (folder path) [1]. If you need to access files inside subfolders, you must perform separate list calls for those subfolder paths [1]. Pagination Semantics The list method uses offset-based pagination [2][3]. You can control the results via the options object using: - limit: Specifies the maximum number of items to return (default is 100) [2][3][4]. - offset: Specifies the starting position for the result set (default is 0) [2][3][4]. Note: Supabase has introduced a newer, experimental listV2 method (available in recent versions of @supabase/storage-js) which supports cursor-based pagination and hierarchical grouping (via the with_delimiter option) to better handle complex bucket structures [5][2][6][3]. Summary of Key Behaviors - The standard list(path, options) does not perform recursive lookups [1]. - Folders are returned as entries where fields like id, updated_at, created_at, and metadata are null [7][4][8]. - Standard pagination relies on the limit and offset parameters [2][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Find file:\n'
fd -a 'index.ts$' . | sed 's#^\./##' | rg 'supabase/functions/cleanup-orphaned-storage/index.ts' || true

printf '\nRelevant code:\n'
sed -n '1,180p' supabase/functions/cleanup-orphaned-storage/index.ts 2>/dev/null || true

printf '\nSearch for cleanup function and storage list usage:\n'
rg -n "cleanup|list\\(|offset|cursor|with_delimiter|prefix|depth|listV2" supabase/functions/cleanup-orphaned-storage/index.ts supabase/functions 2>/dev/null || true

printf '\nPackage versions mentioning supabase/storage-js or `@supabase/storage-js`:\n'
rg -n '"`@supabase/`(storage-js|supabase-js)"|supabase-js|storage-js' . -g 'package.json' -g 'deno.json' -g 'supabase/functions/**/*.ts' 2>/dev/null || true

Repository: krushit1307/CampusConnect

Length of output: 12902


Traverse every bucket path page and nested prefix.

The storage list call only returns the first 1,000 root-level children of each bucket; it does not recursively include nested files and later pages are never scanned. Drive the scan until every listing is exhausted, and recurse through folder-like entries (or use an explicit prefix queue) so orphaned files deeper in the bucket are also evaluated and removed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/functions/cleanup-orphaned-storage/index.ts` around lines 108 - 111,
The bucket scan around the storage list call must paginate through all root
entries and recursively traverse folder-like prefixes, rather than processing
only the first 1,000 results. Update the cleanup flow using the existing bucket
iteration and list/delete logic to maintain a prefix/page queue, continue until
each listing is exhausted, and evaluate nested files for orphan removal.
supabase/migrations/20260726010000_get_user_attendance_rate.sql (1)

10-38: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Missing authorization check lets any user query another user's attendance rate.

get_user_attendance_rate is SECURITY DEFINER and granted to authenticated, but never validates that the caller is requesting their own data (or has an admin role). Any authenticated user can call get_user_attendance_rate('<other-user-uuid>') and learn that user's private no-show/check-in behavior, since the function bypasses RLS on event_rsvps/events.

🔒️ Proposed fix
 BEGIN
+  IF auth.uid() IS DISTINCT FROM target_user_id
+     AND NOT EXISTS (SELECT 1 FROM public.profiles WHERE id = auth.uid() AND role = 'club_admin'::user_role)
+  THEN
+    RAISE EXCEPTION 'Not authorized to view this user''s attendance rate';
+  END IF;
+
   SELECT
     COUNT(*)::INTEGER,
📝 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.

CREATE OR REPLACE FUNCTION public.get_user_attendance_rate(target_user_id UUID)
RETURNS INTEGER
LANGUAGE plpgsql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
  total_past_rsvps INTEGER;
  checked_in_count INTEGER;
BEGIN
  IF auth.uid() IS DISTINCT FROM target_user_id
     AND NOT EXISTS (SELECT 1 FROM public.profiles WHERE id = auth.uid() AND role = 'club_admin'::user_role)
  THEN
    RAISE EXCEPTION 'Not authorized to view this user''s attendance rate';
  END IF;

  SELECT
    COUNT(*)::INTEGER,
    COUNT(*) FILTER (WHERE r.checked_in = TRUE)::INTEGER
  INTO
    total_past_rsvps,
    checked_in_count
  FROM public.event_rsvps r
  JOIN public.events e ON e.id = r.event_id
  WHERE r.user_id = target_user_id
    AND COALESCE(e.end_date, e.start_date, e.event_date) < NOW();

  IF total_past_rsvps = 0 THEN
    RETURN 0;
  END IF;

  RETURN ROUND((checked_in_count * 100.0) / total_past_rsvps)::INTEGER;
END;
$$;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260726010000_get_user_attendance_rate.sql` around lines
10 - 38, Update public.get_user_attendance_rate to authorize access before
querying attendance data: allow requests for auth.uid() and permitted admin
users, and reject all other target_user_id values. Preserve the existing
attendance calculation for authorized callers, using the project’s established
admin-role check and an appropriate authorization error for unauthorized
requests.
supabase/migrations/20260726020000_cron_cleanup_orphaned_storage.sql (1)

25-30: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -i -C2 \
  'create schema[[:space:]]+secrets|decrypted_secrets|vault\.decrypted_secrets' \
  supabase || true

Repository: krushit1307/CampusConnect

Length of output: 1145


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== migration =="
cat -n supabase/migrations/20260726020000_cron_cleanup_orphaned_storage.sql

echo
echo "== schema/object references for secrets/vault/decrypted_secrets in tracked files =="
rg -n -i -C2 '(secrets|vault)\.[^.[:space:],;)]+|decrypted_secrets|supabase_vault|create schema|create (schema|view)|grant (select|usage)|pgbouncer|function pgbouncer|pg_temp|pg_catalog' . || true

Repository: krushit1307/CampusConnect

Length of output: 16818


Use the provisioned secrets view for the scheduled job.

secrets.decrypted_secrets is referenced here, but this repo does not create that schema or view. If Supabase Vault supplies the secrets as vault.decrypted_secrets, point the cron job at that source; otherwise the weekly cleanup job will fail when resolving SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260726020000_cron_cleanup_orphaned_storage.sql` around
lines 25 - 30, Update the net.http_post call in the scheduled cleanup job to
read SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY from the provisioned
vault.decrypted_secrets view instead of secrets.decrypted_secrets, preserving
the existing secret-name filters and request construction.
supabase/migrations/20260726030000_admin_audit_logs_immutability.sql (1)

32-43: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how 'club_admin' is used/defined elsewhere to confirm scope (global vs per-club)
rg -n "club_admin" supabase/schema.sql supabase/migrations
rg -n "CREATE TYPE user_role|user_role" supabase/schema.sql

Repository: krushit1307/CampusConnect

Length of output: 10978


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== profile and club-related definitions =="
rg -n "CREATE TABLE public\\.profiles|CREATE TABLE .*club_members|profiles.*role|club_members" supabase/schema.sql supabase/migrations | head -n 80

echo
echo "== audit_logs policy/functions around migration =="
wc -l supabase/migrations/20260726030000_admin_audit_logs_immutability.sql
cat -n supabase/migrations/20260726030000_admin_audit_logs_immutability.sql | sed -n '1,140p'

echo
echo "== rbac helper definitions =="
cat -n supabase/migrations/20260720000006_dynamic_club_roles.sql | sed -n '1,140p'

echo
echo "== read-only structural check: club_admin role grants and admin checks in migrations =="
python3 - <<'PY'
from pathlib import Path
import re

files = sorted([p for p in Path('supabase/migrations').glob('*.sql')])
for p in files:
    text = p.read_text(errors='ignore')
    entries = []
    for m in re.finditer(r'(?i)role\s*=?\s*[\'\"]([^\'\"]*club_admin[^\'\"]*|[^\'\"]*admin[^\'\"]*)[\'\"]', text):
        snippet = text[max(0, m.start()-220):min(len(text), m.end()+220)].replace('\n',' ')
        entries.append((m.start(), snippet))
    for m in re.finditer(r'(?i)role[[:space:]]+user_role\b.*\(', text):
        entries.append((m.start(), text[max(0, m.start()-80):min(len(text), m.end()+220)].replace('\n',' ').strip()))
    for m in re.finditer(r'(?i)(is_system_admin|is_verified_club_admin|is_club_admin|club_admin)\b', text):
        entries.append((m.start(), text[max(0, m.start()-250):min(len(text), m.end()+250)].replace('\n',' ').strip()))
    if entries:
        print(f"\n--- {p} ---")
        for pos, snippet in sorted(set(entries), key=lambda x:x[0]):
            for line in snippet.split('\n'):
                line=line.strip()
                if any(k in line for k in ['role', 'admin', 'club_admin', 'system_admin']):
                    print(line)
PY

Repository: krushit1307/CampusConnect

Length of output: 50381


Restrict audit log access to intended admins.

If this policy is meant to grant platform-wide audit access, scope the profile role to system_admin; currently every club_admin can read all audit_logs rows, which is over-broad relative to the app’s existing club-scoped admin checks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260726030000_admin_audit_logs_immutability.sql` around
lines 32 - 43, The "System admins can view audit logs" policy currently grants
all club_admin users access; update its profiles role condition to require
system_admin instead. Preserve the policy’s authenticated SELECT behavior and
existing auth.uid() profile matching.
supabase/migrations/20260726050000_private_groups_rls.sql (2)

21-29: 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Any user can self-join a private group with status = 'approved', bypassing private-group protection entirely.

The INSERT WITH CHECK only verifies auth.uid() = user_id; it never checks groups.is_private or restricts the status value. Combined with status defaulting to 'approved' (line 26), any authenticated user who knows a private group's id can insert their own group_members row and instantly gain full membership — reading private group_posts, posting, etc. — with no approval step, even though the schema clearly models a pending/approved workflow.

🔒️ Proposed fix
 DROP POLICY IF EXISTS "Users can join groups" ON public.group_members;
 CREATE POLICY "Users can join groups" ON public.group_members
   FOR INSERT TO authenticated
-  WITH CHECK (auth.uid() = user_id);
+  WITH CHECK (
+    auth.uid() = user_id AND
+    (
+      status = 'pending' OR
+      EXISTS (
+        SELECT 1 FROM public.groups g
+        WHERE g.id = group_id AND (g.is_private IS FALSE OR g.is_private IS NULL)
+      )
+    )
+  );

Please also add a pgTAP regression test in groups_rls.test.sql covering self-join to a private group. Would you like me to draft that test?

Also applies to: 134-137

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260726050000_private_groups_rls.sql` around lines 21 -
29, The group_members INSERT policy must prevent self-joining private groups as
approved. Update the policy governing group_members inserts to require public
groups for self-service membership and restrict user-supplied status so
private-group requests cannot bypass approval; preserve the pending/approved
workflow for administrator-approved joins. Add a pgTAP regression case in
groups_rls.test.sql that verifies an authenticated user cannot obtain approved
membership by inserting into a private group.

166-180: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Post author can reassign group_id to any group, including private groups they don't belong to.

The UPDATE policy on group_posts (lines 174-180) has only USING, no WITH CHECK. Postgres reuses the USING expression for the new row when WITH CHECK is omitted, so the new row must satisfy auth.uid() = author_id OR is_group_admin(...). Since an author never loses auth.uid() = author_id by changing group_id, they can move their post into any group — even a private one they aren't a member of — bypassing the membership requirement enforced by the INSERT policy (lines 166-172).

🔒️ Proposed fix
 DROP POLICY IF EXISTS "Authors or group admins can update posts" ON public.group_posts;
 CREATE POLICY "Authors or group admins can update posts" ON public.group_posts
   FOR UPDATE TO authenticated
   USING (
     auth.uid() = author_id OR
     public.is_group_admin(group_id, auth.uid())
-  );
+  )
+  WITH CHECK (
+    (auth.uid() = author_id AND public.is_group_member(group_id, auth.uid())) OR
+    public.is_group_admin(group_id, auth.uid())
+  );
📝 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.

DROP POLICY IF EXISTS "Group members can insert posts" ON public.group_posts;
CREATE POLICY "Group members can insert posts" ON public.group_posts
  FOR INSERT TO authenticated
  WITH CHECK (
    auth.uid() = author_id AND
    public.is_group_member(group_id, auth.uid())
  );

DROP POLICY IF EXISTS "Authors or group admins can update posts" ON public.group_posts;
CREATE POLICY "Authors or group admins can update posts" ON public.group_posts
  FOR UPDATE TO authenticated
  USING (
    auth.uid() = author_id OR
    public.is_group_admin(group_id, auth.uid())
  )
  WITH CHECK (
    (auth.uid() = author_id AND public.is_group_member(group_id, auth.uid())) OR
    public.is_group_admin(group_id, auth.uid())
  );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260726050000_private_groups_rls.sql` around lines 166 -
180, Add a WITH CHECK clause to the "Authors or group admins can update posts"
policy requiring the updated row’s group_id to belong to a group where
auth.uid() is a member, while preserving author/admin authorization in the
existing USING clause. Ensure post authors cannot reassign posts to groups they
do not belong to.

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

Labels

backend Blue All CI checks are passing on this PR ECSoC26-L3 ECSoC26 Elite Coders Summer Of Code'26 - Open Source Program good-pr optimization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Cursor-Based Pagination for GraphQL Events Query

2 participants