feat(backend): SQLite-Schicht mit WAL, Dokumenten und Historie
This commit is contained in:
parent
3b0632c4b9
commit
9714d845bb
2 changed files with 80 additions and 0 deletions
51
server/db.js
Normal file
51
server/db.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const Database = require('better-sqlite3');
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, '..', 'data');
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
const DB_PATH = path.join(DATA_DIR, 'dienstplan.db');
|
||||
|
||||
const db = new Database(DB_PATH);
|
||||
db.pragma('journal_mode = WAL');
|
||||
db.pragma('synchronous = NORMAL');
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS documents (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
replaced_at TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
function getDoc(key) {
|
||||
const row = db.prepare('SELECT value, updated_at FROM documents WHERE key = ?').get(key);
|
||||
if (!row) return null;
|
||||
return { value: JSON.parse(row.value), updatedAt: row.updated_at };
|
||||
}
|
||||
|
||||
// ponytail: history wächst unbegrenzt. Bei Single-User/kleinen Docs jahrelang egal.
|
||||
// Pruning (z.B. > 500 Einträge pro key löschen) nachrüsten, falls es je wächst.
|
||||
function putDoc(key, value, now) {
|
||||
const json = JSON.stringify(value);
|
||||
const tx = db.transaction(() => {
|
||||
const existing = db.prepare('SELECT value FROM documents WHERE key = ?').get(key);
|
||||
if (existing) {
|
||||
db.prepare('INSERT INTO history (key, value, replaced_at) VALUES (?, ?, ?)')
|
||||
.run(key, existing.value, now);
|
||||
}
|
||||
db.prepare(`
|
||||
INSERT INTO documents (key, value, updated_at) VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
|
||||
`).run(key, json, now);
|
||||
});
|
||||
tx();
|
||||
}
|
||||
|
||||
module.exports = { db, getDoc, putDoc, DB_PATH, DATA_DIR };
|
||||
29
server/db.test.js
Normal file
29
server/db.test.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
const { test } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
// DATA_DIR auf Temp umbiegen, BEVOR db.js geladen wird
|
||||
process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'dp-db-'));
|
||||
const { getDoc, putDoc } = require('./db');
|
||||
|
||||
test('putDoc/getDoc Roundtrip', () => {
|
||||
putDoc('employees', ['Max', 'Anna'], '2026-07-07T10:00:00.000Z');
|
||||
const doc = getDoc('employees');
|
||||
assert.deepStrictEqual(doc.value, ['Max', 'Anna']);
|
||||
assert.strictEqual(doc.updatedAt, '2026-07-07T10:00:00.000Z');
|
||||
});
|
||||
|
||||
test('getDoc für unbekannten Key liefert null', () => {
|
||||
assert.strictEqual(getDoc('gibtsnicht'), null);
|
||||
});
|
||||
|
||||
test('putDoc snapshottet den alten Wert in history', () => {
|
||||
const { db } = require('./db');
|
||||
putDoc('duties', { v: 1 }, '2026-07-07T10:00:00.000Z');
|
||||
putDoc('duties', { v: 2 }, '2026-07-07T11:00:00.000Z');
|
||||
const rows = db.prepare("SELECT value FROM history WHERE key = 'duties' ORDER BY id").all();
|
||||
assert.strictEqual(rows.length, 1);
|
||||
assert.deepStrictEqual(JSON.parse(rows[0].value), { v: 1 });
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue