Files
breedr/server/routes/litters.js
jason b8633863b0 fix: add pagination to unbounded GET endpoints
All list endpoints now accept ?page and ?limit (default 50, max 200) and
return { data, total, page, limit } instead of a bare array, preventing
memory and performance failures at scale.

- GET /api/dogs: adds pagination, server-side search (?search) and sex
  filter (?sex), and a stats aggregate (total/males/females) for the
  Dashboard to avoid counting from the array
- GET /api/litters: adds pagination; also fixes N+1 query by fetching
  all puppies for the current page in a single query instead of one per
  litter
- DogList: moves search/sex filtering server-side with 300ms debounce;
  adds Prev/Next pagination controls
- LitterList: uses paginated response; adds Prev/Next pagination controls
- Dashboard: reads counts from stats/total fields instead of array length
- LitterDetail, LitterForm: switch dogs fetch to /api/dogs/all (complete
  list, no pagination, for sire/dam dropdowns)
- DogForm: updates litters fetch to use paginated response shape

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 16:40:28 -05:00

253 lines
8.6 KiB
JavaScript

const express = require('express');
const router = express.Router();
const { getDatabase } = require('../db/init');
// GET all litters (paginated)
// ?page=1&limit=50
// Response: { data, total, page, limit }
router.get('/', (req, res) => {
try {
const db = getDatabase();
const page = Math.max(1, parseInt(req.query.page, 10) || 1);
const limit = Math.min(200, Math.max(1, parseInt(req.query.limit, 10) || 50));
const offset = (page - 1) * limit;
const total = db.prepare('SELECT COUNT(*) as count FROM litters').get().count;
const litters = db.prepare(`
SELECT l.*,
s.name as sire_name, s.registration_number as sire_reg,
d.name as dam_name, d.registration_number as dam_reg
FROM litters l
JOIN dogs s ON l.sire_id = s.id
JOIN dogs d ON l.dam_id = d.id
ORDER BY l.breeding_date DESC
LIMIT ? OFFSET ?
`).all(limit, offset);
if (litters.length > 0) {
const litterIds = litters.map(l => l.id);
const placeholders = litterIds.map(() => '?').join(',');
const allPuppies = db.prepare(`
SELECT * FROM dogs WHERE litter_id IN (${placeholders}) AND is_active = 1
`).all(...litterIds);
const puppiesByLitter = {};
allPuppies.forEach(p => {
p.photo_urls = p.photo_urls ? JSON.parse(p.photo_urls) : [];
if (!puppiesByLitter[p.litter_id]) puppiesByLitter[p.litter_id] = [];
puppiesByLitter[p.litter_id].push(p);
});
litters.forEach(l => {
l.puppies = puppiesByLitter[l.id] || [];
l.actual_puppy_count = l.puppies.length;
});
}
res.json({ data: litters, total, page, limit });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// GET single litter with puppies
router.get('/:id', (req, res) => {
try {
const db = getDatabase();
const litter = db.prepare(`
SELECT l.*,
s.name as sire_name, s.registration_number as sire_reg, s.breed as sire_breed,
d.name as dam_name, d.registration_number as dam_reg, d.breed as dam_breed
FROM litters l
JOIN dogs s ON l.sire_id = s.id
JOIN dogs d ON l.dam_id = d.id
WHERE l.id = ?
`).get(req.params.id);
if (!litter) {
return res.status(404).json({ error: 'Litter not found' });
}
litter.puppies = db.prepare(`
SELECT * FROM dogs WHERE litter_id = ? AND is_active = 1
`).all(litter.id);
litter.puppies.forEach(puppy => {
puppy.photo_urls = puppy.photo_urls ? JSON.parse(puppy.photo_urls) : [];
});
litter.actual_puppy_count = litter.puppies.length;
res.json(litter);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// POST create new litter
router.post('/', (req, res) => {
try {
const { sire_id, dam_id, breeding_date, whelping_date, puppy_count, notes } = req.body;
if (!sire_id || !dam_id || !breeding_date) {
return res.status(400).json({ error: 'Sire, dam, and breeding date are required' });
}
const db = getDatabase();
const sire = db.prepare('SELECT sex FROM dogs WHERE id = ?').get(sire_id);
const dam = db.prepare('SELECT sex FROM dogs WHERE id = ?').get(dam_id);
if (!sire || sire.sex !== 'male') {
return res.status(400).json({ error: 'Invalid sire' });
}
if (!dam || dam.sex !== 'female') {
return res.status(400).json({ error: 'Invalid dam' });
}
const result = db.prepare(`
INSERT INTO litters (sire_id, dam_id, breeding_date, whelping_date, puppy_count, notes)
VALUES (?, ?, ?, ?, ?, ?)
`).run(sire_id, dam_id, breeding_date, whelping_date || null, puppy_count || 0, notes || null);
const litter = db.prepare('SELECT * FROM litters WHERE id = ?').get(result.lastInsertRowid);
res.status(201).json(litter);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// PUT update litter
router.put('/:id', (req, res) => {
try {
const { breeding_date, whelping_date, puppy_count, notes } = req.body;
const db = getDatabase();
db.prepare(`
UPDATE litters
SET breeding_date = ?, whelping_date = ?, puppy_count = ?, notes = ?
WHERE id = ?
`).run(breeding_date, whelping_date || null, puppy_count || 0, notes || null, req.params.id);
const litter = db.prepare('SELECT * FROM litters WHERE id = ?').get(req.params.id);
res.json(litter);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// POST link puppy to litter
router.post('/:id/puppies/:puppyId', (req, res) => {
try {
const { id: litterId, puppyId } = req.params;
const db = getDatabase();
const litter = db.prepare('SELECT sire_id, dam_id FROM litters WHERE id = ?').get(litterId);
if (!litter) return res.status(404).json({ error: 'Litter not found' });
const puppy = db.prepare('SELECT id FROM dogs WHERE id = ?').get(puppyId);
if (!puppy) return res.status(404).json({ error: 'Puppy not found' });
db.prepare('UPDATE dogs SET litter_id = ? WHERE id = ?').run(litterId, puppyId);
const existingParents = db.prepare('SELECT parent_type FROM parents WHERE dog_id = ?').all(puppyId);
const hasSire = existingParents.some(p => p.parent_type === 'sire');
const hasDam = existingParents.some(p => p.parent_type === 'dam');
if (!hasSire) {
db.prepare('INSERT OR IGNORE INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, \'sire\')').run(puppyId, litter.sire_id);
}
if (!hasDam) {
db.prepare('INSERT OR IGNORE INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, \'dam\')').run(puppyId, litter.dam_id);
}
res.json({ message: 'Puppy linked to litter successfully' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// DELETE remove puppy from litter
router.delete('/:id/puppies/:puppyId', (req, res) => {
try {
const { puppyId } = req.params;
const db = getDatabase();
db.prepare('UPDATE dogs SET litter_id = NULL WHERE id = ?').run(puppyId);
res.json({ message: 'Puppy removed from litter' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// ─── Puppy Weight / Health Log ───────────────────────────────────────────────
// GET weight/health logs for a puppy
router.get('/:litterId/puppies/:puppyId/logs', (req, res) => {
try {
const db = getDatabase();
// Use health_records table with note field to store weight logs
const logs = db.prepare(`
SELECT * FROM health_records
WHERE dog_id = ? AND record_type = 'weight_log'
ORDER BY record_date ASC
`).all(req.params.puppyId);
res.json(logs);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// POST add weight/health log entry for a puppy
router.post('/:litterId/puppies/:puppyId/logs', (req, res) => {
try {
const { puppyId } = req.params;
const { record_date, weight_oz, weight_lbs, notes, record_type } = req.body;
if (!record_date) return res.status(400).json({ error: 'record_date is required' });
const db = getDatabase();
// Store weight as notes JSON in health_records
const description = JSON.stringify({
weight_oz: weight_oz || null,
weight_lbs: weight_lbs || null,
notes: notes || ''
});
const result = db.prepare(`
INSERT INTO health_records (dog_id, record_type, record_date, description, vet_name)
VALUES (?, ?, ?, ?, ?)
`).run(puppyId, record_type || 'weight_log', record_date, description, null);
const log = db.prepare('SELECT * FROM health_records WHERE id = ?').get(result.lastInsertRowid);
res.status(201).json(log);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// DELETE weight/health log entry
router.delete('/:litterId/puppies/:puppyId/logs/:logId', (req, res) => {
try {
const db = getDatabase();
db.prepare('DELETE FROM health_records WHERE id = ? AND dog_id = ?').run(req.params.logId, req.params.puppyId);
res.json({ message: 'Log entry deleted' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// DELETE litter
router.delete('/:id', (req, res) => {
try {
const db = getDatabase();
db.prepare('UPDATE dogs SET litter_id = NULL WHERE litter_id = ?').run(req.params.id);
db.prepare('DELETE FROM litters WHERE id = ?').run(req.params.id);
res.json({ message: 'Litter deleted successfully' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;