Skip to content
This repository has been archived by the owner on May 23, 2023. It is now read-only.

Commit

Permalink
Merge branch 'release/v2.1.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
lucas-a-pelegrino committed May 1, 2020
2 parents d8d89c1 + 8db4bbb commit 317ebab
Show file tree
Hide file tree
Showing 14 changed files with 98 additions and 87 deletions.
27 changes: 0 additions & 27 deletions .github/ISSUE_TEMPLATE/bug-report.md

This file was deleted.

20 changes: 0 additions & 20 deletions .github/ISSUE_TEMPLATE/feature-request.md

This file was deleted.

4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Node API Bloodboiler :rocket:

![Build](https://github.com/lucas-a-pelegrino/node-bloodboiler/workflows/Build/badge.svg) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/7a7eafd4c4c145faad8aece85c786b2d)](https://www.codacy.com/manual/lucas.assuncao.p/node-bloodboiler?utm_source=github.com&utm_medium=referral&utm_content=lucas-a-pelegrino/node-bloodboiler&utm_campaign=Badge_Grade) [![codecov](https://codecov.io/gh/lucas-a-pelegrino/node-bloodboiler/branch/develop/graph/badge.svg)](https://codecov.io/gh/lucas-a-pelegrino/node-bloodboiler) [![Version](https://badge.fury.io/gh/tterb%2FHyde.svg)](https://badge.fury.io/gh/tterb%2FHyde) [![GitHub Release](https://img.shields.io/github/v/release/lucas-a-pelegrino/node-bloodboiler?sort=semver)]() [![MIT License](https://img.shields.io/apm/l/atomic-design-ui.svg?)](https://opensource.org/licenses/MIT)
![Build](https://github.com/lucas-a-pelegrino/node-bloodboiler/workflows/Build/badge.svg) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/7a7eafd4c4c145faad8aece85c786b2d)](https://www.codacy.com/manual/lucas.assuncao.p/node-bloodboiler?utm_source=github.com&utm_medium=referral&utm_content=lucas-a-pelegrino/node-bloodboiler&utm_campaign=Badge_Grade) [![codecov](https://codecov.io/gh/lucas-a-pelegrino/node-bloodboiler/branch/develop/graph/badge.svg)](https://codecov.io/gh/lucas-a-pelegrino/node-bloodboiler) [![GitHub Release](https://img.shields.io/github/v/release/lucas-a-pelegrino/node-bloodboiler?sort=semver)]() [![MIT License](https://img.shields.io/apm/l/atomic-design-ui.svg?)](https://opensource.org/licenses/MIT) [![sequelized version](https://img.shields.io/badge/bloodboiler-sequelized-blue)](https://github.com/lucas-a-pelegrino/node-bloodboiler-sequelized)

> A API boilerplate built on top of ExpressJS.
Expand All @@ -20,6 +20,8 @@
- **Linting:** [ESLint](https://eslint.org)/[Prettier](https://prettier.io);
- **API Documentation:** [Swagger](https://swagger.io)/[Postman](https://www.postman.com);

> This boilerplate is also available with Sequelize/PostgreSQL on this [repository](https://github.com/lucas-a-pelegrino/node-bloodboiler-sequelized)!
## Getting Started

### Installation Steps
Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "node-bloodboiler",
"version": "v2.0.0",
"version": "2.0.0",
"description": "A API built on top of expressJS",
"main": "app.js",
"author": "Lucas A Pelegrino <[email protected]>",
Expand Down Expand Up @@ -31,7 +31,6 @@
"express": "^4.17.1",
"helmet": "^3.22.0",
"jsonwebtoken": "^8.5.1",
"lodash": "^4.17.15",
"moment": "^2.24.0",
"mongoose": "^5.9.9",
"morgan": "^1.10.0",
Expand Down
23 changes: 6 additions & 17 deletions src/helpers/queryHelper.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,27 @@
const { ApplicationError } = require('../utils');

module.exports.queryHelper = (options) => {
const sort = options.sortBy || 'createdAt:desc';
const limit = parseInt(options.perPage || 10, 10);
let skip = parseInt(options.page || 1, 10);
const sort = options.sortBy;
const limit = parseInt(options.perPage, 10);
const skip = parseInt(options.page, 10);

const pipeline = [
{
$facet: {
metadata: [
{ $count: 'total' },
{ $addFields: { currentPage: skip, totalPages: Math.ceil(skip / limit) } },
],
metadata: [{ $count: 'total' }],
data: [],
},
},
{ $unwind: '$metadata' },
];

skip = limit * (skip - 1);

pipeline[0].$facet.data.push({
$limit: limit,
$skip: limit * (skip - 1),
});

pipeline[0].$facet.data.push({
$skip: skip,
$limit: limit,
});

const [sortKey, sortValue] = sort.trim().split(':');
if (!['asc', 'desc'].includes(sortValue)) {
throw new ApplicationError("Sort order must be one of the following: 'asc' or 'desc'", 400);
}

pipeline[0].$facet.data.push({
$sort: { [sortKey]: sortValue === 'desc' ? -1 : 1 },
});
Expand Down
15 changes: 12 additions & 3 deletions src/helpers/validations/users.validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,18 @@ const yup = require('yup');

const list = {
query: yup.object().shape({
page: yup.number().integer(),
perPage: yup.number().integer(),
sortBy: yup.string(),
page: yup
.number()
.integer()
.default(1),
perPage: yup
.number()
.integer()
.default(10),
sortBy: yup
.string()
.matches(/[:](asc|desc)/i, "sorting order must be one of the following: 'asc' or 'desc'")
.default('createdAt:desc'),
}),
};

Expand Down
1 change: 1 addition & 0 deletions src/middlewares/error.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const errorHandler = (err, req, res, next) => {
res.status(status).json({
name: err.name,
message,
...(err.status === 400 && { errors: err.errors }),
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
});
};
Expand Down
13 changes: 9 additions & 4 deletions src/middlewares/validate.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
const yup = require('yup');
const { pick } = require('lodash');
const { ApplicationError } = require('../utils');

module.exports = (schema) => async (req, res, next) => {
const requestObject = pick(req, Object.keys(schema));
const requestObject = Object.fromEntries(
Object.entries(req).filter(([key]) => ['query', 'params', 'body'].includes(key)),
);

try {
const value = await yup
Expand All @@ -14,7 +15,11 @@ module.exports = (schema) => async (req, res, next) => {
Object.assign(req, value);
next();
} catch (error) {
const message = error.errors.join(', ');
next(new ApplicationError(message, 400));
const errors = {};
error.inner.forEach((error) => {
const [outerKey, innerKey] = error.path.split('.');
errors[outerKey] = { [innerKey]: error.message };
});
next(new ApplicationError('Invalid Fields', 400, true, '', errors));
}
};
5 changes: 3 additions & 2 deletions src/models/user.model.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const mongoose = require('mongoose');
const { omit } = require('lodash');
const { encryptor } = require('../helpers');

const userSchema = mongoose.Schema(
Expand Down Expand Up @@ -36,7 +35,9 @@ const userSchema = mongoose.Schema(

userSchema.methods.toJSON = function() {
const userRaw = this;
return omit(userRaw.toObject(), ['password', '__v']);
return Object.fromEntries(
Object.entries(userRaw.toObject()).filter(([key]) => !['password', '__v'].includes(key)),
);
};

userSchema.pre('save', async function(next) {
Expand Down
13 changes: 11 additions & 2 deletions src/services/users/list.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,16 @@ module.exports.list = async (options) => {

query[0].$facet.data.push({ $project: { password: 0, __v: 0 } });

const [response] = await usersRepository.list(query);
const [{ metadata, data }] = await usersRepository.list(query);
const totalPages = Math.ceil(metadata.total / options.perPage);

return response;
return {
metadata: {
...metadata,
totalPages,
...(options.page > 1 && { previousPage: options.page - 1 }),
...(options.page < metadata.total && options.page < totalPages && { nextPage: options.page + 1 }),
},
data,
};
};
49 changes: 44 additions & 5 deletions src/tests/integration/users.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ describe('User Endpoints', () => {

const { body } = response;
expect(body).toMatchObject({
metadata: expect.any(Object),
metadata: expect.objectContaining({
total: expect.any(Number),
totalPages: expect.any(Number),
}),
data: expect.any(Array),
});
});
Expand All @@ -75,6 +78,26 @@ describe('User Endpoints', () => {

expect(response.status).toBe(200);

const { body } = response;
expect(body).toMatchObject({
metadata: expect.objectContaining({
total: expect.any(Number),
totalPages: expect.any(Number),
}),
data: expect.any(Array),
});
});

test('Should return metadata with nextPage params', async () => {
const page = 1;
const perPage = 1;
const sortBy = 'createdAt:asc';
const response = await request(app)
.get(`${baseURL}?page=${page}&perPage=${perPage}&sortBy=${sortBy}`)
.set('Authorization', `Bearer ${token}`);

expect(response.status).toBe(200);

const { body } = response;
expect(body).toMatchObject({
metadata: expect.any(Object),
Expand All @@ -101,9 +124,16 @@ describe('User Endpoints', () => {
.set('Authorization', `Bearer ${token}`);

expect(response.status).toBe(400);

const { body } = response;
expect(body).toHaveProperty('message', "Sort order must be one of the following: 'asc' or 'desc'");
expect(response.body).toEqual(
expect.objectContaining({
message: 'Invalid Fields',
errors: {
query: {
sortBy: "sorting order must be one of the following: 'asc' or 'desc'",
},
},
}),
);
});
});

Expand All @@ -123,7 +153,16 @@ describe('User Endpoints', () => {
.set('Authorization', `Bearer ${token}`);

expect(response.status).toBe(400);
expect(response.body.message).toEqual(expect.stringMatching('id must be a valid mongo id'));
expect(response.body).toEqual(
expect.objectContaining({
message: 'Invalid Fields',
errors: {
params: {
id: 'id must be a valid mongo id',
},
},
}),
);
});

test('Should return 404 - Not Found', async () => {
Expand Down
6 changes: 5 additions & 1 deletion src/utils/ApplicationError.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
module.exports.ApplicationError = class ApplicationError extends Error {
constructor(message, status, isOperational = true, stack = '') {
constructor(message, status, isOperational = true, stack = '', errors = null) {
super(message);

this.name = this.constructor.name;
this.status = status;
this.isOperational = isOperational;

if (errors) {
this.errors = errors;
}

if (stack) {
this.stack = stack;
} else {
Expand Down
2 changes: 1 addition & 1 deletion src/utils/logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ module.exports.logger = winston.createLogger({
level: process.env.NODE_ENV === 'development' ? 'debug' : 'info',
format: winston.format.combine(
errorFormat(),
process.env.NODE_ENV === 'development' ? winston.format.colorize() : winston.format.uncolorize(),
winston.format.colorize(),
winston.format.splat(),
winston.format.printf(({ level, message }) => `${level}: ${message}`),
),
Expand Down
4 changes: 2 additions & 2 deletions src/utils/morgan.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ const { logger } = require('./logger');
morgan.token('message', (req, res) => res.locals.errorMessage || '');

const getIPFormat = () => (process.env.NODE_ENV === 'production' ? ':remote-addr - ' : '');
const successResponseFormat = `${getIPFormat()}:method :url - :status - :response-time ms`;
const errorResponseFormat = `${getIPFormat()}:method :url - :status - :response-time ms - message: :message`;
const successResponseFormat = `${getIPFormat()}:method :url - :status - :response-time ms - :date[iso]`;
const errorResponseFormat = `${getIPFormat()}:method :url - :status - :response-time ms - message: :message - :date[iso]`;

const successHandler = morgan(successResponseFormat, {
skip: (req, res) => res.statusCode >= 400,
Expand Down

0 comments on commit 317ebab

Please sign in to comment.