Skip to content

Commit

Permalink
Merge pull request #48 from jeremydaly/v0.7.0
Browse files Browse the repository at this point in the history
v0.7.0
  • Loading branch information
jeremydaly authored Jun 16, 2018
2 parents 7c76f28 + d4a8749 commit 8d2bf79
Show file tree
Hide file tree
Showing 17 changed files with 5,166 additions and 130 deletions.
3 changes: 3 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
coverage
node_modules
test
38 changes: 38 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"env": {
"es6": true,
"node": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 8,
"sourceType": "module"
},
"rules": {
"indent": [
"error",
2
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"never"
],
"indent": [
"error",
2,
{ "SwitchCase": 1 }
]
},
"globals": {
"expect": true,
"it": true
}
}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ node_modules

# Local REDIS test data
dump.rdb

# Coverage reports
.nyc_output
coverage
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ language: node_js

node_js:
- "8"

script: "npm run-script test-ci"
36 changes: 33 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
[![Build Status](https://travis-ci.org/jeremydaly/lambda-api.svg?branch=master)](https://travis-ci.org/jeremydaly/lambda-api)
[![npm](https://img.shields.io/npm/v/lambda-api.svg)](https://www.npmjs.com/package/lambda-api)
[![npm](https://img.shields.io/npm/l/lambda-api.svg)](https://www.npmjs.com/package/lambda-api)
[![Coverage Status](https://coveralls.io/repos/github/jeremydaly/lambda-api/badge.svg?branch=master)](https://coveralls.io/github/jeremydaly/lambda-api?branch=master)

### Lightweight web framework for your serverless applications

Expand Down Expand Up @@ -109,6 +110,9 @@ const api = require('lambda-api')({ version: 'v1.0', base: 'v1' });
## Recent Updates
For detailed release notes see [Releases](https://github.com/jeremydaly/lambda-api/releases).

### v0.7: Restrict middleware execution to certain paths
Middleware now supports an optional path parameter that supports multiple paths, wildcards, and parameter matching to better control middleware execution. See [middleware](#middleware) for more information.

### v0.6: Support for both `callback-style` and `async-await`
In additional to `res.send()`, you can now simply `return` the body from your route and middleware functions. See [Returning Responses](#returning-responses) for more information.

Expand Down Expand Up @@ -320,6 +324,7 @@ The `REQUEST` object contains a parsed and normalized request from API Gateway.

- `app`: A reference to an instance of the app
- `version`: The version set at initialization
- `id`: The awsRequestId from the Lambda `context`
- `params`: Dynamic path parameters parsed from the path (see [path parameters](#path-parameters))
- `method`: The HTTP method of the request
- `path`: The path passed in by the request including the `base` and any `prefix` assigned to routes
Expand All @@ -336,6 +341,7 @@ The `REQUEST` object contains a parsed and normalized request from API Gateway.
- `auth`: An object containing the `type` and `value` of an authorization header. Currently supports `Bearer`, `Basic`, `OAuth`, and `Digest` schemas. For the `Basic` schema, the object is extended with additional fields for username/password. For the `OAuth` schema, the object is extended with key/value pairs of the supplied OAuth 1.0 values.
- `namespace` or `ns`: A reference to modules added to the app's namespace (see [namespaces](#namespaces))
- `cookies`: An object containing cookies sent from the browser (see the [cookie](#cookiename-value-options) `RESPONSE` method)
- `context`: Reference to the `context` passed into the Lambda handler function

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.

Expand Down Expand Up @@ -642,7 +648,7 @@ api.options('/users/*', (req,res) => {
```

## Middleware
The API supports middleware to preprocess requests before they execute their matching routes. Middleware is defined using the `use` method and require a function with three parameters for the `REQUEST`, `RESPONSE`, and `next` callback. For example:
The API supports middleware to preprocess requests before they execute their matching routes. Middleware is defined using the `use` method and requires a function with three parameters for the `REQUEST`, `RESPONSE`, and `next` callback. For example:

```javascript
api.use((req,res,next) => {
Expand All @@ -667,7 +673,31 @@ api.use((req,res,next) => {

The `next()` callback tells the system to continue executing. If this is not called then the system will hang and eventually timeout unless another request ending call such as `error` is called. You can define as many middleware functions as you want. They will execute serially and synchronously in the order in which they are defined.

**NOTE:** Middleware can use either callbacks like `res.send()` or `return` to trigger a response to the user. Please note that calling either one of these from within a middleware function will terminate execution and return the response immediately.
**NOTE:** Middleware can use either callbacks like `res.send()` or `return` to trigger a response to the user. Please note that calling either one of these from within a middleware function will return the response immediately.

### Restricting middleware execution to certain path(s)

By default, middleware will execute on every path. If you only need it to execute for specific paths, pass the path (or array of paths) as the first parameter to the `use` function.

```javascript
// Single path
api.use('/users', (req,res,next) => { next() })

// Wildcard path
api.use('/users/*', (req,res,next) => { next() })

// Multiple path
api.use(['/users','/posts'], (req,res,next) => { next() })

// Parameterized paths
api.use('/users/:userId',(req,res,next) => { next() })

// Multiple paths with parameters and wildcards
api.use(['/comments','/users/:userId','/posts/*'],(req,res,next) => { next() })
```

Path matching checks both the supplied `path` and the defined `route`. This means that parameterized paths can be matched by either the parameter (e.g. `/users/:param1`) or by an exact matching path (e.g. `/users/123`).


## Clean Up
The API has a built-in clean up method called 'finally()' that will execute after all middleware and routes have been completed, but before execution is complete. This can be used to close database connections or to perform other clean up functions. A clean up function can be defined using the `finally` method and requires a function with two parameters for the REQUEST and the RESPONSE as its only argument. For example:
Expand Down Expand Up @@ -814,4 +844,4 @@ Routes must be configured in API Gateway in order to support routing to the Lamb
Simply create a `{proxy+}` route that uses the `ANY` method and all requests will be routed to your Lambda function and processed by the `lambda-api` module. In order for a "root" path mapping to work, you also need to create an `ANY` route for `/`.

## Contributions
Contributions, ideas and bug reports are welcome and greatly appreciated. Please add [issues](https://github.com/jeremydaly/lambda-api/issues) for suggestions and bug reports.
Contributions, ideas and bug reports are welcome and greatly appreciated. Please add [issues](https://github.com/jeremydaly/lambda-api/issues) for suggestions and bug reports or create a pull request.
55 changes: 40 additions & 15 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
'use strict';
'use strict'

/**
* Lightweight web framework for your serverless applications
* @author Jeremy Daly <[email protected]>
* @version 0.6.0
* @version 0.7.0
* @license MIT
*/

Expand Down Expand Up @@ -31,7 +31,9 @@ class API {
this._routes = {}

// Default callback
this._cb = function(err,res) { console.log('No callback specified') }
this._cb = function() {
console.log('No callback specified') // eslint-disable-line no-console
}

// Middleware stack
this._middleware = []
Expand Down Expand Up @@ -106,7 +108,7 @@ class API {
handler: handler,
route: '/'+parsedPath.join('/'),
path: '/'+this._prefix.concat(parsedPath).join('/') }
} : {}),
} : {}),
route.slice(0,i+1)
)
}
Expand All @@ -124,7 +126,7 @@ class API {

// Set the event, context and callback
this._event = event
this._context = context
this._context = this.context = context
this._cb = cb

// Initalize request and response objects
Expand All @@ -140,9 +142,28 @@ class API {
for (const mw of this._middleware) {
// Only run middleware if in processing state
if (response._state !== 'processing') break

// Init for matching routes
let matched = false

// Test paths if they are supplied
for (const path of mw[0]) {
if (
path === request.path || // If exact path match
path === request.route || // If exact route match
// If a wildcard match
(path.substr(-1) === '*' && new RegExp('^' + path.slice(0, -1) + '.*$').test(request.route))
) {
matched = true
break
}
}

if (mw[0].length > 0 && !matched) continue

// Promisify middleware
await new Promise(r => {
let rtn = mw(request,response,() => { r() })
let rtn = mw[1](request,response,() => { r() })
if (rtn) response.send(rtn)
})
} // end for
Expand Down Expand Up @@ -170,15 +191,15 @@ class API {
// Strip the headers (TODO: find a better way to handle this)
response._headers = {}

let message;
let message

if (e instanceof Error) {
response.status(this._errorStatus)
message = e.message
!this._test && console.log(e)
!this._test && console.log(e) // eslint-disable-line no-console
} else {
message = e
!this._test && console.log('API Error:',e)
!this._test && console.log('API Error:',e) // eslint-disable-line no-console
}

// If first time through, process error middleware
Expand Down Expand Up @@ -222,9 +243,13 @@ class API {


// Middleware handler
use(fn) {
use(path,handler) {

let fn = typeof path === 'function' ? path : handler
let routes = typeof path === 'string' ? Array.of(path) : (Array.isArray(path) ? path : [])

if (fn.length === 3) {
this._middleware.push(fn)
this._middleware.push([routes,fn])
} else if (fn.length === 4) {
this._errors.push(fn)
} else {
Expand All @@ -250,8 +275,8 @@ class API {

// Recursive function to create routes object
setRoute(obj, value, path) {
if (typeof path === "string") {
let path = path.split('.')
if (typeof path === 'string') {
let path = path.split('.')
}

if (path.length > 1){
Expand Down Expand Up @@ -280,7 +305,7 @@ class API {
try {
this._app[namespace] = packages[namespace]
} catch(e) {
console.error(e.message)
console.error(e.message) // eslint-disable-line no-console
}
}
} else if (arguments.length === 2 && typeof packages === 'string') {
Expand Down Expand Up @@ -317,7 +342,7 @@ class API {
let routes = UTILS.extractRoutes(this._routes)

if (format) {
prettyPrint(routes)
console.log(prettyPrint(routes)) // eslint-disable-line no-console
} else {
return routes
}
Expand Down
1 change: 0 additions & 1 deletion lib/mimemap.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ module.exports = {
txt: 'text/plain',
webmanifest: 'application/manifest+json',
xml: 'application/xml',
xls: 'application/xml',

// other binary
gz: 'application/gzip',
Expand Down
24 changes: 17 additions & 7 deletions lib/prettyPrint.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,27 @@
*/

module.exports = routes => {

let out = ''

// Calculate column widths
let widths = routes.reduce((acc,row) => {
return [Math.max(acc[0],row[0].length),Math.max(acc[1],row[1].length)]
return [
Math.max(acc[0],Math.max(6,row[0].length)),
Math.max(acc[1],Math.max(5,row[1].length))
]
},[0,0])

console.log('╔══' + ''.padEnd(widths[0],'═') + '══╤══' + ''.padEnd(widths[1],'═') + '══╗')
console.log('║ ' + "\u001b[1m" + 'METHOD'.padEnd(widths[0]) + "\u001b[0m" + ' │ ' + "\u001b[1m" + 'ROUTE'.padEnd(widths[1]) + "\u001b[0m" + ' ║')
console.log('╟──' + ''.padEnd(widths[0],'─') + '──┼──' + ''.padEnd(widths[1],'─') + '──╢')
out += '╔══' + ''.padEnd(widths[0],'═') + '══╤══' + ''.padEnd(widths[1],'═') + '══╗\n'
out += '║ ' + '\u001b[1m' + 'METHOD'.padEnd(widths[0]) + '\u001b[0m' + ' │ ' + '\u001b[1m' + 'ROUTE'.padEnd(widths[1]) + '\u001b[0m' + ' ║\n'
out += '╟──' + ''.padEnd(widths[0],'─') + '──┼──' + ''.padEnd(widths[1],'─') + '──╢\n'
routes.forEach((route,i) => {
console.log('║ ' + route[0].padEnd(widths[0]) + ' │ ' + route[1].padEnd(widths[1]) + ' ║')
if (i < routes.length-1) { console.log('╟──' + ''.padEnd(widths[0],'─') + '──┼──' + ''.padEnd(widths[1],'─') + '──╢') }
out += '║ ' + route[0].padEnd(widths[0]) + ' │ ' + route[1].padEnd(widths[1]) + ' ║\n'
if (i < routes.length-1) {
out += '╟──' + ''.padEnd(widths[0],'─') + '──┼──' + ''.padEnd(widths[1],'─') + '──╢\n'
} // end if
})
console.log('╚══' + ''.padEnd(widths[0],'═') + '══╧══' + ''.padEnd(widths[1],'═') + '══╝')
out += '╚══' + ''.padEnd(widths[0],'═') + '══╧══' + ''.padEnd(widths[1],'═') + '══╝'

return out
}
22 changes: 15 additions & 7 deletions lib/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ class REQUEST {
this.app = app

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

// Expose Namespaces
this.namespace = this.ns = app._app
Expand Down Expand Up @@ -68,14 +70,20 @@ class REQUEST {
// Set the requestContext
this.requestContext = this.app._event.requestContext

// Parse id from context
this.id = this.app.context.awsRequestId ? this.app.context.awsRequestId : null

// Add context
this.context = typeof this.app.context === 'object' ? this.app.context : {}

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

// Set the body (decode it if base64 encoded)
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")) {
if (this.headers['content-type'] && this.headers['content-type'].includes('application/x-www-form-urlencoded')) {
this.body = QS.parse(this.body)
} else if (typeof this.body === 'object') {
this.body = this.body
Expand Down Expand Up @@ -113,11 +121,11 @@ class REQUEST {

// Select ROUTE if exist for method, default ANY, apply wildcards, alias HEAD requests
let route = routes['__'+this.method] ? routes['__'+this.method] :
(routes['__ANY'] ? routes['__ANY'] :
(wildcard && wildcard['__'+this.method] ? wildcard['__'+this.method] :
(wildcard && wildcard['__ANY'] ? wildcard['__ANY'] :
(this.method === 'HEAD' && routes['__GET'] ? routes['__GET'] :
undefined))))
(routes['__ANY'] ? routes['__ANY'] :
(wildcard && wildcard['__'+this.method] ? wildcard['__'+this.method] :
(wildcard && wildcard['__ANY'] ? wildcard['__ANY'] :
(this.method === 'HEAD' && routes['__GET'] ? routes['__GET'] :
undefined))))

// Check for the requested method
if (route) {
Expand Down
4 changes: 2 additions & 2 deletions lib/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class RESPONSE {
// Default the header
this._headers = {
// Set the Content-Type by default
"content-type": "application/json" //charset=UTF-8
'content-type': 'application/json' //charset=UTF-8
}

// base64 encoding flag
Expand Down Expand Up @@ -128,7 +128,7 @@ class RESPONSE {
cookie(name,value,opts={}) {

// Set the name and value of the cookie
let cookieString = (typeof name !== 'String' ? name.toString() : name)
let cookieString = (typeof name !== 'string' ? name.toString() : name)
+ '=' + encodeURIComponent(UTILS.encodeBody(value))

// domain (String): Domain name for the cookie
Expand Down
Loading

0 comments on commit 8d2bf79

Please sign in to comment.