UNPKG

18 kBJavaScriptView Raw
1(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.wrapper = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
2/* @flow */
3'use strict'
4
5const createInternal = require('./utils/internal.js').createInternal
6
7/* ::
8import type {Headers} from '../types.js'
9*/
10
11const internal = createInternal()
12
13class BmResponse {
14 constructor () {
15 Object.assign(internal(this), {
16 headers: {},
17 payload: undefined,
18 statusCode: 200
19 })
20 }
21
22 get headers () /* : Headers */ {
23 return Object.assign({}, internal(this).headers)
24 }
25
26 get payload () /* : any */ {
27 return internal(this).payload
28 }
29
30 get statusCode () /* : number */ {
31 return internal(this).statusCode
32 }
33
34 setHeader (
35 key /* : string */,
36 value /* : string */
37 ) /* : BmResponse */ {
38 key = key.toLowerCase()
39 internal(this).headers[key] = value
40 return this
41 }
42
43 setPayload (
44 payload /* : any */
45 ) /* : BmResponse */ {
46 internal(this).payload = payload
47 return this
48 }
49
50 setStatusCode (
51 code /* : number */
52 ) /* : BmResponse */ {
53 internal(this).statusCode = code
54 return this
55 }
56}
57
58module.exports = BmResponse
59
60},{"./utils/internal.js":3}],2:[function(require,module,exports){
61/* @flow */
62'use strict'
63
64/* ::
65import type {
66 Handler,
67 BmRequest,
68 RouteConfiguration
69} from '../types.js'
70*/
71
72const uniloc = require('uniloc')
73
74const BmResponse = require('../lib/bm-response.js')
75
76function executeHandler (
77 handler /* : Handler */,
78 request /* : BmRequest */
79) /* : Promise<BmResponse> */ {
80 const response = new BmResponse()
81 return Promise.resolve()
82 .then(() => handler(request, response))
83 .then((result) => {
84 // If a result has been returned:
85 // try and set status code or
86 // try and set payload
87 if (result && result !== response) {
88 if (Number.isFinite(result)) {
89 response.setStatusCode(result)
90 } else {
91 response.setPayload(result)
92 }
93 }
94 return response
95 })
96}
97
98function getHandler (
99 module /* : string */,
100 method /* : string */
101) /* : Promise<Handler | void> */ {
102 try {
103 // $FlowIssue in this case, we explicitly `require()` dynamically
104 let handler = require(module)
105 if (handler && method && typeof handler[method] === 'function') {
106 handler = handler[method]
107 }
108 return Promise.resolve(handler)
109 } catch (err) {
110 return Promise.reject(err)
111 }
112}
113
114function findRouteConfig (
115 route /* : string */,
116 routeConfigs /* : RouteConfiguration[] */
117) /* : RouteConfiguration */ {
118 const unilocRoutes = routeConfigs.reduce((memo, r) => {
119 memo[r.route] = `GET ${r.route.replace(/{/g, ':').replace(/}/g, '')}`
120 return memo
121 }, {})
122 const unilocRouter = uniloc(unilocRoutes)
123 const unilocRoute = unilocRouter.lookup(route, 'GET')
124
125 const routeConfig = routeConfigs.find((routeConfig) => routeConfig.route === unilocRoute.name)
126 if (!routeConfig) {
127 throw new Error(`Route has not been implemented: ${route}`)
128 }
129
130 routeConfig.params = unilocRoute.options
131 return routeConfig
132}
133
134module.exports = {
135 findRouteConfig,
136 executeHandler,
137 getHandler
138}
139
140},{"../lib/bm-response.js":1,"uniloc":5}],3:[function(require,module,exports){
141/* @flow */
142
143/* :: import type { MapObject } from '../../types.js' */
144
145// https://developer.mozilla.org/en-US/Add-ons/SDK/Guides/Contributor_s_Guide/Private_Properties
146
147// simpler than alternative: https://www.npmjs.com/package/namespace
148
149function createInternal () {
150 const map /* : WeakMap<Object, MapObject> */ = new WeakMap()
151 return (object /* : Object */) /* : MapObject */ => {
152 const values = map.get(object) || {}
153 if (!map.has(object)) {
154 map.set(object, values)
155 }
156 return values
157 }
158}
159
160module.exports = {
161 createInternal
162}
163
164},{}],4:[function(require,module,exports){
165/* @flow */
166'use strict'
167
168/* ::
169import type {
170 BmRequest,
171 Headers,
172 MapObject,
173 Protocol
174} from '../types'
175*/
176
177function keysToLowerCase (
178 object /* : MapObject */
179) /* : MapObject */ {
180 return Object.keys(object).reduce((result, key) => {
181 result[key.toLowerCase()] = object[key]
182 return result
183 }, {})
184}
185
186function normaliseMethod (
187 method /* : string */
188) /* : string */ {
189 return method.toLowerCase()
190}
191
192/**
193https://www.w3.org/TR/url-1/#dom-urlutils-protocol
194protocol ends with ':', same as in Node.js 'url' module
195https://en.wikipedia.org/wiki/List_of_HTTP_header_fields
196*/
197function protocolFromHeaders (
198 headers /* : Headers */
199) /* : Protocol */ {
200 if (headers['x-forwarded-proto'] === 'https') {
201 return `https:`
202 }
203 if (headers.forwarded && ~headers.forwarded.indexOf('proto=https')) {
204 return `https:`
205 }
206 if (headers['front-end-https'] === 'on') {
207 return `https:`
208 }
209 return 'http:'
210}
211
212module.exports = {
213 keysToLowerCase,
214 normaliseMethod,
215 protocolFromHeaders
216}
217
218},{}],5:[function(require,module,exports){
219(function(root) {
220 function assert(condition, format) {
221 if (!condition) {
222 var args = [].slice.call(arguments, 2);
223 var argIndex = 0;
224 throw new Error(
225 'Unirouter Assertion Failed: ' +
226 format.replace(/%s/g, function() { return args[argIndex++]; })
227 );
228 }
229 }
230
231 function pathParts(path) {
232 return path == '' ? [] : path.split('/')
233 }
234
235 function routeParts(route) {
236 var split = route.split(/\s+/)
237 var method = split[0]
238 var path = split[1]
239
240 // Validate route format
241 assert(
242 split.length == 2,
243 "Route `%s` separates method and path with a single block of whitespace", route
244 )
245
246 // Validate method format
247 assert(
248 /^[A-Z]+$/.test(method),
249 "Route `%s` starts with an UPPERCASE method", route
250 )
251
252 // Validate path format
253 assert(
254 !/\/{2,}/.test(path),
255 "Path `%s` has no adjacent `/` characters: `%s`", path
256 )
257 assert(
258 path[0] == '/',
259 "Path `%s` must start with the `/` character", path
260 )
261 assert(
262 path == '/' || !/\/$/.test(path),
263 "Path `%s` does not end with the `/` character", path
264 )
265 assert(
266 path.indexOf('#') === -1 && path.indexOf('?') === -1,
267 "Path `%s` does not contain the `#` or `?` characters", path
268 )
269
270 return pathParts(path.slice(1)).concat(method)
271 }
272
273
274 function LookupTree() {
275 this.tree = {}
276 }
277
278 function lookupTreeReducer(tree, part) {
279 return tree && (tree[part] || tree[':'])
280 }
281
282 LookupTree.prototype.find = function(parts) {
283 return (parts.reduce(lookupTreeReducer, this.tree) || {})['']
284 }
285
286 LookupTree.prototype.add = function(parts, route) {
287 var i, branch
288 var branches = parts.map(function(part) { return part[0] == ':' ? ':' : part })
289 var currentTree = this.tree
290
291 for (i = 0; i < branches.length; i++) {
292 branch = branches[i]
293 if (!currentTree[branch]) {
294 currentTree[branch] = {}
295 }
296 currentTree = currentTree[branch]
297 }
298
299 assert(
300 !currentTree[branch],
301 "Path `%s` conflicts with another path", parts.join('/')
302 )
303
304 currentTree[''] = route
305 }
306
307
308 function createRouter(routes, aliases) {
309 var parts, name, route;
310 var routesParams = {};
311 var lookupTree = new LookupTree;
312
313 // By default, there are no aliases
314 aliases = aliases || {};
315
316 // Copy routes into lookup tree
317 for (name in routes) {
318 if (routes.hasOwnProperty(name)) {
319 route = routes[name]
320
321 assert(
322 typeof route == 'string',
323 "Route '%s' must be a string", name
324 )
325 assert(
326 name.indexOf('.') == -1,
327 "Route names must not contain the '.' character", name
328 )
329
330 parts = routeParts(route)
331
332 routesParams[name] = parts
333 .map(function(part, i) { return part[0] == ':' && [part.substr(1), i] })
334 .filter(function(x) { return x })
335
336 lookupTree.add(parts, name)
337 }
338 }
339
340 // Copy aliases into lookup tree
341 for (route in aliases) {
342 if (aliases.hasOwnProperty(route)) {
343 name = aliases[route]
344
345 assert(
346 routes[name],
347 "Alias from '%s' to non-existent route '%s'.", route, name
348 )
349
350 lookupTree.add(routeParts(route), name);
351 }
352 }
353
354
355 return {
356 lookup: function(uri, method) {
357 method = method ? method.toUpperCase() : 'GET'
358
359 var i, x
360
361 var split = uri
362 // Strip leading and trailing '/' (at end or before query string)
363 .replace(/^\/|\/($|\?)/g, '')
364 // Strip fragment identifiers
365 .replace(/#.*$/, '')
366 .split('?', 2)
367
368 var parts = pathParts(split[0]).map(decodeURIComponent).concat(method)
369 var name = lookupTree.find(parts)
370 var options = {}
371 var params, queryParts
372
373 params = routesParams[name] || []
374 queryParts = split[1] ? split[1].split('&') : []
375
376 for (i = 0; i != queryParts.length; i++) {
377 x = queryParts[i].split('=')
378 options[x[0]] = decodeURIComponent(x[1])
379 }
380
381 // Named parameters overwrite query parameters
382 for (i = 0; i != params.length; i++) {
383 x = params[i]
384 options[x[0]] = parts[x[1]]
385 }
386
387 return {name: name, options: options}
388 },
389
390
391 generate: function(name, options) {
392 options = options || {}
393
394 var params = routesParams[name] || []
395 var paramNames = params.map(function(x) { return x[0]; })
396 var route = routes[name]
397 var query = []
398 var inject = []
399 var key
400
401 assert(route, "No route with name `%s` exists", name)
402
403 var path = route.split(' ')[1]
404
405 for (key in options) {
406 if (options.hasOwnProperty(key)) {
407 if (paramNames.indexOf(key) === -1) {
408 assert(
409 /^[a-zA-Z0-9-_]+$/.test(key),
410 "Non-route parameters must use only the following characters: A-Z, a-z, 0-9, -, _"
411 )
412
413 query.push(key+'='+encodeURIComponent(options[key]))
414 }
415 else {
416 inject.push(key)
417 }
418 }
419 }
420
421 assert(
422 inject.sort().join() == paramNames.slice(0).sort().join(),
423 "You must specify options for all route params when using `uri`."
424 )
425
426 var uri =
427 paramNames.reduce(function pathReducer(injected, key) {
428 return injected.replace(':'+key, encodeURIComponent(options[key]))
429 }, path)
430
431 if (query.length) {
432 uri += '?' + query.join('&')
433 }
434
435 return uri
436 }
437 };
438 }
439
440
441 if (typeof module !== 'undefined' && module.exports) {
442 module.exports = createRouter
443 }
444 else {
445 root.unirouter = createRouter
446 }
447})(this);
448
449},{}],6:[function(require,module,exports){
450/**
451This module exports a "handler" function,
452that wraps a customer function.
453We bundle this module and its dependencies to ../dist/wrapper.js .
454To bundle: `npm run build`
455*/
456/* @flow */
457'use strict'
458
459/* ::
460import type {
461 BmRequest,
462 Headers,
463 LambdaEvent
464} from '../types.js'
465*/
466
467const path = require('path')
468
469const handlers = require('../lib/handlers.js')
470const wrapper = require('../lib/wrapper.js')
471
472// return only the pertinent data from a API Gateway + Lambda event
473function normaliseLambdaRequest (
474 event /* : LambdaEvent */
475) /* : BmRequest */ {
476 const headers = wrapper.keysToLowerCase(event.headers)
477 let body = event.body
478 try {
479 body = JSON.parse(body)
480 } catch (e) {
481 // Do nothing...
482 }
483 const host = headers['x-forwarded-host'] || headers.host
484 return {
485 body,
486 headers,
487 method: wrapper.normaliseMethod(event.httpMethod),
488 route: event.path,
489 url: {
490 host,
491 hostname: host,
492 params: {},
493 pathname: event.path,
494 protocol: wrapper.protocolFromHeaders(headers),
495 query: event.queryStringParameters || {}
496 }
497 }
498}
499
500function handler (
501 event /* : LambdaEvent */,
502 context /* : any */,
503 cb /* : (error: null, response: {
504 body: string,
505 headers: Headers,
506 statusCode: number
507 }) => void */
508) /* : Promise<void> */ {
509 const startTime = Date.now()
510 const request = normaliseLambdaRequest(event)
511 const internalHeaders = {}
512 internalHeaders['Content-Type'] = 'application/json'
513 const finish = (statusCode, body, customHeaders) => {
514 const headers = Object.assign(internalHeaders, customHeaders)
515 const endTime = Date.now()
516 const requestTime = endTime - startTime
517 console.log('BLINKM_ANALYTICS_EVENT', JSON.stringify({ // eslint-disable-line no-console
518 request: {
519 method: request.method.toUpperCase(),
520 query: request.url.query,
521 port: 443,
522 path: request.route,
523 hostName: request.url.hostname,
524 params: request.url.params,
525 protocol: request.url.protocol
526 },
527 response: {
528 statusCode: statusCode
529 },
530 requestTime: {
531 startDateTime: new Date(startTime),
532 startTimeStamp: startTime,
533 endDateTime: new Date(endTime),
534 endTimeStamp: endTime,
535 ms: requestTime,
536 s: requestTime / 1000
537 }
538 }, null, 2))
539 cb(null, {
540 body: JSON.stringify(body, null, 2),
541 headers: wrapper.keysToLowerCase(headers),
542 statusCode: statusCode
543 })
544 }
545
546 return Promise.resolve()
547 // $FlowFixMe requiring file without string literal to accomodate for __dirname
548 .then(() => require(path.join(__dirname, 'bm-server.json')))
549 .then((config) => {
550 // Get handler module based on route
551 let routeConfig
552 try {
553 routeConfig = handlers.findRouteConfig(event.path, config.routes)
554 request.url.params = routeConfig.params || {}
555 request.route = routeConfig.route
556 } catch (error) {
557 return finish(404, {
558 error: 'Not Found',
559 message: error.message,
560 statusCode: 404
561 })
562 }
563
564 // Check for browser requests and apply CORS if required
565 if (request.headers.origin) {
566 if (!config.cors) {
567 // No cors, we will return 405 result and let browser handler error
568 return finish(405, {
569 error: 'Method Not Allowed',
570 message: 'OPTIONS method has not been implemented',
571 statusCode: 405
572 })
573 }
574 if (!config.cors.origins.some((origin) => origin === '*' || origin === request.headers.origin)) {
575 // Invalid origin, we will return 200 result and let browser handler error
576 return finish(200)
577 }
578 // Headers for all cross origin requests
579 internalHeaders['Access-Control-Allow-Origin'] = request.headers.origin
580 internalHeaders['Access-Control-Expose-Headers'] = config.cors.exposedHeaders.join(',')
581 // Headers for OPTIONS cross origin requests
582 if (request.method === 'options' && request.headers['access-control-request-method']) {
583 internalHeaders['Access-Control-Allow-Headers'] = config.cors.headers.join(',')
584 internalHeaders['Access-Control-Allow-Methods'] = request.headers['access-control-request-method']
585 internalHeaders['Access-Control-Max-Age'] = config.cors.maxAge
586 }
587 // Only set credentials header if truthy
588 if (config.cors.credentials) {
589 internalHeaders['Access-Control-Allow-Credentials'] = true
590 }
591 }
592 if (request.method === 'options') {
593 // For OPTIONS requests, we can just finish
594 // as we have created our own implementation of CORS
595 return finish(200)
596 }
597
598 // Change current working directory to the project
599 // to accomadate for packages using process.cwd()
600 const projectPath = path.join(__dirname, 'project')
601 if (process.cwd() !== projectPath) {
602 try {
603 process.chdir(projectPath)
604 } catch (err) {
605 return Promise.reject(new Error(`Could not change current working directory to '${projectPath}': ${err}`))
606 }
607 }
608
609 return handlers.getHandler(path.join(__dirname, routeConfig.module), request.method)
610 .then((handler) => {
611 if (typeof handler !== 'function') {
612 return finish(405, {
613 error: 'Method Not Allowed',
614 message: `${request.method.toUpperCase()} method has not been implemented`,
615 statusCode: 405
616 })
617 }
618
619 return handlers.executeHandler(handler, request)
620 .then((response) => finish(response.statusCode, response.payload, response.headers))
621 })
622 })
623 .catch((error) => {
624 if (error && error.stack) {
625 console.error(error.stack) // eslint-disable-line no-console
626 }
627 if (error && error.isBoom && error.output && error.output.payload && error.output.statusCode) {
628 if (error.data) {
629 console.error('Boom Data: ', JSON.stringify(error.data, null, 2)) // eslint-disable-line no-console
630 }
631 return finish(error.output.statusCode, error.output.payload, error.output.headers)
632 }
633 finish(500, {
634 error: 'Internal Server Error',
635 message: 'An internal server error occurred',
636 statusCode: 500
637 })
638 })
639}
640
641module.exports = {
642 handler,
643 normaliseLambdaRequest
644}
645
646},{"../lib/handlers.js":2,"../lib/wrapper.js":4,"path":undefined}]},{},[6])(6)
647});
\No newline at end of file