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: λ returning json results :mailbox:
|
13 |
|
14 | Here is a vanilla AWS Lambda example for performing a sum. Given `event.query.x = 1` it will return `{count:2}`.
|
15 |
|
16 | ```javascript
|
17 | exports.handler = function sum(event, context) {
|
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 |
|
42 | A huge amount of this code is working around quirky parameter validation. 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 |
|
44 | We can do better:
|
45 |
|
46 | ```javascript
|
47 | var validate = require('@smallwins/validate')
|
48 | var lambda = require('@smallwins/lambda')
|
49 |
|
50 | function 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 |
|
65 | exports.handler = lambda(sum)
|
66 | ```
|
67 |
|
68 | `@smallwins/validate` takes care of parameter validation. 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 |
|
72 | Building 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 returns data safely
|
77 | - Or if anything fails return JSON serialized `Error` array
|
78 |
|
79 | ```javascript
|
80 | var validate = require('@smallwins/validate')
|
81 | var lambda = require('@smallwins/lambda')
|
82 |
|
83 | function 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 |
|
92 | function 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 |
|
108 | function safe(event, callback) {
|
109 | callback(null, {account:event.account})
|
110 | }
|
111 |
|
112 | exports.handler = lambda(valid, authorized, safe)
|
113 | ```
|
114 |
|
115 | In 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 | #### :floppy_disk: save a record from a dynamodb trigger :boom::gun:
|
118 | AWS DynamoDB can invoke a Lambda function if anything happens to a table.
|
119 |
|
120 | ```javascript
|
121 | var lambda = require('@smallwins/lambda')
|
122 |
|
123 | function save(record, callback) {
|
124 | console.log('save a version ', record)
|
125 | callback(null, record)
|
126 | }
|
127 |
|
128 | exports.handler = lambda.sources.dynamo.save(save)
|
129 | ```
|
130 |
|
131 | #### :love_letter: api :thought_balloon: :sparkles:
|
132 |
|
133 | - `lambda(...fns)`
|
134 | - `lambda.sources.dynamo.all(...fns)`
|
135 | - `lambda.sources.dynamo.save(...fns)`
|
136 | - `lambda.sources.dynamo.insert(...fns)`
|
137 | - `lambda.sources.dynamo.modify(...fns)`
|
138 | - `lambda.sources.dynamo.remove(...fns)`
|
139 |
|
140 | A handler looks something like this
|
141 |
|
142 | ```javascript
|
143 | function handler(event, callback) {
|
144 | // process event, use to pass data
|
145 | var result = {ok:true, event:event}
|
146 | callback(null, result)
|
147 | }
|
148 | ```
|
149 |
|
150 | #### :heavy_exclamation_mark: regarding errors :x::interrobang:
|
151 |
|
152 | Good 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:
|
153 |
|
154 | ```javascript
|
155 | function fails(event, callback) {
|
156 | callback(Error('something went wrong')
|
157 | }
|
158 | ```
|
159 |
|
160 | Or an `Error` array:
|
161 |
|
162 | ```javascript
|
163 | function fails(event, callback) {
|
164 | callback([
|
165 | Error('missing email'),
|
166 | Error('missing password')
|
167 | ])
|
168 | }
|
169 | ```
|
170 |
|
171 | `@smallwins/lambda` serializes error into Slack RPC style JSON making them easy to work from API Gateway:
|
172 |
|
173 | ```javascript
|
174 | {
|
175 | ok: false,
|
176 | errors: [
|
177 | {name:'Error', message:'missing email', stack'...'},
|
178 | {name:'Error', message:'missing password', stack'...'}
|
179 | ]
|
180 | }
|
181 | ```
|
182 |
|
183 | #### <kbd>#! scripting api</kbd> :memo:
|
184 |
|
185 | `@smallwins/lambda` includes some helpful automation code perfect for npm scripts. If you have a project that looks like this:
|
186 |
|
187 | ```
|
188 | project-of-lambdas/
|
189 | |-test/
|
190 | |-src/
|
191 | | '-lambdas/
|
192 | | |-signup/
|
193 | | | |-index.js
|
194 | | | |-test.js
|
195 | | | '-package.json
|
196 | | |-login/
|
197 | | '-logout/
|
198 | '-package.json
|
199 |
|
200 | ```
|
201 |
|
202 | And a `package.json` like this:
|
203 |
|
204 | ```javascript
|
205 | {
|
206 | "name":"project-of-lambdas",
|
207 | "scripts": {
|
208 | "create":"AWS_PROFILE=smallwins lambda-create",
|
209 | "list":"AWS_PROFILE=smallwins lambda-list",
|
210 | "deploy":"AWS_PROFILE=smallwins lambda-deploy",
|
211 | "invoke":"AWS_PROFILE=smallwins lambda-invoke",
|
212 | "deps":"AWS_PROFILE=smallwins lambda-deps"
|
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 | - :point_right:<kbd>npm run deps src/lambdas/*</kbd> for a report of all your lambda deps
|
222 |
|
223 | The `./scripts/invoke.js` is also a module and can be useful for testing.
|
224 |
|
225 | ```javscript
|
226 | var invoke = require('@smallwins/lambda/scripts/invoke')
|
227 |
|
228 | invoke('path/to/lambda', alias, payload, (err, response)=> {
|
229 | console.log(err, response)
|
230 | })
|
231 | ```
|
232 |
|