This commit is contained in:
parent
4235ed7a50
commit
4e57efdc53
|
|
@ -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"]
|
||||||
|
|
|
||||||
118
server/db.js
118
server/db.js
|
|
@ -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()
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue