Skip to content

Commit 8d2bf79

Browse files
authored
Merge pull request #48 from jeremydaly/v0.7.0
v0.7.0
2 parents 7c76f28 + d4a8749 commit 8d2bf79

17 files changed

+5166
-130
lines changed

.eslintignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
coverage
2+
node_modules
3+
test

.eslintrc.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"env": {
3+
"es6": true,
4+
"node": true
5+
},
6+
"extends": "eslint:recommended",
7+
"parserOptions": {
8+
"ecmaVersion": 8,
9+
"sourceType": "module"
10+
},
11+
"rules": {
12+
"indent": [
13+
"error",
14+
2
15+
],
16+
"linebreak-style": [
17+
"error",
18+
"unix"
19+
],
20+
"quotes": [
21+
"error",
22+
"single"
23+
],
24+
"semi": [
25+
"error",
26+
"never"
27+
],
28+
"indent": [
29+
"error",
30+
2,
31+
{ "SwitchCase": 1 }
32+
]
33+
},
34+
"globals": {
35+
"expect": true,
36+
"it": true
37+
}
38+
}

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,7 @@ node_modules
66

77
# Local REDIS test data
88
dump.rdb
9+
10+
# Coverage reports
11+
.nyc_output
12+
coverage

.travis.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ language: node_js
22

33
node_js:
44
- "8"
5+
6+
script: "npm run-script test-ci"

README.md

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
[![Build Status](https://travis-ci.org/jeremydaly/lambda-api.svg?branch=master)](https://travis-ci.org/jeremydaly/lambda-api)
44
[![npm](https://img.shields.io/npm/v/lambda-api.svg)](https://www.npmjs.com/package/lambda-api)
55
[![npm](https://img.shields.io/npm/l/lambda-api.svg)](https://www.npmjs.com/package/lambda-api)
6+
[![Coverage Status](https://coveralls.io/repos/github/jeremydaly/lambda-api/badge.svg?branch=master)](https://coveralls.io/github/jeremydaly/lambda-api?branch=master)
67

78
### Lightweight web framework for your serverless applications
89

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

113+
### v0.7: Restrict middleware execution to certain paths
114+
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.
115+
112116
### v0.6: Support for both `callback-style` and `async-await`
113117
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.
114118

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

321325
- `app`: A reference to an instance of the app
322326
- `version`: The version set at initialization
327+
- `id`: The awsRequestId from the Lambda `context`
323328
- `params`: Dynamic path parameters parsed from the path (see [path parameters](#path-parameters))
324329
- `method`: The HTTP method of the request
325330
- `path`: The path passed in by the request including the `base` and any `prefix` assigned to routes
@@ -336,6 +341,7 @@ The `REQUEST` object contains a parsed and normalized request from API Gateway.
336341
- `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.
337342
- `namespace` or `ns`: A reference to modules added to the app's namespace (see [namespaces](#namespaces))
338343
- `cookies`: An object containing cookies sent from the browser (see the [cookie](#cookiename-value-options) `RESPONSE` method)
344+
- `context`: Reference to the `context` passed into the Lambda handler function
339345

340346
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.
341347

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

644650
## Middleware
645-
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:
651+
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:
646652

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

668674
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.
669675

670-
**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.
676+
**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.
677+
678+
### Restricting middleware execution to certain path(s)
679+
680+
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.
681+
682+
```javascript
683+
// Single path
684+
api.use('/users', (req,res,next) => { next() })
685+
686+
// Wildcard path
687+
api.use('/users/*', (req,res,next) => { next() })
688+
689+
// Multiple path
690+
api.use(['/users','/posts'], (req,res,next) => { next() })
691+
692+
// Parameterized paths
693+
api.use('/users/:userId',(req,res,next) => { next() })
694+
695+
// Multiple paths with parameters and wildcards
696+
api.use(['/comments','/users/:userId','/posts/*'],(req,res,next) => { next() })
697+
```
698+
699+
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`).
700+
671701

672702
## Clean Up
673703
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:
@@ -814,4 +844,4 @@ Routes must be configured in API Gateway in order to support routing to the Lamb
814844
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 `/`.
815845

816846
## Contributions
817-
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.
847+
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.

index.js

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
'use strict';
1+
'use strict'
22

33
/**
44
* Lightweight web framework for your serverless applications
55
* @author Jeremy Daly <[email protected]>
6-
* @version 0.6.0
6+
* @version 0.7.0
77
* @license MIT
88
*/
99

@@ -31,7 +31,9 @@ class API {
3131
this._routes = {}
3232

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

3638
// Middleware stack
3739
this._middleware = []
@@ -106,7 +108,7 @@ class API {
106108
handler: handler,
107109
route: '/'+parsedPath.join('/'),
108110
path: '/'+this._prefix.concat(parsedPath).join('/') }
109-
} : {}),
111+
} : {}),
110112
route.slice(0,i+1)
111113
)
112114
}
@@ -124,7 +126,7 @@ class API {
124126

125127
// Set the event, context and callback
126128
this._event = event
127-
this._context = context
129+
this._context = this.context = context
128130
this._cb = cb
129131

130132
// Initalize request and response objects
@@ -140,9 +142,28 @@ class API {
140142
for (const mw of this._middleware) {
141143
// Only run middleware if in processing state
142144
if (response._state !== 'processing') break
145+
146+
// Init for matching routes
147+
let matched = false
148+
149+
// Test paths if they are supplied
150+
for (const path of mw[0]) {
151+
if (
152+
path === request.path || // If exact path match
153+
path === request.route || // If exact route match
154+
// If a wildcard match
155+
(path.substr(-1) === '*' && new RegExp('^' + path.slice(0, -1) + '.*$').test(request.route))
156+
) {
157+
matched = true
158+
break
159+
}
160+
}
161+
162+
if (mw[0].length > 0 && !matched) continue
163+
143164
// Promisify middleware
144165
await new Promise(r => {
145-
let rtn = mw(request,response,() => { r() })
166+
let rtn = mw[1](request,response,() => { r() })
146167
if (rtn) response.send(rtn)
147168
})
148169
} // end for
@@ -170,15 +191,15 @@ class API {
170191
// Strip the headers (TODO: find a better way to handle this)
171192
response._headers = {}
172193

173-
let message;
194+
let message
174195

175196
if (e instanceof Error) {
176197
response.status(this._errorStatus)
177198
message = e.message
178-
!this._test && console.log(e)
199+
!this._test && console.log(e) // eslint-disable-line no-console
179200
} else {
180201
message = e
181-
!this._test && console.log('API Error:',e)
202+
!this._test && console.log('API Error:',e) // eslint-disable-line no-console
182203
}
183204

184205
// If first time through, process error middleware
@@ -222,9 +243,13 @@ class API {
222243

223244

224245
// Middleware handler
225-
use(fn) {
246+
use(path,handler) {
247+
248+
let fn = typeof path === 'function' ? path : handler
249+
let routes = typeof path === 'string' ? Array.of(path) : (Array.isArray(path) ? path : [])
250+
226251
if (fn.length === 3) {
227-
this._middleware.push(fn)
252+
this._middleware.push([routes,fn])
228253
} else if (fn.length === 4) {
229254
this._errors.push(fn)
230255
} else {
@@ -250,8 +275,8 @@ class API {
250275

251276
// Recursive function to create routes object
252277
setRoute(obj, value, path) {
253-
if (typeof path === "string") {
254-
let path = path.split('.')
278+
if (typeof path === 'string') {
279+
let path = path.split('.')
255280
}
256281

257282
if (path.length > 1){
@@ -280,7 +305,7 @@ class API {
280305
try {
281306
this._app[namespace] = packages[namespace]
282307
} catch(e) {
283-
console.error(e.message)
308+
console.error(e.message) // eslint-disable-line no-console
284309
}
285310
}
286311
} else if (arguments.length === 2 && typeof packages === 'string') {
@@ -317,7 +342,7 @@ class API {
317342
let routes = UTILS.extractRoutes(this._routes)
318343

319344
if (format) {
320-
prettyPrint(routes)
345+
console.log(prettyPrint(routes)) // eslint-disable-line no-console
321346
} else {
322347
return routes
323348
}

lib/mimemap.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ module.exports = {
3232
txt: 'text/plain',
3333
webmanifest: 'application/manifest+json',
3434
xml: 'application/xml',
35-
xls: 'application/xml',
3635

3736
// other binary
3837
gz: 'application/gzip',

lib/prettyPrint.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,27 @@
77
*/
88

99
module.exports = routes => {
10+
11+
let out = ''
12+
1013
// Calculate column widths
1114
let widths = routes.reduce((acc,row) => {
12-
return [Math.max(acc[0],row[0].length),Math.max(acc[1],row[1].length)]
15+
return [
16+
Math.max(acc[0],Math.max(6,row[0].length)),
17+
Math.max(acc[1],Math.max(5,row[1].length))
18+
]
1319
},[0,0])
1420

15-
console.log('╔══' + ''.padEnd(widths[0],'═') + '══╤══' + ''.padEnd(widths[1],'═') + '══╗')
16-
console.log('║ ' + "\u001b[1m" + 'METHOD'.padEnd(widths[0]) + "\u001b[0m" + ' │ ' + "\u001b[1m" + 'ROUTE'.padEnd(widths[1]) + "\u001b[0m" + ' ║')
17-
console.log('╟──' + ''.padEnd(widths[0],'─') + '──┼──' + ''.padEnd(widths[1],'─') + '──╢')
21+
out += '╔══' + ''.padEnd(widths[0],'═') + '══╤══' + ''.padEnd(widths[1],'═') + '══╗\n'
22+
out += '║ ' + '\u001b[1m' + 'METHOD'.padEnd(widths[0]) + '\u001b[0m' + ' │ ' + '\u001b[1m' + 'ROUTE'.padEnd(widths[1]) + '\u001b[0m' + ' ║\n'
23+
out += '╟──' + ''.padEnd(widths[0],'─') + '──┼──' + ''.padEnd(widths[1],'─') + '──╢\n'
1824
routes.forEach((route,i) => {
19-
console.log('║ ' + route[0].padEnd(widths[0]) + ' │ ' + route[1].padEnd(widths[1]) + ' ║')
20-
if (i < routes.length-1) { console.log('╟──' + ''.padEnd(widths[0],'─') + '──┼──' + ''.padEnd(widths[1],'─') + '──╢') }
25+
out += '║ ' + route[0].padEnd(widths[0]) + ' │ ' + route[1].padEnd(widths[1]) + ' ║\n'
26+
if (i < routes.length-1) {
27+
out += '╟──' + ''.padEnd(widths[0],'─') + '──┼──' + ''.padEnd(widths[1],'─') + '──╢\n'
28+
} // end if
2129
})
22-
console.log('╚══' + ''.padEnd(widths[0],'═') + '══╧══' + ''.padEnd(widths[1],'═') + '══╝')
30+
out += '╚══' + ''.padEnd(widths[0],'═') + '══╧══' + ''.padEnd(widths[1],'═') + '══╝'
31+
32+
return out
2333
}

lib/request.js

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ class REQUEST {
1818
this.app = app
1919

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

2325
// Expose Namespaces
2426
this.namespace = this.ns = app._app
@@ -68,14 +70,20 @@ class REQUEST {
6870
// Set the requestContext
6971
this.requestContext = this.app._event.requestContext
7072

73+
// Parse id from context
74+
this.id = this.app.context.awsRequestId ? this.app.context.awsRequestId : null
75+
76+
// Add context
77+
this.context = typeof this.app.context === 'object' ? this.app.context : {}
78+
7179
// Capture the raw body
7280
this.rawBody = this.app._event.body
7381

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

7785
// Set the body
78-
if (this.headers['content-type'] && this.headers['content-type'].includes("application/x-www-form-urlencoded")) {
86+
if (this.headers['content-type'] && this.headers['content-type'].includes('application/x-www-form-urlencoded')) {
7987
this.body = QS.parse(this.body)
8088
} else if (typeof this.body === 'object') {
8189
this.body = this.body
@@ -113,11 +121,11 @@ class REQUEST {
113121

114122
// Select ROUTE if exist for method, default ANY, apply wildcards, alias HEAD requests
115123
let route = routes['__'+this.method] ? routes['__'+this.method] :
116-
(routes['__ANY'] ? routes['__ANY'] :
117-
(wildcard && wildcard['__'+this.method] ? wildcard['__'+this.method] :
118-
(wildcard && wildcard['__ANY'] ? wildcard['__ANY'] :
119-
(this.method === 'HEAD' && routes['__GET'] ? routes['__GET'] :
120-
undefined))))
124+
(routes['__ANY'] ? routes['__ANY'] :
125+
(wildcard && wildcard['__'+this.method] ? wildcard['__'+this.method] :
126+
(wildcard && wildcard['__ANY'] ? wildcard['__ANY'] :
127+
(this.method === 'HEAD' && routes['__GET'] ? routes['__GET'] :
128+
undefined))))
121129

122130
// Check for the requested method
123131
if (route) {

lib/response.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class RESPONSE {
3131
// Default the header
3232
this._headers = {
3333
// Set the Content-Type by default
34-
"content-type": "application/json" //charset=UTF-8
34+
'content-type': 'application/json' //charset=UTF-8
3535
}
3636

3737
// base64 encoding flag
@@ -128,7 +128,7 @@ class RESPONSE {
128128
cookie(name,value,opts={}) {
129129

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

134134
// domain (String): Domain name for the cookie

0 commit comments

Comments
 (0)