feat(cms): improve catalog, editorial recovery and operations
This commit is contained in:
1 parent
2fead134e6
commit
c389c3893d
122 files changed
+8137
-703
No files matched your search
@@ -4,7 +4,9 @@ import { and, eq, ne } from "drizzle-orm";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { ArticleDrafts, ArticleRevisions } from "@/db/article-editor";
|
||||
import { requirePermission } from "@/lib/admin/guard";
|
||||
import { articleEditToken } from "@/lib/article-edit-token";
|
||||
import {
|
||||
ArticleInputError,
|
||||
type ArticleSaveResult,
|
||||
@@ -23,11 +25,15 @@ import { PERMS } from "@/lib/permissions";
|
||||
import { invalidateNewsCache } from "@/lib/services/news-cache";
|
||||
import { notify } from "@/lib/services/webhook";
|
||||
|
||||
async function uniqueSlug(title: string, excludeId?: bigint): Promise<string> {
|
||||
async function uniqueSlug(
|
||||
title: string,
|
||||
excludeId?: bigint,
|
||||
connection: Pick<typeof db, "select"> = db,
|
||||
): Promise<string> {
|
||||
const base = slugify(title);
|
||||
let slug = base;
|
||||
for (let n = 2; ; n++) {
|
||||
const [existing] = await db
|
||||
const [existing] = await connection
|
||||
.select({ id: WebsiteArticles.id })
|
||||
.from(WebsiteArticles)
|
||||
.where(
|
||||
@@ -105,39 +111,54 @@ export async function updateArticle(
|
||||
if (!id) return { ok: false, error: t("articleNotFound") };
|
||||
let input: ReturnType<typeof readArticleInput>;
|
||||
let becamePublished = false;
|
||||
let slug: string;
|
||||
let slug = "";
|
||||
try {
|
||||
input = readArticleInput(formData);
|
||||
const [existing] = await db
|
||||
.select({
|
||||
slug: WebsiteArticles.slug,
|
||||
status: WebsiteArticles.status,
|
||||
publishedAt: WebsiteArticles.publishedAt,
|
||||
})
|
||||
.from(WebsiteArticles)
|
||||
.where(eq(WebsiteArticles.id, id))
|
||||
.limit(1);
|
||||
if (!existing) return { ok: false, error: t("articleNotFound") };
|
||||
const { rawSlug, ...fields } = input;
|
||||
const requestedSlug = rawSlug ? slugify(rawSlug) : existing.slug;
|
||||
slug =
|
||||
requestedSlug === existing.slug
|
||||
? existing.slug
|
||||
: await uniqueSlug(requestedSlug, id);
|
||||
becamePublished =
|
||||
fields.status === "published" && existing.status !== "published";
|
||||
await db
|
||||
.update(WebsiteArticles)
|
||||
.set({
|
||||
...fields,
|
||||
slug,
|
||||
publishedAt:
|
||||
fields.status === "published"
|
||||
? (existing.publishedAt ?? new Date())
|
||||
: null,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(WebsiteArticles.id, id));
|
||||
await db.transaction(async (tx) => {
|
||||
const [existing] = await tx
|
||||
.select()
|
||||
.from(WebsiteArticles)
|
||||
.where(eq(WebsiteArticles.id, id))
|
||||
.limit(1)
|
||||
.for("update");
|
||||
if (!existing) throw new ArticleInputError("articleNotFound");
|
||||
if (formData.get("baseToken") !== articleEditToken(existing))
|
||||
throw new ArticleInputError("editConflict");
|
||||
await tx.insert(ArticleRevisions).values({
|
||||
articleId: id,
|
||||
userId: staff.id,
|
||||
payload: JSON.stringify({
|
||||
title: existing.title,
|
||||
slug: existing.slug,
|
||||
image: existing.image,
|
||||
shortStory: existing.shortStory,
|
||||
fullStory: existing.fullStory,
|
||||
status: existing.status,
|
||||
publishAt: existing.publishAt?.toISOString() ?? "",
|
||||
baseToken: "",
|
||||
}),
|
||||
});
|
||||
const { rawSlug, ...fields } = input;
|
||||
const requestedSlug = rawSlug ? slugify(rawSlug) : existing.slug;
|
||||
slug =
|
||||
requestedSlug === existing.slug
|
||||
? existing.slug
|
||||
: await uniqueSlug(requestedSlug, id, tx);
|
||||
becamePublished =
|
||||
fields.status === "published" && existing.status !== "published";
|
||||
await tx
|
||||
.update(WebsiteArticles)
|
||||
.set({
|
||||
...fields,
|
||||
slug,
|
||||
publishedAt:
|
||||
fields.status === "published"
|
||||
? (existing.publishedAt ?? new Date())
|
||||
: null,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(WebsiteArticles.id, id));
|
||||
});
|
||||
} catch (error) {
|
||||
return saveFailure(error, "update");
|
||||
}
|
||||
@@ -170,6 +191,12 @@ export async function deleteArticle(formData: FormData): Promise<void> {
|
||||
await tx
|
||||
.delete(WebsiteArticleComments)
|
||||
.where(eq(WebsiteArticleComments.articleId, id));
|
||||
await tx
|
||||
.delete(ArticleRevisions)
|
||||
.where(eq(ArticleRevisions.articleId, id));
|
||||
await tx
|
||||
.delete(ArticleDrafts)
|
||||
.where(eq(ArticleDrafts.articleKey, String(id)));
|
||||
await tx.delete(WebsiteArticles).where(eq(WebsiteArticles.id, id));
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user