├── .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 | [](http://badge.fury.io/js/hapi-decorators)
9 | [](https://travis-ci.org/knownasilya/hapi-decorators)
10 | [](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 |
--------------------------------------------------------------------------------