Skip to content

Commit

Permalink
Merge pull request #8 from paralect/ak_enhancement/rethink-validator
Browse files Browse the repository at this point in the history
[*] Move validator to middleware
  • Loading branch information
ezhivitsa authored Jun 16, 2018
2 parents f12069d + d6cf740 commit 37a3ab7
Show file tree
Hide file tree
Showing 19 changed files with 455 additions and 213 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.idea
node_modules
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"start": "node src/app.js",
"test": "run-s test:**",
"test:eslint": "eslint ./src",
"test:mocha": "NODE_ENV=test mocha --timeout 20000 --recursive --exit -c -R spec src/**/*.spec.js",
"test:mocha": "NODE_ENV=test mocha --timeout 20000 --recursive --exit -c -R spec 'src/**/*.spec.js'",
"development": "NODE_ENV=development nodemon --watch src src/app.js",
"format": "prettier-eslint --write \"src/**/*.js\"",
"add-contributor": "all-contributors add",
Expand Down
47 changes: 47 additions & 0 deletions src/helpers/joi.adapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const Joi = require('joi');
const _ = require('lodash');

const joiOptions = {
abortEarly: false,
allowUnknown: true,
stripUnknown: {
objects: true,
},
};

/**
* Parse and return list of errors
* @param {object} joiError
* @return {object[]}
*/
const parseJoiErrors = (joiError) => {
let resultErrors = [];
if (joiError && _.isArray(joiError.details)) {
resultErrors = joiError.details.map((error) => {
const pathLastPart = error.path.slice(error.path.length - error.context.key.length);

if (pathLastPart === error.context.key) {
return { [error.path]: error.message };
}

return { [error.context.key]: error.message };
});
}

return resultErrors;
};

const validate = _.curry((schema, payload) => {
const { error, value } = Joi.validate(payload, schema, joiOptions);

return {
errors: parseJoiErrors(error),
value,
};
});

module.exports = {
...Joi,
__validate: Joi.validate,
validate,
};
28 changes: 28 additions & 0 deletions src/helpers/joi.adapter.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const Joi = require('./joi.adapter');
const chai = require('chai');

chai.should();

describe('joi validator', () => {
it('should apply args partially', () => {
const schema = {
email: Joi.string(),
};
const validationResult = Joi.validate(schema, {
email: '[email protected]',
});

const validationResultPartial = Joi.validate(schema)({
email: '[email protected]',
});

validationResult.should.be.deep.equal(validationResultPartial);

validationResult.should.be.deep.equal({
errors: [],
value: {
email: '[email protected]',
},
});
});
});
40 changes: 40 additions & 0 deletions src/helpers/validator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const _ = require('lodash');

const Symbols = {
PERSISTENT: Symbol('persistent'),
};

module.exports.Symbols = Symbols;

const getValidators = (validators = []) => {
if (_.isFunction(validators)) {
return [validators];
}

if (!Array.isArray(validators) || !validators.every(_.isFunction)) {
throw Error('Validators must be a function or array of functions');
}

return validators;
};

module.exports.validate = (payload, validators = []) => {
const persistentData = payload[Symbols.PERSISTENT];
return getValidators(validators).reduce(async (result, validator) => {
const data = await result;

if (data.errors.length) {
return data;
}

const validationResult = await validator(data.value, persistentData);

return {
errors: validationResult.errors || [],
value: validationResult.value,
};
}, {
errors: [],
value: payload,
});
};
72 changes: 72 additions & 0 deletions src/helpers/validator.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
const { validate, Symbols } = require('./validator');
const chai = require('chai');

chai.should();

describe('validator', () => {
it("should return '{ errors: [], payload }' with empty validators array", async () => {
const validationResult = await validate({}, []);
validationResult.should.be.deep.equal({
errors: [],
value: {},
});
});

it('should return the value from the last validator', async () => {
const validators = [
() => ({ value: 1 }),
() => ({ value: 2 }),
];

const validationResult = await validate({}, validators);
validationResult.should.be.deep.equal({
errors: [],
value: 2,
});
});

it('should skip validators after errors appeared', async () => {
const validators = [
() => ({ value: 1 }),
() => ({ value: 2, errors: ['Errors was appear'] }),
() => ({ value: 3 }),
];

const validationResult = await validate({}, validators);
validationResult.should.be.deep.equal({
errors: ['Errors was appear'],
value: 2,
});
});

it('should apply persistent data to each validator', async () => {
const persistentData = {
persistant: 'Wow! persistent!',
};

const payload = {
[Symbols.PERSISTENT]: persistentData,
};

const validators = [
(data, persistent) => {
persistent.should.be.equal(persistentData);
return {
value: 1,
};
},
(data, persist) => {
persist.should.be.equal(persistentData);
return {
value: 2,
};
},
];

const validationResult = await validate(payload, validators);
validationResult.should.be.deep.equal({
errors: [],
value: 2,
});
});
});
36 changes: 36 additions & 0 deletions src/middlewares/validate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const { validate, Symbols } = require('helpers/validator');

const defaultOptions = {
throwOnInvalid: true,
};

const validateMiddleware = (validators = [], options = defaultOptions) => async (ctx, next) => {
const {
throwOnInvalid,
} = {
...defaultOptions,
...options,
};

const payload = {
...ctx.request.body,
...ctx.query,
...ctx.params,
[Symbols.PERSISTENT]: ctx,
};

const result = await validate(payload, validators);

if (throwOnInvalid && result.errors.length) {
ctx.body = {
errors: result.errors,
};

ctx.throw(400);
}

ctx.validatedRequest = result;
await next();
};

module.exports = validateMiddleware;
60 changes: 60 additions & 0 deletions src/middlewares/validate.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const path = require('path');
const _ = require('lodash');
require('app-module-path').addPath(path.resolve(__dirname, '../'));

const validate = require('./validate');
const Joi = require('helpers/joi.adapter');
const chai = require('chai');

chai.should();

describe('validator', () => {
const ctx = {
request: {
body: {
test: 'test',
},
},
query: {},
params: {},
throw: (status) => {
throw new Error(status);
},
};

const noop = () => { };

it('should add validatedRequest to ctx', async () => {
const schema = {
test: Joi.string(),
};
const ctxMock = _.cloneDeep(ctx);

await validate(Joi.validate(schema))(ctxMock, noop);
ctxMock.validatedRequest.should.deep.equal({ errors: [], value: { test: 'test' } });
});

it('should throw error for wrong validation', async () => {
const schema = {
test: Joi.string().email(),
};
const ctxMock = _.cloneDeep(ctx);

try {
await validate(Joi.validate(schema))(ctxMock, noop);
} catch (err) {
err.message.should.be.equal('400');
}
});


it('should throw error if validators is not a an function', async () => {
const ctxMock = _.cloneDeep(ctx);

try {
await validate('wrong validate func')(ctxMock, noop);
} catch (err) {
err.message.should.be.equal('Validators must be a function or array of functions');
}
});
});
29 changes: 6 additions & 23 deletions src/resources/account/account.controller.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
const validators = require('./validators');

const userService = require('resources/user/user.service');
const authService = require('auth.service');
const emailService = require('email.service');
Expand Down Expand Up @@ -34,11 +32,8 @@ const createUserAccount = async (userData) => {
* Create user, company, default app, send signup confirmation email and
* create auth token for user to login
*/
exports.signup = async (ctx, next) => {
const result = await validators.signup.validate(ctx);
ctx.assert(!result.errors, 400);

const { value: userData } = result;
exports.signup = async (ctx) => {
const userData = ctx.validatedRequest.value;
const user = await createUserAccount(userData);

const response = {};
Expand All @@ -53,10 +48,7 @@ exports.signup = async (ctx, next) => {
* sets `emailVerified` to true if token is valid
*/
exports.verifyEmail = async (ctx, next) => {
const result = await validators.verifyEmail.validate(ctx);
ctx.assert(!result.errors, 400);

const { value: data } = result;
const data = ctx.validatedRequest.value;
const user = await userService.markEmailAsVerified(data.userId);

const token = authService.createAuthToken({
Expand All @@ -72,10 +64,7 @@ exports.verifyEmail = async (ctx, next) => {
* Loads user by email and compare password hashes
*/
exports.signin = async (ctx, next) => {
const result = await validators.signin.validate(ctx);
ctx.assert(!result.errors, 400);

const { value: signinData } = result;
const signinData = ctx.validatedRequest.value;

const token = authService.createAuthToken({ userId: signinData.userId });

Expand All @@ -90,10 +79,7 @@ exports.signin = async (ctx, next) => {
* `forgotPasswordToken` field. If user not found, returns validator's error
*/
exports.forgotPassword = async (ctx, next) => {
const result = await validators.forgotPassword.validate(ctx);
ctx.assert(!result.errors, 400);

const { value: data } = result;
const data = ctx.validatedRequest.value;
const user = await userService.findOne({ email: data.email });

let { resetPasswordToken } = user;
Expand All @@ -111,10 +97,7 @@ exports.forgotPassword = async (ctx, next) => {
* Updates user password, used in combination with forgotPassword
*/
exports.resetPassword = async (ctx, next) => {
const result = await validators.resetPassword.validate(ctx);
ctx.assert(!result.errors, 400);

const { userId, password } = result.value;
const { userId, password } = ctx.validatedRequest.value;

await userService.updatePassword(userId, password);
await userService.updateResetPasswordToken(userId, '');
Expand Down
12 changes: 7 additions & 5 deletions src/resources/account/public.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
const Router = require('koa-router');
const validate = require('middlewares/validate');
const validators = require('./validators');

const router = new Router();
const controller = require('./account.controller');

router.post('/signup', controller.signup);
router.get('/verifyEmail/:token', controller.verifyEmail);
router.post('/signin', controller.signin);
router.post('/forgotPassword', controller.forgotPassword);
router.put('/resetPassword', controller.resetPassword);
router.post('/signup', validate(validators.signup), controller.signup);
router.get('/verifyEmail/:token', validate(validators.verifyEmail), controller.verifyEmail);
router.post('/signin', validate(validators.signin), controller.signin);
router.post('/forgotPassword', validate(validators.forgotPassword), controller.forgotPassword);
router.put('/resetPassword', validate(validators.resetPassword), controller.resetPassword);
router.post('/resend', controller.resendVerification);

module.exports = router.routes();
Loading

0 comments on commit 37a3ab7

Please sign in to comment.