From 154abe67e0f748dba47b70271ce73fd71ec00ba2 Mon Sep 17 00:00:00 2001 From: byverdu Date: Tue, 23 May 2023 00:22:06 +0100 Subject: [PATCH 1/7] defined types file + refactor config files --- .eslintrc.json | 4 +++- .gitignore | 1 - .husky/pre-commit | 2 +- .vscode/settings.json | 3 +++ package.json | 1 + src/app.mjs | 14 ++++---------- src/server.mjs | 14 +++----------- utils/types.mjs | 39 +++++++++++++++++++++++++++++++++++++++ 8 files changed, 54 insertions(+), 24 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 utils/types.mjs diff --git a/.eslintrc.json b/.eslintrc.json index 508213f..9c1fc19 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -13,6 +13,8 @@ }, "rules": { "no-unused-vars": ["error", { "vars": "all", "args": "after-used", "ignoreRestSiblings": false }], - "semi": "off" + "semi": "off", + "space-before-function-paren": "off", + "comma-dangle":"off" } } diff --git a/.gitignore b/.gitignore index 7fe89e3..415c268 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ node_modules yarn-error.log .DS_Store -.vscode coverage/ mockedFolder lib/ diff --git a/.husky/pre-commit b/.husky/pre-commit index 0b71a80..ae01268 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -2,4 +2,4 @@ . "$(dirname -- "$0")/_/husky.sh" -pnpm run prettify && pnpm run lint +pnpm run prettier:check && pnpm run lint diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..88a9464 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "stylelint.enable": false +} \ No newline at end of file diff --git a/package.json b/package.json index c187d2f..eed1a7f 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "release:notes": "./scripts/release_notes.sh", "release": "./scripts/release.sh", "prepare": "husky install", + "prettier:check": "prettier --check ./src/*", "prettify": "prettier ./src/* --write" }, "keywords": [ diff --git a/src/app.mjs b/src/app.mjs index 3147e8f..73bcf9a 100644 --- a/src/app.mjs +++ b/src/app.mjs @@ -6,17 +6,11 @@ import { healthRouter } from './routes/health.mjs'; /** * @preserve - * @param {{ - * routes: Array<{ - * method: 'get' | 'put' | 'delete' | 'post' | 'patch', - * path: string, - * handler: function(Request, Response) - * }> - * middleware: Array - * }} - * @returns @type Express + * @param {import("../utils/types.mjs").Params} + * + * @returns {import("express").Express} */ -function expressApp({ routes, middleware } = {}) { +function expressApp({ routes, middleware }) { if (!routes || !Array.isArray(routes)) { throw new Error('Either routes is not defined or it is not an Array'); } diff --git a/src/server.mjs b/src/server.mjs index dd30e76..49bd866 100644 --- a/src/server.mjs +++ b/src/server.mjs @@ -2,20 +2,12 @@ import { expressApp } from './app.mjs'; /** * @preserve - * @param {{ - * port: number, - * routes: Array<{ - * method: 'get' | 'put' | 'delete' | 'post' | 'patch', - * path: string, - * handler: function(Request, Response) - * }> - * middleware: Array - * }} + * @param {import("../utils/types.mjs").Params} * - * @returns @type Express + * @returns {import("http").Server} */ -function httpServer({ port, routes, middleware = [] } = {}) { +function httpServer({ port, routes, middleware }) { const PORT = port || 3000; const server = expressApp({ routes, middleware }); diff --git a/utils/types.mjs b/utils/types.mjs new file mode 100644 index 0000000..f354fb2 --- /dev/null +++ b/utils/types.mjs @@ -0,0 +1,39 @@ +/** + * @typedef {Object} Options + * + * @property {boolean} useCors + */ + +/** + * CRUD operations + * @typedef {'get' | 'put' | 'delete' | 'post' | 'patch'} Operations + */ + +/** + * Route params + * @typedef {Object} Route + * + * @property {Operations} method + * @property {string} path + * @property {(req: Express.Request, resp: Express.Response) => void} handler + */ + +/** + * @typedef {Object} Middleware + * + * @property {import("express").Request} req + * @property {import("express").Response} res + * @property {import("express").NextFunction} next + */ + +/** + * Server and App params + * @typedef {Object} Params + * + * @property {number} [port] + * @property {Array} middleware + * @property {Options} [options] + * @property {Array} routes + */ + +export {}; From 5666346635997aabea718f8d41715baf3bb9a075 Mon Sep 17 00:00:00 2001 From: byverdu Date: Sun, 28 May 2023 01:22:17 +0100 Subject: [PATCH 2/7] wip validator util --- README.md | 10 ++++ src/utils/validator.mjs | 117 ++++++++++++++++++++++++++++++++++++++++ utils/types.mjs | 9 ++++ 3 files changed, 136 insertions(+) create mode 100644 src/utils/validator.mjs diff --git a/README.md b/README.md index 77452bc..52784fb 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,16 @@ # http-server + + + + This package is meant to be used as a quick way to setup an Express application. The module does not have too much defined. The default functionality is the following. - `cors` enabled diff --git a/src/utils/validator.mjs b/src/utils/validator.mjs new file mode 100644 index 0000000..95a6667 --- /dev/null +++ b/src/utils/validator.mjs @@ -0,0 +1,117 @@ +class Validator { + nextValidator = null; + + /** + * @param {import("../../utils/types.mjs").Params} model + * @returns {import("../../utils/types.mjs").Validator} + */ + isValid(model) { + if (this.nextValidator) { + return this.nextValidator.isValid(model); + } + + return { + isValid: true, + }; + } + + setNextValidator(nextValidator) { + this.nextValidator = nextValidator; + } +} + +class PortValidator extends Validator { + /** + * @param {import("../../utils/types.mjs").Params} model + * @returns {number} + */ + getPort(model) { + return model.port; + } + + /** + * @param {import("../../utils/types.mjs").Params} model + * @returns {import("../../utils/types.mjs").Validator} + */ + isValid(model) { + const port = this.getPort(model); + const typeofPort = typeof port; + + if (typeofPort !== 'number') { + return { + isValid: false, + errorMsg: `"port" property should be a number but a ${typeofPort} was given`, + }; + } + + return super.isValid(model); + } +} + +class MsgValidator extends Validator { + /** + * @param {import("../../utils/types.mjs").Params} model + * @returns {string} + */ + getMsg(model) { + return model.msg; + } + + /** + * @param {import("../../utils/types.mjs").Params} model + * @returns {import("../../utils/types.mjs").Validator} + */ + isValid(model) { + const msg = this.getMsg(model); + const typeofMsg = typeof msg; + + if (typeofMsg !== 'string') { + return { + isValid: false, + errorMsg: `"msg" property should be a string but a ${typeofMsg} was given`, + }; + } + + return super.isValid(model); + } +} + +class ValidatorChain { + constructor() { + this.first = null; + this.last = null; + } + + add(validator) { + if (!this.first) { + this.first = validator; + this.last = validator; + + // return this; + } + + this.last.setNextValidator(validator); + this.last = validator; + + console.log(this); + + return this; + } + + getFirst() { + return this.first; + } +} + +const options = { + port: 9000, + msg: 'Boolean', +}; + +const validation = new ValidatorChain() + .add(new PortValidator()) + .add(new MsgValidator()) + .getFirst() + .isValid(options); + +console.log(validation); diff --git a/utils/types.mjs b/utils/types.mjs index f354fb2..e6d551e 100644 --- a/utils/types.mjs +++ b/utils/types.mjs @@ -31,9 +31,18 @@ * @typedef {Object} Params * * @property {number} [port] + * @property {string} [msg] // delete * @property {Array} middleware * @property {Options} [options] * @property {Array} routes */ +/** + * Validator return Object + * @typedef {Object} Validator + * + * @property {boolean} isValid + * @property {string} [errorMsg] + */ + export {}; From ed0a5e7982c9e39880eaa6ab9cce432f85aa16b3 Mon Sep 17 00:00:00 2001 From: Albert Vallverdu Date: Wed, 31 May 2023 16:36:29 +0100 Subject: [PATCH 3/7] validator cleanup --- src/utils/validator.mjs | 44 +++++++++++------------------------------ 1 file changed, 11 insertions(+), 33 deletions(-) diff --git a/src/utils/validator.mjs b/src/utils/validator.mjs index 95a6667..82d995d 100644 --- a/src/utils/validator.mjs +++ b/src/utils/validator.mjs @@ -5,9 +5,9 @@ class Validator { * @param {import("../../utils/types.mjs").Params} model * @returns {import("../../utils/types.mjs").Validator} */ - isValid(model) { + validate(model) { if (this.nextValidator) { - return this.nextValidator.isValid(model); + return this.nextValidator.validate(model); } return { @@ -21,21 +21,12 @@ class Validator { } class PortValidator extends Validator { - /** - * @param {import("../../utils/types.mjs").Params} model - * @returns {number} - */ - getPort(model) { - return model.port; - } - /** * @param {import("../../utils/types.mjs").Params} model * @returns {import("../../utils/types.mjs").Validator} */ - isValid(model) { - const port = this.getPort(model); - const typeofPort = typeof port; + validate(model) { + const typeofPort = typeof model.port; if (typeofPort !== 'number') { return { @@ -44,26 +35,17 @@ class PortValidator extends Validator { }; } - return super.isValid(model); + return super.validate(model); } } class MsgValidator extends Validator { - /** - * @param {import("../../utils/types.mjs").Params} model - * @returns {string} - */ - getMsg(model) { - return model.msg; - } - /** * @param {import("../../utils/types.mjs").Params} model * @returns {import("../../utils/types.mjs").Validator} */ - isValid(model) { - const msg = this.getMsg(model); - const typeofMsg = typeof msg; + validate(model) { + const typeofMsg = typeof model.msg; if (typeofMsg !== 'string') { return { @@ -72,7 +54,7 @@ class MsgValidator extends Validator { }; } - return super.isValid(model); + return super.validate(model); } } @@ -86,19 +68,15 @@ class ValidatorChain { if (!this.first) { this.first = validator; this.last = validator; - - // return this; } this.last.setNextValidator(validator); this.last = validator; - console.log(this); - return this; } - getFirst() { + startValidation() { return this.first; } } @@ -111,7 +89,7 @@ const options = { const validation = new ValidatorChain() .add(new PortValidator()) .add(new MsgValidator()) - .getFirst() - .isValid(options); + .startValidation() + .validate(options); console.log(validation); From d79a7a0616bd1df80731246f85b3c7de158279b7 Mon Sep 17 00:00:00 2001 From: byverdu Date: Mon, 19 Jun 2023 23:41:01 +0100 Subject: [PATCH 4/7] wip validator util --- __tests__/validator.spec.mjs | 370 ++++++++++++++++++++++++++ src/utils/validator.mjs | 95 ------- src/validator/baseClass.mjs | 28 ++ src/validator/index.mjs | 7 + src/validator/middlewareValidator.mjs | 19 ++ src/validator/optionsValidator.mjs | 25 ++ src/validator/portValidator.mjs | 17 ++ src/validator/routesValidator.mjs | 77 ++++++ src/validator/utils.mjs | 19 ++ src/validator/validatorChain.mjs | 35 +++ utils/types.mjs | 35 ++- 11 files changed, 618 insertions(+), 109 deletions(-) create mode 100644 __tests__/validator.spec.mjs delete mode 100644 src/utils/validator.mjs create mode 100644 src/validator/baseClass.mjs create mode 100644 src/validator/index.mjs create mode 100644 src/validator/middlewareValidator.mjs create mode 100644 src/validator/optionsValidator.mjs create mode 100644 src/validator/portValidator.mjs create mode 100644 src/validator/routesValidator.mjs create mode 100644 src/validator/utils.mjs create mode 100644 src/validator/validatorChain.mjs diff --git a/__tests__/validator.spec.mjs b/__tests__/validator.spec.mjs new file mode 100644 index 0000000..8dd75d0 --- /dev/null +++ b/__tests__/validator.spec.mjs @@ -0,0 +1,370 @@ +import { jest } from '@jest/globals'; +import { + Validator, + PortValidator, + ValidatorChain, + OptionsValidator, + RoutesValidator, + MiddlewareValidator, + utils, +} from '../src/validator/index.mjs'; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('utils', () => { + describe('validatorErrorMsgGenerator', () => { + it('should be an instance of function', () => { + expect(utils.validatorErrorMsgGenerator).toBeInstanceOf(Function); + }); + it.each([ + [ + 'port', + 'undefined', + '"port" property should be typeof number but undefined was given', + ], + [ + 'options', + 'undefined', + '"options" property should be typeof object but undefined was given', + ], + [ + 'options.useCors', + 'undefined', + '"options.useCors" property should be typeof boolean but undefined was given', + ], + [ + 'handler', + 'undefined', + '"handler" property should be typeof function but undefined was given', + ], + ])( + 'utils.validatorErrorMsgGenerator(%s, %s)', + (propToValidate, typeofProp, errorMsg) => { + const expected = { + errorMsg, + isValid: false, + }; + expect( + utils.validatorErrorMsgGenerator(propToValidate, typeofProp) + ).toMatchObject(expected); + } + ); + }); +}); + +describe('Validator', () => { + it('should be defined', () => { + expect(Validator).toBeInstanceOf(Function); + }); + + it('should have a "validate" property and be a Function', () => { + expect(Validator.prototype).toHaveProperty('validate'); + expect(Validator.prototype.validate).toBeInstanceOf(Function); + }); + + it('should have a "setNextValidator" property and be a Function', () => { + expect(Validator.prototype).toHaveProperty('setNextValidator'); + expect(Validator.prototype.setNextValidator).toBeInstanceOf(Function); + }); + + it('should have a "nextValidator" property initialized to null', () => { + const validator = new Validator(); + expect(validator).toHaveProperty('nextValidator'); + expect(validator.nextValidator).toEqual(null); + }); +}); + +describe('ValidatorChain', () => { + it('should be defined', () => { + expect(ValidatorChain).toBeInstanceOf(Function); + }); + + it('should have a first, last and validators properties', () => { + const chain = new ValidatorChain(); + expect(chain.first).toEqual(null); + expect(chain.last).toEqual(null); + expect(chain.validators).toEqual([]); + }); + + it('should have a "startValidation" property', () => { + expect(ValidatorChain.prototype).toHaveProperty('startValidation'); + }); + + it('should be able to chain 1 validator', () => { + jest.spyOn(ValidatorChain.prototype, 'add'); + jest.spyOn(PortValidator.prototype, 'setNextValidator'); + + const portValidator = new PortValidator(); + const chain = new ValidatorChain().add(portValidator); + + expect(ValidatorChain.prototype.add).toBeCalledTimes(1); + expect(ValidatorChain.prototype.add).toBeCalledWith(portValidator); + expect(PortValidator.prototype.setNextValidator).not.toBeCalled(); + expect(chain.validators).toEqual(['PortValidator']); + expect(chain.first).toEqual(portValidator); + expect(chain.last).toEqual(portValidator); + }); + + it('should be able to chain 2 validators', () => { + jest.spyOn(ValidatorChain.prototype, 'add'); + jest.spyOn(PortValidator.prototype, 'setNextValidator'); + + const firstValidator = new PortValidator(); + const secondValidator = new PortValidator(); + const chain = new ValidatorChain().add(firstValidator).add(secondValidator); + + expect(ValidatorChain.prototype.add).toBeCalledTimes(2); + expect(ValidatorChain.prototype.add).nthCalledWith(1, firstValidator); + expect(ValidatorChain.prototype.add).nthCalledWith(2, secondValidator); + expect(chain.validators).toEqual(['PortValidator', 'PortValidator']); + expect(chain.first).toEqual(firstValidator); + expect(chain.last).toEqual(secondValidator); + }); + + it('should start a validation', () => { + jest.spyOn(ValidatorChain.prototype, 'add'); + jest.spyOn(ValidatorChain.prototype, 'startValidation'); + jest.spyOn(PortValidator.prototype, 'validate'); + + const options = { + port: 9000, + }; + const portValidator = new PortValidator(); + const validation = new ValidatorChain() + .add(portValidator) + .startValidation() + .validate(options); + + expect(ValidatorChain.prototype.add).toBeCalledTimes(1); + expect(ValidatorChain.prototype.add).toBeCalledWith(portValidator); + expect(ValidatorChain.prototype.startValidation).toBeCalledTimes(1); + expect(PortValidator.prototype.validate).toBeCalledTimes(1); + expect(PortValidator.prototype.validate).toBeCalledWith(options); + expect(validation).toEqual({ isValid: true }); + }); +}); + +describe('PortValidator', () => { + it('should extend Validator base class', () => { + expect(PortValidator.prototype).toBeInstanceOf(Validator); + }); + + it('should validate the port when is wrong', () => { + jest.spyOn(Validator.prototype, 'validate'); + + const example = new PortValidator().validate({ port: '9000' }); + + expect(Validator.prototype.validate).toBeCalledTimes(0); + expect(example).toEqual({ + errorMsg: '"port" property should be typeof number but string was given', + isValid: false, + }); + }); + + it('should call Validator.validate if port is valid', () => { + jest.spyOn(Validator.prototype, 'validate'); + + const example = new PortValidator().validate({ port: 9000 }); + + expect(Validator.prototype.validate).toBeCalledTimes(1); + expect(example).toEqual({ + isValid: true, + }); + }); +}); + +describe('OptionsValidator', () => { + it('should extend Validator base class', () => { + expect(OptionsValidator.prototype).toBeInstanceOf(Validator); + }); + + it('should validate the options when are wrong if they are not an object', () => { + jest.spyOn(Validator.prototype, 'validate'); + + const example = new OptionsValidator().validate({ options: '9000' }); + + expect(Validator.prototype.validate).toBeCalledTimes(0); + expect(example).toEqual({ + errorMsg: + '"options" property should be typeof object but string was given', + isValid: false, + }); + }); + + it('should validate the wrong options passed', () => { + jest.spyOn(Validator.prototype, 'validate'); + + const example = new OptionsValidator().validate({ + options: { useCors: 'test' }, + }); + + expect(Validator.prototype.validate).toBeCalledTimes(0); + expect(example).toEqual({ + errorMsg: + '"options.useCors" property should be typeof boolean but string was given', + isValid: false, + }); + }); + + it('should call Validator.validate if the options are valid', () => { + jest.spyOn(Validator.prototype, 'validate'); + + const example = new OptionsValidator().validate({ + options: { useCors: true }, + }); + + expect(Validator.prototype.validate).toBeCalledTimes(1); + expect(example).toEqual({ + isValid: true, + }); + }); +}); + +describe('RoutesValidator', () => { + it('should extend Validator base class', () => { + expect(RoutesValidator.prototype).toBeInstanceOf(Validator); + }); + + it('should validate that "routes" is an array', () => { + jest.spyOn(Validator.prototype, 'validate'); + + const example = new RoutesValidator().validate({ routes: '9000' }); + + expect(Validator.prototype.validate).toBeCalledTimes(0); + expect(example).toEqual({ + errorMsg: '"routes" property should be an Array but string was given', + isValid: false, + }); + }); + + it('should check that all props in a route are valid', () => { + jest.spyOn(Validator.prototype, 'validate'); + jest.spyOn(RoutesValidator.prototype, 'validateRoutesProps'); + + const example = new RoutesValidator().validate({ + routes: [ + { + methods: 'foo', + }, + ], + }); + + expect(Validator.prototype.validate).toBeCalledTimes(0); + expect(RoutesValidator.prototype.validateRoutesProps).toBeCalledTimes(1); + expect(RoutesValidator.prototype.validateRoutesProps).toBeCalledWith({ + methods: 'foo', + }); + expect(example).toEqual({ + errorMsg: '"routes" items have invalid props', + isValid: false, + }); + }); + + it('should check that in a route the "method" is valid', () => { + jest.spyOn(Validator.prototype, 'validate'); + jest.spyOn(RoutesValidator.prototype, 'validateRouteMethod'); + + const example = new RoutesValidator().validate({ + routes: [ + { + method: true, + handler: '', + path: '', + }, + ], + }); + + expect(Validator.prototype.validate).toBeCalledTimes(0); + expect(RoutesValidator.prototype.validateRouteMethod).toBeCalledTimes(1); + expect(example).toEqual({ + errorMsg: 'true is not a valid method in a route', + isValid: false, + }); + }); + + it('should check that in a route the "path" is valid', () => { + jest.spyOn(Validator.prototype, 'validate'); + jest.spyOn(RoutesValidator.prototype, 'validateRoutePath'); + + const example = new RoutesValidator().validate({ + routes: [ + { + method: 'get', + handler: '', + path: true, + }, + ], + }); + + expect(Validator.prototype.validate).toBeCalledTimes(0); + expect(RoutesValidator.prototype.validateRoutePath).toBeCalledTimes(1); + expect(example).toEqual({ + errorMsg: '"path" in a route must start with "/"', + isValid: false, + }); + }); + + it('should check that in a route the "handler" is valid', () => { + jest.spyOn(Validator.prototype, 'validate'); + jest.spyOn(RoutesValidator.prototype, 'validateRoutePath'); + + const example = new RoutesValidator().validate({ + routes: [ + { + method: 'get', + handler: '', + path: '/test', + }, + ], + }); + + expect(Validator.prototype.validate).toBeCalledTimes(0); + expect(RoutesValidator.prototype.validateRoutePath).toBeCalledTimes(1); + expect(example).toEqual({ + errorMsg: + '"handler" property should be typeof function but string was given', + isValid: false, + }); + }); + + it('should call Validator.validate if routes are valid', () => { + jest.spyOn(Validator.prototype, 'validate'); + jest.spyOn(RoutesValidator.prototype, 'validateRoutePath'); + + const example = new RoutesValidator().validate({ + routes: [ + { + method: 'get', + handler: () => {}, + path: '/test', + }, + ], + }); + + expect(Validator.prototype.validate).toBeCalledTimes(1); + expect(RoutesValidator.prototype.validateRoutePath).toBeCalledTimes(1); + expect(example).toEqual({ + isValid: true, + }); + }); +}); + +describe('MiddlewareValidator', () => { + it('should extend Validator base class', () => { + expect(MiddlewareValidator.prototype).toBeInstanceOf(Validator); + }); + + it('should validate that "middleware" is an array', () => { + jest.spyOn(Validator.prototype, 'validate'); + + const example = new MiddlewareValidator().validate({ middleware: '9000' }); + + expect(Validator.prototype.validate).toBeCalledTimes(0); + expect(example).toEqual({ + errorMsg: '"middleware" property should be an Array but string was given', + isValid: false, + }); + }); +}); diff --git a/src/utils/validator.mjs b/src/utils/validator.mjs deleted file mode 100644 index 82d995d..0000000 --- a/src/utils/validator.mjs +++ /dev/null @@ -1,95 +0,0 @@ -class Validator { - nextValidator = null; - - /** - * @param {import("../../utils/types.mjs").Params} model - * @returns {import("../../utils/types.mjs").Validator} - */ - validate(model) { - if (this.nextValidator) { - return this.nextValidator.validate(model); - } - - return { - isValid: true, - }; - } - - setNextValidator(nextValidator) { - this.nextValidator = nextValidator; - } -} - -class PortValidator extends Validator { - /** - * @param {import("../../utils/types.mjs").Params} model - * @returns {import("../../utils/types.mjs").Validator} - */ - validate(model) { - const typeofPort = typeof model.port; - - if (typeofPort !== 'number') { - return { - isValid: false, - errorMsg: `"port" property should be a number but a ${typeofPort} was given`, - }; - } - - return super.validate(model); - } -} - -class MsgValidator extends Validator { - /** - * @param {import("../../utils/types.mjs").Params} model - * @returns {import("../../utils/types.mjs").Validator} - */ - validate(model) { - const typeofMsg = typeof model.msg; - - if (typeofMsg !== 'string') { - return { - isValid: false, - errorMsg: `"msg" property should be a string but a ${typeofMsg} was given`, - }; - } - - return super.validate(model); - } -} - -class ValidatorChain { - constructor() { - this.first = null; - this.last = null; - } - - add(validator) { - if (!this.first) { - this.first = validator; - this.last = validator; - } - - this.last.setNextValidator(validator); - this.last = validator; - - return this; - } - - startValidation() { - return this.first; - } -} - -const options = { - port: 9000, - msg: 'Boolean', -}; - -const validation = new ValidatorChain() - .add(new PortValidator()) - .add(new MsgValidator()) - .startValidation() - .validate(options); - -console.log(validation); diff --git a/src/validator/baseClass.mjs b/src/validator/baseClass.mjs new file mode 100644 index 0000000..ef59fe4 --- /dev/null +++ b/src/validator/baseClass.mjs @@ -0,0 +1,28 @@ +export class Validator { + /** + * @type {import("../../utils/types.mjs").ValidatorClass} + */ + nextValidator = null; + + /** + * @param {import("../../utils/types.mjs").Params} model + * @returns {import("../../utils/types.mjs").Validator} + */ + validate(model) { + if (this.nextValidator) { + return this.nextValidator.validate(model); + } + + return { + isValid: true, + }; + } + + /** + * + * @param {import("../../utils/types.mjs").ValidatorClass} nextValidator + */ + setNextValidator(nextValidator) { + this.nextValidator = nextValidator; + } +} diff --git a/src/validator/index.mjs b/src/validator/index.mjs new file mode 100644 index 0000000..ff92577 --- /dev/null +++ b/src/validator/index.mjs @@ -0,0 +1,7 @@ +export { Validator } from './baseClass.mjs'; +export { PortValidator } from './portValidator.mjs'; +export { OptionsValidator } from './optionsValidator.mjs'; +export { MiddlewareValidator } from './middlewareValidator.mjs'; +export { RoutesValidator } from './routesValidator.mjs'; +export { ValidatorChain } from './validatorChain.mjs'; +export * as utils from './utils.mjs'; diff --git a/src/validator/middlewareValidator.mjs b/src/validator/middlewareValidator.mjs new file mode 100644 index 0000000..b139108 --- /dev/null +++ b/src/validator/middlewareValidator.mjs @@ -0,0 +1,19 @@ +import { Validator } from './index.mjs'; + +export class MiddlewareValidator extends Validator { + /** + * @param {import("../../utils/types.mjs").Params} model + * @returns {import("../../utils/types.mjs").Validator} + */ + validate(model) { + const { middleware } = model; + const typeofMiddleware = typeof middleware; + + if (!Array.isArray(middleware)) { + return { + isValid: false, + errorMsg: `"middleware" property should be an Array but ${typeofMiddleware} was given`, + }; + } + } +} diff --git a/src/validator/optionsValidator.mjs b/src/validator/optionsValidator.mjs new file mode 100644 index 0000000..bceb21c --- /dev/null +++ b/src/validator/optionsValidator.mjs @@ -0,0 +1,25 @@ +import { Validator, utils } from './index.mjs'; + +export class OptionsValidator extends Validator { + /** + * @param {import("../../utils/types.mjs").Params} model + * @returns {import("../../utils/types.mjs").Validator} + */ + validate(model) { + const typeofOptions = typeof model?.options; + const typeofOptionsUseCors = typeof model?.options?.useCors; + + if (typeofOptions !== 'object') { + return utils.validatorErrorMsgGenerator('options', typeofOptions); + } + + if (typeofOptionsUseCors !== 'boolean') { + return utils.validatorErrorMsgGenerator( + 'options.useCors', + typeofOptionsUseCors + ); + } + + return super.validate(model); + } +} diff --git a/src/validator/portValidator.mjs b/src/validator/portValidator.mjs new file mode 100644 index 0000000..515140a --- /dev/null +++ b/src/validator/portValidator.mjs @@ -0,0 +1,17 @@ +import { Validator, utils } from './index.mjs'; + +export class PortValidator extends Validator { + /** + * @param {import("../../utils/types.mjs").Params} model + * @returns {import("../../utils/types.mjs").Validator} + */ + validate(model) { + const typeofPort = typeof model.port; + + if (typeofPort !== 'number') { + return utils.validatorErrorMsgGenerator('port', typeofPort); + } + + return super.validate(model); + } +} diff --git a/src/validator/routesValidator.mjs b/src/validator/routesValidator.mjs new file mode 100644 index 0000000..2ef8c0c --- /dev/null +++ b/src/validator/routesValidator.mjs @@ -0,0 +1,77 @@ +import { Validator, utils } from './index.mjs'; + +export class RoutesValidator extends Validator { + /** + * @param {import("../../utils/types.mjs").Route} route + * @returns {boolean} + */ + validateRoutesProps(route) { + return ['method', 'path', 'handler'] + .map(prop => Object.keys(route).includes(prop)) + .every(Boolean); + } + + /** + * @param {import("../../utils/types.mjs").Operations} operation + * @returns {boolean} + */ + validateRouteMethod(operation) { + return ['delete', 'get', 'patch', 'post', 'put'].includes(operation); + } + + validateRoutePath(path) { + return `${path}`.startsWith('/'); + } + + /** + * @param {import("../../utils/types.mjs").Params} model + * @returns {import("../../utils/types.mjs").Validator} + */ + validate(model) { + const { routes } = model; + const typeofRoutes = typeof routes; + + if (!Array.isArray(routes)) { + return { + isValid: false, + errorMsg: `"routes" property should be an Array but ${typeofRoutes} was given`, + }; + } + + /** + * @type {import("../../utils/types.mjs").Operations[]} + */ + + for (const route of routes) { + const hasInvalidProps = !this.validateRoutesProps(route); + + if (hasInvalidProps) { + return { + isValid: false, + errorMsg: '"routes" items have invalid props', + }; + } + + if (!this.validateRouteMethod(route.method)) { + return { + isValid: false, + errorMsg: `${route.method} is not a valid method in a route`, + }; + } + + if (!this.validateRoutePath(route.path)) { + return { + isValid: false, + errorMsg: '"path" in a route must start with "/"', + }; + } + + const typeofHandler = typeof route.handler; + if (typeofHandler !== 'function') { + return utils.validatorErrorMsgGenerator('handler', typeofHandler); + } + } + + return super.validate(model); + } +} diff --git a/src/validator/utils.mjs b/src/validator/utils.mjs new file mode 100644 index 0000000..fd815eb --- /dev/null +++ b/src/validator/utils.mjs @@ -0,0 +1,19 @@ +/** + * + * @param {"port" | "options" | "options.useCors" | "handler"} propToValidate + * @param {string} typeofProp + * @returns {import("../../utils/types.mjs").Validator} + */ +export function validatorErrorMsgGenerator(propToValidate, typeofProp) { + const typeofExpected = { + port: 'number', + options: 'object', + 'options.useCors': 'boolean', + handler: 'function', + }[propToValidate]; + + return { + isValid: false, + errorMsg: `"${propToValidate}" property should be typeof ${typeofExpected} but ${typeofProp} was given`, + }; +} diff --git a/src/validator/validatorChain.mjs b/src/validator/validatorChain.mjs new file mode 100644 index 0000000..706a811 --- /dev/null +++ b/src/validator/validatorChain.mjs @@ -0,0 +1,35 @@ +export class ValidatorChain { + constructor() { + this.first = null; + this.last = null; + /** @type Array */ + this.validators = []; + } + + /** + * @param {import("../../utils/types.mjs").ValidatorClass} validator + * @returns + */ + add(validator) { + if (!this.first) { + this.first = validator; + this.last = validator; + } + + this.validators.push(validator.constructor.name); + + if (this.validators.length > 1) { + this.last.setNextValidator(validator); + this.last = validator; + } + + return this; + } + + /** + * @returns {import("../../utils/types.mjs").ValidatorClass} + */ + startValidation() { + return this.first; + } +} diff --git a/utils/types.mjs b/utils/types.mjs index e6d551e..6b564a6 100644 --- a/utils/types.mjs +++ b/utils/types.mjs @@ -15,34 +15,41 @@ * * @property {Operations} method * @property {string} path - * @property {(req: Express.Request, resp: Express.Response) => void} handler + * @property {(req: import("express").Request, resp: import("express").Response) => void} handler */ /** - * @typedef {Object} Middleware + * Validator return Object + * @typedef {Object} ValidatorClass * - * @property {import("express").Request} req - * @property {import("express").Response} res - * @property {import("express").NextFunction} next + * @property {(param: Params) => Validator} validate + * @property {(param: ValidatorClass) => void} setNextValidator */ /** - * Server and App params - * @typedef {Object} Params + * Returns if model is valid + * @typedef {Object} Validator * - * @property {number} [port] - * @property {string} [msg] // delete - * @property {Array} middleware - * @property {Options} [options] - * @property {Array} routes + * @property {boolean} isValid + * @property {string} [errorMsg] */ /** - * Validator return Object - * @typedef {Object} Validator + * Returns if model is valid + * @typedef {Object} ValidatorChain * * @property {boolean} isValid * @property {string} [errorMsg] */ +/** + * Server and App params + * @typedef {Object} Params + * + * @property {number} [port] + * @property {Array<(req: import("express").Request, resp: import("express").Response, next: import("express").NextFunction) => void>} [middleware] + * @property {Options} [options] + * @property {Array} [routes] + */ + export {}; From 5244e6ff37e21a2e67678fc2e681b1fead565ec9 Mon Sep 17 00:00:00 2001 From: Albert Vallverdu Date: Tue, 20 Jun 2023 08:01:12 +0100 Subject: [PATCH 5/7] middleware validator implementation --- __tests__/validator.spec.mjs | 41 +++++++++++++++++++++++++++ src/validator/middlewareValidator.mjs | 20 ++++++++++++- src/validator/utils.mjs | 3 +- 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/__tests__/validator.spec.mjs b/__tests__/validator.spec.mjs index 8dd75d0..86b9d56 100644 --- a/__tests__/validator.spec.mjs +++ b/__tests__/validator.spec.mjs @@ -367,4 +367,45 @@ describe('MiddlewareValidator', () => { isValid: false, }); }); + + it('should validate that "middleware" is an array of functions', () => { + jest.spyOn(Validator.prototype, 'validate'); + + const example = new MiddlewareValidator().validate({ middleware: [true] }); + + expect(Validator.prototype.validate).toBeCalledTimes(0); + expect(example).toEqual({ + errorMsg: + '"middleware" property should be typeof function but boolean was given', + isValid: false, + }); + }); + + it('should validate that a "middleware" has 3 or 4 arguments', () => { + jest.spyOn(Validator.prototype, 'validate'); + + const example = new MiddlewareValidator().validate({ + middleware: [(req, res) => {}], + }); + + expect(Validator.prototype.validate).toBeCalledTimes(0); + expect(example).toEqual({ + errorMsg: + '"middleware" should have 3 args (req, res, next) or 4 args for error (error, req, res, next)', + isValid: false, + }); + }); + + it('should call Validator.validate if middleware are valid', () => { + jest.spyOn(Validator.prototype, 'validate'); + + const example = new MiddlewareValidator().validate({ + middleware: [(req, res, next) => {}], + }); + + expect(Validator.prototype.validate).toBeCalledTimes(1); + expect(example).toEqual({ + isValid: true, + }); + }); }); diff --git a/src/validator/middlewareValidator.mjs b/src/validator/middlewareValidator.mjs index b139108..858a4b9 100644 --- a/src/validator/middlewareValidator.mjs +++ b/src/validator/middlewareValidator.mjs @@ -1,4 +1,4 @@ -import { Validator } from './index.mjs'; +import { Validator, utils } from './index.mjs'; export class MiddlewareValidator extends Validator { /** @@ -15,5 +15,23 @@ export class MiddlewareValidator extends Validator { errorMsg: `"middleware" property should be an Array but ${typeofMiddleware} was given`, }; } + + for (const fn of middleware) { + const typeofFn = typeof fn; + + if (typeofFn !== 'function') { + return utils.validatorErrorMsgGenerator('middleware', typeofFn); + } + + if (fn.length !== 3 && fn.length !== 4) { + return { + errorMsg: + '"middleware" should have 3 args (req, res, next) or 4 args for error (error, req, res, next)', + isValid: false, + }; + } + } + + return super.validate(model); } } diff --git a/src/validator/utils.mjs b/src/validator/utils.mjs index fd815eb..b5dc47c 100644 --- a/src/validator/utils.mjs +++ b/src/validator/utils.mjs @@ -1,6 +1,6 @@ /** * - * @param {"port" | "options" | "options.useCors" | "handler"} propToValidate + * @param {"port" | "options" | "options.useCors" | "handler" | "middleware"} propToValidate * @param {string} typeofProp * @returns {import("../../utils/types.mjs").Validator} */ @@ -10,6 +10,7 @@ export function validatorErrorMsgGenerator(propToValidate, typeofProp) { options: 'object', 'options.useCors': 'boolean', handler: 'function', + middleware: 'function', }[propToValidate]; return { From f083d73fa303671534d3906737558cfb25f657d7 Mon Sep 17 00:00:00 2001 From: byverdu Date: Wed, 21 Jun 2023 14:23:08 +0100 Subject: [PATCH 6/7] refactor for Validator implementation --- __tests__/app.spec.mjs | 104 ++++++++++----------------- __tests__/server.spec.mjs | 116 +++++++++++++++++++++++++----- __tests__/utils.spec.mjs | 20 ++++++ __tests__/validator.spec.mjs | 13 ++-- jest/setupTests.js | 3 + package.json | 11 +-- src/app.mjs | 37 ---------- src/server.mjs | 41 +++++++++-- src/utils/index.mjs | 24 +++++++ src/validator/routesValidator.mjs | 23 ++++-- 10 files changed, 249 insertions(+), 143 deletions(-) create mode 100644 __tests__/utils.spec.mjs create mode 100644 jest/setupTests.js create mode 100644 src/utils/index.mjs diff --git a/__tests__/app.spec.mjs b/__tests__/app.spec.mjs index bae96e0..27a1c30 100644 --- a/__tests__/app.spec.mjs +++ b/__tests__/app.spec.mjs @@ -1,80 +1,48 @@ -import request from 'supertest' -import { jest } from '@jest/globals' -import { expressApp } from '../src/app.mjs' +import request from 'supertest'; +import { jest } from '@jest/globals'; +import { expressApp } from '../src/app.mjs'; -const mockedMiddleware = jest.fn +const mockedMiddleware = jest.fn; describe('App', () => { it('should be defined', () => { - expect(expressApp).toBeInstanceOf(Function) - }) - - it('should throw Error if no routes are specified', () => { - expect(() => expressApp({ middleware: [] })).toThrowError('Either routes is not defined or it is not an Array') - }) - - it('should throw Error if routes is not an Array', () => { - expect(() => expressApp({ routes: 100, middleware: [] })).toThrowError('Either routes is not defined or it is not an Array') - }) - - it('should throw Error if no middleware are specified', () => { - expect(() => expressApp({ routes: [] })).toThrowError('Either middleware is not defined or it is not an Array') - }) - - it('should throw Error if middleware is not an Array', () => { - expect(() => expressApp({ routes: [], middleware: 100 })).toThrowError('Either middleware is not defined or it is not an Array') - }) - - it('should throw Error if middleware is not an array of functions', () => { - expect(() => expressApp({ routes: [], middleware: [100] })).toThrowError('handler must be a function. Actual type is "number"') - }) - - it('should throw Error if routes do not provide a valid HTTP method', () => { - expect(() => expressApp({ middleware: [mockedMiddleware], routes: [{ method: 'posted' }] })).toThrowError('posted is not a valid HTTP method \n Allowed methods are get - delete - post - put - patch') - }) - - it('should throw Error if routes do not provide a function for the handler', () => { - expect(() => expressApp({ middleware: [mockedMiddleware], routes: [{ method: 'get', handler: true }] })).toThrowError('handler must be a function. Actual type is "boolean"') - }) - - it('should throw Error if routes do not provide a string for the path', () => { - expect(() => expressApp({ middleware: [mockedMiddleware], routes: [{ method: 'get', handler: () => {}, path: true }] })).toThrowError('path must be a string. Actual type is "boolean"') - }) - - it('should throw Error if routes provide an empty string for the path', () => { - expect(() => expressApp({ middleware: [mockedMiddleware], routes: [{ method: 'get', handler: () => {}, path: '' }] })).toThrowError('path can not be an empty string') - }) - - it('should throw Error if routes provide a string for the path that does not start with "/"', () => { - expect(() => expressApp({ middleware: [mockedMiddleware], routes: [{ method: 'get', handler: () => {}, path: 'somePath' }] })).toThrowError('path has to start with "/"') - }) + expect(expressApp).toBeInstanceOf(Function); + }); it('should have a /health route by default', async () => { - const app = expressApp({ middleware: [mockedMiddleware], routes: [] }) - const resp = await request(app).get('/health') + const app = expressApp({ middleware: [mockedMiddleware], routes: [] }); + const resp = await request(app).get('/health'); - expect(resp.ok).toEqual(true) - expect(resp.type).toEqual('text/html') - expect(resp.text).toEqual('ok') - }) + expect(resp.ok).toEqual(true); + expect(resp.type).toEqual('text/html'); + expect(resp.text).toEqual('ok'); + }); it('should handle 404 requests', async () => { - const app = expressApp({ middleware: [mockedMiddleware], routes: [] }) - const resp = await request(app).get('/notFound') + const app = expressApp({ middleware: [mockedMiddleware], routes: [] }); + const resp = await request(app).get('/notFound'); - expect(resp.status).toEqual(404) - expect(resp.type).toEqual('text/html') - expect(resp.text).toEqual('No handler found for /notFound') - }) + expect(resp.status).toEqual(404); + expect(resp.type).toEqual('text/html'); + expect(resp.text).toEqual('No handler found for /notFound'); + }); it('should register all routes passed', async () => { - const routes = [{ method: 'get', handler: (req, res) => { res.json({ value: 100 }) }, path: '/someRoute' }] - const server = expressApp({ routes, middleware: [mockedMiddleware] }) - - const resp = await request(server).get('/someRoute').send() - - expect(resp.ok).toEqual(true) - expect(resp.type).toEqual('application/json') - expect(resp.body).toEqual({ value: 100 }) - }) -}) + const routes = [ + { + method: 'get', + handler: (req, res) => { + res.json({ value: 100 }); + }, + path: '/someRoute', + }, + ]; + const server = expressApp({ routes, middleware: [mockedMiddleware] }); + + const resp = await request(server).get('/someRoute').send(); + + expect(resp.ok).toEqual(true); + expect(resp.type).toEqual('application/json'); + expect(resp.body).toEqual({ value: 100 }); + }); +}); diff --git a/__tests__/server.spec.mjs b/__tests__/server.spec.mjs index 861f8c5..d4b2857 100644 --- a/__tests__/server.spec.mjs +++ b/__tests__/server.spec.mjs @@ -1,32 +1,112 @@ -import { httpServer } from '../src/server.mjs' +import { jest } from '@jest/globals'; +import { httpServer } from '../src/server.mjs'; +import { + ValidatorChain, + PortValidator, + OptionsValidator, + RoutesValidator, + MiddlewareValidator, +} from '../src/validator/index.mjs'; +import utils from '../src/utils/index.mjs'; -let server +let server; beforeAll(() => { - server = httpServer({ routes: [], middleware: [() => {}] }) -}) + server = httpServer({ routes: [], middleware: [() => {}] }); +}); -afterAll(() => { - server.close() -}) +afterAll(done => { + server.close(done); +}); + +afterEach(() => { + jest.clearAllMocks(); +}); describe('Server', () => { it('should be defined', () => { - expect(httpServer).toBeInstanceOf(Function) - }) + expect(httpServer).toBeInstanceOf(Function); + }); it('should return an instance of Express Server', () => { - expect(server.constructor.name).toEqual('Server') - }) + expect(server.constructor.name).toEqual('Server'); + }); it('should default port to 3000 if is not specified', () => { - expect(server.address().port).toEqual(3000) - }) + expect(server.address().port).toEqual(3000); + }); it('should set the port number', () => { - const server = httpServer({ port: 6969, routes: [], middleware: [() => {}] }) + const server = httpServer({ + port: 6969, + routes: [], + middleware: [() => {}], + }); + + expect(server.address().port).toEqual(6969); + server.close(); + }); + + it('should validate the options by calling all the Validators', () => { + jest.spyOn(ValidatorChain.prototype, 'startValidation'); + jest.spyOn(PortValidator.prototype, 'validate'); + jest.spyOn(OptionsValidator.prototype, 'validate'); + jest.spyOn(RoutesValidator.prototype, 'validate'); + jest.spyOn(MiddlewareValidator.prototype, 'validate'); + + const server = httpServer({ + port: 6969, + routes: [], + middleware: [() => {}], + }); + + expect(ValidatorChain.prototype.startValidation).toBeCalledTimes(1); + expect(PortValidator.prototype.validate).toBeCalledTimes(1); + expect(OptionsValidator.prototype.validate).toBeCalledTimes(1); + expect(RoutesValidator.prototype.validate).toBeCalledTimes(1); + expect(MiddlewareValidator.prototype.validate).toBeCalledTimes(1); + + server.close(); + }); + + it('should stop the app if the Validator fails', () => { + jest.spyOn(console, 'error'); + jest.spyOn(process, 'exit').mockImplementation(error => error); + + process.env.NODE = 'dev'; + + const server = httpServer({ + port: '6969', + routes: [], + middleware: [() => {}], + }); + + expect(console.error).toBeCalledTimes(1); + expect(console.error).toBeCalledWith( + '"port" property should be typeof number but string was given' + ); + expect(process.exit).toBeCalledTimes(1); + expect(process.exit).toBeCalledWith(1); + + server.close(); + }); + + it('should call msgBuilder if the Validator is successful', () => { + jest.spyOn(utils, 'msgBuilder'); + + const appParams = { + port: 6969, + routes: [], + middleware: [() => {}], + }; + const server = httpServer(appParams); + + expect(utils.msgBuilder).toBeCalledTimes(1); + expect(utils.msgBuilder).toBeCalledWith({ + ...appParams, + options: { useCors: true }, + }); - expect(server.address().port).toEqual(6969) - server.close() - }) -}) + server.close(); + }); +}); diff --git a/__tests__/utils.spec.mjs b/__tests__/utils.spec.mjs new file mode 100644 index 0000000..960b861 --- /dev/null +++ b/__tests__/utils.spec.mjs @@ -0,0 +1,20 @@ +import utils from '../src/utils/index.mjs'; + +describe('msgBuilder', () => { + it('should be defined', () => { + expect(utils.msgBuilder).toBeInstanceOf(Function); + }); + + it('should return a msg for the props passed', () => { + const props = { + port: 4000, + routes: [{ handler: () => {}, path: '/some', method: 'get' }], + middleware: [() => {}], + options: { useCors: true }, + }; + + expect(utils.msgBuilder(props)).toEqual( + 'Your app is running with the following settings:\nPORT: 4000\nROUTES: [{"path":"/some","method":"get"}]\nMIDDLEWARE: 1 registered\nOPTIONS: {"useCors":true}' + ); + }); +}); diff --git a/__tests__/validator.spec.mjs b/__tests__/validator.spec.mjs index 86b9d56..542aa3d 100644 --- a/__tests__/validator.spec.mjs +++ b/__tests__/validator.spec.mjs @@ -247,17 +247,20 @@ describe('RoutesValidator', () => { routes: [ { methods: 'foo', + pat: '/route', }, ], }); expect(Validator.prototype.validate).toBeCalledTimes(0); expect(RoutesValidator.prototype.validateRoutesProps).toBeCalledTimes(1); - expect(RoutesValidator.prototype.validateRoutesProps).toBeCalledWith({ - methods: 'foo', - }); + expect(RoutesValidator.prototype.validateRoutesProps).toBeCalledWith([ + 'methods', + 'pat', + ]); expect(example).toEqual({ - errorMsg: '"routes" items have invalid props', + errorMsg: + '"routes" item have invalid or missing props.\nReceived: methods-pat\nValid: method-path-handler', isValid: false, }); }); @@ -293,7 +296,7 @@ describe('RoutesValidator', () => { { method: 'get', handler: '', - path: true, + path: '', }, ], }); diff --git a/jest/setupTests.js b/jest/setupTests.js new file mode 100644 index 0000000..f6803f7 --- /dev/null +++ b/jest/setupTests.js @@ -0,0 +1,3 @@ +import { jest } from '@jest/globals'; + +console.error = jest.fn(); diff --git a/package.json b/package.json index eed1a7f..84a5c69 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,10 @@ "type": "module", "main": "./lib/server.mjs", "scripts": { - "start": "node server.mjs", - "dev": "nodemon server.mjs", - "test:dev": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch --detectOpenHandles", - "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --collect-coverage --detectOpenHandles --forceExit", + "start": "node ./src/server.mjs", + "dev": "nodemon ./src/server.mjs", + "test:dev": "NODE=test node --experimental-vm-modules node_modules/jest/bin/jest.js --watch --detectOpenHandles", + "test": "NODE=test node --experimental-vm-modules node_modules/jest/bin/jest.js --collect-coverage --detectOpenHandles --forceExit", "test:clear:cache": "jest --clearCache", "lint": "eslint src/**/*.mjs utils/*mjs --fix", "build": "node utils/minify.mjs", @@ -55,6 +55,9 @@ "jest": { "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(mjs?|js?)$", "transform": {}, + "setupFilesAfterEnv": [ + "/jest/setupTests.js" + ], "moduleFileExtensions": [ "mjs", "js" diff --git a/src/app.mjs b/src/app.mjs index 73bcf9a..d250e18 100644 --- a/src/app.mjs +++ b/src/app.mjs @@ -5,22 +5,12 @@ import bodyParser from 'body-parser'; import { healthRouter } from './routes/health.mjs'; /** - * @preserve * @param {import("../utils/types.mjs").Params} * * @returns {import("express").Express} */ function expressApp({ routes, middleware }) { - if (!routes || !Array.isArray(routes)) { - throw new Error('Either routes is not defined or it is not an Array'); - } - - if (!middleware || !Array.isArray(middleware)) { - throw new Error('Either middleware is not defined or it is not an Array'); - } - const app = express(); - const validHTTPMethods = ['get', 'delete', 'post', 'put', 'patch']; app.use(cors()); app.use(morgan('dev')); @@ -29,27 +19,6 @@ function expressApp({ routes, middleware }) { app.use(bodyParser.json()); for (const { path, handler, method } of routes) { - if (!validHTTPMethods.includes(method)) { - const errorMsg = `${method} is not a valid HTTP method \n Allowed methods are ${validHTTPMethods.join( - ' - ' - )}`; - throw new Error(errorMsg); - } - - if (typeof handler !== 'function') { - throw new Error( - `handler must be a function. Actual type is "${typeof handler}"` - ); - } - - if (typeof path !== 'string') { - throw new Error(`path must be a string. Actual type is "${typeof path}"`); - } else if (path.length === 0) { - throw new Error('path can not be an empty string'); - } else if (!path.startsWith('/')) { - throw new Error('path has to start with "/"'); - } - // Register all the handlers app[method](path, handler); } @@ -59,12 +28,6 @@ function expressApp({ routes, middleware }) { }); for (const handler of middleware) { - if (typeof handler !== 'function') { - throw new Error( - `handler must be a function. Actual type is "${typeof handler}"` - ); - } - // Register all the middleware app.use(handler); } diff --git a/src/server.mjs b/src/server.mjs index 49bd866..224974c 100644 --- a/src/server.mjs +++ b/src/server.mjs @@ -1,4 +1,12 @@ import { expressApp } from './app.mjs'; +import utils from './utils/index.mjs'; +import { + ValidatorChain, + PortValidator, + OptionsValidator, + RoutesValidator, + MiddlewareValidator, +} from './validator/index.mjs'; /** * @preserve @@ -6,13 +14,38 @@ import { expressApp } from './app.mjs'; * * @returns {import("http").Server} */ - -function httpServer({ port, routes, middleware }) { +function httpServer({ + port, + routes = [], + middleware = [], + options = { useCors: true }, +} = {}) { + const NODE_ENV = process.env.NODE || 'PROD'; const PORT = port || 3000; - const server = expressApp({ routes, middleware }); + const validation = new ValidatorChain() + .add(new PortValidator()) + .add(new OptionsValidator()) + .add(new RoutesValidator()) + .add(new MiddlewareValidator()) + .startValidation() + .validate({ port: PORT, routes, middleware, options }); + + if (NODE_ENV !== 'test' && !validation.isValid) { + console.error(validation.errorMsg); + + process.exit(1); + } + + const server = expressApp({ routes, middleware, options }); + const successMsg = utils.msgBuilder({ + port: PORT, + routes, + middleware, + options, + }); return server.listen(PORT, () => { - console.log(`App running on: ${PORT}`); + console.log(`\x1b[32m${successMsg}\x1b[0m`); }); } diff --git a/src/utils/index.mjs b/src/utils/index.mjs new file mode 100644 index 0000000..6a111b6 --- /dev/null +++ b/src/utils/index.mjs @@ -0,0 +1,24 @@ +/** + * + * @param {import("../../utils/types.mjs").Params} params + */ +function msgBuilder(params) { + const entries = Object.entries(params); + let msg = 'Your app is running with the following settings:'; + + for (const [key, value] of entries) { + let stringifiedValue = JSON.stringify(value); + + if (key === 'middleware' && Array.isArray(value)) { + stringifiedValue = `${value.length} registered`; + } + + msg += `\n${key.toUpperCase()}: ${stringifiedValue}`; + } + + return msg; +} + +export default { + msgBuilder, +}; diff --git a/src/validator/routesValidator.mjs b/src/validator/routesValidator.mjs index 2ef8c0c..d896929 100644 --- a/src/validator/routesValidator.mjs +++ b/src/validator/routesValidator.mjs @@ -1,13 +1,14 @@ import { Validator, utils } from './index.mjs'; export class RoutesValidator extends Validator { + static routeProps = ['method', 'path', 'handler']; /** - * @param {import("../../utils/types.mjs").Route} route + * @param {string[]} routeProps * @returns {boolean} */ - validateRoutesProps(route) { - return ['method', 'path', 'handler'] - .map(prop => Object.keys(route).includes(prop)) + validateRoutesProps(routeProps) { + return RoutesValidator.routeProps + .map(prop => routeProps.includes(prop)) .every(Boolean); } @@ -19,8 +20,13 @@ export class RoutesValidator extends Validator { return ['delete', 'get', 'patch', 'post', 'put'].includes(operation); } + /** + * + * @param {string} path + * @returns + */ validateRoutePath(path) { - return `${path}`.startsWith('/'); + return path.length > 0 && `${path}`.startsWith('/'); } /** @@ -43,12 +49,15 @@ export class RoutesValidator extends Validator { */ for (const route of routes) { - const hasInvalidProps = !this.validateRoutesProps(route); + const routeProps = Object.keys(route); + const hasInvalidProps = !this.validateRoutesProps(routeProps); if (hasInvalidProps) { return { isValid: false, - errorMsg: '"routes" items have invalid props', + errorMsg: `"routes" item have invalid or missing props.\nReceived: ${routeProps.join( + '-' + )}\nValid: ${RoutesValidator.routeProps.join('-')}`, }; } From 173480a32f7711b00559f94ecd41470f4ac62608 Mon Sep 17 00:00:00 2001 From: byverdu Date: Wed, 21 Jun 2023 14:27:44 +0100 Subject: [PATCH 7/7] removed usage of NODE env --- __tests__/server.spec.mjs | 4 +--- jest/setupTests.js | 1 + package.json | 4 ++-- src/server.mjs | 3 +-- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/__tests__/server.spec.mjs b/__tests__/server.spec.mjs index d4b2857..f854df8 100644 --- a/__tests__/server.spec.mjs +++ b/__tests__/server.spec.mjs @@ -70,10 +70,8 @@ describe('Server', () => { }); it('should stop the app if the Validator fails', () => { + // process.exit it's been spied on jest/setupTests.js jest.spyOn(console, 'error'); - jest.spyOn(process, 'exit').mockImplementation(error => error); - - process.env.NODE = 'dev'; const server = httpServer({ port: '6969', diff --git a/jest/setupTests.js b/jest/setupTests.js index f6803f7..a65da9d 100644 --- a/jest/setupTests.js +++ b/jest/setupTests.js @@ -1,3 +1,4 @@ import { jest } from '@jest/globals'; console.error = jest.fn(); +jest.spyOn(process, 'exit').mockImplementation(error => error); diff --git a/package.json b/package.json index 84a5c69..588c4e6 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "scripts": { "start": "node ./src/server.mjs", "dev": "nodemon ./src/server.mjs", - "test:dev": "NODE=test node --experimental-vm-modules node_modules/jest/bin/jest.js --watch --detectOpenHandles", - "test": "NODE=test node --experimental-vm-modules node_modules/jest/bin/jest.js --collect-coverage --detectOpenHandles --forceExit", + "test:dev": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch --detectOpenHandles", + "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --collect-coverage --detectOpenHandles --forceExit", "test:clear:cache": "jest --clearCache", "lint": "eslint src/**/*.mjs utils/*mjs --fix", "build": "node utils/minify.mjs", diff --git a/src/server.mjs b/src/server.mjs index 224974c..6d38641 100644 --- a/src/server.mjs +++ b/src/server.mjs @@ -20,7 +20,6 @@ function httpServer({ middleware = [], options = { useCors: true }, } = {}) { - const NODE_ENV = process.env.NODE || 'PROD'; const PORT = port || 3000; const validation = new ValidatorChain() .add(new PortValidator()) @@ -30,7 +29,7 @@ function httpServer({ .startValidation() .validate({ port: PORT, routes, middleware, options }); - if (NODE_ENV !== 'test' && !validation.isValid) { + if (!validation.isValid) { console.error(validation.errorMsg); process.exit(1);