├── .babelrc ├── .github └── FUNDING.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── docpress.json ├── index.d.ts ├── index.js ├── package-lock.json ├── package.json └── test ├── fixtures ├── default.js ├── invalid-route.js └── no-routes.js └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-decorators-legacy", 4 | "transform-object-rest-spread", 5 | "transform-class-properties", 6 | "transform-async-to-module-method" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: knownasilya 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | logs 3 | *.log 4 | node_modules 5 | dist 6 | tmp 7 | coverage 8 | npm-debug.log 9 | _docpress 10 | .vscode -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | docpress.json 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | - '10' 5 | - '12' 6 | after_script: 7 | - npm run coveralls 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | # [2.0.0](https://github.com/knownasilya/hapi-decorators/compare/v1.0.0...v2.0.0) (2019-11-12) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * Bump lodash.template from 4.4.0 to 4.5.0 ([#18](https://github.com/knownasilya/hapi-decorators/issues/18)) ([c02062c](https://github.com/knownasilya/hapi-decorators/commit/c02062c)) 12 | * drop node 6 ([172ed1c](https://github.com/knownasilya/hapi-decorators/commit/172ed1c)) 13 | * Update to use current version of hapi ([#17](https://github.com/knownasilya/hapi-decorators/issues/17)) ([1e41646](https://github.com/knownasilya/hapi-decorators/commit/1e41646)) 14 | 15 | 16 | ### BREAKING CHANGES 17 | 18 | * drop node v6 support 19 | * drop hapi < 18.4 20 | 21 | * Security fixes - non-breaking changes 22 | 23 | * Security vulnerabilities with force fix 24 | 25 | * Updating some packages 26 | 27 | * Updating to the new hapi libraries 28 | 29 | 30 | 31 | 32 | # [1.0.0](https://github.com/knownasilya/hapi-decorators/compare/v0.4.3...v1.0.0) (2018-06-15) 33 | 34 | 35 | ### Bug Fixes 36 | 37 | * remove es2015 preset ([04bb0bf](https://github.com/knownasilya/hapi-decorators/commit/04bb0bf)) 38 | * run audit ([49d2269](https://github.com/knownasilya/hapi-decorators/commit/49d2269)) 39 | * update package lock ([72afa45](https://github.com/knownasilya/hapi-decorators/commit/72afa45)) 40 | 41 | 42 | 43 | # Change Log 44 | 45 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hapi-decorators 2 | 3 | Decorators for HapiJS routes. 4 | Heavily inspired and borrowed from https://github.com/stewartml/express-decorators 5 | 6 | Great to mix with https://github.com/jayphelps/core-decorators.js 7 | 8 | [![npm version](https://badge.fury.io/js/hapi-decorators.svg)](http://badge.fury.io/js/hapi-decorators) 9 | [![Build Status](https://travis-ci.org/knownasilya/hapi-decorators.svg)](https://travis-ci.org/knownasilya/hapi-decorators) 10 | [![Coverage Status](https://coveralls.io/repos/knownasilya/hapi-decorators/badge.svg?branch=master&service=github)](https://coveralls.io/github/knownasilya/hapi-decorators?branch=master) 11 | 12 | ## Usage 13 | 14 | Prerequisits: 15 | 16 | - Hapi 18.4+ (Use 1.x for Hapi 17) 17 | - Node 8+ (Use 1.x for Node 6) 18 | 19 | ```sh 20 | npm install --save hapi-decorators 21 | ``` 22 | 23 | ```js 24 | import { 25 | get, 26 | controller 27 | } from 'hapi-decorators' 28 | import Hapi from '@hapi/hapi' 29 | 30 | const server = new Hapi.Server() 31 | 32 | server.connection({ 33 | host: 'localhost', 34 | port: 3000 35 | }) 36 | 37 | // Define your endpoint controller 38 | @controller('/hello') 39 | class TestController { 40 | constructor(target) { 41 | this.target = target 42 | } 43 | 44 | @get('/world') 45 | sayHello(request, reply) { 46 | reply({ message: `hello, ${this.target}` }) 47 | } 48 | } 49 | 50 | // InitializeController 51 | let test = new TestController('world') 52 | 53 | // Add Test Controller routes to server 54 | server.route(test.routes()) 55 | 56 | // Start the server 57 | server.start((err) => { 58 | if (err) throw err 59 | console.log(`Server running at: ${server.info.uri}`) 60 | }) 61 | ``` 62 | 63 | ### Setup Babel 64 | 65 | Run the above script with the following command, after installing [babel]. 66 | 67 | ```no-highlight 68 | babel-node --optional es7.decorators,es7.objectRestSpread index.js 69 | ``` 70 | 71 | Note: Decorators are currently unsupported in Babel 6. To work around that [issue] 72 | use the [transform-decorators-legacy] plugin. See this [post] for detailed instructions. 73 | 74 | 75 | ## Decorators 76 | 77 | ### `@controller(basePath)` 78 | 79 | **REQUIRED** This decorator is required at the class level, since it processes the other decorators, and adds 80 | the `instance.routes()` function, which returns the routes that can be used with Hapi, e.g. `server.routes(users.routes())`. 81 | 82 | 83 | ### `@route(method, path)` 84 | 85 | This decorator should be attached to a method of a class, e.g. 86 | 87 | ```js 88 | @controller('/users') 89 | class Users { 90 | @route('post', '/') 91 | newUser(request, reply) { 92 | reply([]) 93 | } 94 | } 95 | ``` 96 | 97 | **Helper Decorators** 98 | 99 | * `@get(path)` 100 | * `@post(path)` 101 | * `@put(path)` 102 | * `@patch(path)` 103 | * `@delete(path)` 104 | * `@del(path)` 105 | * `@all(path)` 106 | 107 | These are shortcuts for `@route(method, path)` where `@get('/revoke')` would be `@route('get', '/revoke')`. 108 | 109 | ### `@options(options)` 110 | 111 | Overall options setting if none of the other decorators are sufficient. 112 | 113 | ### `@validate(validateConfig)` 114 | 115 | Add a validation object for the different types, except for the response. 116 | `config` is an object, with keys for the different types, e.g. `payload`. 117 | 118 | ### `@cache(cacheConfig)` 119 | 120 | Cache settings for the route config object. 121 | 122 | ### `@pre(preArray)` 123 | 124 | Set prerequisite middleware array for a given route. 125 | Expects an array, but if passed something else, it will put it into the pre array. 126 | 127 | [babel]: https://www.npmjs.com/package/babel 128 | [transform-decorators-legacy]: https://www.npmjs.com/package/babel-plugin-transform-decorators-legacy 129 | [issue]: https://phabricator.babeljs.io/T2645 130 | [post]: http://technologyadvice.github.io/es7-decorators-babel6 131 | -------------------------------------------------------------------------------- /docpress.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": "knownasilya/hapi-decorators" 3 | } 4 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for hapi-decorators v0.4.3 2 | // Project: https://github.com/knownasilya/hapi-decorators 3 | // Definitions by: Ken Howard 4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 5 | // TypeScript Version: 2.4 6 | import * as hapi from '@hapi/hapi'; 7 | 8 | interface ControllerStatic { 9 | new(...args: any[]): Controller; 10 | } 11 | export interface Controller { 12 | baseUrl: string; 13 | routes: () => hapi.ServerRoute[]; 14 | } 15 | export function controller(baseUrl: string): (target: ControllerStatic) => void; 16 | interface IRouteSetup { 17 | (target: any, key: string, descriptor: PropertyDescriptor): PropertyDescriptor; 18 | } 19 | interface IRouteDecorator { 20 | (method: string, path: string): IRouteSetup; 21 | } 22 | interface IRouteConfig { 23 | (path: string): IRouteSetup; 24 | } 25 | export const route: IRouteDecorator; 26 | export const get: IRouteConfig; 27 | export const post: IRouteConfig; 28 | export const put: IRouteConfig; 29 | // export const delete: IRouteConfig; 30 | export const del: IRouteConfig; 31 | export const patch: IRouteConfig; 32 | export const all: IRouteConfig; 33 | export function options(options: hapi.RouteOptions | ((server: hapi.Server) => hapi.RouteOptions)): (target: any, key: string, descriptor: PropertyDescriptor) => PropertyDescriptor; 34 | export function validate(config: hapi.RouteOptionsValidate): (target: any, key: string, descriptor: PropertyDescriptor) => PropertyDescriptor; 35 | export function cache(cacheConfig: false | hapi.RouteOptionsCache): (target: any, key: string, descriptor: PropertyDescriptor) => PropertyDescriptor; 36 | export function pre(pre: hapi.RouteOptionsPreArray): (target: any, key: string, descriptor: PropertyDescriptor) => PropertyDescriptor; 37 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var extend = require('extend') 4 | var debug = require('debug')('hapi-decorators') 5 | var routeMethods = { 6 | get: 'get', 7 | post: 'post', 8 | put: 'put', 9 | delete: 'delete', 10 | del: 'delete', 11 | patch: 'patch', 12 | all: '*' 13 | } 14 | 15 | exports.controller = function controller (baseUrl) { 16 | debug(`@controller setup`) 17 | return function (target) { 18 | target.prototype.baseUrl = baseUrl 19 | 20 | target.prototype.routes = function () { 21 | var self = this 22 | var base = trimslash(this.baseUrl) 23 | 24 | debug('Pre-trim baseUrl: %s', this.baseUrl) 25 | debug('Post-trim baseUrl: %s', base) 26 | 27 | if (!this.rawRoutes) { 28 | return [] 29 | } 30 | 31 | return this.rawRoutes.map(function (route) { 32 | if (!route.path) { 33 | throw new Error('Route path must be set with `@route` or another alias') 34 | } 35 | 36 | debug('Route path before merge with baseUrl: %s', route.path) 37 | var url = (base + trimslash(route.path)) || '/' 38 | 39 | var hapiRoute = extend({}, route) 40 | 41 | hapiRoute.path = url 42 | hapiRoute.options.bind = self 43 | 44 | return hapiRoute 45 | }) 46 | } 47 | } 48 | } 49 | 50 | function route (method, path) { 51 | debug('@route (or alias) setup') 52 | return function (target, key, descriptor) { 53 | var targetName = target.constructor.name 54 | var routeId = targetName + '.' + key 55 | 56 | setRoute(target, key, { 57 | method: method, 58 | path: path, 59 | options: { 60 | id: routeId 61 | }, 62 | handler: descriptor.value 63 | }) 64 | 65 | return descriptor 66 | } 67 | } 68 | 69 | exports.route = route 70 | 71 | // Export route aliases 72 | Object.keys(routeMethods).forEach(function (key) { 73 | exports[key] = route.bind(null, routeMethods[key]) 74 | }) 75 | 76 | function options (options) { 77 | debug('@options or @config setup') 78 | return function (target, key, descriptor) { 79 | setRoute(target, key, { 80 | options: options 81 | }) 82 | 83 | return descriptor 84 | } 85 | } 86 | 87 | exports.options = options 88 | 89 | function validate (config) { 90 | debug('@validate setup') 91 | return function (target, key, descriptor) { 92 | setRoute(target, key, { 93 | options: { 94 | validate: config 95 | } 96 | }) 97 | 98 | return descriptor 99 | } 100 | } 101 | 102 | exports.validate = validate 103 | 104 | function cache (cacheConfig) { 105 | debug('@cache setup') 106 | return function (target, key, descriptor) { 107 | setRoute(target, key, { 108 | options: { 109 | cache: cacheConfig 110 | } 111 | }) 112 | 113 | return descriptor 114 | } 115 | } 116 | 117 | exports.cache = cache 118 | 119 | function pre (pre) { 120 | debug('@pre setup') 121 | return function (target, key, descriptor) { 122 | if (typeof pre === 'string') { 123 | pre = [{ method: target.middleware[pre] }] 124 | } else if (!Array.isArray(pre)) { 125 | pre = [pre] 126 | } 127 | 128 | setRoute(target, key, { 129 | options: { 130 | pre: pre 131 | } 132 | }) 133 | 134 | return descriptor 135 | } 136 | } 137 | 138 | exports.pre = pre 139 | 140 | function middleware () { 141 | return function (target, key, descriptor) { 142 | if (!target.middleware) { 143 | target.middleware = {} 144 | } 145 | 146 | target.middleware[key] = descriptor.value 147 | 148 | return descriptor 149 | } 150 | } 151 | 152 | exports.middleware = middleware 153 | 154 | function setRoute (target, key, value) { 155 | if (!target.rawRoutes) { 156 | target.rawRoutes = [] 157 | } 158 | 159 | var targetName = target.constructor.name 160 | var routeId = targetName + '.' + key 161 | var defaultRoute = { 162 | options: { 163 | id: routeId 164 | } 165 | } 166 | var found = target.rawRoutes.find(r => r.options.id === routeId) 167 | 168 | if (found) { 169 | debug('Subsequent configuration of route object for: %s', routeId) 170 | extend(true, found, value) 171 | } else { 172 | debug('Initial setup of route object for: %s', routeId) 173 | target.rawRoutes.push(extend(true, defaultRoute, value)) 174 | } 175 | } 176 | 177 | function trimslash (s) { 178 | return s[s.length - 1] === '/' 179 | ? s.slice(0, s.length - 1) 180 | : s 181 | } 182 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-decorators", 3 | "version": "2.0.0", 4 | "description": "Decorators for HapiJS routes using ES6 classes", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "babel index.js test/*.js test/**/*.js -d dist --source-maps", 8 | "lint": "standard | snazzy index.js test/*.js test/**/*.js", 9 | "pretest": "npm run lint", 10 | "test": "npm run build && node dist/test/index.js | tspec", 11 | "posttest": "npm run lint", 12 | "coverage": "babel-node node_modules/.bin/isparta cover test/*.js | tspec", 13 | "coveralls": "npm run coverage -s && coveralls < coverage/lcov.info", 14 | "postcoveralls": "rimraf ./coverage", 15 | "deploy:docs": "docpress build && git-update-ghpages knownasilya/hapi-decorators _docpress", 16 | "prepublish": "in-publish && npm run deploy:docs || not-in-publish", 17 | "release": "standard-version" 18 | }, 19 | "author": "Ilya Radchenko (https://github.com/knownasilya)", 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/knownasilya/hapi-decorators.git" 23 | }, 24 | "keywords": [ 25 | "decorators", 26 | "hapijs", 27 | "hapi", 28 | "controller" 29 | ], 30 | "license": "ISC", 31 | "dependencies": { 32 | "debug": "^3.1.0", 33 | "extend": "^3.0.2", 34 | "in-publish": "^2.0.0" 35 | }, 36 | "devDependencies": { 37 | "@types/hapi__hapi": "^18.2.6", 38 | "babel-cli": "^6.26.0", 39 | "babel-eslint": "^10.0.3", 40 | "babel-plugin-transform-async-to-module-method": "^6.24.1", 41 | "babel-plugin-transform-class-properties": "^6.24.1", 42 | "babel-plugin-transform-decorators-legacy": "^1.3.5", 43 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 44 | "coveralls": "^3.0.7", 45 | "docpress": "^0.8.0", 46 | "git-update-ghpages": "^1.3.0", 47 | "isparta": "^4.0.0", 48 | "rimraf": "^2.4.3", 49 | "snazzy": "^7.1.1", 50 | "standard": "^11.0.1", 51 | "standard-version": "^8.0.1", 52 | "tap-spec": "^5.0.0", 53 | "tape": "^4.2.0" 54 | }, 55 | "standard": { 56 | "parser": "babel-eslint" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/fixtures/default.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const web = require('../../') 4 | const { controller, route, cache, options, validate, pre, middleware } = web 5 | 6 | @controller('/check') 7 | class Check { 8 | @middleware() 9 | someMethod () { 10 | return 'test' 11 | } 12 | 13 | @route('get', '/in') 14 | @validate({ payload: true }) 15 | checkIn (request, reply) { 16 | // intentionally empty 17 | } 18 | 19 | @route('get', '/out') 20 | @pre('someMethod') 21 | @options({ test: 'hello' }) 22 | checkOut (request, reply) { 23 | 24 | } 25 | 26 | @route('get', '/') 27 | @cache({ privacy: 'public' }) 28 | listAll (request, reply) { 29 | 30 | } 31 | } 32 | module.exports = Check 33 | -------------------------------------------------------------------------------- /test/fixtures/invalid-route.js: -------------------------------------------------------------------------------- 1 | var web = require('../../') 2 | 3 | @web.controller('/check') 4 | class InvalidRoutes { 5 | @web.validate({ payload: true }) 6 | checkIn (request, reply) { 7 | // intentionally empty 8 | } 9 | 10 | @web.options({ test: 'hello' }) 11 | checkOut (request, reply) { 12 | // intentionally empty 13 | } 14 | } 15 | 16 | module.exports = InvalidRoutes 17 | -------------------------------------------------------------------------------- /test/fixtures/no-routes.js: -------------------------------------------------------------------------------- 1 | var web = require('../../') 2 | 3 | @web.controller('/check') 4 | class NoRoutes { 5 | } 6 | 7 | module.exports = NoRoutes 8 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var test = require('tape') 4 | var NoRoutes = require('./fixtures/no-routes') 5 | var Default = require('./fixtures/default') 6 | var Invalid = require('./fixtures/invalid-route') 7 | 8 | test('instance has routes function', function (t) { 9 | let instance = new Default() 10 | 11 | t.ok(typeof instance.routes === 'function', 'Has `routes` function') 12 | t.end() 13 | }) 14 | 15 | test('instance has no routes', function (t) { 16 | let instance = new NoRoutes() 17 | 18 | t.same(instance.routes(), [], 'No routes returns empty array') 19 | t.end() 20 | }) 21 | 22 | test('instance generates routes array', function (t) { 23 | let instance = new Default() 24 | let results = instance.routes() 25 | 26 | t.ok(Array.isArray(results), 'results is an array') 27 | t.equal(results.length, 3, 'Has right number of routes') 28 | 29 | let first = results[0] 30 | let second = results[1] 31 | 32 | t.equal(first.method, 'get', 'method is get') 33 | t.equal(first.path, '/check/in', 'path is merged with controller path') 34 | t.equal(typeof first.handler, 'function', 'handler is a function') 35 | if (second) { 36 | t.equal(second.options.pre.length, 1, 'Has a pre assigned') 37 | } 38 | t.end() 39 | }) 40 | 41 | test('route paths remain valid after repeated calls to `routes()` method', function (t) { 42 | let instance = new Default() 43 | instance.routes() 44 | instance.routes() 45 | let results3 = instance.routes() 46 | 47 | let first = results3[0] 48 | 49 | t.equal(first.path, '/check/in', 'path remains valid') 50 | t.end() 51 | }) 52 | 53 | test('validate sets up options correctly', function (t) { 54 | let instance = new Default() 55 | let results = instance.routes() 56 | let first = results[0] 57 | 58 | t.same(first.options, { 59 | id: 'Check.checkIn', 60 | bind: instance, 61 | validate: { 62 | payload: true 63 | } 64 | }, 'validate options is valid') 65 | t.end() 66 | }) 67 | 68 | test('cache sets up options correctly', function (t) { 69 | let instance = new Default() 70 | let results = instance.routes() 71 | let route = results[2] 72 | 73 | t.ok(route, 'A third route was not found') 74 | if (route) { 75 | t.same(route.options.cache, { 76 | privacy: 'public' 77 | }, 'cache options is valid') 78 | } 79 | t.end() 80 | }) 81 | 82 | test('options sets up options correctly', function (t) { 83 | let instance = new Default() 84 | let results = instance.routes() 85 | let second = results[1] 86 | t.ok(second, 'A second route was not found') 87 | 88 | if (second) { 89 | t.equal(second.options.id, 'Check.checkOut') 90 | t.equal(second.options.bind, instance) 91 | t.equal(second.options.test, 'hello') 92 | } 93 | t.end() 94 | }) 95 | 96 | test('invalid setup with no routes', function (t) { 97 | let instance = new Invalid() 98 | 99 | t.throws(function () { 100 | instance.routes() 101 | }, /Route path must be set/) 102 | t.end() 103 | }) 104 | --------------------------------------------------------------------------------