UNPKG

6.93 kBMarkdownView Raw
1[ ![Codeship Status for smallwins/lambda](https://codeship.com/projects/2e4082e0-d808-0133-2035-1eae90b9310e/status?branch=master)](https://codeship.com/projects/143109)
2
3---
4
5## :raised_hands::seedling: @smallwins/lambda λ
6
7- Author your AWS Lambda functions as pure node style callbacks (aka errbacks)
8- Familiar middleware pattern for composition
9- Event sources like DynamoDB triggers and SNS topics too
10- Helpful npm scripts `lambda-create`, `lambda-list`, `lambda-deploy` and `lambda-invoke`
11
12#### :satellite::satellite::satellite: return json results :mailbox:
13
14Lets look at a vanilla AWS Lambda example. Here is a Lambda for performing a sum. Given `event.query.x = 1` it will return `{count:2}`.
15
16```javascript
17exports.handler = function sum(event, callback) {
18 var errors = []
19 if (typeof event.query === 'undefined') {
20 errors.push(ReferenceError('missing event.query'))
21 }
22 if (event.query && typeof event.query != 'object') {
23 errors.push(TypeError('event.query not an object'))
24 }
25 if (typeof event.query.x === 'undefined') {
26 errors.push(ReferenceError('event.query not an object'))
27 }
28 if (event.query.x && typeof event.query.x != 'number') {
29 errors.push(TypeError('event.query not an object'))
30 }
31 if (errors.length) {
32 // otherwise Error would return [{}, {}, {}, {}]
33 var err = errors.map(function(e) {return e.message})
34 context.fail(err)
35 }
36 else {
37 context.succeed({count:event.query.x + 1})
38 }
39}
40```
41
42A huge amount of this code is working around quirky parameter validations. Builtin `Error` needs manual serialization (and you still lose the stack trace). The latter part of the code uses the funky AWS `context` object.
43
44We can do better:
45
46```javascript
47var validate = require('@smallwins/validate')
48var lambda = require('@smallwins/lambda')
49
50function sum(event, callback) {
51 var schema = {
52 'query': {required:true, type:Object},
53 'query.x': {required:true, type:Number}
54 }
55 var errors = validate(event, schema)
56 if (errors) {
57 callback(errors)
58 }
59 else {
60 var result = {count:event.query.x + 1}
61 callback(null, result)
62 }
63}
64
65exports.handler = lambda(sum)
66```
67
68`@smallwins/validate` takes care of parameter validations. The callback style above enjoys symmetry with the rest of Node and will automatically serialize `Error`s into JSON friendly objects including any stack trace. All you need to do is wrap a vanilla node errback function in `lambda` which returns your function with an AWS Lambda friendly signature.
69
70#### :loop::loop::loop: easily chain dependant actions ala middleware :loop::loop::loop:
71
72Building on this foundation we can compose multiple errbacks into a Lambda. Lets compose a Lambda that:
73
74- Validates parameters
75- Checks for an authorized account
76- And then either returns data safely
77- Or if anything fails return JSON serialized `Error` array
78
79```javascript
80var validate = require('@smallwins/validate')
81var lambda = require('@smallwins/lambda')
82
83function valid(event, callback) {
84 var schema = {
85 'body': {required:true, type:Object},
86 'body.username': {required:true, type:String},
87 'body.password': {required:true, type:String}
88 }
89 validate(event, schema, callback)
90}
91
92function authorized(event, callback) {
93 var loggedIn = event.body.username === 'sutro' && event.body.password === 'cat'
94 if (!loggedIn) {
95 // err first
96 callback(Error('not found'))
97 }
98 else {
99 // successful login
100 event.account = {
101 loggedIn: loggedIn,
102 name: 'sutro furry pants'
103 }
104 callback(null, event)
105 }
106}
107
108function safe(event, callback) {
109 callback(null, {account:event.account})
110}
111
112exports.handler = lambda(valid, authorized, safe)
113```
114
115In the example above our functions are executed in series passing event through each invocation. `valid` will pass event to `authorized` which in turn passes it to `save`. Any `Error` returns immediately so if we make it the last function we just send back the resulting account data. Clean!
116
117#### :point_right: save a record from a dynamodb trigger :floppy_disk:
118
119AWS DynamoDB can invoke a Lambda function if anything happens to a table.
120
121```javascript
122var lambda = require('@smallwins/lambda')
123
124function save(record, callback) {
125 console.log('save a version ', record)
126 callback(null, record)
127}
128
129exports.handler = lambda.sources.dynamo.save(save)
130```
131
132#### :love_letter: api :thought_balloon: :sparkles:
133
134- `lambda(...fns)`
135- `lambda.sources.dynamo.all(...fns)`
136- `lambda.sources.dynamo.save(...fns)`
137- `lambda.sources.dynamo.insert(...fns)`
138- `lambda.sources.dynamo.modify(...fns)`
139- `lambda.sources.dynamo.remove(...fns)`
140
141A handler looks something like this
142
143```javascript
144function handler(event, callback) {
145 // process event, use to pass data
146 var result = {ok:true, event:event}
147 callback(null, result)
148}
149```
150
151#### :heavy_exclamation_mark: regarding errors :x::interrobang:
152
153Good error handling makes your programs far easier to maintain. (This is a good guide.)[https://www.joyent.com/developers/node/design/errors]. When using `@smallwins/lambda` always use `Error` type as the first parameter to callback:
154
155```javascript
156function fails(event, callback) {
157 callback(Error('something went wrong')
158}
159```
160
161Or an `Error` array:
162
163```javascript
164function fails(event, callback) {
165 callback([
166 Error('missing email'),
167 Error('missing password')
168 ])
169}
170```
171
172`@smallwins/lambda` serializes error into Slack RPC style JSON making them easy to work from API Gateway:
173
174```javascript
175{
176 ok: false,
177 errors: [
178 {name:'Error', message:'missing email', stack'...'},
179 {name:'Error', message:'missing password', stack'...'}
180 ]
181}
182```
183
184#### <kbd>#! scripting api</kbd> :memo:
185
186`@smallwins/lambda` includes some helpful automation code perfect for npm scripts. If you have a project that looks like this:
187
188```
189project-of-lambdas/
190 |-test/
191 |-src/
192 | '-lambdas/
193 | |-signup/
194 | | |-index.js
195 | | |-test.js
196 | | '-package.json
197 | |-login/
198 | '-logout/
199 '-package.json
200
201```
202
203And a `package.json` like this:
204
205```javascript
206{
207 "name":"project-of-lambdas",
208 "scripts": {
209 "create":"AWS_PROFILE=smallwins lambda-create",
210 "list":"AWS_PROFILE=smallwins lambda-list",
211 "deploy":"AWS_PROFILE=smallwins lambda-deploy"
212 "invoke":"AWS_PROFILE=smallwins lambda-invoke"
213 }
214}
215```
216
217- :point_right: <kbd>npm run create src/lambdas/forgot</kbd> creates a new lambda
218- :point_right: <kbd>npm run list</kbd> lists all deployed lambdas
219- :point_right: <kbd>npm run deploy src/lambdas/signup brian</kbd> deploys the lambda with the alias `brian`
220- :point_right:<kbd>npm run invoke src/lambdas/login brian '{"email":"b@brian.io", "pswd":"..."}'</kbd> to invoke a lambda
221
222The `./scripts/invoke.js` is also a module and useful for testing.
223
224```
225var invoke = require('@smallwins/lambda/scripts/invoke')
226invoke('path/to/lambda', alias, payload, (err, response)=> {
227 console.log(err, response)
228})
229```
230