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>
This commit is contained in:
@@ -2,31 +2,50 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDatabase } = require('../db/init');
|
||||
|
||||
// GET all litters
|
||||
// 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.*,
|
||||
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
|
||||
`).all();
|
||||
|
||||
litters.forEach(litter => {
|
||||
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) : [];
|
||||
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);
|
||||
});
|
||||
litter.actual_puppy_count = litter.puppies.length;
|
||||
});
|
||||
|
||||
res.json(litters);
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user