/** * API Routes for Test Data Database * * Fixed version - uses a single persistent database connection instead of * opening and closing on every request. WAL journal mode enabled for * concurrent read support. Limit parameter capped at 1000. */ const express = require('express'); const path = require('path'); const Database = require('better-sqlite3'); const { generateDatasheet } = require('../templates/datasheet'); const router = express.Router(); // --------------------------------------------------------------------------- // Singleton database connection - opened once at module load // --------------------------------------------------------------------------- const DB_PATH = path.join(__dirname, '..', 'database', 'testdata.db'); const db = new Database(DB_PATH, { readonly: false }); db.pragma('journal_mode = WAL'); db.pragma('busy_timeout = 5000'); // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- const MAX_LIMIT = 1000; function clampLimit(value) { const parsed = parseInt(value, 10); if (isNaN(parsed) || parsed < 1) return 100; return Math.min(parsed, MAX_LIMIT); } function clampOffset(value) { const parsed = parseInt(value, 10); if (isNaN(parsed) || parsed < 0) return 0; return parsed; } // --------------------------------------------------------------------------- // GET /api/search // Search test records // Query params: serial, model, from, to, result, q, station, logtype, limit, offset // --------------------------------------------------------------------------- router.get('/search', (req, res) => { try { const { serial, model, from, to, result, q, station, logtype } = req.query; const limit = clampLimit(req.query.limit || 100); const offset = clampOffset(req.query.offset || 0); let sql = 'SELECT * FROM test_records WHERE 1=1'; const params = []; if (serial) { sql += ' AND serial_number LIKE ?'; params.push(serial.includes('%') ? serial : `%${serial}%`); } if (model) { sql += ' AND model_number LIKE ?'; params.push(model.includes('%') ? model : `%${model}%`); } if (from) { sql += ' AND test_date >= ?'; params.push(from); } if (to) { sql += ' AND test_date <= ?'; params.push(to); } if (result) { sql += ' AND overall_result = ?'; params.push(result.toUpperCase()); } if (station) { sql += ' AND test_station = ?'; params.push(station); } if (logtype) { sql += ' AND log_type = ?'; params.push(logtype); } if (q) { // Full-text search - rebuild query with FTS sql = `SELECT test_records.* FROM test_records JOIN test_records_fts ON test_records.id = test_records_fts.rowid WHERE test_records_fts MATCH ?`; params.length = 0; params.push(q); if (serial) { sql += ' AND serial_number LIKE ?'; params.push(serial.includes('%') ? serial : `%${serial}%`); } if (model) { sql += ' AND model_number LIKE ?'; params.push(model.includes('%') ? model : `%${model}%`); } if (station) { sql += ' AND test_station = ?'; params.push(station); } if (logtype) { sql += ' AND log_type = ?'; params.push(logtype); } if (result) { sql += ' AND overall_result = ?'; params.push(result.toUpperCase()); } if (from) { sql += ' AND test_date >= ?'; params.push(from); } if (to) { sql += ' AND test_date <= ?'; params.push(to); } } sql += ' ORDER BY test_date DESC, serial_number'; sql += ' LIMIT ? OFFSET ?'; params.push(limit, offset); const records = db.prepare(sql).all(...params); // Get total count let countSql = sql.replace(/SELECT .* FROM/, 'SELECT COUNT(*) as count FROM') .replace(/ORDER BY.*$/, ''); countSql = countSql.replace(/LIMIT \? OFFSET \?/, ''); const countParams = params.slice(0, -2); const total = db.prepare(countSql).get(...countParams); res.json({ records, total: total?.count || records.length, limit, offset }); } catch (err) { console.error(`[${new Date().toISOString()}] [SEARCH ERROR] ${err.message}`); res.status(500).json({ error: err.message }); } }); // --------------------------------------------------------------------------- // GET /api/record/:id // Get single record by ID // --------------------------------------------------------------------------- router.get('/record/:id', (req, res) => { try { const record = db.prepare('SELECT * FROM test_records WHERE id = ?').get(req.params.id); if (!record) { return res.status(404).json({ error: 'Record not found' }); } res.json(record); } catch (err) { console.error(`[${new Date().toISOString()}] [RECORD ERROR] ${err.message}`); res.status(500).json({ error: err.message }); } }); // --------------------------------------------------------------------------- // GET /api/datasheet/:id // Generate datasheet for a record // Query params: format (html, txt) // --------------------------------------------------------------------------- router.get('/datasheet/:id', (req, res) => { try { const record = db.prepare('SELECT * FROM test_records WHERE id = ?').get(req.params.id); if (!record) { return res.status(404).json({ error: 'Record not found' }); } const format = req.query.format || 'html'; const datasheet = generateDatasheet(record, format); if (format === 'html') { res.type('html').send(datasheet); } else { res.type('text/plain').send(datasheet); } } catch (err) { console.error(`[${new Date().toISOString()}] [DATASHEET ERROR] ${err.message}`); res.status(500).json({ error: err.message }); } }); // --------------------------------------------------------------------------- // GET /api/stats // Get database statistics // --------------------------------------------------------------------------- router.get('/stats', (req, res) => { try { const stats = { total_records: db.prepare('SELECT COUNT(*) as count FROM test_records').get().count, by_log_type: db.prepare(` SELECT log_type, COUNT(*) as count FROM test_records GROUP BY log_type ORDER BY count DESC `).all(), by_result: db.prepare(` SELECT overall_result, COUNT(*) as count FROM test_records GROUP BY overall_result `).all(), by_station: db.prepare(` SELECT test_station, COUNT(*) as count FROM test_records WHERE test_station IS NOT NULL AND test_station != '' GROUP BY test_station ORDER BY test_station `).all(), date_range: db.prepare(` SELECT MIN(test_date) as oldest, MAX(test_date) as newest FROM test_records `).get(), recent_serials: db.prepare(` SELECT DISTINCT serial_number, model_number, test_date FROM test_records ORDER BY test_date DESC LIMIT 10 `).all() }; res.json(stats); } catch (err) { console.error(`[${new Date().toISOString()}] [STATS ERROR] ${err.message}`); res.status(500).json({ error: err.message }); } }); // --------------------------------------------------------------------------- // GET /api/filters // Get available filter options (test stations, log types, models) // --------------------------------------------------------------------------- router.get('/filters', (req, res) => { try { const filters = { stations: db.prepare(` SELECT DISTINCT test_station FROM test_records WHERE test_station IS NOT NULL AND test_station != '' ORDER BY test_station `).all().map(r => r.test_station), log_types: db.prepare(` SELECT DISTINCT log_type FROM test_records ORDER BY log_type `).all().map(r => r.log_type), models: db.prepare(` SELECT DISTINCT model_number, COUNT(*) as count FROM test_records GROUP BY model_number ORDER BY count DESC LIMIT 500 `).all() }; res.json(filters); } catch (err) { console.error(`[${new Date().toISOString()}] [FILTERS ERROR] ${err.message}`); res.status(500).json({ error: err.message }); } }); // --------------------------------------------------------------------------- // GET /api/export // Export search results as CSV // --------------------------------------------------------------------------- router.get('/export', (req, res) => { try { const { serial, model, from, to, result, station, logtype } = req.query; let sql = 'SELECT * FROM test_records WHERE 1=1'; const params = []; if (serial) { sql += ' AND serial_number LIKE ?'; params.push(serial.includes('%') ? serial : `%${serial}%`); } if (model) { sql += ' AND model_number LIKE ?'; params.push(model.includes('%') ? model : `%${model}%`); } if (from) { sql += ' AND test_date >= ?'; params.push(from); } if (to) { sql += ' AND test_date <= ?'; params.push(to); } if (result) { sql += ' AND overall_result = ?'; params.push(result.toUpperCase()); } if (station) { sql += ' AND test_station = ?'; params.push(station); } if (logtype) { sql += ' AND log_type = ?'; params.push(logtype); } sql += ' ORDER BY test_date DESC, serial_number LIMIT 10000'; const records = db.prepare(sql).all(...params); // Generate CSV const headers = ['id', 'log_type', 'model_number', 'serial_number', 'test_date', 'test_station', 'overall_result', 'source_file']; let csv = headers.join(',') + '\n'; for (const record of records) { const row = headers.map(h => { const val = record[h] || ''; return `"${String(val).replace(/"/g, '""')}"`; }); csv += row.join(',') + '\n'; } res.setHeader('Content-Type', 'text/csv'); res.setHeader('Content-Disposition', 'attachment; filename=test_records.csv'); res.send(csv); } catch (err) { console.error(`[${new Date().toISOString()}] [EXPORT ERROR] ${err.message}`); res.status(500).json({ error: err.message }); } }); // --------------------------------------------------------------------------- // Cleanup function for graceful shutdown // --------------------------------------------------------------------------- function cleanup() { try { db.close(); } catch (err) { console.error(`[${new Date().toISOString()}] [CLEANUP ERROR] ${err.message}`); } } module.exports = router; module.exports.cleanup = cleanup;