Skip to content

Commit 5e23d38

Browse files
authored
Merge pull request #13 from jeremydaly/v0.3.0
v0.3.0
2 parents 6ecf2f5 + 9f03a4c commit 5e23d38

16 files changed

+721
-54
lines changed

README.md

+91-14
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,24 @@
99

1010
Lambda API is a lightweight web framework for use with AWS API Gateway and AWS Lambda using Lambda Proxy integration. This closely mirrors (and is based on) other routers like Express.js, but is significantly stripped down to maximize performance with Lambda's stateless, single run executions.
1111

12+
**IMPORTANT:** There is a [breaking change](#breaking-change-in-v03) in v0.3 that affects instantiation.
13+
1214
## Simple Example
1315

1416
```javascript
15-
const API = require('lambda-api') // API library
16-
17-
// Init API instance
18-
const api = new API({ version: 'v1.0', base: 'v1' });
17+
// Require the framework and instantiate it
18+
const api = require('lambda-api')()
1919

20+
// Define a route
2021
api.get('/test', function(req,res) {
2122
res.status(200).json({ status: 'ok' })
2223
})
2324

25+
// Declare your Lambda handler
2426
module.exports.handler = (event, context, callback) => {
25-
2627
// Run the request
27-
api.run(event,context,callback);
28-
29-
} // end handler
28+
api.run(event, context, callback)
29+
}
3030
```
3131

3232
## Why Another Web Framework?
@@ -38,6 +38,21 @@ Lambda API has **ONE** dependency. We use [Bluebird](http://bluebirdjs.com/docs/
3838

3939
Lambda API was written to be extremely lightweight and built specifically for serverless applications using AWS Lambda. It provides support for API routing, serving up HTML pages, issuing redirects, and much more. It has a powerful middleware and error handling system, allowing you to implement everything from custom authentication to complex logging systems. Best of all, it was designed to work with Lambda's Proxy Integration, automatically handling all the interaction with API Gateway for you. It parses **REQUESTS** and formats **RESPONSES** for you, allowing you to focus on your application's core functionality, instead of fiddling with inputs and outputs.
4040

41+
## Breaking Change in v0.3
42+
Please note that the invocation method has been changed. You no longer need to use the `new` keyword to instantiate Lambda API. It can now be instantiated in one line:
43+
44+
```javascript
45+
const api = require('lambda-api')()
46+
```
47+
48+
`lambda-api` returns a `function` now instead of a `class`, so options can be passed in as its only argument:
49+
50+
```javascript
51+
const api = require('lambda-api')({ version: 'v1.0', base: 'v1' });
52+
```
53+
54+
**IMPORTANT:** Upgrading to v0.3.0 requires either removing the `new` keyword or switching to the one-line format. This provides more flexibility for instantiating Lambda API in future releases.
55+
4156
## Lambda Proxy integration
4257
Lambda Proxy Integration is an option in API Gateway that allows the details of an API request to be passed as the `event` parameter of a Lambda function. A typical API Gateway request event with Lambda Proxy Integration enabled looks like this:
4358

@@ -104,13 +119,11 @@ The API automatically parses this information to create a normalized `REQUEST` o
104119

105120
## Configuration
106121

107-
Include the `lambda-api` module into your Lambda handler script and initialize an instance. You can initialize the API with an optional `version` which can be accessed via the `REQUEST` object and a `base` path. The base path can be used to route multiple versions to different instances.
122+
Require the `lambda-api` module into your Lambda handler script and instantiate it. You can initialize the API with an optional `version` which can be accessed via the `REQUEST` object and a `base` path.
108123

109124
```javascript
110-
const API = require('lambda-api') // API library
111-
112-
// Init API instance with optional version and base path
113-
const api = new API({ version: 'v1.0', base: 'v1' });
125+
// Require the framework and instantiate it with optional version and base parameters
126+
const api = require('lambda-api')({ version: 'v1.0', base: 'v1' });
114127
```
115128

116129
## Routes and HTTP Methods
@@ -156,6 +169,7 @@ The `REQUEST` object contains a parsed and normalized request from API Gateway.
156169
- `route`: The matched route of the request
157170
- `requestContext`: The `requestContext` passed from the API Gateway
158171
- `namespace` or `ns`: A reference to modules added to the app's namespace (see [namespaces](#namespaces))
172+
- `cookies`: An object containing cookies sent from the browser (see the [cookie](#cookie) `RESPONSE` method)
159173

160174
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.
161175

@@ -185,14 +199,43 @@ api.get('/users', function(req,res) {
185199
The `send` methods triggers the API to return data to the API Gateway. The `send` method accepts one parameter and sends the contents through as is, e.g. as an object, string, integer, etc. AWS Gateway expects a string, so the data should be converted accordingly.
186200

187201
### json
188-
There is a `json` convenience method for the `send` method that will set the headers to `application\json` as well as perform `JSON.stringify()` on the contents passed to it.
202+
There is a `json` convenience method for the `send` method that will set the headers to `application/json` as well as perform `JSON.stringify()` on the contents passed to it.
189203

190204
```javascript
191205
api.get('/users', function(req,res) {
192206
res.json({ message: 'This will be converted automatically' })
193207
})
194208
```
195209

210+
### jsonp
211+
There is a `jsonp` convenience method for the `send` method that will set the headers to `application/json`, perform `JSON.stringify()` on the contents passed to it, and wrap the results in a callback function. By default, the callback function is named `callback`.
212+
213+
```javascript
214+
res.jsonp({ foo: 'bar' })
215+
// => callback({ "foo": "bar" })
216+
217+
res.status(500).jsonp({ error: 'some error'})
218+
// => callback({ "error": "some error" })
219+
```
220+
221+
The default can be changed by passing in `callback` as a URL parameter, e.g. `?callback=foo`.
222+
223+
```javascript
224+
// ?callback=foo
225+
res.jsonp({ foo: 'bar' })
226+
// => foo({ "foo": "bar" })
227+
```
228+
229+
You can change the default URL parameter using the optional `callback` option when initializing the API.
230+
231+
```javascript
232+
const api = require('lambda-api')({ callback: 'cb' });
233+
234+
// ?cb=bar
235+
res.jsonp({ foo: 'bar' })
236+
// => bar({ "foo": "bar" })
237+
```
238+
196239
### html
197240
There is also an `html` convenience method for the `send` method that will set the headers to `text/html` and pass through the contents.
198241

@@ -237,6 +280,40 @@ api.get('/users', function(req,res) {
237280
})
238281
```
239282

283+
### cookie
284+
285+
Convenience method for setting cookies. This method accepts a `name`, `value` and an optional `options` object with the following parameters:
286+
287+
| Property | Type | Description |
288+
| -------- | ---- | ----------- |
289+
| domain | `String` | Domain name to use for the cookie. This defaults to the current domain. |
290+
| expires | `Date` | The expiration date of the cookie. Local dates will be converted to GMT. Creates session cookie if this value is not specified. |
291+
| httpOnly | `Boolean` | Sets the cookie to be accessible only via a web server, not JavaScript. |
292+
| maxAge | `Number` | Set the expiration time relative to the current time in milliseconds. Automatically sets the `expires` property if not explicitly provided. |
293+
| path | `String` | Path for the cookie. Defaults to "/" for the root directory. |
294+
| secure | `Boolean` | Sets the cookie to be used with HTTPS only. |
295+
|sameSite | `Boolean` or `String` | Sets the SameSite value for cookie. `true` or `false` sets `Strict` or `Lax` respectively. Also allows a string value. See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00#section-4.1.1 |
296+
297+
The `name` attribute should be a string (auto-converted if not), but the `value` attribute can be any type of value. The `value` will be serialized (if an object, array, etc.) and then encoded using `encodeURIComponent` for safely assigning the cookie value. Cookies are automatically parsed, decoded, and available via the `REQUEST` object (see [REQUEST](#request)).
298+
299+
**NOTE:** The `cookie()` method only sets the header. A execution ending method like `send()`, `json()`, etc. must be called to send the response.
300+
301+
```javascript
302+
res.cookie('foo', 'bar', { maxAge: 3600*1000, secure: true }).send()
303+
res.cookie('fooObject', { foo: 'bar' }, { domain: '.test.com', path: '/admin', httpOnly: true }).send()
304+
res.cookie('fooArray', [ 'one', 'two', 'three' ], { path: '/', httpOnly: true }).send()
305+
```
306+
307+
### clearCookie
308+
Convenience method for expiring cookies. Requires the `name` and optional `options` object as specified in the [cookie](#cookie) method. This method will automatically set the expiration time. However, most browsers require the same options to clear a cookie as was used to set it. E.g. if you set the `path` to "/admin" when you set the cookie, you must use this same value to clear it.
309+
310+
```javascript
311+
res.clearCookie('foo', { secure: true }).send()
312+
res.clearCookie('fooObject', { domain: '.test.com', path: '/admin', httpOnly: true }).send()
313+
res.clearCookie('fooArray', { path: '/', httpOnly: true }).send()
314+
```
315+
**NOTE:** The `clearCookie()` method only sets the header. A execution ending method like `send()`, `json()`, etc. must be called to send the response.
316+
240317
## Path Parameters
241318
Path parameters are extracted from the path sent in by API Gateway. Although API Gateway supports path parameters, the API doesn't use these values but insteads extracts them from the actual path. This gives you more flexibility with the API Gateway configuration. Path parameters are defined in routes using a colon `:` as a prefix.
242319

index.js

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
'use strict';
22

33
/**
4-
* Lightweight Node.js API for AWS Lambda
4+
* Lightweight web framework for your serverless applications
55
* @author Jeremy Daly <[email protected]>
6-
* @version 0.1.0
6+
* @version 0.3.0
77
* @license MIT
88
*/
99

@@ -18,8 +18,9 @@ class API {
1818
constructor(props) {
1919

2020
// Set the version and base paths
21-
this._version = props.version ? props.version : 'v1'
22-
this._base = props.base ? props.base.trim() : ''
21+
this._version = props && props.version ? props.version : 'v1'
22+
this._base = props && props.base ? props.base.trim() : ''
23+
this._callbackName = props && props.callback ? props.callback.trim() : 'callback'
2324

2425
// Stores timers for debugging
2526
this._timers = {}
@@ -356,4 +357,6 @@ class API {
356357
} // end API class
357358

358359
// Export the API class
359-
module.exports = API
360+
module.exports = opts => new API(opts)
361+
362+
// console.error('DEPRECATED: constructor method. Use require(\'lambda-api\')({ version: \'v1.0\', base: \'v1\' }) to initialize the framework instead')

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "lambda-api",
3-
"version": "0.2.1",
3+
"version": "0.3.0",
44
"description": "Lightweight web framework for your serverless applications",
55
"main": "index.js",
66
"scripts": {
@@ -11,6 +11,7 @@
1111
"url": "git+https://github.com/jeremydaly/lambda-api.git"
1212
},
1313
"keywords": [
14+
"serverless",
1415
"nodejs",
1516
"api",
1617
"awslambda",

request.js

+14-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
'use strict'
22

33
/**
4-
* Lightweight Node.js API for AWS Lambda
4+
* Lightweight web framework for your serverless applications
55
* @author Jeremy Daly <[email protected]>
66
* @license MIT
77
*/
88

99
const QS = require('querystring') // Require the querystring library
10+
const parseBody = require('./utils.js').parseBody
1011

1112
class REQUEST {
1213

@@ -37,6 +38,17 @@ class REQUEST {
3738
// Set the headers
3839
this.headers = app._event.headers
3940

41+
// Set and parse cookies
42+
this.cookies = app._event.headers.Cookie ?
43+
app._event.headers.Cookie.split(';')
44+
.reduce(
45+
(acc,cookie) => {
46+
cookie = cookie.trim().split('=')
47+
return Object.assign(acc,{ [cookie[0]] : parseBody(decodeURIComponent(cookie[1])) })
48+
},
49+
{}
50+
) : {}
51+
4052
// Set the requestContext
4153
this.requestContext = app._event.requestContext
4254

@@ -46,11 +58,7 @@ class REQUEST {
4658
} else if (typeof app._event.body === 'object') {
4759
this.body = app._event.body
4860
} else {
49-
try {
50-
this.body = JSON.parse(app._event.body)
51-
} catch(e) {
52-
this.body = app._event.body;
53-
}
61+
this.body = parseBody(app._event.body)
5462
}
5563

5664
// Extract path from event (strip querystring just in case)

response.js

+65-11
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
'use strict'
22

33
/**
4-
* Lightweight Node.js API for AWS Lambda
4+
* Lightweight web framework for your serverless applications
55
* @author Jeremy Daly <[email protected]>
66
* @license MIT
77
*/
88

9-
const escapeHtml = require('./utils.js').escapeHtml;
10-
const encodeUrl = require('./utils.js').encodeUrl;
9+
const escapeHtml = require('./utils.js').escapeHtml
10+
const encodeUrl = require('./utils.js').encodeUrl
11+
const encodeBody = require('./utils.js').encodeBody
1112

1213
class RESPONSE {
1314

@@ -25,6 +26,9 @@ class RESPONSE {
2526
// Set the Content-Type by default
2627
"Content-Type": "application/json" //charset=UTF-8
2728
}
29+
30+
// Default callback function
31+
this._callback = 'callback'
2832
}
2933

3034
// Sets the statusCode
@@ -44,10 +48,15 @@ class RESPONSE {
4448
this.header('Content-Type','application/json').send(JSON.stringify(body))
4549
}
4650

47-
// TODO: Convenience method for JSONP
48-
// jsonp(body) {
49-
// this.header('Content-Type','application/json').send(JSON.stringify(body))
50-
// }
51+
// Convenience method for JSONP
52+
jsonp(body) {
53+
// Check the querystring for callback or cb
54+
let query = this.app._event.queryStringParameters || {}
55+
let cb = query[this.app._callbackName]
56+
57+
this.header('Content-Type','application/json')
58+
.send((cb ? cb.replace(' ','_') : 'callback') + '(' + JSON.stringify(body) + ')')
59+
}
5160

5261
// Convenience method for HTML
5362
html(body) {
@@ -79,13 +88,58 @@ class RESPONSE {
7988
this.location(path)
8089
.status(statusCode)
8190
.html(`<p>${statusCode} Redirecting to <a href="${url}">${url}</a></p>`)
91+
} // end redirect
92+
93+
94+
// Convenience method for setting cookies
95+
// see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
96+
cookie(name,value,opts={}) {
97+
98+
// Set the name and value of the cookie
99+
let cookieString = (typeof name !== 'String' ? name.toString() : name)
100+
+ '=' + encodeURIComponent(encodeBody(value))
101+
102+
// domain (String): Domain name for the cookie
103+
cookieString += opts.domain ? '; Domain=' + opts.domain : ''
104+
105+
// expires (Date): Expiry date of the cookie, convert to GMT
106+
cookieString += opts.expires && typeof opts.expires.toUTCString === 'function' ?
107+
'; Expires=' + opts.expires.toUTCString() : ''
108+
109+
// httpOnly (Boolean): Flags the cookie to be accessible only by the web server
110+
cookieString += opts.httpOnly && opts.httpOnly === true ? '; HttpOnly' : ''
111+
112+
// maxAge (Number) Set expiry time relative to the current time in milliseconds
113+
cookieString += opts.maxAge && !isNaN(opts.maxAge) ?
114+
'; MaxAge=' + (opts.maxAge/1000|0)
115+
+ (!opts.expires ? '; Expires=' + new Date(Date.now() + opts.maxAge).toUTCString() : '')
116+
: ''
117+
118+
// path (String): Path for the cookie
119+
cookieString += opts.path ? '; Path=' + opts.path : '; Path=/'
120+
121+
// secure (Boolean): Marks the cookie to be used with HTTPS only
122+
cookieString += opts.secure && opts.secure === true ? '; Secure' : ''
123+
124+
// sameSite (Boolean or String) Value of the “SameSite” Set-Cookie attribute
125+
// see https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00#section-4.1.1.
126+
cookieString += opts.sameSite !== undefined ? '; SameSite='
127+
+ (opts.sameSite === true ? 'Strict' :
128+
(opts.sameSite === false ? 'Lax' : opts.sameSite ))
129+
: ''
130+
131+
this.header('Set-Cookie',cookieString)
132+
return this
133+
}
134+
135+
// Convenience method for clearing cookies
136+
clearCookie(name,opts={}) {
137+
let options = Object.assign(opts, { expires: new Date(1), maxAge: -1000 })
138+
return this.cookie(name,'',options)
82139
}
83140

84-
// TODO: cookie
85-
// TODO: clearCookie
86141
// TODO: attachement
87142
// TODO: download
88-
// TODO: location
89143
// TODO: sendFile
90144
// TODO: sendStatus
91145
// TODO: type
@@ -98,7 +152,7 @@ class RESPONSE {
98152
const response = {
99153
headers: this._headers,
100154
statusCode: this._statusCode,
101-
body: typeof body === 'object' ? JSON.stringify(body) : (body && typeof body !== 'string' ? body.toString() : (body ? body : ''))
155+
body: encodeBody(body)
102156
}
103157

104158
// Trigger the callback function

0 commit comments

Comments
 (0)