Skip to content

Commit 8e4b008

Browse files
Implement pagination logic and add comprehensive tests for pagination feature
1 parent b0fd650 commit 8e4b008

File tree

2 files changed

+307
-9
lines changed

2 files changed

+307
-9
lines changed

src/lib/server.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -241,10 +241,11 @@ export class JsonServer {
241241
* @returns Boolean indicating if there are more pages to iterate
242242
*/
243243
continueToIterate(currentPage: number, pageSize: number, totalItems: number): boolean {
244-
// The pagination should continue if there are still more items to display
245-
const startIndex = (currentPage - 1) * pageSize;
246-
const endIndex = startIndex + pageSize;
247-
return endIndex < totalItems;
244+
// Calculate total number of pages
245+
const totalPages = Math.ceil(totalItems / pageSize);
246+
247+
// Check if current page is less than total pages
248+
return currentPage < totalPages;
248249
}
249250

250251
/**
@@ -394,20 +395,31 @@ export class JsonServer {
394395
const { resource } = req.params;
395396
const resourceData = this.db[resource] || [];
396397

397-
// Handle query parameters for filtering
398+
// Filter data based on non-pagination query parameters first
399+
let filteredData = [...resourceData];
398400
if (Object.keys(req.query).length > 0) {
399-
const filteredData = resourceData.filter((item) => {
401+
filteredData = resourceData.filter((item) => {
400402
return Object.entries(req.query).every(([key, value]) => {
401-
// Skip special query parameters like _page, _limit, _sort
403+
// Skip special query parameters like _page, _per_page, _sort
402404
if (key.startsWith('_')) return true;
403405
return String(item[key]) === String(value);
404406
});
405407
});
408+
}
409+
410+
// Handle pagination
411+
const pageParam = req.query._page;
412+
const perPageParam = req.query._per_page;
413+
414+
if (pageParam !== undefined || perPageParam !== undefined) {
415+
const page = pageParam ? parseInt(pageParam as string) : 1;
416+
const perPage = perPageParam ? parseInt(perPageParam as string) : 10;
406417

407-
return res.json(filteredData);
418+
// Apply pagination and return the paginated result
419+
return res.json(this.getPaginatedData(filteredData, page, perPage));
408420
}
409421

410-
res.json(resourceData);
422+
res.json(filteredData);
411423
}) as RequestHandler);
412424

413425
// Get single entry by ID

test/pagination.test.ts

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
// filepath: /Users/devlinduldulao/Downloads/json-server/test/pagination.test.ts
2+
import { JsonServer } from '../src/lib/server';
3+
import { ServerOptions } from '../src/types';
4+
import express, { Request, Response } from 'express';
5+
import request from 'supertest';
6+
7+
// Mock the file utilities
8+
jest.mock('../src/utils/utils', () => ({
9+
fileExists: jest.fn(() => true),
10+
loadJsonFile: jest.fn(() => ({
11+
items: Array.from({ length: 100 }, (_, i) => ({
12+
id: String(i + 1),
13+
name: `Item ${i + 1}`,
14+
value: i + 1,
15+
})),
16+
emptyResource: [],
17+
smallCollection: Array.from({ length: 5 }, (_, i) => ({
18+
id: String(i + 1),
19+
name: `Small Item ${i + 1}`,
20+
})),
21+
})),
22+
saveJsonFile: jest.fn(),
23+
parseRoutesFile: jest.fn(),
24+
}));
25+
26+
describe('Pagination Feature Tests', () => {
27+
let server: JsonServer;
28+
let app: express.Express;
29+
30+
beforeEach(() => {
31+
const options: ServerOptions = {
32+
port: 3000,
33+
host: 'localhost',
34+
cors: true,
35+
static: [],
36+
middlewares: [],
37+
bodyParser: true,
38+
noCors: false,
39+
noGzip: false,
40+
delay: 0,
41+
quiet: true,
42+
readOnly: false,
43+
};
44+
45+
// Create a real server instance for testing
46+
server = new JsonServer(options);
47+
server.loadDatabase('test-db.json');
48+
app = server.getApp();
49+
50+
// Manually call start to set up routes
51+
// but don't actually listen on a port
52+
const originalStart = server.start;
53+
server.start = jest.fn().mockImplementation(() => {
54+
// Set up routes without starting the server
55+
(server as any).createResourceRoutes();
56+
(server as any).applyCustomRoutes();
57+
return Promise.resolve();
58+
});
59+
server.start();
60+
server.start = originalStart;
61+
});
62+
63+
// Test 1: Basic pagination with default parameters
64+
test('getPaginatedData should return paginated data with default values', () => {
65+
const mockCollection = Array.from({ length: 30 }, (_, i) => ({ id: i + 1 }));
66+
const result = (server as any).getPaginatedData(mockCollection);
67+
68+
expect(result).toHaveProperty('data');
69+
expect(result).toHaveProperty('first', 1);
70+
expect(result).toHaveProperty('last');
71+
expect(result).toHaveProperty('pages');
72+
expect(result).toHaveProperty('items', 30);
73+
expect(result.data).toHaveLength(10); // Default perPage is 10
74+
});
75+
76+
// Test 2: Custom page and perPage parameters
77+
test('getPaginatedData should use custom page and perPage values', () => {
78+
const mockCollection = Array.from({ length: 30 }, (_, i) => ({ id: i + 1 }));
79+
const result = (server as any).getPaginatedData(mockCollection, 2, 5);
80+
81+
expect(result.data).toHaveLength(5); // perPage is 5
82+
expect(result.data[0].id).toBe(6); // Second page, first item should be id 6
83+
expect(result.prev).toBe(1); // Previous page should exist
84+
expect(result.next).toBe(3); // Next page should exist
85+
});
86+
87+
// Test 3: Last page with fewer items than perPage
88+
test('getPaginatedData should handle last page with fewer items', () => {
89+
const mockCollection = Array.from({ length: 22 }, (_, i) => ({ id: i + 1 }));
90+
const result = (server as any).getPaginatedData(mockCollection, 3, 10);
91+
92+
expect(result.data).toHaveLength(2); // Last page has only 2 items
93+
expect(result.prev).toBe(2); // Previous page should exist
94+
expect(result.next).toBeNull(); // No next page
95+
expect(result.pages).toBe(3); // Total pages
96+
});
97+
98+
// Test 4: Empty collection
99+
test('getPaginatedData should handle empty collections', () => {
100+
const result = (server as any).getPaginatedData([]);
101+
102+
expect(result.data).toHaveLength(0);
103+
expect(result.prev).toBeNull();
104+
expect(result.next).toBeNull();
105+
expect(result.pages).toBe(1); // Minimum 1 page even if empty
106+
});
107+
108+
// Test 5: Page out of bounds (larger than total pages)
109+
test('getPaginatedData should handle page number larger than total pages', () => {
110+
const mockCollection = Array.from({ length: 10 }, (_, i) => ({ id: i + 1 }));
111+
const result = (server as any).getPaginatedData(mockCollection, 3, 5);
112+
113+
expect(result.data).toHaveLength(0); // No items on this page
114+
expect(result.prev).toBe(2); // Previous page exists
115+
expect(result.next).toBeNull(); // No next page
116+
});
117+
118+
// Test 6: GET /:resource with _page and _per_page params
119+
test('GET /:resource should apply pagination when _page and _per_page are specified', async () => {
120+
// Mock the app.get implementation for this specific test
121+
const mockReq = {
122+
params: { resource: 'items' },
123+
query: { _page: '2', _per_page: '5' },
124+
} as unknown as Request;
125+
126+
// Create a mock response object
127+
const mockRes = {
128+
json: jest.fn(),
129+
} as unknown as Response;
130+
131+
// Find the route handler and call it directly
132+
const handlers = app.get.mock.calls.find((call) => call[0] === '/:resource');
133+
if (handlers && typeof handlers[1] === 'function') {
134+
await handlers[1](mockReq, mockRes, () => {});
135+
136+
// Check that response.json was called with paginated data
137+
expect(mockRes.json).toHaveBeenCalled();
138+
const responseData = mockRes.json.mock.calls[0][0];
139+
140+
expect(responseData).toHaveProperty('data');
141+
expect(responseData.data).toHaveLength(5); // perPage is 5
142+
expect(responseData).toHaveProperty('prev', 1);
143+
expect(responseData).toHaveProperty('next', 3);
144+
} else {
145+
fail('Route handler for /:resource not found');
146+
}
147+
});
148+
149+
// Test 7: GET /:resource with only _page param (default _per_page)
150+
test('GET /:resource should use default perPage when only _page is specified', async () => {
151+
const mockReq = {
152+
params: { resource: 'items' },
153+
query: { _page: '2' },
154+
} as unknown as Request;
155+
156+
const mockRes = {
157+
json: jest.fn(),
158+
} as unknown as Response;
159+
160+
const handlers = app.get.mock.calls.find((call) => call[0] === '/:resource');
161+
if (handlers && typeof handlers[1] === 'function') {
162+
await handlers[1](mockReq, mockRes, () => {});
163+
164+
const responseData = mockRes.json.mock.calls[0][0];
165+
expect(responseData.data).toHaveLength(10); // Default perPage is 10
166+
} else {
167+
fail('Route handler for /:resource not found');
168+
}
169+
});
170+
171+
// Test 8: GET /:resource with only _per_page param (default page 1)
172+
test('GET /:resource should use page 1 when only _per_page is specified', async () => {
173+
const mockReq = {
174+
params: { resource: 'items' },
175+
query: { _per_page: '15' },
176+
} as unknown as Request;
177+
178+
const mockRes = {
179+
json: jest.fn(),
180+
} as unknown as Response;
181+
182+
const handlers = app.get.mock.calls.find((call) => call[0] === '/:resource');
183+
if (handlers && typeof handlers[1] === 'function') {
184+
await handlers[1](mockReq, mockRes, () => {});
185+
186+
const responseData = mockRes.json.mock.calls[0][0];
187+
expect(responseData.data).toHaveLength(15);
188+
expect(responseData.prev).toBeNull(); // No previous page
189+
expect(responseData.first).toBe(1); // First page is 1
190+
} else {
191+
fail('Route handler for /:resource not found');
192+
}
193+
});
194+
195+
// Test 9: Pagination with filtering
196+
test('GET /:resource should filter before pagination', async () => {
197+
const mockReq = {
198+
params: { resource: 'items' },
199+
query: { _page: '1', _per_page: '5', value: '10' },
200+
} as unknown as Request;
201+
202+
const mockRes = {
203+
json: jest.fn(),
204+
} as unknown as Response;
205+
206+
const handlers = app.get.mock.calls.find((call) => call[0] === '/:resource');
207+
if (handlers && typeof handlers[1] === 'function') {
208+
await handlers[1](mockReq, mockRes, () => {});
209+
210+
const responseData = mockRes.json.mock.calls[0][0];
211+
// Should only be one item with value=10
212+
expect(responseData.data).toHaveLength(1);
213+
expect(responseData.data[0].value).toBe(10);
214+
} else {
215+
fail('Route handler for /:resource not found');
216+
}
217+
});
218+
219+
// Test 10: Non-existent resource
220+
test('GET /:resource should return empty array for non-existent resource', async () => {
221+
const mockReq = {
222+
params: { resource: 'nonExistentResource' },
223+
query: { _page: '1', _per_page: '5' },
224+
} as unknown as Request;
225+
226+
const mockRes = {
227+
json: jest.fn(),
228+
} as unknown as Response;
229+
230+
const handlers = app.get.mock.calls.find((call) => call[0] === '/:resource');
231+
if (handlers && typeof handlers[1] === 'function') {
232+
await handlers[1](mockReq, mockRes, () => {});
233+
234+
const responseData = mockRes.json.mock.calls[0][0];
235+
expect(responseData.data).toHaveLength(0);
236+
expect(responseData.items).toBe(0);
237+
} else {
238+
fail('Route handler for /:resource not found');
239+
}
240+
});
241+
242+
// Test 11: Empty resource
243+
test('GET /:resource should handle empty resources correctly', async () => {
244+
const mockReq = {
245+
params: { resource: 'emptyResource' },
246+
query: { _page: '1', _per_page: '5' },
247+
} as unknown as Request;
248+
249+
const mockRes = {
250+
json: jest.fn(),
251+
} as unknown as Response;
252+
253+
const handlers = app.get.mock.calls.find((call) => call[0] === '/:resource');
254+
if (handlers && typeof handlers[1] === 'function') {
255+
await handlers[1](mockReq, mockRes, () => {});
256+
257+
const responseData = mockRes.json.mock.calls[0][0];
258+
expect(responseData.data).toHaveLength(0);
259+
expect(responseData.pages).toBe(1); // Minimum of 1 page
260+
} else {
261+
fail('Route handler for /:resource not found');
262+
}
263+
});
264+
265+
// Test 12: Small collection pagination
266+
test('getPaginatedData should handle small collections correctly', () => {
267+
const mockReq = {
268+
params: { resource: 'smallCollection' },
269+
query: { _page: '1', _per_page: '10' },
270+
} as unknown as Request;
271+
272+
const mockRes = {
273+
json: jest.fn(),
274+
} as unknown as Response;
275+
276+
const handlers = app.get.mock.calls.find((call) => call[0] === '/:resource');
277+
if (handlers && typeof handlers[1] === 'function') {
278+
handlers[1](mockReq, mockRes, () => {});
279+
280+
const responseData = mockRes.json.mock.calls[0][0];
281+
expect(responseData.data).toHaveLength(5); // All 5 items
282+
expect(responseData.next).toBeNull(); // No next page
283+
expect(responseData.pages).toBe(1); // Only 1 page
284+
}
285+
});
286+
});

0 commit comments

Comments
 (0)