Compare commits

...

4 Commits

Author SHA1 Message Date
6d8b052eb6 Add date offset to baseline — project actuals into forecast period
The baseline operation now accepts a date_offset interval (e.g. "1 year",
"6 months") and applies it to every date when inserting rows, shifting
historical actuals into the target forecast period.

SQL: {date_col} + '{{date_offset}}'::interval)::date at insert time.
Route: defaults to '0 days' if omitted so existing calls are unaffected.
UI: year/month spinners with a live before→after month chip preview so
the projected landing period is visible before submitting.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 12:48:28 -04:00
ddd16bc7a0 Show params column in log grid
Exposes the stored params (e.g. date_from/date_to for baseline/reference)
so the date range used in each operation is visible in the log.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 12:28:03 -04:00
10441a4761 Show baseline/reference form in a modal with live month preview
Replaces the small inline form with a centred modal dialog. When both
dates are selected, a live chip list shows every month covered (up to
36 months) so it is immediately clear what periods will be loaded.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 12:24:20 -04:00
cfee3e96b9 Return inserted rows from change operations for incremental grid updates
Instead of re-fetching all forecast data after scale/recode/clone/reference,
the routes now return the inserted rows directly. The frontend uses ag-Grid's
applyTransaction to add only the new rows, eliminating the full reload round-trip.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 12:04:28 -04:00
5 changed files with 271 additions and 48 deletions

View File

@ -49,6 +49,9 @@ function generateSQL(source, colMeta) {
} }
function buildBaseline() { function buildBaseline() {
const baselineSelect = dataCols.map(c =>
c === dateCol ? `(${q(c)} + '{{date_offset}}'::interval)::date` : q(c)
).join(', ');
return ` return `
WITH WITH
ilog AS ( ilog AS (
@ -61,7 +64,7 @@ ilog AS (
) )
,ins AS ( ,ins AS (
INSERT INTO {{fc_table}} (${insertCols}) INSERT INTO {{fc_table}} (${insertCols})
SELECT ${selectData}, 'baseline', (SELECT id FROM ilog), '{{pf_user}}', now() SELECT ${baselineSelect}, 'baseline', (SELECT id FROM ilog), '{{pf_user}}', now()
FROM ${srcTable} FROM ${srcTable}
WHERE ${q(dateCol)} BETWEEN '{{date_from}}' AND '{{date_to}}' WHERE ${q(dateCol)} BETWEEN '{{date_from}}' AND '{{date_to}}'
RETURNING * RETURNING *
@ -84,7 +87,7 @@ ilog AS (
WHERE ${q(dateCol)} BETWEEN '{{date_from}}' AND '{{date_to}}' WHERE ${q(dateCol)} BETWEEN '{{date_from}}' AND '{{date_to}}'
RETURNING * RETURNING *
) )
SELECT count(*) AS rows_affected FROM ins`.trim(); SELECT * FROM ins`.trim();
} }
function buildScale() { function buildScale() {
@ -124,7 +127,7 @@ ilog AS (
FROM base FROM base
RETURNING * RETURNING *
) )
SELECT count(*) AS rows_affected FROM ins`.trim(); SELECT * FROM ins`.trim();
} }
function buildRecode() { function buildRecode() {
@ -146,16 +149,16 @@ ilog AS (
SELECT ${dimsJoined}, ${q(dateCol)}, ${effectiveValue ? `-${q(effectiveValue)}` : '0'}, ${effectiveUnits ? `-${q(effectiveUnits)}` : '0'}, SELECT ${dimsJoined}, ${q(dateCol)}, ${effectiveValue ? `-${q(effectiveValue)}` : '0'}, ${effectiveUnits ? `-${q(effectiveUnits)}` : '0'},
'recode', (SELECT id FROM ilog), '{{pf_user}}', now() 'recode', (SELECT id FROM ilog), '{{pf_user}}', now()
FROM src FROM src
RETURNING id RETURNING *
) )
,ins AS ( ,ins AS (
INSERT INTO {{fc_table}} (${insertCols}) INSERT INTO {{fc_table}} (${insertCols})
SELECT {{set_clause}}, ${q(dateCol)}, ${effectiveValue ? q(effectiveValue) : '0'}, ${effectiveUnits ? q(effectiveUnits) : '0'}, SELECT {{set_clause}}, ${q(dateCol)}, ${effectiveValue ? q(effectiveValue) : '0'}, ${effectiveUnits ? q(effectiveUnits) : '0'},
'recode', (SELECT id FROM ilog), '{{pf_user}}', now() 'recode', (SELECT id FROM ilog), '{{pf_user}}', now()
FROM src FROM src
RETURNING id RETURNING *
) )
SELECT (SELECT count(*) FROM neg) + (SELECT count(*) FROM ins) AS rows_affected`.trim(); SELECT * FROM neg UNION ALL SELECT * FROM ins`.trim();
} }
function buildClone() { function buildClone() {
@ -179,7 +182,7 @@ ilog AS (
{{exclude_clause}} {{exclude_clause}}
RETURNING * RETURNING *
) )
SELECT count(*) AS rows_affected FROM ins`.trim(); SELECT * FROM ins`.trim();
} }
function buildUndo() { function buildUndo() {

View File

@ -375,10 +375,81 @@ function showLoadForm(op) {
state.loadDataOp = op; state.loadDataOp = op;
document.getElementById('load-data-title').textContent = document.getElementById('load-data-title').textContent =
op === 'baseline' ? 'Load Baseline' : 'Load Reference'; op === 'baseline' ? 'Load Baseline' : 'Load Reference';
document.getElementById('load-data-form').classList.remove('hidden'); document.getElementById('load-date-from').value = '';
document.getElementById('load-date-to').value = '';
document.getElementById('load-offset-years').value = '0';
document.getElementById('load-offset-months').value = '0';
document.getElementById('load-note').value = '';
document.getElementById('load-date-preview').classList.add('hidden');
const showOffset = op === 'baseline';
document.getElementById('load-offset-fields').classList.toggle('hidden', !showOffset);
document.getElementById('load-data-modal').classList.remove('hidden');
document.getElementById('load-date-from').focus(); document.getElementById('load-date-from').focus();
} }
function hideLoadModal() {
document.getElementById('load-data-modal').classList.add('hidden');
}
function buildMonthList(fromVal, toVal) {
const from = new Date(fromVal + 'T00:00:00');
const to = new Date(toVal + 'T00:00:00');
if (isNaN(from) || isNaN(to) || from > to) return null;
const months = [];
const cur = new Date(from.getFullYear(), from.getMonth(), 1);
const end = new Date(to.getFullYear(), to.getMonth(), 1);
while (cur <= end) { months.push(new Date(cur)); cur.setMonth(cur.getMonth() + 1); }
return months;
}
function renderChips(months, fmt) {
if (months.length <= 36) {
return months.map(m => `<span class="date-chip">${fmt.format(m)}</span>`).join('');
}
return `<span class="date-chip-summary">${months.length} months — ${fmt.format(months[0])}${fmt.format(months[months.length - 1])}</span>`;
}
function updateDatePreview() {
const fromVal = document.getElementById('load-date-from').value;
const toVal = document.getElementById('load-date-to').value;
const preview = document.getElementById('load-date-preview');
const simple = document.getElementById('load-preview-simple');
const offset = document.getElementById('load-preview-offset');
if (!fromVal || !toVal) { preview.classList.add('hidden'); return; }
const months = buildMonthList(fromVal, toVal);
if (!months) { preview.classList.add('hidden'); return; }
const fmt = new Intl.DateTimeFormat('en-US', { month: 'short', year: 'numeric' });
if (state.loadDataOp === 'baseline') {
const years = parseInt(document.getElementById('load-offset-years').value) || 0;
const mths = parseInt(document.getElementById('load-offset-months').value) || 0;
const projected = months.map(d => {
const p = new Date(d);
p.setFullYear(p.getFullYear() + years);
p.setMonth(p.getMonth() + mths);
return p;
});
document.getElementById('load-chips-source').innerHTML = renderChips(months, fmt);
document.getElementById('load-chips-projected').innerHTML = renderChips(projected, fmt);
simple.classList.add('hidden');
offset.classList.remove('hidden');
} else {
simple.querySelector('.load-preview-label').textContent =
`${months.length} month${months.length !== 1 ? 's' : ''} covered`;
document.getElementById('load-date-chips').innerHTML = renderChips(months, fmt);
offset.classList.add('hidden');
simple.classList.remove('hidden');
}
preview.classList.remove('hidden');
}
async function submitLoadData() { async function submitLoadData() {
const date_from = document.getElementById('load-date-from').value; const date_from = document.getElementById('load-date-from').value;
const date_to = document.getElementById('load-date-to').value; const date_to = document.getElementById('load-date-to').value;
@ -391,10 +462,19 @@ async function submitLoadData() {
note: document.getElementById('load-note').value.trim() || undefined note: document.getElementById('load-note').value.trim() || undefined
}; };
if (state.loadDataOp === 'baseline') {
const years = parseInt(document.getElementById('load-offset-years').value) || 0;
const months = parseInt(document.getElementById('load-offset-months').value) || 0;
const parts = [];
if (years) parts.push(`${years} year${years !== 1 ? 's' : ''}`);
if (months) parts.push(`${months} month${months !== 1 ? 's' : ''}`);
body.date_offset = parts.length ? parts.join(' ') : '0 days';
}
try { try {
showStatus(`Loading ${state.loadDataOp}...`, 'info'); showStatus(`Loading ${state.loadDataOp}...`, 'info');
const result = await api('POST', `/versions/${state.selectedVersionId}/${state.loadDataOp}`, body); const result = await api('POST', `/versions/${state.selectedVersionId}/${state.loadDataOp}`, body);
document.getElementById('load-data-form').classList.add('hidden'); hideLoadModal();
showStatus(`${state.loadDataOp} loaded — ${result.rows_affected} rows`, 'success'); showStatus(`${state.loadDataOp} loaded — ${result.rows_affected} rows`, 'success');
} catch (err) { } catch (err) {
showStatus(err.message, 'error'); showStatus(err.message, 'error');
@ -451,6 +531,17 @@ function openForecast() {
/* ============================================================ /* ============================================================
FORECAST VIEW data loading FORECAST VIEW data loading
============================================================ */ ============================================================ */
function parseNumericRows(rows) {
const numericCols = state.colMeta
.filter(c => c.role === 'value' || c.role === 'units')
.map(c => c.cname);
return rows.map(row => {
const r = { ...row };
numericCols.forEach(col => { if (r[col] != null) r[col] = parseFloat(r[col]); });
return r;
});
}
async function loadForecastData() { async function loadForecastData() {
if (!state.version) return; if (!state.version) return;
document.getElementById('forecast-label').textContent = document.getElementById('forecast-label').textContent =
@ -462,14 +553,7 @@ async function loadForecastData() {
} }
showStatus('Loading forecast data...', 'info'); showStatus('Loading forecast data...', 'info');
const rawData = await api('GET', `/versions/${state.version.id}/data`); const rawData = await api('GET', `/versions/${state.version.id}/data`);
const numericCols = state.colMeta const data = parseNumericRows(rawData);
.filter(c => c.role === 'value' || c.role === 'units')
.map(c => c.cname);
const data = rawData.map(row => {
const r = { ...row };
numericCols.forEach(col => { if (r[col] != null) r[col] = parseFloat(r[col]); });
return r;
});
initPivotGrid(data); initPivotGrid(data);
showStatus(`Loaded ${data.length.toLocaleString()} rows`, 'success'); showStatus(`Loaded ${data.length.toLocaleString()} rows`, 'success');
} catch (err) { } catch (err) {
@ -695,7 +779,7 @@ async function submitScale() {
document.getElementById('scale-value-incr').value = ''; document.getElementById('scale-value-incr').value = '';
document.getElementById('scale-units-incr').value = ''; document.getElementById('scale-units-incr').value = '';
document.getElementById('scale-note').value = ''; document.getElementById('scale-note').value = '';
await loadForecastData(); state.grids.pivot.applyTransaction({ add: parseNumericRows(result.rows) });
} catch (err) { } catch (err) {
showStatus(err.message, 'error'); showStatus(err.message, 'error');
} }
@ -721,7 +805,7 @@ async function submitRecode() {
}); });
showStatus(`Recode applied — ${result.rows_affected} rows inserted`, 'success'); showStatus(`Recode applied — ${result.rows_affected} rows inserted`, 'success');
document.querySelectorAll('#recode-fields input[data-col], #recode-fields select[data-col]').forEach(i => { i.value = ''; }); document.querySelectorAll('#recode-fields input[data-col], #recode-fields select[data-col]').forEach(i => { i.value = ''; });
await loadForecastData(); state.grids.pivot.applyTransaction({ add: parseNumericRows(result.rows) });
} catch (err) { } catch (err) {
showStatus(err.message, 'error'); showStatus(err.message, 'error');
} }
@ -750,7 +834,7 @@ async function submitClone() {
showStatus(`Clone applied — ${result.rows_affected} rows inserted`, 'success'); showStatus(`Clone applied — ${result.rows_affected} rows inserted`, 'success');
document.querySelectorAll('#clone-fields input[data-col], #clone-fields select[data-col]').forEach(i => { i.value = ''; }); document.querySelectorAll('#clone-fields input[data-col], #clone-fields select[data-col]').forEach(i => { i.value = ''; });
document.getElementById('clone-scale').value = '1'; document.getElementById('clone-scale').value = '1';
await loadForecastData(); state.grids.pivot.applyTransaction({ add: parseNumericRows(result.rows) });
} catch (err) { } catch (err) {
showStatus(err.message, 'error'); showStatus(err.message, 'error');
} }
@ -780,6 +864,8 @@ function renderLogGrid(logs) {
{ field: 'operation', headerName: 'Operation', width: 90 }, { field: 'operation', headerName: 'Operation', width: 90 },
{ field: 'slice', headerName: 'Slice', flex: 1, { field: 'slice', headerName: 'Slice', flex: 1,
valueFormatter: p => p.value ? JSON.stringify(p.value) : '' }, valueFormatter: p => p.value ? JSON.stringify(p.value) : '' },
{ field: 'params', headerName: 'Params', flex: 1,
valueFormatter: p => p.value ? JSON.stringify(p.value) : '' },
{ field: 'note', headerName: 'Note', flex: 1 }, { field: 'note', headerName: 'Note', flex: 1 },
{ {
headerName: '', headerName: '',
@ -875,9 +961,12 @@ document.addEventListener('DOMContentLoaded', () => {
document.getElementById('vbtn-toggle').addEventListener('click', toggleVersionStatus); document.getElementById('vbtn-toggle').addEventListener('click', toggleVersionStatus);
document.getElementById('vbtn-delete').addEventListener('click', deleteVersion); document.getElementById('vbtn-delete').addEventListener('click', deleteVersion);
document.getElementById('btn-load-submit').addEventListener('click', submitLoadData); document.getElementById('btn-load-submit').addEventListener('click', submitLoadData);
document.getElementById('btn-load-cancel').addEventListener('click', () => { document.getElementById('btn-load-cancel').addEventListener('click', hideLoadModal);
document.getElementById('load-data-form').classList.add('hidden'); document.getElementById('btn-load-close').addEventListener('click', hideLoadModal);
}); document.getElementById('load-date-from').addEventListener('change', updateDatePreview);
document.getElementById('load-date-to').addEventListener('change', updateDatePreview);
document.getElementById('load-offset-years').addEventListener('input', updateDatePreview);
document.getElementById('load-offset-months').addEventListener('input', updateDatePreview);
// forecast view buttons // forecast view buttons
document.getElementById('btn-forecast-refresh').addEventListener('click', loadForecastData); document.getElementById('btn-forecast-refresh').addEventListener('click', loadForecastData);

View File

@ -94,18 +94,6 @@
<button class="btn" id="vbtn-toggle">Close Version</button> <button class="btn" id="vbtn-toggle">Close Version</button>
<button class="btn btn-danger" id="vbtn-delete">Delete</button> <button class="btn btn-danger" id="vbtn-delete">Delete</button>
</div> </div>
<div id="load-data-form" class="inline-form hidden">
<h3 id="load-data-title">Load Baseline</h3>
<div class="form-row">
<label>Date From<input type="date" id="load-date-from" /></label>
<label>Date To<input type="date" id="load-date-to" /></label>
<label>Note<input type="text" id="load-note" placeholder="optional" /></label>
</div>
<div class="form-actions">
<button id="btn-load-submit" class="btn btn-primary">Load</button>
<button id="btn-load-cancel" class="btn">Cancel</button>
</div>
</div>
</div> </div>
<!-- ===== FORECAST VIEW ===== --> <!-- ===== FORECAST VIEW ===== -->
@ -170,6 +158,54 @@
</main> </main>
</div> </div>
<!-- Load baseline / reference modal -->
<div id="load-data-modal" class="modal-overlay hidden">
<div class="modal load-data-modal">
<div class="modal-header">
<span id="load-data-title">Load Baseline</span>
<button id="btn-load-close" class="btn-icon">×</button>
</div>
<div id="load-data-body">
<div class="load-form-fields">
<label>Date From<input type="date" id="load-date-from" /></label>
<label>Date To<input type="date" id="load-date-to" /></label>
<div id="load-offset-fields">
<div class="load-offset-row">
<label>Offset Years<input type="number" id="load-offset-years" min="0" value="0" /></label>
<label>Offset Months<input type="number" id="load-offset-months" min="0" value="0" /></label>
</div>
</div>
<label>Note<input type="text" id="load-note" placeholder="optional" /></label>
</div>
<div id="load-date-preview" class="load-date-preview hidden">
<!-- reference: single chip list -->
<div id="load-preview-simple">
<div class="load-preview-label"></div>
<div id="load-date-chips" class="date-chips"></div>
</div>
<!-- baseline: before → after -->
<div id="load-preview-offset" class="hidden">
<div class="load-preview-columns">
<div class="load-preview-col">
<div class="load-preview-label">Source</div>
<div id="load-chips-source" class="date-chips"></div>
</div>
<div class="load-preview-arrow"></div>
<div class="load-preview-col">
<div class="load-preview-label">Projected</div>
<div id="load-chips-projected" class="date-chips"></div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button id="btn-load-submit" class="btn btn-primary">Load</button>
<button id="btn-load-cancel" class="btn">Cancel</button>
</div>
</div>
</div>
<!-- Table preview modal --> <!-- Table preview modal -->
<div id="modal-overlay" class="modal-overlay hidden"> <div id="modal-overlay" class="modal-overlay hidden">
<div class="modal"> <div class="modal">

View File

@ -312,6 +312,99 @@ body {
#modal-body { padding: 16px 18px; overflow-y: auto; flex: 1; font-size: 12px; } #modal-body { padding: 16px 18px; overflow-y: auto; flex: 1; font-size: 12px; }
.modal-footer { padding: 10px 18px; border-top: 1px solid #eee; display: flex; justify-content: flex-end; gap: 8px; } .modal-footer { padding: 10px 18px; border-top: 1px solid #eee; display: flex; justify-content: flex-end; gap: 8px; }
/* ============================================================
LOAD BASELINE / REFERENCE MODAL
============================================================ */
.load-data-modal { width: 480px; max-height: 80vh; }
#load-data-body {
padding: 20px 24px;
display: flex;
flex-direction: column;
gap: 20px;
overflow-y: auto;
}
.load-form-fields {
display: flex;
flex-direction: column;
gap: 12px;
}
.load-form-fields label {
font-size: 11px;
color: #555;
display: flex;
flex-direction: column;
gap: 4px;
}
.load-form-fields input[type=date],
.load-form-fields input[type=text] {
border: 1px solid #dce1e7;
padding: 7px 10px;
border-radius: 3px;
font-size: 13px;
color: #333;
width: 100%;
}
.load-form-fields input[type=date]:focus,
.load-form-fields input[type=text]:focus {
outline: none;
border-color: #2980b9;
box-shadow: 0 0 0 2px rgba(41,128,185,.15);
}
.load-offset-row { display: flex; gap: 12px; }
.load-offset-row label { flex: 1; }
.load-date-preview { display: flex; flex-direction: column; gap: 8px; }
.load-date-preview.hidden { display: none; }
.load-preview-columns {
display: flex;
gap: 10px;
align-items: flex-start;
}
.load-preview-col { flex: 1; display: flex; flex-direction: column; gap: 6px; }
.load-preview-arrow {
font-size: 20px;
color: #aaa;
padding-top: 18px;
flex-shrink: 0;
}
.load-preview-label {
font-size: 11px;
font-weight: 600;
color: #7f8c8d;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.date-chips {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.date-chip {
background: #eaf4fb;
color: #1a6fa8;
border: 1px solid #c5dff0;
padding: 3px 9px;
border-radius: 12px;
font-size: 11px;
white-space: nowrap;
}
.date-chip-summary {
font-size: 12px;
color: #555;
font-style: italic;
}
.preview-section h4 { font-size: 12px; margin-bottom: 6px; color: #555; } .preview-section h4 { font-size: 12px; margin-bottom: 6px; color: #555; }
.preview-section + .preview-section { margin-top: 16px; } .preview-section + .preview-section { margin-top: 16px; }
.preview-table { border-collapse: collapse; width: 100%; font-size: 11px; } .preview-table { border-collapse: collapse; width: 100%; font-size: 11px; }

View File

@ -77,29 +77,31 @@ module.exports = function(pool) {
// load baseline rows from source table for a date range // load baseline rows from source table for a date range
// deletes existing iter='baseline' rows before inserting (handled inside stored SQL) // deletes existing iter='baseline' rows before inserting (handled inside stored SQL)
router.post('/versions/:id/baseline', async (req, res) => { router.post('/versions/:id/baseline', async (req, res) => {
const { date_from, date_to, pf_user, note, replay } = req.body; const { date_from, date_to, date_offset, pf_user, note, replay } = req.body;
if (!date_from || !date_to) { if (!date_from || !date_to) {
return res.status(400).json({ error: 'date_from and date_to are required' }); return res.status(400).json({ error: 'date_from and date_to are required' });
} }
if (replay) { if (replay) {
return res.status(501).json({ error: 'replay is not yet implemented' }); return res.status(501).json({ error: 'replay is not yet implemented' });
} }
const dateOffset = date_offset || '0 days';
try { try {
const ctx = await getContext(parseInt(req.params.id), 'baseline'); const ctx = await getContext(parseInt(req.params.id), 'baseline');
if (!guardOpen(ctx.version, res)) return; if (!guardOpen(ctx.version, res)) return;
const sql = applyTokens(ctx.sql, { const sql = applyTokens(ctx.sql, {
fc_table: ctx.table, fc_table: ctx.table,
version_id: ctx.version.id, version_id: ctx.version.id,
pf_user: esc(pf_user || ''), pf_user: esc(pf_user || ''),
note: esc(note || ''), note: esc(note || ''),
params: esc(JSON.stringify({ date_from, date_to })), params: esc(JSON.stringify({ date_from, date_to, date_offset: dateOffset })),
date_from: esc(date_from), date_from: esc(date_from),
date_to: esc(date_to) date_to: esc(date_to),
date_offset: esc(dateOffset)
}); });
const result = await runSQL(sql); const result = await runSQL(sql);
res.json(result.rows[0]); res.json({ rows_affected: result.rows.length });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
res.status(err.status || 500).json({ error: err.message }); res.status(err.status || 500).json({ error: err.message });
@ -127,7 +129,7 @@ module.exports = function(pool) {
}); });
const result = await runSQL(sql); const result = await runSQL(sql);
res.json(result.rows[0]); res.json({ rows: result.rows, rows_affected: result.rows.length });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
res.status(err.status || 500).json({ error: err.message }); res.status(err.status || 500).json({ error: err.message });
@ -183,7 +185,7 @@ module.exports = function(pool) {
}); });
const result = await runSQL(sql); const result = await runSQL(sql);
res.json(result.rows[0]); res.json({ rows: result.rows, rows_affected: result.rows.length });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
res.status(err.status || 500).json({ error: err.message }); res.status(err.status || 500).json({ error: err.message });
@ -218,7 +220,7 @@ module.exports = function(pool) {
}); });
const result = await runSQL(sql); const result = await runSQL(sql);
res.json(result.rows[0]); res.json({ rows: result.rows, rows_affected: result.rows.length });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
res.status(err.status || 500).json({ error: err.message }); res.status(err.status || 500).json({ error: err.message });
@ -255,7 +257,7 @@ module.exports = function(pool) {
}); });
const result = await runSQL(sql); const result = await runSQL(sql);
res.json(result.rows[0]); res.json({ rows: result.rows, rows_affected: result.rows.length });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
res.status(err.status || 500).json({ error: err.message }); res.status(err.status || 500).json({ error: err.message });