← back to Inline Editor
src/db.js
91 lines
/**
* DB layer for inline-editor. Uses local-socket psql by default — same pattern
* Sister Parish viewer uses — so we avoid needing the dw_admin TCP password.
*
* If DATABASE_URL is set and contains a working password, switch to pg client.
*/
const { execSync, spawnSync } = require('child_process');
function psql(sql, opts = {}) {
const res = spawnSync('psql', ['dw_unified','-At','-q'], {
input: sql,
encoding: 'utf8',
timeout: opts.timeout || 5000
});
if (res.status !== 0) throw new Error(`psql failed: ${res.stderr}`);
return res.stdout.trim();
}
function esc(s) {
if (s == null) return 'NULL';
return "'" + String(s).replace(/'/g, "''") + "'";
}
function getOverridesFor(site, path) {
const sql = `SELECT json_object_agg(selector, json_build_object('kind', kind, 'value', value)) FROM page_overrides WHERE site=${esc(site)} AND path=${esc(path)};`;
const raw = psql(sql);
if (!raw || raw === '') return {};
try { return JSON.parse(raw) || {}; } catch { return {}; }
}
function saveOverride({ site, path, selector, kind, value, editorEmail, editorIp, editorUa, isRevert = false }) {
// 1) get old value (for log)
const oldSql = `SELECT value FROM page_overrides WHERE site=${esc(site)} AND path=${esc(path)} AND selector=${esc(selector)};`;
const oldValue = psql(oldSql) || null;
// 2) upsert override
const upsertSql = `
INSERT INTO page_overrides (site, path, selector, kind, value, updated_at, updated_by)
VALUES (${esc(site)}, ${esc(path)}, ${esc(selector)}, ${esc(kind)}, ${esc(value)}, NOW(), ${esc(editorEmail)})
ON CONFLICT (site, path, selector) DO UPDATE
SET kind=EXCLUDED.kind, value=EXCLUDED.value, updated_at=NOW(), updated_by=EXCLUDED.updated_by;
`;
psql(upsertSql);
// 3) append to log
const logSql = `
INSERT INTO page_edit_log (site, path, selector, kind, old_value, new_value, editor_email, editor_ip, editor_ua, is_revert)
VALUES (${esc(site)}, ${esc(path)}, ${esc(selector)}, ${esc(kind)}, ${oldValue ? esc(oldValue) : 'NULL'}, ${esc(value)}, ${esc(editorEmail)}, ${editorIp ? esc(editorIp) : 'NULL'}, ${editorUa ? esc(editorUa) : 'NULL'}, ${isRevert ? 'TRUE' : 'FALSE'});
`;
psql(logSql);
return { oldValue };
}
function getLogFor(site, path, limit = 50) {
const sql = `SELECT json_agg(t) FROM (SELECT id, selector, kind, old_value, new_value, editor_email, is_revert, ts FROM page_edit_log WHERE site=${esc(site)} AND path=${esc(path)} ORDER BY ts DESC LIMIT ${limit | 0}) t;`;
const raw = psql(sql);
try { return JSON.parse(raw) || []; } catch { return []; }
}
function recordUpload({ site, url, absPath, mimeType, bytes, uploadedBy, uploaderIp }) {
const sql = `
INSERT INTO page_uploads (site, url, abs_path, mime_type, bytes, uploaded_by, uploader_ip)
VALUES (${esc(site)}, ${esc(url)}, ${esc(absPath)}, ${esc(mimeType)}, ${bytes | 0}, ${esc(uploadedBy)}, ${uploaderIp ? esc(uploaderIp) : 'NULL'});
`;
psql(sql);
}
function rateLimitCheck(editorEmail, perMinute = 10) {
// Insert + count last 60s. If count > perMinute, block.
const sql = `
INSERT INTO page_edit_ratelimit (editor_email) VALUES (${esc(editorEmail)});
DELETE FROM page_edit_ratelimit WHERE ts < NOW() - INTERVAL '5 minutes';
SELECT COUNT(*) FROM page_edit_ratelimit WHERE editor_email=${esc(editorEmail)} AND ts > NOW() - INTERVAL '1 minute';
`;
const out = psql(sql);
const n = parseInt(out, 10);
return { count: n, blocked: n > perMinute };
}
function deleteOverride({ site, path, selector, editorEmail, editorIp, editorUa, kind }) {
const oldSql = `SELECT value FROM page_overrides WHERE site=${esc(site)} AND path=${esc(path)} AND selector=${esc(selector)};`;
const oldValue = psql(oldSql) || null;
psql(`DELETE FROM page_overrides WHERE site=${esc(site)} AND path=${esc(path)} AND selector=${esc(selector)};`);
psql(`
INSERT INTO page_edit_log (site, path, selector, kind, old_value, new_value, editor_email, editor_ip, editor_ua, is_revert)
VALUES (${esc(site)}, ${esc(path)}, ${esc(selector)}, ${esc(kind || 'text')}, ${oldValue ? esc(oldValue) : 'NULL'}, '', ${esc(editorEmail)}, ${editorIp ? esc(editorIp) : 'NULL'}, ${editorUa ? esc(editorUa) : 'NULL'}, TRUE);
`);
}
module.exports = { psql, esc, getOverridesFor, saveOverride, deleteOverride, getLogFor, recordUpload, rateLimitCheck };