/** * @fileoverview JSON file-backed in-memory store (MVP persistence). * @module store/db */ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'; import { dirname } from 'node:path'; import { config } from '../config.js'; import { createDebugger } from '../debug/logger.js'; const log = createDebugger('app'); /** * @typedef {Object} StoreShape * @property {Record} tenants * @property {Record} apiKeys - apiKey → tenantId * @property {Record} usage - tenantId → counters * @property {object[]} webhooks * @property {object[]} jobWatchers */ /** @type {StoreShape|null} */ let store = null; /** * Create an empty store document. * @returns {StoreShape} */ export function emptyStore() { return { tenants: {}, apiKeys: {}, usage: {}, webhooks: [], jobWatchers: [], tokenRefs: {}, blobs: {}, shares: {}, trees: {}, }; } /** * Load store from disk into memory (or create empty if missing). * @param {string} [path=config.storePath] * @returns {StoreShape} */ export function loadStore(path = config.storePath) { if (store) return store; if (existsSync(path)) { try { const raw = readFileSync(path, 'utf8'); const parsed = JSON.parse(raw); store = { ...emptyStore(), ...parsed, tenants: parsed.tenants ?? {}, apiKeys: parsed.apiKeys ?? {}, usage: parsed.usage ?? {}, webhooks: Array.isArray(parsed.webhooks) ? parsed.webhooks : [], jobWatchers: Array.isArray(parsed.jobWatchers) ? parsed.jobWatchers : [], tokenRefs: parsed.tokenRefs ?? {}, }; log.debug('store loaded', { path, tenants: Object.keys(store.tenants).length }); } catch (err) { log.error('store load failed, using empty', { path, error: err.message }); store = emptyStore(); } } else { store = emptyStore(); log.debug('store initialized empty', { path }); } return store; } /** * Access the in-memory store (loads if needed). * @returns {StoreShape} */ export function getStore() { if (!store) return loadStore(); return store; } /** * Replace the in-memory store (tests only). * @param {StoreShape|null} next * @returns {void} */ export function setStoreForTests(next) { store = next; } /** * Persist the in-memory store to disk. * @param {string} [path=config.storePath] * @returns {void} */ export function persist(path = config.storePath) { const data = getStore(); const dir = dirname(path); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } writeFileSync(path, JSON.stringify(data, null, 2), 'utf8'); log.debug('store persisted', { path }); } /** * Force reload from disk (drops memory). * @param {string} [path=config.storePath] * @returns {StoreShape} */ export function reloadStore(path = config.storePath) { store = null; return loadStore(path); }