const express = require('express'); const { fcTable, mapType } = require('../lib/utils'); module.exports = function(pool) { const router = express.Router(); // list versions for a source router.get('/sources/:id/versions', async (req, res) => { try { const result = await pool.query( `SELECT * FROM pf.version WHERE source_id = $1 ORDER BY created_at DESC`, [req.params.id] ); res.json(result.rows); } catch (err) { console.error(err); res.status(500).json({ error: err.message }); } }); // create a new version // inserts version row, then CREATE TABLE pf.fc_{tname}_{version_id} in one transaction router.post('/sources/:id/versions', async (req, res) => { const sourceId = parseInt(req.params.id); const { name, description, created_by, exclude_iters } = req.body; if (!name) return res.status(400).json({ error: 'name is required' }); const client = await pool.connect(); try { // fetch source const srcResult = await client.query( `SELECT * FROM pf.source WHERE id = $1`, [sourceId] ); if (srcResult.rows.length === 0) { return res.status(404).json({ error: 'Source not found' }); } const source = srcResult.rows[0]; // fetch col_meta joined to information_schema for data types const colResult = await client.query(` SELECT m.cname, m.role, m.opos, i.data_type, i.numeric_precision, i.numeric_scale FROM pf.col_meta m JOIN information_schema.columns i ON i.table_schema = $2 AND i.table_name = $3 AND i.column_name = m.cname WHERE m.source_id = $1 AND m.role NOT IN ('ignore') ORDER BY m.opos `, [sourceId, source.schema, source.tname]); if (colResult.rows.length === 0) { return res.status(400).json({ error: 'No usable columns in col_meta — configure roles before creating a version' }); } await client.query('BEGIN'); // insert version to get id const verResult = await client.query(` INSERT INTO pf.version (source_id, name, description, created_by, exclude_iters) VALUES ($1, $2, $3, $4, $5) RETURNING * `, [ sourceId, name, description || null, created_by || null, exclude_iters ? JSON.stringify(exclude_iters) : '["reference"]' ]); const version = verResult.rows[0]; // build CREATE TABLE DDL using col_meta + mapped data types const table = fcTable(source.tname, version.id); const colDefs = colResult.rows .filter(c => c.cname !== 'id') .map(c => { const pgType = mapType(c.data_type, c.numeric_precision, c.numeric_scale); const quoted = `"${c.cname}"`; return ` ${quoted.padEnd(26)}${pgType}`; }).join(',\n'); const ddl = ` CREATE TABLE ${table} ( id bigserial PRIMARY KEY, ${colDefs}, iter text NOT NULL, logid bigint NOT NULL, pf_user text, created_at timestamptz NOT NULL DEFAULT now() ) `; await client.query(ddl); await client.query('COMMIT'); res.status(201).json({ ...version, fc_table: table }); } catch (err) { await client.query('ROLLBACK'); console.error(err); if (err.code === '23505') { return res.status(409).json({ error: 'A version with that name already exists for this source' }); } res.status(500).json({ error: err.message }); } finally { client.release(); } }); // update version name, description, or exclude_iters router.put('/versions/:id', async (req, res) => { const { name, description, exclude_iters } = req.body; try { const result = await pool.query(` UPDATE pf.version SET name = COALESCE($2, name), description = COALESCE($3, description), exclude_iters = COALESCE($4, exclude_iters) WHERE id = $1 RETURNING * `, [ req.params.id, name || null, description || null, exclude_iters ? JSON.stringify(exclude_iters) : null ]); if (result.rows.length === 0) { return res.status(404).json({ error: 'Version not found' }); } res.json(result.rows[0]); } catch (err) { console.error(err); res.status(500).json({ error: err.message }); } }); // close a version — blocks further edits router.post('/versions/:id/close', async (req, res) => { const { pf_user } = req.body; try { const result = await pool.query(` UPDATE pf.version SET status = 'closed', closed_at = now(), closed_by = $2 WHERE id = $1 AND status = 'open' RETURNING * `, [req.params.id, pf_user || null]); if (result.rows.length === 0) { return res.status(404).json({ error: 'Version not found or already closed' }); } res.json(result.rows[0]); } catch (err) { console.error(err); res.status(500).json({ error: err.message }); } }); // reopen a closed version router.post('/versions/:id/reopen', async (req, res) => { try { const result = await pool.query(` UPDATE pf.version SET status = 'open', closed_at = NULL, closed_by = NULL WHERE id = $1 AND status = 'closed' RETURNING * `, [req.params.id]); if (result.rows.length === 0) { return res.status(404).json({ error: 'Version not found or already open' }); } res.json(result.rows[0]); } catch (err) { console.error(err); res.status(500).json({ error: err.message }); } }); // delete a version — drops forecast table then deletes version record // log entries are removed by ON DELETE CASCADE on pf.log.version_id router.delete('/versions/:id', async (req, res) => { const versionId = parseInt(req.params.id); const client = await pool.connect(); try { const verResult = await client.query(` SELECT v.*, s.tname FROM pf.version v JOIN pf.source s ON s.id = v.source_id WHERE v.id = $1 `, [versionId]); if (verResult.rows.length === 0) { return res.status(404).json({ error: 'Version not found' }); } const { tname } = verResult.rows[0]; const table = fcTable(tname, versionId); await client.query('BEGIN'); await client.query(`DROP TABLE IF EXISTS ${table}`); await client.query(`DELETE FROM pf.version WHERE id = $1`, [versionId]); await client.query('COMMIT'); res.json({ message: 'Version deleted', fc_table: table }); } catch (err) { await client.query('ROLLBACK'); console.error(err); res.status(500).json({ error: err.message }); } finally { client.release(); } }); return router; };