feat(housekeeping): deliver content vertical

This commit is contained in:
Simo committed 2026-08-30 01:54:43 +02:00
1 parent bdab924bdf
commit fd68819d9b
67 files changed
+5742 -1916

No files matched your search

@@ -0,0 +1,607 @@
import "server-only";
import { eq, sql } from "drizzle-orm";
import type { ResultSetHeader } from "mysql2";
import {
db,
EmailTemplates,
Taggables,
Tags,
User,
WebsiteAds,
WebsiteArticleComments,
WebsiteArticleReactions,
WebsiteArticles,
WebsiteBanner,
WebsiteEvent,
WebsiteEventPrize,
WebsiteEventType,
WebsiteEventWinner,
WebsiteHelpCenterCategories,
WebsitePoll,
WebsitePollQuestion,
WebsiteWriteableBoxes,
} from "@/lib/db";
import { slugify } from "@/lib/format";
import { canonicalize } from "@/lib/foundation/security";
import { logStaffActivity } from "@/lib/services/staff-activity";
import {
createEventSchema,
eventPrizeSchema,
eventTypeSchema,
eventWinnerSchema,
updateEventSchema,
} from "@/lib/validators/event";
import {
createPollSchema,
pollQuestionSchema,
updatePollSchema,
} from "@/lib/validators/poll";
import {
type ContentMutationContext,
ContentMutationFailure,
type ContentMutationOperation,
type ContentMutationSnapshot,
} from "./mutations";
type ContentDatabase = typeof db;
function database(transaction: unknown): ContentDatabase {
return (transaction ?? db) as ContentDatabase;
}
function record(value: unknown): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw validation();
}
return value as Record<string, unknown>;
}
function validation(): ContentMutationFailure {
return new ContentMutationFailure(
"VALIDATION",
"errors.housekeeping.validation",
);
}
function notFound(): ContentMutationFailure {
return new ContentMutationFailure("NOT_FOUND", "errors.housekeeping.notFound");
}
function text(value: unknown, maximum: number, required = false): string {
const normalized = String(value ?? "").normalize("NFC").trim().slice(0, maximum);
if (required && !normalized) throw validation();
return normalized;
}
function rawText(value: unknown, maximum = 100_000): string {
return String(value ?? "").normalize("NFC").slice(0, maximum);
}
function positiveBigInt(value: unknown): bigint {
const normalized = typeof value === "bigint" ? value.toString() : String(value ?? "").trim();
if (!/^[1-9]\d*$/u.test(normalized)) throw validation();
return BigInt(normalized);
}
function positiveInteger(value: unknown): number {
const parsed = Number(value);
if (!Number.isSafeInteger(parsed) || parsed <= 0) throw validation();
return parsed;
}
function nonNegativeInteger(value: unknown, fallback = 0): number {
const parsed = Number(value);
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : fallback;
}
function insertedId(result: unknown): string {
const value = (result as ResultSetHeader | undefined)?.insertId;
return positiveBigInt(value).toString();
}
async function activity(
context: ContentMutationContext,
action: string,
description: string,
targetType?: string,
targetId?: number,
): Promise<void> {
await logStaffActivity({
staffId: context.capability.actor.id,
action,
description,
...(targetType ? { targetType } : {}),
...(targetId === undefined ? {} : { targetId }),
});
}
async function uniqueSlug(
databaseConnection: ContentDatabase,
value: string,
): Promise<string> {
const base = slugify(value) || "article";
let candidate = base;
let suffix = 2;
for (;;) {
const [existing] = await databaseConnection
.select({ id: WebsiteArticles.id })
.from(WebsiteArticles)
.where(eq(WebsiteArticles.slug, candidate))
.limit(1);
if (!existing) return candidate;
candidate = base + "-" + suffix;
suffix += 1;
}
}
async function articleChange(
input: unknown,
context: ContentMutationContext,
transaction: unknown,
): Promise<ContentMutationSnapshot> {
const data = record(input);
const action = text(data.action, 16, true);
const connection = database(transaction);
if (action === "create") {
const title = text(data.title, 255, true);
const now = new Date();
const rawSlug = text(data.slug, 255);
const slug = await uniqueSlug(connection, rawSlug || title);
const [result] = (await connection.insert(WebsiteArticles).values({
slug,
title,
shortStory: text(data.shortStory ?? data.summary, 255),
fullStory: rawText(data.fullStory ?? data.content),
image: text(data.image, 255),
userId: context.capability.actor.id,
createdAt: now,
updatedAt: now,
})) as unknown as [ResultSetHeader];
const id = insertedId(result);
return { before: null, after: { id, title, slug }, output: { id } };
}
const id = positiveBigInt(data.id);
const [existing] = await connection
.select({ id: WebsiteArticles.id, title: WebsiteArticles.title, slug: WebsiteArticles.slug })
.from(WebsiteArticles)
.where(eq(WebsiteArticles.id, id))
.limit(1);
if (!existing) throw notFound();
if (action === "delete") {
await connection.delete(WebsiteArticleReactions).where(eq(WebsiteArticleReactions.articleId, id));
await connection.delete(WebsiteArticleComments).where(eq(WebsiteArticleComments.articleId, id));
await connection.delete(WebsiteArticles).where(eq(WebsiteArticles.id, id));
return { before: { id: id.toString(), title: existing.title, slug: existing.slug }, after: null };
}
if (action !== "update") throw validation();
const title = text(data.title, 255, true);
const rawSlug = text(data.slug, 255);
const slug = rawSlug ? await uniqueSlug(connection, rawSlug) : existing.slug;
await connection.update(WebsiteArticles).set({
title,
slug,
shortStory: text(data.shortStory ?? data.summary, 255),
fullStory: rawText(data.fullStory ?? data.content),
image: text(data.image, 255),
updatedAt: new Date(),
}).where(eq(WebsiteArticles.id, id));
return {
before: { id: id.toString(), title: existing.title, slug: existing.slug },
after: { id: id.toString(), title, slug },
output: { id: id.toString() },
};
}
async function adChange(
input: unknown,
context: ContentMutationContext,
transaction: unknown,
): Promise<ContentMutationSnapshot> {
const data = record(input);
const action = text(data.action, 16, true);
const connection = database(transaction);
if (action === "create") {
const image = text(data.image, 255, true);
const now = new Date();
const [result] = (await connection.insert(WebsiteAds).values({ image, createdAt: now, updatedAt: now })) as unknown as [ResultSetHeader];
const id = insertedId(result);
await activity(context, "ad_create", "Created advertisement #" + id + " (" + image + ")", "website_ad", Number(id));
return { before: null, after: { id, image }, output: { id } };
}
const id = positiveBigInt(data.id);
const [existing] = await connection.select({ id: WebsiteAds.id, image: WebsiteAds.image }).from(WebsiteAds).where(eq(WebsiteAds.id, id)).limit(1);
if (!existing) throw notFound();
if (action === "delete") {
await connection.delete(WebsiteAds).where(eq(WebsiteAds.id, id));
await activity(context, "ad_delete", "Deleted advertisement #" + id, "website_ad", Number(id));
return { before: { id: id.toString(), image: existing.image }, after: null };
}
if (action !== "update") throw validation();
const image = text(data.image, 255, true);
await connection.update(WebsiteAds).set({ image, updatedAt: new Date() }).where(eq(WebsiteAds.id, id));
await activity(context, "ad_update", "Updated advertisement #" + id + " (" + image + ")", "website_ad", Number(id));
return { before: { id: id.toString(), image: existing.image }, after: { id: id.toString(), image }, output: { id: id.toString() } };
}
function bannerValues(data: Record<string, unknown>) {
return {
title: text(data.title, 255, true),
subtitle: text(data.subtitle, 500),
image: text(data.image, 500),
link: text(data.link, 500),
color: text(data.color, 20),
isActive: Math.min(1, nonNegativeInteger(data.isActive, 1)),
sortOrder: nonNegativeInteger(data.sortOrder),
startDate: data.startDate ? text(data.startDate, 50) : null,
endDate: data.endDate ? text(data.endDate, 50) : null,
};
}
async function bannerChange(input: unknown, transaction: unknown): Promise<ContentMutationSnapshot> {
const data = record(input);
const action = text(data.action, 16, true);
const connection = database(transaction);
if (action === "create") {
const values = bannerValues(data);
const [result] = (await connection.insert(WebsiteBanner).values(values)) as unknown as [ResultSetHeader];
const id = insertedId(result);
return { before: null, after: { id, title: values.title }, output: { id } };
}
const id = positiveInteger(data.id);
const [existing] = await connection.select({ id: WebsiteBanner.id, title: WebsiteBanner.title }).from(WebsiteBanner).where(eq(WebsiteBanner.id, id)).limit(1);
if (!existing) throw notFound();
if (action === "delete") {
await connection.delete(WebsiteBanner).where(eq(WebsiteBanner.id, id));
return { before: existing, after: null };
}
if (action !== "update") throw validation();
const values = bannerValues({ ...data, title: data.title ?? existing.title });
await connection.update(WebsiteBanner).set(values).where(eq(WebsiteBanner.id, id));
return { before: existing, after: { id, title: values.title }, output: { id: String(id) } };
}
async function eventTypeChange(input: unknown, transaction: unknown): Promise<ContentMutationSnapshot> {
const data = record(input);
const action = text(data.action, 16, true);
const connection = database(transaction);
if (action === "create") {
const parsed = eventTypeSchema.safeParse(data);
if (!parsed.success) throw validation();
const [result] = await connection.insert(WebsiteEventType).values(parsed.data);
const id = insertedId(result);
return { before: null, after: { id, name: parsed.data.name }, output: { id } };
}
const id = positiveInteger(data.id);
const [existing] = await connection.select({ id: WebsiteEventType.id, name: WebsiteEventType.name }).from(WebsiteEventType).where(eq(WebsiteEventType.id, id)).limit(1);
if (!existing) throw notFound();
if (action === "delete") {
await connection.delete(WebsiteEventType).where(eq(WebsiteEventType.id, id));
return { before: existing, after: null };
}
if (action !== "update") throw validation();
const parsed = eventTypeSchema.partial().safeParse(data);
if (!parsed.success) throw validation();
const { action: _action, id: _id, ...values } = parsed.data as Record<string, unknown>;
await connection.update(WebsiteEventType).set(values).where(eq(WebsiteEventType.id, id));
return { before: existing, after: { id, ...values }, output: { id: String(id) } };
}
async function eventChange(
input: unknown,
context: ContentMutationContext,
transaction: unknown,
): Promise<ContentMutationSnapshot> {
const data = record(input);
const action = text(data.action, 16, true);
const connection = database(transaction);
if (action === "create") {
const parsed = createEventSchema.safeParse(data);
if (!parsed.success) throw validation();
const [result] = await connection.insert(WebsiteEvent).values({ ...parsed.data, hostUserId: context.capability.actor.id, updatedAt: new Date() });
const id = insertedId(result);
return { before: null, after: { id, title: parsed.data.title, status: parsed.data.status }, output: { id } };
}
const id = positiveInteger(data.id);
const [existing] = await connection.select({ id: WebsiteEvent.id, title: WebsiteEvent.title, status: WebsiteEvent.status }).from(WebsiteEvent).where(eq(WebsiteEvent.id, id)).limit(1);
if (!existing) throw notFound();
if (action === "delete") {
await connection.delete(WebsiteEvent).where(eq(WebsiteEvent.id, id));
return { before: existing, after: null };
}
if (action !== "update") throw validation();
const parsed = updateEventSchema.safeParse(data);
if (!parsed.success) throw validation();
const { action: _action, id: _id, ...values } = parsed.data as Record<string, unknown>;
await connection.update(WebsiteEvent).set({ ...values, updatedAt: new Date() }).where(eq(WebsiteEvent.id, id));
return { before: existing, after: { id, ...values }, output: { id: String(id) } };
}
async function eventPrizeChange(input: unknown, transaction: unknown): Promise<ContentMutationSnapshot> {
const data = record(input);
const action = text(data.action, 16, true);
const connection = database(transaction);
if (action === "delete") {
const id = positiveInteger(data.id);
await connection.delete(WebsiteEventPrize).where(eq(WebsiteEventPrize.id, id));
return { before: { id }, after: null };
}
if (action !== "create") throw validation();
const parsed = eventPrizeSchema.safeParse(data);
if (!parsed.success) throw validation();
const [result] = await connection.insert(WebsiteEventPrize).values(parsed.data);
const id = insertedId(result);
return { before: null, after: { id, eventId: parsed.data.eventId, position: parsed.data.position }, output: { id } };
}
async function eventWinnerAdd(input: unknown, transaction: unknown): Promise<ContentMutationSnapshot> {
const parsed = eventWinnerSchema.safeParse(record(input));
if (!parsed.success) throw validation();
const [result] = await database(transaction).insert(WebsiteEventWinner).values(parsed.data);
const id = insertedId(result);
return { before: null, after: { id, eventId: parsed.data.eventId, userId: parsed.data.userId, position: parsed.data.position }, output: { id } };
}
async function pollChange(input: unknown, transaction: unknown): Promise<ContentMutationSnapshot> {
const data = record(input);
const action = text(data.action, 16, true);
const connection = database(transaction);
if (action === "create") {
const parsed = createPollSchema.safeParse(data);
if (!parsed.success) throw validation();
const [result] = await connection.insert(WebsitePoll).values({ ...parsed.data, updatedAt: new Date() });
const id = insertedId(result);
return { before: null, after: { id, title: parsed.data.title, status: parsed.data.status }, output: { id } };
}
const id = positiveInteger(data.id);
const [existing] = await connection.select({ id: WebsitePoll.id, title: WebsitePoll.title, status: WebsitePoll.status }).from(WebsitePoll).where(eq(WebsitePoll.id, id)).limit(1);
if (!existing) throw notFound();
if (action === "delete") {
await connection.delete(WebsitePoll).where(eq(WebsitePoll.id, id));
return { before: existing, after: null };
}
if (action !== "update") throw validation();
const parsed = updatePollSchema.safeParse(data);
if (!parsed.success) throw validation();
const { action: _action, id: _id, ...values } = parsed.data as Record<string, unknown>;
await connection.update(WebsitePoll).set({ ...values, updatedAt: new Date() }).where(eq(WebsitePoll.id, id));
return { before: existing, after: { id, ...values }, output: { id: String(id) } };
}
async function pollQuestionChange(input: unknown, transaction: unknown): Promise<ContentMutationSnapshot> {
const data = record(input);
const action = text(data.action, 16, true);
const connection = database(transaction);
if (action === "create") {
const parsed = pollQuestionSchema.safeParse(data);
if (!parsed.success) throw validation();
const [result] = await connection.insert(WebsitePollQuestion).values(parsed.data);
const id = insertedId(result);
return { before: null, after: { id, pollId: parsed.data.pollId, question: parsed.data.question }, output: { id } };
}
const id = positiveInteger(data.id);
if (action === "delete") {
await connection.delete(WebsitePollQuestion).where(eq(WebsitePollQuestion.id, id));
return { before: { id }, after: null };
}
if (action !== "update") throw validation();
const parsed = pollQuestionSchema.partial().safeParse(data);
if (!parsed.success) throw validation();
const { action: _action, id: _id, ...values } = parsed.data as Record<string, unknown>;
await connection.update(WebsitePollQuestion).set(values).where(eq(WebsitePollQuestion.id, id));
return { before: { id }, after: { id, ...values }, output: { id: String(id) } };
}
async function tagChange(input: unknown, context: ContentMutationContext, transaction: unknown): Promise<ContentMutationSnapshot> {
const data = record(input);
const action = text(data.action, 16, true);
const connection = database(transaction);
if (action === "create") {
const name = text(data.name, 255, true);
const backgroundColor = text(data.backgroundColor, 10) || "#888888";
const now = new Date();
const [result] = (await connection.insert(Tags).values({ name, backgroundColor, createdAt: now, updatedAt: now })) as unknown as [ResultSetHeader];
const id = insertedId(result);
await activity(context, "tag_create", "Created tag " + name + " (#" + id + ")", "tag", Number(id));
return { before: null, after: { id, name, backgroundColor }, output: { id } };
}
const id = positiveBigInt(data.id);
if (action === "delete") {
await connection.delete(Taggables).where(eq(Taggables.tagId, id));
await connection.delete(Tags).where(eq(Tags.id, id));
await activity(context, "tag_delete", "Deleted tag #" + id, "tag", Number(id));
return { before: { id: id.toString() }, after: null };
}
if (action !== "update") throw validation();
const name = text(data.name, 255, true);
const backgroundColor = text(data.backgroundColor, 10) || "#888888";
await connection.update(Tags).set({ name, backgroundColor, updatedAt: new Date() }).where(eq(Tags.id, id));
await activity(context, "tag_update", "Updated tag #" + id + " to " + name, "tag", Number(id));
return { before: { id: id.toString() }, after: { id: id.toString(), name, backgroundColor }, output: { id: id.toString() } };
}
async function prefixChange(input: unknown, transaction: unknown): Promise<ContentMutationSnapshot> {
const data = record(input);
const action = text(data.action, 16, true);
const connection = database(transaction);
if (action === "create") {
const username = text(data.username, 255, true);
const [user] = await connection.select({ id: User.id }).from(User).where(eq(User.username, username)).limit(1);
if (!user) throw notFound();
await connection.execute(sql`INSERT INTO custom_prefixes (user_id, text, color, icon, effect, active) VALUES (${user.id}, ${text(data.text, 255, true)}, ${text(data.color, 32, true)}, ${text(data.icon, 255)}, ${text(data.effect, 255)}, ${Math.min(1, nonNegativeInteger(data.active, 1))})`);
return { before: null, after: { username, text: text(data.text, 255, true) } };
}
const id = positiveInteger(data.id);
if (action === "delete") {
await connection.execute(sql`DELETE FROM custom_prefixes WHERE id = ${id}`);
return { before: { id }, after: null };
}
if (action !== "update") throw validation();
const prefixText = text(data.text, 255, true);
const color = text(data.color, 32, true);
const icon = text(data.icon, 255);
const effect = text(data.effect, 255);
const active = Math.min(1, nonNegativeInteger(data.active, 1));
await connection.execute(sql`UPDATE custom_prefixes SET text = ${prefixText}, color = ${color}, icon = ${icon}, effect = ${effect}, active = ${active} WHERE id = ${id}`);
return { before: { id }, after: { id, text: prefixText, color, icon, effect, active }, output: { id: String(id) } };
}
async function prefixBlacklistChange(input: unknown, transaction: unknown): Promise<ContentMutationSnapshot> {
const data = record(input);
const action = text(data.action, 16, true);
const connection = database(transaction);
if (action === "add") {
const word = text(data.word, 100, true);
await connection.execute(sql`INSERT INTO custom_prefix_blacklist (word) VALUES (${word})`);
return { before: null, after: { word } };
}
if (action !== "remove" && action !== "delete") throw validation();
const id = positiveInteger(data.id);
await connection.execute(sql`DELETE FROM custom_prefix_blacklist WHERE id = ${id}`);
return { before: { id }, after: null };
}
const PREFIX_SETTINGS = new Set(["enabled", "max_length", "min_rank", "min_rank_to_buy", "allow_colors", "allow_bold", "allow_italic", "default_color", "price_credits", "price_points", "points_type"]);
async function prefixSettingsUpdate(input: unknown, transaction: unknown): Promise<ContentMutationSnapshot> {
const settings = record(record(input).settings ?? input);
const accepted: Record<string, string> = {};
for (const [key, rawValue] of Object.entries(settings)) {
if (!PREFIX_SETTINGS.has(key)) continue;
const value = text(rawValue, 255);
accepted[key] = value;
await database(transaction).execute(sql`INSERT INTO custom_prefix_settings (\`key\`, \`value\`) VALUES (${key}, ${value}) ON DUPLICATE KEY UPDATE \`value\` = ${value}`);
}
return { before: null, after: { keys: Object.keys(accepted).sort() } };
}
async function helpQuestionChange(input: unknown, context: ContentMutationContext, transaction: unknown): Promise<ContentMutationSnapshot> {
const data = record(input);
const action = text(data.action, 16, true);
const connection = database(transaction);
if (action === "delete") {
const id = positiveBigInt(data.id);
await connection.delete(WebsiteHelpCenterCategories).where(eq(WebsiteHelpCenterCategories.id, id));
await activity(context, "help_delete", "Deleted help-center entry #" + id, "help_center_category", Number(id));
return { before: { id: id.toString() }, after: null };
}
const name = text(data.name, 255, true);
const content = canonicalize(rawText(data.content ?? data.answer, 100_000));
if (!content) throw validation();
const values = {
name,
content,
position: Math.max(1, positiveInteger(data.position ?? 1)),
imageUrl: text(data.imageUrl, 255) || null,
buttonText: text(data.buttonText, 255) || null,
buttonUrl: text(data.buttonUrl, 255) || null,
buttonColor: text(data.buttonColor, 16) || "#eeb425",
buttonBorderColor: text(data.buttonBorderColor, 16) || "#facc15",
smallBox: Boolean(data.smallBox),
};
if (action === "create") {
const [result] = (await connection.insert(WebsiteHelpCenterCategories).values(values)) as unknown as [ResultSetHeader];
const id = insertedId(result);
await activity(context, "help_create", "Created help-center entry #" + id + " (" + name + ")", "help_center_category", Number(id));
return { before: null, after: { id, name }, output: { id } };
}
if (action !== "update") throw validation();
const id = positiveBigInt(data.id);
await connection.update(WebsiteHelpCenterCategories).set(values).where(eq(WebsiteHelpCenterCategories.id, id));
await activity(context, "help_update", "Updated help-center entry #" + id + " (" + name + ")", "help_center_category", Number(id));
return { before: { id: id.toString() }, after: { id: id.toString(), name }, output: { id: id.toString() } };
}
async function writeableBoxChange(input: unknown, context: ContentMutationContext, transaction: unknown): Promise<ContentMutationSnapshot> {
const data = record(input);
const action = text(data.action, 16, true);
const connection = database(transaction);
if (action === "delete") {
const id = positiveBigInt(data.id);
await connection.delete(WebsiteWriteableBoxes).where(eq(WebsiteWriteableBoxes.id, id));
await activity(context, "writeable_box_delete", "Deleted writeable box #" + id, "writeable_box", Number(id));
return { before: { id: id.toString() }, after: null };
}
if (action === "toggle") {
const id = positiveBigInt(data.id);
const isActive = data.next === "1" || data.next === 1 || data.next === true;
await connection.update(WebsiteWriteableBoxes).set({ isActive, updatedAt: new Date() }).where(eq(WebsiteWriteableBoxes.id, id));
await activity(context, "writeable_box_toggle", (isActive ? "Activated" : "Hid") + " writeable box #" + id, "writeable_box", Number(id));
return { before: { id: id.toString() }, after: { id: id.toString(), isActive }, output: { id: id.toString() } };
}
const title = text(data.title, 255, true);
const values = {
title,
icon: text(data.icon, 255) || null,
content: rawText(data.content, 100_000),
position: nonNegativeInteger(data.position),
isActive: data.isActive === "1" || data.isActive === 1 || data.isActive === true,
updatedAt: new Date(),
};
if (action === "create") {
const now = new Date();
const [result] = (await connection.insert(WebsiteWriteableBoxes).values({ ...values, createdAt: now })) as unknown as [ResultSetHeader];
const id = insertedId(result);
await activity(context, "writeable_box_create", "Created writeable box " + title + " (#" + id + ")", "writeable_box", Number(id));
return { before: null, after: { id, title }, output: { id } };
}
if (action !== "update") throw validation();
const id = positiveBigInt(data.id);
await connection.update(WebsiteWriteableBoxes).set(values).where(eq(WebsiteWriteableBoxes.id, id));
await activity(context, "writeable_box_update", "Updated writeable box #" + id + " (" + title + ")", "writeable_box", Number(id));
return { before: { id: id.toString() }, after: { id: id.toString(), title }, output: { id: id.toString() } };
}
async function emailTemplateChange(input: unknown, transaction: unknown): Promise<ContentMutationSnapshot> {
const data = record(input);
const action = text(data.action, 16, true);
const connection = database(transaction);
if (action === "delete") {
const id = positiveBigInt(data.id);
await connection.delete(EmailTemplates).where(eq(EmailTemplates.id, id));
return { before: { id: id.toString() }, after: null };
}
const subject = text(data.subject, 255, true);
const body = rawText(data.body, 500_000);
if (!body) throw validation();
const values = { subject, body, variables: text(data.variables, 20_000) || null, isActive: Boolean(data.isActive) };
if (action === "create") {
const name = text(data.name, 255, true);
const [result] = (await connection.insert(EmailTemplates).values({ name, ...values })) as unknown as [ResultSetHeader];
const id = insertedId(result);
return { before: null, after: { id, name, subject, isActive: values.isActive }, output: { id } };
}
if (action !== "update") throw validation();
const id = positiveBigInt(data.id);
await connection.update(EmailTemplates).set(values).where(eq(EmailTemplates.id, id));
return { before: { id: id.toString() }, after: { id: id.toString(), subject, isActive: values.isActive }, output: { id: id.toString() } };
}
const DATABASE_HANDLERS: Partial<Record<ContentMutationOperation, (input: unknown, context: ContentMutationContext, transaction: unknown) => Promise<ContentMutationSnapshot>>> = {
"article.change": articleChange,
"ad.change": adChange,
"banner.change": (input, _context, transaction) => bannerChange(input, transaction),
"event-type.change": (input, _context, transaction) => eventTypeChange(input, transaction),
"event.change": eventChange,
"event-prize.change": (input, _context, transaction) => eventPrizeChange(input, transaction),
"event-winner.add": (input, _context, transaction) => eventWinnerAdd(input, transaction),
"poll.change": (input, _context, transaction) => pollChange(input, transaction),
"poll-question.change": (input, _context, transaction) => pollQuestionChange(input, transaction),
"tag.change": tagChange,
"prefix.change": (input, _context, transaction) => prefixChange(input, transaction),
"prefix-blacklist.change": (input, _context, transaction) => prefixBlacklistChange(input, transaction),
"prefix-settings.update": (input, _context, transaction) => prefixSettingsUpdate(input, transaction),
"help-question.change": helpQuestionChange,
"writeable-box.change": writeableBoxChange,
"email-template.change": (input, _context, transaction) => emailTemplateChange(input, transaction),
};
export async function executeContentDatabaseMutation(
operation: ContentMutationOperation,
input: unknown,
context: ContentMutationContext,
transaction: unknown,
): Promise<ContentMutationSnapshot | null> {
const handler = DATABASE_HANDLERS[operation];
return handler ? handler(input, context, transaction) : null;
}