Permalink
Browse files

decouple API, request, and response to remove variable cross-contamin…

…ation
  • Loading branch information...
jeremydaly committed May 17, 2018
1 parent bc3df07 commit ed1f1b87a3bf631797313d39be570928c758ad39
Showing with 60 additions and 47 deletions.
  1. +23 −25 index.js
  2. +23 −13 lib/request.js
  3. +14 −9 lib/response.js
@@ -27,10 +27,6 @@ class API {
// Prefix stack w/ base
this._prefix = this.parseRoute(this._base)

// Stores timers for debugging
this._timers = {}
this._procTimes = {}

// Stores route mappings
this._routes = {}

@@ -136,45 +132,47 @@ class API {
this._event = event
this._context = context
this._cb = cb
this._done = false
this._error = false

// Initalize request and response objects
let request = new REQUEST(this)
let response = new RESPONSE(this,request)

try {
// Initalize response and request objects
this.response = new RESPONSE(this)
this.request = new REQUEST(this)

// Parse the request
request.parseRequest()

// Loop through the middleware and await response
for (const mw of this._middleware) {
if (this._done || this._error) break
if (response._state !== 'processing') break
// Promisify middleware
await new Promise(r => { mw(this.request,this.response,() => { r() }) })
await new Promise(r => { mw(request,response,() => { r() }) })
} // end for

// Execute the primary handler
if (!this._done && !this._error) await this.handler(this.request,this.response)
if (response._state === 'processing') await request._handler(request,response)

} catch(e) {
this.catchErrors(e)
this.catchErrors(e,response)
}

} // end run function



// Catch all async/sync errors
async catchErrors(e) {
async catchErrors(e,response) {

// Error messages should never be base64 encoded
this.response._isBase64 = false
response._isBase64 = false

// Strip the headers (TODO: find a better way to handle this)
this.response._headers = {}
response._headers = {}

let message;

if (e instanceof Error) {
this.response.status(this._errorStatus)
response.status(this._errorStatus)
message = e.message
!this._test && console.log(e)
} else {
@@ -183,34 +181,34 @@ class API {
}

// If first time through, process error middleware
if (!this._error) {
if (response._state === 'processing') {

// Flag error state (this will avoid infinite error loops)
this._error = true
response._state === 'error'

// Execute error middleware
for (const err of this._errors) {
if (this._done) break
if (response._state === 'done') break
// Promisify error middleware
await new Promise(r => { err(e,this.request,this.response,() => { r() }) })
await new Promise(r => { err(e,response._request,response,() => { r() }) })
} // end for
}

// Throw standard error unless callback has already been executed
if (!this._done) this.response.json({'error':message})
if (response._state !== 'done') response.json({'error':message})

} // end catch



// Custom callback
async _callback(err, res) {
async _callback(err, res, response) {

// Set done status
this._done = true
response._state = 'done'

// Execute finally
await this._finally(this.request,this.response)
await this._finally(response._request,response)

// Execute the primary callback
this._cb(err,res)
@@ -17,6 +17,9 @@ class REQUEST {
// Create a reference to the app
this.app = app

// Init and default the handler
this._handler = function() { console.log('No handler specified') }

// Expose Namespaces
this.namespace = this.ns = app._app

@@ -26,17 +29,24 @@ class REQUEST {
// Init the params
this.params = {}

this.headers = {}

} // end constructor

// Parse the request
parseRequest() {

// Set the method
this.method = app._event.httpMethod.toUpperCase()
this.method = this.app._event.httpMethod.toUpperCase()

// Set the path
this.path = app._event.path
this.path = this.app._event.path

// Set the query parameters
this.query = app._event.queryStringParameters ? app._event.queryStringParameters : {}
this.query = this.app._event.queryStringParameters ? this.app._event.queryStringParameters : {}

// Set the headers
this.rawHeaders = app._event.headers
this.rawHeaders = this.app._event.headers

this.headers = Object.keys(this.rawHeaders).reduce((acc,header) =>
Object.assign(acc,{[header.toLowerCase()]:this.rawHeaders[header]}), {})
@@ -56,13 +66,13 @@ class REQUEST {
this.auth = UTILS.parseAuth(this.headers.authorization)

// Set the requestContext
this.requestContext = app._event.requestContext
this.requestContext = this.app._event.requestContext

// Capture the raw body
this.rawBody = app._event.body
this.rawBody = this.app._event.body

// Set the body (decode it if base64 encoded)
this.body = app._event.isBase64Encoded ? Buffer.from(app._event.body, 'base64').toString() : app._event.body
this.body = this.app._event.isBase64Encoded ? Buffer.from(this.app._event.body, 'base64').toString() : this.app._event.body

// Set the body
if (this.headers['content-type'] && this.headers['content-type'].includes("application/x-www-form-urlencoded")) {
@@ -74,13 +84,13 @@ class REQUEST {
}

// Extract path from event (strip querystring just in case)
let path = app._event.path.trim().split('?')[0].replace(/^\/(.*?)(\/)*$/,'$1').split('/')
let path = this.app._event.path.trim().split('?')[0].replace(/^\/(.*?)(\/)*$/,'$1').split('/')

// Init the route
this.route = null

// Create a local routes reference
let routes = app._routes
let routes = this.app._routes

let wildcard = {}

@@ -96,7 +106,7 @@ class REQUEST {
} else if (routes['__VAR__']) {
routes = routes['__VAR__']
} else {
app._errorStatus = 404
this.app._errorStatus = 404
throw new Error('Route not found')
}
} // end for loop
@@ -121,14 +131,14 @@ class REQUEST {
this.route = route.route

// Set the handler
app.handler = route.handler
this._handler = route.handler

} else {
app._errorStatus = 405
this.app._errorStatus = 405
throw new Error('Method not allowed')
}

} // end constructor
} // end parseRequest

} // end REQUEST class

@@ -14,11 +14,17 @@ const path = require('path') // Require Node.js path
class RESPONSE {

// Create the constructor function.
constructor(app) {
constructor(app,request) {

// Create a reference to the app
this.app = app

// Create a reference to the request
this._request = request

// Set the default state to processing
this._state = 'processing'

// Default statusCode to 200
this._statusCode = 200

@@ -363,18 +369,17 @@ class RESPONSE {

// Generate Etag
if ( this._etag // if etag support enabled
&& this.app.request // if request exists
&& ['GET','HEAD'].includes(this.app.request.method)
&& ['GET','HEAD'].includes(this._request.method)
&& !this.hasHeader('etag')
&& this._statusCode === 200
) {
this.header('etag','"'+UTILS.generateEtag(body)+'"')
}

// Check for matching Etag
if ( this.app.request
&& this.app.request.headers['if-none-match']
&& this.app.request.headers['if-none-match'] === this.getHeader('etag')
if (
this._request.headers['if-none-match']
&& this._request.headers['if-none-match'] === this.getHeader('etag')
) {
this.status(304)
body = ''
@@ -384,19 +389,19 @@ class RESPONSE {
const response = {
headers: this._headers,
statusCode: this._statusCode,
body: this.app.request && this.app.request.method === 'HEAD' ? '' : UTILS.encodeBody(body),
body: this._request.method === 'HEAD' ? '' : UTILS.encodeBody(body),
isBase64Encoded: this._isBase64
}

// Trigger the callback function
this.app._callback(null, response)
this.app._callback(null, response, this)

} // end send


// Trigger API error
error(e) {
this.app.catchErrors(e)
this.app.catchErrors(e,this)
} // end error

} // end Response class

0 comments on commit ed1f1b8

Please sign in to comment.