Replace raw db.execute tuple casts with queryRows/rowsFrom/execResult/ affectedRows helpers from lib/db, drop redundant mysql2 casts on typed query builders, and centralize per-test fakeForm into test/fake-form. Update db mocks in tests so helpers resolve against mocked execute.
92 lines
2.6 KiB
TypeScript
92 lines
2.6 KiB
TypeScript
"use server";
|
|
import { sql } from "drizzle-orm";
|
|
import { z } from "zod";
|
|
import { namedTableView, tableViewPath } from "@/lib/admin/table-view-state";
|
|
import { db, queryRows, rowsFrom } from "@/lib/db";
|
|
import { PERMS } from "@/lib/permission-slugs";
|
|
import { adminAction } from "@/lib/safe-action";
|
|
|
|
const tableViewPermissions = [
|
|
PERMS.ADMIN_DASHBOARD,
|
|
PERMS.MOD_DASHBOARD,
|
|
PERMS.MODERATION_VIEW,
|
|
PERMS.MOD_CFH_VIEW,
|
|
PERMS.MOD_ACTIONS,
|
|
PERMS.MOD_BANS_VIEW,
|
|
PERMS.MOD_TICKETS_VIEW,
|
|
PERMS.MOD_TEAM_VIEW,
|
|
PERMS.MOD_USERS_VIEW,
|
|
PERMS.TICKETS_VIEW,
|
|
] as const;
|
|
|
|
export const listTableViews = adminAction(
|
|
{
|
|
permission: tableViewPermissions,
|
|
schema: z.object({ path: tableViewPath }),
|
|
},
|
|
async ({ session, data }) => {
|
|
const views = (
|
|
await queryRows<{ name: string; state: string }>(
|
|
sql`SELECT name, state FROM website_admin_table_views WHERE user_id = ${Number(session.user.id)} AND path = ${data.path} ORDER BY name LIMIT 20`,
|
|
)
|
|
).flatMap((row) => {
|
|
try {
|
|
const parsed = namedTableView.safeParse({
|
|
name: row.name,
|
|
state: JSON.parse(row.state),
|
|
});
|
|
return parsed.success ? [parsed.data] : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
});
|
|
return { ok: true, data: { views } };
|
|
},
|
|
);
|
|
export const saveTableView = adminAction(
|
|
{
|
|
permission: tableViewPermissions,
|
|
schema: namedTableView.extend({ path: tableViewPath }),
|
|
rateLimitKey: "table-view-save",
|
|
rateLimitMax: 30,
|
|
rateLimitWindowMs: 60000,
|
|
},
|
|
async ({ session, data }) => {
|
|
const userId = Number(session.user.id);
|
|
await db.transaction(async (tx) => {
|
|
await tx.execute(
|
|
sql`SELECT id FROM users WHERE id = ${userId} FOR UPDATE`,
|
|
);
|
|
const names = rowsFrom<{ name: string }>(
|
|
await tx.execute(
|
|
sql`SELECT name FROM website_admin_table_views WHERE user_id = ${userId} AND path = ${data.path}`,
|
|
),
|
|
);
|
|
if (
|
|
names.length >= 20 &&
|
|
!names.some(
|
|
(row) =>
|
|
row.name.toLocaleLowerCase() === data.name.toLocaleLowerCase(),
|
|
)
|
|
)
|
|
throw new Error("Maximum 20 saved views per table");
|
|
await tx.execute(
|
|
sql`INSERT INTO website_admin_table_views (user_id,path,name,state) VALUES (${userId},${data.path},${data.name},${JSON.stringify(data.state)}) ON DUPLICATE KEY UPDATE state = VALUES(state)`,
|
|
);
|
|
});
|
|
return { ok: true };
|
|
},
|
|
);
|
|
export const deleteTableView = adminAction(
|
|
{
|
|
permission: tableViewPermissions,
|
|
schema: z.object({ path: tableViewPath, name: z.string().min(1).max(60) }),
|
|
},
|
|
async ({ session, data }) => {
|
|
await db.execute(
|
|
sql`DELETE FROM website_admin_table_views WHERE user_id = ${Number(session.user.id)} AND path = ${data.path} AND name = ${data.name}`,
|
|
);
|
|
return { ok: true };
|
|
},
|
|
);
|