From 9b6d8f0555aab0bc059148b29e913051701a6157 Mon Sep 17 00:00:00 2001 From: mikl0s Date: Sun, 22 Feb 2026 21:50:10 +0000 Subject: [PATCH] refactor: clean DB schema, remove users/sessions tables Co-Authored-By: Claude Opus 4.6 --- lib/db.ts | 180 ++++++++++++------------------------------------------ 1 file changed, 38 insertions(+), 142 deletions(-) diff --git a/lib/db.ts b/lib/db.ts index c359d5a..203dac2 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -1,11 +1,10 @@ import Database from "better-sqlite3" import path from "path" +import fs from "fs" -// Database file stored in project root const DB_PATH = path.join(process.cwd(), "data", "quotes.db") // Ensure data directory exists -import fs from "fs" const dataDir = path.dirname(DB_PATH) if (!fs.existsSync(dataDir)) { fs.mkdirSync(dataDir, { recursive: true }) @@ -13,7 +12,10 @@ if (!fs.existsSync(dataDir)) { const db = new Database(DB_PATH) -// Initialize database schema +// Enable WAL mode for better concurrent read performance +db.pragma("journal_mode = WAL") + +// Create schema if not exists db.exec(` CREATE TABLE IF NOT EXISTS quotes ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -21,65 +23,38 @@ db.exec(` address TEXT, area REAL NOT NULL, height REAL NOT NULL, - include_floor_heating INTEGER NOT NULL DEFAULT 1, - flooring_type TEXT NOT NULL DEFAULT 'STANDARD', + include_floor_heating INTEGER DEFAULT 1, + flooring_type TEXT DEFAULT 'STANDARD', customer_name TEXT NOT NULL, customer_email TEXT NOT NULL, customer_phone TEXT NOT NULL, remarks TEXT, - total_excl_vat REAL, - total_incl_vat REAL, - status TEXT NOT NULL DEFAULT 'new', - created_at TEXT DEFAULT CURRENT_TIMESTAMP + total_excl_vat REAL NOT NULL, + total_incl_vat REAL NOT NULL, + status TEXT DEFAULT 'new', + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + email_opened_at TEXT ) `) -// Migration: Add status column if it doesn't exist -try { - db.exec("ALTER TABLE quotes ADD COLUMN status TEXT NOT NULL DEFAULT 'new'") -} catch { - // Column already exists -} - -// Migration: Add email_opened_at column for tracking -try { - db.exec("ALTER TABLE quotes ADD COLUMN email_opened_at TEXT") -} catch { - // Column already exists -} - -// Users table for authentication -db.exec(` - CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - email TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - name TEXT NOT NULL, - created_at TEXT DEFAULT CURRENT_TIMESTAMP - ) -`) - -// Sessions table for login sessions -db.exec(` - CREATE TABLE IF NOT EXISTS sessions ( - id TEXT PRIMARY KEY, - user_id INTEGER NOT NULL, - expires_at TEXT NOT NULL, - FOREIGN KEY (user_id) REFERENCES users(id) - ) -`) - -// Start IDs at 1000 +// Ensure quote IDs start at 1000 const countResult = db.prepare("SELECT COUNT(*) as count FROM quotes").get() as { count: number } if (countResult.count === 0) { - db.exec( - "INSERT INTO quotes (id, postal_code, area, height, customer_name, customer_email, customer_phone) VALUES (999, '0000', 0, 0, 'init', 'init', 'init')" - ) - db.exec("DELETE FROM quotes WHERE id = 999") - // Reset autoincrement to start at 1000 - db.exec("UPDATE sqlite_sequence SET seq = 999 WHERE name = 'quotes'") + try { + const seqExists = db.prepare("SELECT seq FROM sqlite_sequence WHERE name = 'quotes'").get() + if (!seqExists) { + db.exec( + "INSERT INTO quotes (id, postal_code, area, height, customer_name, customer_email, customer_phone, total_excl_vat, total_incl_vat) VALUES (999, '0000', 0, 0, 'init', 'init', 'init', 0, 0)" + ) + db.exec("DELETE FROM quotes WHERE id = 999") + } + } catch { + // sqlite_sequence may not exist yet on fresh DB — that's fine + } } +// ─── Types ───────────────────────────────────────────────────────── + export interface QuoteInput { postalCode: string address?: string @@ -91,8 +66,8 @@ export interface QuoteInput { customerEmail: string customerPhone: string remarks?: string - totalExclVat?: number - totalInclVat?: number + totalExclVat: number + totalInclVat: number } export type QuoteStatus = "new" | "contacted" | "accepted" | "rejected" @@ -104,6 +79,8 @@ export interface StoredQuote extends QuoteInput { emailOpenedAt: string | null } +// ─── Queries ─────────────────────────────────────────────────────── + export function saveQuote(quote: QuoteInput): { id: number; slug: string } { const stmt = db.prepare(` INSERT INTO quotes ( @@ -123,8 +100,8 @@ export function saveQuote(quote: QuoteInput): { id: number; slug: string } { quote.customerEmail, quote.customerPhone, quote.remarks || null, - quote.totalExclVat || null, - quote.totalInclVat || null + quote.totalExclVat, + quote.totalInclVat ) const id = result.lastInsertRowid as number @@ -140,39 +117,32 @@ export function getQuoteBySlug(slug: string): StoredQuote | null { const [, postalCode, idStr] = match const id = parseInt(idStr, 10) - const stmt = db.prepare("SELECT * FROM quotes WHERE id = ? AND postal_code = ?") - const row = stmt.get(id, postalCode) as any - + const row = db.prepare("SELECT * FROM quotes WHERE id = ? AND postal_code = ?").get(id, postalCode) as any if (!row) return null return rowToQuote(row) } export function getQuoteById(id: number): StoredQuote | null { - const stmt = db.prepare("SELECT * FROM quotes WHERE id = ?") - const row = stmt.get(id) as any + const row = db.prepare("SELECT * FROM quotes WHERE id = ?").get(id) as any if (!row) return null return rowToQuote(row) } export function getAllQuotes(): StoredQuote[] { - const stmt = db.prepare("SELECT * FROM quotes ORDER BY id DESC") - const rows = stmt.all() as any[] + const rows = db.prepare("SELECT * FROM quotes ORDER BY id DESC").all() as any[] return rows.map(rowToQuote) } export function updateQuoteStatus(id: number, status: QuoteStatus): boolean { - const stmt = db.prepare("UPDATE quotes SET status = ? WHERE id = ?") - const result = stmt.run(status, id) + const result = db.prepare("UPDATE quotes SET status = ? WHERE id = ?").run(status, id) return result.changes > 0 } export function markEmailOpened(id: number): boolean { - // Only update if not already set (first open) - const stmt = db.prepare( + const result = db.prepare( "UPDATE quotes SET email_opened_at = ? WHERE id = ? AND email_opened_at IS NULL" - ) - const result = stmt.run(new Date().toISOString(), id) + ).run(new Date().toISOString(), id) return result.changes > 0 } @@ -196,77 +166,3 @@ function rowToQuote(row: any): StoredQuote { emailOpenedAt: row.email_opened_at || null, } } - -// User management -export interface User { - id: number - email: string - name: string - createdAt: string -} - -export interface UserWithPassword extends User { - passwordHash: string -} - -export function createUser(email: string, passwordHash: string, name: string): User { - const stmt = db.prepare("INSERT INTO users (email, password_hash, name) VALUES (?, ?, ?)") - const result = stmt.run(email, passwordHash, name) - return { - id: result.lastInsertRowid as number, - email, - name, - createdAt: new Date().toISOString(), - } -} - -export function getUserByEmail(email: string): UserWithPassword | null { - const stmt = db.prepare("SELECT * FROM users WHERE email = ?") - const row = stmt.get(email) as any - if (!row) return null - return { - id: row.id, - email: row.email, - name: row.name, - passwordHash: row.password_hash, - createdAt: row.created_at, - } -} - -export function getUserById(id: number): User | null { - const stmt = db.prepare("SELECT id, email, name, created_at FROM users WHERE id = ?") - const row = stmt.get(id) as any - if (!row) return null - return { - id: row.id, - email: row.email, - name: row.name, - createdAt: row.created_at, - } -} - -// Session management -export function createSession(sessionId: string, userId: number, expiresAt: Date): void { - const stmt = db.prepare("INSERT INTO sessions (id, user_id, expires_at) VALUES (?, ?, ?)") - stmt.run(sessionId, userId, expiresAt.toISOString()) -} - -export function getSession(sessionId: string): { userId: number; expiresAt: Date } | null { - const stmt = db.prepare("SELECT user_id, expires_at FROM sessions WHERE id = ?") - const row = stmt.get(sessionId) as any - if (!row) return null - return { - userId: row.user_id, - expiresAt: new Date(row.expires_at), - } -} - -export function deleteSession(sessionId: string): void { - const stmt = db.prepare("DELETE FROM sessions WHERE id = ?") - stmt.run(sessionId) -} - -export function cleanExpiredSessions(): void { - const stmt = db.prepare("DELETE FROM sessions WHERE expires_at < ?") - stmt.run(new Date().toISOString()) -}