├── .coveralls.yml
├── .npmignore
├── logo.png
├── .husky
├── pre-push
└── pre-commit
├── example
├── src
│ ├── expressive.js
│ ├── index.ts
│ ├── middlewares
│ │ ├── user-middleware.ts
│ │ └── error.ts
│ ├── controllers
│ │ ├── health.ts
│ │ └── users
│ │ │ ├── index.ts
│ │ │ ├── create-user.ts
│ │ │ └── get-user.ts
│ ├── router.ts
│ └── app.ts
├── package.json
└── tsconfig.json
├── .gitignore
├── src
├── middleware
│ ├── response.js
│ ├── requestId.js
│ └── MiddlewareManager.js
├── subroute.js
├── Utils.js
├── Route.js
├── redoc
│ ├── registerRedoc.js
│ └── redocHtmlTemplate.js
├── index.js
├── SwaggerBuilder.js
├── AuthUtil.js
├── BaseController.js
├── RouteUtil.js
├── ExpressApp.js
├── RouterFactory.js
├── index.d.ts
├── SwaggerUtils.js
└── CelebrateUtils.js
├── .prettierrc.json
├── .codebeatignore
├── .codeclimate.yml
├── .travis.yml
├── .vscode
├── tasks.json
└── launch.json
├── __tests__
├── index.test.js
├── app.test.js
├── Models.test.js
├── redoc.test.js
├── ExpressApp.test.js
├── SwaggerBuilder.test.js
├── AuthUtil.test.js
├── RouteUtil.test.js
├── BaseController.test.js
├── RouterFactory.test.js
├── CelebrateUtils.test.js
├── SwaggerUtils.test.js
└── Middleware.test.js
├── .github
└── workflows
│ └── nodejs.yml
├── .eslintrc.js
├── LICENSE
├── README.md
├── package.json
└── CODE_OF_CONDUCT.md
/.coveralls.yml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /src/
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/siddiqus/expressive/HEAD/logo.png
--------------------------------------------------------------------------------
/.husky/pre-push:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | yarn test
5 |
--------------------------------------------------------------------------------
/example/src/expressive.js:
--------------------------------------------------------------------------------
1 | const expressive = require('../../src');
2 | module.exports = expressive;
3 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | yarn test && npx lint-staged
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .sonarlint
3 | dist
4 | coverage
5 | .coverage
6 | reports
7 | build
8 | .eslintcache
9 | .stryker-tmp
--------------------------------------------------------------------------------
/src/middleware/response.js:
--------------------------------------------------------------------------------
1 | module.exports = (req, res, next) => {
2 | res.setHeader('Content-Type', 'application/json');
3 | next();
4 | };
5 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "endOfLine": "lf",
3 | "semi": true,
4 | "singleQuote": true,
5 | "tabWidth": 2,
6 | "trailingComma": "none"
7 | }
8 |
--------------------------------------------------------------------------------
/.codebeatignore:
--------------------------------------------------------------------------------
1 | example/
2 | ts-example/
3 | node_modules
4 | .sonarlint
5 | dist
6 | coverage
7 | .coverage
8 | reports
9 | build
10 | __tests__/
--------------------------------------------------------------------------------
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | exclude_patterns:
2 | - "__tests__/"
3 | - "build/"
4 | - "dist/"
5 | - "node_modules/"
6 | - "reports/"
7 | - "example/"
8 | - "ts-example/"
9 |
--------------------------------------------------------------------------------
/example/src/index.ts:
--------------------------------------------------------------------------------
1 | import app from "./app";
2 |
3 | const port = Number(process.env.PORT || 8080);
4 |
5 | app.listen(port, () => console.log('Listening on port ' + port));
6 | module.exports = app;
7 |
--------------------------------------------------------------------------------
/example/src/middlewares/user-middleware.ts:
--------------------------------------------------------------------------------
1 | import { Handler } from "../../../src";
2 |
3 | export const someMiddleware: Handler = async (req) => {
4 | console.log(`some user middleware`, req.user)
5 | }
--------------------------------------------------------------------------------
/example/src/controllers/health.ts:
--------------------------------------------------------------------------------
1 | import { BaseController } from "../../../src";
2 |
3 | export class HealthController extends BaseController {
4 | async handleRequest() {
5 | this.ok({
6 | healthy: true
7 | })
8 | }
9 | }
--------------------------------------------------------------------------------
/example/src/middlewares/error.ts:
--------------------------------------------------------------------------------
1 | import { ErrorRequestHandler } from "../../../src";
2 |
3 | export const errorHandler: ErrorRequestHandler = (err, _req, res, next) => {
4 | res.status(500).json({
5 | message: err.message
6 | });
7 |
8 | next()
9 | }
--------------------------------------------------------------------------------
/src/subroute.js:
--------------------------------------------------------------------------------
1 | module.exports = (
2 | path,
3 | router,
4 | { authorizer, middleware, validationSchema } = {}
5 | ) => {
6 | return {
7 | path,
8 | router,
9 | authorizer,
10 | validationSchema,
11 | middleware: middleware || []
12 | };
13 | };
14 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | git:
4 | depth: 1
5 |
6 | node_js:
7 | - "12.22.7"
8 |
9 | cache:
10 | directories:
11 | - node_modules
12 |
13 | jobs:
14 | include:
15 | - stage: Produce Coverage
16 | node_js: node
17 | script: npm run coveralls
18 |
--------------------------------------------------------------------------------
/src/middleware/requestId.js:
--------------------------------------------------------------------------------
1 | const uuid = require('uuid').v4;
2 | const headerName = 'X-Request-Id';
3 |
4 | module.exports = function requestId() {
5 | return (req, res, next) => {
6 | const oldValue = req.get(headerName);
7 | const id = oldValue || uuid();
8 |
9 | res.set(headerName, id);
10 |
11 | req.id = id;
12 |
13 | next();
14 | };
15 | };
16 |
--------------------------------------------------------------------------------
/example/src/router.ts:
--------------------------------------------------------------------------------
1 | import { ExpressiveRouter, Route, subroute } from "../../src";
2 | import { HealthController } from "./controllers/health";
3 | import { userRouter } from "./controllers/users";
4 |
5 | export const router: ExpressiveRouter = {
6 | routes: [
7 | Route.get('/v0/health', new HealthController()),
8 | ],
9 | subroutes: [
10 | subroute('/v1/users', userRouter)
11 | ]
12 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=733558
3 | // for the documentation about the tasks.json format
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "label": "build-ts-example",
8 | "type": "typescript",
9 | "tsconfig": "example/tsconfig.json",
10 | "problemMatcher": [
11 | "$tsc"
12 | ],
13 | "group": "build"
14 | }
15 | ]
16 | }
--------------------------------------------------------------------------------
/example/src/controllers/users/index.ts:
--------------------------------------------------------------------------------
1 | import { ExpressiveRouter, Route } from "../../../../src";
2 | import { CreateUserController } from "./create-user";
3 | import { GetUserController, GetUsersController } from "./get-user";
4 |
5 | // /users
6 | export const userRouter: ExpressiveRouter = {
7 | routes: [
8 | Route.get('/', new GetUsersController()),
9 | Route.get('/:userId', new GetUserController()),
10 | Route.post('/', new CreateUserController())
11 | ]
12 | }
--------------------------------------------------------------------------------
/src/Utils.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | normalizePathSlashes(path) {
3 | let normalizedPath = path;
4 | if (path && path.charAt(path.length - 1) !== '/') {
5 | normalizedPath = `${path}/`;
6 | }
7 | normalizedPath = normalizedPath.replace(/\/\//g, '/');
8 |
9 | return normalizedPath;
10 | },
11 | clearNullValuesInObject(obj) {
12 | Object.keys(obj).forEach(
13 | (key) => (obj[key] === undefined || obj[key] === null) && delete obj[key]
14 | );
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/__tests__/index.test.js:
--------------------------------------------------------------------------------
1 | const expressive = require('../src/index');
2 |
3 | describe('module index', () => {
4 | it('should have ExpressApp', () => {
5 | expect(expressive.ExpressApp).toBeDefined();
6 | });
7 | it('should have SwaggerUtils', () => {
8 | expect(expressive.SwaggerUtils).toBeDefined();
9 | });
10 | it('should have Route', () => {
11 | expect(expressive.Route).toBeDefined();
12 | });
13 | it('should have subroute', () => {
14 | expect(expressive.subroute).toBeDefined();
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ts-example",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "build": "tsc -p .",
8 | "start": "NODE_ENV=development nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/index.ts"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "devDependencies": {
14 | "@types/multer": "1.4.3",
15 | "@types/node": "13.7.4",
16 | "nodemon": "2.0.15",
17 | "ts-node": "8.8.2",
18 | "typescript": "3.8.2"
19 | },
20 | "dependencies": {
21 | "multer": "1.4.2"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/.github/workflows/nodejs.yml:
--------------------------------------------------------------------------------
1 | name: Node.js CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | - feature/*
8 | pull_request:
9 | branches:
10 | - master
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | matrix:
19 | node-version: [10.x, 12.x]
20 |
21 | steps:
22 | - uses: actions/checkout@v2
23 | - name: Use Node.js ${{ matrix.node-version }}
24 | uses: actions/setup-node@v1
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 | - run: npm install
28 | - run: npm test
29 | env:
30 | CI: true
31 |
--------------------------------------------------------------------------------
/example/src/controllers/users/create-user.ts:
--------------------------------------------------------------------------------
1 | // import Joi from "@hapi/joi";
2 | import { BaseController, Handler, Joi, ValidationSchema } from "../../../../src";
3 | import { someMiddleware } from "../../middlewares/user-middleware";
4 |
5 | export class CreateUserController extends BaseController {
6 | middleware?: Handler[] | undefined = [someMiddleware]
7 |
8 | validationSchema?: ValidationSchema = {
9 | body: {
10 | firstName: Joi.string().required(),
11 | lastName: Joi.string().required()
12 | }
13 | }
14 |
15 | async handleRequest() {
16 | const { firstName, lastName } = this.getData().body;
17 |
18 | this.ok({
19 | hello: `Hello ${firstName} ${lastName}!`
20 | });
21 | }
22 | }
--------------------------------------------------------------------------------
/src/Route.js:
--------------------------------------------------------------------------------
1 | function getRouteFn(method) {
2 | return (...args) => {
3 | const [path] = args;
4 |
5 | if (args.length === 2 && args[1].__proto__.constructor.name === 'Object') {
6 | // 2nd is not object
7 | return {
8 | method,
9 | path,
10 | ...args[1]
11 | };
12 | }
13 |
14 | const [, controller, params = {}] = args;
15 | return {
16 | method,
17 | path,
18 | controller,
19 | ...params
20 | };
21 | };
22 | }
23 |
24 | module.exports = {
25 | get: getRouteFn('get'),
26 | post: getRouteFn('post'),
27 | put: getRouteFn('put'),
28 | delete: getRouteFn('delete'),
29 | patch: getRouteFn('patch'),
30 | head: getRouteFn('head'),
31 | options: getRouteFn('options')
32 | };
33 |
--------------------------------------------------------------------------------
/src/redoc/registerRedoc.js:
--------------------------------------------------------------------------------
1 | const basicAuth = require('express-basic-auth');
2 | const redocHtml = require('./redocHtmlTemplate');
3 |
4 | module.exports = function registerRedoc({ app, swaggerJson, url, authUser }) {
5 | const specUrl = '/docs/swagger.json';
6 |
7 | const { user, password } = authUser;
8 | const basicAuthMiddleware = basicAuth({
9 | challenge: true,
10 | users: { [user]: password }
11 | });
12 |
13 | app.get(specUrl, basicAuthMiddleware, (_, res) => {
14 | res.status(200).send(swaggerJson);
15 | });
16 |
17 | app.get(url, basicAuthMiddleware, (_, res) => {
18 | res.type('html');
19 | res.status(200).send(
20 | redocHtml({
21 | title: 'ReDoc',
22 | specUrl
23 | })
24 | );
25 | });
26 | };
27 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const { Joi, isCelebrateError: isValidationError } = require('celebrate');
3 | const celebrate = require('celebrate');
4 | const ExpressApp = require('./ExpressApp');
5 | const RouteUtil = require('./RouteUtil');
6 | const SwaggerUtils = require('./SwaggerUtils');
7 | const Route = require('./Route');
8 | const subroute = require('./subroute');
9 | const BaseController = require('./BaseController');
10 | const SwaggerBuilder = require('./SwaggerBuilder');
11 |
12 | const Expressive = {
13 | ExpressApp,
14 | RouteUtil,
15 | Route,
16 | subroute,
17 | BaseController,
18 | SwaggerUtils,
19 | SwaggerBuilder,
20 | express,
21 | Joi,
22 | isValidationError,
23 | celebrate
24 | };
25 |
26 | module.exports = Expressive;
27 |
--------------------------------------------------------------------------------
/example/src/controllers/users/get-user.ts:
--------------------------------------------------------------------------------
1 | import { BaseController, ValidationSchema, Joi } from "../../../../src";
2 |
3 | export class GetUserController extends BaseController {
4 | validationSchema?: ValidationSchema = {
5 | params: {
6 | userId: Joi.number().positive().required()
7 | },
8 | options: {
9 | allowUnknown: true
10 | }
11 | }
12 | async handleRequest() {
13 | const { userId } = this.getData().params;
14 |
15 | this.ok({
16 | id: userId,
17 | name: 'Sabbir'
18 | })
19 | }
20 | }
21 |
22 | export class GetUsersController extends BaseController {
23 | async handleRequest() {
24 | this.ok([
25 | {
26 | id: 1,
27 | name: 'Sabbir'
28 | },
29 | {
30 | id: 2,
31 | name: 'John'
32 | }
33 | ])
34 | }
35 | }
--------------------------------------------------------------------------------
/example/src/app.ts:
--------------------------------------------------------------------------------
1 | import { ExpressApp } from "../../src";
2 | import { errorHandler } from "./middlewares/error";
3 | import { router } from "./router";
4 |
5 | const swaggerInfo = {
6 | version: '2.0.0',
7 | title: 'Example Expressive App',
8 | contact: {
9 | name: 'Sabbir Siddiqui',
10 | email: 'sabbir.m.siddiqui@gmail.com'
11 | }
12 | };
13 |
14 | // todo -> add validation schema cascade
15 | // const rootValidationSchema: ValidationSchema = {
16 | // query: {
17 | // q: Joi.string().required().valid('emnei')
18 | // }
19 | // }
20 |
21 | export default new ExpressApp(router, {
22 | basePath: '/',
23 | allowCors: true,
24 | swaggerInfo,
25 | errorHandler,
26 | authorizer: (req, res) => {
27 | console.log(`${req.url}: auth from top`);
28 | res.setHeader('testingAuth', 1234);
29 | },
30 | requestLoggerOptions: {
31 | disabled: false
32 | }
33 | }).express;
--------------------------------------------------------------------------------
/src/redoc/redocHtmlTemplate.js:
--------------------------------------------------------------------------------
1 | const html = `
2 |
3 |
4 | [[title]]
5 |
6 |
7 |
10 |
16 |
17 |
18 |
19 |
20 |
21 | `;
22 |
23 | function redocHtml(
24 | options = {
25 | title: 'ReDoc',
26 | specUrl: 'http://petstore.swagger.io/v2/swagger.json'
27 | }
28 | ) {
29 | const { title, specUrl } = options;
30 | return html.replace('[[title]]', title).replace('[[spec-url]]', specUrl);
31 | }
32 |
33 | module.exports = redocHtml;
34 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es6: true
5 | },
6 | extends: ['google', 'plugin:prettier/recommended'],
7 | globals: {
8 | Atomics: 'readonly',
9 | SharedArrayBuffer: 'readonly'
10 | },
11 | parserOptions: {
12 | ecmaFeatures: {
13 | jsx: true
14 | },
15 | ecmaVersion: 2018,
16 | sourceType: 'module'
17 | },
18 | rules: {
19 | 'linebreak-style': [
20 | 'error',
21 | process.platform === 'win32' ? 'windows' : 'unix'
22 | ],
23 | 'max-len': [1, 100, 4],
24 | 'no-underscore-dangle': 0,
25 | 'no-param-reassign': ['error', { props: false }],
26 | 'prefer-destructuring': 'warn',
27 | 'global-require': 'warn',
28 | // "import/no-dynamic-require": "warn",
29 | 'no-plusplus': 'warn',
30 | indent: ['error', 2],
31 | 'comma-dangle': 0,
32 | 'prefer-rest-params': 0,
33 | quotes: ['error', 'single', { allowTemplateLiterals: true }],
34 | 'class-methods-use-this': 0,
35 | 'require-jsdoc': 0,
36 | 'object-curly-spacing': 0,
37 | 'prettier/prettier': 'error'
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Sabbir Siddiqui
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/siddiqus/expressive/)
2 |
3 | [](https://coveralls.io/github/siddiqus/expressive?branch=master)
4 | [](https://codeclimate.com/github/siddiqus/expressive/maintainability)
5 | [](https://codebeat.co/projects/github-com-siddiqus-expressive-master)
6 | [](https://github.com/facebook/jest)
7 | [](https://github.com/google/eslint-config-google)
8 |
9 | # Expressive
10 |
11 | Fast, opinionated, minimalist, and conventional REST API framework for [node](http://nodejs.org).
12 | (With Typescript Support :star:)
13 |
14 | # Introduction
15 |
16 | **Expressive** is a NodeJS REST API framework built on ExpressJs and best practices for smooth development.
17 |
18 | Please visit the [wiki page](https://github.com/siddiqus/expressive/wiki) for the full documentation.
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "name": "vscode-jest-tests",
10 | "request": "launch",
11 | "args": [
12 | "--runInBand"
13 | ],
14 | "cwd": "${workspaceFolder}",
15 | "console": "integratedTerminal",
16 | "internalConsoleOptions": "neverOpen",
17 | "disableOptimisticBPs": true,
18 | "program": "${workspaceFolder}/node_modules/jest/bin/jest"
19 | },
20 | {
21 | "type": "node",
22 | "request": "launch",
23 | "name": "ts-example",
24 | "program": "${workspaceFolder}/example/src/index.ts",
25 | "preLaunchTask": "build-ts-example",
26 | "outFiles": [
27 | "${workspaceFolder}/example/dist/**/*.js"
28 | ],
29 | "cwd": "${workspaceFolder}/example/",
30 | "env": {
31 | "NODE_ENV": "development"
32 | }
33 | }
34 | ]
35 | }
--------------------------------------------------------------------------------
/__tests__/app.test.js:
--------------------------------------------------------------------------------
1 | const request = require('supertest');
2 | const { ExpressApp, BaseController, Route } = require('../src');
3 |
4 | describe('Express App Tests', () => {
5 | class GetHello extends BaseController {
6 | handleRequest() {
7 | this.ok({
8 | hello: 'world'
9 | });
10 | }
11 | }
12 | class PostHello extends BaseController {
13 | handleRequest() {
14 | const { body } = this.getData();
15 | const { firstName, lastName } = body;
16 | this.ok({
17 | message: `hello ${firstName} ${lastName}!`
18 | });
19 | }
20 | }
21 | const router = {
22 | routes: [
23 | Route.get('/v1/hello', new GetHello()),
24 | Route.post('/v1/hello', new PostHello())
25 | ]
26 | };
27 | const app = new ExpressApp(router).express;
28 |
29 | it('should run healthcheck properly', async () => {
30 | const response = await request(app).get('/v1/hello');
31 | expect(response.statusCode).toBe(200);
32 | expect(response.body).toStrictEqual({
33 | hello: 'world'
34 | });
35 | });
36 |
37 | it('should send post data properly', async () => {
38 | const response = await request(app).post('/v1/hello').send({
39 | firstName: 'Sabbir',
40 | lastName: 'Siddiqui'
41 | });
42 | expect(response.statusCode).toBe(200);
43 | expect(response.body).toStrictEqual({
44 | message: 'hello Sabbir Siddiqui!'
45 | });
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/src/SwaggerBuilder.js:
--------------------------------------------------------------------------------
1 | function _addParam(
2 | parameters,
3 | paramType,
4 | { name, description, type, required, schema }
5 | ) {
6 | parameters.push({
7 | in: paramType,
8 | name,
9 | description,
10 | type,
11 | required,
12 | schema
13 | });
14 | }
15 |
16 | module.exports = class SwaggerEndpoint {
17 | constructor(summary, description) {
18 | this.summary = summary;
19 | this.description = description;
20 | this.responses = {};
21 | this.parameters = [];
22 | }
23 |
24 | addResponse({ code, description, schema, produces = ['application/json'] }) {
25 | this.responses[code] = {
26 | produces,
27 | description,
28 | schema
29 | };
30 | return this;
31 | }
32 |
33 | addPathParam({ name, description, type, required, schema }) {
34 | _addParam(this.parameters, 'path', {
35 | name,
36 | description,
37 | schema,
38 | required,
39 | type
40 | });
41 | return this;
42 | }
43 |
44 | addRequestBody({ description, required, schema, name = 'body' }) {
45 | _addParam(this.parameters, 'body', {
46 | name,
47 | description,
48 | schema,
49 | required
50 | });
51 | return this;
52 | }
53 |
54 | addQueryParam({ name, description, required, schema }) {
55 | _addParam(this.parameters, 'query', {
56 | name,
57 | description,
58 | schema,
59 | required
60 | });
61 | return this;
62 | }
63 | };
64 |
--------------------------------------------------------------------------------
/src/AuthUtil.js:
--------------------------------------------------------------------------------
1 | const RouteUtil = require('./RouteUtil');
2 |
3 | function _getUniqueArray(arr) {
4 | const strArray = arr.map((a) => a.toString());
5 | const uniqueStrArray = strArray.filter((value, index, self) => {
6 | return self.indexOf(value) === index;
7 | });
8 | const indicesArray = uniqueStrArray.map((u) => strArray.indexOf(u));
9 | return indicesArray.map((i) => arr[i]);
10 | }
11 |
12 | module.exports = class AuthUtil {
13 | constructor() {
14 | this.routeUtil = RouteUtil;
15 | }
16 |
17 | getMiddlewareForInjectingAuthProperties(authObject) {
18 | return (req) => {
19 | let { authorizer } = req;
20 | if (!authorizer) authorizer = [];
21 |
22 | authorizer.push(authObject);
23 | // eslint-disable-next-line prefer-spread
24 | authorizer = [].concat.apply([], authorizer);
25 |
26 | authorizer = _getUniqueArray(authorizer); // Ramda.uniqWith(Ramda.eqProps, authorizer);
27 | req.authorizer = authorizer;
28 | };
29 | }
30 |
31 | getAuthorizerMiddleware(authorizer, authObjectHandler) {
32 | if (!authorizer) return null;
33 |
34 | const isObject = ['object', 'string'].includes(typeof authorizer);
35 |
36 | if (isObject && !authObjectHandler) {
37 | throw new Error(
38 | `'authorizer' object declared, but 'authObjectHandler' ` +
39 | `is not defined in ExpressApp constructor params`
40 | );
41 | }
42 |
43 | let handlers = [];
44 | if (isObject) {
45 | handlers = [
46 | this.getMiddlewareForInjectingAuthProperties(authorizer),
47 | authObjectHandler
48 | ];
49 | } else {
50 | handlers = [authorizer];
51 | }
52 |
53 | return handlers.map((h) => this.routeUtil.getHandlerWithManagedNextCall(h));
54 | }
55 | };
56 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@siddiqus/expressive",
3 | "version": "4.0.17",
4 | "description": "Fast, opinionated, minimalist, and conventional REST API framework for NodeJS, built on ExpressJs.",
5 | "main": "src/index.js",
6 | "files": [
7 | "src"
8 | ],
9 | "scripts": {
10 | "clean": "rimraf dist",
11 | "test": "jest",
12 | "test:watch": "jest --watch",
13 | "coverage": "jest --coverage",
14 | "coveralls": "jest --coverage && cat reports/coverage/lcov.info | coveralls",
15 | "lint": "eslint src -f node_modules/eslint-detailed-reporter/lib/detailed.js -o reports/lint.html || echo Lint report: reports/lint.html",
16 | "lintFix": "eslint --fix src || echo Linting done!",
17 | "dev": "cross-env NODE_ENV=development nodemon -w example -w src --exec node example/index.js",
18 | "dev:ts": "cd ts-example && npm start",
19 | "test:report": "npm test -- --reporters default jest-stare --testResultsProcessor=jest-stare"
20 | },
21 | "author": "Sabbir Siddiqui ",
22 | "license": "MIT",
23 | "dependencies": {
24 | "@types/cors": "2.8.12",
25 | "@types/express": "4.17.13",
26 | "celebrate": "15.0.0",
27 | "cors": "2.8.5",
28 | "express": "4.17.3",
29 | "express-basic-auth": "1.2.1",
30 | "helmet": "5.0.2",
31 | "morgan-body": "2.6.6",
32 | "swagger-ui-express": "4.3.0",
33 | "uuid": "8.3.2"
34 | },
35 | "jest-stare": {
36 | "resultDir": "reports/jest-stare",
37 | "coverageLink": "../jest/lcov-report/index.html"
38 | },
39 | "jest": {
40 | "coverageDirectory": "reports/coverage",
41 | "collectCoverage": true,
42 | "collectCoverageFrom": [
43 | "src/**/*.{js,jsx}"
44 | ],
45 | "modulePathIgnorePatterns": [
46 | ".stryker-tmp"
47 | ]
48 | },
49 | "devDependencies": {
50 | "@types/cors": "2.8.12",
51 | "@types/express": "4.17.13",
52 | "@types/jest": "25.1.4",
53 | "coveralls": "3.1.1",
54 | "cross-env": "5.2.0",
55 | "eslint": "8.10.0",
56 | "eslint-config-google": "0.12.0",
57 | "eslint-config-prettier": "6.10.1",
58 | "eslint-detailed-reporter": "0.8.0",
59 | "eslint-plugin-prettier": "3.1.2",
60 | "husky": "7.0.4",
61 | "jest": "27.4.5",
62 | "lint-staged": "10.1.3",
63 | "nodemon": "2.0.15",
64 | "prettier": "2.0.4",
65 | "supertest": "6.1.6"
66 | },
67 | "homepage": "https://github.com/siddiqus/expressive#readme",
68 | "bugs": {
69 | "url": "https://github.com/siddiqus/expressive/issues"
70 | },
71 | "repository": {
72 | "type": "git",
73 | "url": "git+https://github.com/siddiqus/expressive.git"
74 | },
75 | "keywords": [
76 | "express",
77 | "framework",
78 | "nodejs",
79 | "server",
80 | "javascript",
81 | "rest api",
82 | "convention based routing"
83 | ],
84 | "publishConfig": {
85 | "registry": "https://registry.npmjs.org/"
86 | },
87 | "types": "./src/index.d.ts",
88 | "lint-staged": {
89 | "*.js": "eslint --cache --fix"
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/BaseController.js:
--------------------------------------------------------------------------------
1 | function _sendJsonResponseWithMessage(res, code, message) {
2 | return _sendJsonResponse(res, code, { message });
3 | }
4 |
5 | function _sendJsonResponse(res, code, data) {
6 | const wrappedData = BaseController.responseMapper(data);
7 | if (!!data) {
8 | res.status(code);
9 | res.json(wrappedData);
10 | } else {
11 | res.sendStatus(code);
12 | }
13 | }
14 |
15 | class BaseController {
16 | static responseMapper(data) {
17 | return data;
18 | }
19 |
20 | static requestMapper(req) {
21 | return req;
22 | }
23 |
24 | constructor() {
25 | this.req = null;
26 | this.res = null;
27 | this.next = null;
28 |
29 | this.validationSchema = null;
30 | this.authorizer = null;
31 | this.middleware = null;
32 | this.doc = null;
33 | this.pre = null;
34 |
35 | this.resolvedBy = null;
36 | }
37 |
38 | async handleRequest() {
39 | throw new Error(
40 | `'handleRequest' not implemented in ${this.constructor.name}`
41 | );
42 | }
43 |
44 | getData() {
45 | const { body, query, params, fileUpload, user } = this.req;
46 | return {
47 | body,
48 | query,
49 | params,
50 | fileUpload,
51 | user
52 | };
53 | }
54 |
55 | getHeaders() {
56 | return this.req.headers;
57 | }
58 |
59 | getCookies() {
60 | return {
61 | cookies: this.req.cookies,
62 | signedCookies: this.req.signedCookies
63 | };
64 | }
65 |
66 | ok(dto = null) {
67 | this.resolvedBy = 'ok';
68 | return _sendJsonResponse(this.res, 200, dto);
69 | }
70 |
71 | created(data = null) {
72 | this.resolvedBy = 'created';
73 | return _sendJsonResponse(this.res, 201, data);
74 | }
75 |
76 | accepted(data = null) {
77 | this.resolvedBy = 'accepted';
78 | return _sendJsonResponse(this.res, 202, data);
79 | }
80 |
81 | noContent() {
82 | this.resolvedBy = 'noContent';
83 | return _sendJsonResponse(this.res, 204);
84 | }
85 |
86 | badRequest(message = 'Bad request') {
87 | this.resolvedBy = 'badRequest';
88 | return _sendJsonResponseWithMessage(this.res, 400, message);
89 | }
90 |
91 | unauthorized(message = 'Unauthorized') {
92 | this.resolvedBy = 'unauthorized';
93 | return _sendJsonResponseWithMessage(this.res, 401, message);
94 | }
95 |
96 | forbidden(message = 'Forbidden') {
97 | this.resolvedBy = 'forbidden';
98 | return _sendJsonResponseWithMessage(this.res, 403, message);
99 | }
100 |
101 | notFound(message = 'Not Found') {
102 | this.resolvedBy = 'notFound';
103 | return _sendJsonResponseWithMessage(this.res, 404, message);
104 | }
105 |
106 | tooMany(message = 'Too many requests') {
107 | this.resolvedBy = 'tooMany';
108 | return _sendJsonResponseWithMessage(this.res, 429, message);
109 | }
110 |
111 | internalServerError(message = 'Internal server error', body = {}) {
112 | this.resolvedBy = 'internalServerError';
113 | return _sendJsonResponse(this.res, 500, {
114 | message,
115 | ...body
116 | });
117 | }
118 |
119 | notImplemented(message = 'Not implemented') {
120 | this.resolvedBy = 'notImplemented';
121 | return _sendJsonResponseWithMessage(this.res, 501, message);
122 | }
123 | }
124 |
125 | module.exports = BaseController;
126 |
--------------------------------------------------------------------------------
/src/RouteUtil.js:
--------------------------------------------------------------------------------
1 | const Utils = require('./Utils');
2 | // const FUNCTION_STRING = 'function';
3 | // const CLASS_STRING = 'class';
4 | // const FUNCTION_STRING_LENGTH = FUNCTION_STRING.length;
5 |
6 | function _addRouteToPaths(paths, parentPath, route) {
7 | const routeData = {
8 | ...route,
9 | path: Utils.normalizePathSlashes(`${parentPath}${route.path}`),
10 | parentPath,
11 | validationSchema: route.controller.validationSchema,
12 | doc: route.controller.doc,
13 | authorizer: route.controller.authorizer,
14 | controllerName: route.controller.constructor.name
15 | };
16 |
17 | delete routeData.controller;
18 |
19 | Utils.clearNullValuesInObject(routeData);
20 |
21 | paths.push(routeData);
22 | }
23 |
24 | function _getSubroutes(paths, parentPath, subroute) {
25 | return RouteUtil.getRoutesInfo(
26 | subroute.router,
27 | paths,
28 | parentPath + subroute.path
29 | );
30 | }
31 |
32 | class RouteUtil {
33 | static getRoutesInfo(router, paths = [], parentPath = '') {
34 | if (!router) {
35 | return paths;
36 | }
37 | if (router.routes) {
38 | router.routes.forEach(({ path, method, controller }) =>
39 | _addRouteToPaths(paths, parentPath, {
40 | path,
41 | method,
42 | controller
43 | })
44 | );
45 | }
46 |
47 | if (router.subroutes) {
48 | router.subroutes.forEach((subroute) =>
49 | _getSubroutes(paths, parentPath, subroute)
50 | );
51 | }
52 | return paths;
53 | }
54 |
55 | static getHandlerWithManagedNextCall(handler) {
56 | return async (req, res, next) => {
57 | try {
58 | await handler(req, res, next);
59 |
60 | if (handler.length !== 3) {
61 | return next();
62 | }
63 | } catch (error) {
64 | return next(error);
65 | }
66 | };
67 | }
68 |
69 | static getErrorHandlerWithManagedNextCall(handler) {
70 | return async (err, req, res, next) => {
71 | try {
72 | await handler(err, req, res, next);
73 |
74 | if (handler.length !== 4) {
75 | return next();
76 | }
77 | } catch (error) {
78 | return next(error);
79 | }
80 | };
81 | }
82 |
83 | // static isFunction(functionToCheck) {
84 | // const stringPrefix = functionToCheck
85 | // .toString()
86 | // .substring(0, FUNCTION_STRING_LENGTH);
87 | // if (stringPrefix.includes(CLASS_STRING)) return false;
88 | // return (
89 | // functionToCheck instanceof Function || stringPrefix === FUNCTION_STRING
90 | // );
91 | // }
92 |
93 | static isUrlPath(string) {
94 | if (typeof string !== 'string') return false;
95 | const regex = /^(\/[a-zA-Z0-9\-:]+)*\/?$/g;
96 | return regex.test(string);
97 | }
98 |
99 | static getDuplicateUrls(expressiveRouter) {
100 | const routeList = RouteUtil.getRoutesInfo(expressiveRouter);
101 | const urlStrings = routeList.map(({ path, method }) => {
102 | const sanitizedPath = Utils.normalizePathSlashes(path);
103 | return `${method} ${sanitizedPath}`;
104 | });
105 |
106 | const duplicates = urlStrings.filter(
107 | (item, index) => urlStrings.indexOf(item) !== index
108 | );
109 | return duplicates;
110 | }
111 | }
112 |
113 | module.exports = RouteUtil;
114 |
--------------------------------------------------------------------------------
/src/ExpressApp.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const RouterFactory = require('./RouterFactory');
3 | const RouteUtil = require('./RouteUtil');
4 | const AuthUtil = require('./AuthUtil');
5 | const MiddlewareManager = require('./middleware/MiddlewareManager');
6 |
7 | // const expressStatusMonitor = require('express-status-monitor');
8 |
9 | module.exports = class ExpressApp {
10 | constructor(
11 | expressiveRouter,
12 | {
13 | basePath = '/',
14 | showSwaggerOnlyInDev = true,
15 | swaggerInfo = undefined,
16 | swaggerDefinitions = undefined,
17 | swaggerSecurityDefinitions = null,
18 | allowCors = false,
19 | corsConfig = null,
20 | middleware = null,
21 | authorizer = null,
22 | errorHandler = null,
23 | bodyLimit = '100kb',
24 | helmetOptions = null,
25 | celebrateErrorHandler = null,
26 | notFoundHandler = null,
27 | authObjectHandler = null,
28 | requestLoggerOptions = undefined
29 | // expressStatusMonitorConfig = {}
30 | } = {}
31 | ) {
32 | this.config = {
33 | swaggerInfo,
34 | showSwaggerOnlyInDev,
35 | swaggerDefinitions,
36 | swaggerSecurityDefinitions,
37 | basePath,
38 | allowCors,
39 | corsConfig,
40 | middleware,
41 | errorHandler,
42 | bodyLimit,
43 | helmetOptions,
44 | authorizer,
45 | celebrateErrorHandler,
46 | notFoundHandler,
47 | authObjectHandler,
48 | requestLoggerOptions
49 | // expressStatusMonitorConfig
50 | };
51 | this.expressiveRouter = expressiveRouter;
52 |
53 | this._init();
54 |
55 | this.registerHandlers();
56 | }
57 |
58 | _init() {
59 | this.routeUtil = RouteUtil;
60 | this.authUtil = new AuthUtil();
61 | this.routerFactory = new RouterFactory(this.config);
62 |
63 | this.express = express();
64 | this.listen = this.express.listen.bind(this.express);
65 | this.middlewareManager = new MiddlewareManager(this.config, this.express);
66 | }
67 |
68 | _registerRoutes() {
69 | const expressRouter = this.routerFactory.getExpressRouter(
70 | this.expressiveRouter
71 | );
72 | this.express.use(this.config.basePath, expressRouter);
73 | }
74 |
75 | _registerErrorHandlers() {
76 | if (!this.config.errorHandler) return;
77 |
78 | let errorHandler;
79 | if (Array.isArray(this.config.errorHandler)) {
80 | errorHandler = this.config.errorHandler.map((e) =>
81 | this.routeUtil.getErrorHandlerWithManagedNextCall(e)
82 | );
83 | } else {
84 | errorHandler = this.routeUtil.getErrorHandlerWithManagedNextCall(
85 | this.config.errorHandler
86 | );
87 | }
88 | this.express.use(errorHandler);
89 | }
90 |
91 | registerHandlers() {
92 | // this.express.use(
93 | // expressStatusMonitor(this.config.expressStatusMonitorConfig)
94 | // );
95 |
96 | this.middlewareManager.registerDocs(this.expressiveRouter);
97 |
98 | this.middlewareManager.configureCors();
99 |
100 | this.middlewareManager.registerBasicMiddleware();
101 |
102 | this.middlewareManager.registerAuth();
103 |
104 | this._registerRoutes();
105 |
106 | this.middlewareManager.registerNotFoundHandler();
107 |
108 | this.middlewareManager.registerCelebrateErrorMiddleware();
109 |
110 | this._registerErrorHandlers();
111 | }
112 | };
113 |
--------------------------------------------------------------------------------
/__tests__/Models.test.js:
--------------------------------------------------------------------------------
1 | const Route = require('../src/Route');
2 | const subroute = require('../src/subroute');
3 |
4 | function getRouteObj(method) {
5 | return Route[method]('/somepath', {
6 | controller: 'someController',
7 | validator: 'validatorFunction',
8 | doc: 'docJs'
9 | });
10 | }
11 |
12 | function testRouteObj(routeObject) {
13 | expect(routeObject.path).toEqual('/somepath');
14 | expect(routeObject.controller).toEqual('someController');
15 | expect(routeObject.validator).toEqual('validatorFunction');
16 | expect(routeObject.doc).toEqual('docJs');
17 | }
18 |
19 | describe('subroute', () => {
20 | it('should return object', () => {
21 | const someRouter = { some: 'router' };
22 | const somePath = '/somepath';
23 | const obj = subroute(somePath, someRouter);
24 | expect(obj.path).toEqual(somePath);
25 | expect(obj.router).toEqual(someRouter);
26 | });
27 | });
28 |
29 | describe('Route model', () => {
30 | it('Should have GET function available', () => {
31 | const routeObject = getRouteObj('get');
32 | expect(routeObject.method).toEqual('get');
33 | testRouteObj(routeObject);
34 | });
35 |
36 | it('Should have POST function available', () => {
37 | const routeObject = getRouteObj('post');
38 | expect(routeObject.method).toEqual('post');
39 | testRouteObj(routeObject);
40 | });
41 |
42 | it('Should have PUT function available', () => {
43 | const routeObject = getRouteObj('put');
44 | expect(routeObject.method).toEqual('put');
45 | testRouteObj(routeObject);
46 | });
47 |
48 | it('Should have DELETE function available', () => {
49 | const routeObject = getRouteObj('delete');
50 | expect(routeObject.method).toEqual('delete');
51 | testRouteObj(routeObject);
52 | });
53 |
54 | it('Should have HEAD function available', () => {
55 | const routeObject = getRouteObj('head');
56 | expect(routeObject.method).toEqual('head');
57 | testRouteObj(routeObject);
58 | });
59 |
60 | it('Should have PATCH function available', () => {
61 | const routeObject = getRouteObj('patch');
62 | expect(routeObject.method).toEqual('patch');
63 | testRouteObj(routeObject);
64 | });
65 |
66 | it('Should have OPTIONS function available', () => {
67 | const routeObject = getRouteObj('options');
68 | expect(routeObject.method).toEqual('options');
69 | testRouteObj(routeObject);
70 | });
71 |
72 | it('Should set authorizer if given', () => {
73 | const routeObject = Route.get('/some/path', {
74 | controller: 'someOtherController',
75 | authorizer: 'someAuthorizer'
76 | });
77 |
78 | expect(routeObject.authorizer).toEqual('someAuthorizer');
79 | });
80 |
81 | it('Should set middleware if given', () => {
82 | const routeObject = Route.get('/some/path', {
83 | controller: 'someOtherController',
84 | middleware: 'someMiddleware'
85 | });
86 |
87 | expect(routeObject.middleware).toEqual('someMiddleware');
88 | });
89 |
90 | it('getRouteFn: Should allow route without optional parameters', () => {
91 | expect(Route.get('/', () => {})).toBeDefined();
92 | });
93 |
94 | it('getRouteFn: Should work with both 3 and 2 args', () => {
95 | expect(Route.get('/', 'someController').controller).toEqual(
96 | 'someController'
97 | );
98 | expect(
99 | Route.get('/', {
100 | controller: 'someController'
101 | }).controller
102 | ).toEqual('someController');
103 | });
104 | });
105 |
--------------------------------------------------------------------------------
/__tests__/redoc.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable prefer-destructuring */
2 | const registerRedoc = require('../src/redoc/registerRedoc');
3 | const redocHtmlTemplate = require('../src/redoc/redocHtmlTemplate');
4 |
5 | describe('Redoc', () => {
6 | describe('registerRedoc', () => {
7 | it('Should register redoc with express app', () => {
8 | const app = {
9 | get: jest.fn()
10 | };
11 |
12 | registerRedoc({
13 | app,
14 | swaggerJson: {},
15 | url: '/docs',
16 | authUser: {
17 | user: 'admin',
18 | password: 'admin'
19 | }
20 | });
21 |
22 | expect(app.get).toHaveBeenCalledTimes(2);
23 | expect(app.get.mock.calls[0][0]).toEqual('/docs/swagger.json');
24 | expect(app.get.mock.calls[1][0]).toEqual('/docs');
25 | });
26 |
27 | it('Should register first handler properly', () => {
28 | const app = {
29 | get: jest.fn()
30 | };
31 |
32 | const someSwagger = {
33 | hello: 'world'
34 | };
35 |
36 | registerRedoc({
37 | app: app,
38 | swaggerJson: someSwagger,
39 | url: '/docs',
40 | authUser: {
41 | user: 'admin',
42 | password: 'admin'
43 | }
44 | });
45 | expect(app.get).toHaveBeenCalledTimes(2);
46 |
47 | const mockSend = jest.fn();
48 | const mockRes = {
49 | type: jest.fn(),
50 | status: jest.fn().mockReturnValue({
51 | send: mockSend
52 | })
53 | };
54 |
55 | const firstHandler = app.get.mock.calls[0][2];
56 | firstHandler(null, mockRes);
57 | expect(mockRes.status).toHaveBeenCalledWith(200);
58 | expect(mockSend).toHaveBeenCalledWith(someSwagger);
59 | });
60 |
61 | it('Should register second handler properly', () => {
62 | const app = {
63 | get: jest.fn()
64 | };
65 |
66 | const someSwagger = {
67 | hello: 'world'
68 | };
69 |
70 | registerRedoc({
71 | app: app,
72 | swaggerJson: someSwagger,
73 | url: '/docs',
74 | authUser: {
75 | user: 'admin',
76 | password: 'admin'
77 | }
78 | });
79 | expect(app.get).toHaveBeenCalledTimes(2);
80 |
81 | const mockSend = jest.fn();
82 | const mockRes = {
83 | type: jest.fn(),
84 | status: jest.fn().mockReturnValue({
85 | send: mockSend
86 | })
87 | };
88 |
89 | const secondHandler = app.get.mock.calls[1][2];
90 | secondHandler(null, mockRes);
91 |
92 | const expectedRedoc = redocHtmlTemplate({
93 | title: 'ReDoc',
94 | specUrl: '/docs/swagger.json'
95 | });
96 |
97 | expect(mockRes.status).toHaveBeenCalledWith(200);
98 | expect(mockSend).toHaveBeenCalledWith(expectedRedoc);
99 | });
100 | });
101 |
102 | describe('redocHtmlTemplate', () => {
103 | it('Should give proper html with defaults', () => {
104 | const html = redocHtmlTemplate();
105 | expect(html).toContain('ReDoc');
106 | expect(html).toContain('http://petstore.swagger.io/v2/swagger.json');
107 | });
108 |
109 | it('Should give proper html with given params', () => {
110 | const html = redocHtmlTemplate({
111 | title: 'This is my title',
112 | specUrl: '/some/spec'
113 | });
114 | expect(html).toContain('This is my title');
115 | expect(html).toContain('/some/spec');
116 | });
117 | });
118 | });
119 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at siddiqus@live.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/__tests__/ExpressApp.test.js:
--------------------------------------------------------------------------------
1 | const ExpressApp = require('../src/ExpressApp');
2 |
3 | describe('ExpressApp', () => {
4 | it('should be defined', () => {
5 | expect(ExpressApp).toBeDefined();
6 | });
7 |
8 | describe('constructor', () => {
9 | it('Should init with all defaults', () => {
10 | const app = new ExpressApp({});
11 | expect(app.express).toBeDefined();
12 | });
13 |
14 | it('Should init with all overridden values', () => {
15 | const app = new ExpressApp(
16 | {},
17 | {
18 | allowCors: true,
19 | middleware: [() => {}],
20 | swaggerDefinitions: {},
21 | basePath: '/api',
22 | errorHandler: () => {},
23 | authorizer: true,
24 | authObjectHandler: (req, res) => {
25 | console.log(req.authorizer);
26 | },
27 | showSwaggerOnlyInDev: false,
28 | swaggerInfo: {}
29 | }
30 | );
31 |
32 | expect(app.express).toBeDefined();
33 | });
34 |
35 | it('constructor with Cors config', () => {
36 | const app = new ExpressApp(
37 | {},
38 | {
39 | allowCors: true,
40 | corsConfig: {
41 | origin: 'http://somepath'
42 | },
43 | middleware: [() => {}],
44 | swaggerDefinitions: {},
45 | basePath: '/api',
46 | errorHandler: () => {},
47 | showSwaggerOnlyInDev: false,
48 | swaggerInfo: {}
49 | }
50 | );
51 |
52 | expect(app.express).toBeDefined();
53 | });
54 |
55 | it('Should register proper error handler for array', async () => {
56 | const app = new ExpressApp(
57 | {},
58 | {
59 | allowCors: true,
60 | corsConfig: {
61 | origin: 'http://somepath'
62 | },
63 | middleware: [() => {}],
64 | swaggerDefinitions: {},
65 | basePath: '/api',
66 | showSwaggerOnlyInDev: false,
67 | swaggerInfo: {}
68 | }
69 | );
70 |
71 | app.express = {
72 | use: jest.fn()
73 | };
74 |
75 | app.config.errorHandler = [
76 | (err, req, res) => ({ err, req, res, next: 1 }),
77 | (err, req, res, next) => {
78 | return next();
79 | }
80 | ];
81 |
82 | app._registerErrorHandlers();
83 |
84 | expect(app.express.use).toHaveBeenCalledTimes(1);
85 | expect(app.express.use.mock.calls[0][0][0].length).toEqual(4);
86 | expect(app.express.use.mock.calls[0][0][1].length).toEqual(4);
87 |
88 | const mockNext = jest.fn().mockReturnValue(123);
89 | await app.express.use.mock.calls[0][0][0](1, 2, 3, mockNext);
90 | await app.express.use.mock.calls[0][0][1](1, 2, 3, mockNext);
91 |
92 | expect(mockNext).toHaveBeenCalledTimes(2);
93 | });
94 |
95 | it('Should register proper error handler for single function', async () => {
96 | const app = new ExpressApp(
97 | {},
98 | {
99 | allowCors: true,
100 | corsConfig: {
101 | origin: 'http://somepath'
102 | },
103 | middleware: [() => {}],
104 | swaggerDefinitions: {},
105 | basePath: '/api',
106 | showSwaggerOnlyInDev: false,
107 | swaggerInfo: {}
108 | }
109 | );
110 |
111 | app.express = {
112 | use: jest.fn()
113 | };
114 | app.config.errorHandler = (err, req, res) => ({ err, req, res, next: 1 });
115 |
116 | app._registerErrorHandlers();
117 |
118 | const mockNext = jest.fn().mockReturnValue(123);
119 |
120 | expect(app.express.use).toHaveBeenCalledTimes(1);
121 | expect(app.express.use.mock.calls[0][0].length).toEqual(4);
122 | const handlerResponse = await app.express.use.mock.calls[0][0](
123 | 1,
124 | 2,
125 | 3,
126 | mockNext
127 | );
128 | expect(handlerResponse).toEqual(123);
129 | });
130 | });
131 | });
132 |
--------------------------------------------------------------------------------
/__tests__/SwaggerBuilder.test.js:
--------------------------------------------------------------------------------
1 | const SwaggerEndpoint = require('../src/SwaggerBuilder');
2 |
3 | const expectedOutput = {
4 | summary: 'Get a list of CVs for a given job description',
5 | description: 'Get a list of CVs for a given job description',
6 | parameters: [
7 | {
8 | name: 'jdId',
9 | in: 'path',
10 | required: true,
11 | type: 'string',
12 | description: 'Job description ID',
13 | schema: {
14 | type: 'object',
15 | properties: {
16 | bucketName: {
17 | type: 'string'
18 | },
19 | bucketKey: {
20 | type: 'string'
21 | }
22 | }
23 | }
24 | },
25 | {
26 | name: 'body',
27 | in: 'body',
28 | required: true,
29 | type: 'string',
30 | description: 'CV ID',
31 | schema: {
32 | type: 'object',
33 | properties: {
34 | bucketName: {
35 | type: 'string'
36 | },
37 | bucketKey: {
38 | type: 'string'
39 | }
40 | }
41 | }
42 | }
43 | ],
44 | responses: {
45 | 200: {
46 | produces: ['application/json'],
47 | description: 'List of CVs for the given job description',
48 | schema: {
49 | type: 'object',
50 | properties: {
51 | url: {
52 | type: 'string'
53 | }
54 | }
55 | }
56 | },
57 | 400: {
58 | produces: ['application/json'],
59 | description: 'Some error description',
60 | schema: {
61 | type: 'object',
62 | properties: {
63 | errorMessage: {
64 | type: 'string'
65 | }
66 | }
67 | }
68 | }
69 | }
70 | };
71 |
72 | describe('SwaggerBuilder', () => {
73 | it('Should transform endpoint doc', () => {
74 | const output = new SwaggerEndpoint(
75 | expectedOutput.summary,
76 | expectedOutput.description
77 | );
78 |
79 | expect(output.summary).toEqual(expectedOutput.summary);
80 | expect(output.description).toEqual(expectedOutput.description);
81 |
82 | output.addResponse({
83 | code: 200,
84 | description: 'List of CVs for the given job description',
85 | schema: {
86 | type: 'object',
87 | properties: {
88 | url: {
89 | type: 'string'
90 | }
91 | }
92 | }
93 | });
94 |
95 | expect(output.responses[200]).toEqual(expectedOutput.responses[200]);
96 |
97 | output.addResponse({
98 | code: 400,
99 | description: 'Some error description',
100 | schema: {
101 | type: 'object',
102 | properties: {
103 | errorMessage: {
104 | type: 'string'
105 | }
106 | }
107 | },
108 | produces: ['application/json']
109 | });
110 |
111 | expect(output.responses[400]).toEqual(expectedOutput.responses[400]);
112 |
113 | output.addPathParam({
114 | name: 'jdId',
115 | required: true,
116 | type: 'string',
117 | description: 'Job description ID',
118 | schema: {
119 | type: 'object',
120 | properties: {
121 | bucketName: {
122 | type: 'string'
123 | },
124 | bucketKey: {
125 | type: 'string'
126 | }
127 | }
128 | }
129 | });
130 |
131 | expect(output.parameters[0]).toEqual(expectedOutput.parameters[0]);
132 |
133 | const requestBody = {
134 | required: true,
135 | description: 'CV ID',
136 | schema: {
137 | type: 'object',
138 | properties: {
139 | bucketName: {
140 | type: 'string'
141 | },
142 | bucketKey: {
143 | type: 'string'
144 | }
145 | }
146 | }
147 | };
148 |
149 | output.addRequestBody(requestBody);
150 |
151 | expect(output.parameters[1]).toEqual({
152 | ...requestBody,
153 | name: 'body',
154 | in: 'body'
155 | });
156 |
157 | output.addRequestBody({
158 | ...requestBody,
159 | name: 'someBody'
160 | });
161 |
162 | expect(output.parameters[2]).toEqual({
163 | ...requestBody,
164 | name: 'someBody',
165 | in: 'body'
166 | });
167 |
168 | const queryParam = {
169 | name: 'some query param',
170 | description: 'some query desc',
171 | required: true,
172 | schema: {
173 | type: 'integer'
174 | }
175 | };
176 |
177 | output.addQueryParam(queryParam);
178 |
179 | expect(output.parameters[3]).toEqual({
180 | ...queryParam,
181 | in: 'query'
182 | });
183 | });
184 | });
185 |
--------------------------------------------------------------------------------
/__tests__/AuthUtil.test.js:
--------------------------------------------------------------------------------
1 | const AuthUtil = require('../src/AuthUtil');
2 |
3 | describe('AuthUtil', () => {
4 | describe('getMiddlewareForInjectingAuthProperties', () => {
5 | it('should inject auth if none previously', () => {
6 | const authUtil = new AuthUtil();
7 |
8 | const handler = authUtil.getMiddlewareForInjectingAuthProperties(1);
9 |
10 | const mockReq = {};
11 |
12 | handler(mockReq);
13 |
14 | expect(mockReq.authorizer).toEqual([1]);
15 | });
16 |
17 | it('should inject auth with previous auth', () => {
18 | const authUtil = new AuthUtil();
19 |
20 | const handler = authUtil.getMiddlewareForInjectingAuthProperties(1);
21 |
22 | const mockReq = {
23 | authorizer: ['hehe']
24 | };
25 |
26 | handler(mockReq);
27 |
28 | expect(mockReq.authorizer).toEqual(['hehe', 1]);
29 | });
30 |
31 | it('should maintain unique auth object', () => {
32 | const authUtil = new AuthUtil();
33 |
34 | const someAuthorizer = {
35 | someAuthPrivileges: ['hello', 'hey']
36 | };
37 | const handler = authUtil.getMiddlewareForInjectingAuthProperties(
38 | someAuthorizer
39 | );
40 |
41 | const mockReq = {
42 | authorizer: [someAuthorizer]
43 | };
44 |
45 | handler(mockReq);
46 |
47 | expect(mockReq.authorizer).toEqual([someAuthorizer]);
48 | });
49 |
50 | it('should flatten array for authorizers', () => {
51 | const authUtil = new AuthUtil();
52 |
53 | const someAuthorizer = ['hehe'];
54 | const handler = authUtil.getMiddlewareForInjectingAuthProperties(
55 | someAuthorizer
56 | );
57 |
58 | const mockReq = {
59 | authorizer: ['hoho']
60 | };
61 |
62 | handler(mockReq);
63 |
64 | expect(mockReq.authorizer).toEqual(['hoho', 'hehe']);
65 | });
66 | });
67 |
68 | describe('getAuthorizerMiddleware', () => {
69 | it('Should return null if authorizer is falsy', () => {
70 | // setup
71 | const authUtil = new AuthUtil();
72 | // execute
73 | const result = authUtil.getAuthorizerMiddleware(null);
74 | // assert
75 | expect(result).toBeNull();
76 | });
77 |
78 | it('Should throw error if authorizer is defined but auth object handler is not', () => {
79 | // setup
80 | const authUtil = new AuthUtil();
81 | // execute
82 | let result;
83 | try {
84 | authUtil.getAuthorizerMiddleware([1, 2, 3]);
85 | } catch (error) {
86 | result = error;
87 | }
88 | // assert
89 |
90 | expect(result.message).toEqual(
91 | `'authorizer' object declared, but 'authObjectHandler' is not defined in ExpressApp constructor params`
92 | );
93 | });
94 |
95 | it('Should return proper middleware if authorizer is object', () => {
96 | // setup
97 | const authUtil = new AuthUtil();
98 | authUtil.routeUtil = {
99 | getHandlerWithManagedNextCall: jest.fn().mockReturnValue(1)
100 | };
101 | const mockAuthorizer = ['hello'];
102 | const mockAuthObjectHandler = jest.fn();
103 | // execute
104 | const result = authUtil.getAuthorizerMiddleware(
105 | mockAuthorizer,
106 | mockAuthObjectHandler
107 | );
108 | // assert
109 | expect(result).not.toBeNull();
110 | expect(
111 | authUtil.routeUtil.getHandlerWithManagedNextCall
112 | ).toHaveBeenCalledTimes(2);
113 | expect(
114 | authUtil.routeUtil.getHandlerWithManagedNextCall
115 | ).toHaveBeenCalledWith(mockAuthObjectHandler);
116 | });
117 |
118 | it('Should return proper middleware if authorizer is string', () => {
119 | // setup
120 | const authUtil = new AuthUtil();
121 | authUtil.routeUtil = {
122 | getHandlerWithManagedNextCall: jest.fn().mockReturnValue(1)
123 | };
124 | const mockAuthorizer = 'hello';
125 | const mockAuthObjectHandler = jest.fn();
126 | // execute
127 | const result = authUtil.getAuthorizerMiddleware(
128 | mockAuthorizer,
129 | mockAuthObjectHandler
130 | );
131 | // assert
132 | expect(result).not.toBeNull();
133 | expect(
134 | authUtil.routeUtil.getHandlerWithManagedNextCall
135 | ).toHaveBeenCalledTimes(2);
136 | expect(
137 | authUtil.routeUtil.getHandlerWithManagedNextCall
138 | ).toHaveBeenCalledWith(mockAuthObjectHandler);
139 | });
140 |
141 | it('Should return proper middleware if authorizer is a function', () => {
142 | // setup
143 | const authUtil = new AuthUtil();
144 | authUtil.routeUtil = {
145 | getHandlerWithManagedNextCall: jest.fn().mockReturnValue(1)
146 | };
147 | const mockAuthorizer = (_) => {};
148 | const mockAuthObjectHandler = jest.fn();
149 | // execute
150 | const result = authUtil.getAuthorizerMiddleware(
151 | mockAuthorizer,
152 | mockAuthObjectHandler
153 | );
154 | // assert
155 | expect(result).not.toBeNull();
156 | expect(
157 | authUtil.routeUtil.getHandlerWithManagedNextCall
158 | ).toHaveBeenCalledTimes(1);
159 | expect(
160 | authUtil.routeUtil.getHandlerWithManagedNextCall
161 | ).toHaveBeenCalledWith(mockAuthorizer);
162 | });
163 | });
164 | });
165 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | "incremental": true,
5 | /* Enable incremental compilation */
6 | "target": "esnext",
7 | /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
8 | "module": "commonjs",
9 | /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
10 | // "lib": [], /* Specify library files to be included in the compilation. */
11 | "allowJs": false,
12 | /* Allow javascript files to be compiled. */
13 | "checkJs": false,
14 | /* Report errors in .js files. */
15 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
16 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
17 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
18 | "sourceMap": true /* Generates corresponding '.map' file. */,
19 | // "outFile": "./", /* Concatenate and emit output to single file. */
20 | "outDir": "./dist",
21 | /* Redirect output structure to the directory. */
22 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
23 | // "composite": true, /* Enable project compilation */
24 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
25 | // "removeComments": true, /* Do not emit comments to output. */
26 | // "noEmit": true, /* Do not emit outputs. */
27 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
28 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
29 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
30 |
31 | /* Strict Type-Checking Options */
32 | "strict": true,
33 | /* Enable all strict type-checking options. */
34 | "noImplicitAny": true,
35 | /* Raise error on expressions and declarations with an implied 'any' type. */
36 | "strictNullChecks": true,
37 | /* Enable strict null checks. */
38 | "strictFunctionTypes": true,
39 | /* Enable strict checking of function types. */
40 | "strictBindCallApply": true,
41 | /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
42 | "strictPropertyInitialization": true,
43 | /* Enable strict checking of property initialization in classes. */
44 | "noImplicitThis": true,
45 | /* Raise error on 'this' expressions with an implied 'any' type. */
46 | "alwaysStrict": false,
47 | /* Parse in strict mode and emit "use strict" for each source file. */
48 |
49 | /* Additional Checks */
50 | // "noUnusedLocals": true,
51 | /* Report errors on unused locals. */
52 | // "noUnusedParameters": true,
53 | /* Report errors on unused parameters. */
54 | "noImplicitReturns": true,
55 | /* Report error when not all code paths in function return a value. */
56 | "noFallthroughCasesInSwitch": true,
57 | /* Report errors for fallthrough cases in switch statement. */
58 |
59 | /* Module Resolution Options */
60 | "moduleResolution": "node",
61 | "resolveJsonModule": true,
62 | /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
63 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
64 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
65 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
66 | "typeRoots": [
67 | "./node_modules/@types"
68 | ],
69 | "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
70 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
71 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
72 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
73 |
74 | /* Source Map Options */
75 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
76 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
77 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
78 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
79 |
80 | /* Experimental Options */
81 | "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
82 | "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */
83 | },
84 | "include": ["src/**/*"],
85 | "exclude": ["src/configs", "node_modules"]
86 | }
87 |
--------------------------------------------------------------------------------
/src/middleware/MiddlewareManager.js:
--------------------------------------------------------------------------------
1 | const expressModule = require('express');
2 | const helmet = require('helmet');
3 | const cors = require('cors');
4 | const morganBody = require('morgan-body');
5 | const { errors: celebrateErrors } = require('celebrate');
6 | const responseMiddleware = require('./response');
7 | const RouteUtil = require('../RouteUtil');
8 | const AuthUtil = require('../AuthUtil');
9 | const SwaggerUtils = require('../SwaggerUtils');
10 | const registerRedoc = require('../redoc/registerRedoc');
11 | const addRequestId = require('./requestId');
12 |
13 | module.exports = class MiddlewareManager {
14 | constructor(expressiveConfig, expressApp) {
15 | this.options = expressiveConfig;
16 | this.express = expressApp;
17 |
18 | this._initDependencies();
19 | }
20 |
21 | _initDependencies() {
22 | this.expressModule = expressModule;
23 | this.addRequestId = addRequestId;
24 | this.helmet = helmet;
25 |
26 | this.routeUtil = RouteUtil;
27 | this.authUtil = new AuthUtil();
28 | this.SwaggerUtils = SwaggerUtils;
29 | this.registerRedoc = registerRedoc;
30 | this.celebrateErrors = celebrateErrors;
31 | this.morganBody = morganBody;
32 | }
33 |
34 | defaultNotFoundHandler(req, res) {
35 | res.status(404);
36 | res.json({
37 | message: `Route '${req.path}' not found`
38 | });
39 | }
40 |
41 | _registerHelmet() {
42 | const helmet = this.options.helmetOptions
43 | ? this.helmet(this.options.helmetOptions)
44 | : this.helmet();
45 | this.express.use(helmet);
46 | }
47 |
48 | _registerBodyParser() {
49 | this.express.use(
50 | this.expressModule.json({
51 | limit: this.options.bodyLimit
52 | })
53 | );
54 | this.express.use(this.expressModule.urlencoded({ extended: true }));
55 | }
56 |
57 | _registerMorganBody() {
58 | if (
59 | this.options.requestLoggerOptions &&
60 | this.options.requestLoggerOptions.disabled
61 | ) {
62 | return;
63 | }
64 |
65 | this.morganBody(this.express, {
66 | logRequestId: true,
67 | prettify: false,
68 | skip: (req) => {
69 | return (
70 | req.originalUrl === '/' ||
71 | req.originalUrl.match(/^\/favicon\/*/) ||
72 | (this.options.requestLoggerOptions &&
73 | this.options.requestLoggerOptions.skip &&
74 | this.options.requestLoggerOptions.skip(req))
75 | );
76 | },
77 | includeNewLine: true,
78 | immediateReqLog: true,
79 | ...(this.options.requestLoggerOptions || {})
80 | });
81 | }
82 |
83 | registerBasicMiddleware() {
84 | this._registerBodyParser();
85 | this.express.use(responseMiddleware);
86 | this.express.use(this.addRequestId());
87 |
88 | this._registerHelmet();
89 | this._registerMorganBody();
90 |
91 | const { middleware: userMiddleware } = this.options;
92 | if (userMiddleware && userMiddleware.length > 0) {
93 | const nextManagedMiddlewares = userMiddleware.map((m) =>
94 | this.routeUtil.getHandlerWithManagedNextCall(m)
95 | );
96 | this.express.use(nextManagedMiddlewares);
97 | }
98 | }
99 |
100 | registerNotFoundHandler() {
101 | const { notFoundHandler } = this.options;
102 | if (notFoundHandler) {
103 | this.express.use(notFoundHandler);
104 | } else {
105 | this.express.use(this.defaultNotFoundHandler);
106 | }
107 | }
108 |
109 | registerAuth() {
110 | const { authorizer, authObjectHandler } = this.options;
111 | const authMiddleware = this.authUtil.getAuthorizerMiddleware(
112 | authorizer,
113 | authObjectHandler
114 | );
115 |
116 | if (authMiddleware) {
117 | this.express.use(authMiddleware);
118 | }
119 | }
120 |
121 | registerCelebrateErrorMiddleware() {
122 | const { celebrateErrorHandler } = this.options;
123 | if (celebrateErrorHandler) {
124 | this.express.use(celebrateErrorHandler);
125 | } else {
126 | this.express.use(this.celebrateErrors());
127 | }
128 | }
129 |
130 | configureCors() {
131 | if (!this.options.allowCors) return;
132 |
133 | const corsMiddleware = this.options.corsConfig
134 | ? cors(this.options.corsConfig)
135 | : cors();
136 | this.express.use(corsMiddleware);
137 | }
138 |
139 | registerDocs(expressiveRouter) {
140 | const env = process.env.NODE_ENV || 'development';
141 | const shouldRegister =
142 | (this.options.showSwaggerOnlyInDev && env === 'development') ||
143 | !this.options.showSwaggerOnlyInDev;
144 |
145 | if (!shouldRegister) return;
146 |
147 | const { basePath, swaggerInfo, swaggerSecurityDefinitions } = this.options;
148 | const swaggerHeader = this.SwaggerUtils.getSwaggerHeader({
149 | basePath,
150 | swaggerInfo,
151 | swaggerSecurityDefinitions
152 | });
153 |
154 | const swaggerJson = this.SwaggerUtils.convertDocsToSwaggerDoc(
155 | expressiveRouter,
156 | swaggerHeader,
157 | this.options.swaggerDefinitions
158 | );
159 |
160 | const authUser = {
161 | user: process.env.EXPRESS_SWAGGER_USER || 'admin',
162 | password: process.env.EXPRESS_SWAGGER_PASSWORD || 'admin'
163 | };
164 |
165 | this.SwaggerUtils.registerExpress({
166 | app: this.express,
167 | swaggerJson,
168 | url: '/docs/swagger',
169 | authUser
170 | });
171 | console.log('Swagger doc up and running on /docs');
172 |
173 | this.registerRedoc({
174 | app: this.express,
175 | swaggerJson,
176 | url: '/docs/redoc',
177 | authUser
178 | });
179 | }
180 | };
181 |
--------------------------------------------------------------------------------
/src/RouterFactory.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-invalid-this */
2 | const { Router } = require('express');
3 | const { celebrate: celebrateMiddleware } = require('celebrate');
4 | const RouteUtil = require('./RouteUtil');
5 | const AuthUtil = require('./AuthUtil');
6 | const CelebrateUtils = require('./CelebrateUtils');
7 | const BaseController = require('./BaseController');
8 |
9 | async function _handleRequestBase(req, res, next) {
10 | this.req = req;
11 | this.res = res;
12 | this.next = next;
13 |
14 | return this.handleRequest();
15 | }
16 |
17 | module.exports = class RouterFactory {
18 | constructor(expressiveOptions) {
19 | this.expressiveOptions = expressiveOptions;
20 | this.routeUtil = RouteUtil;
21 | this.authUtil = new AuthUtil();
22 | this.celebrateMiddleware = celebrateMiddleware;
23 | this.CelebrateUtils = CelebrateUtils;
24 | }
25 |
26 | _getWrappedController(controllerInstance) {
27 | return async (req, res, next) => {
28 | try {
29 | const mappedReq = BaseController.requestMapper(req);
30 | await _handleRequestBase.call(controllerInstance, mappedReq, res, next);
31 | if (!controllerInstance.res.headersSent) {
32 | controllerInstance.internalServerError(
33 | 'Server did not send any response'
34 | );
35 | }
36 | } catch (e) {
37 | return next(e);
38 | }
39 | };
40 | }
41 |
42 | _registerCelebrateErrorMiddleware(validationSchema, routerArgs) {
43 | if (!validationSchema) return;
44 |
45 | const { options, ...validationSchemaObj } = validationSchema;
46 |
47 | const sanitizedValidationSchema = this.CelebrateUtils.getSanitizedValidationSchema(
48 | validationSchemaObj
49 | );
50 |
51 | if (sanitizedValidationSchema) {
52 | this.CelebrateUtils.lowercaseHeaderSchemaProperties(
53 | sanitizedValidationSchema
54 | );
55 | routerArgs.push(
56 | this.celebrateMiddleware(sanitizedValidationSchema, {
57 | abortEarly: false,
58 | ...(options || {})
59 | })
60 | );
61 | }
62 | }
63 |
64 | _setAuthorizerMiddleware(authorizer, routerArgs) {
65 | const authMiddleware = this.authUtil.getAuthorizerMiddleware(
66 | authorizer,
67 | this.expressiveOptions.authObjectHandler
68 | );
69 |
70 | if (authMiddleware) {
71 | routerArgs.push(authMiddleware);
72 | }
73 | }
74 |
75 | _setFileUploadValidationMiddleware(validationSchema, routerArgs) {
76 | if (!validationSchema || !validationSchema.fileUpload) return;
77 |
78 | routerArgs.push(
79 | this.CelebrateUtils.getCelebrateValidationMiddlewareForFileUpload(
80 | validationSchema.fileUpload
81 | )
82 | );
83 | }
84 |
85 | _registerMiddleware(
86 | routerArgs,
87 | { validationSchema, authorizer, middleware }
88 | ) {
89 | this._setAuthorizerMiddleware(authorizer, routerArgs);
90 | this._registerCelebrateErrorMiddleware(validationSchema, routerArgs);
91 |
92 | const nextAdjustedMiddleware = !middleware
93 | ? []
94 | : middleware.map((m) => this.routeUtil.getHandlerWithManagedNextCall(m));
95 |
96 | routerArgs.push(...nextAdjustedMiddleware);
97 |
98 | this._setFileUploadValidationMiddleware(validationSchema, routerArgs);
99 | }
100 |
101 | _registerPreHandlers(routerArgs, handler) {
102 | if (!handler) return;
103 |
104 | if (Array.isArray(handler)) {
105 | const nextAdjustedMiddleware = handler.map((m) =>
106 | this.routeUtil.getHandlerWithManagedNextCall(m)
107 | );
108 | routerArgs.push(...nextAdjustedMiddleware);
109 | } else {
110 | routerArgs.push(this.routeUtil.getHandlerWithManagedNextCall(handler));
111 | }
112 | }
113 |
114 | _registerRoute(router, { method, path, controller }) {
115 | const routerArgs = [path];
116 |
117 | this._registerPreHandlers(routerArgs, controller.pre);
118 |
119 | this._registerMiddleware(routerArgs, {
120 | validationSchema: controller.validationSchema,
121 | authorizer: controller.authorizer,
122 | middleware: controller.middleware
123 | });
124 |
125 | routerArgs.push(this._getWrappedController(controller));
126 |
127 | router[method](...routerArgs);
128 | }
129 |
130 | _registerSubroute(
131 | router,
132 | {
133 | path,
134 | router: subrouter,
135 | authorizer,
136 | validationSchema = null,
137 | middleware = [],
138 | pre = null
139 | }
140 | ) {
141 | const routerArgs = [path];
142 |
143 | this._registerPreHandlers(routerArgs, pre);
144 |
145 | this._registerMiddleware(routerArgs, {
146 | validationSchema,
147 | authorizer,
148 | middleware
149 | });
150 |
151 | routerArgs.push(this.getExpressRouter(subrouter));
152 |
153 | router.use(...routerArgs);
154 | }
155 |
156 | _getRouter() {
157 | return new Router({
158 | mergeParams: true
159 | });
160 | }
161 |
162 | _handleDuplicateUrls(expressiveRouter) {
163 | const duplicateUrls = this.routeUtil.getDuplicateUrls(expressiveRouter);
164 | if (duplicateUrls.length > 0) {
165 | throw new Error(
166 | `Duplicate endpoints detected! -> ${duplicateUrls.join(', ')}`
167 | );
168 | }
169 | }
170 |
171 | getExpressRouter(expressiveRouter) {
172 | this._handleDuplicateUrls(expressiveRouter);
173 |
174 | const router = this._getRouter();
175 |
176 | if (expressiveRouter.routes) {
177 | expressiveRouter.routes.forEach((routeConf) => {
178 | this._registerRoute(router, routeConf);
179 | });
180 | }
181 |
182 | if (expressiveRouter.subroutes) {
183 | expressiveRouter.subroutes.forEach((subroute) => {
184 | this._registerSubroute(router, subroute);
185 | });
186 | }
187 |
188 | return router;
189 | }
190 | };
191 |
--------------------------------------------------------------------------------
/src/index.d.ts:
--------------------------------------------------------------------------------
1 | import { CorsOptions as CorsLibOptions } from 'cors';
2 | import type {
3 | ErrorRequestHandler as ExpressErrorRequestHandler,
4 | Express as ExpressType,
5 | NextFunction as ExpressNextFunction,
6 | Response as ExpressResponse,
7 | Request as ExpressRequest,
8 | RequestHandler as ExpressHandler
9 | } from 'express';
10 |
11 | export declare type express = typeof import('express');
12 |
13 | interface Request extends ExpressRequest {
14 | id?: string
15 | authorizer?: AuthorizerType;
16 | user?: Record;
17 | }
18 |
19 | export type Response = ExpressResponse;
20 | export type NextFunction = ExpressNextFunction;
21 |
22 | export interface Handler {
23 | (req: Request, res: Response, next: NextFunction): void
24 | }
25 |
26 | export type HelmetOptions = typeof import('helmet').HelmetOptions
27 |
28 | export type Express = ExpressType;
29 | export type ErrorRequestHandler = ExpressErrorRequestHandler;
30 | export type CorsOptions = CorsLibOptions;
31 |
32 | export declare const Joi: typeof import('celebrate').Joi;
33 | export declare const isValidationError: typeof import('celebrate').isCelebrateError;
34 | export declare const celebrate: typeof import('celebrate');
35 |
36 | import { celebrate as CelebrateFn } from 'celebrate';
37 | export type ValidationOptions = Parameters[1]
38 |
39 | export declare interface SwaggerInfoContact {
40 | name?: string;
41 | email?: string;
42 | }
43 |
44 | export declare interface SwaggerInfo {
45 | version?: string;
46 | title?: string;
47 | contact?: SwaggerInfoContact;
48 | }
49 |
50 | interface BaseController {
51 | validationSchema?: ValidationSchema;
52 | authorizer?: AuthorizerType;
53 | doc?: SwaggerEndpointDoc;
54 | middleware?: Handler[];
55 | pre?: Handler | Handler[];
56 | }
57 |
58 | export declare abstract class BaseController {
59 | static responseMapper(data: any): any;
60 | static requestMapper(data: any): any;
61 |
62 | abstract handleRequest(): Promise;
63 |
64 | req: Request;
65 | res: Response;
66 | next: NextFunction;
67 |
68 | getData(): {
69 | body: Record
70 | query: Record
71 | params: Record
72 | fileUpload: {
73 | file: any
74 | files: any
75 | }
76 | user: Record
77 | };
78 |
79 | ok(data?: any): void;
80 |
81 | created(data?: any): void;
82 |
83 | accepted(data?: any): void;
84 |
85 | noContent(): void;
86 |
87 | badRequest(message?: string): void;
88 |
89 | unauthorized(message?: string): void;
90 |
91 | forbidden(message?: string): void;
92 |
93 | notFound(message?: string): void;
94 |
95 | tooMany(message?: string): void;
96 |
97 | internalServerError(message?: string, body?: any): void;
98 | }
99 | export declare interface ValidationSchema {
100 | body?: object;
101 | params?: object;
102 | query?: object;
103 | headers?: object;
104 | cookies?: object;
105 | signedCookies?: object;
106 | fileUpload?: {
107 | file?: object,
108 | files?: object
109 | }
110 | options?: ValidationOptions
111 | }
112 |
113 | interface SwaggerResponseMap {
114 | [key: number]: object;
115 | }
116 |
117 | export declare interface SwaggerEndpointDoc {
118 | description?: string;
119 | summary?: string;
120 | responses?: SwaggerResponseMap;
121 | tags?: string[];
122 | }
123 |
124 | type AuthorizerType = Handler | Handler[] | string | string[] | object | object[];
125 |
126 | export declare type RouteMethod =
127 | | 'get'
128 | | 'post'
129 | | 'put'
130 | | 'delete'
131 | | 'head'
132 | | 'patch'
133 | | 'options';
134 |
135 | export declare interface Endpoint {
136 | controller: BaseController;
137 | method: RouteMethod;
138 | path: string;
139 | }
140 |
141 | export declare class Route {
142 | static get(
143 | path: string,
144 | controller: BaseController
145 | ): Endpoint;
146 |
147 | static post(
148 | path: string,
149 | controller: BaseController
150 | ): Endpoint;
151 |
152 | static delete(
153 | path: string,
154 | controller: BaseController
155 | ): Endpoint;
156 |
157 | static put(
158 | path: string,
159 | controller: BaseController
160 | ): Endpoint;
161 |
162 | static head(
163 | path: string,
164 | controller: BaseController
165 | ): Endpoint;
166 |
167 | static options(
168 | path: string,
169 | controller: BaseController
170 | ): Endpoint;
171 |
172 | static patch(
173 | path: string,
174 | controller: BaseController
175 | ): Endpoint;
176 | }
177 |
178 | export declare interface Subroute {
179 | path: string;
180 | controller: BaseController;
181 | authorizer?: AuthorizerType;
182 | middleware?: Handler[];
183 | validationSchema?: ValidationSchema;
184 | pre?: Handler | Handler[];
185 | }
186 |
187 | export declare function subroute(
188 | path: string,
189 | router: ExpressiveRouter,
190 | options?: Pick
191 | ): Subroute
192 |
193 | export declare interface ExpressiveRouter {
194 | routes?: Endpoint[];
195 | subroutes?: Subroute[];
196 | }
197 |
198 | export type SwaggerSecurityDefinitions = {
199 | [key in string]: {
200 | type: 'basic'
201 | } | {
202 | type: 'apiKey'
203 | in: 'header' | 'query'
204 | name: string
205 | } | {
206 | type: 'oauth2'
207 | flow: string
208 | authorizationUrl: string
209 | tokenUrl: string
210 | scopes: {
211 | [key in string]: string
212 | }
213 | }
214 | }
215 |
216 | export interface ExpressiveOptions {
217 | basePath?: string;
218 | showSwaggerOnlyInDev?: boolean;
219 | swaggerInfo?: SwaggerInfo;
220 | swaggerDefinitions?: any;
221 | swaggerBasicAuth?: {
222 | user: string;
223 | password: string;
224 | }
225 | swaggerSecurityDefinitions?: SwaggerSecurityDefinitions;
226 | allowCors?: boolean;
227 | corsConfig?: CorsOptions;
228 | middleware?: Handler[];
229 | authorizer?: AuthorizerType;
230 | errorHandler?: ErrorRequestHandler | ErrorRequestHandler[];
231 | bodyLimit?: string;
232 | helmetOptions?: HelmetOptions;
233 | celebrateErrorHandler?: ErrorRequestHandler;
234 | notFoundHandler?: Handler;
235 | authObjectHandler?: Handler;
236 | requestLoggerOptions?: import('morgan-body').IMorganBodyOptions & {
237 | disabled: boolean
238 | }
239 | }
240 |
241 | export declare class ExpressApp {
242 | constructor(router: ExpressiveRouter, options?: ExpressiveOptions);
243 | express: Express;
244 | listen(port: number, cb: Function): void;
245 | }
246 |
--------------------------------------------------------------------------------
/src/SwaggerUtils.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const basicAuth = require('express-basic-auth');
3 | const SwaggerUi = require('swagger-ui-express');
4 | const RouteUtil = require('./RouteUtil.js');
5 | const Utils = require('./Utils');
6 |
7 | const { joiSchemaToSwaggerRequestParameters } = require('./CelebrateUtils');
8 |
9 | function registerExpress({ app, swaggerJson, url, authUser }) {
10 | const { user, password } = authUser;
11 | const basicAuthMiddleware = basicAuth({
12 | challenge: true,
13 | users: { [user]: password }
14 | });
15 |
16 | app.use(
17 | url,
18 | basicAuthMiddleware,
19 | SwaggerUi.serve,
20 | SwaggerUi.setup(swaggerJson, {
21 | explorer: true
22 | })
23 | );
24 |
25 | const redirectUrl =
26 | url.charAt(0) === '/' ? url.substring(1, url.length) : url;
27 | app.get('/docs', basicAuthMiddleware, (_, res) => res.redirect(redirectUrl));
28 | }
29 |
30 | function _sanitizeSwaggerPath(path) {
31 | const normalizedPath = Utils.normalizePathSlashes(path);
32 | if (!normalizedPath.includes(':')) return normalizedPath;
33 |
34 | const split = normalizedPath.split('/');
35 |
36 | split.forEach((p, index) => {
37 | if (p.includes(':')) {
38 | split[index] = `{${p.replace(':', '')}}`;
39 | }
40 | });
41 | return split.join('/');
42 | }
43 |
44 | function _setDocResponses(doc, validationSchema) {
45 | const docResponses = doc.responses || {};
46 |
47 | if (!docResponses[400] && validationSchema) {
48 | docResponses[400] = {
49 | description: 'Schema validation error response'
50 | };
51 | }
52 |
53 | if (!docResponses[200]) {
54 | docResponses[200] = {
55 | description: 'Success response'
56 | };
57 | }
58 |
59 | doc.responses = docResponses;
60 | }
61 |
62 | function _setAuthorizerDocInDescription(doc, authorizer) {
63 | if (!authorizer) return;
64 | const authStr = `Authorized: ${JSON.stringify(authorizer)}`;
65 | if (doc.description) {
66 | doc.description = `${authStr}\n\n${doc.description}`;
67 | } else {
68 | doc.description = authStr;
69 | }
70 | }
71 |
72 | function _setDocParameters(doc, validationSchema) {
73 | doc.parameters =
74 | doc.parameters || joiSchemaToSwaggerRequestParameters(validationSchema);
75 |
76 | if (doc.parameters.find((p) => p.type === 'file')) {
77 | doc.consumes = ['multipart/form-data'];
78 | }
79 | }
80 |
81 | function _getUniqueArray(arr) {
82 | return Array.from(new Set(arr));
83 | }
84 |
85 | function _capitalize(string) {
86 | if (string.length <= 1) return string.toUpperCase();
87 | return string.charAt(0).toUpperCase() + string.slice(1);
88 | }
89 |
90 | function _setTagFromPath(path, doc) {
91 | if (!path) return;
92 | const tags = path
93 | .split('/')
94 | .filter(Boolean)
95 | .filter((s) => !s.includes(':'))
96 | .map((s) => s.replace(/-|_/g, ' ').split(' ').map(_capitalize).join(' '));
97 | if (!tags.length) return;
98 |
99 | if (!doc.tags) doc.tags = [];
100 | doc.tags = _getUniqueArray([...tags, ...doc.tags]);
101 | }
102 |
103 | function _addPathDoc(paths, route, tags) {
104 | let {
105 | path,
106 | validationSchema,
107 | authorizer,
108 | parentPath,
109 | controllerName,
110 | doc = {}
111 | } = route;
112 |
113 | _setTagFromPath(parentPath, doc);
114 | const { method } = route;
115 | path = _sanitizeSwaggerPath(path);
116 | doc.summary = `${doc.summary || ''} [${controllerName}]`.trim();
117 |
118 | _setAuthorizerDocInDescription(doc, authorizer);
119 | _setDocParameters(doc, validationSchema);
120 | _setDocResponses(doc, validationSchema);
121 |
122 | paths[path] = paths[path] || {};
123 | paths[path][method] = doc;
124 |
125 | if (doc.tags) tags.push(...doc.tags);
126 | }
127 |
128 | // // _handleRedirects is deprecated in v2
129 | // function _handleRedirects(paths, route) {
130 | // const { method, path } = route;
131 | // if (!route.redirectUrl) return;
132 |
133 | // const redirectUrl = Utils.normalizePathSlashes(route.redirectUrl);
134 |
135 | // const doc =
136 | // paths[redirectUrl] && paths[redirectUrl][method]
137 | // ? { ...paths[redirectUrl][method] }
138 | // : {};
139 |
140 | // doc.description = `[Redirected to ${redirectUrl}] ${doc.description || ''}`;
141 | // doc.summary = `[Redirected to ${redirectUrl}] ${doc.summary || ''}`;
142 |
143 | // paths[path][method] = doc;
144 | // }
145 |
146 | function convertDocsToSwaggerDoc(
147 | router,
148 | swaggerHeader,
149 | swaggerDefinitions = undefined
150 | ) {
151 | const infoList = RouteUtil.getRoutesInfo(router);
152 | const paths = {};
153 | let tags = [];
154 |
155 | infoList.forEach((route) => _addPathDoc(paths, route, tags));
156 |
157 | tags = Array.from(new Set(tags)).map((t) => ({ name: t }));
158 |
159 | const swaggerDoc = Object.assign({}, swaggerHeader);
160 | swaggerDoc.definitions = swaggerDefinitions;
161 | swaggerDoc.tags = tags;
162 | swaggerDoc.paths = paths;
163 | return swaggerDoc;
164 | }
165 |
166 | const sampleSwaggerInfo = {
167 | version: '1.0.0',
168 | title: 'Expressive API',
169 | contact: {
170 | name: 'Author',
171 | email: 'Your email address',
172 | url: ''
173 | }
174 | };
175 |
176 | function writeSwaggerJson({
177 | router,
178 | output,
179 | basePath = '/',
180 | swaggerInfo = null,
181 | swaggerSecurityDefinitions = null
182 | }) {
183 | const swaggerHeader = getSwaggerHeader({
184 | basePath,
185 | swaggerInfo: swaggerInfo || sampleSwaggerInfo,
186 | swaggerSecurityDefinitions
187 | });
188 | const swaggerJson = convertDocsToSwaggerDoc(router, swaggerHeader);
189 | fs.writeFileSync(output, JSON.stringify(swaggerJson, null, 4));
190 | }
191 |
192 | function getSwaggerHeader({
193 | basePath = '/',
194 | swaggerInfo = null,
195 | swaggerSecurityDefinitions = null
196 | }) {
197 | const swaggerHeader = {
198 | swagger: '2.0',
199 | info: swaggerInfo || sampleSwaggerInfo,
200 | basePath: basePath,
201 | schemes: ['https', 'http'],
202 | consumes: ['application/json'],
203 | produces: ['application/json']
204 | };
205 |
206 | if (swaggerSecurityDefinitions) {
207 | swaggerHeader.securityDefinitions = swaggerSecurityDefinitions;
208 | swaggerHeader.security = Object.keys(swaggerSecurityDefinitions).map(
209 | (s) => ({
210 | [s]: []
211 | })
212 | );
213 | }
214 |
215 | return swaggerHeader;
216 | }
217 |
218 | module.exports = {
219 | getSwaggerHeader,
220 | registerExpress,
221 | convertDocsToSwaggerDoc,
222 | writeSwaggerJson,
223 | _sanitizeSwaggerPath
224 | };
225 |
--------------------------------------------------------------------------------
/__tests__/RouteUtil.test.js:
--------------------------------------------------------------------------------
1 | const BaseController = require('../src/BaseController');
2 | const RouteUtil = require('../src/RouteUtil');
3 |
4 | const mockSubroutes = [
5 | {
6 | path: '/users',
7 | router: {
8 | routes: [
9 | {
10 | path: '/',
11 | method: 'get',
12 | controller: new BaseController()
13 | },
14 | {
15 | path: '/',
16 | method: 'post',
17 | controller: new BaseController()
18 | }
19 | ],
20 | subroutes: [
21 | {
22 | path: '/:userId/posts',
23 | router: {
24 | routes: [
25 | {
26 | path: '/',
27 | method: 'get',
28 | controller: new BaseController()
29 | },
30 | {
31 | path: '/',
32 | method: 'post',
33 | controller: new BaseController()
34 | }
35 | ]
36 | }
37 | }
38 | ]
39 | }
40 | }
41 | ];
42 |
43 | describe('RouteUtil', () => {
44 | describe('getRoutesInfo', () => {
45 | it('Should register all routes and subroutes with redirects', () => {
46 | const expectedRoutes = [
47 | {
48 | path: '/',
49 | parentPath: '',
50 | method: 'get',
51 | doc: 'hello',
52 | controllerName: 'BaseController'
53 | },
54 | {
55 | path: '/users/',
56 | parentPath: '/users',
57 | method: 'get',
58 | controllerName: 'BaseController'
59 | },
60 | {
61 | path: '/users/',
62 | parentPath: '/users',
63 | method: 'post',
64 | controllerName: 'BaseController'
65 | },
66 | {
67 | path: '/users/:userId/posts/',
68 | parentPath: '/users/:userId/posts',
69 | method: 'get',
70 | controllerName: 'BaseController'
71 | },
72 | {
73 | path: '/users/:userId/posts/',
74 | parentPath: '/users/:userId/posts',
75 | method: 'post',
76 | controllerName: 'BaseController'
77 | }
78 | ];
79 |
80 | const rootController = new BaseController();
81 | rootController.doc = 'hello';
82 | const mockRouter = {
83 | routes: [
84 | {
85 | path: '/',
86 | method: 'get',
87 | controller: rootController
88 | }
89 | ],
90 | subroutes: mockSubroutes
91 | };
92 |
93 | const result = RouteUtil.getRoutesInfo(mockRouter);
94 | expect(result).toEqual(expectedRoutes);
95 | });
96 |
97 | it('Should register all subroutes from top', () => {
98 | const expectedRoutes = [
99 | {
100 | path: '/users/',
101 | parentPath: '/users',
102 | method: 'get',
103 | controllerName: 'BaseController'
104 | },
105 | {
106 | path: '/users/',
107 | parentPath: '/users',
108 | method: 'post',
109 | controllerName: 'BaseController'
110 | },
111 | {
112 | path: '/users/:userId/posts/',
113 | parentPath: '/users/:userId/posts',
114 | method: 'get',
115 | controllerName: 'BaseController'
116 | },
117 | {
118 | path: '/users/:userId/posts/',
119 | parentPath: '/users/:userId/posts',
120 | method: 'post',
121 | controllerName: 'BaseController'
122 | }
123 | ];
124 |
125 | const mockRouter = {
126 | subroutes: mockSubroutes
127 | };
128 |
129 | const result = RouteUtil.getRoutesInfo(mockRouter);
130 | expect(result).toEqual(expectedRoutes);
131 | });
132 | });
133 |
134 | describe('getHandlerWithManagedNextCall', () => {
135 | it('Should return handler with next call managed if no next defined', async () => {
136 | const fn = RouteUtil.getHandlerWithManagedNextCall(
137 | async (req, res) => 123
138 | );
139 |
140 | const mockReq = 1;
141 | const mockRes = 2;
142 | const mockNext = jest.fn();
143 |
144 | await fn(mockReq, mockRes, mockNext);
145 |
146 | expect(mockNext).toHaveBeenCalled();
147 | });
148 |
149 | it('Should return regular handler if 3 args', async () => {
150 | const fn = RouteUtil.getHandlerWithManagedNextCall(
151 | async (req, res, next) => next(123)
152 | );
153 |
154 | const mockReq = 1;
155 | const mockRes = 2;
156 | const mockNext = jest.fn();
157 |
158 | await fn(mockReq, mockRes, mockNext);
159 |
160 | expect(mockNext).toHaveBeenCalledWith(123);
161 | });
162 |
163 | it('Should return handler with proper catch block', async () => {
164 | const someError = new Error('Some error');
165 |
166 | const fn = RouteUtil.getHandlerWithManagedNextCall(
167 | async (req, res, next) => {
168 | throw someError;
169 | }
170 | );
171 |
172 | const mockReq = 1;
173 | const mockRes = 2;
174 | const mockNext = jest.fn();
175 |
176 | await fn(mockReq, mockRes, mockNext);
177 |
178 | expect(mockNext).toHaveBeenCalledWith(someError);
179 | });
180 | });
181 |
182 | describe('getErrorHandlerWithManagedNextCall', () => {
183 | it('Should return handler with next call managed if no next defined', async () => {
184 | const fn = RouteUtil.getErrorHandlerWithManagedNextCall(
185 | async (err, req, res) => 123
186 | );
187 |
188 | const mockReq = 1;
189 | const mockRes = 2;
190 | const mockNext = jest.fn();
191 |
192 | await fn(null, mockReq, mockRes, mockNext);
193 |
194 | expect(mockNext).toHaveBeenCalled();
195 | });
196 |
197 | it('Should return regular handler if 3 args', async () => {
198 | const fn = RouteUtil.getErrorHandlerWithManagedNextCall(
199 | async (err, req, res, next) => next(123)
200 | );
201 |
202 | const mockReq = 1;
203 | const mockRes = 2;
204 | const mockNext = jest.fn();
205 |
206 | await fn(null, mockReq, mockRes, mockNext);
207 |
208 | expect(mockNext).toHaveBeenCalledWith(123);
209 | });
210 |
211 | it('Should return handler with proper catch block', async () => {
212 | const someError = new Error('Some error');
213 |
214 | const fn = RouteUtil.getErrorHandlerWithManagedNextCall(
215 | async (err, req, res, next) => {
216 | throw someError;
217 | }
218 | );
219 |
220 | const mockReq = 1;
221 | const mockRes = 2;
222 | const mockNext = jest.fn();
223 |
224 | await fn(null, mockReq, mockRes, mockNext);
225 |
226 | expect(mockNext).toHaveBeenCalledWith(someError);
227 | });
228 | });
229 |
230 | // describe('isFunction', () => {
231 | // it('Should return false if it is a class', () => {
232 | // class SomeClass {}
233 |
234 | // const result = RouteUtil.isFunction(SomeClass);
235 |
236 | // expect(result).toBeFalsy();
237 | // });
238 |
239 | // it('Should return true if a named function', () => {
240 | // function someFunc() {}
241 |
242 | // const result = RouteUtil.isFunction(someFunc);
243 |
244 | // expect(result).toBeTruthy();
245 | // });
246 |
247 | // it('Should return true if an unnamed function', () => {
248 | // const result = RouteUtil.isFunction(() => {});
249 |
250 | // expect(result).toBeTruthy();
251 | // });
252 | // });
253 |
254 | describe('isUrlPath', () => {
255 | it('Should return true for valid urls', () => {
256 | ['/hehe/some', '/hehe', '/hehe-blah/:id/ahhs/:someId/hash', '/'].forEach(
257 | (str) => {
258 | expect(RouteUtil.isUrlPath(str)).toBeTruthy();
259 | }
260 | );
261 | });
262 |
263 | it('Should return false for invalid urls', () => {
264 | ['ajdlasjdksad', '10980du90', '/hh*ehe', 'hehe/', '/heheh ooo'].forEach(
265 | (str) => {
266 | expect(RouteUtil.isUrlPath(str)).toBeFalsy();
267 | }
268 | );
269 | });
270 |
271 | it('Should return false if non string input is given', () => {
272 | expect(RouteUtil.isUrlPath(() => {})).toBeFalsy();
273 | expect(RouteUtil.isUrlPath(class SomeClass {})).toBeFalsy();
274 | });
275 | });
276 | });
277 |
--------------------------------------------------------------------------------
/__tests__/BaseController.test.js:
--------------------------------------------------------------------------------
1 | const BaseController = require('../src/BaseController');
2 |
3 | describe('BaseController', () => {
4 | it('should be defined and should have handleRequest method', () => {
5 | expect(BaseController).toBeDefined();
6 |
7 | const base = new BaseController();
8 | base.handleRequest();
9 | expect(base.handleRequest).toBeDefined();
10 | });
11 |
12 | it('should throw error if handleRequest is not implemented', async () => {
13 | class SomeController extends BaseController {}
14 |
15 | const controller = new SomeController();
16 |
17 | let result;
18 | try {
19 | await controller.handleRequest();
20 | } catch (error) {
21 | result = error;
22 | }
23 |
24 | expect(result).toBeInstanceOf(Error);
25 | expect(result.message).toEqual(
26 | `'handleRequest' not implemented in SomeController`
27 | );
28 | });
29 |
30 | describe('response', () => {
31 | const controller = new BaseController();
32 |
33 | const mockRes = {
34 | status: jest.fn(),
35 | send: jest.fn(),
36 | json: jest.fn(),
37 | jsonp: jest.fn(),
38 | sendFile: jest.fn(),
39 | sendStatus: jest.fn(),
40 | end: jest.fn()
41 | };
42 |
43 | controller.res = mockRes;
44 |
45 | beforeEach(() => {
46 | Object.keys(mockRes).forEach((fn) => {
47 | mockRes[fn].mockClear();
48 | });
49 | });
50 |
51 | it('should handle method ok properly', () => {
52 | controller.ok();
53 | expect(mockRes.sendStatus).toHaveBeenCalledWith(200);
54 |
55 | controller.ok(123);
56 | expect(mockRes.status).toHaveBeenCalledWith(200);
57 | expect(mockRes.json).toHaveBeenCalledWith(123);
58 | });
59 |
60 | it('should handle method created properly with dto', () => {
61 | controller.created();
62 | expect(mockRes.sendStatus).toHaveBeenCalledWith(201);
63 |
64 | controller.created(123);
65 | expect(mockRes.status).toHaveBeenCalledWith(201);
66 | expect(mockRes.json).toHaveBeenCalledWith(123);
67 | });
68 |
69 | it('should handle method accepted properly with dto', () => {
70 | controller.accepted();
71 | expect(mockRes.sendStatus).toHaveBeenCalledWith(202);
72 |
73 | controller.accepted(123);
74 | expect(mockRes.status).toHaveBeenCalledWith(202);
75 | expect(mockRes.json).toHaveBeenCalledWith(123);
76 | });
77 |
78 | it('should handle method noContent properly with dto', () => {
79 | controller.noContent();
80 | expect(mockRes.sendStatus).toHaveBeenCalledWith(204);
81 | });
82 |
83 | it('should handle method badRequest properly with message', () => {
84 | controller.badRequest();
85 | expect(mockRes.status).toHaveBeenCalledWith(400);
86 | expect(mockRes.json).toHaveBeenCalledWith({
87 | message: 'Bad request'
88 | });
89 |
90 | controller.badRequest('some message');
91 | expect(mockRes.status).toHaveBeenCalledWith(400);
92 | expect(mockRes.json).toHaveBeenCalledWith({
93 | message: 'some message'
94 | });
95 | });
96 |
97 | it('should handle method unauthorized properly with message', () => {
98 | controller.unauthorized();
99 | expect(mockRes.status).toHaveBeenCalledWith(401);
100 | expect(mockRes.json).toHaveBeenCalledWith({
101 | message: 'Unauthorized'
102 | });
103 |
104 | controller.unauthorized('some message');
105 | expect(mockRes.status).toHaveBeenCalledWith(401);
106 | expect(mockRes.json).toHaveBeenCalledWith({
107 | message: 'some message'
108 | });
109 | });
110 |
111 | it('should handle method forbidden properly with message', () => {
112 | controller.forbidden();
113 | expect(mockRes.status).toHaveBeenCalledWith(403);
114 | expect(mockRes.json).toHaveBeenCalledWith({
115 | message: 'Forbidden'
116 | });
117 |
118 | controller.forbidden('some message');
119 | expect(mockRes.status).toHaveBeenCalledWith(403);
120 | expect(mockRes.json).toHaveBeenCalledWith({
121 | message: 'some message'
122 | });
123 | });
124 |
125 | it('should handle method notFound properly with message', () => {
126 | controller.notFound();
127 | expect(mockRes.status).toHaveBeenCalledWith(404);
128 | expect(mockRes.json).toHaveBeenCalledWith({
129 | message: 'Not Found'
130 | });
131 |
132 | controller.notFound('some message');
133 | expect(mockRes.status).toHaveBeenCalledWith(404);
134 | expect(mockRes.json).toHaveBeenCalledWith({
135 | message: 'some message'
136 | });
137 | });
138 |
139 | it('should handle method tooMany properly with message', () => {
140 | controller.tooMany();
141 | expect(mockRes.status).toHaveBeenCalledWith(429);
142 | expect(mockRes.json).toHaveBeenCalledWith({
143 | message: 'Too many requests'
144 | });
145 |
146 | controller.tooMany('some message');
147 | expect(mockRes.status).toHaveBeenCalledWith(429);
148 | expect(mockRes.json).toHaveBeenCalledWith({
149 | message: 'some message'
150 | });
151 | });
152 |
153 | it('Should handle method internalServerError properly', () => {
154 | controller.internalServerError();
155 | expect(mockRes.status).toHaveBeenCalledWith(500);
156 | expect(mockRes.json).toHaveBeenCalledWith({
157 | message: 'Internal server error'
158 | });
159 |
160 | controller.internalServerError('some message');
161 | expect(mockRes.json).toHaveBeenCalledWith({
162 | message: 'some message'
163 | });
164 |
165 | controller.internalServerError('some message', {
166 | some: 'data'
167 | });
168 | expect(mockRes.json).toHaveBeenCalledWith({
169 | message: 'some message',
170 | some: 'data'
171 | });
172 | });
173 |
174 | it('Should handle method notImplemented properly', () => {
175 | controller.notImplemented();
176 | expect(mockRes.status).toHaveBeenCalledWith(501);
177 | expect(mockRes.json).toHaveBeenCalledWith({
178 | message: 'Not implemented'
179 | });
180 |
181 | controller.notImplemented('some message');
182 | expect(mockRes.json).toHaveBeenCalledWith({
183 | message: 'some message'
184 | });
185 | });
186 | });
187 |
188 | it('should return all data for getData given all params have some data', () => {
189 | const req = {
190 | body: {
191 | name: 'john'
192 | },
193 | query: {
194 | age: 2
195 | },
196 | params: {
197 | userId: 4
198 | },
199 | fileUpload: {
200 | file: 1,
201 | files: 2
202 | }
203 | };
204 |
205 | const controller = new BaseController();
206 |
207 | controller.req = req;
208 |
209 | expect(controller.getData()).toEqual(req);
210 | });
211 |
212 | it('should return all data for getData given only body has data', () => {
213 | const req = {
214 | body: {
215 | name: 'john'
216 | }
217 | };
218 | const controller = new BaseController();
219 |
220 | controller.req = req;
221 |
222 | expect(controller.getData().body).toEqual({
223 | name: 'john'
224 | });
225 | });
226 |
227 | it('should return empty object for no data from getData', () => {
228 | const req = {};
229 | const controller = new BaseController();
230 |
231 | controller.req = req;
232 |
233 | expect(controller.getData()).toEqual({});
234 | });
235 |
236 | it('should return headers from getHeaders', () => {
237 | const req = {
238 | headers: {
239 | name: 'john'
240 | }
241 | };
242 | const controller = new BaseController();
243 |
244 | controller.req = req;
245 |
246 | expect(controller.getHeaders()).toEqual({
247 | name: 'john'
248 | });
249 | });
250 |
251 | it('should return cookies from getCookies', () => {
252 | const req = {
253 | cookies: {
254 | name: 'john'
255 | },
256 | signedCookies: {
257 | some: 'otherCookie'
258 | }
259 | };
260 | const controller = new BaseController();
261 |
262 | controller.req = req;
263 |
264 | expect(controller.getCookies()).toEqual(req);
265 | });
266 |
267 | it('should call internal handler if this.ok is called', () => {
268 | class SomeController extends BaseController {
269 | handleRequest() {
270 | this.ok({ some: 'data' });
271 | }
272 | }
273 | const someRes = {
274 | status: jest.fn(),
275 | json: jest.fn()
276 | };
277 | const mockHandleInternalError = jest.fn();
278 |
279 | const controller = new SomeController();
280 | controller.res = someRes;
281 |
282 | controller.internalServerError = mockHandleInternalError;
283 |
284 | controller.handleRequest();
285 |
286 | expect(someRes.json).toHaveBeenCalled();
287 | expect(someRes.status).toHaveBeenCalled();
288 | expect(mockHandleInternalError).not.toHaveBeenCalled();
289 | });
290 | });
291 |
--------------------------------------------------------------------------------
/src/CelebrateUtils.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable new-cap */
2 | const { Joi, CelebrateError, Segments } = require('celebrate');
3 | const Utils = require('./Utils');
4 |
5 | function _isJoiObject(obj) {
6 | return Boolean(obj.type === 'object' && obj.$_terms);
7 | }
8 |
9 | function _getJoiObjectKeys(obj) {
10 | return obj.$_terms.keys;
11 | }
12 |
13 | function _getTypeFromjoiSchema(joiSchema) {
14 | const { type } = joiSchema;
15 | if (['string', 'array', 'object', 'boolean'].includes(type)) {
16 | return joiSchema.type;
17 | }
18 |
19 | if (type === 'number') {
20 | return (
21 | (joiSchema._rules.some((e) => e.name === 'integer') && 'integer') ||
22 | 'number'
23 | );
24 | }
25 |
26 | return 'string';
27 | }
28 |
29 | function _getMinMaxFromSchemaDefition(joiSchema) {
30 | let min = joiSchema._rules.find((r) => r.name === 'min');
31 | min = (min && min.args.limit) || null;
32 |
33 | let max = joiSchema._rules.find((r) => r.name === 'max');
34 | max = (max && max.args.limit) || null;
35 |
36 | return {
37 | min,
38 | max
39 | };
40 | }
41 |
42 | function _setMinMaxInSwaggerSchema(type, joiSchema, swaggerSchema) {
43 | const { min, max } = _getMinMaxFromSchemaDefition(joiSchema);
44 |
45 | let minKey = 'minimum';
46 | let maxKey = 'maximum';
47 |
48 | if (type === 'string') {
49 | minKey = 'minLength';
50 | maxKey = 'maxLength';
51 | } else if (type === 'array') {
52 | minKey = 'minItems';
53 | maxKey = 'maxItems';
54 | }
55 |
56 | swaggerSchema[minKey] = min;
57 | swaggerSchema[maxKey] = max;
58 | }
59 |
60 | function _setSwaggerPropsForObject(type, joiSchema, swaggerSchema) {
61 | if (
62 | !(
63 | type === 'object' &&
64 | _getJoiObjectKeys(joiSchema) &&
65 | _getJoiObjectKeys(joiSchema).length > 0
66 | )
67 | )
68 | return;
69 | const requiredProperties = [];
70 | const objectjoiSchemaMap = {};
71 |
72 | _getJoiObjectKeys(joiSchema).forEach((objectSchema) => {
73 | if (objectSchema.schema._flags.presence === 'required') {
74 | requiredProperties.push(objectSchema.key);
75 | }
76 | objectjoiSchemaMap[objectSchema.key] = _getSchemaDefinitionForSwagger(
77 | objectSchema.schema
78 | );
79 | });
80 | swaggerSchema.required =
81 | (requiredProperties.length > 0 && requiredProperties) || null;
82 | swaggerSchema.properties = objectjoiSchemaMap;
83 | }
84 |
85 | function _setSwaggerPropsForArray(type, joiSchema, swaggerSchema) {
86 | if (type !== 'array') return;
87 |
88 | const hasItemSchema = joiSchema.$_terms.items.length > 0;
89 | swaggerSchema.items =
90 | (hasItemSchema &&
91 | _getSchemaDefinitionForSwagger(joiSchema.$_terms.items[0])) ||
92 | {};
93 | }
94 |
95 | function _setMultipleOfSwaggerSchema(joiSchema, swaggerSchema) {
96 | const multipleOf = joiSchema._rules.find((r) => r.name === 'multiple');
97 | swaggerSchema.multipleOf = (multipleOf && multipleOf.args.base) || null;
98 | }
99 |
100 | function _setPatternSwaggerSchema(joiSchema, swaggerSchema) {
101 | const pattern = joiSchema._rules.find((r) => r.name === 'pattern');
102 | swaggerSchema.pattern = (pattern && String(pattern.args.regex)) || null;
103 | }
104 |
105 | function _setDefaultValueForSwaggerSchema(joiSchema, swaggerSchema) {
106 | const defaultValue = joiSchema._flags.default;
107 | swaggerSchema.default =
108 | (defaultValue && JSON.stringify(defaultValue)) || null;
109 | }
110 |
111 | function _setSwaggerPropsEnums(joiSchema, swaggerSchema) {
112 | if (!joiSchema._valids) return;
113 | const validValues = [...joiSchema._valids._values.values()];
114 | swaggerSchema.enum = validValues;
115 | }
116 |
117 | function _setSwaggerPropsNullable(swaggerSchema) {
118 | if (swaggerSchema.enum && swaggerSchema.enum.includes(null)) {
119 | swaggerSchema.nullable = true;
120 | }
121 | }
122 |
123 | function _getSchemaDefinitionForSwagger(joiSchema) {
124 | const type = _getTypeFromjoiSchema(joiSchema);
125 |
126 | const swaggerSchema = {
127 | type
128 | };
129 |
130 | _setDefaultValueForSwaggerSchema(joiSchema, swaggerSchema);
131 | _setMultipleOfSwaggerSchema(joiSchema, swaggerSchema);
132 | _setPatternSwaggerSchema(joiSchema, swaggerSchema);
133 | _setSwaggerPropsEnums(joiSchema, swaggerSchema);
134 | _setSwaggerPropsNullable(swaggerSchema);
135 | _setMinMaxInSwaggerSchema(type, joiSchema, swaggerSchema);
136 | _setSwaggerPropsForObject(type, joiSchema, swaggerSchema);
137 | _setSwaggerPropsForArray(type, joiSchema, swaggerSchema);
138 |
139 | Utils.clearNullValuesInObject(swaggerSchema);
140 |
141 | return swaggerSchema;
142 | }
143 |
144 | function _getAllSwaggerParamsFromValidationSchema(schema, paramIn) {
145 | return Object.keys(schema).map((key) => {
146 | const joiSchema = schema[key];
147 | const schemaDefinition = _getSchemaDefinitionForSwagger(joiSchema);
148 | const isRequired = joiSchema._flags.presence === 'required';
149 | const swaggerParams = {
150 | name: key,
151 | in: paramIn,
152 | required: isRequired,
153 | schema: schemaDefinition
154 | };
155 |
156 | if (paramIn === 'fileUpload') {
157 | swaggerParams.type = 'file';
158 | swaggerParams.in = 'formData';
159 | }
160 | return swaggerParams;
161 | });
162 | }
163 |
164 | function _isJoiAny(schema) {
165 | return Boolean(schema.type === 'any' && schema.$_terms);
166 | }
167 |
168 | function _getSwaggerParamsForBody(bodySchema) {
169 | const joiSchema =
170 | _isJoiObject(bodySchema) || _isJoiAny(bodySchema)
171 | ? bodySchema
172 | : Joi.object(bodySchema);
173 |
174 | const swaggerSchema = {
175 | type: 'object'
176 | };
177 |
178 | _setSwaggerPropsForObject('object', joiSchema, swaggerSchema);
179 | Utils.clearNullValuesInObject(swaggerSchema);
180 |
181 | return {
182 | name: 'body',
183 | in: 'body',
184 | schema: swaggerSchema
185 | };
186 | }
187 |
188 | function _getObjectNormalizedSchema(schema) {
189 | if (_isJoiObject(schema)) {
190 | return _getJoiObjectKeys(schema).reduce((map, schemaObj) => {
191 | map[schemaObj.key] = schemaObj.schema;
192 | return map;
193 | }, {});
194 | }
195 | return schema;
196 | }
197 |
198 | function _lowercaseHeaderSchemaKeys(headerSchema, paramLocation = 'header') {
199 | if (paramLocation === 'header') {
200 | Object.keys(headerSchema).forEach((key) => {
201 | headerSchema[key.toLowerCase()] = headerSchema[key];
202 | delete headerSchema[key];
203 | });
204 | }
205 | }
206 |
207 | function _addSwaggerParamsForNonBodyProps(parameterKeyMap, parameters) {
208 | Object.keys(parameterKeyMap).forEach((paramLocation) => {
209 | const normalizedSchema = _getObjectNormalizedSchema(
210 | parameterKeyMap[paramLocation]
211 | );
212 | _lowercaseHeaderSchemaKeys(normalizedSchema, paramLocation);
213 | const swaggerParams = _getAllSwaggerParamsFromValidationSchema(
214 | normalizedSchema,
215 | paramLocation
216 | );
217 | parameters.push(...swaggerParams);
218 | });
219 | }
220 |
221 | function joiSchemaToSwaggerRequestParameters(validationSchema) {
222 | if (!validationSchema) return [];
223 |
224 | const {
225 | query,
226 | params: path,
227 | headers: header,
228 | body,
229 | fileUpload
230 | } = validationSchema;
231 |
232 | const parameters = [];
233 | if (body && Object.keys(body).length > 0) {
234 | parameters.push(_getSwaggerParamsForBody(body));
235 | }
236 |
237 | const parameterKeyMap = { query, path, header, fileUpload };
238 | Utils.clearNullValuesInObject(parameterKeyMap);
239 | _addSwaggerParamsForNonBodyProps(parameterKeyMap, parameters);
240 |
241 | return parameters;
242 | }
243 |
244 | function lowercaseHeaderSchemaProperties(validationSchema) {
245 | if (!validationSchema.headers) return;
246 |
247 | const { headers } = validationSchema;
248 |
249 | if (_isJoiObject(headers)) {
250 | _getJoiObjectKeys(validationSchema.headers).forEach((obj) => {
251 | obj.key = obj.key.toLowerCase();
252 | });
253 |
254 | const byKey = validationSchema.headers._ids._byKey;
255 | const keysForById = [...byKey.keys()];
256 | keysForById.forEach((key) => {
257 | const lowercaseKey = key.toLowerCase();
258 | const mapValue = byKey.get(key);
259 | mapValue.id = mapValue.id.toLowerCase();
260 | byKey.set(lowercaseKey, mapValue);
261 | byKey.delete(key);
262 | });
263 | } else {
264 | _lowercaseHeaderSchemaKeys(validationSchema.headers);
265 | }
266 | }
267 |
268 | function getSanitizedValidationSchema(validationSchema) {
269 | if (
270 | validationSchema.fileUpload &&
271 | Object.keys(validationSchema).length === 1
272 | ) {
273 | return null;
274 | }
275 |
276 | const newValidationSchema = { ...validationSchema };
277 | delete newValidationSchema.fileUpload;
278 | return newValidationSchema;
279 | }
280 |
281 | function getCelebrateValidationMiddlewareForFileUpload(fileUploadValidation) {
282 | let schema = fileUploadValidation;
283 | if (!_isJoiObject(schema)) {
284 | schema = Joi.object(fileUploadValidation);
285 | }
286 |
287 | return async (req, _, next) => {
288 | const { file, files } = req;
289 | const testObj = {
290 | ...(file && { file }),
291 | ...(files && { files })
292 | };
293 |
294 | try {
295 | await schema.validateAsync(testObj);
296 | return next();
297 | } catch (error) {
298 | error.isJoi = true;
299 | return next(
300 | new CelebrateError(error, Segments.BODY, { celebrated: true })
301 | );
302 | }
303 | };
304 | }
305 |
306 | module.exports = {
307 | joiSchemaToSwaggerRequestParameters,
308 | lowercaseHeaderSchemaProperties,
309 | getCelebrateValidationMiddlewareForFileUpload,
310 | getSanitizedValidationSchema
311 | };
312 |
--------------------------------------------------------------------------------
/__tests__/RouterFactory.test.js:
--------------------------------------------------------------------------------
1 | const RouterFactory = require('../src/RouterFactory');
2 | const BaseController = require('../src/BaseController');
3 | const Route = require('../src/Route');
4 |
5 | class MockControllerThrowsError extends BaseController {}
6 | const mockErrorJestFn = jest.fn().mockImplementation(() => {
7 | throw new Error('mockErrorJestFn');
8 | });
9 | MockControllerThrowsError.prototype.handleRequest = mockErrorJestFn;
10 |
11 | const mockSubroutes = [
12 | {
13 | path: '/users',
14 | router: {
15 | routes: [
16 | Route.get('/', new BaseController()),
17 | Route.post('/', new BaseController())
18 | ],
19 | subroutes: [
20 | {
21 | path: '/:userId/posts',
22 | router: {
23 | routes: [
24 | Route.get('/', new BaseController()),
25 | Route.post('/', new BaseController())
26 | ]
27 | }
28 | }
29 | ]
30 | }
31 | }
32 | ];
33 |
34 | describe('RouterFactory', () => {
35 | beforeEach(() => {
36 | mockErrorJestFn.mockClear();
37 | });
38 |
39 | afterEach(() => {
40 | mockErrorJestFn.mockClear();
41 | });
42 |
43 | describe('getExpressRouter', () => {
44 | it('Should register all routes and subroutes', () => {
45 | const mockRouter = {
46 | routes: [
47 | {
48 | path: '/',
49 | method: 'get',
50 | controller: new BaseController()
51 | }
52 | ],
53 | subroutes: mockSubroutes
54 | };
55 |
56 | const routerFactory = new RouterFactory({});
57 | const mockExpressRouter = {
58 | get: jest.fn(),
59 | use: jest.fn(),
60 | post: jest.fn()
61 | };
62 | routerFactory._getRouter = jest.fn().mockReturnValue(mockExpressRouter);
63 | routerFactory.getExpressRouter(mockRouter);
64 |
65 | expect(mockExpressRouter.get).toHaveBeenCalledTimes(3);
66 | expect(mockExpressRouter.get.mock.calls[0][0]).toEqual('/');
67 | expect(mockExpressRouter.get.mock.calls[1][0]).toEqual('/');
68 | expect(mockExpressRouter.get.mock.calls[2][0]).toEqual('/');
69 |
70 | expect(mockExpressRouter.post).toHaveBeenCalledTimes(2);
71 | expect(mockExpressRouter.get.mock.calls[0][0]).toEqual('/');
72 | expect(mockExpressRouter.get.mock.calls[1][0]).toEqual('/');
73 |
74 | expect(mockExpressRouter.use).toHaveBeenCalledTimes(2);
75 | expect(mockExpressRouter.use.mock.calls[0][0]).toEqual('/:userId/posts');
76 | expect(mockExpressRouter.use.mock.calls[1][0]).toEqual('/users');
77 | });
78 |
79 | it('Should register all subroutes from top', () => {
80 | const mockRouter = {
81 | subroutes: mockSubroutes
82 | };
83 | const routerFactory = new RouterFactory({});
84 | const mockExpressRouter = {
85 | get: jest.fn(),
86 | use: jest.fn(),
87 | post: jest.fn()
88 | };
89 | routerFactory._getRouter = jest.fn().mockReturnValue(mockExpressRouter);
90 | routerFactory.getExpressRouter(mockRouter);
91 |
92 | expect(mockExpressRouter.get).toHaveBeenCalledTimes(2);
93 | expect(mockExpressRouter.get.mock.calls[0][0]).toEqual('/');
94 | expect(mockExpressRouter.get.mock.calls[1][0]).toEqual('/');
95 |
96 | expect(mockExpressRouter.post).toHaveBeenCalledTimes(2);
97 | expect(mockExpressRouter.get.mock.calls[0][0]).toEqual('/');
98 | expect(mockExpressRouter.get.mock.calls[1][0]).toEqual('/');
99 |
100 | expect(mockExpressRouter.use).toHaveBeenCalledTimes(2);
101 | expect(mockExpressRouter.use.mock.calls[0][0]).toEqual('/:userId/posts');
102 | expect(mockExpressRouter.use.mock.calls[1][0]).toEqual('/users');
103 | });
104 | });
105 |
106 | describe('_registerRoute', () => {
107 | it('Should register route with middleware properly', () => {
108 | const factory = new RouterFactory({});
109 |
110 | const mockExpressRouter = {
111 | get: jest.fn(),
112 | use: jest.fn(),
113 | post: jest.fn()
114 | };
115 |
116 | factory.routeUtil = {
117 | getDuplicateUrls: jest.fn().mockReturnValue([]),
118 | getHandlerWithManagedNextCall: jest.fn()
119 | };
120 |
121 | class SomeController extends BaseController {}
122 | const controller = new SomeController();
123 | controller.router = {
124 | routes: [Route.get('/hello', new BaseController())]
125 | };
126 | controller.middleware = [(req, res) => 1, (req, res) => 2];
127 | controller.authorizer = (req, res) => {};
128 |
129 | factory._registerRoute(mockExpressRouter, {
130 | method: 'get',
131 | path: '/',
132 | controller
133 | });
134 |
135 | expect(
136 | factory.routeUtil.getHandlerWithManagedNextCall
137 | ).toHaveBeenCalledTimes(2);
138 | expect(mockExpressRouter.get).toHaveBeenCalled();
139 | });
140 |
141 | it('Should use celebrate validation schema', () => {
142 | const factory = new RouterFactory({});
143 |
144 | factory.celebrateMiddleware = jest.fn().mockReturnValue(1);
145 |
146 | const mockExpressRouter = {
147 | get: jest.fn(),
148 | use: jest.fn(),
149 | post: jest.fn()
150 | };
151 |
152 | factory.routeUtil = {
153 | getHandlerWithManagedNextCall: jest.fn()
154 | };
155 |
156 | const schema = {
157 | body: {
158 | someId: 1
159 | }
160 | };
161 |
162 | class SomeController extends BaseController {}
163 | const controller = new SomeController();
164 | controller.middleware = [(req, res) => 1, (req, res) => 2];
165 | controller.authorizer = (req, res) => {};
166 | controller.validationSchema = schema;
167 |
168 | factory._registerRoute(mockExpressRouter, {
169 | method: 'get',
170 | path: '/',
171 | controller
172 | });
173 |
174 | expect(factory.celebrateMiddleware).toHaveBeenCalledWith(schema, {
175 | abortEarly: false
176 | });
177 | expect(
178 | factory.routeUtil.getHandlerWithManagedNextCall
179 | ).toHaveBeenCalledTimes(2);
180 | expect(mockExpressRouter.get).toHaveBeenCalled();
181 | });
182 |
183 | it('Should not use celebrate validation schema with file upload', () => {
184 | const factory = new RouterFactory({});
185 |
186 | factory.celebrateMiddleware = jest.fn().mockReturnValue(1);
187 |
188 | const mockExpressRouter = {
189 | get: jest.fn(),
190 | use: jest.fn(),
191 | post: jest.fn()
192 | };
193 |
194 | factory.routeUtil = {
195 | getHandlerWithManagedNextCall: jest.fn()
196 | };
197 |
198 | const schema = {
199 | fileUpload: {
200 | someId: 1
201 | }
202 | };
203 |
204 | class SomeCtr extends BaseController {}
205 | const controller = new SomeCtr();
206 | controller.middleware = [(req, res) => 1, (req, res) => 2];
207 | controller.authorizer = (req, res) => {};
208 | controller.validationSchema = schema;
209 |
210 | factory._registerRoute(mockExpressRouter, {
211 | method: 'get',
212 | path: '/',
213 | controller
214 | });
215 |
216 | expect(factory.celebrateMiddleware).not.toHaveBeenCalledWith(schema);
217 | expect(
218 | factory.routeUtil.getHandlerWithManagedNextCall
219 | ).toHaveBeenCalledTimes(2);
220 | expect(mockExpressRouter.get).toHaveBeenCalled();
221 | });
222 | });
223 |
224 | describe('_registerPreHandlers', () => {
225 | it('Should not change routerArgs if no pre handlers defined', () => {
226 | const factory = new RouterFactory({});
227 |
228 | const routerArgs = [];
229 |
230 | factory._registerPreHandlers(routerArgs, null);
231 |
232 | expect(routerArgs).toEqual([]);
233 | });
234 |
235 | it('Should register handler if single handler defined', () => {
236 | const factory = new RouterFactory({});
237 | factory.routeUtil = {
238 | getHandlerWithManagedNextCall: jest.fn().mockReturnValue(123)
239 | };
240 | const routerArgs = [];
241 | const handler = (req, res) => {};
242 | factory._registerPreHandlers(routerArgs, handler);
243 |
244 | expect(routerArgs).toEqual([123]);
245 | expect(
246 | factory.routeUtil.getHandlerWithManagedNextCall
247 | ).toHaveBeenCalledWith(handler);
248 | });
249 |
250 | it('Should register handler if array of handlers defined', () => {
251 | const factory = new RouterFactory({});
252 | factory.routeUtil = {
253 | getHandlerWithManagedNextCall: jest.fn().mockReturnValue(123)
254 | };
255 | const routerArgs = [];
256 | const handlers = [
257 | (req, res) => {
258 | return 1;
259 | },
260 | (req, res) => {
261 | return 2;
262 | }
263 | ];
264 | factory._registerPreHandlers(routerArgs, handlers);
265 |
266 | expect(routerArgs).toEqual([123, 123]);
267 | expect(
268 | factory.routeUtil.getHandlerWithManagedNextCall
269 | ).toHaveBeenCalledWith(handlers[0]);
270 | expect(
271 | factory.routeUtil.getHandlerWithManagedNextCall
272 | ).toHaveBeenCalledWith(handlers[1]);
273 | });
274 | });
275 |
276 | describe('_getWrappedController', () => {
277 | it('should execute given base controller child', async () => {
278 | const factory = new RouterFactory();
279 |
280 | const mockFn = jest.fn();
281 |
282 | class SomeController extends BaseController {
283 | handleRequest() {
284 | mockFn();
285 | }
286 | }
287 |
288 | const controller = new SomeController();
289 |
290 | const mockHandleInternalError = jest.fn();
291 | controller.internalServerError = mockHandleInternalError;
292 |
293 | const fn = factory._getWrappedController(controller);
294 |
295 | const someReq = 1;
296 | const someRes = {
297 | status: jest.fn(),
298 | json: jest.fn,
299 | headersSent: false
300 | };
301 | const someNext = 3;
302 |
303 | await fn(someReq, someRes, someNext);
304 |
305 | expect(mockFn).toHaveBeenCalled();
306 | expect(mockHandleInternalError).toHaveBeenCalledWith(
307 | 'Server did not send any response'
308 | );
309 | });
310 |
311 | it('should pass error to next', async () => {
312 | const factory = new RouterFactory();
313 |
314 | class SomeController extends BaseController {
315 | handleRequest() {
316 | throw new Error('Some error');
317 | }
318 | }
319 |
320 | const fn = factory._getWrappedController(new SomeController());
321 |
322 | const someReq = 1;
323 | const someRes = 2;
324 | const someNext = jest.fn();
325 |
326 | await fn(someReq, someRes, someNext);
327 |
328 | // expect(mockFn).toHaveBeenCalledWith(someReq, someRes, someNext);
329 | expect(someNext).toHaveBeenCalledWith(new Error('Some error'));
330 | });
331 |
332 | it('should not call internal if resolvedBy was populated', async () => {
333 | const factory = new RouterFactory();
334 | class SomeController extends BaseController {
335 | handleRequest() {
336 | this.ok({ some: 'data' });
337 | }
338 | }
339 |
340 | const controller = new SomeController();
341 | controller.resolvedBy = 'test';
342 | controller.internalServerError = jest.fn();
343 | const fn = factory._getWrappedController(controller);
344 |
345 | const someReq = 1;
346 | const someRes = {
347 | status: jest.fn(),
348 | json: jest.fn(),
349 | headersSent: true
350 | };
351 | const someNext = jest.fn();
352 |
353 | await fn(someReq, someRes, someNext);
354 |
355 | expect(someNext).not.toHaveBeenCalled();
356 | expect(controller.internalServerError).not.toHaveBeenCalled();
357 | });
358 | });
359 |
360 | it('Should get router from method', () => {
361 | const factory = new RouterFactory();
362 |
363 | const result = factory._getRouter();
364 |
365 | expect(result).toBeDefined();
366 | });
367 |
368 | it('Should throw error if duplicate urls', () => {
369 | let response;
370 | const mockRoutes = {
371 | routes: [
372 | Route.get('/hello', () => {}),
373 | Route.get('/hello/', () => {}),
374 | Route.get('/v1/hey', () => {})
375 | ],
376 | subroutes: [
377 | {
378 | path: '/v1',
379 | router: {
380 | routes: [Route.get('/hey', () => {})]
381 | }
382 | }
383 | ]
384 | };
385 | try {
386 | new RouterFactory({})._handleDuplicateUrls(mockRoutes);
387 | } catch (error) {
388 | response = error;
389 | }
390 |
391 | expect(response.message).toEqual(
392 | 'Duplicate endpoints detected! -> get /hello/, get /v1/hey/'
393 | );
394 | });
395 | });
396 |
--------------------------------------------------------------------------------
/__tests__/CelebrateUtils.test.js:
--------------------------------------------------------------------------------
1 | const { Joi } = jest.requireActual('celebrate');
2 | const CelebrateUtils = require('../src/CelebrateUtils');
3 |
4 | describe('CelebrateUtils', () => {
5 | describe('joiSchemaToSwaggerRequestParameters', () => {
6 | it('Should convert validationSchema to swagger parameter json for basic query, params, and headers', () => {
7 | const validationSchema = {
8 | headers: Joi.object({
9 | 'X-Auth': Joi.string().required()
10 | }),
11 | query: {
12 | page: Joi.number().integer().optional()
13 | },
14 | params: {
15 | userId: Joi.number().integer().required()
16 | }
17 | };
18 |
19 | const json = CelebrateUtils.joiSchemaToSwaggerRequestParameters(
20 | validationSchema
21 | );
22 |
23 | expect(json).toContainEqual({
24 | name: 'page',
25 | in: 'query',
26 | required: false,
27 | schema: {
28 | type: 'integer'
29 | }
30 | });
31 |
32 | expect(json).toContainEqual({
33 | name: 'userId',
34 | in: 'path',
35 | required: true,
36 | schema: {
37 | type: 'integer'
38 | }
39 | });
40 |
41 | expect(json).toContainEqual({
42 | name: 'x-auth',
43 | in: 'header',
44 | schema: {
45 | type: 'string'
46 | },
47 | required: true
48 | });
49 | });
50 |
51 | it('Should convert validationSchema to swagger if body is Joi object', () => {
52 | const validationSchema = {
53 | body: Joi.object({
54 | name: Joi.string().required()
55 | })
56 | };
57 |
58 | const json = CelebrateUtils.joiSchemaToSwaggerRequestParameters(
59 | validationSchema
60 | );
61 |
62 | expect(json).toContainEqual({
63 | in: 'body',
64 | name: 'body',
65 | schema: {
66 | type: 'object',
67 | properties: {
68 | name: {
69 | type: 'string'
70 | }
71 | },
72 | required: ['name']
73 | }
74 | });
75 | });
76 |
77 | it('Should convert validationSchema to swagger for property with options - valid, empty, and nullable', () => {
78 | const validationSchema = {
79 | body: Joi.object({
80 | name: Joi.string().valid('hey', 'you').required(),
81 | someAllow: Joi.string().allow('huh'),
82 | nullable: Joi.string().valid('hey').allow(null)
83 | })
84 | };
85 |
86 | const json = CelebrateUtils.joiSchemaToSwaggerRequestParameters(
87 | validationSchema
88 | );
89 |
90 | expect(json).toContainEqual({
91 | in: 'body',
92 | name: 'body',
93 | schema: {
94 | type: 'object',
95 | properties: {
96 | name: {
97 | type: 'string',
98 | enum: ['hey', 'you']
99 | },
100 | someAllow: {
101 | type: 'string',
102 | enum: ['huh']
103 | },
104 | nullable: {
105 | type: 'string',
106 | nullable: true,
107 | enum: ['hey', null]
108 | }
109 | },
110 | required: ['name']
111 | }
112 | });
113 | });
114 |
115 | it('Should convert validationSchema to swagger parameter json for complex body', () => {
116 | const validationSchema = {
117 | body: {
118 | name: Joi.string().required(),
119 | age: Joi.number().integer().required(),
120 | income: Joi.number().required(),
121 | dob: Joi.date()
122 | .optional()
123 | .default(new Date('2020-01-31T18:00:00.000Z')),
124 | email: Joi.string().email().optional(),
125 | patternString: Joi.string().pattern(/someregex/),
126 | boundedNumber: Joi.number().min(3).max(5),
127 | favoriteString: Joi.string().min(5).max(10),
128 | someMultiple: Joi.number().multiple(5)
129 | }
130 | };
131 |
132 | const json = CelebrateUtils.joiSchemaToSwaggerRequestParameters(
133 | validationSchema
134 | );
135 |
136 | expect(json).toContainEqual({
137 | in: 'body',
138 | name: 'body',
139 | schema: {
140 | type: 'object',
141 | properties: {
142 | name: {
143 | type: 'string'
144 | },
145 | age: {
146 | type: 'integer'
147 | },
148 | income: {
149 | type: 'number'
150 | },
151 | dob: {
152 | type: 'string',
153 | default: '"2020-01-31T18:00:00.000Z"'
154 | },
155 | email: {
156 | type: 'string'
157 | },
158 | patternString: {
159 | type: 'string',
160 | pattern: '/someregex/'
161 | },
162 | boundedNumber: {
163 | type: 'number',
164 | minimum: 3,
165 | maximum: 5
166 | },
167 | favoriteString: {
168 | type: 'string',
169 | minLength: 5,
170 | maxLength: 10
171 | },
172 | someMultiple: {
173 | type: 'number',
174 | multipleOf: 5
175 | }
176 | },
177 | required: ['name', 'age', 'income']
178 | }
179 | });
180 | });
181 |
182 | it('Should convert swagger json for nested object schema', () => {
183 | const validationSchema = {
184 | body: {
185 | address: Joi.object()
186 | .keys({
187 | street: Joi.string().required(),
188 | house: Joi.string().required(),
189 | extra: Joi.object()
190 | .keys({
191 | floor: Joi.number(),
192 | flat: Joi.number(),
193 | houseName: Joi.string()
194 | })
195 | .required(),
196 | zipCode: Joi.number().integer().optional()
197 | })
198 | .required()
199 | }
200 | };
201 |
202 | const json = CelebrateUtils.joiSchemaToSwaggerRequestParameters(
203 | validationSchema
204 | );
205 |
206 | expect(json).toContainEqual({
207 | in: 'body',
208 | name: 'body',
209 | schema: {
210 | type: 'object',
211 | properties: {
212 | address: {
213 | type: 'object',
214 | properties: {
215 | street: {
216 | type: 'string'
217 | },
218 | house: {
219 | type: 'string'
220 | },
221 | zipCode: {
222 | type: 'integer'
223 | },
224 | extra: {
225 | type: 'object',
226 | properties: {
227 | floor: {
228 | type: 'number'
229 | },
230 | flat: {
231 | type: 'number'
232 | },
233 | houseName: {
234 | type: 'string'
235 | }
236 | }
237 | // required: ["flat"]
238 | }
239 | },
240 | required: ['street', 'house', 'extra']
241 | }
242 | },
243 | required: ['address']
244 | }
245 | });
246 | });
247 |
248 | it('Should convert json for array of items', () => {
249 | const validationSchema = {
250 | body: {
251 | blankArray: Joi.array().min(2).max(5),
252 | favoriteNumbers: Joi.array().items(Joi.number()),
253 | favoriteBooks: Joi.array().items(
254 | Joi.object().keys({
255 | author: Joi.string().required(),
256 | publishDate: Joi.date()
257 | })
258 | )
259 | }
260 | };
261 |
262 | const json = CelebrateUtils.joiSchemaToSwaggerRequestParameters(
263 | validationSchema
264 | );
265 |
266 | expect(json).toContainEqual({
267 | in: 'body',
268 | name: 'body',
269 | schema: {
270 | type: 'object',
271 | properties: {
272 | blankArray: {
273 | type: 'array',
274 | items: {},
275 | maxItems: 5,
276 | minItems: 2
277 | },
278 | favoriteNumbers: {
279 | type: 'array',
280 | items: {
281 | type: 'number'
282 | }
283 | },
284 | favoriteBooks: {
285 | type: 'array',
286 | items: {
287 | type: 'object',
288 | properties: {
289 | author: {
290 | type: 'string'
291 | },
292 | publishDate: {
293 | type: 'string'
294 | }
295 | },
296 | required: ['author']
297 | }
298 | }
299 | }
300 | }
301 | });
302 | });
303 |
304 | it('Should allow body joi object check', () => {
305 | const validationSchema = {
306 | body: Joi.object().required()
307 | };
308 |
309 | const json = CelebrateUtils.joiSchemaToSwaggerRequestParameters(
310 | validationSchema
311 | );
312 |
313 | expect(json).toContainEqual({
314 | in: 'body',
315 | name: 'body',
316 | schema: {
317 | type: 'object'
318 | }
319 | });
320 | });
321 |
322 | it('Should allow body joi any check', () => {
323 | const validationSchema = {
324 | body: Joi.any().required()
325 | };
326 |
327 | const json = CelebrateUtils.joiSchemaToSwaggerRequestParameters(
328 | validationSchema
329 | );
330 |
331 | expect(json).toContainEqual({
332 | in: 'body',
333 | name: 'body',
334 | schema: {
335 | type: 'object'
336 | }
337 | });
338 | });
339 | });
340 |
341 | describe('lowercaseHeaderSchemaProperties', () => {
342 | it('Should do nothing if no headers present', () => {
343 | const validationSchema = {
344 | body: {
345 | some: 'validation'
346 | }
347 | };
348 |
349 | CelebrateUtils.lowercaseHeaderSchemaProperties(validationSchema);
350 |
351 | expect(validationSchema).toEqual(validationSchema);
352 | });
353 |
354 | it('Should lowercase if headers object provided', () => {
355 | const validationSchema = {
356 | body: {
357 | some: 'validation'
358 | },
359 | headers: {
360 | Authorization: 1234,
361 | ContentType: 'json'
362 | }
363 | };
364 |
365 | CelebrateUtils.lowercaseHeaderSchemaProperties(validationSchema);
366 |
367 | expect(validationSchema).toEqual({
368 | body: {
369 | some: 'validation'
370 | },
371 | headers: {
372 | authorization: 1234,
373 | contenttype: 'json'
374 | }
375 | });
376 | });
377 |
378 | it('Should lowercase if headers object provided as joi object', () => {
379 | const validationSchema = {
380 | body: {
381 | some: 'validation'
382 | },
383 | headers: Joi.object({
384 | Authorization: 1234,
385 | ContentType: 'json'
386 | })
387 | };
388 |
389 | CelebrateUtils.lowercaseHeaderSchemaProperties(validationSchema);
390 |
391 | expect(JSON.stringify(validationSchema.headers)).toEqual(
392 | JSON.stringify(
393 | Joi.object({
394 | authorization: 1234,
395 | contenttype: 'json'
396 | })
397 | )
398 | );
399 | });
400 | });
401 |
402 | describe('getCelebrateValidationMiddlewareForFileUpload', () => {
403 | it('should return proper handler with non joi object - no error', async () => {
404 | const fileUpload = {
405 | file: Joi.any().required()
406 | };
407 |
408 | const handler = CelebrateUtils.getCelebrateValidationMiddlewareForFileUpload(
409 | fileUpload
410 | );
411 |
412 | const mockNext = jest.fn();
413 | const mockReq = {
414 | file: 'some file'
415 | };
416 | await handler(mockReq, null, mockNext);
417 |
418 | expect(mockNext).toHaveBeenCalledWith();
419 | });
420 |
421 | it('should return proper handler with non joi object - no error - multiple files', async () => {
422 | const fileUpload = {
423 | files: Joi.any().required()
424 | };
425 |
426 | const handler = CelebrateUtils.getCelebrateValidationMiddlewareForFileUpload(
427 | fileUpload
428 | );
429 |
430 | const mockNext = jest.fn();
431 | const mockReq = {
432 | files: ['some files']
433 | };
434 | await handler(mockReq, null, mockNext);
435 |
436 | expect(mockNext).toHaveBeenCalledWith();
437 | });
438 |
439 | it('should return proper handler with non joi object - with error', async () => {
440 | const fileUpload = {
441 | file: Joi.any().required()
442 | };
443 |
444 | const handler = CelebrateUtils.getCelebrateValidationMiddlewareForFileUpload(
445 | fileUpload
446 | );
447 |
448 | const mockNext = jest.fn();
449 | const mockReq = {};
450 | await handler(mockReq, null, mockNext);
451 |
452 | expect(mockNext.mock.calls[0][0].message).toEqual(
453 | `ValidationError: "file" is required`
454 | );
455 | });
456 |
457 | it('should return proper handler with joi object - with error', async () => {
458 | const fileUpload = Joi.object({
459 | file: Joi.any().required()
460 | });
461 |
462 | const handler = CelebrateUtils.getCelebrateValidationMiddlewareForFileUpload(
463 | fileUpload
464 | );
465 |
466 | const mockNext = jest.fn();
467 | const mockReq = {};
468 | await handler(mockReq, null, mockNext);
469 |
470 | expect(mockNext.mock.calls[0][0].message).toEqual(
471 | `ValidationError: "file" is required`
472 | );
473 | });
474 | });
475 | });
476 |
--------------------------------------------------------------------------------
/__tests__/SwaggerUtils.test.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 | const SwaggerUtils = require('../src/SwaggerUtils');
4 | const { Joi } = require('celebrate');
5 | const Route = require('../src/Route');
6 | const BaseController = require('../src/BaseController');
7 | const { subroute } = require('../src');
8 |
9 | class SomeTestController extends BaseController {
10 | constructor() {
11 | super();
12 |
13 | this.doc = {
14 | tags: ['SomeTag']
15 | };
16 | }
17 | }
18 | const mockRouterWithTopRoutes = {
19 | routes: [Route.get('/', new SomeTestController())],
20 | subroutes: [
21 | {
22 | path: '/users',
23 | router: {
24 | routes: [
25 | Route.get('/', new SomeTestController()),
26 | Route.post('/', new SomeTestController())
27 | ],
28 | subroutes: [
29 | subroute('/:userId/posts', {
30 | routes: [
31 | Route.get('/', new BaseController()),
32 | Route.post('/', new BaseController())
33 | ]
34 | })
35 | ]
36 | }
37 | }
38 | ]
39 | };
40 |
41 | describe('SwaggerUtils', () => {
42 | describe('registerExpress', () => {
43 | it('Should register both url and redirect', () => {
44 | const mockApp = {
45 | use: jest.fn(),
46 | get: jest.fn()
47 | };
48 |
49 | SwaggerUtils.registerExpress({
50 | app: mockApp,
51 | swaggerJson: {},
52 | url: '/someurl',
53 | authUser: {
54 | user: 'admin',
55 | password: 'admin'
56 | }
57 | });
58 |
59 | expect(mockApp.use).toHaveBeenCalled();
60 | expect(mockApp.get).toHaveBeenCalled();
61 |
62 | // eslint-disable-next-line prefer-destructuring
63 | const [, , mockRedirectHandler] = mockApp.get.mock.calls[0];
64 | const mockRes = {
65 | redirect: jest.fn()
66 | };
67 |
68 | mockRedirectHandler(null, mockRes);
69 |
70 | expect(mockRes.redirect).toHaveBeenCalledWith('someurl');
71 | });
72 |
73 | it('Should sanitize redirect url', () => {
74 | const mockApp = {
75 | use: jest.fn(),
76 | get: jest.fn()
77 | };
78 |
79 | SwaggerUtils.registerExpress({
80 | app: mockApp,
81 | swaggerJson: {},
82 | url: 'someurl',
83 | authUser: {
84 | user: 'admin',
85 | password: 'admin'
86 | }
87 | });
88 |
89 | expect(mockApp.use).toHaveBeenCalled();
90 | expect(mockApp.get).toHaveBeenCalled();
91 |
92 | // eslint-disable-next-line prefer-destructuring
93 | const [, , mockRedirectHandler] = mockApp.get.mock.calls[0];
94 | const mockRes = {
95 | redirect: jest.fn()
96 | };
97 |
98 | mockRedirectHandler(null, mockRes);
99 |
100 | expect(mockRes.redirect).toHaveBeenCalledWith('someurl');
101 | });
102 | });
103 |
104 | describe('_sanitizeSwaggerPath', () => {
105 | it('Should sanitize path parameter at the end of the url', () => {
106 | const result = SwaggerUtils._sanitizeSwaggerPath('some/:path');
107 | expect(result).toEqual('some/{path}/');
108 | });
109 |
110 | it('Should sanitize path parameter at the end of the url with extra slash', () => {
111 | const result = SwaggerUtils._sanitizeSwaggerPath('some/:path/');
112 | expect(result).toEqual('some/{path}/');
113 | });
114 |
115 | it('Should sanitize path parameter in the middle of url', () => {
116 | const result = SwaggerUtils._sanitizeSwaggerPath('/some/:path/other');
117 | expect(result).toEqual('/some/{path}/other/');
118 | });
119 |
120 | it('Should sanitize multiple path parameters', () => {
121 | const result = SwaggerUtils._sanitizeSwaggerPath(
122 | '/some/:path/other/:url/'
123 | );
124 | expect(result).toEqual('/some/{path}/other/{url}/');
125 | });
126 | });
127 |
128 | describe('convertDocsToSwaggerDoc', () => {
129 | it('Should handle case for parent tags to capitalize', () => {
130 | const routes = {
131 | subroutes: [
132 | {
133 | path: '/',
134 | router: {
135 | routes: [
136 | {
137 | path: '/hey',
138 | controller: new BaseController(),
139 | method: 'get'
140 | }
141 | ]
142 | }
143 | },
144 | {
145 | path: '/v',
146 | router: {
147 | routes: [
148 | {
149 | path: '/hey',
150 | controller: new BaseController(),
151 | method: 'get'
152 | }
153 | ]
154 | }
155 | },
156 | {
157 | path: '/hey-there',
158 | router: {
159 | routes: [
160 | {
161 | path: '/hey',
162 | controller: new BaseController(),
163 | method: 'get'
164 | }
165 | ]
166 | }
167 | },
168 | {
169 | path: '/hi_there',
170 | router: {
171 | routes: [
172 | {
173 | path: '/hey',
174 | controller: new BaseController(),
175 | method: 'get'
176 | }
177 | ]
178 | }
179 | }
180 | ]
181 | };
182 |
183 | const swaggerDoc = SwaggerUtils.convertDocsToSwaggerDoc(routes);
184 | expect(swaggerDoc.tags.map((t) => t.name)).toContain('V');
185 | expect(swaggerDoc.tags.map((t) => t.name)).toContain('Hey There');
186 | expect(swaggerDoc.tags.map((t) => t.name)).toContain('Hi There');
187 | expect(swaggerDoc).toBeDefined();
188 | });
189 |
190 | it('Should handle case for no parent tags, and tags with multi word', () => {
191 | const routes = {
192 | subroutes: [
193 | {
194 | path: '/',
195 | controller: {
196 | routes: [
197 | {
198 | path: '/hey',
199 | controller: new BaseController(),
200 | method: 'get'
201 | }
202 | ]
203 | }
204 | }
205 | ]
206 | };
207 |
208 | const swaggerDoc = SwaggerUtils.convertDocsToSwaggerDoc(routes);
209 | expect(swaggerDoc).toBeDefined();
210 | });
211 |
212 | it('Should write json for swagger with redirects', () => {
213 | const mockRoutes1 = { ...mockRouterWithTopRoutes };
214 | mockRoutes1.routes.push(
215 | {
216 | path: '/hey',
217 | controller: new BaseController(),
218 | method: 'get',
219 | doc: {
220 | summary: 'hey route',
221 | responses: {
222 | 200: {},
223 | 400: {}
224 | }
225 | }
226 | },
227 | {
228 | path: '/hello',
229 | controller: new BaseController(),
230 | method: 'get',
231 | doc: {
232 | responses: {
233 | 200: {},
234 | 400: {}
235 | }
236 | }
237 | }
238 | );
239 |
240 | let swaggerDoc = SwaggerUtils.convertDocsToSwaggerDoc(mockRoutes1);
241 |
242 | mockRoutes1.routes.push({
243 | path: '/hello',
244 | controller: '/hey/',
245 | method: 'get',
246 | doc: {
247 | responses: {}
248 | },
249 | authorizer: (req, res) => {}
250 | });
251 | swaggerDoc = SwaggerUtils.convertDocsToSwaggerDoc(mockRoutes1);
252 |
253 | expect(swaggerDoc).toBeDefined();
254 | });
255 |
256 | it('Should write json for swagger with authorizer object', () => {
257 | const mockRoutes1 = { ...mockRouterWithTopRoutes };
258 | class Controller extends BaseController {
259 | constructor() {
260 | super();
261 | this.doc = {
262 | summary: 'hey route',
263 | responses: {
264 | 200: {}
265 | }
266 | };
267 | this.authorizer = ['hello from hey route'];
268 | }
269 | }
270 | mockRoutes1.routes.push({
271 | path: '/hey',
272 | controller: new Controller(),
273 | method: 'get'
274 | });
275 |
276 | const swaggerDoc = SwaggerUtils.convertDocsToSwaggerDoc(mockRoutes1);
277 |
278 | expect(swaggerDoc).toBeDefined();
279 |
280 | const swaggerstr = JSON.stringify(swaggerDoc);
281 | expect(
282 | swaggerstr.includes(`Authorized: [\\"hello from hey route\\"]`)
283 | ).toBeTruthy();
284 | });
285 |
286 | it('Should write json for swagger with responses defined', () => {
287 | const mockRoutes1 = { routes: [] };
288 |
289 | class Controller extends BaseController {
290 | constructor() {
291 | super();
292 | this.doc = {
293 | summary: 'hey route',
294 | responses: {
295 | 200: {}
296 | }
297 | };
298 | this.authorizer = ['hello'];
299 | this.validationSchema = {
300 | some: 'schema'
301 | };
302 | }
303 | }
304 |
305 | mockRoutes1.routes.push({
306 | path: '/hey',
307 | controller: new Controller(),
308 | method: 'get'
309 | });
310 |
311 | const swaggerDoc = SwaggerUtils.convertDocsToSwaggerDoc(mockRoutes1);
312 | expect(swaggerDoc).toBeDefined();
313 |
314 | const swaggerstr = JSON.stringify(swaggerDoc);
315 | expect(
316 | swaggerstr.includes(`Schema validation error response`)
317 | ).toBeTruthy();
318 | });
319 |
320 | it('Should write json for swagger with file upload defined', () => {
321 | const mockRoutes1 = { routes: [] };
322 | class FileUploadController extends BaseController {
323 | constructor() {
324 | super();
325 |
326 | this.doc = {
327 | summary: 'hey route',
328 | responses: {
329 | 200: {}
330 | }
331 | };
332 | this.authorizer = ['hello'];
333 | this.validationSchema = {
334 | fileUpload: {
335 | file: Joi.any().required()
336 | }
337 | };
338 | }
339 | }
340 |
341 | mockRoutes1.routes.push({
342 | path: '/hey',
343 | controller: new FileUploadController(),
344 | method: 'get'
345 | });
346 |
347 | const swaggerDoc = SwaggerUtils.convertDocsToSwaggerDoc(mockRoutes1);
348 | expect(swaggerDoc).toBeDefined();
349 |
350 | const swaggerstr = JSON.stringify(swaggerDoc);
351 | expect(swaggerstr.includes(`multipart/form-data`)).toBeTruthy();
352 | });
353 |
354 | it('Should write json for swagger with responses defined for validation', () => {
355 | const mockRoutes1 = { routes: [] };
356 | class Controller extends BaseController {
357 | constructor() {
358 | super();
359 | this.doc = {
360 | summary: 'hey route',
361 | description: 'hey route',
362 | responses: {
363 | 200: {},
364 | 400: {
365 | response: 'this is a response schema'
366 | }
367 | },
368 | tags: ['hello']
369 | };
370 | this.authorizer = ['hello'];
371 | }
372 | }
373 |
374 | mockRoutes1.routes.push({
375 | path: '/hey',
376 | controller: new Controller(),
377 | method: 'get'
378 | });
379 |
380 | const swaggerDoc = SwaggerUtils.convertDocsToSwaggerDoc(mockRoutes1);
381 | expect(swaggerDoc).toBeDefined();
382 |
383 | const swaggerstr = JSON.stringify(swaggerDoc);
384 | expect(
385 | swaggerstr.includes(`Schema validation error response`)
386 | ).toBeFalsy();
387 |
388 | expect(swaggerstr.includes(`this is a response schema`)).toBeTruthy();
389 | });
390 | });
391 |
392 | describe('writeSwaggerJson', () => {
393 | it('should write json for swagger', () => {
394 | const sampleSwaggerInfo = {
395 | version: '1.0.0',
396 | title: 'Expressive API',
397 | contact: {
398 | name: 'Author',
399 | email: 'Your email address',
400 | url: ''
401 | }
402 | };
403 |
404 | const outputPath = path.resolve(__dirname, 'output.json');
405 | SwaggerUtils.writeSwaggerJson({
406 | router: mockRouterWithTopRoutes,
407 | output: outputPath,
408 | basePath: '/api',
409 | swaggerInfo: sampleSwaggerInfo
410 | });
411 |
412 | const file = fs.readFileSync(outputPath);
413 | expect(file).toBeDefined();
414 | fs.unlinkSync(outputPath);
415 | });
416 |
417 | it('should write json for swagger using defaults', () => {
418 | const outputPath = path.resolve(__dirname, 'output.json');
419 | SwaggerUtils.writeSwaggerJson({
420 | router: mockRouterWithTopRoutes,
421 | output: outputPath
422 | });
423 |
424 | const file = fs.readFileSync(outputPath);
425 | expect(file).toBeDefined();
426 | fs.unlinkSync(outputPath);
427 | });
428 | });
429 |
430 | describe('getSwaggerHeader', () => {
431 | it('works with defaults', () => {
432 | const header = SwaggerUtils.getSwaggerHeader({});
433 | expect(header).toBeDefined();
434 | });
435 |
436 | it('works with non defaults', () => {
437 | const header = SwaggerUtils.getSwaggerHeader({ basePath: '/api' });
438 | expect(header).toBeDefined();
439 | });
440 |
441 | it('should set auth properly for header', () => {
442 | const header = SwaggerUtils.getSwaggerHeader({
443 | basePath: '/api',
444 | swaggerSecurityDefinitions: {
445 | someAuth: {
446 | type: 'apiKey',
447 | in: 'header',
448 | name: 'X-API-KEY'
449 | }
450 | }
451 | });
452 |
453 | expect(header).toBeDefined();
454 | expect(header.securityDefinitions).toEqual({
455 | someAuth: {
456 | type: 'apiKey',
457 | in: 'header',
458 | name: 'X-API-KEY'
459 | }
460 | });
461 | expect(header.security).toEqual([
462 | {
463 | someAuth: []
464 | }
465 | ]);
466 | });
467 | });
468 | });
469 |
--------------------------------------------------------------------------------
/__tests__/Middleware.test.js:
--------------------------------------------------------------------------------
1 | const response = require('../src/middleware/response');
2 | const MiddlewareManager = require('../src/middleware/MiddlewareManager');
3 |
4 | const OLD_ENV = process.env;
5 |
6 | describe('response middleware', () => {
7 | it('Should call fn properly', () => {
8 | const mockNext = jest.fn();
9 | const mockRes = {
10 | setHeader: jest.fn()
11 | };
12 | response(null, mockRes, mockNext);
13 |
14 | expect(mockNext).toHaveBeenCalled();
15 | expect(mockRes.setHeader).toHaveBeenCalledWith(
16 | 'Content-Type',
17 | 'application/json'
18 | );
19 | });
20 | });
21 |
22 | describe('Middleware Manager', () => {
23 | describe('registerMiddleware', () => {
24 | it('should skip for morgan body properl with defaults', () => {
25 | const mockExpress = {
26 | use: jest.fn()
27 | };
28 | const mockUserMiddleware = ['abc'];
29 | const manager = new MiddlewareManager(
30 | {
31 | middleware: mockUserMiddleware
32 | },
33 | mockExpress
34 | );
35 |
36 | let morganConfigs = {};
37 | manager.morganBody = (app, configs) => {
38 | morganConfigs = configs;
39 | };
40 |
41 | manager._registerMorganBody();
42 |
43 | expect(morganConfigs.skip).toBeDefined();
44 |
45 | const mockReq = {
46 | originalUrl: '/'
47 | };
48 | expect(morganConfigs.skip(mockReq)).toBeTruthy();
49 | mockReq.originalUrl = '/favicon';
50 | expect(morganConfigs.skip(mockReq)).toBeTruthy();
51 | mockReq.originalUrl = '/some/url';
52 | expect(morganConfigs.skip(mockReq)).toBeFalsy();
53 | });
54 | it('should ignore morgan if disabled', () => {
55 | const mockExpress = {
56 | use: jest.fn()
57 | };
58 | const mockUserMiddleware = ['abc'];
59 | const manager = new MiddlewareManager(
60 | {
61 | middleware: mockUserMiddleware,
62 | requestLoggerOptions: {
63 | disabled: true
64 | }
65 | },
66 | mockExpress
67 | );
68 | manager.morganBody = jest.fn();
69 | manager._registerMorganBody();
70 |
71 | expect(manager.morganBody).not.toBeCalled();
72 | });
73 | it('should skip for morgan body properl with given requestLoggerOptions', () => {
74 | const mockExpress = {
75 | use: jest.fn()
76 | };
77 | const mockUserMiddleware = ['abc'];
78 | const manager = new MiddlewareManager(
79 | {
80 | middleware: mockUserMiddleware,
81 | requestLoggerOptions: {
82 | skip: (req) => req.originalUrl === '/skip-this'
83 | }
84 | },
85 | mockExpress
86 | );
87 |
88 | let morganConfigs = {};
89 | manager.morganBody = (app, configs) => {
90 | morganConfigs = configs;
91 | };
92 |
93 | manager._registerMorganBody();
94 |
95 | expect(morganConfigs.skip).toBeDefined();
96 |
97 | const mockReq = {
98 | originalUrl: '/skip-this'
99 | };
100 | expect(morganConfigs.skip(mockReq)).toBeTruthy();
101 | });
102 | it('Should register defaults and user middleware', () => {
103 | const mockExpress = {
104 | use: jest.fn()
105 | };
106 | const mockUserMiddleware = ['abc'];
107 | const manager = new MiddlewareManager(
108 | {
109 | middleware: mockUserMiddleware
110 | },
111 | mockExpress
112 | );
113 | manager.expressModule = {
114 | json: jest.fn().mockReturnValue(1),
115 | urlencoded: jest.fn().mockReturnValue(5)
116 | };
117 | manager.addRequestId = jest.fn().mockReturnValue(2);
118 | manager.helmet = jest.fn().mockReturnValue(3);
119 | manager.routeUtil = {
120 | getHandlerWithManagedNextCall: jest.fn().mockImplementation((d) => d)
121 | };
122 | manager.morganBody = (app) => app;
123 | manager.registerBasicMiddleware();
124 |
125 | expect(mockExpress.use).toHaveBeenCalledTimes(6);
126 |
127 | expect(manager.expressModule.json).toHaveBeenCalledWith({
128 | limit: manager.options.bodyLimit
129 | });
130 |
131 | expect(mockExpress.use.mock.calls[0][0]).toEqual(1);
132 | expect(mockExpress.use.mock.calls[1][0]).toEqual(5);
133 | expect(mockExpress.use.mock.calls[2][0]).toEqual(response);
134 | expect(mockExpress.use.mock.calls[3][0]).toEqual(2);
135 | expect(mockExpress.use.mock.calls[4][0]).toEqual(3);
136 | expect(mockExpress.use.mock.calls[5][0]).toEqual(mockUserMiddleware);
137 | });
138 |
139 | it('Should register defaults without user middleware', () => {
140 | const mockExpress = {
141 | use: jest.fn()
142 | };
143 | const manager = new MiddlewareManager({}, mockExpress);
144 | manager.morganBody = (app) => app;
145 | manager.expressModule = {
146 | json: jest.fn().mockReturnValue(1),
147 | urlencoded: jest.fn().mockReturnValue(2)
148 | };
149 | manager.addRequestId = jest.fn().mockReturnValue(3);
150 |
151 | manager.registerBasicMiddleware();
152 |
153 | expect(mockExpress.use).toHaveBeenCalledTimes(5); // registering 4 middleware
154 |
155 | expect(manager.expressModule.json).toHaveBeenCalledWith({
156 | limit: manager.options.bodyLimit
157 | });
158 |
159 | expect(mockExpress.use.mock.calls[0][0]).toEqual(1);
160 | expect(mockExpress.use.mock.calls[1][0]).toEqual(2);
161 | expect(mockExpress.use.mock.calls[2][0]).toEqual(response);
162 | expect(mockExpress.use.mock.calls[3][0]).toEqual(3);
163 | });
164 | });
165 |
166 | describe('registerNotFoundHandler', () => {
167 | it('Should register given handler', () => {
168 | const mockExpress = {
169 | use: jest.fn()
170 | };
171 |
172 | const someMockHandler = 'hehe';
173 | const options = {
174 | notFoundHandler: someMockHandler
175 | };
176 | const manager = new MiddlewareManager(options, mockExpress);
177 |
178 | manager.registerNotFoundHandler(mockExpress, someMockHandler);
179 |
180 | expect(mockExpress.use).toHaveBeenCalledWith(someMockHandler);
181 | });
182 |
183 | it('Should run default handler properly', () => {
184 | const manager = new MiddlewareManager();
185 | const mockReq = {
186 | path: '/some/path'
187 | };
188 |
189 | const mockRes = {
190 | status: jest.fn(),
191 | json: jest.fn()
192 | };
193 |
194 | manager.defaultNotFoundHandler(mockReq, mockRes);
195 |
196 | expect(mockRes.status).toHaveBeenCalledWith(404);
197 | expect(mockRes.json).toHaveBeenCalledWith({
198 | message: `Route \'/some/path\' not found`
199 | });
200 | });
201 | });
202 |
203 | describe('_registerHelmet', () => {
204 | it('Should register helmet with defaults if no config is given', () => {
205 | const mockExpress = {
206 | use: jest.fn()
207 | };
208 | const middlewareManager = new MiddlewareManager({}, mockExpress);
209 | middlewareManager.helmet = jest.fn().mockReturnValue(123);
210 |
211 | middlewareManager._registerHelmet();
212 | expect(mockExpress.use).toHaveBeenCalledWith(123);
213 | expect(middlewareManager.helmet).toHaveBeenCalledTimes(1);
214 | expect(middlewareManager.helmet.mock.calls[0].length).toEqual(0);
215 | });
216 |
217 | it('Should register helmet with given config', () => {
218 | const mockHelmetOptions = {
219 | hello: 'world'
220 | };
221 |
222 | const mockExpress = {
223 | use: jest.fn()
224 | };
225 | const middlewareManager = new MiddlewareManager(
226 | {
227 | helmetOptions: mockHelmetOptions
228 | },
229 | mockExpress
230 | );
231 | middlewareManager.helmet = jest.fn().mockReturnValue(123);
232 |
233 | middlewareManager._registerHelmet();
234 | expect(mockExpress.use).toHaveBeenCalledWith(123);
235 | expect(middlewareManager.helmet).toHaveBeenCalledTimes(1);
236 | expect(middlewareManager.helmet).toHaveBeenCalledWith(mockHelmetOptions);
237 | });
238 | });
239 |
240 | describe('registerCelebrateErrorMiddleware', () => {
241 | it('Should register custom celebrate error middleware is provided', () => {
242 | const customCelebrateHandler = jest.fn();
243 |
244 | const mockUse = jest.fn();
245 |
246 | const mockExpress = {
247 | use: mockUse
248 | };
249 |
250 | const manager = new MiddlewareManager(
251 | {
252 | celebrateErrorHandler: customCelebrateHandler
253 | },
254 | mockExpress
255 | );
256 | manager.registerCelebrateErrorMiddleware();
257 |
258 | expect(mockUse).toHaveBeenCalledWith(customCelebrateHandler);
259 | });
260 |
261 | it('Should register default celebrate error middleware if custom is not provided', () => {
262 | const mockUse = jest.fn();
263 |
264 | const mockExpress = {
265 | use: mockUse
266 | };
267 | const manager = new MiddlewareManager({}, mockExpress);
268 |
269 | const mockCelebrateErrors = jest.fn().mockReturnValue(1);
270 | manager.celebrateErrors = mockCelebrateErrors;
271 | manager.registerCelebrateErrorMiddleware();
272 |
273 | expect(mockCelebrateErrors).toHaveBeenCalled();
274 | expect(mockUse).toHaveBeenCalledWith(1);
275 | });
276 | });
277 |
278 | describe('registerDocs', () => {
279 | it('Should register and redoc properly', () => {
280 | const swaggerInfo = { name: 'John Smith' };
281 | const mockRouter = {
282 | some: 'routes'
283 | };
284 | const mockSwaggerDefinitions = {
285 | some: 'Definition'
286 | };
287 |
288 | const options = {
289 | basePath: '/',
290 | swaggerInfo,
291 | swaggerDefinitions: mockSwaggerDefinitions,
292 | showSwaggerOnlyInDev: false
293 | };
294 |
295 | const mockExpress = {
296 | get: jest.fn()
297 | };
298 | const manager = new MiddlewareManager(options, mockExpress);
299 |
300 | const mockSwaggerHeader = { some: 'Header' };
301 | const mockSwaggerJson = { hello: 'world' };
302 | manager.SwaggerUtils = {
303 | getSwaggerHeader: jest.fn().mockReturnValue(mockSwaggerHeader),
304 | convertDocsToSwaggerDoc: jest.fn().mockReturnValue(mockSwaggerJson),
305 | registerExpress: jest.fn()
306 | };
307 | manager.registerRedoc = jest.fn();
308 |
309 | manager.registerDocs(mockRouter);
310 |
311 | expect(manager.registerRedoc).toHaveBeenCalledWith({
312 | app: mockExpress,
313 | swaggerJson: mockSwaggerJson,
314 | url: '/docs/redoc',
315 | authUser: {
316 | user: 'admin',
317 | password: 'admin'
318 | }
319 | });
320 | expect(manager.SwaggerUtils.getSwaggerHeader).toHaveBeenCalledWith({
321 | basePath: '/',
322 | swaggerInfo,
323 | swaggerSecurityDefinitions: undefined
324 | });
325 | expect(manager.SwaggerUtils.convertDocsToSwaggerDoc).toHaveBeenCalledWith(
326 | mockRouter,
327 | mockSwaggerHeader,
328 | mockSwaggerDefinitions
329 | );
330 | expect(manager.SwaggerUtils.registerExpress).toHaveBeenCalledWith({
331 | app: manager.express,
332 | swaggerJson: mockSwaggerJson,
333 | url: '/docs/swagger',
334 | authUser: {
335 | user: 'admin',
336 | password: 'admin'
337 | }
338 | });
339 | });
340 |
341 | describe('test with env set username and password for swagger', () => {
342 | beforeEach(() => {
343 | jest.resetModules(); // this is important - it clears the cache
344 | process.env = {
345 | ...OLD_ENV,
346 | EXPRESS_SWAGGER_USER: 'expressive',
347 | EXPRESS_SWAGGER_PASSWORD: 'expressive321'
348 | };
349 | });
350 |
351 | afterEach(() => {
352 | process.env = OLD_ENV;
353 | });
354 |
355 | it('description', () => {
356 | const swaggerInfo = { name: 'John Smith' };
357 | const mockRouter = {
358 | some: 'routes'
359 | };
360 | const mockSwaggerDefinitions = {
361 | some: 'Definition'
362 | };
363 |
364 | const options = {
365 | basePath: '/',
366 | swaggerInfo,
367 | swaggerDefinitions: mockSwaggerDefinitions,
368 | showSwaggerOnlyInDev: false
369 | };
370 |
371 | const mockExpress = {
372 | get: jest.fn()
373 | };
374 | const manager = new MiddlewareManager(options, mockExpress);
375 |
376 | const mockSwaggerHeader = { some: 'Header' };
377 | const mockSwaggerJson = { hello: 'world' };
378 | manager.SwaggerUtils = {
379 | getSwaggerHeader: jest.fn().mockReturnValue(mockSwaggerHeader),
380 | convertDocsToSwaggerDoc: jest.fn().mockReturnValue(mockSwaggerJson),
381 | registerExpress: jest.fn()
382 | };
383 | manager.registerRedoc = jest.fn();
384 |
385 | manager.registerDocs(mockRouter);
386 |
387 | expect(manager.registerRedoc).toHaveBeenCalledWith({
388 | app: mockExpress,
389 | swaggerJson: mockSwaggerJson,
390 | url: '/docs/redoc',
391 | authUser: {
392 | user: 'expressive',
393 | password: 'expressive321'
394 | }
395 | });
396 |
397 | expect(manager.SwaggerUtils.registerExpress).toHaveBeenCalledWith({
398 | app: mockExpress,
399 | swaggerJson: mockSwaggerJson,
400 | url: '/docs/swagger',
401 | authUser: {
402 | user: 'expressive',
403 | password: 'expressive321'
404 | }
405 | });
406 | });
407 | });
408 |
409 | describe('test without NODE_ENV', () => {
410 | beforeEach(() => {
411 | jest.resetModules(); // this is important - it clears the cache
412 | process.env = { ...OLD_ENV };
413 | delete process.env.NODE_ENV;
414 | });
415 |
416 | afterEach(() => {
417 | process.env = OLD_ENV;
418 | });
419 |
420 | it('Should register with default NODE_ENV', () => {
421 | const swaggerInfo = { name: 'John Smith' };
422 | const mockRouter = {
423 | some: 'routes'
424 | };
425 | const mockSwaggerDefinitions = {
426 | some: 'Definition'
427 | };
428 |
429 | const options = {
430 | basePath: '/',
431 | swaggerInfo,
432 | swaggerDefinitions: mockSwaggerDefinitions,
433 | showSwaggerOnlyInDev: false
434 | };
435 |
436 | const mockExpress = {
437 | get: jest.fn()
438 | };
439 | const manager = new MiddlewareManager(options, mockExpress);
440 |
441 | const mockSwaggerHeader = { some: 'Header' };
442 | const mockSwaggerJson = { hello: 'world' };
443 | manager.SwaggerUtils = {
444 | getSwaggerHeader: jest.fn().mockReturnValue(mockSwaggerHeader),
445 | convertDocsToSwaggerDoc: jest.fn().mockReturnValue(mockSwaggerJson),
446 | registerExpress: jest.fn()
447 | };
448 | manager.registerRedoc = jest.fn();
449 |
450 | manager.registerDocs(mockRouter);
451 |
452 | expect(manager.registerRedoc).toHaveBeenCalledWith({
453 | app: mockExpress,
454 | swaggerJson: mockSwaggerJson,
455 | url: '/docs/redoc',
456 | authUser: {
457 | user: 'admin',
458 | password: 'admin'
459 | }
460 | });
461 |
462 | expect(manager.SwaggerUtils.registerExpress).toHaveBeenCalledWith({
463 | app: mockExpress,
464 | swaggerJson: mockSwaggerJson,
465 | url: '/docs/swagger',
466 | authUser: {
467 | user: 'admin',
468 | password: 'admin'
469 | }
470 | });
471 | });
472 | });
473 | });
474 |
475 | describe('registerAuth', () => {
476 | it('Should throw error if authorizer is declared without authObjectHandler', () => {
477 | let response;
478 | try {
479 | const manager = new MiddlewareManager({
480 | authorizer: ['hehe']
481 | });
482 |
483 | manager.registerAuth();
484 | } catch (error) {
485 | response = error;
486 | }
487 |
488 | expect(response.message).toEqual(
489 | `'authorizer' object declared, but 'authObjectHandler' is not defined in ExpressApp constructor params`
490 | );
491 | });
492 |
493 | it('Should not throw error if authorizer and authObjectHandler both declared', () => {
494 | let response;
495 | const mockExpress = {
496 | use: jest.fn()
497 | };
498 | const manager = new MiddlewareManager(
499 | {
500 | authorizer: ['hehe'],
501 | authObjectHandler: (req, res) => {
502 | return null;
503 | }
504 | },
505 | mockExpress
506 | );
507 | manager.authUtil = {
508 | getAuthorizerMiddleware: jest.fn().mockReturnValue(1234)
509 | };
510 |
511 | try {
512 | manager.registerAuth();
513 | } catch (error) {
514 | response = error;
515 | }
516 |
517 | expect(response).not.toBeInstanceOf(Error);
518 | expect(mockExpress.use).toHaveBeenCalledWith(1234);
519 | });
520 |
521 | it('Should throw error if authObjectHandler body is empty', () => {
522 | let response;
523 | try {
524 | response = new ExpressApp(
525 | {},
526 | {
527 | authorizer: ['hehe'],
528 | authObjectHandler: (req, res) => {}
529 | }
530 | );
531 | } catch (error) {
532 | response = error;
533 | }
534 |
535 | expect(response).toBeInstanceOf(Error);
536 | });
537 | });
538 | });
539 |
--------------------------------------------------------------------------------