From 1e39c74b16f37d025777602bdfa24a2ae751c687 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Sat, 11 Jul 2026 13:55:36 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20artist=20item=20API=20=E2=80=94=20detai?= =?UTF-8?q?l,=20auto-monitor=20toggle,=20unfollow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/app/api/artists/[id]/route.test.ts | 60 ++++++++++++++++++++++ web/src/app/api/artists/[id]/route.ts | 57 ++++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 web/src/app/api/artists/[id]/route.test.ts create mode 100644 web/src/app/api/artists/[id]/route.ts diff --git a/web/src/app/api/artists/[id]/route.test.ts b/web/src/app/api/artists/[id]/route.test.ts new file mode 100644 index 0000000..2f5bfa1 --- /dev/null +++ b/web/src/app/api/artists/[id]/route.test.ts @@ -0,0 +1,60 @@ +import { describe, it, expect } from "vitest"; +import { prisma } from "@/lib/db"; +import { GET, PATCH, DELETE } from "./route"; + +const ctx = (id: string) => ({ params: Promise.resolve({ id }) }); + +async function seedArtist() { + return prisma.watchedArtist.create({ + data: { + mbid: "a1", + name: "John Mayer", + releases: { + create: [ + { artistMbid: "a1", artistName: "John Mayer", rgMbid: "rg1", album: "Continuum", primaryType: "Album", secondaryTypes: [], firstReleaseDate: "2006-09-12", monitored: true, state: "grabbed", currentQualityClass: 1 }, + ], + }, + }, + }); +} + +function patchReq(body: unknown) { + return new Request("http://localhost/x", { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify(body) }); +} + +describe("artist item API", () => { + it("returns detail with releases", async () => { + const a = await seedArtist(); + const res = await GET(new Request("http://localhost/x"), ctx(a.id)); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.name).toBe("John Mayer"); + expect(body.releases[0]).toMatchObject({ rgMbid: "rg1", album: "Continuum", monitored: true, state: "grabbed", currentQualityClass: 1 }); + }); + + it("404s an unknown artist on GET", async () => { + expect((await GET(new Request("http://localhost/x"), ctx("nope"))).status).toBe(404); + }); + + it("toggles autoMonitorFuture", async () => { + const a = await seedArtist(); + const res = await PATCH(patchReq({ autoMonitorFuture: true }), ctx(a.id)); + expect(res.status).toBe(200); + expect((await res.json()).autoMonitorFuture).toBe(true); + expect((await prisma.watchedArtist.findUnique({ where: { id: a.id } }))!.autoMonitorFuture).toBe(true); + }); + + it("400s a non-boolean toggle and 404s an unknown artist", async () => { + const a = await seedArtist(); + expect((await PATCH(patchReq({ autoMonitorFuture: "yes" }), ctx(a.id))).status).toBe(400); + expect((await PATCH(patchReq({ autoMonitorFuture: true }), ctx("nope"))).status).toBe(404); + }); + + it("unfollows and cascades releases", async () => { + const a = await seedArtist(); + const res = await DELETE(new Request("http://localhost/x"), ctx(a.id)); + expect(res.status).toBe(204); + expect(await prisma.watchedArtist.findUnique({ where: { id: a.id } })).toBeNull(); + expect(await prisma.monitoredRelease.count({ where: { rgMbid: "rg1" } })).toBe(0); + }); +}); diff --git a/web/src/app/api/artists/[id]/route.ts b/web/src/app/api/artists/[id]/route.ts new file mode 100644 index 0000000..a7f9226 --- /dev/null +++ b/web/src/app/api/artists/[id]/route.ts @@ -0,0 +1,57 @@ +import { prisma } from "@/lib/db"; + +export async function GET(_request: Request, { params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + const artist = await prisma.watchedArtist.findUnique({ + where: { id }, + include: { releases: { orderBy: [{ firstReleaseDate: "desc" }, { album: "asc" }] } }, + }); + if (!artist) return Response.json({ error: "not found" }, { status: 404 }); + return Response.json({ + id: artist.id, + mbid: artist.mbid, + name: artist.name, + autoMonitorFuture: artist.autoMonitorFuture, + releases: artist.releases.map((r) => ({ + id: r.id, + rgMbid: r.rgMbid, + album: r.album, + primaryType: r.primaryType, + secondaryTypes: r.secondaryTypes, + firstReleaseDate: r.firstReleaseDate, + monitored: r.monitored, + state: r.state, + currentQualityClass: r.currentQualityClass, + })), + }); +} + +export async function PATCH(request: Request, { params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + let body: unknown; + try { + body = await request.json(); + } catch { + return Response.json({ error: "invalid JSON" }, { status: 400 }); + } + const { autoMonitorFuture } = (body ?? {}) as { autoMonitorFuture?: unknown }; + if (typeof autoMonitorFuture !== "boolean") { + return Response.json({ error: "autoMonitorFuture (boolean) is required" }, { status: 400 }); + } + try { + const updated = await prisma.watchedArtist.update({ where: { id }, data: { autoMonitorFuture } }); + return Response.json({ id: updated.id, autoMonitorFuture: updated.autoMonitorFuture }); + } catch { + return Response.json({ error: "not found" }, { status: 404 }); + } +} + +export async function DELETE(_request: Request, { params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + try { + await prisma.watchedArtist.delete({ where: { id } }); + return new Response(null, { status: 204 }); + } catch { + return Response.json({ error: "not found" }, { status: 404 }); + } +}