Files
tally-app-svelte/backend/index.js

121 lines
4.0 KiB
JavaScript

const express = require('express');
const sqlite3 = require('sqlite3').verbose();
const cors = require('cors');
const path = require('path');
const multer = require('multer');
const fs = require('fs');
const app = express();
// Use DATA_DIR env var if set, otherwise use local "data" folder
const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, 'data');
const dbPath = path.join(DATA_DIR, 'db.sqlite');
const db = new sqlite3.Database(dbPath);
const upload = multer({ dest: path.join(DATA_DIR, 'uploads/') });
app.use(cors());
app.use(express.json());
app.use('/uploads', express.static(path.join(DATA_DIR, 'uploads')));
// Create uploads directory if it doesn't exist
fs.mkdirSync(path.join(DATA_DIR, 'uploads'), { recursive: true });
// Initialize database and ensure counters table exists
db.serialize(() => {
db.run(`CREATE TABLE IF NOT EXISTS counters (
id INTEGER PRIMARY KEY AUTOINCREMENT,
value INTEGER NOT NULL DEFAULT 0,
name TEXT NOT NULL DEFAULT 'Counter',
image TEXT
)`);
});
// Get all counters
app.get('/api/counters', (req, res) => {
db.all('SELECT * FROM counters', (err, rows) => {
if (err) return res.status(500).json({ error: err.message });
// Ensure id and value are numbers
const counters = rows.map(row => ({
...row,
id: Number(row.id),
value: Number(row.value)
}));
res.json(counters);
});
});
// Create a new counter with image
app.post('/api/counters', upload.single('image'), (req, res) => {
const { name = 'Counter', value = 0 } = req.body;
const image = req.file ? `/uploads/${req.file.filename}` : null;
db.run('INSERT INTO counters (name, value, image) VALUES (?, ?, ?)', [name, value, image], function (err) {
if (err) return res.status(500).json({ error: err.message });
res.json({ id: Number(this.lastID), name, value: Number(value), image });
});
});
// Update a counter (now supports image upload)
app.put('/api/counters/:id', upload.single('image'), (req, res) => {
const { name, value } = req.body;
const id = req.params.id;
let image = null;
// If a new image is uploaded, get its path
if (req.file) {
image = `/uploads/${req.file.filename}`;
// Optionally, delete the old image file
db.get('SELECT image FROM counters WHERE id = ?', [id], (err, row) => {
if (row && row.image) {
const oldImagePath = path.join(__dirname, '..', row.image);
fs.unlink(oldImagePath, () => { }); // Ignore errors
}
});
}
// Build dynamic SQL and params
let fields = [];
let params = [];
if (name !== undefined) { fields.push('name = ?'); params.push(name); }
if (value !== undefined) { fields.push('value = ?'); params.push(value); }
if (image !== null) { fields.push('image = ?'); params.push(image); }
if (fields.length === 0) return res.status(400).json({ error: 'No valid fields to update.' });
params.push(id);
const sql = `UPDATE counters SET ${fields.join(', ')} WHERE id = ?`;
db.run(sql, params, function (err) {
if (err) return res.status(500).json({ error: err.message });
res.json({ updated: this.changes });
});
});
// Delete a counter
app.delete('/api/counters/:id', (req, res) => {
db.run('DELETE FROM counters WHERE id = ?', [req.params.id], function (err) {
if (err) return res.status(500).json({ error: err.message });
res.json({ deleted: this.changes });
});
});
// Serve static frontend files
const clientBuildPath = path.join(__dirname, '..', 'frontend', 'dist');
app.use(express.static(clientBuildPath));
// For SPA: serve index.html for any unknown route (after API and uploads)
app.get('/*path', (req, res) => {
if (req.path.startsWith('/api') || req.path.startsWith('/uploads')) return res.status(404).end();
res.sendFile(path.join(clientBuildPath, 'index.html'));
});
const PORT = 3000;
app.listen(PORT, () => {
console.log(`API server running on http://localhost:${PORT}`);
});
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully...');
db.close(() => {
console.log('Database connection closed.');
process.exit(0);
});
});