import fs from 'node:fs/promises'; import path from 'node:path'; import { DatabaseSync } from 'node:sqlite'; import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; const dataDirectory = path.resolve(process.cwd(), 'data'); const sqlitePath = path.join(dataDirectory, 'flow.db'); const seedJsonPath = path.join(dataDirectory, 'flow-db.json'); let database; async function readSeedJson() { try { const raw = await fs.readFile(seedJsonPath, 'utf8'); return JSON.parse(raw); } catch (error) { if (error.code === 'ENOENT') return null; throw error; } } async function getDatabase() { if (database) return database; await fs.mkdir(dataDirectory, { recursive: true }); database = new DatabaseSync(sqlitePath); database.exec(` CREATE TABLE IF NOT EXISTS app_state ( id INTEGER PRIMARY KEY CHECK (id = 1), content TEXT NOT NULL, program_states TEXT NOT NULL, updated_at TEXT NOT NULL ) `); const existing = database.prepare('SELECT id FROM app_state WHERE id = 1').get(); if (!existing) { const seed = await readSeedJson(); if (seed) writeDatabase(seed); } else { const seed = await readSeedJson(); const row = database.prepare('SELECT content FROM app_state WHERE id = 1').get(); const existingContent = row ? JSON.parse(row.content) : null; const existingExtraCount = existingContent?.extraPrograms?.length ?? 0; const seedExtraCount = seed?.content?.extraPrograms?.length ?? 0; if (existingExtraCount === 0 && seedExtraCount > 0) { writeDatabase(seed); } } return database; } async function readDatabase() { const db = await getDatabase(); const row = db .prepare('SELECT content, program_states AS programStates, updated_at AS updatedAt FROM app_state WHERE id = 1') .get(); if (!row) return null; return { content: JSON.parse(row.content), programStates: JSON.parse(row.programStates), updatedAt: row.updatedAt }; } async function writeDatabase(data) { const db = await getDatabase(); const updatedAt = new Date().toISOString(); const content = data.content ?? {}; const programStates = data.programStates ?? {}; db.prepare(` INSERT INTO app_state (id, content, program_states, updated_at) VALUES (1, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET content = excluded.content, program_states = excluded.program_states, updated_at = excluded.updated_at `).run(JSON.stringify(content), JSON.stringify(programStates), updatedAt); return { content, programStates, updatedAt }; } function readRequestBody(request) { return new Promise((resolve, reject) => { let body = ''; request.on('data', (chunk) => { body += chunk; if (body.length > 5_000_000) { reject(new Error('Request body is too large')); request.destroy(); } }); request.on('end', () => resolve(body)); request.on('error', reject); }); } function sendJson(response, statusCode, payload) { response.statusCode = statusCode; response.setHeader('Content-Type', 'application/json; charset=utf-8'); response.end(JSON.stringify(payload)); } function serverDatabasePlugin() { return { name: 'flow-server-sqlite-database', configureServer(server) { server.middlewares.use('/api/state', async (request, response) => { try { if (request.method === 'GET') { const data = await readDatabase(); if (!data) { sendJson(response, 404, { error: 'Database is empty' }); return; } sendJson(response, 200, data); return; } if (request.method === 'PUT' || request.method === 'PATCH') { const current = request.method === 'PATCH' ? (await readDatabase()) ?? {} : {}; const body = await readRequestBody(request); const payload = body ? JSON.parse(body) : {}; const nextData = await writeDatabase({ ...current, ...payload }); sendJson(response, 200, nextData); return; } response.statusCode = 405; response.end(); } catch (error) { sendJson(response, 500, { error: error.message }); } }); } }; } export default defineConfig({ plugins: [react(), serverDatabasePlugin()] });