├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENCE ├── README.md ├── _config.yml ├── index.d.ts ├── package.json ├── src ├── core.ts ├── decorators │ ├── action.ts │ ├── chain.ts │ ├── controller.ts │ ├── error.ts │ ├── filter.ts │ ├── index.ts │ ├── inject.ts │ ├── injectable.ts │ ├── module.ts │ ├── param.ts │ ├── produces.ts │ ├── provider.ts │ └── websocket.ts ├── error.ts ├── index.ts ├── injector │ ├── injector.ts │ └── metadata.ts ├── interfaces │ ├── iaction.ts │ ├── ibodyparser.ts │ ├── iconnection.ts │ ├── icontroller.ts │ ├── idecorators.ts │ ├── ifilter.ts │ ├── imodule.ts │ ├── index.ts │ ├── iparam.ts │ ├── iprovider.ts │ ├── iroute.ts │ └── iwebsocket.ts ├── logger │ ├── inspect.ts │ ├── level.ts │ ├── log.ts │ └── logger.ts ├── parsers │ ├── index.ts │ └── multipart.ts ├── router │ ├── index.ts │ ├── route-parser.ts │ ├── route-rule.ts │ └── router.ts ├── server │ ├── bootstrap.ts │ ├── controller-resolver.ts │ ├── http.ts │ ├── index.ts │ ├── mocks.ts │ ├── request-resolver.ts │ ├── request.ts │ ├── socket-resolver.ts │ ├── socket.ts │ └── status-code.ts └── tests │ ├── core.spec.ts │ ├── fake-request.ts │ ├── fake-socket.ts │ ├── injector.spec.ts │ ├── mocks │ └── multipart-samples.ts │ ├── module.bootstrap.spec.ts │ ├── multipart.spec.ts │ ├── request-controller.spec.ts │ ├── request-resolver.spec.ts │ ├── request.spec.ts │ ├── route-parser.spec.ts │ ├── route-rule.spec.ts │ └── router.spec.ts ├── target └── .history ├── tsconfig.json ├── tsd.json ├── tslint.json └── typedoc.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | max_line_length = 0 14 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | docs 4 | build 5 | typings 6 | npm-debug.log 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | typings 3 | demo-app 4 | .npmignore 5 | src 6 | docs 7 | build/tests 8 | typedoc.json 9 | 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "7.8.0" 5 | 6 | install: 7 | - npm install 8 | 9 | script: 10 | - npm run test 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.0.0-rc.0 2 | - Web sockets support 3 | 4 | ## 1.0.0 5 | - First stable release 6 | 7 | ## 1.0.0-rc.2 8 | - Make sure that Injector delivers correct Token at correct Injection point! 9 | 10 | ## 1.0.0-rc.1 11 | - DI Decorators - Avoid Typescript abstract class inheritance injection issue 12 | 13 | ## 1.0.0-rc.0 14 | - Release candidate 15 | 16 | ## 1.0.0-beta.23 17 | - Update dev packages and && fix controller resolver type issue 18 | 19 | ## 1.0.0-beta.22 20 | - Restructure logging types 21 | 22 | ## 1.0.0-beta.21 23 | - Added multipart parser 24 | 25 | ## 1.0.0-beta.20 26 | - Update route parser add more flexibility 27 | 28 | ## 1.0.0-beta.19 29 | - Added more flexibility to router, route parsing and pattern matching 30 | 31 | ## 1.0.0-beta.18 32 | - Fix Route parser and route resolver 33 | 34 | ## 1.0.0-beta.16 35 | - Fix Logger && Router provider resolving and verification 36 | 37 | ## 1.0.0-beta.15 38 | - Added better error resolution for modules which are not registered in system 39 | 40 | ## 1.0.0-beta.14 41 | - Fix module duplication resolution 42 | 43 | ## 1.0.0-beta.13 44 | - Update to typescript 2.3.x 45 | 46 | ## 1.0.0-beta.12 47 | - Fix Logger, Router references 48 | - Added module name duplication check 49 | 50 | ## 1.0.0-beta.11 51 | - Fix nested module imports 52 | 53 | ## 1.0.0-beta.10 54 | - Copy query params to params if thy are not defined from path param 55 | - Don't throw exception if @Param is not defined in route! 56 | - Injector -> Expose exports to importers 57 | 58 | ## 1.0.0-beta.9 59 | - Fix module imports -> Imported modules should export it's exports to module which imports them not only to it's imports 60 | - IModuleMetadata no longer requires controller as default type 61 | 62 | ## 1.0.0-beta.8 63 | - Fix router rule resolution issue only false or IResolvedRoute is allowed 64 | 65 | ## 1.0.0-beta.7 66 | - Export Headers interface from route 67 | 68 | ## 1.0.0-beta.6 69 | - Fix router addRule 70 | 71 | ## 1.0.0-beta.5 72 | - Update router addRule second parameter as optional 73 | 74 | ## 1.0.0-beta.4 75 | - Improve merging IProvider algorithm 76 | 77 | ## 1.0.0-beta.3 78 | - Fix merge algorithm on nested modules which have wrong execution order for providers 79 | 80 | ## 1.0.0-beta.2 81 | - Remove OnError decorator since is not used anymore 82 | 83 | ## 1.0.0-beta.1 84 | - Remove forwarder / forwarded 85 | - Since beta no major API changes will happen. 86 | 87 | ## 1.0.0-alpha-37 88 | - Added benchmark to logger 89 | 90 | ## 1.0.0-alpha-36 91 | - Added fakeControllerActionCall for better testing purposes 92 | 93 | ## 1.0.0-alpha-35 94 | - Rename Error to ErrorMessage decorator 95 | 96 | ## 1.0.0-alpha-34 97 | - Implemented global error route handler , every module can have own error handler route 98 | 99 | 100 | ## 1.0.0-alpha-33 101 | - StatusCode renamed to Status 102 | - fix status code bug while redirect 103 | 104 | ## 1.0.0-alpha-32 105 | - Exchanged status code api 106 | - Added redirect to request 107 | 108 | ## 1.0.0-alpha-31 109 | - Added fake http api for testing 110 | - Remove demo app from framework repo 111 | 112 | 113 | ## 1.0.0-alpha-30 114 | - Changed @Chain @BeforeEach @AfterEach to not be function calls 115 | - Updated tests for controller 116 | - Exchanged controllerResolver api 117 | 118 | ## 1.0.0-alpha-28 119 | - Fixed injections for constructor and action params 120 | - Added tests for Injector 121 | 122 | ## 1.0.0-alpha-25 123 | - Remove unnecessary dependencies in package.json 124 | 125 | ## 1.0.0-alpha-24 126 | - Fix injector processor 127 | - Fix status code and content type resolver 128 | 129 | ## 1.0.0-alpha-23 130 | - Refactored request processing 131 | - Implemented module system 132 | 133 | ## 1.0.0-alpha-22 134 | - Added module implementation sketches, missing implementation for modules 135 | 136 | ## 1.0.0-alpha-21 137 | 138 | - Implemented filters 139 | - Fixed request chain processing 140 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Igor Ivanovic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Typescript framework for Node.js 2 | 3 | [![NPM Version][npm-image]][npm-url] 4 | 5 | 6 | * Works with node gte 6.x.x 7 | * Typeix has dependency injection inspired by Angular 2 8 | 9 | 10 | * [View demo application][demo-app] 11 | * [Documentation][docs] 12 | 13 | ### Typeix is developed by [GSG group](http://www.global-savings-group.com) 14 | 15 | ## Features 16 | * Dependency Injection - partial JSR 330 standard 17 | * Modular design 18 | * MVC structure 19 | * Component driven -> singletons -> depends on injection level 20 | * Request Filters 21 | * Nice routing features -> supports static && dynamic routing && reverse router 22 | * Controller inheritance 23 | 24 | 25 | 26 | ## Typeix Munich node user group 2017.03.23 27 | 28 | [![Typeix MNUG 2017.03.23](https://img.youtube.com/vi/IWT6hVTFX8g/0.jpg)](https://youtu.be/IWT6hVTFX8g "Typeix MNUG 2017.03.23") 29 | 30 | [npm-image]: https://badge.fury.io/js/typeix.svg 31 | [npm-url]: https://badge.fury.io/js/typeix 32 | [demo-app]: https://github.com/igorzg/typeix-demo-app 33 | [docs]: https://igorivanovic.gitbooks.io/typeix 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | title: Typeix - Typescript Node.js Framework 2 | description: Node.js framework written in typescript which enables javascript developers to use highly-productive development tool made for scaling teams. 3 | theme: jekyll-theme-architect 4 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./build/index"; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typeix", 3 | "version": "2.0.0-rc.0", 4 | "dependencies": { 5 | "reflect-metadata": "^0.1.10", 6 | "ws": "^3.2.0" 7 | }, 8 | "description": "Typescript API for RESTful Services for (Node.js)", 9 | "main": "build/index.js", 10 | "scripts": { 11 | "test": "npm run compile && mocha build/tests/ --debug --full-trace", 12 | "docs": "typedoc --options typedoc.json --includeDeclarations node_modules/@types/ src/", 13 | "compile": "tsc -p tsconfig.json" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git://github.com/igorzg/typeix.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/igorzg/typeix/issues", 21 | "email": "igor.zg1987@gmail.com" 22 | }, 23 | "homepage": "https://github.com/igorzg/typeix", 24 | "engines": { 25 | "node": ">=6.0.0" 26 | }, 27 | "keywords": [ 28 | "typeix", 29 | "enterprise", 30 | "development", 31 | "javascript", 32 | "rest", 33 | "api", 34 | "restful", 35 | "services" 36 | ], 37 | "author": "igor.zg1987@gmail.com", 38 | "contributors": [ 39 | { 40 | "name": "Michael Rose", 41 | "email": "michael_rose@gmx.de" 42 | } 43 | ], 44 | "license": "MIT", 45 | "devDependencies": { 46 | "@types/chai": "^4.0.2", 47 | "@types/mocha": "^2.2.41", 48 | "@types/node": "^8.0.20", 49 | "@types/sinon": "^2.3.3", 50 | "@types/sinon-chai": "^2.7.28", 51 | "@types/ws": "^3.2.0", 52 | "chai": "^4.1.1", 53 | "mocha": "^3.5.0", 54 | "sinon": "^2.4.1", 55 | "sinon-chai": "^2.12.0", 56 | "tslint": "^5.6.0", 57 | "typedoc": "^0.8.0", 58 | "typescript": "^2.3.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/core.ts: -------------------------------------------------------------------------------- 1 | import {inspect} from "util"; 2 | /** 3 | * Create unique id 4 | * 5 | * @returns {string} 6 | */ 7 | export function uuid(): string { 8 | let d = new Date().getTime(); 9 | return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { 10 | let r = (d + Math.random() * 16) % 16 | 0; 11 | d = Math.floor(d / 16); 12 | return (c == "x" ? r : (r & 0x3 | 0x8)).toString(16); 13 | }); 14 | } 15 | 16 | /** 17 | * Check if token is in array 18 | * @param arr 19 | * @param token 20 | * @return {boolean} 21 | */ 22 | export function inArray(arr: Array, token: any): boolean { 23 | return isArray(arr) && arr.indexOf(token) > -1; 24 | } 25 | /** 26 | * @since 1.0.0 27 | * @author Igor Ivanovic 28 | * @function toString 29 | * 30 | * @description 31 | * Try to serialize object 32 | */ 33 | export function toString(value: any, toJsonString = true): string { 34 | if (!isString(value)) { 35 | try { 36 | if (toJsonString) { 37 | return JSON.stringify(value); 38 | } 39 | return inspect(value, {depth: 10}); 40 | } catch (e) { 41 | return inspect(value, {depth: 10}); 42 | } 43 | } 44 | return value; 45 | } 46 | /** 47 | * @since 1.0.0 48 | * @author Igor Ivanovic 49 | * @function isBoolean 50 | * 51 | * @description 52 | * Check if value is boolean 53 | */ 54 | export function isBoolean(value): boolean { 55 | return typeof value === "boolean"; 56 | } 57 | /** 58 | * @since 1.0.0 59 | * @author Igor Ivanovic 60 | * @function isUndefined 61 | * 62 | * @description 63 | * Check if value is un-defined 64 | */ 65 | export function isUndefined(value): boolean { 66 | return typeof value === "undefined"; 67 | } 68 | /** 69 | * @since 1.0.0 70 | * @author Igor Ivanovic 71 | * @function isString 72 | * 73 | * @description 74 | * Check if value is string 75 | */ 76 | export function isString(value): boolean { 77 | return typeof value === "string"; 78 | } 79 | /** 80 | * @since 1.0.0 81 | * @author Igor Ivanovic 82 | * @function isNumber 83 | * 84 | * @description 85 | * Check if value is isNumber 86 | */ 87 | export function isNumber(value): boolean { 88 | return typeof value === "number" && !isNaN(value); 89 | } 90 | 91 | /** 92 | * @since 1.0.0 93 | * @author Igor Ivanovic 94 | * @function isArray 95 | * 96 | * @description 97 | * Check if value is array 98 | */ 99 | export function isArray(value): boolean { 100 | return Array.isArray(value); 101 | } 102 | /** 103 | * @since 1.0.0 104 | * @author Igor Ivanovic 105 | * @function isNull 106 | * 107 | * @description 108 | * Check if value is funciton 109 | */ 110 | export function isNull(value): boolean { 111 | return value === null; 112 | } 113 | /** 114 | * @since 1.0.0 115 | * @author Igor Ivanovic 116 | * @function isFunction 117 | * 118 | * @description 119 | * Check if value is funciton 120 | */ 121 | export function isFunction(value): boolean { 122 | return typeof value === "function"; 123 | } 124 | /** 125 | * @since 1.0.0 126 | * @author Igor Ivanovic 127 | * @function isArray 128 | * 129 | * @description 130 | * Check if value is array 131 | */ 132 | export function isDate(value): boolean { 133 | return Object.prototype.toString.call(value) === "[object Date]"; 134 | } 135 | /** 136 | * @since 1.0.0 137 | * @author Igor Ivanovic 138 | * @function isRegExp 139 | * 140 | * @description 141 | * Check if object is an regular expression 142 | */ 143 | export function isRegExp(value): boolean { 144 | return Object.prototype.toString.call(value) === "[object RegExp]"; 145 | } 146 | /** 147 | * @since 1.0.0 148 | * @author Igor Ivanovic 149 | * @function isObject 150 | * 151 | * @description 152 | * Check if value is object 153 | */ 154 | export function isObject(value): boolean { 155 | return !isNull(value) && typeof value === "object"; 156 | } 157 | 158 | /** 159 | * @since 1.0.0 160 | * @author Igor Ivanovic 161 | * @function isPresent 162 | * 163 | * @description 164 | * Check if value is object 165 | */ 166 | export function isPresent(value): boolean { 167 | return !isNull(value) && !isUndefined(value); 168 | } 169 | 170 | /** 171 | * @since 1.0.0 172 | * @author Igor Ivanovic 173 | * @function isTruthy 174 | * 175 | * @description 176 | * we are doing data type conversion to see if value is considered true value 177 | */ 178 | export function isTruthy(value): boolean { 179 | return !isFalsy(value); 180 | } 181 | 182 | /** 183 | * @since 1.0.0 184 | * @author Igor Ivanovic 185 | * @function isFalsy 186 | * 187 | * @description 188 | * we are doing data type conversion to see if value is considered false value 189 | */ 190 | export function isFalsy(value): boolean { 191 | return isNull(value) || isUndefined(value) || value === "" || value === false || value === 0 || 192 | (typeof value === "number" && isNaN(value)); 193 | } 194 | 195 | /** 196 | * @since 1.0.0 197 | * @author Igor Ivanovic 198 | * @function isClass 199 | * 200 | * @description 201 | * Check if type is class 202 | */ 203 | export function isClass(value): boolean { 204 | return isFunction(value) && /^\s*class\s+/.test(value.toString()); 205 | } 206 | /** 207 | * @since 1.0.0 208 | * @author Igor Ivanovic 209 | * @function isEqual 210 | * 211 | * @description 212 | * Check if two objects are equal 213 | */ 214 | export function isEqual(a, b): boolean { 215 | if (isString(a)) { 216 | return a === b; 217 | } else if (_isNumber(a)) { 218 | if (isNaN(a) || isNaN(b)) { 219 | return isNaN(a) === isNaN(b); 220 | } 221 | return a === b; 222 | } else if (isBoolean(a)) { 223 | return a === b; 224 | } else if (isDate(a)) { 225 | return a.getTime() === b.getTime(); 226 | } else if (isRegExp(a)) { 227 | return a.source === b.source; 228 | } else if (isArray(a) && isArray(b)) { 229 | 230 | // check references first 231 | if (a === b) { 232 | return true; 233 | } else if (a.constructor.name !== b.constructor.name) { 234 | return false; 235 | } else if (a.length === 0 && b.length === 0) { 236 | return true; 237 | } 238 | 239 | try { 240 | if (a.length !== b.length) { 241 | return false; 242 | } 243 | return a.every((item, index) => isEqual(item, b[index])); 244 | } catch (e) { 245 | throw e; 246 | } 247 | } else if (isObject(a) && isObject(b)) { 248 | let equal: Array = []; 249 | let aLen = Object.keys(a).length; 250 | let bLen = Object.keys(b).length; 251 | // check references first 252 | 253 | if (a === b) { 254 | return true; 255 | } else if (a.constructor.name !== b.constructor.name) { 256 | return false; 257 | } else if (aLen === 0 && bLen === 0) { 258 | return true; 259 | } 260 | 261 | try { 262 | if (aLen === bLen) { 263 | Object.keys(a).forEach(key => equal.push(isEqual(a[key], b[key]))); 264 | } 265 | } catch (e) { 266 | throw e; 267 | } 268 | 269 | if (equal.length === 0) { 270 | return false; 271 | } 272 | return equal.every((item) => item === true); 273 | /// compare undefined and nulls 274 | } else if (a === b) { 275 | return true; 276 | } 277 | 278 | return false; 279 | } 280 | 281 | /** 282 | * Internal is number 283 | * @param value 284 | * @returns {boolean} 285 | * @private 286 | */ 287 | function _isNumber(value): boolean { 288 | return typeof value === "number"; 289 | } 290 | -------------------------------------------------------------------------------- /src/decorators/action.ts: -------------------------------------------------------------------------------- 1 | import {FUNCTION_KEYS, Metadata} from "../injector/metadata"; 2 | import {isEqual} from "../core"; 3 | import {IAction} from "../interfaces/iaction"; 4 | 5 | /** 6 | * Action decorator 7 | * @decorator 8 | * @function 9 | * @private 10 | * @name mapAction 11 | * 12 | * @param {String} type 13 | * 14 | * @description 15 | * Multiple action type providers 16 | */ 17 | let mapAction = (type) => (value: string): Function => { 18 | return (Class: Function, key: string, descriptor: PropertyDescriptor): any => { 19 | let metadata: Array = []; 20 | let className: string = Metadata.getName(Class); 21 | if (Metadata.hasMetadata(Class, FUNCTION_KEYS)) { 22 | metadata = Metadata.getMetadata(Class, FUNCTION_KEYS); 23 | } 24 | if (metadata.find(item => item.type === type && item.key === key && item.className === className)) { 25 | throw new TypeError(`Only one @${type} definition is allowed on ${key} ${Metadata.getName(Class, "on class ")}`); 26 | } else if (!Metadata.isDescriptor(descriptor) && !isEqual(Class, descriptor)) { 27 | throw new TypeError(`@${type} is allowed ony on function type ${Metadata.getName(Class, "on class ")}`); 28 | } 29 | let iAction: IAction = { 30 | type, 31 | key, 32 | value, 33 | proto: Class 34 | }; 35 | metadata.push(iAction); 36 | Metadata.defineMetadata(Class, FUNCTION_KEYS, metadata); 37 | if (Metadata.isDescriptor(descriptor)) { 38 | descriptor.configurable = false; 39 | descriptor.writable = false; 40 | } 41 | return Class; 42 | }; 43 | }; 44 | /** 45 | * Action decorator 46 | * @decorator 47 | * @function 48 | * @name mapEachAction 49 | * 50 | * @param {String} type 51 | * 52 | * @description 53 | * Map each action type 54 | */ 55 | let mapEachAction = (type) => 56 | (Class: any, key: string, descriptor: PropertyDescriptor): any => { 57 | let metadata: Array = []; 58 | let className: string = Metadata.getName(Class); 59 | if (Metadata.hasMetadata(Class, FUNCTION_KEYS)) { 60 | metadata = Metadata.getMetadata(Class, FUNCTION_KEYS); 61 | } 62 | if (metadata.find(item => item.type === type && item.className === className)) { 63 | throw new TypeError(`Only one @${type} definition is allowed ${Metadata.getName(Class, "on class ")}`); 64 | } else if (!Metadata.isDescriptor(descriptor) && !isEqual(Class, descriptor)) { 65 | throw new TypeError(`@${type} is allowed ony on function type ${Metadata.getName(Class, "on class ")}`); 66 | } 67 | let iAction: IAction = { 68 | type, 69 | key, 70 | value: null, 71 | proto: Class 72 | }; 73 | metadata.push(iAction); 74 | Metadata.defineMetadata(Class, FUNCTION_KEYS, metadata); 75 | if (Metadata.isDescriptor(descriptor)) { 76 | descriptor.configurable = false; 77 | descriptor.writable = false; 78 | } 79 | return Class; 80 | }; 81 | 82 | 83 | /** 84 | * Action decorator 85 | * @decorator 86 | * @function 87 | * @name BeforeEach 88 | * 89 | * @description 90 | * Before each action 91 | */ 92 | export let BeforeEach = mapEachAction("BeforeEach"); 93 | 94 | /** 95 | * Action decorator 96 | * @decorator 97 | * @function 98 | * @name AfterEach 99 | * 100 | * @description 101 | * After each action 102 | */ 103 | export let AfterEach = mapEachAction("AfterEach"); 104 | /** 105 | * Action decorator 106 | * @decorator 107 | * @function 108 | * @name Action 109 | * 110 | * @param {String} value 111 | * 112 | * @description 113 | * Define name of action to class 114 | */ 115 | export let Action = mapAction("Action"); 116 | /** 117 | * Before Action decorator 118 | * @decorator 119 | * @function 120 | * @name Before 121 | * 122 | * @param {String} value 123 | * 124 | * @description 125 | * Define name of before action to class 126 | */ 127 | export let Before = mapAction("Before"); 128 | /** 129 | * After Action decorator 130 | * @decorator 131 | * @function 132 | * @name After 133 | * 134 | * @param {String} value 135 | * 136 | * @description 137 | * Define name of after action to class 138 | */ 139 | export let After = mapAction("After"); 140 | 141 | /** 142 | * @since 2.0.0 143 | * @decorator 144 | * @function 145 | * @name Hook 146 | * 147 | * @param {String} value One of "verify"|"open"|"message" 148 | * 149 | * @description 150 | * Define a WebSocket method hook to be called for one of the actions "verify", 151 | * "open", or "message". All hooks support injection with {@link Inject} and {@link Param}. 152 | * 153 | * The "verify" hook is called before the socket is fully established. If you want to deny 154 | * opening the socket just throw an {@link HttpError}. 155 | * 156 | * When the socket is opened the "open" hook is called. You can inject the {@link Socket} 157 | * as parameter in this method in order to get access to the underlying Socket API. 158 | * 159 | * To receive and handle messages from the other side provide a "message" hook. You can inject 160 | * a parameter "message" there which is the data received over the socket. 161 | * 162 | * @example 163 | * import {WebSocket, Hook, BaseRequest} from "typeix"; 164 | * 165 | * \@WebSocket({...}) 166 | * export class MySocket { 167 | * \@Inject(BaseRequest) 168 | * private readonly request: BaseRequest; 169 | * 170 | * \@Hook("verify") 171 | * verify(): void { 172 | * if (!this.request.getRequestHeader("my-header") === "approved") { 173 | * throw new HttpError(403, "You are not approved"); 174 | * } 175 | * } 176 | * 177 | * \@Hook("open") 178 | * open(@Inject(Socket) socket: Socket) { 179 | * } 180 | * } 181 | */ 182 | export let Hook: (value: "verify" | "open" | "message") => any = mapAction("Hook"); 183 | -------------------------------------------------------------------------------- /src/decorators/chain.ts: -------------------------------------------------------------------------------- 1 | import {isNumber} from "../core"; 2 | import {Metadata, FUNCTION_PARAMS} from "../injector/metadata"; 3 | 4 | /** 5 | * @since 1.0.0 6 | * @decorator 7 | * @function 8 | * @name Chain 9 | * 10 | * @description 11 | * Chain propagate data from FilterBefore -> BeforeEach -> Before -> Action -> After -> AfterEach -> FilterAfter 12 | * 13 | * @example 14 | * import {Chain, Param, Controller, Action, Inject} from "typeix"; 15 | * 16 | * \@Controller({ 17 | * name: "myController" 18 | * }) 19 | * class MyController{ 20 | * 21 | * \@Before("index") 22 | * actionIndex() { 23 | * return "My Index"; 24 | * } 25 | * 26 | * \@Action("index") 27 | * actionIndex(@Chain data, @Param("file") file: string) { 28 | * return "My Index " + data; 29 | * } 30 | * } 31 | */ 32 | export let Chain = (Class: any, key?: any, paramIndex?: any): any => { 33 | let type = "Chain"; 34 | let metadata: Array = []; 35 | if (Metadata.hasMetadata(Class, FUNCTION_PARAMS)) { 36 | metadata = Metadata.getMetadata(Class, FUNCTION_PARAMS); 37 | } 38 | if (!isNumber(paramIndex)) { 39 | throw new TypeError(`@${type} is not allowed ${Metadata.getName(Class, "on class ")} on ${paramIndex} 40 | @${type} is allowed only as parameter type!`); 41 | } 42 | metadata.push({ 43 | type, 44 | key, 45 | value: null, 46 | paramIndex 47 | }); 48 | Metadata.defineMetadata(Class, FUNCTION_PARAMS, metadata); 49 | return Class; 50 | }; 51 | -------------------------------------------------------------------------------- /src/decorators/controller.ts: -------------------------------------------------------------------------------- 1 | import {isArray, isClass} from "../core"; 2 | import {Metadata} from "../injector/metadata"; 3 | import {IControllerMetadata} from "../interfaces/icontroller"; 4 | 5 | /** 6 | * Controller 7 | * @decorator 8 | * @function 9 | * @name Controller 10 | * 11 | * @param {IControllerMetadata} config 12 | * @returns {function(any): any} 13 | * 14 | * @description 15 | * Define controller of application 16 | */ 17 | export let Controller = (config: IControllerMetadata) => (Class) => { 18 | if (!isArray(config.providers)) { 19 | config.providers = []; 20 | } 21 | config.providers = config.providers.map(ProviderClass => Metadata.verifyProvider(ProviderClass)); 22 | if (!isClass(Class)) { 23 | throw new TypeError(`@Controller is allowed only on class`); 24 | } 25 | Metadata.setComponentConfig(Class, config); 26 | return Class; 27 | }; 28 | -------------------------------------------------------------------------------- /src/decorators/error.ts: -------------------------------------------------------------------------------- 1 | import {isNumber} from "../core"; 2 | import {Metadata, FUNCTION_PARAMS} from "../injector/metadata"; 3 | 4 | /** 5 | * @since 1.0.0 6 | * @decorator 7 | * @function 8 | * @name Error 9 | * 10 | * @description 11 | * Chain propagate data from FilterBefore -> BeforeEach -> Before -> Action -> After -> AfterEach -> FilterAfter 12 | * 13 | * @example 14 | * import {Chain, Param, Controller, Action, Inject} from "typeix"; 15 | * 16 | * \@Controller({ 17 | * name: "core" 18 | * }) 19 | * class MyController{ 20 | * 21 | * \@Action("error") 22 | * actionIndex(@ErrorMessage data) { 23 | * return "My Index " + data; 24 | * } 25 | * } 26 | */ 27 | export let ErrorMessage = (Class: any, key?: any, paramIndex?: any): any => { 28 | let type = "ErrorMessage"; 29 | let metadata: Array = []; 30 | if (Metadata.hasMetadata(Class, FUNCTION_PARAMS)) { 31 | metadata = Metadata.getMetadata(Class, FUNCTION_PARAMS); 32 | } 33 | if (!isNumber(paramIndex)) { 34 | throw new TypeError(`@${type} is not allowed ${Metadata.getName(Class, "on class ")} on ${paramIndex} 35 | @${type} is allowed only as parameter type!`); 36 | } 37 | metadata.push({ 38 | type, 39 | key, 40 | value: null, 41 | paramIndex 42 | }); 43 | Metadata.defineMetadata(Class, FUNCTION_PARAMS, metadata); 44 | return Class; 45 | }; 46 | -------------------------------------------------------------------------------- /src/decorators/filter.ts: -------------------------------------------------------------------------------- 1 | import {TFilter} from "../interfaces/ifilter"; 2 | import {isClass} from "../core"; 3 | import {Metadata} from "../injector/metadata"; 4 | /** 5 | * @since 1.0.0 6 | * @decorator 7 | * @function 8 | * @name Filter 9 | * 10 | * @description 11 | * Filter is used as pre controller and after controller actions 12 | * 13 | * @example 14 | * import {IFilter, Filter, Request, Inject} from "typeix"; 15 | * 16 | * \@Filter(100) 17 | * export class Cache implements IFilter { 18 | * 19 | * \@Inject(Request) 20 | * request: Request; 21 | * 22 | * 23 | * before(): string|Buffer|Promise { 24 | * return "Before controller"; 25 | * } 26 | * 27 | * after(data: string): string|Buffer|Promise { 28 | * return "After controller <- " + data; 29 | * } 30 | * 31 | *} 32 | */ 33 | export let Filter = (priority: number, route: string = "*"): Function => { 34 | return (Class: TFilter): any => { 35 | if (!isClass(Class)) { 36 | throw new TypeError(`Filter is only allowed on class type of IFilter! Error found on ${Class.toString()}`); 37 | } 38 | Metadata.setComponentConfig(Class, { 39 | priority, 40 | route 41 | }); 42 | return Class; 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /src/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export {Action, Before, After, BeforeEach, AfterEach, Hook} from "./action"; 2 | export {Controller} from "./controller"; 3 | export {WebSocket} from "./websocket"; 4 | export {Inject} from "./inject"; 5 | export {Filter} from "./filter"; 6 | export {Injectable} from "./injectable"; 7 | export {Module} from "./module"; 8 | export {Produces} from "./produces"; 9 | export {Provider} from "./provider"; 10 | export {Param} from "./param"; 11 | export {Chain} from "./chain"; 12 | export {ErrorMessage} from "./error"; 13 | -------------------------------------------------------------------------------- /src/decorators/inject.ts: -------------------------------------------------------------------------------- 1 | import {isUndefined} from "../core"; 2 | import {FUNCTION_PARAMS, INJECT_KEYS, Metadata} from "../injector/metadata"; 3 | import {IInjectKey} from "../interfaces/idecorators"; 4 | import {IParam} from "../interfaces/iparam"; 5 | 6 | /** 7 | * @since 1.0.0 8 | * @decorator 9 | * @function 10 | * @name Inject 11 | * 12 | * @description 13 | * Inject is used to define metadata which will be injected at class construct time by Injector 14 | * 15 | * @example 16 | * import {Provider, Inject} from "typeix"; 17 | * import {MyService} form "./services/my-service"; 18 | * 19 | * \@Provider([MyService]) 20 | * class AssetLoader{ 21 | * \@Inject(MyService) 22 | * private myService; 23 | * } 24 | */ 25 | export let Inject = (value: Function | string, isMutable?: boolean) => { 26 | return (Class: any, key?: string, paramIndex?: any): any => { 27 | let type = "Inject"; 28 | let metadata: Array = []; 29 | let metadataKey = isUndefined(paramIndex) ? INJECT_KEYS : FUNCTION_PARAMS; 30 | if (Metadata.hasMetadata(Class, metadataKey)) { 31 | metadata = Metadata.getMetadata(Class, metadataKey); 32 | } 33 | 34 | if (Metadata.isDescriptor(paramIndex)) { 35 | throw new TypeError(`@${type} is not allowed ${Metadata.getName(Class, "on class ")} on ${paramIndex.value} 36 | @Inject is allowed only as param type!`); 37 | } else if (isUndefined(value)) { 38 | throw new TypeError(`@Inject is not allowed with undefined value ${Metadata.getName(Class, "on ")}.${key} 39 | - make sure there are no circular dependencies`); 40 | } 41 | 42 | metadata.push(isUndefined(paramIndex) ? { 43 | Class: Class.constructor, 44 | value, 45 | isMutable: !!isMutable, 46 | key 47 | } : { 48 | Class: Class.constructor, 49 | type, 50 | key, 51 | value: value, 52 | paramIndex 53 | }); 54 | 55 | Metadata.defineMetadata(Class, metadataKey, metadata); 56 | return Class; 57 | }; 58 | }; 59 | -------------------------------------------------------------------------------- /src/decorators/injectable.ts: -------------------------------------------------------------------------------- 1 | import {Provider} from "./provider"; 2 | /** 3 | * @since 1.0.0 4 | * @decorator 5 | * @function 6 | * @name Injectable 7 | * 8 | * @description 9 | * Injectable decorator is used to define injectable class in order that Injector can recognise it 10 | * 11 | * @example 12 | * import {Injectable} from "typeix"; 13 | * 14 | * \@Injectable() 15 | * class AssetLoader{ 16 | * constructor() { 17 | * 18 | * } 19 | * } 20 | */ 21 | export let Injectable = () => Provider([]); 22 | -------------------------------------------------------------------------------- /src/decorators/module.ts: -------------------------------------------------------------------------------- 1 | import {IModuleMetadata} from "../interfaces/imodule"; 2 | import {isArray, isClass} from "../core"; 3 | import {Metadata} from "../injector/metadata"; 4 | import {Router} from "../router/router"; 5 | import {Logger} from "../logger/logger"; 6 | 7 | 8 | /** 9 | * Module decorator 10 | * @decorator 11 | * @function 12 | * @name Module 13 | * 14 | * @param {IModuleMetadata} config 15 | * @returns {function(any): any} 16 | * 17 | * @description 18 | * Define module in your application 19 | * 20 | * @example 21 | * import {Module, Router} from "typeix"; 22 | * 23 | * \@Module({ 24 | * providers:[Logger, Router] 25 | * }) 26 | * class Application{ 27 | * constructor(router: Router) { 28 | * 29 | * } 30 | * } 31 | */ 32 | export let Module = (config: IModuleMetadata) => (Class) => { 33 | if (!isClass(Class)) { 34 | throw new TypeError(`@Module is allowed only on class`); 35 | } 36 | if (!isArray(config.providers)) { 37 | config.providers = []; 38 | } 39 | if (!isArray(config.exports)) { 40 | config.exports = []; 41 | } 42 | if (Metadata.inProviders(config.providers, Logger) && !Metadata.inProviders(config.exports, Logger)) { 43 | config.exports.unshift(Logger); 44 | } 45 | if (Metadata.inProviders(config.providers, Router) && !Metadata.inProviders(config.exports, Router)) { 46 | config.exports.unshift(Router); 47 | } 48 | config.providers = config.providers.map(ProviderClass => Metadata.verifyProvider(ProviderClass)); 49 | Metadata.setComponentConfig(Class, config); 50 | return Class; 51 | }; 52 | -------------------------------------------------------------------------------- /src/decorators/param.ts: -------------------------------------------------------------------------------- 1 | import {isNumber} from "../core"; 2 | import {Metadata, FUNCTION_PARAMS} from "../injector/metadata"; 3 | import {IParam} from "../interfaces/iparam"; 4 | 5 | /** 6 | * @since 1.0.0 7 | * @decorator 8 | * @function 9 | * @name Param 10 | * 11 | * @description 12 | * Define Param metadata to deliver it from router 13 | * 14 | * @example 15 | * import {Param, Controller, Action, Inject} from "typeix"; 16 | * 17 | * \@Controller({ 18 | * name: "myController" 19 | * }) 20 | * class MyController{ 21 | * 22 | * \@Inject(AssetLoader) 23 | * myAssetLoaderService: AssetLoader; 24 | * 25 | * \@Action("index") 26 | * assetLoader(@Param("file") file: string) { 27 | * return this.myAssetLoaderService.load(file); 28 | * } 29 | * } 30 | */ 31 | export let Param = (value: string) => { 32 | return (Class: any, key?: any, paramIndex?: any): any => { 33 | let type = "Param"; 34 | let metadata: Array = []; 35 | if (Metadata.hasMetadata(Class, FUNCTION_PARAMS)) { 36 | metadata = Metadata.getMetadata(Class, FUNCTION_PARAMS); 37 | } 38 | if (!isNumber(paramIndex)) { 39 | throw new TypeError(`@Param is not allowed ${Metadata.getName(Class, "on class ")} on ${paramIndex} 40 | @Param is allowed only as parameter type!`); 41 | } 42 | let param: IParam = { 43 | Class: Class.constructor, 44 | type, 45 | key, 46 | value, 47 | paramIndex 48 | }; 49 | metadata.push(param); 50 | Metadata.defineMetadata(Class, FUNCTION_PARAMS, metadata); 51 | return Class; 52 | }; 53 | }; 54 | -------------------------------------------------------------------------------- /src/decorators/produces.ts: -------------------------------------------------------------------------------- 1 | import {Metadata, FUNCTION_KEYS} from "../injector/metadata"; 2 | import {isEqual} from "../core"; 3 | import {IAction} from "../interfaces/iaction"; 4 | /** 5 | * Produces response type 6 | * @decorator 7 | * @function 8 | * @name Produces 9 | * 10 | * @param {String} value 11 | * 12 | * @description 13 | * Produces content type 14 | */ 15 | export let Produces = (value: string) => (Class: any, key: string, descriptor: PropertyDescriptor) => { 16 | let type = "Produces"; 17 | let metadata: Array = []; 18 | if (Metadata.hasMetadata(Class, FUNCTION_KEYS)) { 19 | metadata = Metadata.getMetadata(Class, FUNCTION_KEYS); 20 | } 21 | if (metadata.find(item => item.type === type && item.key === key)) { 22 | throw new TypeError(`Only one @${type} definition is allowed on ${key} ${Metadata.getName(Class, "on class ")}`); 23 | } else if (!Metadata.isDescriptor(descriptor) && !isEqual(Class, descriptor)) { 24 | throw new TypeError(`@${type} is allowed ony on function type ${Metadata.getName(Class, "on class ")}`); 25 | } 26 | let iAction: IAction = { 27 | type, 28 | key, 29 | value, 30 | proto: Class 31 | }; 32 | metadata.push(iAction); 33 | Metadata.defineMetadata(Class, FUNCTION_KEYS, metadata); 34 | if (Metadata.isDescriptor(descriptor)) { 35 | descriptor.configurable = false; 36 | descriptor.writable = false; 37 | } 38 | return Class; 39 | }; 40 | 41 | -------------------------------------------------------------------------------- /src/decorators/provider.ts: -------------------------------------------------------------------------------- 1 | import {IProvider} from "../interfaces/iprovider"; 2 | import {isArray, isClass, isPresent} from "../core"; 3 | import {Metadata} from "../injector/metadata"; 4 | 5 | /** 6 | * @since 1.0.0 7 | * @decorator 8 | * @function 9 | * @name Provider 10 | * 11 | * @description 12 | * Provider decorator is used to define injectable and injections for class itself 13 | * 14 | * @example 15 | * import {Provider} from "typeix"; 16 | * import {MyService} form "./services/my-service"; 17 | * 18 | * \@Provider([MyService]) 19 | * class AssetLoader{ 20 | * constructor(myService: MyService) { 21 | * 22 | * } 23 | * } 24 | */ 25 | export let Provider = (config: Array) => { 26 | return (Class) => { 27 | if (!isClass(Class)) { 28 | throw new TypeError(`Provider is only allowed on class definition! Error found on ${Class.toString()}`); 29 | } else if (isPresent(config) && !isArray(config)) { 30 | throw new TypeError(`Provider value must be array of IProvider`); 31 | } 32 | Metadata.setComponentConfig(Class, { 33 | providers: isPresent(config) ? config.map(ProviderClass => Metadata.verifyProvider(ProviderClass)) : [] 34 | }); 35 | return Class; 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /src/decorators/websocket.ts: -------------------------------------------------------------------------------- 1 | import {isArray, isClass} from "../core"; 2 | import {Metadata} from "../injector/metadata"; 3 | import {IWebSocketMetadata} from "../interfaces"; 4 | 5 | /** 6 | * @since 2.0.0 7 | * @decorator 8 | * @function 9 | * @name WebSocket 10 | * 11 | * @param {IWebSocketMetadata} config WebSocket configuration 12 | * @returns {function(any): any} 13 | * 14 | * @description 15 | * Defines a WebSocket endpoint of an application. 16 | * 17 | * The {@link Inject} decorator can be used for class members in the same way 18 | * as for controllers. 19 | * 20 | * If you want to access the underlying request that opened the socket you can 21 | * inject {@link BaseRequest} which provides a minimal interface for basic access. 22 | * 23 | * @example 24 | * import {WebSocket} from "typeix"; 25 | * 26 | * \@WebSocket({ 27 | * name: "mySocket" 28 | * }) 29 | * class MySocketEndpoint { 30 | * \@Inject(BaseRequest) 31 | * private readonly request: BaseRequest; 32 | * 33 | * ... 34 | * } 35 | */ 36 | export let WebSocket = (config: IWebSocketMetadata) => (Class) => { 37 | if (!isArray(config.providers)) { 38 | config.providers = []; 39 | } 40 | config.providers = config.providers.map(ProviderClass => Metadata.verifyProvider(ProviderClass)); 41 | if (!isClass(Class)) { 42 | throw new TypeError(`@WebSocket is allowed only on class`); 43 | } 44 | Metadata.setComponentConfig(Class, config); 45 | return Class; 46 | }; 47 | -------------------------------------------------------------------------------- /src/error.ts: -------------------------------------------------------------------------------- 1 | import {inspect} from "./logger/inspect"; 2 | import {Status} from "./server/status-code"; 3 | import {isTruthy} from "./core"; 4 | /** 5 | * @since 1.0.0 6 | * @class 7 | * @name HttpException 8 | * @param {Number} code status code 9 | * @param {String} message 10 | * @param {Object} data 11 | * @constructor 12 | * @description 13 | * HttpException use it in endpoint actions 14 | */ 15 | export class HttpError extends Error { 16 | 17 | static merge(error: Error) { 18 | if (!(error instanceof HttpError)) { 19 | let _error: Error = error; 20 | error = new HttpError(Status.Internal_Server_Error, _error.message, {}); 21 | error.stack = _error.stack; 22 | } 23 | return error; 24 | } 25 | 26 | constructor(private code: Status | number, message?: string, data?: Object) { 27 | super(message); 28 | if (isTruthy(data)) { 29 | this.stack += "\n\nDATA: " + inspect(data, 5); 30 | } 31 | this.stack += "\n\nCODE: " + inspect(code, 5); 32 | } 33 | 34 | 35 | getMessage() { 36 | return this.message; 37 | } 38 | 39 | getCode(): Status { 40 | return this.code; 41 | } 42 | 43 | toString() { 44 | return this.stack; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Export decorators 3 | */ 4 | export { 5 | Chain, 6 | Action, 7 | Before, 8 | After, 9 | BeforeEach, 10 | AfterEach, 11 | Hook, 12 | Filter, 13 | Controller, 14 | WebSocket, 15 | Inject, 16 | Injectable, 17 | Module, 18 | Param, 19 | Produces, 20 | Provider, 21 | ErrorMessage 22 | } from "./decorators/index"; 23 | /** 24 | * Export parsers 25 | */ 26 | export { 27 | MultiPartField, 28 | MultiPartFile, 29 | MultiPart 30 | } from "./parsers/index"; 31 | /** 32 | * Export logger 33 | */ 34 | export { 35 | Logger, LogLevels 36 | } from "./logger/logger"; 37 | /** 38 | * Export interfaces 39 | */ 40 | export { 41 | IControllerMetadata, 42 | IWebSocketMetadata, 43 | IModuleMetadata, 44 | IProvider, 45 | IAfterConstruct, 46 | IAfterClose, 47 | IFilter, 48 | Route, 49 | RouteRuleConfig, 50 | IResolvedRoute, 51 | Headers 52 | } from "./interfaces/index"; 53 | 54 | /** 55 | * Export router 56 | */ 57 | export { 58 | Router, 59 | Methods, 60 | getMethod, 61 | RouteParser, 62 | RouteRule 63 | } from "./router/index"; 64 | /** 65 | * Export Injector 66 | */ 67 | export { 68 | Injector 69 | } from "./injector/injector"; 70 | /** 71 | * Export http server 72 | */ 73 | export { 74 | httpServer, 75 | HttpOptions, 76 | getModule, 77 | Request, 78 | Status, 79 | Socket, 80 | fakeHttpServer, 81 | fakeControllerActionCall, 82 | FakeServerApi, 83 | FakeWebSocketApi, 84 | FakeResponseApi, 85 | IFakeServerConfig 86 | } from "./server/index"; 87 | /** 88 | * Export all from core 89 | */ 90 | export * from "./core"; 91 | export {HttpError} from "./error"; 92 | -------------------------------------------------------------------------------- /src/injector/injector.ts: -------------------------------------------------------------------------------- 1 | import {isArray, isClass, isFunction, isPresent, isUndefined, toString, uuid} from "../core"; 2 | import {Metadata} from "./metadata"; 3 | import {IProvider} from "../interfaces/iprovider"; 4 | import {IInjectKey} from "../interfaces/idecorators"; 5 | 6 | /** 7 | * @since 1.0.0 8 | * @function 9 | * @name ProviderList 10 | * 11 | * @description 12 | * Provider list holder for easier debugging 13 | */ 14 | class ProviderList { 15 | private _list = {}; 16 | 17 | /** 18 | * @since 1.0.0 19 | * @static 20 | * @function 21 | * @name ProviderList#constructor 22 | * @param {string} _id 23 | * @param {Array} keys 24 | * 25 | * @description 26 | * Create a new instance with new id 27 | */ 28 | constructor(private _id: string, private keys: Array = []) { 29 | } 30 | 31 | /** 32 | * @since 1.0.0 33 | * @static 34 | * @function 35 | * @name ProviderList#isMutable 36 | * @param {Object} key 37 | * 38 | * @description 39 | * Check if key is mutable 40 | */ 41 | isMutable(key: any): boolean { 42 | return this.keys.indexOf(key) > -1; 43 | } 44 | 45 | /** 46 | * @since 1.0.0 47 | * @static 48 | * @function 49 | * @name ProviderList#set 50 | * @param {Object} key 51 | * @param {Object} value 52 | * 53 | * @description 54 | * Simulate set as on Map object 55 | */ 56 | set (key: any, value: Object): void { 57 | if (!this.has(key) || this.isMutable(key)) { 58 | Object.defineProperty(this._list, key, { 59 | configurable: false, 60 | value: value, 61 | writable: this.isMutable(key) 62 | }); 63 | } else { 64 | throw new TypeError(`${isClass(key) ? Metadata.getName(key) : key} is already defined in injector, value: ${value}`); 65 | } 66 | } 67 | 68 | /** 69 | * @since 1.0.0 70 | * @static 71 | * @function 72 | * @name ProviderList#get 73 | * @param {Object} key 74 | * 75 | * @description 76 | * Simulate get as on Map object 77 | */ 78 | get (key: any): any { 79 | return this._list[key]; 80 | } 81 | 82 | /** 83 | * @since 1.0.0 84 | * @static 85 | * @function 86 | * @name ProviderList#clear 87 | * 88 | * @description 89 | * Simulate clear as on Map object 90 | */ 91 | clear() { 92 | this._list = {}; 93 | } 94 | 95 | /** 96 | * @since 1.0.0 97 | * @static 98 | * @function 99 | * @name ProviderList#has 100 | * @param {Object} key 101 | * 102 | * @description 103 | * Simulate has as on Map object 104 | */ 105 | has(key: any): boolean { 106 | return this._list.hasOwnProperty(key); 107 | } 108 | } 109 | 110 | /** 111 | * @since 1.0.0 112 | * @function 113 | * @name Injector 114 | * 115 | * @param {Injector} parent injector 116 | * 117 | * @description 118 | * Dependency injection for class injection 119 | * 120 | */ 121 | export class Injector { 122 | // injector identifier 123 | private _uid: string = uuid(); 124 | private _name: string; 125 | private _providers: ProviderList; 126 | private _children: Array = []; 127 | 128 | /** 129 | * @since 1.0.0 130 | * @static 131 | * @function 132 | * @name Injector#createAndResolveChild 133 | * @param {Injector} parent 134 | * @param {Function} Class 135 | * @param {Array} providers 136 | * @return {Injector} instance 137 | * 138 | * @description 139 | * Static method which creates child injector on current injector and creates instance of Injectable class 140 | * 141 | * @example 142 | * \@Injectable() 143 | * class MyInjectableClass{ 144 | * \@Inject("config") 145 | * private config: Object; 146 | * } 147 | * 148 | * let parent = new Injector(); 149 | * let injector = Injector.createAndResolveChild( 150 | * parent, 151 | * MyInjectableClass, 152 | * [ 153 | * {provide: "config", useValue: {id: 1, message: "This is custom provider for injector"}} 154 | * ] 155 | * ); 156 | * let myInstance = injector.get(MyInjectableClass); 157 | */ 158 | static createAndResolveChild(parent: Injector, Class: IProvider | Function, providers: Array): Injector { 159 | let child = new Injector(parent); 160 | let provider = Metadata.verifyProvider(Class); 161 | child.setName(provider); 162 | child.createAndResolve(provider, Metadata.verifyProviders(providers)); 163 | parent.setChild(child); 164 | return child; 165 | } 166 | 167 | /** 168 | * @since 1.0.0 169 | * @static 170 | * @function 171 | * @name Injector#createAndResolve 172 | * @param {Function} Class 173 | * @param {Array} providers 174 | * @return {Injector} instance 175 | * 176 | * @description 177 | * Static method which creates injector and instance of Injectable class 178 | * 179 | * @example 180 | * \@Injectable() 181 | * class MyInjectableClass{ 182 | * \@Inject("config") 183 | * private config: Object; 184 | * } 185 | * 186 | * let injector = Injector.createAndResolve( 187 | * MyInjectableClass, 188 | * [ 189 | * {provide: "config", useValue: {id: 1, message: "This is custom provider for injector"}} 190 | * ] 191 | * ); 192 | * let myInstance = injector.get(MyInjectableClass); 193 | */ 194 | static createAndResolve(Class: IProvider | Function, providers: Array): Injector { 195 | let injector = new Injector(); 196 | let provider = Metadata.verifyProvider(Class); 197 | injector.setName(provider); 198 | injector.createAndResolve(provider, Metadata.verifyProviders(providers)); 199 | return injector; 200 | } 201 | 202 | /** 203 | * @since 1.0.0 204 | * @constructor 205 | * @function 206 | * @name Injector#constructor 207 | * @param {Injector} parent 208 | * @param {Array} keys which are mutable 209 | * 210 | * @description 211 | * Injector constructor 212 | */ 213 | constructor(private parent?: Injector, keys: Array = []) { 214 | if (isArray(keys) && keys.indexOf(Injector) === -1) { 215 | keys.push(Injector); 216 | } 217 | this._providers = new ProviderList(this._uid, keys); 218 | } 219 | 220 | /** 221 | * @since 1.0.0 222 | * @function 223 | * @name Injector#createAndResolve 224 | * @param {IProvider} provider 225 | * @param {Array} providers 226 | * 227 | * @description 228 | * Creates instance of verified provider and creates instances of current providers and assign it to current injector instance 229 | * This method is used internally in most cases you should use static method Injector.createAndResolve or Injector.createAndResolveChild 230 | */ 231 | createAndResolve(provider: IProvider, providers: Array): any { 232 | // first initializer will assign name to injector for better debugging purposes 233 | this.setName(provider); 234 | // merge _providers 235 | // we need to keep merge algorithm in this order because we want to copy correct order do not change this :) 236 | providers = Metadata.mergeProviders(Metadata.getConstructorProviders(provider.provide), providers); 237 | // create _providers first 238 | providers.forEach(item => this.createAndResolve(item, Metadata.getConstructorProviders(item.provide))); 239 | 240 | // set correct injector 241 | if (!this.has(Injector)) { 242 | this.set(Injector, this); // set local injector 243 | } 244 | /** 245 | * If provider is already resolved return resolved one 246 | */ 247 | if (this.has(provider.provide)) { 248 | return this.get(provider.provide); 249 | } else if (isPresent(provider.useValue)) { // if provider.useValue is present return value 250 | this.set(provider.provide, provider.useValue); 251 | return this.get(provider.provide); 252 | // if it's factory invoke it and set invoked factory as value 253 | } else if (isPresent(provider.useFactory)) { 254 | this.set( 255 | provider.provide, 256 | provider.useFactory.apply( 257 | null, 258 | provider.deps.map(item => this.get(Metadata.verifyProvider(item).provide)) 259 | ) 260 | ); 261 | return this.get(provider.provide); 262 | } 263 | // get constructor args 264 | let keys = Metadata.getConstructorInjectKeys(provider.provide); 265 | // get providers for constructor 266 | let args = keys.map(arg => this.get(arg, provider)); 267 | // create instance 268 | let instance = Reflect.construct(provider.useClass, args); 269 | // get @Inject data 270 | let protoKeys = Metadata.getConstructorPrototypeKeys(provider.useClass); 271 | 272 | // let keyString = protoKeys.map(item => item.key).join(","); 273 | // assign injected values 274 | if (isArray(protoKeys)) { 275 | protoKeys.forEach((item: IInjectKey) => { 276 | // make sure that key exists in instance 277 | if (instance instanceof item.Class) { 278 | let value = this.get(item.value, provider); 279 | Reflect.defineProperty(instance, item.key, { 280 | value: value, 281 | writable: item.isMutable 282 | }); 283 | } 284 | }); 285 | } 286 | // set provider and value 287 | this.set(provider.provide, instance); 288 | // invoke after construct 289 | if (provider.useClass.prototype.hasOwnProperty("afterConstruct") && isFunction(instance.afterConstruct)) { 290 | instance.afterConstruct(); 291 | } 292 | 293 | return instance; 294 | } 295 | 296 | 297 | /** 298 | * @since 1.0.0 299 | * @function 300 | * @name Injector#destroy 301 | * 302 | * @description 303 | * Do cleanup on current injector and all children so we are ready for gc this is used internally by framework 304 | */ 305 | destroy() { 306 | if (this.parent instanceof Injector) { 307 | this.parent.removeChild(this); 308 | } 309 | this._children.forEach(injector => injector.destroy()); 310 | this._children = []; 311 | this.parent = undefined; 312 | this._providers.clear(); 313 | } 314 | 315 | 316 | /** 317 | * @since 1.0.0 318 | * @function 319 | * @name Injector#has 320 | * @param {any} key 321 | * 322 | * @description 323 | * Check if Injectable class has instance on current injector 324 | */ 325 | has(key: any): boolean { 326 | return this._providers.has(key); 327 | } 328 | 329 | /** 330 | * @since 1.0.0 331 | * @function 332 | * @name Injector#get 333 | * @param {any} provider 334 | * @param {IProvider} Class 335 | * 336 | * @description 337 | * Gets current Injectable instance throws exception if Injectable class is not created 338 | */ 339 | get (provider: any, Class?: IProvider): any { 340 | if (this.has(provider)) { 341 | return this._providers.get(provider); 342 | } else if (this.parent instanceof Injector) { 343 | return this.parent.get(provider, Class); 344 | } 345 | if (isPresent(Class)) { 346 | throw new Error(`No provider for ${ 347 | isFunction(provider) ? provider.name : toString(provider) 348 | } on class ${isFunction(Class.provide) ? Class.provide.name : Class.provide} , injector: ${this._uid} on provider ${this._name}`); 349 | } 350 | throw new Error(`No provider for ${isFunction(provider) ? provider.name : toString(provider)}, injector: ${this._uid} on provider ${this._name}`); 351 | } 352 | 353 | /** 354 | * @since 1.0.0 355 | * @function 356 | * @name Injector#set 357 | * @param {any} key 358 | * @param {Object} value 359 | * 360 | * @description 361 | * Sets Injectable instance to current injector instance 362 | */ 363 | set (key: any, value: Object): void { 364 | this._providers.set(key, value); 365 | } 366 | 367 | /** 368 | * @since 1.0.0 369 | * @function 370 | * @name Injector#getId 371 | * @private 372 | * 373 | * @description 374 | * Get injector id 375 | */ 376 | getId(): string { 377 | return this._uid; 378 | } 379 | 380 | /** 381 | * @since 1.0.0 382 | * @function 383 | * @name Injector#setName 384 | * @private 385 | * 386 | * @description 387 | * Set injector name 388 | */ 389 | setName(provider: IProvider): void { 390 | if (isUndefined(this._name)) { 391 | if (isFunction(provider.provide)) { 392 | this._name = provider.provide.name; 393 | } else { 394 | this._name = toString(provider.provide); 395 | } 396 | } 397 | } 398 | 399 | /** 400 | * @since 1.0.0 401 | * @function 402 | * @name Injector#setChild 403 | * @param {Injector} injector 404 | * @private 405 | * 406 | * @description 407 | * Append child Injector 408 | */ 409 | private setChild(injector: Injector): void { 410 | this._children.push(injector); 411 | } 412 | 413 | /** 414 | * @since 1.0.0 415 | * @function 416 | * @name Injector#setChild 417 | * @param {Injector} injector 418 | * @private 419 | * 420 | * @description 421 | * Remove child injector 422 | */ 423 | private removeChild(injector: Injector): void { 424 | this._children.splice(this._children.indexOf(injector), 1); 425 | } 426 | } 427 | 428 | -------------------------------------------------------------------------------- /src/injector/metadata.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import {isArray, isClass, isEqual, isFalsy, isFunction, isObject, isPresent, isString, toString} from "../core"; 3 | import {IProvider} from "../interfaces/iprovider"; 4 | 5 | export const INJECT_KEYS = "inject:paramtypes"; 6 | export const FUNCTION_KEYS = "function:decorators"; 7 | export const FUNCTION_PARAMS = "function:paramtypes"; 8 | export const DESIGN_PARAMTYPES_KEYS = "design:paramtypes"; 9 | export const DESIGN_KEYS = "design:type"; 10 | export const DESIGN_RETURN = "design:returntype"; 11 | export const COMPONENT_CONFIG_KEYS = "component:paramtypes"; 12 | 13 | /** 14 | * @since 1.0.0 15 | * @constructor 16 | * @function 17 | * @name Metadata 18 | * 19 | * @description 20 | * Metadata is responsible for getting or setting metadata definitions for some Class 21 | * It's crucial for injector 22 | */ 23 | export class Metadata { 24 | /** 25 | * @since 1.0.0 26 | * @static 27 | * @function 28 | * @name Metadata#getName 29 | * @param {Object} Class 30 | * @param {string} prefix 31 | * 32 | * @description 33 | * Get class name 34 | */ 35 | static getName(Class: any, prefix?: string): string { 36 | if (Metadata.isProvider(Class)) { 37 | return Metadata.getName(Class.provide); 38 | } else if (isString(Class)) { 39 | return "String: " + Class; 40 | } 41 | let message = ""; 42 | if (prefix) { 43 | message += prefix; 44 | } 45 | if (isPresent(Class)) { 46 | if (isPresent(Class.name)) { 47 | message += Class.name; 48 | } else if (isPresent(Class.constructor) && isPresent(Class.constructor.name)) { 49 | message += Class.constructor.name; 50 | } 51 | } 52 | return message; 53 | }; 54 | 55 | /** 56 | * @since 1.0.0 57 | * @static 58 | * @function 59 | * @name Metadata#inVerifiedProvider 60 | * 61 | * @param providers 62 | * @param provider 63 | * @returns {boolean} 64 | * 65 | * @description 66 | * Check if provider is in array of providers 67 | */ 68 | static inVerifiedProviders(providers: Array, provider: IProvider): boolean { 69 | return providers.some(item => isEqual(item.provide, provider.provide)); 70 | } 71 | 72 | /** 73 | * @since 1.0.0 74 | * @static 75 | * @function 76 | * @name Metadata#getVerifiedProvider 77 | * 78 | * @param providers 79 | * @param provider 80 | * @returns {boolean} 81 | * 82 | * @description 83 | * Get verified provider in providers 84 | */ 85 | static getVerifiedProvider(providers: Array, provider: IProvider): IProvider { 86 | return providers.find(item => isEqual(item.provide, provider.provide)); 87 | } 88 | 89 | /** 90 | * @since 1.0.0 91 | * @static 92 | * @function 93 | * @name Metadata#inProviders 94 | * 95 | * @param providers 96 | * @param provider 97 | * @returns {boolean} 98 | * 99 | * @description 100 | * Check if provider is in array of providers 101 | */ 102 | static inProviders(providers: Array, provider: any): boolean { 103 | return Metadata.inVerifiedProviders(Metadata.verifyProviders(providers), Metadata.verifyProvider(provider)); 104 | } 105 | 106 | /** 107 | * @since 1.0.0 108 | * @static 109 | * @function 110 | * @name Metadata#getProviderInProviders 111 | * 112 | * @param {Array} providers 113 | * @param {any} provider 114 | * @returns IProvider 115 | * 116 | * @description 117 | * Return provider in providers 118 | */ 119 | static getProviderInProviders(providers: Array, provider: any): IProvider { 120 | return Metadata.getVerifiedProvider(Metadata.verifyProviders(providers), Metadata.verifyProvider(provider)); 121 | } 122 | 123 | /** 124 | * @since 1.0.0 125 | * @static 126 | * @function 127 | * @name Metadata#isProvider 128 | * @param token 129 | * @returns {boolean} 130 | * 131 | * @description 132 | * Check if token is provider 133 | */ 134 | static isProvider(token: any): boolean { 135 | return isPresent(token) && 136 | isObject(token) && 137 | isPresent(token.provide) && 138 | (isString(token.provide) || isClass(token.provide)) && 139 | (isPresent(token.useValue) || isClass(token.useClass) || isFunction(token.useFactory)); 140 | } 141 | 142 | /** 143 | * @since 1.0.0 144 | * @static 145 | * @function 146 | * @name Metadata#isDescriptor 147 | * @param {Object} value 148 | * 149 | * @description 150 | * Check if current object is descriptor object 151 | */ 152 | static isDescriptor(value: Object): boolean { 153 | return isPresent(value) && ["writable", "configurable", "value", "enumerable"].every(key => value.hasOwnProperty(key)); 154 | }; 155 | 156 | /** 157 | * @since 1.0.0 158 | * @static 159 | * @function 160 | * @name Metadata#defineMetadata 161 | * @param {Function} token 162 | * @param {string} name 163 | * @param {Object} value 164 | * @return {any} value 165 | * 166 | * @description 167 | * Define metadata to some class 168 | */ 169 | static defineMetadata(token: Function, name: string, value: any): boolean { 170 | if (isPresent(value)) { 171 | Reflect.defineMetadata(name, value, token); 172 | return true; 173 | } 174 | return false; 175 | } 176 | 177 | /** 178 | * @since 1.0.0 179 | * @static 180 | * @function 181 | * @name Metadata#hasMetadata 182 | * @param {Function} token 183 | * @param {string} name 184 | * 185 | * @description 186 | * Check if some class has metadata by key 187 | */ 188 | static hasMetadata(token: Function, name: string): boolean { 189 | try { 190 | return Reflect.hasMetadata(name, token); 191 | } catch (e) { 192 | return false; 193 | } 194 | } 195 | 196 | /** 197 | * @since 1.0.0 198 | * @static 199 | * @function 200 | * @name Metadata#getMetadata 201 | * @param {Function} token 202 | * @param {String} name 203 | * @param {any} defaultValue 204 | * 205 | * @description 206 | * Get class metadata if not present return defaultValue 207 | */ 208 | static getMetadata(token: Function, name: string, defaultValue: any = []) { 209 | return Metadata.hasMetadata(token, name) ? Reflect.getMetadata(name, token) : defaultValue; 210 | } 211 | 212 | /** 213 | * @since 1.0.0 214 | * @static 215 | * @function 216 | * @name Metadata#getComponentConfig 217 | * @param {Function} Class 218 | * 219 | * @description 220 | * Get component config 221 | */ 222 | static getComponentConfig(Class: Function): any { 223 | return Metadata.getMetadata(Class, COMPONENT_CONFIG_KEYS, {}); 224 | } 225 | 226 | /** 227 | * @since 1.0.0 228 | * @static 229 | * @function 230 | * @name Metadata#setComponentConfig 231 | * @param {Function} Class 232 | * @param {any} config 233 | * 234 | * @description 235 | * Sets component config 236 | */ 237 | static setComponentConfig(Class: Function, config: any): void { 238 | Metadata.defineMetadata(Class, COMPONENT_CONFIG_KEYS, config); 239 | } 240 | 241 | /** 242 | * @since 1.0.0 243 | * @static 244 | * @function 245 | * @name Metadata#getConstructorProviders 246 | * @param {Function} Class 247 | * 248 | * @description 249 | * Return constructor providers in order to be delivered new instance to current injectable class 250 | */ 251 | static getConstructorProviders(Class: Function): Array { 252 | if (isFunction(Class)) { 253 | let config = Metadata.getMetadata(Class, COMPONENT_CONFIG_KEYS); 254 | if (isArray(config.providers)) { 255 | return config.providers.map(ProviderClass => Metadata.verifyProvider(ProviderClass)); 256 | } 257 | } 258 | return []; 259 | } 260 | 261 | /** 262 | * @since 1.0.0 263 | * @static 264 | * @function 265 | * @name Metadata#getConstructorPrototypeKeys 266 | * @param {Function} Class 267 | * 268 | * @description 269 | * Get keys metadata in order to know what Injector should do with them 270 | */ 271 | static getConstructorPrototypeKeys(Class: Function) { 272 | return Metadata.getMetadata(Class.prototype, INJECT_KEYS); 273 | } 274 | 275 | /** 276 | * @since 1.0.0 277 | * @static 278 | * @function 279 | * @name Metadata#getConstructorInjectKeys 280 | * @param {Function} Class 281 | * 282 | * @description 283 | * Get all metadata on Class constructor so Injector can decide what to do with them 284 | */ 285 | static getConstructorInjectKeys(Class: Function): Array { 286 | let providers = Metadata.getMetadata(Class, DESIGN_PARAMTYPES_KEYS); 287 | let injectors = Metadata.getMetadata(Class, FUNCTION_PARAMS); 288 | if (isArray(injectors)) { 289 | injectors.forEach(item => providers.splice(item.paramIndex, 1, item.value)); 290 | } 291 | return providers; 292 | } 293 | 294 | /** 295 | * @since 1.0.0 296 | * @static 297 | * @function 298 | * @name Metadata#hasProvider 299 | * @param {Array} providers 300 | * @param {Function} Class 301 | * 302 | * @description 303 | * Check if some list of providers are containing provider Class 304 | */ 305 | static hasProvider(providers: Array, Class: Function): boolean { 306 | return providers.some(item => { 307 | if (isObject(item)) { 308 | return item.provide === Class; 309 | } 310 | return item === Class; 311 | }); 312 | } 313 | 314 | /** 315 | * @since 1.0.0 316 | * @static 317 | * @function 318 | * @name Metadata#mergeProviders 319 | * @param {Array} a 320 | * @param {Array} b 321 | * 322 | * @description 323 | * Merge two provider definitions, this is used by Injector internally to know what to deliver at what time. 324 | * 325 | * This might look confusing but it does copy only from b if does't exist in a and b is added to beginning of 326 | * sequence. 327 | * 328 | * It must be like that because module nesting issue. 329 | * 330 | */ 331 | static mergeProviders(a: Array, b: Array) { 332 | return b.filter((bI: IProvider) => isFalsy(a.find((aI: IProvider) => aI.provide === bI.provide))).concat(a); 333 | } 334 | 335 | /** 336 | * @since 1.0.0 337 | * @static 338 | * @function 339 | * @name Metadata#verifyProviders 340 | * @param {Array} providers 341 | * 342 | * @description 343 | * Verify all providers in list 344 | */ 345 | static verifyProviders(providers: Array): Array { 346 | if (isArray(providers)) { 347 | return providers.map(ProviderClass => Metadata.verifyProvider(ProviderClass)); 348 | } 349 | throw new TypeError(`Providers must be an Array type`); 350 | } 351 | 352 | /** 353 | * @since 1.0.0 354 | * @static 355 | * @function 356 | * @name Metadata#verifyProvider 357 | * @param {Any} value 358 | * 359 | * @description 360 | * Verify provider to be sure that metadata configuration is provided correctly so it can be used by Injector 361 | */ 362 | static verifyProvider(value: any): IProvider { 363 | if (isFunction(value)) { 364 | return { 365 | provide: value, 366 | useClass: value 367 | }; 368 | } else if (isObject(value)) { 369 | if (!isPresent(value.provide) || (!isString(value.provide) && !isFunction(value.provide))) { 370 | throw new TypeError(`IProvider.provider must be string or Class type, ${toString(value)}`); 371 | } 372 | 373 | if (isPresent(value.useClass)) { 374 | if (isPresent(value.useValue) || isPresent(value.useFactory)) { 375 | throw new TypeError(`IProvider.useClass provider cannot have assigned useValue or useFactory`); 376 | } else if (!isFunction(value.useClass)) { 377 | throw new TypeError(`IProvider.useClass must be Class type`); 378 | } 379 | } 380 | 381 | if (isPresent(value.useValue)) { 382 | if (isPresent(value.useClass) || isPresent(value.useFactory)) { 383 | throw new TypeError(`IProvider.useValue provider cannot have assigned useClass or useFactory`); 384 | } 385 | } 386 | 387 | if (isPresent(value.useFactory)) { 388 | if (isPresent(value.useClass) || isPresent(value.useValue)) { 389 | throw new TypeError(`IProvider.useFactory provider cannot have assigned useClass or useValue`); 390 | } else if (!isFunction(value.useFactory)) { 391 | throw new TypeError(`IProvider.useFactory must be Function type`); 392 | } 393 | } 394 | 395 | if (!isPresent(value.useClass) && !isPresent(value.useValue) && !isPresent(value.useFactory)) { 396 | throw new TypeError(`IProvider members useClass or useValue or useFactory must be provided with IProvider`); 397 | } 398 | 399 | return value; 400 | } 401 | 402 | throw new TypeError(`Invalid provider config, provider must be an Class or IProvider type`); 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /src/interfaces/iaction.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 1.0.0 3 | * @interface 4 | * @name ErrorMessage 5 | * @param {number} status 6 | * @param {string} message 7 | * 8 | * @description 9 | * ErrorMessage message 10 | */ 11 | export interface ErrorMessage { 12 | status: number; 13 | message: string; 14 | } 15 | /** 16 | * @since 1.0.0 17 | * @interface 18 | * @name IAction 19 | * @param {String} type of annotation 20 | * @param {String} key mapped function name 21 | * @param {String} value route name 22 | * 23 | * @description 24 | * IAction 25 | */ 26 | export interface IAction { 27 | type: string; 28 | key: string; 29 | value: string | ErrorMessage; 30 | proto: Function; 31 | } 32 | -------------------------------------------------------------------------------- /src/interfaces/ibodyparser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 1.0.0 3 | * @interface 4 | * @name IBodyParser 5 | * 6 | * @description 7 | * Body parser interface 8 | */ 9 | export interface IBodyParser { 10 | parse(content: Buffer): any; 11 | } 12 | -------------------------------------------------------------------------------- /src/interfaces/iconnection.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 1.0.0 3 | * @interface 4 | * @name IConnection 5 | * @param {String} method 6 | * @param {String} url 7 | * @param {String} httpVersion 8 | * @param {Number} httpVersionMajor 9 | * @param {Number} httpVersionMinor 10 | * @param {String} remoteAddress 11 | * @param {String} remoteFamily 12 | * @param {Number} remotePort 13 | * @param {String} localAddress 14 | * @param {Number} localPort 15 | * 16 | * @description 17 | * Current connection data 18 | */ 19 | export interface IConnection { 20 | uuid: string; 21 | method: string; 22 | url: string; 23 | httpVersion: string; 24 | httpVersionMajor: number; 25 | httpVersionMinor: number; 26 | remoteAddress: string; 27 | remoteFamily: string; 28 | remotePort: number; 29 | localAddress: string; 30 | localPort: number; 31 | } 32 | -------------------------------------------------------------------------------- /src/interfaces/icontroller.ts: -------------------------------------------------------------------------------- 1 | import {IProvider} from "./iprovider"; 2 | import {TFilter} from "./ifilter"; 3 | /** 4 | * @since 1.0.0 5 | * @interface 6 | * @name IControllerMetadata 7 | * @param {String} name 8 | * @param {Array} providers 9 | * 10 | * @description 11 | * Controller metadata 12 | */ 13 | export interface IControllerMetadata { 14 | name: string; 15 | filters?: Array; 16 | providers?: Array; 17 | } 18 | -------------------------------------------------------------------------------- /src/interfaces/idecorators.ts: -------------------------------------------------------------------------------- 1 | import {IProvider} from "./iprovider"; 2 | /** 3 | * @since 1.0.0 4 | * @interface 5 | * @name IInjectKey 6 | * @param {Object} value 7 | * @param {Object} key 8 | * @param {Boolean} isMutable 9 | * 10 | * @description 11 | * Injection param is used internally by framework as metadata type 12 | */ 13 | export interface IInjectKey { 14 | key: any; 15 | Class: Function; 16 | isMutable: boolean; 17 | value: any; 18 | } 19 | /** 20 | * @since 1.0.0 21 | * @interface 22 | * @name IComponentMetaData 23 | * @param {IProvider[]} providers 24 | * 25 | * @description 26 | * Injection param is used internally by framework as metadata type 27 | */ 28 | export interface IComponentMetaData { 29 | providers: IProvider[]; 30 | } 31 | -------------------------------------------------------------------------------- /src/interfaces/ifilter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 1.0.0 3 | * @interface 4 | * @name IFilter 5 | * 6 | * @description 7 | * Filter for controller before and after processing 8 | */ 9 | export interface IFilter { 10 | before(data: string | Buffer): string | Buffer | Promise; 11 | after(data: string | Buffer): string | Buffer | Promise; 12 | } 13 | 14 | /** 15 | * @since 1.0.0 16 | * @interface 17 | * @name IFilter 18 | * 19 | * @description 20 | * Filter for controller before and after processing 21 | */ 22 | export interface TFilter { 23 | new (): IFilter; 24 | } 25 | -------------------------------------------------------------------------------- /src/interfaces/imodule.ts: -------------------------------------------------------------------------------- 1 | import {IProvider} from "./iprovider"; 2 | import {IResolvedRoute, RouteRuleConfig} from "./iroute"; 3 | import {Injector} from "../injector/injector"; 4 | /** 5 | * @since 1.0.0 6 | * @interface 7 | * @name IModuleMetadata 8 | * @param {Array} imports 9 | * @param {Array} exports 10 | * @param {String} name 11 | * @param {Array} modules 12 | * @param {Array} controllers 13 | * @param {Array} providers 14 | * 15 | * @description 16 | * Bootstrap class config metadata 17 | */ 18 | export interface IModuleMetadata { 19 | imports?: Array; 20 | exports?: Array; 21 | name?: string; 22 | controllers?: Array; 23 | sockets?: Array; 24 | providers?: Array; 25 | } 26 | 27 | /** 28 | * @since 1.0.0 29 | * @interface 30 | * @name IModule 31 | * @param {Injector} injector 32 | * @param {string} name 33 | * 34 | * @description 35 | * Bootstraped module injector instance 36 | */ 37 | export interface IModule { 38 | injector: Injector; 39 | provider: IProvider; 40 | name: string; 41 | } 42 | /** 43 | * @since 1.0.0 44 | * @interface 45 | * @name IResolvedModule 46 | * @param {Object} module 47 | * @param {Array} data 48 | * @param {String} controller 49 | * @param {String} action 50 | * 51 | * @description 52 | * Resolved module data from resolved route 53 | */ 54 | export interface IResolvedModule { 55 | module: IModule; 56 | data?: Array; 57 | resolvedRoute: IResolvedRoute; 58 | endpoint: string; 59 | action: string; 60 | } 61 | -------------------------------------------------------------------------------- /src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export {IControllerMetadata} from "./icontroller"; 2 | export {IFilter} from "./ifilter"; 3 | export {IModuleMetadata} from "./imodule"; 4 | export {IProvider, IAfterConstruct} from "./iprovider"; 5 | export {Route, RouteRuleConfig, IResolvedRoute, Headers} from "./iroute"; 6 | export {IWebSocketMetadata, IAfterClose} from "./iwebsocket"; 7 | -------------------------------------------------------------------------------- /src/interfaces/iparam.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 1.0.0 3 | * @interface 4 | * @name IParam 5 | * @param {Class} Function constructor of parameter 6 | * @param {String} type of annotation 7 | * @param {String} key mapped function name 8 | * @param {String} value name 9 | * @param {Number} paramIndex 10 | * 11 | * @description 12 | * IParam definition type 13 | */ 14 | export interface IParam { 15 | Class: Function; 16 | type: string; 17 | key: string; 18 | value: string; 19 | paramIndex: number; 20 | } 21 | -------------------------------------------------------------------------------- /src/interfaces/iprovider.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 1.0.0 3 | * @interface 4 | * @name IProvider 5 | * @param {any} provide 6 | * @param {Function} useClass 7 | * @param {any} useValue 8 | * @param {Function} useFactory 9 | * 10 | * @description 11 | * Provider config 12 | */ 13 | export interface IProvider { 14 | provide: any; 15 | useValue?: any; 16 | useClass?: Function; 17 | useFactory?: Function; 18 | deps?: Array; 19 | } 20 | 21 | /** 22 | * @since 1.0.0 23 | * @interface 24 | * @name IAfterConstruct 25 | * 26 | * @description 27 | * After construct interface 28 | */ 29 | export interface IAfterConstruct { 30 | afterConstruct(): void; 31 | } 32 | -------------------------------------------------------------------------------- /src/interfaces/iroute.ts: -------------------------------------------------------------------------------- 1 | import {Methods} from "../router/router"; 2 | /** 3 | * @since 1.0.0 4 | * @interface 5 | * @name IResolvedRoute 6 | * 7 | * @description 8 | * If we match route we resolve it with this interface 9 | */ 10 | export interface IResolvedRoute { 11 | method: Methods; 12 | params: Object; 13 | route: string; 14 | } 15 | /** 16 | * @since 1.0.0 17 | * @interface 18 | * @name IUrlTreePath 19 | * 20 | * @param {IUrlTreePath} child 21 | * @param {string} path 22 | * 23 | * @description 24 | * Metadata for RouteParser 25 | */ 26 | export interface IUrlTreePath { 27 | child?: IUrlTreePath; 28 | path: string; 29 | } 30 | /** 31 | * @since 1.0.0 32 | * @interface 33 | * @name Headers 34 | * 35 | * @description 36 | * ControllerResolver headers 37 | */ 38 | export interface Headers {} 39 | /** 40 | * @since 1.0.0 41 | * @interface 42 | * @name Route 43 | * 44 | * @description 45 | * Route definition 46 | */ 47 | export interface Route { 48 | parseRequest(pathName: string, method: string, headers: Headers): Promise; 49 | createUrl(routeName: string, params: Object): Promise; 50 | } 51 | /** 52 | * @since 1.0.0 53 | * @type 54 | * @name TRoute 55 | * 56 | * @description 57 | * TRoute declaration for type constructor 58 | */ 59 | export declare type TRoute = { 60 | new (): Route; 61 | }; 62 | /** 63 | * @since 1.0.0 64 | * @interface 65 | * @name RouteRuleConfig 66 | * 67 | * @description 68 | * Route rule definition 69 | */ 70 | export interface RouteRuleConfig { 71 | url: string; 72 | route: string; 73 | methods: Array; 74 | } 75 | 76 | -------------------------------------------------------------------------------- /src/interfaces/iwebsocket.ts: -------------------------------------------------------------------------------- 1 | import {IProvider} from "./iprovider"; 2 | 3 | /** 4 | * @since 2.0.0 5 | * @interface 6 | * @name IWebSocketMetadata 7 | * 8 | * @description 9 | * WebSocket metadata 10 | */ 11 | export interface IWebSocketMetadata { 12 | /** 13 | * Name of the socket - used in route definition 14 | */ 15 | name: string; 16 | 17 | /** 18 | * Additional providers for injector 19 | */ 20 | providers?: Array; 21 | } 22 | 23 | /** 24 | * @since 2.0.0 25 | * @interface 26 | * @name IAfterClose 27 | * 28 | * @description 29 | * Provides a method to implement for a {@link WebSocket} in order to be 30 | * called when the socket has been closed, e.g. to free up resources. 31 | */ 32 | export interface IAfterClose { 33 | /** 34 | * Called after the socket has already been closed. 35 | */ 36 | afterClose(): void; 37 | } 38 | -------------------------------------------------------------------------------- /src/logger/inspect.ts: -------------------------------------------------------------------------------- 1 | import {inspect as utilInspect} from "util"; 2 | import {isString} from "../core"; 3 | // cleanups on inspect 4 | let colors = new Array(100); 5 | let i = 0; 6 | while (i < 100) { 7 | colors.push(new RegExp("\\[" + i + "m", "ig")); 8 | ++i; 9 | } 10 | /** 11 | * @since 1.0.0 12 | * @function 13 | * @name clean 14 | * @param {String} message 15 | * @description 16 | * Clean inspect message 17 | * @return {String} message 18 | */ 19 | export function clean(message: string): string { 20 | if (isString(message)) { 21 | colors.forEach(value => message = message.replace(value, "")); 22 | message = message.replace(/\\'/g, "\'"); 23 | message = message.replace(/\\n/g, "\n"); 24 | return message.replace(/\\u001b/g, "\u001b"); 25 | } 26 | return message; 27 | } 28 | 29 | /** 30 | * @since 1.0.0 31 | * @function 32 | * @name inspect 33 | * @param {Object} data 34 | * @param {Number} level 35 | * @description 36 | * Inspect log data output 37 | */ 38 | export function inspect(data, level) { 39 | return utilInspect(data, {colors: true, depth: level}); 40 | } 41 | -------------------------------------------------------------------------------- /src/logger/level.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Mit Licence 2015 3 | * @since 1.0.0 4 | * @name Level 5 | * @private 6 | * @constructor 7 | * @description 8 | * Level is private class used by logger to define log levels 9 | */ 10 | export class Level { 11 | constructor(private name: string, 12 | private level: number, 13 | private callback: Function) { 14 | } 15 | /** 16 | * @since 1.0.0 17 | * @function 18 | * @name Level#getName 19 | * 20 | * @description 21 | * Get level name 22 | */ 23 | getName(): string { 24 | return this.name; 25 | } 26 | /** 27 | * @since 1.0.0 28 | * @function 29 | * @name Level#getLevel 30 | * 31 | * @description 32 | * Get level value 33 | */ 34 | getLevel(): number { 35 | return this.level; 36 | } 37 | /** 38 | * @since 1.0.0 39 | * @function 40 | * @name Level#exec 41 | * 42 | * @description 43 | * Execute hook 44 | */ 45 | exec(...args) { 46 | return this.callback(...args); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/logger/log.ts: -------------------------------------------------------------------------------- 1 | import {inspect, clean} from "./inspect"; 2 | import {Level} from "./level"; 3 | /** 4 | * @license Mit Licence 2015 5 | * @since 1.0.0 6 | * @name Log 7 | * @private 8 | * @constructor 9 | * @description 10 | * Log is private class used by logger to present logs to outputs 11 | */ 12 | export class Log { 13 | private data: any; 14 | private created: string = new Date().toISOString(); 15 | 16 | constructor(private message: string, data: any, private level: Level) { 17 | this.data = data instanceof Error ? data.stack : data; 18 | } 19 | /** 20 | * @since 1.0.0 21 | * @function 22 | * @name Log#getName 23 | * 24 | * @description 25 | * Get log level name 26 | */ 27 | getName(): string { 28 | return this.level.getName(); 29 | } 30 | /** 31 | * @since 1.0.0 32 | * @function 33 | * @name Log#getLevel 34 | * 35 | * @description 36 | * Get log level name 37 | */ 38 | getLevel(): number { 39 | return this.level.getLevel(); 40 | } 41 | /** 42 | * @since 1.0.0 43 | * @function 44 | * @name Level#prettify 45 | * 46 | * @description 47 | * Prettify output and print it 48 | */ 49 | prettify(prettify?: boolean): string { 50 | 51 | let log = inspect({ 52 | created: this.created, 53 | data: this.data, 54 | level: this.getLevel(), 55 | message: this.message, 56 | type: this.getName() 57 | }, 5); 58 | 59 | if (!!prettify) { 60 | log = clean(log); 61 | } 62 | 63 | return log; 64 | } 65 | /** 66 | * @since 1.0.0 67 | * @function 68 | * @name Level#toString 69 | * 70 | * @description 71 | * Convert log to string so we can write it on file for example 72 | */ 73 | toString(): string { 74 | return JSON.stringify(this.prettify(true)); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/logger/logger.ts: -------------------------------------------------------------------------------- 1 | import {Level} from "./level"; 2 | import {Log} from "./log"; 3 | import {Injectable} from "../decorators/injectable"; 4 | import {isEqual} from "../core"; 5 | 6 | /** 7 | * @since 1.0.0 8 | * @enum 9 | * @name LogLevels 10 | */ 11 | export enum LogLevels { 12 | TRACE = 10, 13 | BENCHMARK = 20, 14 | DEBUG = 30, 15 | INFO = 40, 16 | WARN = 60, 17 | ERROR = 80, 18 | FATAL = 100 19 | } 20 | /** 21 | * @since 1.0.0 22 | * @name Logger 23 | * 24 | * @constructor 25 | * @description 26 | * Logger is a component in easy node application. 27 | * Logger handler for easy node, there a various type of logs 28 | * [INFO, TRACE, DEBUG, WARN, ERROR, FATAL] 29 | * By default only ERROR and FATAL are enabled in production mode. 30 | * Logger in system is delivered as component 31 | * @example 32 | */ 33 | @Injectable() 34 | export class Logger { 35 | private hooks: Set = new Set(); 36 | private levels: Array = []; 37 | private enabled: boolean = false; 38 | private debugLevel: LogLevels = LogLevels.ERROR; 39 | 40 | constructor() { 41 | this.levels.push(new Level("TRACE", LogLevels.TRACE, console.info)); 42 | this.levels.push(new Level("BENCHMARK", LogLevels.BENCHMARK, console.info)); 43 | this.levels.push(new Level("DEBUG", LogLevels.DEBUG, console.info)); 44 | this.levels.push(new Level("INFO", LogLevels.INFO, console.info)); 45 | this.levels.push(new Level("WARN", LogLevels.WARN, console.warn)); 46 | this.levels.push(new Level("ERROR", LogLevels.ERROR, console.error)); 47 | this.levels.push(new Level("FATAL", LogLevels.FATAL, console.error)); 48 | } 49 | /** 50 | * @since 1.0.0 51 | * @function 52 | * @name Logger#isDebugLevel 53 | * 54 | * @description 55 | * Check if is certian log level 56 | */ 57 | isDebugLevel(logLevel: LogLevels): boolean { 58 | return isEqual(this.debugLevel, logLevel); 59 | } 60 | 61 | /** 62 | * @since 1.0.0 63 | * @function 64 | * @name Logger#setDebugLevel 65 | * 66 | * @description 67 | * Set debug level 68 | */ 69 | setDebugLevel(value: LogLevels) { 70 | this.debugLevel = value; 71 | } 72 | 73 | /** 74 | * @since 1.0.0 75 | * @function 76 | * @name Logger#enable 77 | * 78 | * @description 79 | * enable logger 80 | */ 81 | enable() { 82 | this.enabled = true; 83 | } 84 | 85 | /** 86 | * @since 1.0.0 87 | * @function 88 | * @name Logger#printToConsole 89 | * 90 | * @description 91 | * Print to console logs 92 | */ 93 | printToConsole() { 94 | this.levels.forEach((item: Level) => 95 | this.addHook((log: Log) => { 96 | if (log.getLevel() === item.getLevel()) { 97 | item.exec(log.prettify()); 98 | } 99 | }) 100 | ); 101 | } 102 | 103 | /** 104 | * @since 1.0.0 105 | * @function 106 | * @name Logger#trace 107 | * 108 | * @description 109 | * Trace 110 | */ 111 | trace(message, ...args): boolean { 112 | return this.log(message, args, this.filter(LogLevels.TRACE)); 113 | } 114 | 115 | /** 116 | * @since 1.0.0 117 | * @function 118 | * @name Logger#info 119 | * 120 | * @description 121 | * Log info case 122 | */ 123 | info(message, ...args): boolean { 124 | return this.log(message, args, this.filter(LogLevels.INFO)); 125 | } 126 | 127 | /** 128 | * @since 1.0.0 129 | * @function 130 | * @name Logger#debug 131 | * 132 | * @description 133 | * Debug 134 | */ 135 | debug(message, ...args): boolean { 136 | return this.log(message, args, this.filter(LogLevels.DEBUG)); 137 | } 138 | 139 | /** 140 | * @since 1.0.0 141 | * @function 142 | * @name Logger#warn 143 | * 144 | * @description 145 | * Log warn case 146 | */ 147 | warn(message, ...args): boolean { 148 | return this.log(message, args, this.filter(LogLevels.WARN)); 149 | } 150 | 151 | /** 152 | * @since 1.0.0 153 | * @function 154 | * @name Logger#benchmark 155 | * 156 | * @description 157 | * Benchmarking pourposes 158 | */ 159 | benchmark(message, ...args): boolean { 160 | return this.log(message, args, this.filter(LogLevels.BENCHMARK)); 161 | } 162 | 163 | /** 164 | * @since 1.0.0 165 | * @function 166 | * @name Logger#error 167 | * 168 | * @description 169 | * Log error case 170 | */ 171 | error(message, ...args): boolean { 172 | return this.log(message, args, this.filter(LogLevels.ERROR)); 173 | } 174 | 175 | /** 176 | * @since 1.0.0 177 | * @function 178 | * @name Logger#fatal 179 | * 180 | * @description 181 | * Fatal error 182 | */ 183 | fatal(message, ...args): boolean { 184 | return this.log(message, args, this.filter(LogLevels.FATAL)); 185 | } 186 | 187 | /** 188 | * @since 1.0.0 189 | * @function 190 | * @name Logger#filter 191 | * @private 192 | * @description 193 | * Get level name 194 | * This is used internally by logger class 195 | */ 196 | filter(level: LogLevels): Level { 197 | return this.levels.find((item: Level) => { 198 | return item.getLevel() === level; 199 | }); 200 | } 201 | 202 | /** 203 | * @since 1.0.0 204 | * @function 205 | * @name Logger#addHook 206 | * @param {Function} callback 207 | * 208 | * @description 209 | * Add hook to log output so developer can extend where to store log 210 | */ 211 | addHook(callback: Function): void { 212 | this.hooks.add(callback); 213 | } 214 | 215 | /** 216 | * @since 1.0.0 217 | * @function 218 | * @name Logger#log 219 | * @private 220 | * @description 221 | * Write to file and exec hooks 222 | */ 223 | private log(message, data, level: Level): boolean { 224 | if (!this.enabled || level.getLevel() < this.debugLevel) { 225 | return false; 226 | } 227 | this.hooks.forEach(hook => hook(new Log(message, data, level))); 228 | return true; 229 | } 230 | 231 | } 232 | -------------------------------------------------------------------------------- /src/parsers/index.ts: -------------------------------------------------------------------------------- 1 | export {MultiPartField, MultiPartFile, MultiPart} from "./multipart"; 2 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | export {Router, Methods, getMethod} from "./router"; 2 | export {RouteParser} from "./route-parser"; 3 | export {RouteRule} from "./route-rule"; 4 | -------------------------------------------------------------------------------- /src/router/route-parser.ts: -------------------------------------------------------------------------------- 1 | import {isFalsy, isObject, isPresent, isTruthy} from "../core"; 2 | import {IUrlTreePath} from "../interfaces/iroute"; 3 | import {Router} from "./router"; 4 | 5 | const IS_ANY_PATTERN = /<([^>]+)>/; 6 | const PATTERN_MATCH = /<(\w+):([^>]+)>/g; 7 | const HAS_GROUP_START = /^\(/; 8 | const HAS_GROUP_END = /\)$/; 9 | const PATH_START = /^\//; 10 | const PATH = /\//; 11 | const PATH_SPLIT = /\/([^><]+|<\w+:[^>]+>|\w+)\//; 12 | 13 | /** 14 | * @since 1.0.0 15 | * @function 16 | * @name PatternChunk 17 | * @constructor 18 | * 19 | * @param {string} replace 20 | * @param {string} param 21 | * @param {number} index 22 | * @param {string} source 23 | * 24 | * @description 25 | * Pattern chunk used by pattern 26 | */ 27 | export class PatternChunk { 28 | private regex: RegExp; 29 | 30 | constructor(private index: number, 31 | private replace: string, 32 | private source: string, 33 | private param?: string) { 34 | this.regex = new RegExp("^" + source + "$"); 35 | } 36 | 37 | /** 38 | * @since 1.0.0 39 | * @function 40 | * @name PatternChunk#getRegex 41 | * 42 | * @description 43 | * Get regex from source 44 | */ 45 | getRegex(): RegExp { 46 | return this.regex; 47 | } 48 | 49 | /** 50 | * @since 1.0.0 51 | * @function 52 | * @name PatternChunk#getIndex 53 | * 54 | * @description 55 | * Get index 56 | */ 57 | getIndex(): number { 58 | return this.index; 59 | } 60 | 61 | /** 62 | * @since 1.0.0 63 | * @function 64 | * @name PatternChunk#getReplaceMatcher 65 | * 66 | * @description 67 | * Get replace matcher 68 | */ 69 | getReplaceMatcher(): string { 70 | return this.replace; 71 | } 72 | 73 | /** 74 | * @since 1.0.0 75 | * @function 76 | * @name PatternChunk#getSource 77 | * 78 | * @description 79 | * Get source 80 | */ 81 | getSource(): string { 82 | return this.source; 83 | } 84 | 85 | /** 86 | * @since 1.0.0 87 | * @function 88 | * @name PatternChunk#getParam 89 | * 90 | * @description 91 | * Get parameter 92 | */ 93 | getParam(): string { 94 | return this.param; 95 | } 96 | } 97 | 98 | /** 99 | * @since 1.0.0 100 | * @function 101 | * @name Pattern 102 | * @constructor 103 | * 104 | * @param {path} path 105 | * @param {regex} replace 106 | * @param {chunks} param 107 | * 108 | * @description 109 | * Route match pattern 110 | */ 111 | export class Pattern { 112 | constructor(private path: string, 113 | private source: string, 114 | private regex: RegExp, 115 | private chunks: Array) { 116 | } 117 | 118 | /** 119 | * @since 1.0.0 120 | * @function 121 | * @name Pattern#normalizePath 122 | * 123 | * @description 124 | * Creates path from chunks throws error if no param is correct data type or if it dosen't exist 125 | * 126 | * @throws Error 127 | */ 128 | normalizePath(params: Object): string { 129 | let path = this.path; 130 | this.chunks.forEach(chunk => { 131 | if (!params.hasOwnProperty(chunk.getParam())) { 132 | throw new Error(`Param ${chunk.getParam()} is missing in parameters provided!`); 133 | } 134 | let param = params[chunk.getParam()]; 135 | if (!chunk.getRegex().test(param)) { 136 | throw new TypeError(`Param ${chunk.getParam()} is not valid type, provided value: "${param}" 137 | is tested with regex: ${chunk.getSource()}`); 138 | } 139 | path = path.replace(chunk.getReplaceMatcher(), param); 140 | }); 141 | return path; 142 | } 143 | 144 | /** 145 | * @since 1.0.0 146 | * @function 147 | * @name Pattern#getSource 148 | * 149 | * @description 150 | * Get source pattern 151 | */ 152 | getSource(): string { 153 | return this.source; 154 | } 155 | 156 | /** 157 | * @since 1.0.0 158 | * @function 159 | * @name Pattern#getChunks 160 | * 161 | * @description 162 | * Get array chunks 163 | */ 164 | getChunks(): Array { 165 | return this.chunks; 166 | } 167 | } 168 | 169 | /** 170 | * @since 1.0.0 171 | * @function 172 | * @name RouteParser 173 | * @constructor 174 | * 175 | * @param {IUrlTreePath} tree 176 | * 177 | * @description 178 | * RouteParser is responsible for parsing routes 179 | */ 180 | export class RouteParser { 181 | 182 | path: string; 183 | pattern: Pattern; 184 | child: RouteParser; 185 | parent: RouteParser; 186 | 187 | /** 188 | * @since 1.0.0 189 | * @function 190 | * @name RouteParser#toPattern 191 | * @param {String} path 192 | * @static 193 | * 194 | * @description 195 | * Creates pattern based on path provided 196 | * @example 197 | * RouteParser.toPattern("--now--not"); 198 | */ 199 | static toPattern(path: string): Pattern { 200 | let patterns = []; 201 | let pattern = null; 202 | /** 203 | * Parse patterns 204 | */ 205 | if (PATTERN_MATCH.test(path)) { 206 | pattern = path.replace(PATTERN_MATCH, (replace, key, source, index) => { 207 | if (!HAS_GROUP_START.test(source) || !HAS_GROUP_END.test(source)) { 208 | source = "(" + source + ")"; 209 | } 210 | patterns.push( 211 | new PatternChunk( 212 | index, 213 | replace, 214 | source, 215 | key 216 | ) 217 | ); 218 | return source; 219 | }); 220 | } else if (IS_ANY_PATTERN.test(path)) { 221 | pattern = path.replace(IS_ANY_PATTERN, (replace, key, index) => { 222 | let source = "([\\s\\S]+)"; 223 | patterns.push( 224 | new PatternChunk( 225 | index, 226 | replace, 227 | source, 228 | key 229 | ) 230 | ); 231 | return source; 232 | }); 233 | } else if (path === "*") { 234 | pattern = "([\\s\\S]+)"; 235 | } else { 236 | pattern = path; 237 | } 238 | return new Pattern(path, pattern, new RegExp("^" + pattern + "$"), patterns); 239 | } 240 | 241 | /** 242 | * @since 1.0.0 243 | * @function 244 | * @name RouteParser#parse 245 | * @param {String} url 246 | * @static 247 | * 248 | * @description 249 | * Creates url tree which is used by RouteParser for easier pattern creation 250 | * 251 | * @example 252 | * RouteParser.parse("/canone//shoulddo-it/--now--not/user/"); 253 | * 254 | */ 255 | static parse(url: string): RouteParser { 256 | let chunks = url.split(PATH_SPLIT).filter(isTruthy); 257 | if (isFalsy(url) || ["/", "*"].indexOf(url.charAt(0)) === -1) { 258 | throw new Error("Url must start with \/ or it has to be * which match all patterns"); 259 | } else if (chunks.length > 1) { 260 | let tree: any = chunks.reduceRight((cTree: any, currentValue: any) => { 261 | let obj: any = { 262 | child: null, 263 | path: currentValue 264 | }; 265 | if (isPresent(cTree)) { 266 | obj.child = isObject(cTree) ? cTree : {child: null, path: cTree}; 267 | } 268 | return obj; 269 | }); 270 | return new RouteParser(tree); 271 | } 272 | return new RouteParser({ 273 | child: null, 274 | path: url 275 | }); 276 | } 277 | 278 | /** 279 | * @since 1.0.0 280 | * @function 281 | * @name RouteParser 282 | * @param {IUrlTreePath} tree 283 | * @param {RouteParser} parent 284 | * @constructor 285 | * 286 | * @description 287 | * Creates pattern based on path provided 288 | * @example 289 | * let tree = RouteParser.toUrlTree("/--now--not/bcd"); 290 | * let parsedRoute = new RouteParser(tree); 291 | */ 292 | constructor(tree: IUrlTreePath, parent?: RouteParser) { 293 | this.path = tree.path; 294 | if (isPresent(parent)) { 295 | this.parent = parent; 296 | } 297 | if (isPresent(tree.child)) { 298 | this.child = new RouteParser(tree.child, this); 299 | } 300 | this.pattern = RouteParser.toPattern(this.path); 301 | } 302 | 303 | /** 304 | * @since 1.0.0 305 | * @function 306 | * @name RouteParser#isValid 307 | * @param {String} url 308 | * @description 309 | * Check if url is valid 310 | */ 311 | isValid(url: string): boolean { 312 | let pattern = this.getPattern(); 313 | let regex = new RegExp("^" + pattern + "$"); 314 | return regex.test(url); 315 | } 316 | 317 | /** 318 | * @since 1.0.0 319 | * @function 320 | * @name RouteParser#createUrl 321 | * @param {Object} params 322 | * @description 323 | * Create url if params are correct type if params are not valid it throws error 324 | * @throws Error 325 | */ 326 | createUrl(params: Object): string { 327 | let head = this.getHead(); 328 | let url = ""; 329 | while (isPresent(head)) { 330 | let path = head.pattern.normalizePath(params); 331 | if (isTruthy(path)) { 332 | url += Router.prefixSlash(path); 333 | } 334 | head = head.child; 335 | } 336 | return url; 337 | } 338 | 339 | /** 340 | * @since 1.0.0 341 | * @function 342 | * @name RouteParser#getParams 343 | * @param {String} url 344 | * @description 345 | * Extract params from url 346 | * @throws Error 347 | */ 348 | getParams(url: string): Object { 349 | let data = {}; 350 | let pattern = this.getPattern(); 351 | if (!this.isValid(url)) { 352 | throw new Error(`Url ${url} is not matching current pattern!`); 353 | } 354 | let chunks = url.match(pattern).slice(1); 355 | this.toChunksArray().forEach((item, index) => data[item.getParam()] = chunks[index]); 356 | return data; 357 | } 358 | 359 | /** 360 | * @since 1.0.0 361 | * @function 362 | * @name RouteParser#toArray 363 | * @description 364 | * Convert parser tree to array 365 | */ 366 | toChunksArray(): Array { 367 | let data: Array = []; 368 | let chunk = this.getHead(); 369 | while (isTruthy(chunk)) { 370 | data = data.concat(chunk.pattern.getChunks()); 371 | chunk = chunk.child; 372 | } 373 | return data; 374 | } 375 | 376 | /** 377 | * @since 1.0.0 378 | * @function 379 | * @name RouteParser#getPattern 380 | * @description 381 | * Get route pattern 382 | */ 383 | private getPattern(): string { 384 | let pattern = ""; 385 | let chunk = this.getHead(); 386 | while (isTruthy(chunk)) { 387 | let source = chunk.pattern.getSource(); 388 | if (PATH_START.test(source)) { 389 | pattern = source; 390 | } else { 391 | pattern += PATH.source + source; 392 | } 393 | chunk = chunk.child; 394 | } 395 | return pattern; 396 | } 397 | 398 | /** 399 | * @since 1.0.0 400 | * @function 401 | * @name RouteParser#getHead 402 | * @return {RouteParser} 403 | * @private 404 | * 405 | * @description 406 | * Return head RouteParser 407 | * 408 | */ 409 | private getHead() { 410 | if (isPresent(this.parent)) { 411 | return this.parent.getHead(); 412 | } 413 | return this; 414 | } 415 | } 416 | -------------------------------------------------------------------------------- /src/router/route-rule.ts: -------------------------------------------------------------------------------- 1 | import {Route, Headers, RouteRuleConfig, IResolvedRoute} from "../interfaces/iroute"; 2 | import {RouteParser} from "./route-parser"; 3 | import {getMethod} from "./router"; 4 | import {IAfterConstruct} from "../interfaces/iprovider"; 5 | import {Injectable} from "../decorators/injectable"; 6 | import {Inject} from "../decorators/inject"; 7 | 8 | /** 9 | * @since 1.0.0 10 | * @function 11 | * @name RouteRule 12 | * @constructor 13 | * 14 | * @param {RouteRuleConfig} config 15 | * 16 | * @description 17 | * Route rule provider is used by router to parse request and create route url 18 | */ 19 | @Injectable() 20 | export class RouteRule implements Route, IAfterConstruct { 21 | 22 | @Inject("config") 23 | private config: RouteRuleConfig; 24 | private routeParser: RouteParser; 25 | /** 26 | * @since 1.0.0 27 | * @function 28 | * @name RouteRule#afterConstruct 29 | * @private 30 | * 31 | * @description 32 | * After construct apply config data 33 | */ 34 | afterConstruct(): void { 35 | this.routeParser = RouteParser.parse(this.config.url); 36 | } 37 | /** 38 | * @since 1.0.0 39 | * @function 40 | * @name RouteRule#parseRequest 41 | * @param {String} path 42 | * @param {String} method 43 | * @param {Headers} headers 44 | * @private 45 | * 46 | * @return {Promise} 47 | * @static 48 | * 49 | * @description 50 | * Parse request is used internally by Router to be able to parse request 51 | */ 52 | parseRequest(path: string, method: string, headers: Headers): Promise { 53 | if (!this.routeParser.isValid(path) || this.config.methods.indexOf(getMethod(method)) === -1) { 54 | return Promise.resolve(false); 55 | } 56 | return Promise.resolve({ 57 | method: getMethod(method), 58 | params: this.routeParser.getParams(path), 59 | route: this.config.route 60 | }); 61 | } 62 | 63 | /** 64 | * @since 1.0.0 65 | * @function 66 | * @name RouteRule#parseRequest 67 | * @param {String} routeName 68 | * @param {Object} params 69 | * @private 70 | * 71 | * @return {Promise} 72 | * @static 73 | * 74 | * @description 75 | * It try's to create url 76 | */ 77 | createUrl(routeName: string, params: Object): Promise { 78 | if (this.config.route === routeName) { 79 | return Promise.resolve(this.routeParser.createUrl(params)); 80 | } 81 | return Promise.resolve(false); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/router/router.ts: -------------------------------------------------------------------------------- 1 | import {Logger} from "../logger/logger"; 2 | import {HttpError} from "../error"; 3 | import {Route, Headers, RouteRuleConfig, IResolvedRoute, TRoute} from "../interfaces/iroute"; 4 | import {isTruthy, isString, isEqual, isPresent} from "../core"; 5 | import {Injector} from "../injector/injector"; 6 | import {RouteRule} from "./route-rule"; 7 | import {Injectable} from "../decorators/injectable"; 8 | import {Inject} from "../decorators/inject"; 9 | import {Status} from "../server/status-code"; 10 | /** 11 | * @since 1.0.0 12 | * @enum 13 | * @name Methods 14 | * 15 | * @description 16 | * ControllerResolver methods 17 | */ 18 | export enum Methods { 19 | GET, 20 | HEAD, 21 | DELETE, 22 | TRACE, 23 | OPTIONS, 24 | CONNECT, 25 | POST, 26 | PUT, 27 | PATCH 28 | } 29 | 30 | /** 31 | * @since 1.0.0 32 | * @function 33 | * @name getMethodName 34 | * @param {string} method 35 | * 36 | * @description 37 | * Get method name from Methods enum 38 | */ 39 | export function getMethodName(method: Methods): string { 40 | if (isEqual(Methods.GET, method)) { 41 | return "GET"; 42 | } else if (isEqual(Methods.HEAD, method)) { 43 | return "HEAD"; 44 | } else if (isEqual(Methods.DELETE, method)) { 45 | return "DELETE"; 46 | } else if (isEqual(Methods.TRACE, method)) { 47 | return "TRACE"; 48 | } else if (isEqual(Methods.OPTIONS, method)) { 49 | return "OPTIONS"; 50 | } else if (isEqual(Methods.CONNECT, method)) { 51 | return "CONNECT"; 52 | } else if (isEqual(Methods.POST, method)) { 53 | return "POST"; 54 | } else if (isEqual(Methods.PUT, method)) { 55 | return "PUT"; 56 | } else if (isEqual(Methods.PATCH, method)) { 57 | return "PATCH"; 58 | } 59 | } 60 | /** 61 | * @since 1.0.0 62 | * @function 63 | * @name getMethod 64 | * @param {string} method 65 | * 66 | * @description 67 | * Get method enum from method string 68 | * @throws TypeError 69 | */ 70 | export function getMethod(method: string): Methods { 71 | if (method === "GET") { 72 | return Methods.GET; 73 | } else if (method === "HEAD") { 74 | return Methods.HEAD; 75 | } else if (method === "DELETE") { 76 | return Methods.DELETE; 77 | } else if (method === "TRACE") { 78 | return Methods.TRACE; 79 | } else if (method === "OPTIONS") { 80 | return Methods.OPTIONS; 81 | } else if (method === "CONNECT") { 82 | return Methods.CONNECT; 83 | } else if (method === "POST") { 84 | return Methods.POST; 85 | } else if (method === "PUT") { 86 | return Methods.PUT; 87 | } else if (method === "PATCH") { 88 | return Methods.PATCH; 89 | } 90 | throw new TypeError(`Method ${method} is not known method by standard!`); 91 | } 92 | /** 93 | * @since 1.0.0 94 | * @class 95 | * @name Router 96 | * @constructor 97 | * @description 98 | * Router is a component for handling routing in system. 99 | * All routes should be added during bootstrap process 100 | * @example 101 | * import { Bootstrap, Router } from "../core"; 102 | * import { Assets } from "./components/assets"; 103 | * 104 | * \@Bootstrap({ 105 | * port: 9000 106 | * }) 107 | * export class Application { 108 | * constructor(assets: Assets, router: Router) { 109 | * router.add() 110 | * } 111 | * } 112 | */ 113 | 114 | @Injectable() 115 | export class Router { 116 | 117 | /** 118 | * @param {Logger} logger 119 | */ 120 | @Inject(Logger) 121 | private logger: Logger; 122 | /** 123 | * @param {Injector} injector 124 | */ 125 | @Inject(Injector) 126 | private injector: Injector; 127 | /** 128 | * @param {Array} routes 129 | */ 130 | private routes: Array = []; 131 | /** 132 | * ErrorMessage route definition 133 | * @param {String} errorRoute 134 | */ 135 | private errorRoutes: Array = []; 136 | 137 | /** 138 | * @since 1.0.0 139 | * @function 140 | * @name Router#prefixSlash 141 | * @param {string} value 142 | * @static 143 | * @private 144 | * 145 | * @description 146 | * Prefixes url with starting slash 147 | */ 148 | static prefixSlash(value: string): string { 149 | return value.charAt(0) === "/" ? value : "/" + value; 150 | } 151 | 152 | /** 153 | * @since 1.0.0 154 | * @function 155 | * @name Router#getError 156 | * 157 | * @description 158 | * Returns error route string 159 | */ 160 | getError(module?: string): string { 161 | let routes = this.errorRoutes.slice(); 162 | let item: string; 163 | while (routes.length > 0) { 164 | item = routes.pop(); 165 | if (item.startsWith(module + "/")) { 166 | return item; 167 | } 168 | } 169 | return item; 170 | } 171 | 172 | /** 173 | * @since 1.0.0 174 | * @function 175 | * @name Router#hasErrorRoute 176 | * 177 | * @description 178 | * Check if error route is provided 179 | */ 180 | hasError(): boolean { 181 | return isTruthy(this.errorRoutes.length > 0); 182 | } 183 | 184 | /** 185 | * @since 1.0.0 186 | * @function 187 | * @name Router#setError 188 | * @param {string} route 189 | * 190 | * @description 191 | * Add error route 192 | */ 193 | setError(route: string): void { 194 | if (this.errorRoutes.indexOf(route) === -1 && isString(route)) { 195 | let list = route.split("/"); 196 | if (list.length < 2) { 197 | throw new Error(`Invalid route structure: "${route}"! Valid are controller/action or module/controller/action!`); 198 | } 199 | this.errorRoutes.push(route); 200 | } 201 | } 202 | 203 | /** 204 | * @since 1.0.0 205 | * @function 206 | * @name Router#addRules 207 | * @param {Array} rules 208 | * 209 | * @description 210 | * Add route to routes list. 211 | * All routes must be inherited from Route interface. 212 | */ 213 | addRules(rules: Array): void { 214 | this.logger.info("Router.addRules", rules); 215 | rules.forEach(config => this.routes.push(this.createRule(RouteRule, config))); 216 | } 217 | 218 | /** 219 | * @since 1.0.0 220 | * @function 221 | * @name Router#addRule 222 | * @param {Function} Class 223 | * @param {RouteRuleConfig} config 224 | * 225 | * @description 226 | * Create rule and add rule to list 227 | */ 228 | addRule(Class: TRoute, config?: RouteRuleConfig): void { 229 | this.logger.info("Router.addRule", Class); 230 | this.routes.push(this.createRule(Class, config)); 231 | } 232 | 233 | /** 234 | * @since 1.0.0 235 | * @function 236 | * @name Router#createRule 237 | * @param {Function} Class 238 | * @param {RouteRuleConfig} config 239 | * 240 | * @description 241 | * Initialize rule 242 | */ 243 | private createRule(Class: TRoute, config?: RouteRuleConfig): Route { 244 | let injector = Injector.createAndResolveChild( 245 | this.injector, 246 | Class, 247 | isPresent(config) ? [{provide: "config", useValue: config}] : [] 248 | ); 249 | return injector.get(Class); 250 | } 251 | 252 | /** 253 | * @since 1.0.0 254 | * @function 255 | * @name Router#parseRequest 256 | * @param {String} pathName 257 | * @param {String} method 258 | * @param {Headers} headers 259 | * 260 | * @description 261 | * Parse request based on pathName and method 262 | */ 263 | async parseRequest(pathName: string, method: string, headers: Headers): Promise { 264 | for (let route of this.routes) { 265 | let result = await route.parseRequest(pathName, method, headers); 266 | if (isTruthy(result) && !isEqual(true, result)) { 267 | this.logger.trace("Router.parseRequest", result); 268 | return Promise.resolve( result); 269 | } 270 | } 271 | throw new HttpError(Status.Not_Found, `Router.parseRequest: ${pathName} no route found, method: ${method}`, { 272 | pathName, 273 | method 274 | }); 275 | } 276 | 277 | /** 278 | * @since 1.0.0 279 | * @function 280 | * @name Router#createUrl 281 | * @param {String} routeName 282 | * @param {Object} params 283 | * 284 | * @description 285 | * Create url based on route and params 286 | */ 287 | async createUrl(routeName: string, params: Object): Promise { 288 | for (let route of this.routes) { 289 | let result = await > route.createUrl(routeName, params); 290 | if (isTruthy(result) && !isEqual(true, result)) { 291 | this.logger.info("Router.createUrl", result); 292 | return Promise.resolve(Router.prefixSlash(result)); 293 | } 294 | } 295 | if (Object.keys(params).length > 0) { 296 | routeName += "?"; 297 | Object.keys(params).forEach((k) => { 298 | routeName += k + "=" + encodeURIComponent(params[k]); 299 | }); 300 | } 301 | this.logger.info("Router.createUrl", Router.prefixSlash(routeName)); 302 | return Promise.resolve(Router.prefixSlash(routeName)); 303 | } 304 | 305 | } 306 | -------------------------------------------------------------------------------- /src/server/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import {Injector} from "../injector/injector"; 2 | import {Logger} from "../logger/logger"; 3 | import {IncomingMessage, ServerResponse} from "http"; 4 | import {isArray, isEqual, isFalsy, isTruthy, uuid} from "../core"; 5 | import {IModule, IModuleMetadata} from "../interfaces/imodule"; 6 | import {Metadata} from "../injector/metadata"; 7 | import {HttpRequestResolver} from "./request-resolver"; 8 | import {parse} from "url"; 9 | import {IProvider} from "../interfaces/iprovider"; 10 | import {EventEmitter} from "events"; 11 | import {Status} from "./status-code"; 12 | import {Router} from "../router/router"; 13 | 14 | export const BOOTSTRAP_PROVIDERS = [Logger, Router]; 15 | export const BOOTSTRAP_MODULE = "root"; 16 | 17 | /** 18 | * @since 1.0.0 19 | * @function 20 | * @name doModulesDuplicationCheck 21 | * @param {Array} modules 22 | * @param {IModuleMetadata} metadata 23 | * @param {IProvider} provider 24 | */ 25 | export function doModulesDuplicationCheck(modules: Array, metadata: IModuleMetadata, provider: IProvider) { 26 | let duplicationCheck = modules.slice(); 27 | /** 28 | * Duplication check 29 | */ 30 | while (duplicationCheck.length) { 31 | let importedModule: IModule = duplicationCheck.pop(); 32 | /** 33 | * Provider duplication check 34 | */ 35 | if (!isEqual(provider, importedModule) && isEqual(metadata.name, importedModule.name)) { 36 | let message = "Multiple modules with same name detected! Module name: "; 37 | message += importedModule.name + " is defined in modules: [" + Metadata.getName(provider) + ", "; 38 | message += Metadata.getName(importedModule) + "]"; 39 | message += " Please provide unique names to modules!"; 40 | throw new Error(message); 41 | } 42 | } 43 | } 44 | /** 45 | * @since 1.0.0 46 | * @function 47 | * @name getModule 48 | * @param {Array} modules 49 | * @param {String} name 50 | * 51 | * @description 52 | * Find root module 53 | */ 54 | export function getModule(modules: Array, name: string = BOOTSTRAP_MODULE): IModule { 55 | return modules.find(item => item.name === name); 56 | } 57 | /** 58 | * @since 1.0.0 59 | * @function 60 | * @name createModule 61 | * @param {Provider|Function} Class 62 | * @param {Injector} sibling 63 | * @param {Array} modules list 64 | * 65 | * @description 66 | * Bootstrap modules recursive handler imported modules and export services which is needed 67 | * 68 | */ 69 | export function createModule(Class: IProvider | Function, sibling?: Injector, modules = []): Array { 70 | 71 | let provider: IProvider = Metadata.verifyProvider(Class); 72 | let metadata: IModuleMetadata = Metadata.getComponentConfig(provider.provide); 73 | 74 | // Check if module is present then return module reference list 75 | if (isTruthy(getModule(modules, metadata.name))) { 76 | return modules; 77 | } 78 | 79 | // Create new injector instance 80 | let injector = new Injector(); 81 | let providers: Array = []; 82 | // Set name for provider 83 | injector.setName(provider); 84 | 85 | // Create instance of Router && Logger if thy are provided 86 | 87 | BOOTSTRAP_PROVIDERS.forEach(iClass => { 88 | if (Metadata.inProviders(metadata.providers, iClass)) { 89 | injector.createAndResolve(Metadata.getProviderInProviders(metadata.providers, iClass), []); 90 | } else if (isTruthy(sibling) && sibling.has(iClass)) { 91 | injector.set(iClass, sibling.get(iClass)); 92 | } 93 | }); 94 | /** 95 | * Imports must be initialized before first 96 | */ 97 | if (isArray(metadata.imports)) { 98 | metadata.imports.forEach(importModule => { 99 | let importProvider = Metadata.verifyProvider(importModule); 100 | let importMetadata: IModuleMetadata = Metadata.getComponentConfig(importProvider.provide); 101 | 102 | /** 103 | * Create module only if is not already created 104 | */ 105 | if (isFalsy(getModule(modules, importMetadata.name))) { 106 | /** 107 | * Create module first 108 | * @type {Array} 109 | */ 110 | let importModules = createModule(importModule, injector, modules); 111 | let module = getModule(importModules, importMetadata.name); 112 | /** 113 | * Export providers to importers 114 | */ 115 | if (isArray(importMetadata.exports)) { 116 | providers = providers.concat(importMetadata.exports.map((iClass: IProvider) => { 117 | return { 118 | provide: iClass, 119 | useValue: module.injector.get(iClass) 120 | }; 121 | })); 122 | } 123 | modules = importModules.filter((bI: IModule) => isFalsy(modules.find((aI: IModule) => aI.name === bI.name))).concat(modules); 124 | } else { 125 | let module: IModule = getModule(modules, importMetadata.name); 126 | /** 127 | * Export providers to importers 128 | */ 129 | if (isArray(importMetadata.exports)) { 130 | providers = providers.concat(importMetadata.exports.map((iClass: IProvider) => { 131 | return { 132 | provide: iClass, 133 | useValue: module.injector.get(iClass) 134 | }; 135 | })); 136 | } 137 | } 138 | }); 139 | } 140 | 141 | /** 142 | * Create module after imports are initialized 143 | */ 144 | injector.createAndResolve(provider, providers); 145 | /** 146 | * Check duplicates 147 | */ 148 | doModulesDuplicationCheck(modules, metadata, provider); 149 | /** 150 | * Add module to list 151 | */ 152 | modules.push({ 153 | injector, 154 | provider, 155 | name: metadata.name 156 | }); 157 | 158 | return modules; 159 | } 160 | 161 | 162 | /** 163 | * @since 1.0.0 164 | * @function 165 | * @name fireRequest 166 | * @param {Array} modules list of all modules bootstrapped 167 | * @param {IncomingMessage} request event emitter 168 | * @param {ServerResponse} response event emitter 169 | * @return {string|Buffer} data from controller 170 | * 171 | * @description 172 | * Use fireRequest to process request itself, this function is used by http/https server or 173 | * You can fire fake request 174 | */ 175 | export function fireRequest(modules: Array, 176 | request: IncomingMessage, 177 | response: ServerResponse): Promise { 178 | 179 | let rootInjector: Injector = getModule(modules).injector; 180 | let logger: Logger = rootInjector.get(Logger); 181 | /** 182 | * Create HttpRequestResolver injector 183 | */ 184 | let routeResolverInjector = Injector.createAndResolveChild( 185 | rootInjector, 186 | HttpRequestResolver, 187 | [ 188 | {provide: "url", useValue: parse(request.url, true)}, 189 | {provide: "UUID", useValue: uuid()}, 190 | {provide: "data", useValue: []}, 191 | {provide: "contentType", useValue: "text/html"}, 192 | {provide: "statusCode", useValue: Status.OK}, 193 | {provide: "request", useValue: request}, 194 | {provide: "response", useValue: response}, 195 | {provide: "modules", useValue: modules}, 196 | EventEmitter 197 | ] 198 | ); 199 | /** 200 | * Get HttpRequestResolver instance 201 | */ 202 | let rRouteResolver: HttpRequestResolver = routeResolverInjector.get(HttpRequestResolver); 203 | 204 | /** 205 | * On finish destroy injector 206 | */ 207 | response.on("finish", () => routeResolverInjector.destroy()); 208 | 209 | return rRouteResolver 210 | .process() 211 | .catch(error => 212 | logger.error("ControllerResolver.error", { 213 | stack: error.stack, 214 | url: request.url, 215 | error 216 | }) 217 | ); 218 | } 219 | -------------------------------------------------------------------------------- /src/server/http.ts: -------------------------------------------------------------------------------- 1 | import {Injector} from "../injector/injector"; 2 | import {createServer, IncomingMessage, Server, ServerResponse} from "http"; 3 | import {Logger} from "../logger/logger"; 4 | import {isArray, isFunction, isPresent, isString} from "../core"; 5 | import {BOOTSTRAP_MODULE, createModule, fireRequest, getModule} from "./bootstrap"; 6 | import {IModule, IModuleMetadata} from "../interfaces/imodule"; 7 | import {Metadata} from "../injector/metadata"; 8 | import * as WebSocket from "ws"; 9 | import {fireWebSocket, IWebSocketResult} from "./socket"; 10 | import {HttpError} from "../error"; 11 | 12 | const TYPEX_SOCKET_ID_HEADER = "__typeix_id"; 13 | 14 | /** 15 | * @since 2.0.0 16 | * @interface 17 | * 18 | * @description 19 | * Configuration options for the HTTP server 20 | */ 21 | export interface HttpOptions { 22 | /** 23 | * Port for the HTTP server to listen on 24 | */ 25 | port: number; 26 | /** 27 | * Hostname for the HTTP server to listen on 28 | */ 29 | hostname?: string; 30 | /** 31 | * If enabled the server will also configure WebSockets 32 | */ 33 | enableWebSockets?: boolean; 34 | } 35 | 36 | /** 37 | * @since 1.0.0 38 | * @function 39 | * @name httpServer 40 | * @param {Function} Class Root application module to bootstrap 41 | * @param {HttpOptions} options Additional HTTP Server options 42 | * @returns {Injector} 43 | * 44 | * @description 45 | * Run the HTTP server for a given root module. 46 | */ 47 | export function httpServer(Class: Function, options: HttpOptions): Array { 48 | let metadata: IModuleMetadata = Metadata.getComponentConfig(Class); 49 | // override bootstrap module 50 | metadata.name = BOOTSTRAP_MODULE; 51 | // set module config 52 | Metadata.setComponentConfig(Class, metadata); 53 | 54 | let modules: Array = createModule(Class); 55 | let injector = getModule(modules).injector; 56 | let logger: Logger = injector.get(Logger); 57 | let server = createServer(); 58 | 59 | server.on("request", 60 | (request: IncomingMessage, response: ServerResponse) => 61 | fireRequest(modules, request, response) 62 | ); 63 | 64 | if (isString(options.hostname)) { 65 | server.listen(options.port, options.hostname); 66 | } else { 67 | server.listen(options.port); 68 | } 69 | 70 | logger.info("Module.info: Server started", options); 71 | server.on("error", (e) => logger.error(e.stack)); 72 | 73 | if (options.enableWebSockets) { 74 | configureAndStartWebSockets(modules, logger, server); 75 | } 76 | 77 | return modules; 78 | } 79 | 80 | /** 81 | * @since 2.0.0 82 | * @function 83 | * @param {Array} modules The list of bootstrapped modules 84 | * @param {Logger} logger The logger instance 85 | * @param {"http".Server} server Configured HTTP server 86 | * 87 | * @description 88 | * Configures and starts the WebSockets extensions 89 | */ 90 | function configureAndStartWebSockets(modules: Array, logger: Logger, server: Server) { 91 | const socketResultMap: Map = new Map(); 92 | const wss = new WebSocket.Server({ 93 | server, 94 | verifyClient: (info, cb) => { 95 | fireWebSocket(modules, info.req) 96 | .then(result => { 97 | if (result.error || !isFunction(result.open)) { 98 | if (result.error instanceof HttpError) { 99 | cb(false, result.error.getCode(), result.error.getMessage()); 100 | } else { 101 | cb(false); 102 | } 103 | } 104 | 105 | info.req.headers[TYPEX_SOCKET_ID_HEADER] = result.uuid; 106 | socketResultMap.set(result.uuid, result); 107 | 108 | cb(true); 109 | }) 110 | .catch(error => { 111 | logger.error("WSS.verifyClient: Verification failed", {error, info}); 112 | cb(false); 113 | }); 114 | } 115 | }); 116 | 117 | wss.on("connection", (ws: WebSocket, request: IncomingMessage) => { 118 | logger.info("WSS.info: Socket connected", {url: request.url}); 119 | 120 | const idFromHeader = request.headers[TYPEX_SOCKET_ID_HEADER]; 121 | if (!isPresent(idFromHeader) || isArray(idFromHeader) || !socketResultMap.has( idFromHeader)) { 122 | ws.close(); 123 | } else { 124 | const socketResult = socketResultMap.get( idFromHeader); 125 | 126 | const cleanup = () => { 127 | socketResult.finished(); 128 | socketResultMap.delete( idFromHeader); 129 | }; 130 | ws.on("close", cleanup); 131 | ws.on("error", cleanup); 132 | 133 | return socketResult.open(ws); 134 | } 135 | }); 136 | wss.on("error", (e) => logger.error(e.stack)); 137 | } 138 | -------------------------------------------------------------------------------- /src/server/index.ts: -------------------------------------------------------------------------------- 1 | export {httpServer, HttpOptions} from "./http"; 2 | export {getModule} from "./bootstrap"; 3 | export {BaseRequest, Request} from "./request"; 4 | export {Socket} from "./socket"; 5 | export {Status} from "./status-code"; 6 | export {fakeHttpServer, fakeControllerActionCall, FakeServerApi, FakeResponseApi, FakeWebSocketApi, IFakeServerConfig} from "./mocks"; 7 | -------------------------------------------------------------------------------- /src/server/request.ts: -------------------------------------------------------------------------------- 1 | import {isDate, isNumber, isPresent, isString, isTruthy} from "../core"; 2 | import {Inject, Injectable} from "../decorators"; 3 | import {IResolvedRoute} from "../interfaces"; 4 | import {MultiPart, MultiPartField, MultiPartFile} from "../parsers"; 5 | import {Status} from "./status-code"; 6 | import {Methods} from "../router"; 7 | import {IConnection} from "../interfaces/iconnection"; 8 | import {IncomingMessage} from "http"; 9 | import {ControllerResolver} from "./controller-resolver"; 10 | 11 | /** 12 | * Cookie parse regex 13 | * @type {RegExp} 14 | */ 15 | const COOKIE_PARSE_REGEX = /(\w+[^=]+)=([^;]+)/g; 16 | 17 | export abstract class AbstractRequest { 18 | /** 19 | * @param cookies 20 | * @description 21 | * Cookies object are stored to this object first time they are parsed 22 | */ 23 | private cookies: { [key: string]: string }; 24 | 25 | /** 26 | * @since 1.0.0 27 | * @function 28 | * @name Request#getConnection 29 | * 30 | * @description 31 | * Get connection data 32 | */ 33 | getConnection(): IConnection { 34 | const request = this.getIncomingMessage(); 35 | return { 36 | uuid: this.getId(), 37 | method: request.method, 38 | url: request.url, 39 | httpVersion: request.httpVersion, 40 | httpVersionMajor: request.httpVersionMajor, 41 | httpVersionMinor: request.httpVersionMinor, 42 | remoteAddress: request.connection.remoteAddress, 43 | remoteFamily: request.connection.remoteFamily, 44 | remotePort: request.connection.remotePort, 45 | localAddress: request.connection.localAddress, 46 | localPort: request.connection.localPort 47 | }; 48 | } 49 | 50 | /** 51 | * @since 1.0.0 52 | * @function 53 | * @name Request#getCookies 54 | * 55 | * @description 56 | * Return parsed cookies 57 | */ 58 | getCookies(): { [key: string]: string } { 59 | 60 | if (isPresent(this.cookies)) { 61 | return this.cookies; 62 | } 63 | // get cookie string 64 | let cookie: string = this.getRequestHeader("Cookie"); 65 | 66 | if (isPresent(cookie)) { 67 | 68 | this.cookies = {}; 69 | 70 | // parse cookies 71 | cookie.match(COOKIE_PARSE_REGEX) 72 | .map(item => item.split(COOKIE_PARSE_REGEX).slice(1, -1)) 73 | .map(item => { 74 | return { 75 | key: item.shift(), 76 | value: item.shift() 77 | }; 78 | }) 79 | .forEach(item => { 80 | this.cookies[item.key] = item.value; 81 | }); 82 | 83 | return this.cookies; 84 | } 85 | 86 | return {}; 87 | } 88 | 89 | /** 90 | * @since 1.0.0 91 | * @function 92 | * @name Request#getCookie 93 | * 94 | * @description 95 | * Return request headers 96 | */ 97 | getCookie(name: string): string { 98 | let cookies = this.getCookies(); 99 | return cookies[name]; 100 | } 101 | 102 | /** 103 | * @since 1.0.0 104 | * @function 105 | * @name Request#getRequestHeaders 106 | * 107 | * @description 108 | * Return request headers 109 | */ 110 | getRequestHeaders(): any { 111 | return this.getIncomingMessage().headers; 112 | } 113 | 114 | /** 115 | * @since 1.0.0 116 | * @function 117 | * @name Request#getRequestHeader 118 | * 119 | * @description 120 | * Return request header by name 121 | */ 122 | getRequestHeader(name: string): any { 123 | let requestHeaders = this.getRequestHeaders(); 124 | let headers = isPresent(requestHeaders) ? requestHeaders : {}; 125 | return headers[name.toLocaleLowerCase()]; 126 | } 127 | 128 | /** 129 | * @since 1.0.0 130 | * @function 131 | * @name Request#getParams 132 | * 133 | * @description 134 | * Get all request parameters 135 | */ 136 | getParams(): Object { 137 | return isPresent(this.getResolvedRoute().params) ? this.getResolvedRoute().params : {}; 138 | } 139 | 140 | /** 141 | * @since 1.0.0 142 | * @function 143 | * @name Request#getParam 144 | * @param {string} name 145 | * 146 | * @description 147 | * Get resolve route param 148 | */ 149 | getParam(name: string): string { 150 | let params = this.getParams(); 151 | return params[name]; 152 | } 153 | 154 | /** 155 | * @since 1.0.0 156 | * @function 157 | * @name Request#getMethod 158 | * 159 | * @description 160 | * Return resolved route method 161 | */ 162 | getMethod(): Methods { 163 | return this.getResolvedRoute().method; 164 | } 165 | 166 | /** 167 | * @since 1.0.0 168 | * @function 169 | * @name Request#getRoute 170 | * 171 | * @description 172 | * Return resolved route name 173 | */ 174 | getRoute(): string { 175 | return this.getResolvedRoute().route; 176 | } 177 | 178 | /** 179 | * @since 1.0.0 180 | * @function 181 | * @name Request#getBody 182 | * @private 183 | * 184 | * @description 185 | * Get request body if present only on POST, PUT, PATCH 186 | */ 187 | getBody(): Buffer { 188 | return Buffer.concat(this.getRawData()); 189 | } 190 | 191 | /** 192 | * @since 1.0.0 193 | * @function 194 | * @name Request#getBodyAsMultiPart 195 | * @param {string} encoding 196 | * 197 | * @description 198 | * Get request body as multipart if present only on POST, PUT, PATCH 199 | */ 200 | getBodyAsMultiPart(encoding = "utf8"): Array { 201 | let buffer: Buffer = this.getBody(); 202 | let contentType = this.getRequestHeader("content-type"); 203 | let parser = new MultiPart(contentType, encoding); 204 | return parser.parse(buffer); 205 | } 206 | 207 | abstract getId(): string; 208 | 209 | protected abstract getIncomingMessage(): IncomingMessage; 210 | 211 | protected abstract getResolvedRoute(): IResolvedRoute; 212 | 213 | protected abstract getRawData(): Array; 214 | } 215 | 216 | /** 217 | * @since 2.0.0 218 | * @class 219 | * @name BaseRequest 220 | * @constructor 221 | * @description 222 | * Provides a basic API to the original request - does only support retrieving information. 223 | */ 224 | @Injectable() 225 | export class BaseRequest extends AbstractRequest { 226 | @Inject("request") 227 | private readonly incomingMessage: IncomingMessage; 228 | 229 | @Inject("UUID") 230 | private readonly id: string; 231 | 232 | @Inject("data") 233 | private readonly data: Array; 234 | 235 | @Inject("resolvedRoute") 236 | private readonly resolvedRoute: IResolvedRoute; 237 | 238 | getId(): string { 239 | return this.id; 240 | } 241 | 242 | protected getIncomingMessage(): IncomingMessage { 243 | return this.incomingMessage; 244 | } 245 | 246 | protected getResolvedRoute(): IResolvedRoute { 247 | return this.resolvedRoute; 248 | } 249 | 250 | protected getRawData(): Array { 251 | return this.data; 252 | } 253 | } 254 | 255 | /** 256 | * @since 1.0.0 257 | * @class 258 | * @name Request 259 | * @constructor 260 | * @description 261 | * Get request reflection to limit public api 262 | */ 263 | @Injectable() 264 | export class Request extends AbstractRequest { 265 | /** 266 | * @param ControllerResolver 267 | * @description 268 | * Current internal ControllerResolver instance 269 | */ 270 | @Inject("controllerResolver") 271 | private readonly controllerResolver: ControllerResolver; 272 | 273 | /** 274 | * @since 1.0.0 275 | * @function 276 | * @name Request#onDestroy 277 | * 278 | * @description 279 | * Add destroy event to public api 280 | */ 281 | onDestroy(callback: (...args: any[]) => void): void { 282 | this.controllerResolver.getEventEmitter().once("destroy", callback); 283 | } 284 | 285 | /** 286 | * @since 0.0.1 287 | * @function 288 | * @name Request#setResponseCookie 289 | * @param {String} key cookie name 290 | * @param {String} value cookie value 291 | * @param {String|Object|Number} expires expire date 292 | * @param {String} path cookie path 293 | * @param {String} domain cookie domain 294 | * @param {Boolean} isHttpOnly is http only 295 | * @description 296 | * Sets an cookie header 297 | */ 298 | setCookie(key: string, value: string, expires?: number | Date | string, path?: string, domain?: string, isHttpOnly?: boolean) { 299 | 300 | let cookie = key + "=" + value; 301 | 302 | if (isPresent(expires) && isNumber(expires)) { 303 | let date: Date = new Date(); 304 | date.setTime(date.getTime() + ( expires)); 305 | cookie += "; Expires="; 306 | cookie += date.toUTCString(); 307 | } else if (isPresent(expires) && isString(expires)) { 308 | cookie += "; Expires=" + expires; 309 | } else if (isPresent(expires) && isDate(expires)) { 310 | cookie += "; Expires="; 311 | cookie += ( expires).toUTCString(); 312 | } 313 | 314 | if (isPresent(path)) { 315 | cookie += "; Path=" + path; 316 | } 317 | if (isPresent(domain)) { 318 | cookie += "; Domain=" + domain; 319 | } 320 | if (isTruthy(isHttpOnly)) { 321 | cookie += "; HttpOnly"; 322 | } 323 | this.setResponseHeader("Set-cookie", cookie); 324 | 325 | } 326 | 327 | /** 328 | * @since 1.0.0 329 | * @function 330 | * @name Request#setResponseHeader 331 | * @param {String} name 332 | * @param {String} value 333 | * 334 | * @description 335 | * Set response header 336 | */ 337 | setResponseHeader(name: string, value: string | string[]): void { 338 | this.controllerResolver.getServerResponse().setHeader(name, value); 339 | } 340 | 341 | /** 342 | * @since 1.0.0 343 | * @function 344 | * @name Request#setContentType 345 | * @param {String} value 346 | * 347 | * @description 348 | * Set response content type 349 | */ 350 | setContentType(value: string) { 351 | this.controllerResolver.getEventEmitter().emit("contentType", value); 352 | } 353 | 354 | /** 355 | * @since 1.0.0 356 | * @function 357 | * @name Request#setStatusCode 358 | * 359 | * @description 360 | * Set status code 361 | */ 362 | setStatusCode(code: Status | number) { 363 | this.controllerResolver.getEventEmitter().emit("statusCode", code); 364 | } 365 | 366 | /** 367 | * @since 1.0.0 368 | * @function 369 | * @name Request#stopChain 370 | * 371 | * @description 372 | * Stops action chain 373 | */ 374 | stopChain() { 375 | this.controllerResolver.stopChain(); 376 | } 377 | 378 | /** 379 | * @since 1.0.0 380 | * @function 381 | * @name Request#redirectTo 382 | * 383 | * @description 384 | * Stops action chain 385 | */ 386 | redirectTo(url: string, code: Status | number) { 387 | this.stopChain(); 388 | this.controllerResolver.getEventEmitter().emit("redirectTo", { 389 | code, url 390 | }); 391 | } 392 | 393 | getId(): string { 394 | return this.controllerResolver.getId(); 395 | } 396 | 397 | protected getIncomingMessage(): IncomingMessage { 398 | return this.controllerResolver.getIncomingMessage(); 399 | } 400 | 401 | protected getResolvedRoute(): IResolvedRoute { 402 | return this.controllerResolver.getResolvedRoute(); 403 | } 404 | 405 | protected getRawData(): Array { 406 | return this.controllerResolver.getBody(); 407 | } 408 | } 409 | -------------------------------------------------------------------------------- /src/server/socket-resolver.ts: -------------------------------------------------------------------------------- 1 | import {IncomingMessage} from "http"; 2 | import {isFunction, isPresent} from "../core"; 3 | import {Logger} from "../logger/logger"; 4 | import {Injector} from "../injector/injector"; 5 | import {IControllerMetadata, IProvider, IResolvedRoute} from "../interfaces"; 6 | import {EventEmitter} from "events"; 7 | import {Inject, Injectable} from "../decorators"; 8 | import {FUNCTION_KEYS, FUNCTION_PARAMS, Metadata} from "../injector/metadata"; 9 | import {IParam} from "../interfaces/iparam"; 10 | import {BaseRequest} from "./request"; 11 | import * as WebSocket from "ws"; 12 | import {IAction} from "../interfaces/iaction"; 13 | import {ControllerResolver} from "./controller-resolver"; 14 | import {Socket} from "./socket"; 15 | 16 | /** 17 | * @since 2.0.0 18 | * @private 19 | * @class 20 | * @name SocketResolver 21 | * @constructor 22 | * 23 | * @description 24 | * This component is responsible for finding and instantiating the correctly annotated {@link WebSocket} class 25 | * in the application. It is used internally by the framework. 26 | */ 27 | @Injectable() 28 | export class SocketResolver { 29 | 30 | /** 31 | * The original incoming upgrade request 32 | */ 33 | @Inject("request") 34 | private request: IncomingMessage; 35 | 36 | /** 37 | * Data received by client on initial POST, PATCH, PUT requests 38 | */ 39 | @Inject("data") 40 | private data: Array; 41 | 42 | /** 43 | * UUID identifier of request 44 | */ 45 | @Inject("UUID") 46 | private id: string; 47 | 48 | /** 49 | * The designated {@link WebSocket} provider 50 | */ 51 | @Inject("socketProvider") 52 | private socketProvider: IProvider; 53 | 54 | /** 55 | * Resolved route from injector 56 | */ 57 | @Inject("resolvedRoute") 58 | private resolvedRoute: IResolvedRoute; 59 | 60 | /** 61 | * Injector which created request 62 | */ 63 | @Inject(Injector) 64 | private injector: Injector; 65 | 66 | /** 67 | * Provided by injector 68 | */ 69 | @Inject(Logger) 70 | private logger: Logger; 71 | 72 | /** 73 | * Responsible for handling events 74 | */ 75 | @Inject(EventEmitter) 76 | private eventEmitter: EventEmitter; 77 | 78 | /** 79 | * Created socket instance 80 | */ 81 | private socket: any; 82 | 83 | /** 84 | * @since 2.0.0 85 | * @private 86 | * @function 87 | * @name SocketResolver#getEventEmitter 88 | * 89 | * @description 90 | * Get request event emitter 91 | */ 92 | getEventEmitter(): EventEmitter { 93 | return this.eventEmitter; 94 | } 95 | 96 | /** 97 | * @since 2.0.0 98 | * @private 99 | * @function 100 | * @name SocketResolver#getIncomingMessage 101 | * 102 | * @description 103 | * Get underlying upgrade request 104 | */ 105 | getIncomingMessage(): IncomingMessage { 106 | return this.request; 107 | } 108 | 109 | /** 110 | * @since 2.0.0 111 | * @private 112 | * @function 113 | * @name SocketResolver#getResolvedRoute 114 | * 115 | * @description 116 | * Get originally resolved route 117 | */ 118 | getResolvedRoute(): IResolvedRoute { 119 | return this.resolvedRoute; 120 | } 121 | 122 | /** 123 | * @since 2.0.0 124 | * @private 125 | * @function 126 | * @name SocketResolver#getId 127 | * 128 | * @description 129 | * Get unique request ID 130 | */ 131 | getId(): string { 132 | return this.id; 133 | } 134 | 135 | /** 136 | * @since 2.0.0 137 | * @private 138 | * @function 139 | * @name SocketResolver#getBody 140 | * 141 | * @description 142 | * Get the data sent with the upgrade request 143 | */ 144 | getBody(): Array { 145 | return this.data; 146 | } 147 | 148 | /** 149 | * @since 2.0.0 150 | * @private 151 | * @function 152 | * @name SocketResolver#destroy 153 | * 154 | * @description 155 | * Destroy all references to free memory 156 | */ 157 | destroy() { 158 | if (isPresent(this.socket) && isFunction(this.socket.afterClose)) { 159 | this.socket.afterClose(); 160 | } 161 | 162 | this.eventEmitter.emit("destroy"); 163 | this.eventEmitter.removeAllListeners(); 164 | this.socket = null; 165 | } 166 | 167 | /** 168 | * @since 2.0.0 169 | * @private 170 | * @function 171 | * @name SocketResolver#process 172 | * @return {Promise} Promise to resolve when the socket was created and verified successfully 173 | * 174 | * @description 175 | * Process request logic by creating and verifying the socket 176 | */ 177 | async process(): Promise { 178 | // set request reflection 179 | const reflectionInjector = Injector.createAndResolveChild(this.injector, BaseRequest, [ 180 | {provide: "data", useValue: this.data}, 181 | {provide: "request", useValue: this.request}, 182 | {provide: "UUID", useValue: this.id}, 183 | {provide: "resolvedRoute", useValue: this.resolvedRoute} 184 | ]); 185 | 186 | const metadata: IControllerMetadata = Metadata.getComponentConfig(this.socketProvider.provide); 187 | const providers: Array = Metadata.verifyProviders(metadata.providers); 188 | // limit socket api 189 | const limitApi = ["request", "response", "modules"]; 190 | limitApi.forEach(item => providers.push({provide: item, useValue: {}})); 191 | 192 | const socketInjector = Injector.createAndResolveChild( 193 | reflectionInjector, 194 | this.socketProvider, 195 | Metadata.verifyProviders(providers) 196 | ); 197 | 198 | this.socket = socketInjector.get(this.socketProvider.provide); 199 | 200 | await this.processSocketHook("verify"); 201 | return this; 202 | } 203 | 204 | /** 205 | * @sicne 2.0.0 206 | * @function 207 | * @name SocketResolver#openSocket 208 | * @param {WebSocket} ws The "ws".WebSocket representing the real socket 209 | * @return {Promise} Promise to resolve when the socket has been opened successfully 210 | * 211 | * @description 212 | * Tries to call the open method on the created {@link WebSocket} and sets up message listeners. 213 | */ 214 | async openSocket(ws: WebSocket): Promise { 215 | this.injector.set(Socket, new Socket(ws)); 216 | 217 | ws.on("message", (data: WebSocket.Data) => { 218 | this.injector.set("message", data); 219 | this.processSocketHook("message"); 220 | }); 221 | 222 | this.processSocketHook("open"); 223 | } 224 | 225 | /** 226 | * @since 2.0.0 227 | * @function 228 | * @name SocketResolver#processSocketHook 229 | * @param {string} actionName Name of the {@link Hook} to invoke 230 | * @return {Promise} Promise resolved when the hook had been invoked successfully 231 | * 232 | * @description 233 | * Tries to invoke the {@link Hook} for the given name on the created socket. 234 | */ 235 | private async processSocketHook(actionName: string) { 236 | const action = this.getMappedHook(this.socketProvider, actionName); 237 | 238 | if (!isPresent(action) || !isFunction(this.socket[action.key])) { 239 | this.logger.debug("Tried to call socket @Hook but not available", { 240 | hook: actionName 241 | }); 242 | return; 243 | } 244 | 245 | const func: Function = this.socket[action.key].bind(this.socket); 246 | const args = this.getAnnotatedArguments(this.socketProvider, action.key); 247 | 248 | return await func.apply(this.socket, args); 249 | } 250 | 251 | /** 252 | * @since 2.0.0 253 | * @private 254 | * @function 255 | * @name SocketResolver#getMappedHook 256 | * @param {IProvider} socketProvider The provider of the socket 257 | * @param {String} actionName The hook name to call 258 | * @return {IAction} The action representing the hook 259 | * 260 | * @description 261 | * Investigates the metadata of the given provider to find the action representing the hook with the given name. 262 | */ 263 | private getMappedHook(socketProvider: IProvider, actionName: String): IAction { 264 | // get mappings from controller 265 | let mappings = Metadata 266 | .getMetadata(this.socketProvider.provide.prototype, FUNCTION_KEYS) 267 | .filter((item: IAction) => 268 | item.type === "Hook" && item.value === actionName && 269 | ControllerResolver.isControllerInherited(this.socketProvider.provide, item.proto) 270 | ); 271 | 272 | let mappedAction; 273 | // search mapped on current controller 274 | if (mappings.length > 0) { 275 | mappedAction = mappings.find(item => item.className === Metadata.getName(socketProvider.provide)); 276 | } 277 | // get first parent one from inheritance 278 | if (!isPresent(mappedAction)) { 279 | mappedAction = mappings.pop(); 280 | } 281 | return mappedAction; 282 | } 283 | 284 | /** 285 | * @since 2.0.0 286 | * @private 287 | * @function 288 | * @name SocketResolver#getAnnotatedArguments 289 | * @param {IProvider} provider The socket provider 290 | * @param {string} functionName The name of the function to get argument information for 291 | * @return {Array} The values to use as arguments 292 | * 293 | * @description 294 | * Inspects the given function of the provider and returns the correct argument values 295 | */ 296 | private getAnnotatedArguments(provider: IProvider, functionName: string): Array { 297 | const params: Array = Metadata 298 | .getMetadata(provider.provide.prototype, FUNCTION_PARAMS) 299 | .filter((param: IParam) => param.key === functionName); 300 | 301 | return params 302 | .sort((p1, p2) => p1.paramIndex - p2.paramIndex) 303 | .map((param: IParam) => { 304 | switch (param.type) { 305 | case "Param": 306 | if (isPresent(this.resolvedRoute.params) && this.resolvedRoute.params.hasOwnProperty(param.value)) { 307 | return this.resolvedRoute.params[param.value]; 308 | } else { 309 | return null; 310 | } 311 | case "Inject": 312 | return this.injector.get(param.value); 313 | } 314 | }); 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /src/server/socket.ts: -------------------------------------------------------------------------------- 1 | import {IncomingMessage} from "http"; 2 | import {IModule} from "../interfaces/imodule"; 3 | import {Logger} from "../logger/logger"; 4 | import {SocketRequestResolver} from "./request-resolver"; 5 | import {isPresent, uuid} from "../core"; 6 | import {Injector} from "../injector/injector"; 7 | import {Status} from "./status-code"; 8 | import {getModule} from "./bootstrap"; 9 | import {parse} from "url"; 10 | import {EventEmitter} from "events"; 11 | import {HttpError} from "../error"; 12 | import {SocketResolver} from "./socket-resolver"; 13 | import * as WebSocket from "ws"; 14 | 15 | export interface IWebSocketResult { 16 | uuid: string; 17 | error?: any; 18 | open?: (ws: WebSocket) => Promise; 19 | finished: () => void; 20 | } 21 | 22 | /** 23 | * @since 2.0.0 24 | * @class 25 | * @constructor 26 | * @name Socket 27 | * 28 | * @description 29 | * Basic API for accessing a WebSocket in order to get state information, send data, or close the socket. 30 | */ 31 | export class Socket { 32 | 33 | /** 34 | * @since 2.0.0 35 | * @param {WebSocket} ws Underlying WebSocket to wrap 36 | * 37 | * @description 38 | * Creates a new API wrapping the given raw WebSocket 39 | */ 40 | constructor(private readonly ws: WebSocket) { 41 | } 42 | 43 | /** 44 | * @since 2.0.0 45 | * @function 46 | * @name Socket#getReadyState 47 | * @return {number} Socket readyState 48 | * 49 | * @description 50 | * Get the underlying WebSocket's readyState (see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket#Ready_state_constants) 51 | */ 52 | getReadyState(): number { 53 | return this.ws.readyState; 54 | } 55 | 56 | /** 57 | * @since 2.0.0 58 | * @function 59 | * @name Socket#close 60 | * @param {number} status Status code to send as close reason 61 | * @param {string} data Data to send as close reason 62 | * 63 | * @description 64 | * Closes the underlying WebSocket 65 | */ 66 | close(status?: number, data?: string): void { 67 | this.ws.close(status, data); 68 | } 69 | 70 | /** 71 | * @since 2.0.0 72 | * @function 73 | * @name Socket#send 74 | * @param data Data to send to the client 75 | * @param {{mask?: boolean; binary?: boolean}|(err: Error) => void} options Either send options or the error callback 76 | * @param {(err: Error) => void} cb (only if second parameter are options) error callback 77 | */ 78 | send(data: any, options?: { mask?: boolean; binary?: boolean } | ((err: Error) => void), cb?: (err: Error) => void): void { 79 | this.ws.send(data, options as any, cb); 80 | } 81 | 82 | } 83 | 84 | /** 85 | * @since 2.0.0 86 | * @function 87 | * @name fireWebSocket 88 | * @param {Array} modules Bootstrapped modules 89 | * @param {"http".IncomingMessage} request Incoming HTTP request 90 | * @return {Promise} Promise which will resolve once the socket has been verified or reject on failure 91 | * 92 | * @description 93 | * This method will trigger a WebSocket resolution and will try to find a matching {@link WebSocket} registered in the system. 94 | * Typically you do not use this method directly but it is called automatically by the framework. For testing best 95 | * refer to {@link FakeServerApi#createSocket}. 96 | */ 97 | export function fireWebSocket(modules: Array, request: IncomingMessage): Promise { 98 | let rootInjector: Injector = getModule(modules).injector; 99 | let logger: Logger = rootInjector.get(Logger); 100 | 101 | const requestUuid = uuid(); 102 | 103 | /** 104 | * Create SocketRequestResolver injector 105 | */ 106 | let socketResolverInjector = Injector.createAndResolveChild( 107 | rootInjector, 108 | SocketRequestResolver, 109 | [ 110 | {provide: "url", useValue: parse(request.url, true)}, 111 | {provide: "UUID", useValue: requestUuid}, 112 | {provide: "data", useValue: []}, 113 | {provide: "request", useValue: request}, 114 | {provide: "modules", useValue: modules}, 115 | EventEmitter 116 | ] 117 | ); 118 | 119 | const socketResolver: SocketRequestResolver = socketResolverInjector.get(SocketRequestResolver); 120 | logger.debug("Resolved SocketRequestResolver - starting processing", {request}); 121 | 122 | return socketResolver 123 | .process() 124 | .then((socket: SocketResolver) => { 125 | logger.debug("socketResolver.then result:", {socket}); 126 | if (!isPresent(socket)) { 127 | throw new HttpError(Status.Internal_Server_Error, "Could not resolve socket"); 128 | } else { 129 | const result: IWebSocketResult = { 130 | uuid: requestUuid, 131 | open: (ws: WebSocket) => { 132 | logger.info("Resuming request", {uuid: requestUuid}); 133 | return socket.openSocket(ws); 134 | }, 135 | finished: () => { 136 | socket.destroy(); 137 | socketResolverInjector.destroy(); 138 | } 139 | }; 140 | return result; 141 | } 142 | }) 143 | .catch((error) => { 144 | logger.error("fireWebSocket: Failed to create socket", {error}); 145 | return { 146 | uuid: requestUuid, 147 | error: error, 148 | finished: () => { 149 | socketResolverInjector.destroy(); 150 | } 151 | }; 152 | }); 153 | } 154 | -------------------------------------------------------------------------------- /src/server/status-code.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 1.0.0 3 | * @interface 4 | * @name Redirect 5 | * @description 6 | * Status enums 7 | */ 8 | export interface IRedirect { 9 | url: string; 10 | code: Status; 11 | } 12 | /** 13 | * @since 1.0.0 14 | * @enum 15 | * @name Redirect 16 | * @description 17 | * Status enums 18 | */ 19 | export enum Status { 20 | // 1xx Informational 21 | Continue = 100, 22 | Switching_Protocols = 101, 23 | Processing = 102, 24 | // 2xx Success 25 | OK = 200, 26 | Created = 201, 27 | Accepted = 202, 28 | Non_Authoritative_Information = 203, 29 | No_Content = 204, 30 | Reset_Content = 205, 31 | Partial_Content = 206, 32 | Multi_Status = 207, 33 | Already_Reported = 208, 34 | IM_Used = 226, 35 | // 3xx Redirection 36 | Multiple_Choices = 300, 37 | Moved_Permanently = 301, 38 | Found = 302, 39 | See_Other = 303, 40 | Not_Modified = 304, 41 | Use_Proxy = 305, 42 | Switch_Proxy = 306, 43 | Temporary_Redirect = 307, 44 | Permanent_Redirect = 308, 45 | // 4xx Client ErrorMessage 46 | Bad_Request = 400, 47 | Unauthorized = 401, 48 | Payment_Required = 402, 49 | Forbidden = 403, 50 | Not_Found = 404, 51 | Method_Not_Allowed = 405, 52 | Not_Acceptable = 407, 53 | Proxy_Authentication_Required = 408, 54 | Conflict = 409, 55 | Gone = 410, 56 | Length_Required = 411, 57 | Precondition_Failed = 412, 58 | Payload_Too_Large = 413, 59 | URI_Too_Long = 414, 60 | Unsupported_Media_Type = 415, 61 | Range_Not_Satisfiable = 416, 62 | Expectation_Failed = 417, 63 | Im_a_teapot = 418, 64 | Misdirected_Request = 421, 65 | Unprocessable_Entity = 422, 66 | Locked = 423, 67 | Failed_Dependency = 424, 68 | Upgrade_Required = 426, 69 | Precondition_Required = 428, 70 | Too_Many_Requests = 429, 71 | Request_Header_Fields_Too_Large = 431, 72 | Unavailable_For_Legal_Reasons = 451, 73 | // 5xx Server ErrorMessage 74 | Internal_Server_Error = 500, 75 | Not_Implemented = 501, 76 | Bad_Gateway = 502, 77 | Service_Unavailable = 503, 78 | Gateway_Time_out = 504, 79 | HTTP_Version_Not_Supported = 505, 80 | Variant_Also_Negotiates = 506, 81 | Insufficient_Storage = 507, 82 | Loop_Detected = 508, 83 | Not_Extended = 510, 84 | Network_Authentication_Required = 511 85 | } 86 | -------------------------------------------------------------------------------- /src/tests/core.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isString, isBoolean, isUndefined, isArray, isNull, isFunction, isDate, isRegExp, isObject, 3 | isPresent, isFalsy 4 | } from "../core"; 5 | import {isNumber} from "util"; 6 | import {assert} from "chai"; 7 | 8 | describe("Core functions", () => { 9 | it("Should be valid string", () => { 10 | assert.isTrue(isString("value")); 11 | assert.isFalse(isString(null)); 12 | }); 13 | 14 | it("Should be valid boolean", () => { 15 | assert.isTrue(isBoolean(true)); 16 | assert.isFalse(isBoolean(null)); 17 | }); 18 | 19 | it("Should be valid undefined", () => { 20 | assert.isTrue(isUndefined(undefined)); 21 | assert.isFalse(isUndefined(true)); 22 | assert.isFalse(isUndefined(null)); 23 | }); 24 | 25 | 26 | it("Should be valid number", () => { 27 | assert.isTrue(isNumber(1)); 28 | assert.isTrue(isNumber(NaN)); 29 | assert.isFalse(isNumber(undefined)); 30 | assert.isFalse(isNumber(true)); 31 | assert.isFalse(isNumber(null)); 32 | }); 33 | 34 | it("Should be valid array", () => { 35 | assert.isTrue(isArray([])); 36 | assert.isFalse(isArray({})); 37 | assert.isFalse(isArray(1)); 38 | assert.isFalse(isArray(NaN)); 39 | assert.isFalse(isArray(undefined)); 40 | assert.isFalse(isArray(true)); 41 | assert.isFalse(isArray(null)); 42 | }); 43 | 44 | it("Should be valid null", () => { 45 | assert.isFalse(isNull([])); 46 | assert.isFalse(isNull({})); 47 | assert.isFalse(isNull(1)); 48 | assert.isFalse(isNull(NaN)); 49 | assert.isFalse(isNull(undefined)); 50 | assert.isFalse(isNull(true)); 51 | assert.isTrue(isNull(null)); 52 | }); 53 | 54 | 55 | it("Should be valid function", () => { 56 | assert.isTrue(isFunction(Array)); 57 | assert.isFalse(isFunction([])); 58 | assert.isFalse(isFunction({})); 59 | assert.isFalse(isFunction(1)); 60 | assert.isFalse(isFunction(NaN)); 61 | assert.isFalse(isFunction(undefined)); 62 | assert.isFalse(isFunction(true)); 63 | assert.isFalse(isFunction(null)); 64 | }); 65 | 66 | it("Should be valid date", () => { 67 | assert.isTrue(isDate(new Date)); 68 | assert.isFalse(isDate([])); 69 | assert.isFalse(isDate({})); 70 | assert.isFalse(isDate(1)); 71 | assert.isFalse(isDate(NaN)); 72 | assert.isFalse(isDate(undefined)); 73 | assert.isFalse(isDate(true)); 74 | assert.isFalse(isDate(null)); 75 | }); 76 | 77 | it("Should be valid regex", () => { 78 | assert.isTrue(isRegExp(new RegExp("abc"))); 79 | assert.isFalse(isRegExp([])); 80 | assert.isFalse(isRegExp({})); 81 | assert.isFalse(isRegExp(1)); 82 | assert.isFalse(isRegExp(NaN)); 83 | assert.isFalse(isRegExp(undefined)); 84 | assert.isFalse(isRegExp(true)); 85 | assert.isFalse(isRegExp(null)); 86 | }); 87 | 88 | it("Should be valid object", () => { 89 | assert.isTrue(isObject(new RegExp("abc"))); 90 | assert.isTrue(isObject([])); 91 | assert.isTrue(isObject({})); 92 | assert.isFalse(isObject(1)); 93 | assert.isFalse(isObject(NaN)); 94 | assert.isFalse(isObject(undefined)); 95 | assert.isFalse(isObject(true)); 96 | assert.isFalse(isObject(null)); 97 | }); 98 | 99 | it("Should be present", () => { 100 | assert.isTrue(isPresent(new RegExp("abc"))); 101 | assert.isTrue(isPresent([])); 102 | assert.isTrue(isPresent({})); 103 | assert.isTrue(isPresent(1)); 104 | assert.isTrue(isPresent(NaN)); 105 | assert.isFalse(isPresent(undefined)); 106 | assert.isTrue(isPresent(true)); 107 | assert.isFalse(isPresent(null)); 108 | }); 109 | 110 | 111 | it("Is Falsy", () => { 112 | assert.isTrue(isFalsy(null)); 113 | assert.isTrue(isFalsy(0)); 114 | assert.isTrue(isFalsy(false)); 115 | assert.isTrue(isFalsy("")); 116 | assert.isTrue(isFalsy(undefined)); 117 | assert.isTrue(isFalsy(NaN)); 118 | assert.isFalse(isFalsy([])); 119 | assert.isFalse(isFalsy(1)); 120 | assert.isFalse(isFalsy({})); 121 | assert.isFalse(isFalsy("abc")); 122 | }); 123 | 124 | }); 125 | -------------------------------------------------------------------------------- /src/tests/fake-request.ts: -------------------------------------------------------------------------------- 1 | import {assert, use} from "chai"; 2 | import * as sinonChai from "sinon-chai"; 3 | import {Methods, Router} from "../router"; 4 | import {Logger, LogLevels} from "../logger/logger"; 5 | import {Action, Before, Chain, Controller, ErrorMessage, Inject, Module} from "../decorators"; 6 | import {IAfterConstruct} from "../interfaces"; 7 | import {fakeHttpServer, FakeResponseApi, FakeServerApi, Request, Status} from "../server"; 8 | import {isEqual} from "../core"; 9 | import {HttpError} from "../error"; 10 | 11 | // use chai spies 12 | use(sinonChai); 13 | 14 | describe("fakeHttpServer", () => { 15 | 16 | let server: FakeServerApi; 17 | 18 | beforeEach(() => { 19 | 20 | @Controller({ 21 | name: "core" 22 | }) 23 | class MyController { 24 | 25 | @Inject(Request) 26 | private request: Request; 27 | 28 | 29 | @Action("error") 30 | actionError(@ErrorMessage message: HttpError) { 31 | return "ERROR=" + message.getMessage(); 32 | } 33 | 34 | 35 | @Action("fire") 36 | actionFireError() { 37 | throw new HttpError(500, "FIRE ERROR CASE"); 38 | } 39 | 40 | @Before("index") 41 | beforeIndex() { 42 | return "BEFORE"; 43 | } 44 | 45 | @Action("index") 46 | actionIndex(@Chain data) { 47 | return "VALUE <- " + data; 48 | } 49 | 50 | @Action("call") 51 | actionAjax() { 52 | return "CALL=" + this.request.getBody(); 53 | } 54 | 55 | @Action("redirect") 56 | actionRedirect() { 57 | return this.request.redirectTo("/mypage", Status.Temporary_Redirect); 58 | } 59 | } 60 | 61 | @Module({ 62 | name: "root", 63 | providers: [Logger, Router], 64 | controllers: [MyController] 65 | }) 66 | class MyModule implements IAfterConstruct { 67 | afterConstruct(): void { 68 | this.router.addRules([ 69 | { 70 | methods: [Methods.GET, Methods.OPTIONS, Methods.CONNECT, Methods.DELETE, Methods.HEAD, Methods.TRACE], 71 | url: "/", 72 | route: "core/index" 73 | }, 74 | { 75 | methods: [Methods.POST, Methods.PUT, Methods.PATCH], 76 | url: "/ajax/call", 77 | route: "core/call" 78 | }, 79 | { 80 | methods: [Methods.GET], 81 | url: "/redirect", 82 | route: "core/redirect" 83 | }, 84 | { 85 | methods: [Methods.GET], 86 | url: "/fire-error", 87 | route: "core/fire" 88 | } 89 | ]); 90 | this.router.setError("core/error"); 91 | this.logger.printToConsole(); 92 | this.logger.enable(); 93 | this.logger.setDebugLevel(LogLevels.BENCHMARK); 94 | } 95 | 96 | @Inject(Logger) 97 | private logger: Logger; 98 | 99 | @Inject(Router) 100 | private router: Router; 101 | } 102 | 103 | server = fakeHttpServer(MyModule); 104 | }); 105 | 106 | 107 | it("Should do GET redirect", (done) => { 108 | server.GET("/redirect").then((api: FakeResponseApi) => { 109 | assert.equal(api.getStatusCode(), 307); 110 | assert.isTrue(isEqual(api.getHeaders(), {"Location": "/mypage"})); 111 | done(); 112 | }).catch(done); 113 | }); 114 | 115 | it("Should do GET found error", (done) => { 116 | server.GET("/fire-error").then((api: FakeResponseApi) => { 117 | assert.isTrue(api.getBody().toString().indexOf("ERROR=FIRE ERROR CASE") > -1); 118 | assert.equal(api.getStatusCode(), 500); 119 | done(); 120 | }).catch(done); 121 | }); 122 | 123 | 124 | it("Should do GET not found", (done) => { 125 | server.GET("/abc").then((api: FakeResponseApi) => { 126 | assert.isTrue(api.getBody().toString().indexOf("ERROR=Router.parseRequest: /abc no route found, method: GET") > -1); 127 | assert.equal(api.getStatusCode(), 404); 128 | done(); 129 | }).catch(done); 130 | }); 131 | 132 | it("Should do GET index", (done) => { 133 | server.GET("/").then((api: FakeResponseApi) => { 134 | assert.equal(api.getBody().toString(), "VALUE <- BEFORE"); 135 | assert.equal(api.getStatusCode(), 200); 136 | done(); 137 | }).catch(done); 138 | }); 139 | 140 | it("Should do OPTIONS index", (done) => { 141 | server.OPTIONS("/").then((api: FakeResponseApi) => { 142 | assert.equal(api.getBody().toString(), "VALUE <- BEFORE"); 143 | assert.equal(api.getStatusCode(), Status.OK); 144 | done(); 145 | }).catch(done); 146 | }); 147 | 148 | it("Should do CONNECT index", (done) => { 149 | server.CONNECT("/").then((api: FakeResponseApi) => { 150 | assert.equal(api.getBody().toString(), "VALUE <- BEFORE"); 151 | done(); 152 | }).catch(done); 153 | }); 154 | 155 | it("Should do DELETE index", (done) => { 156 | server.DELETE("/").then((api: FakeResponseApi) => { 157 | assert.equal(api.getBody().toString(), "VALUE <- BEFORE"); 158 | done(); 159 | }).catch(done); 160 | }); 161 | 162 | it("Should do HEAD index", (done) => { 163 | server.HEAD("/").then((api: FakeResponseApi) => { 164 | assert.equal(api.getBody().toString(), "VALUE <- BEFORE"); 165 | done(); 166 | }).catch(done); 167 | }); 168 | 169 | it("Should do TRACE index", (done) => { 170 | server.TRACE("/").then((api: FakeResponseApi) => { 171 | assert.equal(api.getBody().toString(), "VALUE <- BEFORE"); 172 | done(); 173 | }).catch(done); 174 | }); 175 | 176 | it("Should do POST index", (done) => { 177 | server.POST("/ajax/call", Buffer.from("SENT_FROM_CLIENT")).then((api: FakeResponseApi) => { 178 | assert.equal(api.getBody().toString(), "CALL=SENT_FROM_CLIENT"); 179 | done(); 180 | }).catch(done); 181 | }); 182 | 183 | it("Should do PUT index", (done) => { 184 | server.PUT("/ajax/call", Buffer.from("SENT_FROM_CLIENT")).then((api: FakeResponseApi) => { 185 | assert.equal(api.getBody().toString(), "CALL=SENT_FROM_CLIENT"); 186 | done(); 187 | }).catch(done); 188 | }); 189 | 190 | it("Should do PATCH index", (done) => { 191 | server.PATCH("/ajax/call", Buffer.from("SENT_FROM_CLIENT")).then((api: FakeResponseApi) => { 192 | assert.equal(api.getBody().toString(), "CALL=SENT_FROM_CLIENT"); 193 | done(); 194 | }).catch(done); 195 | }); 196 | 197 | 198 | }); 199 | -------------------------------------------------------------------------------- /src/tests/fake-socket.ts: -------------------------------------------------------------------------------- 1 | import {assert, use} from "chai"; 2 | import * as sinonChai from "sinon-chai"; 3 | import {Methods, Router} from "../router/router"; 4 | import {Logger, LogLevels} from "../logger/logger"; 5 | import {Module} from "../decorators/module"; 6 | import {IAfterConstruct} from "../interfaces/iprovider"; 7 | import {Inject} from "../decorators/inject"; 8 | import {fakeHttpServer, FakeServerApi} from "../server/mocks"; 9 | import {WebSocket} from "../decorators/websocket"; 10 | import {Hook} from "../decorators/action"; 11 | import {BaseRequest} from "../server/request"; 12 | import {HttpError} from "../error"; 13 | import {Socket} from "../server/socket"; 14 | import {IAfterClose} from "../interfaces/iwebsocket"; 15 | import {Param} from "../decorators"; 16 | 17 | // use chai spies 18 | use(sinonChai); 19 | 20 | describe("fakeHttpServer with Sockets", () => { 21 | 22 | let server: FakeServerApi; 23 | let dummyResourceLocked = false; 24 | 25 | beforeEach(() => { 26 | 27 | @WebSocket({ 28 | name: "socket" 29 | }) 30 | class MySocket implements IAfterClose { 31 | @Inject(Logger) 32 | private readonly logger: Logger; 33 | 34 | @Inject(BaseRequest) 35 | private readonly request: BaseRequest; 36 | 37 | private socket: Socket; 38 | private name: string; 39 | 40 | @Hook("verify") 41 | verify(): void { 42 | if (this.request.getRequestHeader("should-fail")) { 43 | throw new HttpError(403, "You shall not pass"); 44 | } 45 | } 46 | 47 | @Hook("open") 48 | open(@Inject(Socket) socket: Socket, @Param("name") name: string) { 49 | this.socket = socket; 50 | this.name = name; 51 | dummyResourceLocked = true; 52 | 53 | if (this.request.getRequestHeader("send-name")) { 54 | this.socket.send(name); 55 | } 56 | } 57 | 58 | @Hook("message") 59 | receive(@Inject("message") message: any): void { 60 | this.socket.send(message); 61 | 62 | if (message === "timer") { 63 | setTimeout(() => { 64 | this.socket.send("timeout"); 65 | }, 200); 66 | } 67 | } 68 | 69 | afterClose(): void { 70 | dummyResourceLocked = false; 71 | } 72 | } 73 | 74 | @Module({ 75 | name: "root", 76 | providers: [Logger, Router], 77 | sockets: [MySocket] 78 | }) 79 | class MyModule implements IAfterConstruct { 80 | @Inject(Logger) 81 | private readonly logger: Logger; 82 | 83 | @Inject(Router) 84 | private readonly router: Router; 85 | 86 | afterConstruct(): void { 87 | this.router.addRules([ 88 | { 89 | methods: [Methods.GET], 90 | url: "/echo", 91 | route: "socket" 92 | }, 93 | { 94 | methods: [Methods.GET], 95 | url: "/echo-", 96 | route: "socket" 97 | } 98 | 99 | ]); 100 | this.logger.printToConsole(); 101 | this.logger.enable(); 102 | this.logger.setDebugLevel(LogLevels.BENCHMARK); 103 | } 104 | } 105 | 106 | server = fakeHttpServer(MyModule); 107 | }); 108 | 109 | 110 | it("Should create a socket", (done) => { 111 | server.createSocket("/echo") 112 | .then(api => { 113 | assert.isDefined(api); 114 | done(); 115 | }) 116 | .catch(done); 117 | }); 118 | 119 | it("Should fail verification when header is set", (done) => { 120 | server 121 | .createSocket("/echo", { 122 | "should-fail": true 123 | }) 124 | .then(api => { 125 | assert.fail(); 126 | }, error => { 127 | assert.instanceOf(error, HttpError); 128 | assert.equal(error.getCode(), 403); 129 | }) 130 | .then(done, done); 131 | }); 132 | 133 | it("Should extract parameters properly", (done) => { 134 | server 135 | .createSocket("/echo-VINCENT", { 136 | "send-name": true 137 | }) 138 | .then(api => 139 | api.open() 140 | .then(() => { 141 | assert.equal(api.getLastReceivedMessage(), "VINCENT"); 142 | }) 143 | ) 144 | .then(done, done); 145 | }); 146 | 147 | 148 | it("Should be possible to echo a message", (done) => { 149 | server 150 | .createSocket("/echo") 151 | .then(api => 152 | api 153 | .open() 154 | .then(() => { 155 | api.send("test data"); 156 | assert.equal(api.getLastReceivedMessage(), "test data"); 157 | api.close(); 158 | }) 159 | ) 160 | .then(done, done); 161 | }); 162 | 163 | it("Should send a message after timeout without direct echo", (done) => { 164 | server 165 | .createSocket("/echo") 166 | .then(api => 167 | api 168 | .open() 169 | .then(() => { 170 | api.onMessage(message => { 171 | if (message === "timeout") { 172 | api.close(); 173 | done(); 174 | } 175 | }); 176 | api.send("timer"); 177 | }) 178 | ) 179 | .catch(done); 180 | }); 181 | 182 | it("Should free resources after close", (done) => { 183 | server 184 | .createSocket("/echo") 185 | .then(api => 186 | api 187 | .open() 188 | .then(() => { 189 | api.close(); 190 | assert.equal(dummyResourceLocked, false); 191 | }) 192 | ) 193 | .then(done, done); 194 | }); 195 | }); 196 | -------------------------------------------------------------------------------- /src/tests/injector.spec.ts: -------------------------------------------------------------------------------- 1 | import {Router} from "../router/router"; 2 | import {Injector} from "../injector/injector"; 3 | import {Logger} from "../logger/logger"; 4 | import {assert, use} from "chai"; 5 | import * as sinonChai from "sinon-chai"; 6 | import {assert as assertSpy, spy} from "sinon"; 7 | import {Inject} from "../decorators/inject"; 8 | import {Injectable} from "../decorators/injectable"; 9 | import {Metadata} from "../injector/metadata"; 10 | 11 | // use chai spies 12 | use(sinonChai); 13 | 14 | describe("Injector", () => { 15 | it("Should create instance of Router", () => { 16 | let rootInjector = new Injector(); 17 | let injector = Injector.createAndResolve(Router, [ 18 | {provide: Injector, useValue: rootInjector}, 19 | {provide: Logger, useClass: Logger} 20 | ]); 21 | let router: Router = injector.get(Router); 22 | assert.isNotNull(router); 23 | }); 24 | 25 | 26 | it("@Injectable Should inject correctly on constructor and @Inject", () => { 27 | 28 | @Injectable() 29 | class A { 30 | 31 | @Inject(Logger) 32 | private c; 33 | 34 | constructor(private a: Logger, @Inject(Logger) private b) { 35 | } 36 | 37 | getA(): Logger { 38 | return this.a; 39 | } 40 | 41 | getB(): Logger { 42 | return this.b; 43 | } 44 | 45 | getC(): Logger { 46 | return this.c; 47 | } 48 | } 49 | 50 | let injector = Injector.createAndResolve(A, [ 51 | {provide: Logger, useClass: Logger} 52 | ]); 53 | let a: A = injector.get(A); 54 | assert.instanceOf(a.getA(), Logger); 55 | assert.instanceOf(a.getB(), Logger); 56 | assert.instanceOf(a.getC(), Logger); 57 | }); 58 | 59 | 60 | it("@Injectable Should inject correctly with useFactory", () => { 61 | 62 | @Injectable() 63 | class A { 64 | 65 | @Inject("logger") 66 | private c; 67 | 68 | constructor(private a: Logger, @Inject("logger") private b) { 69 | } 70 | 71 | getA(): Logger { 72 | return this.a; 73 | } 74 | 75 | getB(): Logger { 76 | return this.b; 77 | } 78 | 79 | getC(): Logger { 80 | return this.c; 81 | } 82 | } 83 | 84 | let provider = { 85 | provide: "logger", 86 | useFactory: (logger) => { 87 | assert.instanceOf(logger, Logger); 88 | return logger; 89 | }, 90 | deps: [Logger] 91 | }; 92 | let aSpy = spy(provider, "useFactory"); 93 | let injector = Injector.createAndResolve(A, [ 94 | Logger, 95 | provider 96 | ]); 97 | assertSpy.called(aSpy); 98 | let a: A = injector.get(A); 99 | assert.instanceOf(a.getA(), Logger); 100 | assert.instanceOf(a.getB(), Logger); 101 | assert.instanceOf(a.getC(), Logger); 102 | }); 103 | 104 | 105 | it("Make sure that @Injection of abstract implementation always delivers correct Token!", () => { 106 | 107 | 108 | @Injectable() 109 | class ServiceA { 110 | } 111 | 112 | @Injectable() 113 | class ServiceB { 114 | } 115 | 116 | @Injectable() 117 | class ServiceC { 118 | } 119 | 120 | @Injectable() 121 | abstract class AbstractClass { 122 | 123 | @Inject(ServiceA) 124 | public serviceAClass: ServiceA; 125 | 126 | } 127 | 128 | @Injectable() 129 | class ImplementationA extends AbstractClass { 130 | 131 | @Inject(ServiceB) 132 | public config: ServiceB; 133 | 134 | @Inject(ServiceB) 135 | public serviceBClass: ServiceB; 136 | 137 | } 138 | 139 | @Injectable() 140 | class ImplementationB extends AbstractClass { 141 | 142 | @Inject(ServiceC) 143 | public config: ServiceC; 144 | 145 | @Inject(ServiceC) 146 | public serviceCClass: ServiceC; 147 | 148 | } 149 | 150 | let providerA = Metadata.verifyProvider(ImplementationA); 151 | let providerB = Metadata.verifyProvider(ImplementationB); 152 | 153 | let parent = new Injector(); 154 | parent.createAndResolve(Metadata.verifyProvider(AbstractClass), [Metadata.verifyProvider(ServiceA)]); 155 | 156 | let injector = new Injector(parent); 157 | injector.createAndResolve(providerA, [Metadata.verifyProvider(ServiceB)]); 158 | injector.createAndResolve(providerB, [Metadata.verifyProvider(ServiceC)]); 159 | 160 | let instanceA = injector.get(ImplementationA); 161 | let instanceB = injector.get(ImplementationB); 162 | 163 | assert.instanceOf(instanceA.serviceAClass, ServiceA); 164 | assert.instanceOf(instanceA.config, ServiceB); 165 | assert.instanceOf(instanceA.serviceBClass, ServiceB); 166 | assert.isUndefined(instanceA.serviceCClass); 167 | 168 | assert.instanceOf(instanceB.serviceAClass, ServiceA); 169 | assert.instanceOf(instanceB.config, ServiceC); 170 | assert.instanceOf(instanceB.serviceCClass, ServiceC); 171 | assert.isUndefined(instanceB.serviceBClass); 172 | 173 | }); 174 | 175 | }); 176 | -------------------------------------------------------------------------------- /src/tests/mocks/multipart-samples.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by igorivanovic on 05.05.17. 3 | */ 4 | 5 | export const multipart_1 = ["------WebKitFormBoundaryTB2MiQ36fnSJlrhY", 6 | "Content-Disposition: form-data; name=\"cont\"", 7 | "", 8 | "some random content", 9 | "------WebKitFormBoundaryTB2MiQ36fnSJlrhY", 10 | "Content-Disposition: form-data; name=\"pass\"", 11 | "", 12 | "some random pass", 13 | "------WebKitFormBoundaryTB2MiQ36fnSJlrhY", 14 | "Content-Disposition: form-data; name=\"bit\"", 15 | "", 16 | "2", 17 | "------WebKitFormBoundaryTB2MiQ36fnSJlrhY--" 18 | ].join("\r\n"); 19 | 20 | export const multipart_1_contentType = "multipart/form-data; boundary=----WebKitFormBoundaryTB2MiQ36fnSJlrhY"; 21 | 22 | 23 | export const multipart_2 = ["-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", 24 | "Content-Disposition: form-data; name=\"file_name_0\"", 25 | "", 26 | "super alpha file", 27 | "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", 28 | "Content-Disposition: form-data; name=\"file_name_1\"", 29 | "", 30 | "super beta file", 31 | "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", 32 | "Content-Disposition: form-data; name=\"upload_file_0\"; filename=\"1k_a.dat\"", 33 | "Content-Type: application/octet-stream", 34 | "", 35 | "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", 36 | "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", 37 | "Content-Disposition: form-data; name=\"upload_file_1\"; filename=\"1k_b.dat\"", 38 | "Content-Type: application/octet-stream", 39 | "", 40 | "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", 41 | "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--" 42 | ].join("\r\n"); 43 | 44 | export const multipart_2_contentType = "multipart/form-data; boundary=---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k"; 45 | 46 | 47 | export const multipart_3 = [ 48 | "--893e5556-f402-4fec-8180-c59333354c6f\r\n" + 49 | "Content-Disposition: form-data; name=\"image\"; filename*=utf-8''%E6%B5%8B%E8%AF%95%E6%96%87%E6%A1%A3\r\n" + 50 | "\r\n" + 51 | "\r\n" + 52 | "--893e5556-f402-4fec-8180-c59333354c6f--\r\n" 53 | ].join("\r\n"); 54 | 55 | export const multipart_3_contentType = "multipart/form-data; boundary=893e5556-f402-4fec-8180-c59333354c6f"; 56 | 57 | 58 | export const multipart_4 = [ 59 | "------WebKitFormBoundaryvfUZhxgsZDO7FXLF", 60 | "Content-Disposition: form-data; name=\"title\"", 61 | "", 62 | "foofoo1", 63 | "", 64 | "------WebKitFormBoundaryvfUZhxgsZDO7FXLF", 65 | "Content-Disposition: form-data; name=\"text\"", 66 | "", 67 | "hi1", 68 | "", 69 | "------WebKitFormBoundaryvfUZhxgsZDO7FXLF--" 70 | ].join("\r\n"); 71 | export const multipart_4_contentType = "multipart/form-data; boundary=----WebKitFormBoundaryvfUZhxgsZDO7FXLF"; 72 | 73 | 74 | 75 | export const multipart_5 = "------WebKitFormBoundaryvfUZhxgsZDO7FXLF\r\n" + 76 | "Content-Disposition: form-data; name=\"title\"\r\n" + 77 | "\r\n" + 78 | "foofoo" + 79 | "\r\n" + 80 | "------WebKitFormBoundaryvfUZhxgsZDO7FXLF\r\n" + 81 | "Content-Disposition: form-data; name=\"upload\"; filename=\"blah1.txt\"\r\n" + 82 | "Content-Type: text/plain\r\n" + 83 | "\r\n" + 84 | "hi1\r\n" + 85 | "\r\n" + 86 | "------WebKitFormBoundaryvfUZhxgsZDO7FXLF\r\n" + 87 | "Content-Disposition: form-data; name=\"upload\"; filename=\"blah2.txt\"\r\n" + 88 | "Content-Type: text/plain\r\n" + 89 | "\r\n" + 90 | "hi2\r\n" + 91 | "\r\n" + 92 | "------WebKitFormBoundaryvfUZhxgsZDO7FXLF\r\n" + 93 | "Content-Disposition: form-data; name=\"upload\"; filename=\"blah3.txt\"\r\n" + 94 | "Content-Type: text/plain\r\n" + 95 | "\r\n" + 96 | "hi3\r\n" + 97 | "\r\n" + 98 | "------WebKitFormBoundaryvfUZhxgsZDO7FXLF\r\n" + 99 | "Content-Disposition: form-data; name=\"upload\"; filename=\"blah4.txt\"\r\n" + 100 | "Content-Type: text/plain\r\n" + 101 | "\r\n" + 102 | "hi4\r\n" + 103 | "\r\n" + 104 | "------WebKitFormBoundaryvfUZhxgsZDO7FXLF--\r\n"; 105 | 106 | export const multipart_5_contentType = "multipart/form-data; boundary=----WebKitFormBoundaryvfUZhxgsZDO7FXLF"; 107 | -------------------------------------------------------------------------------- /src/tests/multipart.spec.ts: -------------------------------------------------------------------------------- 1 | import {assert, expect, use} from "chai"; 2 | import * as sinonChai from "sinon-chai"; 3 | import {spy, stub, assert as assertSpy} from "sinon"; 4 | import { 5 | multipart_1, multipart_1_contentType, 6 | multipart_2, 7 | multipart_2_contentType, 8 | multipart_3, 9 | multipart_3_contentType, multipart_4, multipart_4_contentType, multipart_5, 10 | multipart_5_contentType 11 | } from "./mocks/multipart-samples"; 12 | import {MultiPart, MultiPartField, MultiPartFile} from "../parsers/multipart"; 13 | 14 | use(sinonChai); 15 | 16 | describe("Multipart parser", () => { 17 | 18 | it("Parse multipart_5", () => { 19 | let data = new Buffer(multipart_5); 20 | let multipart = new MultiPart(multipart_5_contentType); 21 | let parsed = multipart.parse(data); 22 | assert.isDefined(parsed); 23 | assert.isTrue(parsed.length === 5); 24 | 25 | let a1 = parsed.shift(); 26 | let a2 = parsed.shift(); 27 | let a3 = parsed.shift(); 28 | let a4 = parsed.shift(); 29 | let a5 = parsed.shift(); 30 | assert.instanceOf(a1, MultiPartField); 31 | assert.instanceOf(a2, MultiPartFile); 32 | assert.instanceOf(a3, MultiPartFile); 33 | assert.instanceOf(a4, MultiPartFile); 34 | assert.instanceOf(a5, MultiPartFile); 35 | 36 | assert.equal(a1.getFieldName(), "title"); 37 | assert.equal(a1.getFieldValue(), "foofoo"); 38 | 39 | assert.equal(a2.getFieldName(), "upload"); 40 | assert.equal(a2.getFileName(), "blah1.txt"); 41 | assert.equal(a2.getBuffer().toString(), "hi1\r\n"); 42 | 43 | 44 | assert.equal(a3.getFieldName(), "upload"); 45 | assert.equal(a3.getFileName(), "blah2.txt"); 46 | assert.equal(a3.getBuffer().toString(), "hi2\r\n"); 47 | 48 | assert.equal(a4.getFieldName(), "upload"); 49 | assert.equal(a4.getFileName(), "blah3.txt"); 50 | assert.equal(a4.getBuffer().toString(), "hi3\r\n"); 51 | 52 | assert.equal(a5.getFieldName(), "upload"); 53 | assert.equal(a5.getFileName(), "blah4.txt"); 54 | assert.equal(a5.getBuffer().toString(), "hi4\r\n"); 55 | 56 | }); 57 | 58 | 59 | it("Parse multipart_4", () => { 60 | let data = new Buffer(multipart_4); 61 | let multipart = new MultiPart(multipart_4_contentType); 62 | let parsed = multipart.parse(data); 63 | 64 | assert.isDefined(parsed); 65 | assert.isTrue(parsed.length === 2); 66 | 67 | let a1 = parsed.shift(); 68 | let a2 = parsed.shift(); 69 | assert.equal(a1.getFieldName(), "title"); 70 | assert.equal(a1.getFieldValue(), "foofoo1\r\n"); 71 | 72 | assert.equal(a2.getFieldName(), "text"); 73 | assert.equal(a2.getFieldValue(), "hi1\r\n"); 74 | 75 | }); 76 | 77 | 78 | it("Parse multipart_3", () => { 79 | let data = new Buffer(multipart_3); 80 | let multipart = new MultiPart(multipart_3_contentType); 81 | let parsed = multipart.parse(data); 82 | assert.isDefined(parsed); 83 | let a2 = parsed.shift(); 84 | 85 | assert.equal(a2.getFieldName(), "image"); 86 | assert.equal(a2.getFileName(), "测试文档"); 87 | assert.equal(a2.getBuffer().toString(), ""); 88 | }); 89 | 90 | it("Parse multipart_2", () => { 91 | let data = new Buffer(multipart_2); 92 | let multipart = new MultiPart(multipart_2_contentType); 93 | let parsed = multipart.parse(data); 94 | assert.isDefined(parsed); 95 | 96 | let a1 = parsed.shift(); 97 | let a2 = parsed.shift(); 98 | let a3 = parsed.shift(); 99 | let a4 = parsed.shift(); 100 | assert.instanceOf(a1, MultiPartField); 101 | assert.instanceOf(a2, MultiPartField); 102 | assert.instanceOf(a3, MultiPartFile); 103 | assert.instanceOf(a4, MultiPartFile); 104 | 105 | assert.equal(a1.getFieldName(), "file_name_0"); 106 | assert.equal(a1.getFieldValue(), "super alpha file"); 107 | 108 | assert.equal(a2.getFieldName(), "file_name_1"); 109 | assert.equal(a2.getFieldValue(), "super beta file"); 110 | 111 | assert.equal(a3.getFieldName(), "upload_file_0"); 112 | assert.equal(a3.getFileName(), "1k_a.dat"); 113 | assert.equal(a3.getBuffer().toString(), "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); 114 | 115 | assert.equal(a4.getFieldName(), "upload_file_1"); 116 | assert.equal(a4.getFileName(), "1k_b.dat"); 117 | assert.equal(a4.getBuffer().toString(), "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); 118 | 119 | }); 120 | 121 | 122 | it("Parse multipart_1", () => { 123 | let data = new Buffer(multipart_1); 124 | let multipart = new MultiPart(multipart_1_contentType); 125 | let parsed = multipart.parse(data); 126 | assert.isDefined(parsed); 127 | 128 | 129 | let a1 = parsed.shift(); 130 | let a2 = parsed.shift(); 131 | let a3 = parsed.shift(); 132 | 133 | assert.equal(a1.getFieldName(), "cont"); 134 | assert.equal(a1.getFieldValue(), "some random content"); 135 | 136 | assert.equal(a2.getFieldName(), "pass"); 137 | assert.equal(a2.getFieldValue(), "some random pass"); 138 | 139 | assert.equal(a3.getFieldName(), "bit"); 140 | assert.equal(a3.getFieldValue(), "2"); 141 | 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /src/tests/request-resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import {assert, use} from "chai"; 2 | import * as sinonChai from "sinon-chai"; 3 | import {spy, assert as assertSpy} from "sinon"; 4 | import {IResolvedRoute} from "../interfaces/iroute"; 5 | import {Methods, Router} from "../router/router"; 6 | import {uuid} from "../core"; 7 | import {EventEmitter} from "events"; 8 | import {Injector} from "../injector/injector"; 9 | import {HttpRequestResolver, RenderType} from "../server/request-resolver"; 10 | import {parse} from "url"; 11 | import {Logger} from "../logger/logger"; 12 | import {IResolvedModule, IModule} from "../interfaces/imodule"; 13 | import {Module} from "../decorators/module"; 14 | import {Controller} from "../decorators/controller"; 15 | import {Action} from "../decorators/action"; 16 | import {createModule, getModule} from "../server/bootstrap"; 17 | import {IProvider, IAfterConstruct} from "../interfaces/iprovider"; 18 | import {Metadata} from "../injector/metadata"; 19 | import {Inject} from "../decorators/inject"; 20 | import {setTimeout} from "timers"; 21 | 22 | // use chai spies 23 | use(sinonChai); 24 | 25 | describe("HttpRequestResolver", () => { 26 | let resolvedRoute: IResolvedRoute; 27 | let routeResolver: HttpRequestResolver; 28 | let request, response, data, id = uuid(); 29 | 30 | beforeEach(() => { 31 | resolvedRoute = { 32 | method: Methods.GET, 33 | params: { 34 | a: 1, 35 | b: 2 36 | }, 37 | route: "core/index" 38 | }; 39 | class ResponseEmitter extends EventEmitter { 40 | writeHead() { 41 | } 42 | 43 | write() { 44 | } 45 | 46 | end() { 47 | } 48 | 49 | invalid() { 50 | return 1; 51 | } 52 | } 53 | response = new ResponseEmitter(); 54 | request = new ResponseEmitter(); 55 | data = [new Buffer(1), new Buffer(1)]; 56 | let injector = Injector.createAndResolveChild( 57 | new Injector(), 58 | HttpRequestResolver, 59 | [ 60 | Logger, 61 | Router, 62 | {provide: "url", useValue: parse("/", true)}, 63 | {provide: "UUID", useValue: id}, 64 | {provide: "data", useValue: data}, 65 | {provide: "contentType", useValue: "text/html"}, 66 | {provide: "statusCode", useValue: 200}, 67 | {provide: "request", useValue: request}, 68 | {provide: "response", useValue: response}, 69 | {provide: "modules", useValue: []}, 70 | EventEmitter 71 | ] 72 | ); 73 | routeResolver = injector.get(HttpRequestResolver); 74 | }); 75 | 76 | 77 | it("Should initialize", () => { 78 | assert.isNotNull(routeResolver); 79 | }); 80 | 81 | it("Should render", (done) => { 82 | let toRender = "RENDER"; 83 | let aSpy = spy(response, "writeHead"); 84 | let a2Spy = spy(response, "write"); 85 | let a3Spy = spy(response, "end"); 86 | let resolve = routeResolver.render(toRender, RenderType.DATA_HANDLER); 87 | resolve.then(rendered => { 88 | assertSpy.calledWith(aSpy, 200, {"Content-Type": "text/html"}); 89 | assertSpy.calledWith(a2Spy, toRender); 90 | assertSpy.called(a3Spy); 91 | assert.equal(rendered, toRender); 92 | done(); 93 | }).catch(done); 94 | }); 95 | 96 | it("Should render throws error", (done) => { 97 | let resolve = routeResolver.render(Reflect.get(response, "invalid").call(), RenderType.DATA_HANDLER); 98 | resolve.catch(result => { 99 | assert.equal(result.message, "ResponseType must be string or buffer"); 100 | }).then(done).catch(done); 101 | }); 102 | 103 | 104 | it("Should getControllerProvider", () => { 105 | 106 | @Controller({ 107 | name: "core" 108 | }) 109 | class MyController { 110 | 111 | @Action("index") 112 | actionIndex() { 113 | } 114 | } 115 | 116 | @Module({ 117 | name: "root", 118 | controllers: [MyController] 119 | }) 120 | class MyModule { 121 | } 122 | 123 | let modules: Array = createModule(MyModule); 124 | let module: IResolvedModule = { 125 | module: getModule(modules), 126 | endpoint: "core", 127 | action: "index", 128 | resolvedRoute, 129 | data 130 | }; 131 | 132 | let provider = Metadata.verifyProvider(MyController); 133 | let controllerProvider: IProvider = HttpRequestResolver.getControllerProvider(module); 134 | assert.deepEqual(provider, controllerProvider); 135 | }); 136 | 137 | 138 | it("Should getControllerProvider no route", () => { 139 | 140 | @Controller({ 141 | name: "core" 142 | }) 143 | class MyController { 144 | 145 | @Action("index") 146 | actionIndex() { 147 | } 148 | } 149 | 150 | @Module({ 151 | name: "root", 152 | controllers: [MyController] 153 | }) 154 | class MyModule { 155 | } 156 | 157 | let modules: Array = createModule(MyModule); 158 | let module: IResolvedModule = { 159 | module: getModule(modules), 160 | endpoint: "test", 161 | action: "index", 162 | resolvedRoute, 163 | data 164 | }; 165 | 166 | assert.throws(() => { 167 | HttpRequestResolver.getControllerProvider(module); 168 | }, "You must define controller within current route: core/index"); 169 | }); 170 | 171 | 172 | it("Should processModule", (done) => { 173 | 174 | let value = "MY_VALUE"; 175 | 176 | @Controller({ 177 | name: "core" 178 | }) 179 | class MyController { 180 | 181 | @Action("index") 182 | actionIndex() { 183 | return value; 184 | } 185 | } 186 | 187 | @Module({ 188 | name: "root", 189 | providers: [Logger, Router], 190 | controllers: [MyController] 191 | }) 192 | class MyModule { 193 | } 194 | 195 | let modules: Array = createModule(MyModule); 196 | let module: IResolvedModule = { 197 | module: getModule(modules), 198 | endpoint: "core", 199 | action: "index", 200 | resolvedRoute, 201 | data 202 | }; 203 | 204 | Promise.resolve(routeResolver.processModule(module)) 205 | .then(resolved => { 206 | assert.equal(resolved, value); 207 | done(); 208 | }).catch(done); 209 | }); 210 | 211 | 212 | it("Should process GET", (done) => { 213 | 214 | let value = "MY_VALUE"; 215 | 216 | @Controller({ 217 | name: "core" 218 | }) 219 | class MyController { 220 | 221 | @Action("index") 222 | actionIndex() { 223 | return value; 224 | } 225 | } 226 | 227 | @Module({ 228 | name: "root", 229 | providers: [Logger, Router], 230 | controllers: [MyController] 231 | }) 232 | class MyModule implements IAfterConstruct { 233 | afterConstruct(): void { 234 | this.router.addRules([{ 235 | methods: [Methods.GET], 236 | url: "/", 237 | route: "core/index" 238 | }]); 239 | } 240 | 241 | @Inject(Router) 242 | private router: Router; 243 | } 244 | 245 | request.method = "GET"; 246 | request.url = "/"; 247 | request.headers = {}; 248 | 249 | let modules: Array = createModule(MyModule); 250 | let injector = Injector.createAndResolveChild( 251 | getModule(modules).injector, 252 | HttpRequestResolver, 253 | [ 254 | {provide: "url", useValue: parse("/", true)}, 255 | {provide: "UUID", useValue: id}, 256 | {provide: "data", useValue: data}, 257 | {provide: "contentType", useValue: "text/html"}, 258 | {provide: "statusCode", useValue: 200}, 259 | {provide: "request", useValue: request}, 260 | {provide: "response", useValue: response}, 261 | {provide: "modules", useValue: modules}, 262 | EventEmitter 263 | ] 264 | ); 265 | let myRouteResolver = injector.get(HttpRequestResolver); 266 | 267 | 268 | let aSpy = spy(myRouteResolver, "render"); 269 | 270 | Promise.resolve(myRouteResolver.process()) 271 | .then(resolved => { 272 | assert.equal(resolved, value); 273 | assertSpy.calledWith(aSpy, value); 274 | done(); 275 | }).catch(done); 276 | }); 277 | 278 | 279 | it("Should process POST", (done) => { 280 | 281 | let value = "MY_VALUE"; 282 | 283 | @Controller({ 284 | name: "core" 285 | }) 286 | class MyController { 287 | 288 | @Action("index") 289 | actionIndex() { 290 | return value; 291 | } 292 | } 293 | 294 | @Module({ 295 | name: "root", 296 | providers: [Logger, Router], 297 | controllers: [MyController] 298 | }) 299 | class MyModule implements IAfterConstruct { 300 | afterConstruct(): void { 301 | this.router.addRules([{ 302 | methods: [Methods.POST], 303 | url: "/", 304 | route: "core/index" 305 | }]); 306 | } 307 | 308 | @Inject(Router) 309 | private router: Router; 310 | } 311 | 312 | request.method = "POST"; 313 | request.url = "/"; 314 | request.headers = {}; 315 | 316 | let modules: Array = createModule(MyModule); 317 | let injector = Injector.createAndResolveChild( 318 | getModule(modules).injector, 319 | HttpRequestResolver, 320 | [ 321 | {provide: "url", useValue: parse("/", true)}, 322 | {provide: "UUID", useValue: id}, 323 | {provide: "data", useValue: []}, 324 | {provide: "contentType", useValue: "text/html"}, 325 | {provide: "statusCode", useValue: 200}, 326 | {provide: "request", useValue: request}, 327 | {provide: "response", useValue: response}, 328 | {provide: "modules", useValue: modules}, 329 | EventEmitter 330 | ] 331 | ); 332 | let myRouteResolver = injector.get(HttpRequestResolver); 333 | 334 | let a = [Buffer.from("a"), Buffer.from("b"), Buffer.from("c")]; 335 | 336 | // simulate async data processing 337 | setTimeout(() => { 338 | request.emit("data", a[0]); 339 | request.emit("data", a[1]); 340 | request.emit("data", a[2]); 341 | request.emit("end"); 342 | }, 0); 343 | 344 | let aSpy = spy(myRouteResolver, "render"); 345 | let bSpy = spy(myRouteResolver, "processModule"); 346 | 347 | Promise.resolve(myRouteResolver.process()) 348 | .then(resolved => { 349 | let module: IResolvedModule = { 350 | module: getModule(modules, "root"), 351 | endpoint: "core", 352 | action: "index", 353 | resolvedRoute: { 354 | method: Methods.POST, 355 | params: {}, 356 | route: "core/index" 357 | }, 358 | data: a 359 | }; 360 | assert.equal(resolved, value); 361 | assertSpy.calledWith(aSpy, value); 362 | assertSpy.calledWith(bSpy, module); 363 | done(); 364 | }).catch(done); 365 | }); 366 | 367 | 368 | }); 369 | -------------------------------------------------------------------------------- /src/tests/request.spec.ts: -------------------------------------------------------------------------------- 1 | import {Methods} from "../router/router"; 2 | import {Injector} from "../injector/injector"; 3 | import {IResolvedRoute} from "../interfaces/iroute"; 4 | import {ControllerResolver} from "../server/controller-resolver"; 5 | import {assert, expect, use} from "chai"; 6 | import * as sinonChai from "sinon-chai"; 7 | import {assert as assertSpy, spy, stub} from "sinon"; 8 | import {EventEmitter} from "events"; 9 | import {isEqual, uuid} from "../core"; 10 | import {BaseRequest, Request} from "../server/request"; 11 | 12 | // use chai spies 13 | use(sinonChai); 14 | 15 | describe("BaseRequest", () => { 16 | const resolvedRoute: IResolvedRoute = { 17 | method: Methods.GET, 18 | params: { 19 | a: 1, 20 | b: 2 21 | }, 22 | route: "core/index" 23 | }; 24 | const incomingMessage = Object.create({ 25 | uuid: "1", 26 | method: "GET", 27 | url: "/", 28 | httpVersion: "1.1", 29 | httpVersionMajor: 1, 30 | httpVersionMinor: 1, 31 | connection: { 32 | remoteAddress: "192.0.0.1", 33 | remoteFamily: "", 34 | remotePort: 9000, 35 | localAddress: "192.0.0.1", 36 | localPort: 9000 37 | }, 38 | headers: {} 39 | }); 40 | const data: Array = [Buffer.from(["a", "b", "c"])]; 41 | const UUID: string = uuid(); 42 | 43 | let request: BaseRequest; 44 | 45 | beforeEach(() => { 46 | const injector = Injector.createAndResolve(BaseRequest, [ 47 | {provide: "resolvedRoute", useValue: resolvedRoute}, 48 | {provide: "request", useValue: incomingMessage}, 49 | {provide: "UUID", useValue: UUID}, 50 | {provide: "data", useValue: data} 51 | ]); 52 | request = injector.get(BaseRequest); 53 | }); 54 | 55 | it("BaseRequest.constructor", () => { 56 | assert.isTrue(request instanceof BaseRequest); 57 | }); 58 | 59 | it("BaseRequest.getConnection", () => { 60 | let conn = request.getConnection(); 61 | assert.deepEqual(conn, { 62 | uuid: UUID, 63 | method: "GET", 64 | url: "/", 65 | httpVersion: "1.1", 66 | httpVersionMajor: 1, 67 | httpVersionMinor: 1, 68 | remoteAddress: "192.0.0.1", 69 | remoteFamily: "", 70 | remotePort: 9000, 71 | localAddress: "192.0.0.1", 72 | localPort: 9000 73 | }); 74 | }); 75 | 76 | it("BaseRequest.getCookies", () => { 77 | let aSpy = stub(request, "getRequestHeader"); 78 | aSpy.returns("__id=d6ad83ce4a84516bf7d438504e81b139a1483565272; __id2=1;"); 79 | let requestCookies = request.getCookies(); 80 | let cookies = { 81 | __id: "d6ad83ce4a84516bf7d438504e81b139a1483565272", 82 | __id2: "1" 83 | }; 84 | assertSpy.called(aSpy); 85 | assert.isTrue(isEqual(requestCookies, cookies)); 86 | }); 87 | 88 | it("BaseRequest.getCookie", () => { 89 | let aSpy = stub(request, "getCookies"); 90 | aSpy.returns({ 91 | __id: "d6ad83ce4a84516bf7d438504e81b139a1483565272", 92 | __id2: "1" 93 | }); 94 | let id = request.getCookie("__id"); 95 | assertSpy.called(aSpy); 96 | assert.equal(id, "d6ad83ce4a84516bf7d438504e81b139a1483565272"); 97 | }); 98 | 99 | it("BaseRequest.getCookies null", () => { 100 | let aSpy = stub(request, "getRequestHeader"); 101 | aSpy.returns(null); 102 | let requestCookies = request.getCookies(); 103 | let cookies = {}; 104 | assertSpy.called(aSpy); 105 | assert.isTrue(isEqual(requestCookies, cookies)); 106 | }); 107 | 108 | it("BaseRequest.getRequestHeaders", () => { 109 | let rHeaders = request.getRequestHeaders(); 110 | assert.isTrue(isEqual(rHeaders, {})); 111 | }); 112 | 113 | it("BaseRequest.getRequestHeader", () => { 114 | let aSpy = stub(request, "getRequestHeaders"); 115 | let headers = {accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"}; 116 | aSpy.returns(headers); 117 | let rHeaders = request.getRequestHeader("Accept"); 118 | assertSpy.called(aSpy); 119 | assert.isTrue(isEqual(rHeaders, headers.accept)); 120 | }); 121 | 122 | it("BaseRequest.getParams", () => { 123 | assert.isTrue(isEqual(request.getParams(), resolvedRoute.params)); 124 | }); 125 | 126 | it("BaseRequest.getParam", () => { 127 | assert.isTrue(isEqual(request.getParam("a"), 1)); 128 | }); 129 | 130 | it("BaseRequest.getMethod", () => { 131 | assert.isTrue(isEqual(request.getMethod(), Methods.GET)); 132 | }); 133 | 134 | it("BaseRequest.getRoute", () => { 135 | assert.isTrue(isEqual(request.getRoute(), "core/index")); 136 | }); 137 | 138 | it("BaseRequest.getBody", () => { 139 | const body = request.getBody(); 140 | assert.isTrue(isEqual(body, Buffer.concat(data))); 141 | }); 142 | }); 143 | 144 | describe("Request", () => { 145 | const resolvedRoute: IResolvedRoute = { 146 | method: Methods.GET, 147 | params: { 148 | a: 1, 149 | b: 2 150 | }, 151 | route: "core/index" 152 | }; 153 | const incomingMessage = Object.create({ 154 | headers: {} 155 | }); 156 | const data = [Buffer.from(["a", "b", "c"])]; 157 | const UUID = 1; 158 | 159 | let eventEmitter: EventEmitter; 160 | let controllerResolver: ControllerResolver; 161 | let request: Request; 162 | 163 | beforeEach(() => { 164 | eventEmitter = new EventEmitter(); 165 | 166 | controllerResolver = Object.create({ 167 | getEventEmitter: () => { 168 | return eventEmitter; 169 | }, 170 | getRequestHeader: () => { 171 | // dummy 172 | }, 173 | getServerResponse: () => { 174 | // dummy 175 | }, 176 | getIncomingMessage: () => { 177 | return incomingMessage; 178 | }, 179 | getResolvedRoute: () => { 180 | return resolvedRoute; 181 | }, 182 | getId: () => { 183 | return UUID; 184 | }, 185 | getRawData: () => { 186 | return data; 187 | }, 188 | setContentType: () => { 189 | // dummy 190 | }, 191 | setStatusCode: () => { 192 | // dummy 193 | }, 194 | stopChain: () => { 195 | // dummy 196 | } 197 | }); 198 | let injector = Injector.createAndResolve(Request, [ 199 | {provide: "controllerResolver", useValue: controllerResolver} 200 | ]); 201 | request = injector.get(Request); 202 | }); 203 | 204 | it("Request.constructor", () => { 205 | assert.isTrue(request instanceof Request); 206 | }); 207 | 208 | it("Request.onDestroy", () => { 209 | let eSpy = spy(controllerResolver, "getEventEmitter"); 210 | let isCalled = false; 211 | 212 | function destroy() { 213 | isCalled = true; 214 | } 215 | 216 | request.onDestroy(destroy); 217 | assertSpy.called(eSpy); 218 | 219 | expect(EventEmitter.listenerCount(eventEmitter, "destroy")).to.be.eq(1); 220 | eventEmitter.emit("destroy"); 221 | assert.isTrue(isCalled); 222 | }); 223 | 224 | it("Request.setResponseHeader", () => { 225 | let api = { 226 | setHeader: (a, b) => { 227 | // dummy 228 | } 229 | }; 230 | let bSpy = stub(api, "setHeader"); 231 | let aSpy = stub(controllerResolver, "getServerResponse"); 232 | aSpy.returns(api); 233 | request.setResponseHeader("Content-Type", "application/javascript"); 234 | assertSpy.called(aSpy); 235 | assertSpy.calledWith(bSpy, "Content-Type", "application/javascript"); 236 | }); 237 | 238 | it("Request.setContentType", () => { 239 | let aSpy = spy(eventEmitter, "emit"); 240 | request.setContentType("application/javascript"); 241 | assertSpy.calledWith(aSpy, "contentType", "application/javascript"); 242 | }); 243 | 244 | it("Request.setStatusCode", () => { 245 | let aSpy = spy(eventEmitter, "emit"); 246 | request.setStatusCode(400); 247 | assertSpy.calledWith(aSpy, "statusCode", 400); 248 | }); 249 | 250 | it("Request.stopChain", () => { 251 | let aSpy = stub(controllerResolver, "stopChain"); 252 | request.stopChain(); 253 | assertSpy.called(aSpy); 254 | }); 255 | }); 256 | -------------------------------------------------------------------------------- /src/tests/route-parser.spec.ts: -------------------------------------------------------------------------------- 1 | import {RouteParser} from "../router/route-parser"; 2 | import {assert, expect} from "chai"; 3 | import {isEqual} from "../core"; 4 | 5 | describe("RouterParser", () => { 6 | 7 | it("Initialize", () => { 8 | let pattern = RouteParser.parse("/canone//shoulddo-it/" + 9 | "--now--not/user/"); 10 | assert.isTrue(pattern instanceof RouteParser); 11 | }); 12 | 13 | it("Should test patterns on /canone//shoulddo-it/--now--not/user/", () => { 14 | let pattern = RouteParser.parse("/canone//shoulddo-it/" + 15 | "--now--not/user/"); 16 | assert.isFalse(pattern.isValid("")); 17 | assert.isTrue(pattern.isValid("/canbeone/igor/should#+do-it/whata-smile-now-2306-not/user/1412")); 18 | assert.isFalse(pattern.isValid("/canbeone/igor/should#+do-it/whata-smile-now-2306-not/user/1412a")); 19 | assert.isFalse(pattern.isValid("/canbeone/igor/should#+do-it/whata-smile-now-2306-not/user/1412/abc")); 20 | assert.isFalse(pattern.isValid("/igor/should#+do-it/whata-smile-now-2306-not/user/1412")); 21 | assert.isFalse(pattern.isValid("//igor/should#+do-it/whata-smile-now-2306-not/user/1412")); 22 | assert.isFalse(pattern.isValid("/canbeone/igor/should#+do-it/whata-smile-now-2306-not/usera/1412")); 23 | assert.isFalse(pattern.isValid("/canbeone/igor/should#+do-it/whata-smile-now-2306a-not/user/1412")); 24 | assert.isFalse(pattern.isValid("/canbeone/igor/should#+do-it/whata-smile-nowa-2306-not/user/1412")); 25 | assert.isTrue(pattern.isValid("/canbeone/igor/should#+do-it/whata1231-smile-now-2306-not/user/1412")); 26 | assert.isFalse(pattern.isValid("/canbeone/igor/should#+do-it/whata1231!-smile-now-2306-not/user/1412")); 27 | assert.isFalse(pattern.isValid("/canbeone/igor/should#+do-it/whata123-smile123-now-2306-not/user/1412")); 28 | assert.isFalse(pattern.isValid("/canbeone/igor/should--be-able-do-it/whata123-smile-now-2306-not/user/1412")); 29 | assert.isTrue(pattern.isValid("/can1454zfhg=?`='( ()=(one/igor/should#+do-it/whata-smile-now-2306-not/user/1412")); 30 | 31 | let params = pattern.getParams("/can1454zfhg=?`='( ()=(one/igor/should#+do-it/whata-smile-now-2306-not/user/1412"); 32 | assert.isTrue(isEqual(params, { 33 | "any": "1454zfhg=?`='( ()=(", 34 | "name": "igor", 35 | "now": "#+", 36 | "see": "whata", 37 | "nice": "smile", 38 | "only": "2306", 39 | "id": "1412" 40 | })); 41 | 42 | expect(pattern.createUrl(params)).to.be.eq("/can1454zfhg=?`='( ()=(one/igor/should#+do-it/whata-smile-now-2306-not/user/1412"); 43 | }); 44 | 45 | 46 | it("Should test patterns on /canone//shoulddo-it/--now--not/user/", () => { 47 | let pattern = RouteParser.parse("/canone//shoulddo-it/--now--not/user/"); 48 | 49 | let params = pattern.getParams("/can1454zfhg=?`='( ()=(one/igor/should#+do-it/whata-smile-now-2306-not/user/1412"); 50 | assert.isTrue(isEqual(params, { 51 | "any": "1454zfhg=?`='( ()=(", 52 | "name": "igor", 53 | "now": "#+", 54 | "see": "whata", 55 | "nice": "smile", 56 | "only": "2306", 57 | "id": "1412" 58 | })); 59 | 60 | expect(pattern.createUrl(params)).to.be.eq("/can1454zfhg=?`='( ()=(one/igor/should#+do-it/whata-smile-now-2306-not/user/1412"); 61 | 62 | let params1 = pattern.getParams("/can1454zfhg=?`='( ()=(one/igor/should#+abc/next-toitdo-it/whata-smile-now-2306-not/user/1412"); 63 | assert.isTrue(isEqual(params1, { 64 | "any": "1454zfhg=?`='( ()=(", 65 | "name": "igor", 66 | "now": "#+abc/next-toit", 67 | "see": "whata", 68 | "nice": "smile", 69 | "only": "2306", 70 | "id": "1412" 71 | })); 72 | 73 | expect(pattern.createUrl(params1)).to.be.eq("/can1454zfhg=?`='( ()=(one/igor/should#+abc/next-toitdo-it/whata-smile-now-2306-not/user/1412"); 74 | }); 75 | 76 | 77 | it("Should test patterns on /canone//shoulddo-it/", () => { 78 | let pattern = RouteParser.parse("/canone//shoulddo-it/"); 79 | assert.isFalse(pattern.isValid("")); 80 | assert.isTrue(pattern.isValid("/canbeone/igor/should#+do-it/whata")); 81 | assert.isTrue(pattern.isValid("/canbeone/cn/should#+do-it/all")); 82 | assert.isTrue(pattern.isValid("/canbeone/ws/should#+do-it/good")); 83 | let params = pattern.getParams("/canbeone/ws/should#+do-it/good"); 84 | assert.isTrue(isEqual(params, { 85 | "any": "be", 86 | "name": "ws", 87 | "now": "#+", 88 | "see": "good" 89 | })); 90 | 91 | expect(pattern.createUrl(params)).to.be.eq("/canbeone/ws/should#+do-it/good"); 92 | }); 93 | 94 | it("Should test patterns on //", () => { 95 | let pattern = RouteParser.parse("//"); 96 | assert.isFalse(pattern.isValid("/category/1/page/1")); 97 | assert.isFalse(pattern.isValid("/category/abc1/abc")); 98 | assert.isTrue(pattern.isValid("/category/abc1")); 99 | let params = pattern.getParams("/category/abc1"); 100 | assert.isTrue(isEqual(params, { 101 | "clientId": "category", 102 | "url": "abc1" 103 | })); 104 | }); 105 | 106 | 107 | it("Should test patterns on /home", () => { 108 | let pattern = RouteParser.parse("/home"); 109 | assert.isTrue(pattern.isValid("/home")); 110 | let params = pattern.getParams("/home"); 111 | assert.isTrue(isEqual(params, {})); 112 | }); 113 | 114 | 115 | it("Should test patterns on /home/test", () => { 116 | let pattern = RouteParser.parse("/home/test"); 117 | assert.isTrue(pattern.isValid("/home/test")); 118 | let params = pattern.getParams("/home/test"); 119 | assert.isTrue(isEqual(params, {})); 120 | }); 121 | 122 | 123 | it("Should test patterns on /home/test/abc", () => { 124 | let pattern = RouteParser.parse("/home/test/abc"); 125 | assert.isTrue(pattern.isValid("/home/test/abc")); 126 | let params = pattern.getParams("/home/test/abc"); 127 | assert.isTrue(isEqual(params, {})); 128 | }); 129 | 130 | 131 | 132 | it("Should test patterns on /category//page/", () => { 133 | let pattern = RouteParser.parse("/category//page/"); 134 | assert.isTrue(pattern.isValid("/category/1/page/1")); 135 | assert.isFalse(pattern.isValid("/category/abc1/abc")); 136 | assert.isFalse(pattern.isValid("/category/abc1")); 137 | let params = pattern.getParams("/category/1/page/1"); 138 | assert.isTrue(isEqual(params, { 139 | "category": "1", 140 | "pagenumber": "1" 141 | })); 142 | }); 143 | 144 | it("Should test patterns on //page/", () => { 145 | let pattern = RouteParser.parse("//page/"); 146 | assert.isTrue(pattern.isValid("/category/page/1")); 147 | assert.isTrue(pattern.isValid("/category/page/1/abc")); 148 | assert.isTrue(pattern.isValid("/category/value/page/1/abc")); 149 | assert.isFalse(pattern.isValid("/category/value/page1/1/abc")); 150 | assert.isFalse(pattern.isValid("/category/page1/1/abc")); 151 | let params = pattern.getParams("/category/value/page/1/abc"); 152 | let params1 = pattern.getParams("/category/page/1"); 153 | 154 | assert.isTrue(isEqual(params, { 155 | "category": "category/value", 156 | "pageNum": "1/abc" 157 | })); 158 | 159 | expect(pattern.createUrl(params)).to.be.eq("/category/value/page/1/abc"); 160 | 161 | assert.isTrue(isEqual(params1, { 162 | "category": "category", 163 | "pageNum": "1" 164 | })); 165 | 166 | expect(pattern.createUrl(params1)).to.be.eq("/category/page/1"); 167 | }); 168 | 169 | 170 | it("Should test patterns on /home/", () => { 171 | let pattern = RouteParser.parse("/home/"); 172 | assert.isFalse(pattern.isValid("")); 173 | assert.isTrue(pattern.isValid("/home/123")); 174 | assert.isFalse(pattern.isValid("/home/123/")); 175 | assert.isFalse(pattern.isValid("/home/cn/all")); 176 | assert.isFalse(pattern.isValid("/home/abc")); 177 | assert.isFalse(pattern.isValid("/home/abc/")); 178 | assert.isTrue(pattern.isValid("/home/1")); 179 | let params = pattern.getParams("/home/123"); 180 | assert.isTrue(isEqual(params, { 181 | "id": "123" 182 | })); 183 | }); 184 | 185 | it("Should test patterns on /home/", () => { 186 | let pattern = RouteParser.parse("/home/"); 187 | assert.isFalse(pattern.isValid("")); 188 | assert.isTrue(pattern.isValid("/home/123")); 189 | assert.isTrue(pattern.isValid("/home/works")); 190 | assert.isFalse(pattern.isValid("/home/123/")); 191 | assert.isFalse(pattern.isValid("/home/cn/all")); 192 | assert.isTrue(pattern.isValid("/home/abc")); 193 | assert.isFalse(pattern.isValid("/home/abc/")); 194 | assert.isTrue(pattern.isValid("/home/1")); 195 | let params = pattern.getParams("/home/123"); 196 | assert.isTrue(isEqual(params, { 197 | "name": "123" 198 | })); 199 | }); 200 | 201 | 202 | it("Should test patterns on /", () => { 203 | let pattern = RouteParser.parse("/"); 204 | assert.isTrue(pattern.isValid("/")); 205 | assert.isFalse(pattern.isValid("")); 206 | assert.throw(() => RouteParser.parse(""), "Url must start with \/"); 207 | assert.throw(() => RouteParser.parse("abc/"), "Url must start with \/"); 208 | }); 209 | 210 | it("Should get correct parameters on /canone//shoulddo-it/", () => { 211 | let pattern = RouteParser.parse("/canone//shoulddo-it/" + 212 | "--now--not/user/"); 213 | let url = "/can1454zfhg=?`='( ()=(one/igor/should#+do-it/whata-smile-now-2306-not/user/1412"; 214 | let params = pattern.getParams(url); 215 | assert.isTrue(isEqual(params, { 216 | any: "1454zfhg=?`='( ()=(", 217 | id: "1412", 218 | name: "igor", 219 | nice: "smile", 220 | now: "#+", 221 | only: "2306", 222 | see: "whata" 223 | })); 224 | expect(pattern.createUrl(params)).to.be.eq(url); 225 | }); 226 | 227 | it("Should test pattern for /assets/", () => { 228 | let pattern = RouteParser.parse("/assets/"); 229 | let url = "/assets/css/main.css"; 230 | assert.isTrue(pattern.isValid(url)); 231 | assert.isFalse(pattern.isValid("")); 232 | assert.isTrue(pattern.isValid("/assets/css/main.css")); 233 | 234 | let params = pattern.getParams("/assets/css/main.css"); 235 | 236 | assert.isTrue(isEqual(params, { 237 | "file": "css/main.css" 238 | })); 239 | }); 240 | 241 | it("Should test patterns on ///page/", () => { 242 | let pattern = RouteParser.parse("///page/"); 243 | assert.isTrue(pattern.isValid("/ab123sbr/this-is-test/abc-123/page/123123")); 244 | let params = pattern.getParams("/ab123sbr/this-is-test/abc-123/page/123123"); 245 | assert.isTrue(isEqual(params, { 246 | "clientId": "ab123sbr", 247 | "url": "this-is-test/abc-123", 248 | "number": "123123" 249 | })); 250 | }); 251 | 252 | 253 | it("Should get correct parameters on /assets/", () => { 254 | let pattern = RouteParser.parse("/assets/"); 255 | let url = "/assets/css/main.css"; 256 | assert.isTrue(pattern.isValid(url)); 257 | let params = pattern.getParams(url); 258 | assert.isTrue(isEqual(params, {file: "css/main.css"})); 259 | expect(pattern.createUrl(params)).to.be.eq(url); 260 | }); 261 | }); 262 | -------------------------------------------------------------------------------- /src/tests/route-rule.spec.ts: -------------------------------------------------------------------------------- 1 | import {RouteRule} from "../router/route-rule"; 2 | import {Methods} from "../router/router"; 3 | import {Injector} from "../injector/injector"; 4 | import {assert, expect} from "chai"; 5 | import {isEqual} from "../core"; 6 | 7 | describe("RouteRule", () => { 8 | it("Parse route and create url", () => { 9 | let config = { 10 | methods: [Methods.GET, Methods.POST], 11 | route: "core/index", 12 | url: "/home/" 13 | }; 14 | let injector = Injector.createAndResolve(RouteRule, [{provide: "config", useValue: config}]); 15 | let route = injector.get(RouteRule); 16 | assert.isNotNull(route); 17 | return Promise 18 | .all([ 19 | route.parseRequest("/home/123", "GET", {}), 20 | route.parseRequest("/home/123", "POST", {}), 21 | route.parseRequest("/home/123", "CONNECT", {}), 22 | route.createUrl("core/index", {id: 123}) 23 | ]) 24 | .then(data => { 25 | let result = [ 26 | { 27 | method: Methods.GET, 28 | params: { 29 | id: "123" 30 | }, 31 | route: "core/index" 32 | }, 33 | { 34 | method: Methods.POST, 35 | params: { 36 | id: "123" 37 | }, 38 | route: "core/index" 39 | }, 40 | false, 41 | "/home/123" 42 | ]; 43 | assert.isTrue(isEqual(data, result)); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/tests/router.spec.ts: -------------------------------------------------------------------------------- 1 | import {Methods, Router} from "../router/router"; 2 | import {Injector} from "../injector/injector"; 3 | import {Logger} from "../logger/logger"; 4 | import {assert} from "chai"; 5 | import {isEqual} from "../core"; 6 | import {IResolvedRoute, Route} from "../interfaces/iroute"; 7 | import {HttpError} from "../error"; 8 | 9 | describe("Router", () => { 10 | 11 | let router: Router; 12 | 13 | beforeEach(() => { 14 | let rootInjector = new Injector(); 15 | let injector = Injector.createAndResolve(Router, [ 16 | {provide: Injector, useValue: rootInjector}, 17 | {provide: Logger, useClass: Logger} 18 | ]); 19 | router = injector.get(Router); 20 | }); 21 | 22 | it("Parse request and create dynamic url", () => { 23 | 24 | class DynamicRule implements Route { 25 | parseRequest(pathName: string, method: string, headers: Headers): Promise { 26 | return Promise.resolve(true); 27 | } 28 | 29 | createUrl(routeName: string, params: Object): Promise { 30 | return null; 31 | } 32 | 33 | } 34 | 35 | router.addRule(DynamicRule); 36 | 37 | return router.parseRequest("/", "GET", {}).then((data) => { 38 | let result = []; 39 | assert.isTrue(isEqual(data, result)); 40 | }) 41 | .catch((error: HttpError) => { 42 | assert.equal(error.getMessage(), "Router.parseRequest: / no route found, method: GET"); 43 | }); 44 | }); 45 | 46 | 47 | it("Parse request and create url", () => { 48 | 49 | 50 | router.addRules([ 51 | { 52 | methods: [Methods.OPTIONS], 53 | route: "controller/test", 54 | url: "*" 55 | }, 56 | { 57 | methods: [Methods.GET, Methods.POST], 58 | route: "controller/index", 59 | url: "/" 60 | }, 61 | { 62 | methods: [Methods.GET, Methods.POST], 63 | route: "controller/home", 64 | url: "/home" 65 | }, 66 | { 67 | methods: [Methods.GET], 68 | route: "controller/view", 69 | url: "/home/" 70 | } 71 | ]); 72 | 73 | return Promise.all([ 74 | router.parseRequest("/", "POST", {}), 75 | router.parseRequest("/authenticate", "OPTIONS", {}), 76 | router.parseRequest("/home", "GET", {}), 77 | router.parseRequest("/home/123", "GET", {}), 78 | router.createUrl("controller/view", {id: 123}), 79 | router.createUrl("controller/index", {}), 80 | router.createUrl("controller/home", {}), 81 | router.createUrl("controller/indexs", {}) 82 | ]).then((data) => { 83 | let result = [ 84 | { 85 | method: Methods.POST, 86 | params: {}, 87 | route: "controller/index" 88 | }, 89 | { 90 | method: Methods.OPTIONS, 91 | params: {}, 92 | route: "controller/test" 93 | }, 94 | { 95 | method: Methods.GET, 96 | params: {}, 97 | route: "controller/home" 98 | }, 99 | { 100 | method: Methods.GET, 101 | params: { 102 | id: "123" 103 | }, 104 | route: "controller/view" 105 | }, 106 | "/home/123", 107 | "/", 108 | "/home", 109 | "/controller/indexs" 110 | ]; 111 | 112 | assert.isTrue(isEqual(data, result)); 113 | }); 114 | }); 115 | 116 | 117 | it("Should invoke getError|hasError|setError", () => { 118 | let route = "core/error"; 119 | router.setError(route); 120 | assert.isTrue(router.hasError()); 121 | assert.equal(route, router.getError()); 122 | router.setError("ABC/D"); // ignore second global route definition 123 | assert.equal(route, router.getError()); 124 | router.setError("admin/error/index"); 125 | assert.equal("admin/error/index", router.getError("admin")); 126 | }); 127 | 128 | 129 | }); 130 | -------------------------------------------------------------------------------- /target/.history: -------------------------------------------------------------------------------- 1 | exit 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "declaration": true, 5 | "target": "es6", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "outDir": "build", 9 | "rootDir": "src", 10 | "sourceMap": true, 11 | "inlineSources": false, 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "removeComments": false, 15 | "noImplicitAny": false, 16 | "typeRoots": [ 17 | "node_modules/@types" 18 | ] 19 | }, 20 | "include": [ 21 | "src/**/*.ts" 22 | ], 23 | "exclude": [ 24 | "node_modules" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /tsd.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v4", 3 | "repo": "borisyankov/DefinitelyTyped", 4 | "ref": "master", 5 | "path": "typings", 6 | "bundle": "typings/tsd.d.ts", 7 | "installed": { 8 | "node/node.d.ts": { 9 | "commit": "70bf7e2bfeb0d5b1b651ef3219bcc65c8eec117e" 10 | }, 11 | "jasmine/jasmine.d.ts": { 12 | "commit": "70bf7e2bfeb0d5b1b651ef3219bcc65c8eec117e" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [ 5 | true, 6 | "check-space" 7 | ], 8 | "curly": true, 9 | "eofline": true, 10 | "forin": true, 11 | "indent": [ 12 | true, 13 | "spaces" 14 | ], 15 | "label-position": true, 16 | "label-undefined": true, 17 | "max-line-length": [ 18 | true, 19 | 160 20 | ], 21 | "member-access": false, 22 | "member-ordering": [ 23 | true, 24 | "public-before-private", 25 | "static-before-instance", 26 | "variables-before-functions" 27 | ], 28 | "no-arg": true, 29 | "no-bitwise": false, 30 | "no-console": [ 31 | true, 32 | "debug", 33 | "info", 34 | "time", 35 | "timeEnd", 36 | "trace" 37 | ], 38 | "no-construct": true, 39 | "no-debugger": true, 40 | "no-duplicate-key": true, 41 | "no-duplicate-variable": true, 42 | "no-empty": true, 43 | "no-eval": true, 44 | "no-inferrable-types": false, 45 | "no-shadowed-variable": true, 46 | "no-string-literal": true, 47 | "no-switch-case-fall-through": true, 48 | "no-trailing-whitespace": true, 49 | "no-unused-expression": true, 50 | "no-unused-variable": true, 51 | "no-unreachable": true, 52 | "no-use-before-declare": true, 53 | "no-var-keyword": true, 54 | "object-literal-sort-keys": false, 55 | "one-line": [ 56 | true, 57 | "check-open-brace", 58 | "check-catch", 59 | "check-else", 60 | "check-finally", 61 | "check-whitespace" 62 | ], 63 | "quotemark": [ 64 | true, 65 | "double", 66 | "avoid-escape" 67 | ], 68 | "radix": true, 69 | "semicolon": true, 70 | "trailing-comma": [ 71 | true, 72 | { 73 | "singleline": "never", 74 | "multiline": "never" 75 | } 76 | ], 77 | "triple-equals": [ 78 | true, 79 | "allow-null-check" 80 | ], 81 | "typedef-whitespace": [ 82 | true, 83 | { 84 | "call-signature": "nospace", 85 | "index-signature": "nospace", 86 | "parameter": "nospace", 87 | "property-declaration": "nospace", 88 | "variable-declaration": "nospace" 89 | } 90 | ], 91 | "variable-name": false, 92 | "whitespace": [ 93 | true, 94 | "check-branch", 95 | "check-decl", 96 | "check-operator", 97 | "check-separator", 98 | "check-type" 99 | ] 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typeix", 3 | "mode": "modules", 4 | "out": "docs/", 5 | "exclude": "**/*.spec.ts", 6 | "theme": "minimal", 7 | "externalPattern": "**/*.d.ts", 8 | "excludeExternals": true, 9 | "ignoreCompilerErrors": "true", 10 | "experimentalDecorators": "true", 11 | "emitDecoratorMetadata": "true", 12 | "hideGenerator": true, 13 | "target": "ES5", 14 | "moduleResolution": "node", 15 | "preserveConstEnums": "true", 16 | "stripInternal": "true", 17 | "suppressExcessPropertyErrors": "true", 18 | "suppressImplicitAnyIndexErrors": "true", 19 | "module": "commonjs" 20 | } 21 | --------------------------------------------------------------------------------