fix: switch to sql.js for Railway compatibility

This commit is contained in:
Krishna Kumar
2026-02-04 10:37:55 -06:00
parent e46c1c7063
commit f6d55376f8
2 changed files with 115 additions and 81 deletions

170
index.js
View File

@@ -1,6 +1,7 @@
const express = require('express'); const express = require('express');
const cors = require('cors'); const cors = require('cors');
const Database = require('better-sqlite3'); const initSqlJs = require('sql.js');
const fs = require('fs');
const path = require('path'); const path = require('path');
const app = express(); const app = express();
@@ -8,12 +9,38 @@ const PORT = process.env.PORT || 3000;
const API_KEY = process.env.API_KEY || null; const API_KEY = process.env.API_KEY || null;
const ALLOWED_ORIGINS = (process.env.ALLOWED_ORIGINS || 'https://chatlabsai.com').split(','); const ALLOWED_ORIGINS = (process.env.ALLOWED_ORIGINS || 'https://chatlabsai.com').split(',');
// Initialize SQLite database // Database setup
const dbPath = process.env.DATABASE_PATH || path.join(__dirname, 'data', 'forms.db'); const dataDir = process.env.DATA_DIR || path.join(__dirname, 'data');
const db = new Database(dbPath); const dbPath = path.join(dataDir, 'forms.db');
let db;
// Ensure data directory exists
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
// Save database to file periodically
function saveDb() {
if (db) {
const data = db.export();
fs.writeFileSync(dbPath, Buffer.from(data));
}
}
// Initialize database
async function initDb() {
const SQL = await initSqlJs();
// Load existing database or create new one
if (fs.existsSync(dbPath)) {
const fileBuffer = fs.readFileSync(dbPath);
db = new SQL.Database(fileBuffer);
} else {
db = new SQL.Database();
}
// Create tables // Create tables
db.exec(` db.run(`
CREATE TABLE IF NOT EXISTS submissions ( CREATE TABLE IF NOT EXISTS submissions (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
form_name TEXT NOT NULL, form_name TEXT NOT NULL,
@@ -22,18 +49,22 @@ db.exec(`
ip TEXT, ip TEXT,
user_agent TEXT, user_agent TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP created_at DATETIME DEFAULT CURRENT_TIMESTAMP
); )
CREATE INDEX IF NOT EXISTS idx_form_name ON submissions(form_name);
CREATE INDEX IF NOT EXISTS idx_email ON submissions(email);
CREATE INDEX IF NOT EXISTS idx_created_at ON submissions(created_at);
`); `);
db.run(`CREATE INDEX IF NOT EXISTS idx_form_name ON submissions(form_name)`);
db.run(`CREATE INDEX IF NOT EXISTS idx_email ON submissions(email)`);
saveDb();
// Auto-save every 30 seconds
setInterval(saveDb, 30000);
}
// Middleware // Middleware
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
app.use(cors({ app.use(cors({
origin: (origin, callback) => { origin: (origin, callback) => {
// Allow requests with no origin (mobile apps, curl, etc.)
if (!origin) return callback(null, true); if (!origin) return callback(null, true);
if (ALLOWED_ORIGINS.includes(origin) || ALLOWED_ORIGINS.includes('*')) { if (ALLOWED_ORIGINS.includes(origin) || ALLOWED_ORIGINS.includes('*')) {
return callback(null, true); return callback(null, true);
@@ -58,7 +89,6 @@ app.post('/submit', (req, res) => {
return res.status(400).json({ error: 'Email is required' }); return res.status(400).json({ error: 'Email is required' });
} }
// Basic email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) { if (!emailRegex.test(email)) {
return res.status(400).json({ error: 'Invalid email format' }); return res.status(400).json({ error: 'Invalid email format' });
@@ -67,28 +97,23 @@ app.post('/submit', (req, res) => {
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress; const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
const userAgent = req.headers['user-agent'] || null; const userAgent = req.headers['user-agent'] || null;
const stmt = db.prepare(` db.run(
INSERT INTO submissions (form_name, email, source, ip, user_agent) `INSERT INTO submissions (form_name, email, source, ip, user_agent, created_at) VALUES (?, ?, ?, ?, ?, datetime('now'))`,
VALUES (?, ?, ?, ?, ?) [form_name, email.toLowerCase().trim(), source, ip, userAgent]
`); );
const result = stmt.run(form_name, email.toLowerCase().trim(), source, ip, userAgent); saveDb();
res.json({ res.json({ success: true, message: 'Submission received' });
success: true,
id: result.lastInsertRowid,
message: 'Submission received'
});
} catch (error) { } catch (error) {
console.error('Submit error:', error); console.error('Submit error:', error);
res.status(500).json({ error: 'Internal server error' }); res.status(500).json({ error: 'Internal server error' });
} }
}); });
// Auth middleware for protected routes // Auth middleware
const requireAuth = (req, res, next) => { const requireAuth = (req, res, next) => {
if (!API_KEY) return next(); // No auth if no API_KEY set if (!API_KEY) return next();
const authHeader = req.headers.authorization; const authHeader = req.headers.authorization;
if (!authHeader || authHeader !== `Bearer ${API_KEY}`) { if (!authHeader || authHeader !== `Bearer ${API_KEY}`) {
return res.status(401).json({ error: 'Unauthorized' }); return res.status(401).json({ error: 'Unauthorized' });
@@ -96,7 +121,7 @@ const requireAuth = (req, res, next) => {
next(); next();
}; };
// List submissions (protected) // List submissions
app.get('/emails', requireAuth, (req, res) => { app.get('/emails', requireAuth, (req, res) => {
try { try {
const { form_name, limit = 100, offset = 0 } = req.query; const { form_name, limit = 100, offset = 0 } = req.query;
@@ -112,43 +137,51 @@ app.get('/emails', requireAuth, (req, res) => {
query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?'; query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
params.push(parseInt(limit), parseInt(offset)); params.push(parseInt(limit), parseInt(offset));
const submissions = db.prepare(query).all(...params); const stmt = db.prepare(query);
const countQuery = form_name stmt.bind(params);
? db.prepare('SELECT COUNT(*) as count FROM submissions WHERE form_name = ?').get(form_name)
: db.prepare('SELECT COUNT(*) as count FROM submissions').get();
res.json({ const submissions = [];
submissions, while (stmt.step()) {
total: countQuery.count, submissions.push(stmt.getAsObject());
limit: parseInt(limit), }
offset: parseInt(offset) stmt.free();
});
const countStmt = form_name
? db.prepare('SELECT COUNT(*) as count FROM submissions WHERE form_name = ?')
: db.prepare('SELECT COUNT(*) as count FROM submissions');
if (form_name) countStmt.bind([form_name]);
countStmt.step();
const total = countStmt.getAsObject().count;
countStmt.free();
res.json({ submissions, total, limit: parseInt(limit), offset: parseInt(offset) });
} catch (error) { } catch (error) {
console.error('List error:', error); console.error('List error:', error);
res.status(500).json({ error: 'Internal server error' }); res.status(500).json({ error: 'Internal server error' });
} }
}); });
// Export as CSV (protected) // Export CSV
app.get('/export', requireAuth, (req, res) => { app.get('/export', requireAuth, (req, res) => {
try { try {
const { form_name } = req.query; const { form_name } = req.query;
let query = 'SELECT email, form_name, source, created_at FROM submissions'; let query = 'SELECT email, form_name, source, created_at FROM submissions';
const params = []; if (form_name) query += ' WHERE form_name = ?';
if (form_name) {
query += ' WHERE form_name = ?';
params.push(form_name);
}
query += ' ORDER BY created_at DESC'; query += ' ORDER BY created_at DESC';
const submissions = db.prepare(query).all(...params); const stmt = db.prepare(query);
if (form_name) stmt.bind([form_name]);
const rows = [];
while (stmt.step()) {
rows.push(stmt.getAsObject());
}
stmt.free();
const csv = [ const csv = [
'email,form_name,source,created_at', 'email,form_name,source,created_at',
...submissions.map(s => `${s.email},${s.form_name},${s.source || ''},${s.created_at}`) ...rows.map(s => `${s.email},${s.form_name},${s.source || ''},${s.created_at}`)
].join('\n'); ].join('\n');
res.setHeader('Content-Type', 'text/csv'); res.setHeader('Content-Type', 'text/csv');
@@ -160,41 +193,42 @@ app.get('/export', requireAuth, (req, res) => {
} }
}); });
// Stats (protected) // Stats
app.get('/stats', requireAuth, (req, res) => { app.get('/stats', requireAuth, (req, res) => {
try { try {
const stats = db.prepare(` const stmt = db.prepare(`
SELECT SELECT form_name, COUNT(*) as count,
form_name,
COUNT(*) as count,
MIN(created_at) as first_submission, MIN(created_at) as first_submission,
MAX(created_at) as last_submission MAX(created_at) as last_submission
FROM submissions FROM submissions GROUP BY form_name ORDER BY count DESC
GROUP BY form_name `);
ORDER BY count DESC
`).all();
const total = db.prepare('SELECT COUNT(*) as count FROM submissions').get(); const stats = [];
while (stmt.step()) {
stats.push(stmt.getAsObject());
}
stmt.free();
res.json({ const totalStmt = db.prepare('SELECT COUNT(*) as count FROM submissions');
total: total.count, totalStmt.step();
by_form: stats const total = totalStmt.getAsObject().count;
}); totalStmt.free();
res.json({ total, by_form: stats });
} catch (error) { } catch (error) {
console.error('Stats error:', error); console.error('Stats error:', error);
res.status(500).json({ error: 'Internal server error' }); res.status(500).json({ error: 'Internal server error' });
} }
}); });
// Ensure data directory exists // Start server after DB init
const fs = require('fs'); initDb().then(() => {
const dataDir = path.dirname(dbPath);
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`Form capture service running on port ${PORT}`); console.log(`Form capture running on port ${PORT}`);
console.log(`Allowed origins: ${ALLOWED_ORIGINS.join(', ')}`); console.log(`Allowed origins: ${ALLOWED_ORIGINS.join(', ')}`);
console.log(`API key protection: ${API_KEY ? 'enabled' : 'disabled'}`); console.log(`API key: ${API_KEY ? 'enabled' : 'disabled'}`);
});
}).catch(err => {
console.error('Failed to init database:', err);
process.exit(1);
}); });

View File

@@ -7,7 +7,7 @@
"start": "node index.js" "start": "node index.js"
}, },
"dependencies": { "dependencies": {
"better-sqlite3": "^11.7.0", "sql.js": "^1.11.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.21.2" "express": "^4.21.2"
}, },