Files
buzz-sheet/app/admin/actions.ts
T
gunshiz 5aa0ae80c1
CI / Verify (push) Successful in 1m28s
CI / Build immutable images and deploy (push) Successful in 2m5s
feat : update
2026-09-05 18:15:06 +07:00

340 lines
11 KiB
TypeScript

"use server";
import { updateTag } from "next/cache";
import * as z from "zod";
import type { AutosaveResponse } from "@/lib/editor/autosave";
import { GLOSSARY_CACHE_TAG, pageCacheTag } from "@/lib/cache/tags";
import {
createGlossaryAlias,
deleteGlossaryAlias,
} from "@/lib/glossary-repository";
import {
createNamedRevision,
saveDataSourceVersion,
savePageDraft,
} from "@/lib/content/mutations";
import { dataSourceSnapshotSchema } from "@/lib/content/data-source";
import { slugSchema } from "@/lib/content/lifecycle";
import type { DataSourceSnapshot, PageSnapshot } from "@/lib/content/types";
import { requireAdmin } from "@/lib/auth/server";
import { builtInTemplates } from "@/lib/templates";
import { persistTemplateApplication } from "@/lib/templates/persist";
import {
createGuide,
GuideVersionConflictError,
permanentlyDeleteGuide,
restoreGuide,
runCatalogSync,
saveArtifacts,
saveConstellations,
saveExtraSections,
saveTeams,
setGuideVisibility,
saveWeapons,
trashGuide,
updateGuideBasics,
} from "@/lib/guides/mutations";
const serializedBlockSchema = z.strictObject({
id: z.string().uuid(),
type: z.string().min(1).max(64),
schemaVersion: z.number().int().nonnegative(),
config: z.unknown(),
responsiveLayout: z.unknown().optional(),
sortOrder: z.number().int().nonnegative(),
});
const editorDraftSchema = z.strictObject({
pageId: z.string().uuid(),
expectedVersion: z.number().int().positive(),
title: z.string().max(300),
slug: z.string().max(120),
visible: z.boolean(),
publicNote: z.string().max(10_000).nullable().optional(),
blocks: z.array(serializedBlockSchema).max(500),
});
export type EditorSaveInput = z.input<typeof editorDraftSchema>;
export type EditorSaveResult = AutosaveResponse<PageSnapshot>;
export async function saveEditorPage(input: unknown): Promise<EditorSaveResult> {
const admin = await requireAdmin();
const parsed = editorDraftSchema.safeParse(input);
if (!parsed.success) {
return {
status: "validation-error",
issues: parsed.error.issues.map((issue) => issue.message),
};
}
if (process.env.BUZZ_DEMO_MODE === "true") {
return { status: "saved", version: parsed.data.expectedVersion + 1 };
}
try {
const result = await savePageDraft({
...parsed.data,
authorId: admin.user.id,
});
if (result.status === "accepted") {
return {
status: "saved",
version: result.value.page.version,
snapshot: result.value,
};
}
if (result.status === "not-found") {
return {
status: "validation-error",
issues: ["ไม่พบหน้าที่ต้องการบันทึก"],
};
}
return result;
} catch (cause) {
return {
status: "transient-error",
message: cause instanceof Error ? cause.message : "บันทึกไม่สำเร็จ",
};
}
}
export async function saveEditorDataSource(
input: unknown,
): Promise<AutosaveResponse<DataSourceSnapshot>> {
const admin = await requireAdmin();
const parsed = dataSourceSnapshotSchema.safeParse(input);
if (!parsed.success) {
return {
status: "validation-error",
issues: parsed.error.issues.map((issue) => issue.message),
};
}
if (process.env.BUZZ_DEMO_MODE === "true") {
return {
status: "saved",
version: parsed.data.version + 1,
snapshot: { ...parsed.data, version: parsed.data.version + 1 },
};
}
try {
const result = await saveDataSourceVersion({
...parsed.data,
expectedVersion: parsed.data.version,
authorId: admin.user.id,
});
if (result.status === "accepted") {
return {
status: "saved",
version: result.value.version,
snapshot: result.value,
};
}
if (result.status === "not-found") {
return { status: "validation-error", issues: ["ไม่พบแหล่งข้อมูล"] };
}
return result;
} catch (cause) {
return {
status: "transient-error",
message: cause instanceof Error ? cause.message : "บันทึกข้อมูลไม่สำเร็จ",
};
}
}
export async function createEditorRevision(
pageId: string,
label: string,
): Promise<{ status: "saved"; id: string } | { status: "error" }> {
const admin = await requireAdmin();
const parsed = z.string().uuid().safeParse(pageId);
const safeLabel = z.string().trim().min(1).max(160).safeParse(label);
if (!parsed.success || !safeLabel.success) return { status: "error" };
if (process.env.BUZZ_DEMO_MODE === "true") {
return { status: "saved", id: crypto.randomUUID() };
}
const id = await createNamedRevision(parsed.data, safeLabel.data, admin.user.id);
return id ? { status: "saved", id } : { status: "error" };
}
const templateApplicationSchema = z.strictObject({
templateSlug: z.string().min(1).max(120),
characterSlug: slugSchema,
nameTh: z.string().trim().min(1).max(160),
nameEn: z.string().trim().max(160).optional(),
description: z.string().trim().max(2_000).optional(),
});
export async function applyEditorTemplate(input: unknown): Promise<
| { status: "created"; location: string }
| { status: "validation-error"; issues: string[] }
| { status: "error"; message: string }
> {
const admin = await requireAdmin();
const parsed = templateApplicationSchema.safeParse(input);
if (!parsed.success) {
return {
status: "validation-error",
issues: parsed.error.issues.map((issue) => issue.message),
};
}
const template = builtInTemplates.find(
(candidate) => candidate.slug === parsed.data.templateSlug,
);
if (!template) {
return { status: "validation-error", issues: ["ไม่พบเทมเพลต"] };
}
if (process.env.BUZZ_DEMO_MODE === "true") {
return { status: "created", location: "/admin/astra/info" };
}
try {
const applied = await persistTemplateApplication(
template,
{
slug: parsed.data.characterSlug,
nameTh: parsed.data.nameTh,
nameEn: parsed.data.nameEn,
description: parsed.data.description,
},
admin.user.id,
);
return {
status: "created",
location: `/admin/${applied.characterSlug}/${applied.firstPageSlug}`,
};
} catch (cause) {
return {
status: "error",
message: cause instanceof Error ? cause.message : "สร้างคู่มือไม่สำเร็จ",
};
}
}
type GuideActionResult =
| { status: "saved"; version?: number; location?: string }
| { status: "conflict"; currentVersion: number; message: string }
| { status: "error"; message: string };
type CreateGuideActionResult = Exclude<GuideActionResult, { status: "conflict" }>;
async function runGuideAction(action: () => Promise<{ version?: number }>): Promise<GuideActionResult> {
try {
const result = await action();
return { status: "saved", version: result.version };
} catch (cause) {
if (cause instanceof GuideVersionConflictError) {
return { status: "conflict", currentVersion: cause.currentVersion, message: cause.message };
}
if (cause instanceof z.ZodError) {
return { status: "error", message: cause.issues.map((issue) => issue.message).join(" • ") };
}
return { status: "error", message: cause instanceof Error ? cause.message : "บันทึกไม่สำเร็จ" };
}
}
export async function createStructuredGuide(input: unknown): Promise<CreateGuideActionResult> {
await requireAdmin();
try {
const guide = await createGuide(input);
return { status: "saved", version: guide.version, location: `/admin/${guide.characterKey}` };
} catch (cause) {
return { status: "error", message: cause instanceof Error ? cause.message : "สร้าง Guide ไม่สำเร็จ" };
}
}
export async function saveWeaponSection(guideId: string, input: unknown) {
await requireAdmin();
return runGuideAction(() => saveWeapons(guideId, input));
}
export async function saveGuideOverview(guideId: string, input: unknown) {
await requireAdmin();
return runGuideAction(() => updateGuideBasics(guideId, input));
}
export async function saveArtifactSection(guideId: string, input: unknown) {
await requireAdmin();
return runGuideAction(() => saveArtifacts(guideId, input));
}
export async function saveConstellationSection(guideId: string, input: unknown) {
await requireAdmin();
return runGuideAction(() => saveConstellations(guideId, input));
}
export async function saveTeamSection(guideId: string, input: unknown) {
await requireAdmin();
return runGuideAction(() => saveTeams(guideId, input));
}
export async function saveExtraSectionList(guideId: string, input: unknown) {
await requireAdmin();
const result = await runGuideAction(() => saveExtraSections(guideId, input));
if (result.status === "saved") updateTag(pageCacheTag(guideId));
return result;
}
export async function moveGuideToTrash(guideId: string) {
await requireAdmin();
return runGuideAction(() => trashGuide(guideId));
}
export async function restoreStructuredGuide(guideId: string) {
await requireAdmin();
return runGuideAction(() => restoreGuide(guideId));
}
export async function setStructuredGuideVisibility(
guideId: string,
isPublic: boolean,
) {
await requireAdmin();
return runGuideAction(() => setGuideVisibility(guideId, isPublic));
}
export async function deleteStructuredGuide(guideId: string) {
await requireAdmin();
return runGuideAction(() => permanentlyDeleteGuide(guideId));
}
export async function startCatalogSync() {
await requireAdmin();
try {
return await runCatalogSync();
} catch (cause) {
return { status: "error" as const, message: cause instanceof Error ? cause.message : "เริ่ม Sync ไม่สำเร็จ" };
}
}
export async function addGlossaryAlias(input: unknown) {
await requireAdmin();
try {
const alias = await createGlossaryAlias(input);
updateTag(GLOSSARY_CACHE_TAG);
return { status: "saved" as const, alias };
} catch (cause) {
const message = cause instanceof Error && cause.message.includes("unique")
? "Shortcut นี้ถูกใช้งานแล้ว"
: cause instanceof z.ZodError
? cause.issues.map((issue) => issue.message).join(" • ")
: cause instanceof Error
? cause.message
: "เพิ่ม Shortcut ไม่สำเร็จ";
return { status: "error" as const, message };
}
}
export async function removeGlossaryAlias(id: string) {
await requireAdmin();
try {
await deleteGlossaryAlias(z.string().uuid().parse(id));
updateTag(GLOSSARY_CACHE_TAG);
return { status: "saved" as const };
} catch (cause) {
return {
status: "error" as const,
message: cause instanceof Error ? cause.message : "ลบ Shortcut ไม่สำเร็จ",
};
}
}