import type { ActionFunctionArgs } from "react-router"; import { createArtist, createBand, getArtistBySlug, getBandBySlug, getIpAddress, toSlug, type MemberInput, } from "~/lib/db.server"; interface LinkInput { label: string; url: string; } interface ArtistImport { name: string; links?: LinkInput[]; } interface MemberImport { name: string; role?: string | null; since?: string; until?: string; note?: string; } interface BandImport { name: string; slug?: string; area?: string | null; description?: string | null; status?: string; links?: LinkInput[]; members?: MemberImport[]; message?: string; } interface ImportPayload { artists?: ArtistImport[]; bands: BandImport[]; } export async function action({ request }: ActionFunctionArgs) { if (request.method !== "POST") { return Response.json({ error: "Method not allowed" }, { status: 405 }); } let payload: ImportPayload; try { payload = await request.json(); } catch { return Response.json({ error: "Invalid JSON body" }, { status: 400 }); } if (!payload || !Array.isArray(payload.bands) || payload.bands.length === 0) { return Response.json({ error: "`bands` must be a non-empty array" }, { status: 400 }); } const ip = getIpAddress(request); const artistIdByName = new Map(); const artistsCreated: { id: string; name: string }[] = []; const artistsReused: { id: string; name: string }[] = []; const bandsCreated: { id: string; name: string; slug: string }[] = []; const bandsSkipped: { name: string; reason: string }[] = []; const errors: string[] = []; // resolves (creating if necessary) an artist by name; reuses an existing // artist when its derived slug already exists so re-running an import is idempotent function resolveArtist(name: string, links: LinkInput[] = []): string | null { const cached = artistIdByName.get(name); if (cached) return cached; const baseSlug = toSlug(name); const existing = baseSlug ? getArtistBySlug(baseSlug) : null; if (existing) { artistIdByName.set(name, existing.id); artistsReused.push({ id: existing.id, name: existing.name }); return existing.id; } const fallbackBase = baseSlug || crypto.randomUUID().slice(0, 8); for (let attempt = 0; attempt < 3; attempt++) { const id = crypto.randomUUID(); const slug = attempt === 0 ? fallbackBase : `${fallbackBase}-${id.slice(0, 6)}`; try { createArtist({ id, slug, name, links, message: "JSONインポートで自動作成", ip_address: ip }); artistIdByName.set(name, id); artistsCreated.push({ id, name }); return id; } catch (e) { if (e instanceof Error && e.message.includes("UNIQUE constraint failed")) continue; errors.push(`アーティスト「${name}」の作成に失敗: ${e instanceof Error ? e.message : "不明なエラー"}`); return null; } } errors.push(`アーティスト「${name}」の作成に失敗: slugの衝突が解消しませんでした`); return null; } for (const a of payload.artists ?? []) { if (a?.name) resolveArtist(a.name, a.links ?? []); } for (const b of payload.bands) { if (!b?.name) { errors.push("バンド名が空のエントリをスキップしました"); continue; } const baseSlug = b.slug ? toSlug(b.slug) : toSlug(b.name); if (baseSlug && getBandBySlug(baseSlug)) { bandsSkipped.push({ name: b.name, reason: `slug "${baseSlug}" は既に使用されています` }); continue; } const members: MemberInput[] = []; for (const m of b.members ?? []) { if (!m?.name) continue; const artistId = resolveArtist(m.name); if (!artistId) continue; members.push({ artist_id: artistId, role: m.role ?? null, since: m.since ?? "", until: m.until ?? "", note: m.note ?? "", }); } const fallbackBase = baseSlug || crypto.randomUUID().slice(0, 8); let stop = false; for (let attempt = 0; attempt < 3 && !stop; attempt++) { const id = crypto.randomUUID(); const slug = attempt === 0 ? fallbackBase : `${fallbackBase}-${id.slice(0, 6)}`; try { createBand({ id, slug, name: b.name, area: b.area ?? null, description: b.description ?? null, status: b.status ?? "active", links: b.links ?? [], members, message: b.message ?? "JSONインポートで自動登録", ip_address: ip, }); bandsCreated.push({ id, name: b.name, slug }); stop = true; } catch (e) { if (e instanceof Error && e.message.includes("UNIQUE constraint failed")) continue; errors.push(`バンド「${b.name}」の作成に失敗: ${e instanceof Error ? e.message : "不明なエラー"}`); stop = true; } } } return Response.json({ ok: errors.length === 0, artistsCreated, artistsReused, bandsCreated, bandsSkipped, errors, }); } export default function () { return null; }