all repos — snow-editor @ 13d855e25e8510a57608b0ce77f62cbeed372d0e

small and cozy markdown, and orgmode editor

backend/src/db.js (view raw)

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
import fs from 'fs';
import path from 'path';
import { DatabaseSync } from 'node:sqlite';

let db;

export function getDb() {
  if (!db) {
    throw new Error('Database not initialized');
  }
  return db;
}

export function initDb(databasePath) {
  const dir = path.dirname(databasePath);
  fs.mkdirSync(dir, { recursive: true });

  db = new DatabaseSync(databasePath);
  db.exec('PRAGMA journal_mode = WAL');
  db.exec('PRAGMA foreign_keys = ON');
  initSchema(db);
  return db;
}

function initSchema(database) {
  database.exec(`
    CREATE TABLE IF NOT EXISTS documents (
      id TEXT PRIMARY KEY,
      title TEXT NOT NULL DEFAULT 'Untitled document',
      mode TEXT NOT NULL CHECK (mode IN ('markdown', 'org')),
      content TEXT NOT NULL DEFAULT '',
      view_token TEXT UNIQUE NOT NULL,
      edit_token TEXT UNIQUE NOT NULL,
      expires_at TEXT,
      created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
      updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
    );

    CREATE TABLE IF NOT EXISTS edit_locks (
      id TEXT PRIMARY KEY,
      document_id TEXT NOT NULL,
      lock_token TEXT UNIQUE NOT NULL,
      client_id TEXT NOT NULL,
      expires_at TEXT NOT NULL,
      created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
      updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE
    );

    CREATE TABLE IF NOT EXISTS document_versions (
      id TEXT PRIMARY KEY,
      document_id TEXT NOT NULL,
      title TEXT NOT NULL,
      mode TEXT NOT NULL CHECK (mode IN ('markdown', 'org')),
      content TEXT NOT NULL,
      created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE
    );

    CREATE INDEX IF NOT EXISTS idx_documents_view_token ON documents(view_token);
    CREATE INDEX IF NOT EXISTS idx_documents_edit_token ON documents(edit_token);
    CREATE INDEX IF NOT EXISTS idx_edit_locks_document_id ON edit_locks(document_id);
    CREATE INDEX IF NOT EXISTS idx_edit_locks_lock_token ON edit_locks(lock_token);
  `);
}

export function purgeExpiredLocks(database) {
  const now = new Date().toISOString();
  database.prepare('DELETE FROM edit_locks WHERE expires_at <= ?').run(now);
}

// Expired documents only ever answered 410 before; the rows (and their
// versions) stayed in SQLite forever. Locks and versions cascade via FK.
export function purgeExpiredDocuments(database) {
  const now = new Date().toISOString();
  const result = database
    .prepare(
      'DELETE FROM documents WHERE expires_at IS NOT NULL AND expires_at <= ?',
    )
    .run(now);
  return result.changes;
}

export function checkDbHealth(database) {
  try {
    database.prepare('SELECT 1 AS ok').get();
    return true;
  } catch {
    return false;
  }
}