diff --git a/lib/sql_generator.js b/lib/sql_generator.js
index 90ea433..2b9040b 100644
--- a/lib/sql_generator.js
+++ b/lib/sql_generator.js
@@ -59,14 +59,11 @@ ilog AS (
VALUES ({{version_id}}, '{{pf_user}}', 'baseline', NULL, '{{params}}'::jsonb, '{{note}}')
RETURNING id
)
-,del AS (
- DELETE FROM {{fc_table}} WHERE iter = 'baseline'
-)
,ins AS (
INSERT INTO {{fc_table}} (${insertCols})
SELECT ${baselineSelect}, 'baseline', (SELECT id FROM ilog), '{{pf_user}}', now()
FROM ${srcTable}
- WHERE ${q(dateCol)} BETWEEN '{{date_from}}' AND '{{date_to}}'
+ WHERE {{filter_clause}}
RETURNING *
)
SELECT count(*) AS rows_affected FROM ins`.trim();
@@ -248,10 +245,44 @@ function buildSetClause(dimCols, setObj) {
}).join(', ');
}
+// build a SQL WHERE clause from an array of filter objects { col, op, values }
+// only allows columns with role 'date' or 'filter'
+function buildFilterClause(filters, colMeta) {
+ if (!filters || filters.length === 0) {
+ const err = new Error('At least one filter is required');
+ err.status = 400; throw err;
+ }
+ const allowed = new Set(
+ colMeta.filter(c => c.role === 'date' || c.role === 'filter').map(c => c.cname)
+ );
+ const parts = filters.map(({ col, op, values = [] }) => {
+ if (!allowed.has(col)) {
+ const err = new Error(`Column "${col}" is not available for baseline filtering`);
+ err.status = 400; throw err;
+ }
+ const c = `"${col}"`;
+ const v = values.map(x => `'${esc(String(x))}'`);
+ switch (op) {
+ case '=': return `${c} = ${v[0]}`;
+ case '!=': return `${c} != ${v[0]}`;
+ case 'IN': return `${c} IN (${v.join(', ')})`;
+ case 'NOT IN': return `${c} NOT IN (${v.join(', ')})`;
+ case 'BETWEEN': return `${c} BETWEEN ${v[0]} AND ${v[1]}`;
+ case 'IS NULL': return `${c} IS NULL`;
+ case 'IS NOT NULL': return `${c} IS NOT NULL`;
+ default: {
+ const err = new Error(`Unsupported operator "${op}"`);
+ err.status = 400; throw err;
+ }
+ }
+ });
+ return parts.join('\nAND ');
+}
+
// escape a value for safe SQL string substitution
function esc(val) {
if (val === null || val === undefined) return '';
return String(val).replace(/'/g, "''");
}
-module.exports = { generateSQL, applyTokens, buildWhere, buildExcludeClause, buildSetClause, esc };
+module.exports = { generateSQL, applyTokens, buildWhere, buildExcludeClause, buildSetClause, buildFilterClause, esc };
diff --git a/public/app.js b/public/app.js
index 71ecacf..9975f70 100644
--- a/public/app.js
+++ b/public/app.js
@@ -9,6 +9,7 @@ const state = {
colMeta: [], // col_meta for selected source
slice: {}, // current pivot selection
loadDataOp: null, // 'baseline' | 'reference'
+ baselineFilterCols: [],
previewSchema: null,
previewTname: null,
grids: {
@@ -50,7 +51,7 @@ function showStatus(msg, type = 'info') {
NAVIGATION
============================================================ */
function switchView(name) {
- if ((name === 'forecast' || name === 'log') && !state.version) {
+ if ((name === 'forecast' || name === 'log' || name === 'baseline') && !state.version) {
showStatus('Select a version first', 'error');
return;
}
@@ -68,6 +69,7 @@ function switchView(name) {
if (name === 'versions') renderVersions();
if (name === 'forecast') loadForecastData();
if (name === 'log') loadLogData();
+ if (name === 'baseline') openBaselineWorkbench();
}
function setSource(source) {
@@ -245,9 +247,9 @@ function renderColMetaGrid(colMeta) {
{
field: 'role', headerName: 'Role', width: 110, editable: true,
cellEditor: 'agSelectCellEditor',
- cellEditorParams: { values: ['dimension', 'value', 'units', 'date', 'ignore'] },
+ cellEditorParams: { values: ['dimension', 'value', 'units', 'date', 'filter', 'ignore'] },
cellStyle: p => {
- const colors = { dimension: '#eaf4fb', value: '#eafaf1', units: '#fef9e7', date: '#fdf2f8', ignore: '#f9f9f9' };
+ const colors = { dimension: '#eaf4fb', value: '#eafaf1', units: '#fef9e7', date: '#fdf2f8', filter: '#fef5e4', ignore: '#f9f9f9' };
return { background: colors[p.value] || '' };
}
},
@@ -528,6 +530,290 @@ function openForecast() {
switchView('forecast');
}
+function openVersionBaseline() {
+ if (!state.selectedVersionId) return;
+ let v = null;
+ state.grids.versions.forEachNode(n => {
+ if (n.data.id === state.selectedVersionId) v = n.data;
+ });
+ if (!v) return;
+ setVersion(v);
+ switchView('baseline');
+}
+
+/* ============================================================
+ BASELINE WORKBENCH
+ ============================================================ */
+async function openBaselineWorkbench() {
+ if (!state.version) return;
+ document.getElementById('baseline-label').textContent =
+ `${state.source?.tname || ''} — ${state.version.name} [${state.version.status}]`;
+
+ // ensure colMeta loaded
+ if (!state.colMeta.length && state.source) {
+ state.colMeta = await api('GET', `/sources/${state.source.id}/cols`);
+ }
+ state.baselineFilterCols = state.colMeta.filter(c => c.role === 'date' || c.role === 'filter');
+
+ // reset form
+ document.getElementById('seg-description').value = '';
+ document.getElementById('seg-offset-years').value = '0';
+ document.getElementById('seg-offset-months').value = '0';
+ document.getElementById('seg-filter-rows').innerHTML = '';
+ document.getElementById('seg-timeline').classList.add('hidden');
+ addFilterRow(); // start with one empty filter row
+
+ await loadBaselineSegments();
+}
+
+async function loadBaselineSegments() {
+ if (!state.version) return;
+ try {
+ const logs = await api('GET', `/versions/${state.version.id}/log`);
+ const segments = logs.filter(l => l.operation === 'baseline');
+ renderBaselineSegments(segments);
+ } catch (err) {
+ showStatus(err.message, 'error');
+ }
+}
+
+function renderBaselineSegments(segments) {
+ const el = document.getElementById('baseline-segments-list');
+ if (segments.length === 0) {
+ el.innerHTML = '
No segments loaded yet.
';
+ return;
+ }
+ el.innerHTML = segments.map(s => {
+ const params = s.params || {};
+ const filters = (params.filters || [])
+ .map(f => {
+ if (f.op === 'IS NULL' || f.op === 'IS NOT NULL') return `${f.col} ${f.op}`;
+ if (f.op === 'BETWEEN') return `${f.col} BETWEEN ${(f.values||[]).join(' AND ')}`;
+ if (f.op === 'IN' || f.op === 'NOT IN') return `${f.col} ${f.op} (${(f.values||[]).join(', ')})`;
+ return `${f.col} ${f.op} ${(f.values||[])[0] || ''}`;
+ }).join('\n');
+ const offset = params.date_offset && params.date_offset !== '0 days' ? ` · offset: ${params.date_offset}` : '';
+ const stamp = s.stamp ? new Date(s.stamp).toLocaleString() : '';
+ return `
+
+
+
${filters}${offset}
+
`;
+ }).join('');
+}
+
+function addFilterRow() {
+ const container = document.getElementById('seg-filter-rows');
+ const idx = container.children.length;
+ const colOptions = state.baselineFilterCols
+ .map(c => ``)
+ .join('');
+
+ const div = document.createElement('div');
+ div.className = 'filter-row';
+ div.dataset.index = idx;
+ div.innerHTML = `
+
+
+
+
+ `;
+
+ div.querySelector('.filter-op-select').addEventListener('change', () => {
+ updateFilterValueInputs(div);
+ updateTimelinePreview();
+ });
+ div.querySelector('.filter-col-select').addEventListener('change', () => {
+ updateTimelinePreview();
+ });
+ div.querySelector('.filter-remove-btn').addEventListener('click', () => {
+ div.remove();
+ updateTimelinePreview();
+ });
+
+ updateFilterValueInputs(div);
+ container.appendChild(div);
+}
+
+function updateFilterValueInputs(row) {
+ const op = row.querySelector('.filter-op-select').value;
+ const container = row.querySelector('.filter-value-container');
+
+ if (op === 'IS NULL' || op === 'IS NOT NULL') {
+ container.innerHTML = '';
+ return;
+ }
+ if (op === 'BETWEEN') {
+ container.innerHTML = `
+
+ and
+
+ `;
+ container.querySelectorAll('input').forEach(i => i.addEventListener('input', updateTimelinePreview));
+ return;
+ }
+ if (op === 'IN' || op === 'NOT IN') {
+ container.innerHTML = ``;
+ return;
+ }
+ // = or !=
+ container.innerHTML = ``;
+}
+
+function getFilterRows() {
+ const rows = [];
+ document.querySelectorAll('#seg-filter-rows .filter-row').forEach(row => {
+ const col = row.querySelector('.filter-col-select').value;
+ const op = row.querySelector('.filter-op-select').value;
+ if (!col) return;
+
+ const filter = { col, op, values: [] };
+
+ if (op === 'BETWEEN') {
+ const from = row.querySelector('.filter-val-from')?.value.trim();
+ const to = row.querySelector('.filter-val-to')?.value.trim();
+ if (from) filter.values.push(from);
+ if (to) filter.values.push(to);
+ } else if (op === 'IN' || op === 'NOT IN') {
+ const raw = row.querySelector('.filter-val-list')?.value.trim();
+ if (raw) filter.values = raw.split(',').map(s => s.trim()).filter(Boolean);
+ } else if (op !== 'IS NULL' && op !== 'IS NOT NULL') {
+ const val = row.querySelector('.filter-val-single')?.value.trim();
+ if (val) filter.values.push(val);
+ }
+
+ rows.push(filter);
+ });
+ return rows;
+}
+
+function updateTimelinePreview() {
+ const el = document.getElementById('seg-timeline');
+
+ // find first BETWEEN filter on a date-role column
+ let dateFrom = null, dateTo = null;
+ document.querySelectorAll('#seg-filter-rows .filter-row').forEach(row => {
+ if (dateFrom) return;
+ const col = row.querySelector('.filter-col-select').value;
+ const op = row.querySelector('.filter-op-select').value;
+ if (op !== 'BETWEEN') return;
+ const meta = state.baselineFilterCols.find(c => c.cname === col);
+ if (!meta || meta.role !== 'date') return;
+ const from = row.querySelector('.filter-val-from')?.value.trim();
+ const to = row.querySelector('.filter-val-to')?.value.trim();
+ if (from && to) { dateFrom = from; dateTo = to; }
+ });
+
+ if (!dateFrom || !dateTo) { el.classList.add('hidden'); return; }
+
+ const from = new Date(dateFrom + 'T00:00:00');
+ const to = new Date(dateTo + 'T00:00:00');
+ if (isNaN(from) || isNaN(to) || from > to) { el.classList.add('hidden'); return; }
+
+ const months = (to.getFullYear() - from.getFullYear()) * 12 + (to.getMonth() - from.getMonth()) + 1;
+ const fmt = new Intl.DateTimeFormat('en-US', { month: 'short', year: 'numeric' });
+ const offsetYears = parseInt(document.getElementById('seg-offset-years').value) || 0;
+ const offsetMonths = parseInt(document.getElementById('seg-offset-months').value) || 0;
+
+ const projFrom = new Date(from); projFrom.setFullYear(projFrom.getFullYear() + offsetYears); projFrom.setMonth(projFrom.getMonth() + offsetMonths);
+ const projTo = new Date(to); projTo.setFullYear(projTo.getFullYear() + offsetYears); projTo.setMonth(projTo.getMonth() + offsetMonths);
+
+ let html = `
+
+
Source
+
+
+
+ ${fmt.format(from)}
+ ${months} month${months !== 1 ? 's' : ''}
+ ${fmt.format(to)}
+
+
+
`;
+
+ if (offsetYears || offsetMonths) {
+ const parts = [];
+ if (offsetYears) parts.push(`${offsetYears} yr`);
+ if (offsetMonths) parts.push(`${offsetMonths} mo`);
+ html += `
+ + ${parts.join(' ')} →
+
+
Projected
+
+
+
+ ${fmt.format(projFrom)}
+ ${months} month${months !== 1 ? 's' : ''}
+ ${fmt.format(projTo)}
+
+
+
`;
+ }
+
+ el.innerHTML = html;
+ el.classList.remove('hidden');
+}
+
+async function submitBaselineSegment() {
+ if (!state.version) return;
+ const filters = getFilterRows();
+ if (filters.length === 0) { showStatus('Add at least one filter', 'error'); return; }
+
+ const years = parseInt(document.getElementById('seg-offset-years').value) || 0;
+ const months = parseInt(document.getElementById('seg-offset-months').value) || 0;
+ const parts = [];
+ if (years) parts.push(`${years} year${years !== 1 ? 's' : ''}`);
+ if (months) parts.push(`${months} month${months !== 1 ? 's' : ''}`);
+ const date_offset = parts.length ? parts.join(' ') : '0 days';
+
+ const body = {
+ filters,
+ date_offset,
+ pf_user: getPfUser(),
+ note: document.getElementById('seg-description').value.trim() || undefined
+ };
+
+ try {
+ showStatus('Loading segment...', 'info');
+ const result = await api('POST', `/versions/${state.version.id}/baseline`, body);
+ showStatus(`Segment loaded — ${result.rows_affected} rows`, 'success');
+ document.getElementById('seg-description').value = '';
+ document.getElementById('seg-filter-rows').innerHTML = '';
+ document.getElementById('seg-timeline').classList.add('hidden');
+ addFilterRow();
+ await loadBaselineSegments();
+ } catch (err) {
+ showStatus(err.message, 'error');
+ }
+}
+
+async function clearBaseline() {
+ if (!state.version) return;
+ if (!confirm('Delete all baseline rows and segment history for this version? This cannot be undone.')) return;
+ try {
+ const result = await api('DELETE', `/versions/${state.version.id}/baseline`);
+ showStatus(`Baseline cleared — ${result.rows_deleted} rows removed`, 'success');
+ await loadBaselineSegments();
+ } catch (err) {
+ showStatus(err.message, 'error');
+ }
+}
+
/* ============================================================
FORECAST VIEW — data loading
============================================================ */
@@ -956,7 +1242,7 @@ document.addEventListener('DOMContentLoaded', () => {
document.getElementById('new-version-form').classList.add('hidden');
});
document.getElementById('vbtn-forecast').addEventListener('click', openForecast);
- document.getElementById('vbtn-baseline').addEventListener('click', () => showLoadForm('baseline'));
+ document.getElementById('vbtn-baseline').addEventListener('click', openVersionBaseline);
document.getElementById('vbtn-reference').addEventListener('click', () => showLoadForm('reference'));
document.getElementById('vbtn-toggle').addEventListener('click', toggleVersionStatus);
document.getElementById('vbtn-delete').addEventListener('click', deleteVersion);
@@ -988,6 +1274,23 @@ document.addEventListener('DOMContentLoaded', () => {
if (btn) undoOperation(parseInt(btn.dataset.logid));
});
+ // baseline workbench
+ document.getElementById('btn-add-filter-row').addEventListener('click', addFilterRow);
+ document.getElementById('btn-load-segment').addEventListener('click', submitBaselineSegment);
+ document.getElementById('btn-clear-baseline').addEventListener('click', clearBaseline);
+ document.getElementById('seg-offset-years').addEventListener('input', updateTimelinePreview);
+ document.getElementById('seg-offset-months').addEventListener('input', updateTimelinePreview);
+
+ // undo in baseline segments list
+ document.getElementById('baseline-segments-list').addEventListener('click', async e => {
+ const btn = e.target.closest('[data-logid]');
+ if (!btn) return;
+ const logid = parseInt(btn.dataset.logid);
+ const result = await api('DELETE', `/log/${logid}`);
+ showStatus(`Segment undone — ${result.rows_deleted} rows removed`, 'success');
+ await loadBaselineSegments();
+ });
+
// init sources view
initSourcesView().catch(err => showStatus(err.message, 'error'));
});
diff --git a/public/index.html b/public/index.html
index 6be4881..a9d7e62 100644
--- a/public/index.html
+++ b/public/index.html
@@ -17,6 +17,7 @@
- Sources
- Versions
+ - Baseline
- Forecast
- Log
@@ -96,6 +97,45 @@
+
+
+
+ No version selected
+
+
+
+
+
+
Loaded Segments
+
+
No segments loaded yet.
+
+
+
+
+
diff --git a/public/styles.css b/public/styles.css
index 14e23bd..f5690a7 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -411,3 +411,191 @@ body {
.preview-table th, .preview-table td { border: 1px solid #e0e0e0; padding: 4px 8px; text-align: left; }
.preview-table th { background: #f5f7f9; font-weight: 600; }
.preview-table tr:hover td { background: #fafbfc; }
+
+/* ============================================================
+ BASELINE WORKBENCH
+ ============================================================ */
+.workbench-toolbar {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex-shrink: 0;
+}
+#baseline-label { font-weight: 600; font-size: 13px; flex: 1; }
+
+.baseline-layout {
+ display: flex;
+ gap: 10px;
+ flex: 1;
+ overflow: hidden;
+ min-height: 0;
+}
+
+.baseline-form-panel {
+ width: 360px;
+ flex-shrink: 0;
+ background: white;
+ border-radius: 5px;
+ padding: 14px 16px;
+ box-shadow: 0 1px 3px rgba(0,0,0,.08);
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+}
+
+.baseline-segments-panel {
+ flex: 1;
+ background: white;
+ border-radius: 5px;
+ padding: 14px 16px;
+ box-shadow: 0 1px 3px rgba(0,0,0,.08);
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ min-width: 0;
+}
+
+.panel-section-title {
+ font-size: 10px;
+ font-weight: 700;
+ text-transform: uppercase;
+ color: #7f8c8d;
+ letter-spacing: 0.05em;
+ margin-bottom: 12px;
+}
+
+.baseline-form {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.baseline-field-label {
+ font-size: 11px;
+ color: #555;
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+}
+
+.baseline-field-label input[type=text],
+.baseline-field-label input[type=number] {
+ border: 1px solid #dce1e7;
+ padding: 5px 8px;
+ border-radius: 3px;
+ font-size: 12px;
+ color: #333;
+}
+
+.offset-row { display: flex; gap: 10px; }
+.offset-row label { flex: 1; }
+
+.filter-section { display: flex; flex-direction: column; gap: 6px; }
+.filter-section-label {
+ font-size: 11px;
+ color: #555;
+ font-weight: 600;
+}
+.required-star { color: #e74c3c; }
+
+.filter-row {
+ display: flex;
+ gap: 4px;
+ align-items: flex-start;
+ flex-wrap: wrap;
+ padding: 6px 8px;
+ background: #f8fafc;
+ border: 1px solid #e8ecf0;
+ border-radius: 3px;
+ margin-bottom: 4px;
+}
+
+.filter-row select,
+.filter-row input[type=text] {
+ border: 1px solid #dce1e7;
+ padding: 4px 6px;
+ border-radius: 3px;
+ font-size: 11px;
+ color: #333;
+ background: white;
+}
+
+.filter-col-select { min-width: 120px; }
+.filter-op-select { min-width: 90px; }
+.filter-val-single,
+.filter-val-list { min-width: 120px; flex: 1; }
+.filter-val-from,
+.filter-val-to { min-width: 90px; flex: 1; }
+.filter-between-sep { font-size: 11px; color: #aaa; padding: 5px 2px; }
+.filter-value-container { display: flex; align-items: center; gap: 4px; flex: 1; min-width: 0; }
+
+/* Timeline preview */
+.timeline-preview { display: flex; flex-direction: column; gap: 6px; padding: 10px 0 4px; }
+.timeline-preview.hidden { display: none; }
+
+.timeline-row { display: flex; align-items: center; gap: 8px; }
+.timeline-row-label {
+ font-size: 10px;
+ font-weight: 700;
+ text-transform: uppercase;
+ color: #7f8c8d;
+ letter-spacing: 0.04em;
+ width: 60px;
+ flex-shrink: 0;
+ text-align: right;
+}
+.timeline-bar-wrap { flex: 1; display: flex; flex-direction: column; gap: 3px; }
+.timeline-bar {
+ height: 14px;
+ background: #2980b9;
+ border-radius: 3px;
+ width: 100%;
+}
+.timeline-bar-projected { background: #27ae60; }
+.timeline-bar-labels {
+ display: flex;
+ justify-content: space-between;
+ font-size: 10px;
+ color: #666;
+}
+.timeline-offset-indicator {
+ font-size: 11px;
+ color: #aaa;
+ padding: 2px 0 2px 68px;
+ font-style: italic;
+}
+
+/* Segment cards */
+.segment-card {
+ border: 1px solid #e8ecf0;
+ border-radius: 4px;
+ padding: 10px 12px;
+ background: #fafbfc;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+.segment-card-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.segment-card-note {
+ font-size: 12px;
+ font-weight: 600;
+ flex: 1;
+}
+.segment-card-meta {
+ font-size: 11px;
+ color: #888;
+}
+.segment-card-params {
+ font-size: 11px;
+ color: #555;
+ font-family: monospace;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+.segments-empty { font-size: 12px; color: #aaa; font-style: italic; }
diff --git a/routes/operations.js b/routes/operations.js
index a8f2bdc..5ddc15d 100644
--- a/routes/operations.js
+++ b/routes/operations.js
@@ -1,5 +1,5 @@
const express = require('express');
-const { applyTokens, buildWhere, buildExcludeClause, buildSetClause, esc } = require('../lib/sql_generator');
+const { applyTokens, buildWhere, buildExcludeClause, buildSetClause, buildFilterClause, esc } = require('../lib/sql_generator');
const { fcTable } = require('../lib/utils');
module.exports = function(pool) {
@@ -74,12 +74,11 @@ module.exports = function(pool) {
}
});
- // load baseline rows from source table for a date range
- // deletes existing iter='baseline' rows before inserting (handled inside stored SQL)
+ // load baseline rows from source table — additive, no delete
router.post('/versions/:id/baseline', async (req, res) => {
- const { date_from, date_to, date_offset, pf_user, note, replay } = req.body;
- if (!date_from || !date_to) {
- return res.status(400).json({ error: 'date_from and date_to are required' });
+ const { filters, date_offset, pf_user, note, replay } = req.body;
+ if (!filters || filters.length === 0) {
+ return res.status(400).json({ error: 'filters are required' });
}
if (replay) {
return res.status(501).json({ error: 'replay is not yet implemented' });
@@ -89,19 +88,50 @@ module.exports = function(pool) {
const ctx = await getContext(parseInt(req.params.id), 'baseline');
if (!guardOpen(ctx.version, res)) return;
+ const filterClause = buildFilterClause(filters, ctx.colMeta);
+
const sql = applyTokens(ctx.sql, {
- fc_table: ctx.table,
- version_id: ctx.version.id,
- pf_user: esc(pf_user || ''),
- note: esc(note || ''),
- params: esc(JSON.stringify({ date_from, date_to, date_offset: dateOffset })),
- date_from: esc(date_from),
- date_to: esc(date_to),
- date_offset: esc(dateOffset)
+ fc_table: ctx.table,
+ version_id: ctx.version.id,
+ pf_user: esc(pf_user || ''),
+ note: esc(note || ''),
+ params: esc(JSON.stringify({ filters, date_offset: dateOffset })),
+ filter_clause: filterClause,
+ date_offset: esc(dateOffset)
});
const result = await runSQL(sql);
- res.json({ rows_affected: result.rows.length });
+ res.json(result.rows[0]);
+ } catch (err) {
+ console.error(err);
+ res.status(err.status || 500).json({ error: err.message });
+ }
+ });
+
+ // delete all baseline rows and log entries for a version
+ router.delete('/versions/:id/baseline', async (req, res) => {
+ const versionId = parseInt(req.params.id);
+ try {
+ const ctx = await getContext(versionId, 'baseline');
+ if (!guardOpen(ctx.version, res)) return;
+ const client = await pool.connect();
+ try {
+ await client.query('BEGIN');
+ const delRows = await client.query(
+ `DELETE FROM ${ctx.table} WHERE iter = 'baseline' RETURNING id`
+ );
+ const delLog = await client.query(
+ `DELETE FROM pf.log WHERE version_id = $1 AND operation = 'baseline'`,
+ [versionId]
+ );
+ await client.query('COMMIT');
+ res.json({ rows_deleted: delRows.rowCount, log_entries_deleted: delLog.rowCount });
+ } catch (err) {
+ await client.query('ROLLBACK');
+ throw err;
+ } finally {
+ client.release();
+ }
} catch (err) {
console.error(err);
res.status(err.status || 500).json({ error: err.message });