fix(artists): deleteArtist must not hard-delete a shared canonical with song/catalog dependencies#770
Conversation
…th song dependencies deleteArtist removed the caller's roster link, then hard-deleted the canonical artist account whenever zero links remained across all accounts. account_artist_ids.artist_id is ON DELETE CASCADE, so this destroyed song_artists and the shared song graph for a canonical with a catalog but a single roster link. Guard the deleteAccountById call with a song_artists existence check (fails safe on query error) so the canonical account and song graph survive; artists with no dependencies still hard-delete as before. Fixes recoupable/chat#1860 (promoted deleteArtist-guard item). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
Warning Review limit reached
Next review available in: 24 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (4)
📒 Files selected for processing (4)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
All reported issues were addressed across 4 files
Confidence score: 5/5
- Safe to merge after the addressed issues were fixed.
Architecture diagram
sequenceDiagram
participant Client as Caller (e.g. API route)
participant deleteArtist as deleteArtist()
participant DeleteLink as deleteAccountArtistId()
participant GetLinks as getAccountArtistIds()
participant CheckSongs as getSongArtistExistsByArtist()
participant DeleteAccount as deleteAccountById()
participant DB as Database (Supabase)
Note over Client,DB: NEW: deleteArtist song-dependency guard flow
Client->>deleteArtist: deleteArtist({artistId, requesterAccountId})
deleteArtist->>DeleteLink: deleteAccountArtistId(link)
DeleteLink-->>deleteArtist: deleted link or []
alt Link deletion failed
deleteArtist-->>Client: throw "Failed to delete artist link"
else Link deleted successfully
deleteArtist->>GetLinks: getAccountArtistIds(artistId)
GetLinks-->>deleteArtist: remaining roster links
alt Other roster links still exist
Note over deleteArtist: Shared canonical, just remove link
deleteArtist-->>Client: return artistId
else No remaining links (last link removed)
Note over deleteArtist,DeleteAccount: NEW: Check song dependencies before hard-delete
deleteArtist->>CheckSongs: getSongArtistExistsByArtist(artistId)
CheckSongs->>DB: SELECT count(*) FROM song_artists WHERE artist = artistId (head only)
alt Query error
DB-->>CheckSongs: error
Note over CheckSongs: Fails safe → treat as dependencies exist
CheckSongs-->>deleteArtist: true
else No errors
DB-->>CheckSongs: count (0 or >0)
CheckSongs-->>deleteArtist: (count ?? 0) > 0
end
alt hasSongDependencies is true
Note over deleteArtist: Canonical has songs — keep account, song graph intact
deleteArtist-->>Client: return artistId
else hasSongDependencies is false
Note over deleteArtist: Clean slate — hard-delete as before
deleteArtist->>DeleteAccount: deleteAccountById(artistId)
DeleteAccount->>DB: DELETE FROM accounts WHERE id = artistId
Note over DB: CASCADE deletes account_artist_ids row
DeleteAccount-->>deleteArtist: void
deleteArtist-->>Client: return artistId
end
end
end
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Preview verification — deleteArtist shared-canonical guardPreview: Guardrails (non-destructive — no delete proceeds)
Branch 1 — no dependencies → hard-delete (unchanged behavior, verified end-to-end)Self-contained throwaway fixture so nothing shared was touched:
Branch 2 — has song dependencies → keep canonical (the new guard)This is the destructive branch the PR adds. It cannot be safely executed live: there is no API to write
For any of these, VerdictGuardrails (401/400/404/403) and the no-dependency hard-delete path are verified live end-to-end; the protective guard's dependency signal is confirmed real against multiple 100+-song canonicals. The destructive last-link-with-deps branch is unit-covered and intentionally not executed against shared prod data. No regressions observed. Throwaway fixture fully cleaned up (hard-deleted). 🤖 Generated with Claude Code |
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
… selectSongArtistsByArtist Per review: supabase libs should be generic/reusable, not single-purpose. Swap getSongArtistExistsByArtist (boolean, fail-safe baked in) for a generic selectSongArtistsByArtist(artist) that returns rows (null on error), mirroring selectSongArtistsBySongs. The fail-closed policy moves into deleteArtist where it is domain logic: a null (query error) or any rows => keep the canonical; only an empty array hard-deletes. Behavior preserved. 111 tests green (added a fail-closed deleteArtist case for the null/error path); tsc + lint + prettier clean on touched files. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
Made the song_artists lib generic (review follow-up)Per feedback that supabase libs should be generic/reusable rather than single-purpose, replaced The fail-closed policy moved into const songArtists = await selectSongArtistsByArtist(artistId);
// Fail closed: null (query error) or any rows => keep the canonical.
const hasSongDependencies = songArtists === null || songArtists.length > 0;
if (!hasSongDependencies) {
await deleteAccountById(artistId);
}So the safety-critical bias is unchanged: a query error still keeps the canonical (never hard-deletes on an unknown state). Added a Note: the one efficiency trade-off is this now |
There was a problem hiding this comment.
0 issues found across 6 files (changes from recent commits).
Requires human review: Bug fix for artist deletion logic with song dependency check.
Re-trigger cubic
…ists
Per review: match the shared param-driven select convention used elsewhere
(e.g. getAccountArtistIds({ artistIds?, accountIds? })). Replace the two
single-column helpers (selectSongArtistsBySongs / selectSongArtistsByArtist)
with one selectSongArtists({ songs?, artists? }) — chunked .in(), null on
error. Both callers updated:
- attachCanonicalArtistToAccount -> selectSongArtists({ songs }) (coalesces
null to [] for its best-effort path, behavior unchanged)
- deleteArtist -> selectSongArtists({ artists: [artistId] }) (fail-closed:
null or any rows keeps the canonical)
172 tests green across lib/artists, lib/supabase/song_artists, lib/catalog;
tsc + lint + prettier clean on touched files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
Consolidated to one param-driven
|
There was a problem hiding this comment.
0 issues found across 10 files (changes from recent commits).
Requires human review: Prevents data loss by guarding hard-delete of canonical artists with song dependencies; adds new helper to check song_artists rows. Refactors existing helper to support multiple filters. Core business logic changes require human review.
Re-trigger cubic
Guard-200 verified live — the last-link + has-dependencies branchFollow-up to the earlier run (which only covered the no-dependency hard-delete). This exercises the branch the PR actually adds: last roster link removed on a canonical that has Preview: To prove the guard fired (and not the trivial "other links remain" path), I needed to be the sole roster link at delete time. Found a real canonical — artist account
This is the whole point of the PR: pre-guard, that same sole-link delete would have hit The fixture is left exactly as found (link removed → back to 0 roster links, 2 songs, account intact) — net-zero, no shared data mutated. Read-only DB queries were used only to locate a sole-link candidate and to verify survival; the delete itself went through the real API. Coverage summary (this PR)
🤖 Generated with Claude Code |
Problem
Promoted deleteArtist-guard item from recoupable/chat#1860.
lib/artists/deleteArtist.tsremoves the caller's own roster link (correctly scoped), but then hard-deletes the canonical artist account viadeleteAccountById(artistId)whenever zero roster links remain across ALL accounts.account_artist_ids.artist_idisON DELETE CASCADE, so this cascades intosong_artistsand destroys the shared song graph. A canonical artist with a catalog but a single roster link gets permanently destroyed when that one account removes it: irreversible cross-customer data loss.Fix
lib/supabase/song_artists/getSongArtistExistsByArtist.ts: head-count existence check forsong_artistsrows by artist account id. Fails safe: on query error it returnstrueso an unknown state is never treated as "no dependencies".deleteArtistnow checks for song dependencies before thedeleteAccountByIdcall. When dependencies exist, only the caller's link is removed and the canonical account + song graph stay intact.Behavior
song_artistsrows: link removed, NO hard delete, song graph intact.Verification
pnpm exec vitest run lib/artists lib/supabase/song_artists: 19 files, 110 tests, all passing (includes 4 newdeleteArtistguard tests + 3 new supabase-helper tests).pnpm exec tsc --noEmit: 200 pre-existing errors on main, identical count with this change: zero new errors, none in touched files.Preview verification pending — tracked in chat#1860.
🤖 Generated with Claude Code
Summary by cubic
Prevent hard-deleting a canonical artist that still has song dependencies.
deleteArtistnow removes the caller’s roster link and only hard-deletes when no links remain and nosong_artistsrows exist; lookup errors keep the canonical.Bug Fixes
deleteAccountByIdinlib/artists/deleteArtist.tsusingselectSongArtists({ artists: [artistId] }); null or non-empty => keep canonical; empty => delete.Refactors
selectSongArtistsBySongs/selectSongArtistsByArtistwithselectSongArtists({ songs?, artists? })and updatedattachCanonicalArtistToAccountto use it; removed old helpers and updated tests.Written for commit 7cf611a. Summary will update on new commits.