From 2e4a9031085869e5566150ff8c8c379c002225c4 Mon Sep 17 00:00:00 2001 From: "Aaron K. Clark" Date: Sun, 17 May 2026 21:20:27 -0500 Subject: [PATCH] feat(api): Link header + true total count on timeentry/bycompany MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Continues the Link-header rollout (#71). Two related changes bundled here: 1) Switch listByCompany from TimeEntry.findAll to findAndCountAll so the `count` field in the body reflects the **total** matching rows, not just the current page's length. Old behavior was misleading — a 2-page result with 30 total entries (page size 25) returned `count: 25` on page 1 and `count: 5` on page 2, with no way to know there were 30 in all. 2) Wire the Link header builder. Same Access-Control-Expose- Headers: Link header as customer/bycompany. 3) Also surfaces `offset` in the body envelope (it was missing from listByCompany even though the schema accepts it). No new tests added — the existing timeentry suite covers the auth contract and the body still has count/limit/timeEntries (plus the new offset). Link-header tests live in tests/unit/pagination.test.js. Suite: 269 / 269 + 4 integration skipped (unchanged). Co-Authored-By: Claude Opus 4.7 (1M context) --- app/controllers/timeentrycontroller.js | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/app/controllers/timeentrycontroller.js b/app/controllers/timeentrycontroller.js index f4f53e0..18930fc 100644 --- a/app/controllers/timeentrycontroller.js +++ b/app/controllers/timeentrycontroller.js @@ -5,6 +5,7 @@ const db = require('../config/db.config.js'); const log = require('../config/logger.js'); const auth = require('../middleware/auth.js'); +const { buildLinkHeader } = require('../middleware/pagination.js'); const TimeEntry = db.TimeEntry; // Auth helpers used to live inline here — they now share a single @@ -163,17 +164,34 @@ exports.listByCompany = async (req, res) => { const limit = Number.isInteger(requestedLimit) && requestedLimit > 0 ? Math.min(requestedLimit, 500) : 100; + const requestedOffset = parseInt(req.query.offset, 10); + const offset = Number.isInteger(requestedOffset) && requestedOffset >= 0 + ? requestedOffset + : 0; try { - const entries = await TimeEntry.findAll({ where, limit, order: [['teStartedAt', 'DESC']] }); + // Switched to findAndCountAll so the body carries a true total + // (previously `count` was just the page length — misleading on + // anything past the first page). Link header builds off the + // total too. + const { count, rows } = await TimeEntry.findAndCountAll({ + where, + limit, + offset, + order: [['teStartedAt', 'DESC']], + }); + const link = buildLinkHeader({ req, limit, offset, count }); + if (link) res.setHeader('Link', link); + res.setHeader('Access-Control-Expose-Headers', 'Link'); return res.status(200).json({ message: "Found.", - count: entries.length, + count, limit, - timeEntries: entries, + offset, + timeEntries: rows, }); } catch (error) { - log.error({ err: error }, 'TimeEntry.findAll failed'); + log.error({ err: error }, 'TimeEntry.findAndCountAll failed'); return res.status(500).json({ message: "Error!", error: String(error) }); } };