adding backup and data perstance accross devices

This commit is contained in:
nmemmert
2026-05-28 08:32:33 -04:00
parent 48b3790cd2
commit d8da3744ca
9 changed files with 479 additions and 66 deletions
+100
View File
@@ -0,0 +1,100 @@
import Database from 'better-sqlite3';
import { mkdirSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const DATA_DIR = process.env.DATA_DIR || join(__dirname, 'data');
const DB_PATH = join(DATA_DIR, 'projects.db');
let db;
// ---------------------------------------------------------------------------
// Init — create tables if they don't exist
// ---------------------------------------------------------------------------
export function initDb() {
mkdirSync(DATA_DIR, { recursive: true });
db = new Database(DB_PATH);
// Enable WAL for better concurrent read performance
db.pragma('journal_mode = WAL');
db.exec(`
CREATE TABLE IF NOT EXISTS projects (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
last_edited INTEGER NOT NULL,
chapter_summary TEXT,
data TEXT NOT NULL
);
`);
console.log(`SQLite database ready at ${DB_PATH}`);
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function buildSummary(project) {
if (!Array.isArray(project.chapters)) return '';
return project.chapters.map((ch) => `${ch.book} ${ch.chapter}`).join(', ');
}
// ---------------------------------------------------------------------------
// Queries
// ---------------------------------------------------------------------------
/**
* Returns all project summaries (id, title, lastEdited, chapterSummary).
* Does NOT return full project data to keep the response small.
*/
export function getAllProjects() {
const rows = db.prepare(`
SELECT id, title, last_edited AS lastEdited, chapter_summary AS chapterSummary
FROM projects
ORDER BY last_edited DESC
`).all();
return rows;
}
/**
* Returns a single full project by id, or null if not found.
*/
export function getProject(id) {
const row = db.prepare('SELECT data FROM projects WHERE id = ?').get(id);
if (!row) return null;
try {
return JSON.parse(row.data);
} catch {
return null;
}
}
/**
* Insert or replace a project. Returns the summary.
*/
export function upsertProject(project) {
const lastEdited = project.lastEdited ?? Date.now();
const chapterSummary = buildSummary(project);
const updated = { ...project, lastEdited };
db.prepare(`
INSERT INTO projects (id, title, last_edited, chapter_summary, data)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
title = excluded.title,
last_edited = excluded.last_edited,
chapter_summary = excluded.chapter_summary,
data = excluded.data
`).run(project.id, project.title, lastEdited, chapterSummary, JSON.stringify(updated));
return { id: project.id, title: project.title, lastEdited, chapterSummary };
}
/**
* Delete a project by id. No-op if not found.
*/
export function deleteProject(id) {
db.prepare('DELETE FROM projects WHERE id = ?').run(id);
}
+102
View File
@@ -0,0 +1,102 @@
import express from 'express';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { initDb, getAllProjects, getProject, upsertProject, deleteProject } from './db.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const app = express();
const PORT = process.env.PORT || 3001;
app.use(express.json({ limit: '10mb' }));
// ---------------------------------------------------------------------------
// Health check
// ---------------------------------------------------------------------------
app.get('/api/health', (_req, res) => {
res.json({ ok: true });
});
// ---------------------------------------------------------------------------
// GET /api/projects — list all project summaries (no full data)
// ---------------------------------------------------------------------------
app.get('/api/projects', (_req, res) => {
try {
const projects = getAllProjects();
res.json(projects);
} catch (err) {
console.error('GET /api/projects error:', err);
res.status(500).json({ error: 'Failed to list projects.' });
}
});
// ---------------------------------------------------------------------------
// GET /api/projects/:id — fetch a single full project
// ---------------------------------------------------------------------------
app.get('/api/projects/:id', (req, res) => {
try {
const project = getProject(req.params.id);
if (!project) return res.status(404).json({ error: 'Project not found.' });
res.json(project);
} catch (err) {
console.error('GET /api/projects/:id error:', err);
res.status(500).json({ error: 'Failed to load project.' });
}
});
// ---------------------------------------------------------------------------
// PUT /api/projects/:id — create or update a project
// ---------------------------------------------------------------------------
app.put('/api/projects/:id', (req, res) => {
try {
const body = req.body;
if (!body || typeof body !== 'object') {
return res.status(400).json({ error: 'Invalid JSON body.' });
}
if (!body.id || !body.title || !Array.isArray(body.chapters)) {
return res.status(400).json({ error: 'Missing required fields: id, title, chapters.' });
}
if (body.id !== req.params.id) {
return res.status(400).json({ error: 'URL id does not match body id.' });
}
const saved = upsertProject(body);
res.json(saved);
} catch (err) {
console.error('PUT /api/projects/:id error:', err);
res.status(500).json({ error: 'Failed to save project.' });
}
});
// ---------------------------------------------------------------------------
// DELETE /api/projects/:id — remove a project
// ---------------------------------------------------------------------------
app.delete('/api/projects/:id', (req, res) => {
try {
deleteProject(req.params.id);
res.json({ ok: true });
} catch (err) {
console.error('DELETE /api/projects/:id error:', err);
res.status(500).json({ error: 'Failed to delete project.' });
}
});
// ---------------------------------------------------------------------------
// Serve Vite production build (when NODE_ENV=production)
// ---------------------------------------------------------------------------
if (process.env.NODE_ENV === 'production') {
const distPath = join(__dirname, '..', 'dist');
app.use(express.static(distPath));
app.get('*', (_req, res) => {
res.sendFile(join(distPath, 'index.html'));
});
}
// ---------------------------------------------------------------------------
// Start
// ---------------------------------------------------------------------------
initDb();
app.listen(PORT, () => {
console.log(`Bible Study API running on http://localhost:${PORT}`);
if (process.env.NODE_ENV === 'production') {
console.log('Serving Vite build from /dist');
}
});