- 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>
179 lines
5.4 KiB
JavaScript
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;
|
|
};
|