UNPKG

6.42 kBJavaScriptView Raw
1/**
2This module exports a "handler" function,
3that wraps a customer function.
4We bundle this module and its dependencies to ../dist/wrapper.js .
5To bundle: `npm run build`
6*/
7/* @flow */
8'use strict'
9
10/* ::
11import type {
12 BmRequest,
13 Headers,
14 LambdaEvent
15} from '../types.js'
16*/
17
18const path = require('path')
19
20const handlers = require('../lib/handlers.js')
21const wrapper = require('../lib/wrapper.js')
22
23// return only the pertinent data from a API Gateway + Lambda event
24function normaliseLambdaRequest (
25 event /* : LambdaEvent */
26) /* : BmRequest */ {
27 const headers = wrapper.keysToLowerCase(event.headers)
28 let body = event.body
29 try {
30 body = JSON.parse(body)
31 } catch (e) {
32 // Do nothing...
33 }
34 const host = headers['x-forwarded-host'] || headers.host
35 return {
36 body,
37 headers,
38 method: wrapper.normaliseMethod(event.httpMethod),
39 route: event.path,
40 url: {
41 host,
42 hostname: host,
43 params: {},
44 pathname: event.path,
45 protocol: wrapper.protocolFromHeaders(headers),
46 query: event.queryStringParameters || {}
47 }
48 }
49}
50
51function handler (
52 event /* : LambdaEvent */,
53 context /* : any */,
54 cb /* : (error: null, response: {
55 body: string,
56 headers: Headers,
57 statusCode: number
58 }) => void */
59) /* : Promise<void> */ {
60 const startTime = Date.now()
61 const request = normaliseLambdaRequest(event)
62 const internalHeaders = {}
63 internalHeaders['Content-Type'] = 'application/json'
64 const finish = (statusCode, body, customHeaders) => {
65 const headers = Object.assign(internalHeaders, customHeaders)
66 const endTime = Date.now()
67 const requestTime = endTime - startTime
68 console.log('BLINKM_ANALYTICS_EVENT', JSON.stringify({ // eslint-disable-line no-console
69 request: {
70 method: request.method.toUpperCase(),
71 query: request.url.query,
72 port: 443,
73 path: request.route,
74 hostName: request.url.hostname,
75 params: request.url.params,
76 protocol: request.url.protocol
77 },
78 response: {
79 statusCode: statusCode
80 },
81 requestTime: {
82 startDateTime: new Date(startTime),
83 startTimeStamp: startTime,
84 endDateTime: new Date(endTime),
85 endTimeStamp: endTime,
86 ms: requestTime,
87 s: requestTime / 1000
88 }
89 }, null, 2))
90 cb(null, {
91 body: JSON.stringify(body, null, 2),
92 headers: wrapper.keysToLowerCase(headers),
93 statusCode: statusCode
94 })
95 }
96
97 return Promise.resolve()
98 // $FlowFixMe requiring file without string literal to accomodate for __dirname
99 .then(() => require(path.join(__dirname, 'bm-server.json')))
100 .then((config) => {
101 // Get handler module based on route
102 let routeConfig
103 try {
104 routeConfig = handlers.findRouteConfig(event.path, config.routes)
105 request.url.params = routeConfig.params || {}
106 request.route = routeConfig.route
107 } catch (error) {
108 return finish(404, {
109 error: 'Not Found',
110 message: error.message,
111 statusCode: 404
112 })
113 }
114
115 // Check for browser requests and apply CORS if required
116 if (request.headers.origin) {
117 if (!config.cors) {
118 // No cors, we will return 405 result and let browser handler error
119 return finish(405, {
120 error: 'Method Not Allowed',
121 message: 'OPTIONS method has not been implemented',
122 statusCode: 405
123 })
124 }
125 if (!config.cors.origins.some((origin) => origin === '*' || origin === request.headers.origin)) {
126 // Invalid origin, we will return 200 result and let browser handler error
127 return finish(200)
128 }
129 // Headers for all cross origin requests
130 internalHeaders['Access-Control-Allow-Origin'] = request.headers.origin
131 internalHeaders['Access-Control-Expose-Headers'] = config.cors.exposedHeaders.join(',')
132 // Headers for OPTIONS cross origin requests
133 if (request.method === 'options' && request.headers['access-control-request-method']) {
134 internalHeaders['Access-Control-Allow-Headers'] = config.cors.headers.join(',')
135 internalHeaders['Access-Control-Allow-Methods'] = request.headers['access-control-request-method']
136 internalHeaders['Access-Control-Max-Age'] = config.cors.maxAge
137 }
138 // Only set credentials header if truthy
139 if (config.cors.credentials) {
140 internalHeaders['Access-Control-Allow-Credentials'] = true
141 }
142 }
143 if (request.method === 'options') {
144 // For OPTIONS requests, we can just finish
145 // as we have created our own implementation of CORS
146 return finish(200)
147 }
148
149 // Change current working directory to the project
150 // to accomadate for packages using process.cwd()
151 const projectPath = path.join(__dirname, 'project')
152 if (process.cwd() !== projectPath) {
153 try {
154 process.chdir(projectPath)
155 } catch (err) {
156 return Promise.reject(new Error(`Could not change current working directory to '${projectPath}': ${err}`))
157 }
158 }
159
160 return handlers.getHandler(path.join(__dirname, routeConfig.module), request.method)
161 .then((handler) => {
162 if (typeof handler !== 'function') {
163 return finish(405, {
164 error: 'Method Not Allowed',
165 message: `${request.method.toUpperCase()} method has not been implemented`,
166 statusCode: 405
167 })
168 }
169
170 return handlers.executeHandler(handler, request)
171 .then((response) => finish(response.statusCode, response.payload, response.headers))
172 })
173 })
174 .catch((error) => {
175 if (error && error.stack) {
176 console.error(error.stack) // eslint-disable-line no-console
177 }
178 if (error && error.isBoom && error.output && error.output.payload && error.output.statusCode) {
179 if (error.data) {
180 console.error('Boom Data: ', JSON.stringify(error.data, null, 2)) // eslint-disable-line no-console
181 }
182 return finish(error.output.statusCode, error.output.payload, error.output.headers)
183 }
184 finish(500, {
185 error: 'Internal Server Error',
186 message: 'An internal server error occurred',
187 statusCode: 500
188 })
189 })
190}
191
192module.exports = {
193 handler,
194 normaliseLambdaRequest
195}