diff --git a/app/api/[[...slugs]]/route.ts b/app/api/[[...slugs]]/route.ts index 128c3fa..5e6989c 100644 --- a/app/api/[[...slugs]]/route.ts +++ b/app/api/[[...slugs]]/route.ts @@ -9,9 +9,6 @@ import { hardwareRoutes, performanceRoutes, performanceVerifyRoutes, - settingCategoriesRoutes, - settingDefinitionsRoutes, - settingsFullRoutes, presetsRoutes, presetUpvoteRoutes, presetSettingsRoutes, @@ -62,10 +59,6 @@ export const app = new Elysia({ prefix: "/api" }) // Performance .use(performanceRoutes) .use(performanceVerifyRoutes) - // Settings - .use(settingCategoriesRoutes) - .use(settingDefinitionsRoutes) - .use(settingsFullRoutes) // Presets .use(presetsRoutes) .use(presetUpvoteRoutes) diff --git a/drizzle/0003_schema_expansion.sql b/drizzle/0003_schema_expansion.sql index 5ceaab9..a4d2064 100644 --- a/drizzle/0003_schema_expansion.sql +++ b/drizzle/0003_schema_expansion.sql @@ -3,8 +3,6 @@ CREATE TYPE "game_source" AS ENUM ('steam', 'manual', 'gog', 'epic'); CREATE TYPE "online_multiplayer_status" AS ENUM ('none', 'supported', 'unknown'); CREATE TYPE "fsr_version" AS ENUM ('none', 'fsr1', 'fsr2', 'fsr3'); CREATE TYPE "frame_gen_method" AS ENUM ('none', 'fsr_fg', 'dlss_fg'); -CREATE TYPE "setting_input_type" AS ENUM ('toggle', 'select', 'range', 'number'); -CREATE TYPE "impact_level" AS ENUM ('minor', 'moderate', 'major'); -- Alter games table ALTER TABLE "games" ALTER COLUMN "steam_app_id" DROP NOT NULL; @@ -35,31 +33,6 @@ CREATE INDEX "perf_user_idx" ON "performance_entries" ("user_id"); -- Alter hardware table ALTER TABLE "hardware" ADD COLUMN "sort_order" integer DEFAULT 0 NOT NULL; --- New table: setting_categories -CREATE TABLE "setting_categories" ( - "id" text PRIMARY KEY NOT NULL, - "name" text NOT NULL, - "slug" text NOT NULL, - "sort_order" integer DEFAULT 0 NOT NULL, - "created_at" timestamp DEFAULT now() NOT NULL, - CONSTRAINT "setting_categories_slug_unique" UNIQUE("slug") -); - --- New table: setting_definitions -CREATE TABLE "setting_definitions" ( - "id" text PRIMARY KEY NOT NULL, - "category_id" text NOT NULL, - "name" text NOT NULL, - "slug" text NOT NULL, - "input_type" "setting_input_type" NOT NULL, - "options" jsonb, - "impact_level" "impact_level" DEFAULT 'minor' NOT NULL, - "sort_order" integer DEFAULT 0 NOT NULL, - "created_at" timestamp DEFAULT now() NOT NULL, - CONSTRAINT "setting_category_slug_unique" UNIQUE("category_id","slug") -); -ALTER TABLE "setting_definitions" ADD CONSTRAINT "setting_definitions_category_id_setting_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "setting_categories"("id") ON DELETE cascade; - -- New table: community_presets CREATE TABLE "community_presets" ( "id" text PRIMARY KEY NOT NULL, @@ -69,6 +42,7 @@ CREATE TABLE "community_presets" ( "description" text, "created_by" text, "upvotes" integer DEFAULT 0 NOT NULL, + "settings_json" jsonb, "created_at" timestamp DEFAULT now() NOT NULL, "updated_at" timestamp DEFAULT now() NOT NULL, CONSTRAINT "preset_game_hardware_name_unique" UNIQUE("game_id","hardware_slug","name") @@ -77,18 +51,6 @@ ALTER TABLE "community_presets" ADD CONSTRAINT "community_presets_game_id_games_ ALTER TABLE "community_presets" ADD CONSTRAINT "community_presets_hardware_slug_hardware_slug_fk" FOREIGN KEY ("hardware_slug") REFERENCES "hardware"("slug") ON DELETE restrict; ALTER TABLE "community_presets" ADD CONSTRAINT "community_presets_created_by_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "user"("id") ON DELETE set null; --- New table: preset_settings -CREATE TABLE "preset_settings" ( - "id" text PRIMARY KEY NOT NULL, - "preset_id" text NOT NULL, - "setting_definition_id" text NOT NULL, - "value" jsonb NOT NULL, - "created_at" timestamp DEFAULT now() NOT NULL, - CONSTRAINT "preset_setting_unique" UNIQUE("preset_id","setting_definition_id") -); -ALTER TABLE "preset_settings" ADD CONSTRAINT "preset_settings_preset_id_community_presets_id_fk" FOREIGN KEY ("preset_id") REFERENCES "community_presets"("id") ON DELETE cascade; -ALTER TABLE "preset_settings" ADD CONSTRAINT "preset_settings_setting_definition_id_setting_definitions_id_fk" FOREIGN KEY ("setting_definition_id") REFERENCES "setting_definitions"("id") ON DELETE cascade; - -- New table: game_comments CREATE TABLE "game_comments" ( "id" text PRIMARY KEY NOT NULL, diff --git a/lib/api/index.ts b/lib/api/index.ts index 34a40d1..8edc429 100644 --- a/lib/api/index.ts +++ b/lib/api/index.ts @@ -3,11 +3,6 @@ export { userRoutes } from "./user" export { gamesRoutes, gameVersionsRoutes } from "./games" export { hardwareRoutes } from "./hardware" export { performanceRoutes, performanceVerifyRoutes } from "./performance" -export { - settingCategoriesRoutes, - settingDefinitionsRoutes, - settingsFullRoutes, -} from "./settings" export { presetsRoutes, presetUpvoteRoutes, diff --git a/lib/api/presets.ts b/lib/api/presets.ts index 179115f..22d7a57 100644 --- a/lib/api/presets.ts +++ b/lib/api/presets.ts @@ -1,6 +1,6 @@ import { Elysia, t } from "elysia" import { createCrudRoutes } from "./crud-builder" -import { communityPresets, presetSettings, settingDefinitions } from "@/lib/db/schema" +import { communityPresets } from "@/lib/db/schema" import { db } from "@/lib/db/index" import { eq, sql } from "drizzle-orm" import { requireRole } from "@/lib/auth/guard" @@ -57,40 +57,33 @@ export const presetUpvoteRoutes = new Elysia({ prefix: "/presets" }).post( ) // ── Preset Settings (nested under /presets/:presetId/settings) ──── +// Settings are stored as freeform JSON on the preset itself. export const presetSettingsRoutes = new Elysia({ prefix: "/presets/:presetId/settings", }) - // LIST all settings for a preset (with definition details) + // GET the settings JSON for a preset .get( "/", - async ({ params }) => { - const data = await db - .select({ - id: presetSettings.id, - presetId: presetSettings.presetId, - settingDefinitionId: presetSettings.settingDefinitionId, - value: presetSettings.value, - settingName: settingDefinitions.name, - settingSlug: settingDefinitions.slug, - inputType: settingDefinitions.inputType, - options: settingDefinitions.options, - impactLevel: settingDefinitions.impactLevel, - }) - .from(presetSettings) - .innerJoin( - settingDefinitions, - eq(presetSettings.settingDefinitionId, settingDefinitions.id), - ) - .where(eq(presetSettings.presetId, params.presetId)) + async ({ params, set }) => { + const [preset] = await db + .select({ settingsJson: communityPresets.settingsJson }) + .from(communityPresets) + .where(eq(communityPresets.id, params.presetId)) + .limit(1) - return data + if (!preset) { + set.status = 404 + return { error: "Preset not found" } + } + + return preset.settingsJson ?? [] }, { params: t.Object({ presetId: t.String() }), }, ) - // UPSERT settings for a preset (bulk create/update) - .post( + // PUT (replace) the settings JSON for a preset + .put( "/", async ({ params, body, request, set }) => { const guard = await requireRole(request.headers, [ @@ -120,36 +113,29 @@ export const presetSettingsRoutes = new Elysia({ return { error: "Not authorized to modify this preset" } } - // Delete existing settings and re-insert - await db - .delete(presetSettings) - .where(eq(presetSettings.presetId, params.presetId)) + const [updated] = await db + .update(communityPresets) + .set({ + settingsJson: body.settings, + updatedAt: new Date(), + }) + .where(eq(communityPresets.id, params.presetId)) + .returning() - if (body.settings.length > 0) { - await db.insert(presetSettings).values( - body.settings.map((s: { settingDefinitionId: string; value: boolean | string | number }) => ({ - presetId: params.presetId, - settingDefinitionId: s.settingDefinitionId, - value: s.value, - })), - ) - } - - // Return the updated settings - const data = await db - .select() - .from(presetSettings) - .where(eq(presetSettings.presetId, params.presetId)) - - return data + return updated }, { params: t.Object({ presetId: t.String() }), body: t.Object({ settings: t.Array( t.Object({ - settingDefinitionId: t.String(), - value: t.Union([t.Boolean(), t.String(), t.Number()]), + category: t.String(), + settings: t.Array( + t.Object({ + title: t.String(), + value: t.Union([t.String(), t.Number(), t.Boolean()]), + }), + ), }), ), }), diff --git a/lib/api/settings.ts b/lib/api/settings.ts deleted file mode 100644 index b3d6a3a..0000000 --- a/lib/api/settings.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Elysia } from "elysia" -import { createCrudRoutes } from "./crud-builder" -import { settingCategories, settingDefinitions } from "@/lib/db/schema" -import { db } from "@/lib/db/index" -import { asc } from "drizzle-orm" - -// ── Setting Categories CRUD ─────────────────────────────────────── -export const settingCategoriesRoutes = createCrudRoutes(settingCategories, { - prefix: "/settings/categories", - name: "Setting Category", - auth: { read: "public", write: "admin", delete: "admin" }, -}) - -// ── Setting Definitions CRUD ────────────────────────────────────── -export const settingDefinitionsRoutes = createCrudRoutes(settingDefinitions, { - prefix: "/settings/definitions", - name: "Setting Definition", - auth: { read: "public", write: "admin", delete: "admin" }, - filter: { fields: ["categoryId", "inputType", "impactLevel"] }, -}) - -// ── Combined endpoint: categories with their definitions ────────── -// This is the main endpoint the frontend uses to render dynamic forms. -export const settingsFullRoutes = new Elysia({ - prefix: "/settings/full", -}).get( - "/", - async () => { - const categories = await db - .select() - .from(settingCategories) - .orderBy(asc(settingCategories.sortOrder)) - - const definitions = await db - .select() - .from(settingDefinitions) - .orderBy(asc(settingDefinitions.sortOrder)) - - // Group definitions by category - const result = categories.map((cat) => ({ - ...cat, - definitions: definitions.filter((d) => d.categoryId === cat.id), - })) - - return result - }, -) diff --git a/lib/db/schema/communityPresets.ts b/lib/db/schema/communityPresets.ts index d96bbc2..3676bb4 100644 --- a/lib/db/schema/communityPresets.ts +++ b/lib/db/schema/communityPresets.ts @@ -1,5 +1,6 @@ import { integer, + jsonb, pgTable, text, timestamp, @@ -27,6 +28,13 @@ export const communityPresets = pgTable( onDelete: "set null", }), upvotes: integer("upvotes").default(0).notNull(), + // Freeform settings JSON — same flexible structure as performanceEntries.settingsJson + settingsJson: jsonb("settings_json").$type< + { + category: string + settings: { title: string; value: string | number | boolean }[] + }[] + >(), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }, diff --git a/lib/db/schema/index.ts b/lib/db/schema/index.ts index 3b33a60..cd6d644 100644 --- a/lib/db/schema/index.ts +++ b/lib/db/schema/index.ts @@ -5,8 +5,5 @@ export * from "./gameVersions" export * from "./hardware" export * from "./performanceEntries" export * from "./gamePlatformSupport" -export * from "./settingCategories" -export * from "./settingDefinitions" export * from "./communityPresets" -export * from "./presetSettings" export * from "./gameComments" diff --git a/lib/db/schema/presetSettings.ts b/lib/db/schema/presetSettings.ts deleted file mode 100644 index dcfdbba..0000000 --- a/lib/db/schema/presetSettings.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { jsonb, pgTable, text, timestamp, unique } from "drizzle-orm/pg-core" -import { communityPresets } from "./communityPresets" -import { settingDefinitions } from "./settingDefinitions" - -export const presetSettings = pgTable( - "preset_settings", - { - id: text("id") - .primaryKey() - .$defaultFn(() => crypto.randomUUID()), - presetId: text("preset_id") - .notNull() - .references(() => communityPresets.id, { onDelete: "cascade" }), - settingDefinitionId: text("setting_definition_id") - .notNull() - .references(() => settingDefinitions.id, { onDelete: "cascade" }), - // Value stored as JSON — boolean for toggle, string for select, - // number for range/number - value: jsonb("value").notNull().$type(), - createdAt: timestamp("created_at").defaultNow().notNull(), - }, - (table) => [ - unique("preset_setting_unique").on(table.presetId, table.settingDefinitionId), - ], -) diff --git a/lib/db/schema/settingCategories.ts b/lib/db/schema/settingCategories.ts deleted file mode 100644 index d512c9d..0000000 --- a/lib/db/schema/settingCategories.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core" - -export const settingCategories = pgTable("setting_categories", { - id: text("id") - .primaryKey() - .$defaultFn(() => crypto.randomUUID()), - name: text("name").notNull(), - slug: text("slug").notNull().unique(), - sortOrder: integer("sort_order").default(0).notNull(), - createdAt: timestamp("created_at").defaultNow().notNull(), -}) diff --git a/lib/db/schema/settingDefinitions.ts b/lib/db/schema/settingDefinitions.ts deleted file mode 100644 index 37f253b..0000000 --- a/lib/db/schema/settingDefinitions.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { - integer, - jsonb, - pgEnum, - pgTable, - text, - timestamp, - unique, -} from "drizzle-orm/pg-core" -import { settingCategories } from "./settingCategories" - -export const settingInputTypeEnum = pgEnum("setting_input_type", [ - "toggle", - "select", - "range", - "number", -]) - -export const impactLevelEnum = pgEnum("impact_level", [ - "minor", - "moderate", - "major", -]) - -export const settingDefinitions = pgTable( - "setting_definitions", - { - id: text("id") - .primaryKey() - .$defaultFn(() => crypto.randomUUID()), - categoryId: text("category_id") - .notNull() - .references(() => settingCategories.id, { onDelete: "cascade" }), - name: text("name").notNull(), - slug: text("slug").notNull(), - inputType: settingInputTypeEnum("input_type").notNull(), - // For `select`: string[] of option labels - // For `range`: { min: number, max: number, step: number } - // For `toggle`/`number`: null - options: jsonb("options").$type(), - impactLevel: impactLevelEnum("impact_level").default("minor").notNull(), - sortOrder: integer("sort_order").default(0).notNull(), - createdAt: timestamp("created_at").defaultNow().notNull(), - }, - (table) => [unique("setting_category_slug_unique").on(table.categoryId, table.slug)], -) diff --git a/lib/db/seed.ts b/lib/db/seed.ts index e9cfbc3..bf91896 100644 --- a/lib/db/seed.ts +++ b/lib/db/seed.ts @@ -1,10 +1,7 @@ import "dotenv/config" import { drizzle } from "drizzle-orm/node-postgres" import { Pool } from "pg" -import { eq } from "drizzle-orm" import { hardware } from "./schema/hardware" -import { settingCategories } from "./schema/settingCategories" -import { settingDefinitions } from "./schema/settingDefinitions" const pool = new Pool({ connectionString: process.env.DATABASE_URL, @@ -45,213 +42,6 @@ async function seed() { } console.log(`Seeded ${devices.length} hardware devices.`) - - // ── Setting Categories ──────────────────────────────────────────── - console.log("Seeding setting categories...") - - const categories = [ - { name: "Graphics", slug: "graphics", sortOrder: 0 }, - { name: "Display", slug: "display", sortOrder: 1 }, - { name: "Audio", slug: "audio", sortOrder: 2 }, - { name: "Controls", slug: "controls", sortOrder: 3 }, - { name: "Gameplay", slug: "gameplay", sortOrder: 4 }, - ] - - const insertedCategories: Record = {} // slug → id - - for (const cat of categories) { - const [inserted] = await db - .insert(settingCategories) - .values(cat) - .onConflictDoNothing({ target: settingCategories.slug }) - .returning() - - if (inserted) { - insertedCategories[cat.slug] = inserted.id - } else { - // Fetch existing - const existing = await db - .select() - .from(settingCategories) - .where(eq(settingCategories.slug, cat.slug)) - .limit(1) - if (existing[0]) { - insertedCategories[cat.slug] = existing[0].id - } - } - } - - console.log(`Seeded ${categories.length} setting categories.`) - - // ── Setting Definitions ────────────────────────────────────────── - console.log("Seeding setting definitions...") - - const definitions = [ - // Graphics - { - categorySlug: "graphics", - name: "Resolution", - slug: "resolution", - inputType: "select" as const, - options: ["720p", "800p", "1080p", "1200p", "1440p", "4K"], - impactLevel: "major" as const, - sortOrder: 0, - }, - { - categorySlug: "graphics", - name: "Texture Quality", - slug: "texture-quality", - inputType: "select" as const, - options: ["Low", "Medium", "High", "Ultra"], - impactLevel: "major" as const, - sortOrder: 1, - }, - { - categorySlug: "graphics", - name: "Shadow Quality", - slug: "shadow-quality", - inputType: "select" as const, - options: ["Off", "Low", "Medium", "High", "Ultra"], - impactLevel: "major" as const, - sortOrder: 2, - }, - { - categorySlug: "graphics", - name: "Anti-Aliasing", - slug: "anti-aliasing", - inputType: "select" as const, - options: ["Off", "FXAA", "TAA", "MSAA 2x", "MSAA 4x"], - impactLevel: "moderate" as const, - sortOrder: 3, - }, - { - categorySlug: "graphics", - name: "Volumetric Fog", - slug: "volumetric-fog", - inputType: "toggle" as const, - options: null, - impactLevel: "moderate" as const, - sortOrder: 4, - }, - { - categorySlug: "graphics", - name: "Motion Blur", - slug: "motion-blur", - inputType: "toggle" as const, - options: null, - impactLevel: "minor" as const, - sortOrder: 5, - }, - // Display - { - categorySlug: "display", - name: "Refresh Rate", - slug: "refresh-rate", - inputType: "select" as const, - options: ["30Hz", "40Hz", "60Hz", "90Hz", "120Hz"], - impactLevel: "moderate" as const, - sortOrder: 0, - }, - { - categorySlug: "display", - name: "V-Sync", - slug: "v-sync", - inputType: "toggle" as const, - options: null, - impactLevel: "moderate" as const, - sortOrder: 1, - }, - { - categorySlug: "display", - name: "Frame Rate Limit", - slug: "frame-rate-limit", - inputType: "range" as const, - options: { min: 15, max: 120, step: 5 }, - impactLevel: "minor" as const, - sortOrder: 2, - }, - // Audio - { - categorySlug: "audio", - name: "Master Volume", - slug: "master-volume", - inputType: "range" as const, - options: { min: 0, max: 100, step: 5 }, - impactLevel: "minor" as const, - sortOrder: 0, - }, - { - categorySlug: "audio", - name: "SFX Volume", - slug: "sfx-volume", - inputType: "range" as const, - options: { min: 0, max: 100, step: 5 }, - impactLevel: "minor" as const, - sortOrder: 1, - }, - { - categorySlug: "audio", - name: "Music Volume", - slug: "music-volume", - inputType: "range" as const, - options: { min: 0, max: 100, step: 5 }, - impactLevel: "minor" as const, - sortOrder: 2, - }, - // Controls - { - categorySlug: "controls", - name: "Controller Vibration", - slug: "controller-vibration", - inputType: "toggle" as const, - options: null, - impactLevel: "minor" as const, - sortOrder: 0, - }, - { - categorySlug: "controls", - name: "Aim Sensitivity", - slug: "aim-sensitivity", - inputType: "range" as const, - options: { min: 1, max: 10, step: 1 }, - impactLevel: "minor" as const, - sortOrder: 1, - }, - // Gameplay - { - categorySlug: "gameplay", - name: "Difficulty", - slug: "difficulty", - inputType: "select" as const, - options: ["Easy", "Normal", "Hard", "Extreme"], - impactLevel: "minor" as const, - sortOrder: 0, - }, - ] - - let definitionCount = 0 - for (const def of definitions) { - const categoryId = insertedCategories[def.categorySlug] - if (!categoryId) continue - - await db - .insert(settingDefinitions) - .values({ - categoryId, - name: def.name, - slug: def.slug, - inputType: def.inputType, - options: def.options, - impactLevel: def.impactLevel, - sortOrder: def.sortOrder, - }) - .onConflictDoNothing({ target: settingDefinitions.slug }) - - definitionCount++ - } - - console.log(`Seeded ${definitionCount} setting definitions.`) - await pool.end() }