dataflow/api/routes/mappings.js
Paul Trowbridge 928a54932d Add multi-capture regex, computed view fields, collapsible rules, and live preview
- Support multi-capture-group regex: mappings.input_value changed to JSONB,
  regexp_match() result stored as scalar or array JSONB in transformed column
- Computed expression fields in generated views: {fieldname} refs substituted
  with (transformed->>'fieldname')::numeric for arithmetic in view columns
- Fix generate_source_view to DROP VIEW before CREATE (avoids column drop error)
- Collapsible rule cards that open directly to inline edit form
- Debounced live regex preview (extract + replace) with popout modal for 50 rows
- Records page now shows dfv.<source> view output instead of raw records
- Unified field table in Sources: single table with In view, Seq, expression columns
- Fix "Rule already exists" error when editing by passing rule.id directly to submit
- Fix Sources page clearing on F5 by watching sourceObj?.name in useEffect dep

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 16:37:15 -04:00

179 lines
5.4 KiB
JavaScript

/**
* Mappings Routes
* Manage value mappings
*/
const express = require('express');
module.exports = (pool) => {
const router = express.Router();
// List all mappings for a source
router.get('/source/:source_name', async (req, res, next) => {
try {
const { rule_name } = req.query;
let query = 'SELECT * FROM mappings WHERE source_name = $1';
const params = [req.params.source_name];
if (rule_name) {
query += ' AND rule_name = $2';
params.push(rule_name);
}
query += ' ORDER BY rule_name, input_value';
const result = await pool.query(query, params);
res.json(result.rows);
} catch (err) {
next(err);
}
});
// Get unmapped values
router.get('/source/:source_name/unmapped', async (req, res, next) => {
try {
const { rule_name } = req.query;
const result = await pool.query(
'SELECT * FROM get_unmapped_values($1, $2)',
[req.params.source_name, rule_name || null]
);
res.json(result.rows);
} catch (err) {
next(err);
}
});
// Get single mapping
router.get('/:id', async (req, res, next) => {
try {
const result = await pool.query(
'SELECT * FROM mappings WHERE id = $1',
[req.params.id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Mapping not found' });
}
res.json(result.rows[0]);
} catch (err) {
next(err);
}
});
// Create mapping
router.post('/', async (req, res, next) => {
try {
const { source_name, rule_name, input_value, output } = req.body;
if (!source_name || !rule_name || !input_value || !output) {
return res.status(400).json({
error: 'Missing required fields: source_name, rule_name, input_value, output'
});
}
const result = await pool.query(
`INSERT INTO mappings (source_name, rule_name, input_value, output)
VALUES ($1, $2, $3, $4)
RETURNING *`,
[source_name, rule_name, JSON.stringify(input_value), JSON.stringify(output)]
);
res.status(201).json(result.rows[0]);
} catch (err) {
if (err.code === '23505') { // Unique violation
return res.status(409).json({ error: 'Mapping already exists' });
}
if (err.code === '23503') { // Foreign key violation
return res.status(404).json({ error: 'Source or rule not found' });
}
next(err);
}
});
// Bulk create mappings
router.post('/bulk', async (req, res, next) => {
const client = await pool.connect();
try {
const { mappings } = req.body;
if (!Array.isArray(mappings)) {
return res.status(400).json({ error: 'Expected array of mappings' });
}
await client.query('BEGIN');
const results = [];
for (const mapping of mappings) {
const { source_name, rule_name, input_value, output } = mapping;
const result = await client.query(
`INSERT INTO mappings (source_name, rule_name, input_value, output)
VALUES ($1, $2, $3, $4)
ON CONFLICT (source_name, rule_name, input_value)
DO UPDATE SET output = EXCLUDED.output
RETURNING *`,
[source_name, rule_name, JSON.stringify(input_value), JSON.stringify(output)]
);
results.push(result.rows[0]);
}
await client.query('COMMIT');
res.status(201).json({ count: results.length, mappings: results });
} catch (err) {
await client.query('ROLLBACK');
next(err);
} finally {
client.release();
}
});
// Update mapping
router.put('/:id', async (req, res, next) => {
try {
const { input_value, output } = req.body;
const result = await pool.query(
`UPDATE mappings
SET input_value = COALESCE($2, input_value),
output = COALESCE($3, output)
WHERE id = $1
RETURNING *`,
[req.params.id, input_value, output ? JSON.stringify(output) : null]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Mapping not found' });
}
res.json(result.rows[0]);
} catch (err) {
next(err);
}
});
// Delete mapping
router.delete('/:id', async (req, res, next) => {
try {
const result = await pool.query(
'DELETE FROM mappings WHERE id = $1 RETURNING id',
[req.params.id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Mapping not found' });
}
res.json({ success: true, deleted: result.rows[0].id });
} catch (err) {
next(err);
}
});
return router;
};