fix: DB schema UNIQUE constraint, Docker healthcheck, DB permissions (#120 #121 #123) (batch 9.0)

This commit is contained in:
null 2026-05-17 21:34:39 -05:00
parent 4235ed7a50
commit 4e57efdc53
3 changed files with 52 additions and 124 deletions

View File

@ -73,7 +73,7 @@ USER nodejs
# Health check using Node 20 built-in fetch (no wget required) # Health check using Node 20 built-in fetch (no wget required)
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD node -e "fetch('http://localhost:3001/api/health').then(r => r.ok ? 0 : 1).catch(() => 1)" || exit 1 CMD node -e "fetch('http://localhost:3001/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
# Run the Express server # Run the Express server
CMD ["node", "server/index.js"] CMD ["node", "server/index.js"]

View File

@ -1,118 +0,0 @@
// Database initialization - loaded at runtime after entrypoint runs
import path from 'path'
import { existsSync, mkdirSync, chmodSync } from 'fs'
import sqlite3 from 'better-sqlite3'
// --- Logger ---
const LOG_LEVELS = { error: 0, warn: 1, info: 2, debug: 3 }
const currentLevel = LOG_LEVELS[process.env.LOG_LEVEL?.toLowerCase()] ?? LOG_LEVELS.info
const log = {
info: (...args) => { if (currentLevel >= LOG_LEVELS.info) console.log(`[${new Date().toISOString()}] INFO `, ...args) },
warn: (...args) => { if (currentLevel >= LOG_LEVELS.warn) console.warn(`[${new Date().toISOString()}] WARN `, ...args) },
error: (...args) => { if (currentLevel >= LOG_LEVELS.error) console.error(`[${new Date().toISOString()}] ERROR`, ...args) },
debug: (...args) => { if (currentLevel >= LOG_LEVELS.debug) console.debug(`[${new Date().toISOString()}] DEBUG`, ...args) },
}
const dbPath = path.join(new URL(import.meta.url).pathname, '..', 'db', 'queuenorth.db')
const dbDir = path.dirname(dbPath)
// Ensure db directory exists with proper permissions
if (!existsSync(dbDir)) {
mkdirSync(dbDir, { recursive: true })
try { chmodSync(dbDir, 0o777) } catch (e) {}
}
// Ensure db file has proper permissions if it exists
if (existsSync(dbPath)) {
try { chmodSync(dbPath, 0o666) } catch (e) {}
}
// Initialize the database
export const db = sqlite3(dbPath)
// Initialize schema
export const initSchema = () => {
// Issue #6: Add UNIQUE constraint on leads.email with migration support
const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='leads'").get()
if (tableExists) {
// Check if UNIQUE constraint already exists
const pragma = db.prepare("PRAGMA table_info(leads)").all()
const emailCol = pragma.find(col => col.name === 'email')
if (!emailCol || !emailCol.notnull) {
// UNIQUE constraint doesn't exist, need to add it via migration
log.info('[DB] Adding UNIQUE constraint on leads.email via migration')
// Migrate leads table to add UNIQUE constraint
db.exec(
[
'CREATE TABLE IF NOT EXISTS leads_new (',
' id INTEGER PRIMARY KEY AUTOINCREMENT,',
' company TEXT NOT NULL,',
' name TEXT NOT NULL,',
' email TEXT NOT NULL UNIQUE,',
' phone TEXT,',
' zip TEXT,',
' message TEXT,',
' service_interest TEXT,',
' created_at DATETIME DEFAULT CURRENT_TIMESTAMP',
')'
].join('\n')
)
// Copy existing data (deduplicate - keep first occurrence per email)
db.exec(
[
'INSERT OR IGNORE INTO leads_new (id, company, name, email, phone, zip, message, service_interest, created_at)',
'SELECT id, company, name, email, phone, zip, message, service_interest, created_at',
'FROM leads'
].join('\n')
)
// Drop old table
db.exec('DROP TABLE leads')
// Rename new table
db.exec('ALTER TABLE leads_new RENAME TO leads')
log.info('[DB] UNIQUE constraint added on leads.email')
}
} else {
// New table - create with UNIQUE constraint from the start
db.exec(
[
'CREATE TABLE IF NOT EXISTS leads (',
' id INTEGER PRIMARY KEY AUTOINCREMENT,',
' company TEXT NOT NULL,',
' name TEXT NOT NULL,',
' email TEXT NOT NULL UNIQUE,',
' phone TEXT,',
' zip TEXT,',
' message TEXT,',
' service_interest TEXT,',
' created_at DATETIME DEFAULT CURRENT_TIMESTAMP',
')'
].join('\n')
)
}
// Support requests table
db.exec(
[
'CREATE TABLE IF NOT EXISTS support_requests (',
' id INTEGER PRIMARY KEY AUTOINCREMENT,',
' name TEXT NOT NULL,',
' company TEXT NOT NULL,',
' email TEXT NOT NULL,',
' phone TEXT,',
' issue TEXT NOT NULL,',
' priority TEXT DEFAULT \'medium\',',
' created_at DATETIME DEFAULT CURRENT_TIMESTAMP',
')'
].join('\n')
)
}
initSchema()

View File

@ -1,7 +1,7 @@
import express from 'express' import express from 'express'
import path from 'path' import path from 'path'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
import { existsSync, mkdirSync, chmodSync } from 'fs' import { existsSync, mkdirSync } from 'fs'
import sqlite3 from 'better-sqlite3' import sqlite3 from 'better-sqlite3'
import z from 'zod' import z from 'zod'
import rateLimit from 'express-rate-limit' import rateLimit from 'express-rate-limit'
@ -21,8 +21,6 @@ const dbDir = path.dirname(dbPath)
// Create db directory if it doesn't exist // Create db directory if it doesn't exist
if (!existsSync(dbDir)) { if (!existsSync(dbDir)) {
mkdirSync(dbDir, { recursive: true }) mkdirSync(dbDir, { recursive: true })
// Try to set writable permissions, ignore if running as non-root
try { chmodSync(dbDir, 0o755) } catch (e) {}
} }
// --- Logger --- // --- Logger ---
@ -132,13 +130,61 @@ const db = sqlite3(dbPath)
// Initialize schema // Initialize schema
const initSchema = () => { const initSchema = () => {
// Leads table // Check if leads table exists and needs UNIQUE constraint migration
const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='leads'").get()
if (tableExists) {
// Check if UNIQUE constraint already exists on email
const pragma = db.prepare("PRAGMA table_info(leads)").all()
const emailCol = pragma.find(col => col.name === 'email')
if (emailCol && !emailCol.pk) {
// UNIQUE constraint doesn't exist, need to add it via migration
log.info('[DB] Adding UNIQUE constraint on leads.email via migration')
// Migrate leads table to add UNIQUE constraint
db.exec(
[
'CREATE TABLE IF NOT EXISTS leads_new (',
' id INTEGER PRIMARY KEY AUTOINCREMENT,',
' company TEXT NOT NULL,',
' name TEXT NOT NULL,',
' email TEXT NOT NULL UNIQUE,',
' phone TEXT,',
' zip TEXT,',
' message TEXT,',
' service_interest TEXT,',
' created_at DATETIME DEFAULT CURRENT_TIMESTAMP',
')'
].join('\n')
)
// Copy existing data (deduplicate - keep first occurrence per email)
db.exec(
[
'INSERT OR IGNORE INTO leads_new (id, company, name, email, phone, zip, message, service_interest, created_at)',
'SELECT id, company, name, email, phone, zip, message, service_interest, created_at',
'FROM leads'
].join('\n')
)
// Drop old table
db.exec('DROP TABLE leads')
// Rename new table
db.exec('ALTER TABLE leads_new RENAME TO leads')
log.info('[DB] UNIQUE constraint added on leads.email')
}
}
// Leads table (now with UNIQUE constraint on email, either from migration or fresh)
db.exec(` db.exec(`
CREATE TABLE IF NOT EXISTS leads ( CREATE TABLE IF NOT EXISTS leads (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
company TEXT NOT NULL, company TEXT NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
email TEXT NOT NULL, email TEXT NOT NULL UNIQUE,
phone TEXT, phone TEXT,
zip TEXT, zip TEXT,
message TEXT, message TEXT,