├── .gitignore ├── README.MD ├── countitems ├── event.json └── index.js ├── dataset ├── import.sh └── restaurants.json ├── email ├── .gitignore ├── Templates │ └── Restaurants.html ├── config.js ├── event.json ├── index.js ├── package.json └── zip.sh ├── exec_sf.sh ├── restaurants ├── .gitignore ├── event.json ├── index.js ├── ll.sh ├── package.json └── zip.sh ├── sms ├── .gitignore ├── config.js ├── event.json ├── index.js ├── package.json └── zip.sh └── statemachine.json /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.zip 3 | 4 | restaurants/ll_local.sh 5 | 6 | statemachine_local.json 7 | 8 | sms/_index.js 9 | 10 | email/ll_local.sh 11 | 12 | API_Gateway_TestInput.txt 13 | 14 | exec_sf_local.sh 15 | 16 | restaurants/claudia.json 17 | restaurants/package-lock.json 18 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # AWS Step Function Sample State Machine with MongoDB Atlas 2 | 3 | This repository provides the source code of 3 [AWS Lambda](https://aws.amazon.com/lambda/) functions written in Node.js and combined into one AWS Step Function State Machine to illustrate the workflow and orchestration capabilities of [AWS Step Functions](https://aws.amazon.com/step-functions). 4 | 5 | ![AWS Step Functions Visual Workflow](https://webassets.mongodb.com/_com_assets/cms/Step_Functions_Management_Console-wymgq6t8yf.png) 6 | 7 | ## Software Requirements 8 | 9 | In order to use this repository, you will need: 10 | 11 | 1. A local computer with [Node.js](https://nodejs.org) 12 | 2. An [Amazon Web Services](https://aws.amazon.com/) account 13 | 3. A [MongoDB Atlas](https://www.mongodb.com/atlas?jmp=adref) cluster (see this [video](https://youtu.be/_d8CBOtadRA) to get started with a free M0 cluster) 14 | 4. A [Twilio](https://www.twilio.com/) account. A trial account is fine, but you will need to add the phone numbers you'd like to send text messages to on the [Verified Called IDs page](https://www.twilio.com/console/phone-numbers/verified). 15 | 16 | __Note__: this repository was built and tested on Mac OS X Sierra and Node.js 6.9.4 17 | 18 | ## Setup 19 | 20 | ### MongoDB Atlas 21 | 22 | Since this Step Function relies on an existing dataset of restaurants, you must first import a sample dataset to your MongoDB Atlas cluster: 23 | 24 | 1. Customize the [import.sh](/dataset/import.sh) script to import the [restaurants.json](/dataset/restaurants.json) file. 25 | 1. Alternatively, you can download [this dataset](https://raw.githubusercontent.com/mongodb/docs-assets/primer-dataset/primer-dataset.json) (also used in the [MongoDB Shell Tutorial](https://docs.mongodb.com/getting-started/shell/import-data)) and follow the next steps below. 26 | 1. Edit the following command line by replacing the ``$`` parameters with your own values: 27 | ``mongoimport --host $ATLAS_CLUSTER_URI --ssl -u $ATLAS_ADMIN -p $ATLAS_PWD --authenticationDatabase admin --db travel --collection restaurants --drop --file primer-dataset.json`` 28 | 1. Run your customized ``mongoimport`` command above to import the dataset to your Atlas cluster. 29 | 1. (Optional) verify with [MongoDB Compass](https://www.mongodb.com/download-center?jmp=adref#compass) that your ``restaurants`` collection was properly imported. The [MongoDB Compass with Atlas](https://www.mongodb.com/blog/post/your-mongodb-atlas-toolkit-logging-into-mongodb-atlas-with-compass?jmp=adref) blog post might help you configure Compass with MongoDB Atlas. 30 | 31 | ### Amazon Web Services 32 | This repository is made of the following Lambda functions: 33 | 34 | - A **[GetRestaurants](https://github.com/raphaellondner-mongodb/aws-stepfunctions-samples/tree/master/restaurants)** function that queries a collection of restaurants stored in a [MongoDB Atlas](https://www.mongodb.com/atlas?jmp=adref) database. 35 | - A **[CountItems](https://github.com/raphaellondner-mongodb/aws-stepfunctions-samples/tree/master/countitems)** helper function whose role is solely to count the number of items in an array. 36 | - A **[SendBySMS](https://github.com/raphaellondner-mongodb/aws-stepfunctions-samples/tree/master/sms)** function that sends a text message using [SMS by Twilio](https://www.twilio.com/sms). 37 | - A **[SendByEmail](https://github.com/raphaellondner-mongodb/aws-stepfunctions-samples/tree/master/email)** function that sends an email using [AWS Simple Email Service](https://aws.amazon.com/ses). 38 | 39 | The __Common setup tasks__ below lists the tasks that you must perform for each of the 3 Lambda functions. 40 | 41 | The __Function-specific configuration tasks__ section below lists the tasks that are specific to each Lambda function. 42 | 43 | #### Common setup tasks 44 | 45 | For __each__ of the 4 Lambda functions listed above (except the CountItems function), please perform the following tasks: 46 | 47 | 1. Navigate to the root folder of the Lambda function (i.e. _restaurants_, _email_ and _sms_) and run ``npm install`` from a Terminal console to restore the required Node.js dependencies. 48 | 1. Run the ``zip.sh`` script to zip all the files required by AWS Lambda (you might have to run ``chmod 744 zip.sh`` first to allow the ``zip.sh`` file to run). This will generate an ``archive.zip`` file in every function folder. 49 | 1. Create a new AWS Lambda function and use the ``archive.zip`` file as the *Upload a .ZIP file* **Code entry type**. Refer to [this AWS Lambda tutorial](https://www.mongodb.com/blog/post/serverless-development-with-nodejs-aws-lambda-mongodb-atlas) for additional help on AWS Lambda development and deployment. 50 | 4. Increase the timeout to 5 or 6s (optional but strongly recommended) 51 | 52 | #### Function-specific configuration tasks 53 | 54 | Here are the specific configuration tasks you must make for each Lambda function: 55 | 56 | - **GetRestaurants** function: 57 | - Add a ``MONGODB_ATLAS_CLUSTER_URI`` environment variable with the value of your MongoDB Atlas connection string (see [this blog post](https://www.mongodb.com/blog/post/serverless-development-with-nodejs-aws-lambda-mongodb-atlas) for further details). 58 | - Copy/paste the contents of the [``event.json``](https://github.com/rlondner/aws-stepfunctions-samples/blob/master/restaurants/event.json) file in the Lambda *Input test event* window. 59 | - Note how the Node.js code is using [performance optimization enhancements](http://bit.ly/lambdaperf) to speed up queries after the first call to the Lambda function. 60 | - **CountItems** function: 61 | - Create a blank Lambda function and copy/paste the content of the [``index.js``](https://github.com/rlondner/aws-stepfunctions-samples/blob/master/countitems/index.js) file into the AWS Lambda code editor. 62 | - Copy/paste the contents of the [``event.json``](https://github.com/rlondner/aws-stepfunctions-samples/blob/master/countitems/index.js) file in the Lambda *Input test event* window. 63 | - **SendBySMS** function: 64 | - Add a ``TWILIO_ACCOUNT_SID`` environment variable with the value of your Twilio account ID. 65 | - Add a ``TWILIO_AUTH_TOKEN`` environment variable with the value of your Twilio Auth Token. 66 | - Add a ``TWILIO_PHONE_NUMBER`` environment variable with the value of your Twilio Phone Number. 67 | - Copy/paste the contents of the [``event.json``](https://github.com/rlondner/aws-stepfunctions-samples/blob/master/sms/event.json) file in the Lambda *Input test event* window. 68 | - **SendByEmail** function: 69 | - You must first set up [AWS Simple Email Service](https://aws.amazon.com/ses/) in order for it to accept your sender email address as well as the recipient adress(es) you are planning to use. 70 | - Create an S3 bucket and in that S3 bucket, create a *Templates* folder. Upload the [__*Templates/Restaurants.html*__](https://github.com/rlondner/aws-stepfunctions-samples/blob/master/email/Templates/Restaurants.html) file into this *Templates* bucket. 71 | - Add an ``S3_BUCKET`` environment variable with the value of the S3 bucket you created in the previous step. 72 | - Add an ``FROM_ADDRESS`` environment variable with the value of SES-verified email address you want to use as your sender email address. 73 | - Copy/paste the [``event.json``](https://github.com/rlondner/aws-stepfunctions-samples/blob/master/email/event.json) file as your test event input and at least update the ``emailTo`` attribute with an SES-verified email address (and potentially the ``firstnameTo`` attribute too). 74 | - Make sure the IAM role you use to run your Lambda function includes the following permissions (created as an inline or managed policy): 75 | 76 | ```json 77 | { 78 | "Version": "2012-10-17", 79 | "Statement": [ 80 | { 81 | "Effect": "Allow", 82 | "Action": [ 83 | "ses:SendEmail" 84 | ], 85 | "Resource": "*" 86 | }, 87 | { 88 | "Effect": "Allow", 89 | "Action": [ 90 | "s3:GetObject" 91 | ], 92 | "Resource": "*" 93 | } 94 | ] 95 | } 96 | ``` 97 | 98 | ### AWS Step Functions 99 | 100 | Once you have successfully created and tested the 4 Lambda functions above, head over (or go back) to the [AWS Step Functions setup and code walkthrough](http://bit.ly/mdbawssf) for the last mile! 101 | -------------------------------------------------------------------------------- /countitems/event.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "address": { 4 | "building": "235-237", 5 | "street": "West 48 Street" 6 | }, 7 | "borough": "Manhattan", 8 | "name": "La Masseria" 9 | }, 10 | { 11 | "address": { 12 | "building": "315", 13 | "street": "West 48 Street" 14 | }, 15 | "borough": "Manhattan", 16 | "name": "Maria'S Mont Blanc Restaurant" 17 | }, 18 | { 19 | "address": { 20 | "building": "654", 21 | "street": "9 Avenue" 22 | }, 23 | "borough": "Manhattan", 24 | "name": "Cara Mia" 25 | } 26 | ] -------------------------------------------------------------------------------- /countitems/index.js: -------------------------------------------------------------------------------- 1 | exports.handler = (event, context, callback) => { 2 | console.log(event.length) 3 | callback(null, event.length); 4 | }; -------------------------------------------------------------------------------- /dataset/import.sh: -------------------------------------------------------------------------------- 1 | ATLAS_PRIMARY="%CLUSTER_NAME%-shard-00-00-%SUFFIX%.mongodb.net:27017" 2 | ATLAS_ADMIN="%ATLAS_CLUSTER_ADMIN_USERNAME%" 3 | ATLAS_PWD="%ATLAS_CLUSTER_ADMIN_PASSWORD%" 4 | 5 | mongoimport --host $ATLAS_PRIMARY --ssl -u $ATLAS_ADMIN -p $ATLAS_PWD --authenticationDatabase admin --db travel --collection restaurants --drop --file restaurants.json -------------------------------------------------------------------------------- /email/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /email/Templates/Restaurants.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Matching Restaurants List 7 | 8 | 9 | 10 |

11 | Here is the list of restaurants matching your request! 12 |

13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {{#restaurants}} 21 | 22 | 23 | 24 | 25 | 26 | 27 | {{/restaurants}} 28 |
NameAddressZipBorough/City
{{name}}{{address.building}} {{address.street}}{{address.zipcode}}{{borough}}
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /email/config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var config = { 4 | "templateKey" : "Templates/Restaurants.html", 5 | "defaultSubject" : "Your list of restaurants" 6 | } 7 | 8 | module.exports = config -------------------------------------------------------------------------------- /email/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "firstnameTo": "Raphael", 3 | "emailTo": "raphael@example.com", 4 | "subject": "List of restaurants for {{firstnameTo}}", 5 | "restaurants": [ 6 | { 7 | "address": { 8 | "building": "235-237", 9 | "street": "West 48 Street" 10 | }, 11 | "borough": "Manhattan", 12 | "name": "La Masseria" 13 | }, 14 | { 15 | "address": { 16 | "building": "315", 17 | "street": "West 48 Street" 18 | }, 19 | "borough": "Manhattan", 20 | "name": "Maria'S Mont Blanc Restaurant" 21 | }, 22 | { 23 | "address": { 24 | "building": "654", 25 | "street": "9 Avenue" 26 | }, 27 | "borough": "Manhattan", 28 | "name": "Cara Mia" 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /email/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var aws = require('aws-sdk'); 4 | var Mustache = require('mustache'); 5 | 6 | exports.handler = function (event, context, callback) { 7 | 8 | console.log("Event: " + JSON.stringify(event)); 9 | var aws_region = process.env['AWS_DEFAULT_REGION']; 10 | if(aws_region == undefined) { 11 | aws_region = process.env['AWS_REGION_LOCAL']; 12 | } 13 | var s3_bucket = process.env['S3_BUCKET']; 14 | var from_address = process.env['FROM_ADDRESS']; 15 | 16 | aws.config.update({ region: aws_region }); 17 | 18 | var config = require('./config.js'); 19 | 20 | console.log('Loading template from ' + config.templateKey + ' in ' + s3_bucket); 21 | 22 | var s3 = new aws.S3(); 23 | // Read the template file from S3 24 | s3.getObject({ 25 | Bucket: s3_bucket, 26 | Key: config.templateKey 27 | }, function (err, data) { 28 | if (err) { 29 | // Error 30 | console.log(err, err.stack); 31 | context.fail('Internal Error: Failed to load template from s3.') 32 | } else { 33 | var templateBody = data.Body.toString(); 34 | 35 | //generate dynamic content based on Mustache tags 36 | var subject = Mustache.render(event.subject, event); 37 | var message = Mustache.render(templateBody, event); 38 | 39 | 40 | var params = { 41 | Destination: { 42 | ToAddresses: [ 43 | event.emailTo 44 | ] 45 | }, 46 | Message: { 47 | Subject: { 48 | Data: subject, 49 | Charset: 'UTF-8' 50 | } 51 | }, 52 | Source: from_address, 53 | ReplyToAddresses: [ 54 | from_address 55 | ] 56 | }; 57 | 58 | var fileExtension = config.templateKey.split(".").pop(); 59 | if (fileExtension.toLowerCase() == 'html') { 60 | params.Message.Body = { 61 | Html: { 62 | Data: message, 63 | Charset: 'UTF-8' 64 | } 65 | }; 66 | } else if (fileExtension.toLowerCase() == 'txt') { 67 | params.Message.Body = { 68 | Text: { 69 | Data: message, 70 | Charset: 'UTF-8' 71 | } 72 | }; 73 | } else { 74 | context.fail('Internal Error: Unrecognized template file extension: ' + fileExtension); 75 | return; 76 | } 77 | 78 | var ses = new aws.SES(); 79 | 80 | // Send the email 81 | ses.sendEmail(params, function (err, data) { 82 | if (err) { 83 | console.log(err, err.stack); 84 | callback('Internal Error: The email could not be sent. ' + err.message); 85 | } else { 86 | console.log(data); // successful response 87 | callback(null, 'The email was successfully sent to ' + event.emailTo); 88 | } 89 | }); 90 | } 91 | }); 92 | }; -------------------------------------------------------------------------------- /email/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "awslambda-email", 3 | "version": "1.0.0", 4 | "description": "An AWS Lambda function that sends an email through AWS SES (Simple Email Service)", 5 | "main": "email.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Raphael Londner", 10 | "license": "Apache-2.0", 11 | "dependencies": { 12 | "mustache": "^2.3.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /email/zip.sh: -------------------------------------------------------------------------------- 1 | zip -r archive.zip node_modules/ index.js config.js -------------------------------------------------------------------------------- /exec_sf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Courtesy of Paul Sears, Amazon Web Services 3 | 4 | EMAIL="test@example.com" 5 | PHONE="+15555555555" 6 | NAME="TestUser" 7 | STATEARN="arn:aws:states:us-west-2:ACCOUNT_ID:stateMachine:WhatsThisRestaurantAgain" 8 | API="https://API_GATEWAY_PREFIX.execute-api.us-west-2.amazonaws.com/STAGE_NAME/execution" 9 | STARTS=$1 10 | CU=$2 11 | ZIP=$3 12 | 13 | EXECUTIONNAME=`uuidgen` 14 | 15 | curl -X POST -d '{"input": "{ \"startsWith\": \"'$STARTS'\", \"cuisine\": \"'$CU'\", \"zipcode\": \"'$ZIP'\", \"phoneTo\": \"'$PHONE'\", \"firstnameTo\": \"'$NAME'\", \"emailTo\": \"'$EMAIL'\", \"subject\": \"List of restaurants for {{firstnameTo}}\" }","name": "'.$EXECUTIONNAME'","stateMachineArn": "'$STATEARN'"}' $API 16 | -------------------------------------------------------------------------------- /restaurants/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /restaurants/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "startsWith": "M", 3 | "cuisine": "Italian", 4 | "zipcode": "10036", 5 | "phoneTo": "+15555555555", 6 | "firstnameTo": "Raphael", 7 | "emailTo": "raphael@example.com", 8 | "subject": "List of restaurants for {{firstnameTo}}" 9 | } -------------------------------------------------------------------------------- /restaurants/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | //const AWS = require('aws-sdk'); 3 | 4 | var MongoClient = require('mongodb').MongoClient; 5 | 6 | //Performance optimization Step 1: declare the database connection object outside the handler method 7 | let cachedDb = null; 8 | 9 | let atlas_connection_uri = null; 10 | 11 | exports.handler = (event, context, callback) => { 12 | 13 | var uri = process.env['MONGODB_ATLAS_CLUSTER_URI']; 14 | 15 | console.log('remaining time =', context.getRemainingTimeInMillis()); 16 | console.log('functionName =', context.functionName); 17 | console.log('AWSrequestID =', context.awsRequestId); 18 | console.log('logGroupName =', context.logGroupName); 19 | console.log('logStreamName =', context.logStreamName); 20 | console.log('clientContext =', context.clientContext); 21 | 22 | //Performance optimization Step 2: set context.callbackWaitsForEmptyEventLoop to false to prevent the Lambda function from waiting for all resources (such as the database connection) to be released before returning it 23 | context.callbackWaitsForEmptyEventLoop = false; 24 | 25 | if (atlas_connection_uri == null) { 26 | atlas_connection_uri = uri; 27 | /* 28 | const kms = new AWS.KMS(); 29 | kms.decrypt({ CiphertextBlob: new Buffer(uri, 'base64') }, (err, data) => { 30 | if (err) { 31 | console.log('Decrypt error:', err); 32 | return callback(err); 33 | } 34 | 35 | atlas_connection_uri = data.Plaintext.toString('ascii'); 36 | }); 37 | */ 38 | } 39 | processEvent(event, context, callback); 40 | }; 41 | 42 | function connectToDatabase(uri) { 43 | 44 | //Performance optimization Step 3: test that database connection exists and is valid 45 | //before re-using it 46 | if (cachedDb && cachedDb.serverConfig.isConnected()) { 47 | console.log('=> using cached database instance'); 48 | return Promise.resolve(cachedDb); 49 | } 50 | const dbName = 'travel'; 51 | return MongoClient.connect(uri) 52 | .then(client => { cachedDb = client.db(dbName); return cachedDb; }); 53 | } 54 | 55 | function processEvent(event, context, callback) { 56 | connectToDatabase(atlas_connection_uri) 57 | .then(db => queryDatabase(db, event)) 58 | .then(result => { 59 | console.log('query results: ', result); 60 | callback(null, result); 61 | }) 62 | .catch(err => { 63 | console.log('=> an error occurred: ', err); 64 | callback(err); 65 | }); 66 | } 67 | 68 | function queryDatabase(db, event) { 69 | var jsonContents = JSON.parse(JSON.stringify(event)); 70 | 71 | //handling API Gateway input where the event is embedded into the 'body' element 72 | if (event.body !== null && event.body !== undefined) { 73 | console.log('retrieving payload from event.body'); 74 | jsonContents = JSON.parse(event.body); 75 | } 76 | 77 | console.log('query parameters: ', jsonContents); 78 | return db.collection('restaurants').aggregate([{ $match: { "address.zipcode": jsonContents.zipcode, "cuisine": jsonContents.cuisine, "name": new RegExp(jsonContents.startsWith) } }, 79 | { $project: { "_id": 0, "name": 1, "address.building": 1, "address.street": 1, "borough": 1, "address.zipcode": 1, "healthScoreAverage": { $avg: "$grades.score" }, "healthScoreWorst": { $max: "$grades.score" } } } 80 | ]).toArray() 81 | .then(docs => { return docs;}) 82 | .catch(err => { return err; }); 83 | } -------------------------------------------------------------------------------- /restaurants/ll.sh: -------------------------------------------------------------------------------- 1 | ATLAS_USERNAME="" 2 | ATLAS_PASSWORD="" 3 | ATLAS_CLUSTER_NAME="" 4 | ATLAS_CLUSTER_SUFFIX="" 5 | DATABASE_NAME="travel" 6 | 7 | ATLAS_URI="\"mongodb://$ATLAS_USERNAME:$ATLAS_PASSWORD@$ATLAS_CLUSTER_NAME-shard-00-00-$ATLAS_CLUSTER_SUFFIX.mongodb.net:27017,$ATLAS_CLUSTER_NAME-shard-00-01-$ATLAS_CLUSTER_SUFFIX.mongodb.net:27017,$ATLAS_CLUSTER_NAME-shard-00-02-$ATLAS_CLUSTER_SUFFIX.mongodb.net:27017/$DATABASE_NAME?ssl=true&replicaSet=$ATLAS_CLUSTER_NAME-shard-0&authSource=admin\"" 8 | 9 | #echo $ATLAS_URI 10 | lambda-local -l index.js -e event.json -E {\"MONGODB_ATLAS_CLUSTER_URI\":$ATLAS_URI} 11 | -------------------------------------------------------------------------------- /restaurants/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "awslambda-restaurants", 3 | "version": "1.0.0", 4 | "description": "An AWS Lambda function that retrieves a list of restaurants from a database hosted in MongoDB Atlas", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "Apache-2.0", 11 | "dependencies": { 12 | "claudia-api-builder": "^2.5.1", 13 | "mongodb": "^3.0.5" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /restaurants/zip.sh: -------------------------------------------------------------------------------- 1 | zip -r archive.zip node_modules/ index.js -------------------------------------------------------------------------------- /sms/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules/ 3 | .env 4 | -------------------------------------------------------------------------------- /sms/config.js: -------------------------------------------------------------------------------- 1 | var cfg = {}; 2 | 3 | cfg.smsTemplate = "Here's the restaurant you're looking for: {{#restaurants}}{{name}}, {{address.building}} {{address.street}}, {{borough}} {{address.zipcode}}{{/restaurants}}"; 4 | 5 | module.exports = cfg; 6 | -------------------------------------------------------------------------------- /sms/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "phoneTo" : "+1555-555-5555", 3 | "firstnameTo": "Raphael", 4 | "emailTo": "raphael@example.com", 5 | "subject": "List of restaurants for {{firstnameTo}}", 6 | "restaurants": [ 7 | { 8 | "address": { 9 | "building": "235-237", 10 | "street": "West 48 Street" 11 | }, 12 | "borough": "Manhattan", 13 | "name": "La Masseria" 14 | }] 15 | } -------------------------------------------------------------------------------- /sms/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var cfg = require("./config.js") 4 | var Mustache = require('mustache'); 5 | 6 | exports.handler = (event, context, callback) => { 7 | 8 | var twilioAccoundSid = process.env.TWILIO_ACCOUNT_SID; 9 | var twilioAuthToken = process.env.TWILIO_AUTH_TOKEN; 10 | var twilioPhoneNumber = process.env.TWILIO_PHONE_NUMBER; 11 | 12 | var client = require('twilio')( 13 | twilioAccoundSid, 14 | twilioAuthToken 15 | ); 16 | 17 | var message = Mustache.render(cfg.smsTemplate, event); 18 | console.log(message); 19 | 20 | client.messages.create({ 21 | from: twilioPhoneNumber, 22 | to: event.phoneTo, 23 | body: message 24 | }, function (err, message) { 25 | if (err) { 26 | console.error(err.message); 27 | callback(err); 28 | } 29 | else { 30 | callback(null, "An SMS was properly sent to " + event.phoneTo); 31 | } 32 | }); 33 | 34 | }; -------------------------------------------------------------------------------- /sms/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "awslambda-twilio", 3 | "version": "1.0.0", 4 | "description": "An AWS Lambda function that sends a text message through Twilio's SMS Service", 5 | "main": "sms.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "Apache-2.0", 11 | "dependencies": { 12 | "mustache": "^2.3.0", 13 | "twilio": "^2.11.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /sms/zip.sh: -------------------------------------------------------------------------------- 1 | zip -r archive.zip node_modules/ index.js config.js -------------------------------------------------------------------------------- /statemachine.json: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "A state machine showcasing the use of MongoDB Atlas to notify a user by text message or email depending on the number of returned restaurants", 3 | "StartAt": "GetRestaurants", 4 | "States": { 5 | "GetRestaurants": { 6 | "Type": "Task", 7 | "Resource": "", 8 | "ResultPath": "$.restaurants", 9 | "Next": "CountItems" 10 | }, 11 | "CountItems": { 12 | "Type": "Task", 13 | "Resource": "", 14 | "InputPath": "$.restaurants", 15 | "ResultPath": "$.count", 16 | "Next": "NotificationMethodChoice" 17 | }, 18 | "NotificationMethodChoice": { 19 | "Type": "Choice", 20 | "Choices": [ 21 | { 22 | "Variable": "$.count", 23 | "NumericGreaterThan": 1, 24 | "Next": "SendByEmail" 25 | }, 26 | { 27 | "Variable": "$.count", 28 | "NumericLessThanEquals": 1, 29 | "Next": "SendBySMS" 30 | } 31 | ], 32 | "Default": "SendByEmail" 33 | }, 34 | "SendByEmail": { 35 | "Type": "Task", 36 | "Resource": "", 37 | "End": true 38 | }, 39 | "SendBySMS": { 40 | "Type": "Task", 41 | "Resource": "", 42 | "End": true 43 | } 44 | } 45 | } --------------------------------------------------------------------------------