feat: add offset pagination and X-Prev-Last-Date header to GET /sessions
Made-with: Cursor
This commit is contained in:
@@ -23,6 +23,9 @@ router.get('/', (req, res) => {
|
||||
try {
|
||||
const filter = req.query.filter || 'default';
|
||||
const limitParam = req.query.limit || 'all';
|
||||
const offsetParam = req.query.offset || '0';
|
||||
let offset = parseInt(offsetParam, 10);
|
||||
if (isNaN(offset) || offset < 0) offset = 0;
|
||||
|
||||
let whereClause = '';
|
||||
if (filter === 'default') {
|
||||
@@ -45,6 +48,11 @@ router.get('/', (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
let offsetClause = '';
|
||||
if (offset > 0) {
|
||||
offsetClause = `OFFSET ${offset}`;
|
||||
}
|
||||
|
||||
const sessions = db.prepare(`
|
||||
SELECT
|
||||
s.id,
|
||||
@@ -60,6 +68,7 @@ router.get('/', (req, res) => {
|
||||
GROUP BY s.id
|
||||
ORDER BY s.created_at DESC
|
||||
${limitClause}
|
||||
${offsetClause}
|
||||
`).all();
|
||||
|
||||
const result = sessions.map(({ notes, ...session }) => {
|
||||
@@ -69,6 +78,19 @@ router.get('/', (req, res) => {
|
||||
|
||||
const absoluteTotal = db.prepare('SELECT COUNT(*) as total FROM sessions').get();
|
||||
|
||||
if (offset > 0) {
|
||||
const prevRow = db.prepare(`
|
||||
SELECT s.created_at
|
||||
FROM sessions s
|
||||
${whereClause}
|
||||
ORDER BY s.created_at DESC
|
||||
LIMIT 1 OFFSET ${offset - 1}
|
||||
`).get();
|
||||
if (prevRow) {
|
||||
res.set('X-Prev-Last-Date', prevRow.created_at);
|
||||
}
|
||||
}
|
||||
|
||||
res.set('X-Total-Count', String(countRow.total));
|
||||
res.set('X-Absolute-Total', String(absoluteTotal.total));
|
||||
res.json(result);
|
||||
|
||||
@@ -92,6 +92,97 @@ describe('GET /api/sessions — filter and limit', () => {
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toHaveLength(8);
|
||||
});
|
||||
|
||||
test('offset skips the first N sessions', async () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
seedSession({ is_active: 0, notes: null });
|
||||
}
|
||||
|
||||
const allRes = await request(app).get('/api/sessions?filter=all&limit=all');
|
||||
const offsetRes = await request(app).get('/api/sessions?filter=all&limit=2&offset=2');
|
||||
expect(offsetRes.status).toBe(200);
|
||||
expect(offsetRes.body).toHaveLength(2);
|
||||
expect(offsetRes.body[0].id).toBe(allRes.body[2].id);
|
||||
expect(offsetRes.body[1].id).toBe(allRes.body[3].id);
|
||||
});
|
||||
|
||||
test('offset defaults to 0 when not provided', async () => {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
seedSession({ is_active: 0, notes: null });
|
||||
}
|
||||
|
||||
const res = await request(app).get('/api/sessions?filter=all&limit=2');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('negative offset is clamped to 0', async () => {
|
||||
seedSession({ is_active: 0, notes: null });
|
||||
|
||||
const res = await request(app).get('/api/sessions?filter=all&offset=-5');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('non-numeric offset is clamped to 0', async () => {
|
||||
seedSession({ is_active: 0, notes: null });
|
||||
|
||||
const res = await request(app).get('/api/sessions?filter=all&offset=abc');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('offset past end returns empty array', async () => {
|
||||
seedSession({ is_active: 0, notes: null });
|
||||
|
||||
const res = await request(app).get('/api/sessions?filter=all&limit=5&offset=100');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toHaveLength(0);
|
||||
expect(res.headers['x-total-count']).toBe('1');
|
||||
});
|
||||
|
||||
test('X-Prev-Last-Date header is set with correct value when offset > 0', async () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
seedSession({ is_active: 0, notes: null });
|
||||
}
|
||||
|
||||
const allRes = await request(app).get('/api/sessions?filter=all&limit=all');
|
||||
const res = await request(app).get('/api/sessions?filter=all&limit=2&offset=2');
|
||||
expect(res.headers['x-prev-last-date']).toBe(allRes.body[1].created_at);
|
||||
});
|
||||
|
||||
test('X-Prev-Last-Date header is absent when offset is 0', async () => {
|
||||
seedSession({ is_active: 0, notes: null });
|
||||
|
||||
const res = await request(app).get('/api/sessions?filter=all&limit=2');
|
||||
expect(res.headers['x-prev-last-date']).toBeUndefined();
|
||||
});
|
||||
|
||||
test('X-Total-Count is unaffected by offset', async () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
seedSession({ is_active: 0, notes: null });
|
||||
}
|
||||
|
||||
const res = await request(app).get('/api/sessions?filter=all&limit=3&offset=6');
|
||||
expect(res.headers['x-total-count']).toBe('10');
|
||||
expect(res.body).toHaveLength(3);
|
||||
});
|
||||
|
||||
test('offset works with filter=default', async () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
seedSession({ is_active: 0, notes: null });
|
||||
}
|
||||
const archived = seedSession({ is_active: 0, notes: null });
|
||||
require('../helpers/test-utils').db.prepare(
|
||||
'UPDATE sessions SET archived = 1 WHERE id = ?'
|
||||
).run(archived.id);
|
||||
|
||||
const res = await request(app).get('/api/sessions?filter=default&limit=2&offset=2');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toHaveLength(2);
|
||||
expect(res.headers['x-total-count']).toBe('5');
|
||||
res.body.forEach(s => expect(s.archived).toBe(0));
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/sessions/:id/archive', () => {
|
||||
|
||||
Reference in New Issue
Block a user