feat: POST /api/catalogs (create + materialize from valuation snapshot)

Merged
SW
4mo ago
+562-5

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. validateAuthContext resolves the owner from the Privy bearer JWT or x-api-key. No account_id is read from the request → no IDOR.
  • from.snapshot_id materialization. Verifies the snapshot is owned by the caller (403 otherwise), then creates the catalogs row, links account_catalogs, adds the snapshot's measured ISRCs as catalog_songs, and records the catalog on the snapshot.
  • Idempotent. The snapshot's existing catalog FK 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)
  • 400 neither name nor from / bad snapshot_id · 401 unauth · 403 snapshot owned by another account · 404 snapshot 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); no account_id in the body.
    • Materialize from an owned snapshot: create catalogs, link account_catalogs, load measured ISRCs from song_measurements into catalog_songs, and record the catalog on the snapshot.
    • Defaults to "Valuation Catalog" when name isn’t provided; CORS OPTIONS added.
  • Refactors

    • Flattened request body to { name?, snapshot? } to match the merged contract.
    • Reused shared successResponse; renamed to createSnapshotCatalog; replaced a redundant update helper with updatePlaycountSnapshot.
    • Reused selectSongMeasurements with a new snapshot filter (dropped the dedicated helper) and deduped ISRCs before insert.

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.