├── .gitignore ├── .npmignore ├── .publishrc ├── .travis.yml ├── .vscode ├── launch.json └── settings.json ├── ISSUE_TEMPLATE.md ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── gulpfile.js ├── package.json ├── src ├── constants.ts ├── decorators.ts ├── index.ts ├── interfaces.ts └── server.ts ├── test ├── bugs.test.ts ├── decorators.test.ts ├── framework.test.ts └── server.test.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | build 36 | node_modules 37 | bower_components 38 | docs 39 | bundled 40 | 41 | typings 42 | .typingsrc 43 | dist 44 | lib 45 | es 46 | dts 47 | 48 | type_definitions/inversify/*.js 49 | 50 | src/*.js 51 | src/**/*.js 52 | 53 | src/*.js.map 54 | src/**/*.js.map 55 | 56 | src/*.d.ts 57 | src/decorator/*.d.ts 58 | src/factory/*.d.ts 59 | src/syntax/*.d.ts 60 | 61 | test/*.js 62 | test/**/*.js 63 | test/**/*.js.map 64 | 65 | type_definitions/**/*.js 66 | type_definitions/*.js 67 | package-lock.json 68 | 69 | .DS_Store -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test 3 | typings 4 | bundled 5 | build 6 | coverage 7 | docs 8 | wiki 9 | gulpfile.js 10 | bower.json 11 | karma.conf.js 12 | tsconfig.json 13 | typings.json 14 | CONTRIBUTING.md 15 | ISSUE_TEMPLATE.md 16 | PULL_REQUEST_TEMPLATE.md 17 | tslint.json 18 | wallaby.js 19 | .travis.yml 20 | .gitignore 21 | .vscode 22 | type_definitions 23 | package-lock.json -------------------------------------------------------------------------------- /.publishrc: -------------------------------------------------------------------------------- 1 | { 2 | "validations": { 3 | "vulnerableDependencies": false, 4 | "uncommittedChanges": true, 5 | "untrackedFiles": true, 6 | "sensitiveData": true, 7 | "branch": "master", 8 | "gitTag": true 9 | }, 10 | "confirm": true, 11 | "publishTag": "latest", 12 | "prePublishScript": "gulp" 13 | } 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | - 16 5 | - 14 6 | - 12 7 | - 10 8 | - 8 9 | before_install: 10 | - npm install -g codeclimate-test-reporter 11 | after_success: 12 | - codeclimate-test-reporter < coverage/lcov.info 13 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Mocha Tests", 11 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 12 | "args": [ 13 | "--require", 14 | "reflect-metadata", 15 | "-u", 16 | "tdd", 17 | "--timeout", 18 | "999999", 19 | "--colors", 20 | "${workspaceRoot}/test/**/*.test.js" 21 | ], 22 | "sourceMaps": true, 23 | "outFiles": [ 24 | "${workspaceRoot}/test", 25 | "${workspaceRoot}/src" 26 | ], 27 | "internalConsoleOptions": "openOnSessionStart" 28 | }, 29 | { 30 | "type": "node", 31 | "request": "launch", 32 | "name": "Launch Program", 33 | "program": "${workspaceRoot}/lib/inversify.js", 34 | "outFiles": [ 35 | "${workspaceRoot}/out/**/*.js" 36 | ] 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.git": true, 4 | "**/.DS_Store": true, 5 | "src/**/*.js": true, 6 | "test/**/*.js": true, 7 | "**/*.js.map": true, 8 | "**/es": true, 9 | "**/lib": true, 10 | "**/amd": true, 11 | "**/dts": true, 12 | "**/temp": true, 13 | "**/coverage": true, 14 | "**/dist": true, 15 | "**/docs": true, 16 | "type_definitions/**/*.js": true 17 | }, 18 | "typescript.tsdk": "node_modules\\typescript\\lib" 19 | } 20 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | PLEASE DO NOT REPORT THE ISSUE HERE! 2 | 3 | PLEASE USE THE INVERSIFYJS REPO INSTEAD YOU CAN FIND THE REPO AT: 4 | 5 | https://github.com/inversify/InversifyJS/issues 6 | 7 | YOU CAN ALSO FIND US ON GITTER AT: 8 | 9 | https://gitter.im/inversify/InversifyJS 10 | 11 | THANKS! 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 inversify 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 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Related Issue 7 | 8 | 9 | 10 | 11 | 12 | ## Motivation and Context 13 | 14 | 15 | ## How Has This Been Tested? 16 | 17 | 18 | 19 | 20 | ## Types of changes 21 | - [ ] Bug fix (non-breaking change which fixes an issue) 22 | - [ ] New feature (non-breaking change which adds functionality) 23 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 24 | 25 | ## Checklist: 26 | 27 | 28 | - [ ] My code follows the code style of this project. 29 | - [ ] My change requires a change to the documentation. 30 | - [ ] I have updated the documentation accordingly. 31 | - [ ] I have read the **CONTRIBUTING** document. 32 | - [ ] I have added tests to cover my changes. 33 | - [ ] All new and existing tests passed. 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # inversify-restify-utils 2 | 3 | [![Join the chat at https://gitter.im/inversify/InversifyJS](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/inversify/InversifyJS?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | [![Build Status](https://secure.travis-ci.org/inversify/inversify-restify-utils.svg?branch=master)](https://travis-ci.org/inversify/inversify-restify-utils) 5 | [![Test Coverage](https://codeclimate.com/github/inversify/inversify-restify-utils/badges/coverage.svg)](https://codeclimate.com/github/inversify/inversify-restify-utils/coverage) 6 | [![npm version](https://badge.fury.io/js/inversify-restify-utils.svg)](http://badge.fury.io/js/inversify-restify-utils) 7 | [![Dependencies](https://david-dm.org/inversify/inversify-restify-utils.svg)](https://david-dm.org/inversify/inversify-restify-utils#info=dependencies) 8 | [![img](https://david-dm.org/inversify/inversify-restify-utils/dev-status.svg)](https://david-dm.org/inversify/inversify-restify-utils/#info=devDependencies) 9 | [![img](https://david-dm.org/inversify/inversify-restify-utils/peer-status.svg)](https://david-dm.org/inversify/inversify-restify-utils/#info=peerDependenciess) 10 | [![Known Vulnerabilities](https://snyk.io/test/github/inversify/inversify-restify-utils/badge.svg)](https://snyk.io/test/github/inversify/inversify-restify-utils) 11 | 12 | [![NPM](https://nodei.co/npm/inversify-restify-utils.png?downloads=true&downloadRank=true)](https://nodei.co/npm/inversify-restify-utils/) 13 | [![NPM](https://nodei.co/npm-dl/inversify-restify-utils.png?months=9&height=3)](https://nodei.co/npm/inversify-restify-utils/) 14 | 15 | Some utilities for the development of restify application with Inversify. 16 | 17 | ## Installation 18 | You can install `inversify-restify-utils` using npm: 19 | 20 | ``` 21 | $ npm install inversify inversify-restify-utils reflect-metadata restify --save 22 | ``` 23 | 24 | The `inversify-restify-utils` type definitions are included in the npm module and require TypeScript 2.0. 25 | Please refer to the [InversifyJS documentation](https://github.com/inversify/InversifyJS#installation) to learn more about the installation process. 26 | 27 | ## The Basics 28 | 29 | ### Step 1: Decorate your controllers 30 | To use a class as a "controller" for your restify app, simply add the `@Controller` decorator to the class. Similarly, decorate methods of the class to serve as request handlers. 31 | The following example will declare a controller that responds to `GET /foo'. 32 | 33 | ```ts 34 | import { Request } from 'restify'; 35 | import { Controller, Get, interfaces } from 'inversify-restify-utils'; 36 | import { injectable, inject } from 'inversify'; 37 | 38 | @Controller('/foo') 39 | @injectable() 40 | export class FooController implements interfaces.Controller { 41 | 42 | constructor( @inject('FooService') private fooService: FooService ) {} 43 | 44 | @Get('/') 45 | private index(req: Request): string { 46 | return this.fooService.get(req.query.id); 47 | } 48 | } 49 | ``` 50 | 51 | ### Step 2: Configure container and server 52 | Configure the inversify container in your composition root as usual. 53 | 54 | Then, pass the container to the InversifyRestifyServer constructor. This will allow it to register all controllers and their dependencies from your container and attach them to the restify app. 55 | Then just call server.build() to prepare your app. 56 | 57 | In order for the InversifyRestifyServer to find your controllers, you must bind them to the `TYPE.Controller` service identifier and tag the binding with the controller's name. 58 | The `Controller` interface exported by inversify-restify-utils is empty and solely for convenience, so feel free to implement your own if you want. 59 | 60 | ```ts 61 | import { Container } from 'inversify'; 62 | import { interfaces, InversifyRestifyServer, TYPE } from 'inversify-restify-utils'; 63 | 64 | // set up container 65 | let container = new Container(); 66 | 67 | // note that you *must* bind your controllers to Controller 68 | container.bind(TYPE.Controller).to(FooController).whenTargetNamed('FooController'); 69 | container.bind('FooService').to(FooService); 70 | 71 | // create server 72 | let server = new InversifyRestifyServer(container); 73 | 74 | let app = server.build(); 75 | app.listen(3000); 76 | ``` 77 | 78 | Restify ServerOptions can be provided as a second parameter to the InversifyRestifyServer constructor: 79 | 80 | ```let server = new InversifyRestifyServer(container, { name: "my-server" });``` 81 | 82 | Restify ServerOptions can be extended with `defaultRoot` where one can define a default path that will be prepended to all your controllers: 83 | 84 | ```let server = new InversifyRestifyServer(container, { name: "my-server", defaultRoot: "/v1" });``` 85 | 86 | ## InversifyRestifyServer 87 | A wrapper for a restify Application. 88 | 89 | ### `.setConfig(configFn)` 90 | Optional - exposes the restify application object for convenient loading of server-level middleware. 91 | 92 | ```ts 93 | import * as morgan from 'morgan'; 94 | // ... 95 | let server = new InversifyRestifyServer(container); 96 | server.setConfig((app) => { 97 | const logger = morgan('combined') 98 | app.use(logger); 99 | }); 100 | ``` 101 | 102 | ### `.build()` 103 | Attaches all registered controllers and middleware to the restify application. Returns the application instance. 104 | 105 | ```ts 106 | // ... 107 | let server = new InversifyRestifyServer(container); 108 | server 109 | .setConfig(configFn) 110 | .build() 111 | .listen(3000, 'localhost', callback); 112 | ``` 113 | 114 | ## Decorators 115 | 116 | ### `@Controller(path, [middleware, ...])` 117 | 118 | Registers the decorated class as a controller with a root path, and optionally registers any global middleware for this controller. 119 | 120 | ### `@Method(method, path, [middleware, ...])` 121 | 122 | Registers the decorated controller method as a request handler for a particular path and method, where the method name is a valid restify routing method. 123 | 124 | ### `@SHORTCUT(path, [middleware, ...])` 125 | 126 | Shortcut decorators which are simply wrappers for `@Method`. Right now these include `@Get`, `@Post`, `@Put`, `@Patch`, `@Head`, `@Delete`, and `@Options`. For anything more obscure, use `@Method` (Or make a PR :smile:). 127 | 128 | ## Middleware 129 | Middleware can be either an instance of `restify.RequestHandler` or an InversifyJS service idenifier. 130 | 131 | The simplest way to use middleware is to define a `restify.RequestHandler` instance and pass that handler as decorator parameter. 132 | 133 | ```ts 134 | // ... 135 | const loggingHandler = (req: restify.Request, res: restify.Response, next: restify.Next) => { 136 | console.log(req); 137 | next(); 138 | }; 139 | 140 | @Controller('/foo', loggingHandler) 141 | @injectable() 142 | export class FooController implements interfaces.Controller { 143 | 144 | constructor( @inject('FooService') private fooService: FooService ) {} 145 | 146 | @Get('/', loggingHandler) 147 | private index(req: restify.Request): string { 148 | return this.fooService.get(req.query.id); 149 | } 150 | } 151 | ``` 152 | 153 | But if you wish to take full advantage of InversifyJS you can bind the same handler to your IOC container and pass the handler's service identifier to decorators. 154 | 155 | ```ts 156 | // ... 157 | import { TYPES } from 'types'; 158 | // ... 159 | const loggingHandler = (req: restify.Request, res: restify.Response, next: restify.Next) => { 160 | console.log(req); 161 | next(); 162 | }; 163 | container.bind(TYPES.LoggingMiddleware).toConstantValue(loggingHandler); 164 | // ... 165 | @Controller('/foo', TYPES.LoggingMiddleware) 166 | @injectable() 167 | export class FooController implements interfaces.Controller { 168 | 169 | constructor( @inject('FooService') private fooService: FooService ) {} 170 | 171 | @Get('/', TYPES.LoggingMiddleware) 172 | private index(req: restify.Request): string { 173 | return this.fooService.get(req.query.id); 174 | } 175 | } 176 | ``` 177 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | //****************************************************************************** 4 | //* DEPENDENCIES 5 | //****************************************************************************** 6 | const gulp = require("gulp"), 7 | tslint = require("gulp-tslint"), 8 | tsc = require("gulp-typescript"), 9 | mocha = require("gulp-mocha"), 10 | istanbul = require("gulp-istanbul"), 11 | sourcemaps = require("gulp-sourcemaps"), 12 | del = require('del'); 13 | 14 | //****************************************************************************** 15 | //* CLEAN 16 | //****************************************************************************** 17 | gulp.task("clean", function () { 18 | return del([ 19 | "src/**/*.js", 20 | "test/**/*.test.js", 21 | "src/*.js", 22 | "test/*.test.js", 23 | "lib", 24 | "es", 25 | "amd" 26 | ]); 27 | }); 28 | 29 | //****************************************************************************** 30 | //* LINT 31 | //****************************************************************************** 32 | gulp.task("lint", function () { 33 | 34 | const config = { 35 | fornatter: "verbose", 36 | emitError: (process.env.CI) ? true : false 37 | }; 38 | 39 | return gulp.src([ 40 | "src/**/**.ts", 41 | "test/**/**.test.ts" 42 | ]) 43 | .pipe(tslint(config)) 44 | .pipe(tslint.report()); 45 | }); 46 | 47 | //****************************************************************************** 48 | //* SOURCE 49 | //****************************************************************************** 50 | const tsLibProject = tsc.createProject("tsconfig.json", { 51 | module: "commonjs" 52 | }); 53 | 54 | gulp.task("build-lib", function () { 55 | return gulp.src([ 56 | "src/**/*.ts" 57 | ]) 58 | .pipe(tsLibProject()) 59 | .on("error", function (err) { 60 | process.exit(1); 61 | }) 62 | .js.pipe(gulp.dest("lib/")); 63 | }); 64 | 65 | const tsEsProject = tsc.createProject("tsconfig.json", { 66 | module: "es2015" 67 | }); 68 | 69 | gulp.task("build-es", function () { 70 | return gulp.src([ 71 | "src/**/*.ts" 72 | ]) 73 | .pipe(tsEsProject()) 74 | .on("error", function (err) { 75 | process.exit(1); 76 | }) 77 | .js.pipe(gulp.dest("es/")); 78 | }); 79 | 80 | const tsDtsProject = tsc.createProject("tsconfig.json", { 81 | declaration: true, 82 | noResolve: false 83 | }); 84 | 85 | gulp.task("build-dts", function () { 86 | return gulp.src([ 87 | "src/**/*.ts" 88 | ]) 89 | .pipe(tsDtsProject()) 90 | .on("error", function (err) { 91 | process.exit(1); 92 | }) 93 | .dts.pipe(gulp.dest("dts")); 94 | 95 | }); 96 | 97 | //****************************************************************************** 98 | //* TESTS 99 | //****************************************************************************** 100 | const tstProject = tsc.createProject("tsconfig.json"); 101 | 102 | gulp.task("build-src", function () { 103 | return gulp.src([ 104 | "src/**/*.ts" 105 | ]) 106 | .pipe(sourcemaps.init()) 107 | .pipe(tstProject()) 108 | .on("error", function (err) { 109 | process.exit(1); 110 | }) 111 | .js.pipe(sourcemaps.write(".", { 112 | sourceRoot: function (file) { 113 | return file.cwd + '/src'; 114 | } 115 | })) 116 | .pipe(gulp.dest("src/")); 117 | }); 118 | 119 | const tsTestProject = tsc.createProject("tsconfig.json"); 120 | gulp.task("build-test", function () { 121 | return gulp.src([ 122 | "test/**/*.ts" 123 | ]) 124 | .pipe(sourcemaps.init()) 125 | .pipe(tsTestProject()) 126 | .on("error", function (err) { 127 | process.exit(1); 128 | }) 129 | .js.pipe(sourcemaps.write(".", { 130 | sourceRoot: function (file) { 131 | return file.cwd + '/test'; 132 | } 133 | })) 134 | .pipe(gulp.dest("test/"));; 135 | }); 136 | 137 | gulp.task("mocha", function () { 138 | return gulp.src([ 139 | "node_modules/reflect-metadata/Reflect.js", 140 | "test/**/*.test.js" 141 | ]) 142 | .pipe(mocha({ 143 | ui: "bdd" 144 | })) 145 | .pipe(istanbul.writeReports()); 146 | }); 147 | 148 | gulp.task("istanbul:hook", function () { 149 | return gulp.src(["src/**/*.js"]) 150 | // Covering files 151 | .pipe(istanbul()) 152 | // Force `require` to return covered files 153 | .pipe(istanbul.hookRequire()); 154 | }); 155 | 156 | gulp.task("test", gulp.series( 157 | "istanbul:hook", 158 | "mocha", 159 | )); 160 | 161 | gulp.task("build", 162 | gulp.series( 163 | "lint", 164 | gulp.parallel( 165 | "build-src" 166 | , "build-test" 167 | , "build-es" 168 | , "build-lib" 169 | , "build-dts" 170 | ), 171 | 172 | ) 173 | ); 174 | 175 | //****************************************************************************** 176 | //* DEFAULT 177 | //****************************************************************************** 178 | gulp.task("default", gulp.series( 179 | "build", 180 | "test", 181 | )); 182 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inversify-restify-utils", 3 | "version": "3.6.0", 4 | "description": "Some utilities for the development of restify applications with Inversify", 5 | "main": "lib/index.js", 6 | "jsnext:main": "es/index.js", 7 | "typings": "./dts/index.d.ts", 8 | "scripts": { 9 | "test": "gulp", 10 | "publish-please": "publish-please", 11 | "prepublish": "publish-please guard", 12 | "update": "updates --update --minor && npm install", 13 | "postupdate": "git diff-files --quiet package-lock.json || npm test" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/inversify/inversify-restify-utils.git" 18 | }, 19 | "keywords": [ 20 | "InversifyJS", 21 | "restify", 22 | "dependency", 23 | "injection" 24 | ], 25 | "author": "Cody Simms", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/inversify/inversify-restify-utils/issues" 29 | }, 30 | "homepage": "https://github.com/inversify/inversify-restify-utils#readme", 31 | "devDependencies": { 32 | "@types/chai": "^4.3.20", 33 | "@types/mocha": "^10.0.9", 34 | "@types/node": "^18.19.59", 35 | "@types/restify": "^8.5.12", 36 | "@types/sinon": "^10.0.20", 37 | "@types/supertest": "^2.0.16", 38 | "chai": "^4.5.0", 39 | "del": "^6.1.1", 40 | "gulp": "^4.0.2", 41 | "gulp-istanbul": "^1.1.3", 42 | "gulp-mocha": "^8.0.0", 43 | "gulp-sourcemaps": "^3.0.0", 44 | "gulp-tslint": "^8.1.4", 45 | "gulp-typescript": "^5.0.1", 46 | "inversify": "^5.1.1", 47 | "mocha": "^10.7.3", 48 | "publish-please": "^5.5.2", 49 | "reflect-metadata": "^0.2.2", 50 | "sinon": "^15.2.0", 51 | "source-map-support": "^0.5.21", 52 | "supertest": "^6.3.4", 53 | "tslint": "^5.20.1", 54 | "typescript": "^5.6.3", 55 | "updates": "^13.4.0" 56 | }, 57 | "peerDependencies": { 58 | "restify": ">=8.6.1" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | const TYPE = { 2 | Controller: Symbol.for("Controller") 3 | }; 4 | 5 | const METADATA_KEY = { 6 | controller: "_controller", 7 | controllerMethod: "_controller-method" 8 | }; 9 | 10 | export { TYPE, METADATA_KEY }; 11 | -------------------------------------------------------------------------------- /src/decorators.ts: -------------------------------------------------------------------------------- 1 | import { interfaces } from "./interfaces"; 2 | import { METADATA_KEY } from "./constants"; 3 | 4 | export function Controller(path: string, ...middleware: interfaces.Middleware[]) { 5 | return function (target: any) { 6 | let metadata: interfaces.ControllerMetadata = {path, middleware, target}; 7 | Reflect.defineMetadata(METADATA_KEY.controller, metadata, target); 8 | }; 9 | } 10 | 11 | export function Get(options: interfaces.RouteOptions, ...middleware: interfaces.Middleware[]): interfaces.HandlerDecorator { 12 | return Method("get", options, ...middleware); 13 | } 14 | 15 | export function Post(options: interfaces.RouteOptions, ...middleware: interfaces.Middleware[]): interfaces.HandlerDecorator { 16 | return Method("post", options, ...middleware); 17 | } 18 | 19 | export function Put(options: interfaces.RouteOptions, ...middleware: interfaces.Middleware[]): interfaces.HandlerDecorator { 20 | return Method("put", options, ...middleware); 21 | } 22 | 23 | export function Patch(options: interfaces.RouteOptions, ...middleware: interfaces.Middleware[]): interfaces.HandlerDecorator { 24 | return Method("patch", options, ...middleware); 25 | } 26 | 27 | export function Head(options: interfaces.RouteOptions, ...middleware: interfaces.Middleware[]): interfaces.HandlerDecorator { 28 | return Method("head", options, ...middleware); 29 | } 30 | 31 | export function Delete(options: interfaces.RouteOptions, ...middleware: interfaces.Middleware[]): interfaces.HandlerDecorator { 32 | return Method("del", options, ...middleware); 33 | } 34 | 35 | export function Options(options: interfaces.RouteOptions, ...middleware: interfaces.Middleware[]): interfaces.HandlerDecorator { 36 | return Method("opts", options, ...middleware); 37 | } 38 | 39 | export function Method( 40 | method: string, 41 | options: interfaces.RouteOptions, 42 | ...middleware: interfaces.Middleware[] 43 | ): interfaces.HandlerDecorator { 44 | return function (target: any, key: string) { 45 | let metadata: interfaces.ControllerMethodMetadata = {options, middleware, method, target, key}; 46 | let metadataList: interfaces.ControllerMethodMetadata[] = []; 47 | 48 | if (!Reflect.hasOwnMetadata(METADATA_KEY.controllerMethod, target.constructor)) { 49 | Reflect.defineMetadata(METADATA_KEY.controllerMethod, metadataList, target.constructor); 50 | } else { 51 | metadataList = Reflect.getOwnMetadata(METADATA_KEY.controllerMethod, target.constructor); 52 | } 53 | 54 | metadataList.push(metadata); 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { InversifyRestifyServer } from "./server"; 2 | import { Controller, Method, Get, Put, Post, Patch, Head, Options, Delete } from "./decorators"; 3 | import { TYPE } from "./constants"; 4 | import { interfaces } from "./interfaces"; 5 | 6 | export { 7 | interfaces, 8 | InversifyRestifyServer, 9 | Controller, 10 | Method, 11 | Get, 12 | Put, 13 | Post, 14 | Patch, 15 | Head, 16 | Options, 17 | Delete, 18 | TYPE 19 | }; 20 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { interfaces as inversifyInterfaces } from "inversify"; 2 | import { RequestHandler, Server, ServerOptions as RestifyServerOptions } from "restify"; 3 | 4 | namespace interfaces { 5 | 6 | export type Middleware = (inversifyInterfaces.ServiceIdentifier | RequestHandler); 7 | 8 | export interface ControllerMetadata { 9 | path: string; 10 | middleware: Middleware[]; 11 | target: any; 12 | } 13 | 14 | export type StrOrRegex = string | RegExp; 15 | export type RouteOptions = StrOrRegex | { path: StrOrRegex } | { options: Object, path: StrOrRegex } & Object; 16 | 17 | export interface ControllerMethodMetadata { 18 | options: RouteOptions; 19 | middleware: Middleware[]; 20 | target: any; 21 | method: string; 22 | key: string; 23 | } 24 | 25 | export interface Controller {} 26 | 27 | export interface HandlerDecorator { 28 | (target: any, key: string, value: any): void; 29 | } 30 | 31 | export interface ConfigFunction { 32 | (app: Server): void; 33 | } 34 | 35 | export interface ServerOptions extends RestifyServerOptions { 36 | defaultRoot?: string; 37 | } 38 | } 39 | 40 | export { interfaces }; 41 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import { Container } from "inversify"; 2 | import { createServer, Next, Request, RequestHandler, Response, Server, ServerOptions } from "restify"; 3 | import { METADATA_KEY, TYPE } from "./constants"; 4 | import { interfaces } from "./interfaces"; 5 | 6 | /** 7 | * Wrapper for the restify server. 8 | */ 9 | export class InversifyRestifyServer { 10 | private container: Container; 11 | private app: Server; 12 | private configFn: interfaces.ConfigFunction; 13 | private defaultRoot: string | null = null; 14 | 15 | /** 16 | * Wrapper for the restify server. 17 | * 18 | * @param container Container loaded with all controllers and their dependencies. 19 | */ 20 | constructor(container: Container, opts?: (ServerOptions & interfaces.ServerOptions)) { 21 | opts = { 22 | ignoreTrailingSlash : true, 23 | ...opts 24 | }; 25 | 26 | this.container = container; 27 | this.app = createServer(opts as ServerOptions); 28 | if ( 29 | opts && 30 | opts.hasOwnProperty("defaultRoot") && 31 | typeof (opts as interfaces.ServerOptions).defaultRoot === "string" 32 | ) { 33 | this.defaultRoot = (opts as interfaces.ServerOptions).defaultRoot as string; 34 | } 35 | } 36 | 37 | /** 38 | * Sets the configuration function to be applied to the application. 39 | * Note that the config function is not actually executed until a call to InversifyRestifyServer.build(). 40 | * 41 | * This method is chainable. 42 | * 43 | * @param fn Function in which app-level middleware can be registered. 44 | */ 45 | public setConfig(fn: interfaces.ConfigFunction): InversifyRestifyServer { 46 | this.configFn = fn; 47 | return this; 48 | } 49 | 50 | /** 51 | * Applies all routes and configuration to the server, returning the restify application. 52 | */ 53 | public build(): Server { 54 | // register server-level middleware before anything else 55 | if (this.configFn) { 56 | this.configFn.apply(undefined, [this.app]); 57 | } 58 | 59 | this.registerControllers(); 60 | 61 | return this.app; 62 | } 63 | 64 | private registerControllers() { 65 | 66 | let controllers: interfaces.Controller[] = this.container.getAll(TYPE.Controller); 67 | 68 | controllers.forEach((controller: interfaces.Controller) => { 69 | 70 | let controllerMetadata: interfaces.ControllerMetadata = Reflect.getOwnMetadata( 71 | METADATA_KEY.controller, 72 | controller.constructor 73 | ); 74 | 75 | if (this.defaultRoot !== null && typeof controllerMetadata.path === "string") { 76 | controllerMetadata.path = this.defaultRoot + controllerMetadata.path; 77 | } else if (this.defaultRoot !== null) { 78 | controllerMetadata.path = this.defaultRoot; 79 | } 80 | 81 | let methodMetadata: interfaces.ControllerMethodMetadata[] = Reflect.getOwnMetadata( 82 | METADATA_KEY.controllerMethod, 83 | controller.constructor 84 | ); 85 | 86 | if (controllerMetadata && methodMetadata) { 87 | let controllerMiddleware = this.resolveMiddleware(...controllerMetadata.middleware); 88 | methodMetadata.forEach((metadata: interfaces.ControllerMethodMetadata) => { 89 | let handler: RequestHandler = this.handlerFactory(controllerMetadata.target.name, metadata.key); 90 | let routeOptions: any = typeof metadata.options === "string" ? { path: metadata.options } : metadata.options; 91 | let routeMiddleware = this.resolveMiddleware(...metadata.middleware); 92 | if (typeof routeOptions.path === "string" && typeof controllerMetadata.path === "string" 93 | && controllerMetadata.path !== "/") { 94 | routeOptions.path = routeOptions.path === "/" ? 95 | controllerMetadata.path : controllerMetadata.path + routeOptions.path; 96 | } else if (routeOptions.path instanceof RegExp && controllerMetadata.path !== "/") { 97 | routeOptions.path = new RegExp(controllerMetadata.path + routeOptions.path.source); 98 | } 99 | (this.app as any)[metadata.method](routeOptions, [...controllerMiddleware, ...routeMiddleware], handler); 100 | }); 101 | } 102 | }); 103 | } 104 | 105 | private resolveMiddleware(...middleware: interfaces.Middleware[]): RequestHandler[] { 106 | return middleware.map(middlewareItem => { 107 | try { 108 | return this.container.get(middlewareItem); 109 | } catch (_) { 110 | return middlewareItem as RequestHandler; 111 | } 112 | }); 113 | } 114 | 115 | private handlerFactory(controllerName: any, key: string): RequestHandler { 116 | return (req: Request, res: Response, next: Next) => { 117 | 118 | let result: any = (this.container.getNamed(TYPE.Controller, controllerName) as any)[key](req, res, next); 119 | 120 | // try to resolve promise 121 | if (result && result instanceof Promise) { 122 | 123 | result.then((value: any) => { 124 | if (value && !res.headersSent) { 125 | res.send(value); 126 | next(); 127 | } 128 | }) 129 | .catch((error: any) => { 130 | next(new Error(error)); 131 | }); 132 | 133 | } else if (result && !res.headersSent) { 134 | res.send(result); 135 | next(); 136 | } 137 | 138 | }; 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /test/bugs.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Container, injectable } from "inversify"; 3 | import { spy } from "sinon"; 4 | import request from "supertest"; 5 | import { TYPE } from "../src/constants"; 6 | import { Controller, Get } from "../src/decorators"; 7 | import { interfaces } from "../src/interfaces"; 8 | import { InversifyRestifyServer } from "../src/server"; 9 | 10 | describe("Unit Test: Bugs", () => { 11 | let container = new Container(); 12 | let server: InversifyRestifyServer; 13 | 14 | it("should fire the 'after' event when the controller function returns a Promise", (done) => { 15 | @injectable() 16 | @Controller("/") 17 | class TestController { 18 | @Get("/promise") public getTest() { 19 | return new Promise(((resolve) => { 20 | setTimeout(resolve, 100, "GET"); 21 | })); 22 | } 23 | } 24 | 25 | let spyA = spy((req: any, res: any) => null); 26 | 27 | container.bind(TYPE.Controller).to(TestController).whenTargetNamed("TestController"); 28 | 29 | server = new InversifyRestifyServer(container); 30 | server.setConfig((app) => { 31 | app.on("after", spyA); 32 | }); 33 | 34 | request(server.build()) 35 | .get("/noPromise") 36 | .set("Accept", "text/plain") 37 | .expect(200, "GET", () => { 38 | expect(spyA.calledOnce).to.eq(true); 39 | done(); 40 | }); 41 | 42 | }); 43 | 44 | it("should fire the 'after' event when the controller function returns", (done) => { 45 | @injectable() 46 | @Controller("/") 47 | class TestController { 48 | @Get("/noPromise") public getNoPromise() { 49 | return "GET"; 50 | } 51 | } 52 | 53 | let spyA = spy((req: any, res: any) => null); 54 | 55 | container.bind(TYPE.Controller).to(TestController).whenTargetNamed("TestController"); 56 | 57 | server = new InversifyRestifyServer(container); 58 | server.setConfig((app) => { 59 | app.on("after", spyA); 60 | }); 61 | 62 | request(server.build()) 63 | .get("/") 64 | .set("Accept", "text/plain") 65 | .expect(200, "GET", () => { 66 | expect(spyA.calledOnce).to.eq(true); 67 | done(); 68 | }); 69 | 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/decorators.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Controller, Method } from "../src/decorators"; 3 | import { interfaces } from "../src/interfaces"; 4 | 5 | describe("Unit Test: Controller Decorators", () => { 6 | 7 | it("should add controller metadata to a class when decorated with @Controller", (done) => { 8 | let middleware = [function() { return; }, "foo", Symbol.for("bar")]; 9 | let path = "foo"; 10 | 11 | @Controller(path, ...middleware) 12 | class TestController {} 13 | 14 | let controllerMetadata: interfaces.ControllerMetadata = Reflect.getMetadata("_controller", TestController); 15 | 16 | expect(controllerMetadata.middleware).eql(middleware); 17 | expect(controllerMetadata.path).eql(path); 18 | expect(controllerMetadata.target).eql(TestController); 19 | done(); 20 | }); 21 | 22 | 23 | it("should add method metadata to a class when decorated with @Method", (done) => { 24 | let middleware = [function() { return; }, "bar", Symbol.for("baz")]; 25 | let path = "foo"; 26 | let method = "get"; 27 | 28 | class TestController { 29 | @Method(method, path, ...middleware) 30 | public test() { return; } 31 | 32 | @Method("foo", "bar") 33 | public test2() { return; } 34 | 35 | @Method("bar", "foo") 36 | public test3() { return; } 37 | } 38 | 39 | let methodMetadata: interfaces.ControllerMethodMetadata[] = Reflect.getMetadata("_controller-method", TestController); 40 | 41 | expect(methodMetadata.length).eql(3); 42 | 43 | let metadata: interfaces.ControllerMethodMetadata = methodMetadata[0]; 44 | 45 | expect(metadata.middleware).eql(middleware); 46 | expect(metadata.options).eql(path); 47 | expect(metadata.target.constructor).eql(TestController); 48 | expect(metadata.key).eql("test"); 49 | expect(metadata.method).eql(method); 50 | done(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/framework.test.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | 3 | import { expect } from "chai"; 4 | import { Container, injectable } from "inversify"; 5 | import { Next, Request, RequestHandler, Response } from "restify"; 6 | import { spy } from "sinon"; 7 | import request from "supertest"; 8 | import { TYPE } from "../src/constants"; 9 | import { Controller, Delete, Get, Head, Method, Patch, Post, Put } from "../src/decorators"; 10 | import { interfaces } from "../src/interfaces"; 11 | import { InversifyRestifyServer } from "../src/server"; 12 | 13 | 14 | describe("Integration Tests:", () => { 15 | let server: InversifyRestifyServer; 16 | let container: Container; 17 | 18 | beforeEach((done) => { 19 | // refresh container 20 | container = new Container(); 21 | done(); 22 | }); 23 | 24 | describe("Routing & Request Handling:", () => { 25 | 26 | it("should work for async controller methods", (done) => { 27 | @injectable() 28 | @Controller("/") 29 | class TestController { 30 | @Get("/") public getTest() { 31 | return new Promise(((resolve) => { 32 | setTimeout(resolve, 100, "GET"); 33 | })); 34 | } 35 | } 36 | container.bind(TYPE.Controller).to(TestController).whenTargetNamed("TestController"); 37 | 38 | server = new InversifyRestifyServer(container); 39 | request(server.build()) 40 | .get("/") 41 | .set("Accept", "text/plain") 42 | .expect(200, "GET", done); 43 | }); 44 | 45 | it("should work for async controller methods that fails", (done) => { 46 | @injectable() 47 | @Controller("/") 48 | class TestController { 49 | @Get("/") public getTest() { 50 | return new Promise(((resolve, reject) => { 51 | setTimeout(reject, 100, "GET"); 52 | })); 53 | } 54 | } 55 | container.bind(TYPE.Controller).to(TestController).whenTargetNamed("TestController"); 56 | 57 | server = new InversifyRestifyServer(container); 58 | request(server.build()) 59 | .get("/") 60 | .expect(500, done); 61 | }); 62 | 63 | 64 | it("should work for each shortcut decorator", (done) => { 65 | @injectable() 66 | @Controller("/") 67 | class TestController { 68 | @Get("/") public getTest(req: Request, res: Response) { res.send("GET"); } 69 | @Post("/") public postTest(req: Request, res: Response) { res.send("POST"); } 70 | @Put("/") public putTest(req: Request, res: Response) { res.send("PUT"); } 71 | @Patch("/") public patchTest(req: Request, res: Response) { res.send("PATCH"); } 72 | @Head("/") public headTest(req: Request, res: Response) { res.send("HEAD"); } 73 | @Delete("/") public deleteTest(req: Request, res: Response) { res.send("DELETE"); } 74 | } 75 | container.bind(TYPE.Controller).to(TestController).whenTargetNamed("TestController"); 76 | 77 | server = new InversifyRestifyServer(container); 78 | let agent = request(server.build()); 79 | 80 | let deleteFn = () => { agent.delete("/").set("Accept", "text/plain").expect(200, "DELETE", done); }; 81 | let head = () => { agent.head("/").set("Accept", "text/plain").expect(200, "HEAD", deleteFn); }; 82 | let patch = () => { agent.patch("/").set("Accept", "text/plain").expect(200, "PATCH", head); }; 83 | let put = () => { agent.put("/").set("Accept", "text/plain").expect(200, "PUT", patch); }; 84 | let post = () => { agent.post("/").set("Accept", "text/plain").expect(200, "POST", put); }; 85 | let get = () => { agent.get("/").set("Accept", "text/plain").expect(200, "GET", post); }; 86 | 87 | get(); 88 | }); 89 | 90 | 91 | it("should work for more obscure HTTP methods using the Method decorator", (done) => { 92 | @injectable() 93 | @Controller("/") 94 | class TestController { 95 | @Method("opts", "/") public getTest(req: Request, res: Response) { res.send("OPTIONS"); } 96 | } 97 | container.bind(TYPE.Controller).to(TestController).whenTargetNamed("TestController"); 98 | 99 | server = new InversifyRestifyServer(container); 100 | request(server.build()) 101 | .options("/") 102 | .set("Accept", "text/plain") 103 | .expect(200, "OPTIONS", done); 104 | }); 105 | 106 | 107 | it("should use returned values as response", (done) => { 108 | let result = {"hello": "world"}; 109 | 110 | @injectable() 111 | @Controller("/") 112 | class TestController { 113 | @Get("/") public getTest(req: Request, res: Response) { return result; } 114 | } 115 | container.bind(TYPE.Controller).to(TestController).whenTargetNamed("TestController"); 116 | 117 | server = new InversifyRestifyServer(container); 118 | request(server.build()) 119 | .get("/") 120 | .expect(200, JSON.stringify(result), done); 121 | }); 122 | 123 | it("should allow server options", (done) => { 124 | let result = {"hello": "world"}; 125 | let customHeaderName = "custom-header-name"; 126 | let customHeaderValue = "custom-header-value"; 127 | 128 | @injectable() 129 | @Controller("/") 130 | class TestController { 131 | @Get("/") public getTest(req: Request, res: Response) { return result; } 132 | } 133 | container.bind(TYPE.Controller).to(TestController).whenTargetNamed("TestController"); 134 | 135 | server = new InversifyRestifyServer( 136 | container, 137 | { 138 | formatters: { 139 | "application/json": (req: Request, res: Response, body: any) => { 140 | res.setHeader(customHeaderName, customHeaderValue); 141 | return null; 142 | } 143 | } 144 | } 145 | ); 146 | request(server.build()) 147 | .get("/") 148 | .expect(customHeaderName, customHeaderValue) 149 | .expect(200, done); 150 | }); 151 | 152 | it("should allow server options with defaultRoot", (done) => { 153 | let result = {"hello": "world"}; 154 | let customHeaderName = "custom-header-name"; 155 | let customHeaderValue = "custom-header-value"; 156 | 157 | @injectable() 158 | @Controller("/") 159 | class TestController { 160 | @Get("/") public getTest(req: Request, res: Response) { return result; } 161 | } 162 | container.bind(TYPE.Controller).to(TestController).whenTargetNamed("TestController"); 163 | 164 | server = new InversifyRestifyServer( 165 | container, 166 | { 167 | defaultRoot: "/v1", 168 | formatters: { 169 | "application/json": (req: Request, res: Response, body: any) => { 170 | res.setHeader(customHeaderName, customHeaderValue); 171 | return null; 172 | } 173 | } 174 | } 175 | ); 176 | request(server.build()) 177 | .get("/v1") 178 | .expect(customHeaderName, customHeaderValue) 179 | .expect(200, done); 180 | }); 181 | }); 182 | 183 | 184 | describe("Middleware:", () => { 185 | let result: string; 186 | let middleware: any = { 187 | a: function (req: Request, res: Response, next: Next) { 188 | result += "a"; 189 | next(); 190 | }, 191 | b: function (req: Request, res: Response, next: Next) { 192 | result += "b"; 193 | next(); 194 | }, 195 | c: function (req: Request, res: Response, next: Next) { 196 | result += "c"; 197 | next(); 198 | } 199 | }; 200 | let spyA = spy(middleware, "a"); 201 | let spyB = spy(middleware, "b"); 202 | let spyC = spy(middleware, "c"); 203 | 204 | beforeEach((done) => { 205 | result = ""; 206 | spyA.resetHistory(); 207 | spyB.resetHistory(); 208 | spyC.resetHistory(); 209 | done(); 210 | }); 211 | 212 | it("should call method-level middleware correctly", (done) => { 213 | @injectable() 214 | @Controller("/") 215 | class TestController { 216 | @Get("/", spyA, spyB, spyC) public getTest(req: Request, res: Response) { res.send("GET"); } 217 | } 218 | container.bind(TYPE.Controller).to(TestController).whenTargetNamed("TestController"); 219 | 220 | server = new InversifyRestifyServer(container); 221 | request(server.build()) 222 | .get("/") 223 | .expect(200, "GET", function () { 224 | expect(spyA.calledOnce).to.eq(true); 225 | expect(spyB.calledOnce).to.eq(true); 226 | expect(spyC.calledOnce).to.eq(true); 227 | expect(result).to.equal("abc"); 228 | done(); 229 | }); 230 | }); 231 | 232 | 233 | it("should call controller-level middleware correctly", (done) => { 234 | @injectable() 235 | @Controller("/", spyA, spyB, spyC) 236 | class TestController { 237 | @Get("/") public getTest(req: Request, res: Response) { res.send("GET"); } 238 | } 239 | container.bind(TYPE.Controller).to(TestController).whenTargetNamed("TestController"); 240 | 241 | server = new InversifyRestifyServer(container); 242 | request(server.build()) 243 | .get("/") 244 | .expect(200, "GET", function () { 245 | expect(spyA.calledOnce).to.eq(true); 246 | expect(spyB.calledOnce).to.eq(true); 247 | expect(spyC.calledOnce).to.eq(true); 248 | expect(result).to.equal("abc"); 249 | done(); 250 | }); 251 | }); 252 | 253 | 254 | it("should call server-level middleware correctly", (done) => { 255 | @injectable() 256 | @Controller("/") 257 | class TestController { 258 | @Get("/") public getTest(req: Request, res: Response) { res.send("GET"); } 259 | } 260 | container.bind(TYPE.Controller).to(TestController).whenTargetNamed("TestController"); 261 | 262 | server = new InversifyRestifyServer(container); 263 | 264 | server.setConfig((app) => { 265 | app.use(spyA); 266 | app.use(spyB); 267 | app.use(spyC); 268 | }); 269 | 270 | request(server.build()) 271 | .get("/") 272 | .expect(200, "GET", function () { 273 | expect(spyA.calledOnce).to.eq(true); 274 | expect(spyB.calledOnce).to.eq(true); 275 | expect(spyC.calledOnce).to.eq(true); 276 | expect(result).to.equal("abc"); 277 | done(); 278 | }); 279 | }); 280 | 281 | 282 | it("should call all middleware in correct order", (done) => { 283 | @injectable() 284 | @Controller("/", spyB) 285 | class TestController { 286 | @Get("/", spyC) public getTest(req: Request, res: Response) { res.send("GET"); } 287 | } 288 | container.bind(TYPE.Controller).to(TestController).whenTargetNamed("TestController"); 289 | 290 | server = new InversifyRestifyServer(container); 291 | 292 | server.setConfig((app) => { 293 | app.use(spyA); 294 | }); 295 | 296 | request(server.build()) 297 | .get("/") 298 | .expect(200, "GET", function () { 299 | expect(spyA.calledOnce).to.eq(true); 300 | expect(spyB.calledOnce).to.eq(true); 301 | expect(spyC.calledOnce).to.eq(true); 302 | expect(result).to.equal("abc"); 303 | done(); 304 | }); 305 | }); 306 | 307 | it("should resolve controller-level middleware", (done) => { 308 | const symbolId = Symbol.for("spyA"); 309 | const strId = "spyB"; 310 | 311 | @injectable() 312 | @Controller("/", symbolId, strId) 313 | class TestController { 314 | @Get("/") public getTest(req: Request, res: Response) { res.send("GET"); } 315 | } 316 | 317 | container.bind(TYPE.Controller).to(TestController).whenTargetNamed("TestController"); 318 | container.bind(symbolId).toConstantValue(spyA); 319 | container.bind(strId).toConstantValue(spyB); 320 | 321 | server = new InversifyRestifyServer(container); 322 | 323 | request(server.build()) 324 | .get("/") 325 | .expect(200, "GET", function() { 326 | expect(spyA.calledOnce).to.eq(true); 327 | expect(spyB.calledOnce).to.eq(true); 328 | expect(result).to.equal("ab"); 329 | done(); 330 | }); 331 | }); 332 | 333 | it("should resolve method-level middleware", (done) => { 334 | const symbolId = Symbol.for("spyA"); 335 | const strId = "spyB"; 336 | 337 | @injectable() 338 | @Controller("/") 339 | class TestController { 340 | @Get("/", symbolId, strId) 341 | public getTest(req: Request, res: Response) { res.send("GET"); } 342 | } 343 | 344 | container.bind(TYPE.Controller).to(TestController).whenTargetNamed("TestController"); 345 | container.bind(symbolId).toConstantValue(spyA); 346 | container.bind(strId).toConstantValue(spyB); 347 | 348 | server = new InversifyRestifyServer(container); 349 | 350 | request(server.build()) 351 | .get("/") 352 | .expect(200, "GET", function() { 353 | expect(spyA.calledOnce).to.eq(true); 354 | expect(spyB.calledOnce).to.eq(true); 355 | expect(result).to.equal("ab"); 356 | done(); 357 | }); 358 | }); 359 | 360 | it("should compose controller- and method-level middleware", (done) => { 361 | const symbolId = Symbol.for("spyA"); 362 | const strId = "spyB"; 363 | 364 | @injectable() 365 | @Controller("/", symbolId) 366 | class TestController { 367 | @Get("/", strId) 368 | public getTest(req: Request, res: Response) { res.send("GET"); } 369 | } 370 | 371 | container.bind(TYPE.Controller).to(TestController).whenTargetNamed("TestController"); 372 | container.bind(symbolId).toConstantValue(spyA); 373 | container.bind(strId).toConstantValue(spyB); 374 | 375 | server = new InversifyRestifyServer(container); 376 | 377 | request(server.build()) 378 | .get("/") 379 | .expect(200, "GET", function() { 380 | expect(spyA.calledOnce).to.eq(true); 381 | expect(spyB.calledOnce).to.eq(true); 382 | expect(result).to.equal("ab"); 383 | done(); 384 | }); 385 | }); 386 | }); 387 | }); 388 | -------------------------------------------------------------------------------- /test/server.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Container, injectable } from "inversify"; 3 | import { Next, Request, Response, Server } from "restify"; 4 | import { spy } from "sinon"; 5 | import { TYPE } from "../src/constants"; 6 | import { Controller, Method } from "../src/decorators"; 7 | import { InversifyRestifyServer } from "../src/server"; 8 | 9 | describe("Unit Test: InversifyRestifyServer", () => { 10 | 11 | it("should call the configFn", () => { 12 | let middleware = function(req: Request, res: Response, next: Next) { return; }; 13 | let configFn = spy((app: Server) => { app.use(middleware); }); 14 | let container = new Container(); 15 | 16 | @injectable() 17 | class TestController {} 18 | 19 | container.bind(TYPE.Controller).to(TestController); 20 | let server = new InversifyRestifyServer(container); 21 | 22 | server.setConfig(configFn); 23 | 24 | expect(configFn.called).to.eq(false); 25 | 26 | server.build(); 27 | 28 | expect(configFn.calledOnce).to.eq(true); 29 | }); 30 | 31 | it("should generate routes for controller methods", () => { 32 | 33 | @injectable() 34 | @Controller("/root") 35 | class TestController { 36 | @Method("get", "/routeOne") 37 | public routeOne() { return; } 38 | 39 | @Method("get", { options: "test", path: "/routeTwo" }) 40 | public routeTwo() { return; } 41 | 42 | @Method("get", { path: "/routeThree" }) 43 | public routeThree() { return; } 44 | } 45 | 46 | let container = new Container(); 47 | container.bind(TYPE.Controller).to(TestController); 48 | let server = new InversifyRestifyServer(container); 49 | let app = server.build(); 50 | 51 | let routes = (Object).values(app.router.getRoutes()); 52 | 53 | let routeOne = routes.find((route: any) => route.path === "/root/routeOne" && route.method === "GET"); 54 | expect(routeOne).not.to.eq(undefined); 55 | 56 | let routeTwo = routes.find((route: any) => route.path === "/root/routeTwo" && route.method === "GET"); 57 | expect(routeTwo).not.to.eq(undefined); 58 | expect((routeTwo).spec.options).to.eq("test"); 59 | 60 | let routeThree = routes.find((route: any) => route.path === "/root/routeThree" && route.method === "GET"); 61 | expect(routeThree).not.to.eq(undefined); 62 | 63 | }); 64 | 65 | it("should generate routes for controller methods using defaultRoot", () => { 66 | 67 | @injectable() 68 | @Controller("/root") 69 | class TestController { 70 | @Method("get", "/routeOne") 71 | public routeOne() { return; } 72 | 73 | @Method("get", { options: "test", path: "/routeTwo" }) 74 | public routeTwo() { return; } 75 | 76 | @Method("get", { path: "/routeThree" }) 77 | public routeThree() { return; } 78 | } 79 | 80 | let container = new Container(); 81 | container.bind(TYPE.Controller).to(TestController); 82 | 83 | let server = new InversifyRestifyServer(container, { 84 | defaultRoot: "/v1" 85 | }); 86 | 87 | let app = server.build(); 88 | 89 | let routes = (Object).values(app.router.getRoutes()); 90 | 91 | let routeOne = routes.find((route: any) => route.path === "/v1/root/routeOne" && route.method === "GET"); 92 | expect(routeOne).not.to.eq(undefined); 93 | 94 | let routeTwo = routes.find((route: any) => route.path === "/v1/root/routeTwo" && route.method === "GET"); 95 | expect(routeTwo).not.to.eq(undefined); 96 | expect((routeTwo).spec.options).to.eq("test"); 97 | 98 | let routeThree = routes.find((route: any) => route.path === "/v1/root/routeThree" && route.method === "GET"); 99 | expect(routeThree).not.eq(undefined); 100 | 101 | }); 102 | 103 | }); 104 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2017"], 4 | "module": "commonjs", 5 | "target": "es2017", // node 8 6 | "strict": true, 7 | "strictPropertyInitialization": false, // should be true 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "moduleResolution": "node", 12 | "allowUnusedLabels": false, 13 | "allowUnreachableCode": false, 14 | "noFallthroughCasesInSwitch": true, 15 | "noImplicitReturns": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": false, // should be true 18 | "importsNotUsedAsValues": "remove", // should be error 19 | "types": ["inversify", "mocha", "reflect-metadata", "restify"], 20 | "experimentalDecorators": true, 21 | "emitDecoratorMetadata": true, 22 | "removeComments": true, 23 | "rootDir": "." 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "rules": { 4 | "class-name": true, 5 | "comment-format": [true, "check-space"], 6 | "curly": true, 7 | "eofline": true, 8 | "forin": true, 9 | "indent": [true, "spaces"], 10 | "label-position": true, 11 | "max-line-length": [true, 140], 12 | "member-access": true, 13 | "member-ordering": [true, 14 | "public-before-private", 15 | "static-before-instance", 16 | "variables-before-functions" 17 | ], 18 | "no-arg": true, 19 | "no-bitwise": true, 20 | "no-console": [true, 21 | "debug", 22 | "info", 23 | "time", 24 | "timeEnd", 25 | "trace" 26 | ], 27 | "no-construct": true, 28 | "no-debugger": true, 29 | "no-duplicate-variable": true, 30 | "no-empty": true, 31 | "no-eval": true, 32 | "no-inferrable-types": true, 33 | "no-shadowed-variable": true, 34 | "no-string-literal": true, 35 | "no-switch-case-fall-through": false, 36 | "no-trailing-whitespace": true, 37 | "no-unused-expression": true, 38 | "no-var-keyword": true, 39 | "object-literal-sort-keys": true, 40 | "one-line": [true, 41 | "check-open-brace", 42 | "check-catch", 43 | "check-else", 44 | "check-whitespace" 45 | ], 46 | "quotemark": [true, "double", "avoid-escape"], 47 | "radix": true, 48 | "semicolon": true, 49 | "trailing-comma": false, 50 | "triple-equals": [true, "allow-null-check"], 51 | "typedef-whitespace": [true, { 52 | "call-signature": "nospace", 53 | "index-signature": "nospace", 54 | "parameter": "nospace", 55 | "property-declaration": "nospace", 56 | "variable-declaration": "nospace" 57 | }], 58 | "variable-name": false, 59 | "whitespace": [true, 60 | "check-branch", 61 | "check-decl", 62 | "check-operator", 63 | "check-separator", 64 | "check-type" 65 | ] 66 | } 67 | } --------------------------------------------------------------------------------