From d145fbcd3326c50f50647a5fda53c39fff58c5b0 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Wed, 14 Mar 2018 12:32:56 -0400 Subject: [PATCH 01/14] fix whitespace and add app reference --- request.js | 184 +++++++++++++++++++++++++++-------------------------- 1 file changed, 94 insertions(+), 90 deletions(-) diff --git a/request.js b/request.js index 119e110..571f87d 100644 --- a/request.js +++ b/request.js @@ -5,96 +5,100 @@ const QS = require('querystring') // Require the querystring library class REQUEST { // Create the constructor function. - constructor(app) { - - // Set the version - this.version = app._version - - // Init the params - this.params = {} - - // Set the method - this.method = app._event.httpMethod.toUpperCase() - - // Set the path - this.path = app._event.path - - // Set the query parameters - this.query = app._event.queryStringParameters ? app._event.queryStringParameters : {} - - // Set the headers - this.headers = app._event.headers - - // Set the requestContext - this.requestContext = app._event.requestContext - - // Set the body - if (this.headers['Content-Type'] && this.headers['Content-Type'].includes("application/x-www-form-urlencoded")) { - this.body = QS.parse(app._event.body) - } else if (typeof app._event.body === 'object') { - this.body = app._event.body - } else { - try { - this.body = JSON.parse(app._event.body) - } catch(e) { - this.body = app._event.body; - } - } - - // Extract path from event (strip querystring just in case) - let path = app._event.path.trim().split('?')[0].replace(/^\/(.*?)(\/)*$/,'$1').split('/') - - // Remove base if it exists - if (app._base && app._base === path[0]) { - path.shift() - } // end remove base - - // Init the route - this.route = null - - // Create a local routes reference - let routes = app._routes - - // Loop the routes and see if this matches - for (let i=0; i Date: Wed, 14 Mar 2018 12:33:44 -0400 Subject: [PATCH 02/14] fix whitespace --- response.js | 80 ++++++++++++++++++++++++++--------------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/response.js b/response.js index 0a35df0..bab6692 100644 --- a/response.js +++ b/response.js @@ -3,22 +3,22 @@ class RESPONSE { // Create the constructor function. - constructor(app) { + constructor(app) { - // Create a local copy of the app + // Create a reference to the app this.app = app - // Default statusCode to 200 + // Default statusCode to 200 this._statusCode = 200 - // Default the header + // Default the header this._headers = { - // Set the Content-Type by default + // Set the Content-Type by default "Content-Type": "application/json" //charset=UTF-8 } } - // Sets the statusCode + // Sets the statusCode status(code) { this._statusCode = code return this @@ -30,52 +30,52 @@ class RESPONSE { return this } - // Convenience method for JSON - json(body) { - this.header('Content-Type','application/json').send(JSON.stringify(body)) - } - - // TODO: Convenience method for JSONP - // jsonp(body) { - // this.header('Content-Type','application/json').send(JSON.stringify(body)) - // } - - // Convenience method for HTML - html(body) { - this.header('Content-Type','text/html').send(body) - } - - // TODO: cookie - // TODO: clearCookie - // TODO: attachement - // TODO: download - // TODO: location - // TODO: redirect - // TODO: sendFile - // TODO: sendStatus - // TODO: type - - - // Sends the request to the main callback + // Convenience method for JSON + json(body) { + this.header('Content-Type','application/json').send(JSON.stringify(body)) + } + + // TODO: Convenience method for JSONP + // jsonp(body) { + // this.header('Content-Type','application/json').send(JSON.stringify(body)) + // } + + // Convenience method for HTML + html(body) { + this.header('Content-Type','text/html').send(body) + } + + // TODO: cookie + // TODO: clearCookie + // TODO: attachement + // TODO: download + // TODO: location + // TODO: redirect + // TODO: sendFile + // TODO: sendStatus + // TODO: type + + + // Sends the request to the main callback send(body) { - // Create the response + // Create the response const response = { headers: this._headers, statusCode: this._statusCode, body: typeof body === 'object' ? JSON.stringify(body) : (body && typeof body !== 'string' ? body.toString() : (body ? body : '')) } - // Trigger the callback function + // Trigger the callback function return this.app._callback(null, response) } // end send - // Trigger API error - error(err) { - // Reject promise - this.app._reject(err) - } // end error + // Trigger API error + error(err) { + // Reject promise + this.app._reject(err) + } // end error } // end Response class From e280d465373e0beb6931d5e8c66742eb3cffa412 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Wed, 14 Mar 2018 12:34:30 -0400 Subject: [PATCH 03/14] fix whitespace --- test/errorHandling.js | 98 +++++++++++++++++++++---------------------- test/headers.js | 58 ++++++++++++------------- test/middleware.js | 60 +++++++++++++------------- test/modules.js | 78 +++++++++++++++++----------------- test/responses.js | 98 +++++++++++++++++++++---------------------- 5 files changed, 196 insertions(+), 196 deletions(-) diff --git a/test/errorHandling.js b/test/errorHandling.js index 6d6864d..374ff6a 100644 --- a/test/errorHandling.js +++ b/test/errorHandling.js @@ -93,54 +93,54 @@ api.get('/testErrorPromise', function(req,res) { describe('Error Handling Tests:', function() { - it('Called Error', function() { - let _event = Object.assign({},event,{ path: '/testError'}) - - return new Promise((resolve,reject) => { - api.run(_event,{},function(err,res) { resolve(res) }) - }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 500, body: '{"error":"This is a test error message"}' }) - }) - }) // end it - - it('Thrown Error', function() { - let _event = Object.assign({},event,{ path: '/testErrorThrow'}) - - return new Promise((resolve,reject) => { - api.run(_event,{},function(err,res) { resolve(res) }) - }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 500, body: '{"error":"This is a test thrown error"}' }) - }) - }) // end it - - it('Simulated Error', function() { - let _event = Object.assign({},event,{ path: '/testErrorSimulated'}) - - return new Promise((resolve,reject) => { - api.run(_event,{},function(err,res) { resolve(res) }) - }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 405, body: '{"error":"This is a simulated error"}' }) - }) - }) // end it - - it('Error Middleware', function() { - let _event = Object.assign({},event,{ path: '/testErrorMiddleware'}) - - return new Promise((resolve,reject) => { - api.run(_event,{},function(err,res) { resolve(res) }) - }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/plain' }, statusCode: 500, body: 'This is a test error message: 123/456' }) - }) - }) // end it - - it('Error Middleware w/ Promise', function() { - let _event = Object.assign({},event,{ path: '/testErrorPromise'}) - - return new Promise((resolve,reject) => { - api.run(_event,{},function(err,res) { resolve(res) }) - }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/plain' }, statusCode: 500, body: 'This is a test error message: 123/456' }) - }) - }) // end it + it('Called Error', function() { + let _event = Object.assign({},event,{ path: '/testError'}) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 500, body: '{"error":"This is a test error message"}' }) + }) + }) // end it + + it('Thrown Error', function() { + let _event = Object.assign({},event,{ path: '/testErrorThrow'}) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 500, body: '{"error":"This is a test thrown error"}' }) + }) + }) // end it + + it('Simulated Error', function() { + let _event = Object.assign({},event,{ path: '/testErrorSimulated'}) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 405, body: '{"error":"This is a simulated error"}' }) + }) + }) // end it + + it('Error Middleware', function() { + let _event = Object.assign({},event,{ path: '/testErrorMiddleware'}) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/plain' }, statusCode: 500, body: 'This is a test error message: 123/456' }) + }) + }) // end it + + it('Error Middleware w/ Promise', function() { + let _event = Object.assign({},event,{ path: '/testErrorPromise'}) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/plain' }, statusCode: 500, body: 'This is a test error message: 123/456' }) + }) + }) // end it }) // end ERROR HANDLING tests diff --git a/test/headers.js b/test/headers.js index 44b9326..a806c02 100644 --- a/test/headers.js +++ b/test/headers.js @@ -47,34 +47,34 @@ api.get('/testJSONP', function(req,res) { describe('Header Tests:', function() { - it('New Header: /test -- test: testVal', function() { - let _event = Object.assign({},event,{}) - - return new Promise((resolve,reject) => { - api.run(_event,{},function(err,res) { resolve(res) }) - }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json', 'test': 'testVal' }, statusCode: 200, body: '{"method":"get","status":"ok"}' }) - }) - }) // end it - - it('Override Header: /testOveride -- Content-Type: text/html', function() { - let _event = Object.assign({},event,{ path: '/testOverride'}) - - return new Promise((resolve,reject) => { - api.run(_event,{},function(err,res) { resolve(res) }) - }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/html' }, statusCode: 200, body: '
testHTML
' }) - }) - }) // end it - - it('HTML Convenience Method: /testHTML', function() { - let _event = Object.assign({},event,{ path: '/testHTML'}) - - return new Promise((resolve,reject) => { - api.run(_event,{},function(err,res) { resolve(res) }) - }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/html' }, statusCode: 200, body: '
testHTML
' }) - }) - }) // end it + it('New Header: /test -- test: testVal', function() { + let _event = Object.assign({},event,{}) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json', 'test': 'testVal' }, statusCode: 200, body: '{"method":"get","status":"ok"}' }) + }) + }) // end it + + it('Override Header: /testOveride -- Content-Type: text/html', function() { + let _event = Object.assign({},event,{ path: '/testOverride'}) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/html' }, statusCode: 200, body: '
testHTML
' }) + }) + }) // end it + + it('HTML Convenience Method: /testHTML', function() { + let _event = Object.assign({},event,{ path: '/testHTML'}) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/html' }, statusCode: 200, body: '
testHTML
' }) + }) + }) // end it }) // end HEADER tests diff --git a/test/middleware.js b/test/middleware.js index a6dcb3b..24d1db0 100644 --- a/test/middleware.js +++ b/test/middleware.js @@ -77,35 +77,35 @@ api.get('/testPromise', function(req,res) { describe('Middleware Tests:', function() { - it('Set Values in res object', function() { - let _event = Object.assign({},event,{}) - - return new Promise((resolve,reject) => { - api.run(_event,{},function(err,res) { resolve(res) }) - }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","testMiddleware":"123","testMiddleware2":"456"}' }) - }) - }) // end it - - it('Access params, querystring, and body values', function() { - let _event = Object.assign({},event,{ httpMethod: 'post', path: '/test/123', queryStringParameters: { test: "456" }, body: { test: "789" } }) - - return new Promise((resolve,reject) => { - api.run(_event,{},function(err,res) { resolve(res) }) - }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","testMiddleware3":"123","testMiddleware4":"456","testMiddleware5":"789"}' }) - }) - }) // end it - - - it('Middleware with Promise/Delay', function() { - let _event = Object.assign({},event,{ path: '/testPromise'}) - - return new Promise((resolve,reject) => { - api.run(_event,{},function(err,res) { resolve(res) }) - }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","testMiddlewarePromise":"test"}' }) - }) - }) // end it + it('Set Values in res object', function() { + let _event = Object.assign({},event,{}) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","testMiddleware":"123","testMiddleware2":"456"}' }) + }) + }) // end it + + it('Access params, querystring, and body values', function() { + let _event = Object.assign({},event,{ httpMethod: 'post', path: '/test/123', queryStringParameters: { test: "456" }, body: { test: "789" } }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","testMiddleware3":"123","testMiddleware4":"456","testMiddleware5":"789"}' }) + }) + }) // end it + + + it('Middleware with Promise/Delay', function() { + let _event = Object.assign({},event,{ path: '/testPromise'}) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","testMiddlewarePromise":"test"}' }) + }) + }) // end it }) // end MIDDLEWARE tests diff --git a/test/modules.js b/test/modules.js index 466f681..f301539 100644 --- a/test/modules.js +++ b/test/modules.js @@ -48,44 +48,44 @@ api.get('/testAppThrownError', function(req,res) { describe('Module Tests:', function() { - it('Standard module response', function() { - let _event = Object.assign({},event,{ path:'/testApp' }) - - return new Promise((resolve,reject) => { - api.run(_event,{},function(err,res) { resolve(res) }) - }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","app":"app1"}' }) - }) - }) // end it - - it('Module with promise', function() { - let _event = Object.assign({},event,{ path:'/testAppPromise' }) - - return new Promise((resolve,reject) => { - api.run(_event,{},function(err,res) { resolve(res) }) - }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","app":"app2"}' }) - }) - }) // end it - - it('Module with called error', function() { - let _event = Object.assign({},event,{ path:'/testAppError' }) - - return new Promise((resolve,reject) => { - api.run(_event,{},function(err,res) { resolve(res) }) - }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 500, body: '{"error":"This is a called module error"}' }) - }) - }) // end it - - it('Module with thrown error', function() { - let _event = Object.assign({},event,{ path:'/testAppThrownError' }) - - return new Promise((resolve,reject) => { - api.run(_event,{},function(err,res) { resolve(res) }) - }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 500, body: '{"error":"This is a thrown module error"}' }) - }) - }) // end it + it('Standard module response', function() { + let _event = Object.assign({},event,{ path:'/testApp' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","app":"app1"}' }) + }) + }) // end it + + it('Module with promise', function() { + let _event = Object.assign({},event,{ path:'/testAppPromise' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","app":"app2"}' }) + }) + }) // end it + + it('Module with called error', function() { + let _event = Object.assign({},event,{ path:'/testAppError' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 500, body: '{"error":"This is a called module error"}' }) + }) + }) // end it + + it('Module with thrown error', function() { + let _event = Object.assign({},event,{ path:'/testAppThrownError' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 500, body: '{"error":"This is a thrown module error"}' }) + }) + }) // end it }) // end MODULE tests diff --git a/test/responses.js b/test/responses.js index 9c47165..507a47a 100644 --- a/test/responses.js +++ b/test/responses.js @@ -49,54 +49,54 @@ api.get('/testEmptyResponse', function(req,res) { describe('Response Tests:', function() { - it('Object response: convert to string', function() { - let _event = Object.assign({},event,{ path: '/testObjectResponse'}) - - return new Promise((resolve,reject) => { - api.run(_event,{},function(err,res) { resolve(res) }) - }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"object":true}' }) - }) - }) // end it - - it('Numeric response: convert to string', function() { - let _event = Object.assign({},event,{ path: '/testNumberResponse'}) - - return new Promise((resolve,reject) => { - api.run(_event,{},function(err,res) { resolve(res) }) - }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '123' }) - }) - }) // end it - - it('Array response: convert to string', function() { - let _event = Object.assign({},event,{ path: '/testArrayResponse'}) - - return new Promise((resolve,reject) => { - api.run(_event,{},function(err,res) { resolve(res) }) - }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '[1,2,3]' }) - }) - }) // end it - - it('String response: no conversion', function() { - let _event = Object.assign({},event,{ path: '/testStringResponse'}) - - return new Promise((resolve,reject) => { - api.run(_event,{},function(err,res) { resolve(res) }) - }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: 'this is a string' }) - }) - }) // end it - - it('Empty response', function() { - let _event = Object.assign({},event,{ path: '/testEmptyResponse'}) - - return new Promise((resolve,reject) => { - api.run(_event,{},function(err,res) { resolve(res) }) - }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '' }) - }) - }) // end it + it('Object response: convert to string', function() { + let _event = Object.assign({},event,{ path: '/testObjectResponse'}) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"object":true}' }) + }) + }) // end it + + it('Numeric response: convert to string', function() { + let _event = Object.assign({},event,{ path: '/testNumberResponse'}) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '123' }) + }) + }) // end it + + it('Array response: convert to string', function() { + let _event = Object.assign({},event,{ path: '/testArrayResponse'}) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '[1,2,3]' }) + }) + }) // end it + + it('String response: no conversion', function() { + let _event = Object.assign({},event,{ path: '/testStringResponse'}) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: 'this is a string' }) + }) + }) // end it + + it('Empty response', function() { + let _event = Object.assign({},event,{ path: '/testEmptyResponse'}) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '' }) + }) + }) // end it }) // end ERROR HANDLING tests From 1a37cd7c8378ff67f10f64e59ccae341ec4ef01b Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Wed, 14 Mar 2018 16:39:10 -0400 Subject: [PATCH 04/14] add namespaces and required tests --- index.js | 356 +++++++++++++++++++++++---------------------- request.js | 3 + test/_testApp.js | 6 + test/_testData.js | 12 ++ test/namespaces.js | 83 +++++++++++ 5 files changed, 284 insertions(+), 176 deletions(-) create mode 100644 test/_testData.js create mode 100644 test/namespaces.js diff --git a/index.js b/index.js index fede7d2..48b3848 100644 --- a/index.js +++ b/index.js @@ -14,10 +14,10 @@ const Promise = require('bluebird') // Promise library // Create the API class class API { - // Create the constructor function. - constructor(props) { + // Create the constructor function. + constructor(props) { - // Set the version and base paths + // Set the version and base paths this._version = props.version ? props.version : 'v1' this._base = props.base ? props.base.trim() : '' @@ -28,34 +28,34 @@ class API { // Stores route mappings this._routes = {} - // Default callback - this._cb = function(err,res) { console.log('No callback specified') } + // Default callback + this._cb = function(err,res) { console.log('No callback specified') } - // Middleware stack - this._middleware = [] + // Middleware stack + this._middleware = [] - // Error middleware stack - this._errors = [] + // Error middleware stack + this._errors = [] - // Store app packages and namespaces - this._app = {} + // Store app packages and namespaces + this._app = {} - // Keep track of callback execution - this._done = false + // Keep track of callback execution + this._done = false - // Executed after the callback - this._finally = () => {} + // Executed after the callback + this._finally = () => {} - // Promise placeholder for final route promise resolution - this._promise = function() { console.log('no promise to resolve') } - this._reject = function() { console.log('no promise to reject') } + // Promise placeholder for final route promise resolution + this._promise = function() { console.log('no promise to resolve') } + this._reject = function() { console.log('no promise to reject') } - // Global error status - this._errorStatus = 500 + // Global error status + this._errorStatus = 500 - // Testing flag - this._test = false - } + // Testing flag + this._test = false + } // end constructor // GET: convenience method get(path, handler) { @@ -117,186 +117,185 @@ class API { // RUN: This runs the routes run(event,context,cb) { // TODO: Make this dynamic - this.startTimer('total') - - this._done = false - - // Set the event, context and callback - this._event = event - this._context = context - this._cb = cb - - // Initalize response object - let response = new RESPONSE(this) - let request = {} + this.startTimer('total') - Promise.try(() => { // Start a promise + this._done = false - // Initalize the request object - request = new REQUEST(this) + // Set the event, context and callback + this._event = event + this._context = context + this._cb = cb - // Execute the request - return this.execute(request,response) + // Initalize response object + let response = new RESPONSE(this) + let request = {} - }).catch((e) => { + Promise.try(() => { // Start a promise - let message; + // Initalize the request object + request = new REQUEST(this) - if (e instanceof Error) { - response.status(this._errorStatus) - message = e.message - !this._test && console.log(e) - } else { - message = e - !this._test && console.log('API Error:',e) - } + // Execute the request + return this.execute(request,response) - // Execute error middleware - if (this._errors.length > 0) { + }).catch((e) => { - // Init stack queue - let queue = [] + let message; - // Loop through the middleware and queue promises - for (let i in this._errors) { - queue.push(() => { - return new Promise((resolve, reject) => { - this._promise = () => { resolve() } // keep track of the last resolve() - this._reject = (e) => { reject(e) } // keep track of the last reject() - this._errors[i](e,request,response,() => { resolve() }) // execute the errors with the resolve callback - }) // end promise - }) // end queue - } // end for + if (e instanceof Error) { + response.status(this._errorStatus) + message = e.message + !this._test && console.log(e) + } else { + message = e + !this._test && console.log('API Error:',e) + } - // Return Promise.each serialially - return Promise.each(queue, function(queue_item) { - return queue_item() - }).then(() => { - response.json({'error':message}) - }) + // Execute error middleware + if (this._errors.length > 0) { + + // Init stack queue + let queue = [] + + // Loop through the middleware and queue promises + for (let i in this._errors) { + queue.push(() => { + return new Promise((resolve, reject) => { + this._promise = () => { resolve() } // keep track of the last resolve() + this._reject = (e) => { reject(e) } // keep track of the last reject() + this._errors[i](e,request,response,() => { resolve() }) // execute the errors with the resolve callback + }) // end promise + }) // end queue + } // end for + + // Return Promise.each serialially + return Promise.each(queue, function(queue_item) { + return queue_item() + }).then(() => { + response.json({'error':message}) + }) - } else { - response.json({'error':message}) - } + } else { + response.json({'error':message}) + } - }).finally(() => { - this._finally(request,response) - }) + }).finally(() => { + this._finally(request,response) + }) } // end run function - // Custom callback - _callback(err, res) { - - // Resolve any outstanding promise - this._promise() + // Custom callback + _callback(err, res) { - this._done = true + // Resolve any outstanding promise + this._promise() - //console.log(this); - this.endTimer('total') + this._done = true - if (res) { - if (this._debug) { - console.log(this._procTimes) - } - } + this.endTimer('total') - // Execute the primary callback - this._cb(err,res) + if (res) { + if (this._debug) { + console.log(this._procTimes) + } + } - } // end _callback + // Execute the primary callback + this._cb(err,res) + } // end _callback - // Middleware handler - use(fn) { - if (fn.length === 3) { - this._middleware.push(fn) - } else if (fn.length === 4) { - this._errors.push(fn) - } else { - throw new Error('Middleware must have 3 or 4 parameters') - } - } // end use - // Finally function - finally(fn) { - this._finally = fn - } + // Middleware handler + use(fn) { + if (fn.length === 3) { + this._middleware.push(fn) + } else if (fn.length === 4) { + this._errors.push(fn) + } else { + throw new Error('Middleware must have 3 or 4 parameters') + } + } // end use - // Process - execute(req,res) { + // Finally function + finally(fn) { + this._finally = fn + } - // Init stack queue - let queue = [] + // Process + execute(req,res) { + + // Init stack queue + let queue = [] + + // If execute is called after the app is done, just return out + if (this._done) { return; } + + // If there is middleware + if (this._middleware.length > 0) { + // Loop through the middleware and queue promises + for (let i in this._middleware) { + queue.push(() => { + return new Promise((resolve, reject) => { + this._promise = () => { resolve() } // keep track of the last resolve() + this._reject = (e) => { reject(e) } // keep track of the last reject() + this._middleware[i](req,res,() => { resolve() }) // execute the middleware with the resolve callback + }) // end promise + }) // end queue + } // end for + } // end if - // If execute is called after the app is done, just return out - if (this._done) { return; } + // Push the main execution path to the queue stack + queue.push(() => { + return new Promise((resolve, reject) => { + this._promise = () => { resolve() } // keep track of the last resolve() + this._reject = (e) => { reject(e) } // keep track of the last reject() + this.handler(req,res) // execute the handler with no callback + }) + }) - // If there is middleware - if (this._middleware.length > 0) { - // Loop through the middleware and queue promises - for (let i in this._middleware) { - queue.push(() => { - return new Promise((resolve, reject) => { - this._promise = () => { resolve() } // keep track of the last resolve() - this._reject = (e) => { reject(e) } // keep track of the last reject() - this._middleware[i](req,res,() => { resolve() }) // execute the middleware with the resolve callback - }) // end promise - }) // end queue - } // end for + // Return Promise.each serialially + return Promise.each(queue, function(queue_item) { + return queue_item() + }) - } // end if + } // end execute - // Push the main execution path to the queue stack - queue.push(() => { - return new Promise((resolve, reject) => { - this._promise = () => { resolve() } // keep track of the last resolve() - this._reject = (e) => { reject(e) } // keep track of the last reject() - this.handler(req,res) // execute the handler with no callback - }) - }) - // Return Promise.each serialially - return Promise.each(queue, function(queue_item) { - return queue_item() - }) - } // end execute + //-------------------------------------------------------------------------// + // TIMER FUNCTIONS + //-------------------------------------------------------------------------// + // Returns the calculated processing times from all stopped timers + getTimers(timer) { + if (timer) { + return this._procTimes[timer] + } else { + return this._procTimes + } + } // end getTimers + + // Starts a timer for debugging purposes + startTimer(name) { + this._timers[name] = Date.now() + } // end startTimer + + // Ends a timer and calculates the total processing time + endTimer(name) { + try { + this._procTimes[name] = (Date.now()-this._timers[name]) + ' ms' + delete this._timers[name] + } catch(e) { + console.error('Could not end timer: ' + name) + } + } // end endTimer //-------------------------------------------------------------------------// - //-------------------------------------------------------------------------// - // TIMER FUNCTIONS - //-------------------------------------------------------------------------// - //-------------------------------------------------------------------------// - - // Returns the calculated processing times from all stopped timers - getTimers(timer) { - - if (timer) { - return this._procTimes[timer] - } else { - return this._procTimes - } - } // end getTimers - - // Starts a timer for debugging purposes - startTimer(name) { - this._timers[name] = Date.now() - } // end startTimer - - // Ends a timer and calculates the total processing time - endTimer(name) { - try { - this._procTimes[name] = (Date.now()-this._timers[name]) + ' ms' - delete this._timers[name] - } catch(e) { - console.error('Could not end timer: ' + name) - } - } // end endTimer - + // UTILITY FUNCTIONS + //-------------------------------------------------------------------------// deepFind(obj, path) { let paths = path//.split('.'), @@ -330,11 +329,13 @@ class API { obj[path[0]] = Object.assign(value,obj[path[0]]) } } - } // end setDeepValue + } // end setRoute + - // Load app packages - app(packages) { - // Check for supplied packages + // Load app packages + app(packages) { + + // Check for supplied packages if (typeof packages === 'object') { // Loop through and set package namespaces for (let namespace in packages) { @@ -344,12 +345,15 @@ class API { console.error(e.message) } } - } // end if + } else if (arguments.length === 2 && typeof packages === 'string') { + this._app[packages] = arguments[1] + }// end if - // Return a reference - return this._app - } + // Return a reference + return this._app + } } // end API class +// Export the API class module.exports = API diff --git a/request.js b/request.js index 571f87d..9e99a70 100644 --- a/request.js +++ b/request.js @@ -10,6 +10,9 @@ class REQUEST { // Create a reference to the app this.app = app + // Expose Namespaces + this.namespace = this.ns = app._app + // Set the version this.version = app._version diff --git a/test/_testApp.js b/test/_testApp.js index fca0ad8..47b19ca 100644 --- a/test/_testApp.js +++ b/test/_testApp.js @@ -27,4 +27,10 @@ module.exports = { throw new Error('This is a thrown module error') }, + dataTest: function(req,res) { + // Use data namespace + let data = req.ns.data.dataCall() + res.json({ method:'get', status:'ok', data: data }) + }, + } // end exports diff --git a/test/_testData.js b/test/_testData.js new file mode 100644 index 0000000..daf4601 --- /dev/null +++ b/test/_testData.js @@ -0,0 +1,12 @@ +'use strict' + +module.exports = { + + dataCall: function() { + return { + "foo": "sample data", + "bar": "additional sample data" + } + } + +} // end exports diff --git a/test/namespaces.js b/test/namespaces.js new file mode 100644 index 0000000..1236675 --- /dev/null +++ b/test/namespaces.js @@ -0,0 +1,83 @@ +'use strict'; + +const Promise = require('bluebird') // Promise library +const expect = require('chai').expect // Assertion library +const API = require('../index') // API library + +// Init API instance +const api = new API({ version: 'v1.0', base: 'v1' }) + +// Add the data namespace +api.app('data',require('./_testData')) + +// Add additional namespaces using object +api.app({ + 'data2': require('./_testData'), + 'data3': require('./_testData') +}) + + +// NOTE: Set test to true +api._test = true; + +let event = { + httpMethod: 'get', + path: '/test', + body: {}, + headers: { + 'Content-Type': 'application/json' + } +} + +/******************************************************************************/ +/*** DEFINE TEST ROUTES ***/ +/******************************************************************************/ + +// This route invokes 'dataCall' using the 'data' namespace +api.get('/testData', function(req,res) { + let data = req.namespace.data.dataCall() + res.json({ method:'get', status:'ok', data: data }) +}) + +// This route loads a module directly which accesses the data namespace +api.get('/testAppData', require('./_testApp').dataTest) + + + +/******************************************************************************/ +/*** BEGIN TESTS ***/ +/******************************************************************************/ + +describe('Namespace Tests:', function() { + + it('Check namespace loading', function() { + expect(Object.keys(api._app).length).to.equal(3) + expect(api._app).to.have.property('data') + expect(api._app).to.have.property('data2') + expect(api._app).to.have.property('data3') + expect(api._app.data).to.have.property('dataCall') + }) // end it + + it('Invoke namespace', function() { + let _event = Object.assign({},event,{ path:'/testData' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + // console.log("RESULTS:",result); + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","data":{"foo":"sample data","bar":"additional sample data"}}' }) + }) + }) // end it + + it('Invoke namespace via required module', function() { + let _event = Object.assign({},event,{ path:'/testAppData' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + // console.log("RESULTS:",result); + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","data":{"foo":"sample data","bar":"additional sample data"}}' }) + }) + }) // end it + +}) // end MODULE tests From 3352c294f5cd228d841c4ab980629d2afea277a8 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Wed, 14 Mar 2018 17:05:51 -0400 Subject: [PATCH 05/14] add namespace documentation and other minor adjustments --- README.md | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6dbaa4c..54a0701 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,7 @@ api.METHOD('patch','/users', function(req,res) { The `REQUEST` object contains a parsed and normalized request from API Gateway. It contains the following values by default: +- `app`: A reference to an instance of the app - `version`: The version set at initialization - `params`: Dynamic path parameters parsed from the path (see [path parameters](#path-parameters)) - `method`: The HTTP method of the request @@ -145,6 +146,7 @@ The `REQUEST` object contains a parsed and normalized request from API Gateway. - Otherwise it will be plain text. - `route`: The matched route of the request - `requestContext`: The `requestContext` passed from the API Gateway +- `namespace` or `ns`: A reference to modules added to the app's namespace (see [namespaces](#namespaces)) The request object can be used to pass additional information through the processing chain. For example, if you are using a piece of authentication middleware, you can add additional keys to the `REQUEST` object with information about the user. See [middleware](#middleware) for more information. @@ -272,8 +274,45 @@ api.use(function(err,req,res,next) { The `next()` callback will cause the script to continue executing and eventually call the standard error handling function. You can short-circuit the default handler by calling a request ending method such as `send`, `html`, or `json`. +## Namespaces +Lambda API allows you to map specific modules to namespaces that can be accessed from the `REQUEST` object. This is helpful when using the pattern in which you create a module that exports middleware, error, or route functions. In the example below, the `data` namespace is added to app and then accessed by reference within an included module. + +The main handler file might look like this: + +```javascript +// Use app() function to add 'data' namespace +api.app('data',require('./lib/data.js')) + +// Create a get route to load user details +api.get('/users/:userId', require('./lib/users.js')) +``` + +The users.js module might look like this: + +```javascript +module.exports = function(req, res) { + let userInfo = req.namespace.data.getUser(req.params.userId) + res.json({ 'userInfo': userInfo }) +}); +``` + +By saving references in namespaces, you can access them without needing to require them in every module. Namespaces can be added using the `app()` method of the API. `app()` accepts either two parameters: a string representing the name of the namespace and a function reference, OR an object with string names as keys and function references as the values. For example: + +```javascript +api.app('namespace',require('./lib/ns-functions.js')) + +// OR + +api.app({ + 'namespace1': require('./lib/ns1-functions.js'), + 'namespace2': require('./lib/ns2-functions.js') +}) +``` + ## Promises -The API uses Bluebird promises to manage asynchronous script execution. Additional methods such as `async / await` or simple callbacks should be supported. The API will wait for a request ending call before returning data back to the client. Middleware will wait for the `next()` callback before proceeding to the next step. +The API uses Bluebird promises to manage asynchronous script execution. The API will wait for a request ending call before returning data back to the client. Middleware will wait for the `next()` callback before proceeding to the next step. + +**NOTE:** AWS Lambda currently only supports Node v6.10, which doesn't support `async / await`. If you'd like to use `async / await`, you'll need to polyfill. ## CORS Support CORS can be implemented using the [wildcard routes](#wildcard-routes) feature. A typical implementation would be as follows: From 72c638038f25221c1f0ba416ff312a2263485a49 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Thu, 15 Mar 2018 12:28:26 -0400 Subject: [PATCH 06/14] add util functions escapeHtml and encodeUrl w/ tests --- test/utils.js | 44 ++++++++++++++++++++++++++++++++++++++++++++ utils.js | 27 +++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 test/utils.js create mode 100644 utils.js diff --git a/test/utils.js b/test/utils.js new file mode 100644 index 0000000..0a6de24 --- /dev/null +++ b/test/utils.js @@ -0,0 +1,44 @@ +'use strict'; + +const Promise = require('bluebird') // Promise library +const expect = require('chai').expect // Assertion library + +const utils = require('../utils') + +/******************************************************************************/ +/*** BEGIN TESTS ***/ +/******************************************************************************/ + +describe('Utility Function Tests:', function() { + + describe('escapeHtml:', function() { + + it('Escape &, <, >, ", and \'', function() { + expect(utils.escapeHtml('&<>"\'')).to.equal('&<>"'') + }) // end it + + }) // end escapeHtml tests + + describe('encodeUrl:', function() { + + it('Unencoded with space in param', function() { + expect(utils.encodeUrl('http://www.github.com/?foo=bar with space')).to.equal('http://www.github.com/?foo=bar%20with%20space') + }) // end it + + it('Encoded URL with additional invalid sequence', function() { + expect(utils.encodeUrl('http://www.github.com/?foo=bar%20with%20space%foo')).to.equal('http://www.github.com/?foo=bar%20with%20space%25foo') + }) // end it + + it('Encode special characters, double encode, decode', function() { + let url = 'http://www.github.com/?foo=шеллы' + let encoded = utils.encodeUrl(url) + let doubleEncoded = utils.encodeUrl(encoded) + let decoded = decodeURI(encoded) + expect(encoded).to.equal('http://www.github.com/?foo=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B') + expect(doubleEncoded).to.equal('http://www.github.com/?foo=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B') + expect(decoded).to.equal(url) + }) // end it + + }) // end escapeHtml tests + +}) // end UTILITY tests diff --git a/utils.js b/utils.js new file mode 100644 index 0000000..b59ea37 --- /dev/null +++ b/utils.js @@ -0,0 +1,27 @@ +'use strict' + +/** + * Lightweight Node.js API for AWS Lambda + * @author Jeremy Daly + * @license MIT + */ + + const entityMap = { + "&": "&", + "<": "<", + ">": ">", + '"': '"', + "'": ''' + } + +module.exports.escapeHtml = html => html.replace(/[&<>"']/g, s => entityMap[s]) + + +// From encodeurl by Douglas Christopher Wilson +let ENCODE_CHARS_REGEXP = /(?:[^\x21\x25\x26-\x3B\x3D\x3F-\x5B\x5D\x5F\x61-\x7A\x7E]|%(?:[^0-9A-Fa-f]|[0-9A-Fa-f][^0-9A-Fa-f]|$))+/g +let UNMATCHED_SURROGATE_PAIR_REGEXP = /(^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF]([^\uDC00-\uDFFF]|$)/g +let UNMATCHED_SURROGATE_PAIR_REPLACE = '$1\uFFFD$2' + +module.exports.encodeUrl = url => String(url) + .replace(UNMATCHED_SURROGATE_PAIR_REGEXP, UNMATCHED_SURROGATE_PAIR_REPLACE) + .replace(ENCODE_CHARS_REGEXP, encodeURI) From 8f1cc3abdeacfdec1169e1ca7f499524bedea99b Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Thu, 15 Mar 2018 12:29:10 -0400 Subject: [PATCH 07/14] add slow() setting for promise tests --- test/errorHandling.js | 2 ++ test/middleware.js | 2 ++ test/modules.js | 2 ++ 3 files changed, 6 insertions(+) diff --git a/test/errorHandling.js b/test/errorHandling.js index 374ff6a..fd09ef7 100644 --- a/test/errorHandling.js +++ b/test/errorHandling.js @@ -93,6 +93,8 @@ api.get('/testErrorPromise', function(req,res) { describe('Error Handling Tests:', function() { + this.slow(300); + it('Called Error', function() { let _event = Object.assign({},event,{ path: '/testError'}) diff --git a/test/middleware.js b/test/middleware.js index 24d1db0..798046b 100644 --- a/test/middleware.js +++ b/test/middleware.js @@ -77,6 +77,8 @@ api.get('/testPromise', function(req,res) { describe('Middleware Tests:', function() { + this.slow(300); + it('Set Values in res object', function() { let _event = Object.assign({},event,{}) diff --git a/test/modules.js b/test/modules.js index f301539..85935c6 100644 --- a/test/modules.js +++ b/test/modules.js @@ -48,6 +48,8 @@ api.get('/testAppThrownError', function(req,res) { describe('Module Tests:', function() { + this.slow(300); + it('Standard module response', function() { let _event = Object.assign({},event,{ path:'/testApp' }) From 2fb2e3b64155dd9b21cf5b91a8cf23a14c9c01f1 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Thu, 15 Mar 2018 12:29:30 -0400 Subject: [PATCH 08/14] version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index df293c6..ee9b609 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lambda-api", - "version": "0.1.1", + "version": "0.2.0", "description": "Lightweight Node.js API for AWS Lambda", "main": "index.js", "scripts": { From 22fc1e18621972c20b1437ab486d7a8cc441e382 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Thu, 15 Mar 2018 12:30:05 -0400 Subject: [PATCH 09/14] add author info --- request.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/request.js b/request.js index 9e99a70..fb6729a 100644 --- a/request.js +++ b/request.js @@ -1,5 +1,11 @@ 'use strict' +/** + * Lightweight Node.js API for AWS Lambda + * @author Jeremy Daly + * @license MIT + */ + const QS = require('querystring') // Require the querystring library class REQUEST { From 8b9a7460ea13a82dbd718958ab00054f183d7c60 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Thu, 15 Mar 2018 12:31:15 -0400 Subject: [PATCH 10/14] add location and redirect convenience methods --- response.js | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/response.js b/response.js index bab6692..15e3ada 100644 --- a/response.js +++ b/response.js @@ -1,5 +1,14 @@ 'use strict' +/** + * Lightweight Node.js API for AWS Lambda + * @author Jeremy Daly + * @license MIT + */ + +const escapeHtml = require('./utils.js').escapeHtml; +const encodeUrl = require('./utils.js').encodeUrl; + class RESPONSE { // Create the constructor function. @@ -45,12 +54,38 @@ class RESPONSE { this.header('Content-Type','text/html').send(body) } + // Convenience method for setting Location header + location(path) { + this.header('Location',encodeUrl(path)) + return this + } + + // Convenience method for Redirect + redirect(path) { + let statusCode = 302 // default + + // If status code is provided + if (arguments.length === 2) { + if ([300,301,302,303,307,308].includes(arguments[0])) { + statusCode = arguments[0] + path = arguments[1] + } else { + throw new Error(arguments[0] + ' is an invalid redirect status code') + } + } + + let url = escapeHtml(path) + + this.location(path) + .status(statusCode) + .html(`

${statusCode} Redirecting to ${url}

`) + } + // TODO: cookie // TODO: clearCookie // TODO: attachement // TODO: download // TODO: location - // TODO: redirect // TODO: sendFile // TODO: sendStatus // TODO: type From e5cafd47912b04749e715b511a30b17f2ef63c87 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Thu, 15 Mar 2018 12:31:46 -0400 Subject: [PATCH 11/14] add tests for new location and redirect methods --- test/responses.js | 70 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/test/responses.js b/test/responses.js index 507a47a..899ea2c 100644 --- a/test/responses.js +++ b/test/responses.js @@ -43,6 +43,26 @@ api.get('/testEmptyResponse', function(req,res) { res.send() }) +api.get('/location', function(req,res) { + res.location('http://www.github.com').html('Location header set') +}) + +api.get('/locationEncode', function(req,res) { + res.location('http://www.github.com?foo=bar with space').html('Location header set') +}) + +api.get('/redirect', function(req,res) { + res.redirect('http://www.github.com') +}) + +api.get('/redirect301', function(req,res) { + res.redirect(301,'http://www.github.com') +}) + +api.get('/redirectHTML', function(req,res) { + res.redirect('http://www.github.com?foo=bar&bat=baz') +}) + /******************************************************************************/ /*** BEGIN TESTS ***/ /******************************************************************************/ @@ -99,4 +119,54 @@ describe('Response Tests:', function() { }) }) // end it + it('Location method', function() { + let _event = Object.assign({},event,{ path: '/location'}) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/html', 'Location': 'http://www.github.com' }, statusCode: 200, body: 'Location header set' }) + }) + }) // end it + + it('Location method (encode URL)', function() { + let _event = Object.assign({},event,{ path: '/locationEncode'}) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/html', 'Location': 'http://www.github.com?foo=bar%20with%20space' }, statusCode: 200, body: 'Location header set' }) + }) + }) // end it + + it('Redirect (default 302)', function() { + let _event = Object.assign({},event,{ path: '/redirect'}) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/html', 'Location': 'http://www.github.com' }, statusCode: 302, body: '

302 Redirecting to http://www.github.com

' }) + }) + }) // end it + + it('Redirect (301)', function() { + let _event = Object.assign({},event,{ path: '/redirect301'}) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/html', 'Location': 'http://www.github.com' }, statusCode: 301, body: '

301 Redirecting to http://www.github.com

' }) + }) + }) // end it + + it('Redirect (escape html)', function() { + let _event = Object.assign({},event,{ path: '/redirectHTML'}) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/html', 'Location': 'http://www.github.com?foo=bar&bat=baz%3Cscript%3Ealert(\'not%20good\')%3C/script%3E' }, statusCode: 302, body: '

302 Redirecting to http://www.github.com?foo=bar&bat=baz<script>alert('not good')</script>

' }) + }) + }) // end it + }) // end ERROR HANDLING tests From 2dfdb1043be965eea8ff67cd82bd307617ec8505 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Thu, 15 Mar 2018 12:32:31 -0400 Subject: [PATCH 12/14] update documentation with new methods --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index 54a0701..c810171 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,32 @@ api.get('/users', function(req,res) { }) ``` +### location +The `location` convenience method sets the `Location:` header with the value of a single string argument. The value passed in is not validated but will be encoded before being added to the header. Values that are already encoded can be safely passed in. Note that a valid `3xx` status code must be set to trigger browser redirection. The value can be a relative or absolute path OR a FQDN. + +```javascript +api.get('/redirectToHome', function(req,res) { + res.location('/home').status(302).html('
Redirect to Home
') +}) + +api.get('/redirectToGithub', function(req,res) { + res.location('https://github.com').status(302).html('
Redirect to GitHub
') +}) +``` + +### redirect +The `redirect` convenience method triggers a redirection and ends the current API execution. This method is similar to the `location()` method, but it automatically sets the status code and calls the `send()` method. The redirection URL (relative or absolute path OR a FQDN) can be specified as the only parameter or as a second parameter when a valid `3xx` status code is supplied as the first parameter. The status code is set to 302 by default, but can be changed to 300, 301, 302, 303, 307, or 308 by adding it as the first parameter. + +```javascript +api.get('/redirectToHome', function(req,res) { + res.redirect('/home') +}) + +api.get('/redirectToGithub', function(req,res) { + res.redirect(301,'https://github.com') +}) +``` + ### error An error can be triggered by calling the `error` method. This will cause the API to stop execution and return the message to the client. Custom error handling can be accomplished using the [Error Handling](#error-handling) feature. From 7e1f6bf3c854f75b83c9933918e820efcfe8b684 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Thu, 15 Mar 2018 12:49:19 -0400 Subject: [PATCH 13/14] minor edits --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c810171..64e0d52 100644 --- a/README.md +++ b/README.md @@ -194,7 +194,7 @@ api.get('/users', function(req,res) { ``` ### location -The `location` convenience method sets the `Location:` header with the value of a single string argument. The value passed in is not validated but will be encoded before being added to the header. Values that are already encoded can be safely passed in. Note that a valid `3xx` status code must be set to trigger browser redirection. The value can be a relative or absolute path OR a FQDN. +The `location` convenience method sets the `Location:` header with the value of a single string argument. The value passed in is not validated but will be encoded before being added to the header. Values that are already encoded can be safely passed in. Note that a valid `3xx` status code must be set to trigger browser redirection. The value can be a relative/absolute path OR a FQDN. ```javascript api.get('/redirectToHome', function(req,res) { @@ -207,7 +207,7 @@ api.get('/redirectToGithub', function(req,res) { ``` ### redirect -The `redirect` convenience method triggers a redirection and ends the current API execution. This method is similar to the `location()` method, but it automatically sets the status code and calls the `send()` method. The redirection URL (relative or absolute path OR a FQDN) can be specified as the only parameter or as a second parameter when a valid `3xx` status code is supplied as the first parameter. The status code is set to 302 by default, but can be changed to 300, 301, 302, 303, 307, or 308 by adding it as the first parameter. +The `redirect` convenience method triggers a redirection and ends the current API execution. This method is similar to the `location()` method, but it automatically sets the status code and calls `send()`. The redirection URL (relative/absolute path OR a FQDN) can be specified as the only parameter or as a second parameter when a valid `3xx` status code is supplied as the first parameter. The status code is set to `302` by default, but can be changed to `300`, `301`, `302`, `303`, `307`, or `308` by adding it as the first parameter. ```javascript api.get('/redirectToHome', function(req,res) { @@ -301,7 +301,7 @@ api.use(function(err,req,res,next) { The `next()` callback will cause the script to continue executing and eventually call the standard error handling function. You can short-circuit the default handler by calling a request ending method such as `send`, `html`, or `json`. ## Namespaces -Lambda API allows you to map specific modules to namespaces that can be accessed from the `REQUEST` object. This is helpful when using the pattern in which you create a module that exports middleware, error, or route functions. In the example below, the `data` namespace is added to app and then accessed by reference within an included module. +Lambda API allows you to map specific modules to namespaces that can be accessed from the `REQUEST` object. This is helpful when using the pattern in which you create a module that exports middleware, error, or route functions. In the example below, the `data` namespace is added to the API and then accessed by reference within an included module. The main handler file might look like this: @@ -322,7 +322,7 @@ module.exports = function(req, res) { }); ``` -By saving references in namespaces, you can access them without needing to require them in every module. Namespaces can be added using the `app()` method of the API. `app()` accepts either two parameters: a string representing the name of the namespace and a function reference, OR an object with string names as keys and function references as the values. For example: +By saving references in namespaces, you can access them without needing to require them in every module. Namespaces can be added using the `app()` method of the API. `app()` accepts either two parameters: a string representing the name of the namespace and a function reference *OR* an object with string names as keys and function references as the values. For example: ```javascript api.app('namespace',require('./lib/ns-functions.js')) @@ -359,3 +359,6 @@ Conditional route support could be added via middleware or with conditional logi Routes must be configured in API Gateway in order to support routing to the Lambda function. The easiest way to support all of your routes without recreating them is to use [API Gateway's Proxy Integration](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-set-up-simple-proxy.html#api-gateway-proxy-resource?icmpid=docs_apigateway_console). Simply create one `{proxy+}` route that uses the `ANY` method and all requests will be routed to your Lambda function and processed by the `lambda-api` module. + +## Contributions +Contributions, ideas and bug reports are welcome and greatly appreciated. Please add [issues](https://github.com/jeremydaly/lambda-api/issues) for suggestions and bugs reports. From ee1ce57baacfdee15c4efee34ca5a42d6dccd1f7 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Thu, 15 Mar 2018 13:03:29 -0400 Subject: [PATCH 14/14] remove node 4 --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 47122c2..ddcf415 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,4 +4,3 @@ node_js: - "8" - "7" - "6" - - "4"