129 lines
5.5 KiB
TypeScript
129 lines
5.5 KiB
TypeScript
"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");
|
|
}
|