├── .github └── PULL_REQUEST_TEMPLATE.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── elasticache ├── event.json ├── index.js ├── package-lock.json └── package.json ├── neptune ├── addlike.event.json ├── index.js ├── package-lock.json ├── package.json └── recommend.event.json ├── schema.graphql ├── setup ├── index.js ├── neptune.js ├── package-lock.json ├── package.json └── restaurants.js ├── stream ├── index.js ├── package-lock.json └── package.json └── template.yaml /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/aws-samples/aws-appsync-alternative-data-sources/issues), or [recently closed](https://github.com/aws-samples/aws-appsync-alternative-data-sources/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws-samples/aws-appsync-alternative-data-sources/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/aws-samples/aws-appsync-alternative-data-sources/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Enabling alternative data sources with AWS AppSync 2 | 3 | > Exploring how AWS AppSync can utilize [AWS Lambda](https://aws.amazon.com/lambda/) to integrate with alternative data sources, including Amazon ElastiCache and Amazon Neptune. 4 | 5 | As CTO and VP of [Amazon.com](http://amazon.com/), Werner Vogels, has [pointed out](https://www.allthingsdistributed.com/2018/06/purpose-built-databases-in-aws.html) “Seldom can one database fit the needs of multiple distinct use cases. The days of the one-size-fits-all monolithic database are behind us, and developers are now building highly distributed applications using a multitude of purpose-built databases.” AWS now offers a number purpose-built databases, including those mentioned including document, graph, and in-memory. 6 | 7 | In this project, we explore how AWS AppSync can utilize [AWS Lambda](https://aws.amazon.com/lambda/) to integrate with alternative data sources, those not supported out-of-the-box by AppSync. In specific, we will focus on Amazon ElastiCache (in-memory database) and Amazon Neptune (graph database) to build functionality of a Chicago-style hot dog finder. 8 | 9 | Further detail on this project can be found in the accompanying [blog post](https://aws.amazon.com/blogs/mobile/integrating-aws-appsync-neptune-elasticache/). 10 | 11 | ## Getting Started 12 | 13 | To get started, clone this repository. The repository contains an [AWS SAM Template](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) template and sample code. 14 | 15 | ### Prerequisites 16 | 17 | To run the sample, you will need to: 18 | 19 | 1. Select an AWS region that offers AWS AppSync (currently N. Virginia, Ohio, Oregon, Ireland, Frankfurt, London, Singapore, Tokyo, Sydney, Seoul, and Mumbai). 20 | 2. [Install the AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html). 21 | 22 | ## Deployment 23 | 24 | We will use AWS SAM to deploy cloud resources (note: deployment can take 20 minutes or more): 25 | 26 | ``` bash 27 | # install node modules 28 | $ cd elasticache && npm install & cd .. 29 | $ cd neptune && npm install & cd .. 30 | $ cd setup && npm install & cd .. 31 | $ cd stream && npm install & cd .. 32 | 33 | # select a unique bucket name 34 | $ aws s3 mb s3:// 35 | 36 | # package for deployment 37 | $ sam package --output-template-file packaged.yaml \ 38 | --s3-bucket 39 | 40 | # deploy cloud resources 41 | $ sam deploy --template-file packaged.yaml \ 42 | --stack-name aws-appsync-alt-data-sources \ 43 | --capabilities CAPABILITY_NAMED_IAM 44 | ``` 45 | 46 | ### Sample Data 47 | 48 | Before we take our API for a test run, we'll need to load data. To simplify data loading, we have provided an AWS Step Functions state machine. The first step will load data in DynamoDB, which in turn pushes data to ElastiCache via DynamoDB Streams. Next, we will load restaurant, user, and like data to Neptune. 49 | 50 | To start execution of the state machine, run the following commands: 51 | 52 | ``` bash 53 | # 1. get the ARN of the State Machine 54 | $ export SFN_ARN=$(aws cloudformation describe-stacks --stack-name aws-appsync-alt-data-sources \ 55 | --query 'Stacks[*].Outputs[?OutputKey==`SetupStateMachine`].OutputValue' \ 56 | --output text) 57 | 58 | # 2. start execution 59 | $ aws stepfunctions start-execution --state-machine-arn $SFN_ARN 60 | ``` 61 | 62 | ## Test Run 63 | 64 | Once you have deployed the project resources and sample data, we can take the GraphQL API for a test run. You can use the GraphQL IDE of your choice, including GraphiQL, Insomnia, Postman, or the AppSync Console. 65 | 66 | To retrieve the DNS name and API Key for your AppSync API: 67 | 68 | ``` bash 69 | # DNS Name 70 | $ aws cloudformation describe-stacks --stack-name aws-appsync-alt-data-sources \ 71 | --query 'Stacks[*].Outputs[?OutputKey==`ApiEndpoint`].OutputValue' \ 72 | --output text 73 | 74 | # API Key 75 | $ aws cloudformation describe-stacks --stack-name aws-appsync-alt-data-sources \ 76 | --query 'Stacks[*].Outputs[?OutputKey==`ApiKey`].OutputValue' \ 77 | --output text 78 | ``` 79 | 80 | First, let's query for restaurants close to a particular location. The sample location is in downtown Chicago, but in a real-life scenario, you could use the native functionality of the user's device to retrieve his coordinates (with permission): 81 | 82 | ``` graphql 83 | query SearchByLocation { 84 | searchByLocation(location: { 85 | latitude: 41.8781, 86 | longitude: -87.6298 87 | }) { 88 | restaurant { 89 | name 90 | } 91 | distance 92 | units 93 | } 94 | } 95 | ``` 96 | 97 | In this scenario, we are leveraging Amazon ElastiCache to perform a geospatial query for locations within a default radius of 10 miles (note: you can also specify the search radius as a query parameter). The response is as follows: 98 | 99 | ``` json 100 | { 101 | "data": { 102 | "searchByLocation": [ 103 | { 104 | "restaurant": { 105 | "name": "Portillo’s" 106 | }, 107 | "distance": "1.0694", 108 | "units": "mi" 109 | }, 110 | { 111 | "restaurant": { 112 | "name": "Fatso’s Last Stand" 113 | }, 114 | "distance": "3.0622", 115 | "units": "mi" 116 | }, 117 | ... 118 | ] 119 | } 120 | } 121 | ``` 122 | 123 | Next, we can query for recommended restaurants via integration with Amazon Neptune as follows: 124 | 125 | ``` graphql 126 | query Recommendations { 127 | getRecommendationsFor(user: "Dorothy") { 128 | id 129 | name 130 | } 131 | } 132 | ``` 133 | 134 | The result for Dorothy is as follows. You can query for different recommendations for our users by changing the parameter value to Sam, Joe, or Jane. 135 | 136 | ``` json 137 | { 138 | "data": { 139 | "getRecommendationsFor": [ 140 | { 141 | "id": "EE1CAA5B-B271-4E08-9ED8-5C05D9B1EE94", 142 | "name": "Gene & Jude’s Red Hot Stand" 143 | }, 144 | { 145 | "id": "2C33902E-C049-4667-A8E8-5C66C5E2875E", 146 | "name": "Original Jimmy’s Red Hots" 147 | }, 148 | ... 149 | ] 150 | } 151 | } 152 | ``` 153 | 154 | Finally, let's mutate data in the Neptune Graph Database by calling the `AddLike` mutation. In this case, we add a like for user Joe to Chicago's Dog House. 155 | 156 | ``` graphql 157 | mutation AddLike { 158 | addLike( 159 | user: "Joe", 160 | restaurantId: "EB8941AC-C3AD-4263-B97D-B7A29B36FB5F" 161 | ) { 162 | user 163 | } 164 | } 165 | ``` 166 | 167 | And the result: 168 | 169 | ``` json 170 | { 171 | "data": { 172 | "addLike": { 173 | "user": "Joe" 174 | } 175 | } 176 | } 177 | ``` 178 | 179 | Try adding likes for other user + restaurant combinations, intermittently querying for new recommendations. 180 | 181 | ## Cleaning Up 182 | 183 | Once finished, feel free to clean-up the sample code: 184 | 185 | ``` bash 186 | $ aws cloudformation delete-stack \ 187 | --stack-name aws-appsync-alt-data-sources 188 | 189 | $ aws s3 rm s3:// 190 | ``` 191 | 192 | Thanks! 193 | 194 | ## License Summary 195 | 196 | This sample code is made available under a modified MIT license. See the LICENSE file. 197 | -------------------------------------------------------------------------------- /elasticache/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "searchByLocation", 3 | "arguments": { 4 | "radius": 20, 5 | "location": { 6 | "latitude": 41.8781, 7 | "longitude": -87.6298 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /elasticache/index.js: -------------------------------------------------------------------------------- 1 | const Redis = require("ioredis") 2 | 3 | const GEO_KEY = process.env.ELASTICACHE_GEO_KEY 4 | 5 | let redis = new Redis.Cluster([ 6 | { 7 | host: process.env.ELASTICACHE_ENDPOINT, 8 | port: process.env.ELASTICACHE_PORT 9 | } 10 | ]) 11 | 12 | async function searchByGeo(lat, lon, radius=10, units="mi") { 13 | try { 14 | let result = await redis.georadius( 15 | GEO_KEY, // key 16 | lon, // longitude 17 | lat, // longitude 18 | radius, // search radius 19 | units, // search radius units 20 | "WITHCOORD", 21 | "WITHDIST" 22 | ) 23 | 24 | if (!result) { return [] } 25 | 26 | // map from Redis response 27 | return result.map( (r) => { 28 | return { id: r[0], dist: r[1], units: units } 29 | }).sort((a, b) => { return a.dist - b.dist }) 30 | 31 | } catch (error) { 32 | console.error(JSON.stringify(error)) 33 | return { error: error.message } 34 | } 35 | } 36 | 37 | exports.handler = async(event) => { 38 | switch(event.action) { 39 | case "searchByLocation": 40 | let location = event.arguments.location 41 | let radius = event.arguments.radius 42 | return await searchByGeo(location.latitude, location.longitude, radius) 43 | default: 44 | throw("No such method") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /elasticache/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-appsync-alternative-data-sources-elasticache", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "cluster-key-slot": { 8 | "version": "1.0.12", 9 | "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.0.12.tgz", 10 | "integrity": "sha512-21O0kGmvED5OJ7ZTdqQ5lQQ+sjuez33R+d35jZKLwqUb5mqcPHUsxOSzj61+LHVtxGZd1kShbQM3MjB/gBJkVg==" 11 | }, 12 | "debug": { 13 | "version": "3.2.6", 14 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", 15 | "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", 16 | "requires": { 17 | "ms": "^2.1.1" 18 | } 19 | }, 20 | "denque": { 21 | "version": "1.4.1", 22 | "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", 23 | "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==" 24 | }, 25 | "ioredis": { 26 | "version": "4.10.0", 27 | "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.10.0.tgz", 28 | "integrity": "sha512-bAdt/sKdOvUyKhjLJ8HKFmO6ZQ+OHHmfFgWn9X/ecsp1lJNnOtmh/Xl2+AdKwUdSkl/Rrw1CKOkR8+Kv8tRinQ==", 29 | "requires": { 30 | "cluster-key-slot": "^1.0.6", 31 | "debug": "^3.1.0", 32 | "denque": "^1.1.0", 33 | "lodash.defaults": "^4.2.0", 34 | "lodash.flatten": "^4.4.0", 35 | "redis-commands": "1.5.0", 36 | "redis-errors": "^1.2.0", 37 | "redis-parser": "^3.0.0", 38 | "standard-as-callback": "^2.0.1" 39 | } 40 | }, 41 | "lodash.defaults": { 42 | "version": "4.2.0", 43 | "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", 44 | "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" 45 | }, 46 | "lodash.flatten": { 47 | "version": "4.4.0", 48 | "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", 49 | "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" 50 | }, 51 | "ms": { 52 | "version": "2.1.2", 53 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 54 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 55 | }, 56 | "redis-commands": { 57 | "version": "1.5.0", 58 | "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.5.0.tgz", 59 | "integrity": "sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg==" 60 | }, 61 | "redis-errors": { 62 | "version": "1.2.0", 63 | "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", 64 | "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=" 65 | }, 66 | "redis-parser": { 67 | "version": "3.0.0", 68 | "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", 69 | "integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=", 70 | "requires": { 71 | "redis-errors": "^1.0.0" 72 | } 73 | }, 74 | "standard-as-callback": { 75 | "version": "2.0.1", 76 | "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.0.1.tgz", 77 | "integrity": "sha512-NQOxSeB8gOI5WjSaxjBgog2QFw55FV8TkS6Y07BiB3VJ8xNTvUYm0wl0s8ObgQ5NhdpnNfigMIKjgPESzgr4tg==" 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /elasticache/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-appsync-alternative-data-sources-elasticache", 3 | "version": "1.0.0", 4 | "description": "Integrates AppSync with ElastiCache", 5 | "main": "index.js", 6 | "author": "jkahn", 7 | "license": "Apache2.0", 8 | "dependencies": { 9 | "ioredis": "^4.10.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /neptune/addlike.event.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "addLike", 3 | "arguments": { 4 | "user": "Joe", 5 | "restaurantId": "478452A9-6C9E-4D44-AA80-92EFDECC45A6" 6 | } 7 | } -------------------------------------------------------------------------------- /neptune/index.js: -------------------------------------------------------------------------------- 1 | const gremlin = require('gremlin') 2 | const DriverRemoteConnection = gremlin.driver.DriverRemoteConnection 3 | 4 | const Graph = gremlin.structure.Graph 5 | const P = gremlin.process.P 6 | const Order = gremlin.process.order 7 | const Scope = gremlin.process.scope 8 | const Column = gremlin.process.column 9 | 10 | const dc = new DriverRemoteConnection( 11 | `wss://${process.env.NEPTUNE_ENDPOINT}:${process.env.NEPTUNE_PORT}/gremlin` 12 | ) 13 | const graph = new Graph() 14 | const g = graph.traversal().withRemote(dc) 15 | 16 | // based on Gremlin recommendation recipe: 17 | // http://tinkerpop.apache.org/docs/current/recipes/#recommendation 18 | async function getRecommendationsFor(userName) { 19 | try { 20 | let result = await g.V() 21 | .has('User', 'name', userName).as('user') 22 | .out('likes').aggregate('self') 23 | .in_('likes').where(P.neq('user')) 24 | .out('likes').where(P.without('self')) 25 | .values('id') 26 | .groupCount() 27 | .order(Scope.local) 28 | .by(Column.values, Order.decr) 29 | .select(Column.keys) 30 | .next() 31 | 32 | return result.value.map( (r) => { 33 | return { id: r } 34 | }) 35 | } catch (error) { 36 | console.error(JSON.stringify(error)) 37 | return { error: error.message } 38 | } 39 | } 40 | 41 | async function addLike(user, restaurantId) { 42 | try { 43 | await g.V() 44 | .has("Restaurant", "id", restaurantId).as("restaurant") 45 | .V() 46 | .has("User", "name", user) 47 | .addE("likes") 48 | .to("restaurant") 49 | .next() 50 | 51 | return { success: true } 52 | } catch (error) { 53 | console.error(JSON.stringify(error)) 54 | return { error: error.message } 55 | } 56 | } 57 | 58 | exports.handler = async(event) => { 59 | switch(event.action) { 60 | case "getRecommendations": 61 | return await getRecommendationsFor(event.arguments.user) 62 | case "addLike": 63 | return await addLike(event.arguments.user, event.arguments.restaurantId) 64 | default: 65 | return { error: "No such method" } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /neptune/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-appsync-alternative-data-sources-neptune", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "async-limiter": { 8 | "version": "1.0.0", 9 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", 10 | "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" 11 | }, 12 | "gremlin": { 13 | "version": "3.4.2", 14 | "resolved": "https://registry.npmjs.org/gremlin/-/gremlin-3.4.2.tgz", 15 | "integrity": "sha512-FRxbRfHBHRYq4c3+ZL9ttVktBOjUXIImhzmCsnWKyidLPjv+J/QoYjq9O9Di+HD2ToJEXfKfz1/pzgSL2aJccg==", 16 | "requires": { 17 | "ws": "^3.0.0" 18 | } 19 | }, 20 | "safe-buffer": { 21 | "version": "5.1.2", 22 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 23 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 24 | }, 25 | "ultron": { 26 | "version": "1.1.1", 27 | "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", 28 | "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" 29 | }, 30 | "ws": { 31 | "version": "3.3.3", 32 | "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", 33 | "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", 34 | "requires": { 35 | "async-limiter": "~1.0.0", 36 | "safe-buffer": "~5.1.0", 37 | "ultron": "~1.1.0" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /neptune/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-appsync-alternative-data-sources-neptune", 3 | "version": "1.0.0", 4 | "description": "Integrated AppSync with Neptune", 5 | "main": "index.js", 6 | "author": "jkahn", 7 | "license": "Apache2.0", 8 | "dependencies": { 9 | "gremlin": "^3.4.2" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /neptune/recommend.event.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "getRecommendations", 3 | "arguments": { 4 | "user": "Dorothy" 5 | } 6 | } -------------------------------------------------------------------------------- /schema.graphql: -------------------------------------------------------------------------------- 1 | type Restaurant { 2 | id: ID! 3 | name: String! 4 | description: String 5 | address: String! 6 | city: String! 7 | state: String! 8 | zip: Int! 9 | longitude: Float! 10 | latitude: Float! 11 | } 12 | 13 | type Like { 14 | user: String! 15 | restaurantId: ID! 16 | } 17 | 18 | type SearchResult { 19 | restaurant: Restaurant! 20 | distance: String 21 | units: String 22 | } 23 | 24 | input GPSInput { 25 | latitude: Float! 26 | longitude: Float! 27 | radius: Float 28 | } 29 | 30 | type Query { 31 | listRestaurants(limit: Int, nextToken: String): [Restaurant] 32 | getRestaurant(id: ID!): Restaurant 33 | searchByLocation(location: GPSInput!): [SearchResult] 34 | getRecommendationsFor(user: String!): [Restaurant] 35 | } 36 | 37 | type Mutation { 38 | addLike(user: String!, restaurantId: ID!): Like 39 | } 40 | 41 | type Subscription { 42 | onLike(user: String): Like 43 | @aws_subscribe(mutations: ["addLike"]) 44 | } 45 | 46 | schema { 47 | query: Query 48 | mutation: Mutation 49 | subscription: Subscription 50 | } 51 | -------------------------------------------------------------------------------- /setup/index.js: -------------------------------------------------------------------------------- 1 | const DynamoDB = require("aws-sdk/clients/dynamodb") 2 | const restaurants = require("./restaurants") 3 | 4 | const RESTAURANT_TABLE = process.env.RESTAURANT_TABLE 5 | 6 | let ddb = new DynamoDB.DocumentClient() 7 | 8 | exports.handler = async(event, context) => { 9 | for (let restaurant of restaurants) { 10 | let record = { 11 | id: restaurant.id, 12 | name: restaurant.name, 13 | address: restaurant.address, 14 | city: restaurant.city, 15 | state: restaurant.state, 16 | zip: restaurant.zip, 17 | latitude: restaurant.latitude, 18 | longitude: restaurant.longitude, 19 | // description: restaurant.description 20 | } 21 | 22 | try { 23 | await ddb.put({ 24 | TableName: RESTAURANT_TABLE, 25 | Item: record 26 | }).promise() 27 | } catch(error) { 28 | console.error(error) 29 | throw error 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /setup/neptune.js: -------------------------------------------------------------------------------- 1 | const restaurants = require("./restaurants") 2 | const gremlin = require('gremlin') 3 | const DriverRemoteConnection = gremlin.driver.DriverRemoteConnection 4 | const { addV, unfold } = gremlin.process.statics 5 | 6 | const Graph = gremlin.structure.Graph 7 | 8 | const dc = new DriverRemoteConnection( 9 | `wss://${process.env.NEPTUNE_ENDPOINT}:${process.env.NEPTUNE_PORT}/gremlin` 10 | ) 11 | const graph = new Graph() 12 | const g = graph.traversal().withRemote(dc) 13 | 14 | 15 | async function createRestaurant(id, name) { 16 | try { 17 | await g.V().has("Restaurant", "id", id) 18 | .fold() 19 | .coalesce( 20 | unfold(), 21 | addV("Restaurant").property("id", id).property("name", name) 22 | ) 23 | .next() 24 | } catch (error) { 25 | console.error(error) 26 | } 27 | } 28 | 29 | async function createUser(name) { 30 | try { 31 | await g.V().has("User", "name", name) 32 | .fold() 33 | .coalesce( 34 | unfold(), 35 | addV("User").property("name", name) 36 | ) 37 | .next() 38 | } catch (error) { 39 | console.error(error) 40 | } 41 | } 42 | 43 | async function addLike(userName, restaurantId) { 44 | try { 45 | await g.V().has("Restaurant", "id", restaurantId).as("restaurant") 46 | .V().has("User", "name", userName) 47 | .addE("likes").to("restaurant") 48 | .next() 49 | } catch (error) { 50 | console.error(error) 51 | } 52 | } 53 | 54 | 55 | 56 | exports.handler = async(event, context) => { 57 | for (let restaurant of restaurants) { 58 | console.log(`Adding ${restaurant.name}`) 59 | await createRestaurant(restaurant.id, restaurant.name) 60 | } 61 | 62 | let users = [ "Sam", "Joe", "Jane", "Dorothy" ] 63 | for (let user of users) { 64 | await createUser(user) 65 | } 66 | 67 | addLike("Sam", "27BE498A-5841-40DF-877A-CE6686073B0D") 68 | addLike("Sam", "E891C174-23ED-4495-A9D1-39A26F58C394") 69 | addLike("Sam", "F24B37E4-C89B-48AA-871B-46E5DE47118F") 70 | addLike("Sam", "D1E9ABF7-6A37-4138-80E3-E6983B93706A") 71 | addLike("Joe", "EE1CAA5B-B271-4E08-9ED8-5C05D9B1EE94") 72 | addLike("Joe", "27BE498A-5841-40DF-877A-CE6686073B0D") 73 | addLike("Joe", "2A3EE6F0-3970-4B7E-9553-A4095E5525DA") 74 | addLike("Joe", "2C33902E-C049-4667-A8E8-5C66C5E2875E") 75 | addLike("Jane", "2A3EE6F0-3970-4B7E-9553-A4095E5525DA") 76 | addLike("Jane", "F24B37E4-C89B-48AA-871B-46E5DE47118F") 77 | addLike("Jane", "2C33902E-C049-4667-A8E8-5C66C5E2875E") 78 | addLike("Jane", "96B7CB80-6DC4-445F-8925-69316B222DCC") 79 | addLike("Jane", "27BE498A-5841-40DF-877A-CE6686073B0D") 80 | addLike("Jane", "EE1CAA5B-B271-4E08-9ED8-5C05D9B1EE94") 81 | addLike("Dorothy", "27BE498A-5841-40DF-877A-CE6686073B0D") 82 | addLike("Dorothy", "2A3EE6F0-3970-4B7E-9553-A4095E5525DA") 83 | 84 | }; 85 | -------------------------------------------------------------------------------- /setup/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-appsync-alternative-data-sources-setup", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "async-limiter": { 8 | "version": "1.0.0", 9 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", 10 | "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" 11 | }, 12 | "aws-sdk": { 13 | "version": "2.486.0", 14 | "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.486.0.tgz", 15 | "integrity": "sha512-Gd5IB3HPG+fBE7sh16ATwEuHLv7DcpRLmG381i+0UwrWmka+0t6Dc7/yHKkLOXQKVBDI5FvRdlsWAsP/LFpOxw==", 16 | "requires": { 17 | "buffer": "4.9.1", 18 | "events": "1.1.1", 19 | "ieee754": "1.1.8", 20 | "jmespath": "0.15.0", 21 | "querystring": "0.2.0", 22 | "sax": "1.2.1", 23 | "url": "0.10.3", 24 | "uuid": "3.3.2", 25 | "xml2js": "0.4.19" 26 | } 27 | }, 28 | "base64-js": { 29 | "version": "1.3.0", 30 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", 31 | "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" 32 | }, 33 | "buffer": { 34 | "version": "4.9.1", 35 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", 36 | "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", 37 | "requires": { 38 | "base64-js": "^1.0.2", 39 | "ieee754": "^1.1.4", 40 | "isarray": "^1.0.0" 41 | } 42 | }, 43 | "events": { 44 | "version": "1.1.1", 45 | "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", 46 | "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" 47 | }, 48 | "gremlin": { 49 | "version": "3.4.2", 50 | "resolved": "https://registry.npmjs.org/gremlin/-/gremlin-3.4.2.tgz", 51 | "integrity": "sha512-FRxbRfHBHRYq4c3+ZL9ttVktBOjUXIImhzmCsnWKyidLPjv+J/QoYjq9O9Di+HD2ToJEXfKfz1/pzgSL2aJccg==", 52 | "requires": { 53 | "ws": "^3.0.0" 54 | } 55 | }, 56 | "ieee754": { 57 | "version": "1.1.8", 58 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", 59 | "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" 60 | }, 61 | "isarray": { 62 | "version": "1.0.0", 63 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 64 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 65 | }, 66 | "jmespath": { 67 | "version": "0.15.0", 68 | "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", 69 | "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" 70 | }, 71 | "punycode": { 72 | "version": "1.3.2", 73 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", 74 | "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" 75 | }, 76 | "querystring": { 77 | "version": "0.2.0", 78 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", 79 | "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" 80 | }, 81 | "safe-buffer": { 82 | "version": "5.1.2", 83 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 84 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 85 | }, 86 | "sax": { 87 | "version": "1.2.1", 88 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", 89 | "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" 90 | }, 91 | "ultron": { 92 | "version": "1.1.1", 93 | "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", 94 | "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" 95 | }, 96 | "url": { 97 | "version": "0.10.3", 98 | "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", 99 | "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", 100 | "requires": { 101 | "punycode": "1.3.2", 102 | "querystring": "0.2.0" 103 | } 104 | }, 105 | "uuid": { 106 | "version": "3.3.2", 107 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 108 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 109 | }, 110 | "ws": { 111 | "version": "3.3.3", 112 | "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", 113 | "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", 114 | "requires": { 115 | "async-limiter": "~1.0.0", 116 | "safe-buffer": "~5.1.0", 117 | "ultron": "~1.1.0" 118 | } 119 | }, 120 | "xml2js": { 121 | "version": "0.4.19", 122 | "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", 123 | "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", 124 | "requires": { 125 | "sax": ">=0.6.0", 126 | "xmlbuilder": "~9.0.1" 127 | } 128 | }, 129 | "xmlbuilder": { 130 | "version": "9.0.7", 131 | "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", 132 | "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /setup/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-appsync-alternative-data-sources-setup", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "dependencies": { 7 | "aws-sdk": "^2.486.0", 8 | "gremlin": "^3.4.2" 9 | }, 10 | "devDependencies": {}, 11 | "author": "jkahn", 12 | "license": "ISC" 13 | } 14 | -------------------------------------------------------------------------------- /setup/restaurants.js: -------------------------------------------------------------------------------- 1 | const restaurants = [ 2 | { 3 | id: '27BE498A-5841-40DF-877A-CE6686073B0D', 4 | name: 'Portillo’s', 5 | description: '', 6 | address: '100 W. Ontario', 7 | city: 'Chicago', 8 | state: 'IL', 9 | zip: 60654, 10 | latitude: -87.6315139, 11 | longitude: 41.89352030000001 12 | }, 13 | { 14 | id: '478452A9-6C9E-4D44-AA80-92EFDECC45A6', 15 | name: 'Murphy’s Red Hots', 16 | description: '', 17 | address: '1211 West Belmont Ave', 18 | city: 'Chicago', 19 | state: 'IL', 20 | zip: 60657, 21 | latitude: -87.65953789999999, 22 | longitude: 41.9396292 23 | }, 24 | { 25 | id: '5500BA8D-289F-47D1-A43D-EE51E5EF41A7', 26 | name: 'Mustard’s Last Stand', 27 | description: '', 28 | address: '1613 Central Street', 29 | city: 'Evanston', 30 | state: 'IL', 31 | zip: 60201, 32 | latitude: -87.6948779, 33 | longitude: 42.0645223 34 | }, 35 | { 36 | id: 'EB8941AC-C3AD-4263-B97D-B7A29B36FB5F', 37 | name: 'Chicago\'s Dog House', 38 | description: '', 39 | address: '816 West Fullerton', 40 | city: 'Chicago', 41 | state: 'IL', 42 | zip: 60614, 43 | latitude: -87.64953930000001, 44 | longitude: 41.9255674 45 | }, 46 | { 47 | id: '6BC2C5D2-BA6B-475E-A680-15F47D66CE4B', 48 | name: 'Wrigleyville Dogs', 49 | description: 'Located across from Wrigley Field in Wrigleyville.', 50 | address: '3737 North Clark', 51 | city: 'Chicago', 52 | state: 'IL', 53 | zip: 60057, 54 | latitude: -87.658481, 55 | longitude: 41.9500302 56 | }, 57 | { 58 | id: 'EE1CAA5B-B271-4E08-9ED8-5C05D9B1EE94', 59 | name: 'Gene & Jude’s Red Hot Stand', 60 | description: '', 61 | address: '2720 N River Rd', 62 | city: 'River Grove', 63 | state: 'IL', 64 | zip: 60171, 65 | latitude: -87.8466948, 66 | longitude: 41.9299848 67 | }, 68 | { 69 | id: 'E891C174-23ED-4495-A9D1-39A26F58C394', 70 | name: 'Weiner’s Circle', 71 | description: '', 72 | address: '2622 North Clark', 73 | city: 'Chicago', 74 | state: 'IL', 75 | zip: 60657, 76 | latitude: -87.6438051, 77 | longitude: 41.9301457 78 | }, 79 | { 80 | id: 'D1E9ABF7-6A37-4138-80E3-E6983B93706A', 81 | name: 'Redhot Ranch', 82 | description: '', 83 | address: '2072 N Western Ave', 84 | city: 'Chicago', 85 | state: 'IL', 86 | zip: 60647, 87 | latitude: -87.68780199999999, 88 | longitude: 41.9198013 89 | }, 90 | { 91 | id: '2A3EE6F0-3970-4B7E-9553-A4095E5525DA', 92 | name: 'Superdawg', 93 | description: '', 94 | address: '6363 North Milwaukee', 95 | city: 'Chicago', 96 | state: 'IL', 97 | zip: 60646, 98 | latitude: -87.7870165, 99 | longitude: 41.9967617 100 | }, 101 | { 102 | id: 'F24B37E4-C89B-48AA-871B-46E5DE47118F', 103 | name: 'Wolfy\'s', 104 | description: '', 105 | address: '2734 West Peterson', 106 | city: 'Chicago', 107 | state: 'IL', 108 | zip: 60659, 109 | latitude: -87.6985935, 110 | longitude: 41.9907261 111 | }, 112 | { 113 | id: 'CC3863B8-772C-4BFE-AFD1-9026D6F28EAD', 114 | name: 'Fatso\’s Last Stand', 115 | description: '', 116 | address: '2258 W Chicago Ave', 117 | city: 'Chicago', 118 | state: 'IL', 119 | zip: 60622, 120 | latitude: -87.68425599999999, 121 | longitude: 41.8959726 122 | }, 123 | { 124 | id: 'A12244BA-A9CA-4AE0-83FD-40AA3CA178C8', 125 | name: 'Morrie O\'Malley\'s Hot Dogs', 126 | description: '', 127 | address: '3501 S Union Ave', 128 | city: 'Chicago', 129 | state: 'IL', 130 | zip: 60609, 131 | latitude: -87.64340059999999, 132 | longitude: 41.8306589 133 | }, 134 | { 135 | id: '96B7CB80-6DC4-445F-8925-69316B222DCC', 136 | name: 'Byron\'s', 137 | description: '', 138 | address: '1017 West Irving Park Road', 139 | city: 'Chicago', 140 | state: 'IL', 141 | zip: 60640, 142 | latitude: -87.6552421, 143 | longitude: 41.9543355 144 | }, 145 | { 146 | id: '96B7CB80-6DC4-445F-8925-69316B222DCC', 147 | name: 'Hot \'G\' Dog', 148 | description: '', 149 | address: '5009 N Clark St', 150 | city: 'Chicago', 151 | state: 'IL', 152 | zip: 60640, 153 | latitude: -87.6677351, 154 | longitude: 41.9728272 155 | }, 156 | { 157 | id: '2C33902E-C049-4667-A8E8-5C66C5E2875E', 158 | name: 'Original Jimmy’s Red Hots', 159 | description: '', 160 | address: '4000 W Grand Ave', 161 | city: 'Chicago', 162 | state: 'IL', 163 | zip: 60651, 164 | latitude: -87.72659279999999, 165 | longitude: 41.9064253 166 | } 167 | ] 168 | 169 | module.exports = restaurants 170 | -------------------------------------------------------------------------------- /stream/index.js: -------------------------------------------------------------------------------- 1 | const Redis = require("ioredis") 2 | 3 | const GEO_KEY = process.env.ELASTICACHE_GEO_KEY 4 | let redis = new Redis.Cluster([ 5 | { 6 | host: process.env.ELASTICACHE_ENDPOINT, 7 | port: process.env.ELASTICACHE_PORT 8 | } 9 | ]) 10 | 11 | const addToRedis = async function(restaurantId, record) { 12 | let restaurant = { 13 | id: restaurantId, 14 | longitude: record.dynamodb.NewImage.longitude.N, 15 | latitude: record.dynamodb.NewImage.latitude.N 16 | } 17 | 18 | try { 19 | await redis.geoadd( 20 | GEO_KEY, 21 | restaurant.latitude, 22 | restaurant.longitude, 23 | restaurant.id 24 | ) 25 | } catch(error) { 26 | throw error 27 | } 28 | } 29 | 30 | exports.handler = async(event, context, callback) => { 31 | for (let record of event.Records) { 32 | let id = record.dynamodb.Keys.id.S 33 | console.log(`Restaurant: ${id}`) 34 | 35 | switch(record.eventName) { 36 | case 'INSERT': 37 | case 'MODIFY': 38 | await addToRedis(id, record) 39 | break 40 | case 'REMOVE': 41 | // not implemented 42 | break 43 | default: 44 | callback("No such method") 45 | } 46 | } 47 | 48 | return { message: `Finished processing ${event.Records.length} records` } 49 | } 50 | -------------------------------------------------------------------------------- /stream/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-appsync-alternative-data-sources-stream", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "cluster-key-slot": { 8 | "version": "1.0.12", 9 | "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.0.12.tgz", 10 | "integrity": "sha512-21O0kGmvED5OJ7ZTdqQ5lQQ+sjuez33R+d35jZKLwqUb5mqcPHUsxOSzj61+LHVtxGZd1kShbQM3MjB/gBJkVg==" 11 | }, 12 | "debug": { 13 | "version": "3.2.6", 14 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", 15 | "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", 16 | "requires": { 17 | "ms": "^2.1.1" 18 | } 19 | }, 20 | "denque": { 21 | "version": "1.4.1", 22 | "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", 23 | "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==" 24 | }, 25 | "ioredis": { 26 | "version": "4.10.0", 27 | "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.10.0.tgz", 28 | "integrity": "sha512-bAdt/sKdOvUyKhjLJ8HKFmO6ZQ+OHHmfFgWn9X/ecsp1lJNnOtmh/Xl2+AdKwUdSkl/Rrw1CKOkR8+Kv8tRinQ==", 29 | "requires": { 30 | "cluster-key-slot": "^1.0.6", 31 | "debug": "^3.1.0", 32 | "denque": "^1.1.0", 33 | "lodash.defaults": "^4.2.0", 34 | "lodash.flatten": "^4.4.0", 35 | "redis-commands": "1.5.0", 36 | "redis-errors": "^1.2.0", 37 | "redis-parser": "^3.0.0", 38 | "standard-as-callback": "^2.0.1" 39 | } 40 | }, 41 | "lodash.defaults": { 42 | "version": "4.2.0", 43 | "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", 44 | "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" 45 | }, 46 | "lodash.flatten": { 47 | "version": "4.4.0", 48 | "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", 49 | "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" 50 | }, 51 | "ms": { 52 | "version": "2.1.2", 53 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 54 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 55 | }, 56 | "redis-commands": { 57 | "version": "1.5.0", 58 | "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.5.0.tgz", 59 | "integrity": "sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg==" 60 | }, 61 | "redis-errors": { 62 | "version": "1.2.0", 63 | "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", 64 | "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=" 65 | }, 66 | "redis-parser": { 67 | "version": "3.0.0", 68 | "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", 69 | "integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=", 70 | "requires": { 71 | "redis-errors": "^1.0.0" 72 | } 73 | }, 74 | "standard-as-callback": { 75 | "version": "2.0.1", 76 | "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.0.1.tgz", 77 | "integrity": "sha512-NQOxSeB8gOI5WjSaxjBgog2QFw55FV8TkS6Y07BiB3VJ8xNTvUYm0wl0s8ObgQ5NhdpnNfigMIKjgPESzgr4tg==" 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /stream/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-appsync-alternative-data-sources-stream", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "author": "jkahn", 7 | "license": "ISC", 8 | "dependencies": { 9 | "ioredis": "^4.10.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: Enabling alternative data sources with AWS AppSync 4 | 5 | Parameters: 6 | ProjectName: 7 | Type: String 8 | Default: alternative-data-sources 9 | 10 | ElasticacheInstanceClass: 11 | Type: String 12 | Default: cache.t2.micro 13 | 14 | NeptuneInstanceClass: 15 | Type: String 16 | Default: db.r4.large 17 | 18 | 19 | Globals: 20 | Function: 21 | Runtime: nodejs10.x 22 | Handler: index.handler 23 | MemorySize: 512 24 | Timeout: 15 25 | Tags: 26 | Project: !Ref ProjectName 27 | 28 | Mappings: 29 | SubnetConfig: 30 | VPC: 31 | CIDR: '10.0.0.0/16' 32 | Private1: 33 | CIDR: '10.0.0.0/24' 34 | Private2: 35 | CIDR: '10.0.1.0/24' 36 | Lambda1: 37 | CIDR: '10.0.2.0/24' 38 | Lambda2: 39 | CIDR: '10.0.3.0/24' 40 | Public1: 41 | CIDR: '10.0.4.0/24' 42 | 43 | Resources: 44 | ## AppSync ## 45 | RestaurantApi: 46 | Type: AWS::AppSync::GraphQLApi 47 | Properties: 48 | AuthenticationType: API_KEY 49 | Name: aws-appsync-alt-data-sources 50 | LogConfig: 51 | CloudWatchLogsRoleArn: !GetAtt AppSyncServiceRole.Arn 52 | FieldLogLevel: "ALL" 53 | 54 | RestaurantApiKey: 55 | Type: AWS::AppSync::ApiKey 56 | Properties: 57 | ApiId: !GetAtt RestaurantApi.ApiId 58 | Description: API Key for Restaurant API 59 | 60 | RestaurantSchema: 61 | Type: AWS::AppSync::GraphQLSchema 62 | Properties: 63 | ApiId: !GetAtt RestaurantApi.ApiId 64 | DefinitionS3Location: schema.graphql 65 | 66 | ListRestaurantsQueryResolver: 67 | Type: AWS::AppSync::Resolver 68 | Properties: 69 | ApiId: !GetAtt RestaurantApi.ApiId 70 | TypeName: Query 71 | FieldName: listRestaurants 72 | DataSourceName: !GetAtt RestaurantsTableDataSource.Name 73 | RequestMappingTemplate: | 74 | { 75 | "version" : "2017-02-28", 76 | "operation" : "Scan", 77 | "limit": $util.defaultIfNull(${ctx.args.limit}, 20), 78 | "nextToken": $util.toJson($util.defaultIfNullOrBlank($ctx.args.nextToken, null)) 79 | } 80 | ResponseMappingTemplate: | 81 | $util.toJson($ctx.result) 82 | 83 | GetRestaurantQueryResolver: 84 | Type: AWS::AppSync::Resolver 85 | Properties: 86 | ApiId: !GetAtt RestaurantApi.ApiId 87 | TypeName: Query 88 | FieldName: getRestaurant 89 | DataSourceName: !GetAtt RestaurantsTableDataSource.Name 90 | RequestMappingTemplate: | 91 | { 92 | "version": "2017-02-28", 93 | "operation": "GetItem", 94 | "key": { 95 | "id": $util.dynamodb.toDynamoDBJson($ctx.args.id), 96 | } 97 | } 98 | ResponseMappingTemplate: | 99 | $util.toJson($ctx.result) 100 | 101 | SearchByLocationQueryResolver: 102 | Type: AWS::AppSync::Resolver 103 | Properties: 104 | ApiId: !GetAtt RestaurantApi.ApiId 105 | TypeName: Query 106 | FieldName: searchByLocation 107 | Kind: PIPELINE 108 | PipelineConfig: 109 | Functions: 110 | - !GetAtt SearchByLocationFunction.FunctionId 111 | - !GetAtt BatchGetRestaurantsFunction.FunctionId 112 | RequestMappingTemplate: | 113 | {} 114 | ResponseMappingTemplate: | 115 | #set($distances = $ctx.stash.get("distances")) 116 | #set($result = []) 117 | 118 | #foreach($item in $ctx.prev.result) 119 | #set($idx = $foreach.count - 1) 120 | #set($dist = $distances.get($idx)) 121 | #set($r = { 122 | "restaurant": $item, 123 | "distance": $dist.dist, 124 | "units": $dist.units 125 | }) 126 | $util.qr($result.add($r)) 127 | #end 128 | $util.toJson($result) 129 | 130 | GetRecommendationsForQueryResolver: 131 | Type: AWS::AppSync::Resolver 132 | Properties: 133 | ApiId: !GetAtt RestaurantApi.ApiId 134 | TypeName: Query 135 | FieldName: getRecommendationsFor 136 | Kind: PIPELINE 137 | PipelineConfig: 138 | Functions: 139 | - !GetAtt GetRecommendationsFunction.FunctionId 140 | - !GetAtt BatchGetRestaurantsFunction.FunctionId 141 | RequestMappingTemplate: | 142 | {} 143 | ResponseMappingTemplate: | 144 | $util.toJson($ctx.result) 145 | 146 | AddLikeMutationResolver: 147 | Type: AWS::AppSync::Resolver 148 | Properties: 149 | ApiId: !GetAtt RestaurantApi.ApiId 150 | TypeName: Mutation 151 | FieldName: addLike 152 | DataSourceName: !GetAtt NeptuneIntegrationDataSource.Name 153 | RequestMappingTemplate: | 154 | { 155 | "version": "2017-02-28", 156 | "operation": "Invoke", 157 | "payload": { 158 | "action": "addLike", 159 | "arguments": $utils.toJson($ctx.arguments) 160 | } 161 | } 162 | ResponseMappingTemplate: | 163 | #if($ctx.result && $ctx.result.error) 164 | $util.error($ctx.result.error) 165 | #end 166 | $util.toJson($ctx.arguments) 167 | 168 | SearchByLocationFunction: 169 | Type: AWS::AppSync::FunctionConfiguration 170 | Properties: 171 | ApiId: !GetAtt RestaurantApi.ApiId 172 | Name: search_by_location_elasticache 173 | Description: > 174 | Queries ElastiCache to find a listing of restaurants IDs in a given 175 | search radius from provide coordinates. 176 | DataSourceName: !GetAtt ElastiCacheIntegrationDataSource.Name 177 | FunctionVersion: "2018-05-29" 178 | RequestMappingTemplate: | 179 | { 180 | "version": "2017-02-28", 181 | "operation": "Invoke", 182 | "payload": { 183 | "action": "searchByLocation", 184 | "arguments": $utils.toJson($ctx.arguments) 185 | } 186 | } 187 | ResponseMappingTemplate: | 188 | $util.qr($ctx.stash.put("distances", $ctx.result)) 189 | #if($ctx.result && $ctx.result.error) 190 | $util.error($ctx.result.error) 191 | #end 192 | $util.toJson($ctx.result) 193 | 194 | GetRecommendationsFunction: 195 | Type: AWS::AppSync::FunctionConfiguration 196 | Properties: 197 | ApiId: !GetAtt RestaurantApi.ApiId 198 | Name: get_recommendations_neptune 199 | Description: > 200 | Queries Neptune to retrieve recommendations for the passed 201 | user. 202 | DataSourceName: !GetAtt NeptuneIntegrationDataSource.Name 203 | FunctionVersion: "2018-05-29" 204 | RequestMappingTemplate: | 205 | { 206 | "version": "2017-02-28", 207 | "operation": "Invoke", 208 | "payload": { 209 | "action": "getRecommendations", 210 | "arguments": $utils.toJson($ctx.arguments) 211 | } 212 | } 213 | ResponseMappingTemplate: | 214 | #if($ctx.result && $ctx.result.error) 215 | $util.error($ctx.result.error) 216 | #end 217 | $util.toJson($ctx.result) 218 | 219 | BatchGetRestaurantsFunction: 220 | Type: AWS::AppSync::FunctionConfiguration 221 | Properties: 222 | ApiId: !GetAtt RestaurantApi.ApiId 223 | Name: batch_get_restaurants_ddb 224 | Description: > 225 | Retrieves batch of restaurants details from DynamoDB. 226 | DataSourceName: !GetAtt RestaurantsTableDataSource.Name 227 | FunctionVersion: "2018-05-29" 228 | RequestMappingTemplate: !Sub | 229 | #set($ids = []) 230 | #foreach($result in $ctx.prev.result) 231 | #set($map = {}) 232 | $util.qr($map.put("id", $util.dynamodb.toString($result.id))) 233 | $util.qr($ids.add($map)) 234 | #end 235 | { 236 | "version" : "2018-05-29", 237 | "operation" : "BatchGetItem", 238 | "tables" : { 239 | "${RestaurantsTable}": { 240 | "keys": $util.toJson($ids), 241 | "consistentRead": true 242 | } 243 | } 244 | } 245 | ResponseMappingTemplate: !Sub | 246 | #if($ctx.result && $ctx.result.error) 247 | $util.error($ctx.result.error) 248 | #end 249 | $util.toJson($ctx.result.data.${RestaurantsTable}) 250 | 251 | 252 | # ElastiCache Integration Data Source 253 | ElastiCacheIntegrationDataSource: 254 | Type: AWS::AppSync::DataSource 255 | Properties: 256 | ApiId: !GetAtt RestaurantApi.ApiId 257 | Name: ElastiCacheIntegration 258 | Description: Lambda function to integrate with Elasticache 259 | Type: AWS_LAMBDA 260 | ServiceRoleArn: !GetAtt AppSyncServiceRole.Arn 261 | LambdaConfig: 262 | LambdaFunctionArn: !GetAtt ElastiCacheIntegrationFunction.Arn 263 | 264 | # Neptune Integration Data Source 265 | NeptuneIntegrationDataSource: 266 | Type: AWS::AppSync::DataSource 267 | Properties: 268 | ApiId: !GetAtt RestaurantApi.ApiId 269 | Name: NeptuneIntegration 270 | Description: Lambda function to integrate with Neptune 271 | Type: AWS_LAMBDA 272 | ServiceRoleArn: !GetAtt AppSyncServiceRole.Arn 273 | LambdaConfig: 274 | LambdaFunctionArn: !GetAtt NeptuneIntegrationFunction.Arn 275 | 276 | # DynamoDB Data Source -- Restaurants Table 277 | RestaurantsTableDataSource: 278 | Type: AWS::AppSync::DataSource 279 | Properties: 280 | ApiId: !GetAtt RestaurantApi.ApiId 281 | Name: RestaurantsTable 282 | Description: Restaurants table 283 | Type: AMAZON_DYNAMODB 284 | ServiceRoleArn: !GetAtt AppSyncServiceRole.Arn 285 | DynamoDBConfig: 286 | TableName: !Ref RestaurantsTable 287 | AwsRegion: !Sub ${AWS::Region} 288 | 289 | ## ElastiCache ## 290 | RedisCluster: 291 | Type: AWS::ElastiCache::ReplicationGroup 292 | Properties: 293 | AutoMinorVersionUpgrade: true 294 | # enable Cluster Mode 295 | CacheParameterGroupName: default.redis5.0.cluster.on 296 | CacheNodeType: !Ref ElasticacheInstanceClass 297 | CacheSubnetGroupName: !Ref RedisSubnetGroup 298 | Engine: redis 299 | EngineVersion: 5.0.4 300 | NumNodeGroups: 1 301 | Port: 6379 302 | ReplicasPerNodeGroup: 1 303 | ReplicationGroupDescription: AppSync Alternative Data Sources 304 | SecurityGroupIds: 305 | - !Ref RedisSecurityGroup 306 | Tags: 307 | - Key: Project 308 | Value: !Ref ProjectName 309 | 310 | RedisSubnetGroup: 311 | Type: AWS::ElastiCache::SubnetGroup 312 | Properties: 313 | Description: Redis subnet group 314 | SubnetIds: 315 | - !Ref PrivateSubnet1 316 | - !Ref PrivateSubnet2 317 | 318 | RedisSecurityGroup: 319 | Type: AWS::EC2::SecurityGroup 320 | Properties: 321 | VpcId: !Ref VPC 322 | GroupDescription: Enable Redis access 323 | SecurityGroupIngress: 324 | - IpProtocol: tcp 325 | FromPort: 6379 326 | ToPort: 6379 327 | SourceSecurityGroupId: !Ref LambdaSecurityGroup 328 | Tags: 329 | - Key: Project 330 | Value: !Ref ProjectName 331 | 332 | ## Neptune ## 333 | NeptuneCluster: 334 | Type: AWS::Neptune::DBCluster 335 | Properties: 336 | DBClusterIdentifier: !Ref ProjectName 337 | DBSubnetGroupName: !Ref NeptuneSubnetGroup 338 | VpcSecurityGroupIds: 339 | - !Ref NeptuneSecurityGroup 340 | Tags: 341 | - Key: Name 342 | Value: !Sub "${ProjectName}-neptune-cluster" 343 | - Key: Project 344 | Value: !Ref ProjectName 345 | 346 | NeptuneInstance: 347 | Type: AWS::Neptune::DBInstance 348 | Properties: 349 | DBClusterIdentifier: !Ref NeptuneCluster 350 | DBInstanceClass: !Ref NeptuneInstanceClass 351 | DBInstanceIdentifier: !Sub "${ProjectName}-neptune" 352 | DBSubnetGroupName: !Ref NeptuneSubnetGroup 353 | Tags: 354 | - Key: Name 355 | Value: !Sub "${ProjectName}-neptune-instance" 356 | - Key: Project 357 | Value: !Ref ProjectName 358 | 359 | NeptuneSubnetGroup: 360 | Type: AWS::Neptune::DBSubnetGroup 361 | Properties: 362 | DBSubnetGroupDescription: !Sub "${ProjectName} Subnet Group" 363 | DBSubnetGroupName: !Sub "${ProjectName}-subnet-group" 364 | SubnetIds: 365 | - !Ref PrivateSubnet1 366 | - !Ref PrivateSubnet2 367 | Tags: 368 | - Key: Name 369 | Value: !Sub "${ProjectName}-neptune-subnet-group" 370 | - Key: Project 371 | Value: !Ref ProjectName 372 | 373 | NeptuneSecurityGroup: 374 | Type: AWS::EC2::SecurityGroup 375 | Properties: 376 | GroupName: !Sub "${ProjectName}-neptune-security-group" 377 | GroupDescription: Allow access to Amazon Neptune from Lambda 378 | SecurityGroupIngress: 379 | - IpProtocol: tcp 380 | FromPort: 8182 381 | ToPort: 8182 382 | SourceSecurityGroupId: !Ref LambdaSecurityGroup 383 | VpcId: !Ref VPC 384 | Tags: 385 | - Key: Project 386 | Value: !Ref ProjectName 387 | 388 | ## Lambda ## 389 | ElastiCacheIntegrationFunction: 390 | Type: AWS::Serverless::Function 391 | Properties: 392 | CodeUri: elasticache/ 393 | Description: Integrates AWS AppSync with ElastiCache 394 | Policies: 395 | - VPCAccessPolicy: {} 396 | VpcConfig: 397 | SecurityGroupIds: 398 | - !Ref LambdaSecurityGroup 399 | SubnetIds: 400 | - !Ref LambdaSubnet1 401 | - !Ref LambdaSubnet2 402 | Environment: 403 | Variables: 404 | ELASTICACHE_ENDPOINT: !GetAtt RedisCluster.ConfigurationEndPoint.Address 405 | ELASTICACHE_PORT: !GetAtt RedisCluster.ConfigurationEndPoint.Port 406 | ELASTICACHE_GEO_KEY: 'restaurants:geo' 407 | 408 | NeptuneIntegrationFunction: 409 | Type: AWS::Serverless::Function 410 | Properties: 411 | CodeUri: neptune/ 412 | Handler: index.handler 413 | Description: Integrates AWS AppSync with Neptune 414 | Policies: 415 | - VPCAccessPolicy: {} 416 | VpcConfig: 417 | SecurityGroupIds: 418 | - !Ref LambdaSecurityGroup 419 | SubnetIds: 420 | - !Ref LambdaSubnet1 421 | - !Ref LambdaSubnet2 422 | Environment: 423 | Variables: 424 | NEPTUNE_ENDPOINT: !GetAtt NeptuneCluster.Endpoint 425 | NEPTUNE_PORT: "8182" 426 | 427 | StreamFunction: 428 | Type: AWS::Serverless::Function 429 | Properties: 430 | CodeUri: stream/ 431 | Description: Published updates from DynamoDB Stream to ElastiCache and Neptune 432 | Policies: 433 | - VPCAccessPolicy: {} 434 | VpcConfig: 435 | SecurityGroupIds: 436 | - !Ref LambdaSecurityGroup 437 | SubnetIds: 438 | - !Ref LambdaSubnet1 439 | - !Ref LambdaSubnet2 440 | Environment: 441 | Variables: 442 | ELASTICACHE_ENDPOINT: !GetAtt RedisCluster.ConfigurationEndPoint.Address 443 | ELASTICACHE_PORT: !GetAtt RedisCluster.ConfigurationEndPoint.Port 444 | ELASTICACHE_GEO_KEY: 'restaurants:geo' 445 | Events: 446 | StreamEvent: 447 | Type: DynamoDB 448 | Properties: 449 | Stream: !GetAtt RestaurantsTable.StreamArn 450 | StartingPosition: LATEST 451 | 452 | DDBSetupFunction: 453 | Type: AWS::Serverless::Function 454 | Properties: 455 | CodeUri: setup/ 456 | Description: Adds mock data to DynamoDB table 457 | Policies: 458 | - DynamoDBCrudPolicy: 459 | TableName: !Ref RestaurantsTable 460 | Environment: 461 | Variables: 462 | RESTAURANT_TABLE: !Ref RestaurantsTable 463 | 464 | NeptuneSetupFunction: 465 | Type: AWS::Serverless::Function 466 | Properties: 467 | CodeUri: setup/ 468 | Handler: neptune.handler 469 | Description: Adds mock data to Neptune 470 | Timeout: 30 471 | Policies: 472 | - VPCAccessPolicy: {} 473 | VpcConfig: 474 | SecurityGroupIds: 475 | - !Ref LambdaSecurityGroup 476 | SubnetIds: 477 | - !Ref LambdaSubnet1 478 | - !Ref LambdaSubnet2 479 | Environment: 480 | Variables: 481 | NEPTUNE_ENDPOINT: !GetAtt NeptuneCluster.Endpoint 482 | NEPTUNE_PORT: "8182" 483 | 484 | LambdaSecurityGroup: 485 | Type: AWS::EC2::SecurityGroup 486 | Properties: 487 | VpcId: !Ref VPC 488 | GroupDescription: Enable Redis and Neptune access 489 | Tags: 490 | - Key: Project 491 | Value: !Ref ProjectName 492 | 493 | ## DynamoDB ## 494 | RestaurantsTable: 495 | Type: AWS::DynamoDB::Table 496 | Properties: 497 | BillingMode: PAY_PER_REQUEST 498 | AttributeDefinitions: 499 | - AttributeName: id 500 | AttributeType: S 501 | KeySchema: 502 | - AttributeName: id 503 | KeyType: HASH 504 | StreamSpecification: 505 | StreamViewType: NEW_IMAGE 506 | TableName: !Sub ${ProjectName}-restaurants-table 507 | 508 | ## Step Functions ## 509 | SetupStateMachine: 510 | Type: AWS::StepFunctions::StateMachine 511 | Properties: 512 | StateMachineName: aws-appsync-alt-data-sources 513 | RoleArn: !GetAtt SetupStateMachineRole.Arn 514 | DefinitionString: !Sub | 515 | { 516 | "Comment": "Setup data demo", 517 | "StartAt": "Setup DynamoDB", 518 | "States": { 519 | "Setup DynamoDB": { 520 | "Type": "Task", 521 | "Resource": "${DDBSetupFunction.Arn}", 522 | "ResultPath": "$", 523 | "TimeoutSeconds": 10, 524 | "Next": "Wait for One Minute", 525 | "Catch": [{ 526 | "ErrorEquals": [ "States.ALL" ], 527 | "ResultPath": "$.error", 528 | "Next": "HandleError" 529 | }] 530 | }, 531 | "Wait for One Minute": { 532 | "Type": "Wait", 533 | "Seconds": 60, 534 | "Next": "Setup Neptune" 535 | }, 536 | "Setup Neptune": { 537 | "Type": "Task", 538 | "Resource": "${NeptuneSetupFunction.Arn}", 539 | "ResultPath": "$", 540 | "TimeoutSeconds": 30, 541 | "Catch": [{ 542 | "ErrorEquals": [ "States.ALL" ], 543 | "ResultPath": "$.error", 544 | "Next": "HandleError" 545 | }], 546 | "End": true 547 | }, 548 | "HandleError": { 549 | "Type": "Fail", 550 | "Cause": "$.error" 551 | } 552 | } 553 | } 554 | 555 | ## IAM ## 556 | SetupStateMachineRole: 557 | Type: AWS::IAM::Role 558 | Properties: 559 | Path: / 560 | RoleName: aws-appsync-alt-data-sources-sfn-role 561 | AssumeRolePolicyDocument: 562 | Version: "2012-10-17" 563 | Statement: 564 | - Effect: Allow 565 | Principal: 566 | Service: !Sub states.${AWS::Region}.amazonaws.com 567 | Action: sts:AssumeRole 568 | Policies: 569 | - PolicyName: StatesExecutionPolicy 570 | PolicyDocument: 571 | Version: "2012-10-17" 572 | Statement: 573 | - Effect: Allow 574 | Action: 575 | - lambda:InvokeFunction 576 | Resource: 577 | - !GetAtt DDBSetupFunction.Arn 578 | - !GetAtt NeptuneSetupFunction.Arn 579 | 580 | AppSyncServiceRole: 581 | Type: AWS::IAM::Role 582 | Properties: 583 | Path: / 584 | RoleName: aws-appsync-alt-data-sources-service-role 585 | ManagedPolicyArns: 586 | - arn:aws:iam::aws:policy/service-role/AWSAppSyncPushToCloudWatchLogs 587 | AssumeRolePolicyDocument: 588 | Version: "2012-10-17" 589 | Statement: 590 | - Effect: Allow 591 | Action: 592 | - sts:AssumeRole 593 | Principal: 594 | Service: 595 | - appsync.amazonaws.com 596 | Policies: 597 | - PolicyName: aws-appsync-alt-data-sources-service-role-lambda-policy 598 | PolicyDocument: 599 | Version: "2012-10-17" 600 | Statement: 601 | - Effect: Allow 602 | Action: 603 | - lambda:InvokeFunction 604 | Resource: 605 | - !GetAtt ElastiCacheIntegrationFunction.Arn 606 | - !GetAtt NeptuneIntegrationFunction.Arn 607 | - PolicyName: aws-daily-news-appsync-service-role-ddb-policy 608 | PolicyDocument: 609 | Version: "2012-10-17" 610 | Statement: 611 | - Effect: Allow 612 | Action: 613 | - dynamodb:GetItem 614 | - dynamodb:BatchGetItem 615 | - dynamodb:Query 616 | - dynamodb:Scan 617 | Resource: 618 | - !GetAtt RestaurantsTable.Arn 619 | - !Sub "${RestaurantsTable.Arn}/*" 620 | 621 | ## VPC ## 622 | VPC: 623 | Type: AWS::EC2::VPC 624 | Properties: 625 | EnableDnsSupport: true 626 | EnableDnsHostnames: true 627 | CidrBlock: !FindInMap ['SubnetConfig', 'VPC', 'CIDR'] 628 | Tags: 629 | - Key: Name 630 | Value: !Sub "${ProjectName}-vpc" 631 | - Key: Project 632 | Value: !Ref ProjectName 633 | 634 | PrivateSubnet1: 635 | Type: AWS::EC2::Subnet 636 | Properties: 637 | AvailabilityZone: 638 | Fn::Select: 639 | - 0 640 | - Fn::GetAZs: !Ref AWS::Region 641 | VpcId: !Ref VPC 642 | CidrBlock: !FindInMap ['SubnetConfig', 'Private1', 'CIDR'] 643 | Tags: 644 | - Key: Name 645 | Value: !Sub "${ProjectName}-private-subnet-1" 646 | - Key: Project 647 | Value: !Ref ProjectName 648 | 649 | PrivateSubnet2: 650 | Type: AWS::EC2::Subnet 651 | Properties: 652 | AvailabilityZone: 653 | Fn::Select: 654 | - 1 655 | - Fn::GetAZs: !Ref AWS::Region 656 | VpcId: !Ref VPC 657 | CidrBlock: !FindInMap ['SubnetConfig', 'Private2', 'CIDR'] 658 | Tags: 659 | - Key: Name 660 | Value: !Sub "${ProjectName}-private-subnet-2" 661 | - Key: Project 662 | Value: !Ref ProjectName 663 | 664 | LambdaSubnet1: 665 | Type: AWS::EC2::Subnet 666 | Properties: 667 | AvailabilityZone: 668 | Fn::Select: 669 | - 0 670 | - Fn::GetAZs: !Ref AWS::Region 671 | VpcId: !Ref VPC 672 | CidrBlock: !FindInMap ['SubnetConfig', 'Lambda1', 'CIDR'] 673 | Tags: 674 | - Key: Name 675 | Value: !Sub "${ProjectName}-lambda-subnet-1" 676 | - Key: Project 677 | Value: !Ref ProjectName 678 | 679 | LambdaSubnet2: 680 | Type: AWS::EC2::Subnet 681 | Properties: 682 | AvailabilityZone: 683 | Fn::Select: 684 | - 1 685 | - Fn::GetAZs: !Ref AWS::Region 686 | VpcId: !Ref VPC 687 | CidrBlock: !FindInMap ['SubnetConfig', 'Lambda2', 'CIDR'] 688 | Tags: 689 | - Key: Name 690 | Value: !Sub "${ProjectName}-lambda-subnet-2" 691 | - Key: Project 692 | Value: !Ref ProjectName 693 | 694 | Outputs: 695 | ApiEndpoint: 696 | Description: AppSync Endpoint 697 | Value: !GetAtt RestaurantApi.GraphQLUrl 698 | 699 | ApiId: 700 | Description: AppSync API ID 701 | Value: !GetAtt RestaurantApi.ApiId 702 | 703 | ApiKey: 704 | Description: AppSync API Key 705 | Value: !GetAtt RestaurantApiKey.ApiKey 706 | 707 | SetupStateMachine: 708 | Description: ARN of Setup State Machine 709 | Value: !Ref SetupStateMachine 710 | --------------------------------------------------------------------------------