Skip to content

Commit ce79647

Browse files
feat: Implement pagination for lazy loading in
- Added feature-flag-based () lazy loading to . - Introduced support for , , and __TEXT __DATA __OBJC others dec hex query parameters for pagination. - Validated query parameters and returned structured pagination metadata (, __TEXT __DATA __OBJC others dec hex, , ). - Updated response structure to include enriched group membership information. - Modified test cases in : - Added tests for cursor-based lazy loading behavior. - Handled scenarios with and without cursors. - Verified error handling for database query failures.
1 parent 94ca959 commit ce79647

File tree

5 files changed

+115
-33
lines changed

5 files changed

+115
-33
lines changed

controllers/discordactions.js

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -119,22 +119,57 @@ const deleteGroupRole = async (req, res) => {
119119
};
120120

121121
/**
122-
* Gets all group-roles
122+
* Get Paginated All Group Roles with Lazy Loading
123+
* Implements cursor-based lazy loading with "dev=true" feature flag.
123124
*
125+
* @param req {Object} - Express request object
124126
* @param res {Object} - Express response object
125127
*/
126-
127-
const getPaginatedGroupRoles = async (req, res) => {
128+
const getPaginatedAllGroupRoles = async (req, res) => {
128129
try {
129-
const isDevMode = req.query?.dev === "true";
130-
if (isDevMode) {
131-
const latestDoc = req.query?.latestDoc;
132-
const { groups, newLatestDoc } = await discordRolesModel.getPaginatedGroupRoles(latestDoc);
130+
const { page = 0, size = 10, cursor, dev } = req.query;
131+
132+
const pageNumber = parseInt(page, 10);
133+
const limit = parseInt(size, 10) || 10;
134+
135+
if (limit < 1 || limit > 100) {
136+
return res.boom.badRequest("Invalid size. Must be between 1 and 100.");
137+
}
138+
139+
if (dev === "true") {
140+
let query = discordRolesModel.orderBy("roleid").limit(limit + 1); // Fetch one extra for `hasMore`
141+
142+
if (cursor) {
143+
const cursorDoc = await discordRolesModel.doc(cursor).get();
144+
if (!cursorDoc.exists) {
145+
return res.boom.badRequest("Invalid cursor.");
146+
}
147+
query = query.startAfter(cursorDoc);
148+
}
149+
150+
const snapshot = await query.get();
151+
const roles = snapshot.docs.slice(0, limit).map((doc) => ({
152+
id: doc.id,
153+
...doc.data(),
154+
}));
155+
const nextCursor = snapshot.docs.length > limit ? snapshot.docs[limit - 1].id : null;
156+
const hasMore = !!nextCursor;
157+
133158
const discordId = req.userData?.discordId;
134-
const groupsWithMembershipInfo = await discordRolesModel.enrichGroupDataWithMembershipInfo(discordId, groups);
159+
const groupsWithMembershipInfo = await discordRolesModel.enrichGroupDataWithMembershipInfo(discordId, roles);
160+
161+
const next = nextCursor ? `/roles?cursor=${nextCursor}&page=${pageNumber + 1}&size=${limit}` : null;
162+
const prev = pageNumber > 0 ? `/roles?cursor=${cursor}&page=${pageNumber - 1}&size=${limit}` : null;
163+
135164
return res.json({
136165
message: "Roles fetched successfully!",
137-
newLatestDoc: newLatestDoc,
166+
data: {
167+
page: pageNumber,
168+
size: limit,
169+
next,
170+
prev,
171+
hasMore,
172+
},
138173
groups: groupsWithMembershipInfo,
139174
});
140175
}
@@ -146,7 +181,7 @@ const getPaginatedGroupRoles = async (req, res) => {
146181
groups: groupsWithMembershipInfo,
147182
});
148183
} catch (err) {
149-
logger.error(`Error while getting roles: ${err}`);
184+
logger.error(`Error while fetching paginated group roles: ${err}`);
150185
return res.boom.badImplementation(INTERNAL_SERVER_ERROR);
151186
}
152187
};
@@ -546,7 +581,7 @@ const getUserDiscordInvite = async (req, res) => {
546581
module.exports = {
547582
getGroupsRoleId,
548583
createGroupRole,
549-
getPaginatedGroupRoles,
584+
getPaginatedAllGroupRoles,
550585
addGroupRoleToMember,
551586
deleteRole,
552587
updateDiscordImageForVerification,

middlewares/validators/discordactions.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,24 @@ const validateUpdateUsersNicknameStatusBody = async (req, res, next) => {
4040
}
4141
};
4242

43+
const validateLazyLoadingParams = async (req, res, next) => {
44+
const schema = Joi.object({
45+
page: Joi.number().integer().min(0).optional(),
46+
size: Joi.number().integer().min(1).max(100).optional(),
47+
cursor: Joi.string().optional(),
48+
});
49+
50+
try {
51+
req.query = await schema.validateAsync(req.query);
52+
next();
53+
} catch (error) {
54+
res.boom.badRequest(error.message);
55+
}
56+
};
57+
4358
module.exports = {
4459
validateGroupRoleBody,
4560
validateMemberRoleBody,
4661
validateUpdateUsersNicknameStatusBody,
62+
validateLazyLoadingParams,
4763
};

models/discordactions.js

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -126,24 +126,41 @@ const getAllGroupRoles = async () => {
126126
}
127127
};
128128

129-
const getPaginatedGroupRoles = async (latestDoc) => {
129+
/**
130+
* Get Paginated Group Roles
131+
* Fetches group roles with support for lazy loading.
132+
*
133+
* @param {Object} options - Pagination options
134+
* @param {string} options.cursor - Firestore document ID for cursor
135+
* @param {number} options.limit - Maximum number of roles to fetch
136+
* @returns {Promise<Object>} - Paginated roles with metadata
137+
*/
138+
const getPaginatedGroupRoles = async ({ cursor, limit }) => {
130139
try {
131-
const data = await discordRoleModel
132-
.orderBy("roleid")
133-
.startAfter(latestDoc || 0)
134-
.limit(18)
135-
.get();
136-
const groups = [];
137-
data.forEach((doc) => {
138-
const group = {
139-
id: doc.id,
140-
...doc.data(),
141-
};
142-
groups.push(group);
143-
});
144-
return { groups, newLatestDoc: data.docs[data.docs.length - 1]?.data().roleid };
140+
let query = discordRoleModel.orderBy("roleid").limit(limit + 1); // Fetch one extra for `hasMore`
141+
142+
if (cursor) {
143+
const cursorDoc = await discordRoleModel.doc(cursor).get();
144+
if (!cursorDoc.exists) {
145+
throw new Error("Invalid cursor.");
146+
}
147+
query = query.startAfter(cursorDoc);
148+
}
149+
150+
const snapshot = await query.get();
151+
const roles = snapshot.docs.slice(0, limit).map((doc) => ({
152+
id: doc.id,
153+
...doc.data(),
154+
}));
155+
const nextCursor = snapshot.docs.length > limit ? snapshot.docs[limit - 1].id : null;
156+
157+
return {
158+
roles,
159+
nextCursor,
160+
hasMore: !!nextCursor,
161+
};
145162
} catch (err) {
146-
logger.error("Error in getting all group-roles", err);
163+
logger.error(`Error in getPaginatedGroupRoles: ${err}`);
147164
throw err;
148165
}
149166
};

routes/discordactions.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const authenticate = require("../middlewares/authenticate");
33
const {
44
createGroupRole,
55
getGroupsRoleId,
6-
getPaginatedGroupRoles,
6+
getPaginatedAllGroupRoles,
77
addGroupRoleToMember,
88
deleteRole,
99
updateDiscordImageForVerification,
@@ -21,6 +21,7 @@ const {
2121
validateGroupRoleBody,
2222
validateMemberRoleBody,
2323
validateUpdateUsersNicknameStatusBody,
24+
validateLazyLoadingParams,
2425
} = require("../middlewares/validators/discordactions");
2526
const checkIsVerifiedDiscord = require("../middlewares/verifydiscord");
2627
const checkCanGenerateDiscordLink = require("../middlewares/checkCanGenerateDiscordLink");
@@ -33,7 +34,7 @@ const { authorizeAndAuthenticate } = require("../middlewares/authorizeUsersAndSe
3334
const router = express.Router();
3435

3536
router.post("/groups", authenticate, checkIsVerifiedDiscord, validateGroupRoleBody, createGroupRole);
36-
router.get("/groups", authenticate, checkIsVerifiedDiscord, getPaginatedGroupRoles);
37+
router.get("/groups", authenticate, checkIsVerifiedDiscord, validateLazyLoadingParams, getPaginatedAllGroupRoles);
3738
router.delete("/groups/:groupId", authenticate, checkIsVerifiedDiscord, authorizeRoles([SUPERUSER]), deleteGroupRole);
3839
router.post("/roles", authenticate, checkIsVerifiedDiscord, validateMemberRoleBody, addGroupRoleToMember);
3940
router.get("/invite", authenticate, getUserDiscordInvite);

test/unit/models/discordactions.test.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,15 +120,28 @@ describe("discordactions", function () {
120120
});
121121

122122
it("should return paginated group-roles from the database", async function () {
123-
const result = await getPaginatedGroupRoles();
123+
const result = await getPaginatedGroupRoles({ cursor: groupData, limit: 2 });
124+
124125
expect(result).to.have.property("groups").that.is.an("array");
125-
expect(result).to.have.property("newLatestDoc");
126-
expect(result.groups.length).to.be.at.most(18);
126+
expect(result.groups).to.have.lengthOf(2); // Verify the limit works
127+
expect(result).to.have.property("newLatestDoc").that.is.a("string");
128+
expect(result).to.have.property("hasMore").that.is.a("boolean");
129+
});
130+
131+
it("should handle pagination without a cursor", async function () {
132+
const result = await getPaginatedGroupRoles({ limit: 2 });
133+
134+
// eslint-disable-next-line no-undef
135+
assert(orderByStub.calledOnce);
136+
// eslint-disable-next-line no-undef
137+
assert(startAfterStub.notCalled);
138+
expect(result.groups).to.have.lengthOf(2);
127139
});
128140

129141
it("should throw an error if getting group-roles fails", async function () {
130142
getStub.rejects(new Error("Database error"));
131-
return getPaginatedGroupRoles().catch((err) => {
143+
144+
await getPaginatedGroupRoles({ cursor: groupData, limit: 2 }).catch((err) => {
132145
expect(err).to.be.an.instanceOf(Error);
133146
expect(err.message).to.equal("Database error");
134147
});

0 commit comments

Comments
 (0)