"use server"; import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; import { rawDb, uniqueSlug } from "@/lib/db/client"; type EntityTable = "actresses" | "studios" | "series"; function createEntity(table: EntityTable, name: string): { id: number; slug: string } | null { const trimmed = name.trim(); if (!trimmed) return null; const existing = rawDb.prepare(`SELECT id, slug FROM ${table} WHERE name = ?`).get(trimmed) as | { id: number; slug: string } | undefined; if (existing) return existing; const slug = uniqueSlug(rawDb, table, trimmed); const row = rawDb.prepare(`INSERT INTO ${table} (name, slug) VALUES (?, ?) RETURNING id`).get(trimmed, slug) as { id: number; }; return { id: row.id, slug }; } export async function createActressAction(formData: FormData) { createEntity("actresses", String(formData.get("name") ?? "")); revalidatePath("/actress"); } export async function createStudioAction(formData: FormData) { const created = createEntity("studios", String(formData.get("name") ?? "")); revalidatePath("/studios"); if (created) redirect(`/studios/${created.slug}`); } export async function createSeriesAction(formData: FormData) { const created = createEntity("series", String(formData.get("name") ?? "")); revalidatePath("/series"); if (created) redirect(`/series/${created.slug}`); } /** Deletes a studio. Any covers referencing it have studio_id set to NULL. */ export async function deleteStudio(id: number) { rawDb.prepare(`UPDATE images SET studio_id = NULL WHERE studio_id = ?`).run(id); rawDb.prepare(`DELETE FROM studios WHERE id = ?`).run(id); revalidatePath("/studios"); revalidatePath("/"); redirect("/studios"); } /** Deletes a series. Any covers referencing it have series_id set to NULL. */ export async function deleteSeries(id: number) { rawDb.prepare(`UPDATE images SET series_id = NULL WHERE series_id = ?`).run(id); rawDb.prepare(`DELETE FROM series WHERE id = ?`).run(id); revalidatePath("/series"); revalidatePath("/"); redirect("/series"); } /** Rename a studio. Returns the new slug (which may be re-uniquified) or null. */ export async function renameStudio(id: number, name: string): Promise<{ slug: string } | null> { const trimmed = name.trim(); if (!trimmed) return null; const row = rawDb.prepare(`SELECT name, slug FROM studios WHERE id = ?`).get(id) as { name: string; slug: string } | undefined; if (!row) return null; if (trimmed === row.name) return { slug: row.slug }; const slug = uniqueSlug(rawDb, "studios", trimmed, id); rawDb.prepare(`UPDATE studios SET name = ?, slug = ? WHERE id = ?`).run(trimmed, slug, id); revalidatePath("/studios"); revalidatePath(`/studios/${row.slug}`); if (slug !== row.slug) revalidatePath(`/studios/${slug}`); return { slug }; } /** Rename a series. Returns the new slug or null. */ export async function renameSeries(id: number, name: string): Promise<{ slug: string } | null> { const trimmed = name.trim(); if (!trimmed) return null; const row = rawDb.prepare(`SELECT name, slug FROM series WHERE id = ?`).get(id) as { name: string; slug: string } | undefined; if (!row) return null; if (trimmed === row.name) return { slug: row.slug }; const slug = uniqueSlug(rawDb, "series", trimmed, id); rawDb.prepare(`UPDATE series SET name = ?, slug = ? WHERE id = ?`).run(trimmed, slug, id); revalidatePath("/series"); revalidatePath(`/series/${row.slug}`); if (slug !== row.slug) revalidatePath(`/series/${slug}`); return { slug }; } /** Rename a tag (keyed by name). Returns the new name or null. */ export async function renameTag(oldName: string, newName: string): Promise<{ name: string } | null> { // Tags are stored lowercased (see tags.ts createTag/addTagToImage). // Lookup by exact match must use the same casing or it silently misses. const oldTrim = oldName.trim().toLowerCase(); const newTrim = newName.trim().toLowerCase(); if (!oldTrim || !newTrim) return null; if (oldTrim === newTrim) return { name: oldTrim }; const row = rawDb.prepare(`SELECT id FROM tags WHERE name = ?`).get(oldTrim) as { id: number } | undefined; if (!row) return null; // Wrap merge in a transaction so a concurrent INSERT into image_tags // for the old tag_id can't slip between the UPDATE re-point and the // DELETE — both run atomically against a consistent snapshot. const merge = rawDb.transaction(() => { const conflict = rawDb.prepare(`SELECT id FROM tags WHERE name = ?`).get(newTrim) as { id: number } | undefined; if (conflict && conflict.id !== row.id) { rawDb.prepare(`UPDATE OR IGNORE image_tags SET tag_id = ? WHERE tag_id = ?`).run(conflict.id, row.id); rawDb.prepare(`DELETE FROM image_tags WHERE tag_id = ?`).run(row.id); rawDb.prepare(`DELETE FROM tags WHERE id = ?`).run(row.id); } else { rawDb.prepare(`UPDATE tags SET name = ? WHERE id = ?`).run(newTrim, row.id); } }); merge(); revalidatePath("/tag"); revalidatePath(`/tag/${encodeURIComponent(oldTrim)}`); revalidatePath(`/tag/${encodeURIComponent(newTrim)}`); return { name: newTrim }; } /** Deletes a tag. Any image_tags rows referencing it are removed. */ export async function deleteTag(name: string) { const trimmed = name.trim(); if (!trimmed) return; const row = rawDb.prepare(`SELECT id FROM tags WHERE name = ?`).get(trimmed) as { id: number } | undefined; if (!row) return; rawDb.prepare(`DELETE FROM image_tags WHERE tag_id = ?`).run(row.id); rawDb.prepare(`DELETE FROM tags WHERE id = ?`).run(row.id); revalidatePath("/tag"); revalidatePath("/"); redirect("/tag"); }