153 lines
4.1 KiB
JavaScript
153 lines
4.1 KiB
JavaScript
/**
|
|
* @cc-platform/server/db — SQLite database factory
|
|
*
|
|
* Extracted from the common setup patterns in both
|
|
* remoteturtle/server/database.js and Inventory-Manager-CC/web/server/db.js.
|
|
*
|
|
* Provides:
|
|
* - Database creation with WAL mode and production-ready pragmas
|
|
* - Directory validation and creation
|
|
* - Schema initialization
|
|
* - Debounced save support (batch writes on intervals)
|
|
* - Clean shutdown with flush
|
|
*
|
|
* Usage:
|
|
* const { createDatabase } = require('@cc-platform/server/db');
|
|
* const { db, debouncedSave, close } = createDatabase('/data/myservice.db', {
|
|
* schema: `CREATE TABLE IF NOT EXISTS items (...)`,
|
|
* debounceMs: 2000,
|
|
* });
|
|
*
|
|
* // Use db directly for queries
|
|
* const row = db.prepare('SELECT * FROM items WHERE id = ?').get(1);
|
|
*
|
|
* // Use debouncedSave to batch expensive saves
|
|
* debouncedSave(() => { ... bulk insert ... });
|
|
*
|
|
* // On shutdown
|
|
* close();
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const Database = require('better-sqlite3');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
|
|
/**
|
|
* @typedef {Object} DatabaseOptions
|
|
* @property {string} [schema] SQL to execute on init (CREATE TABLE IF NOT EXISTS ...)
|
|
* @property {number} [debounceMs] Debounce interval for batched saves (default: 2000)
|
|
* @property {number} [cacheSize] SQLite cache size pragma (default: -8000 = ~8MB)
|
|
* @property {string} [tempStore] SQLite temp_store pragma (default: 'MEMORY')
|
|
* @property {boolean} [foreignKeys] Enable foreign keys (default: true)
|
|
*/
|
|
|
|
/**
|
|
* Create and initialize a SQLite database with production-ready defaults.
|
|
*
|
|
* @param {string} dbPath - Path to the database file
|
|
* @param {DatabaseOptions} opts - Configuration options
|
|
* @returns {{ db, debouncedSave, flushPendingSave, close }}
|
|
*/
|
|
function createDatabase(dbPath, opts = {}) {
|
|
// Ensure directory exists and is writable
|
|
const dir = path.dirname(path.resolve(dbPath));
|
|
if (!fs.existsSync(dir)) {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
}
|
|
|
|
// Verify directory is writable
|
|
try {
|
|
fs.accessSync(dir, fs.constants.W_OK);
|
|
} catch (e) {
|
|
throw new Error(`Database directory is not writable: ${dir}`);
|
|
}
|
|
|
|
const db = new Database(dbPath);
|
|
|
|
// Production-ready pragmas
|
|
db.pragma('journal_mode = WAL');
|
|
db.pragma('synchronous = NORMAL');
|
|
|
|
if (opts.foreignKeys !== false) {
|
|
db.pragma('foreign_keys = ON');
|
|
}
|
|
if (opts.cacheSize) {
|
|
db.pragma(`cache_size = ${opts.cacheSize}`);
|
|
}
|
|
if (opts.tempStore) {
|
|
db.pragma(`temp_store = ${opts.tempStore}`);
|
|
}
|
|
|
|
// Schema initialization
|
|
if (opts.schema) {
|
|
db.exec(opts.schema);
|
|
}
|
|
|
|
// --- Debounced save support ---
|
|
const debounceMs = opts.debounceMs || 2000;
|
|
let saveTimer = null;
|
|
let pendingSave = null;
|
|
|
|
/**
|
|
* Schedule a save function to run after the debounce interval.
|
|
* If called again before the timer fires, the latest save function replaces
|
|
* the previous one (only the most recent save runs).
|
|
*
|
|
* @param {Function} saveFn - The save operation to execute
|
|
*/
|
|
function debouncedSave(saveFn) {
|
|
pendingSave = saveFn;
|
|
if (!saveTimer) {
|
|
saveTimer = setTimeout(() => {
|
|
saveTimer = null;
|
|
if (pendingSave) {
|
|
try {
|
|
pendingSave();
|
|
} catch (e) {
|
|
console.error('[db] Debounced save error:', e.message);
|
|
}
|
|
pendingSave = null;
|
|
}
|
|
}, debounceMs);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Immediately execute any pending save and clear the timer.
|
|
* Call this during shutdown to avoid data loss.
|
|
*/
|
|
function flushPendingSave() {
|
|
if (saveTimer) {
|
|
clearTimeout(saveTimer);
|
|
saveTimer = null;
|
|
}
|
|
if (pendingSave) {
|
|
try {
|
|
pendingSave();
|
|
} catch (e) {
|
|
console.error('[db] Flush save error:', e.message);
|
|
}
|
|
pendingSave = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Flush pending saves and close the database connection.
|
|
* Safe to call multiple times.
|
|
*/
|
|
function close() {
|
|
flushPendingSave();
|
|
try {
|
|
db.close();
|
|
} catch (e) {
|
|
// Already closed — ignore
|
|
}
|
|
}
|
|
|
|
return { db, debouncedSave, flushPendingSave, close };
|
|
}
|
|
|
|
module.exports = { createDatabase };
|