Skip to content

fix: replace .single() with .maybeSingle() in reviews, saved-gigs, work-history, api-keys, wallet-addresses#476

Closed
tankgxy wants to merge 1 commit into
profullstack:masterfrom
tankgxy:fix/more-single-maybe-single
Closed

fix: replace .single() with .maybeSingle() in reviews, saved-gigs, work-history, api-keys, wallet-addresses#476
tankgxy wants to merge 1 commit into
profullstack:masterfrom
tankgxy:fix/more-single-maybe-single

Conversation

@tankgxy

@tankgxy tankgxy commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Continues the .single() to .maybeSingle() migration. Fixed routes:\n- GET/POST /api/reviews\n- GET/POST /api/saved-gigs\n- GET/POST /api/work-history\n- GET/DELETE /api/api-keys\n- GET/POST/DELETE /api/profile/wallet-addresses

@greptile-apps

greptile-apps Bot commented Jun 14, 2026

Copy link
Copy Markdown

Greptile Summary

This PR continues the codebase-wide migration from .single() to .maybeSingle() across five route files, preventing Supabase's PGRST116 "no rows" error from surfacing as an unhandled exception on SELECT queries. The migration is applied correctly to all SELECT calls, and api-keys handles the INSERT result properly, but the INSERT usages in reviews, saved-gigs, and work-history are missing the required null guard on the returned data.

  • reviews/route.ts: After the INSERT, only createError is checked — review itself is never guarded. If RLS blocks the SELECT after a successful insert, review is null and the first access (review.id, review.reviewer, etc.) throws a TypeError, turning a recoverable scenario into a generic 500 response.
  • saved-gigs/route.ts & work-history/route.ts: The INSERT result is not null-checked; a silent null yields a 201 response with { saved: null } or { work_history: null }, sending a success status to the client with no usable record.
  • api-keys/route.ts & wallet-addresses/route.ts: All changes are safe — results are already guarded with if (error || !apiKey) and optional chaining respectively.

Confidence Score: 3/5

Not safe to merge as-is — the reviews INSERT path will throw a TypeError and the saved-gigs and work-history INSERT paths will return misleading null payloads under RLS conditions.

Two of the five files correctly handle the null result from .maybeSingle(). The other three INSERT-after-select call sites are missing a null guard: reviews/route.ts accesses review.id and related fields directly, turning a null result into a runtime exception; saved-gigs/route.ts and work-history/route.ts respond 201 with a null body, which consumers cannot distinguish from a real record.

src/app/api/reviews/route.ts (TypeError on null review), src/app/api/saved-gigs/route.ts and src/app/api/work-history/route.ts (null body on 201 response)

Important Files Changed

Filename Overview
src/app/api/reviews/route.ts Five .single().maybeSingle() conversions; the INSERT result (review) is never null-checked before being accessed on multiple lines, risking a TypeError that silently collapses into a generic 500 response.
src/app/api/saved-gigs/route.ts INSERT result savedGig is not null-checked after .maybeSingle(); a null result with no error causes a 201 response with { saved: null } — the record is created but not returned to the caller.
src/app/api/work-history/route.ts INSERT result workHistory is not null-checked after .maybeSingle(); same silent-null pattern as saved-gigs — 201 returned with { work_history: null } if SELECT is blocked by RLS.
src/app/api/api-keys/route.ts Single .single().maybeSingle() on the INSERT; the guard `if (error
src/app/api/profile/wallet-addresses/route.ts Three .single().maybeSingle() changes on SELECT queries; all results are handled with optional chaining or explicit !gig guards — correctly migrated.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[INSERT / SELECT query] --> B{.maybeSingle}
    B --> C["data: row, error: null ✅"]
    B --> D["data: null, error: DBError ✅"]
    B --> E["data: null, error: null ⚠️\n(RLS blocks SELECT after INSERT)"]

    C --> F[Happy path — row returned]
    D --> G[Error guard catches it]
    E --> H{null check present?}

    H -- "Yes (api-keys)" --> I["if error or !apiKey → 400 ✅"]
    H -- "No (reviews)" --> J["review.id → TypeError → 500 ❌"]
    H -- "No (saved-gigs / work-history)" --> K["201 with null body ❌"]
Loading

Comments Outside Diff (2)

  1. src/app/api/saved-gigs/route.ts, line 145-149 (link)

    P1 Silent null response on INSERT with .maybeSingle()

    If the insert succeeds but RLS blocks the SELECT from returning the new row, maybeSingle() gives { data: null, error: null }. The error guard only checks error, so the handler returns { saved: null } with a 201 status — the client receives a success response but no usable record. Adding a null guard mirrors the pattern already used elsewhere (e.g., api-keys).

  2. src/app/api/work-history/route.ts, line 75-79 (link)

    P1 Silent null response on INSERT with .maybeSingle()

    Same issue as the saved-gigs INSERT: if workHistory is null (no error, but SELECT returned no rows), the handler responds 201 with { work_history: null }, leaving the caller without the created entry. Adding || !workHistory to the guard makes the behavior consistent with how .single() used to behave.

Reviews (1): Last reviewed commit: "fix: replace .single() with .maybeSingle..." | Re-trigger Greptile

Comment on lines 246 to 260
@@ -254,7 +254,7 @@ export async function POST(request: NextRequest) {
.from("profiles")
.select("did")
.eq("id", reviewee_id)
.single();
.maybeSingle();
if (userDid) {
onReviewCreated(userDid, review.id, revieweeProfile?.did || undefined);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Missing null check after .maybeSingle() on INSERT

The code only checks createError but not !review after the insert. With .maybeSingle(), a successful insert where RLS prevents the SELECT from returning the row yields { data: null, error: null }. With .single() that scenario produced a PGRST116 error caught in createError. Now review is silently null, and the first access at review.id on line 259 (and review.reviewer/review.reviewee on lines 263–264) throws a TypeError, collapsing the entire handler into the generic 500 catch block instead of returning a meaningful response.

@ralyodio ralyodio closed this Jun 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants