fix: resolve build, lint, and type errors - add paramName support to CRUD builder

This commit is contained in:
2026-04-25 18:36:52 +08:00
parent 1b92aecdb8
commit bc6fc493f6
6 changed files with 24 additions and 18 deletions
+13 -9
View File
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Elysia, t } from "elysia" import { Elysia, t } from "elysia"
import { db } from "@/lib/db/index" import { db } from "@/lib/db/index"
import { getTableColumns } from "drizzle-orm" import { getTableColumns } from "drizzle-orm"
@@ -44,6 +45,7 @@ export type CrudAuthConfig = {
* @param config.filter - Exact-match filter configuration * @param config.filter - Exact-match filter configuration
* @param config.name - Human-readable name for error messages * @param config.name - Human-readable name for error messages
* @param config.primaryKey - Column name used as primary key (default: "id") * @param config.primaryKey - Column name used as primary key (default: "id")
* @param config.paramName - URL parameter name (defaults to primaryKey)
* @param config.softDelete - If true, DELETE sets isRemoved=true instead of deleting * @param config.softDelete - If true, DELETE sets isRemoved=true instead of deleting
*/ */
export function createCrudRoutes<T extends AnyPgTable>( export function createCrudRoutes<T extends AnyPgTable>(
@@ -55,6 +57,7 @@ export function createCrudRoutes<T extends AnyPgTable>(
filter?: CrudFilterConfig filter?: CrudFilterConfig
name?: string name?: string
primaryKey?: string primaryKey?: string
paramName?: string
softDelete?: boolean softDelete?: boolean
}, },
) { ) {
@@ -65,6 +68,7 @@ export function createCrudRoutes<T extends AnyPgTable>(
filter, filter,
name = "resource", name = "resource",
primaryKey = "id", primaryKey = "id",
paramName = primaryKey,
softDelete = false, softDelete = false,
} = config } = config
@@ -152,9 +156,9 @@ export function createCrudRoutes<T extends AnyPgTable>(
// ── GET BY ID ───────────────────────────────────────────────────── // ── GET BY ID ─────────────────────────────────────────────────────
routes.get( routes.get(
`/:${primaryKey}`, `/:${paramName}`,
async ({ params, set }) => { async ({ params, set }) => {
const id = (params as any)[primaryKey] const id = (params as any)[paramName]
const [record] = await db const [record] = await db
.select() .select()
@@ -171,7 +175,7 @@ export function createCrudRoutes<T extends AnyPgTable>(
}, },
{ {
params: t.Object({ params: t.Object({
[primaryKey]: t.String(), [paramName]: t.String(),
}), }),
}, },
) )
@@ -206,7 +210,7 @@ export function createCrudRoutes<T extends AnyPgTable>(
// ── UPDATE ──────────────────────────────────────────────────────── // ── UPDATE ────────────────────────────────────────────────────────
routes.patch( routes.patch(
`/:${primaryKey}`, `/:${paramName}`,
async ({ params, body, request, set }) => { async ({ params, body, request, set }) => {
const roleMap: Record<string, string[]> = { const roleMap: Record<string, string[]> = {
user: ["user", "contributor", "admin"], user: ["user", "contributor", "admin"],
@@ -221,7 +225,7 @@ export function createCrudRoutes<T extends AnyPgTable>(
return { error: guard.error } return { error: guard.error }
} }
const id = (params as any)[primaryKey] const id = (params as any)[paramName]
// Add updatedAt if column exists // Add updatedAt if column exists
const updateData = columns["updatedAt"] const updateData = columns["updatedAt"]
@@ -243,7 +247,7 @@ export function createCrudRoutes<T extends AnyPgTable>(
}, },
{ {
params: t.Object({ params: t.Object({
[primaryKey]: t.String(), [paramName]: t.String(),
}), }),
body: t.Record(t.String(), t.Any()), body: t.Record(t.String(), t.Any()),
}, },
@@ -251,7 +255,7 @@ export function createCrudRoutes<T extends AnyPgTable>(
// ── DELETE ──────────────────────────────────────────────────────── // ── DELETE ────────────────────────────────────────────────────────
routes.delete( routes.delete(
`/:${primaryKey}`, `/:${paramName}`,
async ({ params, request, set }) => { async ({ params, request, set }) => {
const guard = await requireRole(request.headers, [authConfig.delete]) const guard = await requireRole(request.headers, [authConfig.delete])
@@ -260,7 +264,7 @@ export function createCrudRoutes<T extends AnyPgTable>(
return { error: guard.error } return { error: guard.error }
} }
const id = (params as any)[primaryKey] const id = (params as any)[paramName]
if (softDelete && columns["isRemoved"]) { if (softDelete && columns["isRemoved"]) {
const [updated] = (await db const [updated] = (await db
@@ -291,7 +295,7 @@ export function createCrudRoutes<T extends AnyPgTable>(
}, },
{ {
params: t.Object({ params: t.Object({
[primaryKey]: t.String(), [paramName]: t.String(),
}), }),
}, },
) )
+1
View File
@@ -12,6 +12,7 @@ export const gamesRoutes = createCrudRoutes(games, {
auth: { read: "public", write: "contributor", delete: "admin" }, auth: { read: "public", write: "contributor", delete: "admin" },
search: { fields: ["title", "developer", "publisher"] }, search: { fields: ["title", "developer", "publisher"] },
filter: { fields: ["source", "onlineMultiplayerStatus", "syncStatus"] }, filter: { fields: ["source", "onlineMultiplayerStatus", "syncStatus"] },
paramName: "gameId",
}) })
// ── Game Versions (nested under /games/:gameId/versions) ────────── // ── Game Versions (nested under /games/:gameId/versions) ──────────
+2 -2
View File
@@ -2,7 +2,7 @@ import { Elysia, t } from "elysia"
import { createCrudRoutes } from "./crud-builder" import { createCrudRoutes } from "./crud-builder"
import { performanceEntries, games, gameVersions } from "@/lib/db/schema" import { performanceEntries, games, gameVersions } from "@/lib/db/schema"
import { db } from "@/lib/db/index" import { db } from "@/lib/db/index"
import { eq, and, desc, sql } from "drizzle-orm" import { eq, and, sql } from "drizzle-orm"
import { requireRole } from "@/lib/auth/guard" import { requireRole } from "@/lib/auth/guard"
// ── Performance Entries CRUD ────────────────────────────────────── // ── Performance Entries CRUD ──────────────────────────────────────
@@ -84,7 +84,7 @@ export const performanceVerifyRoutes = new Elysia({
conditions.push(eq(performanceEntries.hardwareSlug, hardwareSlug)) conditions.push(eq(performanceEntries.hardwareSlug, hardwareSlug))
} }
if (fsrVersion) { if (fsrVersion) {
conditions.push(eq(performanceEntries.fsrVersion, fsrVersion as any)) conditions.push(eq(performanceEntries.fsrVersion, fsrVersion as any)) // eslint-disable-line @typescript-eslint/no-explicit-any
} }
// Join through gameVersions to get to games // Join through gameVersions to get to games
+5 -4
View File
@@ -12,11 +12,12 @@ export const presetsRoutes = createCrudRoutes(communityPresets, {
auth: { read: "public", write: "user", delete: "admin" }, auth: { read: "public", write: "user", delete: "admin" },
search: { fields: ["name", "description"] }, search: { fields: ["name", "description"] },
filter: { fields: ["gameId", "hardwareSlug"] }, filter: { fields: ["gameId", "hardwareSlug"] },
paramName: "presetId",
}) })
// ── Upvote endpoint ─────────────────────────────────────────────── // ── Upvote endpoint ───────────────────────────────────────────────
export const presetUpvoteRoutes = new Elysia({ prefix: "/presets" }).post( export const presetUpvoteRoutes = new Elysia({ prefix: "/presets" }).post(
"/:id/upvote", "/:presetId/upvote",
async ({ params, request, set }) => { async ({ params, request, set }) => {
const guard = await requireRole(request.headers, [ const guard = await requireRole(request.headers, [
"user", "user",
@@ -31,7 +32,7 @@ export const presetUpvoteRoutes = new Elysia({ prefix: "/presets" }).post(
const [preset] = await db const [preset] = await db
.select() .select()
.from(communityPresets) .from(communityPresets)
.where(eq(communityPresets.id, params.id)) .where(eq(communityPresets.id, params.presetId))
.limit(1) .limit(1)
if (!preset) { if (!preset) {
@@ -45,13 +46,13 @@ export const presetUpvoteRoutes = new Elysia({ prefix: "/presets" }).post(
upvotes: sql`${communityPresets.upvotes} + 1`, upvotes: sql`${communityPresets.upvotes} + 1`,
updatedAt: new Date(), updatedAt: new Date(),
}) })
.where(eq(communityPresets.id, params.id)) .where(eq(communityPresets.id, params.presetId))
.returning() .returning()
return updated return updated
}, },
{ {
params: t.Object({ id: t.String() }), params: t.Object({ presetId: t.String() }),
}, },
) )
+2 -2
View File
@@ -1,8 +1,8 @@
import { Elysia, t } from "elysia" import { Elysia } from "elysia"
import { createCrudRoutes } from "./crud-builder" import { createCrudRoutes } from "./crud-builder"
import { settingCategories, settingDefinitions } from "@/lib/db/schema" import { settingCategories, settingDefinitions } from "@/lib/db/schema"
import { db } from "@/lib/db/index" import { db } from "@/lib/db/index"
import { eq, asc } from "drizzle-orm" import { asc } from "drizzle-orm"
// ── Setting Categories CRUD ─────────────────────────────────────── // ── Setting Categories CRUD ───────────────────────────────────────
export const settingCategoriesRoutes = createCrudRoutes(settingCategories, { export const settingCategoriesRoutes = createCrudRoutes(settingCategories, {
+1 -1
View File
@@ -22,7 +22,7 @@ export const gameComments = pgTable(
userId: text("user_id") userId: text("user_id")
.notNull() .notNull()
.references(() => user.id, { onDelete: "cascade" }), .references(() => user.id, { onDelete: "cascade" }),
parentId: text("parent_id").references((): any => gameComments.id, { parentId: text("parent_id").references((): any => gameComments.id, { // eslint-disable-line @typescript-eslint/no-explicit-any
onDelete: "cascade", onDelete: "cascade",
}), }),
// Tiptap JSON document // Tiptap JSON document