119 lines
2.9 KiB
JavaScript
119 lines
2.9 KiB
JavaScript
/**
|
|
* @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<string, object>} tenants
|
|
* @property {Record<string, string>} apiKeys - apiKey → tenantId
|
|
* @property {Record<string, object>} 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);
|
|
}
|