├── .gitignore ├── .npmignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── doc └── CHANGELOG.md ├── package.json ├── src ├── Library │ ├── Application │ │ ├── Application.ts │ │ ├── ApplicationConfigType.ts │ │ ├── ApplicationEvents.ts │ │ ├── ApplicationModes.ts │ │ └── index.ts │ ├── Cli │ │ ├── Cli.ts │ │ ├── CliConfigType.ts │ │ ├── CliService.ts │ │ ├── CliServiceFactory.ts │ │ ├── CliTypes.ts │ │ └── index.ts │ ├── Command │ │ ├── AbstractCommand.ts │ │ ├── CommandManager.ts │ │ ├── CommandManagerConfigType.ts │ │ ├── CommandManagerFactory.ts │ │ ├── HelpCommand.ts │ │ └── index.ts │ ├── Config │ │ ├── Config.ts │ │ ├── ConfigInterface.ts │ │ ├── ControllerManagerConfigType.ts │ │ ├── LoggerConfigInterface.ts │ │ ├── ModuleManagerConfigInterface.ts │ │ ├── ResponseConfigInterface.ts │ │ ├── RouterConfigInterface.ts │ │ ├── ServerConfigInterface.ts │ │ └── index.ts │ ├── Controller │ │ ├── AbstractActionController.ts │ │ ├── ControllerManager.ts │ │ ├── ControllerManagerFactory.ts │ │ ├── ControllerTypes.ts │ │ └── index.ts │ ├── Core │ │ ├── Types.ts │ │ └── index.ts │ ├── Error │ │ ├── InvalidActionResultError.ts │ │ ├── InvalidArgumentError.ts │ │ ├── NotFoundError.ts │ │ └── index.ts │ ├── EventManager │ │ ├── Event.ts │ │ ├── EventManager.ts │ │ ├── EventManagerFactory.ts │ │ ├── EventManagerTypes.ts │ │ ├── SharedEventManager.ts │ │ └── index.ts │ ├── Interface │ │ ├── ContextInterface.ts │ │ └── index.ts │ ├── Logger │ │ ├── LoggerService.ts │ │ ├── LoggerServiceFactory.ts │ │ └── index.ts │ ├── Middleware │ │ ├── AbstractMiddleware.ts │ │ ├── DispatchMiddleware.ts │ │ ├── MiddlewareInterface.ts │ │ ├── MiddlewareTypes.ts │ │ ├── RequestMiddleware.ts │ │ ├── RouterMiddleware.ts │ │ └── index.ts │ ├── ModuleManager │ │ ├── ModuleClassInterface.ts │ │ ├── ModuleInterface.ts │ │ ├── ModuleManager.ts │ │ ├── ModuleManagerEvents.ts │ │ ├── ModuleManagerFactory.ts │ │ └── index.ts │ ├── Output │ │ ├── Output.ts │ │ └── index.ts │ ├── Response │ │ ├── AbstractResponseHelper.ts │ │ ├── ClientErrorResponse.ts │ │ ├── InformationalResponse.ts │ │ ├── RedirectionResponse.ts │ │ ├── Response.ts │ │ ├── ResponseService.ts │ │ ├── ResponseServiceFactory.ts │ │ ├── ResponseStrategies.ts │ │ ├── ResponseTypes.ts │ │ ├── ServerErrorResponse.ts │ │ ├── SuccessfulResponse.ts │ │ └── index.ts │ ├── Router │ │ ├── RegisteredRouteInterface.ts │ │ ├── Route.ts │ │ ├── RouteInterface.ts │ │ ├── RouterService.ts │ │ ├── RouterServiceFactory.ts │ │ └── index.ts │ ├── Server │ │ ├── HttpStatusCodes.ts │ │ ├── RequestMethods.ts │ │ ├── ServerService.ts │ │ ├── ServerServiceFactory.ts │ │ └── index.ts │ ├── ServiceManager │ │ ├── AbstractFileBasedPluginManager.ts │ │ ├── AbstractPluginManager.ts │ │ ├── FactoryInterface.ts │ │ ├── FileBasedPluginManagerConfigType.ts │ │ ├── FileBasedPluginType.ts │ │ ├── InjectedServiceFactory.ts │ │ ├── ServiceManager.ts │ │ ├── ServiceManagerConfigInterface.ts │ │ ├── ServiceManagerInterface.ts │ │ ├── decorators │ │ │ ├── config.ts │ │ │ ├── index.ts │ │ │ ├── inject.ts │ │ │ └── patch.ts │ │ └── index.ts │ └── index.ts ├── config │ ├── cli.ts │ ├── command.ts │ ├── index.ts │ ├── logger.ts │ ├── response.ts │ ├── routes.ts │ ├── server.ts │ └── services.ts ├── debug.ts └── index.ts ├── stix.svg ├── test └── Library │ ├── Config │ └── Config.test.ts │ └── ServiceManager │ └── ServiceManager.test.ts ├── tsconfig.json ├── tslint.json ├── typings └── yargs-parser │ └── index.d.ts └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | rlib-cov 3 | *.seed 4 | *.log 5 | *.out 6 | *.pid 7 | npm-debug.log 8 | *~ 9 | *# 10 | .DS_STORE 11 | .netbeans 12 | nbproject 13 | node_modules 14 | .idea 15 | .node_history 16 | .nyc_output 17 | .vscode 18 | notes.md 19 | _book 20 | .data 21 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | rlib-cov 3 | *.seed 4 | *.log 5 | *.out 6 | *.pid 7 | npm-debug.log 8 | *~ 9 | *# 10 | .DS_STORE 11 | .netbeans 12 | nbproject 13 | node_modules 14 | .idea 15 | .node_history 16 | .vscode 17 | notes.md 18 | .travis.yml 19 | .gitignore 20 | wallaby.js 21 | yarn.lock 22 | .data 23 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We'd love for you to contribute and to make this project even better! 4 | If this interests you, please begin by reading our [contributing guidelines](https://github.com/SpoonX/about/blob/master/CONTRIBUTING.md), which will provide you with all the information you need to get started. 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 SpoonX 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 | 2 |
3 | 4 | # ![Stix](./stix.svg) 5 | 6 | [![Slack Status](https://spoonx-slack.herokuapp.com/badge.svg)](https://spoonx-slack.herokuapp.com) 7 | 8 | A module-based, TypeScript-first Node.js® framework. 9 | 10 | stix quickstart video 11 | 12 | [Documentation](https://stixjs.io/docs/the-basics/about-stix) - [Website](https://stixjs.io/) 13 |
14 | 15 | ## Usage 16 | 17 | All repositories in a nice list. I like lists. 18 | 19 | - [stix-cli](https://github.com/SpoonX/stix-cli): the cli for your project and its own commands. Includes autocomplete. 20 | - [stix-gates](https://github.com/SpoonX/stix-gates): security and enrichments for your endpoints. 21 | - [stix-wetland](https://github.com/SpoonX/stix-wetland): a stix module for Wetland ORM. 22 | - [stix-swagger](https://github.com/SpoonX/stix-swagger): automatically generate swagger docs based on your stix app. 23 | - [stix-skeleton](https://github.com/SpoonX/stix-skeleton): the official stix skeleton, also used by `stix init`. 24 | - [stix-generator](https://github.com/SpoonX/stix-generator): code generators for stix projects. 25 | - [tape-roller](https://github.com/SpoonX/tape-roller): makes manipulating code projects easier with helpers for packages, files and git. 26 | 27 | ## License 28 | 29 | MIT. 30 | -------------------------------------------------------------------------------- /doc/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [4.5.2](https://github.com/SpoonX/stix/compare/v4.5.1...v4.5.2) (2019-03-30) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * **inject:** apply proper metadata hierarchy to prevent unneeded DI ([1647992](https://github.com/SpoonX/stix/commit/1647992)) 7 | 8 | 9 | 10 | ## [4.5.1](https://github.com/SpoonX/stix/compare/v4.5.0...v4.5.1) (2019-03-30) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * **Router:** only remove existing route ([4598ba2](https://github.com/SpoonX/stix/commit/4598ba2)) 16 | 17 | 18 | 19 | # [4.5.0](https://github.com/SpoonX/stix/compare/v4.4.0...v4.5.0) (2019-03-26) 20 | 21 | 22 | ### Bug Fixes 23 | 24 | * **Router:** prevent duplicate route listeners ([ebc8a48](https://github.com/SpoonX/stix/commit/ebc8a48)) 25 | 26 | 27 | ### Features 28 | 29 | * **ControllerManager:** add debug logging for controller manager ([547d39d](https://github.com/SpoonX/stix/commit/547d39d)) 30 | * **ModuleManager:** await init on modules ([833b4d9](https://github.com/SpoonX/stix/commit/833b4d9)) 31 | 32 | 33 | 34 | # [4.4.0](https://github.com/SpoonX/stix/compare/v4.3.2...v4.4.0) (2019-03-26) 35 | 36 | 37 | 38 | ## [4.3.2](https://github.com/SpoonX/stix/compare/v4.3.1...v4.3.2) (2019-01-28) 39 | 40 | 41 | ### Bug Fixes 42 | 43 | * **Config:** gracefully handle incorrect config merges ([ca7d4e2](https://github.com/SpoonX/stix/commit/ca7d4e2)) 44 | 45 | 46 | 47 | 48 | ## [4.3.1](https://github.com/SpoonX/stix/compare/v4.3.0...v4.3.1) (2018-11-22) 49 | 50 | 51 | ### Bug Fixes 52 | 53 | * **Application:** apply user config last without ref ([f892774](https://github.com/SpoonX/stix/commit/f892774)) 54 | 55 | 56 | ### Features 57 | 58 | * **Response:** add data accessor ([1d946ee](https://github.com/SpoonX/stix/commit/1d946ee)) 59 | 60 | 61 | 62 | 63 | # [4.3.0](https://github.com/SpoonX/stix/compare/v4.2.3...v4.3.0) (2018-11-20) 64 | 65 | 66 | ### Features 67 | 68 | * **decorators:** add config decorator ([ea61c09](https://github.com/SpoonX/stix/commit/ea61c09)) 69 | 70 | 71 | 72 | 73 | ## [4.2.3](https://github.com/SpoonX/stix/compare/v4.2.2...v4.2.3) (2018-11-06) 74 | 75 | 76 | 77 | 78 | ## [4.2.2](https://github.com/SpoonX/stix/compare/v4.2.1...v4.2.2) (2018-11-06) 79 | 80 | 81 | ### Bug Fixes 82 | 83 | * **Route:** fix types for route ([4efa06d](https://github.com/SpoonX/stix/commit/4efa06d)) 84 | 85 | 86 | 87 | 88 | ## [4.2.1](https://github.com/SpoonX/stix/compare/v4.2.0...v4.2.1) (2018-11-01) 89 | 90 | 91 | ### Bug Fixes 92 | 93 | * **AbstractFileBasedPluginManager:** graceful continue when no plugins are found ([66e4cfc](https://github.com/SpoonX/stix/commit/66e4cfc)) 94 | * **CommandManager:** gracefully fail when there are no plugins ([85f59a0](https://github.com/SpoonX/stix/commit/85f59a0)) 95 | * **ControllerManager:** gracefully fail when there are no plugins ([601f9b3](https://github.com/SpoonX/stix/commit/601f9b3)) 96 | * **ModuleManager:** gracefully fail when there are no modules ([460be4c](https://github.com/SpoonX/stix/commit/460be4c)) 97 | 98 | 99 | ### Features 100 | 101 | * **project:** add logo ([bc7e112](https://github.com/SpoonX/stix/commit/bc7e112)) 102 | 103 | 104 | 105 | 106 | # [4.2.0](https://github.com/SpoonX/stix/compare/v4.1.7...v4.2.0) (2018-10-07) 107 | 108 | 109 | ### Bug Fixes 110 | 111 | * **Response:** set response type for html ([445c008](https://github.com/SpoonX/stix/commit/445c008)) 112 | 113 | 114 | ### Features 115 | 116 | * **Application:** add environment helper method ([ba59e3e](https://github.com/SpoonX/stix/commit/ba59e3e)) 117 | * **RequestMiddleware:** make patchContext async ([1f1c4a2](https://github.com/SpoonX/stix/commit/1f1c4a2)) 118 | * **Response:** add file and html responses ([682c7af](https://github.com/SpoonX/stix/commit/682c7af)) 119 | * **ServerService:** add SSL support and url helper ([ce80b56](https://github.com/SpoonX/stix/commit/ce80b56)) 120 | 121 | 122 | 123 | 124 | ## [4.1.7](https://github.com/SpoonX/stix/compare/v4.1.6...v4.1.7) (2018-10-06) 125 | 126 | 127 | 128 | 129 | ## [4.1.6](https://github.com/SpoonX/stix/compare/v4.1.5...v4.1.6) (2018-10-06) 130 | 131 | 132 | ### Features 133 | 134 | * **Output:** add multiple pieces of data at once ([2163981](https://github.com/SpoonX/stix/commit/2163981)) 135 | 136 | 137 | 138 | 139 | ## [4.1.5](https://github.com/SpoonX/stix/compare/v4.1.4...v4.1.5) (2018-10-04) 140 | 141 | 142 | ### Bug Fixes 143 | 144 | * **CliService:** properly parse arguments ([cfc7aaa](https://github.com/SpoonX/stix/commit/cfc7aaa)) 145 | 146 | 147 | 148 | 149 | ## [4.1.4](https://github.com/SpoonX/stix/compare/v4.1.3...v4.1.4) (2018-10-04) 150 | 151 | 152 | ### Bug Fixes 153 | 154 | * **AbstractFileBasedPluginManager:** fix regex to also match files not ending in a d ([8299d65](https://github.com/SpoonX/stix/commit/8299d65)) 155 | 156 | 157 | 158 | 159 | ## [4.1.3](https://github.com/SpoonX/stix/compare/v4.1.2...v4.1.3) (2018-10-04) 160 | 161 | 162 | ### Bug Fixes 163 | 164 | * **AbstractFileBasedPluginManager:** fix regex to also match files ending in a d ([b62028c](https://github.com/SpoonX/stix/commit/b62028c)) 165 | * **Application:** actually set application mode ([cf5cc97](https://github.com/SpoonX/stix/commit/cf5cc97)) 166 | * **CommandManager:** fix typings for getCommand ([a32ae6d](https://github.com/SpoonX/stix/commit/a32ae6d)) 167 | * **Output:** call toString on all tables for output ([dc7a509](https://github.com/SpoonX/stix/commit/dc7a509)) 168 | * **Output:** call toString on table for output ([65866ea](https://github.com/SpoonX/stix/commit/65866ea)) 169 | 170 | 171 | ### Features 172 | 173 | * **CliService:** validate and pass options ([33fafcc](https://github.com/SpoonX/stix/commit/33fafcc)) 174 | 175 | 176 | 177 | 178 | ## [4.1.2](https://github.com/SpoonX/stix/compare/v4.1.1...v4.1.2) (2018-09-30) 179 | 180 | 181 | ### Bug Fixes 182 | 183 | * **Middleware:** fix typings for middleware ([e19a606](https://github.com/SpoonX/stix/commit/e19a606)) 184 | 185 | 186 | 187 | 188 | ## [4.1.1](https://github.com/SpoonX/stix/compare/v4.1.0...v4.1.1) (2018-09-30) 189 | 190 | 191 | 192 | 193 | # [4.1.0](https://github.com/SpoonX/stix/compare/v4.0.1...v4.1.0) (2018-09-30) 194 | 195 | 196 | ### Features 197 | 198 | * **project:** add AbstractResponseHelper ([bea2b59](https://github.com/SpoonX/stix/commit/bea2b59)) 199 | 200 | 201 | 202 | 203 | ## [4.0.1](https://github.com/SpoonX/stix/compare/v4.0.0...v4.0.1) (2018-09-30) 204 | 205 | 206 | ### Bug Fixes 207 | 208 | * **typings:** add [@types](https://github.com/types) to typeRoots ([7f8fd13](https://github.com/SpoonX/stix/commit/7f8fd13)) 209 | 210 | 211 | 212 | 213 | # [4.0.0](https://github.com/SpoonX/stix/compare/v3.0.1...v4.0.0) (2018-09-30) 214 | 215 | 216 | ### Features 217 | 218 | * **Application:** allow application to be booted without starting ([47fb705](https://github.com/SpoonX/stix/commit/47fb705)) 219 | * **Config:** deduping of arrays in patch ([7bbef0c](https://github.com/SpoonX/stix/commit/7bbef0c)) 220 | * **Error:** add InvalidArgumentError ([14f9421](https://github.com/SpoonX/stix/commit/14f9421)) 221 | * **Output:** add Output class for commands ([36526f4](https://github.com/SpoonX/stix/commit/36526f4)) 222 | * **project:** add CLI support ([13fb231](https://github.com/SpoonX/stix/commit/13fb231)) 223 | * **project:** add commands for CLI ([eb555ae](https://github.com/SpoonX/stix/commit/eb555ae)) 224 | * **project:** add default command config with invokable command ([47a2c9a](https://github.com/SpoonX/stix/commit/47a2c9a)) 225 | * **project:** add default help command for cli ([29bb39e](https://github.com/SpoonX/stix/commit/29bb39e)) 226 | * **project:** add middleware classes ([c25d6e0](https://github.com/SpoonX/stix/commit/c25d6e0)) 227 | * **ServiceManager:** and and implement file based plugin manager ([5e5d5ed](https://github.com/SpoonX/stix/commit/5e5d5ed)) 228 | 229 | 230 | 231 | 232 | ## [3.0.1](https://github.com/SpoonX/stix/compare/v3.0.0...v3.0.1) (2018-09-25) 233 | 234 | 235 | ### Features 236 | 237 | * **project:** add support for running in ts-node ([d892ba7](https://github.com/SpoonX/stix/commit/d892ba7)) 238 | 239 | 240 | 241 | 242 | # [3.0.0](https://github.com/SpoonX/stix/compare/v2.0.0...v3.0.0) (2018-09-24) 243 | 244 | 245 | ### Code Refactoring 246 | 247 | * **ControllerManager:** force the default use of references ([d266f90](https://github.com/SpoonX/stix/commit/d266f90)) 248 | 249 | 250 | ### Features 251 | 252 | * **project:** added [@inject](https://github.com/inject) and [@patch](https://github.com/patch) decorators for greatly simplified DI ([b8305bf](https://github.com/SpoonX/stix/commit/b8305bf)) 253 | * **project:** expose namespaced debug logger for modules ([34e6fc9](https://github.com/SpoonX/stix/commit/34e6fc9)) 254 | * **Router:** export registered routes ([13805e0](https://github.com/SpoonX/stix/commit/13805e0)) 255 | * **ServiceManager:** implement InjectedServiceFactory for [@inject](https://github.com/inject) DI ([e6e179d](https://github.com/SpoonX/stix/commit/e6e179d)) 256 | 257 | 258 | ### BREAKING CHANGES 259 | 260 | * **ControllerManager:** this change now registers controllers that were loaded from directories by reference, as invokables. This was needed to cater to Injectables. If you didn't use the recommended reference pattern for controllers, this means you'll have to add aliases for your controllers. 261 | 262 | 263 | 264 | 265 | # [2.0.0](https://github.com/SpoonX/stix/compare/v1.0.2...v2.0.0) (2018-09-22) 266 | 267 | 268 | ### Features 269 | 270 | * **Config:** implement formalized Config service ([9f9ad5f](https://github.com/SpoonX/stix/commit/9f9ad5f)) 271 | * **Controller:** load from multiple locations ([069edd5](https://github.com/SpoonX/stix/commit/069edd5)) 272 | * **Logger:** create EventManager ([b61522b](https://github.com/SpoonX/stix/commit/b61522b)) 273 | * **Logger:** create Logger service ([dc5afc2](https://github.com/SpoonX/stix/commit/dc5afc2)) 274 | * **ModuleManager:** implement formalized module system ([6af926e](https://github.com/SpoonX/stix/commit/6af926e)) 275 | * **project:** add core types ([4e44d2f](https://github.com/SpoonX/stix/commit/4e44d2f)) 276 | * **Server:** add robust middleware support ([6996b9c](https://github.com/SpoonX/stix/commit/6996b9c)) 277 | * **ServiceManager:** fix typings and add support for shareable services ([6b12b66](https://github.com/SpoonX/stix/commit/6b12b66)) 278 | 279 | 280 | 281 | 282 | ## [1.0.2](https://github.com/SpoonX/stix/compare/v1.0.1...v1.0.2) (2018-09-21) 283 | 284 | 285 | ### Bug Fixes 286 | 287 | * **project:** fix typing locations ([5a6d150](https://github.com/SpoonX/stix/commit/5a6d150)) 288 | 289 | 290 | 291 | 292 | ## [1.0.1](https://github.com/SpoonX/stix/compare/v1.0.0...v1.0.1) (2018-09-21) 293 | 294 | 295 | 296 | 297 | # [1.0.0](https://github.com/SpoonX/stix/compare/v0.2.2...v1.0.0) (2018-09-21) 298 | 299 | 300 | ### Features 301 | 302 | * **project:** setup tests and add config with Map support ([3a492b4](https://github.com/SpoonX/stix/commit/3a492b4)) 303 | * **ServiceManager:** implement the service manager ([d778841](https://github.com/SpoonX/stix/commit/d778841)) 304 | 305 | 306 | 307 | 308 | ## [0.2.2](https://github.com/SpoonX/stix/compare/v0.2.1...v0.2.2) (2018-09-18) 309 | 310 | 311 | ### Bug Fixes 312 | 313 | * **Config:** add missing RoutesConfigInterface export ([80cec7b](https://github.com/SpoonX/stix/commit/80cec7b)) 314 | 315 | 316 | 317 | 318 | ## [0.2.1](https://github.com/SpoonX/stix/compare/v0.2.0...v0.2.1) (2018-09-18) 319 | 320 | 321 | ### Bug Fixes 322 | 323 | * **middleware:** fix Middleware typing ([4dce743](https://github.com/SpoonX/stix/commit/4dce743)) 324 | 325 | 326 | 327 | 328 | # [0.2.0](https://github.com/SpoonX/stix/compare/v0.1.3...v0.2.0) (2018-09-18) 329 | 330 | 331 | 332 | 333 | ## [0.1.3](https://github.com/SpoonX/stix/compare/v0.1.2...v0.1.3) (2018-09-18) 334 | 335 | 336 | 337 | 338 | ## [0.1.2](https://github.com/SpoonX/stix/compare/v0.1.1...v0.1.2) (2018-09-18) 339 | 340 | 341 | 342 | 343 | ## [0.1.1](https://github.com/SpoonX/stix/compare/v0.1.0...v0.1.1) (2018-09-18) 344 | 345 | 346 | ### Bug Fixes 347 | 348 | * **Library:** add Module exports and use ControllerType ([c2f2553](https://github.com/SpoonX/stix/commit/c2f2553)) 349 | 350 | 351 | 352 | 353 | # [0.1.0](https://github.com/SpoonX/stix/compare/v0.0.4...v0.1.0) (2018-09-18) 354 | 355 | 356 | ### Features 357 | 358 | * **Controller:** expose ControllerType ([9de95c8](https://github.com/SpoonX/stix/commit/9de95c8)) 359 | 360 | 361 | 362 | 363 | ## [0.0.4](https://github.com/SpoonX/stix/compare/v0.0.3...v0.0.4) (2018-09-18) 364 | 365 | 366 | 367 | 368 | ## [0.0.3](https://github.com/SpoonX/stix/compare/v0.0.2...v0.0.3) (2018-09-18) 369 | 370 | 371 | 372 | 373 | ## [0.0.2](https://github.com/SpoonX/stix/compare/v0.0.1...v0.0.2) (2018-09-18) 374 | 375 | 376 | 377 | 378 | ## 0.0.1 (2018-09-18) 379 | 380 | 381 | 382 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stix", 3 | "version": "4.5.2", 4 | "description": "A module-based, TypeScript-first Node.js® framework.", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/SpoonX/stix.git" 8 | }, 9 | "author": "RWOverdijk ", 10 | "main": "./dist/index.js", 11 | "files": [ 12 | "dist" 13 | ], 14 | "types": "dist/index.d.ts", 15 | "keywords": [ 16 | "api", 17 | "framework", 18 | "typescript", 19 | "tsc", 20 | "koa", 21 | "koajs", 22 | "node", 23 | "wetland" 24 | ], 25 | "bugs": { 26 | "url": "https://github.com/SpoonX/stix/issues" 27 | }, 28 | "homepage": "https://github.com/SpoonX/stix", 29 | "scripts": { 30 | "test": "jest", 31 | "build": "tsc --build tsconfig.json", 32 | "dev": "tsc --build -w tsconfig.json", 33 | "prepare": "yarn build", 34 | "version": "conventional-changelog -p angular -i doc/CHANGELOG.md -s && git add -A doc/CHANGELOG.md", 35 | "postpublish": "git push upstream master && git push upstream --tags" 36 | }, 37 | "jest": { 38 | "transform": { 39 | "^.+\\.tsx?$": "ts-jest" 40 | }, 41 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(ts?)$", 42 | "moduleFileExtensions": [ 43 | "ts", 44 | "js" 45 | ] 46 | }, 47 | "devDependencies": { 48 | "@types/jest": "^24.0.19", 49 | "jest": "^24.9.0", 50 | "ts-jest": "^24.1.0", 51 | "tslint": "^5.11.0", 52 | "tslint-config-prettier": "^1.15.0", 53 | "tslint-eslint-rules": "^5.4.0", 54 | "typescript": "^3.0.3" 55 | }, 56 | "dependencies": { 57 | "@koa/cors": "^2.2.2", 58 | "@types/bytes": "^3.0.0", 59 | "@types/cli-table": "^0.3.0", 60 | "@types/clone": "^0.1.30", 61 | "@types/command-line-usage": "^5.0.1", 62 | "@types/debug": "^0.0.30", 63 | "@types/koa": "^2.0.46", 64 | "@types/koa-bodyparser": "^5.0.1", 65 | "@types/koa-send": "^4.1.1", 66 | "@types/koa__cors": "^2.2.3", 67 | "@types/node": "^10.9.4", 68 | "@types/prettyjson": "^0.0.28", 69 | "chalk": "^2.4.1", 70 | "cli-table": "^0.3.1", 71 | "clone": "^2.1.2", 72 | "command-line-usage": "^5.0.5", 73 | "debug": "^4.0.1", 74 | "koa": "^2.5.3", 75 | "koa-bodyparser": "^4.2.1", 76 | "koa-send": "^5.0.0", 77 | "path-to-regexp": "^2.4.0", 78 | "pretty-error": "^2.1.1", 79 | "prettyjson": "^1.2.1", 80 | "reflect-metadata": "^0.1.12", 81 | "winston": "^3.1.0", 82 | "yargs-parser": "^10.1.0" 83 | }, 84 | "license": "MIT" 85 | } 86 | -------------------------------------------------------------------------------- /src/Library/Application/Application.ts: -------------------------------------------------------------------------------- 1 | import clone from 'clone'; 2 | import { ServerService } from '../Server'; 3 | import { ModuleManager, ModuleManagerFactory } from '../ModuleManager'; 4 | import { Config, ConfigType } from '../Config'; 5 | import { FactoryInterface, ServiceManager } from '../ServiceManager'; 6 | import { EventManager, EventManagerFactory, SharedEventManager } from '../EventManager'; 7 | import { ApplicationEvents } from './ApplicationEvents'; 8 | import { Instantiable } from '../Core'; 9 | import { createDebugLogger } from '../../debug'; 10 | import * as defaultConfig from '../../config'; 11 | import { CliService } from '../Cli'; 12 | import { ApplicationModes } from './ApplicationModes'; 13 | 14 | const debug = createDebugLogger('application'); 15 | 16 | export class Application { 17 | private mode: ApplicationModes; 18 | 19 | private readonly environment: string = process.env.NODE_ENV || 'development'; 20 | 21 | private readonly config: Config; 22 | 23 | private readonly serviceManager: ServiceManager; 24 | 25 | private readonly applicationConfigs: ConfigType[]; 26 | 27 | private moduleManager: ModuleManager; 28 | 29 | private sharedEventManager: SharedEventManager; 30 | 31 | public constructor (...appConfigs: ConfigType[]) { 32 | this.applicationConfigs = appConfigs; 33 | this.config = new Config(defaultConfig, ...clone(this.applicationConfigs)); 34 | this.serviceManager = new ServiceManager({ 35 | aliases: { config: Config, sharedEventManager: SharedEventManager }, 36 | invokables: new Map, Instantiable>([ 37 | [ SharedEventManager, SharedEventManager ], 38 | ]), 39 | factories: new Map([ 40 | [ ModuleManager, ModuleManagerFactory ], 41 | [ EventManager, EventManagerFactory ], 42 | ]), 43 | services: new Map([ 44 | [ Config, this.config ], 45 | [ Application, this ], 46 | ]), 47 | shared: new Map([ 48 | [ EventManager, false ], 49 | ]), 50 | }); 51 | } 52 | 53 | public getMode (): ApplicationModes { 54 | return this.mode; 55 | } 56 | 57 | public getServiceManager (): ServiceManager { 58 | return this.serviceManager; 59 | } 60 | 61 | private async bootstrap (mode: ApplicationModes, loadOnly: boolean = false): Promise { 62 | const config = this.config; 63 | 64 | // Make the module manager. Only one level is allowed to specify module configs.. 65 | this.moduleManager = this.serviceManager.get(ModuleManager); 66 | 67 | // Initialize module manager. Only calls getConfig() 68 | await this.moduleManager.loadModules(config.of('modules')); 69 | 70 | // Now let's patch on the user config once more, to ensure dominance. 71 | this.config.merge(...this.applicationConfigs); 72 | 73 | // Now that we have all the configs, register the services. 74 | this.serviceManager.configure(config.of('services')); 75 | 76 | // go forth and create all core services. 77 | this.sharedEventManager = this.serviceManager.get(SharedEventManager); 78 | 79 | if (mode === ApplicationModes.Cli) { 80 | await this.bootstrapCli(); 81 | } else { 82 | this.bootstrapServer(); 83 | } 84 | 85 | // Don't start the application. We're probably in CLI mode. 86 | if (loadOnly) { 87 | return this; 88 | } 89 | 90 | return await this.start(); 91 | } 92 | 93 | public async start (): Promise { 94 | // Cool cool. Bootstrap the modules, because they can now get all the things. 95 | await this.moduleManager.bootstrap(); 96 | 97 | // Allow listeners to do some work before starting the server. 98 | await this.sharedEventManager.trigger(ApplicationEvents.Ready, this); 99 | 100 | return this; 101 | } 102 | 103 | private async bootstrapCli () { 104 | const cliService = await this.serviceManager.get(CliService); 105 | 106 | this.sharedEventManager.attachOnce(ApplicationEvents.Ready, () => { 107 | cliService.execute(process.argv.slice(2)); 108 | }); 109 | } 110 | 111 | private bootstrapServer () { 112 | const serverService = this.serviceManager.get(ServerService); 113 | 114 | this.sharedEventManager.attachOnce(ApplicationEvents.Ready, () => { 115 | serverService.start(); 116 | }); 117 | } 118 | 119 | public getEnvironment (): string { 120 | return this.environment; 121 | } 122 | 123 | public isProduction (): boolean { 124 | return this.getEnvironment() === 'production'; 125 | } 126 | 127 | public async launch (mode: ApplicationModes = ApplicationModes.Server, loadOnly: boolean = false): Promise { 128 | this.config.merge({ application: { mode } }); 129 | 130 | this.mode = mode; 131 | 132 | debug(`Launching in ${mode} mode`); 133 | 134 | await this.bootstrap(mode, loadOnly); 135 | 136 | debug('Application ready.'); 137 | 138 | return this; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Library/Application/ApplicationConfigType.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationModes } from './ApplicationModes'; 2 | 3 | export type ApplicationConfigType = { 4 | mode: ApplicationModes; 5 | }; 6 | -------------------------------------------------------------------------------- /src/Library/Application/ApplicationEvents.ts: -------------------------------------------------------------------------------- 1 | export enum ApplicationEvents { 2 | Ready = 'Application.Ready', 3 | } 4 | -------------------------------------------------------------------------------- /src/Library/Application/ApplicationModes.ts: -------------------------------------------------------------------------------- 1 | export enum ApplicationModes { 2 | Server = 'server', 3 | Cli = 'cli', 4 | } 5 | -------------------------------------------------------------------------------- /src/Library/Application/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Application'; 2 | export * from './ApplicationEvents'; 3 | export * from './ApplicationModes'; 4 | export * from './ApplicationConfigType'; 5 | -------------------------------------------------------------------------------- /src/Library/Cli/Cli.ts: -------------------------------------------------------------------------------- 1 | import { CliCommandConfigType, CliCommandType, CliProgramConfigType, CliProgramType } from './CliTypes'; 2 | import { AbstractCommand } from '../Command/AbstractCommand'; 3 | 4 | export class Cli { 5 | /** 6 | * Convenience method to help format a command. 7 | * 8 | * ┌─1─┐┌──2────┐ ┌4┐ 9 | * stix generate module user -v 10 | * │ └───────3─────────────┘ 11 | * └─────────5────────────────┘ 12 | * 13 | * Legend: 14 | * 1. Command name Maps to Command 15 | * 2. Token Maps to action 16 | * 3. Arguments Passed to the action 17 | * 4. Options Passed to the action as part of arguments 18 | * 5. Command line The full command line, maps to command. 19 | * 20 | * @param {string} commandLine 21 | * @param {AbstractCommand} Command 22 | * @param {string} action 23 | * @param {CliCommandConfigType} config 24 | * 25 | * @return {CliCommandType} 26 | */ 27 | public static command (commandLine: string, Command: typeof AbstractCommand, action: string, config?: CliCommandConfigType): CliCommandType { 28 | return { commandLine, Command, action, config }; 29 | } 30 | 31 | public static program (program: string, config?: CliProgramConfigType): CliProgramType { 32 | return { program, config }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Library/Cli/CliConfigType.ts: -------------------------------------------------------------------------------- 1 | import { CliCommandType, CliProgramType } from './CliTypes'; 2 | 3 | export type CliConfigType = Partial<{ 4 | bin: string; 5 | title: string; 6 | subtitle: string; 7 | fallbackToken: string; 8 | defaultProgramName: string; 9 | commands: Array; 10 | }>; 11 | -------------------------------------------------------------------------------- /src/Library/Cli/CliService.ts: -------------------------------------------------------------------------------- 1 | import parser from 'yargs-parser'; 2 | import { CliConfigType } from './CliConfigType'; 3 | import { 4 | CliCommandType, 5 | CliProgramConfigType, 6 | CliProgramType, 7 | ParsedCommandType, 8 | ProcessedProgramType, 9 | } from './CliTypes'; 10 | import { CommandManager } from '../Command'; 11 | import { Output } from '../Output'; 12 | 13 | export class CliService { 14 | private readonly config: CliConfigType; 15 | 16 | private programs: { [program: string]: ProcessedProgramType } = {}; 17 | 18 | /** 19 | * Lookup index linking to commands in programs. 20 | */ 21 | private commands: { [token: string]: CliCommandType } = {}; 22 | 23 | private commandManager: CommandManager; 24 | 25 | constructor (commandManager: CommandManager, config: CliConfigType = {}) { 26 | this.config = config; 27 | this.commandManager = commandManager; 28 | 29 | if (config.commands) { 30 | this.registerPrograms(config.commands); 31 | } 32 | } 33 | 34 | resolveToken (args: { _: string[], [key: string]: string[] | string }) { 35 | if (args.help) { 36 | return this.config.fallbackToken; 37 | } 38 | 39 | return args._.shift() || this.config.fallbackToken; 40 | } 41 | 42 | public async execute (argv: string[]) { 43 | const output = await this.resolve(argv); 44 | 45 | output.send(); 46 | } 47 | 48 | private async resolve (argv: string[]): Promise { 49 | const parsed = parser(argv, { alias: { h: 'help' } }); 50 | const token = this.resolveToken(parsed); 51 | const command = this.commands[token] as CliCommandType; 52 | const output = new Output(); 53 | 54 | // Remove the token so we're left with the arguments. 55 | if (token === argv[0]) { 56 | argv.splice(0, 1); 57 | } 58 | 59 | if (!command) { 60 | return output.error(`Unknown command "${token}".`); 61 | } 62 | 63 | const commandInstance = this.commandManager.getCommand(command.Command) as { [key: string]: Function }; 64 | 65 | try { 66 | await commandInstance[command.action](output, this.validate(argv, command)); 67 | } catch (error) { 68 | output.error(error); 69 | } 70 | 71 | return output; 72 | } 73 | 74 | private validate (argv: string[], command: CliCommandType) { 75 | const alias = this.collectAliases(command); 76 | const parsed = parser(argv, { alias }); 77 | 78 | const args: { [key: string]: string } = {}; 79 | 80 | if (Array.isArray(command.args)) { 81 | command.args.forEach(({ required, name }: { name: string, required: boolean }, index: number) => { 82 | if (required && !parsed._[index]) { 83 | throw `Missing required argument "${name}".`; 84 | } 85 | 86 | if (parsed._[index]) { 87 | args[name] = parsed._[index]; 88 | } 89 | }); 90 | } 91 | 92 | if (!command.config || !command.config.options) { 93 | return args; 94 | } 95 | 96 | Object.keys(command.config.options).forEach(option => { 97 | if (command.config.options[option].required && !parsed[option]) { 98 | throw `Missing required option "${option}".`; 99 | } 100 | 101 | args[option] = parsed[option]; 102 | }); 103 | 104 | return args; 105 | } 106 | 107 | private collectAliases ({ config }: CliCommandType) { 108 | if (!config || !config.options) { 109 | return {}; 110 | } 111 | 112 | return Object.keys(config.options).reduce((aliases: { [key: string]: string }, name: string) => { 113 | if (config.options[name].alias) { 114 | aliases[name] = config.options[name].alias; 115 | } 116 | 117 | return aliases; 118 | }, {}); 119 | } 120 | 121 | public getPrograms (): { [program: string]: ProcessedProgramType } { 122 | return this.programs; 123 | } 124 | 125 | public getCommands (): { [token: string]: CliCommandType } { 126 | return this.commands; 127 | } 128 | 129 | public getCommand (command: string): CliCommandType { 130 | return this.commands[command]; 131 | } 132 | 133 | public getConfig (): CliConfigType { 134 | return this.config; 135 | } 136 | 137 | private registerPrograms (programs: Array) { 138 | programs.forEach((programEntry: CliProgramType | CliCommandType) => { 139 | 140 | const programName = (programEntry as CliProgramType).program || this.config.defaultProgramName; 141 | const programConfig = (programEntry.config || { commands: [ programEntry ] }) as CliProgramConfigType; 142 | this.registerProgram(programName, programConfig); 143 | }); 144 | } 145 | 146 | private registerProgram (program: string, { commands, examples }: CliProgramConfigType) { 147 | if (!this.programs[program]) { 148 | this.programs[program] = { program, examples, commands: {} }; 149 | } 150 | 151 | commands.forEach((command: CliCommandType) => { 152 | const { token, args } = this.queParser(command.commandLine); 153 | 154 | command.args = args; 155 | command.token = token; 156 | 157 | this.programs[program].commands[token] = command; 158 | this.commands[token] = command; 159 | }); 160 | } 161 | 162 | private queParser (commandLine: string): ParsedCommandType { 163 | return commandLine.split(' ').reduce((parsed: ParsedCommandType, value) => { 164 | if (value[0] === '<' && value[value.length - 1] === '>') { 165 | parsed.args.push({ required: true, name: value.slice(1, -1) }); 166 | } else if (value[0] === '[' && value[value.length - 1] === ']') { 167 | parsed.args.push({ required: false, name: value.slice(1, -1) }); 168 | } else { 169 | parsed.token = value; 170 | } 171 | 172 | return parsed; 173 | }, { args: [] }) as ParsedCommandType; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/Library/Cli/CliServiceFactory.ts: -------------------------------------------------------------------------------- 1 | import { FactoryInterface, ServiceManager } from '../ServiceManager'; 2 | import { Config } from '../Config'; 3 | import { CliService } from './CliService'; 4 | import { CliConfigType } from './CliConfigType'; 5 | import { CommandManager } from '../Command'; 6 | 7 | export const CliServiceFactory: FactoryInterface = (sm: ServiceManager) => { 8 | return new CliService(sm.get(CommandManager), sm.get(Config).of('cli')); 9 | }; 10 | -------------------------------------------------------------------------------- /src/Library/Cli/CliTypes.ts: -------------------------------------------------------------------------------- 1 | import { AbstractCommand } from '../Command/AbstractCommand'; 2 | 3 | export type CliCommandType = Partial<{ 4 | token: string; 5 | commandLine: string; 6 | Command: typeof AbstractCommand; 7 | examples: string[]; 8 | action: string; 9 | args: { name: string, required: boolean }[]; 10 | config: CliCommandConfigType; 11 | }>; 12 | 13 | export type CliCommandConfigType = Partial<{ 14 | description: string; 15 | examples: string[]; 16 | options: CliCommandOptionsType; 17 | }>; 18 | 19 | export type CliCommandOptionsType = Partial<{ 20 | [option: string]: CliCommandOptionType; 21 | }>; 22 | 23 | export type CliCommandOptionType = Partial<{ 24 | alias: string; 25 | value: string; 26 | description: string; 27 | defaultTo: any 28 | required: boolean; 29 | }>; 30 | 31 | export type CliProgramType = Partial<{ 32 | program: string; 33 | config: CliProgramConfigType; 34 | }>; 35 | 36 | export type ParsedCommandType = { args: { required: boolean; name: string }[]; token: string; }; 37 | 38 | export type ProcessedProgramType = { program: string; examples: string[]; commands: ProcessedCommandsType; }; 39 | 40 | export type ProcessedCommandsType = { [ command: string ]: CliCommandType }; 41 | 42 | export type CliProgramConfigType = Partial<{ 43 | examples: string[]; 44 | commands: CliCommandType[]; 45 | }>; 46 | -------------------------------------------------------------------------------- /src/Library/Cli/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CliConfigType'; 2 | export * from './CliService'; 3 | export * from './Cli'; 4 | export * from './CliServiceFactory'; 5 | export * from './CliTypes'; 6 | -------------------------------------------------------------------------------- /src/Library/Command/AbstractCommand.ts: -------------------------------------------------------------------------------- 1 | export abstract class AbstractCommand { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /src/Library/Command/CommandManager.ts: -------------------------------------------------------------------------------- 1 | import { CommandManagerConfigType } from './CommandManagerConfigType'; 2 | import { ServiceManager, AbstractFileBasedPluginManager } from '../ServiceManager'; 3 | import { Instantiable } from '../Core'; 4 | import { AbstractCommand } from './AbstractCommand'; 5 | 6 | export class CommandManager extends AbstractFileBasedPluginManager { 7 | constructor (creationContext: ServiceManager, config: CommandManagerConfigType = {}) { 8 | const { locations, commands } = config; 9 | 10 | super(creationContext, locations, commands); 11 | } 12 | 13 | public getCommand (Command: typeof AbstractCommand): AbstractCommand { 14 | return this.getPlugin(Command as Instantiable); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Library/Command/CommandManagerConfigType.ts: -------------------------------------------------------------------------------- 1 | import { ServiceManagerConfigType } from '../ServiceManager'; 2 | 3 | export type CommandManagerConfigType = Partial<{ 4 | locations: string[]; 5 | commands: ServiceManagerConfigType; 6 | }>; 7 | -------------------------------------------------------------------------------- /src/Library/Command/CommandManagerFactory.ts: -------------------------------------------------------------------------------- 1 | import { FactoryInterface, ServiceManager } from '../ServiceManager'; 2 | import { CommandManager } from './CommandManager'; 3 | import { CommandManagerConfigType } from './CommandManagerConfigType'; 4 | import { Config } from '../Config'; 5 | 6 | export const CommandManagerFactory: FactoryInterface = (sm: ServiceManager) => { 7 | return new CommandManager(sm, sm.get(Config).of('command')); 8 | }; 9 | -------------------------------------------------------------------------------- /src/Library/Command/HelpCommand.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import commandLineUsage from 'command-line-usage'; 3 | import { AbstractCommand } from './AbstractCommand'; 4 | import { CliCommandOptionsType, CliCommandType, CliService, ProcessedProgramType } from '../Cli'; 5 | import { inject } from '../ServiceManager/decorators'; 6 | import { Output } from '../Output'; 7 | 8 | export class HelpCommand extends AbstractCommand { 9 | @inject(CliService) 10 | private cliService: CliService; 11 | 12 | output (output: Output, params: { command?: string }) { 13 | const config = this.cliService.getConfig(); 14 | const programs = this.cliService.getPrograms(); 15 | 16 | if (params.command) { 17 | const command = this.cliService.getCommand(params.command); 18 | 19 | if (!command) { 20 | return output.error(`Unknown command "${params.command}".`); 21 | } 22 | 23 | return output.addData(commandLineUsage(this.renderUsage(command))); 24 | } 25 | 26 | const sections: { [ key: string ]: any } = [ 27 | { header: config.title, content: `{italic ${config.subtitle}}` }, 28 | { 29 | header: chalk.yellow('Available commands:'), 30 | }, 31 | ]; 32 | 33 | const collectedExamples: string[] = []; 34 | 35 | Object.values(programs).forEach(({ program, commands, examples }: ProcessedProgramType) => { 36 | sections.push({ content: chalk.yellow(program), raw: true }); 37 | sections.push({ content: Object.keys(commands).map((command: string) => this.renderCommand(commands[ command ])) }); 38 | 39 | if (Array.isArray(examples)) { 40 | collectedExamples.push(...examples); 41 | } 42 | }); 43 | 44 | if (collectedExamples) { 45 | sections.push(this.renderExamples(collectedExamples)); 46 | } 47 | 48 | output.addData(commandLineUsage(sections)); 49 | } 50 | 51 | private renderUsage (command: CliCommandType): { [ key: string ]: any } { 52 | 53 | const sections: { [ key: string ]: any } = [ 54 | { header: chalk.yellow('Usage'), content: [ this.renderCommand(command) ] }, 55 | ]; 56 | 57 | if (command.config) { 58 | if (command.config.options) { 59 | sections.push(this.renderOptions(command.config.options)); 60 | } 61 | 62 | if (command.config.examples) { 63 | sections.push(this.renderExamples(command.config.examples)); 64 | } 65 | } 66 | 67 | return sections; 68 | } 69 | 70 | private renderOptions (options: CliCommandOptionsType) { 71 | return { 72 | header: chalk.yellow('Options:'), 73 | optionList: Object.keys(options).map(optionName => { 74 | const { alias, description, value } = options[optionName]; 75 | 76 | return { 77 | description, 78 | alias: chalk.green(alias), 79 | name: chalk.green(optionName), 80 | typeLabel: value && `{underline ${value}}`, 81 | }; 82 | }), 83 | }; 84 | } 85 | 86 | private renderExamples (examples: string[]) { 87 | return { 88 | header: chalk.yellow('Examples:'), 89 | content: examples.map(description => ({ description })), 90 | }; 91 | } 92 | 93 | private renderCommand ({ commandLine, config }: CliCommandType) { 94 | return { name: chalk.green(commandLine), description: (config && config.description) || '{italic No description found}' }; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Library/Command/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AbstractCommand'; 2 | export * from './CommandManager'; 3 | export * from './CommandManagerConfigType'; 4 | export * from './CommandManagerFactory'; 5 | export * from './HelpCommand'; 6 | -------------------------------------------------------------------------------- /src/Library/Config/Config.ts: -------------------------------------------------------------------------------- 1 | interface ConfigData { [key: string]: any; } 2 | 3 | export class Config { 4 | private data: ConfigData = {}; 5 | 6 | constructor (...data: Array) { 7 | this.merge(...data); 8 | } 9 | 10 | of (section: string): T { 11 | return this.data[section]; 12 | } 13 | 14 | all (): ConfigData { 15 | return this.data; 16 | } 17 | 18 | merge (...toMerge: Array | ConfigData>) { 19 | Config.merge(this.data, ...toMerge); 20 | } 21 | 22 | static merge (...toMerge: Array | ConfigData>) { 23 | const target = toMerge.shift(); 24 | 25 | toMerge.forEach(other => Config.patch(target, other)); 26 | } 27 | 28 | static mergeObject (target: ConfigData, other: ConfigData) { 29 | Object.keys(other).reduce((baseData: ConfigData, targetKey: string) => { 30 | baseData[targetKey] = Config.patch(baseData[targetKey], other[targetKey]); 31 | 32 | return baseData; 33 | }, target); 34 | 35 | return target; 36 | } 37 | 38 | static mergeMap (target: Map, other: Map) { 39 | other.forEach((value, key) => { 40 | target.set(key, Config.patch(target.get(key), value)); 41 | }); 42 | 43 | return target; 44 | } 45 | 46 | static patch (base: any, value: any) { 47 | // Do not merge if either side is empty. 48 | if (!value || !base) { 49 | return value; 50 | } 51 | 52 | if (typeof value === 'object' && typeof base === 'object') { 53 | if (Array.isArray(value) && Array.isArray(base)) { 54 | value.forEach(chunk => { 55 | 56 | // Dedupe arrays on merge. 57 | if (base.indexOf(chunk) === -1) { 58 | base.push(chunk); 59 | } 60 | }); 61 | 62 | return base; 63 | } 64 | 65 | if (base.constructor === Object && value.constructor === Object) { 66 | return Config.mergeObject(base, value); 67 | } 68 | } 69 | 70 | if (value instanceof Map && base instanceof Map) { 71 | return Config.mergeMap(base, value); 72 | } 73 | 74 | return value; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Library/Config/ConfigInterface.ts: -------------------------------------------------------------------------------- 1 | import { ServerConfigInterface } from './ServerConfigInterface'; 2 | import { ControllerManagerConfigType } from './ControllerManagerConfigType'; 3 | import { ModuleManagerConfigInterface } from './ModuleManagerConfigInterface'; 4 | import { LoggerConfigInterface } from './LoggerConfigInterface'; 5 | import { ResponseConfigInterface } from './ResponseConfigInterface'; 6 | import { RouterConfigInterface } from './RouterConfigInterface'; 7 | import { ServiceManagerConfigType } from '../ServiceManager'; 8 | 9 | export type ConfigType = Partial<{ 10 | controller: ControllerManagerConfigType; 11 | response: ResponseConfigInterface; 12 | router: RouterConfigInterface; 13 | logger: LoggerConfigInterface; 14 | server: ServerConfigInterface; 15 | services: ServiceManagerConfigType; 16 | modules: ModuleManagerConfigInterface; 17 | [key: string]: any; 18 | }>; 19 | -------------------------------------------------------------------------------- /src/Library/Config/ControllerManagerConfigType.ts: -------------------------------------------------------------------------------- 1 | import { ServiceManagerConfigType } from '../ServiceManager'; 2 | 3 | export type ControllerManagerConfigType = Partial<{ 4 | locations: string[]; 5 | controllers: ServiceManagerConfigType; 6 | }>; 7 | -------------------------------------------------------------------------------- /src/Library/Config/LoggerConfigInterface.ts: -------------------------------------------------------------------------------- 1 | import { LoggerOptions } from 'winston'; 2 | 3 | export interface LoggerConfigInterface extends LoggerOptions { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/Library/Config/ModuleManagerConfigInterface.ts: -------------------------------------------------------------------------------- 1 | import { ModuleClassInterface } from '../ModuleManager'; 2 | 3 | export interface ModuleManagerConfigInterface extends Array { 4 | } 5 | -------------------------------------------------------------------------------- /src/Library/Config/ResponseConfigInterface.ts: -------------------------------------------------------------------------------- 1 | import { InformationalResponse, RedirectionResponse, ClientErrorResponse, ServerErrorResponse, SuccessfulResponse } from '../Response'; 2 | 3 | export interface ResponseConfigInterface { 4 | responses: { 5 | informational: typeof InformationalResponse; 6 | redirection: typeof RedirectionResponse; 7 | serverError: typeof ServerErrorResponse; 8 | clientError: typeof ClientErrorResponse; 9 | successful: typeof SuccessfulResponse; 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/Library/Config/RouterConfigInterface.ts: -------------------------------------------------------------------------------- 1 | import { RouteInterface } from '../Router'; 2 | 3 | export interface RouterConfigInterface { 4 | routes?: Array; 5 | } 6 | -------------------------------------------------------------------------------- /src/Library/Config/ServerConfigInterface.ts: -------------------------------------------------------------------------------- 1 | import cors from 'koa__cors'; 2 | import { ServerOptions } from 'https'; 3 | 4 | export interface ServerConfigInterface { 5 | bootstrap?: Function; 6 | ssl?: ServerOptions; 7 | port?: number; 8 | cors?: { 9 | enabled?: boolean; 10 | options?: cors.Options; 11 | }; 12 | [key: string]: any; 13 | } 14 | -------------------------------------------------------------------------------- /src/Library/Config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Config'; 2 | export * from './ConfigInterface'; 3 | export * from './ControllerManagerConfigType'; 4 | export * from './LoggerConfigInterface'; 5 | export * from './ModuleManagerConfigInterface'; 6 | export * from './ResponseConfigInterface'; 7 | export * from './RouterConfigInterface'; 8 | export * from './ServerConfigInterface'; 9 | -------------------------------------------------------------------------------- /src/Library/Controller/AbstractActionController.ts: -------------------------------------------------------------------------------- 1 | import { AbstractResponseHelper } from '../Response/AbstractResponseHelper'; 2 | 3 | export class AbstractActionController extends AbstractResponseHelper { 4 | } 5 | -------------------------------------------------------------------------------- /src/Library/Controller/ControllerManager.ts: -------------------------------------------------------------------------------- 1 | import { ControllerManagerConfigType } from '../Config'; 2 | import { ServiceManager, AbstractFileBasedPluginManager } from '../ServiceManager'; 3 | import { Instantiable } from '../Core'; 4 | import { createDebugLogger } from '../../debug'; 5 | 6 | const debug = createDebugLogger('controllerManager'); 7 | 8 | export class ControllerManager extends AbstractFileBasedPluginManager { 9 | constructor (creationContext: ServiceManager, config: ControllerManagerConfigType = {}) { 10 | const { locations, controllers } = config; 11 | 12 | debug('Loading controllers'); 13 | 14 | super(creationContext, locations, controllers); 15 | 16 | debug('Finished loading controllers'); 17 | } 18 | 19 | public getController (Controller: Instantiable): Object { 20 | return this.getPlugin(Controller); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Library/Controller/ControllerManagerFactory.ts: -------------------------------------------------------------------------------- 1 | import { FactoryInterface, ServiceManager } from '../ServiceManager'; 2 | import { ControllerManager } from './ControllerManager'; 3 | import { Config, ControllerManagerConfigType } from '../Config'; 4 | 5 | export const ControllerManagerFactory: FactoryInterface = (sm: ServiceManager) => { 6 | return new ControllerManager(sm, sm.get(Config).of('controller')); 7 | }; 8 | -------------------------------------------------------------------------------- /src/Library/Controller/ControllerTypes.ts: -------------------------------------------------------------------------------- 1 | import { AbstractActionController } from './AbstractActionController'; 2 | 3 | export type ControllerType = string | typeof AbstractActionController; 4 | -------------------------------------------------------------------------------- /src/Library/Controller/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ControllerManager'; 2 | export * from './ControllerManagerFactory'; 3 | export * from './ControllerTypes'; 4 | export * from './AbstractActionController'; 5 | -------------------------------------------------------------------------------- /src/Library/Core/Types.ts: -------------------------------------------------------------------------------- 1 | export type Instantiable = {new(...args: any[]): T}; 2 | -------------------------------------------------------------------------------- /src/Library/Core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Types'; 2 | -------------------------------------------------------------------------------- /src/Library/Error/InvalidActionResultError.ts: -------------------------------------------------------------------------------- 1 | export class InvalidActionResultError extends Error { 2 | constructor (error: string) { 3 | super(error); 4 | 5 | // Set the prototype explicitly for some reason. 6 | Object.setPrototypeOf(this, InvalidActionResultError.prototype); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Library/Error/InvalidArgumentError.ts: -------------------------------------------------------------------------------- 1 | export class InvalidArgumentError extends Error { 2 | constructor (error: string) { 3 | super(error); 4 | 5 | // Set the prototype explicitly. 6 | Object.setPrototypeOf(this, InvalidArgumentError.prototype); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Library/Error/NotFoundError.ts: -------------------------------------------------------------------------------- 1 | export class NotFoundError extends Error { 2 | constructor (error: string) { 3 | super(error); 4 | 5 | // Set the prototype explicitly for some reason. 6 | Object.setPrototypeOf(this, NotFoundError.prototype); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Library/Error/index.ts: -------------------------------------------------------------------------------- 1 | export * from './InvalidActionResultError'; 2 | export * from './NotFoundError'; 3 | export * from './InvalidArgumentError'; 4 | -------------------------------------------------------------------------------- /src/Library/EventManager/Event.ts: -------------------------------------------------------------------------------- 1 | export class Event { 2 | constructor (private event: string, private target: any, private payload: any) { } 3 | 4 | getEvent (): string { 5 | return this.event; 6 | } 7 | 8 | getTarget (): T { 9 | return this.target; 10 | } 11 | 12 | getPayload (): any { 13 | return this.payload; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Library/EventManager/EventManager.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import { Event } from './Event'; 3 | import { SelfDestructingCallbackInterface } from './EventManagerTypes'; 4 | import { createDebugLogger } from '../../debug'; 5 | 6 | const debug = createDebugLogger('eventManager'); 7 | 8 | export class EventManager extends EventEmitter { 9 | protected sharedEventManager: EventManager; 10 | 11 | private hooks: { [eventName: string]: Array } = {}; 12 | 13 | public constructor (sharedEventManager: EventManager = null) { 14 | super(); 15 | 16 | this.sharedEventManager = sharedEventManager; 17 | } 18 | 19 | async trigger (eventName: string, target: any, payload?: any): Promise { 20 | debug(`Triggering event: ${eventName}.`); 21 | 22 | if (!this.hooks[eventName]) { 23 | debug('No listeners attached.'); 24 | 25 | return false; 26 | } 27 | 28 | const callbacks = this.hooks[eventName]; 29 | 30 | let triggered = 0; 31 | let detached = 0; 32 | let remaining = callbacks.length; 33 | 34 | for (let i = 0; i < remaining; i++) { 35 | const callback = callbacks[i] as SelfDestructingCallbackInterface; 36 | 37 | await callback(new Event(eventName, target, payload)); 38 | 39 | if (callback._isSelfDestructingCallback) { 40 | callbacks.splice(i, 1); 41 | 42 | detached++; 43 | remaining--; 44 | i--; 45 | } 46 | 47 | triggered++; 48 | } 49 | 50 | if (!remaining) { 51 | delete this.hooks[eventName]; 52 | } 53 | 54 | debug(`Called ${triggered} listeners for: ${eventName} (detached ${detached} one-time listeners).`); 55 | 56 | return true; 57 | } 58 | 59 | has (event: string, callback: Function): boolean { 60 | return this.hooks[event] && this.hooks[event].indexOf(callback) > -1; 61 | } 62 | 63 | attachOnce (eventName: string, callback: Function, index?: number) { 64 | (callback as SelfDestructingCallbackInterface)._isSelfDestructingCallback = true; 65 | 66 | this.attach(eventName, callback, index); 67 | } 68 | 69 | attach (event: string, callback: Function, index?: number): this { 70 | this.hooks[event] = this.hooks[event] || []; 71 | 72 | if (index) { 73 | this.hooks[event].splice(index, 0, callback); 74 | } else { 75 | this.hooks[event].push(callback); 76 | } 77 | 78 | return this; 79 | } 80 | 81 | attachAt (index: number, event: string, callback: Function): this { 82 | return this.attach(event, callback, index); 83 | } 84 | 85 | detach (event: string, callback: Function): this { 86 | if (!this.hooks[event]) { 87 | return this; 88 | } 89 | 90 | const hookIndex: number = this.hooks[event].indexOf(callback); 91 | 92 | if (hookIndex === -1) { 93 | return this; 94 | } 95 | 96 | this.hooks[event].splice(hookIndex, 1); 97 | 98 | if (this.hooks[event].length === 0) { 99 | delete this.hooks[event]; 100 | } 101 | 102 | return this; 103 | } 104 | 105 | getSharedEventManager (): EventManager { 106 | return this.sharedEventManager; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Library/EventManager/EventManagerFactory.ts: -------------------------------------------------------------------------------- 1 | import { ServiceManager } from '../ServiceManager'; 2 | import { SharedEventManager } from './SharedEventManager'; 3 | import { EventManager } from './EventManager'; 4 | 5 | export const EventManagerFactory = (sm: ServiceManager) => { 6 | return new EventManager(sm.has(SharedEventManager) ? sm.get(SharedEventManager) : null); 7 | }; 8 | -------------------------------------------------------------------------------- /src/Library/EventManager/EventManagerTypes.ts: -------------------------------------------------------------------------------- 1 | import { Event } from './Event'; 2 | 3 | export interface SelfDestructingCallbackInterface extends Function { 4 | (event: Event): Promise; 5 | _isSelfDestructingCallback?: boolean; 6 | } 7 | -------------------------------------------------------------------------------- /src/Library/EventManager/SharedEventManager.ts: -------------------------------------------------------------------------------- 1 | import { EventManager } from './EventManager'; 2 | 3 | export class SharedEventManager extends EventManager { 4 | } 5 | -------------------------------------------------------------------------------- /src/Library/EventManager/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Event'; 2 | export * from './EventManager'; 3 | export * from './EventManagerFactory'; 4 | export * from './SharedEventManager'; 5 | export * from './EventManagerTypes'; 6 | -------------------------------------------------------------------------------- /src/Library/Interface/ContextInterface.ts: -------------------------------------------------------------------------------- 1 | import Koa from 'koa'; 2 | import { Response } from '../Response'; 3 | import { AbstractActionController } from '../Controller'; 4 | 5 | export interface ContextInterface extends Koa.Context { 6 | state: { 7 | params?: { [key: string]: any }; 8 | response?: Response, 9 | dispatch?: { 10 | controllerName: string; 11 | controller: typeof AbstractActionController; 12 | action: string; 13 | }; 14 | [key: string]: any; 15 | }; 16 | [key: string]: any; 17 | } 18 | -------------------------------------------------------------------------------- /src/Library/Interface/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ContextInterface'; 2 | -------------------------------------------------------------------------------- /src/Library/Logger/LoggerService.ts: -------------------------------------------------------------------------------- 1 | import { Logger as WinstonLogger } from 'winston'; 2 | import { LoggerConfigInterface } from '../Config'; 3 | import winston from 'winston'; 4 | 5 | export class LoggerService { 6 | private readonly adapter: WinstonLogger; 7 | 8 | constructor (config: LoggerConfigInterface) { 9 | this.adapter = winston.createLogger(config); 10 | } 11 | 12 | public getAdapter (): WinstonLogger { 13 | return this.adapter; 14 | } 15 | 16 | log (level: string | object, message?: any, ...logArguments: any[]): this { 17 | this.adapter.log(level as string, message, ...logArguments); 18 | 19 | return this; 20 | } 21 | 22 | error (message: string | object, ...logArguments: any[]): this { 23 | return this.log('error', message, ...logArguments); 24 | } 25 | 26 | warn (message: string | object, ...logArguments: any[]): this { 27 | return this.log('warn', message, ...logArguments); 28 | } 29 | 30 | help (message: string | object, ...logArguments: any[]): this { 31 | return this.log('help', message, ...logArguments); 32 | } 33 | 34 | data (message: string | object, ...logArguments: any[]): this { 35 | return this.log('data', message, ...logArguments); 36 | } 37 | 38 | info (message: string | object, ...logArguments: any[]): this { 39 | return this.log('info', message, ...logArguments); 40 | } 41 | 42 | debug (message: string | object, ...logArguments: any[]): this { 43 | return this.log('debug', message, ...logArguments); 44 | } 45 | 46 | prompt (message: string | object, ...logArguments: any[]): this { 47 | return this.log('prompt', message, ...logArguments); 48 | } 49 | 50 | http (message: string | object, ...logArguments: any[]): this { 51 | return this.log('http', message, ...logArguments); 52 | } 53 | 54 | verbose (message: string | object, ...logArguments: any[]): this { 55 | return this.log('verbose', message, ...logArguments); 56 | } 57 | 58 | input (message: string | object, ...logArguments: any[]): this { 59 | return this.log('input', message, ...logArguments); 60 | } 61 | 62 | silly (message: string | object, ...logArguments: any[]): this { 63 | return this.log('silly', message, ...logArguments); 64 | } 65 | 66 | emerg (message: string | object, ...logArguments: any[]): this { 67 | return this.log('emerg', message, ...logArguments); 68 | } 69 | 70 | alert (message: string | object, ...logArguments: any[]): this { 71 | return this.log('alert', message, ...logArguments); 72 | } 73 | 74 | crit (message: string | object, ...logArguments: any[]): this { 75 | return this.log('crit', message, ...logArguments); 76 | } 77 | 78 | warning (message: string | object, ...logArguments: any[]): this { 79 | return this.log('warning', message, ...logArguments); 80 | } 81 | 82 | notice (message: string | object, ...logArguments: any[]): this { 83 | return this.log('notice', message, ...logArguments); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Library/Logger/LoggerServiceFactory.ts: -------------------------------------------------------------------------------- 1 | import { ServiceManager } from '../ServiceManager'; 2 | import { Config, LoggerConfigInterface } from '../Config'; 3 | import { LoggerService } from './LoggerService'; 4 | 5 | export const LoggerServiceFactory = (sm: ServiceManager) => { 6 | return new LoggerService(sm.get(Config).of('logger')); 7 | }; 8 | -------------------------------------------------------------------------------- /src/Library/Logger/index.ts: -------------------------------------------------------------------------------- 1 | export * from './LoggerService'; 2 | export * from './LoggerServiceFactory'; 3 | -------------------------------------------------------------------------------- /src/Library/Middleware/AbstractMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { ContextInterface } from '../Interface'; 2 | import { RegisteredMiddlewareType } from './MiddlewareTypes'; 3 | 4 | export abstract class AbstractMiddleware { 5 | abstract pass (ctx?: ContextInterface, next?: Function): any; 6 | 7 | public asCallback (): RegisteredMiddlewareType { 8 | const callback: RegisteredMiddlewareType = (context: ContextInterface, next: () => Promise) => this.pass(context, next); 9 | 10 | callback._name = this.constructor.name; 11 | callback._fromClass = this.constructor as typeof AbstractMiddleware; 12 | 13 | return callback; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Library/Middleware/DispatchMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { AbstractMiddleware } from './AbstractMiddleware'; 2 | import { ContextInterface } from '../Interface'; 3 | import { Response, ResponseService } from '../Response'; 4 | import { InvalidActionResultError } from '../Error'; 5 | import { createDebugLogger } from '../../debug'; 6 | import { inject } from '../ServiceManager/decorators'; 7 | import { LoggerService } from '../Logger'; 8 | 9 | const debug = createDebugLogger('middleware:dispatch'); 10 | 11 | export class DispatchMiddleware extends AbstractMiddleware { 12 | @inject(ResponseService) 13 | private responseService: ResponseService; 14 | 15 | @inject(LoggerService) 16 | private logger: LoggerService; 17 | 18 | public async pass (ctx: ContextInterface, next: Function) { 19 | if (ctx.state.response) { 20 | debug('Response found on context, calling next.'); 21 | 22 | return next(); 23 | } 24 | 25 | const { controller, action, controllerName } = ctx.state.dispatch; 26 | 27 | debug(`Dispatching ${controllerName}.${action}.`); 28 | 29 | // Route found, controller found... but the action doesn't exist. Or isn't a method. 30 | if (typeof (controller as any)[action] as any !== 'function') { 31 | debug(`${controllerName}.${action} not found, calling next.`); 32 | 33 | this.logger.error(`Action "${action}" not found on controller "${controllerName}" for request path "${ctx.path}".`); 34 | 35 | ctx.state.response = this.responseService.serverError().notImplemented(); 36 | 37 | return next(); 38 | } 39 | 40 | let response; 41 | 42 | try { 43 | response = await (controller as any)[action](ctx); 44 | 45 | if (!(response instanceof Response)) { 46 | throw new InvalidActionResultError([ 47 | `Action "${controllerName}.${action}" failed to produce a Response instance,`, 48 | `instead got type "${typeof response}".`, 49 | 'Did you forget to add a return statement in front of your response?', 50 | ].join(' ')); 51 | } 52 | } catch (error) { 53 | this.logger.error(error.message, error); 54 | 55 | response = this.responseService.serverError().internalServerError(null, null, { error }); 56 | } 57 | 58 | ctx.state.response = response; 59 | 60 | debug(`Dispatched ${controllerName}.${action}, calling next.`); 61 | 62 | next(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Library/Middleware/MiddlewareInterface.ts: -------------------------------------------------------------------------------- 1 | import { ContextInterface } from '../Interface'; 2 | 3 | export interface MiddlewareInterface { 4 | pass (ctx?: ContextInterface, next?: Function): any; 5 | } 6 | -------------------------------------------------------------------------------- /src/Library/Middleware/MiddlewareTypes.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from 'koa'; 2 | import { AbstractMiddleware } from './AbstractMiddleware'; 3 | 4 | export type RegisteredMiddlewareType = Middleware & { _name?: string; _fromClass?: typeof AbstractMiddleware; }; 5 | 6 | export type MiddlewareLookupType = string | typeof AbstractMiddleware | Middleware; 7 | 8 | export type MiddlewareType = AbstractMiddleware | Middleware; 9 | -------------------------------------------------------------------------------- /src/Library/Middleware/RequestMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { ContextInterface } from '../Interface'; 2 | import { Response } from '../Response'; 3 | import { RequestMethods } from '../Server'; 4 | import bytes from 'bytes'; 5 | import { createDebugLogger } from '../../debug'; 6 | import { AbstractMiddleware } from './AbstractMiddleware'; 7 | 8 | const debug = createDebugLogger('middleware:request'); 9 | 10 | export class RequestMiddleware extends AbstractMiddleware { 11 | public async pass (ctx: ContextInterface, next: Function) { 12 | debug(`<-- ${ctx.method.toUpperCase()} ${ctx.path}`); 13 | await next(); 14 | 15 | const response: Response = ctx.state.response; 16 | 17 | await response.patchContext(ctx); 18 | 19 | // Head doesn't want a body. Hehe. I get why it's called head now. 20 | // Raphaela seems to think that's not why they call it head. I'm filing for divorce. 21 | // She looks sad now as she's reading along while I type this. For the record: I'm joking! 22 | // No divorce. 23 | // ... 24 | // Yet. 25 | // JK! 26 | // Okay time to be productive again. 27 | if (ctx.method === RequestMethods.Head) { 28 | delete ctx.body; 29 | } 30 | 31 | debug(`--> ${ctx.method.toUpperCase()} ${ctx.path} ${response.getStatusCode()} ${bytes(ctx.length)}`); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Library/Middleware/RouterMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { ContextInterface } from '../Interface'; 2 | import { RequestMethods } from '../Server'; 3 | import { ControllerManager } from '../Controller'; 4 | import { createDebugLogger } from '../../debug'; 5 | import { RouterService } from '../Router'; 6 | import { inject } from '../ServiceManager/decorators'; 7 | import { ResponseService } from '../Response'; 8 | import { AbstractMiddleware } from './AbstractMiddleware'; 9 | 10 | const debug = createDebugLogger('middleware:router'); 11 | 12 | export class RouterMiddleware extends AbstractMiddleware{ 13 | @inject(RouterService) 14 | private routerService: RouterService; 15 | 16 | @inject(ResponseService) 17 | private responseService: ResponseService; 18 | 19 | @inject(ControllerManager) 20 | private controllerManager: ControllerManager; 21 | 22 | public async pass (ctx: ContextInterface, next: Function) { 23 | debug(`Routing for path "${ctx.path}".`); 24 | 25 | const method = ctx.method === RequestMethods.Head ? RequestMethods.Get : ctx.method; 26 | const match = this.routerService.resolve(method as RequestMethods, ctx.path); 27 | 28 | if (!match) { 29 | ctx.state.response = this.responseService.clientError().notFound(); 30 | 31 | debug('No match, returning not found.'); 32 | 33 | return; 34 | } 35 | 36 | const { route, parameters } = match; 37 | const controllerName = ControllerManager.getPluginName(route.controller); 38 | const controller: any = this.controllerManager.get(route.controller); 39 | 40 | ctx.state.params = parameters; 41 | 42 | ctx.state.dispatch = { 43 | controllerName, 44 | controller, 45 | action: route.action, 46 | }; 47 | 48 | debug(`Route matched "${controllerName}.${route.action}()".`); 49 | 50 | return next(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Library/Middleware/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AbstractMiddleware'; 2 | export * from './DispatchMiddleware'; 3 | export * from './MiddlewareInterface'; 4 | export * from './RequestMiddleware'; 5 | export * from './RouterMiddleware'; 6 | export * from './MiddlewareTypes'; 7 | -------------------------------------------------------------------------------- /src/Library/ModuleManager/ModuleClassInterface.ts: -------------------------------------------------------------------------------- 1 | import { ModuleInterface } from './ModuleInterface'; 2 | import { Instantiable } from '../Core'; 3 | 4 | export interface ModuleClassInterface extends Instantiable { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /src/Library/ModuleManager/ModuleInterface.ts: -------------------------------------------------------------------------------- 1 | import { ModuleManager } from './ModuleManager'; 2 | import { Event } from '../EventManager'; 3 | 4 | export interface ModuleInterface { 5 | onBootstrap?: (event?: Event) => void | Promise; 6 | getConfig?: (mode?: string) => { [key: string]: any } | Promise<{ [key: string]: any }>; 7 | getServerConfig?: () => { [key: string]: any } | Promise<{ [key: string]: any }>; 8 | getCliConfig?: () => { [key: string]: any } | Promise<{ [key: string]: any }>; 9 | init?: (moduleManager?: ModuleManager) => void; 10 | } 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Library/ModuleManager/ModuleManager.ts: -------------------------------------------------------------------------------- 1 | import { Config, ModuleManagerConfigInterface } from '../Config'; 2 | import { EventManager } from '../EventManager'; 3 | import { ModuleManagerEvents } from './ModuleManagerEvents'; 4 | import { ModuleClassInterface } from './ModuleClassInterface'; 5 | import { Application, ApplicationModes, ApplicationConfigType } from '../Application'; 6 | import { createDebugLogger } from '../../debug'; 7 | 8 | const debug = createDebugLogger('modules'); 9 | 10 | export class ModuleManager { 11 | private readonly config: Config; 12 | 13 | private readonly eventManager: EventManager; 14 | 15 | private readonly application: Application; 16 | 17 | constructor (application: Application, eventManager: EventManager, config: Config) { 18 | this.application = application; 19 | this.eventManager = eventManager; 20 | this.config = config; 21 | } 22 | 23 | public async bootstrap () { 24 | return await this.eventManager.trigger(ModuleManagerEvents.OnBootstrap, this); 25 | } 26 | 27 | public async loadModule (ModuleClass: ModuleClassInterface): Promise { 28 | debug('Loading module ' + ModuleClass.name); 29 | 30 | const mode = this.config.of('application').mode; 31 | const eventManager = this.eventManager; 32 | const module = new ModuleClass(); 33 | 34 | if (typeof module.getConfig === 'function') { 35 | this.config.merge(await module.getConfig(mode)); 36 | } 37 | 38 | if (mode === ApplicationModes.Cli && typeof module.getCliConfig === 'function') { 39 | this.config.merge(await module.getCliConfig()); 40 | } 41 | 42 | if (mode === ApplicationModes.Server && typeof module.getServerConfig === 'function') { 43 | this.config.merge(await module.getServerConfig()); 44 | } 45 | 46 | if (typeof module.init === 'function') { 47 | await module.init(this); 48 | } 49 | 50 | // Allow for a convenience onBootstrap method. 51 | if (typeof module.onBootstrap === 'function' && !eventManager.has(ModuleManagerEvents.OnBootstrap, module.onBootstrap)) { 52 | debug(`Auto-attaching onBootstrap listener for ${ModuleClass.name}.`); 53 | eventManager.attachOnce(ModuleManagerEvents.OnBootstrap, module.onBootstrap); 54 | } 55 | 56 | debug('Initialized module ' + ModuleClass.name); 57 | 58 | return this; 59 | } 60 | 61 | public getEventManager (): EventManager { 62 | return this.eventManager; 63 | } 64 | 65 | public getApplication (): Application { 66 | return this.application; 67 | } 68 | 69 | public async loadModules (config: ModuleManagerConfigInterface): Promise { 70 | debug('Loading modules'); 71 | 72 | if (!config) { 73 | debug('No modules registered.'); 74 | 75 | return; 76 | } 77 | 78 | for(let i = 0; i < config.length; i++) { 79 | await this.loadModule(config[i]); 80 | } 81 | 82 | debug('Loaded modules'); 83 | 84 | return this; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Library/ModuleManager/ModuleManagerEvents.ts: -------------------------------------------------------------------------------- 1 | export enum ModuleManagerEvents { 2 | OnBootstrap = 'ModuleManager.OnBootstrap', 3 | } 4 | -------------------------------------------------------------------------------- /src/Library/ModuleManager/ModuleManagerFactory.ts: -------------------------------------------------------------------------------- 1 | import { ServiceManager, FactoryInterface } from '../ServiceManager'; 2 | import { ModuleManager } from './ModuleManager'; 3 | import { Config } from '../Config'; 4 | import { EventManager } from '../EventManager'; 5 | import { Application } from '../Application'; 6 | 7 | export const ModuleManagerFactory: FactoryInterface = (sm: ServiceManager) => { 8 | return new ModuleManager(sm.get(Application), sm.get(EventManager), sm.get(Config)); 9 | }; 10 | -------------------------------------------------------------------------------- /src/Library/ModuleManager/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ModuleManager'; 2 | export * from './ModuleManagerFactory'; 3 | export * from './ModuleClassInterface'; 4 | export * from './ModuleInterface'; 5 | -------------------------------------------------------------------------------- /src/Library/Output/Output.ts: -------------------------------------------------------------------------------- 1 | import Table from 'cli-table'; 2 | import prettyjson from 'prettyjson'; 3 | import PrettyError from 'pretty-error'; 4 | import chalk from 'chalk'; 5 | 6 | export class Output { 7 | private exitCode: number = 0; 8 | 9 | private data: any[] = []; 10 | 11 | private prettyError: PrettyError = new PrettyError(); 12 | 13 | public static create () { 14 | return new this(); 15 | } 16 | 17 | public static errorOutput (error?: string | Error, exitCode?: number) { 18 | const output = new this(); 19 | 20 | output.error(error, exitCode); 21 | 22 | return output; 23 | } 24 | 25 | /** 26 | * Exit code to use upon send. 27 | * 28 | * NOTE: Exit codes 1 - 2, 126 - 165, and 255 have special meanings 29 | * and should therefore be avoided for user-specified exit parameters. 30 | * Try restricting user-defined exit codes to the range 64 - 113 (in addition to 0, for success). 31 | * 32 | * 1 - Catchall for general errors 33 | * 2 - Misuse of shell builtins (according to Bash documentation) 34 | * 126 - Command invoked cannot execute 35 | * 127 - “command not found” 36 | * 128 - Invalid argument to exit 37 | * 128+n - Fatal error signal “n” 38 | * 130 - Script terminated by Control-C 39 | * 255\* - Exit status out of range 40 | * 41 | * @see http://tldp.org/LDP/abs/html/exitcodes.html 42 | * 43 | * @param {Number} exitCode 44 | * 45 | * @return {this} 46 | */ 47 | public setExitCode (exitCode: number): this { 48 | this.exitCode = exitCode; 49 | 50 | return this; 51 | } 52 | 53 | public addData (...data: any[]): this { 54 | data.forEach(item => { 55 | if (item === undefined) { 56 | return; 57 | } 58 | 59 | if (item.constructor === Object) { 60 | return this.addData(prettyjson.render(item)); 61 | } 62 | 63 | this.data.push(item); 64 | }); 65 | 66 | return this; 67 | } 68 | 69 | public success (message: string, data?: any) { 70 | this.addData(chalk`\n {green {bold Success!}} ${message}\n`, data); 71 | } 72 | 73 | public resetData (): this { 74 | this.data = []; 75 | 76 | return this; 77 | } 78 | 79 | public addHorizontalTable (head: string[], data: Array[], options: any = {}): this { 80 | const table = new Table({ head, ...options }); 81 | 82 | table.push(...data); 83 | 84 | this.addData(table.toString()); 85 | 86 | return this; 87 | } 88 | 89 | public addVerticalTable (data: {[key: string]: string}[], options?: any): this { 90 | const table = new Table(options); 91 | 92 | table.push(...data); 93 | 94 | this.addData(table.toString()); 95 | 96 | return this; 97 | } 98 | 99 | public addCrossTable (head: string[], data: {[key: string]: string[]}[], options: any = {}):this { 100 | const table = new Table(options); 101 | 102 | table.push(...data); 103 | 104 | this.addData(table.toString()); 105 | 106 | return this; 107 | } 108 | 109 | /** 110 | * Add/set an error to output. 111 | * 112 | * @param {Error|string} [error] The error to write. Default to "Unknown error". 113 | * @param {Number|null} [exitCode] The code to exit with. Default to 1. Use null to not change the exitCode. 114 | * @param {boolean} [clear] Clear previously set data. Default to false. 115 | * 116 | * @return {this} 117 | */ 118 | public error (error: Error | string = 'Unknown error', exitCode: number = 1, clear: boolean = false): this { 119 | if (!this.prettyError) { 120 | this.prettyError = new PrettyError(); 121 | } 122 | 123 | if (exitCode) { 124 | this.setExitCode(exitCode); 125 | } 126 | 127 | if (clear) { 128 | this.resetData(); 129 | } 130 | 131 | this.addData(this.prettyError.render(error)); 132 | 133 | return this; 134 | } 135 | 136 | /** 137 | * Write the output to the console. 138 | */ 139 | public flush () { 140 | this.data.forEach((piece: any) => console.log(piece)); 141 | } 142 | 143 | public send () { 144 | this.flush(); 145 | 146 | process.exit(this.exitCode); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Library/Output/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Output'; 2 | -------------------------------------------------------------------------------- /src/Library/Response/AbstractResponseHelper.ts: -------------------------------------------------------------------------------- 1 | import { inject } from '../ServiceManager/decorators'; 2 | import { ResponseService } from './ResponseService'; 3 | import { SuccessfulResponse } from './SuccessfulResponse'; 4 | import { ClientErrorResponse } from './ClientErrorResponse'; 5 | import { ServerErrorResponse } from './ServerErrorResponse'; 6 | import { RedirectionResponse } from './RedirectionResponse'; 7 | 8 | export class AbstractResponseHelper { 9 | @inject(ResponseService) 10 | protected responseService: ResponseService; 11 | 12 | protected getResponseService (): ResponseService { 13 | return this.responseService; 14 | } 15 | 16 | protected okResponse (data?: any, meta?: any): SuccessfulResponse { 17 | return this.responseService.successful().ok(data, meta); 18 | } 19 | 20 | protected createdResponse (data?: any, meta?: any): SuccessfulResponse { 21 | return this.responseService.successful().created(data, meta); 22 | } 23 | 24 | protected notFoundResponse (message?: string, data?: any, meta?: any): ClientErrorResponse { 25 | return this.responseService.clientError().notFound(message, data, meta); 26 | } 27 | 28 | protected requestTimeoutResponse (message?: string, data?: any, meta?: any): ClientErrorResponse { 29 | return this.responseService.clientError().requestTimeout(message, data, meta); 30 | } 31 | 32 | protected forbiddenResponse (message?: string, data?: any, meta?: any): ClientErrorResponse { 33 | return this.responseService.clientError().forbidden(message, data, meta); 34 | } 35 | 36 | protected badRequestResponse (message?: string, data?: any, meta?: Object): ClientErrorResponse { 37 | return this.responseService.clientError().badRequest(message, data, meta); 38 | } 39 | 40 | protected unauthorizedResponse (message?: string, data?: any, meta?: Object): ClientErrorResponse { 41 | return this.responseService.clientError().unauthorized(message, data, meta); 42 | } 43 | 44 | protected internalServerErrorResponse (message?: string, data?: any, meta?: Object): ServerErrorResponse { 45 | return this.responseService.serverError().internalServerError(message, data, meta); 46 | } 47 | 48 | protected permanentRedirectResponse (location: string, alt?: string, meta?: any): RedirectionResponse { 49 | return this.responseService.redirection().permanentRedirect(location, alt, meta); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Library/Response/ClientErrorResponse.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatusCodes } from '../Server'; 2 | import { Response } from './Response'; 3 | 4 | export class ClientErrorResponse extends Response { 5 | public static create (statusCode: HttpStatusCodes, message?: string, data?: any, meta?: any): ClientErrorResponse { 6 | return new this({ statusCode, message, data, meta }); 7 | } 8 | 9 | public static badRequest (message?: string, data?: any, meta?: any): ClientErrorResponse { 10 | return this.create(HttpStatusCodes.BadRequest, message, data, meta); 11 | } 12 | 13 | public static unauthorized (message?: string, data?: any, meta?: any): ClientErrorResponse { 14 | return this.create(HttpStatusCodes.Unauthorized, message, data, meta); 15 | } 16 | 17 | public static paymentRequired (message?: string, data?: any, meta?: any): ClientErrorResponse { 18 | return this.create(HttpStatusCodes.PaymentRequired, message, data, meta); 19 | } 20 | 21 | public static forbidden (message?: string, data?: any, meta?: any): ClientErrorResponse { 22 | return this.create(HttpStatusCodes.Forbidden, message, data, meta); 23 | } 24 | 25 | public static notFound (message?: string, data?: any, meta?: any): ClientErrorResponse { 26 | return this.create(HttpStatusCodes.NotFound, message, data, meta); 27 | } 28 | 29 | public static methodNotAllowed (message?: string, data?: any, meta?: any): ClientErrorResponse { 30 | return this.create(HttpStatusCodes.MethodNotAllowed, message, data, meta); 31 | } 32 | 33 | public static notAcceptable (message?: string, data?: any, meta?: any): ClientErrorResponse { 34 | return this.create(HttpStatusCodes.NotAcceptable, message, data, meta); 35 | } 36 | 37 | public static proxyAuthenticationRequired (message?: string, data?: any, meta?: any): ClientErrorResponse { 38 | return this.create(HttpStatusCodes.ProxyAuthenticationRequired, message, data, meta); 39 | } 40 | 41 | public static requestTimeout (message?: string, data?: any, meta?: any): ClientErrorResponse { 42 | return this.create(HttpStatusCodes.RequestTimeout, message, data, meta); 43 | } 44 | 45 | public static conflict (message?: string, data?: any, meta?: any): ClientErrorResponse { 46 | return this.create(HttpStatusCodes.Conflict, message, data, meta); 47 | } 48 | 49 | public static gone (message?: string, data?: any, meta?: any): ClientErrorResponse { 50 | return this.create(HttpStatusCodes.Gone, message, data, meta); 51 | } 52 | 53 | public static lengthRequired (message?: string, data?: any, meta?: any): ClientErrorResponse { 54 | return this.create(HttpStatusCodes.LengthRequired, message, data, meta); 55 | } 56 | 57 | public static preconditionFailed (message?: string, data?: any, meta?: any): ClientErrorResponse { 58 | return this.create(HttpStatusCodes.PreconditionFailed, message, data, meta); 59 | } 60 | 61 | public static payloadTooLarge (message?: string, data?: any, meta?: any): ClientErrorResponse { 62 | return this.create(HttpStatusCodes.PayloadTooLarge, message, data, meta); 63 | } 64 | 65 | public static uriTooLong (message?: string, data?: any, meta?: any): ClientErrorResponse { 66 | return this.create(HttpStatusCodes.UriTooLong, message, data, meta); 67 | } 68 | 69 | public static unsupportedMediaType (message?: string, data?: any, meta?: any): ClientErrorResponse { 70 | return this.create(HttpStatusCodes.UnsupportedMediaType, message, data, meta); 71 | } 72 | 73 | public static rangeNotSatisfiable (message?: string, data?: any, meta?: any): ClientErrorResponse { 74 | return this.create(HttpStatusCodes.RangeNotSatisfiable, message, data, meta); 75 | } 76 | 77 | public static expectationFailed (message?: string, data?: any, meta?: any): ClientErrorResponse { 78 | return this.create(HttpStatusCodes.ExpectationFailed, message, data, meta); 79 | } 80 | 81 | public static iAmATeapot (message?: string, data?: any, meta?: any): ClientErrorResponse { 82 | return this.create(HttpStatusCodes.IAmATeapot, message, data, meta); 83 | } 84 | 85 | public static misdirectedRequest (message?: string, data?: any, meta?: any): ClientErrorResponse { 86 | return this.create(HttpStatusCodes.MisdirectedRequest, message, data, meta); 87 | } 88 | 89 | public static unprocessableEntity (message?: string, data?: any, meta?: any): ClientErrorResponse { 90 | return this.create(HttpStatusCodes.UnprocessableEntity, message, data, meta); 91 | } 92 | 93 | public static locked (message?: string, data?: any, meta?: any): ClientErrorResponse { 94 | return this.create(HttpStatusCodes.Locked, message, data, meta); 95 | } 96 | 97 | public static failedDependency (message?: string, data?: any, meta?: any): ClientErrorResponse { 98 | return this.create(HttpStatusCodes.FailedDependency, message, data, meta); 99 | } 100 | 101 | public static upgradeRequired (message?: string, data?: any, meta?: any): ClientErrorResponse { 102 | return this.create(HttpStatusCodes.UpgradeRequired, message, data, meta); 103 | } 104 | 105 | public static preconditionRequired (message?: string, data?: any, meta?: any): ClientErrorResponse { 106 | return this.create(HttpStatusCodes.PreconditionRequired, message, data, meta); 107 | } 108 | 109 | public static tooManyRequests (message?: string, data?: any, meta?: any): ClientErrorResponse { 110 | return this.create(HttpStatusCodes.TooManyRequests, message, data, meta); 111 | } 112 | 113 | public static requestHeaderFieldsTooLarge (message?: string, data?: any, meta?: any): ClientErrorResponse { 114 | return this.create(HttpStatusCodes.RequestHeaderFieldsTooLarge, message, data, meta); 115 | } 116 | 117 | public static unavailableForLegalReasons (message?: string, data?: any, meta?: any): ClientErrorResponse { 118 | return this.create(HttpStatusCodes.UnavailableForLegalReasons, message, data, meta); 119 | } 120 | 121 | format () { 122 | return { message: this.message, data: this.data }; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Library/Response/InformationalResponse.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatusCodes } from '../Server'; 2 | import { Response } from './Response'; 3 | 4 | export class InformationalResponse extends Response { 5 | public static create (statusCode: HttpStatusCodes, meta?: any): InformationalResponse { 6 | return new this({ statusCode, meta }); 7 | } 8 | 9 | public static continue (meta?: any): InformationalResponse { 10 | return this.create(HttpStatusCodes.Continue, meta); 11 | } 12 | 13 | public static switchingProtocols (meta?: any): InformationalResponse { 14 | return this.create(HttpStatusCodes.SwitchingProtocols, meta); 15 | } 16 | 17 | public static processing (meta?: any): InformationalResponse { 18 | return this.create(HttpStatusCodes.Processing, meta); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Library/Response/RedirectionResponse.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatusCodes } from '../Server'; 2 | import { Response } from './Response'; 3 | 4 | export class RedirectionResponse extends Response { 5 | public static create (statusCode: HttpStatusCodes, location: string, alt?: string, meta?: any): RedirectionResponse { 6 | return new this({ statusCode, data: { location, alt }, meta }); 7 | } 8 | 9 | public static multipleChoices (location: string, alt?: string, meta?: any): RedirectionResponse { 10 | return this.create(HttpStatusCodes.MultipleChoices, location, alt, meta); 11 | } 12 | 13 | public static movedPermanently (location: string, alt?: string, meta?: any): RedirectionResponse { 14 | return this.create(HttpStatusCodes.MovedPermanently, location, alt, meta); 15 | } 16 | 17 | public static found (location: string, alt?: string, meta?: any): RedirectionResponse { 18 | return this.create(HttpStatusCodes.Found, location, alt, meta); 19 | } 20 | 21 | public static seeOther (location: string, alt?: string, meta?: any): RedirectionResponse { 22 | return this.create(HttpStatusCodes.SeeOther, location, alt, meta); 23 | } 24 | 25 | public static notModified (location: string, alt?: string, meta?: any): RedirectionResponse { 26 | return this.create(HttpStatusCodes.NotModified, location, alt, meta); 27 | } 28 | 29 | public static useProxy (location: string, alt?: string, meta?: any): RedirectionResponse { 30 | return this.create(HttpStatusCodes.UseProxy, location, alt, meta); 31 | } 32 | 33 | public static switchProxy (location: string, alt?: string, meta?: any): RedirectionResponse { 34 | return this.create(HttpStatusCodes.SwitchProxy, location, alt, meta); 35 | } 36 | 37 | public static temporaryRedirect (location: string, alt?: string, meta?: any): RedirectionResponse { 38 | return this.create(HttpStatusCodes.TemporaryRedirect, location, alt, meta); 39 | } 40 | 41 | public static permanentRedirect (location: string, alt?: string, meta?: any): RedirectionResponse { 42 | return this.create(HttpStatusCodes.PermanentRedirect, location, alt, meta); 43 | } 44 | 45 | apply () { 46 | this.ctx.redirect(this.data.location, this.data.alt); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Library/Response/Response.ts: -------------------------------------------------------------------------------- 1 | import send, { SendOptions } from 'koa-send'; 2 | import path from 'path'; 3 | import { HttpStatusCodes } from '../Server'; 4 | import { ContextInterface } from '../Interface'; 5 | import { ResponseStrategies } from './ResponseStrategies'; 6 | 7 | export class Response { 8 | protected ctx: ContextInterface; 9 | 10 | protected headers: { [header: string]: string | Array } = {}; 11 | 12 | protected strategy: string = ResponseStrategies.Json; 13 | 14 | protected statusCode: HttpStatusCodes; 15 | 16 | protected meta: any; 17 | 18 | protected data: any; 19 | 20 | protected message: string; 21 | 22 | protected constructor ({ message, data, meta, statusCode }: ResponseArgumentsInterface) { 23 | this.data = data; 24 | this.meta = meta; 25 | this.message = message; 26 | this.statusCode = statusCode; 27 | } 28 | 29 | public setData (data: any): this { 30 | this.data = data; 31 | 32 | return this; 33 | } 34 | 35 | public getData (): any { 36 | return this.data; 37 | } 38 | 39 | public async patchContext (ctx: ContextInterface) { 40 | this.ctx = ctx; 41 | 42 | // Basic stuff 43 | this.applyStatusCode(this.statusCode).applyHeaders(); 44 | 45 | if (this.strategy === ResponseStrategies.File) { 46 | await this.sendFile(); 47 | } else if (this.strategy === ResponseStrategies.Html) { 48 | ctx.type = 'html'; 49 | 50 | this.applyBody(this.data); 51 | } else { 52 | // Default to json. 53 | this.applyBody(this.format()); 54 | } 55 | 56 | // Allow hooks. 57 | if (typeof this.apply === 'function') { 58 | await this.apply(); 59 | } 60 | } 61 | 62 | protected async sendFile () { 63 | const { location, options = {} } = this.meta; 64 | const { dir, base } = path.parse(location); 65 | 66 | if (options.root) { 67 | return await send(this.ctx, location, options); 68 | } 69 | 70 | await send(this.ctx, base, Object.assign({ root: dir }, options)); 71 | } 72 | 73 | public applyStatusCode (statusCode: HttpStatusCodes): this { 74 | this.ctx.status = statusCode; 75 | 76 | return this; 77 | } 78 | 79 | public setStatusCode (statusCode: HttpStatusCodes): this { 80 | this.statusCode = statusCode; 81 | 82 | return this; 83 | } 84 | 85 | public getStatusCode (): HttpStatusCodes { 86 | return this.statusCode; 87 | } 88 | 89 | protected applyBody (body: any): this { 90 | if (body) { 91 | this.ctx.body = body; 92 | } 93 | 94 | return this; 95 | } 96 | 97 | public setHeaders (headers: { [header: string]: string | Array }): this { 98 | this.headers = headers; 99 | 100 | return this; 101 | } 102 | 103 | public addHeaders (headers: { [header: string]: string | Array }): this{ 104 | Reflect.ownKeys(headers).forEach((header: string) => this.setHeader(header, headers[header])); 105 | 106 | return this; 107 | } 108 | 109 | public setHeader (header: string, value: string | Array): this { 110 | this.headers[header] = value; 111 | 112 | return this; 113 | } 114 | 115 | public appendHeader (header: string, value: string | Array) { 116 | this.headers[header] = [].concat(this.headers[header], value); 117 | } 118 | 119 | public removeHeader (header: string): this { 120 | delete this.headers[header]; 121 | 122 | return this; 123 | } 124 | 125 | public applyHeaders (): this { 126 | Reflect.ownKeys(this.headers).forEach((header: string) => this.ctx.set(header, this.headers[header])); 127 | 128 | return this; 129 | } 130 | 131 | public file (location: string, options?: SendOptions): this { 132 | this.strategy = ResponseStrategies.File; 133 | this.meta = { location, options }; 134 | 135 | return this; 136 | } 137 | 138 | public json (data: any): this { 139 | this.strategy = ResponseStrategies.Json; 140 | this.data = data; 141 | 142 | return this; 143 | } 144 | 145 | public html (data: any): this { 146 | this.strategy = ResponseStrategies.Html; 147 | this.data = data; 148 | 149 | return this; 150 | } 151 | 152 | protected apply (): void { 153 | return; 154 | } 155 | 156 | protected format (): any { 157 | return; 158 | } 159 | } 160 | 161 | export interface ResponseArgumentsInterface { 162 | statusCode: HttpStatusCodes; 163 | message?: string; 164 | data?: any; 165 | meta?: any; 166 | } 167 | -------------------------------------------------------------------------------- /src/Library/Response/ResponseService.ts: -------------------------------------------------------------------------------- 1 | import { ResponseConfigInterface } from '../Config'; 2 | import { InformationalResponse } from './InformationalResponse'; 3 | import { RedirectionResponse } from './RedirectionResponse'; 4 | import { ServerErrorResponse } from './ServerErrorResponse'; 5 | import { ClientErrorResponse } from './ClientErrorResponse'; 6 | import { SuccessfulResponse } from './SuccessfulResponse'; 7 | 8 | export class ResponseService { 9 | private config: ResponseConfigInterface; 10 | 11 | constructor (config: ResponseConfigInterface) { 12 | this.config = config; 13 | } 14 | 15 | public informational (): typeof InformationalResponse { 16 | return this.config.responses.informational; 17 | } 18 | 19 | public redirection (): typeof RedirectionResponse { 20 | return this.config.responses.redirection; 21 | } 22 | 23 | public serverError (): typeof ServerErrorResponse { 24 | return this.config.responses.serverError; 25 | } 26 | 27 | public clientError (): typeof ClientErrorResponse { 28 | return this.config.responses.clientError; 29 | } 30 | 31 | public successful (): typeof SuccessfulResponse { 32 | return this.config.responses.successful; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Library/Response/ResponseServiceFactory.ts: -------------------------------------------------------------------------------- 1 | import { FactoryInterface } from '../ServiceManager/FactoryInterface'; 2 | import { ServiceManager } from '../ServiceManager'; 3 | import { ResponseService } from './ResponseService'; 4 | import { Config, ResponseConfigInterface } from '../Config'; 5 | 6 | export const ResponseServiceFactory: FactoryInterface = (sm: ServiceManager) => { 7 | return new ResponseService(sm.get(Config).of('response')); 8 | }; 9 | -------------------------------------------------------------------------------- /src/Library/Response/ResponseStrategies.ts: -------------------------------------------------------------------------------- 1 | export enum ResponseStrategies { 2 | Json = 'Json', 3 | File = 'File', 4 | Html = 'Html', 5 | } 6 | -------------------------------------------------------------------------------- /src/Library/Response/ResponseTypes.ts: -------------------------------------------------------------------------------- 1 | export enum ResponseTypes { 2 | Informational = 'Informational', 3 | Redirection = 'Redirection', 4 | ServerError = 'ServerError', 5 | ClientError = 'ClientError', 6 | Successful = 'Successful', 7 | } 8 | -------------------------------------------------------------------------------- /src/Library/Response/ServerErrorResponse.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatusCodes } from '../Server'; 2 | import { Response } from './Response'; 3 | 4 | export class ServerErrorResponse extends Response { 5 | public static create (statusCode: HttpStatusCodes, message?: string, data?: any, meta?: Object): ServerErrorResponse { 6 | return new this({ statusCode, message, data, meta }); 7 | } 8 | 9 | public static internalServerError (message?: string, data?: any, meta?: Object): ServerErrorResponse { 10 | return this.create(HttpStatusCodes.InternalServerError, message, data, meta); 11 | } 12 | 13 | public static notImplemented (message?: string, data?: any, meta?: Object): ServerErrorResponse { 14 | return this.create(HttpStatusCodes.NotImplemented, message, data, meta); 15 | } 16 | 17 | public static badGateway (message?: string, data?: any, meta?: Object): ServerErrorResponse { 18 | return this.create(HttpStatusCodes.BadGateway, message, data, meta); 19 | } 20 | 21 | public static serviceUnavailable (message?: string, data?: any, meta?: Object): ServerErrorResponse { 22 | return this.create(HttpStatusCodes.ServiceUnavailable, message, data, meta); 23 | } 24 | 25 | public static gatewayTimeout (message?: string, data?: any, meta?: any): ServerErrorResponse { 26 | return this.create(HttpStatusCodes.GatewayTimeout, message, data, meta); 27 | } 28 | 29 | public static httpVersionNotSupported (message?: string, data?: any, meta?: any): ServerErrorResponse { 30 | return this.create(HttpStatusCodes.HttpVersionNotSupported, message, data, meta); 31 | } 32 | 33 | public static variantAlsoNegotiates (message?: string, data?: any, meta?: any): ServerErrorResponse { 34 | return this.create(HttpStatusCodes.VariantAlsoNegotiates, message, data, meta); 35 | } 36 | 37 | public static insufficientStorage (message?: string, data?: any, meta?: any): ServerErrorResponse { 38 | return this.create(HttpStatusCodes.InsufficientStorage, message, data, meta); 39 | } 40 | 41 | public static loopDetected (message?: string, data?: any, meta?: any): ServerErrorResponse { 42 | return this.create(HttpStatusCodes.LoopDetected, message, data, meta); 43 | } 44 | 45 | public static notExtended (message?: string, data?: any, meta?: any): ServerErrorResponse { 46 | return this.create(HttpStatusCodes.NotExtended, message, data, meta); 47 | } 48 | 49 | public static networkAuthenticationRequired (message?: string, data?: any, meta?: any): ServerErrorResponse { 50 | return this.create(HttpStatusCodes.NetworkAuthenticationRequired, message, data, meta); 51 | } 52 | 53 | format () { 54 | const body: { message?: string, data?: any } = {}; 55 | 56 | if (this.message) { 57 | body.message = this.message; 58 | } 59 | 60 | if (this.data) { 61 | body.data = this.data; 62 | } 63 | 64 | return body; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Library/Response/SuccessfulResponse.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatusCodes } from '../Server'; 2 | import { Response } from './Response'; 3 | 4 | export class SuccessfulResponse extends Response { 5 | public static create (statusCode: HttpStatusCodes, data?: any, meta?: any): SuccessfulResponse { 6 | return new this({ statusCode, data, meta }); 7 | } 8 | 9 | public static ok (data?: any, meta?: any): SuccessfulResponse { 10 | return this.create(HttpStatusCodes.Ok, data, meta); 11 | } 12 | 13 | public static created (data?: any, meta?: any): SuccessfulResponse { 14 | return this.create(HttpStatusCodes.Created, data, meta); 15 | } 16 | 17 | public static accepted (data?: any, meta?: any): SuccessfulResponse { 18 | return this.create(HttpStatusCodes.Accepted, data, meta); 19 | } 20 | 21 | public static nonAuthoritativeInformation (data?: any, meta?: any): SuccessfulResponse { 22 | return this.create(HttpStatusCodes.NonAuthoritativeInformation, data, meta); 23 | } 24 | 25 | public static noContent (data?: any, meta?: any): SuccessfulResponse { 26 | return this.create(HttpStatusCodes.NoContent, data, meta); 27 | } 28 | 29 | public static resetContent (data?: any, meta?: any): SuccessfulResponse { 30 | return this.create(HttpStatusCodes.ResetContent, data, meta); 31 | } 32 | 33 | public static partialContent (data?: any, meta?: any): SuccessfulResponse { 34 | return this.create(HttpStatusCodes.PartialContent, data, meta); 35 | } 36 | 37 | public static multiStatus (data?: any, meta?: any): SuccessfulResponse { 38 | return this.create(HttpStatusCodes.MultiStatus, data, meta); 39 | } 40 | 41 | public static alreadyReported (data?: any, meta?: any): SuccessfulResponse { 42 | return this.create(HttpStatusCodes.AlreadyReported, data, meta); 43 | } 44 | 45 | public static imUsed (data?: any, meta?: any): SuccessfulResponse { 46 | return this.create(HttpStatusCodes.ImUsed, data, meta); 47 | } 48 | 49 | format () { 50 | // Like it says, no content. 51 | if (this.statusCode === HttpStatusCodes.NoContent) { 52 | return; 53 | } 54 | 55 | return this.data; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Library/Response/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AbstractResponseHelper'; 2 | export * from './Response'; 3 | export * from './ResponseTypes'; 4 | export * from './ServerErrorResponse'; 5 | export * from './SuccessfulResponse'; 6 | export * from './ClientErrorResponse'; 7 | export * from './InformationalResponse'; 8 | export * from './RedirectionResponse'; 9 | export * from './ResponseServiceFactory'; 10 | export * from './ResponseService'; 11 | -------------------------------------------------------------------------------- /src/Library/Router/RegisteredRouteInterface.ts: -------------------------------------------------------------------------------- 1 | import { RequestMethods } from '../Server'; 2 | import { Key } from 'path-to-regexp'; 3 | import { AbstractActionController } from '../Controller'; 4 | 5 | export interface RegisteredRouteInterface { 6 | method: RequestMethods; 7 | regex: RegExp; 8 | controller: typeof AbstractActionController; 9 | action: string; 10 | keys: Array; 11 | route: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/Library/Router/Route.ts: -------------------------------------------------------------------------------- 1 | import { RequestMethods } from '../Server'; 2 | import { RouteInterface } from './RouteInterface'; 3 | import { AbstractActionController } from '../Controller'; 4 | 5 | export class Route { 6 | public static method (method: RequestMethods, route: string, controller: string | typeof AbstractActionController, action: string): RouteInterface { 7 | return { method, route, action, controller: controller as typeof AbstractActionController }; 8 | } 9 | 10 | public static get (route: string, controller: string | typeof AbstractActionController, action: string): RouteInterface { 11 | return Route.method(RequestMethods.Get, route, controller, action); 12 | } 13 | 14 | public static post (route: string, controller: string | typeof AbstractActionController, action: string): RouteInterface { 15 | return Route.method(RequestMethods.Post, route, controller, action); 16 | } 17 | 18 | public static put (route: string, controller: string | typeof AbstractActionController, action: string): RouteInterface { 19 | return Route.method(RequestMethods.Put, route, controller, action); 20 | } 21 | 22 | public static patch (route: string, controller: string | typeof AbstractActionController, action: string): RouteInterface { 23 | return Route.method(RequestMethods.Patch, route, controller, action); 24 | } 25 | 26 | public static delete (route: string, controller: string | typeof AbstractActionController, action: string): RouteInterface { 27 | return Route.method(RequestMethods.Delete, route, controller, action); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Library/Router/RouteInterface.ts: -------------------------------------------------------------------------------- 1 | import { RequestMethods } from '../Server'; 2 | import { AbstractActionController } from '../Controller'; 3 | 4 | export interface RouteInterface { 5 | method: RequestMethods; 6 | route: string; 7 | controller: typeof AbstractActionController; 8 | action: string; 9 | meta?: any; 10 | } 11 | -------------------------------------------------------------------------------- /src/Library/Router/RouterService.ts: -------------------------------------------------------------------------------- 1 | import pathToRegexp, { Key } from 'path-to-regexp'; 2 | import { RequestMethods } from '../Server'; 3 | import { RegisteredRouteInterface } from './RegisteredRouteInterface'; 4 | import { RouteInterface } from './RouteInterface'; 5 | import { RouterConfigInterface } from '../Config'; 6 | import { AbstractActionController } from '../Controller'; 7 | 8 | export class RouterService { 9 | private routes: RegisteredRouteInterface[] = []; 10 | 11 | private config: RouterConfigInterface; 12 | 13 | constructor (config: RouterConfigInterface) { 14 | this.config = config; 15 | 16 | this.registerRoutes(this.config.routes); 17 | } 18 | 19 | public resolve (method: RequestMethods, target: string): { route: RegisteredRouteInterface, parameters: {} } | null { 20 | for (let i = 0; i < this.routes.length; i++) { 21 | const route = this.routes[i]; 22 | 23 | if (method !== route.method) { 24 | continue; 25 | } 26 | 27 | const parameters = this.match(target, route); 28 | 29 | if (parameters) { 30 | return { route, parameters }; 31 | } 32 | } 33 | 34 | return null; 35 | } 36 | 37 | public match (target: string, route: RegisteredRouteInterface): {} { 38 | const { regex, keys } = route; 39 | const result = regex.exec(target); 40 | 41 | if (!result) { 42 | return null; 43 | } 44 | 45 | // We don't care about the path. 46 | result.splice(0, 1); 47 | 48 | return this.buildParameters(keys, result); 49 | } 50 | 51 | public registerRoutes (routes: Array>): this { 52 | routes.forEach(newRoute => { 53 | if (Array.isArray(newRoute)) { 54 | return this.registerRoutes(newRoute); 55 | } 56 | 57 | const { method, route, action, controller } = newRoute; 58 | 59 | this.registerRoute(method, route, controller, action); 60 | }); 61 | 62 | return this; 63 | } 64 | 65 | public registerRoute (method: RequestMethods, route: string, controller: typeof AbstractActionController, action: string): this { 66 | const keys: Array = []; 67 | const regex: RegExp = pathToRegexp(route, keys); 68 | 69 | // Remove route if previously registered. 70 | const routeIndex = this.routes.findIndex(target => target.route === route && target.method === method); 71 | 72 | if (routeIndex > -1) { 73 | this.routes.splice(routeIndex, 1); 74 | } 75 | 76 | this.routes.push({ regex, controller, action, keys, method, route }); 77 | 78 | return this; 79 | } 80 | 81 | public getRegisteredRoutes (): RegisteredRouteInterface[] { 82 | return this.routes; 83 | } 84 | 85 | public buildParameters (from: Array, result: RegExpExecArray): {} { 86 | return result.reduce((params, match, index): {} => { 87 | return Object.assign(params, { [from[index].name]: match }); 88 | }, {}); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Library/Router/RouterServiceFactory.ts: -------------------------------------------------------------------------------- 1 | import { RouterService } from './RouterService'; 2 | import { Config, RouterConfigInterface } from '../Config'; 3 | import { ServiceManager } from '../ServiceManager'; 4 | import { FactoryInterface } from '../ServiceManager'; 5 | 6 | export const RouterServiceFactory: FactoryInterface = (sm: ServiceManager) => { 7 | return new RouterService(sm.get(Config).of('router')); 8 | }; 9 | -------------------------------------------------------------------------------- /src/Library/Router/index.ts: -------------------------------------------------------------------------------- 1 | export * from './RouterService'; 2 | export * from './RouterServiceFactory'; 3 | export * from './RegisteredRouteInterface'; 4 | export * from './RouteInterface'; 5 | export * from './Route'; 6 | -------------------------------------------------------------------------------- /src/Library/Server/HttpStatusCodes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Hypertext Transfer Protocol (HTTP) response status codes. 3 | * 4 | * @see {@link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes} 5 | */ 6 | export enum HttpStatusCodes { 7 | 8 | /** 9 | * The server has received the request headers and the client should proceed to send the request body 10 | * (in the case of a request for which a body needs to be sent; for example, a POST request). 11 | * Sending a large request body to a server after a request has been rejected for inappropriate headers would be inefficient. 12 | * To have a server check the request's headers, a client must send Expect: 100-continue as a header in its initial request 13 | * and receive a 100 Continue status code in response before sending the body. The response 417 Expectation Failed indicates the request should not be continued. 14 | */ 15 | Continue = 100, 16 | 17 | /** 18 | * The requester has asked the server to switch protocols and the server has agreed to do so. 19 | */ 20 | SwitchingProtocols = 101, 21 | 22 | /** 23 | * A WebDAV request may contain many sub-requests involving file operations, requiring a long time to complete the request. 24 | * This code indicates that the server has received and is processing the request, but no response is available yet. 25 | * This prevents the client from timing out and assuming the request was lost. 26 | */ 27 | Processing = 102, 28 | 29 | /** 30 | * Standard response for successful HTTP requests. 31 | * The actual response will depend on the request method used. 32 | * In a GET request, the response will contain an entity corresponding to the requested resource. 33 | * In a POST request, the response will contain an entity describing or containing the result of the action. 34 | */ 35 | Ok = 200, 36 | 37 | /** 38 | * The request has been fulfilled, resulting in the creation of a new resource. 39 | */ 40 | Created = 201, 41 | 42 | /** 43 | * The request has been accepted for processing, but the processing has not been completed. 44 | * The request might or might not be eventually acted upon, and may be disallowed when processing occurs. 45 | */ 46 | Accepted = 202, 47 | 48 | /** 49 | * SINCE HTTP/1.1 50 | * The server is a transforming proxy that received a 200 OK from its origin, 51 | * but is returning a modified version of the origin's response. 52 | */ 53 | NonAuthoritativeInformation = 203, 54 | 55 | /** 56 | * The server successfully processed the request and is not returning any content. 57 | */ 58 | NoContent = 204, 59 | 60 | /** 61 | * The server successfully processed the request, but is not returning any content. 62 | * Unlike a 204 response, this response requires that the requester reset the document view. 63 | */ 64 | ResetContent = 205, 65 | 66 | /** 67 | * The server is delivering only part of the resource (byte serving) due to a range header sent by the client. 68 | * The range header is used by HTTP clients to enable resuming of interrupted downloads, 69 | * or split a download into multiple simultaneous streams. 70 | */ 71 | PartialContent = 206, 72 | 73 | /** 74 | * The message body that follows is an XML message and can contain a number of separate response codes, 75 | * depending on how many sub-requests were made. 76 | */ 77 | MultiStatus = 207, 78 | 79 | /** 80 | * The members of a DAV binding have already been enumerated in a preceding part of the (multistatus) response, 81 | * and are not being included again. 82 | */ 83 | AlreadyReported = 208, 84 | 85 | /** 86 | * The server has fulfilled a request for the resource, 87 | * and the response is a representation of the result of one or more instance-manipulations applied to the current instance. 88 | */ 89 | ImUsed = 226, 90 | 91 | /** 92 | * Indicates multiple options for the resource from which the client may choose (via agent-driven content negotiation). 93 | * For example, this code could be used to present multiple video format options, 94 | * to list files with different filename extensions, or to suggest word-sense disambiguation. 95 | */ 96 | MultipleChoices = 300, 97 | 98 | /** 99 | * This and all future requests should be directed to the given URI. 100 | */ 101 | MovedPermanently = 301, 102 | 103 | /** 104 | * This is an example of industry practice contradicting the standard. 105 | * The HTTP/1.0 specification (RFC 1945) required the client to perform a temporary redirect 106 | * (the original describing phrase was "Moved Temporarily"), but popular browsers implemented 302 107 | * with the functionality of a 303 See Other. Therefore, HTTP/1.1 added status codes 303 and 307 108 | * to distinguish between the two behaviours. However, some Web applications and frameworks 109 | * use the 302 status code as if it were the 303. 110 | */ 111 | Found = 302, 112 | 113 | /** 114 | * SINCE HTTP/1.1 115 | * The response to the request can be found under another URI using a GET method. 116 | * When received in response to a POST (or PUT/DELETE), the client should presume that 117 | * the server has received the data and should issue a redirect with a separate GET message. 118 | */ 119 | SeeOther = 303, 120 | 121 | /** 122 | * Indicates that the resource has not been modified since the version specified by the request headers If-Modified-Since or If-None-Match. 123 | * In such case, there is no need to retransmit the resource since the client still has a previously-downloaded copy. 124 | */ 125 | NotModified = 304, 126 | 127 | /** 128 | * SINCE HTTP/1.1 129 | * The requested resource is available only through a proxy, the address for which is provided in the response. 130 | * Many HTTP clients (such as Mozilla and Internet Explorer) do not correctly handle responses with this status code, primarily for security reasons. 131 | */ 132 | UseProxy = 305, 133 | 134 | /** 135 | * No longer used. Originally meant "Subsequent requests should use the specified proxy." 136 | */ 137 | SwitchProxy = 306, 138 | 139 | /** 140 | * SINCE HTTP/1.1 141 | * In this case, the request should be repeated with another URI; however, future requests should still use the original URI. 142 | * In contrast to how 302 was historically implemented, the request method is not allowed to be changed when reissuing the original request. 143 | * For example, a POST request should be repeated using another POST request. 144 | */ 145 | TemporaryRedirect = 307, 146 | 147 | /** 148 | * The request and all future requests should be repeated using another URI. 149 | * 307 and 308 parallel the behaviors of 302 and 301, but do not allow the HTTP method to change. 150 | * So, for example, submitting a form to a permanently redirected resource may continue smoothly. 151 | */ 152 | PermanentRedirect = 308, 153 | 154 | /** 155 | * The server cannot or will not process the request due to an apparent client error 156 | * (e.g., malformed request syntax, too large size, invalid request message framing, or deceptive request routing). 157 | */ 158 | BadRequest = 400, 159 | 160 | /** 161 | * Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet 162 | * been provided. The response must include a WWW-Authenticate header field containing a challenge applicable to the 163 | * requested resource. See Basic access authentication and Digest access authentication. 401 semantically means 164 | * "unauthenticated",i.e. the user does not have the necessary credentials. 165 | */ 166 | Unauthorized = 401, 167 | 168 | /** 169 | * Reserved for future use. The original intention was that this code might be used as part of some form of digital 170 | * cash or micro payment scheme, but that has not happened, and this code is not usually used. 171 | * Google Developers API uses this status if a particular developer has exceeded the daily limit on requests. 172 | */ 173 | PaymentRequired = 402, 174 | 175 | /** 176 | * The request was valid, but the server is refusing action. 177 | * The user might not have the necessary permissions for a resource. 178 | */ 179 | Forbidden = 403, 180 | 181 | /** 182 | * The requested resource could not be found but may be available in the future. 183 | * Subsequent requests by the client are permissible. 184 | */ 185 | NotFound = 404, 186 | 187 | /** 188 | * A request method is not supported for the requested resource; 189 | * for example, a GET request on a form that requires data to be presented via POST, or a PUT request on a read-only resource. 190 | */ 191 | MethodNotAllowed = 405, 192 | 193 | /** 194 | * The requested resource is capable of generating only content not acceptable according to the Accept headers sent in the request. 195 | */ 196 | NotAcceptable = 406, 197 | 198 | /** 199 | * The client must first authenticate itself with the proxy. 200 | */ 201 | ProxyAuthenticationRequired = 407, 202 | 203 | /** 204 | * The server timed out waiting for the request. 205 | * According to HTTP specifications: 206 | * "The client did not produce a request within the time that the server was prepared to wait. The client MAY repeat the request without modifications at any later time." 207 | */ 208 | RequestTimeout = 408, 209 | 210 | /** 211 | * Indicates that the request could not be processed because of conflict in the request, 212 | * such as an edit conflict between multiple simultaneous updates. 213 | */ 214 | Conflict = 409, 215 | 216 | /** 217 | * Indicates that the resource requested is no longer available and will not be available again. 218 | * This should be used when a resource has been intentionally removed and the resource should be purged. 219 | * Upon receiving a 410 status code, the client should not request the resource in the future. 220 | * Clients such as search engines should remove the resource from their indices. 221 | * Most use cases do not require clients and search engines to purge the resource, and a "404 Not Found" may be used instead. 222 | */ 223 | Gone = 410, 224 | 225 | /** 226 | * The request did not specify the length of its content, which is required by the requested resource. 227 | */ 228 | LengthRequired = 411, 229 | 230 | /** 231 | * The server does not meet one of the preconditions that the requester put on the request. 232 | */ 233 | PreconditionFailed = 412, 234 | 235 | /** 236 | * The request is larger than the server is willing or able to process. Previously called "Request Entity Too Large". 237 | */ 238 | PayloadTooLarge = 413, 239 | 240 | /** 241 | * The URI provided was too long for the server to process. Often the result of too much data being encoded as a query-string of a GET request, 242 | * in which case it should be converted to a POST request. 243 | * Called "Request-URI Too Long" previously. 244 | */ 245 | UriTooLong = 414, 246 | 247 | /** 248 | * The request entity has a media type which the server or resource does not support. 249 | * For example, the client uploads an image as image/svg+xml, but the server requires that images use a different format. 250 | */ 251 | UnsupportedMediaType = 415, 252 | 253 | /** 254 | * The client has asked for a portion of the file (byte serving), but the server cannot supply that portion. 255 | * For example, if the client asked for a part of the file that lies beyond the end of the file. 256 | * Called "Requested Range Not Satisfiable" previously. 257 | */ 258 | RangeNotSatisfiable = 416, 259 | 260 | /** 261 | * The server cannot meet the requirements of the Expect request-header field. 262 | */ 263 | ExpectationFailed = 417, 264 | 265 | /** 266 | * This code was defined in 1998 as one of the traditional IETF April Fools' jokes, in RFC 2324, Hyper Text Coffee Pot Control Protocol, 267 | * and is not expected to be implemented by actual HTTP servers. The RFC specifies this code should be returned by 268 | * teapots requested to brew coffee. This HTTP status is used as an Easter egg in some websites, including Google.com. 269 | */ 270 | IAmATeapot = 418, 271 | 272 | /** 273 | * The request was directed at a server that is not able to produce a response (for example because a connection reuse). 274 | */ 275 | MisdirectedRequest = 421, 276 | 277 | /** 278 | * The request was well-formed but was unable to be followed due to semantic errors. 279 | */ 280 | UnprocessableEntity = 422, 281 | 282 | /** 283 | * The resource that is being accessed is locked. 284 | */ 285 | Locked = 423, 286 | 287 | /** 288 | * The request failed due to failure of a previous request (e.g., a PROPPATCH). 289 | */ 290 | FailedDependency = 424, 291 | 292 | /** 293 | * The client should switch to a different protocol such as TLS/1.0, given in the Upgrade header field. 294 | */ 295 | UpgradeRequired = 426, 296 | 297 | /** 298 | * The origin server requires the request to be conditional. 299 | * Intended to prevent "the 'lost update' problem, where a client 300 | * GETs a resource's state, modifies it, and PUTs it back to the server, 301 | * when meanwhile a third party has modified the state on the server, leading to a conflict." 302 | */ 303 | PreconditionRequired = 428, 304 | 305 | /** 306 | * The user has sent too many requests in a given amount of time. Intended for use with rate-limiting schemes. 307 | */ 308 | TooManyRequests = 429, 309 | 310 | /** 311 | * The server is unwilling to process the request because either an individual header field, 312 | * or all the header fields collectively, are too large. 313 | */ 314 | RequestHeaderFieldsTooLarge = 431, 315 | 316 | /** 317 | * A server operator has received a legal demand to deny access to a resource or to a set of resources 318 | * that includes the requested resource. The code 451 was chosen as a reference to the novel Fahrenheit 451. 319 | */ 320 | UnavailableForLegalReasons = 451, 321 | 322 | /** 323 | * A generic error message, given when an unexpected condition was encountered and no more specific message is suitable. 324 | */ 325 | InternalServerError = 500, 326 | 327 | /** 328 | * The server either does not recognize the request method, or it lacks the ability to fulfill the request. 329 | * Usually this implies future availability (e.g., a new feature of a web-service API). 330 | */ 331 | NotImplemented = 501, 332 | 333 | /** 334 | * The server was acting as a gateway or proxy and received an invalid response from the upstream server. 335 | */ 336 | BadGateway = 502, 337 | 338 | /** 339 | * The server is currently unavailable (because it is overloaded or down for maintenance). 340 | * Generally, this is a temporary state. 341 | */ 342 | ServiceUnavailable = 503, 343 | 344 | /** 345 | * The server was acting as a gateway or proxy and did not receive a timely response from the upstream server. 346 | */ 347 | GatewayTimeout = 504, 348 | 349 | /** 350 | * The server does not support the HTTP protocol version used in the request 351 | */ 352 | HttpVersionNotSupported = 505, 353 | 354 | /** 355 | * Transparent content negotiation for the request results in a circular reference. 356 | */ 357 | VariantAlsoNegotiates = 506, 358 | 359 | /** 360 | * The server is unable to store the representation needed to complete the request. 361 | */ 362 | InsufficientStorage = 507, 363 | 364 | /** 365 | * The server detected an infinite loop while processing the request. 366 | */ 367 | LoopDetected = 508, 368 | 369 | /** 370 | * Further extensions to the request are required for the server to fulfill it. 371 | */ 372 | NotExtended = 510, 373 | 374 | /** 375 | * The client needs to authenticate to gain network access. 376 | * Intended for use by intercepting proxies used to control access to the network (e.g., "captive portals" used 377 | * to require agreement to Terms of Service before granting full Internet access via a Wi-Fi hotspot). 378 | */ 379 | NetworkAuthenticationRequired = 511, 380 | } 381 | -------------------------------------------------------------------------------- /src/Library/Server/RequestMethods.ts: -------------------------------------------------------------------------------- 1 | export enum RequestMethods { 2 | Acl='ACL', 3 | Bind='BIND', 4 | Checkout='CHECKOUT', 5 | Connect='CONNECT', 6 | Copy='COPY', 7 | Delete='DELETE', 8 | Get='GET', 9 | Head='HEAD', 10 | Link='LINK', 11 | Lock='LOCK', 12 | MSearch='M-SEARCH', 13 | Merge='MERGE', 14 | MkActivity='MKACTIVITY', 15 | MkCalendar='MKCALENDAR', 16 | MkCol='MKCOL', 17 | Move='MOVE', 18 | Notify='NOTIFY', 19 | Options='OPTIONS', 20 | Patch='PATCH', 21 | Post='POST', 22 | PropFind='PROPFIND', 23 | PropPatch='PROPPATCH', 24 | Purge='PURGE', 25 | Put='PUT', 26 | Rebind='REBIND', 27 | Report='REPORT', 28 | Search='SEARCH', 29 | Source='SOURCE', 30 | Subscribe='SUBSCRIBE', 31 | Trace='TRACE', 32 | Unbind='UNBIND', 33 | Unlink='UNLINK', 34 | Unlock='UNLOCK', 35 | Unsubscribe='UNSUBSCRIBE', 36 | } 37 | -------------------------------------------------------------------------------- /src/Library/Server/ServerService.ts: -------------------------------------------------------------------------------- 1 | import Koa, { Middleware } from 'koa'; 2 | import url from 'url'; 3 | import https from 'https'; 4 | import { ApplicationModes } from '../Application'; 5 | import { AbstractMiddleware, MiddlewareLookupType, MiddlewareType, RegisteredMiddlewareType } from '../Middleware'; 6 | import { ServerConfigInterface } from '../Config'; 7 | 8 | export class ServerService { 9 | private readonly server: Koa; 10 | 11 | private readonly config: ServerConfigInterface; 12 | 13 | private middleware: Array = []; 14 | 15 | constructor (mode: ApplicationModes, config: ServerConfigInterface, middleware: Array) { 16 | this.config = config; 17 | 18 | if (mode === ApplicationModes.Server) { 19 | this.server = new Koa(); 20 | this.middleware = this.server.middleware; 21 | } 22 | 23 | this.use(...middleware); 24 | } 25 | 26 | public use (...middlewares: Array): this { 27 | middlewares.forEach(middleware => { 28 | this.middleware.push(this.asCallback(middleware)); 29 | }); 30 | 31 | return this; 32 | } 33 | 34 | public useBefore (middleware: MiddlewareLookupType, ...middlewares: Array): this { 35 | return this.updateMiddleware(this.indexOfMiddleware(middleware), 0, ...middlewares); 36 | } 37 | 38 | public useAfter (middleware: MiddlewareLookupType, ...middlewares: Array): this { 39 | return this.updateMiddleware(this.indexOfMiddleware(middleware) + 1, 0, ...middlewares); 40 | } 41 | 42 | public replace (middleware: MiddlewareLookupType, ...middlewares: Array): this { 43 | return this.updateMiddleware(this.indexOfMiddleware(middleware), 1, ...middlewares); 44 | } 45 | 46 | public updateMiddleware (at: number, remove: number, ...middlewares: Array): this { 47 | this.middleware.splice(at === -1 ? 0 : at, remove, ...middlewares.map(middleware => this.asCallback(middleware))); 48 | 49 | return this; 50 | } 51 | 52 | public indexOfMiddleware (middleware: MiddlewareLookupType) { 53 | if (!this.server) { 54 | return -1; 55 | } 56 | 57 | return this.middleware.findIndex((suspect: RegisteredMiddlewareType) => { 58 | if (typeof middleware === 'string') { 59 | if (suspect._name) { 60 | return suspect._name === middleware; 61 | } 62 | 63 | return suspect.name === middleware; 64 | } 65 | 66 | if (suspect._fromClass) { 67 | return middleware === suspect._fromClass; 68 | } 69 | 70 | return suspect === middleware; 71 | }); 72 | } 73 | 74 | public getServer (): Koa { 75 | return this.server; 76 | } 77 | 78 | public getURL () { 79 | const { port, ssl, hostname } = this.config; 80 | 81 | return url.format(`http${ssl ? 's' : ''}://${hostname}:${port == 80 ? '' : port}`); 82 | } 83 | 84 | public start (): this { 85 | if (this.config.ssl) { 86 | https.createServer(this.config.ssl, this.server.callback()).listen(this.config.port); 87 | } else { 88 | this.server.listen(this.config.port); 89 | } 90 | 91 | return this; 92 | } 93 | 94 | private asCallback (middleware: MiddlewareType): Middleware { 95 | return middleware instanceof AbstractMiddleware ? middleware.asCallback() : middleware; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Library/Server/ServerServiceFactory.ts: -------------------------------------------------------------------------------- 1 | import bodyParser from 'koa-bodyparser'; 2 | import { ServiceManagerInterface } from '../ServiceManager'; 3 | import { ServerService } from './ServerService'; 4 | import { Application } from '../Application'; 5 | import { DispatchMiddleware, RequestMiddleware, RouterMiddleware } from '../Middleware'; 6 | import { Config, ServerConfigInterface } from '../Config'; 7 | import cors from '@koa/cors'; 8 | 9 | export const ServerServiceFactory = (sm: ServiceManagerInterface) => { 10 | const config = sm.get(Config).of('server'); 11 | const middleware = [ 12 | sm.get(RequestMiddleware), 13 | bodyParser(), 14 | sm.get(RouterMiddleware), 15 | sm.get(DispatchMiddleware), 16 | ]; 17 | 18 | if (config.cors.enabled) { 19 | middleware.unshift(cors(config.cors.options)); 20 | } 21 | 22 | return new ServerService(sm.get(Application).getMode(), config, middleware); 23 | }; 24 | -------------------------------------------------------------------------------- /src/Library/Server/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ServerService'; 2 | export * from './ServerServiceFactory'; 3 | export * from './HttpStatusCodes'; 4 | export * from './RequestMethods'; 5 | -------------------------------------------------------------------------------- /src/Library/ServiceManager/AbstractFileBasedPluginManager.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { FileBasedPluginType, ServiceManagerConfigType } from '.'; 4 | import { ServiceManager, AbstractPluginManager } from '../ServiceManager'; 5 | import { Instantiable } from '../Core'; 6 | import { createDebugLogger } from '../../debug'; 7 | 8 | const debug = createDebugLogger('pluginManager'); 9 | 10 | export class AbstractFileBasedPluginManager extends AbstractPluginManager { 11 | constructor (creationContext: ServiceManager, locations: string[], config: ServiceManagerConfigType) { 12 | super(creationContext, config); 13 | 14 | if (locations) { 15 | this.loadFromLocations(locations); 16 | } 17 | } 18 | 19 | public static getPluginName (plugin: FileBasedPluginType): string { 20 | if (typeof plugin === 'string') { 21 | return plugin; 22 | } 23 | 24 | return plugin.name; 25 | } 26 | 27 | public loadFromLocations (pluginDirectories?: string[]): this { 28 | if (pluginDirectories) { 29 | pluginDirectories.forEach(directory => this.loadDirectory(directory)); 30 | } 31 | 32 | return this; 33 | } 34 | 35 | public loadDirectory (pluginDirectory: string) { 36 | let stat; 37 | 38 | try { 39 | stat = fs.statSync(pluginDirectory); 40 | } catch (error) { 41 | if (error.code === 'ENOENT') { 42 | debug(`Directory "${pluginDirectory}" not found for plugin manager.`); 43 | 44 | return; 45 | } 46 | 47 | throw error; 48 | } 49 | 50 | if (!stat.isDirectory()) { 51 | throw new Error('Plugin location must be a directory.'); 52 | } 53 | 54 | const plugins: Array> = fs.readdirSync(pluginDirectory) 55 | .filter((fileName: string) => !!fileName.match(/^(?!.*index\.(js|ts)$).*(^.?|\.[^d]|[^.]d|[^.][^d])\.(js|ts)$/)) 56 | .map((fileName: string) => fileName.replace(/\.(js|ts)$/, '')) 57 | .map((plugin: string) => { 58 | const Plugin = require(path.resolve(pluginDirectory, plugin)); 59 | 60 | if (typeof Plugin === 'function') { 61 | return Plugin; 62 | } 63 | 64 | if (typeof Plugin.default === 'function') { 65 | return Plugin.default; 66 | } 67 | 68 | if (typeof Plugin[plugin] === 'function') { 69 | return Plugin[plugin]; 70 | } 71 | 72 | throw new TypeError(`Unable to load plugin "${plugin}" due to missing constructable export.`); 73 | }); 74 | 75 | this.registerPlugins(plugins); 76 | } 77 | 78 | public getPlugin (plugin: Instantiable): Object { 79 | return this.get(plugin); 80 | } 81 | 82 | protected registerPlugins (plugins: Array>): this { 83 | plugins.forEach(Plugin => this.registerPlugin(Plugin)); 84 | 85 | return this; 86 | } 87 | 88 | protected registerPlugin (Plugin: Instantiable): this { 89 | this.registerInvokable(Plugin, Plugin); 90 | 91 | this.registerAlias(Plugin.name, Plugin); 92 | 93 | return this; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Library/ServiceManager/AbstractPluginManager.ts: -------------------------------------------------------------------------------- 1 | import { ServiceManager } from './ServiceManager'; 2 | import { ServiceManagerConfigType } from './ServiceManagerConfigInterface'; 3 | 4 | export abstract class AbstractPluginManager extends ServiceManager { 5 | protected creationContext: ServiceManager; 6 | 7 | constructor (creationContext: ServiceManager, config?: ServiceManagerConfigType) { 8 | super(config); 9 | 10 | this.creationContext = creationContext; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Library/ServiceManager/FactoryInterface.ts: -------------------------------------------------------------------------------- 1 | export interface FactoryInterface extends Function { 2 | } 3 | -------------------------------------------------------------------------------- /src/Library/ServiceManager/FileBasedPluginManagerConfigType.ts: -------------------------------------------------------------------------------- 1 | import { ServiceManagerConfigType } from '../ServiceManager'; 2 | 3 | export type FileBasedPluginManagerConfigType = Partial<{ 4 | plugins: ServiceManagerConfigType; 5 | locations: string[]; 6 | }>; 7 | -------------------------------------------------------------------------------- /src/Library/ServiceManager/FileBasedPluginType.ts: -------------------------------------------------------------------------------- 1 | export type FileBasedPluginType = string | Function; 2 | -------------------------------------------------------------------------------- /src/Library/ServiceManager/InjectedServiceFactory.ts: -------------------------------------------------------------------------------- 1 | import { Instantiable } from '../Core'; 2 | import { getDependencies } from './decorators'; 3 | import { ServiceManager } from './ServiceManager'; 4 | 5 | export type InjectedFactoryPluginType = (sm: ServiceManager, service?: Object) => Object; 6 | 7 | type DIArgumentsType = { 8 | property: string; 9 | dependency: Instantiable; 10 | plugin?: InjectedFactoryPluginType; 11 | }; 12 | 13 | type ServiceType = { [property: string]: any }; 14 | 15 | export const InjectedServiceFactory = (Service: Instantiable) => (sm: ServiceManager) => { 16 | const service = new Service; 17 | const dependencies = getDependencies(service); 18 | 19 | if (!dependencies) { 20 | return service; 21 | } 22 | 23 | return dependencies.reduce((service: ServiceType, { property, dependency, plugin }: DIArgumentsType) => { 24 | service[property] = plugin ? plugin(sm, service) : sm.get(dependency); 25 | 26 | return service; 27 | }, service); 28 | }; 29 | -------------------------------------------------------------------------------- /src/Library/ServiceManager/ServiceManager.ts: -------------------------------------------------------------------------------- 1 | import { ServiceManagerInterface } from './ServiceManagerInterface'; 2 | import { ServicesMapType, FactoriesMapType, AliasesType, ServiceKeyType, ServiceFactoryType, SharedMapType, ServiceManagerConfigType } from './ServiceManagerConfigInterface'; 3 | import { NotFoundError } from '../Error'; 4 | import { Instantiable } from '../Core'; 5 | import { InjectedServiceFactory } from './InjectedServiceFactory'; 6 | import { applyPatches } from './decorators'; 7 | 8 | /** 9 | * @export 10 | * @class ServiceManager 11 | * @implements {ServiceManagerInterface} 12 | */ 13 | export class ServiceManager implements ServiceManagerInterface { 14 | private services: ServicesMapType = new Map(); 15 | 16 | private factories: FactoriesMapType = new Map(); 17 | 18 | private aliases: AliasesType = {}; 19 | 20 | private shared: SharedMapType = new Map(); 21 | 22 | private sharedByDefault: boolean = true; 23 | 24 | protected creationContext: ServiceManager; 25 | 26 | constructor (config?: ServiceManagerConfigType) { 27 | this.creationContext = this; 28 | 29 | if (config) { 30 | this.configure(config); 31 | } 32 | } 33 | 34 | public get (Service: ServiceKeyType, forceTransient: boolean = false): T { 35 | const resolvedName = this.resolveName(Service) as ServiceKeyType; 36 | 37 | if (!this.has(resolvedName)) { 38 | throw new NotFoundError(`Unable to locate service "${typeof Service === 'string' ? Service : Service.name}".`); 39 | } 40 | 41 | if (forceTransient || !this.services.has(resolvedName)) { 42 | const service = this.factories.get(resolvedName)(this.creationContext) as T; 43 | const shared = this.shared.has(resolvedName) ? this.shared.get(resolvedName) : this.sharedByDefault; 44 | 45 | // Apply any patches registered with the @patch decorator 46 | applyPatches(this.creationContext, service); 47 | 48 | // We just needed a new instance or we don't want our instances to be shared.. Return service. 49 | if (forceTransient || !shared) { 50 | return service; 51 | } 52 | 53 | this.services.set(resolvedName, service); 54 | } 55 | 56 | return this.services.get(resolvedName) as T; 57 | } 58 | 59 | has (Service: ServiceKeyType): boolean { 60 | const resolvedName = this.resolveName(Service) as ServiceKeyType; 61 | 62 | return this.services.has(resolvedName) || this.factories.has(resolvedName); 63 | } 64 | 65 | public registerFactory (key: Function | string, value: ServiceFactoryType): this { 66 | this.factories.set(key, value); 67 | 68 | return this; 69 | } 70 | 71 | public registerFactories (factories: FactoriesMapType): this { 72 | factories.forEach((value: ServiceFactoryType, key: Function | string) => { 73 | this.registerFactory(key, value); 74 | }); 75 | 76 | return this; 77 | } 78 | 79 | public registerService (key: Function | string, service: Object): this { 80 | this.services.set(key, service); 81 | 82 | return this; 83 | } 84 | 85 | public configure (config: ServiceManagerConfigType): this { 86 | if (typeof config.sharedByDefault === 'boolean') { 87 | this.sharedByDefault = config.sharedByDefault; 88 | } 89 | 90 | if (config.shared instanceof Map) { 91 | config.shared.forEach((value, key: ServiceKeyType) => this.shared.set(key, value)); 92 | } 93 | 94 | if (config.services instanceof Map) { 95 | config.services.forEach((value, key: ServiceKeyType) => this.services.set(key, value)); 96 | } 97 | 98 | if (config.invokables instanceof Map) { 99 | config.invokables.forEach((value: Instantiable, key: ServiceKeyType) => { 100 | this.registerInvokable(key, value); 101 | }); 102 | } 103 | 104 | if (config.factories instanceof Map) { 105 | this.registerFactories(config.factories); 106 | } 107 | 108 | if (config.aliases) { 109 | Object.assign(this.aliases, config.aliases); 110 | } 111 | 112 | return this; 113 | } 114 | 115 | public registerInvokable (key: ServiceKeyType, value: Instantiable) { 116 | this.factories.set(key, InjectedServiceFactory(value)); 117 | } 118 | 119 | public registerAliases (aliases: AliasesType): this { 120 | Object.assign(this.aliases, aliases); 121 | 122 | return this; 123 | } 124 | 125 | public registerAlias (alias: string, to: string | Function): this { 126 | this.aliases[alias] = to; 127 | 128 | return this; 129 | } 130 | 131 | private resolveName (name: ServiceKeyType): ServiceKeyType { 132 | if (typeof name !== 'string') { 133 | return name; 134 | } 135 | 136 | return this.aliases[name] as ServiceKeyType || name; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Library/ServiceManager/ServiceManagerConfigInterface.ts: -------------------------------------------------------------------------------- 1 | import { ServiceManagerInterface } from './ServiceManagerInterface'; 2 | import { Instantiable } from '../Core'; 3 | import { FactoryInterface } from './FactoryInterface'; 4 | 5 | export interface ServiceFactoryType extends Function { (sm?: ServiceManagerInterface): T; } 6 | 7 | export type FactoriesMapType = Map; 8 | 9 | export type ServicesMapType = Map; 10 | 11 | export type InvokablesMapType = Map | string, Instantiable>; 12 | 13 | export type AliasesType = { [alias: string]: string | Function }; 14 | 15 | export type SharedMapType = Map; 16 | 17 | export type ServiceKeyType = Instantiable | string; 18 | 19 | export type ServiceManagerConfigType = Partial<{ 20 | sharedByDefault: boolean; 21 | shared: SharedMapType; 22 | services: ServicesMapType; 23 | factories: FactoriesMapType; 24 | aliases: AliasesType; 25 | invokables: InvokablesMapType; 26 | }>; 27 | -------------------------------------------------------------------------------- /src/Library/ServiceManager/ServiceManagerInterface.ts: -------------------------------------------------------------------------------- 1 | import { ServiceKeyType } from './ServiceManagerConfigInterface'; 2 | 3 | export interface ServiceManagerInterface { 4 | get (Service: ServiceKeyType): T; 5 | } 6 | -------------------------------------------------------------------------------- /src/Library/ServiceManager/decorators/config.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '../../Config'; 2 | import { ServiceManager } from '../ServiceManager'; 3 | import { inject } from './inject'; 4 | 5 | export const config = (of?: string) => inject(null, (sm: ServiceManager) => sm.get(Config).of(of)); 6 | -------------------------------------------------------------------------------- /src/Library/ServiceManager/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './inject'; 2 | export * from './patch'; 3 | export * from './config'; 4 | -------------------------------------------------------------------------------- /src/Library/ServiceManager/decorators/inject.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { InjectedFactoryPluginType } from '../InjectedServiceFactory'; 3 | 4 | const metaKey = Symbol('stix:di:inject'); 5 | 6 | export const inject = (dependency?: any, plugin?: InjectedFactoryPluginType) => { 7 | return (target: Object, property: string) => { 8 | const meta = Reflect.getMetadata(metaKey, target); 9 | const result = Array.isArray(meta) 10 | ? meta.concat({ property, dependency, plugin }) 11 | : [ { property, dependency, plugin } ]; 12 | 13 | Reflect.defineMetadata(metaKey, result, target); 14 | }; 15 | }; 16 | 17 | export const getDependencies = (target: Object) => Reflect.getMetadata(metaKey, target); 18 | -------------------------------------------------------------------------------- /src/Library/ServiceManager/decorators/patch.ts: -------------------------------------------------------------------------------- 1 | import { ServiceManager } from '../ServiceManager'; 2 | 3 | const metaKey = Symbol('stix:di:patch'); 4 | 5 | type PatchArgumentsType = { 6 | name: string; 7 | method: Function; 8 | factory: boolean; 9 | }; 10 | 11 | export const patch = (name: string, method: Function, factory: boolean = false) => { 12 | return (target: ServiceType) => { 13 | const meta = Reflect.getMetadata(metaKey, target) || []; 14 | 15 | meta.push({ name, method, factory }); 16 | 17 | Reflect.defineMetadata(metaKey, meta, target); 18 | }; 19 | }; 20 | 21 | export const applyPatches = (sm: ServiceManager, target: ServiceType) => { 22 | const patches = Reflect.getMetadata(metaKey, target.constructor); 23 | 24 | if (!patches) { 25 | return target; 26 | } 27 | 28 | return patches.reduce((target: ServiceType, { name, method, factory }: PatchArgumentsType) => { 29 | if (typeof target[name] !== 'function') { 30 | target[name] = (factory ? method(sm, target) : method).bind(target); 31 | } 32 | 33 | return target; 34 | }, target); 35 | }; 36 | 37 | type ServiceType = { [property: string]: any }; 38 | -------------------------------------------------------------------------------- /src/Library/ServiceManager/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ServiceManager'; 2 | export * from './ServiceManagerInterface'; 3 | export * from './ServiceManagerConfigInterface'; 4 | export * from './FactoryInterface'; 5 | export * from './FileBasedPluginType'; 6 | export * from './AbstractPluginManager'; 7 | export * from './AbstractFileBasedPluginManager'; 8 | export * from './decorators'; 9 | -------------------------------------------------------------------------------- /src/Library/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Application'; 2 | export * from './Cli'; 3 | export * from './Command'; 4 | export * from './Config'; 5 | export * from './Controller'; 6 | export * from './Core'; 7 | export * from './Error'; 8 | export * from './EventManager'; 9 | export * from './Interface'; 10 | export * from './Logger'; 11 | export * from './Middleware'; 12 | export * from './ModuleManager'; 13 | export * from './Output'; 14 | export * from './Response'; 15 | export * from './Router'; 16 | export * from './Server'; 17 | export * from './ServiceManager'; 18 | -------------------------------------------------------------------------------- /src/config/cli.ts: -------------------------------------------------------------------------------- 1 | import { Cli } from '../Library/Cli'; 2 | import { HelpCommand } from '../Library/Command/HelpCommand'; 3 | 4 | const fallbackToken = 'help'; 5 | const bin = 'stix'; 6 | 7 | export const cli = { 8 | bin, 9 | fallbackToken, 10 | title: 'Stix CLI', 11 | subtitle: 'Stix CLI tools for your stix CLI needs. \n\t...And by that we mean stix projects.', 12 | defaultProgramName: 'project', 13 | defaultProgramDescription: 'Project-scope commands.', 14 | defaultProgramFooter: '', 15 | commands: [ 16 | Cli.program('cli', { 17 | commands: [ 18 | Cli.command('help [command]', HelpCommand, 'output', { 19 | description: 'Output help for provided command', 20 | }), 21 | ], 22 | examples: [ 23 | `$ ${bin} ${fallbackToken}`, 24 | `$ ${bin} ${fallbackToken} some:command`, 25 | ], 26 | }), 27 | ], 28 | }; 29 | -------------------------------------------------------------------------------- /src/config/command.ts: -------------------------------------------------------------------------------- 1 | import { CommandManagerConfigType } from '../Library/Command'; 2 | import { HelpCommand } from '../Library/Command'; 3 | 4 | export const command: CommandManagerConfigType = { 5 | commands: { 6 | invokables: new Map([ 7 | [ HelpCommand, HelpCommand ], 8 | ]), 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './logger'; 2 | export * from './cli'; 3 | export * from './command'; 4 | export * from './response'; 5 | export * from './routes'; 6 | export * from './server'; 7 | export * from './services'; 8 | -------------------------------------------------------------------------------- /src/config/logger.ts: -------------------------------------------------------------------------------- 1 | import winston from 'winston'; 2 | 3 | export const logger = { 4 | level: 'error', 5 | exitOnError: false, 6 | transports: [ new winston.transports.Console({ format: winston.format.simple() }) ], 7 | }; 8 | -------------------------------------------------------------------------------- /src/config/response.ts: -------------------------------------------------------------------------------- 1 | import { SuccessfulResponse, InformationalResponse, RedirectionResponse, ClientErrorResponse, ServerErrorResponse } from '../Library/Response'; 2 | import { ResponseConfigInterface } from '../Library'; 3 | 4 | /** 5 | * Response configuration. 6 | * 7 | * Register your custom responses with stix. 8 | * This allows stix and stix modules to use your response classes (instead of the builtin classes) to create responses. 9 | */ 10 | export const response: ResponseConfigInterface = { 11 | responses: { 12 | informational: InformationalResponse, 13 | successful: SuccessfulResponse, 14 | redirection: RedirectionResponse, 15 | clientError: ClientErrorResponse, 16 | serverError: ServerErrorResponse, 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /src/config/routes.ts: -------------------------------------------------------------------------------- 1 | import { RouterConfigInterface } from '../Library/Config'; 2 | 3 | export const router: RouterConfigInterface = { 4 | routes: [], 5 | }; 6 | -------------------------------------------------------------------------------- /src/config/server.ts: -------------------------------------------------------------------------------- 1 | import { ServerConfigInterface } from '../Library/Config'; 2 | 3 | export const server: ServerConfigInterface = { 4 | hostname: 'localhost', 5 | port: 1991, 6 | cors: { 7 | enabled: false, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/config/services.ts: -------------------------------------------------------------------------------- 1 | import { ServerService, ServerServiceFactory } from '../Library/Server'; 2 | import { RouterService, RouterServiceFactory } from '../Library/Router'; 3 | import { FactoryInterface } from '../Library/ServiceManager'; 4 | import { ControllerManager, ControllerManagerFactory } from '../Library/Controller'; 5 | import { CommandManager, CommandManagerFactory } from '../Library/Command'; 6 | import { ResponseService, ResponseServiceFactory } from '../Library/Response'; 7 | import { LoggerService, LoggerServiceFactory } from '../Library/Logger'; 8 | import { CliService, CliServiceFactory } from '../Library/Cli'; 9 | import { DispatchMiddleware, RequestMiddleware, RouterMiddleware } from '../Library/Middleware'; 10 | 11 | export const services = { 12 | invokables: new Map([ 13 | [ RouterMiddleware, RouterMiddleware ], 14 | [ RequestMiddleware, RequestMiddleware ], 15 | [ DispatchMiddleware, DispatchMiddleware ], 16 | ]), 17 | factories: new Map([ 18 | [ CliService, CliServiceFactory ], 19 | [ ServerService, ServerServiceFactory ], 20 | [ RouterService, RouterServiceFactory ], 21 | [ CommandManager, CommandManagerFactory ], 22 | [ ControllerManager, ControllerManagerFactory ], 23 | [ ResponseService, ResponseServiceFactory ], 24 | [ LoggerService, LoggerServiceFactory ], 25 | ]), 26 | }; 27 | -------------------------------------------------------------------------------- /src/debug.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug'; 2 | 3 | export const createDebugLogger = (namespace: string, project: string = 'stix:'): debug.IDebugger => debug(project + namespace); 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Application, ConfigType } from './Library'; 2 | 3 | export * from './Library'; 4 | export * from './debug'; 5 | 6 | export default (config: ConfigType) => new Application(config); 7 | -------------------------------------------------------------------------------- /stix.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /test/Library/Config/Config.test.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '../../../src/Library'; 2 | 3 | describe('Config', () => { 4 | describe('constructor()', () => { 5 | 6 | }); 7 | 8 | describe('.merge()', () => { 9 | it('should merge given data to currently assigned data', () => { 10 | class Fbb {} 11 | 12 | class Pbb {} 13 | 14 | const whatever = { 15 | difficult: new Map([ 16 | [ 'hello', { a: { b: 'c', d: 'e' } } ], 17 | [ 18 | Config, new Map([ 19 | [ 20 | Fbb, 21 | { 22 | fattenMeUp: [ 'Hamburgers', 'Milkshakes' ], 23 | downgradeMe: 9, 24 | dontTouchMe: 9, 25 | replaceMe: true, 26 | hello: { 27 | you: 'fool', 28 | I: 'love you', 29 | come: 'on my face', 30 | }, 31 | }, 32 | ], 33 | ]), 34 | ], 35 | ]), 36 | animals: { 37 | cat: [ 'fluffer' ], 38 | }, 39 | people: { 40 | raphaela: { 41 | hasMinions: true, 42 | hasCuteness: true, 43 | properties: [ 'soft', 'weird', 'pretty', 'clever', 'emotional' ], 44 | }, 45 | }, 46 | }; 47 | 48 | const config = new Config(whatever); 49 | 50 | config.merge({ 51 | difficult: new Map([ 52 | [ 'hello', { a: { b: 'x', f: 'g' } } ], 53 | [ 54 | Config, new Map([ 55 | [ Pbb, { edgy: 'as fuck' } ], 56 | [ 57 | Fbb, 58 | { 59 | fattenMeUp: [ 'Hamburgers', 'Fries', 'Scatty' ], 60 | downgradeMe: 0, 61 | replaceMe: false, 62 | hello: { 63 | come: 'join', 64 | the: 'joyride', 65 | }, 66 | }, 67 | ], 68 | ]), 69 | ], 70 | ]), 71 | animals: { 72 | cat: false, 73 | }, 74 | people: { 75 | wesley: { 76 | hasMinions: false, 77 | hasCuteness: true, 78 | }, 79 | raphaela: { 80 | hasCuteness: false, 81 | properties: [ 'married' ], 82 | }, 83 | }, 84 | }); 85 | 86 | const fbb = config.of('difficult').get(Config).get(Fbb); 87 | 88 | expect(fbb.hello).toEqual({ you: 'fool', I: 'love you', come: 'join', the: 'joyride' }); 89 | expect(fbb.dontTouchMe).toBe(9); 90 | expect(fbb.downgradeMe).toBe(0); 91 | expect(fbb.replaceMe).toBe(false); 92 | expect(fbb.fattenMeUp).toEqual([ 'Hamburgers', 'Milkshakes', 'Fries', 'Scatty' ]); 93 | expect(config.of('difficult').get(Config).get(Pbb)).toEqual({ edgy: 'as fuck' }); 94 | expect(config.of('difficult').get('hello')).toEqual({ a: { b: 'x', d: 'e', f: 'g' } }); 95 | expect(config.of('animals').cat).toBe(false); 96 | expect(config.of('people').raphaela.hasMinions).toBe(true); 97 | expect(config.of('people').raphaela.hasCuteness).toBe(false); 98 | expect(config.of('people').raphaela.properties).toEqual([ 99 | 'soft', 100 | 'weird', 101 | 'pretty', 102 | 'clever', 103 | 'emotional', 104 | 'married', 105 | ]); 106 | }); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /test/Library/ServiceManager/ServiceManager.test.ts: -------------------------------------------------------------------------------- 1 | import { ServiceManager, ServiceFactoryType, ServiceKeyType, ServiceManagerConfigType } from '../../../src/Library/ServiceManager'; 2 | 3 | describe('ServiceManager', () => { 4 | class UselessService { 5 | public value: string; 6 | 7 | constructor (value: string) { 8 | this.value = value; 9 | } 10 | } 11 | 12 | const serviceManagerFactory = () => new ServiceManager({ 13 | services: new Map([ [ Date, new Date ] ]), 14 | invokables: new Map([ [ 'StupidWayToGetADateInstance', Date ] ]), 15 | factories: new Map, ServiceFactoryType>([ 16 | [ 'UselessService', () => new UselessService('Really String') ], 17 | [ UselessService, () => new UselessService('Really Reference') ], 18 | [ Date, () => 'This should never be called' ], 19 | ]), 20 | aliases: { 21 | uselessAlias: UselessService, 22 | }, 23 | } as ServiceManagerConfigType); 24 | 25 | describe('constructor()', () => { 26 | it ('Should construct properly without any arguments.', () => { 27 | const serviceManager = new ServiceManager(); 28 | 29 | expect(serviceManager).toBeInstanceOf(ServiceManager); 30 | }); 31 | 32 | it ('Should construct and register services when provided with a config.', () => { 33 | const serviceManager = serviceManagerFactory(); 34 | 35 | expect(serviceManager['services'].get(Date)).toBeInstanceOf(Date); 36 | expect(serviceManager['factories'].get('UselessService')()).toBeInstanceOf(UselessService); 37 | expect(serviceManager['aliases'].uselessAlias).toBe(UselessService); 38 | expect(typeof serviceManager['factories'].get('StupidWayToGetADateInstance')).toBe('function'); 39 | expect(serviceManager['factories'].get('StupidWayToGetADateInstance')()).toBeInstanceOf(Date); 40 | }); 41 | 42 | }); 43 | 44 | describe('.get()', () => { 45 | const serviceManager = serviceManagerFactory(); 46 | 47 | it('resolves to a service based on an alias key', () => { 48 | expect(serviceManager.get('uselessAlias')).toStrictEqual(serviceManager.get(UselessService)); 49 | }); 50 | 51 | it('should not call the factory if the service already exists', () => { 52 | expect(serviceManager.get(Date)).toBeInstanceOf(Date); 53 | }); 54 | 55 | it('should register the service after calling the factory', () => { 56 | expect(serviceManager['services'].has('UselessService')).toBe(false); 57 | expect(serviceManager.get('UselessService')).toBeInstanceOf(UselessService); 58 | expect(serviceManager['services'].has('UselessService')).toBe(true); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/**/*" 4 | ], 5 | "compilerOptions": { 6 | "module": "commonjs", 7 | "noEmit": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "target": "ES2017", 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true, 13 | "esModuleInterop": true, 14 | "noImplicitAny": true, 15 | "moduleResolution": "node", 16 | "declarationDir": "./dist", 17 | "outDir": "./dist" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-eslint-rules", "tslint-config-prettier"], 3 | "rules": { 4 | "quotes": ["error", "single"], 5 | "semi": ["error", "always"], 6 | "comma-dangle": ["error", { 7 | "arrays": "always-multiline", 8 | "objects": "always-multiline", 9 | "imports": "never", 10 | "exports": "never", 11 | "functions": "ignore" 12 | }], 13 | "varspacing/var-spacing": ["error"], 14 | "key-spacing": ["error", {"align": "colon"}], 15 | "padded-blocks": ["error", "never"], 16 | "padding-line-between-statements": [ 17 | "error", 18 | {"blankLine": "always", "prev": ["const", "let", "var"], "next": "*"}, 19 | {"blankLine": "any", "prev": ["const", "let", "var"], "next": ["const", "let", "var"]}, 20 | {"blankLine": "always", "prev": "*", "next": "return"} 21 | ], 22 | "indent": [true, "2"], 23 | "function-paren-newline": ["error", "multiline"], 24 | "space-before-function-paren": [true], 25 | "generator-star-spacing": ["error", "after"], 26 | "object-curly-spacing": [true, "always"], 27 | "array-bracket-spacing": [ 28 | true, 29 | "always" 30 | ], 31 | "quotemark": [true, "single", "jsx-double", "avoid-escape"], 32 | "semicolon": [true, "always"], 33 | "trailing-comma": [ 34 | true, 35 | { 36 | "multiline": "always", 37 | "singleline": "never" 38 | } 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /typings/yargs-parser/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "yargs-parser"; 2 | --------------------------------------------------------------------------------