fix(backend): move /relations and /trial-pairing routes above /:id to prevent Express catch-all swallowing them
This commit is contained in:
@@ -17,7 +17,6 @@ function getAncestorMap(db, dogId, maxGen = 6) {
|
|||||||
if (!dog) return;
|
if (!dog) return;
|
||||||
if (!map.has(id)) map.set(id, []);
|
if (!map.has(id)) map.set(id, []);
|
||||||
map.get(id).push({ id: dog.id, name: dog.name, generation: gen });
|
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) {
|
if (map.get(id).length === 1) {
|
||||||
const parents = db.prepare(`
|
const parents = db.prepare(`
|
||||||
SELECT p.parent_id FROM parents p WHERE p.dog_id = ?
|
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)
|
* isDirectRelation(db, sireId, damId)
|
||||||
* Checks within 3 generations whether one dog is a direct ancestor
|
* Returns { related, relationship } if one dog is a direct ancestor
|
||||||
* of the other. Returns { related, relationship }.
|
* of the other within 3 generations.
|
||||||
*/
|
*/
|
||||||
function isDirectRelation(db, sireId, damId) {
|
function isDirectRelation(db, sireId, damId) {
|
||||||
const sid = parseInt(sireId);
|
const sid = parseInt(sireId);
|
||||||
@@ -57,11 +56,8 @@ function isDirectRelation(db, sireId, damId) {
|
|||||||
/**
|
/**
|
||||||
* calculateCOI(db, sireId, damId)
|
* calculateCOI(db, sireId, damId)
|
||||||
* Wright Path Coefficient method.
|
* Wright Path Coefficient method.
|
||||||
* sire/dam included at gen 0 in their own maps so parent x offspring
|
* Dogs included at gen 0 in their own maps so parent x offspring
|
||||||
* yields the correct 25% COI instead of 0%.
|
* yields 25% COI. Per path: (0.5)^(sireGen + damGen + 1)
|
||||||
*
|
|
||||||
* Per path: COI += (0.5)^(sireGen + damGen + 1)
|
|
||||||
* Parent x offspring: sireGen=0, damGen=1 => (0.5)^2 = 0.25 = 25%
|
|
||||||
*/
|
*/
|
||||||
function calculateCOI(db, sireId, damId) {
|
function calculateCOI(db, sireId, damId) {
|
||||||
const sid = parseInt(sireId);
|
const sid = parseInt(sireId);
|
||||||
@@ -69,7 +65,6 @@ function calculateCOI(db, sireId, damId) {
|
|||||||
const sireMap = getAncestorMap(db, sid);
|
const sireMap = getAncestorMap(db, sid);
|
||||||
const damMap = getAncestorMap(db, did);
|
const damMap = getAncestorMap(db, did);
|
||||||
|
|
||||||
// Common ancestors excluding the pair themselves
|
|
||||||
const commonIds = [...sireMap.keys()].filter(
|
const commonIds = [...sireMap.keys()].filter(
|
||||||
id => damMap.has(id) && id !== sid && id !== did
|
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) => {
|
router.get('/:id', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const db = getDatabase();
|
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) => {
|
router.get('/:id/descendants', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const db = getDatabase();
|
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;
|
module.exports = router;
|
||||||
|
|||||||
Reference in New Issue
Block a user