Skip to content

Commit

Permalink
Add pagination mongoose plugin
Browse files Browse the repository at this point in the history
closes #13
  • Loading branch information
hagopj13 committed May 17, 2020
1 parent c6c4206 commit 97fef4c
Show file tree
Hide file tree
Showing 10 changed files with 165 additions and 83 deletions.
7 changes: 3 additions & 4 deletions src/controllers/user.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@ const { pick } = require('lodash');
const ApiError = require('../utils/ApiError');
const catchAsync = require('../utils/catchAsync');
const { userService } = require('../services');
const { getQueryOptions } = require('../utils/query.utils');

const createUser = catchAsync(async (req, res) => {
const user = await userService.createUser(req.body);
res.status(httpStatus.CREATED).send(user);
});

const getUsers = catchAsync(async (req, res) => {
const filter = pick(req.query, ['name', 'role']);
const options = getQueryOptions(req.query);
const users = await userService.getUsers(filter, options);
const query = pick(req.query, ['name', 'role']);
const options = pick(req.query, ['sortBy', 'limit', 'page']);
const users = await userService.queryUsers(query, options);
res.send(users);
});

Expand Down
66 changes: 66 additions & 0 deletions src/models/plugins.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/* eslint-disable no-param-reassign */

/**
* A mongoose schema plugin which applies the following in the toJSON transform call:
* - removes __v, createdAt, updatedAt, and any path that has private: true
* - replaces _id with id
*/
const toJSON = (schema) => {
let transform;
if (schema.options.toJSON && schema.options.toJSON.transform) {
transform = schema.options.toJSON.transform;
}

schema.options.toJSON = Object.assign(schema.options.toJSON || {}, {
transform(doc, ret, options) {
Object.keys(schema.paths).forEach((path) => {
if (schema.paths[path].options && schema.paths[path].options.private) {
delete ret[path];
}
});

ret.id = ret._id.toString();
delete ret._id;
delete ret.__v;
delete ret.createdAt;
delete ret.updatedAt;
if (transform) {
return transform(doc, ret, options);
}
},
});
};

const paginate = (schema) => {
schema.statics.paginate = async function (query, options) {
const sort = {};
if (options.sortBy) {
const parts = options.sortBy.split(':');
sort[parts[0]] = parts[1] === 'desc' ? -1 : 1;
}
const limit = options.limit && parseInt(options.limit, 10) > 0 ? parseInt(options.limit, 10) : 10;
const page = options.page && parseInt(options.page, 10) > 0 ? parseInt(options.page, 10) : 1;
const skip = (page - 1) * limit;

const countPromise = this.countDocuments(query).exec();
const docsPromise = this.find(query).sort(sort).skip(skip).limit(limit).exec();

return Promise.all([countPromise, docsPromise]).then((values) => {
const [totalResults, results] = values;
const totalPages = Math.ceil(totalResults / limit);
const result = {
results,
page,
limit,
totalPages,
totalResults,
};
return Promise.resolve(result);
});
};
};

module.exports = {
toJSON,
paginate,
};
2 changes: 1 addition & 1 deletion src/models/token.model.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const mongoose = require('mongoose');
const { toJSON } = require('./utils');
const { toJSON } = require('./plugins');

const tokenSchema = mongoose.Schema(
{
Expand Down
3 changes: 2 additions & 1 deletion src/models/user.model.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const mongoose = require('mongoose');
const validator = require('validator');
const bcrypt = require('bcryptjs');
const { toJSON } = require('./utils');
const { toJSON, paginate } = require('./plugins');
const { roles } = require('../config/roles');

const userSchema = mongoose.Schema(
Expand Down Expand Up @@ -48,6 +48,7 @@ const userSchema = mongoose.Schema(

// add plugin that converts mongoose to json
userSchema.plugin(toJSON);
userSchema.plugin(paginate);

userSchema.statics.isEmailTaken = async function (email, excludeUserId) {
const user = await this.findOne({ email, _id: { $ne: excludeUserId } });
Expand Down
36 changes: 0 additions & 36 deletions src/models/utils.js

This file was deleted.

23 changes: 19 additions & 4 deletions src/routes/v1/user.route.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ module.exports = router;
* schema:
* type: integer
* minimum: 1
* default: 100
* default: 10
* description: Maximum number of users
* - in: query
* name: page
Expand All @@ -123,9 +123,24 @@ module.exports = router;
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/User'
* type: object
* properties:
* results:
* type: array
* items:
* $ref: '#/components/schemas/User'
* page:
* type: integer
* example: 1
* limit:
* type: integer
* example: 10
* totalPages:
* type: integer
* example: 1
* totalResults:
* type: integer
* example: 1
* "401":
* $ref: '#/components/responses/Unauthorized'
* "403":
Expand Down
6 changes: 3 additions & 3 deletions src/services/user.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ const createUser = async (userBody) => {
return user;
};

const getUsers = async (filter, options) => {
const users = await User.find(filter, null, options);
const queryUsers = async (query, options) => {
const users = await User.paginate(query, options);
return users;
};

Expand Down Expand Up @@ -47,7 +47,7 @@ const deleteUserById = async (userId) => {

module.exports = {
createUser,
getUsers,
queryUsers,
getUserById,
getUserByEmail,
updateUserById,
Expand Down
17 changes: 0 additions & 17 deletions src/utils/query.utils.js

This file was deleted.

86 changes: 70 additions & 16 deletions tests/integration/user.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ describe('User routes', () => {
});

describe('GET /v1/users', () => {
test('should return 200 and all users', async () => {
test('should return 200 and apply the default query options', async () => {
await insertUsers([userOne, userTwo, admin]);

const res = await request(app)
Expand All @@ -144,9 +144,15 @@ describe('User routes', () => {
.send()
.expect(httpStatus.OK);

expect(res.body).toBeInstanceOf(Array);
expect(res.body).toHaveLength(3);
expect(res.body[0]).toEqual({
expect(res.body).toEqual({
results: expect.any(Array),
page: 1,
limit: 10,
totalPages: 1,
totalResults: 3,
});
expect(res.body.results).toHaveLength(3);
expect(res.body.results[0]).toEqual({
id: userOne._id.toHexString(),
name: userOne.name,
email: userOne.email,
Expand Down Expand Up @@ -180,8 +186,15 @@ describe('User routes', () => {
.send()
.expect(httpStatus.OK);

expect(res.body).toHaveLength(1);
expect(res.body[0].id).toBe(userOne._id.toHexString());
expect(res.body).toEqual({
results: expect.any(Array),
page: 1,
limit: 10,
totalPages: 1,
totalResults: 1,
});
expect(res.body.results).toHaveLength(1);
expect(res.body.results[0].id).toBe(userOne._id.toHexString());
});

test('should correctly apply filter on role field', async () => {
Expand All @@ -194,9 +207,16 @@ describe('User routes', () => {
.send()
.expect(httpStatus.OK);

expect(res.body).toHaveLength(2);
expect(res.body[0].id).toBe(userOne._id.toHexString());
expect(res.body[1].id).toBe(userTwo._id.toHexString());
expect(res.body).toEqual({
results: expect.any(Array),
page: 1,
limit: 10,
totalPages: 1,
totalResults: 2,
});
expect(res.body.results).toHaveLength(2);
expect(res.body.results[0].id).toBe(userOne._id.toHexString());
expect(res.body.results[1].id).toBe(userTwo._id.toHexString());
});

test('should correctly sort returned array if descending sort param is specified', async () => {
Expand All @@ -209,8 +229,17 @@ describe('User routes', () => {
.send()
.expect(httpStatus.OK);

expect(res.body).toHaveLength(3);
expect(res.body[0].id).toBe(userOne._id.toHexString());
expect(res.body).toEqual({
results: expect.any(Array),
page: 1,
limit: 10,
totalPages: 1,
totalResults: 3,
});
expect(res.body.results).toHaveLength(3);
expect(res.body.results[0].id).toBe(userOne._id.toHexString());
expect(res.body.results[1].id).toBe(userTwo._id.toHexString());
expect(res.body.results[2].id).toBe(admin._id.toHexString());
});

test('should correctly sort returned array if ascending sort param is specified', async () => {
Expand All @@ -223,8 +252,17 @@ describe('User routes', () => {
.send()
.expect(httpStatus.OK);

expect(res.body).toHaveLength(3);
expect(res.body[0].id).toBe(admin._id.toHexString());
expect(res.body).toEqual({
results: expect.any(Array),
page: 1,
limit: 10,
totalPages: 1,
totalResults: 3,
});
expect(res.body.results).toHaveLength(3);
expect(res.body.results[0].id).toBe(admin._id.toHexString());
expect(res.body.results[1].id).toBe(userOne._id.toHexString());
expect(res.body.results[2].id).toBe(userTwo._id.toHexString());
});

test('should limit returned array if limit param is specified', async () => {
Expand All @@ -237,7 +275,16 @@ describe('User routes', () => {
.send()
.expect(httpStatus.OK);

expect(res.body).toHaveLength(2);
expect(res.body).toEqual({
results: expect.any(Array),
page: 1,
limit: 2,
totalPages: 2,
totalResults: 3,
});
expect(res.body.results).toHaveLength(2);
expect(res.body.results[0].id).toBe(userOne._id.toHexString());
expect(res.body.results[1].id).toBe(userTwo._id.toHexString());
});

test('should return the correct page if page and limit params are specified', async () => {
Expand All @@ -250,8 +297,15 @@ describe('User routes', () => {
.send()
.expect(httpStatus.OK);

expect(res.body).toHaveLength(1);
expect(res.body[0].id).toBe(admin._id.toHexString());
expect(res.body).toEqual({
results: expect.any(Array),
page: 2,
limit: 2,
totalPages: 2,
totalResults: 3,
});
expect(res.body.results).toHaveLength(1);
expect(res.body.results[0].id).toBe(admin._id.toHexString());
});
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const mongoose = require('mongoose');
const { toJSON } = require('../../../src/models/utils');
const { toJSON } = require('../../../src/models/plugins');

describe('Model utils', () => {
describe('toJSON plugin', () => {
Expand Down

0 comments on commit 97fef4c

Please sign in to comment.