UNPKG

4.26 kBJavaScriptView Raw
1/* @flow */
2'use strict'
3
4/* ::
5import type {
6 BmRequest,
7 CorsConfiguration
8} from '../types.js'
9type HapiCorsConfiguration = {
10 credentials: boolean,
11 exposedHeaders: Array<string>,
12 headers: Array<string>,
13 maxAge: number,
14 origin: Array<string>
15}
16*/
17
18const Boom = require('boom')
19const Hapi = require('hapi')
20const pify = require('pify')
21
22const apis = require('./apis.js')
23const handlers = require('./handlers.js')
24const values = require('./values.js')
25const wrapper = require('./wrapper.js')
26
27function normaliseHapiCors (
28 cors /* : CorsConfiguration | false */
29) /* : HapiCorsConfiguration | false */ {
30 if (!cors) {
31 return false
32 }
33 return {
34 credentials: cors.credentials,
35 exposedHeaders: cors.exposedHeaders,
36 headers: cors.headers,
37 maxAge: cors.maxAge,
38 origin: cors.origins
39 }
40}
41
42// return only the pertinent data from a Hapi Request
43function normaliseHapiRequest (
44 request /* : any */,
45 params /* : { [id:string]: string } */
46) /* : BmRequest */ {
47 const urlInfo = Object.assign({}, request.url, {
48 host: request.info.host,
49 hostname: request.info.hostname,
50 params,
51 protocol: wrapper.protocolFromHeaders(request.headers)
52 })
53 delete urlInfo.auth
54 delete urlInfo.hash
55 delete urlInfo.href
56 delete urlInfo.path
57 delete urlInfo.port
58 delete urlInfo.search
59 delete urlInfo.slashes
60 return {
61 body: request.payload,
62 headers: request.headers,
63 method: request.method,
64 route: '',
65 url: urlInfo
66 }
67}
68
69function startServer (
70 logger/* : typeof console */,
71 options /* : {
72 cors: CorsConfiguration | false,
73 cwd: string,
74 port: string | number
75 } */
76) /* : Promise<any> */ {
77 options = options || {}
78 const server = new Hapi.Server()
79 server.connection({ port: options.port })
80
81 server.route({
82 config: {
83 cors: normaliseHapiCors(options.cors)
84 },
85 handler (request, reply) {
86 // TODO: consider sanitising this input
87 const cwd = options.cwd
88 const route = request.params.route
89 if (!route) {
90 reply(Boom.notImplemented('Must supply a route')) // 501
91 return
92 }
93
94 apis.getRouteConfig(cwd, route)
95 .catch((err) => Promise.reject(Boom.wrap(err, 404)))
96 .then((routeConfig) => {
97 return apis.getHandlerConfig(routeConfig, request.method)
98 .then((handlerConfig) => {
99 const handler = handlerConfig.handler
100 if (typeof handler !== 'function') {
101 reply(Boom.methodNotAllowed(`${request.method.toUpperCase()} method has not been implemented`)) // 405
102 return
103 }
104 const bmRequest = normaliseHapiRequest(request, handlerConfig.params)
105 bmRequest.route = routeConfig.route
106 return handlers.executeHandler(handler, bmRequest)
107 .then((bmResponse) => {
108 const jsonResult = JSON.stringify(bmResponse.payload, null, 2)
109 const response = reply(null, jsonResult)
110 .code(bmResponse.statusCode)
111 .type('application/json')
112 Object.keys(bmResponse.headers).forEach((key) => response.header(key, bmResponse.headers[key]))
113 })
114 })
115 })
116 .catch((err) => {
117 if (!err || !err.isBoom) {
118 err = Boom.wrap(err, 500)
119 }
120 // Want to show customers what went wrong locally for development.
121 // Boom hides this message for 500 status codes.
122 err.output.payload.message = err.message
123
124 logger.error(err.stack)
125 if (err.data) {
126 logger.error('Boom Data:', JSON.stringify(err.data, null, 2))
127 }
128 reply(err)
129 })
130 },
131 // HTTP HEAD is automatically provided based on GET
132 method: values.METHODS.filter((method) => method !== 'OPTIONS'),
133 path: '/{route*}' // catch-all
134 })
135
136 return server.register({
137 register: require('good'),
138 options: {
139 ops: false,
140 reporters: {
141 console: [{
142 module: 'good-console'
143 }, 'stdout']
144 }
145 }
146 })
147 .then(() => pify(server.start.bind(server))())
148 .then(() => server)
149}
150
151module.exports = {
152 normaliseHapiCors,
153 normaliseHapiRequest,
154 startServer
155}