Skip to content

Commit cbd992a

Browse files
feat: Implement Pagination for GET /progreses API (#2325)
* feat: add pagination to GET /progreses API * fix returning 404 on a page with no data * fix joi validator logic * refactor getting totalcount logic * fix dev true * fix merge conflicts * using constant * maked the JsDoc more concise --------- Co-authored-by: Vinit khandal <[email protected]>
1 parent d04fddd commit cbd992a

File tree

7 files changed

+138
-15
lines changed

7 files changed

+138
-15
lines changed

constants/progresses.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ const TYPE_MAP = {
1818
task: "taskId",
1919
};
2020
const PROGRESS_VALID_SORT_FIELDS = ["date", "-date"];
21-
21+
const PROGRESSES_SIZE = 20;
22+
const PROGRESSES_PAGE_SIZE = 0;
2223
const VALID_PROGRESS_TYPES = ["task", "user"];
2324

2425
module.exports = {
@@ -28,4 +29,6 @@ module.exports = {
2829
TYPE_MAP,
2930
VALID_PROGRESS_TYPES,
3031
PROGRESS_VALID_SORT_FIELDS,
32+
PROGRESSES_SIZE,
33+
PROGRESSES_PAGE_SIZE,
3134
};

controllers/progresses.js

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
const { Conflict, NotFound } = require("http-errors");
2+
const progressesModel = require("../models/progresses");
23
const {
3-
createProgressDocument,
4-
getProgressDocument,
5-
getRangeProgressData,
6-
getProgressByDate,
7-
} = require("../models/progresses");
8-
const { PROGRESSES_RESPONSE_MESSAGES, INTERNAL_SERVER_ERROR_MESSAGE } = require("../constants/progresses");
4+
PROGRESSES_RESPONSE_MESSAGES,
5+
INTERNAL_SERVER_ERROR_MESSAGE,
6+
PROGRESSES_SIZE,
7+
PROGRESSES_PAGE_SIZE,
8+
} = require("../constants/progresses");
99
const { sendTaskUpdate } = require("../utils/sendTaskUpdate");
1010
const { PROGRESS_DOCUMENT_RETRIEVAL_SUCCEEDED, PROGRESS_DOCUMENT_CREATED_SUCCEEDED } = PROGRESSES_RESPONSE_MESSAGES;
1111

@@ -49,7 +49,7 @@ const createProgress = async (req, res) => {
4949
body: { type, completed, planned, blockers, taskId },
5050
} = req;
5151
try {
52-
const { data, taskTitle } = await createProgressDocument({ ...req.body, userId: req.userData.id });
52+
const { data, taskTitle } = await progressesModel.createProgressDocument({ ...req.body, userId: req.userData.id });
5353
await sendTaskUpdate(completed, blockers, planned, req.userData.username, taskId, taskTitle);
5454
return res.status(201).json({
5555
data,
@@ -107,8 +107,35 @@ const createProgress = async (req, res) => {
107107
*/
108108

109109
const getProgress = async (req, res) => {
110+
const { dev, page = PROGRESSES_PAGE_SIZE, size = PROGRESSES_SIZE, type, userId, taskId } = req.query;
110111
try {
111-
const data = await getProgressDocument(req.query);
112+
if (dev === "true") {
113+
const { progressDocs, totalProgressCount } = await progressesModel.getPaginatedProgressDocument(req.query);
114+
const limit = parseInt(size, 10);
115+
const offset = parseInt(page, 10) * limit;
116+
const nextPage = offset + limit < totalProgressCount ? parseInt(page, 10) + 1 : null;
117+
const prevPage = page > 0 ? parseInt(page, 10) - 1 : null;
118+
let baseUrl = `${req.baseUrl}`;
119+
if (type) {
120+
baseUrl += `?type=${type}`;
121+
} else if (userId) {
122+
baseUrl += `?userId=${userId}`;
123+
} else if (taskId) {
124+
baseUrl += `?taskId=${taskId}`;
125+
}
126+
const nextLink = nextPage !== null ? `${baseUrl}&page=${nextPage}&size=${size}&dev=${dev}` : null;
127+
const prevLink = prevPage !== null ? `${baseUrl}&page=${prevPage}&size=${size}&dev=${dev}` : null;
128+
return res.json({
129+
message: PROGRESS_DOCUMENT_RETRIEVAL_SUCCEEDED,
130+
count: progressDocs.length,
131+
data: progressDocs,
132+
links: {
133+
prev: prevLink,
134+
next: nextLink,
135+
},
136+
});
137+
}
138+
const data = await progressesModel.getProgressDocument(req.query);
112139
return res.json({
113140
message: PROGRESS_DOCUMENT_RETRIEVAL_SUCCEEDED,
114141
count: data.length,
@@ -163,7 +190,7 @@ const getProgress = async (req, res) => {
163190

164191
const getProgressRangeData = async (req, res) => {
165192
try {
166-
const data = await getRangeProgressData(req.query);
193+
const data = await progressesModel.getRangeProgressData(req.query);
167194
return res.json({
168195
message: PROGRESS_DOCUMENT_RETRIEVAL_SUCCEEDED,
169196
data,
@@ -217,7 +244,7 @@ const getProgressRangeData = async (req, res) => {
217244

218245
const getProgressBydDateController = async (req, res) => {
219246
try {
220-
const data = await getProgressByDate(req.params, req.query);
247+
const data = await progressesModel.getProgressByDate(req.params, req.query);
221248
return res.json({
222249
message: PROGRESS_DOCUMENT_RETRIEVAL_SUCCEEDED,
223250
data,

middlewares/validators/progresses.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,15 @@ const validateGetProgressRecordsQuery = async (req, res, next) => {
7373
.messages({
7474
"string.base": "orderBy must be a string",
7575
}),
76+
size: joi.number().optional().min(1).max(100).messages({
77+
"number.base": "size must be a number",
78+
"number.min": "size must be in the range 1-100",
79+
"number.max": "size must be in the range 1-100",
80+
}),
81+
page: joi.number().optional().min(0).messages({
82+
"number.base": "page must be a number",
83+
"number.min": "page must be a positive number or zero",
84+
}),
7685
})
7786
.xor("type", "userId", "taskId")
7887
.messages({

models/progresses.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ const {
1212
assertTaskExists,
1313
getProgressDateTimestamp,
1414
buildQueryToSearchProgressByDay,
15+
buildQueryToFetchPaginatedDocs,
16+
getPaginatedProgressDocs,
1517
} = require("../utils/progresses");
1618
const { retrieveUsers } = require("../services/dataAccessLayer");
1719
const { PROGRESS_ALREADY_CREATED, PROGRESS_DOCUMENT_NOT_FOUND } = PROGRESSES_RESPONSE_MESSAGES;
@@ -59,6 +61,22 @@ const getProgressDocument = async (queryParams) => {
5961
return progressDocs;
6062
};
6163

64+
/**
65+
* Retrieves a paginated list of progress documents based on the provided query parameters.
66+
* @param {object} queryParams - Query data, including type, userId, taskId, and optional pagination details (page and pageSize).
67+
* @returns {Promise<object>} Resolves with paginated progress documents.
68+
* @throws {Error} If userId or taskId is invalid or not found.
69+
**/
70+
71+
const getPaginatedProgressDocument = async (queryParams) => {
72+
await assertUserOrTaskExists(queryParams);
73+
const page = queryParams.page || 0;
74+
const { baseQuery, totalProgressCount } = await buildQueryToFetchPaginatedDocs(queryParams);
75+
76+
let progressDocs = await getPaginatedProgressDocs(baseQuery, page);
77+
progressDocs = await addUserDetailsToProgressDocs(progressDocs);
78+
return { progressDocs, totalProgressCount };
79+
};
6280
/**
6381
* This function fetches the progress records for a particular user or task within the specified date range, from start to end date.
6482
* @param queryParams {object} This is the data that will be used for querying. It should be an object that includes key-value pairs for the fields - userId, taskId, startDate, and endDate.
@@ -135,6 +153,7 @@ const addUserDetailsToProgressDocs = async (progressDocs) => {
135153
module.exports = {
136154
createProgressDocument,
137155
getProgressDocument,
156+
getPaginatedProgressDocument,
138157
getRangeProgressData,
139158
getProgressByDate,
140159
addUserDetailsToProgressDocs,

test/integration/progressesTasks.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ describe("Test Progress Updates API for Tasks", function () {
229229
.end((err, res) => {
230230
if (err) return done(err);
231231
expect(res).to.have.status(200);
232-
expect(res.body).to.have.keys(["message", "data", "count"]);
232+
expect(res.body).to.have.keys(["message", "data", "count", "links"]);
233233
expect(res.body.data).to.be.an("array");
234234
expect(res.body.message).to.be.equal("Progress document retrieved successfully.");
235235
res.body.data.forEach((progress) => {
@@ -388,7 +388,7 @@ describe("Test Progress Updates API for Tasks", function () {
388388
.end((err, res) => {
389389
if (err) return done(err);
390390
expect(res).to.have.status(200);
391-
expect(res.body).to.have.keys(["message", "data", "count"]);
391+
expect(res.body).to.have.keys(["message", "data", "count", "links"]);
392392
expect(res.body.data).to.be.an("array");
393393
expect(res.body.message).to.be.equal("Progress document retrieved successfully.");
394394
expect(res.body.count).to.be.equal(4);

test/integration/progressesUsers.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ describe("Test Progress Updates API for Users", function () {
233233
.end((err, res) => {
234234
if (err) return done(err);
235235
expect(res).to.have.status(200);
236-
expect(res.body).to.have.keys(["message", "data", "count"]);
236+
expect(res.body).to.have.keys(["message", "data", "count", "links"]);
237237
expect(res.body.data).to.be.an("array");
238238
expect(res.body.message).to.be.equal("Progress document retrieved successfully.");
239239
res.body.data.forEach((progress) => {
@@ -260,7 +260,7 @@ describe("Test Progress Updates API for Users", function () {
260260
.end((err, res) => {
261261
if (err) return done(err);
262262
expect(res).to.have.status(200);
263-
expect(res.body).to.have.keys(["message", "data", "count"]);
263+
expect(res.body).to.have.keys(["message", "data", "count", "links"]);
264264
expect(res.body.data).to.be.an("array");
265265
expect(res.body.message).to.be.equal("Progress document retrieved successfully.");
266266
res.body.data.forEach((progress) => {

utils/progresses.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ const {
88
PROGRESSES_RESPONSE_MESSAGES: { PROGRESS_DOCUMENT_NOT_FOUND },
99
MILLISECONDS_IN_DAY,
1010
PROGRESS_VALID_SORT_FIELDS,
11+
PROGRESSES_PAGE_SIZE,
12+
PROGRESSES_SIZE,
1113
} = require("../constants/progresses");
1214
const { convertTimestampToUTCStartOrEndOfDay } = require("./time");
1315
const progressesCollection = fireStore.collection("progresses");
@@ -120,6 +122,42 @@ const buildQueryToFetchDocs = (queryParams) => {
120122
}
121123
};
122124

125+
/**
126+
* Builds a Firestore query to retrieve a paginated list of progress documents within a date range,
127+
* optionally filtered by user ID, task ID, type, and sorted by a specific field.
128+
* @param {Object} queryParams - Query parameters including userId, taskId, type, orderBy, size, and page.
129+
* @returns {Query} A Firestore query object for filtered and paginated progress documents.
130+
*/
131+
132+
const buildQueryToFetchPaginatedDocs = async (queryParams) => {
133+
const { type, userId, taskId, orderBy, size = PROGRESSES_SIZE, page = PROGRESSES_PAGE_SIZE } = queryParams;
134+
const orderByField = PROGRESS_VALID_SORT_FIELDS[0];
135+
const isAscOrDsc = orderBy && PROGRESS_VALID_SORT_FIELDS[0] === orderBy ? "asc" : "desc";
136+
const limit = parseInt(size, 10);
137+
const offset = parseInt(page, 10) * limit;
138+
139+
let baseQuery;
140+
if (type) {
141+
baseQuery = progressesCollection.where("type", "==", type).orderBy(orderByField, isAscOrDsc);
142+
} else if (userId) {
143+
baseQuery = progressesCollection
144+
.where("type", "==", "user")
145+
.where("userId", "==", userId)
146+
.orderBy(orderByField, isAscOrDsc);
147+
} else {
148+
baseQuery = progressesCollection
149+
.where("type", "==", "task")
150+
.where("taskId", "==", taskId)
151+
.orderBy(orderByField, isAscOrDsc);
152+
}
153+
154+
const totalProgress = await baseQuery.get();
155+
const totalProgressCount = totalProgress.size;
156+
157+
baseQuery = baseQuery.limit(limit).offset(offset);
158+
return { baseQuery, totalProgressCount };
159+
};
160+
123161
/**
124162
* Retrieves progress documents from Firestore based on the given query.
125163
* @param {Query} query - A Firestore query object for fetching progress documents.
@@ -137,6 +175,31 @@ const getProgressDocs = async (query) => {
137175
});
138176
return docsData;
139177
};
178+
/**
179+
* Retrieves progress documents from Firestore based on the given query and page number.
180+
*
181+
* @param {Query} query - A Firestore query object for fetching progress documents.
182+
* @param {number} [pageNumber] - The current page number (optional). If not provided, it will check for documents without pagination.
183+
* @returns {Array.<Object>} An array of objects representing the retrieved progress documents.
184+
* Each object contains the document ID (`id`) and its associated data.
185+
*
186+
* @throws {NotFound} If no progress documents are found and no page number is specified.
187+
*/
188+
const getPaginatedProgressDocs = async (query, page) => {
189+
const progressesDocs = await query.get();
190+
if (!page && !progressesDocs.size) {
191+
throw new NotFound(PROGRESS_DOCUMENT_NOT_FOUND);
192+
}
193+
if (!progressesDocs.size) {
194+
return [];
195+
}
196+
const docsData = [];
197+
progressesDocs.forEach((doc) => {
198+
docsData.push({ id: doc.id, ...doc.data() });
199+
});
200+
return docsData;
201+
};
202+
140203
/**
141204
* Builds a Firestore query for retrieving progress documents within a date range and optionally filtered by user ID or task ID.
142205
* @param {Object} queryParams - An object containing the query parameters.
@@ -231,8 +294,10 @@ module.exports = {
231294
assertUserOrTaskExists,
232295
buildQueryToFetchDocs,
233296
getProgressDocs,
297+
getPaginatedProgressDocs,
234298
buildRangeProgressQuery,
235299
getProgressRecords,
236300
buildQueryToSearchProgressByDay,
237301
buildProgressQueryForMissedUpdates,
302+
buildQueryToFetchPaginatedDocs,
238303
};

0 commit comments

Comments
 (0)