From a1b254a0b2669d3c84ed0a2a2de9fd2ac1fd8780 Mon Sep 17 00:00:00 2001 From: Thorsten Seyschab Date: Sun, 3 May 2026 20:05:38 +0200 Subject: [PATCH 01/12] refactor: run sqlite migrations at startup --- docs/storage.md | 5 +++-- server/plugins/migrations.ts | 17 +++++++++++++++++ server/utils/storage.ts | 5 ++--- 3 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 server/plugins/migrations.ts diff --git a/docs/storage.md b/docs/storage.md index 465ec65..f97a83a 100755 --- a/docs/storage.md +++ b/docs/storage.md @@ -22,7 +22,8 @@ Configuration in `nuxt.config.ts`: ## Initialization -- `initStorage()` in `server/utils/storage.ts` applies pending Drizzle migrations on first storage access. +- `server/plugins/migrations.ts` applies pending Drizzle migrations when the Nitro server starts. +- `initStorage()` in `server/utils/storage.ts` initializes the shared SQLite client after startup. - No bundled seed data is included in the repository. - Emoji cooldown state stays in server memory and is not part of persisted storage. @@ -40,7 +41,7 @@ Reset all stored quiz data: rm -rf .data/ ``` -The next runtime access recreates the SQLite database file. +The next server start recreates the SQLite database file. ## Production Mounts diff --git a/server/plugins/migrations.ts b/server/plugins/migrations.ts new file mode 100644 index 0000000..50415b9 --- /dev/null +++ b/server/plugins/migrations.ts @@ -0,0 +1,17 @@ +import { defineNitroPlugin } from 'nitropack/runtime' +import { + applyLocalMigrations, + getLocalDatabaseClient, +} from '../database/local-sqlite' +import { logger_error } from '../utils/logger' + +/** Applies pending SQLite migrations when the Nitro server starts. */ +export default defineNitroPlugin(() => { + try { + applyLocalMigrations(getLocalDatabaseClient().db) + } + catch (error: unknown) { + logger_error('SQLite migration startup error:', error) + throw error + } +}) diff --git a/server/utils/storage.ts b/server/utils/storage.ts index 8947ca2..c8ade27 100644 --- a/server/utils/storage.ts +++ b/server/utils/storage.ts @@ -20,7 +20,6 @@ import { deserializeQuestion, } from '../database/question-records' import { - applyLocalMigrations, getLocalDatabaseClient, } from '../database/local-sqlite' import { @@ -45,12 +44,12 @@ function getQuestionById(questionId: string): Question | undefined { return row ? deserializeQuestion(row) : undefined } -/** Initializes SQLite access and applies any pending migrations once. */ +/** Initializes SQLite access once after the startup migration plugin has run. */ export async function initStorage(_event?: H3Event) { if (storageInitialized) return try { - applyLocalMigrations(getDatabase()) + getDatabase() storageInitialized = true } catch (error: unknown) { From 73c0e9b67e4df09490466986fe1e5e03b2db83f8 Mon Sep 17 00:00:00 2001 From: Thorsten Seyschab Date: Sun, 3 May 2026 20:18:22 +0200 Subject: [PATCH 02/12] feat: add drizzle studio proxy --- nuxt.config.ts | 1 + .../drizzle-studio/app/[...asset].get.ts | 45 ++++++++ .../api/admin/drizzle-studio/app/index.get.ts | 35 ++++++ server/routes/index.post.ts | 54 ++++++++++ server/routes/init.ts | 10 ++ server/utils/drizzle-studio.ts | 100 ++++++++++++++++++ 6 files changed, 245 insertions(+) create mode 100644 server/api/admin/drizzle-studio/app/[...asset].get.ts create mode 100644 server/api/admin/drizzle-studio/app/index.get.ts create mode 100644 server/routes/index.post.ts create mode 100644 server/routes/init.ts create mode 100644 server/utils/drizzle-studio.ts diff --git a/nuxt.config.ts b/nuxt.config.ts index a37dd46..cca2b92 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -23,6 +23,7 @@ const configBase: InputConfig = { // Private keys (only available server-side) adminPassword: '123', adminUsername: 'admin', + drizzleStudioInternalPort: '64983', jwtSecret: 'tryUJ0zQbstPbTOrezme+Fv+KndzDNRx5lmSeelr2ial2/2yV8HqLeQ2felJafqf', // Public keys (available on both client and server) diff --git a/server/api/admin/drizzle-studio/app/[...asset].get.ts b/server/api/admin/drizzle-studio/app/[...asset].get.ts new file mode 100644 index 0000000..1482d04 --- /dev/null +++ b/server/api/admin/drizzle-studio/app/[...asset].get.ts @@ -0,0 +1,45 @@ +import { verifyAdmin } from '../../../../utils/auth' + +const DRIZZLE_STUDIO_APP_ORIGIN = 'https://local.drizzle.studio' +const FORWARDED_HEADERS = [ + 'cache-control', + 'content-type', + 'etag', + 'last-modified', +] + +export default defineEventHandler(async (event) => { + await verifyAdmin(event) + + const assetPath = event.context.params?.asset + + if (!assetPath) { + throw createError({ + statusCode: 404, + statusMessage: 'Drizzle Studio asset not found', + }) + } + + const requestUrl = getRequestURL(event) + const upstreamUrl = `${DRIZZLE_STUDIO_APP_ORIGIN}/${assetPath}${requestUrl.search}` + const response = await fetch(upstreamUrl) + + if (!response.ok) { + throw createError({ + statusCode: response.status, + statusMessage: `Failed to load Drizzle Studio asset: ${assetPath}`, + }) + } + + setResponseStatus(event, response.status, response.statusText) + + for (const header of FORWARDED_HEADERS) { + const value = response.headers.get(header) + + if (value) { + setHeader(event, header, value) + } + } + + return new Uint8Array(await response.arrayBuffer()) +}) diff --git a/server/api/admin/drizzle-studio/app/index.get.ts b/server/api/admin/drizzle-studio/app/index.get.ts new file mode 100644 index 0000000..c49ee1b --- /dev/null +++ b/server/api/admin/drizzle-studio/app/index.get.ts @@ -0,0 +1,35 @@ +import { verifyAdmin } from '../../../../utils/auth' + +const DRIZZLE_STUDIO_APP_ORIGIN = 'https://local.drizzle.studio' + +/** Removes third-party analytics from the upstream Studio shell. */ +function sanitizeStudioHtml(html: string) { + const analyticsScript = new RegExp( + ' + +