refactor(db): typed query helpers, shared test FormData helper
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.
This commit is contained in:
1 parent
2e25b39364
commit
8638e81444
116 files changed
+866
-897
No files matched your search
@@ -4,7 +4,7 @@ import { redirect } from "next/navigation";
|
||||
import { getLocale, getTranslations } from "next-intl/server";
|
||||
import { StatsCard } from "@/components/admin/stats-card";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Ban, db, User } from "@/lib/db";
|
||||
import { Ban, db, queryRows, User } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { redisCache } from "@/lib/redis-cache";
|
||||
|
||||
@@ -36,8 +36,7 @@ async function safeCount(
|
||||
}
|
||||
|
||||
async function rawRows<T>(query: ReturnType<typeof sql>): Promise<T[]> {
|
||||
const [rows] = (await db.execute(query)) as unknown as [T[], unknown];
|
||||
return rows ?? [];
|
||||
return queryRows<T>(query);
|
||||
}
|
||||
|
||||
async function safeRawCount(
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
db,
|
||||
LogsShopPurchases,
|
||||
MarketplaceItems,
|
||||
queryRows,
|
||||
RoomTradeLog,
|
||||
User,
|
||||
} from "@/lib/db";
|
||||
@@ -63,8 +64,7 @@ async function safeCount(fn: () => Promise<number>): Promise<number> {
|
||||
}
|
||||
|
||||
async function rawRows<T>(query: ReturnType<typeof sql>): Promise<T[]> {
|
||||
const [rows] = (await db.execute(query)) as unknown as [T[], unknown];
|
||||
return rows ?? [];
|
||||
return queryRows<T>(query);
|
||||
}
|
||||
|
||||
async function getEconomyData(
|
||||
|
||||
@@ -12,7 +12,7 @@ import { redirect } from "next/navigation";
|
||||
import { getLocale, getTranslations } from "next-intl/server";
|
||||
import { StatsCard } from "@/components/admin/stats-card";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Ban, db, Rooms, RoomTradeLog, User } from "@/lib/db";
|
||||
import { Ban, db, queryRows, Rooms, RoomTradeLog, User } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { redisCache } from "@/lib/redis-cache";
|
||||
|
||||
@@ -82,8 +82,7 @@ async function safeCount(
|
||||
}
|
||||
|
||||
async function rawRows<T>(query: ReturnType<typeof sql>): Promise<T[]> {
|
||||
const [rows] = (await db.execute(query)) as unknown as [T[], unknown];
|
||||
return rows ?? [];
|
||||
return queryRows<T>(query);
|
||||
}
|
||||
|
||||
async function getAnalyticsData(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||
import { db } from "@/lib/db";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
|
||||
type ListParamsInput = {
|
||||
@@ -37,8 +37,7 @@ function parsePageParams(
|
||||
}
|
||||
|
||||
async function rawRows<T>(query: SqlFragment): Promise<T[]> {
|
||||
const [rows] = (await db.execute(query)) as unknown as [T[], unknown];
|
||||
return rows ?? [];
|
||||
return queryRows<T>(query);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,13 @@ import { asc, inArray, sql } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { db, NavigatorFlatcats, NavigatorPubliccats, Rooms } from "@/lib/db";
|
||||
import {
|
||||
db,
|
||||
NavigatorFlatcats,
|
||||
NavigatorPubliccats,
|
||||
queryRows,
|
||||
Rooms,
|
||||
} from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
|
||||
// navigator_flatcats — emulator-owned flat (private-room) categories shown in the
|
||||
@@ -65,12 +71,11 @@ export default async function AdminNavigation() {
|
||||
|
||||
let publics: PublicRow[] = [];
|
||||
try {
|
||||
const [rows] = (await db.execute(sql`
|
||||
publics = await queryRows<PublicRow>(sql`
|
||||
SELECT public_cat_id, room_id, visible
|
||||
FROM navigator_publics
|
||||
ORDER BY public_cat_id ASC, room_id ASC
|
||||
`)) as unknown as [PublicRow[], unknown];
|
||||
publics = rows ?? [];
|
||||
`);
|
||||
} catch {
|
||||
publics = [];
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { sql } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import Link from "@/components/link";
|
||||
import { db } from "@/lib/db";
|
||||
import { db, queryRows } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { fetchEmulatorRankSummaries } from "@/lib/services/permission-ranks";
|
||||
import { PermissionsList } from "./permissions-list";
|
||||
@@ -25,9 +25,9 @@ export default async function PermissionsPage({
|
||||
const ranks = await fetchEmulatorRankSummaries(db);
|
||||
|
||||
// User counts per rank
|
||||
const [rawCounts] = (await db.execute(
|
||||
const rawCounts = await queryRows<{ rank: number; cnt: number }>(
|
||||
sql`SELECT \`rank\`, COUNT(*) as cnt FROM users GROUP BY \`rank\``,
|
||||
)) as unknown as [{ rank: number; cnt: number }[], unknown[]];
|
||||
);
|
||||
const countMap: Record<number, number> = {};
|
||||
for (const row of rawCounts) {
|
||||
countMap[Number(row.rank)] = Number(row.cnt);
|
||||
|
||||
@@ -2,7 +2,10 @@ import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { beforeEach, expect, it, vi } from "vitest";
|
||||
|
||||
const state = vi.hoisted(() => ({ access: vi.fn(), execute: vi.fn() }));
|
||||
vi.mock("@/lib/db", () => ({ db: { execute: state.execute } }));
|
||||
vi.mock("@/lib/db", async () => ({
|
||||
...(await import("@/test/db-helpers")).createDbHelpers(state.execute),
|
||||
db: { execute: state.execute },
|
||||
}));
|
||||
vi.mock("@/lib/permissions", () => ({
|
||||
PERMS: { PERMISSIONS_MANAGE: "admin.permissions.manage" },
|
||||
getAdminContext: async () => ({
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getTranslations } from "next-intl/server";
|
||||
import Link from "@/components/link";
|
||||
import { isDynamicSuperAdmin } from "@/lib/admin/authorization-policy";
|
||||
import { previewRoleAccess } from "@/lib/admin/permission-preview";
|
||||
import { db } from "@/lib/db";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
|
||||
export default async function PermissionPreview({
|
||||
@@ -17,14 +17,11 @@ export default async function PermissionPreview({
|
||||
redirect("/admin");
|
||||
const t = await getTranslations("pages.admin.permissionPreview");
|
||||
const nav = await getTranslations("pages.admin.nav");
|
||||
const [roleRows] = await db.execute(
|
||||
sql`SELECT id, slug, title FROM acl_roles ORDER BY title, id`,
|
||||
);
|
||||
const roles = roleRows as unknown as {
|
||||
const roles = await queryRows<{
|
||||
id: number;
|
||||
slug: string;
|
||||
title: string;
|
||||
}[];
|
||||
}>(sql`SELECT id, slug, title FROM acl_roles ORDER BY title, id`);
|
||||
const params = await searchParams;
|
||||
const selected =
|
||||
roles.find((r) => String(r.id) === params.role) ??
|
||||
@@ -33,15 +30,13 @@ export default async function PermissionPreview({
|
||||
let superAdmin = false;
|
||||
let permissionTitles: Record<string, string> = {};
|
||||
if (selected) {
|
||||
const [grantRows] = await db.execute(
|
||||
const grants = await queryRows<{ slug: string; title: string }>(
|
||||
sql`SELECT p.slug, p.title FROM acl_model_permissions mp JOIN acl_permissions p ON p.id = mp.permission_id WHERE mp.model_type = 'Role' AND mp.model_id = ${selected.id}`,
|
||||
);
|
||||
const grants = grantRows as unknown as { slug: string; title: string }[];
|
||||
const [highestRows] = await db.execute(
|
||||
sql`SELECT COALESCE((SELECT MAX(u.\`rank\`) FROM users u INNER JOIN permission_ranks pr ON pr.id = u.\`rank\`), (SELECT MAX(id) FROM permission_ranks)) AS highest_rank`,
|
||||
);
|
||||
const highest = (
|
||||
highestRows as unknown as { highest_rank: number | null }[]
|
||||
await queryRows<{ highest_rank: number | null }>(
|
||||
sql`SELECT COALESCE((SELECT MAX(u.\`rank\`) FROM users u INNER JOIN permission_ranks pr ON pr.id = u.\`rank\`), (SELECT MAX(id) FROM permission_ranks)) AS highest_rank`,
|
||||
)
|
||||
)[0]?.highest_rank;
|
||||
const rankMatch = /^rank_([1-9]\d*)$/.exec(selected.slug);
|
||||
superAdmin =
|
||||
@@ -54,14 +49,11 @@ export default async function PermissionPreview({
|
||||
grants.map((g) => g.slug),
|
||||
superAdmin,
|
||||
);
|
||||
const [allRows] = await db.execute(
|
||||
const allRows = await queryRows<{ slug: string; title: string }>(
|
||||
sql`SELECT slug, title FROM acl_permissions ORDER BY slug`,
|
||||
);
|
||||
permissionTitles = Object.fromEntries(
|
||||
(allRows as unknown as { slug: string; title: string }[]).map((p) => [
|
||||
p.slug,
|
||||
p.title,
|
||||
]),
|
||||
allRows.map((p) => [p.slug, p.title]),
|
||||
);
|
||||
}
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getTranslations } from "next-intl/server";
|
||||
import { AdminSimplePager } from "@/components/admin/admin-simple-pager";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||
import { db } from "@/lib/db";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
|
||||
@@ -24,8 +24,7 @@ type SubSqlRow = {
|
||||
};
|
||||
|
||||
async function rawRows<T>(query: ReturnType<typeof sql>): Promise<T[]> {
|
||||
const [rows] = (await db.execute(query)) as unknown as [T[], unknown];
|
||||
return rows ?? [];
|
||||
return queryRows<T>(query);
|
||||
}
|
||||
|
||||
export default async function AdminSubscriptions({
|
||||
|
||||
@@ -7,7 +7,10 @@ const state = vi.hoisted(() => ({
|
||||
allowed: new Set<string>(),
|
||||
execute: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/db", () => ({ db: { execute: state.execute } }));
|
||||
vi.mock("@/lib/db", async () => ({
|
||||
...(await import("@/test/db-helpers")).createDbHelpers(state.execute),
|
||||
db: { execute: state.execute },
|
||||
}));
|
||||
vi.mock("@/lib/permissions", async () => ({
|
||||
...(await import("@/lib/permission-slugs")),
|
||||
canAccess: (_: unknown, permission: string) => state.allowed.has(permission),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type SQL, sql } from "drizzle-orm";
|
||||
import { db } from "@/lib/db";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { canAccess, PERMS, type PermissionSet } from "@/lib/permissions";
|
||||
|
||||
export type OverviewRow = {
|
||||
@@ -27,8 +27,7 @@ export async function loadUserOverview(
|
||||
rank: number,
|
||||
): Promise<OverviewSection[]> {
|
||||
const read = async (query: SQL): Promise<OverviewRow[]> => {
|
||||
const [rows] = await db.execute(query);
|
||||
return rows as unknown as OverviewRow[];
|
||||
return queryRows<OverviewRow>(query);
|
||||
};
|
||||
const sources: {
|
||||
key: string;
|
||||
|
||||
@@ -11,54 +11,56 @@ vi.mock("next/navigation", () => ({
|
||||
throw new Error("not found");
|
||||
},
|
||||
}));
|
||||
vi.mock("@/lib/db", () => {
|
||||
vi.mock("@/lib/db", async () => {
|
||||
const tables = Object.fromEntries(
|
||||
["User", "Rooms", "RoomTradeLog", "UsersBadges", "UsersCurrency"].map(
|
||||
(name) => [name, { name }],
|
||||
),
|
||||
);
|
||||
const db = {
|
||||
select() {
|
||||
let name = "";
|
||||
const rows = () =>
|
||||
name === "User"
|
||||
? [{ id: 42, username: "User", rank: 2, online: "0" }]
|
||||
: [];
|
||||
const query = {
|
||||
from(table: { name: string }) {
|
||||
name = table.name;
|
||||
state.tables.push(name);
|
||||
return query;
|
||||
},
|
||||
where() {
|
||||
return query;
|
||||
},
|
||||
orderBy() {
|
||||
return query;
|
||||
},
|
||||
limit() {
|
||||
return query;
|
||||
},
|
||||
// biome-ignore lint/suspicious/noThenProperty: Drizzle queries are intentionally awaitable.
|
||||
then(resolve: (value: unknown[]) => unknown) {
|
||||
return Promise.resolve(rows()).then(resolve);
|
||||
},
|
||||
catch() {
|
||||
return Promise.resolve(rows());
|
||||
},
|
||||
};
|
||||
return query;
|
||||
},
|
||||
async execute(query: Parameters<MySqlDialect["sqlToQuery"]>[0]) {
|
||||
const compiled = new MySqlDialect().sqlToQuery(query).sql;
|
||||
state.sql.push(compiled);
|
||||
if (state.failLogs && compiled.includes("chatlogs_room"))
|
||||
throw new Error("unavailable");
|
||||
return [[], []];
|
||||
},
|
||||
};
|
||||
return {
|
||||
...tables,
|
||||
db: {
|
||||
select() {
|
||||
let name = "";
|
||||
const rows = () =>
|
||||
name === "User"
|
||||
? [{ id: 42, username: "User", rank: 2, online: "0" }]
|
||||
: [];
|
||||
const query = {
|
||||
from(table: { name: string }) {
|
||||
name = table.name;
|
||||
state.tables.push(name);
|
||||
return query;
|
||||
},
|
||||
where() {
|
||||
return query;
|
||||
},
|
||||
orderBy() {
|
||||
return query;
|
||||
},
|
||||
limit() {
|
||||
return query;
|
||||
},
|
||||
// biome-ignore lint/suspicious/noThenProperty: Drizzle queries are intentionally awaitable.
|
||||
then(resolve: (value: unknown[]) => unknown) {
|
||||
return Promise.resolve(rows()).then(resolve);
|
||||
},
|
||||
catch() {
|
||||
return Promise.resolve(rows());
|
||||
},
|
||||
};
|
||||
return query;
|
||||
},
|
||||
async execute(query: Parameters<MySqlDialect["sqlToQuery"]>[0]) {
|
||||
const compiled = new MySqlDialect().sqlToQuery(query).sql;
|
||||
state.sql.push(compiled);
|
||||
if (state.failLogs && compiled.includes("chatlogs_room"))
|
||||
throw new Error("unavailable");
|
||||
return [[], []];
|
||||
},
|
||||
},
|
||||
db,
|
||||
...(await import("@/test/db-helpers")).createDbHelpers(db.execute),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { asc, count, desc, eq, or, sql } from "drizzle-orm";
|
||||
import { notFound } from "next/navigation";
|
||||
import {
|
||||
db,
|
||||
queryRows,
|
||||
Rooms,
|
||||
RoomTradeLog,
|
||||
User,
|
||||
@@ -10,8 +11,7 @@ import {
|
||||
} from "@/lib/db";
|
||||
|
||||
async function rawRows<T>(query: ReturnType<typeof sql>): Promise<T[]> {
|
||||
const [rows] = (await db.execute(query)) as unknown as [T[], unknown];
|
||||
return rows ?? [];
|
||||
return queryRows<T>(query);
|
||||
}
|
||||
|
||||
export async function loadUserById(
|
||||
|
||||
Reference in new issue
Block a user