├── .gitignore ├── .npmignore ├── _arc.js ├── _dynamo.js ├── _lambda.js ├── _local.js ├── _sns.js ├── contributing.md ├── index.js ├── package.json ├── readme.md └── test ├── dynamo-test.js ├── lambda-custom-format-result-test.js ├── lambda-fail-test.js ├── lambda-local-test.js ├── lambda-sources-test.js ├── lambda-success-test.js ├── lambda-test.js ├── mock.json ├── sns-mock.json └── sns-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | foo-baz/ 3 | bundle.js 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | .gitignore 3 | _dynamo.js 4 | _lambda.js 5 | _local.js 6 | index.js 7 | -------------------------------------------------------------------------------- /_arc.js: -------------------------------------------------------------------------------- 1 | var errback = require('serialize-error') 2 | var lambda = require('./_lambda') 3 | 4 | // 5 | // no errors ever just serialize everything to a {json} payload 6 | // for api gateway endpoints generated w arc-create 7 | // 8 | // usage: 9 | // 10 | // exports.handler = lambda.arc.json(valid, registered) 11 | // 12 | module.exports = function _arcJSON() { 13 | var fns = [].slice.call(arguments) 14 | return lambda(fns, function _fmt(err, json, context) { 15 | if (err) { 16 | json = { 17 | errors: (Array.isArray(err)? err : [err]).map(errback) 18 | } 19 | } 20 | context.succeed({json}) 21 | }) 22 | } 23 | 24 | -------------------------------------------------------------------------------- /_dynamo.js: -------------------------------------------------------------------------------- 1 | var parallel = require('run-parallel') 2 | /** 3 | * var trigger = require('aws-dynamodb-lambda-trigger/lambda') 4 | * 5 | * function onInsert(record, callback) { 6 | * console.log(record) 7 | * callback(null, record) // errback style; results passed to context.succeed 8 | * } 9 | * 10 | * module.exports = trigger.insert(onInsert) 11 | */ 12 | 13 | function __trigger(types, handler) { 14 | return function __lambdaSignature(evt, ctx) { 15 | // dynamo triggers send batches of records so we're going to create a handler for each one 16 | var handlers = evt.Records.map(function(record) { 17 | // for each record we construct a handler function 18 | return function __actualHandler(callback) { 19 | // if isInvoking we invoke the handler with the record 20 | var isInvoking = types.indexOf(record.eventName) > -1 21 | if (isInvoking) { 22 | handler(record, callback) 23 | } 24 | else { 25 | callback() // if not we just call the continuation (callback) 26 | } 27 | } 28 | }) 29 | // executes the handlers in parallel 30 | parallel(handlers, function __processedRecords(err, results) { 31 | if (err) { 32 | ctx.fail(err) 33 | } 34 | else { 35 | ctx.succeed(results) 36 | } 37 | }) 38 | } 39 | } 40 | 41 | module.exports = { 42 | insert: __trigger.bind({}, ['INSERT']), 43 | modify: __trigger.bind({}, ['MODIFY']), 44 | remove: __trigger.bind({}, ['REMOVE']), 45 | all: __trigger.bind({}, ['INSERT', 'MODIFY', 'REMOVE']), 46 | save: __trigger.bind({}, ['INSERT', 'MODIFY']), 47 | change: __trigger.bind({}, ['INSERT', 'REMOVE']) 48 | } 49 | -------------------------------------------------------------------------------- /_lambda.js: -------------------------------------------------------------------------------- 1 | var waterfall = require('run-waterfall') 2 | var isArray = Array.isArray 3 | var isFunction = require('lodash.isfunction') 4 | var reject = require('lodash.reject') 5 | var errback = require('serialize-error') 6 | 7 | module.exports = function lambda() { 8 | 9 | var firstRun = true // important to keep this here in this closure 10 | var args = [].slice.call(arguments, 0) // grab the args 11 | 12 | // fail loudly for programmer not passing anything 13 | if (args.length === 0) { 14 | throw Error('lambda requires at least one callback function') 15 | } 16 | 17 | // check for lambda([], (err, result)=>) sig 18 | var customFormatter = isArray(args[0]) && isFunction(args[1]) 19 | var fmt = customFormatter? args[1] : false 20 | var fns = fmt? args[0] : args 21 | 22 | // we only deal in function values around here 23 | var notOnlyFns = reject(fns, isFunction).length > 0 24 | if (notOnlyFns) { 25 | throw Error('bad argument found: lambda(...fns) or lambda([...],(err, result)=>)') 26 | } 27 | 28 | // returns a lambda sig 29 | return function(event, context) { 30 | 31 | // this is to avoid warm start (sometimes lambda containers are cached … yeaaaaah.) 32 | if (firstRun) { 33 | fns.unshift(function(callback) { 34 | callback(null, event) 35 | }) 36 | firstRun = false 37 | } 38 | else { 39 | // mutates! wtf. remove the cached callback 40 | fns.shift() 41 | // add the fresh event 42 | fns.unshift(function(callback) { 43 | callback(null, event) 44 | }) 45 | } 46 | 47 | // asummptions: 48 | // - err should be an array of Errors 49 | // - because lambda deals in json we need to serialize them 50 | function formatter(err, result) { 51 | if (fmt) { 52 | fmt(err, result, context) 53 | } 54 | else { 55 | if (err) { 56 | result = { 57 | ok: false, 58 | errors: (isArray(err)? err : [err]).map(errback) 59 | } 60 | } 61 | else { 62 | if (typeof result === 'undefined') result = {} 63 | result.ok = true 64 | } 65 | // deliberate use context.succeed; 66 | // there is no (good) use case for the (current) context.fail behavior 67 | // (but happy to discuss in an issue)! 68 | context.succeed(result) 69 | } 70 | } 71 | 72 | // the real worker here 73 | waterfall(fns, formatter) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /_local.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * var lambda = require('@mallwins/lambda') 4 | * 5 | * var fn = lambda(function (event, data) { 6 | * callback(null, {hello:'world'}) 7 | * }) 8 | * 9 | * // fake run locally 10 | * lambda.local(fn, fakeEvent, function done(err, result) { 11 | * if (err) { 12 | * console.error(err) 13 | * } 14 | * else { 15 | * console.log(result) // logs: {ok:true, hello:'world'} 16 | * } 17 | * }) 18 | */ 19 | module.exports = function local(fn, event, callback) { 20 | var context = { 21 | succeed: function offlineSucceed(x) { 22 | if (x.ok) { 23 | callback(null, x) 24 | } 25 | else { 26 | callback(x.errors) 27 | } 28 | } 29 | } 30 | fn(event, context) 31 | } 32 | -------------------------------------------------------------------------------- /_sns.js: -------------------------------------------------------------------------------- 1 | var parallel = require('run-parallel') 2 | /** 3 | * var lambda = require('@smallwins/lambda') 4 | * 5 | * function msg(record, callback) { 6 | * console.log(record) 7 | * callback(null, record) // errback style; results passed to context.succeed 8 | * } 9 | * 10 | * module.exports = lambda.trigger.sns(msg) 11 | */ 12 | module.exports = function _sns(fn) { 13 | return function __lambdaSignature(evt, ctx) { 14 | // sns triggers send batches of records 15 | // so we're going to create a handler for each one 16 | // and execute them in parallel 17 | parallel(evt.Records.map(function _iterator(record) { 18 | // for each record we construct a handler function 19 | return function __actualHandler(callback) { 20 | try { 21 | fn(JSON.parse(record.Sns.Message), callback) 22 | } 23 | catch(e) { 24 | callback(e) 25 | } 26 | } 27 | }), 28 | function __processedRecords(err, results) { 29 | if (err) { 30 | ctx.fail(err) 31 | } 32 | else { 33 | ctx.succeed(results) 34 | } 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | ## First: go read the [Begin Code of Conduct](https://github.com/smallwins/policy/blob/master/begin-community-code-of-conduct.md) 3 | 4 | ### Agreement to the Begin Community Code of Conduct 5 | By participating and contributing to the Small Wins (aka Begin) community -- including, but not limited to its open source projects, any related online venues such as Github, Slack, and in-person events, etc. -- you agree to the [Begin Code of Conduct](https://github.com/smallwins/policy/blob/master/begin-community-code-of-conduct.md), found at the [Begin Policy archive](https://github.com/smallwins/policy). Lack of familiarity with this Code of Conduct is not an excuse for not adhering to it. 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var lambda = require('./_lambda') 2 | var dynamo = require('./_dynamo') 3 | var local = require('./_local') 4 | var sns = require('./_sns') 5 | var json = require('./_arc') 6 | 7 | lambda.local = local 8 | lambda.triggers = {dynamo, sns} 9 | lambda.arc = {} 10 | lambda.arc.json = json 11 | 12 | module.exports = lambda 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@smallwins/lambda", 3 | "version": "6.1.1", 4 | "description": "Author your AWS Lambda functions as node style errbacks.", 5 | "main": "bundle", 6 | "scripts": { 7 | "test": "tape test/**-test.js | tap-spec", 8 | "build": "browserify index.js -t bubleify --standalone utils --node | uglifyjs -c > bundle.js", 9 | "prepublish": "npm test && npm run build" 10 | }, 11 | "keywords": [ 12 | "aws", 13 | "lambda", 14 | "aws lambda" 15 | ], 16 | "author": "Brian LeRoux ", 17 | "license": "Apache-2", 18 | "devDependencies": { 19 | "aws-sdk": "^2.2.47", 20 | "browserify": "^13.1.1", 21 | "bubleify": "^0.6.0", 22 | "tap-spec": "^4.1.1", 23 | "tape": "^4.5.1", 24 | "uglifyjs": "^2.4.10", 25 | "lodash.isfunction": "^3.0.8", 26 | "lodash.reject": "^4.6.0", 27 | "run-parallel": "^1.1.6", 28 | "run-waterfall": "^1.1.3", 29 | "serialize-error": "^2.0.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 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 | ## @smallwins/lambda :seedling::raised_hands:λ 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` (and more) 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 vanilla AWS Lambda code is working around quirky parameter validation. API Gateway gives you control over the parameters you can expect but this still means one or more of: headers, querystring, form body, or url parameters. Event source style lambdas are not much better because you can often still get differing payloads from different origin sources. In the example above we are validating *one* querystring parameter `x`. Imagine a big payload! 😮 43 | 44 | Worse still, writing a good program we want to use JavaScript's builtin `Error` but it still needs manual serialization (and you still lose the stack trace). The latter part of this vanilla code uses the funky AWS `context` object. 45 | 46 | We can do better: 47 | 48 | ```javascript 49 | var validate = require('@smallwins/validate') 50 | var lambda = require('@smallwins/lambda') 51 | 52 | function sum(event, callback) { 53 | var schema = { 54 | 'query': {required:true, type:Object}, 55 | 'query.x': {required:true, type:Number} 56 | } 57 | var errors = validate(event, schema) 58 | if (errors) { 59 | callback(errors) 60 | } 61 | else { 62 | var result = {count:event.query.x + 1} 63 | callback(null, result) 64 | } 65 | } 66 | 67 | exports.handler = lambda(sum) 68 | ``` 69 | 70 | `@smallwins/validate` cleans up 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 your node style function in `lambda` which returns your function with an AWS Lambda friendly signature. 71 | 72 | #### :loop::loop::loop: easily chain dependant actions ala middleware :loop::loop::loop: 73 | 74 | Building on this foundation we can compose multiple functions into a single Lambda. It is very common to want to run functions in series. Lets compose a Lambda that: 75 | 76 | - Validates parameters 77 | - Checks for an authorized account 78 | - And then returns data safely 79 | - Or if anything fails return JSON serialized `Error` array 80 | 81 | ```javascript 82 | var validate = require('@smallwins/validate') 83 | var lambda = require('@smallwins/lambda') 84 | 85 | function valid(event, callback) { 86 | var schema = { 87 | 'body': {required:true, type:Object}, 88 | 'body.username': {required:true, type:String}, 89 | 'body.password': {required:true, type:String} 90 | } 91 | validate(event, schema, callback) 92 | } 93 | 94 | function authorized(event, callback) { 95 | var loggedIn = event.body.username === 'sutro' && event.body.password === 'cat' 96 | if (!loggedIn) { 97 | // err first 98 | callback(Error('not found')) 99 | } 100 | else { 101 | // successful login 102 | event.account = { 103 | loggedIn: loggedIn, 104 | name: 'sutro furry pants' 105 | } 106 | callback(null, event) 107 | } 108 | } 109 | 110 | function safe(event, callback) { 111 | callback(null, {account:event.account}) 112 | } 113 | 114 | exports.handler = lambda(valid, authorized, safe) 115 | ``` 116 | 117 | 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! 118 | 119 | #### :floppy_disk: save a record from a dynamodb trigger :boom::gun: 120 | 121 | AWS DynamoDB triggers invoke a Lambda function if anything happens to a table. The payload is usually a big array of records. `@smallwins/lambda` allows you to focus on processing a single record but executes the function in parallel on all the results in the Dynamo invocation. 122 | 123 | ```javascript 124 | var lambda = require('@smallwins/lambda') 125 | 126 | function save(record, callback) { 127 | console.log('save a version ', record) 128 | callback(null, record) 129 | } 130 | 131 | exports.handler = lambda.triggers.dynamo.save(save) 132 | ``` 133 | 134 | #### :bookmark: respond to a message published on sns 135 | 136 | Its very common to compose your application events using AWS SNS. `@smallwins/lambda` runs in parallel over the records in the trigger, similar to the Dynamo. 137 | 138 | ```javascript 139 | // somewhere in your codebase you'll want to trigger a lambda 140 | var aws = require('aws-sdk') 141 | var sns = new aws.SNS 142 | 143 | sns.publish({ 144 | Message: JSON.stringify({hello:'world'}), 145 | TopicArn: 'arn:aws:sns:us-east-1' 146 | }, console.log) 147 | ``` 148 | 149 | ```javascript 150 | // then, in your lambda 151 | var lambda = require('@smallwins/lambda') 152 | 153 | function msg(message, callback) { 154 | console.log('received msg ', message) // logs {hello:"world"} 155 | callback(null, message) 156 | } 157 | 158 | exports.handler = lambda.triggers.sns(msg) 159 | ``` 160 | 161 | ## :love_letter: api :thought_balloon::sparkles: 162 | 163 | - `lambda(...fns)` create a Lambda that returns a serialized json result `{ok:true|false}` 164 | - `lambda([fns], callback)` create a Lambda and handle result with your own errback formatter 165 | - `lambda.local(fn, fakeEvent, (err, result)=>)` run a Lambda locally offline by faking the event obj 166 | - `lambda.triggers.dynamo.insert(fn)` run on INSERT only 167 | - `lambda.triggers.dynamo.modify(fn)` run on MODIFY only 168 | - `lambda.triggers.dynamo.remove(fn)` run on REMOVE only 169 | - `lambda.triggers.dynamo.all(fn)` run on INSERT, MODIFY and REMOVE 170 | - `lambda.triggers.dynamo.save(fn)` run on INSERT and MODIFY 171 | - `lambda.triggers.dynamo.change(fn)` run on INSERT and REMOVE 172 | - `lambda.triggers.sns(fn)` run for every sns trigger invocation; expects `record.Sns.Message` to be a serialized JSON payload 173 | 174 | A handler looks something like this: 175 | 176 | ```javascript 177 | function handler(event, callback) { 178 | // process event, use to pass data 179 | var result = {ok:true, event:event} 180 | callback(null, result) 181 | } 182 | ``` 183 | 184 | #### :heavy_exclamation_mark: regarding errors :x::interrobang: 185 | 186 | Good error handling makes programs easier to maintain. [This is a great guide digging in more.](https://www.joyent.com/developers/node/design/errors) When using `@smallwins/lambda` always use `Error` type as the first parameter to the callback: 187 | 188 | ```javascript 189 | function fails(event, callback) { 190 | callback(Error('something went wrong') 191 | } 192 | ``` 193 | 194 | Or an `Error` array: 195 | 196 | ```javascript 197 | function fails(event, callback) { 198 | callback([ 199 | Error('missing email'), 200 | Error('missing password') 201 | ]) 202 | } 203 | ``` 204 | 205 | `@smallwins/lambda` serializes errors into Slack RPC style JSON. Easier to work with from API Gateway: 206 | 207 | ```javascript 208 | { 209 | ok: false, 210 | errors: [ 211 | {name:'Error', message:'missing email', stack'...'}, 212 | {name:'Error', message:'missing password', stack'...'} 213 | ] 214 | } 215 | ``` 216 | 217 | #### #! automatations :memo: 218 | 219 | `@smallwins/lambda` includes some helpful automation code perfect for npm scripts. If you have a project that looks like this: 220 | 221 | ``` 222 | project-of-lambdas/ 223 | |-test/ 224 | |-src/ 225 | | '-lambdas/ 226 | | |-signup/ 227 | | | |-index.js 228 | | | |-test.js 229 | | | '-package.json <--- name property should equal the deployed lambda name 230 | | |-login/ 231 | | '-logout/ 232 | '-package.json 233 | 234 | ``` 235 | 236 | And a `package.json` like this: 237 | 238 | ```javascript 239 | { 240 | "name":"project-of-lambdas", 241 | "scripts": { 242 | "create":"AWS_PROFILE=smallwins lambda-create", 243 | "list":"AWS_PROFILE=smallwins lambda-list", 244 | "deploy":"AWS_PROFILE=smallwins lambda-deploy", 245 | "invoke":"AWS_PROFILE=smallwins lambda-invoke", 246 | "local":"AWS_PROFILE=smallwins lambda-local", 247 | "deps":"AWS_PROFILE=smallwins lambda-deps", 248 | "log":"AWS_PROFILE=smallwins lambda-log" 249 | } 250 | } 251 | ``` 252 | 253 | You get: 254 | 255 | #### :fast_forward: npm run scripts :running::dash: 256 | 257 | This is :key:! Staying in the flow with your terminal by reducing hunts for information in the AWS Console. :shipit::chart_with_upwards_trend: 258 | 259 | - :point_right: npm run create src/lambdas/forgot creates a new lambda named `forgot` at `src/lambdas/forgot` 260 | - :point_right: npm run list lists all deployed lambdas and all their alias@versions 261 | - :point_right: npm run deploy src/lambdas/signup brian deploys the lambda with the alias `brian` 262 | - :point_right: npm run invoke src/lambdas/login brian '{"email":"b@brian.io", "pswd":"..."}' to remote invoke a deployed lambda 263 | - :point_right: npm run local src/lambdas/login brian '{"email":"b@brian.io", "pswd":"..."}' to locally invoke a lambda 264 | - :point_right: npm run deps src/lambdas/* for a report of all your lambda deps 265 | - :point_right: npm run log src/lambdas/logout to view the cloudwatch invocation logs for that lambda (remote `console.log` statements show up here) 266 | 267 | _Note: these scripts assume each lambda has it's own nested `package.json` file with a `name` property that matches the lambda name._ 268 | 269 | ### testing :white_check_mark: 270 | 271 | You can invoke a Lambda locally with a mock payload using `lambda.local`. Say you have this lambda function: 272 | 273 | ```javascript 274 | // always-ok.js 275 | var lambda = require('@smallwins/lambda') 276 | 277 | function fakeFn(event, callback) { 278 | callback(null, Object.assign({hello:'world'}, event)) 279 | } 280 | 281 | exports.handler = lambda(fakeFn) 282 | ``` 283 | 284 | You can imagine the test: 285 | 286 | ```javascript 287 | // always-test.js 288 | var fn = require('./always-ok').handler 289 | 290 | lambda.local(fn, {fake:'payload'}, console.log) 291 | // logs {hello:'world', fake:'payload', ok:true} 292 | ``` 293 | 294 | `./scripts/invoke.js` is also a module and can be useful for testing. It will remotely invoke your lambda. 295 | 296 | ```javascript 297 | var invoke = require('@smallwins/lambda/scripts/invoke') 298 | 299 | invoke('path/to/lambda', alias, payload, (err, response)=> { 300 | console.log(err, response) 301 | }) 302 | ``` 303 | 304 | -------------------------------------------------------------------------------- /test/dynamo-test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var mock = require('./mock.json') 3 | var lambda = require('../').triggers.dynamo 4 | 5 | test('sanity', t=> { 6 | t.plan(4) 7 | t.ok(lambda, 'the thing') 8 | t.ok(lambda.insert, 'the thing can listen for insert') 9 | t.ok(lambda.modify, 'the thing can listen for modify') 10 | t.ok(lambda.remove, 'the thing can listen for delete') 11 | }) 12 | 13 | test('can return a lambda', t=> { 14 | t.plan(2) 15 | function testHandler(event, callback) { 16 | console.log('000 --- executing testHandler') 17 | callback(null, event) 18 | } 19 | var fn = lambda.all(testHandler) 20 | t.ok(fn, 'returned a fn') 21 | console.log(fn) 22 | var fakeEvent = { 23 | eventName: 'MODIFY', 24 | Records: [mock] 25 | } 26 | var fakeContext = { 27 | succeed(thing) { 28 | t.ok(thing, 'got a thing from insert') 29 | console.log('success called', thing) 30 | }, 31 | fail(thing) { 32 | console.log('fail called', thing) 33 | } 34 | } 35 | fn(fakeEvent, fakeContext) 36 | }) 37 | -------------------------------------------------------------------------------- /test/lambda-custom-format-result-test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var isArray = Array.isArray 3 | var lambda = require('../') 4 | 5 | /** 6 | * lambda function that always fails with an ugly error 7 | */ 8 | function raiseError(event, callback) { 9 | callback(Error('ugly error')) 10 | } 11 | 12 | /** 13 | * custom formatter example 14 | * 15 | * this formatter removes the stack trace from errors and otherwise just passes data thru 16 | * 17 | */ 18 | function cleanupErrors(err, results, context) { 19 | var res = results || {} 20 | if (err) { 21 | // handle single error or array of errors for good form 22 | err = (isArray(err)? err : [err]) 23 | // remove stack trace 24 | res.errors = err.map(e=>e.message) 25 | } 26 | context.succeed(res) 27 | } 28 | 29 | // create the lambda and then use lambda.local to test it 30 | var fn = lambda([raiseError], cleanupErrors) 31 | 32 | test('removes stack trace', t=> { 33 | t.plan(1) 34 | lambda.local(fn, {}, (err, results)=> { 35 | if (err) { 36 | t.ok(err, 'got the error and stack traces have been removed') 37 | } 38 | else { 39 | t.fail(results, 'should have an error') 40 | } 41 | console.log(err, results) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /test/lambda-fail-test.js: -------------------------------------------------------------------------------- 1 | var isArray = Array.isArray 2 | var lambda = require('../') 3 | var test = require('tape') 4 | 5 | test('can invoke a failful lambda', t=> { 6 | t.plan(1) 7 | // always fails 8 | function tester(event, callback) { 9 | callback(Error('wtf')) 10 | } 11 | var fail = lambda(tester) 12 | fail({}, { 13 | succeed: function fakeSucceed(v) { 14 | t.ok(isArray(v.errors), 'got an Errors array') 15 | console.log('faked fail called with ', v) 16 | } 17 | }) 18 | }) 19 | 20 | -------------------------------------------------------------------------------- /test/lambda-local-test.js: -------------------------------------------------------------------------------- 1 | var lambda = require('../') 2 | var test = require('tape') 3 | 4 | function goodLambda(event, callback) { 5 | var result = Object.assign({good:'times'}, event) 6 | callback(null, result) 7 | } 8 | 9 | function badLambda(event, callback) { 10 | callback(Error('fail')) 11 | } 12 | 13 | var good = lambda(goodLambda) 14 | var bad = lambda(badLambda) 15 | 16 | test('the good', t=> { 17 | t.plan(2) 18 | var mockEvent = {some:'param'} 19 | lambda.local(good, mockEvent, (err, result)=> { 20 | if (err) { 21 | t.fail(err) 22 | } 23 | else { 24 | t.ok(result.ok, 'got ok on result') 25 | t.equal(result.some, 'param', 'params pass thru') 26 | } 27 | }) 28 | }) 29 | 30 | test('the bad', t=> { 31 | t.plan(2) 32 | var mockEvent = {some:'param'} 33 | lambda.local(bad, mockEvent, (err, result)=> { 34 | if (err) { 35 | t.equal(err.length, 1, 'got err array') 36 | t.equal(err[0].message, 'fail', 'got fail err') 37 | } 38 | else { 39 | t.fail(err) 40 | } 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /test/lambda-sources-test.js: -------------------------------------------------------------------------------- 1 | var lambda = require('../') 2 | var test = require('tape') 3 | 4 | function handler(event, callback) { 5 | event.ok = true 6 | callback(null, event) 7 | } 8 | 9 | // get an instance of the lambda 10 | var insert = lambda.triggers.dynamo.insert(handler) 11 | var modify = lambda.triggers.dynamo.modify(handler) 12 | var remove = lambda.triggers.dynamo.remove(handler) 13 | var all = lambda.triggers.dynamo.all(handler) 14 | var save = lambda.triggers.dynamo.save(handler) 15 | 16 | test('insert trigger', t=> { 17 | t.plan(1) 18 | var event = { 19 | Records:[{one:1}, {two:2}], 20 | eventName:'INSERT' 21 | } 22 | var context = { 23 | succeed: function succeed(v) { 24 | t.ok(v, 'insert trigger ran') 25 | console.log(v) 26 | } 27 | } 28 | // invoke the lambda 29 | insert(event, context) 30 | }) 31 | 32 | test('modify trigger', t=> { 33 | t.plan(1) 34 | var event = { 35 | Records:[{three:3}, {four:4}], 36 | eventName:'MODIFY' 37 | } 38 | var context = { 39 | succeed: function succeed(v) { 40 | t.ok(v, 'modify trigger ran') 41 | console.log(v) 42 | } 43 | } 44 | // invoke the lambda 45 | modify(event, context) 46 | }) 47 | 48 | test('remove trigger', t=> { 49 | t.plan(1) 50 | var event = { 51 | Records:[{five:5}, {six:6}], 52 | eventName:'REMOVE' 53 | } 54 | var context = { 55 | succeed: function succeed(v) { 56 | t.ok(v, 'modify trigger ran') 57 | console.log(v) 58 | } 59 | } 60 | // invoke the lambda 61 | remove(event, context) 62 | }) 63 | 64 | test('all trigger', t=> { 65 | t.plan(1) 66 | var event = { 67 | Records:[{seven:5}, {eight:6}, {nine:9}], 68 | eventName:'REMOVE' 69 | } 70 | var context = { 71 | succeed: function succeed(v) { 72 | t.ok(v, 'all trigger ran') 73 | console.log(v) 74 | } 75 | } 76 | // invoke the lambda 77 | all(event, context) 78 | }) 79 | 80 | test('save trigger', t=> { 81 | t.plan(1) 82 | var event = { 83 | Records:[{ten:10}], 84 | eventName:'MODIFY' 85 | } 86 | var context = { 87 | succeed: function succeed(v) { 88 | t.ok(v, 'save trigger ran') 89 | console.log(v) 90 | } 91 | } 92 | // invoke the lambda 93 | save(event, context) 94 | }) 95 | 96 | test('fail trigger', t=> { 97 | t.plan(1) 98 | var event = { 99 | Records:[{ten:10}], 100 | eventName:'MODIFY' 101 | } 102 | var context = { 103 | succeed: function succeed(v) { 104 | t.ok(v, 'modify trigger ran and failed') 105 | console.log(v) 106 | } 107 | } 108 | // invoke the lambda 109 | var handler = lambda.triggers.dynamo.modify(function(event, callback) { 110 | callback(Error('test err')) 111 | }) 112 | handler(event, context) 113 | }) 114 | -------------------------------------------------------------------------------- /test/lambda-success-test.js: -------------------------------------------------------------------------------- 1 | var isArray = Array.isArray 2 | var lambda = require('../') 3 | var test = require('tape') 4 | 5 | test('can invoke a successful lambda', t=> { 6 | t.plan(1) 7 | // always succeeds 8 | function tester(event, callback) { 9 | event.allGood = true 10 | callback(null, event) 11 | } 12 | var fn = lambda(tester) 13 | fn({}, { 14 | succeed: function fakeSucceed(v) { 15 | t.ok(v.allGood, 'got the event!') 16 | console.log('fake succeed called with ', v) 17 | } 18 | }) 19 | }) 20 | 21 | -------------------------------------------------------------------------------- /test/lambda-test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var lambda = require('../') 3 | var isFunction = require('lodash.isfunction') 4 | var isArray = Array.isArray 5 | 6 | test('sanity', t=> { 7 | t.plan(1) 8 | t.ok(lambda, 'lambda exists') 9 | }) 10 | 11 | test('cannot give lambda bad params', t=> { 12 | t.plan(1) 13 | try { 14 | lambda() 15 | } 16 | catch(e) { 17 | t.ok(e, 'failed with bad params and we got a meaningful error') 18 | console.log(e) 19 | } 20 | }) 21 | 22 | test('can call lambda with one fn', t=> { 23 | t.plan(1) 24 | function tester(event, callback) { 25 | callback(null, event) 26 | } 27 | var fn = lambda(tester) 28 | t.ok(isFunction(fn), 'got a function back') 29 | }) 30 | 31 | test('can call lambda with greater than one fn', t=> { 32 | t.plan(1) 33 | function tester(event, callback) { 34 | callback(null, event) 35 | } 36 | function tester2(event, callback) { 37 | callback(null, event) 38 | } 39 | var fn = lambda(tester, tester2) 40 | t.ok(isFunction(fn), 'got a function back') 41 | }) 42 | -------------------------------------------------------------------------------- /test/mock.json: -------------------------------------------------------------------------------- 1 | { 2 | "eventID": "0123987423r282342lkjfwldkfjsdf24", 3 | "eventName": "MODIFY", 4 | "eventVersion": "1.1", 5 | "eventSource": "aws:dynamodb", 6 | "awsRegion": "us-east-1", 7 | "dynamodb": { 8 | "ApproximateCreationDateTime": 1469066940, 9 | "Keys": { 10 | "project-id": { 11 | "S": "7f84d97a-3842-40fb-b6f3-6029007b6593" 12 | }, 13 | "task-id": { 14 | "S": "a47e780f-8b78-4c28-b063-a8ff6e41af19" 15 | } 16 | }, 17 | "NewImage": { 18 | "title": { 19 | "S": "testing trigger event source" 20 | }, 21 | "project-id": { 22 | "S": "7f84d97a-3842-40fb-b6f3-6029007b6593" 23 | }, 24 | "task-id": { 25 | "S": "a47e780f-8b78-4c28-b063-a8ff6e41af19" 26 | } 27 | }, 28 | "OldImage": { 29 | "title": { 30 | "S": "testing the trigger for event source" 31 | }, 32 | "project-id": { 33 | "S": "7f84d97a-3842-40fb-b6f3-6029007b6593" 34 | }, 35 | "task-id": { 36 | "S": "a47e780f-8b78-4c28-b063-a8ff6e41af19" 37 | } 38 | }, 39 | "SequenceNumber": "951326100000000007678127701", 40 | "SizeBytes": 725, 41 | "StreamViewType": "NEW_AND_OLD_IMAGES" 42 | }, 43 | "eventSourceARN": "arn:aws:dynamodb:us-east-1:444444444444:table/stuff/stream/2016-03-12T04:07:09.571" 44 | } 45 | -------------------------------------------------------------------------------- /test/sns-mock.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "EventSource": "aws:sns", 5 | "EventVersion": "1.0", 6 | "EventSubscriptionArn": "arn:aws:sns", 7 | "Sns": { 8 | "Type": "Notification", 9 | "MessageId": "xxx", 10 | "TopicArn": "arn:aws:sns", 11 | "Subject": null, 12 | "Message": "{\"hello\":\"world\"}", 13 | "Timestamp": "2017-01-05T03:06:39.557Z", 14 | "SignatureVersion": "1", 15 | "Signature": "", 16 | "SigningCertUrl": "", 17 | "UnsubscribeUrl": "", 18 | "MessageAttributes": {} 19 | } 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /test/sns-test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var sns = require('../').triggers.sns 3 | var mock = require('./sns-mock.json') 4 | 5 | test('env', t=> { 6 | t.plan(1) 7 | t.ok(sns, 'sns exists in current scope') 8 | }) 9 | 10 | test('can return a lambda', t=> { 11 | t.plan(2) 12 | 13 | function testHandler(event, callback) { 14 | callback(null, event) 15 | } 16 | var fn = sns(testHandler) 17 | t.ok(fn, 'returned a fn') 18 | console.log(fn) 19 | 20 | fn(mock, { 21 | succeed(thing) { 22 | t.ok(thing, 'got json msg from sns') 23 | console.log('success called', thing) 24 | }, 25 | fail(thing) { 26 | console.log('fail called', thing) 27 | } 28 | }) 29 | }) 30 | --------------------------------------------------------------------------------