diff --git a/routes/sources.js b/routes/sources.js index 9500104..67eefcd 100644 --- a/routes/sources.js +++ b/routes/sources.js @@ -87,13 +87,23 @@ module.exports = function(pool) { if (!Array.isArray(cols)) { return res.status(400).json({ error: 'body must be an array' }); } + // Exactly one per source: the scope is a single IN list against a single + // column, and two flagged would silently mean whichever one a .find() + // reached first -- the trap is_key already fell into (see CLAUDE.md). + const territoryCols = cols.filter(c => c.is_territory).map(c => c.cname); + if (territoryCols.length > 1) { + return res.status(400).json({ + error: `Only one column can be the territory. Flagged: ${territoryCols.join(', ')}` + }); + } + const client = await pool.connect(); try { await client.query('BEGIN'); for (const col of cols) { await client.query(` - INSERT INTO pf.col_meta (source_id, cname, label, role, is_key, dim_group, dim_period_col, in_grain, opos) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + INSERT INTO pf.col_meta (source_id, cname, label, role, is_key, dim_group, dim_period_col, in_grain, is_territory, opos) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (source_id, cname) DO UPDATE SET label = EXCLUDED.label, role = EXCLUDED.role, @@ -101,6 +111,7 @@ module.exports = function(pool) { dim_group = EXCLUDED.dim_group, dim_period_col = EXCLUDED.dim_period_col, in_grain = EXCLUDED.in_grain, + is_territory = EXCLUDED.is_territory, opos = EXCLUDED.opos `, [ sourceId, @@ -111,6 +122,7 @@ module.exports = function(pool) { col.dim_group || null, col.dim_period_col || null, col.in_grain || false, + col.is_territory || false, col.opos || null ]); } diff --git a/ui/src/views/Setup.jsx b/ui/src/views/Setup.jsx index 09f1ed1..8b137ac 100644 --- a/ui/src/views/Setup.jsx +++ b/ui/src/views/Setup.jsx @@ -354,6 +354,7 @@ export default function Setup({ refreshSources }) {