feat: POST /api/catalogs (create + materialize from valuation snapshot)
What
Implements POST /api/catalogs per the contract in recoupable/docs#243. Creates a catalog owned by the authenticated account, and optionally materializes it from a completed valuation snapshot — closing the "catalog empty after sign-in" gap from recoupable/chat#1801 (Phase 2).
- Account from credentials, never the body.
validateAuthContextresolves the owner from the Privy bearer JWT orx-api-key. Noaccount_idis read from the request → no IDOR. from.snapshot_idmaterialization. Verifies the snapshot is owned by the caller (403 otherwise), then creates thecatalogsrow, linksaccount_catalogs, adds the snapshot's measured ISRCs ascatalog_songs, and records the catalog on the snapshot.- Idempotent. The snapshot's existing
catalogFK is the dedupe key — re-claiming returns the catalog already created for that run (songs_added: 0), no duplicates. No DB migration needed (the column already exists).
Contract
POST /api/catalogs → { name?, from?: { snapshot_id } } (≥1 required)
200→{ status, catalog, songs_added }(also the idempotent re-claim response)400neither name nor from / bad snapshot_id ·401unauth ·403snapshot owned by another account ·404snapshot not found
TDD
Red → green, one unit at a time:
validateCreateCatalogBody— 6 tests (name-only, from-only, both, empty→400, bad uuid→400, null→400)createCatalogHandler— 8 tests (validator/auth short-circuits, plain create, materialize happy path, 404, 403, idempotent re-claim, generic 500)
SRP: one exported fn/file. New supabase wrappers (insertCatalog, selectCatalogById, insertAccountCatalog, updateSnapshotCatalog) mirror the existing insertCatalogSongs style; materializeSnapshotCatalog extracted from the handler.
pnpm exec vitest run lib/catalog → 24 passed. eslint clean on all new files. tsc --noEmit: my files type-clean (pre-existing unrelated __tests__ type-drift in lib/tasks/lib/trigger/etc. is unchanged by this PR; next build excludes test files).
Merge order
docs#243 (contract) first, then this. Verification against the preview to follow in a comment.
🤖 Generated with Claude Code
Summary by cubic
Adds POST /api/catalogs to create a catalog for the authenticated account, with optional materialization from a valuation snapshot via the snapshot field. Returns the catalog and songs_added; re-claiming a snapshot is idempotent.
-
New Features
- Auth via credentials (Privy bearer JWT or
x-api-key); noaccount_idin the body. - Materialize from an owned snapshot: create
catalogs, linkaccount_catalogs, load measured ISRCs fromsong_measurementsintocatalog_songs, and record the catalog on the snapshot. - Defaults to "Valuation Catalog" when
nameisn’t provided; CORSOPTIONSadded.
- Auth via credentials (Privy bearer JWT or
-
Refactors
- Flattened request body to
{ name?, snapshot? }to match the merged contract. - Reused shared
successResponse; renamed tocreateSnapshotCatalog; replaced a redundant update helper withupdatePlaycountSnapshot. - Reused
selectSongMeasurementswith a newsnapshotfilter (dropped the dedicated helper) and deduped ISRCs before insert.
- Flattened request body to
Summary by CodeRabbit
- New Features
- Users can now create new catalogs with optional custom names through a dedicated API endpoint.
- Support for creating catalogs from existing playcount snapshots, with automatic linking of measured songs from those snapshots to the new catalog.
- Implemented comprehensive request validation and error handling to ensure proper data submission for catalog creation operations.