fix(backend): move named routes above /:id wildcard — Express route order bug causing 0% COI
This commit is contained in:
@@ -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,79 +103,13 @@ function calculateCOI(db, sireId, damId) {
|
||||
};
|
||||
}
|
||||
|
||||
// ── GET pedigree tree ─────────────────────────────────────────────────────────
|
||||
router.get('/:id', (req, res) => {
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const generations = parseInt(req.query.generations) || 5;
|
||||
// =============================================================
|
||||
// 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.
|
||||
// =============================================================
|
||||
|
||||
function buildTree(dogId, currentGen = 0) {
|
||||
if (currentGen >= generations) return null;
|
||||
const dog = db.prepare('SELECT * FROM dogs WHERE id = ?').get(dogId);
|
||||
if (!dog) return null;
|
||||
dog.photo_urls = dog.photo_urls ? JSON.parse(dog.photo_urls) : [];
|
||||
const parents = db.prepare('SELECT parent_type, parent_id FROM parents WHERE dog_id = ?').all(dogId);
|
||||
const sire = parents.find(p => p.parent_type === 'sire');
|
||||
const dam = parents.find(p => p.parent_type === 'dam');
|
||||
return {
|
||||
...dog,
|
||||
generation: currentGen,
|
||||
sire: sire ? buildTree(sire.parent_id, currentGen + 1) : null,
|
||||
dam: dam ? buildTree(dam.parent_id, currentGen + 1) : null
|
||||
};
|
||||
}
|
||||
|
||||
const tree = buildTree(req.params.id);
|
||||
if (!tree) return res.status(404).json({ error: 'Dog not found' });
|
||||
res.json(tree);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ── GET descendants ───────────────────────────────────────────────────────────
|
||||
router.get('/:id/descendants', (req, res) => {
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const generations = parseInt(req.query.generations) || 3;
|
||||
|
||||
function buildDescendantTree(dogId, currentGen = 0) {
|
||||
if (currentGen >= generations) return null;
|
||||
const dog = db.prepare('SELECT * FROM dogs WHERE id = ?').get(dogId);
|
||||
if (!dog) return null;
|
||||
dog.photo_urls = dog.photo_urls ? JSON.parse(dog.photo_urls) : [];
|
||||
const offspring = db.prepare(`
|
||||
SELECT DISTINCT d.id, d.name, d.sex, d.birth_date
|
||||
FROM dogs d JOIN parents p ON d.id = p.dog_id
|
||||
WHERE p.parent_id = ? AND d.is_active = 1
|
||||
`).all(dogId);
|
||||
return {
|
||||
...dog,
|
||||
generation: currentGen,
|
||||
offspring: offspring.map(c => buildDescendantTree(c.id, currentGen + 1))
|
||||
};
|
||||
}
|
||||
|
||||
const tree = buildDescendantTree(req.params.id);
|
||||
if (!tree) return res.status(404).json({ error: 'Dog not found' });
|
||||
res.json(tree);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ── 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 ────────────────────────────────────────────────────────
|
||||
// POST /api/pedigree/trial-pairing
|
||||
router.post('/trial-pairing', (req, res) => {
|
||||
try {
|
||||
const { sire_id, dam_id } = req.body;
|
||||
@@ -214,4 +143,79 @@ router.post('/trial-pairing', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 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();
|
||||
const generations = parseInt(req.query.generations) || 5;
|
||||
|
||||
function buildTree(dogId, currentGen = 0) {
|
||||
if (currentGen >= generations) return null;
|
||||
const dog = db.prepare('SELECT * FROM dogs WHERE id = ?').get(dogId);
|
||||
if (!dog) return null;
|
||||
dog.photo_urls = dog.photo_urls ? JSON.parse(dog.photo_urls) : [];
|
||||
const parents = db.prepare('SELECT parent_type, parent_id FROM parents WHERE dog_id = ?').all(dogId);
|
||||
const sire = parents.find(p => p.parent_type === 'sire');
|
||||
const dam = parents.find(p => p.parent_type === 'dam');
|
||||
return {
|
||||
...dog,
|
||||
generation: currentGen,
|
||||
sire: sire ? buildTree(sire.parent_id, currentGen + 1) : null,
|
||||
dam: dam ? buildTree(dam.parent_id, currentGen + 1) : null
|
||||
};
|
||||
}
|
||||
|
||||
const tree = buildTree(req.params.id);
|
||||
if (!tree) return res.status(404).json({ error: 'Dog not found' });
|
||||
res.json(tree);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/pedigree/:id/descendants
|
||||
router.get('/:id/descendants', (req, res) => {
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const generations = parseInt(req.query.generations) || 3;
|
||||
|
||||
function buildDescendantTree(dogId, currentGen = 0) {
|
||||
if (currentGen >= generations) return null;
|
||||
const dog = db.prepare('SELECT * FROM dogs WHERE id = ?').get(dogId);
|
||||
if (!dog) return null;
|
||||
dog.photo_urls = dog.photo_urls ? JSON.parse(dog.photo_urls) : [];
|
||||
const offspring = db.prepare(`
|
||||
SELECT DISTINCT d.id, d.name, d.sex, d.birth_date
|
||||
FROM dogs d JOIN parents p ON d.id = p.dog_id
|
||||
WHERE p.parent_id = ? AND d.is_active = 1
|
||||
`).all(dogId);
|
||||
return {
|
||||
...dog,
|
||||
generation: currentGen,
|
||||
offspring: offspring.map(c => buildDescendantTree(c.id, currentGen + 1))
|
||||
};
|
||||
}
|
||||
|
||||
const tree = buildDescendantTree(req.params.id);
|
||||
if (!tree) return res.status(404).json({ error: 'Dog not found' });
|
||||
res.json(tree);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
Reference in New Issue
Block a user