summaryrefslogtreecommitdiff
path: root/app/routes
diff options
context:
space:
mode:
authoryyamashita <yyamashita@mosquit.one>2026-05-09 11:21:28 +0900
committeryyamashita <yyamashita@mosquit.one>2026-05-09 11:21:28 +0900
commitcd8787b77dadf752826a967d404b718b3ec92601 (patch)
treea1e5471ba59404caf5c3c7684a4cfc08027a5a4b /app/routes
parent08c410c28eeb3d7a4c41014d8926b765441546c4 (diff)
Add JSON API endpoints and CLI script for band/artist management
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'app/routes')
-rw-r--r--app/routes/api-artists.tsx47
-rw-r--r--app/routes/api-bands.tsx51
2 files changed, 98 insertions, 0 deletions
diff --git a/app/routes/api-artists.tsx b/app/routes/api-artists.tsx
new file mode 100644
index 0000000..ed762bf
--- /dev/null
+++ b/app/routes/api-artists.tsx
@@ -0,0 +1,47 @@
+import type { ActionFunctionArgs } from "react-router";
+import { createArtist, getIpAddress, listArtists, toSlug } from "~/lib/db.server";
+
+export function loader() {
+ return Response.json(listArtists());
+}
+
+export async function action({ request }: ActionFunctionArgs) {
+ if (request.method !== "POST") {
+ return Response.json({ error: "Method not allowed" }, { status: 405 });
+ }
+
+ let body: Record<string, unknown>;
+ try {
+ body = await request.json();
+ } catch {
+ return Response.json({ error: "Invalid JSON body" }, { status: 400 });
+ }
+
+ const name = (body.name as string | undefined)?.trim();
+ if (!name) return Response.json({ error: "name is required" }, { status: 400 });
+
+ const slug = (body.slug as string | undefined)?.trim() || toSlug(name);
+ if (!slug) return Response.json({ error: "could not derive slug from name" }, { status: 400 });
+
+ const id = crypto.randomUUID();
+ try {
+ const artist = createArtist({
+ id,
+ slug,
+ name,
+ links: (body.links as { label: string; url: string }[]) || [],
+ message: (body.message as string) || "API import",
+ ip_address: getIpAddress(request),
+ });
+ return Response.json(artist, { status: 201 });
+ } catch (e) {
+ if (e instanceof Error && e.message.includes("UNIQUE constraint failed: artists.slug")) {
+ return Response.json({ error: "slug already in use" }, { status: 409 });
+ }
+ throw e;
+ }
+}
+
+export default function () {
+ return null;
+}
diff --git a/app/routes/api-bands.tsx b/app/routes/api-bands.tsx
new file mode 100644
index 0000000..64a9269
--- /dev/null
+++ b/app/routes/api-bands.tsx
@@ -0,0 +1,51 @@
+import type { ActionFunctionArgs } from "react-router";
+import { createBand, getIpAddress, listBands, toSlug } from "~/lib/db.server";
+
+export function loader() {
+ return Response.json(listBands());
+}
+
+export async function action({ request }: ActionFunctionArgs) {
+ if (request.method !== "POST") {
+ return Response.json({ error: "Method not allowed" }, { status: 405 });
+ }
+
+ let body: Record<string, unknown>;
+ try {
+ body = await request.json();
+ } catch {
+ return Response.json({ error: "Invalid JSON body" }, { status: 400 });
+ }
+
+ const name = (body.name as string | undefined)?.trim();
+ if (!name) return Response.json({ error: "name is required" }, { status: 400 });
+
+ const slug = (body.slug as string | undefined)?.trim() || toSlug(name);
+ if (!slug) return Response.json({ error: "could not derive slug from name" }, { status: 400 });
+
+ const id = crypto.randomUUID();
+ try {
+ const band = createBand({
+ id,
+ slug,
+ name,
+ area: (body.area as string) || null,
+ description: (body.description as string) || null,
+ status: (body.status as string) || "active",
+ links: (body.links as { label: string; url: string }[]) || [],
+ artists: (body.artists as { id: string; role: string | null }[]) || [],
+ message: (body.message as string) || "API import",
+ ip_address: getIpAddress(request),
+ });
+ return Response.json(band, { status: 201 });
+ } catch (e) {
+ if (e instanceof Error && e.message.includes("UNIQUE constraint failed: bands.slug")) {
+ return Response.json({ error: "slug already in use" }, { status: 409 });
+ }
+ throw e;
+ }
+}
+
+export default function () {
+ return null;
+}