├── .eslintignore ├── .eslintrc.cjs ├── .gitattributes ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc.cjs ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── _config.yml ├── debug └── index.js ├── jest.config.js ├── markdown-style.css ├── package-lock.json ├── package.json ├── public ├── _assets │ ├── css │ │ ├── bootstrap.min.css │ │ └── style.scss │ ├── images │ │ ├── favicon.ico │ │ └── mockserverlogo.png │ └── js │ │ ├── bootstrap.bundle.min.js │ │ ├── index.js │ │ ├── info-box.js │ │ ├── initializer.js │ │ ├── listeners.js │ │ ├── main.js │ │ ├── modal-handler.js │ │ ├── split-screen.js │ │ └── utils.js └── index.html ├── samples ├── .mockserverrc.js └── server │ ├── db.json │ ├── index.js │ ├── injectors.json │ ├── middlewares.js │ ├── rewriters.json │ └── store.json ├── scripts └── jest.js ├── src ├── cli │ ├── argv.ts │ └── index.ts ├── defaults.ts ├── getters-setters.ts ├── index.ts ├── middlewares │ ├── HelperMiddlewares │ │ ├── _CrudOperation.ts │ │ ├── _Fetch.ts │ │ ├── _FetchOnly.ts │ │ ├── _FetchTillData.ts │ │ ├── _IterateResponse.ts │ │ ├── _IterateRoutes.ts │ │ ├── _MockOnly.ts │ │ ├── _ReadOnly.ts │ │ ├── _SendResponse.ts │ │ ├── _SetDelay.ts │ │ ├── _SetFetchDataToMock.ts │ │ ├── _SetHeaders.ts │ │ ├── _SetStatusCode.ts │ │ └── index.ts │ ├── defaults.ts │ ├── errorHandler.ts │ ├── index.ts │ ├── initializer.ts │ ├── pageNotFound.ts │ └── rewriter.ts ├── route-config-setters.ts ├── types │ ├── common.types.ts │ ├── param.types.ts │ ├── user.types.ts │ └── valid.types.ts └── utils │ ├── crud.ts │ ├── fetch.ts │ ├── index.ts │ └── validators.ts ├── tests ├── mock │ ├── config │ │ ├── config.js │ │ └── config.json │ ├── db │ │ ├── db.js │ │ └── db.json │ ├── injectors │ │ ├── injectors.js │ │ └── injectors.json │ ├── middlewares │ │ ├── middlewares.js │ │ └── middlewares.json │ ├── rewriters │ │ ├── rewriters.js │ │ └── rewriters.json │ └── store │ │ ├── store.js │ │ └── store.json ├── samples │ └── server.test.ts └── server │ ├── Helpers │ └── index.ts │ ├── getters-setters.test.ts │ ├── index.test.ts │ ├── integration │ ├── config.ts │ ├── index.test.ts │ └── routeConfig.ts │ └── validators │ ├── getValidConfig.test.ts │ ├── getValidDb.test.ts │ ├── getValidInjectors.test.ts │ ├── getValidMiddleware.test.ts │ ├── getValidRewriters.test.ts │ └── getValidStore.test.ts ├── tsconfig.json └── tsconfig.test.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .vscode 4 | test 5 | *.map 6 | coverage 7 | package-lock.json 8 | *.tgz 9 | samples 10 | public 11 | scripts 12 | tests 13 | samples 14 | 15 | # configs 16 | .eslintrc.cjs 17 | jest.config.js 18 | .prettierrc.cjs 19 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 3 | /* eslint-disable @typescript-eslint/no-var-requires */ 4 | /* eslint-disable import/no-extraneous-dependencies */ 5 | 6 | // @ts-check 7 | const { defineConfig } = require('eslint-define-config'); 8 | 9 | module.exports = defineConfig({ 10 | env: { es2020: true, node: true }, 11 | extends: [ 12 | 'semistandard', 13 | 'standard', 14 | 'eslint:recommended', 15 | 'plugin:@typescript-eslint/eslint-recommended', 16 | 'plugin:@typescript-eslint/recommended', 17 | 'plugin:prettier/recommended', 18 | 'plugin:promise/recommended', 19 | 'plugin:import/recommended', 20 | 'plugin:import/typescript', 21 | ], 22 | parser: '@typescript-eslint/parser', 23 | parserOptions: { ecmaVersion: 'latest', project: true, sourceType: 'module' }, 24 | plugins: ['@typescript-eslint', 'promise', 'import', 'prettier', 'sort-keys-fix'], 25 | root: true, 26 | rules: { 27 | '@typescript-eslint/consistent-type-imports': 'warn', 28 | '@typescript-eslint/no-empty-function': 0, 29 | '@typescript-eslint/no-explicit-any': 0, 30 | '@typescript-eslint/no-non-null-assertion': 0, 31 | '@typescript-eslint/no-unused-vars': 'error', 32 | '@typescript-eslint/prefer-as-const': 'warn', 33 | eqeqeq: 0, 34 | 'import/default': 'error', 35 | 'import/export': 'error', 36 | 'import/named': 'error', 37 | 'import/no-absolute-path': 0, 38 | 'import/no-anonymous-default-export': 0, 39 | 'import/no-duplicates': 'error', 40 | 'import/no-named-as-default': 0, 41 | 'import/no-named-as-default-member': 0, 42 | 'import/no-namespace': 0, 43 | 'linebreak-style': 0, 44 | 'prettier/prettier': ['error', { singleQuote: true }, { properties: { usePrettierrc: true } }], 45 | quotes: ['error', 'single', { allowTemplateLiterals: true, avoidEscape: false }], 46 | semi: [2, 'always'], 47 | 'sort-keys-fix/sort-keys-fix': 'error', 48 | 'tailwindcss/no-custom-classname': 0, 49 | }, 50 | settings: { 51 | 'import/parsers': { 52 | '@typescript-eslint/parser': ['.js', '.ts'], 53 | }, 54 | 'import/resolver': { 55 | node: { 56 | extensions: ['.js', '.ts'], 57 | }, 58 | typescript: { 59 | alwaysTryTypes: true, 60 | project: ['tsconfig.json'], 61 | }, 62 | }, 63 | }, 64 | }); 65 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | test 4 | *.map 5 | coverage 6 | *.tgz 7 | 8 | public/_assets/css/**.min.css 9 | !public/_assets/css/bootstrap.min.css 10 | public/_assets/js/**.min.js 11 | !public/_assets/js/bootstrap.bundle.min.js 12 | prepros.config 13 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | node_modules 3 | .gitignore 4 | .gitattributes 5 | markdown-style.css 6 | preprops.config 7 | package-lock.json 8 | *.map 9 | *.scss 10 | tests 11 | jest.config.js 12 | scripts 13 | 14 | public/_assets/css/**.scss 15 | public/_assets/js/**.js 16 | 17 | !public/_assets/js/**.min.js 18 | !public/_assets/css/**.min.css 19 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | .husky 15 | shells 16 | reports 17 | 18 | # Editor directories and files 19 | .vscode 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | *ignore 28 | LICENSE 29 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'es5', 3 | singleQuote: true, 4 | tabWidth: 2, 5 | semi: true, 6 | endOfLine: 'auto', 7 | printWidth: 140, 8 | }; 9 | -------------------------------------------------------------------------------- /.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 | "name": "Debug Server", 9 | "program": "${workspaceFolder}/debug/index.js", 10 | "request": "launch", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "type": "node", 15 | "console": "integratedTerminal", 16 | "preLaunchTask": "npm: compile" 17 | }, 18 | { 19 | "type": "node", 20 | "request": "launch", 21 | "name": "Test All Files", 22 | "program": "${workspaceFolder}/node_modules/jest/bin/jest.js", 23 | "args": [ 24 | "--collectCoverage=${input:collectCoverage}", 25 | "--collectCoverageFrom=", // Provide your custom coverage path 26 | "--watchAll=${input:watch}", 27 | "--runInBand", 28 | "--verbose=false", 29 | "--noStackTrace", 30 | "--no-cache", 31 | "--silent", 32 | ], 33 | "console": "integratedTerminal", 34 | }, 35 | { 36 | "type": "node", 37 | "request": "launch", 38 | "name": "Test Current File", 39 | "program": "${workspaceFolder}\\scripts\\jest.js", 40 | "args": [ 41 | "${relativeFileDirname}\\${fileBasename}", 42 | "--collectCoverage=${input:collectCoverage}", 43 | // "--collectCoverageFrom=", // Provide your custom coverage path 44 | "--watch=${input:watch}", 45 | "--runTestsByPath", 46 | "--runInBand", 47 | "--no-cache", 48 | "--verbose", 49 | "--silent" 50 | ], 51 | "console": "integratedTerminal", 52 | }, 53 | { 54 | "type": "node", 55 | "request": "launch", 56 | "name": "Test Current Folder", 57 | "program": "${workspaceFolder}\\scripts\\jest.js", 58 | "args": [ 59 | "${relativeFileDirname}", 60 | "--collectCoverage=${input:collectCoverage}", 61 | // "--collectCoverageFrom=", // Provide your custom coverage path 62 | "--watch=${input:watch}", 63 | "--runInBand", 64 | "--no-cache", 65 | "--verbose", 66 | "--silent" 67 | ], 68 | "console": "integratedTerminal", 69 | }, 70 | ], 71 | "inputs": [ 72 | { 73 | "type": "pickString", 74 | "id": "collectCoverage", 75 | "description": "Should collect Coverage ?", 76 | "options": [ 77 | "true", 78 | "false" 79 | ], 80 | "default": "true" 81 | }, 82 | { 83 | "type": "pickString", 84 | "id": "watch", 85 | "description": "Should watch for changes ?", 86 | "options": [ 87 | "true", 88 | "false" 89 | ], 90 | "default": "true" 91 | } 92 | ] 93 | } 94 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll": "explicit", 5 | "source.fixAll.sort-json": "never", 6 | "source.organizeImports": "explicit" 7 | }, 8 | "eslint.format.enable": true, 9 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 10 | "typescript.tsdk": "node_modules\\typescript\\lib", 11 | "prettier.configPath": ".prettierrc.cjs", 12 | "stylelint.configFile": ".stylelintrc.json", 13 | "stylelint.snippet": [ 14 | "css", 15 | "less", 16 | "postcss", 17 | "scss" 18 | ], 19 | "stylelint.validate": [ 20 | "css", 21 | "less", 22 | "postcss", 23 | "scss" 24 | ], 25 | "[javascript]": { 26 | "editor.defaultFormatter": "esbenp.prettier-vscode" 27 | }, 28 | "[scss]": { 29 | "editor.defaultFormatter": "vscode.css-language-features" 30 | }, 31 | "[typescriptreact]": { 32 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 33 | }, 34 | "[typescript]": { 35 | "editor.defaultFormatter": "esbenp.prettier-vscode" 36 | }, 37 | "markdown.styles": [ 38 | "./markdown-style.css" 39 | ], 40 | "[jsonc]": { 41 | "editor.defaultFormatter": "esbenp.prettier-vscode" 42 | }, 43 | "docwriter.style": "Auto-detect" 44 | } 45 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "compile", 7 | "group": "build", 8 | "problemMatcher": [], 9 | "label": "npm: compile", 10 | "detail": "rimraf dist && tsc --sourceMap" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Siva 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 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate 2 | -------------------------------------------------------------------------------- /debug/index.js: -------------------------------------------------------------------------------- 1 | const { MockServer, watcher, chalk, axios } = require('../dist/index.js'); 2 | const mockServer = MockServer.Create({ root: __dirname }); 3 | 4 | const startServer = async () => { 5 | const db = await axios.get('https://jsonplaceholder.typicode.com/db').then((res) => res.data); 6 | return await mockServer.launchServer(db); 7 | }; 8 | 9 | startServer(); 10 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | cache: false, 4 | globals: { 5 | 'ts-jest': { 6 | tsconfig: 'tsconfig.test.json', 7 | }, 8 | }, 9 | moduleNameMapper: { 10 | axios: 'axios/dist/node/axios.cjs', 11 | }, 12 | moduleFileExtensions: ['js', 'json', 'node', 'ts'], 13 | preset: 'ts-jest', 14 | silent: true, 15 | testEnvironment: 'node', 16 | testTimeout: 100000, 17 | verbose: true, 18 | }; 19 | -------------------------------------------------------------------------------- /markdown-style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color: rgb(149, 0, 168); 3 | --rgba: 149, 0, 168; 4 | --primary: rgba(var(--rgba)); 5 | --primary-bg-light: rgba(var(--rgba), 0.07); 6 | --secondary: rgba(var(--rgba), 0.2); 7 | } 8 | 9 | body { 10 | font-size: 15px; 11 | color: #333; 12 | background: #fff; 13 | font-family: Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', 'WenQuanYi Micro Hei', 'tohoma,sans-serif'; 14 | margin: 0; 15 | padding: 20px 10%; 16 | } 17 | 18 | .markdown-body { 19 | background-color: #fff !important; 20 | } 21 | 22 | h1 { 23 | font-size: 2.2em; 24 | font-weight: 700; 25 | line-height: 1.1; 26 | padding-top: 16px; 27 | margin-bottom: 4px; 28 | } 29 | 30 | h2 { 31 | font-size: 1.4em; 32 | margin: 40px 10px 20px 0; 33 | padding-left: 9px; 34 | border-left: 6px solid var(--primary); 35 | font-weight: 700; 36 | padding: 5px 10px; 37 | line-height: 1.4; 38 | } 39 | 40 | h3 { 41 | background-color: var(--primary-bg-light); 42 | border-left: 3px solid var(--primary); 43 | margin: 10px 0 5px; 44 | padding: 3px 10px; 45 | font-weight: 700; 46 | font-size: 1.2em; 47 | } 48 | 49 | h4 { 50 | display: inline-block; 51 | background-color: var(--secondary); 52 | font-weight: 700; 53 | font-size: 1em; 54 | line-height: 1.4; 55 | margin: 10px 0 5px; 56 | padding: 3px 5px; 57 | border-radius: 4px; 58 | } 59 | 60 | a { 61 | color: var(--primary) !important; 62 | text-decoration: none; 63 | } 64 | 65 | a:hover { 66 | text-decoration: underline; 67 | } 68 | 69 | a:focus, 70 | a:active, 71 | a:hover, 72 | a { 73 | outline: none !important; 74 | border: 0 !important; 75 | box-shadow: none !important; 76 | } 77 | 78 | h1:first-child > a:empty:after { 79 | content: '\25B2'; 80 | } 81 | 82 | h1:first-child > a:empty { 83 | position: fixed; 84 | bottom: 10px; 85 | right: 10px; 86 | color: white !important; 87 | text-decoration: none; 88 | font-family: 'Courier New', Courier, monospace; 89 | text-align: center; 90 | display: inline-block; 91 | border-radius: 50px; 92 | font-size: 20px; 93 | line-height: unset; 94 | background-color: #000; 95 | z-index: 10; 96 | margin: 0; 97 | padding: 5px 10px; 98 | opacity: 0.4; 99 | transition: 0.2s all ease-in; 100 | } 101 | 102 | h1:first-child > a:empty:hover { 103 | opacity: 0.8; 104 | } 105 | 106 | code { 107 | color: var(--primary); 108 | background-color: var(--primary-bg-light); 109 | border-radius: 4px; 110 | font-family: Menlo, Monaco, Consolas, 'Courier New', monospace; 111 | font-size: 90%; 112 | } 113 | 114 | pre, 115 | div { 116 | background-color: #000 !important; 117 | border-radius: 4px !important; 118 | } 119 | 120 | li { 121 | margin-top: 0.5em; 122 | } 123 | 124 | table thead { 125 | background-color: #dbdbdb; 126 | } 127 | 128 | table tr:nth-child(even) { 129 | background-color: #f0f0f0; 130 | } 131 | 132 | .function_ { 133 | color: #e7d405; 134 | } 135 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@r35007/mock-server", 3 | "version": "19.1.0", 4 | "description": "Customize Your Own Local Mock Server", 5 | "bin": { 6 | "mock-server": "dist/cli/index.js", 7 | "mock-server-init": "dist/cli/index.js --init" 8 | }, 9 | "main": "dist/index.js", 10 | "types": "dist/index.d.ts", 11 | "scripts": { 12 | "start": "node dist/index.js", 13 | "mock-server": "node dist/cli/index.js", 14 | "lint": "eslint --report-unused-disable-directives --max-warnings 0 .", 15 | "lint:fix": "npm run lint -- --fix", 16 | "format": "prettier --loglevel silent --ignore-unknown --find-config-path --check .", 17 | "format:fix": "prettier --loglevel silent --ignore-unknown --find-config-path --write .", 18 | "lint:ts": "tsc --noEmit", 19 | "build": "rimraf dist && tsc --build", 20 | "compile": "rimraf dist && tsc --build tsconfig.test.json", 21 | "clean": "rimraf node_modules package-lock.json coverage dist", 22 | "test": "jest --collectCoverage --collectCoverageFrom=src/**/*.ts --silent --runInBand --verbose --no-cache" 23 | }, 24 | "files": [ 25 | "dist", 26 | "public", 27 | "samples", 28 | "!public/_assets/css/**.scss", 29 | "public/_assets/css/**.min.css", 30 | "!public/_assets/js/**.js", 31 | "public/_assets/js/**.min.js" 32 | ], 33 | "engines": { 34 | "node": ">=12" 35 | }, 36 | "keywords": [ 37 | "JSON", 38 | "REST", 39 | "API", 40 | "prototyping", 41 | "mock", 42 | "mocking", 43 | "test", 44 | "testing", 45 | "rest", 46 | "data", 47 | "dummy", 48 | "sandbox", 49 | "server", 50 | "fake", 51 | "response", 52 | "db", 53 | "local" 54 | ], 55 | "repository": { 56 | "type": "git", 57 | "url": "https://github.com/R35007/Mock-Server" 58 | }, 59 | "homepage": "https://r35007.github.io/Mock-Server/", 60 | "author": { 61 | "name": "Sivaraman", 62 | "email": "sendmsg2siva@gmail.com" 63 | }, 64 | "license": "MIT", 65 | "dependencies": { 66 | "axios": "^1.3.2", 67 | "chalk": "^4.1.0", 68 | "chokidar": "^3.5.3", 69 | "comment-json": "^4.2.3", 70 | "compression": "^1.7.4", 71 | "cookie-parser": "^1.4.6", 72 | "cors": "^2.8.5", 73 | "cosmiconfig": "^8.1.3", 74 | "express": "^4.18.2", 75 | "express-urlrewrite": "^1.4.0", 76 | "fs-extra": "^11.1.1", 77 | "ip": "^1.1.5", 78 | "json5": "^2.2.3", 79 | "jsonc-require": "^1.0.1", 80 | "lodash": "^4.17.21", 81 | "lodash-id": "^0.14.1", 82 | "method-override": "^3.0.0", 83 | "morgan": "^1.10.0", 84 | "nanoid": "^3.1.23", 85 | "ora": "^5.4.1", 86 | "path-to-regexp": "^6.2.1", 87 | "please-upgrade-node": "^3.2.0", 88 | "response-time": "^2.3.2", 89 | "server-destroy": "^1.0.1", 90 | "update-notifier-cjs": "^5.1.6", 91 | "yargs": "^17.6.2" 92 | }, 93 | "devDependencies": { 94 | "@types/axios": "^0.14.0", 95 | "@types/chalk": "^2.2.0", 96 | "@types/chokidar": "^2.1.3", 97 | "@types/comment-json": "^2.4.2", 98 | "@types/compression": "^1.7.1", 99 | "@types/errorhandler": "^1.5.0", 100 | "@types/express": "^4.17.11", 101 | "@types/express-urlrewrite": "^1.3.0", 102 | "@types/fs-extra": "^11.0.1", 103 | "@types/ip": "^1.1.0", 104 | "@types/jest": "^27.4.1", 105 | "@types/lodash": "^4.14.191", 106 | "@types/method-override": "0.0.32", 107 | "@types/morgan": "^1.9.3", 108 | "@types/nanoid": "^3.0.0", 109 | "@types/node": "^17.0.45", 110 | "@types/ora": "^3.2.0", 111 | "@types/path-to-regexp": "^1.7.0", 112 | "@types/server-destroy": "^1.0.1", 113 | "@types/supertest": "^2.0.12", 114 | "@types/yargs": "^17.0.0", 115 | "@typescript-eslint/eslint-plugin": "^5.59.5", 116 | "@typescript-eslint/parser": "^5.59.5", 117 | "eslint": "^8.40.0", 118 | "eslint-config-prettier": "^8.8.0", 119 | "eslint-config-semistandard": "^17.0.0", 120 | "eslint-config-standard": "^17.0.0", 121 | "eslint-define-config": "^1.20.0", 122 | "eslint-import-resolver-typescript": "^3.5.5", 123 | "eslint-plugin-import": "^2.27.5", 124 | "eslint-plugin-prettier": "^4.2.1", 125 | "eslint-plugin-promise": "^6.1.1", 126 | "eslint-plugin-sort-keys-fix": "^1.1.2", 127 | "jest": "^29.7.0", 128 | "nodemon": "^3.0.2", 129 | "prettier": "^2.8.8", 130 | "supertest": "^6.2.2", 131 | "ts-jest": "^29.1.2", 132 | "typescript": "^5.4.3" 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /public/_assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/R35007/Mock-Server/6f8f4ff11d71520856a23a7ca69af76c23d746ae/public/_assets/images/favicon.ico -------------------------------------------------------------------------------- /public/_assets/images/mockserverlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/R35007/Mock-Server/6f8f4ff11d71520856a23a7ca69af76c23d746ae/public/_assets/images/mockserverlogo.png -------------------------------------------------------------------------------- /public/_assets/js/index.js: -------------------------------------------------------------------------------- 1 | // @prepros-append ./initializer.js 2 | // @prepros-append ./utils.js 3 | // @prepros-append ./split-screen.js 4 | // @prepros-append ./info-box.js 5 | // @prepros-append ./modal-handler.js 6 | // @prepros-append ./main.js 7 | // @prepros-append ./listeners.js 8 | -------------------------------------------------------------------------------- /public/_assets/js/info-box.js: -------------------------------------------------------------------------------- 1 | function toggleInfoBox(id) { 2 | const $li = document.getElementById(id); 3 | const classList = $li.classList; 4 | classList.contains('expanded') ? hideInfoBox($li) : showInfoBox($li, id); 5 | } 6 | 7 | function hideInfoBox($li) { 8 | $li.classList.remove('expanded'); 9 | $li.removeChild($li.lastElementChild); 10 | } 11 | 12 | function showInfoBox($li, id) { 13 | const [_routePath, routeConfig] = findEntry(id); 14 | const hideActions = routeConfig?._default || routeConfig.directUse; 15 | routeConfig.middlewares = routeConfig.middlewares?.filter(Boolean); 16 | 17 | $li.classList.add('expanded'); 18 | $li.appendChild( 19 | parseHTML(` 20 |
21 |
22 |
23 | Loading... 24 |
25 |
26 |
${fieldSet(routeConfig, routeConfig)}
27 | ${ 28 | hideActions 29 | ? '' 30 | : `
31 | Reset 32 | Edit 33 | Clone 34 | Refresh 35 |
` 36 | } 37 |
`) 38 | ); 39 | } 40 | 41 | async function reset(id) { 42 | // show info box loader 43 | const $infoBoxLoader = document.querySelector(`.info-box #loader-${id.replace(/\=/g, '')}`); 44 | $infoBoxLoader.classList.remove('d-none'); 45 | $infoBoxLoader.classList.add('d-block'); 46 | 47 | const restoredRoutes = await window.fetch(localhost + '/_reset/' + id).then((res) => res.json()); 48 | 49 | const [routePath, routeConfig] = Object.entries(restoredRoutes)[0]; 50 | resources[routePath] = routeConfig; 51 | toggleInfoBox(id); 52 | toggleInfoBox(id); 53 | showToast(`${routePath} Restored Successfully`); 54 | } 55 | 56 | async function refresh(id) { 57 | // show info box loader 58 | const $infoBoxLoader = document.querySelector(`.info-box #loader-${id.replace(/\=/g, '')}`); 59 | $infoBoxLoader.classList.remove('d-none'); 60 | $infoBoxLoader.classList.add('d-block'); 61 | 62 | const refreshedRoute = await window.fetch(localhost + '/_db/' + id).then((res) => res.json()); 63 | const [routePath, routeConfig] = Object.entries(refreshedRoute)[0]; 64 | resources[routePath] = routeConfig; 65 | 66 | toggleInfoBox(id); 67 | toggleInfoBox(id); 68 | showToast(`${routePath} Refreshed Successfully`); 69 | } 70 | 71 | function fieldSet(obj, routeConfig) { 72 | const id = routeConfig.id; 73 | return Object.entries(orderRouteConfig(obj)) 74 | .map(([key, val]) => { 75 | if (key === 'fetchData' && Object.keys(val || {}).length) { 76 | return ` 77 |
78 | 85 |
${fieldSet(val, routeConfig)}
86 |
`; 87 | } else { 88 | return getKeyVal(key, val, routeConfig); 89 | } 90 | }) 91 | .join(''); 92 | } 93 | 94 | function getKeyVal(key, val, routeConfig) { 95 | const id = routeConfig.id; 96 | 97 | // Return if key is _default or _config 98 | if (key === '_default' || key === '_config') return ''; 99 | 100 | // Return if val is empty 101 | if (typeof val === 'undefined' || val === null || (typeof val === 'string' && !val.trim().length)) return ''; 102 | 103 | // Create a badges if the value is a array of string 104 | if (Array.isArray(val) && val.every((v) => typeof v === 'string')) { 105 | return ` 106 |
107 | 108 |
109 | ${getBadges(val).join('')} 110 |
111 |
`; 112 | } 113 | 114 | if (typeof val === 'object' && !Array.isArray(val) && val.type === 'Buffer' && routeConfig._request?.url) { 115 | return ` 116 |
117 | 118 |
119 |
120 |
121 |
`; 122 | } 123 | 124 | // Show values in Textarea if Key is one of Textarea field or the value is a array or object 125 | if (textAreaKeys.includes(key) || typeof val === 'object') { 126 | return ` 127 |
128 | 135 |
136 | ${ 137 | typeof val === 'object' ? 'JSON' : 'STRING' 138 | } 139 | 140 |
141 |
`; 142 | } 143 | 144 | return ` 145 |
146 | 147 |
${val}
148 |
`; 149 | } 150 | 151 | const textAreaKeys = ['fetch', 'mock', 'response', 'store', '_request', 'stack']; 152 | -------------------------------------------------------------------------------- /public/_assets/js/initializer.js: -------------------------------------------------------------------------------- 1 | let resources = {}; 2 | let rewriters = {}; 3 | let formValues = {}; 4 | 5 | let totalRoutesCount = 0; 6 | let filteredRoutesCount = 0; 7 | 8 | const homePageRoutes = ['/_assets', '/_db/:id?', '/_rewriters', '/_store', '/_reset/:id?', '/_routes']; 9 | 10 | const $pageLoader = document.getElementById('page-loader'); 11 | const $darkModeSwitch = document.getElementById('darkMode-switch'); 12 | const $container = document.getElementById('container'); 13 | const $resourcesContainer = document.getElementById('resources-container'); 14 | const $dataContainer = document.getElementById('data-container'); 15 | const $rewritersContainer = document.getElementById('rewriters-container'); 16 | const $resourcesList = document.getElementById('resources-list'); 17 | const $rewritersList = document.getElementById('rewriters-list'); 18 | const $resourcesCount = document.getElementById('resources-count'); 19 | const $search = document.getElementById('search'); 20 | const $frameLoader = document.getElementById('iframe-loader'); 21 | const $iframeData = document.getElementById('iframe-data'); 22 | const $download = document.getElementById('download'); 23 | const $routeModal = document.getElementById('routeModal'); 24 | const $modalTitle = document.getElementById('modal-title'); 25 | const $routeConfigForm = document.getElementById('route-config-form'); 26 | const $routeConfig = document.getElementById('routeConfig'); 27 | const $formError = document.getElementById('form-error'); 28 | const $toast = document.getElementById('toast'); 29 | const $resourceRedirect = document.getElementById('resource-redirect'); 30 | const $iframeUrl = document.getElementById('iframe-url'); 31 | 32 | let $routeBsModal; 33 | let $bsToast; 34 | 35 | try { 36 | $routeBsModal = new bootstrap.Modal($routeModal); 37 | $bsToast = new bootstrap.Toast($toast, { animation: true, delay: 2000 }); 38 | } catch (err) { 39 | console.log(err); 40 | $routeBsModal = {}; 41 | $bsToast = {}; 42 | } 43 | 44 | $darkModeSwitch.checked = theme == 'dark'; 45 | -------------------------------------------------------------------------------- /public/_assets/js/listeners.js: -------------------------------------------------------------------------------- 1 | // Load Iframe on entering a url in the iframe search box 2 | function setIframeSource(code, value) { 3 | // Number 13 is the "Enter" key on the keyboard 4 | if (code === 13 || code === 'Enter') { 5 | try { 6 | $iframeData.contentWindow.document.open(); 7 | $iframeData.contentWindow.document.close(); 8 | } catch {} 9 | $frameLoader.style.display = 'grid'; 10 | $iframeData.src = value; 11 | } 12 | $resourceRedirect.href = value; 13 | $download.href = value; 14 | } 15 | 16 | $iframeUrl.addEventListener('keyup', function (event) { 17 | let code; 18 | if (event.key !== undefined) { 19 | code = event.key; 20 | } else if (event.keyIdentifier !== undefined) { 21 | code = event.keyIdentifier; 22 | } else if (event.keyCode !== undefined) { 23 | code = event.keyCode; 24 | } 25 | setIframeSource(code, event.target.value); 26 | }); 27 | 28 | // Show Drop shadow on scroll 29 | const scrollContainer = document.querySelector('#resources-container main'); 30 | scrollContainer.addEventListener('scroll', (e) => { 31 | const nav = scrollContainer.querySelector('nav'); 32 | if (scrollContainer.scrollTop > 0) { 33 | nav.style.boxShadow = '0 8px 10px -11px #212121'; 34 | } else { 35 | nav.style.boxShadow = 'none'; 36 | } 37 | }); 38 | 39 | // Add listeners to Modal Form Controls 40 | const modalTextControls = document.querySelectorAll('#route-config-form .form-control'); 41 | const modalSwitchControls = document.querySelectorAll('#route-config-form .form-check-input'); 42 | 43 | modalTextControls.forEach((formControl) => { 44 | formControl.addEventListener('input', ({ target }) => { 45 | if (['mock', 'fetch', 'store', 'headers', 'fetchData.response', 'fetchData.headers'].includes(target.name)) { 46 | let type = 'JSON'; 47 | try { 48 | const value = JSON.parse(target.value); 49 | set(formValues, target.name, value); 50 | type = 'JSON'; 51 | } catch (err) { 52 | const value = target.value; 53 | set(formValues, target.name, value); 54 | type = 'STRING'; 55 | } 56 | setFormDataType(target.name, type); 57 | } else { 58 | const value = target.value; 59 | set(formValues, target.name, value); 60 | } 61 | }); 62 | }); 63 | modalSwitchControls.forEach((formControl) => { 64 | formControl.addEventListener('click', ({ target }) => { 65 | const value = target.checked; 66 | set(formValues, target.name, value); 67 | }); 68 | }); 69 | 70 | // On Modal Submit Listener 71 | $routeConfigForm.addEventListener('submit', function (e) { 72 | e.preventDefault(); 73 | 74 | if (!Object.keys(formValues).length) { 75 | $routeBsModal?.hide?.(); 76 | return showToast(`No Changes`); 77 | } 78 | 79 | const id = $routeConfigForm.id?.value?.trim(); 80 | const updatedRouteConfig = { _config: true, ...formValues, id }; 81 | updatedRouteConfig.id ? updateRouteConfig(updatedRouteConfig) : addNewRoute(updatedRouteConfig); 82 | }); 83 | 84 | // Set Dark mode 85 | $darkModeSwitch.addEventListener('click', function (event) { 86 | const isChecked = event.target.checked; 87 | if (isChecked) { 88 | localStorage.setItem('theme', 'dark'); 89 | document.getElementsByTagName('html')[0].dataset.theme = 'dark'; 90 | } else { 91 | localStorage.removeItem('theme'); 92 | document.getElementsByTagName('html')[0].dataset.theme = ''; 93 | } 94 | }); 95 | -------------------------------------------------------------------------------- /public/_assets/js/main.js: -------------------------------------------------------------------------------- 1 | async function init() { 2 | $search.value = ''; 3 | 4 | try { 5 | resources = await request(localhost + '/_db'); 6 | } catch (err) { 7 | console.log(err); 8 | } 9 | 10 | try { 11 | rewriters = await request(localhost + '/_rewriters'); 12 | } catch (err) { 13 | console.error(err); 14 | } 15 | createResourcesList(resources); 16 | Object.entries(rewriters).length && createRewritersList(rewriters); 17 | 18 | showToast('Resources Loaded Successfully'); 19 | } 20 | 21 | function setHomePageRoutes(resources) { 22 | const routesList = Object.keys(resources); 23 | 24 | if (!routesList.includes('/_db')) 25 | resources['/_db'] = { 26 | id: window.btoa('/_db'), 27 | description: `Get Db snapshot.
28 | Use ${getUrl( 29 | '/_db' 30 | )}?_clean=true to get a clean db data.
31 | Use ${getUrl( 32 | '/_db' 33 | )}?_config=true to get only db route configs.`, 34 | _default: true, 35 | }; 36 | 37 | if (!routesList.includes('/_routes')) 38 | resources['/_routes'] = { 39 | id: window.btoa('/_routes'), 40 | description: 'Get List of routes used.', 41 | _default: true, 42 | }; 43 | 44 | if (!routesList.includes('/_store')) 45 | resources['/_store'] = { 46 | id: window.btoa('/_store'), 47 | description: 'Get Store values.', 48 | _default: true, 49 | }; 50 | } 51 | 52 | function createResourcesList(resources) { 53 | // collects all expanded list to restore after refresh 54 | const expandedList = []; 55 | $resourcesList.querySelectorAll('li.expanded').forEach((li) => expandedList.push(li.id)); 56 | 57 | // removes all the resources list 58 | while ($resourcesList.lastElementChild) { 59 | $resourcesList.removeChild($resourcesList.lastElementChild); 60 | } 61 | 62 | setHomePageRoutes(resources); 63 | $resourcesList.innerHTML = ResourceList(resources); 64 | 65 | expandedList.forEach(toggleInfoBox); 66 | 67 | filterRoutes(); 68 | } 69 | 70 | function createRewritersList(rewriters) { 71 | $rewritersList.innerHTML = Object.entries(rewriters) 72 | .map(([key, val]) => { 73 | return ` 74 | 83 | `; 84 | }) 85 | .join(''); 86 | $rewritersContainer.style.display = 'block'; 87 | } 88 | 89 | function ResourceList(resources) { 90 | totalRoutesCount = Object.keys(resources).length; 91 | setRoutesCount(totalRoutesCount); 92 | 93 | return ` 94 | ${Object.entries(resources) 95 | .map((routesEntry) => ResourceItem(...routesEntry)) 96 | .join('')} 97 | 102 | 107 | 114 | `; 115 | } 116 | 117 | function ResourceItem(routePath, routeConfig) { 118 | const isDefaultRoute = routeConfig._default; 119 | return ` 120 | 128 | `; 129 | } 130 | 131 | function getUrl(routePath) { 132 | if (routePath.startsWith('http')) return routePath; 133 | 134 | if (!routePath?.trim().length) return localhost; 135 | 136 | // remove optional params> ex : /posts/:id? -> /posts/, /posts/:id/comments -> /posts/1/comments 137 | let validRoutePath = routePath 138 | .split('/') 139 | .map((r) => (r.indexOf(':') >= 0 ? (r.indexOf('?') >= 0 ? '' : random(1, 100)) : r)) 140 | .join('/'); 141 | validRoutePath = validRoutePath.replace(/\/$/gi, ''); // removing trailing slash. ex: /posts/ -> /posts 142 | 143 | const url = localhost + validRoutePath; 144 | 145 | return url; 146 | } 147 | 148 | async function setIframeData($event, $this, routePath) { 149 | // If on ctrl+click or cmd+click then open the link in new tab 150 | if ($event.ctrlKey || $event.metaKey) { 151 | const url = getUrl(routePath); 152 | window.open(url, '_blank'); 153 | return; 154 | } 155 | 156 | try { 157 | clearActiveLink(); 158 | $this.parentNode.classList.add('active'); 159 | try { 160 | $iframeData.contentWindow.document.open(); 161 | $iframeData.contentWindow.document.close(); 162 | } catch {} 163 | $frameLoader.style.display = 'grid'; 164 | $dataContainer.style.display = 'block'; 165 | setIFrameSrc(routePath); 166 | } catch (err) { 167 | console.error(err); 168 | } 169 | } 170 | 171 | function clearActiveLink() { 172 | const li = $resourcesList.querySelectorAll('li .header'); 173 | for (let i = 0; i < li.length; i++) { 174 | li[i].classList.remove('active'); 175 | } 176 | } 177 | 178 | function setIFrameSrc(routePath) { 179 | const url = getUrl(routePath); 180 | if (routePath?.trim().length) { 181 | $resourceRedirect.href = url; 182 | $iframeUrl.value = url; 183 | $iframeData.src = url; 184 | $download.href = url; 185 | } else { 186 | $resourceRedirect.href = url; 187 | $iframeUrl.value = url; 188 | $iframeData.src = ''; 189 | $download.href = ''; 190 | } 191 | } 192 | 193 | function filterRoutes() { 194 | let searchText, routePath, i, txtValue; 195 | searchText = $search.value.toUpperCase(); 196 | routePath = $resourcesList.querySelectorAll('.route-path'); 197 | filteredRoutesCount = 0; 198 | for (i = 0; i < routePath.length; i++) { 199 | txtValue = routePath[i].textContent || routePath[i].innerText; 200 | if (txtValue.toUpperCase().indexOf(searchText) > -1) { 201 | routePath[i].parentNode.parentNode.parentNode.style.display = 'block'; 202 | filteredRoutesCount++; 203 | } else { 204 | routePath[i].parentNode.parentNode.parentNode.style.display = 'none'; 205 | } 206 | } 207 | setRoutesCount(totalRoutesCount, filteredRoutesCount, searchText); 208 | showNoResource(!filteredRoutesCount); 209 | } 210 | 211 | function setRoutesCount(totalRoutesCount, filteredRoutesCount, searchText) { 212 | const count = searchText?.length ? `${filteredRoutesCount} / ${totalRoutesCount}` : totalRoutesCount; 213 | $resourcesCount.innerHTML = count; 214 | } 215 | 216 | function showNoResource(show) { 217 | document.getElementById('no-resource').style.display = show ? 'block' : 'none'; 218 | } 219 | 220 | async function resetAll() { 221 | resources = await request(localhost + '/_reset'); 222 | showToast('Routes Restored Successfully'); 223 | createResourcesList(resources); 224 | } 225 | 226 | init(); 227 | -------------------------------------------------------------------------------- /public/_assets/js/modal-handler.js: -------------------------------------------------------------------------------- 1 | function openModal($button) { 2 | formValues = {}; 3 | hideFormError(); 4 | $routeConfigForm.reset(); 5 | 6 | const modalType = $button.getAttribute('data-type'); 7 | const id = $button.getAttribute('data-id'); 8 | setFormDataType('mock', ''); 9 | setFormDataType('fetch', ''); 10 | setFormDataType('store', ''); 11 | setFormDataType('headers', ''); 12 | setFormDataType('fetchData.response', ''); 13 | setFormDataType('fetchData.headers', ''); 14 | 15 | modalType === 'update' && updateRoute(id); 16 | modalType === 'clone' && cloneRoute(id); 17 | modalType === 'add' && addRoute(); 18 | $routeBsModal?.show?.(); 19 | } 20 | 21 | async function updateRoute(id) { 22 | const refreshedRoute = await request(localhost + '/_db/' + id); 23 | const [routePath, routeConfig = {}] = Object.entries(refreshedRoute)?.[0] || []; 24 | 25 | $routeConfig.value = JSON.stringify(routeConfig); 26 | $routeConfigForm.classList.add('update-form'); 27 | $routeConfigForm.classList.remove('add-form'); 28 | $modalTitle.textContent = routePath; 29 | 30 | setFormValues(routeConfig, routePath); 31 | } 32 | 33 | function addRoute() { 34 | $routeConfig.value = JSON.stringify({ routePath: $search.value }); 35 | $routeConfigForm.routePath.value = $search.value || ''; 36 | formValues.routePath = $search.value || ''; 37 | $routeConfigForm.classList.remove('update-form'); 38 | $routeConfigForm.classList.add('add-form'); 39 | $modalTitle.textContent = 'Add new Route'; 40 | } 41 | 42 | async function cloneRoute(id) { 43 | const refreshedRoute = await request(localhost + '/_db/' + id); 44 | const [routePath, routeConfig = {}] = Object.entries(refreshedRoute)?.[0] || []; 45 | delete routeConfig.id; 46 | 47 | $routeConfig.value = JSON.stringify(routeConfig); 48 | $routeConfigForm.classList.remove('update-form'); 49 | $routeConfigForm.classList.add('add-form'); 50 | $modalTitle.textContent = `Clone: ${routePath}`; 51 | 52 | setFormValues(routeConfig, routePath); 53 | } 54 | 55 | function setFormValues(routeConfig, routePath) { 56 | const { 57 | id, 58 | description, 59 | statusCode, 60 | delay, 61 | fetch, 62 | fetchCount, 63 | mockFirst, 64 | skipFetchError, 65 | mock, 66 | store, 67 | headers, 68 | middlewares, 69 | fetchData = {}, 70 | } = routeConfig; 71 | 72 | const fetchResp = fetchData.response; 73 | const fetchHeaders = fetchData.headers; 74 | 75 | $routeConfigForm.id.value = id || ''; 76 | $routeConfigForm.routePath.value = routePath || ''; 77 | $routeConfigForm.statusCode.value = statusCode ?? ''; 78 | $routeConfigForm.delay.value = delay ?? ''; 79 | $routeConfigForm.fetchCount.value = fetchCount ?? ''; 80 | $routeConfigForm.skipFetchError.checked = skipFetchError + '' == 'true'; 81 | $routeConfigForm.mockFirst.checked = mockFirst + '' == 'true'; 82 | $routeConfigForm.description.value = description ?? ''; 83 | $routeConfigForm.middlewares.value = middlewares?.join(',') ?? ''; 84 | 85 | $routeConfigForm.mock.value = typeof mock === 'object' ? JSON.stringify(mock, null, 8) : mock ?? ''; 86 | $routeConfigForm.fetch.value = typeof fetch === 'object' ? JSON.stringify(fetch, null, 8) : fetch ?? ''; 87 | $routeConfigForm.store.value = typeof store === 'object' ? JSON.stringify(store, null, 8) : store ?? ''; 88 | $routeConfigForm.headers.value = typeof headers === 'object' ? JSON.stringify(headers, null, 8) : headers ?? ''; 89 | $routeConfigForm['fetchData.response'].value = typeof fetchResp === 'object' ? JSON.stringify(fetchResp, null, 8) : fetchResp ?? ''; 90 | $routeConfigForm['fetchData.headers'].value = typeof fetchResp === 'object' ? JSON.stringify(fetchResp, null, 8) : fetchResp ?? ''; 91 | $routeConfigForm['fetchData.statusCode'].value = fetchData.statusCode ?? ''; 92 | 93 | setFormDataType('mock', typeof mock === 'object' ? 'JSON' : 'STRING'); 94 | setFormDataType('fetch', typeof fetch === 'object' ? 'JSON' : 'STRING'); 95 | setFormDataType('store', typeof store === 'object' ? 'JSON' : 'STRING'); 96 | setFormDataType('headers', typeof headers === 'object' ? 'JSON' : 'STRING'); 97 | setFormDataType('fetchData.response', typeof fetchResp === 'object' ? 'JSON' : 'STRING'); 98 | setFormDataType('fetchData.headers', typeof fetchHeaders === 'object' ? 'JSON' : 'STRING'); 99 | } 100 | 101 | async function updateRouteConfig(updatedRouteConfig) { 102 | const existingRouteConfig = parseJson($routeConfig.value) || {}; 103 | 104 | const routePath = $routeConfigForm.routePath?.value?.trim(); 105 | delete updatedRouteConfig.middlewares; 106 | 107 | const fetchData = { 108 | ...(existingRouteConfig.fetchData || {}), 109 | ...(updatedRouteConfig.fetchData || {}), 110 | }; 111 | 112 | const payload = { 113 | [routePath]: { 114 | ...updatedRouteConfig, 115 | ...(Object.keys(fetchData) ? { fetchData } : {}), 116 | }, 117 | }; 118 | 119 | const response = await request(localhost + '/_db/', { 120 | method: 'PUT', 121 | headers: { 122 | Accept: 'application/json', 123 | 'Content-Type': 'application/json', 124 | }, 125 | body: JSON.stringify(payload), 126 | }); 127 | 128 | Object.entries(response).forEach(([routePath, routeConfig]) => { 129 | resources[routePath] = routeConfig; 130 | }); 131 | 132 | createResourcesList(resources); 133 | $routeBsModal?.hide?.(); 134 | showToast(`${routePath} Updated Successfully`); 135 | } 136 | 137 | async function addNewRoute(updatedRouteConfig) { 138 | const existingRouteConfig = parseJson($routeConfig.value) || {}; 139 | 140 | const routePath = $routeConfigForm.routePath?.value?.trim(); 141 | const middlewares = $routeConfigForm.middlewares?.value?.split(',').filter(Boolean) || []; 142 | const fetchData = { 143 | ...(existingRouteConfig.fetchData || {}), 144 | ...(updatedRouteConfig.fetchData || {}), 145 | }; 146 | 147 | const routeConfig = { 148 | ...existingRouteConfig, 149 | ...updatedRouteConfig, 150 | ...(middlewares.length ? { middlewares } : {}), 151 | ...(Object.keys(fetchData) ? { fetchData } : {}), 152 | }; 153 | delete routeConfig.routePath; 154 | delete routeConfig.id; 155 | 156 | if (!routePath?.trim()?.length) { 157 | const error = 'Please Provide Route Path'; 158 | showFormError(error); 159 | return false; 160 | } 161 | if (Object.keys(resources).find((resource) => resource === routePath?.trim())) { 162 | const error = 'Route Path already exist. Please Provide new Route Path'; 163 | showFormError(error); 164 | return false; 165 | } 166 | 167 | const payload = { [routePath]: routeConfig }; 168 | 169 | const response = await request(localhost + '/_db', { 170 | method: 'POST', 171 | headers: { 172 | Accept: 'application/json', 173 | 'Content-Type': 'application/json', 174 | }, 175 | body: JSON.stringify(payload), 176 | }); 177 | 178 | Object.entries(response).forEach(([routePath, routeConfig]) => { 179 | resources[routePath] = routeConfig; 180 | }); 181 | 182 | createResourcesList(resources); 183 | $routeBsModal?.hide?.(); 184 | showToast(`${routePath} Added Successfully`); 185 | } 186 | 187 | function hideFormError() { 188 | $formError.style.display = 'none'; 189 | $formError.textContent = ''; 190 | } 191 | 192 | function showFormError(errorText) { 193 | $formError.style.display = 'block'; 194 | $formError.innerHTML = errorText; 195 | $routeModal.querySelector('.modal-body').scrollTop = 0; 196 | } 197 | -------------------------------------------------------------------------------- /public/_assets/js/split-screen.js: -------------------------------------------------------------------------------- 1 | function toggleDataContainer(shouldOpen) { 2 | $dataContainer.style.display = shouldOpen ? 'block' : 'none'; 3 | } 4 | 5 | // A function is used for dragging and moving 6 | function dragElement(element) { 7 | var md; // remember mouse down info 8 | element.onmousedown = onMouseDown; 9 | 10 | function onMouseDown(e) { 11 | md = { 12 | e, 13 | offsetLeft: element.offsetLeft, 14 | offsetTop: element.offsetTop, 15 | resourcesContainerWidth: $resourcesContainer.offsetWidth, 16 | dataContainerWidth: $dataContainer.offsetWidth, 17 | }; 18 | 19 | $resourcesContainer.style.pointerEvents = 'none'; 20 | $dataContainer.style.pointerEvents = 'none'; 21 | document.onmousemove = onMouseMove; 22 | document.onmouseup = () => { 23 | $resourcesContainer.style.pointerEvents = 'all'; 24 | $dataContainer.style.pointerEvents = 'all'; 25 | document.onmousemove = document.onmouseup = null; // clears drag event 26 | }; 27 | } 28 | 29 | function onMouseMove(e) { 30 | var delta = { 31 | x: e.clientX - md.e.clientX, 32 | y: e.clientY - md.e.clientY, 33 | }; 34 | 35 | // Prevent negative-sized elements 36 | const deltaX = Math.min(Math.max(delta.x, -md.resourcesContainerWidth), md.dataContainerWidth); 37 | let dataContainerWidth = md.dataContainerWidth - deltaX; 38 | let dataContainerMinWidth = (md.dataContainerWidth / 100) * 35; 39 | 40 | if (dataContainerWidth < dataContainerMinWidth) { 41 | $dataContainer.style.display = 'none'; 42 | } else { 43 | $dataContainer.style.display = 'block'; 44 | } 45 | 46 | delta.x = Math.min(Math.max(delta.x, -md.resourcesContainerWidth), md.dataContainerWidth); 47 | element.style.left = md.offsetLeft + delta.x + 'px'; 48 | $dataContainer.style.width = dataContainerWidth + 'px'; 49 | } 50 | } 51 | 52 | dragElement(document.getElementById('separator')); 53 | -------------------------------------------------------------------------------- /public/_assets/js/utils.js: -------------------------------------------------------------------------------- 1 | function getBadges(badgeList) { 2 | return badgeList.map( 3 | (badge) => 4 | `
5 | ${badge} 6 |
` 7 | ); 8 | } 9 | 10 | function parseHTML(html) { 11 | var t = document.createElement('template'); 12 | t.innerHTML = html; 13 | return t.content; 14 | } 15 | 16 | function findEntry(id) { 17 | const route = Object.entries(resources).find(([_key, val]) => val.id == id); 18 | return route || []; 19 | } 20 | 21 | // Allow tab space inside textarea 22 | function allowTabSpaces($event, $textarea) { 23 | if ($event.key == 'Tab') { 24 | $event.preventDefault(); 25 | var start = $textarea.selectionStart; 26 | var end = $textarea.selectionEnd; 27 | 28 | // set textarea value to: text before caret + tab + text after caret 29 | $textarea.value = $textarea.value.substring(0, start) + '\t' + $textarea.value.substring(end); 30 | 31 | // put caret at right position again 32 | $textarea.selectionStart = $textarea.selectionEnd = start + 1; 33 | } 34 | } 35 | 36 | function showToast(message) { 37 | $toast.querySelector('.toast-body').textContent = message; 38 | $bsToast?.show?.(); 39 | } 40 | 41 | function parseJson(data) { 42 | let parsedData; 43 | try { 44 | parsedData = JSON.parse(data || ''); 45 | } catch { 46 | parsedData = data?.trim(); 47 | } 48 | return parsedData; 49 | } 50 | 51 | function orderRouteConfig(routeConfig) { 52 | const clonedRouteConfig = JSON.parse(JSON.stringify(routeConfig)); 53 | const order = [ 54 | 'id', 55 | 'description', 56 | 'middlewares', 57 | 'statusCode', 58 | 'delay', 59 | 'fetchCount', 60 | 'skipFetchError', 61 | 'mock', 62 | 'fetch', 63 | 'fetchError', 64 | 'store', 65 | 'fetchData', 66 | 'status', 67 | 'message', 68 | 'isImage', 69 | 'isError', 70 | 'headers', 71 | 'stack', 72 | 'response', 73 | '_request', 74 | '_isFile', 75 | '_extension', 76 | ]; 77 | 78 | const routeConfigKeys = Object.keys(routeConfig); 79 | routeConfigKeys.forEach((key) => delete routeConfig[key]); // clearing all values in routeConfig 80 | 81 | const orderedKeys = new Set([...order, ...routeConfigKeys]); 82 | orderedKeys.forEach((key) => (routeConfig[key] = clonedRouteConfig[key])); 83 | 84 | return routeConfig; 85 | } 86 | 87 | function random(min, max) { 88 | // min and max included 89 | return Math.floor(Math.random() * (max - min + 1) + min); 90 | } 91 | 92 | function setFormDataType(name, type) { 93 | document.getElementById(`${name}-badge`).innerHTML = type; 94 | } 95 | 96 | function togglePageLoader(showLoader) { 97 | if (showLoader) { 98 | $pageLoader.classList.add('d-block'); 99 | $pageLoader.classList.remove('d-none'); 100 | } else { 101 | $pageLoader.classList.add('d-none'); 102 | $pageLoader.classList.remove('d-block'); 103 | } 104 | } 105 | 106 | async function request(url, options) { 107 | togglePageLoader(true); 108 | const response = await window.fetch(url, options).then((res) => res.json()); 109 | togglePageLoader(false); 110 | return response; 111 | } 112 | 113 | // Lodash implementation of _.set method 114 | const set = (obj, path, value) => { 115 | if (Object(obj) !== obj) return obj; // When obj is not an object 116 | // If not yet an array, get the keys from the string-path 117 | if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || []; 118 | path.slice(0, -1).reduce( 119 | ( 120 | a, 121 | c, 122 | i // Iterate all of them except the last one 123 | ) => 124 | Object(a[c]) === a[c] // Does the key exist and is its value an object? 125 | ? // Yes: then follow that path 126 | a[c] 127 | : // No: create the key. Is the next key a potential array-index? 128 | (a[c] = 129 | Math.abs(path[i + 1]) >> 0 === +path[i + 1] 130 | ? [] // Yes: assign a new array object 131 | : {}), // No: assign a new plain object 132 | obj 133 | )[path[path.length - 1]] = value; // Finally assign the value to the last key 134 | return obj; // Return the top-level object to allow chaining 135 | }; 136 | -------------------------------------------------------------------------------- /samples/.mockserverrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@r35007/mock-server/dist/cli/index').CliOptions} */ 2 | module.exports = { 3 | db: './db.json', 4 | injectors: './injectors.json', 5 | middlewares: './middlewares.js', 6 | rewriters: './rewriters.json', 7 | store: './store.json', 8 | port: 4001, 9 | root: './server', 10 | }; 11 | -------------------------------------------------------------------------------- /samples/server/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": [ 3 | { 4 | "body": "some comment", 5 | "id": 1, 6 | "postId": 1 7 | } 8 | ], 9 | "posts": [ 10 | { 11 | "author": "r35007", 12 | "id": 1, 13 | "title": "mock-server" 14 | } 15 | ], 16 | "profile": { 17 | "name": "r35007" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /samples/server/index.js: -------------------------------------------------------------------------------- 1 | // Run this file in terminal: 2 | // $ node server 3 | 4 | const { MockServer, watcher, chalk } = require('@r35007/mock-server'); 5 | const config = require('../.mockserverrc.js'); 6 | 7 | // Create Mock Server instance with custom config. 8 | const mockServer = MockServer.Create(config); 9 | 10 | const startServer = async () => { 11 | const app = mockServer.app; 12 | 13 | // Set global middlewares, injectors and store to mock server instance 14 | mockServer.setData({ 15 | injectors: config.injectors, 16 | middlewares: config.middlewares, 17 | store: config.store, 18 | }); 19 | 20 | // Make sure to use this at first, before all the resources 21 | const rewriter = mockServer.rewriter(config.rewriters); 22 | app.use(rewriter); 23 | 24 | // Add default Middlewares. 25 | const defaultsMiddlewares = mockServer.defaults(); 26 | app.use(defaultsMiddlewares); 27 | 28 | // Add Database 29 | const resources = mockServer.resources(config.db); 30 | app.use(resources.router); 31 | 32 | // Create the Mock Server Home Page 33 | const homePage = mockServer.homePage(); 34 | app.use(homePage); 35 | 36 | app.use(mockServer.pageNotFound); // Middleware to return `Page Not Found` as response if the route doesn't match 37 | app.use(mockServer.errorHandler); // Error Handler 38 | 39 | // Start server 40 | await mockServer.startServer(); 41 | }; 42 | 43 | startServer().then(() => { 44 | // watch for changes 45 | const watch = watcher.watch([ 46 | mockServer.config.root, 47 | //... provide your custom file or folder path to watch for changes 48 | ]); 49 | 50 | // Restart server on change 51 | watch.on('change', async (changedPath) => { 52 | process.stdout.write(chalk.yellowBright(changedPath) + chalk.gray(' has changed, reloading...\n')); 53 | if (!mockServer.server) return; // return if no server to stop 54 | await MockServer.Destroy(mockServer).then(() => startServer()); // Stop and restart the server on changes 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /samples/server/injectors.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "routes": "/profile", "middlewares": ["DataWrapper"] }, 3 | { "routes": "/(.*)", "delay": 1000 } 4 | ] 5 | -------------------------------------------------------------------------------- /samples/server/middlewares.js: -------------------------------------------------------------------------------- 1 | const DataWrapper = (req, res, next) => { 2 | res.locals.data = { 3 | status: 'Success', 4 | message: 'Retrieved Successfully', 5 | result: res.locals.data, 6 | }; 7 | next(); 8 | }; 9 | 10 | module.exports = (mockServer) => { 11 | const { app, routes, data, getDb, getStore } = mockServer || {}; 12 | const { config, db, injectors, middlewares, rewriters, store } = data || {}; 13 | // Your global middleware logic here before setting default middlewares by the MockServer 14 | // app.use(auth); 15 | 16 | return { DataWrapper }; 17 | }; 18 | -------------------------------------------------------------------------------- /samples/server/rewriters.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api/*": "/$1", 3 | "/posts/:id/comments": "/comments?postId=:id" 4 | } 5 | -------------------------------------------------------------------------------- /samples/server/store.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": "This data can be shared across all routes and can be accessed using 'res.locals.getStore()' in middleware" 3 | } -------------------------------------------------------------------------------- /scripts/jest.js: -------------------------------------------------------------------------------- 1 | `use strict`; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | 7 | const jest = require('jest'); 8 | const fs = require('fs'); 9 | const path = require('path'); 10 | 11 | const root = path.resolve(__dirname, '../'); 12 | 13 | // returns a valid source folder or file until it given sourcePath exist 14 | const getValidSourcePath = (sourcePath) => { 15 | if (fs.existsSync(sourcePath)) return sourcePath; 16 | const dir = path.dirname(sourcePath); // get folder of the sourcePath 17 | if (dir === path.resolve(__dirname, '../src')) return dir; 18 | return getValidSourcePath(dir); 19 | }; 20 | 21 | const collectCoverageFromPath = (testFilePath) => { 22 | const sourceFilePath = testFilePath.replace('tests', 'src').replace('.test.ts', '.ts'); 23 | const validSourcePath = getValidSourcePath(sourceFilePath); 24 | const relativeSourcePath = path.relative(root, validSourcePath).replace(/\\/g, '/'); 25 | 26 | const isFile = fs.statSync(validSourcePath).isFile(); 27 | 28 | return isFile ? relativeSourcePath : relativeSourcePath + '/**/*.ts'; 29 | }; 30 | 31 | const startTesting = () => { 32 | const args = process.argv.slice(2); 33 | const jestArgs = [...args]; 34 | 35 | const testPath = jestArgs[0].replace(/\\/g, '/'); 36 | jestArgs[0] = testPath; 37 | 38 | // If collectCoverage is true and collectCoverageFrom is not specified then set relative collectCoverageFrom path 39 | if (jestArgs.some((arg) => arg.includes('--collectCoverage=true')) && !jestArgs.some((arg) => arg.includes('--collectCoverageFrom='))) { 40 | const collectCoverageFrom = collectCoverageFromPath(testPath); 41 | const collectCoverageFromArg = '--collectCoverageFrom=' + collectCoverageFrom; 42 | console.log('\n', collectCoverageFromArg); 43 | jestArgs.splice(2, 0, collectCoverageFromArg); 44 | } 45 | 46 | console.log('\n'); 47 | 48 | jest.run(jestArgs, root); 49 | }; 50 | 51 | startTesting(); 52 | -------------------------------------------------------------------------------- /src/cli/argv.ts: -------------------------------------------------------------------------------- 1 | import * as yargs from 'yargs'; 2 | // Import cosmiconfig module 3 | import { cosmiconfig } from 'cosmiconfig'; 4 | 5 | export type Paths = { 6 | root: string; 7 | db: string; 8 | middlewares: string; 9 | injectors: string; 10 | rewriters: string; 11 | store: string; 12 | static: string; 13 | snapshots: string; 14 | }; 15 | 16 | export type Configs = { 17 | port: number; 18 | host: string; 19 | base: string; 20 | id: string; 21 | dbMode: 'mock' | 'fetch' | 'multi' | 'config'; 22 | reverse: boolean; 23 | readOnly: boolean; 24 | noCors: boolean; 25 | noGzip: boolean; 26 | noCache: boolean; 27 | logger: boolean; 28 | bodyParser: boolean; 29 | cookieParser: boolean; 30 | watch: boolean; 31 | quiet: boolean; 32 | log: boolean; 33 | homePage: boolean; 34 | }; 35 | 36 | export type CliOptions = Paths & 37 | Configs & { 38 | config: string; 39 | init: boolean; 40 | _: string[]; 41 | }; 42 | 43 | export default async (pkg) => { 44 | const moduleNames = ['mock-server', 'mockserver']; 45 | const searchPlaces = [ 46 | 'package.json', 47 | ...moduleNames.map((moduleName) => `.${moduleName}rc`), 48 | ...moduleNames.map((moduleName) => `.${moduleName}rc.json`), 49 | ...moduleNames.map((moduleName) => `.${moduleName}rc.js`), 50 | ...moduleNames.map((moduleName) => `.${moduleName}rc.cjs`), 51 | ...moduleNames.map((moduleName) => `.${moduleName}.config.json`), 52 | ...moduleNames.map((moduleName) => `.${moduleName}.config.js`), 53 | ...moduleNames.map((moduleName) => `.${moduleName}.config.cjs`), 54 | ...moduleNames.map((moduleName) => `${moduleName}-config.json`), 55 | ...moduleNames.map((moduleName) => `${moduleName}-config.js`), 56 | ...moduleNames.map((moduleName) => `${moduleName}-config.cjs`), 57 | ]; 58 | 59 | // Create a cosmiconfig explorer 60 | const explorer = cosmiconfig('mock-server', { searchPlaces }); 61 | const config = await explorer 62 | .search() 63 | .then((res) => res?.config || {}) 64 | .catch(() => {}); 65 | 66 | const options = yargs 67 | .config('config', () => config) 68 | .usage('mock-server [options] ') 69 | .options({ 70 | base: { alias: 'b', default: '', description: 'Set base route path', type: 'string' }, 71 | bodyParser: { alias: 'bp', default: true, description: 'Enable body-parser', type: 'boolean' }, 72 | config: { alias: 'c', default: '.mockserverrc.json', description: 'Path to config file', type: 'string' }, 73 | cookieParser: { alias: 'cp', default: true, description: 'Enable cookie-parser', type: 'boolean' }, 74 | db: { alias: '', description: 'Path to database file', type: 'string' }, 75 | dbMode: { alias: 'dm', choices: ['mock', 'fetch', 'multi', 'config'], default: 'mock', description: 'Set Db mode', type: 'string' }, 76 | homePage: { alias: 'hp', default: true, description: 'Enable Home Page', type: 'boolean' }, 77 | host: { alias: 'H', default: 'localhost', description: 'Set host', type: 'string' }, 78 | id: { alias: '', default: 'id', description: 'Set database id property', type: 'string' }, 79 | init: { alias: '', default: false, description: 'Create a sample server files', type: 'boolean' }, 80 | injectors: { alias: 'in', description: 'Path to Injectors file', type: 'string' }, 81 | log: { alias: 'log', default: false, description: 'Prevent setters logs', type: 'boolean' }, 82 | logger: { alias: 'l', default: true, description: 'Enable logger', type: 'boolean' }, 83 | middlewares: { alias: 'md', description: 'Path to middlewares file', type: 'string' }, 84 | noCache: { alias: 'nch', default: true, description: 'Disable Cache', type: 'boolean' }, 85 | noCors: { alias: 'nc', default: false, description: 'Disable Cross-Origin Resource Sharing', type: 'boolean' }, 86 | noGzip: { alias: 'ng', default: false, description: 'Disable GZIP Content-Encoding', type: 'boolean' }, 87 | port: { alias: 'P', default: 3000, description: 'Set port', type: 'number' }, 88 | quiet: { alias: 'q', default: false, description: 'Prevent console logs', type: 'boolean' }, 89 | readOnly: { alias: 'ro', default: false, description: 'Allow only GET requests', type: 'boolean' }, 90 | reverse: { alias: 'rv', default: false, description: 'Generate routes in reverse order', type: 'boolean' }, 91 | rewriters: { alias: 'rw', description: 'Path to Rewriter file', type: 'string' }, 92 | root: { alias: 'r', default: './', description: 'Set root directory.', type: 'string' }, 93 | snapshots: { alias: 'ss', default: './', description: 'Set snapshots directory', type: 'string' }, 94 | static: { alias: 's', default: 'public', description: 'Set static files directory', type: 'string' }, 95 | store: { alias: 'st', description: 'Path to Store file', type: 'string' }, 96 | watch: { alias: 'w', default: false, description: 'Watch for changes', type: 'boolean' }, 97 | }) 98 | .boolean('init') 99 | .boolean('reverse') 100 | .boolean('readOnly') 101 | .boolean('noCors') 102 | .boolean('noGzip') 103 | .boolean('noCache') 104 | .boolean('logger') 105 | .boolean('bodyParser') 106 | .boolean('cookieParser') 107 | .boolean('watch') 108 | .boolean('quiet') 109 | .boolean('log') 110 | .boolean('homePage') 111 | .help('help') 112 | .alias('help', 'h') 113 | .example('mock-server --init', 'Create a sample server files') 114 | .example('mock-server db.json', 'Run mock server with db.json file') 115 | .example('mock-server --watch db.json', '') 116 | .example('mock-server http://jsonplaceholder.typicode.com/db', '') 117 | .epilog('https://r35007.github.io/Mock-Server/') 118 | .version(pkg.version) 119 | .alias('version', 'v').argv as CliOptions; 120 | return options; 121 | }; 122 | -------------------------------------------------------------------------------- /src/cli/index.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | import axios from 'axios'; 3 | import chalk from 'chalk'; 4 | import watcher from 'chokidar'; 5 | import * as fs from 'fs'; 6 | import * as fsx from 'fs-extra'; 7 | import ora from 'ora'; 8 | import * as path from 'path'; 9 | import pleaseUpgradeNode from 'please-upgrade-node'; 10 | import updateNotifier from 'update-notifier-cjs'; 11 | import MockServer from '../'; 12 | import type { LaunchServerOptions } from '../types/common.types'; 13 | import type * as ParamTypes from '../types/param.types'; 14 | import { getCleanDb } from '../utils'; 15 | import type { Configs, Paths } from './argv'; 16 | import argv from './argv'; 17 | 18 | const pkgStr = fs.readFileSync(path.join(__dirname, '../../package.json'), 'utf8'); 19 | const pkg = JSON.parse(pkgStr); 20 | 21 | updateNotifier({ pkg }).notify(); 22 | pleaseUpgradeNode(pkg, { 23 | message: function (requiredVersion) { 24 | return chalk.red('Please upgrade Node.\n@r35007/mock-server requires at least version ' + chalk.yellow(requiredVersion) + ' of Node.'); 25 | }, 26 | }); 27 | 28 | const getDataFromUrl = async (root: string, data?: string) => { 29 | try { 30 | if (!data) return; 31 | if (data.startsWith('http')) { 32 | const spinner = !global.quiet ? ora('GET: ' + chalk.gray(data)).start() : false; 33 | try { 34 | const response = await axios.get(data).then((resp) => resp.data); 35 | spinner && spinner.stopAndPersist({ symbol: chalk.green('✔'), text: chalk.green('GET: ') + chalk.gray(data) }); 36 | return response; 37 | } catch (err: any) { 38 | spinner && spinner.stopAndPersist({ symbol: chalk.red('✖'), text: chalk.red('GET: ') + chalk.gray(data) }); 39 | process.stdout.write('\n' + chalk.red('Error: ') + chalk.yellow(err.message) + '\n'); 40 | return; 41 | } 42 | } else { 43 | const resolvedPath = path.resolve(root, data); 44 | if (!fs.existsSync(resolvedPath)) { 45 | process.stdout.write('\n' + chalk.red('Invalid Path: ') + chalk.yellow(resolvedPath) + '\n'); 46 | return; 47 | } 48 | return resolvedPath; 49 | } 50 | } catch (err: any) { 51 | console.error(chalk.red(err.message)); 52 | return {}; 53 | } 54 | }; 55 | 56 | const uncaughtException = async (error, fileWatcher: watcher.FSWatcher) => { 57 | console.error(chalk.red('Something went wrong!'), error); 58 | await fileWatcher.close(); 59 | process.exit(1); 60 | }; 61 | 62 | const errorHandler = async (fileWatcher: watcher.FSWatcher) => { 63 | console.error(chalk.red('Error, cant read from stdin')); 64 | console.error(chalk.red(`Creating a snapshot from the CLI wonn't be possible`)); 65 | await fileWatcher.close(); 66 | process.exit(1); 67 | }; 68 | 69 | const getSnapshot = (mockServer: MockServer, snapshots) => { 70 | process.stdout.write('\n' + chalk.gray('Type s + enter at any time to create a snapshot of the database') + '\n'); 71 | process.stdin.on('data', (chunk) => { 72 | try { 73 | if (chunk.toString().trim().toLowerCase() !== 's') return; 74 | const filename = `db-${Date.now()}.json`; 75 | const file = path.join(snapshots, filename); 76 | const cleanDb = getCleanDb(mockServer.db, mockServer.config.dbMode); 77 | fs.writeFileSync(file, JSON.stringify(cleanDb, null, 2), 'utf-8'); 78 | console.log(chalk.green('Saved snapshot to ') + `${path.relative(process.cwd(), file)}\n`); 79 | } catch (err: any) { 80 | console.error(chalk.red(err.message)); 81 | } 82 | }); 83 | }; 84 | 85 | const startServer = async (mockServer: MockServer, db: ParamTypes.Db, launchServerOptions: LaunchServerOptions) => { 86 | try { 87 | const server = await mockServer.launchServer(db, launchServerOptions); 88 | return server; 89 | } catch (err: any) { 90 | process.exit(1); 91 | } 92 | }; 93 | 94 | const restartServer = async (mockServer: MockServer, db: ParamTypes.Db, launchServerOptions: LaunchServerOptions, changedPath: string) => { 95 | try { 96 | if (!mockServer.server) return; 97 | process.stdout.write(chalk.yellowBright('\n' + path.relative(process.cwd(), changedPath)) + chalk.gray(' has changed, reloading...\n')); 98 | await MockServer.Destroy(mockServer).then(() => startServer(mockServer, db, launchServerOptions)); 99 | } catch (err: any) { 100 | console.error(err.message); 101 | process.exit(1); 102 | } 103 | }; 104 | 105 | const init = async () => { 106 | const args = await argv(pkg); 107 | 108 | if (args.init) { 109 | fsx.copy(path.join(__dirname, '../../samples'), path.resolve(process.cwd())); 110 | return; 111 | } 112 | 113 | const { 114 | _: [source], 115 | db = source, 116 | injectors, 117 | middlewares, 118 | store, 119 | rewriters, 120 | root, 121 | watch, 122 | snapshots, 123 | ...configArgs 124 | } = args; 125 | const config = { ...configArgs, root: path.resolve(process.cwd(), root) }; 126 | 127 | global.quiet = config.quiet; 128 | 129 | const mockServer = new MockServer(config); 130 | 131 | const _db = await getDataFromUrl(config.root, db); 132 | const _middlewares = await getDataFromUrl(config.root, middlewares); 133 | const _injectors = await getDataFromUrl(config.root, injectors); 134 | const _store = await getDataFromUrl(config.root, store); 135 | const _rewriters = await getDataFromUrl(config.root, rewriters); 136 | 137 | const launchServerOptions = { 138 | injectors: _injectors, 139 | middlewares: _middlewares, 140 | rewriters: _rewriters, 141 | store: _store, 142 | }; 143 | 144 | await startServer(mockServer, _db, launchServerOptions); 145 | 146 | let fileWatcher: watcher.FSWatcher; 147 | 148 | if (watch) { 149 | const filesToWatch = [_db, _middlewares, _injectors, _store, _rewriters] 150 | .filter(Boolean) 151 | .filter((file) => typeof file === 'string') 152 | .filter((file) => fs.existsSync(file)); 153 | fileWatcher = watcher.watch(filesToWatch); 154 | fileWatcher.on('change', (changedPath) => restartServer(mockServer, _db, launchServerOptions, changedPath)); 155 | } 156 | 157 | getSnapshot(mockServer, snapshots); 158 | 159 | process.on('uncaughtException', (error) => uncaughtException(error, fileWatcher)); 160 | process.stdin.setEncoding('utf8'); 161 | process.stdin.on('error', () => errorHandler(fileWatcher)); 162 | }; 163 | 164 | init(); 165 | 166 | // These configs are only meant for vscode Mock Server Extension and not for CLI 167 | export type extensionConfigs = { 168 | environment: string; 169 | watchFiles: string[]; 170 | ignoreFiles: string[]; 171 | duplicates: boolean; 172 | openInside: boolean; 173 | showInfoMsg: boolean; 174 | statusBar: { 175 | show: 'true' | 'false'; 176 | position: 'Right' | 'Left'; 177 | priority: string | number; 178 | }; 179 | }; 180 | 181 | export type CliOptions = Partial & Partial; 182 | -------------------------------------------------------------------------------- /src/defaults.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { HelperMiddlewares } from './middlewares'; 3 | import type * as ValidTypes from './types/valid.types'; 4 | 5 | /** 6 | * Configuration object for the server. 7 | * @type {ValidTypes.Config} 8 | */ 9 | export const Config: ValidTypes.Config = { 10 | root: process.cwd(), // Root path of the server. All paths refereed in db data will be relative to this path 11 | port: 3000, // Set Port to 0 to pick a random available port. 12 | host: 'localhost', // Set custom host. Set empty string to set your Local Ip Address 13 | base: '', // Mount db on a base url 14 | id: 'id', // Set db id attribute. 15 | dbMode: 'mock', // Give one of 'multi', 'fetch', 'mock' 16 | static: path.join(process.cwd(), 'public'), // Path to host a static files 17 | reverse: false, // Generate routes in reverse order 18 | noGzip: false, // Disable data compression 19 | noCors: false, // Disable CORS 20 | noCache: true, // Disable cache 21 | readOnly: false, // Allow only GET calls 22 | bodyParser: true, // Enable body-parser 23 | cookieParser: true, // Enable cookie-parser 24 | logger: true, // Enable api logger 25 | quiet: false, // Prevent from console logs 26 | log: false, // Prevent from setter logs 27 | homePage: true, // Enable Mock Server Home page 28 | }; 29 | 30 | export const Db: ValidTypes.Db = {}; 31 | export const Middlewares: ValidTypes.Middlewares = HelperMiddlewares; 32 | export const Injectors: ValidTypes.Injectors = []; 33 | export const Rewriters: ValidTypes.Rewriters = {}; 34 | export const Store: ValidTypes.Store = {}; 35 | -------------------------------------------------------------------------------- /src/middlewares/HelperMiddlewares/_CrudOperation.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import type { Locals } from '../../types/common.types'; 3 | import CRUD from '../../utils/crud'; 4 | 5 | export const _CrudOperation = async (req, res, next) => { 6 | const storeKey = '_CrudOperation'; 7 | const method = req.method; 8 | const locals = res.locals as Locals; 9 | const routeConfig = locals.routeConfig; 10 | 11 | // return if the data is not a collection. 12 | if (!(_.isArray(locals.data) && locals.data.every((d) => _.isPlainObject(d)))) return next(); 13 | 14 | routeConfig.store = _.isPlainObject(routeConfig.store) ? routeConfig.store : {}; 15 | const store = routeConfig.store || {}; 16 | 17 | if (method.toLowerCase() === 'get') { 18 | if (!store[storeKey]) store[storeKey] = _.cloneDeep(locals.data); // load data only on load 19 | locals.data = CRUD.search(req, res, store[storeKey]); 20 | if (JSON.stringify(locals.data) === JSON.stringify(store[storeKey])) delete store[storeKey]; 21 | } else if (method.toLowerCase() === 'put') { 22 | locals.data = CRUD.replace(req, res, locals.data); 23 | } else if (method.toLowerCase() === 'patch') { 24 | locals.data = CRUD.update(req, res, locals.data); 25 | } else if (method.toLowerCase() === 'post') { 26 | locals.data = CRUD.insert(req, res, locals.data); 27 | } else if (method.toLowerCase() === 'delete') { 28 | locals.data = CRUD.remove(req, res, locals.data); 29 | } 30 | 31 | next(); 32 | }; 33 | -------------------------------------------------------------------------------- /src/middlewares/HelperMiddlewares/_Fetch.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosRequestConfig } from 'axios'; 2 | import * as _ from 'lodash'; 3 | import type { Locals } from '../../types/common.types'; 4 | import { cleanObject, interpolate } from '../../utils'; 5 | import { getFileData, getStats, getUrlData, parseUrl } from '../../utils/fetch'; 6 | 7 | const setFetchData = (locals, next) => { 8 | const routeConfig = locals.routeConfig; 9 | 10 | const { isError, response, statusCode, headers, isImage } = routeConfig.fetchData || {}; 11 | locals.data = isError 12 | ? routeConfig.skipFetchError 13 | ? locals.data 14 | : isImage 15 | ? Buffer.from(response) 16 | : response 17 | : isImage 18 | ? Buffer.from(response) 19 | : response; 20 | locals.statusCode = isError ? (routeConfig.skipFetchError ? locals.statusCode : statusCode) : statusCode; 21 | locals.headers = isError ? (routeConfig.skipFetchError ? locals.headers : headers) : headers; 22 | 23 | next(); 24 | }; 25 | 26 | const getValidReq = (req, res, fetch: AxiosRequestConfig): AxiosRequestConfig => { 27 | const locals = res.locals as Locals; 28 | const config = locals.config; 29 | const replacedPath = interpolate({ config, req: _.cloneDeep(req) }, fetch.url?.replace(/\\/g, '/')); 30 | const fetchEntries = Object.entries(fetch).map(([key, val]) => { 31 | return [key, interpolate({ config, req: _.cloneDeep(req) }, val.replace(/\\/g, '/'))]; 32 | }); 33 | const request = { ..._.fromPairs(fetchEntries), url: replacedPath }; 34 | cleanObject(request); 35 | return request as AxiosRequestConfig; 36 | }; 37 | 38 | const getUrlDetail = (req, res) => { 39 | const locals = res.locals as Locals; 40 | const fetch = locals.routeConfig.fetch; 41 | 42 | let request = {} as AxiosRequestConfig; 43 | if (_.isString(fetch)) { 44 | request = { url: fetch }; 45 | } else if (_.isPlainObject(fetch)) { 46 | request = _.cloneDeep(fetch) as AxiosRequestConfig; 47 | } else { 48 | return; 49 | } 50 | 51 | if (request.url?.startsWith('http')) { 52 | request = getValidReq(req, res, request); 53 | return { extension: '', isFile: false, request }; 54 | } else { 55 | const parsedUrl = parseUrl(request.url, locals.config.root).replace(/\\/g, '/'); 56 | const interpolatedUrl = interpolate({ config: locals.config, req: _.cloneDeep(req) }, parsedUrl); 57 | const stats = getStats(interpolatedUrl); 58 | request.url = interpolatedUrl; 59 | return { request, ...stats }; 60 | } 61 | }; 62 | 63 | const setRequestUrl = (req, res) => { 64 | const locals = res.locals; 65 | const routeConfig = locals.routeConfig; 66 | 67 | const fetchCount = parseInt(`${routeConfig.fetchCount ?? ''}`); 68 | routeConfig.fetchCount = isNaN(fetchCount) ? 1 : fetchCount; 69 | const fetch = getUrlDetail(req, res); 70 | 71 | if (!fetch) return; 72 | 73 | routeConfig._request = fetch.request; 74 | routeConfig._isFile = fetch.isFile; 75 | routeConfig._extension = fetch.extension; 76 | }; 77 | 78 | export const _Fetch = async (req, res, next) => { 79 | const locals = res.locals as Locals; 80 | const routeConfig = locals.routeConfig; 81 | 82 | // If mockFirst is true and mock data is defined then skip fetch 83 | if (routeConfig.mockFirst && routeConfig.mock !== undefined) return next(); 84 | 85 | // If Data is already fetched and the fetch count is zero then send fetch response from fetchData 86 | if (routeConfig.fetchCount == 0 && !_.isEmpty(routeConfig.fetchData)) return setFetchData(locals, next); 87 | 88 | // If it don't has any fetch url or request object then skip fetch 89 | if (_.isEmpty(routeConfig.fetch)) return next(); 90 | 91 | // generate valid request object from fetch attribute 92 | setRequestUrl(req, res); 93 | 94 | // if it doesn't has a valid request url then skip fetch 95 | if (!routeConfig._request?.url) return next(); 96 | 97 | // url starts with http then fetch data from url else skip fetch 98 | if (routeConfig._request.url.startsWith('http')) { 99 | routeConfig.fetchData = await getUrlData(routeConfig._request!); 100 | } 101 | 102 | // if url is a file and is one of .json, .jsonc, .har, .txt file then fetch file else skip fetch 103 | if (routeConfig._isFile && ['.json', '.jsonc', '.har', '.txt'].includes(routeConfig._extension || '')) { 104 | routeConfig.fetchData = await getFileData(routeConfig._request!.url!); 105 | } 106 | 107 | // reduce fetch count 108 | routeConfig.fetchCount!--; 109 | 110 | // delete route config store cache due to new fetch data. 111 | delete routeConfig.store?._IterateResponse; 112 | delete routeConfig.store?._IterateRoutes; 113 | delete routeConfig.store?._CrudOperation; 114 | 115 | // set fetch data to locals 116 | return setFetchData(locals, next); 117 | }; 118 | -------------------------------------------------------------------------------- /src/middlewares/HelperMiddlewares/_FetchOnly.ts: -------------------------------------------------------------------------------- 1 | import type { Locals } from '../../types/common.types'; 2 | 3 | export const _FetchOnly = (_req, res, next) => { 4 | const locals = res.locals as Locals; 5 | const routeConfig = locals.routeConfig; 6 | locals.data = routeConfig.fetchData?.response || {}; 7 | next(); 8 | }; 9 | -------------------------------------------------------------------------------- /src/middlewares/HelperMiddlewares/_FetchTillData.ts: -------------------------------------------------------------------------------- 1 | import type { Locals } from '../../types/common.types'; 2 | 3 | export const _FetchTillData = (_req, res, next) => { 4 | const locals = res.locals as Locals; 5 | const routeConfig = locals.routeConfig; 6 | 7 | if (!routeConfig.fetchData) return next(); 8 | 9 | if (!routeConfig.fetchData.isError) { 10 | routeConfig.fetchCount = 0; 11 | } else if (routeConfig.fetchCount !== undefined && routeConfig.fetchCount == 0) { 12 | routeConfig.fetchCount = -1; 13 | } 14 | next(); 15 | }; 16 | -------------------------------------------------------------------------------- /src/middlewares/HelperMiddlewares/_IterateResponse.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import * as _ from 'lodash'; 3 | import type { Locals } from '../../types/common.types'; 4 | 5 | export const _IterateResponse = (_req, res, next) => { 6 | const storeKey = '_IterateResponse'; 7 | const locals = res.locals as Locals; 8 | const routeConfig = locals.routeConfig; 9 | routeConfig.store = _.isPlainObject(routeConfig.store) ? routeConfig.store : {}; 10 | const store = routeConfig.store || {}; 11 | 12 | if (!Array.isArray(locals.data)) { 13 | console.error(chalk.red('To use ') + chalk.yellowBright('_IterateResponse') + chalk.red(' method the data must be of type Array')); 14 | return next(); 15 | } 16 | 17 | if (!store[storeKey]?.nextIndex || store[storeKey].nextIndex > locals.data.length - 1) { 18 | store[storeKey] = { currentIndex: -1, nextIndex: 0 }; 19 | } 20 | locals.data = locals.data[store[storeKey].nextIndex]; 21 | store[storeKey].currentIndex++; 22 | store[storeKey].nextIndex++; 23 | 24 | next(); 25 | }; 26 | -------------------------------------------------------------------------------- /src/middlewares/HelperMiddlewares/_IterateRoutes.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import * as _ from 'lodash'; 3 | import type { Locals } from '../../types/common.types'; 4 | 5 | export const _IterateRoutes = (req, res, next) => { 6 | const storeKey = '_IterateRoutes'; 7 | const locals = res.locals as Locals; 8 | const routeConfig = locals.routeConfig; 9 | routeConfig.store = _.isPlainObject(routeConfig.store) ? routeConfig.store : {}; 10 | const store = routeConfig.store || {}; 11 | 12 | if (!Array.isArray(locals.data)) { 13 | console.error(chalk.red('To use ') + chalk.yellowBright('_IterateRoutes') + chalk.red(' method the data must be of type Array')); 14 | return next(); 15 | } 16 | 17 | if (!store[storeKey]?.nextIndex || store[storeKey].nextIndex > locals.data.length - 1) { 18 | store[storeKey] = { currentIndex: -1, nextIndex: 0 }; 19 | } 20 | 21 | req.url = locals.data[store[storeKey].nextIndex]; 22 | store[storeKey].currentIndex++; 23 | store[storeKey].nextIndex++; 24 | 25 | next('route'); 26 | }; 27 | -------------------------------------------------------------------------------- /src/middlewares/HelperMiddlewares/_MockOnly.ts: -------------------------------------------------------------------------------- 1 | import type { Locals } from '../../types/common.types'; 2 | 3 | export const _MockOnly = (_req, res, next) => { 4 | const locals = res.locals as Locals; 5 | const routeConfig = locals.routeConfig; 6 | locals.data = routeConfig.mock; 7 | next(); 8 | }; 9 | -------------------------------------------------------------------------------- /src/middlewares/HelperMiddlewares/_ReadOnly.ts: -------------------------------------------------------------------------------- 1 | export const _ReadOnly = (req, res, next) => { 2 | if (req.method === 'GET') { 3 | next(); // Continue 4 | } else { 5 | res.sendStatus(403); // Forbidden 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /src/middlewares/HelperMiddlewares/_SendResponse.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import type { Locals } from '../../types/common.types'; 3 | import { interpolate } from '../../utils'; 4 | 5 | export const _SendResponse = async (req, res) => { 6 | if (res.headersSent) return; // return if response is already sent 7 | 8 | const locals = res.locals; 9 | const routeConfig = locals.routeConfig; 10 | 11 | // send File for the the file types other than ".json", ".jsonc", ".har", ".txt", "" 12 | if (routeConfig._isFile && routeConfig._request?.url && !['.json', '.jsonc', '.har', '.txt', ''].includes(routeConfig._extension || '')) { 13 | routeConfig.fetchCount!--; 14 | res.sendFile(routeConfig._request.url); 15 | return; 16 | } 17 | 18 | const response = locals.data || {}; 19 | 20 | if (_.isBuffer(response) || _.isArrayBuffer(response)) return res.send(response); 21 | if (_.isPlainObject(response) || _.isArray(response)) return res.jsonp(response); 22 | if (_.isString(response)) return res.send(interpolate({ config: locals.config, req: _.cloneDeep(req) }, response)); 23 | if (_.isInteger(response)) return res.send(response.toString()); 24 | res.send(response); 25 | }; 26 | -------------------------------------------------------------------------------- /src/middlewares/HelperMiddlewares/_SetDelay.ts: -------------------------------------------------------------------------------- 1 | import type { Locals } from '../../types/common.types'; 2 | 3 | export const _SetDelay = (req, res, next) => { 4 | const locals = res.locals as Locals; 5 | 6 | const delay = locals.routeConfig?.delay || [].concat(req.query?._delay || 0)[0]; 7 | const _delay = parseInt((delay as any) || 0, 10); 8 | 9 | if (!_delay || isNaN(_delay)) return next(); 10 | 11 | setTimeout(() => next(), _delay); 12 | }; 13 | -------------------------------------------------------------------------------- /src/middlewares/HelperMiddlewares/_SetFetchDataToMock.ts: -------------------------------------------------------------------------------- 1 | import type { Locals } from '../../types/common.types'; 2 | 3 | export const _SetFetchDataToMock = (_req, res, next) => { 4 | const locals = res.locals as Locals; 5 | const routeConfig = locals.routeConfig; 6 | if (routeConfig.fetchData) { 7 | const { isError, response } = routeConfig.fetchData; 8 | routeConfig.mock = isError ? (routeConfig.skipFetchError ? routeConfig.mock : response) : response; 9 | } 10 | next(); 11 | }; 12 | -------------------------------------------------------------------------------- /src/middlewares/HelperMiddlewares/_SetHeaders.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import type { Locals } from '../../types/common.types'; 3 | 4 | export const _SetHeaders = (_req, res, next) => { 5 | const locals = res.locals as Locals; 6 | 7 | const headers = locals.headers || locals.routeConfig?.headers; 8 | 9 | if (!_.isPlainObject(headers) || _.isEmpty(headers)) return next(); 10 | 11 | // Set Response Headers 12 | Object.entries(headers).forEach(([headerName, value]) => { 13 | res.set(headerName, value); 14 | }); 15 | 16 | // set no cache 17 | if (res.locals.config.noCache) { 18 | res.set('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'); 19 | res.set('Pragma', 'no-cache'); 20 | res.set('Expires', '-1'); 21 | } 22 | 23 | next(); 24 | }; 25 | -------------------------------------------------------------------------------- /src/middlewares/HelperMiddlewares/_SetStatusCode.ts: -------------------------------------------------------------------------------- 1 | import type { Locals } from '../../types/common.types'; 2 | 3 | export const _SetStatusCode = (req, res, next) => { 4 | const locals = res.locals as Locals; 5 | 6 | const statusCode = locals.statusCode || locals.routeConfig?.statusCode || [].concat(req.query?._statusCode || 0)[0]; 7 | const _statusCode = parseInt((statusCode as any) || 0, 10); 8 | const isStatusInRange = _statusCode >= 100 && _statusCode < 600; 9 | 10 | if (!_statusCode || isNaN(_statusCode) || !isStatusInRange) return next(); 11 | 12 | res.status(_statusCode); 13 | next(); 14 | }; 15 | -------------------------------------------------------------------------------- /src/middlewares/HelperMiddlewares/index.ts: -------------------------------------------------------------------------------- 1 | export * from './_CrudOperation'; 2 | export * from './_Fetch'; 3 | export * from './_FetchOnly'; 4 | export * from './_FetchTillData'; 5 | export * from './_IterateResponse'; 6 | export * from './_IterateRoutes'; 7 | export * from './_MockOnly'; 8 | export * from './_ReadOnly'; 9 | export * from './_SendResponse'; 10 | export * from './_SetDelay'; 11 | export * from './_SetFetchDataToMock'; 12 | export * from './_SetHeaders'; 13 | export * from './_SetStatusCode'; 14 | 15 | export const globals = [ 16 | (_req, _res, next) => { 17 | next(); 18 | }, 19 | ]; 20 | -------------------------------------------------------------------------------- /src/middlewares/defaults.ts: -------------------------------------------------------------------------------- 1 | import compression from 'compression'; 2 | import cookieParser from 'cookie-parser'; 3 | import cors from 'cors'; 4 | import express from 'express'; 5 | import fs from 'fs'; 6 | import methodOverride from 'method-override'; 7 | import morgan from 'morgan'; 8 | import responseTime from 'response-time'; 9 | import * as Defaults from '../defaults'; 10 | import type { DefaultOptions } from '../types/common.types'; 11 | import type * as ValidTypes from '../types/valid.types'; 12 | 13 | export default (opts: DefaultOptions) => { 14 | const _opts = { ...Defaults.Config, ...opts } as ValidTypes.Config; 15 | 16 | const arr: any[] = []; 17 | 18 | // gives response time in Response Header X-Response-Time 19 | arr.push(responseTime()); 20 | 21 | // Serve static files 22 | if (fs.existsSync(_opts.static)) { 23 | const router = express.Router(); 24 | router.use(_opts.base, express.static(_opts.static)); 25 | arr.push(router); 26 | } 27 | 28 | // Compress all requests 29 | if (!_opts.noGzip) { 30 | arr.push(compression()); 31 | } 32 | 33 | // Enable CORS for all the requests, including static files 34 | if (!_opts.noCors) { 35 | arr.push( 36 | cors({ 37 | credentials: true, 38 | origin: true, 39 | }) 40 | ); 41 | } 42 | 43 | // Logger 44 | if (_opts.logger) { 45 | arr.push( 46 | morgan('dev', { 47 | skip: (req: any) => process.env.NODE_ENV === 'test' || req.originalUrl?.includes('/_assets/') || false, 48 | }) 49 | ); 50 | } 51 | 52 | // No cache for IE 53 | // https://support.microsoft.com/en-us/kb/234067 54 | if (_opts.noCache) { 55 | arr.push((_req, res, next) => { 56 | res.set('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'); 57 | res.set('Pragma', 'no-cache'); 58 | res.set('Expires', '-1'); 59 | next(); 60 | }); 61 | } 62 | 63 | // Read-only 64 | if (_opts.readOnly) { 65 | arr.push((req, res, next) => { 66 | if (req.method === 'GET') { 67 | next(); // Continue 68 | } else { 69 | res.sendStatus(403); // Forbidden 70 | } 71 | }); 72 | } // Add middlewares 73 | 74 | // Body Parser 75 | if (_opts.bodyParser) { 76 | arr.push(express.urlencoded({ extended: true })); 77 | arr.push(express.json({ limit: '10mb' })); 78 | } 79 | 80 | // Cookie parser 81 | if (_opts.cookieParser) { 82 | arr.push(cookieParser()); 83 | } 84 | 85 | arr.push(methodOverride()); 86 | 87 | return arr; 88 | }; 89 | -------------------------------------------------------------------------------- /src/middlewares/errorHandler.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | /** 4 | * The default export is an error handling middleware function. 5 | * It sends a response with the error status and message, or defaults to a 500 Internal Server Error if not specified. 6 | * @param {any} err - The error object that may contain a response object with status and data properties. 7 | * @param {express.Request} _req - The request object. 8 | * @param {express.Response} res - The response object. 9 | * @param {express.NextFunction} next - The next middleware function in the stack. 10 | */ 11 | export default (err, _req, res, next) => { 12 | if (!err) return next(); 13 | const response = err.response; 14 | if (response) { 15 | res.status(response.status || 500); 16 | res.send(response.data); 17 | } else { 18 | res.status(err.status || 500); 19 | res.send(err.message || 'Internal Server Error'); 20 | if (err.message !== 'Page Not Found') { 21 | console.log(chalk.red('\nError. Something went wrong !')); 22 | console.log(chalk.gray(err.message) + '\n'); 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | import Defaults from './defaults'; 2 | import ErrorHandler from './errorHandler'; 3 | import * as HelperMiddlewares from './HelperMiddlewares'; 4 | import Initializer from './initializer'; 5 | import PageNotFound from './pageNotFound'; 6 | import Rewriter from './rewriter'; 7 | 8 | export { Defaults, ErrorHandler, Initializer, PageNotFound, Rewriter, HelperMiddlewares }; 9 | -------------------------------------------------------------------------------- /src/middlewares/initializer.ts: -------------------------------------------------------------------------------- 1 | import type * as express from 'express'; 2 | import * as _ from 'lodash'; 3 | import type MockServer from '..'; 4 | import type { Locals } from '../types/common.types'; 5 | import type * as ValidTypes from '../types/valid.types'; 6 | 7 | export default (routePath: string, mockServewr: MockServer) => { 8 | return async (_req: express.Request, res: express.Response, next: express.NextFunction) => { 9 | const routeConfig = (mockServewr.getDb(routePath) || {}) as ValidTypes.RouteConfig; 10 | routeConfig.store && !_.isPlainObject(routeConfig.store) && (routeConfig.store = {}); 11 | 12 | const locals = res.locals as Locals; 13 | locals.routePath = routePath; 14 | locals.routeConfig = routeConfig; 15 | locals.getDb = mockServewr.getDb; 16 | locals.getStore = mockServewr.getStore; 17 | locals.config = mockServewr.config; 18 | 19 | locals.data = routeConfig.mock; 20 | locals.statusCode = routeConfig.statusCode; 21 | locals.headers = routeConfig.headers; 22 | 23 | next(); 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /src/middlewares/pageNotFound.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The default export is a middleware function that creates a new Error object for Page Not Found (404) and passes it to the next error handler in the stack. 3 | * @param {express.Request} _req - The request object. 4 | * @param {express.Response} _res - The response object. 5 | * @param {express.NextFunction} next - The next middleware function in the stack. 6 | */ 7 | export default (_req, _res, next) => { 8 | const err: any = new Error('Page Not Found'); 9 | err.status = 404; 10 | next(err); 11 | }; 12 | -------------------------------------------------------------------------------- /src/middlewares/rewriter.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import rewrite from 'express-urlrewrite'; 3 | import type * as ValidTypes from '../types/valid.types'; 4 | 5 | export default (rewriters: ValidTypes.Rewriters) => { 6 | const router = express.Router(); 7 | Object.keys(rewriters).forEach((key) => router.use(rewrite(key, rewriters[key]))); 8 | return router; 9 | }; 10 | -------------------------------------------------------------------------------- /src/route-config-setters.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosRequestConfig } from 'axios'; 2 | import * as _ from 'lodash'; 3 | import type { DbMode } from './types/common.types'; 4 | import type * as UserTypes from './types/user.types'; 5 | import { toBase64 } from './utils'; 6 | 7 | interface Done { 8 | done: (param?: { log?: string | boolean }) => { [key: string]: UserTypes.RouteConfig }; 9 | } 10 | 11 | export default class RouteConfigSetters implements Done { 12 | #dbMode: DbMode; 13 | routePath: string; 14 | db = {}; 15 | 16 | /** 17 | * Constructs a new route configuration with the given path, middlewares, and database mode. 18 | * @param {string} routePath - The path for the route. 19 | * @param {UserTypes.MiddlewareConfig[]} routeMiddlewares - An array of middleware configurations for the route. 20 | * @param {DbMode} dbMode - The database mode to use for the route. 21 | */ 22 | constructor(routePath: string, routeMiddlewares: UserTypes.MiddlewareConfig[], dbMode: DbMode) { 23 | this.routePath = routePath; 24 | this.#dbMode = dbMode; 25 | this.db[routePath] = { _config: true, id: toBase64(routePath) }; 26 | if (routeMiddlewares.length) { 27 | this.db[routePath].middlewares = routeMiddlewares; 28 | } 29 | } 30 | 31 | /** 32 | * Sets the ID for the route in the database. 33 | * @param {string} value - The ID value to set. 34 | * @returns {this} The current instance for chaining. 35 | */ 36 | id(value: string) { 37 | this.db[this.routePath].id = value; 38 | return this; 39 | } 40 | 41 | /** 42 | * Sets a description for the route in the database. 43 | * @param {string} value - The description to set. 44 | * @returns {this} The current instance for chaining. 45 | */ 46 | description(value: string) { 47 | this.db[this.routePath].description = value; 48 | return this; 49 | } 50 | 51 | /** 52 | * Sends a response for the route in the database, with an optional database mode. 53 | * @param {any} value - The value to send as a response. 54 | * @param {DbMode} [dbMode=this.#dbMode] - The database mode to use for the response. 55 | * @returns {this} The current instance for chaining. 56 | */ 57 | send(value: any, dbMode: DbMode = this.#dbMode) { 58 | let attribute = dbMode === 'fetch' ? 'fetch' : 'mock'; 59 | if (dbMode === 'multi') { 60 | attribute = typeof value === 'string' ? 'fetch' : 'mock'; 61 | } 62 | this.db[this.routePath][attribute] = value; 63 | return this; 64 | } 65 | 66 | /** 67 | * @alias send method. 68 | */ 69 | reply = this.send; 70 | 71 | /** 72 | * Sets headers for the route in the database. 73 | * @param {string | object} key - The header key or an object containing header key-value pairs. 74 | * @param {any} [value] - The value for the header if a key is provided. 75 | */ 76 | headers(key: string | object, value?: any) { 77 | if (_.isPlainObject(key)) { 78 | if (_.isPlainObject(this.db[this.routePath].headers)) { 79 | Object.entries(key).forEach(([headerName, value]) => { 80 | this.db[this.routePath].headers[headerName] = value; 81 | }); 82 | } else { 83 | this.db[this.routePath].headers = value; 84 | } 85 | } 86 | 87 | if (_.isString(key)) { 88 | if (_.isPlainObject(this.db[this.routePath].headers)) { 89 | this.db[this.routePath].headers[key] = value; 90 | } else { 91 | this.db[this.routePath].headers = { [key]: value }; 92 | } 93 | } 94 | } 95 | 96 | /** 97 | * Sets a mock response for the route in the database. 98 | * @param {any} value - The mock value to set. 99 | * @returns {this} The current instance for chaining. 100 | */ 101 | mock(value: any) { 102 | this.db[this.routePath].mock = value; 103 | return this; 104 | } 105 | 106 | /** 107 | * Sets the fetch configuration for the route in the database. 108 | * @param {string | AxiosRequestConfig} value - The URL or Axios request configuration to fetch. 109 | * @returns {this} The current instance for chaining. 110 | */ 111 | fetch(value: string | AxiosRequestConfig) { 112 | this.db[this.routePath].fetch = value; 113 | return this; 114 | } 115 | 116 | /** 117 | * Sets the status code for the route response in the database. 118 | * @param {number} value - The HTTP status code to set. 119 | * @returns {this} The current instance for chaining. 120 | */ 121 | statusCode(value: number) { 122 | this.db[this.routePath].statusCode = value; 123 | return this; 124 | } 125 | 126 | /** 127 | * @alias statusCode method. 128 | */ 129 | status = this.statusCode; 130 | 131 | /** 132 | * Sets the delay before the route response in the database. 133 | * @param {number} value - The delay in milliseconds. 134 | * @returns {this} The current instance for chaining. 135 | */ 136 | delay(value: number) { 137 | this.db[this.routePath].delay = value; 138 | return this; 139 | } 140 | 141 | /** 142 | * Sets the number of times to attempt fetching data for the route. 143 | * @param {number} value - The number of fetch attempts. 144 | * @returns {this} The current instance for chaining. 145 | */ 146 | fetchCount(value: number) { 147 | this.db[this.routePath].fetchCount = value; 148 | return this; 149 | } 150 | 151 | /** 152 | * Determines whether to skip errors during fetch operations for the route. 153 | * @param {boolean} value - Whether to skip fetch errors. 154 | * @returns {this} The current instance for chaining. 155 | */ 156 | skipFetchError(value: boolean) { 157 | this.db[this.routePath].skipFetchError = value; 158 | return this; 159 | } 160 | 161 | /** 162 | * Determines whether to use mock data before attempting to fetch for the route. 163 | * @param {boolean} value - Whether to use mock data first. 164 | * @returns {this} The current instance for chaining. 165 | */ 166 | mockFirst(value: boolean) { 167 | this.db[this.routePath].mockFirst = value; 168 | return this; 169 | } 170 | 171 | /** 172 | * Determines whether to use the route configuration directly without any additional processing. 173 | * @param {boolean} value - Whether to use the route configuration directly. 174 | * @returns {this} The current instance for chaining. 175 | */ 176 | directUse(value: boolean) { 177 | this.db[this.routePath].directUse = value; 178 | return this; 179 | } 180 | 181 | /** 182 | * Finalizes the route configuration and returns the database object. 183 | * @returns {Object} The database object with the configured route. 184 | */ 185 | done() { 186 | return this.db; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/types/common.types.ts: -------------------------------------------------------------------------------- 1 | import type * as express from 'express'; 2 | import type { HelperMiddlewares } from '../middlewares'; 3 | import type RouteConfigSetters from '../route-config-setters'; 4 | import type * as ParamTypes from './param.types'; 5 | import type * as UserTypes from './user.types'; 6 | import type * as ValidTypes from './valid.types'; 7 | 8 | export type HIT = { 9 | [key: string]: any; 10 | _source: { 11 | requestURI: string; 12 | response?: string; 13 | e2eRequestId?: string; 14 | session_id?: string; 15 | request: string; 16 | status_code: string; 17 | }; 18 | }; 19 | 20 | export type KIBANA = { 21 | rawResponse: { 22 | hits: { hits: HIT[] }; 23 | }; 24 | }; 25 | 26 | export type HarEntry = { 27 | [key: string]: any; 28 | _resourceType: string; 29 | request: { 30 | [key: string]: any; 31 | url: string; 32 | }; 33 | response: { 34 | [key: string]: any; 35 | status: number; 36 | content: { 37 | [key: string]: any; 38 | text: string; 39 | }; 40 | }; 41 | }; 42 | 43 | export type HAR = { 44 | log: { 45 | [key: string]: any; 46 | entries: HarEntry[]; 47 | }; 48 | }; 49 | 50 | export type RoutePairs = { [key: string]: string }; 51 | 52 | export type DefaultOptions = Partial>; 53 | 54 | export type DefaultMiddlewares = typeof HelperMiddlewares; 55 | export type UserMiddlweares = { [x: string]: express.RequestHandler | Array }; 56 | export type GlobalMiddlweares = { globals?: express.RequestHandler | Array }; 57 | export type HarMiddleware = { 58 | harEntryCallback?: (entry: HarEntry, routePath: string, routeConfig: UserTypes.RouteConfig) => { [key: string]: UserTypes.RouteConfig }; 59 | harDbCallback?: ( 60 | data: string | UserTypes.Db | { [key: string]: Omit | any[] | string } | HAR, 61 | dbFromHAR: UserTypes.Db 62 | ) => UserTypes.Db; 63 | }; 64 | export type KibanaMiddleware = { 65 | kibanaHitsCallback?: (hit: HIT, routePath: string, routeConfig: UserTypes.RouteConfig) => { [key: string]: UserTypes.RouteConfig }; 66 | kibanaDbCallback?: ( 67 | data: string | UserTypes.Db | { [key: string]: Omit | any[] | string } | HAR, 68 | dbFromKibana: UserTypes.Db 69 | ) => UserTypes.Db; 70 | }; 71 | export type MiddlewareNames = keyof DefaultMiddlewares; 72 | 73 | export type DbMode = 'mock' | 'fetch' | 'multi' | 'config'; 74 | 75 | export interface Locals { 76 | routePath: string; 77 | routeConfig: ValidTypes.RouteConfig; 78 | data: any; 79 | statusCode: number | undefined; 80 | headers: object | undefined; 81 | config: ValidTypes.Config; 82 | getStore: () => ValidTypes.Store; 83 | getDb: (routePath?: string | string[]) => ValidTypes.RouteConfig | ValidTypes.Db; 84 | } 85 | 86 | export type PathDetails = { 87 | fileName: string; 88 | extension: string; 89 | filePath: string; 90 | isFile: boolean; 91 | isDirectory: boolean; 92 | }; 93 | 94 | export type SetData = { 95 | middlewares?: ParamTypes.Middlewares; 96 | injectors?: ParamTypes.Injectors; 97 | store?: ParamTypes.Store; 98 | config?: ParamTypes.Config; 99 | }; 100 | 101 | export type GetData = { 102 | db: ValidTypes.Db; 103 | middlewares: ValidTypes.Middlewares; 104 | injectors: ValidTypes.Injectors; 105 | rewriters: ValidTypes.Rewriters; 106 | store: ValidTypes.Store; 107 | config: ValidTypes.Config; 108 | }; 109 | 110 | export type Server = { 111 | app?: express.Application; 112 | routes?: string[]; 113 | data?: GetData; 114 | getStore?: () => ValidTypes.Store; 115 | getDb?: (routePath?: string | string[]) => ValidTypes.RouteConfig | ValidTypes.Db; 116 | }; 117 | 118 | export type SetterOptions = { 119 | root?: string; 120 | merge?: boolean; 121 | log?: string | boolean; 122 | }; 123 | 124 | export type DbSetterOptions = SetterOptions & { 125 | injectors?: ParamTypes.Injectors; 126 | reverse?: boolean; 127 | dbMode?: DbMode; 128 | }; 129 | 130 | export type ValidatorOptions = { 131 | root?: string; 132 | mockServer?: Server; 133 | }; 134 | 135 | export type DbValidatorOptions = ValidatorOptions & { 136 | injectors?: ParamTypes.Injectors; 137 | reverse?: boolean; 138 | dbMode?: DbMode; 139 | }; 140 | 141 | export type LaunchServerOptions = { 142 | middlewares?: ParamTypes.Middlewares; 143 | injectors?: ParamTypes.Injectors; 144 | rewriters?: ParamTypes.Rewriters; 145 | store?: ParamTypes.Store; 146 | router?: express.Router; 147 | app?: express.Application; 148 | log?: boolean; 149 | }; 150 | 151 | export type RewriterOptions = { 152 | root?: string; 153 | router?: express.Router; 154 | log?: boolean | string; 155 | }; 156 | 157 | export type ResourceOptions = { 158 | reverse?: boolean; 159 | middlewares?: ParamTypes.Middlewares; 160 | injectors?: ParamTypes.Injectors; 161 | root?: string; 162 | router?: express.Router; 163 | dbMode?: DbMode; 164 | log?: boolean | string; 165 | }; 166 | 167 | export type ResourceReturns = { 168 | router: express.Router; 169 | create: (routePath: string, ...middlewares: UserTypes.MiddlewareConfig[]) => RouteConfigSetters; 170 | }; 171 | -------------------------------------------------------------------------------- /src/types/param.types.ts: -------------------------------------------------------------------------------- 1 | import type MockServer from '..'; 2 | import type * as UserTypes from './user.types'; 3 | 4 | export type Config = string | UserTypes.Config | ((mockServer?: MockServer) => UserTypes.Config); 5 | export type Db = string | UserTypes.Db | ((mockServer?: MockServer) => UserTypes.Db); 6 | export type Middlewares = string | UserTypes.Middlewares | ((mockServer?: MockServer) => UserTypes.Middlewares); 7 | export type Injectors = string | UserTypes.Injectors | ((mockServer?: MockServer) => UserTypes.Injectors); 8 | export type Rewriters = string | UserTypes.Rewriters | ((mockServer?: MockServer) => UserTypes.Rewriters); 9 | export type Store = string | UserTypes.Store | ((mockServer?: MockServer) => UserTypes.Store); 10 | -------------------------------------------------------------------------------- /src/types/user.types.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosRequestConfig } from 'axios'; 2 | import type * as express from 'express'; 3 | import type { DefaultMiddlewares, GlobalMiddlweares, MiddlewareNames, RoutePairs, UserMiddlweares } from './common.types'; 4 | import type * as ValidTypes from './valid.types'; 5 | 6 | export type FetchData = { 7 | isError: boolean; 8 | message?: string; 9 | response?: any; 10 | stack?: any; 11 | headers?: any; 12 | statusCode?: number; 13 | isImage?: boolean; 14 | }; 15 | 16 | export type Middlewares = Partial; 17 | export type MiddlewareConfig = express.RequestHandler | MiddlewareNames | string; 18 | 19 | export type RouteConfig = { 20 | _config: boolean; 21 | id?: string | number; 22 | description?: string; 23 | mock?: any; 24 | fetch?: string | AxiosRequestConfig; 25 | fetchData?: FetchData; 26 | store?: { [key: string]: any }; 27 | statusCode?: number; 28 | delay?: number; 29 | fetchCount?: number; 30 | skipFetchError?: boolean; 31 | mockFirst?: boolean; 32 | middlewares?: MiddlewareConfig | MiddlewareConfig[]; 33 | directUse?: boolean; 34 | headers?: { [key: string]: any }; 35 | }; 36 | 37 | export type InjectorConfig = { 38 | routes: string | string[]; 39 | override?: boolean; 40 | exact?: boolean; 41 | } & Partial; 42 | 43 | export type Config = Partial; 44 | export type Db = 45 | | { [key: string]: RouteConfig } 46 | | { [key: string]: express.RequestHandler } 47 | | { [key: string]: Omit } 48 | | { [key: string]: any[] } 49 | | { [key: string]: string }; 50 | export type Injectors = InjectorConfig[]; 51 | export type Rewriters = RoutePairs; 52 | export type Store = { [key: string]: any }; 53 | -------------------------------------------------------------------------------- /src/types/valid.types.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosRequestConfig } from 'axios'; 2 | import type { 3 | DbMode, 4 | DefaultMiddlewares, 5 | GlobalMiddlweares, 6 | HarMiddleware, 7 | KibanaMiddleware, 8 | RoutePairs, 9 | UserMiddlweares, 10 | } from './common.types'; 11 | import type * as UserTypes from './user.types'; 12 | 13 | export type Config = { 14 | /** Root path of the server. All paths refereed in db data will be relative to this path */ 15 | root: string; 16 | /** Set Port to 0 to pick a random available port. */ 17 | port: number; 18 | /** Set custom host. Set empty string to set your Local Ip Address */ 19 | host: string; 20 | /** Mount db on a base url */ 21 | base: string; 22 | /** Set db id attribute. */ 23 | id: string; 24 | /** Give one of 'multi', 'fetch', 'mock' */ 25 | dbMode: DbMode; 26 | /** Path to host a static files */ 27 | static: string; 28 | /** Generate routes in reverse order */ 29 | reverse: boolean; 30 | /** Disable data compression */ 31 | noGzip: boolean; 32 | /** Disable CORS */ 33 | noCors: boolean; 34 | /** Disable cache */ 35 | noCache: boolean; 36 | /** Allow only GET calls */ 37 | readOnly: boolean; 38 | /** Enable body-parser */ 39 | bodyParser: boolean; 40 | /** Enable cookie-parser */ 41 | cookieParser: boolean; 42 | /** Enable api logger */ 43 | logger: boolean; 44 | /** Prevent from console logs */ 45 | quiet: boolean; 46 | /** Prevent from setter logs */ 47 | log: boolean; 48 | /** Enable Mock Server Home page */ 49 | homePage: boolean; 50 | }; 51 | 52 | export type FetchData = UserTypes.FetchData; 53 | 54 | export type RouteConfig = UserTypes.RouteConfig & { 55 | id: string; 56 | middlewares?: UserTypes.MiddlewareConfig[]; 57 | fetchData?: FetchData; 58 | 59 | // fetchData Utils 60 | _isFile?: boolean; 61 | _request?: AxiosRequestConfig; 62 | _extension?: string; 63 | }; 64 | 65 | export type InjectorConfig = { 66 | routes: string[]; 67 | override?: boolean; 68 | exact?: boolean; 69 | } & Partial; 70 | 71 | export type Db = { [key: string]: RouteConfig }; 72 | export type Middlewares = DefaultMiddlewares & GlobalMiddlweares & HarMiddleware & KibanaMiddleware & UserMiddlweares; 73 | export type Injectors = InjectorConfig[]; 74 | export type Rewriters = RoutePairs; 75 | export type Store = object; 76 | -------------------------------------------------------------------------------- /src/utils/crud.ts: -------------------------------------------------------------------------------- 1 | import type * as express from 'express'; 2 | import * as _ from 'lodash'; 3 | import * as lodashIdMixin from 'lodash-id'; 4 | import { nanoid } from 'nanoid'; 5 | import { flatQuery } from '.'; 6 | 7 | const lodashId = _ as any; // just to remove type when using mixin 8 | lodashId.mixin(lodashIdMixin); 9 | 10 | export default class { 11 | static search = (req: express.Request, res: express.Response, data: any[]) => { 12 | const query = req.query; 13 | const params = req.params; 14 | 15 | if (!Object.keys(query).length && !Object.keys(params).length) return data; 16 | 17 | const config = res.locals?.config || {}; 18 | lodashId.id = config.id || 'id'; 19 | const id = lodashId.id || config.id || 'id'; 20 | 21 | let clonedData = _.cloneDeep(data); 22 | 23 | const ids = flatQuery(params[id] || query[id]); 24 | const _groupBy = flatQuery(query._group)[0] as string; 25 | const _sort = flatQuery(query._sort); 26 | const _order = flatQuery(query._order); 27 | const _start = flatQuery(query._start, true)[0] as number; 28 | const _end = flatQuery(query._end, true)[0] as number; 29 | const _limit = flatQuery(query._limit, true)[0] as number; 30 | const _page = flatQuery(query._page, true)[0] as number; 31 | const _per_page = (flatQuery(query._per_page, true)[0] as number) || 10; 32 | const _first = flatQuery(query._first)[0]; 33 | const _last = flatQuery(query._last)[0]; 34 | const _text = flatQuery(query._text); 35 | const q = flatQuery(query.q); 36 | const isRange = _start || _end; 37 | 38 | delete query._group; 39 | delete query._sort; 40 | delete query._order; 41 | delete query._start; 42 | delete query._end; 43 | delete query._limit; 44 | delete query._page; 45 | delete query._per_page; 46 | delete query._text; 47 | delete query._first; 48 | delete query._last; 49 | delete query.q; 50 | 51 | let groupList = _.values(_.groupBy(clonedData, _groupBy)); 52 | 53 | groupList = groupList.map((groupData) => { 54 | let _data = groupData; 55 | 56 | // Automatically delete query parameters that can't be found in database 57 | Object.keys(query).forEach((key) => { 58 | for (const i in _data) { 59 | const path = key.replace(/(_lt|_lte|_gt|_gte|_ne|_like)$/, ''); 60 | if (_.has(_data[i], path) || key === 'callback' || key === '_') return; 61 | } 62 | delete query[key]; 63 | }); 64 | 65 | // Makes query.id=1,2 to query.id=[1,2] 66 | for (const key in query) { 67 | query[key] = flatQuery(query[key]) as string[]; 68 | } 69 | 70 | // Filters By Id 71 | if (ids.length) { 72 | _data = ids.map((id) => lodashId.getById(_data, id)).filter(Boolean); 73 | } 74 | 75 | // Partial Text Search 76 | const searchTexts = [..._text, ...q].filter(Boolean); 77 | if (searchTexts.filter(Boolean).length) { 78 | _data = _data.filter((d) => 79 | searchTexts.some( 80 | (_t) => 81 | _.values(d) 82 | .join(', ') 83 | ?.toLowerCase() 84 | .indexOf(`${_t || ''}`?.toLowerCase()) >= 0 85 | ) 86 | ); 87 | } 88 | 89 | // Attribute Filter -> _like, _ne, _lt, _lte, _gt, _gte 90 | _data = _data.filter((item) => { 91 | for (let [key, val] of _.toPairs(query)) { 92 | const _val: any[] = ([] as any).concat(val).filter((v) => v !== undefined || v !== null); 93 | const path = key.replace(/(_lt|_lte|_gt|_gte|_ne|_like)$/, ''); 94 | const itemVal = _.get(item, path); 95 | 96 | const isMatched = _val.some((paramVal) => { 97 | if (/_lte$/.test(key)) return parseInt(itemVal) <= parseInt(paramVal); 98 | else if (/_lt$/.test(key)) return parseInt(itemVal) < parseInt(paramVal); 99 | else if (/_gte$/.test(key)) return parseInt(itemVal) >= parseInt(paramVal); 100 | else if (/_gt$/.test(key)) return parseInt(itemVal) > parseInt(paramVal); 101 | else if (/_ne$/.test(key)) return paramVal != itemVal; 102 | else if (/_like$/.test(key)) return new RegExp(paramVal, 'i').test(itemVal.toString()); 103 | else return paramVal == itemVal; 104 | }); 105 | 106 | if (!isMatched) return false; 107 | } 108 | return true; 109 | }); 110 | 111 | // Sort and Order 112 | _data = _.orderBy(_data, _sort, _order.map((o) => `${o || ''}`.toLowerCase()) as Array<'asc' | 'desc'>); 113 | 114 | // Ranging 115 | if (isRange) { 116 | const startIndex = _start ?? 0; 117 | const endIndex = _end ?? _data.length; 118 | _data = _data.slice(startIndex, endIndex) || []; 119 | } 120 | 121 | // Pagination 122 | if (_page !== undefined) { 123 | const chunks = _.chunk(_data, _per_page ?? 10); 124 | const links: any = {}; 125 | const fullURL = `http://${req.get('host')}${req.originalUrl}`; 126 | 127 | links.first = fullURL.replace(`_page=${_page}`, '_page=1'); 128 | if (_page > 1) links.prev = fullURL.replace(`_page=${_page}`, `_page=${_page - 1}`); 129 | if (_page < chunks.length) links.next = fullURL.replace(`_page=${_page}`, `_page=${_page + 1}`); 130 | links.last = fullURL.replace(`_page=${_page}`, `_page=${chunks.length}`); 131 | 132 | res.links(links); 133 | _data = chunks[_page - 1] || []; 134 | } 135 | 136 | // Limit 137 | if (_limit !== undefined) { 138 | _data = _.take(_data, _limit) || []; 139 | } 140 | 141 | // First 142 | if (_first == 'true') { 143 | _data = _.head(_data); 144 | } 145 | 146 | // Last 147 | if (_last == 'true') { 148 | _data = _.last(_data); 149 | } 150 | 151 | // Set Headers 152 | if (_start || _end || _limit || _page) { 153 | res.setHeader('X-Total-Count', _data.length); 154 | res.setHeader('Access-Control-Expose-Headers', `X-Total-Count${_page ? ', Link' : ''}`); 155 | } 156 | 157 | if (params[id] && _.isArray(_data) && _data?.length === 0) return {}; 158 | if (params[id] && _.isArray(_data) && _data?.length === 1) return _data[0]; 159 | return _data; 160 | }); 161 | 162 | const flattenData = _.flatten(groupList); 163 | 164 | if (params[id] && _.isArray(flattenData) && flattenData?.length === 0) return {}; 165 | if (params[id] && _.isArray(flattenData) && flattenData?.length === 1) return flattenData[0]; 166 | return flattenData; 167 | }; 168 | 169 | static insert = (req: express.Request, res: express.Response, data: any[]) => { 170 | const config = res.locals?.config || {}; 171 | lodashId.id = config.id || 'id'; 172 | const id = lodashId.id || config.id || 'id'; 173 | 174 | lodashId.createId = (coll) => { 175 | if (_.isEmpty(coll)) { 176 | return 1; 177 | } else { 178 | let maxId = parseInt(lodashId.maxBy(coll, id)[id], 10); // Increment integer id or generate string id 179 | return !_.isNaN(maxId) ? ++maxId : nanoid(7); 180 | } 181 | }; 182 | const body = [].concat(req.body); 183 | if (_.isEmpty(body)) return; 184 | body.forEach((b: any) => delete b.id); 185 | const insertedData = body.reduce((res, b) => res.concat(lodashId.insert(data, b)), []); 186 | return insertedData; 187 | }; 188 | 189 | static remove = (req: express.Request, res: express.Response, data: any[]) => { 190 | const config = res.locals?.config || {}; 191 | lodashId.id = config.id || 'id'; 192 | const id = lodashId.id || config.id || 'id'; 193 | 194 | if (req.params[id]) { 195 | return lodashId.removeById(data, req.params[id]); 196 | } else if (!_.isEmpty(req.query)) { 197 | return lodashId.removeWhere(data, req.query); 198 | } 199 | return {}; 200 | }; 201 | 202 | static update = (req: express.Request, res: express.Response, data: any[]) => { 203 | const config = res.locals?.config || {}; 204 | lodashId.id = config.id || 'id'; 205 | const id = lodashId.id || config.id || 'id'; 206 | 207 | const body = [].concat(req.body)[0]; 208 | if (_.isEmpty(body)) return {}; 209 | 210 | if (req.params[id]) { 211 | return lodashId.updateById(data, req.params[id], body); 212 | } else if (!_.isEmpty(req.query)) { 213 | return lodashId.updateWhere(data, req.query, body); 214 | } 215 | return {}; 216 | }; 217 | 218 | static replace = (req: express.Request, res: express.Response, data: any[]) => { 219 | const config = res.locals?.config || {}; 220 | lodashId.id = config.id || 'id'; 221 | const id = lodashId.id || config.id || 'id'; 222 | 223 | const body = [].concat(req.body)[0]; 224 | if (_.isEmpty(body)) return {}; 225 | 226 | if (req.params[id]) { 227 | return lodashId.replaceById(data, req.params[id], body); 228 | } else if (!_.isEmpty(req.query)) { 229 | const matchedIds = _.filter(data, req.query).map((d) => d[id]); 230 | return matchedIds.reduce((res, matchedId) => res.concat(lodashId.replaceById(data, matchedId, body)), []); 231 | } 232 | return {}; 233 | }; 234 | } 235 | -------------------------------------------------------------------------------- /src/utils/fetch.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosRequestConfig, AxiosResponse } from 'axios'; 2 | import axios from 'axios'; 3 | import chalk from 'chalk'; 4 | import * as cjson from 'comment-json'; 5 | import * as fs from 'fs'; 6 | import * as fsProm from 'fs/promises'; 7 | import json5 from 'json5'; 8 | import * as _ from 'lodash'; 9 | import * as path from 'path'; 10 | import { getParsedJSON } from '.'; 11 | import type { PathDetails } from '../types/common.types'; 12 | import type * as ValidTypes from '../types/valid.types'; 13 | 14 | export const importJsModuleSync = (modulePath: string) => { 15 | delete require.cache[require.resolve(modulePath)]; 16 | return require(require.resolve(modulePath)); 17 | }; 18 | 19 | export const importJsonModuleSync = (modulePath: string) => { 20 | const str = fs.readFileSync(modulePath, 'utf-8'); 21 | try { 22 | return JSON.parse(str); 23 | } catch (err) { 24 | try { 25 | return json5.parse(str); 26 | } catch (err) { 27 | return cjson.parse(str, undefined, true); 28 | } 29 | } 30 | }; 31 | 32 | export const requireFile = ( 33 | directoryPath: string, 34 | { 35 | exclude = [], 36 | recursive = true, 37 | isList = false, 38 | onlyIndex = true, 39 | }: { exclude?: string[]; recursive?: boolean; isList?: boolean; onlyIndex?: boolean } = {} 40 | ) => { 41 | const stats = getStats(directoryPath); 42 | 43 | // If path doesn't exist then return 44 | if (!stats) return; 45 | 46 | // Get File data 47 | if (stats.isFile) { 48 | try { 49 | if (stats.extension.endsWith('js')) return importJsModuleSync(stats.filePath); 50 | return importJsonModuleSync(stats.filePath); 51 | } catch (error: any) { 52 | console.log(chalk.red(`Error reading ${stats.filePath}`)); 53 | console.error(error.message); 54 | return; 55 | } 56 | } 57 | 58 | // If given path is a Directory then return accumulated data of all files in the directoryPath 59 | 60 | const filesList = getFilesList(directoryPath, { exclude, onlyIndex, recursive }); 61 | 62 | if (!filesList.length) return; 63 | 64 | if (filesList.length === 1) return requireFile(filesList[0].filePath, { exclude, isList, onlyIndex, recursive }); 65 | 66 | return isList ? getList(filesList) : getObject(filesList); 67 | }; 68 | 69 | export const getObject = (files: PathDetails[]): object => { 70 | const obj = files.reduce((mock, file) => { 71 | const data = requireFile(file.filePath); 72 | if (_.isEmpty(data) || !_.isPlainObject(data)) return mock; 73 | return { ...mock, ...data }; 74 | }, {}); 75 | return obj; 76 | }; 77 | 78 | export const getList = (files: PathDetails[]): any[] => { 79 | const list = files.reduce((mock, file) => { 80 | const data = requireFile(file.filePath); 81 | if (_.isEmpty(data) || !_.isArray(data)) return mock; 82 | return [...mock, ...data]; 83 | }, [] as any[]); 84 | return list; 85 | }; 86 | 87 | export const getFilesList = ( 88 | directoryPath: string, 89 | { exclude = [], recursive = true, onlyIndex = true }: { exclude?: string[]; recursive?: boolean; onlyIndex?: boolean } = {} 90 | ): PathDetails[] => { 91 | const stats = getStats(directoryPath); 92 | if (!stats) return []; 93 | if (stats.isFile) { 94 | return [stats]; 95 | } else if (stats.isDirectory && exclude.indexOf(directoryPath) < 0) { 96 | if (onlyIndex) { 97 | const indexPath = `${directoryPath}\\index.js`; 98 | const indexStats = getStats(indexPath); 99 | if (indexStats) return [indexStats]; 100 | } 101 | 102 | const files = fs.readdirSync(directoryPath); 103 | const filteredFiles = files.filter((file) => exclude.indexOf(file) < 0); 104 | const filesList = filteredFiles.reduce((res: PathDetails[], file: string) => { 105 | if (recursive) { 106 | return res.concat(getFilesList(`${directoryPath}/${file}`, { exclude, onlyIndex, recursive })); 107 | } 108 | return res.concat(getStats(`${directoryPath}/${file}`) || []); 109 | }, []); 110 | 111 | return filesList; 112 | } 113 | return []; 114 | }; 115 | 116 | export const getStats = (directoryPath: string): PathDetails | undefined => { 117 | if (!fs.existsSync(directoryPath)) return; 118 | const stats = fs.statSync(directoryPath); 119 | const extension = path.extname(directoryPath); 120 | const fileName = path.basename(directoryPath, extension); 121 | return { extension, fileName, filePath: directoryPath, isDirectory: stats.isDirectory(), isFile: stats.isFile() }; 122 | }; 123 | 124 | export const getFileData = async (filePath: string): Promise => { 125 | let fetchData: ValidTypes.FetchData = { isError: false }; 126 | const extension = path.extname(filePath); 127 | try { 128 | if (['.json', '.jsonc', '.har'].includes(extension)) { 129 | const str = await fsProm.readFile(filePath, { encoding: 'utf-8' }); 130 | fetchData.response = _.isEmpty(str) ? {} : getParsedJSON(str); 131 | } else if (extension === '.txt') { 132 | fetchData.response = await fsProm.readFile(filePath, { encoding: 'utf-8' }); 133 | } else { 134 | const str = await fsProm.readFile(filePath, { encoding: 'utf-8' }); 135 | fetchData.response = _.isEmpty(str) ? {} : getParsedJSON(str); 136 | } 137 | } catch (err: any) { 138 | console.error(chalk.red(err.message)); 139 | fetchData = { 140 | isError: true, 141 | message: err.message, 142 | response: {}, 143 | stack: err.stack, 144 | }; 145 | } 146 | return fetchData; 147 | }; 148 | 149 | export const getUrlData = async (request: AxiosRequestConfig): Promise => { 150 | let fetchData: ValidTypes.FetchData = { isError: false }; 151 | try { 152 | let response: AxiosResponse; 153 | if (request.url?.match(/\.(jpeg|jpg|gif|png)$/gi)) { 154 | response = await axios.get(request.url!, { responseType: 'arraybuffer' }); 155 | } else { 156 | response = await axios(request); 157 | } 158 | const isImage = response.headers['content-type']?.match(/image\/(jpeg|jpg|gif|png)$/gi)?.length > 0; 159 | const headers = { ...(response.headers || {}) }; 160 | 161 | delete headers['transfer-encoding']; 162 | delete headers['content-length']; 163 | 164 | fetchData = { 165 | headers, 166 | isError: false, 167 | isImage, 168 | response: response.data, 169 | statusCode: response.status, 170 | }; 171 | } catch (err: any) { 172 | console.error(chalk.red(err.message)); 173 | fetchData = { 174 | headers: err.response?.headers, 175 | isError: true, 176 | message: err.message || err.response?.statusText || 'Internal Server Error', 177 | response: err.response?.data ?? {}, 178 | stack: err.stack, 179 | statusCode: err.response?.status, 180 | }; 181 | } 182 | return fetchData; 183 | }; 184 | 185 | export const parseUrl = (relativeUrl?: string, root: string = process.cwd()): string => { 186 | if (!relativeUrl || typeof relativeUrl !== 'string' || !relativeUrl?.trim().length) return ''; 187 | if (relativeUrl.startsWith('http')) return relativeUrl; 188 | const parsedUrl = decodeURIComponent(path.resolve(root, relativeUrl)); 189 | return parsedUrl; 190 | }; 191 | 192 | export const requireData = ( 193 | data?: any, 194 | { 195 | root = process.cwd(), 196 | isList = false, 197 | onlyIndex = true, 198 | recursive = true, 199 | exclude = [], 200 | }: { exclude?: string[]; root?: string; isList?: boolean; onlyIndex?: boolean; recursive?: boolean } = {} 201 | ) => { 202 | if (!data) return; 203 | 204 | if (_.isFunction(data)) return data; 205 | 206 | if (_.isString(data)) { 207 | const parsedUrl = parseUrl(data, root); 208 | if (data.length && !fs.existsSync(parsedUrl)) { 209 | process.stdout.write('\n' + chalk.red('Invalid Path: ') + chalk.yellow(parsedUrl) + '\n'); 210 | return {}; 211 | } 212 | const requiredFile = requireFile(parsedUrl, { exclude, isList, onlyIndex, recursive }); 213 | return requireData(requiredFile, { exclude, isList, onlyIndex, recursive, root }); 214 | } 215 | 216 | if (isList && _.isArray(data)) return _.cloneDeep(data); 217 | if (!isList && _.isPlainObject(data)) return _.cloneDeep(data); 218 | }; 219 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import * as cjson from 'comment-json'; 2 | import * as _ from 'lodash'; 3 | import { nanoid } from 'nanoid'; 4 | import { match } from 'path-to-regexp'; 5 | import * as Defaults from '../defaults'; 6 | import type { DbMode, HAR, HarEntry, HarMiddleware, HIT, KIBANA, KibanaMiddleware } from '../types/common.types'; 7 | import type * as UserTypes from '../types/user.types'; 8 | import type * as ValidTypes from '../types/valid.types'; 9 | import { getValidRoute, getValidRouteConfig } from './validators'; 10 | 11 | // { "/route1,/route2": { ... } } -> { "/route1": {...}, "/route2": { ... } } 12 | export const normalizeDb = (object: UserTypes.Db, dbMode: DbMode = Defaults.Config.dbMode): UserTypes.Db => { 13 | const normalizedDbEntries = Object.entries(object) 14 | .map(([routePath, routeConfig]) => { 15 | return routePath.split(',').map((route) => { 16 | const validRoute = getValidRoute(route); 17 | return [validRoute, getValidRouteConfig(validRoute, routeConfig, dbMode)]; 18 | }); 19 | }) 20 | .flat(); 21 | return _.fromPairs(normalizedDbEntries) as UserTypes.Db; 22 | }; 23 | 24 | // Combines Injector Configs with the Db Configs 25 | export const getInjectedDb = (db: UserTypes.Db, injectors: ValidTypes.Injectors): ValidTypes.Db => { 26 | const injectedDb = {}; 27 | 28 | const injectConfig = (route: string, currInjectorConfig: ValidTypes.InjectorConfig) => { 29 | const prevDbConfig = (db[route] || {}) as ValidTypes.RouteConfig; 30 | const prevInjectorConfig = (injectedDb[route] || {}) as ValidTypes.RouteConfig; 31 | 32 | const prevMiddlewares = prevInjectorConfig.middlewares?.length ? prevInjectorConfig.middlewares : prevDbConfig.middlewares; 33 | const currMiddlewares = currInjectorConfig.middlewares; 34 | 35 | injectedDb[route] = { 36 | ...currInjectorConfig, 37 | ...prevDbConfig, 38 | ...prevInjectorConfig, 39 | ...(currInjectorConfig.override ? currInjectorConfig : {}), 40 | }; 41 | 42 | const mergedMiddlewares = mergeMiddlewares(prevMiddlewares, currMiddlewares); 43 | injectedDb[route].middlewares = mergedMiddlewares; 44 | if (!mergedMiddlewares?.length) { 45 | delete injectedDb[route].middlewares; 46 | } 47 | 48 | delete injectedDb[route].routes; 49 | delete injectedDb[route].override; 50 | delete injectedDb[route].exact; 51 | }; 52 | 53 | injectors.forEach((currInjectorConfig) => { 54 | const matchedRoutes = _.flatten(currInjectorConfig.routes.map((route) => getMatchedRoutesList(route, db, currInjectorConfig.exact))); 55 | matchedRoutes.forEach((route) => injectConfig(route, currInjectorConfig)); 56 | }); 57 | 58 | return _.cloneDeep({ ...db, ...injectedDb }) as ValidTypes.Db; 59 | }; 60 | 61 | export const getMatchedRoutesList = (routeToMatch: string, db: UserTypes.Db, exact = false): string[] => { 62 | const matched = match(routeToMatch); 63 | return exact ? Object.keys(db).filter((r) => r === routeToMatch) : Object.keys(db).filter((r) => matched(r)); 64 | }; 65 | 66 | export const flatQuery = (data: any, isNumber?: boolean) => { 67 | const filterNumbers = (val) => !isNaN(val) && val !== undefined && val !== null; 68 | const result = _.flattenDeep( 69 | [] 70 | .concat(data) 71 | .filter(Boolean) 72 | .map((s: string) => s.split(',')) 73 | ) 74 | .map((s: string) => (isNumber ? parseInt(s) : s)) 75 | .filter(isNumber ? filterNumbers : Boolean); 76 | return result; 77 | }; 78 | 79 | export const replaceObj = (oldObj: object, newObj: object) => { 80 | for (const key in oldObj) { 81 | delete oldObj[key]; // clearing all existing Route Config values. 82 | } 83 | for (const key in newObj) { 84 | oldObj[key] = newObj[key]; // adding updated Route Config values 85 | } 86 | }; 87 | 88 | export const cleanObject = (obj: any) => { 89 | try { 90 | for (const key in obj) { 91 | if (obj[key] === null || obj[key] === undefined) delete obj[key]; // delete if null or undefined 92 | if (obj[key] !== undefined && !_.toString(obj[key]).trim()) delete obj[key]; // delete if empty string 93 | if (obj[key] !== undefined && _.toString(obj[key]) === 'NaN') delete obj[key]; // delete if NaN 94 | if (obj[key] !== undefined && typeof obj[key] === 'object' && _.isEmpty(obj[key])) delete obj[key]; // delete If empty object 95 | if (obj[key] !== undefined && !_.isEmpty(obj[key]) && _.isPlainObject(obj[key])) cleanObject(obj[key]); // cleanObject if the value is object 96 | } 97 | } catch (err: any) {} 98 | }; 99 | 100 | export const getCleanDb = (db: ValidTypes.Db, dbMode: DbMode = 'mock'): UserTypes.Db => { 101 | for (const routePath in db) { 102 | db[routePath] = getDataByDbMode(db[routePath], dbMode); 103 | } 104 | return db; 105 | }; 106 | 107 | export const getDbConfig = (db: ValidTypes.Db, dbMode: DbMode = 'mock'): UserTypes.Db => { 108 | for (const routePath in db) { 109 | if (!db[routePath]._config) return {}; 110 | 111 | if (dbMode === 'multi') { 112 | delete db[routePath].fetch; 113 | delete db[routePath].mock; 114 | } else { 115 | if (dbMode === 'mock') delete db[routePath].mock; 116 | if (dbMode === 'fetch') delete db[routePath].fetch; 117 | } 118 | } 119 | return db; 120 | }; 121 | 122 | const getDataByDbMode = (routeConfig, dbMode: DbMode = 'mock') => { 123 | const routeConfigKeys = Object.keys(routeConfig); 124 | 125 | if (!routeConfig._config) return routeConfig; 126 | 127 | if (dbMode === 'multi') { 128 | if (routeConfigKeys.includes('fetch') && _.isString(routeConfig.fetch)) return routeConfig.fetch; 129 | if (routeConfigKeys.includes('fetch') && !_.isString(routeConfig.fetch)) return routeConfig; 130 | if (routeConfigKeys.includes('mock') && _.isString(routeConfig.mock)) return routeConfig; 131 | if (routeConfigKeys.includes('mock') && !_.isString(routeConfig.mock)) return routeConfig.mock; 132 | return routeConfig; 133 | } else { 134 | if (dbMode === 'mock' && routeConfigKeys.includes('mock')) return routeConfig.mock; 135 | if (dbMode === 'fetch' && routeConfigKeys.includes('fetch')) return routeConfig.fetch; 136 | return routeConfig; 137 | } 138 | }; 139 | 140 | export const isCollection = (arr: any[]): boolean => { 141 | if (!_.isArray(arr)) return false; 142 | if (!arr.every((i) => _.isPlainObject(i))) return false; 143 | return true; 144 | }; 145 | 146 | export const getURLPathName = (url = '') => { 147 | try { 148 | return new URL(url)?.pathname || ''; 149 | } catch (error: any) { 150 | return ''; 151 | } 152 | }; 153 | 154 | export const getParsedJSON = (json = '') => { 155 | try { 156 | return cjson.parse(json, undefined, true); 157 | } catch (error: any) { 158 | return json; 159 | } 160 | }; 161 | 162 | const getDbFromHarEntries = (entries: HarEntry[], harEntryCallback?: HarMiddleware['harEntryCallback'], iterateDuplicateRoutes = false) => { 163 | const generatedDb = {}; 164 | 165 | entries.forEach((entry: HarEntry) => { 166 | const route = getURLPathName(entry?.request?.url); 167 | const mock = getParsedJSON(entry?.response?.content?.text); 168 | 169 | if (!route) return; 170 | 171 | let routePath: string = getValidRoute(route || ''); 172 | let routeConfig: UserTypes.RouteConfig = { 173 | _config: true, 174 | mock, 175 | }; 176 | 177 | if (_.isFunction(harEntryCallback)) { 178 | const routes = harEntryCallback(entry, routePath, routeConfig) || {}; 179 | [routePath, routeConfig] = Object.entries(routes)[0] || []; 180 | routeConfig = getValidRouteConfig(routePath, routeConfig, Defaults.Config.dbMode); 181 | } 182 | routePath && routeConfig && setRouteRedirects(generatedDb, routePath, routeConfig, iterateDuplicateRoutes); 183 | }); 184 | 185 | return generatedDb as UserTypes.Db; 186 | }; 187 | 188 | const getDbFromKibanaHits = (hits: HIT[], kibanaHitCallback?: KibanaMiddleware['kibanaHitsCallback'], iterateDuplicateRoutes = false) => { 189 | const generatedDb = {}; 190 | 191 | hits.forEach((hit: HIT) => { 192 | const route = getURLPathName(hit?._source?.requestURI); 193 | const mock = getParsedJSON(hit?._source?.response); 194 | 195 | if (!route) return; 196 | 197 | let routePath: string = getValidRoute(route || ''); 198 | let routeConfig: UserTypes.RouteConfig = { 199 | _config: true, 200 | mock, 201 | }; 202 | 203 | if (_.isFunction(kibanaHitCallback)) { 204 | const routes = kibanaHitCallback(hit, routePath, routeConfig) || {}; 205 | [routePath, routeConfig] = Object.entries(routes)[0] || []; 206 | routeConfig = getValidRouteConfig(routePath, routeConfig, Defaults.Config.dbMode); 207 | } 208 | routePath && routeConfig && setRouteRedirects(generatedDb, routePath, routeConfig, iterateDuplicateRoutes); 209 | }); 210 | 211 | return generatedDb as UserTypes.Db; 212 | }; 213 | 214 | const setRouteRedirects = (db: object, routePath: string, currentRouteConfig: UserTypes.RouteConfig, iterateDuplicateRoutes = false) => { 215 | if (iterateDuplicateRoutes && db[routePath]) { 216 | const existingConfig = db[routePath]; 217 | if (db[routePath].middlewares?.[0] !== '_IterateRoutes') { 218 | const iterateRoute1 = getValidRoute(routePath + '/' + nanoid(5)); 219 | const iterateRoute2 = getValidRoute(routePath + '/' + nanoid(5)); 220 | db[routePath] = { 221 | _config: true, 222 | middlewares: ['_IterateRoutes'], 223 | mock: [iterateRoute1, iterateRoute2], 224 | }; 225 | db[iterateRoute1] = existingConfig; 226 | db[iterateRoute2] = currentRouteConfig; 227 | } else { 228 | const iterateRoute = getValidRoute(routePath + '/' + nanoid(5)); 229 | db[routePath].mock.push(iterateRoute); 230 | db[iterateRoute] = currentRouteConfig; 231 | } 232 | } else { 233 | db[routePath] = currentRouteConfig; 234 | } 235 | }; 236 | 237 | const mergeMiddlewares = ( 238 | prevMiddlewares?: UserTypes.MiddlewareConfig[], 239 | currMiddlewares?: UserTypes.MiddlewareConfig[] 240 | ): UserTypes.MiddlewareConfig[] | undefined => { 241 | if (!currMiddlewares) return prevMiddlewares; 242 | 243 | const mergedMiddlewares = currMiddlewares.reduce( 244 | (result: UserTypes.MiddlewareConfig[], middleware) => 245 | middleware === '...' ? [...result, ...(prevMiddlewares || [])] : [...result, middleware], 246 | [] 247 | ); 248 | 249 | return [...new Set(mergedMiddlewares)]; 250 | }; 251 | 252 | export const extractDbFromHAR = ( 253 | har: HAR, 254 | harEntryCallback: HarMiddleware['harEntryCallback'], 255 | harDbCallback: HarMiddleware['harDbCallback'], 256 | iterateDuplicateRoutes = false 257 | ): UserTypes.Db | undefined => { 258 | try { 259 | const entries: HarEntry[] = har?.log?.entries; 260 | const isHAR: boolean = entries?.length > 0; 261 | if (!isHAR) return; 262 | const dbFromHar: UserTypes.Db = getDbFromHarEntries(entries, harEntryCallback, iterateDuplicateRoutes); 263 | return harDbCallback?.(har, dbFromHar) || dbFromHar; 264 | } catch (err: any) { 265 | console.error(err.message); 266 | } 267 | }; 268 | 269 | export const extractDbFromKibana = ( 270 | kibana: KIBANA, 271 | kibanaHitsCallback: KibanaMiddleware['kibanaHitsCallback'], 272 | KibanaDbCallback: KibanaMiddleware['kibanaDbCallback'], 273 | iterateDuplicateRoutes = false 274 | ): UserTypes.Db | undefined => { 275 | try { 276 | const hits: HIT[] = kibana?.rawResponse?.hits?.hits; 277 | const isKibana: boolean = hits?.length > 0; 278 | if (!isKibana) return; 279 | const dbFromHits: UserTypes.Db = getDbFromKibanaHits(hits, kibanaHitsCallback, iterateDuplicateRoutes); 280 | return KibanaDbCallback?.(kibana, dbFromHits) || dbFromHits; 281 | } catch (err: any) { 282 | console.error(err.message); 283 | } 284 | }; 285 | 286 | export const toBase64 = (value = '') => { 287 | try { 288 | return Buffer.from(value).toString('base64'); 289 | } catch { 290 | return value; 291 | } 292 | }; 293 | 294 | // Helps to convert template literal strings to applied values. 295 | // Ex : Object = { config: { host: "localhost", port: 3000 } } , format = "${config.host}:${config.port}" -> "localhost:3000" 296 | export const interpolate = (object: object, format = '') => { 297 | try { 298 | const keys = _.keys(object); 299 | const values = _.values(object); 300 | // eslint-disable-next-line no-new-func 301 | return new Function(...keys, `return \`${format}\`;`)(...values); 302 | } catch (error: any) { 303 | console.error(error.message); 304 | return format; 305 | } 306 | }; 307 | 308 | export const prefixed = (prefix: string, object: object): object => { 309 | const entries = Object.entries(object).map(([route, routeConfig]) => { 310 | const prefixedRoute = getValidRoute(`${getValidRoute(prefix)}/${getValidRoute(route)}`); 311 | return [prefixedRoute, routeConfig]; 312 | }); 313 | return _.fromPairs(entries); 314 | }; 315 | -------------------------------------------------------------------------------- /src/utils/validators.ts: -------------------------------------------------------------------------------- 1 | import type express from 'express'; 2 | import ip from 'ip'; 3 | import * as _ from 'lodash'; 4 | import { getInjectedDb, isCollection, normalizeDb, toBase64 } from '.'; 5 | import * as Defaults from '../defaults'; 6 | import type { DbMode, DbValidatorOptions, ValidatorOptions } from '../types/common.types'; 7 | import type * as ParamTypes from '../types/param.types'; 8 | import type * as UserTypes from '../types/user.types'; 9 | import type * as ValidTypes from '../types/valid.types'; 10 | import { getStats, parseUrl, requireData } from './fetch'; 11 | 12 | export const getValidConfig = ( 13 | config?: ParamTypes.Config, 14 | { root = Defaults.Config.root, mockServer }: ValidatorOptions = {} 15 | ): ValidTypes.Config => { 16 | const requiredData = requireData(config, { root }); 17 | const userConfig: UserTypes.Config = _.isFunction(requiredData) ? requiredData(mockServer) : requiredData; 18 | 19 | if (_.isEmpty(userConfig) || !_.isPlainObject(userConfig)) return _.cloneDeep(Defaults.Config); 20 | 21 | const parsedRoot = parseUrl(userConfig.root, Defaults.Config.root); 22 | const _root = getStats(parsedRoot)?.isDirectory ? parsedRoot : Defaults.Config.root; 23 | 24 | const validConfig: UserTypes.Config = { 25 | ...userConfig, 26 | base: userConfig.base && getValidRoute(userConfig.base) !== '/' ? getValidRoute(userConfig.base) : undefined, 27 | dbMode: ['multi', 'fetch', 'mock', 'config'].includes(userConfig.dbMode || '') ? userConfig.dbMode : undefined, 28 | host: 29 | typeof userConfig.host !== 'undefined' 30 | ? _.isString(userConfig.host) 31 | ? userConfig.host.trim() === '' 32 | ? ip.address() 33 | : userConfig.host 34 | : undefined 35 | : undefined, 36 | id: !_.isEmpty(userConfig.id) && _.isString(userConfig.id) ? userConfig.id : undefined, 37 | port: !_.isNaN(parseInt(userConfig.port as any)) ? parseInt(userConfig.port as any) : undefined, 38 | root: userConfig.root ? _root : undefined, 39 | static: typeof userConfig.static !== 'undefined' ? parseUrl(userConfig.static, _root) : undefined, 40 | }; 41 | 42 | return _.omitBy(validConfig, _.isUndefined) as ValidTypes.Config; 43 | }; 44 | 45 | export const getValidMiddlewares = ( 46 | middlewares?: ParamTypes.Middlewares, 47 | { root = Defaults.Config.root, mockServer }: ValidatorOptions = {} 48 | ): ValidTypes.Middlewares => { 49 | const requiredData = requireData(middlewares, { root }); 50 | const userMiddlewares: UserTypes.Middlewares = _.isFunction(requiredData) ? requiredData(mockServer) : requiredData; 51 | 52 | if (_.isEmpty(userMiddlewares) || !_.isPlainObject(userMiddlewares)) return _.cloneDeep(Defaults.Middlewares); 53 | 54 | const validMiddlewares: ValidTypes.Middlewares = {} as ValidTypes.Middlewares; 55 | 56 | const middlewareNames = Object.keys(userMiddlewares); 57 | for (const name of middlewareNames) { 58 | if (_.isFunction(userMiddlewares[name])) { 59 | validMiddlewares[name] = userMiddlewares[name]!; 60 | } else if (_.isArray(userMiddlewares[name])) { 61 | const validMiddlewaresList = (userMiddlewares[name] as []).filter((m) => _.isFunction(m)); 62 | validMiddlewaresList.length && (validMiddlewares[name] = validMiddlewaresList); 63 | } 64 | } 65 | 66 | if (_.isFunction(validMiddlewares.globals)) { 67 | validMiddlewares.globals = [validMiddlewares.globals]; 68 | } 69 | 70 | if (_.isEmpty(validMiddlewares.globals)) { 71 | validMiddlewares.globals = [ 72 | (_rq, _res, next) => { 73 | next(); 74 | }, 75 | ]; 76 | } 77 | 78 | return { ...Defaults.Middlewares, ...validMiddlewares }; 79 | }; 80 | 81 | export const getValidInjectors = ( 82 | injectors?: ParamTypes.Injectors, 83 | { root = Defaults.Config.root, mockServer }: ValidatorOptions = {} 84 | ): ValidTypes.Injectors => { 85 | const requiredData = requireData(injectors, { isList: true, root }); 86 | const userInjectors: UserTypes.InjectorConfig = _.isFunction(requiredData) ? requiredData(mockServer) : requiredData; 87 | 88 | if (_.isEmpty(userInjectors) || !_.isArray(userInjectors) || !isCollection(userInjectors)) return _.cloneDeep(Defaults.Injectors); 89 | 90 | const validInjectors = userInjectors.map(getValidInjectorConfig); 91 | 92 | return validInjectors; 93 | }; 94 | 95 | export const getValidStore = ( 96 | store?: ParamTypes.Store, 97 | { root = Defaults.Config.root, mockServer }: ValidatorOptions = {} 98 | ): ValidTypes.Store => { 99 | const requiredData = requireData(store, { root }); 100 | const userStore: UserTypes.Store = _.isFunction(requiredData) ? requiredData(mockServer) : requiredData; 101 | 102 | if (_.isEmpty(userStore) || !_.isPlainObject(userStore)) return _.cloneDeep(Defaults.Store); 103 | 104 | return userStore as ValidTypes.Store; 105 | }; 106 | 107 | export const getValidRewriters = ( 108 | rewriters?: ParamTypes.Rewriters, 109 | { root = Defaults.Config.root, mockServer }: ValidatorOptions = {} 110 | ): ValidTypes.Rewriters => { 111 | const requiredData = requireData(rewriters, { root }); 112 | const userRewriters: UserTypes.Rewriters = _.isFunction(requiredData) ? requiredData(mockServer) : requiredData; 113 | 114 | if (_.isEmpty(userRewriters) || !_.isPlainObject(userRewriters)) return _.cloneDeep(Defaults.Rewriters); 115 | 116 | return userRewriters as ValidTypes.Rewriters; 117 | }; 118 | 119 | export const getValidDb = ( 120 | data?: ParamTypes.Db, 121 | { 122 | mockServer, 123 | injectors = Defaults.Injectors, 124 | root = Defaults.Config.root, 125 | reverse = Defaults.Config.reverse, 126 | dbMode = Defaults.Config.dbMode, 127 | }: DbValidatorOptions = {} 128 | ): ValidTypes.Db => { 129 | const requiredData = requireData(data, { root }); 130 | const userData: UserTypes.Db = _.isFunction(requiredData) ? requiredData(mockServer) : requiredData; 131 | 132 | if (_.isEmpty(userData) || !_.isPlainObject(userData)) return _.cloneDeep(Defaults.Db); 133 | 134 | const normalizedDb = normalizeDb(userData, dbMode); 135 | const validInjectors = getValidInjectors(injectors, { mockServer, root }); 136 | const injectedDb = getInjectedDb(normalizedDb, validInjectors); 137 | 138 | const validDb = reverse ? _.fromPairs(Object.entries(injectedDb).reverse()) : injectedDb; 139 | 140 | return validDb; 141 | }; 142 | 143 | export const getValidRouteConfig = (route: string, routeConfig: any, dbMode: DbMode = Defaults.Config.dbMode): ValidTypes.RouteConfig => { 144 | // If the given value is a function or array of function then directly use that function without helper middlewares 145 | if (_.isFunction(routeConfig) || (_.isArray(routeConfig) && routeConfig.every(_.isFunction))) 146 | return { _config: true, directUse: true, id: toBase64(route), middlewares: [routeConfig as express.RequestHandler] }; 147 | 148 | // if db mode is config then strictly expect an config object 149 | if (dbMode === 'config' && _.isPlainObject(routeConfig)) return { id: toBase64(route), mock: {}, ...routeConfig, _config: true }; 150 | if (dbMode === 'config' && !_.isPlainObject(routeConfig)) return { _config: true, id: toBase64(route), mock: {} }; 151 | 152 | // If its not already a config object then define a config based on db mode 153 | if (!_.isPlainObject(routeConfig) || !routeConfig._config) { 154 | if (dbMode === 'multi' && _.isString(routeConfig)) return { _config: true, fetch: routeConfig, id: toBase64(route) }; 155 | if (dbMode === 'multi' && !_.isString(routeConfig)) return { _config: true, id: toBase64(route), mock: routeConfig }; 156 | if (dbMode === 'mock') return { _config: true, id: toBase64(route), mock: routeConfig }; 157 | if (dbMode === 'fetch') return { _config: true, fetch: routeConfig as object, id: toBase64(route) }; 158 | } 159 | 160 | routeConfig.id = `${routeConfig.id || ''}` || toBase64(route); 161 | 162 | if (routeConfig.middlewares) { 163 | routeConfig.middlewares = ([] as UserTypes.MiddlewareConfig[]).concat((routeConfig.middlewares as UserTypes.MiddlewareConfig) || []); 164 | if (!routeConfig.middlewares.length) delete routeConfig.middlewares; 165 | } 166 | return routeConfig as ValidTypes.RouteConfig; 167 | }; 168 | 169 | export const getValidInjectorConfig = (routeConfig: UserTypes.InjectorConfig): ValidTypes.InjectorConfig => { 170 | routeConfig.routes = ([] as string[]).concat(routeConfig.routes as string).map(getValidRoute); 171 | if (routeConfig.middlewares) { 172 | routeConfig.middlewares = ([] as UserTypes.MiddlewareConfig[]).concat((routeConfig.middlewares as UserTypes.MiddlewareConfig) || []); 173 | } 174 | return routeConfig as ValidTypes.InjectorConfig; 175 | }; 176 | 177 | export const getValidRoute = (route: string): string => { 178 | const trimmedRoute = 179 | '/' + 180 | route 181 | .split('/') 182 | .filter((x) => x.trim()) 183 | .map((x) => x.trim()) 184 | .join('/'); 185 | return trimmedRoute; 186 | }; 187 | -------------------------------------------------------------------------------- /tests/mock/config/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: __dirname, 3 | base: '/api', 4 | }; 5 | -------------------------------------------------------------------------------- /tests/mock/config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "readOnly": true, 3 | "port": 4000 4 | } 5 | -------------------------------------------------------------------------------- /tests/mock/db/db.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '/comment': { 3 | _config: true, 4 | id: '1', 5 | middlewares: [], 6 | mock: { 7 | id: 1, 8 | postId: 1, 9 | name: 'Siva', 10 | }, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /tests/mock/db/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "/post": { 3 | "_config": true, 4 | "id": "1", 5 | "mock": { 6 | "id": 1, 7 | "name": "Siva" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/mock/injectors/injectors.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | routes: '/(.*)', 4 | delay: 2000, 5 | }, 6 | { 7 | routes: ['/posts', '/comments'], 8 | statusCode: 500, 9 | override: true, 10 | }, 11 | ]; 12 | -------------------------------------------------------------------------------- /tests/mock/injectors/injectors.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "routes": ["/posts"], 4 | "delay": 1000 5 | }, 6 | { 7 | "routes": ["/comments"], 8 | "statusCode": 404 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /tests/mock/middlewares/middlewares.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | logger: (req, res, next) => { 3 | next(); 4 | }, 5 | auth: [ 6 | (req, res, next) => { 7 | next(); 8 | }, 9 | (req, res, next) => { 10 | next(); 11 | }, 12 | ], 13 | globals: [ 14 | (req, res, next) => { 15 | next(); 16 | }, 17 | {}, 18 | 'XXX', 19 | ], 20 | dummy: {}, 21 | }; 22 | -------------------------------------------------------------------------------- /tests/mock/middlewares/middlewares.json: -------------------------------------------------------------------------------- 1 | { 2 | "authenticate": {} 3 | } 4 | -------------------------------------------------------------------------------- /tests/mock/rewriters/rewriters.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '/posts/:category': '/posts?category=:category', 3 | }; 4 | -------------------------------------------------------------------------------- /tests/mock/rewriters/rewriters.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api/*": "/$1" 3 | } 4 | -------------------------------------------------------------------------------- /tests/mock/store/store.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | post: { id: '1', name: 'siva' }, 3 | }; 4 | -------------------------------------------------------------------------------- /tests/mock/store/store.json: -------------------------------------------------------------------------------- 1 | { 2 | "comment": { 3 | "id": 1, 4 | "postId": 1, 5 | "name": "Siva" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/samples/server.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { MockServer } from '../../src'; 3 | 4 | const server = () => { 5 | describe('Server Testing', () => { 6 | beforeAll(async () => await MockServer.Destroy()); 7 | afterEach(async () => await MockServer.Destroy()); 8 | afterAll(async () => await MockServer.Destroy()); 9 | 10 | it('should run without any exception', async () => { 11 | const mockServer = MockServer.Create({ root: path.resolve(__dirname, '../../samples') }); 12 | 13 | const app = mockServer.app; 14 | 15 | const rewriter = mockServer.rewriter('./rewriters.json'); 16 | app.use(rewriter); 17 | 18 | const defaultsMiddlewares = mockServer.defaults(); 19 | app.use(defaultsMiddlewares); 20 | 21 | app.use((req, res, next) => { 22 | if (isAuthorized(req)) { 23 | next(); 24 | } else { 25 | res.sendStatus(401); 26 | } 27 | }); 28 | const isAuthorized = (_req) => true; 29 | 30 | app.get('/echo', (req, res) => { 31 | res.jsonp(req.query); 32 | }); 33 | 34 | mockServer.setData({ 35 | injectors: './injectors.json', 36 | middlewares: './middleware.js', 37 | store: './store.json', 38 | }); 39 | 40 | const resources = mockServer.resources('./db.json'); 41 | app.use(mockServer.config.base, resources.router); 42 | 43 | const dataResources = mockServer.resources({ 44 | '/data': { 45 | _config: true, 46 | middlewares: (_req, res, next) => { 47 | const store = res.locals.getStore(); 48 | console.log(store); 49 | res.locals.data = store?.data; 50 | next(); 51 | }, 52 | }, 53 | }); 54 | app.use(mockServer.config.base, dataResources.router); 55 | 56 | const homePage = mockServer.homePage(); 57 | app.use(mockServer.config.base, homePage); 58 | 59 | app.use(mockServer.pageNotFound); 60 | app.use(mockServer.errorHandler); 61 | 62 | await mockServer.startServer(); 63 | 64 | console.log(mockServer); 65 | }); 66 | }); 67 | }; 68 | 69 | // TODO: Write Test cases for sample db routes 70 | const sampleDb = () => {}; 71 | 72 | describe('Test in Samples', () => { 73 | server(); // Testing Server.js file 74 | sampleDb(); // Testing all routes in sample db 75 | }); 76 | -------------------------------------------------------------------------------- /tests/server/Helpers/index.ts: -------------------------------------------------------------------------------- 1 | export const invalidInputChecks = (expected) => [ 2 | ['no input', , expected], 3 | ['undefined', undefined, expected], 4 | ['null', null, expected], 5 | ['Truthy', true, expected], 6 | ['Falsy', false, expected], 7 | ['int zero', 0, expected], 8 | ['int non zero', 123, expected], 9 | ['empty object', {}, expected], 10 | ['empty list', [], expected], 11 | ['non empty list', ['XXX'], expected], 12 | ['invalid .json file path', './xxx.json', expected], 13 | ['invalid .js file', './xxx.js', expected], 14 | ['invalid folder', './xxx', expected], 15 | ]; 16 | -------------------------------------------------------------------------------- /tests/server/getters-setters.test.ts: -------------------------------------------------------------------------------- 1 | import * as Defaults from '../../src/defaults'; 2 | import { MockServer } from '../../src/index'; 3 | import { toBase64 } from '../../src/utils'; 4 | 5 | const constructor = () => { 6 | describe('new MockServer() : ', () => { 7 | it('should create with default config', async () => { 8 | const mockServer = new MockServer(); 9 | expect(mockServer.data.config).toEqual(Defaults.Config); 10 | }); 11 | 12 | it('should create with custom config', async () => { 13 | const mockServer = new MockServer({ root: __dirname }); 14 | expect(mockServer.data.config).toEqual({ ...Defaults.Config, root: __dirname }); 15 | }); 16 | }); 17 | }; 18 | 19 | const setConfig = () => { 20 | describe('mockServer.setConfig() : ', () => { 21 | let mockServer: MockServer; 22 | beforeEach(() => { 23 | mockServer = MockServer.Create(); 24 | }); 25 | afterEach(async () => { 26 | await MockServer.Destroy(); 27 | }); 28 | 29 | it('should set default config', async () => { 30 | expect(mockServer.data.config).toEqual(Defaults.Config); 31 | mockServer.setConfig(); 32 | expect(mockServer.data.config).toEqual(Defaults.Config); 33 | }); 34 | 35 | it('should set custom config', async () => { 36 | expect(mockServer.data.config).toEqual(Defaults.Config); 37 | mockServer.setConfig({ root: __dirname }); 38 | expect(mockServer.data.config).toEqual({ ...Defaults.Config, root: __dirname }); 39 | }); 40 | }); 41 | }; 42 | 43 | const setMiddleware = () => { 44 | describe('mockServer.setMiddleware() : ', () => { 45 | let mockServer: MockServer; 46 | beforeEach(() => { 47 | mockServer = MockServer.Create(); 48 | }); 49 | afterEach(async () => { 50 | await MockServer.Destroy(); 51 | }); 52 | 53 | it('should set default middlewares', async () => { 54 | mockServer.setMiddlewares(); 55 | expect(mockServer.data.middlewares).toEqual(Defaults.Middlewares); 56 | }); 57 | 58 | it('should set custom middlewares', async () => { 59 | const middleware = { logger: () => {} }; 60 | mockServer.setMiddlewares(middleware); 61 | expect(Object.keys(mockServer.data.middlewares)).toStrictEqual(Object.keys({ ...Defaults.Middlewares, ...middleware })); 62 | }); 63 | }); 64 | }; 65 | 66 | const setInjectors = () => { 67 | describe('mockServer.setInjectors() : ', () => { 68 | let mockServer: MockServer; 69 | beforeEach(() => { 70 | mockServer = MockServer.Create(); 71 | }); 72 | afterEach(async () => { 73 | await MockServer.Destroy(); 74 | }); 75 | 76 | it('should set default injectors', async () => { 77 | mockServer.setInjectors(); 78 | expect(mockServer.data.injectors).toEqual([]); 79 | }); 80 | 81 | it('should set custom injectors', async () => { 82 | const injectors = [{ routes: ['/posts'], delay: 100 }]; 83 | mockServer.setInjectors(injectors); 84 | expect(mockServer.data.injectors).toEqual(injectors); 85 | }); 86 | }); 87 | }; 88 | 89 | const setStore = () => { 90 | describe('mockServer.setStore() : ', () => { 91 | let mockServer: MockServer; 92 | beforeEach(() => { 93 | mockServer = MockServer.Create(); 94 | }); 95 | afterEach(async () => { 96 | await MockServer.Destroy(); 97 | }); 98 | 99 | it('should set default store', async () => { 100 | mockServer.setStore(); 101 | expect(mockServer.data.store).toEqual({}); 102 | }); 103 | 104 | it('should set custom store', async () => { 105 | const store = { '/posts': { id: '1', name: 'Siva' }, '/comments': { id: '1', postId: '1', name: 'Siva' } }; 106 | mockServer.setStore(store); 107 | expect(mockServer.data.store).toEqual(store); 108 | }); 109 | }); 110 | }; 111 | 112 | const setData = () => { 113 | describe('mockServer.setData() : ', () => { 114 | let mockServer: MockServer; 115 | beforeEach(() => { 116 | mockServer = MockServer.Create(); 117 | }); 118 | afterEach(async () => { 119 | await MockServer.Destroy(); 120 | }); 121 | 122 | it('should set default data', async () => { 123 | mockServer.setData(); 124 | expect(mockServer.data.db).toEqual({}); 125 | expect(mockServer.data.config).toEqual(Defaults.Config); 126 | expect(mockServer.data.middlewares).toEqual(Defaults.Middlewares); 127 | expect(mockServer.data.store).toEqual({}); 128 | expect(mockServer.data.injectors).toEqual([]); 129 | }); 130 | 131 | it('should set custom data', async () => { 132 | const db = { '/posts': { id: '1', name: 'Siva' }, '/comments': { id: '1', postId: '1', name: 'Siva' } }; 133 | const injectors = [{ routes: ['/posts'], delay: 1000 }]; 134 | const store = { '/posts': { id: '1', name: 'Siva' } }; 135 | const middlewares = { logger: () => {} }; 136 | const rewriters = { '/api/*': '/$1' }; 137 | const config = { root: __dirname }; 138 | 139 | mockServer.setData({ injectors, middlewares, store, config }); 140 | mockServer.rewriter(rewriters); 141 | mockServer.resources(db); 142 | expect(mockServer.data.db).toEqual({ 143 | '/comments': { 144 | _config: true, 145 | id: toBase64('/comments'), 146 | mock: { id: '1', postId: '1', name: 'Siva' }, 147 | }, 148 | '/posts': { 149 | _config: true, 150 | id: toBase64('/posts'), 151 | delay: 1000, 152 | mock: { id: '1', name: 'Siva' }, 153 | }, 154 | }); 155 | expect(mockServer.data.config).toEqual({ ...Defaults.Config, root: __dirname }); 156 | expect(Object.keys(mockServer.data.middlewares)).toStrictEqual(Object.keys({ ...Defaults.Middlewares, ...middlewares })); 157 | expect(mockServer.data.store).toEqual(store); 158 | expect(mockServer.data.injectors).toEqual(injectors); 159 | }); 160 | }); 161 | }; 162 | 163 | const getData = () => { 164 | describe('mockServer.getData() : ', () => { 165 | let mockServer: MockServer; 166 | beforeEach(() => { 167 | mockServer = MockServer.Create(); 168 | }); 169 | afterEach(async () => { 170 | await MockServer.Destroy(); 171 | }); 172 | 173 | it('should get data', async () => { 174 | const { db, injectors, store, middlewares, config, rewriters } = mockServer.data as any; 175 | expect(db).toBeDefined(); 176 | expect(injectors).toBeDefined(); 177 | expect(store).toBeDefined(); 178 | expect(middlewares).toBeDefined(); 179 | expect(config).toBeDefined(); 180 | expect(rewriters).toBeDefined(); 181 | }); 182 | }); 183 | }; 184 | 185 | describe('Getter and Setter', () => { 186 | constructor(); // Create a MockServer instance with a custom config 187 | setConfig(); // Set config 188 | setMiddleware(); // Set middlewares 189 | setInjectors(); // Set injectors 190 | setStore(); // Set store 191 | setData(); // Set db, middlewares, injectors, rewriters, store, config 192 | getData(); // Get data 193 | }); 194 | -------------------------------------------------------------------------------- /tests/server/integration/config.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import ip from 'ip'; 3 | import path from 'path'; 4 | import request from 'supertest'; 5 | import { MockServer } from '../../../src'; 6 | 7 | export const config = () => { 8 | describe('Testing all Configs', () => { 9 | let mockServer: MockServer; 10 | 11 | beforeAll(async () => { 12 | await MockServer.Destroy(); 13 | }); 14 | beforeEach(() => { 15 | mockServer = MockServer.Create(); 16 | }); 17 | afterEach(async () => { 18 | await MockServer.Destroy(); 19 | }); 20 | afterAll(async () => { 21 | await MockServer.Destroy(); 22 | }); 23 | 24 | it('should start server at custom port', async () => { 25 | const mock = 'Working !'; 26 | const db = { '/Hi': mock }; 27 | mockServer.setConfig({ port: 4000 }); 28 | await mockServer.launchServer(db); 29 | const response = await axios.get('http://localhost:4000/Hi'); 30 | expect(response.data).toBe(mock); 31 | }); 32 | 33 | it('should start server at custom host', async () => { 34 | const mock = 'Working !'; 35 | const db = { '/Hi': mock }; 36 | const host = ip.address(); 37 | mockServer.setConfig({ host }); 38 | await mockServer.launchServer(db); 39 | const response = await axios.get(`http://${host}:3000/Hi`); 40 | expect(response.data).toBe(mock); 41 | }); 42 | 43 | it('should pick files relative to the root path', async () => { 44 | mockServer.setConfig({ root: path.join(__dirname, '../../mock') }); 45 | await mockServer.launchServer('./db/db.json'); 46 | const response = await request(mockServer.app).get('/post'); 47 | expect(response.body).toEqual(require('../../mock/db/db.json')['/post'].mock); 48 | }); 49 | 50 | it('should not generate db in reverse order', async () => { 51 | mockServer.setConfig(); 52 | const db = { 53 | 'post/:id': 'common post', 54 | 'post/2': 'post 2', 55 | 'post/1': 'post 1', 56 | }; 57 | await mockServer.launchServer(db); 58 | expect((await request(mockServer.app).get('/post/1')).text).toBe('common post'); 59 | expect((await request(mockServer.app).get('/post/2')).text).toBe('common post'); 60 | expect((await request(mockServer.app).get('/post/5')).text).toBe('common post'); 61 | }); 62 | 63 | it('should generate db in reverse order', async () => { 64 | mockServer.setConfig({ reverse: true }); 65 | const db = { 66 | 'post/:id': 'common post', 67 | 'post/2': 'post 2', 68 | 'post/1': 'post 1', 69 | }; 70 | await mockServer.launchServer(db); 71 | expect((await request(mockServer.app).get('/post/1')).text).toBe('post 1'); 72 | expect((await request(mockServer.app).get('/post/2')).text).toBe('post 2'); 73 | expect((await request(mockServer.app).get('/post/5')).text).toBe('common post'); 74 | }); 75 | 76 | it('should generate db with base url', async () => { 77 | mockServer.setConfig({ base: '/api' }); 78 | const mock = 'Working !'; 79 | const db = { '/Hi': mock }; 80 | await mockServer.launchServer(db); 81 | const response1 = await request(mockServer.app).get('/Hi'); 82 | expect(response1.statusCode).toBe(404); 83 | const response2 = await request(mockServer.app).get('/api/Hi'); 84 | expect(response2.statusCode).toBe(200); 85 | expect(response2.text).toBe(mock); 86 | }); 87 | 88 | it('should set request read only ( only GET request is allowed )', async () => { 89 | mockServer.setConfig({ readOnly: true }); 90 | const mock = 'Working !'; 91 | const db = { '/Hi': mock }; 92 | await mockServer.launchServer(db); 93 | const response1 = await request(mockServer.app).post('/Hi'); 94 | expect(response1.statusCode).toBe(403); 95 | expect(response1.text).toBe('Forbidden'); 96 | const response2 = await request(mockServer.app).get('/Hi'); 97 | expect(response2.statusCode).toBe(200); 98 | expect(response2.text).toBe(mock); 99 | }); 100 | }); 101 | }; 102 | -------------------------------------------------------------------------------- /tests/server/integration/index.test.ts: -------------------------------------------------------------------------------- 1 | import { config } from './config'; 2 | import { routeConfig } from './routeConfig'; 3 | 4 | describe('Validators', () => { 5 | config(); // Testing all config 6 | routeConfig(); // Testing all route Configs 7 | }); 8 | -------------------------------------------------------------------------------- /tests/server/integration/routeConfig.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import request from 'supertest'; 3 | import { MockServer } from '../../../src'; 4 | 5 | export const routeConfig = () => { 6 | describe('Testing all Route Configs', () => { 7 | let mockServer: MockServer; 8 | 9 | beforeAll(async () => { 10 | await MockServer.Destroy(); 11 | }); 12 | beforeEach(() => { 13 | mockServer = MockServer.Create({ root: path.join(__dirname, '../../../') }); 14 | }); 15 | afterEach(async () => { 16 | await MockServer.Destroy(); 17 | }); 18 | afterAll(async () => { 19 | await MockServer.Destroy(); 20 | }); 21 | 22 | it('should get string response', async () => { 23 | const mock = 'Working !'; 24 | const db = { '/Hi': mock }; 25 | await mockServer.launchServer(db); 26 | const response = await request(mockServer.app).get('/Hi'); 27 | expect(response.text).toBe(mock); 28 | expect(response.statusCode).toBe(200); 29 | expect(response.type).toBe('text/html'); 30 | }); 31 | 32 | it('should get string response if a mock is a number', async () => { 33 | const db = { '/Hi': 1234 }; 34 | await mockServer.launchServer(db); 35 | const response = await request(mockServer.app).get('/Hi'); 36 | expect(response.text).toBe('1234'); 37 | expect(typeof response.text).toBe('string'); 38 | expect(response.statusCode).toBe(200); 39 | expect(response.type).toBe('text/html'); 40 | }); 41 | 42 | it('should get json object response', async () => { 43 | const mock = { id: '1', name: 'Siva' }; 44 | const db = { '/user': mock }; 45 | await mockServer.launchServer(db); 46 | const response = await request(mockServer.app).get('/user'); 47 | expect(response.body).toEqual(mock); 48 | expect(response.statusCode).toBe(200); 49 | expect(response.type).toEqual('application/json'); 50 | }); 51 | 52 | it('should get list response', async () => { 53 | const mock = [{ id: '1', name: 'Siva' }]; 54 | const db = { '/users': mock }; 55 | await mockServer.launchServer(db); 56 | const response = await request(mockServer.app).get('/users'); 57 | expect(response.body).toEqual(mock); 58 | expect(response.statusCode).toBe(200); 59 | expect(response.type).toEqual('application/json'); 60 | }); 61 | 62 | it('should get response with delay 2s', async () => { 63 | const mock = { id: '1', name: 'Siva' }; 64 | const db = { '/user': { _config: true, delay: 2000, mock } }; 65 | await mockServer.launchServer(db); 66 | const response = await request(mockServer.app).get('/user'); 67 | const responseTime = parseInt(response.header['x-response-time']); 68 | expect(responseTime).toBeGreaterThan(2000); 69 | }); 70 | 71 | it('should get response with statusCode 500', async () => { 72 | const mock = { id: '1', name: 'Siva' }; 73 | const db = { '/user': { _config: true, statusCode: 500, mock } }; 74 | await mockServer.launchServer(db); 75 | const response = await request(mockServer.app).get('/user'); 76 | expect(response.statusCode).toBe(500); 77 | }); 78 | 79 | it('should fetch data from file path', async () => { 80 | const db = { '/users': { _config: true, fetch: './tests/mock/db/db.json' } }; 81 | await mockServer.launchServer(db); 82 | const response = await request(mockServer.app).get('/users'); 83 | expect(response.body).toEqual(require('../../../tests/mock/db/db.json')); 84 | }); 85 | 86 | it('should fetch data from http url', async () => { 87 | const mock = { id: '1', name: 'Siva' }; 88 | const db = { 89 | '/user/1': mock, 90 | '/customer/:id': { _config: true, fetch: 'http://${config.host}:${config.port}/user/${req.params.id}' }, 91 | }; 92 | await mockServer.launchServer(db); 93 | const response = await request(mockServer.app).get('/customer/1'); 94 | expect(response.body).toEqual(mock); 95 | }); 96 | 97 | it('should fetch data only once', async () => { 98 | const mock = [ 99 | { id: '1', name: 'Siva' }, 100 | { id: '2', name: 'Ram' }, 101 | { id: '3', name: 'Harish' }, 102 | ]; 103 | const db = { 104 | '/user': { _config: true, mock, middlewares: ['_IterateResponse'] }, 105 | '/customer': { _config: true, fetch: 'http://${config.host}:${config.port}/user' }, 106 | }; 107 | await mockServer.launchServer(db); 108 | expect((await request(mockServer.app).get('/customer')).body).toEqual(mock[0]); 109 | expect((await request(mockServer.app).get('/customer')).body).toEqual(mock[0]); 110 | }); 111 | 112 | it('should fetch data from http url not more than twice', async () => { 113 | const mock = [ 114 | { id: '1', name: 'Siva' }, 115 | { id: '2', name: 'Ram' }, 116 | { id: '3', name: 'Harish' }, 117 | ]; 118 | const db = { 119 | '/user': { _config: true, mock, middlewares: ['_IterateResponse'] }, 120 | '/customer': { _config: true, fetch: 'http://${config.host}:${config.port}/user', fetchCount: 2 }, 121 | }; 122 | await mockServer.launchServer(db); 123 | expect((await request(mockServer.app).get('/customer')).body).toEqual(mock[0]); 124 | expect((await request(mockServer.app).get('/customer')).body).toEqual(mock[1]); 125 | expect((await request(mockServer.app).get('/customer')).body).toEqual(mock[1]); 126 | }); 127 | 128 | it('should fetch data from http url infinite times', async () => { 129 | const mock = [ 130 | { id: '1', name: 'Siva' }, 131 | { id: '2', name: 'Ram' }, 132 | { id: '3', name: 'Harish' }, 133 | ]; 134 | const db = { 135 | '/user': { _config: true, mock, middlewares: ['_IterateResponse'] }, 136 | '/customer': { _config: true, fetch: 'http://${config.host}:${config.port}/user', fetchCount: -1 }, 137 | }; 138 | await mockServer.launchServer(db); 139 | expect((await request(mockServer.app).get('/customer')).body).toEqual(mock[0]); 140 | expect((await request(mockServer.app).get('/customer')).body).toEqual(mock[1]); 141 | expect((await request(mockServer.app).get('/customer')).body).toEqual(mock[2]); 142 | expect((await request(mockServer.app).get('/customer')).body).toEqual(mock[0]); 143 | }); 144 | 145 | it('should get mock first instead of fetch', async () => { 146 | const users = [ 147 | { id: '1', name: 'Siva' }, 148 | { id: '2', name: 'Ram' }, 149 | { id: '3', name: 'Harish' }, 150 | ]; 151 | const mock = { data: 'Its Working !' }; 152 | const db = { 153 | '/user': { _config: true, mock: users, middlewares: ['_IterateResponse'] }, 154 | '/customer': { _config: true, mock, fetch: 'http://${config.host}:${config.port}/user', mockFirst: true }, 155 | }; 156 | await mockServer.launchServer(db); 157 | expect((await request(mockServer.app).get('/customer')).body).toEqual(mock); 158 | }); 159 | 160 | it('should fetch instead of mock if there is no mock an mockFirst is true', async () => { 161 | const users = [ 162 | { id: '1', name: 'Siva' }, 163 | { id: '2', name: 'Ram' }, 164 | { id: '3', name: 'Harish' }, 165 | ]; 166 | const db = { 167 | '/user': { _config: true, mock: users, middlewares: ['_IterateResponse'] }, 168 | '/customer': { _config: true, fetch: 'http://${config.host}:${config.port}/user', mockFirst: true }, 169 | }; 170 | await mockServer.launchServer(db); 171 | expect((await request(mockServer.app).get('/customer')).body).toEqual(users[0]); 172 | }); 173 | 174 | it('should fetch from url and show error from fetch data', async () => { 175 | const users = [ 176 | { id: '1', name: 'Siva' }, 177 | { id: '2', name: 'Ram' }, 178 | { id: '3', name: 'Harish' }, 179 | ]; 180 | const mock = { data: 'Its Working !' }; 181 | const db = { 182 | '/user': { _config: true, mock: users, middlewares: ['_IterateResponse'], statusCode: 404 }, 183 | '/customer': { _config: true, mock, fetch: 'http://${config.host}:${config.port}/user' }, 184 | }; 185 | await mockServer.launchServer(db); 186 | const response = await request(mockServer.app).get('/customer'); 187 | expect(response.body).toEqual(users[0]); 188 | expect(response.statusCode).toEqual(404); 189 | }); 190 | 191 | it('should mock if any error from fetch', async () => { 192 | const users = [ 193 | { id: '1', name: 'Siva' }, 194 | { id: '2', name: 'Ram' }, 195 | { id: '3', name: 'Harish' }, 196 | ]; 197 | const mock = { data: 'Its Working !' }; 198 | const db = { 199 | '/user': { _config: true, mock: users, middlewares: ['_IterateResponse'], statusCode: 404 }, 200 | '/customer': { _config: true, mock, fetch: 'http://${config.host}:${config.port}/user', skipFetchError: true }, 201 | }; 202 | await mockServer.launchServer(db); 203 | const response = await request(mockServer.app).get('/customer'); 204 | expect(response.body).toEqual(mock); 205 | expect(response.statusCode).toEqual(200); 206 | }); 207 | 208 | it('should run the given middleware name', async () => { 209 | const mock = { id: '1', name: 'Siva' }; 210 | const db = { '/user': { _config: true, middlewares: ['getUser'] } }; 211 | const middlewares = { 212 | getUser: (_req, res) => { 213 | res.send(mock); 214 | }, 215 | }; 216 | await mockServer.launchServer(db, { middlewares }); 217 | const response = await request(mockServer.app).get('/user'); 218 | expect(response.body).toEqual(mock); 219 | expect(response.statusCode).toEqual(200); 220 | }); 221 | 222 | it('should run the given middleware', async () => { 223 | const mock = { id: '1', name: 'Siva' }; 224 | const db = { 225 | '/user': { 226 | _config: true, 227 | middlewares: [ 228 | (_req, res) => { 229 | res.send(mock); 230 | }, 231 | ], 232 | }, 233 | }; 234 | await mockServer.launchServer(db); 235 | const response = await request(mockServer.app).get('/user'); 236 | expect(response.body).toEqual(mock); 237 | expect(response.statusCode).toEqual(200); 238 | }); 239 | }); 240 | }; 241 | -------------------------------------------------------------------------------- /tests/server/validators/getValidConfig.test.ts: -------------------------------------------------------------------------------- 1 | import ip from 'ip'; 2 | import path from 'path'; 3 | import * as Defaults from '../../../src/defaults'; 4 | import * as ParamTypes from '../../../src/types/param.types'; 5 | import { getValidConfig } from '../../../src/utils/validators'; 6 | import { invalidInputChecks } from '../Helpers'; 7 | 8 | describe('getValidConfig() : ', () => { 9 | describe('should return Default Config on invalid input', () => { 10 | test.each(invalidInputChecks(Defaults.Config))('If %s is passed', (_condition, input, expected) => { 11 | const config = getValidConfig(input as ParamTypes.Config); 12 | expect(config).toEqual(expected); 13 | }); 14 | }); 15 | 16 | describe('should return custom Config from a valid Path string', () => { 17 | const jsFile = require('../../mock/config/config.js'); 18 | const jsonFile = require('../../mock/config/config.json'); 19 | 20 | test.each([ 21 | ['valid .js file', './config.js', jsFile], 22 | ['valid .json file', './config.json', jsonFile], 23 | ['valid folder', './', { ...jsFile, ...jsonFile }], 24 | ])('If %s path is passed', (_condition, configPath, expected) => { 25 | const root = path.join(__dirname, '../../mock/config'); 26 | const config = getValidConfig(configPath as string, { root }); 27 | expect(config).toEqual(expected); 28 | }); 29 | }); 30 | 31 | describe('should return a valid Config', () => { 32 | test.each([ 33 | [ 34 | 'root path is not a directory', 35 | { root: path.resolve(__dirname, '../../mock/config/config.json') }, 36 | { root: decodeURIComponent(path.resolve(__dirname, '../../../')) }, 37 | ], 38 | ['root path is a directory', { root: path.join(__dirname, '../../mock') }, { root: path.join(__dirname, '../../mock') }], 39 | ['port is not a number', { port: '' }, {}], 40 | ['port is a number string', { port: '4000' }, { port: 4000 }], 41 | ['port is a number', { port: 4000 }, { port: 4000 }], 42 | ['base is empty string', { base: '' }, {}], 43 | ["base is '/' ", { base: '/' }, {}], 44 | ["base is '/api' ", { base: '/api' }, { base: '/api' }], 45 | ['static is a empty string', { static: '' }, { static: '' }], 46 | [ 47 | 'static is not a valid path', 48 | { root: path.join(__dirname, '../../mock'), static: '/mock' }, 49 | { root: path.join(__dirname, '../../mock'), static: 'C:\\mock' }, 50 | ], 51 | [ 52 | 'static is a valid path', 53 | { root: path.join(__dirname, '../../mock'), static: '../../public' }, 54 | { root: path.join(__dirname, '../../mock'), static: path.join(__dirname, '../../mock', '../../public') }, 55 | ], 56 | ['host is a empty string', { host: '' }, { host: ip.address() }], 57 | ['host is not a string', { host: 129 }, {}], 58 | ['host is a string', { host: 'localhost' }, { host: 'localhost' }], 59 | ])('If %s', (_condition, input, expected) => { 60 | const config = getValidConfig(input as ParamTypes.Config); 61 | expect(config).toEqual(expected); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /tests/server/validators/getValidDb.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import * as Defaults from '../../../src/defaults'; 3 | import * as ParamTypes from '../../../src/types/param.types'; 4 | import { toBase64 } from '../../../src/utils'; 5 | import { getValidDb } from '../../../src/utils/validators'; 6 | import { invalidInputChecks } from '../Helpers'; 7 | 8 | describe('getValidDb() : ', () => { 9 | describe('should return Default db on invalid input', () => { 10 | test.each(invalidInputChecks(Defaults.Db))('If %s is passed', (_condition, dbInput, expected) => { 11 | const db = getValidDb(dbInput as ParamTypes.Db); 12 | expect(db).toEqual(expected); 13 | }); 14 | }); 15 | 16 | describe('should return custom db from a valid Path string', () => { 17 | const jsFile = require('../../mock/db/db.js'); 18 | delete jsFile['/comment'].middlewares; //deletes /comments middleware since its empty 19 | const jsonFile = require('../../mock/db/db.json'); 20 | 21 | test.each([ 22 | ['valid .js file', './db.js', jsFile], 23 | ['valid .json file', './db.json', jsonFile], 24 | ['valid folder', './', { ...jsFile, ...jsonFile }], 25 | ])('If %s path is passed', (_condition, dbPath, expected) => { 26 | const root = path.join(__dirname, '../../mock/db'); 27 | const db = getValidDb(dbPath as string, { root }); 28 | expect(db).toEqual(expected); 29 | }); 30 | }); 31 | 32 | describe('should return valid db', () => { 33 | test('If db with direct response string', () => { 34 | const db = { post: 'Working' }; 35 | const expectedDb = { '/post': { _config: true, id: toBase64('/post'), mock: 'Working' } }; 36 | const validDb = getValidDb(db as ParamTypes.Db); 37 | expect(validDb).toEqual(expectedDb); 38 | }); 39 | 40 | test('If db with direct response object', () => { 41 | const db = { post: { id: '1', name: 'Siva' } }; 42 | const expectedDb = { '/post': { _config: true, id: toBase64('/post'), mock: { id: '1', name: 'Siva' } } }; 43 | const validDb = getValidDb(db as ParamTypes.Db); 44 | expect(validDb).toEqual(expectedDb); 45 | }); 46 | 47 | test('If db with direct response List', () => { 48 | const db = { posts: [{ id: '1', name: 'Siva' }] }; 49 | const expectedDb = { '/posts': { _config: true, id: toBase64('/posts'), mock: [{ id: '1', name: 'Siva' }] } }; 50 | const validDb = getValidDb(db as ParamTypes.Db); 51 | expect(validDb).toEqual(expectedDb); 52 | }); 53 | 54 | test('If db with config', () => { 55 | const db = { post: { _config: true, id: 1, mock: { id: '1', name: 'Siva' }, description: 'testing' } }; 56 | const expectedDb = { '/post': { _config: true, id: '1', mock: { id: '1', name: 'Siva' }, description: 'testing' } }; 57 | const validDb = getValidDb(db as ParamTypes.Db); 58 | expect(validDb).toEqual(expectedDb); 59 | }); 60 | 61 | test('If db with single middlewares', () => { 62 | const testMiddleware = () => {}; 63 | const db = { post: { _config: true, id: 1, mock: { id: '1', name: 'Siva' }, middlewares: testMiddleware } }; 64 | const expectedDb = { '/post': { _config: true, id: '1', mock: { id: '1', name: 'Siva' }, middlewares: [testMiddleware] } }; 65 | const validDb = getValidDb(db as ParamTypes.Db); 66 | expect(validDb).toEqual(expectedDb); 67 | }); 68 | 69 | test('If db with multiple middlewares', () => { 70 | const testMiddleware = () => {}; 71 | const db = { post: { _config: true, id: 1, mock: { id: '1', name: 'Siva' }, middlewares: ['_MockOnly', testMiddleware] } }; 72 | const expectedDb = { 73 | '/post': { _config: true, id: '1', mock: { id: '1', name: 'Siva' }, middlewares: ['_MockOnly', testMiddleware] }, 74 | }; 75 | const validDb = getValidDb(db as ParamTypes.Db); 76 | expect(validDb).toEqual(expectedDb); 77 | }); 78 | 79 | test('If db with empty middlewares', () => { 80 | const db = { post: { _config: true, id: 1, mock: { id: '1', name: 'Siva' }, middlewares: [] } }; 81 | const expectedDb = { '/post': { _config: true, id: '1', mock: { id: '1', name: 'Siva' } } }; 82 | const validDb = getValidDb(db as ParamTypes.Db); 83 | expect(validDb).toEqual(expectedDb); 84 | }); 85 | 86 | test('If db without middlewares', () => { 87 | const db = { post: { _config: true, id: 1, mock: { id: '1', name: 'Siva' } } }; 88 | const expectedDb = { '/post': { _config: true, id: '1', mock: { id: '1', name: 'Siva' } } }; 89 | const validDb = getValidDb(db as ParamTypes.Db); 90 | expect(validDb).toEqual(expectedDb); 91 | }); 92 | 93 | test('If db with comma separated routes', () => { 94 | const db = { '/post1,/post2': { id: '1', name: 'Siva' } }; 95 | const expectedDb = { 96 | '/post1': { _config: true, id: toBase64('/post1'), mock: { id: '1', name: 'Siva' } }, 97 | '/post2': { _config: true, id: toBase64('/post2'), mock: { id: '1', name: 'Siva' } }, 98 | }; 99 | const validDb = getValidDb(db as ParamTypes.Db); 100 | expect(validDb).toEqual(expectedDb); 101 | }); 102 | 103 | test('If db with comma separated routes and overriding rotes', () => { 104 | const db = { 105 | '/post1,/post2': { id: '1', name: 'Siva' }, 106 | '/post1': { id: '1', name: 'Ram', age: 27 }, 107 | }; 108 | const expectedDb = { 109 | '/post2': { _config: true, id: toBase64('/post2'), mock: { id: '1', name: 'Siva' } }, 110 | '/post1': { _config: true, id: toBase64('/post1'), mock: { id: '1', name: 'Ram', age: 27 } }, 111 | }; 112 | const validDb = getValidDb(db as ParamTypes.Db); 113 | expect(validDb).toEqual(expectedDb); 114 | }); 115 | 116 | test('If db with no id', () => { 117 | const db = { '/post': { _config: true, mock: 'Working' } }; 118 | const expectedDb = { '/post': { _config: true, id: toBase64('/post'), mock: 'Working' } }; 119 | const validDb = getValidDb(db as ParamTypes.Db); 120 | expect(validDb).toEqual(expectedDb); 121 | }); 122 | 123 | test('If db with custom id', () => { 124 | const db = { '/post': { _config: true, id: 1, mock: 'Working' } }; 125 | const expectedDb = { '/post': { _config: true, id: '1', mock: 'Working' } }; 126 | const validDb = getValidDb(db as ParamTypes.Db); 127 | expect(validDb).toEqual(expectedDb); 128 | }); 129 | 130 | test('If db without slash prefix for route path', () => { 131 | const db = { 'post,comment': 'Working' }; 132 | const expectedDb = { 133 | '/post': { _config: true, id: toBase64('/post'), mock: 'Working' }, 134 | '/comment': { _config: true, id: toBase64('/comment'), mock: 'Working' }, 135 | }; 136 | const validDb = getValidDb(db as ParamTypes.Db); 137 | expect(validDb).toEqual(expectedDb); 138 | }); 139 | }); 140 | 141 | describe('Db with Injectors', () => { 142 | it('should inject delay only to post', () => { 143 | const db = { 'post,comment': 'Working' }; 144 | const injectors = [{ routes: ['/post'], delay: 1000 }]; 145 | const expectedDb = { 146 | '/post': { _config: true, id: toBase64('/post'), delay: 1000, mock: 'Working' }, 147 | '/comment': { _config: true, id: toBase64('/comment'), mock: 'Working' }, 148 | }; 149 | const validDb = getValidDb(db, { injectors }); 150 | expect(validDb).toEqual(expectedDb); 151 | }); 152 | 153 | it('should inject delay to all routes and not override the existing delay', () => { 154 | const db = { 'post,comment': 'Working', user: { _config: true, delay: 2000, mock: 'Working' } }; 155 | const injectors = [{ routes: ['/(.*)'], delay: 1000 }]; 156 | const expectedDb = { 157 | '/post': { _config: true, id: toBase64('/post'), delay: 1000, mock: 'Working' }, 158 | '/comment': { _config: true, id: toBase64('/comment'), delay: 1000, mock: 'Working' }, 159 | '/user': { _config: true, id: toBase64('/user'), delay: 2000, mock: 'Working' }, 160 | }; 161 | const validDb = getValidDb(db, { injectors }); 162 | expect(validDb).toEqual(expectedDb); 163 | }); 164 | 165 | it('should inject delay to all routes and override the existing delay', () => { 166 | const db = { 'post,comment': 'Working', user: { _config: true, delay: 2000, mock: 'Working' } }; 167 | const injectors = [{ routes: ['/(.*)'], delay: 1000, override: true }]; 168 | const expectedDb = { 169 | '/post': { _config: true, id: toBase64('/post'), delay: 1000, mock: 'Working' }, 170 | '/comment': { _config: true, id: toBase64('/comment'), delay: 1000, mock: 'Working' }, 171 | '/user': { _config: true, id: toBase64('/user'), delay: 1000, mock: 'Working' }, 172 | }; 173 | const validDb = getValidDb(db, { injectors }); 174 | expect(validDb).toEqual(expectedDb); 175 | }); 176 | 177 | it('should inject statusCode only to exact matching routes', () => { 178 | const db = { 179 | '/post/1': { _config: true, id: '1', mock: 'Working' }, 180 | '/post/2': { _config: true, id: '2', mock: 'Working' }, 181 | '/post/:id': { _config: true, id: '3', mock: 'Working' }, 182 | }; 183 | const injectors = [{ routes: ['/post/:id'], statusCode: 500, exact: true }]; 184 | const expectedDb = { 185 | '/post/1': { _config: true, id: '1', mock: 'Working' }, 186 | '/post/2': { _config: true, id: '2', mock: 'Working' }, 187 | '/post/:id': { _config: true, id: '3', mock: 'Working', statusCode: 500 }, 188 | }; 189 | const validDb = getValidDb(db, { injectors }); 190 | expect(validDb).toEqual(expectedDb); 191 | }); 192 | 193 | it('should inject statusCode all pattern matching routes', () => { 194 | const db = { 195 | '/post/1': { _config: true, id: '1', mock: 'Working' }, 196 | '/post/2': { _config: true, id: '2', mock: 'Working' }, 197 | '/post/3/comment': { _config: true, id: '3', mock: 'Working' }, 198 | }; 199 | const injectors = [{ routes: ['/post/:id'], statusCode: 500 }]; 200 | const expectedDb = { 201 | '/post/1': { _config: true, id: '1', mock: 'Working', statusCode: 500 }, 202 | '/post/2': { _config: true, id: '2', mock: 'Working', statusCode: 500 }, 203 | '/post/3/comment': { _config: true, id: '3', mock: 'Working' }, 204 | }; 205 | const validDb = getValidDb(db, { injectors }); 206 | expect(validDb).toEqual(expectedDb); 207 | }); 208 | 209 | it('should inject middlewares and override the existing middlewares', () => { 210 | const db = { '/post': { _config: true, id: '1', mock: 'Working', middlewares: ['_MockOnly'] } }; 211 | const injectors = [{ routes: ['/post'], middlewares: ['_ReadOnly'] }]; 212 | const expectedDb = { '/post': { _config: true, id: '1', mock: 'Working', middlewares: ['_ReadOnly'] } }; 213 | const validDb = getValidDb(db, { injectors }); 214 | expect(validDb).toEqual(expectedDb); 215 | }); 216 | 217 | it('should inject middlewares and merge the existing middlewares', () => { 218 | const db = { '/post': { _config: true, id: '1', mock: 'Working', middlewares: ['_MockOnly'] } }; 219 | const injectors = [{ routes: ['/post'], middlewares: ['...', '_ReadOnly'] }]; 220 | const expectedDb = { '/post': { _config: true, id: '1', mock: 'Working', middlewares: ['_MockOnly', '_ReadOnly'] } }; 221 | const validDb = getValidDb(db, { injectors }); 222 | expect(validDb).toEqual(expectedDb); 223 | }); 224 | 225 | it('should inject middlewares and merge the existing middlewares', () => { 226 | const db = { '/post': { _config: true, id: '1', mock: 'Working', middlewares: ['_MockOnly'] } }; 227 | const injectors = [{ routes: ['/post'], middlewares: ['...', '_ReadOnly'] }]; 228 | const expectedDb = { '/post': { _config: true, id: '1', mock: 'Working', middlewares: ['_MockOnly', '_ReadOnly'] } }; 229 | const validDb = getValidDb(db, { injectors }); 230 | expect(validDb).toEqual(expectedDb); 231 | }); 232 | 233 | it('should inject empty middlewares and remove existing middlewares', () => { 234 | const db = { '/post': { _config: true, id: '1', mock: 'Working', middlewares: ['_MockOnly'] } }; 235 | const injectors = [{ routes: ['/post'], middlewares: [] }]; 236 | const expectedDb = { '/post': { _config: true, id: '1', mock: 'Working' } }; 237 | const validDb = getValidDb(db, { injectors }); 238 | expect(validDb).toEqual(expectedDb); 239 | }); 240 | }); 241 | }); 242 | -------------------------------------------------------------------------------- /tests/server/validators/getValidInjectors.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import * as Defaults from '../../../src/defaults'; 3 | import * as ParamTypes from '../../../src/types/param.types'; 4 | import { getValidInjectors } from '../../../src/utils/validators'; 5 | import { invalidInputChecks } from '../Helpers'; 6 | 7 | describe('getValidInjectors() : ', () => { 8 | describe('should return Default Injectors on invalid input', () => { 9 | test.each(invalidInputChecks(Defaults.Injectors))('If %s is passed', (_condition, input, expected) => { 10 | const injectors = getValidInjectors(input as ParamTypes.Injectors); 11 | expect(injectors).toEqual(expected); 12 | }); 13 | }); 14 | 15 | describe('should return custom Injectors from a valid Path string', () => { 16 | const jsFile = require('../../mock/injectors/injectors.js'); 17 | jsFile[0].routes = [jsFile[0].routes]; // always give list for routes 18 | const jsonFile = require('../../mock/injectors/injectors.json'); 19 | 20 | test.each([ 21 | ['valid .js file', './injectors.js', jsFile], 22 | ['valid .json file', './injectors.json', jsonFile], 23 | ['valid folder', './', [...jsFile, ...jsonFile]], 24 | ])('If %s path is passed', (_condition, injectorsPath, expected) => { 25 | const root = path.join(__dirname, '../../mock/injectors'); 26 | const injectors = getValidInjectors(injectorsPath as string, { root }); 27 | expect(injectors).toEqual(expected); 28 | }); 29 | }); 30 | 31 | describe('should return valid Injectors', () => { 32 | const testMiddleware = () => {}; 33 | test.each([ 34 | ['routes is not a list', [{ routes: '/posts', delay: 1000 }], [{ routes: ['/posts'], delay: 1000 }]], 35 | [ 36 | 'middlewares is not a list', 37 | [{ routes: '/posts', middlewares: testMiddleware }], 38 | [{ routes: ['/posts'], middlewares: [testMiddleware] }], 39 | ], 40 | ['middlewares is not given', [{ routes: '/posts' }], [{ routes: ['/posts'] }]], 41 | ['middlewares is an empty list', [{ routes: '/posts', middlewares: [] }], [{ routes: ['/posts'], middlewares: [] }]], 42 | ])('If %s', (_condition, testInjectors, expected) => { 43 | const injectors = getValidInjectors(testInjectors as ParamTypes.Injectors); 44 | expect(injectors).toEqual(expected); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /tests/server/validators/getValidMiddleware.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import * as Defaults from '../../../src/defaults'; 3 | import * as ParamTypes from '../../../src/types/param.types'; 4 | import { getValidMiddlewares } from '../../../src/utils/validators'; 5 | import { invalidInputChecks } from '../Helpers'; 6 | 7 | describe('getValidMiddlewares() : ', () => { 8 | describe('should return Default Middlewares on invalid input', () => { 9 | test.each(invalidInputChecks(Defaults.Middlewares))('If %s is passed', (_condition, input, expected) => { 10 | const middlewares = getValidMiddlewares(input as ParamTypes.Middlewares); 11 | expect(middlewares).toEqual(expected); 12 | }); 13 | }); 14 | 15 | describe('should return custom Middlewares from a valid Path string', () => { 16 | const expectedMiddleware = { ...Defaults.Middlewares, ...require('../../mock/middlewares/middlewares.js') }; 17 | delete expectedMiddleware.dummy; // deletes it since its not a functions 18 | expectedMiddleware.globals = [expectedMiddleware.globals[0]]; // filters to only function 19 | 20 | test.each([ 21 | ['invalid .json file', '/middlewares.json', Defaults.Middlewares], 22 | ['valid .js file', './middlewares.js', expectedMiddleware], 23 | ['valid folder', './', expectedMiddleware], 24 | ])('If %s path is passed', (_condition, middlewaresPath, expected) => { 25 | const root = path.join(__dirname, '../../mock/middlewares'); 26 | const middlewares = getValidMiddlewares(middlewaresPath as string, { root }); 27 | expect(middlewares).toEqual(expected); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/server/validators/getValidRewriters.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import * as Defaults from '../../../src/defaults'; 3 | import * as ParamTypes from '../../../src/types/param.types'; 4 | import { getValidRewriters } from '../../../src/utils/validators'; 5 | import { invalidInputChecks } from '../Helpers'; 6 | 7 | describe('getValidRewriters() : ', () => { 8 | describe('should return Default rewriters on invalid input', () => { 9 | test.each(invalidInputChecks(Defaults.Rewriters))('If %s is passed', (_condition, input, expected) => { 10 | const rewriters = getValidRewriters(input as ParamTypes.Rewriters); 11 | expect(rewriters).toEqual(expected); 12 | }); 13 | }); 14 | 15 | describe('should return custom rewriters from a valid Path string', () => { 16 | const jsFile = require('../../mock/rewriters/rewriters.js'); 17 | const jsonFile = require('../../mock/rewriters/rewriters.json'); 18 | 19 | test.each([ 20 | ['valid .js file', './rewriters.js', jsFile], 21 | ['valid .json file', './rewriters.json', jsonFile], 22 | ['valid folder', './', { ...jsFile, ...jsonFile }], 23 | ])('If %s path is passed', (_condition, rewritersPath, expected) => { 24 | const root = path.join(__dirname, '../../mock/rewriters'); 25 | const rewriters = getValidRewriters(rewritersPath as string, { root }); 26 | expect(rewriters).toEqual(expected); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/server/validators/getValidStore.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import * as Defaults from '../../../src/defaults'; 3 | import * as ParamTypes from '../../../src/types/param.types'; 4 | import { getValidStore } from '../../../src/utils/validators'; 5 | import { invalidInputChecks } from '../Helpers'; 6 | 7 | describe('getValidStore() : ', () => { 8 | describe('should return Default store on invalid input', () => { 9 | test.each(invalidInputChecks(Defaults.Store))('If %s is passed', (_condition, input, expected) => { 10 | const store = getValidStore(input as ParamTypes.Store); 11 | expect(store).toEqual(expected); 12 | }); 13 | }); 14 | 15 | describe('should return custom store from a valid Path string', () => { 16 | const jsFile = require('../../mock/store/store.js'); 17 | const jsonFile = require('../../mock/store/store.json'); 18 | 19 | test.each([ 20 | ['valid .js file', './store.js', jsFile], 21 | ['valid .json file', './store.json', jsonFile], 22 | ['valid folder', './', { ...jsFile, ...jsonFile }], 23 | ])('If %s path is passed', (_condition, storePath, expected) => { 24 | const root = path.join(__dirname, '../../mock/store'); 25 | const store = getValidStore(storePath as string, { root }); 26 | expect(store).toEqual(expected); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | /* Projects */ 5 | // "incremental": true, /* Enable incremental compilation */ 6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 7 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 11 | /* Language and Environment */ 12 | "target": "ES2015" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 13 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 14 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 15 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 16 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 17 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 18 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 19 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 20 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 21 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 22 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 23 | /* Modules */ 24 | "module": "CommonJS" /* Specify what module code is generated. */, 25 | "rootDir": "./src" /* Specify the root folder within your source files. */, 26 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, 27 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 28 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 29 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 30 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 31 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 32 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 33 | "resolveJsonModule": true /* Enable importing .json files */, 34 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 35 | /* JavaScript Support */ 36 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 37 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 38 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 39 | /* Emit */ 40 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 41 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 42 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 43 | "sourceMap": false /* Create source map files for emitted JavaScript files. */, 44 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 45 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 46 | // "removeComments": true, /* Disable emitting comments. */ 47 | // "noEmit": true, /* Disable emitting files from a compilation. */ 48 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 49 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 50 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 51 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 52 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 53 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 54 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 55 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 56 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 57 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 58 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 59 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 60 | "preserveConstEnums": true /* Disable erasing `const enum` declarations in generated code. */, 61 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 62 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 63 | /* Interop Constraints */ 64 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 65 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 66 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 67 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 68 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 69 | /* Type Checking */ 70 | "strict": true /* Enable all strict type-checking options. */, 71 | "allowSyntheticDefaultImports": true, 72 | "noImplicitAny": false /* Enable error reporting for expressions and declarations with an implied `any` type.. */, 73 | "strictNullChecks": true /* When type checking, take into account `null` and `undefined`. */, 74 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 75 | "strictBindCallApply": true /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */, 76 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 77 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 78 | "useUnknownInCatchVariables": false /* Type catch clause variables as 'unknown' instead of 'any'. */, 79 | "alwaysStrict": true /* Ensure 'use strict' is always emitted. */, 80 | "noUnusedLocals": true /* Enable error reporting when a local variables aren't read. */, 81 | "noUnusedParameters": true /* Raise an error when a function parameter isn't read */, 82 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 83 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 84 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 85 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 86 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 87 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 88 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 89 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 90 | /* Completeness */ 91 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 92 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 93 | }, 94 | "include": ["src"] 95 | } 96 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | /* Projects */ 5 | // "incremental": true, /* Enable incremental compilation */ 6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 7 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 11 | /* Language and Environment */ 12 | "target": "ES2015" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 13 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 14 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 15 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 16 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 17 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 18 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 19 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 20 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 21 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 22 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 23 | /* Modules */ 24 | "module": "CommonJS" /* Specify what module code is generated. */, 25 | "rootDir": "./src" /* Specify the root folder within your source files. */, 26 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, 27 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 28 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 29 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 30 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 31 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 32 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 33 | "resolveJsonModule": true /* Enable importing .json files */, 34 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 35 | /* JavaScript Support */ 36 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 37 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 38 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 39 | /* Emit */ 40 | "declaration": false /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 41 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 42 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 43 | "sourceMap": true /* Create source map files for emitted JavaScript files. */, 44 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 45 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 46 | // "removeComments": true, /* Disable emitting comments. */ 47 | // "noEmit": true, /* Disable emitting files from a compilation. */ 48 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 49 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 50 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 51 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 52 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 53 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 54 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 55 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 56 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 57 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 58 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 59 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 60 | "preserveConstEnums": true /* Disable erasing `const enum` declarations in generated code. */, 61 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 62 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 63 | /* Interop Constraints */ 64 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 65 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 66 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 67 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 68 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 69 | /* Type Checking */ 70 | "strict": true /* Enable all strict type-checking options. */, 71 | "allowSyntheticDefaultImports": true, 72 | "noImplicitAny": false /* Enable error reporting for expressions and declarations with an implied `any` type.. */, 73 | "strictNullChecks": true /* When type checking, take into account `null` and `undefined`. */, 74 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 75 | "strictBindCallApply": true /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */, 76 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 77 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 78 | "useUnknownInCatchVariables": false /* Type catch clause variables as 'unknown' instead of 'any'. */, 79 | "alwaysStrict": true /* Ensure 'use strict' is always emitted. */, 80 | "noUnusedLocals": true /* Enable error reporting when a local variables aren't read. */, 81 | "noUnusedParameters": true /* Raise an error when a function parameter isn't read */, 82 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 83 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 84 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 85 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 86 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 87 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 88 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 89 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 90 | /* Completeness */ 91 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 92 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 93 | }, 94 | "include": ["src"] 95 | } 96 | --------------------------------------------------------------------------------