Skip to content

Fix: skip username uniqueness check on partial profile updates#443

Merged
ralyodio merged 2 commits into
profullstack:masterfrom
tankgxy:fix/profile-patch-single-bug-v2
Jun 14, 2026
Merged

Fix: skip username uniqueness check on partial profile updates#443
ralyodio merged 2 commits into
profullstack:masterfrom
tankgxy:fix/profile-patch-single-bug-v2

Conversation

@tankgxy

@tankgxy tankgxy commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

The PATCH /api/profile endpoint crashed with a 500 error when the request body didn't include a username field. The username uniqueness check used .single() which throws on zero matches. Wrapping the check in if (validationResult.data.username) and using .maybeSingle() allows partial profile updates to work correctly.

@greptile-apps

greptile-apps Bot commented Jun 13, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a 500 crash on PATCH /api/profile when username was omitted, caused by .single() throwing on zero results. It makes username optional in profileSchema and guards the uniqueness check behind if (validationResult.data.username), switching to .maybeSingle().

  • src/lib/validations.ts: username is now .optional() in profileSchema, unblocking requests that omit the field.
  • src/app/api/profile/route.ts: The username uniqueness check is wrapped in a conditional and upgraded to .maybeSingle(), fixing the 500 crash.
  • The update payload is built by spreading all of validationResult.data — fields with Zod .default() values (skills, ai_tools, portfolio_urls, wallet_addresses, is_available) will be written to the database even when absent from the request body, silently erasing existing user data on any partial PATCH.

Confidence Score: 3/5

The crash fix is correct, but the update handler spreads Zod defaults into every DB write, meaning a partial PATCH silently zeros out skills, tools, portfolio URLs, wallet addresses, and resets availability — real data loss for any user who sends a minimal request.

Making username optional and guarding the uniqueness check properly addresses the 500 crash. However, the spread of validationResult.data into the update payload means fields with Zod defaults (skills, ai_tools, portfolio_urls, wallet_addresses, is_available) are unconditionally written to the database when absent from the request body. This turns every partial PATCH into a destructive overwrite of the user's existing data, which is a regression introduced by unlocking partial update behaviour without adjusting the payload construction logic.

src/app/api/profile/route.ts — the updateData construction (lines 112–116) needs to exclude keys absent from the original request body before the Supabase update is safe for partial requests.

Important Files Changed

Filename Overview
src/app/api/profile/route.ts PATCH handler now skips the username uniqueness check when username is absent, and username is now schema-optional — but the update payload still spreads all Zod defaults (empty arrays for skills/ai_tools/portfolio_urls/wallet_addresses, true for is_available), so a partial PATCH silently resets those fields in the database.
src/lib/validations.ts Added .optional() to the username field in profileSchema, correctly allowing partial profile updates to omit the username field without failing Zod validation.

Sequence Diagram

sequenceDiagram
    participant Client
    participant PATCH/PUT Handler
    participant profileSchema (Zod)
    participant Supabase

    Client->>PATCH/PUT Handler: PATCH /api/profile {username?: "...", ...fields}
    PATCH/PUT Handler->>profileSchema (Zod): safeParse(body)
    Note over profileSchema (Zod): username now .optional()<br/>array fields fill .default([])
    profileSchema (Zod)-->>PATCH/PUT Handler: validationResult.data<br/>(skills:[], ai_tools:[], is_available:true, ...)

    alt username present
        PATCH/PUT Handler->>Supabase: SELECT id WHERE username=? AND id!=user.id (.maybeSingle())
        Supabase-->>PATCH/PUT Handler: existingUser or null
        alt username taken
            PATCH/PUT Handler-->>Client: 400 Username taken
        end
    end

    PATCH/PUT Handler->>Supabase: UPDATE profiles SET {...validationResult.data, profile_completed, updated_at}
    Note over Supabase: Zod defaults (skills:[], etc.)<br/>overwrite existing DB values
    Supabase-->>PATCH/PUT Handler: updated profile
    PATCH/PUT Handler-->>Client: 200 {profile}
Loading

Comments Outside Diff (1)

  1. src/app/api/profile/route.ts, line 112-116 (link)

    P1 Zod defaults silently overwrite existing array data on partial PATCH

    Now that username is optional and partial updates are supported, any PATCH request that omits skills, ai_tools, portfolio_urls, or wallet_addresses will write their Zod .default([]) values to the database, erasing the user's existing data. is_available has .default(true), so omitting it also resets a user who set themselves unavailable.

    For example, PATCH {username: "newname"} resolves to validationResult.data with skills: [], ai_tools: [], portfolio_urls: [], wallet_addresses: [], is_available: true — all spread into updateData and sent to Supabase. The profile_completed flag is similarly computed from the partial input, so the same request would set profile_completed: false even if the profile was previously complete.

    The fix is to strip fields that were not present in the original request body before building updateData (e.g. filter out undefined values from validationResult.data and keys that were absent in body). Alternatively, use a separate stricter partial schema that carries no defaults for PATCH, or merge against the existing DB profile before computing isComplete.

Reviews (2): Last reviewed commit: "fix: make username optional in profileSc..." | Re-trigger Greptile

Comment on lines +82 to 96
if (validationResult.data.username) {
const { data: existingUser } = await supabase
.from("profiles")
.select("id")
.eq("username", validationResult.data.username)
.neq("id", user.id)
.maybeSingle();

if (existingUser) {
return NextResponse.json(
{ error: "Username is already taken" },
{ status: 400 }
);
}
}

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 Guard is unreachable — schema still requires username

profileSchema defines username without .optional() (see src/lib/validations.ts lines 76-83), so when a PATCH request omits username, safeParse returns success: false and the handler already returns a 400 at lines 57-62 — the execution never reaches the new if (validationResult.data.username) guard. The partial-update behaviour described in the PR is therefore not actually unlocked: requests without username still fail with a validation error, not a successful partial update.

To make the guard meaningful, username in profileSchema (or a dedicated partial schema used only by PATCH) needs to be marked .optional().

@tankgxy

tankgxy commented Jun 13, 2026

Copy link
Copy Markdown
Contributor Author

Updated: Also fixed profileSchema in validations.ts - username is now .optional(), so PATCH requests without a username field pass Zod validation and reach the route handler.

@ralyodio ralyodio merged commit e2c8863 into profullstack:master Jun 14, 2026
6 checks passed
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