├── .github
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── appsync
├── resolver-mappings
│ ├── Mutation.addInventory.request
│ ├── Mutation.addInventory.response
│ ├── Mutation.createOrder.request
│ ├── Mutation.createOrder.response
│ ├── Mutation.registerUser.request
│ ├── Mutation.registerUser.response
│ ├── Mutation.updateUserBalance.request
│ ├── Mutation.updateUserBalance.response
│ ├── Query.getMe.request
│ ├── Query.getMe.response
│ ├── Query.getMyOrders.request
│ ├── Query.getMyOrders.response
│ ├── Query.getOrder.request
│ ├── Query.getOrder.response
│ ├── Query.listInventory.request
│ ├── Query.listInventory.response
│ ├── User.orders.request
│ └── User.orders.response
├── resolvers.json
└── schema.graphql
├── img
├── 00.png
├── 01.png
├── 02.png
├── 03.png
├── 04.png
├── 05.png
├── 06.png
├── 07.png
├── 08.png
├── 09.png
├── 10.png
├── 11.png
├── 12.png
└── 13.png
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── sam
├── .gitignore
├── UnicornLoyalty
│ ├── index.js
│ └── lambda-payloads.json
├── cfn-deploy.yml
└── template.yaml
└── src
├── App.css
├── App.js
├── App.test.js
├── components
├── Order.js
├── Points.js
└── Unicorns.js
├── images
├── shadowfax.png
├── unicorn.png
└── unicorn_small.png
├── index.css
├── index.js
└── registerServiceWorker.js
/.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 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /build
7 | /out-tsc
8 |
9 | # dependencies
10 | /node_modules
11 |
12 | # IDEs and editors
13 | /.idea
14 | .project
15 | .classpath
16 | .c9/
17 | *.launch
18 | .settings/
19 | *.sublime-workspace
20 |
21 | # IDE - VSCode
22 | .vscode/*
23 | !.vscode/settings.json
24 | !.vscode/tasks.json
25 | !.vscode/launch.json
26 | !.vscode/extensions.json
27 |
28 | # misc
29 | /.sass-cache
30 | /connect.lock
31 | /coverage
32 | /libpeerconnection.log
33 | npm-debug.log
34 | testem.log
35 | /typings
36 |
37 | # e2e
38 | /e2e/*.js
39 | /e2e/*.map
40 |
41 | # System Files
42 | .DS_Store
43 | Thumbs.db
44 |
45 | #awsmobilejs
46 | /awsmobilejs/*
47 | /src/aws-exports.js
48 | /awsmobilejs
--------------------------------------------------------------------------------
/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-serverless-appsync-loyalty/issues), or [recently closed](https://github.com/aws-samples/aws-serverless-appsync-loyalty/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-serverless-appsync-loyalty/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-serverless-appsync-loyalty/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 2018 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 this
4 | software and associated documentation files (the "Software"), to deal in the Software
5 | without restriction, including without limitation the rights to use, copy, modify,
6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
7 | 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 IMPLIED,
10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Unicorn Loyalty: E-Commerce Serverless GraphQL Loyalty Sample App
2 |
3 | Unicorn Loyalty is a new startup that provides fantastic unicorns for customers. The business just started and it's giving away 1000 Unicoin Points for new customers to purchase the unicorns available on inventory.
4 |
5 | (Live Coding walk-through video covering Lambda and AppSync manual setup available on https://www.youtube.com/watch?v=WOQIqRVzkas or https://www.twitch.tv/videos/288401222)
6 |
7 | ### Behind the Scenes
8 |
9 | 
10 |
11 | * AWS AppSync
12 | * AWS Lambda
13 | * Amazon DynamoDB
14 | * Amazon Cognito User Pools
15 | * Amazon Pinpoint
16 | * No Servers!
17 |
18 | #### Prerequisites
19 |
20 | * [AWS Account](https://aws.amazon.com/mobile/details) with appropriate permissions to create the related resources
21 | * [NodeJS v8.10+](https://nodejs.org/en/download/) with [NPM](https://docs.npmjs.com/getting-started/installing-node)
22 | * [AWS Mobile CLI](https://github.com/aws/awsmobile-cli) `(npm install -g awsmobile-cli)`
23 | * [AWS Amplify](https://aws.github.io/aws-amplify/media/react_guide) `(npm install -g aws-amplify-react)`
24 | * [create-react-app](https://github.com/facebook/create-react-app) `(npm install -g create-react-app)`
25 |
26 | #### Optional
27 |
28 | * [AWS Cloud9](https://aws.amazon.com/cloud9/) :
29 | We assume you are using Cloud9 to build this application. You can optionally choose to use any IDE/Text Editor such as Atom or VS Code, in that case you should use the [AWS SAM CLI](https://github.com/awslabs/aws-sam-cli) to deploy Lambda and the DynamoDB tables.
30 |
31 | ### Initial Setup - Mobile CLI and Amplify
32 |
33 | Create a Cloud 9 environment and execute:
34 |
35 | $ create-react-app unicorn-loyalty
36 | $ cd unicorn-loyalty
37 |
38 |
39 | Install and use the latest LTS Node version:
40 |
41 |
42 | $ nvm i v8
43 |
44 |
45 | Set up your AWS resources with the AWS Mobile CLI:
46 |
47 |
48 | $ awsmobile init
49 |
50 |
51 | Chose the default options, make sure you are logged in with a user that has administrator access and click the resulting link to create an IAM user for the Mobile CLI by selecting OPEN:
52 |
53 | 
54 |
55 | Follow the steps in the IAM Console with default options then use the generated credentials to configure the access in the Mobile CLI:
56 |
57 | 
58 |
59 | Now let's add the features we need for our application (User Sign In, Analytics, Hosting and AppSync):
60 |
61 | 
62 |
63 | Execute the following command to commit the changes:
64 |
65 | $ awsmobile push
66 |
67 | To test if everything is working, open App.js and let's add 4 extra lines of code to add AuthN/Z with MFA (withAuthenticator HOC). Replace the existing code with:
68 |
69 | ```javascript
70 | import React, { Component } from 'react';
71 | import logo from './logo.svg';
72 | import './App.css';
73 | import Amplify from 'aws-amplify';
74 | import { withAuthenticator } from 'aws-amplify-react';
75 | import aws_exports from './aws-exports'; // specify the location of aws-exports.js file on your project
76 | Amplify.configure(aws_exports);
77 |
78 | class App extends Component {
79 | render() {
80 | return (
81 |
82 |
83 |
84 |
Welcome to React
85 |
86 |
87 | To get started, edit src/App.js and save to reload.
88 |
89 |
90 | );
91 | }
92 | }
93 |
94 | export default withAuthenticator(App, { includeGreetings: true });
95 | ```
96 | Now execute:
97 |
98 |
99 | $ awsmobile run
100 |
101 |
102 | Then click on PREVIEW -> PREVIEW RUNNING APPLICATION on Cloud9 and sign up a user:
103 |
104 | 
105 |
106 | Download all files from the Github repo. Upload them to your Cloud9 workspace (FILE -> UPLOAD LOCAL FILES), overwriting the files in the local React app folder:
107 |
108 | 
109 |
110 | ### Lambda Setup
111 |
112 | From Cloud9 select the AWS Resources tab on the right, you'll find a local Lambda funcion under the ```sam``` folder called ```UnicornLoyalty```. Right click and select EDIT CONFIG to check the related SAM template and EDIT FUNCTION to check the Lambda code:
113 |
114 | 
115 |
116 | By default the 1000 Unicoins give away special is valid until the last day of 2018. Edit the expiry date accordingly if you want to modify the deadline:
117 |
118 | ```javascript
119 | let expiry = new Date('2018.12.31').getTime() / 1000;
120 | ```
121 |
122 | On the same menu click DEPLOY (or execute ```sam package/deploy``` with the SAM CLI it you're not on Cloud9). The SAM Template will deploy a Lambda function and 3 DynamoDB tables. Lambda will interact directly with the Users table by detecting newly registered users to make sure they will only get the 1000 Unicoin Points special before the expiry date as well as manage and update the user unicoins/points balance when a order is placed. The other tables will be used directly by AppSync.
123 |
124 | ### AppSync Setup
125 |
126 | The Mobile CLI creates a sample Event API on AppSync by default. We wont use that. Go to the AppSync console and paste the contents of the file ```appsync/schema.graphql``` in the SCHEMA setion:
127 |
128 | 
129 |
130 | Go to DATA SOURCES, delete the 2 tables from the sample. Now create 3 data sources as follows, pointing to the Items and Orders tables and the Lambda function created earlier :
131 |
132 | 
133 |
134 | Back to Cloud9, execute the following command to retrieve the AppSync changes:
135 |
136 | $ awsmobile pull
137 |
138 | Go to the folder ```awsmobilejs/backend/appsync``` and delete the file ```resolvers.json``` and the folder ```resolver-mappings```.
139 |
140 | Now go to the folder ```appsync``` in the root of the application directory and copy the file ```resolvers.json``` and the folder ```resolver-mappings``` to the previous folder ```awsmobilejs/backend/appsync```, replacing the deleted files.
141 |
142 | Next step is to configure AppSync authentication. Execute the following command and select the options:
143 |
144 |
145 | $ awsmobile appsync configure
146 |
147 | ? Please specify the auth type: AMAZON_COGNITO_USER_POOLS
148 | ? user pool id:
149 | ? region:
150 | ? default action: ALLOW
151 |
152 |
153 | Execute the following command to commit the changes:
154 |
155 |
156 | $ awsmobile push
157 |
158 |
159 | ### Creating Some Unicorns
160 |
161 | Open the file ```src/aws-exports.js``` generated by the Mobile CLI and copy the value of the key "aws_user_pools_web_client_id"to retrieve the App Client ID the Cognito User Pools is using to authenticate AppSync calls.
162 |
163 | Go to the AppSync console and select the QUERY section. Click LOGIN WITH USER POOLS, use the client ID you just retrieved from ```aws-exports.js``` and the credentials from the user you signed up earlier. You'll also need to provide a MFA code.
164 |
165 | Execute the following GraphQL operation (mutation) to create your first Unicorn:
166 |
167 | ```javascript
168 | mutation {
169 | addInventory(itemDescription: "Amazing Unicorn", price: 50){
170 | itemId
171 | itemDescription
172 | price
173 | }
174 | }
175 | ```
176 |
177 | Create as many Unicorns as you'd like by changing the values. Going to the DynamoDB console, you can confirm the Unicorns were created successfully:
178 |
179 | 
180 |
181 | ### Welcome to the Unicorn Loyalty Shop
182 |
183 | Back to Cloud9 execute:
184 |
185 |
186 | $ awsmobile run
187 |
188 | (In case of errors or missing packages, you might need to run ```npm install``` and try again)
189 |
190 | Then click on PREVIEW -> PREVIEW RUNNING APPLICATION to access the Unicorn Loyalty App:
191 |
192 | 
193 |
194 | Finally you can publish to CloudFront and S3 with a single command:
195 |
196 | $ awsmobile publish
197 |
198 | It will automatically make the Unicorn Loyalty app available in a powerful and reliable global content delivery network backed by a S3 website:
199 |
200 | 
201 |
202 | You can get Analitycs about usage and revenue from Amazon Pinpoint, all thanks to a couple of lines of code required by the AWS Amplify Analytics component and the to the Pinpoint Project AWS Mobile CLI creates by default:
203 |
204 | ```javascript
205 | Analytics.record({
206 | name: 'unicornsPurchase',
207 | attributes: {},
208 | metrics: { totalOrder: order.totalOrder }
209 | });
210 | Analytics.record('_monetization.purchase', {
211 | _currency: 'USD',
212 | _product_id: order.itemId,
213 | }, {
214 | _item_price: order.unitPrice,
215 | _quantity: order.count,
216 | })
217 | ```
218 |
219 | * Usage Data:
220 | 
221 |
222 | * Revenue Data:
223 | 
224 |
225 | ## Feel like a challenge? What's next?
226 |
227 | We added the backend logic to get all orders from the current user:
228 |
229 | ```graphql
230 | query {
231 | getMyOrders{
232 | orderId
233 | itemId
234 | count
235 | date
236 | totalOrder
237 | }
238 | }
239 | ```
240 |
241 | Alternatively, you can get the same result using the relation between the User type (Users Table) and the Order type (Orders Table) by querying the User ID:
242 |
243 | ```graphql
244 | query {
245 | getMe(userId:""){
246 | orders{
247 | orderId
248 | }
249 | }
250 | }
251 | ```
252 |
253 | We also added the backend logic to get the items in a specific order by querying the Order ID:
254 |
255 | ```graphql
256 | query {
257 | getOrder(orderId:""){
258 | orderId
259 | itemId
260 | count
261 | date
262 | totalOrder
263 | }
264 | }
265 | ```
266 |
267 | Using AWS Amplify implement a new feature to the Unicorn Loyalty app so users can get information on all the orders they placed as well as Unicorns that were purchased in a previous order. Bonus points if implemented with the built-in pagination support.
268 |
269 | Go Build with Serverless GraphQL!
270 |
271 | ## License Summary
272 |
273 | This sample code is made available under a modified MIT license. See the LICENSE file.
274 |
--------------------------------------------------------------------------------
/appsync/resolver-mappings/Mutation.addInventory.request:
--------------------------------------------------------------------------------
1 | ## Below example shows how to create an object from all provided GraphQL arguments
2 | ## The primary key of the obejct is a randomly generated UUD using the $util.autoId() utility
3 | ## Other utilities include $util.matches() for regular expressions, $util.time.nowISO8601() or
4 | ## $util.time.nowEpochMilliSeconds() for timestamps, and even List or Map helpers like
5 | ## $util.list.copyAndRetainAll() $util.map.copyAndRemoveAllKeys() for shallow copies
6 | ## Read more: https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html#utility-helpers-in-util
7 |
8 | {
9 | "version" : "2017-02-28",
10 | "operation" : "PutItem",
11 | "key" : {
12 | ## If object "id" should come from GraphQL arguments, change to $util.dynamodb.toDynamoDBJson($ctx.args.id)
13 | "itemId": $util.dynamodb.toDynamoDBJson($util.autoId()),
14 | },
15 | "attributeValues" : $util.dynamodb.toMapValuesJson($ctx.args)
16 | }
--------------------------------------------------------------------------------
/appsync/resolver-mappings/Mutation.addInventory.response:
--------------------------------------------------------------------------------
1 | ## Pass back the result from DynamoDB. **
2 | $util.toJson($ctx.result)
--------------------------------------------------------------------------------
/appsync/resolver-mappings/Mutation.createOrder.request:
--------------------------------------------------------------------------------
1 | ## Below example shows how to create an object from all provided GraphQL arguments
2 | ## The primary key of the obejct is a randomly generated UUD using the $util.autoId() utility
3 | ## Other utilities include $util.matches() for regular expressions, $util.time.nowISO8601() or
4 | ## $util.time.nowEpochMilliSeconds() for timestamps, and even List or Map helpers like
5 | ## $util.list.copyAndRetainAll() $util.map.copyAndRemoveAllKeys() for shallow copies
6 | ## Read more: https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html#utility-helpers-in-util
7 |
8 | {
9 | "version" : "2017-02-28",
10 | "operation" : "PutItem",
11 | "key" : {
12 | "orderId" : { "S" : "${context.arguments.orderId}" }
13 | },
14 | "attributeValues" : {
15 | "itemId": { "S": "${context.arguments.itemId}" },
16 | "userId": { "S": "${context.identity.sub}" },
17 | "totalOrder": { "N": "${context.arguments.totalOrder}" },
18 | "date": { "S": "$util.time.nowFormatted("dd-MM-yyyy")" },
19 | "count": { "S": "${context.arguments.count}" }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/appsync/resolver-mappings/Mutation.createOrder.response:
--------------------------------------------------------------------------------
1 | ## Pass back the result from DynamoDB. **
2 | $util.toJson($ctx.result)
--------------------------------------------------------------------------------
/appsync/resolver-mappings/Mutation.registerUser.request:
--------------------------------------------------------------------------------
1 | #**
2 | The value of 'payload' after the template has been evaluated
3 | will be passed as the event to AWS Lambda.
4 | *#
5 | {
6 | "version" : "2017-02-28",
7 | "operation": "Invoke",
8 | "payload": {
9 | "field": "registerUser",
10 | "arguments": $utils.toJson($context.args)
11 | }
12 | }
--------------------------------------------------------------------------------
/appsync/resolver-mappings/Mutation.registerUser.response:
--------------------------------------------------------------------------------
1 | $util.toJson($context.result)
--------------------------------------------------------------------------------
/appsync/resolver-mappings/Mutation.updateUserBalance.request:
--------------------------------------------------------------------------------
1 | #**
2 | The value of 'payload' after the template has been evaluated
3 | will be passed as the event to AWS Lambda.
4 | *#
5 | {
6 | "version" : "2017-02-28",
7 | "operation": "Invoke",
8 | "payload": {
9 | "field": "updateBalance",
10 | "arguments": $utils.toJson($context.args)
11 | }
12 | }
--------------------------------------------------------------------------------
/appsync/resolver-mappings/Mutation.updateUserBalance.response:
--------------------------------------------------------------------------------
1 | $util.toJson($context.result)
--------------------------------------------------------------------------------
/appsync/resolver-mappings/Query.getMe.request:
--------------------------------------------------------------------------------
1 | #**
2 | The value of 'payload' after the template has been evaluated
3 | will be passed as the event to AWS Lambda.
4 | *#
5 | {
6 | "version" : "2017-02-28",
7 | "operation": "Invoke",
8 | "payload": {
9 | "field": "getUser",
10 | "arguments": $utils.toJson($context.args)
11 | }
12 | }
--------------------------------------------------------------------------------
/appsync/resolver-mappings/Query.getMe.response:
--------------------------------------------------------------------------------
1 | $util.toJson($context.result)
--------------------------------------------------------------------------------
/appsync/resolver-mappings/Query.getMyOrders.request:
--------------------------------------------------------------------------------
1 | {
2 | "version" : "2017-02-28",
3 | "operation" : "Scan",
4 | "filter" : {
5 | "expression": "#userId = :userId",
6 | "expressionNames": {
7 | "#userId": "userId"
8 | },
9 | "expressionValues" : {
10 | ":userId" : {"S": "$ctx.identity.sub"}
11 | }
12 | },
13 | "limit": $util.defaultIfNull($ctx.args.first, 20),
14 | "nextToken": $util.toJson($util.defaultIfNullOrEmpty($ctx.args.after, null))
15 | }
--------------------------------------------------------------------------------
/appsync/resolver-mappings/Query.getMyOrders.response:
--------------------------------------------------------------------------------
1 | #**
2 | Return a flat list of results from a Query or Scan operation.
3 | *#
4 | $util.toJson($ctx.result)
--------------------------------------------------------------------------------
/appsync/resolver-mappings/Query.getOrder.request:
--------------------------------------------------------------------------------
1 | {
2 | "version" : "2017-02-28",
3 | "operation" : "Scan",
4 | "filter" : {
5 | "expression": "#userId = :userId AND #orderId = :orderId",
6 | "expressionNames": {
7 | "#userId": "userId",
8 | "#orderId": "orderId"
9 | },
10 | "expressionValues" : {
11 | ":userId" : {"S": "$ctx.identity.sub"},
12 | ":orderId" : $util.dynamodb.toDynamoDBJson($ctx.args.orderId)
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/appsync/resolver-mappings/Query.getOrder.response:
--------------------------------------------------------------------------------
1 | ## Pass back the result from DynamoDB. **
2 | $util.toJson($ctx.result)
--------------------------------------------------------------------------------
/appsync/resolver-mappings/Query.listInventory.request:
--------------------------------------------------------------------------------
1 | ## Below example will return all items in a table using a Scan
2 | ## Filtering conditions can be optionally added to scans with a "filter" and an "expression", however
3 | ## if possible it is best practice to use a Query and/or Index for frequent conditionals
4 | ## Read more: https://docs.aws.amazon.com/appsync/latest/devguide/resolver-mapping-template-reference-dynamodb.html#aws-appsync-resolver-mapping-template-reference-dynamodb-scan
5 | ## You can also paginate through records in a table by using "nextToken" and "limit", which can be
6 | ## arguments passed from your GraphQL query in a client application (you can uncomment out below)
7 | ## Read more: https://docs.aws.amazon.com/appsync/latest/devguide/configuring-resolvers.html#advanced-resolvers
8 |
9 | {
10 | "version" : "2017-02-28",
11 | "operation" : "Scan",
12 | "limit": $util.defaultIfNull($ctx.args.first, 20),
13 | "nextToken": $util.toJson($util.defaultIfNullOrEmpty($ctx.args.after, null))
14 | }
--------------------------------------------------------------------------------
/appsync/resolver-mappings/Query.listInventory.response:
--------------------------------------------------------------------------------
1 | ## Pass back the result from DynamoDB. **
2 | $util.toJson($ctx.result)
--------------------------------------------------------------------------------
/appsync/resolver-mappings/User.orders.request:
--------------------------------------------------------------------------------
1 | ## Below example shows how to look up an item with a Primary Key of "id" from GraphQL arguments
2 | ## The helper $util.dynamodb.toDynamoDBJson automatically converts to a DynamoDB formatted request
3 | ## There is a "context" object with arguments, identity, headers, and parent field information you can access.
4 | ## It also has a shorthand notation avaialable:
5 | ## - $context or $ctx is the root object
6 | ## - $ctx.arguments or $ctx.args contains arguments
7 | ## - $ctx.identity has caller information, such as $ctx.identity.username
8 | ## - $ctx.request.headers contains headers, such as $context.request.headers.xyz
9 | ## - $ctx.source is a map of the parent field, for instance $ctx.source.xyz
10 | ## Read more: https://docs.aws.amazon.com/appsync/latest/devguide/resolver-mapping-template-reference.html
11 |
12 | {
13 | "version" : "2017-02-28",
14 | "operation" : "Scan",
15 | "filter" : {
16 | "expression": "#userId = :userId",
17 | "expressionNames": {
18 | "#userId": "userId"
19 | },
20 | "expressionValues" : {
21 | ":userId" : { "S":"${context.source.userId}"}
22 | }
23 | }
24 | }
25 |
26 |
--------------------------------------------------------------------------------
/appsync/resolver-mappings/User.orders.response:
--------------------------------------------------------------------------------
1 | #**
2 | Return a flat list of results from a Query or Scan operation.
3 | *#
4 | $util.toJson($ctx.result.items)
--------------------------------------------------------------------------------
/appsync/resolvers.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "typeName": "Mutation",
4 | "fieldName": "addInventory",
5 | "dataSourceName": "Items",
6 | "requestMappingTemplate": "{managed-by-awsmobile-cli}:Mutation.addInventory.request",
7 | "responseMappingTemplate": "{managed-by-awsmobile-cli}:Mutation.addInventory.response"
8 | },
9 | {
10 | "typeName": "Mutation",
11 | "fieldName": "createOrder",
12 | "dataSourceName": "Orders",
13 | "requestMappingTemplate": "{managed-by-awsmobile-cli}:Mutation.createOrder.request",
14 | "responseMappingTemplate": "{managed-by-awsmobile-cli}:Mutation.createOrder.response"
15 | },
16 | {
17 | "typeName": "Mutation",
18 | "fieldName": "registerUser",
19 | "dataSourceName": "Users",
20 | "requestMappingTemplate": "{managed-by-awsmobile-cli}:Mutation.registerUser.request",
21 | "responseMappingTemplate": "{managed-by-awsmobile-cli}:Mutation.registerUser.response"
22 | },
23 | {
24 | "typeName": "Mutation",
25 | "fieldName": "updateUserBalance",
26 | "dataSourceName": "Users",
27 | "requestMappingTemplate": "{managed-by-awsmobile-cli}:Mutation.updateUserBalance.request",
28 | "responseMappingTemplate": "{managed-by-awsmobile-cli}:Mutation.updateUserBalance.response"
29 | },
30 | {
31 | "typeName": "Query",
32 | "fieldName": "getMe",
33 | "dataSourceName": "Users",
34 | "requestMappingTemplate": "{managed-by-awsmobile-cli}:Query.getMe.request",
35 | "responseMappingTemplate": "{managed-by-awsmobile-cli}:Query.getMe.response"
36 | },
37 | {
38 | "typeName": "Query",
39 | "fieldName": "getMyOrders",
40 | "dataSourceName": "Orders",
41 | "requestMappingTemplate": "{managed-by-awsmobile-cli}:Query.getMyOrders.request",
42 | "responseMappingTemplate": "{managed-by-awsmobile-cli}:Query.getMyOrders.response"
43 | },
44 | {
45 | "typeName": "Query",
46 | "fieldName": "getOrder",
47 | "dataSourceName": "Orders",
48 | "requestMappingTemplate": "{managed-by-awsmobile-cli}:Query.getOrder.request",
49 | "responseMappingTemplate": "{managed-by-awsmobile-cli}:Query.getOrder.response"
50 | },
51 | {
52 | "typeName": "Query",
53 | "fieldName": "listInventory",
54 | "dataSourceName": "Items",
55 | "requestMappingTemplate": "{managed-by-awsmobile-cli}:Query.listInventory.request",
56 | "responseMappingTemplate": "{managed-by-awsmobile-cli}:Query.listInventory.response"
57 | },
58 | {
59 | "typeName": "User",
60 | "fieldName": "orders",
61 | "dataSourceName": "Orders",
62 | "requestMappingTemplate": "{managed-by-awsmobile-cli}:User.orders.request",
63 | "responseMappingTemplate": "{managed-by-awsmobile-cli}:User.orders.response"
64 | }
65 | ]
--------------------------------------------------------------------------------
/appsync/schema.graphql:
--------------------------------------------------------------------------------
1 | schema {
2 | query: Query
3 | mutation: Mutation
4 | subscription: Subscription
5 | }
6 |
7 | type Query {
8 | getMe(userId: ID! ): User
9 | listInventory(after: String,first: Int ): ItemsConnection
10 | getOrder(orderId: ID! ): OrdersConnection
11 | getMyOrders(after: String,first: Int ): OrdersConnection
12 | }
13 |
14 | type Mutation {
15 | registerUser(userId: ID!,username: String! ): User
16 | addInventory(itemDescription: String!,price: Int! ): Item
17 | updateUserBalance(userId: ID!,username: String!,points: Int! ): User
18 | createOrder(orderId: ID!,itemId: ID!,date: String,count: Int,totalOrder: Int ): Order
19 | }
20 |
21 | type Subscription {
22 | subscribeToPoints: User
23 | @aws_subscribe(mutations: ["updateUserBalance"])
24 | }
25 |
26 | type User {
27 | userId: ID!
28 | username: String
29 | points: Int
30 | orders: [Order]
31 | }
32 |
33 | type Item {
34 | itemId: ID!
35 | itemDescription: String!
36 | price: Int!
37 | count: Int
38 | }
39 |
40 | type ItemsConnection {
41 | items: [Item]
42 | nextToken: String
43 | }
44 |
45 | type Order {
46 | orderId: ID!
47 | itemId: ID!
48 | userId: ID
49 | date: String
50 | count: Int
51 | totalOrder: Int
52 | }
53 |
54 | type OrdersConnection {
55 | items: [Order]
56 | nextToken: String
57 | }
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/img/00.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/img/00.png
--------------------------------------------------------------------------------
/img/01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/img/01.png
--------------------------------------------------------------------------------
/img/02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/img/02.png
--------------------------------------------------------------------------------
/img/03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/img/03.png
--------------------------------------------------------------------------------
/img/04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/img/04.png
--------------------------------------------------------------------------------
/img/05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/img/05.png
--------------------------------------------------------------------------------
/img/06.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/img/06.png
--------------------------------------------------------------------------------
/img/07.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/img/07.png
--------------------------------------------------------------------------------
/img/08.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/img/08.png
--------------------------------------------------------------------------------
/img/09.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/img/09.png
--------------------------------------------------------------------------------
/img/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/img/10.png
--------------------------------------------------------------------------------
/img/11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/img/11.png
--------------------------------------------------------------------------------
/img/12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/img/12.png
--------------------------------------------------------------------------------
/img/13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/img/13.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "twitch",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "aws-amplify": "^0.4.4",
7 | "aws-amplify-react": "^0.1.50",
8 | "bootstrap": "^4.1.1",
9 | "react": "^16.4.1",
10 | "react-dom": "^16.4.1",
11 | "react-scripts": "1.1.4"
12 | },
13 | "scripts": {
14 | "start": "react-scripts start",
15 | "build": "react-scripts build",
16 | "test": "react-scripts test --env=jsdom",
17 | "eject": "react-scripts eject"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | Unicorn Loyalty
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/sam/.gitignore:
--------------------------------------------------------------------------------
1 | .application.json
2 |
--------------------------------------------------------------------------------
/sam/UnicornLoyalty/index.js:
--------------------------------------------------------------------------------
1 | let AWS = require('aws-sdk');
2 | let dynamo = new AWS.DynamoDB.DocumentClient();
3 | let table = process.env.TABLE_NAME;
4 |
5 | exports.handler = (event, context, callback) => {
6 | let now = Date.now()/1000;
7 | let expiry = new Date('2018.12.31').getTime()/1000;
8 | let getParams = {
9 | TableName: table,
10 | Key:{
11 | "userId": event.arguments.userId
12 | }
13 | };
14 | let putParams = {
15 | TableName:table,
16 | Item:{
17 | "userId": event.arguments.userId,
18 | "username": event.arguments.username,
19 | "points": 1000
20 | }
21 | };
22 | if (expiry < now){
23 | putParams.Item.points = 0;
24 | }
25 | switch(event.field){
26 | case "getUser":
27 | dynamo.get(getParams, function(err,data){
28 | if (err){
29 | console.error("Error JSON: ", JSON.stringify(err,null,2));
30 | callback(err);
31 | } else if (data.Item == undefined) {
32 | let result = {
33 | "userId": "",
34 | "username": "",
35 | "points": 1000
36 | };
37 | callback(null,result);
38 | } else {
39 | console.log("User Exists: ", JSON.stringify(data,null,2));
40 | let result = {
41 | "userId": data.Item.userId,
42 | "username": data.Item.username,
43 | "points": data.Item.points
44 | };
45 | callback(null,result);
46 | }
47 | });
48 | break;
49 | case "registerUser":
50 | dynamo.put(putParams, function(err,data){
51 | if (err){
52 | console.error("Error JSON: ", JSON.stringify(err,null,2));
53 | callback(err);
54 | } else {
55 | console.log("User Added: ", JSON.stringify(data,null,2));
56 | let result = putParams.Item;
57 | callback(null,result);
58 | }
59 | });
60 | break;
61 | case "updateBalance":
62 | putParams.Item.points = event.arguments.points;
63 | dynamo.put(putParams, function(err,data){
64 | if (err){
65 | console.error("Error JSON: ", JSON.stringify(err,null,2));
66 | callback(err);
67 | } else {
68 | console.log("Balance Updated: ", JSON.stringify(data,null,2));
69 | let result = {
70 | "userId": event.arguments.userId,
71 | "username": event.arguments.username,
72 | "points": event.arguments.points
73 | }
74 | callback(null,result);
75 | }
76 | });
77 | break;
78 | default:
79 | callback("Unknown field, unable to resolve" + event.field, null);
80 | break;
81 | }
82 | };
--------------------------------------------------------------------------------
/sam/UnicornLoyalty/lambda-payloads.json:
--------------------------------------------------------------------------------
1 | {
2 | "UnicornLoyalty": {
3 | "lambda": {}
4 | }
5 | }
--------------------------------------------------------------------------------
/sam/cfn-deploy.yml:
--------------------------------------------------------------------------------
1 | AWSTemplateFormatVersion: '2010-09-09'
2 | Description: GraphQL Serverless e-Commerce Sample
3 | Parameters:
4 | userPoolId:
5 | Type: String
6 | Description: User Pool ID associated with this project
7 | Outputs:
8 | UnicornApiId:
9 | Description: Unique AWS AppSync GraphQL API Identifier
10 | Value: !GetAtt unicornApi.ApiId
11 | UnicornApiUrl:
12 | Description: The Endpoint URL of your GraphQL API.
13 | Value: !GetAtt unicornApi.GraphQLUrl
14 | Resources:
15 | UnicornFunction:
16 | Type: 'AWS::Lambda::Function'
17 | Properties:
18 | Handler: index.handler
19 | Runtime: nodejs10.x
20 | Code:
21 | ZipFile: !Sub |
22 | let AWS = require('aws-sdk');
23 | let dynamo = new AWS.DynamoDB.DocumentClient();
24 | let table = process.env.TABLE_NAME;
25 |
26 | exports.handler = (event, context, callback) => {
27 | let now = Date.now()/1000;
28 | let expiry = new Date('2018.12.31').getTime()/1000;
29 | let getParams = {
30 | TableName: table,
31 | Key:{
32 | "userId": event.arguments.userId
33 | }
34 | };
35 | let putParams = {
36 | TableName:table,
37 | Item:{
38 | "userId": event.arguments.userId,
39 | "username": event.arguments.username,
40 | "points": 1000
41 | }
42 | };
43 | if (expiry < now){
44 | putParams.Item.points = 0;
45 | }
46 | switch(event.field){
47 | case "getUser":
48 | dynamo.get(getParams, function(err,data){
49 | if (err){
50 | console.error("Error JSON: ", JSON.stringify(err,null,2));
51 | callback(err);
52 | } else if (data.Item == undefined) {
53 | let result = {
54 | "userId": "",
55 | "username": "",
56 | "points": 1000
57 | };
58 | callback(null,result);
59 | } else {
60 | console.log("User Exists: ", JSON.stringify(data,null,2));
61 | let result = {
62 | "userId": data.Item.userId,
63 | "username": data.Item.username,
64 | "points": data.Item.points
65 | };
66 | callback(null,result);
67 | }
68 | });
69 | break;
70 | case "registerUser":
71 | dynamo.put(putParams, function(err,data){
72 | if (err){
73 | console.error("Error JSON: ", JSON.stringify(err,null,2));
74 | callback(err);
75 | } else {
76 | console.log("User Added: ", JSON.stringify(data,null,2));
77 | let result = putParams.Item;
78 | callback(null,result);
79 | }
80 | });
81 | break;
82 | case "updateBalance":
83 | putParams.Item.points = event.arguments.points;
84 | dynamo.put(putParams, function(err,data){
85 | if (err){
86 | console.error("Error JSON: ", JSON.stringify(err,null,2));
87 | callback(err);
88 | } else {
89 | console.log("Balance Updated: ", JSON.stringify(data,null,2));
90 | let result = {
91 | "userId": event.arguments.userId,
92 | "username": event.arguments.username,
93 | "points": event.arguments.points
94 | }
95 | callback(null,result);
96 | }
97 | });
98 | break;
99 | default:
100 | callback("Unknown field, unable to resolve" + event.field, null);
101 | break;
102 | }
103 | };
104 | MemorySize: 128
105 | Timeout: 15
106 | Role: !GetAtt lambdaRole.Arn
107 | Environment:
108 | Variables:
109 | TABLE_NAME:
110 | Ref: UsersTable
111 | UsersTable:
112 | Type: "AWS::DynamoDB::Table"
113 | Properties:
114 | AttributeDefinitions:
115 | -
116 | AttributeName: "userId"
117 | AttributeType: "S"
118 | KeySchema:
119 | -
120 | AttributeName: "userId"
121 | KeyType: "HASH"
122 | ProvisionedThroughput:
123 | ReadCapacityUnits: "5"
124 | WriteCapacityUnits: "5"
125 | ItemsTable:
126 | Type: "AWS::DynamoDB::Table"
127 | Properties:
128 | AttributeDefinitions:
129 | -
130 | AttributeName: "itemId"
131 | AttributeType: "S"
132 | KeySchema:
133 | -
134 | AttributeName: "itemId"
135 | KeyType: "HASH"
136 | ProvisionedThroughput:
137 | ReadCapacityUnits: "5"
138 | WriteCapacityUnits: "5"
139 | OrdersTable:
140 | Type: "AWS::DynamoDB::Table"
141 | Properties:
142 | AttributeDefinitions:
143 | -
144 | AttributeName: "orderId"
145 | AttributeType: "S"
146 | -
147 | AttributeName: "itemId"
148 | AttributeType: "S"
149 | KeySchema:
150 | -
151 | AttributeName: "orderId"
152 | KeyType: "HASH"
153 | -
154 | AttributeName: "itemId"
155 | KeyType: "RANGE"
156 | ProvisionedThroughput:
157 | ReadCapacityUnits: "5"
158 | WriteCapacityUnits: "5"
159 | awsAppSyncServiceRole:
160 | Type: "AWS::IAM::Role"
161 | Properties:
162 | AssumeRolePolicyDocument:
163 | Version: "2012-10-17"
164 | Statement:
165 | -
166 | Effect: "Allow"
167 | Principal:
168 | Service:
169 | - "appsync.amazonaws.com"
170 | Action:
171 | - "sts:AssumeRole"
172 | Path: "/"
173 | lambdaRole:
174 | Type: "AWS::IAM::Role"
175 | Properties:
176 | AssumeRolePolicyDocument:
177 | Version: "2012-10-17"
178 | Statement:
179 | -
180 | Effect: "Allow"
181 | Principal:
182 | Service:
183 | - "lambda.amazonaws.com"
184 | Action:
185 | - "sts:AssumeRole"
186 | Path: "/"
187 | Policies:
188 | - PolicyName: root
189 | PolicyDocument:
190 | Version: '2012-10-17'
191 | Statement:
192 | - Effect: Allow
193 | Action:
194 | - logs:*
195 | Resource: arn:aws:logs:*:*:*
196 | dynamodbAccessPolicy:
197 | Type: "AWS::IAM::Policy"
198 | Properties:
199 | PolicyName: "dynamodb-access"
200 | PolicyDocument:
201 | Version: "2012-10-17"
202 | Statement:
203 | -
204 | Effect: "Allow"
205 | Action: "dynamodb:*"
206 | Resource:
207 | - !GetAtt ItemsTable.Arn
208 | - !GetAtt OrdersTable.Arn
209 | - !GetAtt UsersTable.Arn
210 | Roles:
211 | -
212 | Ref: "awsAppSyncServiceRole"
213 | -
214 | Ref: "lambdaRole"
215 | lambdaAccessPolicy:
216 | Type: "AWS::IAM::Policy"
217 | Properties:
218 | PolicyName: "lambda-access"
219 | PolicyDocument:
220 | Version: "2012-10-17"
221 | Statement:
222 | -
223 | Effect: "Allow"
224 | Action: "lambda:invokeFunction"
225 | Resource:
226 | - !GetAtt [ UnicornFunction, Arn ]
227 | - !Join [ '', [ !GetAtt [ UnicornFunction, Arn ], ':*' ] ]
228 | Roles:
229 | -
230 | Ref: "awsAppSyncServiceRole"
231 | unicornApi:
232 | Type: "AWS::AppSync::GraphQLApi"
233 | Properties:
234 | Name: "UnicornLoyalty"
235 | AuthenticationType: "AMAZON_COGNITO_USER_POOLS"
236 | UserPoolConfig:
237 | UserPoolId: !Ref userPoolId
238 | AwsRegion: !Ref "AWS::Region"
239 | DefaultAction: "ALLOW"
240 | usersDataSource:
241 | Type: "AWS::AppSync::DataSource"
242 | Properties:
243 | ApiId: !GetAtt unicornApi.ApiId
244 | Name: "Users"
245 | Description: "Users Lambda Data Source"
246 | Type: "AWS_LAMBDA"
247 | ServiceRoleArn: !GetAtt awsAppSyncServiceRole.Arn
248 | LambdaConfig:
249 | LambdaFunctionArn: !GetAtt [ UnicornFunction, Arn ]
250 | itemsDataSource:
251 | Type: "AWS::AppSync::DataSource"
252 | Properties:
253 | ApiId: !GetAtt unicornApi.ApiId
254 | Name: "Items"
255 | Description: "Items DynamoDB Data Source"
256 | Type: "AMAZON_DYNAMODB"
257 | ServiceRoleArn: !GetAtt awsAppSyncServiceRole.Arn
258 | DynamoDBConfig:
259 | TableName: !Ref ItemsTable
260 | AwsRegion: !Ref "AWS::Region"
261 | ordersDataSource:
262 | Type: "AWS::AppSync::DataSource"
263 | Properties:
264 | ApiId: !GetAtt unicornApi.ApiId
265 | Name: "Orders"
266 | Description: "Orders DynamoDB Data Source"
267 | Type: "AMAZON_DYNAMODB"
268 | ServiceRoleArn: !GetAtt awsAppSyncServiceRole.Arn
269 | DynamoDBConfig:
270 | TableName: !Ref OrdersTable
271 | AwsRegion: !Ref "AWS::Region"
272 | unicornSchema:
273 | Type: "AWS::AppSync::GraphQLSchema"
274 | Properties:
275 | ApiId: !GetAtt unicornApi.ApiId
276 | Definition: |
277 | schema {
278 | query: Query
279 | mutation: Mutation
280 | subscription: Subscription
281 | }
282 |
283 | type Query {
284 | getMe(userId: ID! ): User
285 | listInventory(after: String,first: Int ): ItemsConnection
286 | getOrder(orderId: ID! ): OrdersConnection
287 | getMyOrders(after: String,first: Int ): OrdersConnection
288 | }
289 |
290 | type Mutation {
291 | registerUser(userId: ID!,username: String! ): User
292 | addInventory(itemDescription: String!,price: Int! ): Item
293 | updateUserBalance(userId: ID!,username: String!,points: Int! ): User
294 | createOrder(orderId: ID!,itemId: ID!,date: String,count: Int,totalOrder: Int ): Order
295 | }
296 |
297 | type Subscription {
298 | subscribeToPoints: User
299 | @aws_subscribe(mutations: ["updateUserBalance"])
300 | }
301 |
302 | type User {
303 | userId: ID!
304 | username: String
305 | points: Int
306 | orders: [Order]
307 | }
308 |
309 | type Item {
310 | itemId: ID!
311 | itemDescription: String!
312 | price: Int!
313 | count: Int
314 | }
315 |
316 | type ItemsConnection {
317 | items: [Item]
318 | nextToken: String
319 | }
320 |
321 | type Order {
322 | orderId: ID!
323 | itemId: ID!
324 | userId: ID
325 | date: String
326 | count: Int
327 | totalOrder: Int
328 | }
329 |
330 | type OrdersConnection {
331 | items: [Order]
332 | nextToken: String
333 | }
334 | getMeQueryResolver:
335 | Type: "AWS::AppSync::Resolver"
336 | Properties:
337 | ApiId: !GetAtt unicornApi.ApiId
338 | TypeName: "Query"
339 | FieldName: "getMe"
340 | DataSourceName: !GetAtt usersDataSource.Name
341 | RequestMappingTemplate: |
342 | {
343 | "version" : "2017-02-28",
344 | "operation": "Invoke",
345 | "payload": {
346 | "field": "getUser",
347 | "arguments": $utils.toJson($context.args)
348 | }
349 | }
350 | ResponseMappingTemplate: |
351 | $utils.toJson($context.result)
352 | listInventoryQueryResolver:
353 | Type: "AWS::AppSync::Resolver"
354 | Properties:
355 | ApiId: !GetAtt unicornApi.ApiId
356 | TypeName: "Query"
357 | FieldName: "listInventory"
358 | DataSourceName: !GetAtt itemsDataSource.Name
359 | RequestMappingTemplate: |
360 | {
361 | "version" : "2017-02-28",
362 | "operation" : "Scan",
363 | "limit": $util.defaultIfNull($ctx.args.first, 20),
364 | "nextToken": $util.toJson($util.defaultIfNullOrEmpty($ctx.args.after, null))
365 | }
366 | ResponseMappingTemplate: |
367 | $utils.toJson($context.result)
368 | getOrderQueryResolver:
369 | Type: "AWS::AppSync::Resolver"
370 | Properties:
371 | ApiId: !GetAtt unicornApi.ApiId
372 | TypeName: "Query"
373 | FieldName: "getOrder"
374 | DataSourceName: !GetAtt ordersDataSource.Name
375 | RequestMappingTemplate: |
376 | {
377 | "version" : "2017-02-28",
378 | "operation" : "Scan",
379 | "filter" : {
380 | "expression": "#userId = :userId AND #orderId = :orderId",
381 | "expressionNames": {
382 | "#userId": "userId",
383 | "#orderId": "orderId"
384 | },
385 | "expressionValues" : {
386 | ":userId" : {"S": "$ctx.identity.sub"},
387 | ":orderId" : $util.dynamodb.toDynamoDBJson($ctx.args.orderId)
388 | }
389 | }
390 | }
391 | ResponseMappingTemplate: |
392 | $utils.toJson($context.result)
393 | getMyOrdersQueryResolver:
394 | Type: "AWS::AppSync::Resolver"
395 | Properties:
396 | ApiId: !GetAtt unicornApi.ApiId
397 | TypeName: "Query"
398 | FieldName: "getMyOrders"
399 | DataSourceName: !GetAtt ordersDataSource.Name
400 | RequestMappingTemplate: |
401 | {
402 | "version" : "2017-02-28",
403 | "operation" : "Scan",
404 | "filter" : {
405 | "expression": "#userId = :userId",
406 | "expressionNames": {
407 | "#userId": "userId"
408 | },
409 | "expressionValues" : {
410 | ":userId" : {"S": "$ctx.identity.sub"}
411 | }
412 | },
413 | "limit": $util.defaultIfNull($ctx.args.first, 20),
414 | "nextToken": $util.toJson($util.defaultIfNullOrEmpty($ctx.args.after, null))
415 | }
416 | ResponseMappingTemplate: |
417 | $utils.toJson($context.result)
418 | registerUserMutationResolver:
419 | Type: "AWS::AppSync::Resolver"
420 | Properties:
421 | ApiId: !GetAtt unicornApi.ApiId
422 | TypeName: "Mutation"
423 | FieldName: "registerUser"
424 | DataSourceName: !GetAtt usersDataSource.Name
425 | RequestMappingTemplate: |
426 | {
427 | "version" : "2017-02-28",
428 | "operation": "Invoke",
429 | "payload": {
430 | "field": "registerUser",
431 | "arguments": $utils.toJson($context.args)
432 | }
433 | }
434 | ResponseMappingTemplate: |
435 | $utils.toJson($context.result)
436 | addInventoryMutationResolver:
437 | Type: "AWS::AppSync::Resolver"
438 | Properties:
439 | ApiId: !GetAtt unicornApi.ApiId
440 | TypeName: "Mutation"
441 | FieldName: "addInventory"
442 | DataSourceName: !GetAtt itemsDataSource.Name
443 | RequestMappingTemplate: |
444 | {
445 | "version" : "2017-02-28",
446 | "operation" : "PutItem",
447 | "key" : {
448 | ## If object "id" should come from GraphQL arguments, change to $util.dynamodb.toDynamoDBJson($ctx.args.id)
449 | "itemId": $util.dynamodb.toDynamoDBJson($util.autoId()),
450 | },
451 | "attributeValues" : $util.dynamodb.toMapValuesJson($ctx.args)
452 | }
453 | ResponseMappingTemplate: |
454 | $utils.toJson($context.result)
455 | updateUserBalanceMutationResolver:
456 | Type: "AWS::AppSync::Resolver"
457 | Properties:
458 | ApiId: !GetAtt unicornApi.ApiId
459 | TypeName: "Mutation"
460 | FieldName: "updateUserBalance"
461 | DataSourceName: !GetAtt usersDataSource.Name
462 | RequestMappingTemplate: |
463 | {
464 | "version" : "2017-02-28",
465 | "operation": "Invoke",
466 | "payload": {
467 | "field": "updateBalance",
468 | "arguments": $utils.toJson($context.args)
469 | }
470 | }
471 | ResponseMappingTemplate: |
472 | $utils.toJson($context.result)
473 | createOrderMutationResolver:
474 | Type: "AWS::AppSync::Resolver"
475 | Properties:
476 | ApiId: !GetAtt unicornApi.ApiId
477 | TypeName: "Mutation"
478 | FieldName: "createOrder"
479 | DataSourceName: !GetAtt ordersDataSource.Name
480 | RequestMappingTemplate: |
481 | {
482 | "version" : "2017-02-28",
483 | "operation" : "PutItem",
484 | "key" : {
485 | "orderId" : { "S" : "${context.arguments.orderId}" }
486 | },
487 | "attributeValues" : {
488 | "itemId": { "S": "${context.arguments.itemId}" },
489 | "userId": { "S": "${context.identity.sub}" },
490 | "totalOrder": { "N": "${context.arguments.totalOrder}" },
491 | "date": { "S": "$util.time.nowFormatted("dd-MM-yyyy")" },
492 | "count": { "S": "${context.arguments.count}" }
493 | }
494 | }
495 | ResponseMappingTemplate: |
496 | $utils.toJson($context.result)
497 |
--------------------------------------------------------------------------------
/sam/template.yaml:
--------------------------------------------------------------------------------
1 | AWSTemplateFormatVersion: '2010-09-09'
2 | Transform: 'AWS::Serverless-2016-10-31'
3 | Description: An AWS Serverless Specification template describing your function.
4 | Resources:
5 | UnicornLoyalty:
6 | Type: 'AWS::Serverless::Function'
7 | Properties:
8 | Handler: UnicornLoyalty/index.handler
9 | Runtime: nodejs10.x
10 | Description: ''
11 | MemorySize: 128
12 | Timeout: 15
13 | Policies: AmazonDynamoDBFullAccess
14 | Environment:
15 | Variables:
16 | TABLE_NAME:
17 | Ref: UsersTable
18 | UsersTable:
19 | Type: AWS::Serverless::SimpleTable
20 | Properties:
21 | PrimaryKey:
22 | Name: userId
23 | Type: String
24 | ItemsTable:
25 | Type: AWS::Serverless::SimpleTable
26 | Properties:
27 | PrimaryKey:
28 | Name: itemId
29 | Type: String
30 | OrdersTable:
31 | Type: "AWS::DynamoDB::Table"
32 | Properties:
33 | AttributeDefinitions:
34 | -
35 | AttributeName: "orderId"
36 | AttributeType: "S"
37 | -
38 | AttributeName: "itemId"
39 | AttributeType: "S"
40 | KeySchema:
41 | -
42 | AttributeName: "orderId"
43 | KeyType: "HASH"
44 | -
45 | AttributeName: "itemId"
46 | KeyType: "RANGE"
47 | ProvisionedThroughput:
48 | ReadCapacityUnits: "5"
49 | WriteCapacityUnits: "5"
50 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-header {
6 | background-color: #222;
7 | color: white;
8 | }
9 |
10 | .App-title {
11 | font-size: 1.5em;
12 | }
13 |
14 | .App-intro {
15 | font-size: large;
16 | }
17 |
18 | .logo {
19 | height:100px;
20 | }
21 |
22 | .center {
23 | text-align: center;
24 | margin: auto;
25 | }
26 |
27 | .container {
28 | text-align: right;
29 | margin: auto;
30 | padding: 10px;
31 | border-radius: 10px;
32 | border: 1px solid black;
33 | }
34 |
35 | .img-thumbnail {
36 | border-radius: 50%;
37 | height:50px;
38 | }
39 |
40 | input {
41 | width: 50px;
42 | }
43 |
44 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | //Styling
3 | import 'bootstrap/dist/css/bootstrap.min.css';
4 | import "./App.css";
5 | import shadowfax from './images/shadowfax.png';
6 | //Components
7 | import Unicorns from "./components/Unicorns";
8 | import Points from "./components/Points";
9 | import Order from "./components/Order";
10 | //Amplify
11 | import Amplify,{Auth,Analytics,API,graphqlOperation} from 'aws-amplify';
12 | import { withAuthenticator } from 'aws-amplify-react';
13 | import aws_exports from './aws-exports'; // specify the location of aws-exports.js file on your project
14 | Amplify.configure(aws_exports);
15 |
16 | //GraphQL Mutations
17 | const registerUser = `mutation registerUser($userId: ID!, $username: String!) {
18 | registerUser(userId: $userId, username: $username) {
19 | __typename
20 | userId
21 | username
22 | points
23 | }
24 | }`;
25 |
26 | const updateUserBalance = `mutation updateUserBalance($userId: ID!, $username: String!, $points: Int!) {
27 | updateUserBalance(userId: $userId, username: $username, points: $points) {
28 | __typename
29 | points
30 | }
31 | }`;
32 |
33 | const createOrder = `mutation createOrder($orderId:ID!,$itemId:ID!,$totalOrder:Int,$count:Int){
34 | createOrder(orderId:$orderId,itemId:$itemId,totalOrder:$totalOrder,count:$count){
35 | orderId
36 | itemId
37 | count
38 | totalOrder
39 | date
40 | }
41 | }`
42 |
43 |
44 | class App extends Component {
45 |
46 | constructor(props){
47 | super(props);
48 | this.state={
49 | user:"",
50 | sub:"",
51 | points:"",
52 | order:"",
53 | display: false
54 | }
55 | }
56 |
57 | async componentDidMount(){
58 | //Get User Details from Cognito Token
59 | this.session = await Auth.currentSession();
60 | console.log('Decoded Acess Token:');
61 | console.log(JSON.stringify(this.session.accessToken.payload, null, 2));
62 | this.setState({user: this.session.accessToken.payload.username});
63 | this.setState({sub: this.session.accessToken.payload.sub});
64 | //Query the current User ID
65 | const getUser = await API.graphql(graphqlOperation(
66 | `query getMe{
67 | getMe(userId: "`+this.state.sub+`"){
68 | __typename
69 | userId
70 | username
71 | points
72 | }
73 | }`));
74 | //Retrieve current points balance
75 | this.setState({points: getUser.data.getMe.points});
76 | this.userDetails = {
77 | userId: this.state.sub,
78 | username: this.state.user,
79 | points: this.state.points
80 | };
81 | //If the User ID doesn't exist, proceed to register
82 | if (getUser.data.getMe.userId ===""){
83 | const newUser = await API.graphql(graphqlOperation(registerUser, this.userDetails));
84 | this.userDetails.points = newUser.data.registerUser.points;
85 | }
86 | //Update the balance, this will trigger the Subscription
87 | this.updateBalance(this.userDetails.points);
88 | }
89 |
90 | async updateBalance(points){
91 | this.userDetails.points = points;
92 | const lastPoints = await API.graphql(graphqlOperation(updateUserBalance, this.userDetails));
93 | console.log(lastPoints);
94 | }
95 |
96 | //Retrieve Order data from Unicorns component
97 | orderData = (details,balance,total) => {
98 | //Generate Order ID
99 | let orderId = Math.random().toString(36).substring(2, 15).toUpperCase();
100 | //Loop through order items
101 | details.forEach((item)=>{
102 | let orderDetails = {
103 | orderId: orderId,
104 | itemId: "",
105 | unitPrice: "",
106 | count: 0,
107 | totalOrder:0
108 | };
109 | if (item.count !== null){
110 | orderDetails.itemId = item.itemId;
111 | orderDetails.count = item.count;
112 | orderDetails.unitPrice = item.price;
113 | orderDetails.totalOrder = total;
114 | //Clean up and add items to Order
115 | this.createOrder(orderDetails);
116 | }
117 | })
118 | //Update balance after purchase
119 | this.updateBalance(balance);
120 | }
121 |
122 | async createOrder(order){
123 | //Place the order
124 | console.log(order);
125 | const putOrder = await API.graphql(graphqlOperation(createOrder, order));
126 | console.log(JSON.stringify(putOrder));
127 | this.setState({order: putOrder.data.createOrder});
128 | this.setState({display: true}); //display Order component after a successfull purchase
129 | //Send Analytics data to Pinpoint
130 | Analytics.record({
131 | name: 'unicornsPurchase',
132 | attributes: {},
133 | metrics: { totalOrder: order.totalOrder }
134 | });
135 | Analytics.record('_monetization.purchase', {
136 | _currency: 'USD',
137 | _product_id: order.itemId,
138 | }, {
139 | _item_price: order.unitPrice,
140 | _quantity: order.count,
141 | })
142 | }
143 |
144 | render() {
145 | return (
146 |
147 |
148 |
149 | Welcome to Unicorn Loyalty
150 | Powered by Serverless GraphQL
151 |
152 |
153 | Sign up by January 2019 and get 1000 Unicoin points for free!
154 |
105 | )
106 |
107 |
108 | export default Unicorns;
--------------------------------------------------------------------------------
/src/images/shadowfax.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/src/images/shadowfax.png
--------------------------------------------------------------------------------
/src/images/unicorn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/src/images/unicorn.png
--------------------------------------------------------------------------------
/src/images/unicorn_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/src/images/unicorn_small.png
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import registerServiceWorker from './registerServiceWorker';
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 | registerServiceWorker();
9 |
10 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log('New content is available; please refresh.');
69 | } else {
70 | // At this point, everything has been precached.
71 | // It's the perfect time to display a
72 | // "Content is cached for offline use." message.
73 | console.log('Content is cached for offline use.');
74 | }
75 | }
76 | };
77 | };
78 | })
79 | .catch(error => {
80 | console.error('Error during service worker registration:', error);
81 | });
82 | }
83 |
84 | function checkValidServiceWorker(swUrl) {
85 | // Check if the service worker can be found. If it can't reload the page.
86 | fetch(swUrl)
87 | .then(response => {
88 | // Ensure service worker exists, and that we really are getting a JS file.
89 | if (
90 | response.status === 404 ||
91 | response.headers.get('content-type').indexOf('javascript') === -1
92 | ) {
93 | // No service worker found. Probably a different app. Reload the page.
94 | navigator.serviceWorker.ready.then(registration => {
95 | registration.unregister().then(() => {
96 | window.location.reload();
97 | });
98 | });
99 | } else {
100 | // Service worker found. Proceed as normal.
101 | registerValidSW(swUrl);
102 | }
103 | })
104 | .catch(() => {
105 | console.log(
106 | 'No internet connection found. App is running in offline mode.'
107 | );
108 | });
109 | }
110 |
111 | export function unregister() {
112 | if ('serviceWorker' in navigator) {
113 | navigator.serviceWorker.ready.then(registration => {
114 | registration.unregister();
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------