From 764fafa97437d8bcff1a0508643958aefff4a5d5 Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 10 Mar 2026 15:00:37 -0500 Subject: [PATCH] fix(backend): move /relations and /trial-pairing routes above /:id to prevent Express catch-all swallowing them --- server/routes/pedigree.js | 116 ++++++++++++++++++++------------------ 1 file changed, 60 insertions(+), 56 deletions(-) diff --git a/server/routes/pedigree.js b/server/routes/pedigree.js index 5229913..2dfb29c 100644 --- a/server/routes/pedigree.js +++ b/server/routes/pedigree.js @@ -17,7 +17,6 @@ function getAncestorMap(db, dogId, maxGen = 6) { if (!dog) return; if (!map.has(id)) map.set(id, []); map.get(id).push({ id: dog.id, name: dog.name, generation: gen }); - // Only walk parents on first encounter to prevent exponential blowup if (map.get(id).length === 1) { const parents = db.prepare(` SELECT p.parent_id FROM parents p WHERE p.dog_id = ? @@ -32,8 +31,8 @@ function getAncestorMap(db, dogId, maxGen = 6) { /** * isDirectRelation(db, sireId, damId) - * Checks within 3 generations whether one dog is a direct ancestor - * of the other. Returns { related, relationship }. + * Returns { related, relationship } if one dog is a direct ancestor + * of the other within 3 generations. */ function isDirectRelation(db, sireId, damId) { const sid = parseInt(sireId); @@ -57,11 +56,8 @@ function isDirectRelation(db, sireId, damId) { /** * calculateCOI(db, sireId, damId) * Wright Path Coefficient method. - * sire/dam included at gen 0 in their own maps so parent x offspring - * yields the correct 25% COI instead of 0%. - * - * Per path: COI += (0.5)^(sireGen + damGen + 1) - * Parent x offspring: sireGen=0, damGen=1 => (0.5)^2 = 0.25 = 25% + * Dogs included at gen 0 in their own maps so parent x offspring + * yields 25% COI. Per path: (0.5)^(sireGen + damGen + 1) */ function calculateCOI(db, sireId, damId) { const sid = parseInt(sireId); @@ -69,7 +65,6 @@ function calculateCOI(db, sireId, damId) { const sireMap = getAncestorMap(db, sid); const damMap = getAncestorMap(db, did); - // Common ancestors excluding the pair themselves const commonIds = [...sireMap.keys()].filter( id => damMap.has(id) && id !== sid && id !== did ); @@ -108,7 +103,61 @@ function calculateCOI(db, sireId, damId) { }; } -// ── GET pedigree tree ───────────────────────────────────────────────────────── +// ============================================================= +// IMPORTANT: Specific named routes MUST be registered BEFORE +// the /:id wildcard, or Express will match 'relations' and +// 'trial-pairing' as dog IDs and return 404/wrong data. +// ============================================================= + +// POST /api/pedigree/trial-pairing +router.post('/trial-pairing', (req, res) => { + try { + const { sire_id, dam_id } = req.body; + if (!sire_id || !dam_id) { + return res.status(400).json({ error: 'Both sire_id and dam_id are required' }); + } + + const db = getDatabase(); + const sire = db.prepare("SELECT * FROM dogs WHERE id = ? AND sex = 'male'").get(sire_id); + const dam = db.prepare("SELECT * FROM dogs WHERE id = ? AND sex = 'female'").get(dam_id); + + if (!sire || !dam) { + return res.status(404).json({ error: 'Invalid sire or dam — check sex values in database' }); + } + + const relation = isDirectRelation(db, sire_id, dam_id); + const result = calculateCOI(db, sire_id, dam_id); + + res.json({ + sire: { id: sire.id, name: sire.name }, + dam: { id: dam.id, name: dam.name }, + coi: result.coefficient, + commonAncestors: result.commonAncestors, + directRelation: relation.related ? relation.relationship : null, + recommendation: result.coefficient < 5 ? 'Low risk' + : result.coefficient < 10 ? 'Moderate risk' + : 'High risk' + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// GET /api/pedigree/relations/:sireId/:damId +router.get('/relations/:sireId/:damId', (req, res) => { + try { + const db = getDatabase(); + res.json(isDirectRelation(db, req.params.sireId, req.params.damId)); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// ============================================================= +// Wildcard routes last +// ============================================================= + +// GET /api/pedigree/:id router.get('/:id', (req, res) => { try { const db = getDatabase(); @@ -138,7 +187,7 @@ router.get('/:id', (req, res) => { } }); -// ── GET descendants ─────────────────────────────────────────────────────────── +// GET /api/pedigree/:id/descendants router.get('/:id/descendants', (req, res) => { try { const db = getDatabase(); @@ -169,49 +218,4 @@ router.get('/:id/descendants', (req, res) => { } }); -// ── GET direct-relation check (used by frontend before simulate) ────────────── -// GET /api/pedigree/relations/:sireId/:damId -router.get('/relations/:sireId/:damId', (req, res) => { - try { - const db = getDatabase(); - res.json(isDirectRelation(db, req.params.sireId, req.params.damId)); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}); - -// ── POST trial pairing ──────────────────────────────────────────────────────── -router.post('/trial-pairing', (req, res) => { - try { - const { sire_id, dam_id } = req.body; - if (!sire_id || !dam_id) { - return res.status(400).json({ error: 'Both sire_id and dam_id are required' }); - } - - const db = getDatabase(); - const sire = db.prepare("SELECT * FROM dogs WHERE id = ? AND sex = 'male'").get(sire_id); - const dam = db.prepare("SELECT * FROM dogs WHERE id = ? AND sex = 'female'").get(dam_id); - - if (!sire || !dam) { - return res.status(404).json({ error: 'Invalid sire or dam — check sex values in database' }); - } - - const relation = isDirectRelation(db, sire_id, dam_id); - const result = calculateCOI(db, sire_id, dam_id); - - res.json({ - sire: { id: sire.id, name: sire.name }, - dam: { id: dam.id, name: dam.name }, - coi: result.coefficient, - commonAncestors: result.commonAncestors, - directRelation: relation.related ? relation.relationship : null, - recommendation: result.coefficient < 5 ? 'Low risk' - : result.coefficient < 10 ? 'Moderate risk' - : 'High risk' - }); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}); - module.exports = router; -- 2.49.1