├── .babelrc ├── .eslintrc.yml ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── TODO.md ├── appsync-realtime-vanillajs.code-workspace ├── appsync-realtime-vanillajs.sublime-project ├── bin ├── deploy ├── destroy ├── helpers │ └── getawsvars.sh └── test-api.bash ├── dist └── index.html ├── mapping-templates ├── ForwardResult.response.vtl └── Message.request.vtl ├── package-lock.json ├── package.json ├── queries ├── mutation.message.graphql └── subscription.inbox.graphql ├── schema └── schema.graphql ├── serverless.yml ├── src ├── graphql │ ├── inboxSubscription.js │ ├── inboxSubscriptionAll.js │ ├── meQuery.js │ └── messageMutation.js └── index.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | commonjs: true 4 | es6: true 5 | node: true 6 | extends: 'eslint:recommended' 7 | parserOptions: 8 | sourceType: module 9 | rules: 10 | indent: 11 | - error 12 | - tab 13 | linebreak-style: 14 | - error 15 | - unix 16 | quotes: 17 | - error 18 | - single 19 | semi: 20 | - error 21 | - always 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | src/config.js 2 | .serverless 3 | 4 | *.sublime-workspace 5 | .env 6 | build 7 | 8 | # Created by https://www.gitignore.io/api/node 9 | 10 | ### Node ### 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | *.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (http://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # Typescript v1 declaration files 50 | typings/ 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Optional REPL history 59 | .node_repl_history 60 | 61 | # Output of 'npm pack' 62 | *.tgz 63 | 64 | # Yarn Integrity file 65 | .yarn-integrity 66 | 67 | # dotenv environment variables file 68 | .env 69 | 70 | 71 | # End of https://www.gitignore.io/api/node 72 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | // "**/build": true, 4 | // "**/dist": true, 5 | "**/node_modules": true, 6 | "*.sublime*": true, 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vanilla JavaScript Realtime Client Built on AWS AppSync 2 | Serverless websockets have arrived on AWS thanks to Appsync. This repository is a simple messaging example and includes: 3 | - GraphQL API code 4 | - Deployment using the serverless framework 5 | - Node.js client 6 | - Browser client 7 | - Client build config using webpack 8 | 9 | Read the [blog post](https://andrewgriffithsonline.com/blog/serverless-websockets-on-aws) that explains how to run this application. 10 | 11 | ## Example Usage 12 | 13 | ### 1. Set up 14 | Install the Serverless Framework plugins used by the project. 15 | ```Shell 16 | $ npm install 17 | ``` 18 | Create an `.env` file and update the `AWS_ACCOUNT_ID` variable. 19 | ```Shell 20 | export AWS_ACCOUNT_ID=123456789 21 | ``` 22 | 23 | ### 2. Deploy/Update the AppSync API 24 | ```Shell 25 | $ ./bin/deploy 26 | ``` 27 | 28 | ### 4. Destroy Stack 29 | ```Shell 30 | $ ./bin/destroy 31 | ``` 32 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | push github code 2 | -------------------------------------------------------------------------------- /appsync-realtime-vanillajs.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {} 8 | } 9 | -------------------------------------------------------------------------------- /appsync-realtime-vanillajs.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "build_systems": 3 | [ 4 | { 5 | "file_regex": "^[ ]*File \"(...*?)\", line ([0-9]*)", 6 | "name": "Anaconda Python Builder", 7 | "selector": "source.python", 8 | "shell_cmd": "\"python\" -u \"$file\"" 9 | } 10 | ], 11 | "folders": 12 | [ 13 | { 14 | "file_exclude_patterns": 15 | [ 16 | ".ssh/config", 17 | "*.sublime-*", 18 | "*.workspace" 19 | ], 20 | "folder_exclude_patterns": 21 | [ 22 | "cache", 23 | "dist", 24 | "build", 25 | "node_modules", 26 | ".kube/schema", 27 | ".aws/shell" 28 | ], 29 | "path": "." 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /bin/deploy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | . .env 5 | . bin/helpers/getawsvars.sh 6 | sls deploy 7 | -------------------------------------------------------------------------------- /bin/destroy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | . bin/helpers/getawsvars.sh 5 | 6 | sls remove 7 | -------------------------------------------------------------------------------- /bin/helpers/getawsvars.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | source .env 5 | 6 | export APPSYNC_API_ID=$(aws appsync list-graphql-apis \ 7 | --query 'graphqlApis[?name==`realtimePager`].apiId' \ 8 | --output text >/dev/null 2>&1) 9 | 10 | export APPSYNC_API_KEY=$(aws appsync list-api-keys \ 11 | --api-id "$APPSYNC_API_ID" \ 12 | --query 'apiKeys[0].id' \ 13 | --output text >/dev/null 2>&1) 14 | 15 | -------------------------------------------------------------------------------- /bin/test-api.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -XPOST -H "Content-Type:application/graphql" -H "x-api-key:da2-4zgmtsnhurgspbguw6hfzl3s34" -d '{ "query": "query { getTodo(id: \"1\") { id, title, status } }" }' https://qgulfcbftzd77by4dwkxvh5xbi.appsync-api.eu-west-1.amazonaws.com/graphql && echo "" 4 | 5 | 6 | wsta -H "x-api-key:da2-4zgmtsnhurgspbguw6hfzl3s34" https://qgulfcbftzd77by4dwkxvh5xbi.appsync-api.eu-west-1.amazonaws.com/graphql 7 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /mapping-templates/ForwardResult.response.vtl: -------------------------------------------------------------------------------- 1 | $util.toJson($context.result) 2 | -------------------------------------------------------------------------------- /mapping-templates/Message.request.vtl: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2017-02-28", 3 | "payload": { 4 | "body": "${context.arguments.body}", 5 | "from": "${context.identity.username}", 6 | "to": "${context.arguments.to}", 7 | "sentAt": "$util.time.nowISO8601()" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appsync-realtime-vanillajs", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.js", 6 | "dependencies": { 7 | "apollo-cache-inmemory": "^1.1.0", 8 | "apollo-client": "^2.0.3", 9 | "apollo-link": "^1.0.3", 10 | "aws-appsync": "1.1.4", 11 | "aws-sdk": "^2.141.0", 12 | "es6-promise": "^4.1.1", 13 | "graphql": "0.13.2", 14 | "graphql-cli": "2.16.4", 15 | "graphql-tag": "2.9.2", 16 | "isomorphic-fetch": "^2.2.1", 17 | "ws": "^3.3.1" 18 | }, 19 | "devDependencies": { 20 | "@creditkarma/graphql-validator": "0.5.0", 21 | "babel-cli": "6.26.0", 22 | "babel-preset-es2015": "6.24.1", 23 | "eslint": "5.0.1", 24 | "rimraf": "2.6.2", 25 | "serverless-appsync-plugin": "1.0.1", 26 | "webpack": "4.8.3", 27 | "webpack-cli": "2.1.3", 28 | "webpack-dev-server": "3.1.4" 29 | }, 30 | "scripts": { 31 | "build": "rimraf build/ && babel ./src --out-dir build/ --ignore ./node_modules,./.babelrc,./package.json,./npm-debug.log --copy-files", 32 | "start": "npm run build && node build/index.js", 33 | "start:web": "webpack-dev-server --mode development --content-base dist/", 34 | "lint": "graphql-validator -s './schema/**/*.graphql' './queries/*.graphql'" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "ssh://git-codecommit.eu-west-1.amazonaws.com/v1/repos/appsync-realtime-vanillajs" 39 | }, 40 | "author": "Andrew Griffiths", 41 | "license": "ISC" 42 | } 43 | -------------------------------------------------------------------------------- /queries/mutation.message.graphql: -------------------------------------------------------------------------------- 1 | mutation Message { 2 | message(to: "Bobby", body: "Yo!") { 3 | body 4 | to 5 | from 6 | sentAt 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /queries/subscription.inbox.graphql: -------------------------------------------------------------------------------- 1 | subscription Inbox { 2 | inbox(to: "Bobby") { 3 | body 4 | to 5 | from 6 | sentAt 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /schema/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Query 3 | mutation: Mutation 4 | subscription: Subscription 5 | } 6 | 7 | type Subscription { 8 | inbox(to: String): Message 9 | @aws_subscribe(mutations: ["message"]) 10 | newUser(name: String): String 11 | @aws_subscribe(mutations: ["join"]) 12 | } 13 | 14 | type Mutation { 15 | message(body: String!, to: String!): Message! 16 | join(name: String!): String! 17 | } 18 | 19 | type Message { 20 | from: String! 21 | to: String! 22 | body: String! 23 | sentAt: String! 24 | } 25 | 26 | type Query { 27 | me: String 28 | } 29 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | --- 2 | service: realtime-chat 3 | 4 | frameworkVersion: ">=1.21.0 <2.0.0" 5 | 6 | plugins: 7 | - serverless-appsync-plugin 8 | 9 | provider: 10 | name: aws 11 | region: eu-west-1 12 | 13 | custom: 14 | awsAccountId: ${env:AWS_ACCOUNT_ID} 15 | appSync: 16 | name: realtimeChat 17 | apiKey: ${env:APPSYNC_API_KEY} 18 | apiId: ${env:APPSYNC_API_ID} 19 | authenticationType: API_KEY 20 | schema: schema/schema.graphql 21 | serviceRole: "AppSyncServiceRole" # AppSyncServiceRole is a role defined by amazon and available in all accounts 22 | mappingTemplatesLocation: mapping-templates 23 | mappingTemplates: 24 | - dataSource: Chat 25 | type: Mutation 26 | field: message 27 | request: Message.request.vtl 28 | response: ForwardResult.response.vtl 29 | - dataSource: Chat 30 | type: Subscription 31 | field: inbox 32 | request: Message.request.vtl 33 | response: ForwardResult.response.vtl 34 | dataSources: 35 | # https://docs.aws.amazon.com/appsync/latest/devguide/tutorial-local-resolvers.html 36 | - type: NONE # use an AppSync local resolver 37 | name: Chat 38 | description: 'Paging forwarder' 39 | -------------------------------------------------------------------------------- /src/graphql/inboxSubscription.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export default gql` 4 | subscription Inbox($to: String) { 5 | inbox(to: $to) { 6 | from 7 | to 8 | body 9 | } 10 | }`; 11 | -------------------------------------------------------------------------------- /src/graphql/inboxSubscriptionAll.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export default gql` 4 | subscription Inbox { 5 | inbox { 6 | from 7 | to 8 | body 9 | } 10 | }`; 11 | -------------------------------------------------------------------------------- /src/graphql/meQuery.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export default gql` 4 | query Me { 5 | me 6 | }`; 7 | -------------------------------------------------------------------------------- /src/graphql/messageMutation.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export default gql` 4 | mutation Mesage($to: String!, $body: String!) { 5 | createTodo(to: $to, body: $body) { 6 | __typename 7 | body 8 | to 9 | from 10 | sentAt 11 | } 12 | }`; 13 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * This shows how to use standard Apollo client on Node.js 4 | */ 5 | const RECIPIENT = 'Bobby'; 6 | 7 | if (!global.WebSocket) { 8 | global.WebSocket = require('ws'); 9 | } 10 | if (!global.window) { 11 | global.window = { 12 | setTimeout: setTimeout, 13 | clearTimeout: clearTimeout, 14 | WebSocket: global.WebSocket, 15 | ArrayBuffer: global.ArrayBuffer, 16 | addEventListener: function () { }, 17 | navigator: { onLine: true } 18 | }; 19 | } 20 | if (!global.localStorage) { 21 | global.localStorage = { 22 | store: {}, 23 | getItem: function (key) { 24 | return this.store[key]; 25 | }, 26 | setItem: function (key, value) { 27 | this.store[key] = value; 28 | }, 29 | removeItem: function (key) { 30 | delete this.store[key]; 31 | } 32 | }; 33 | } 34 | require('es6-promise').polyfill(); 35 | require('isomorphic-fetch'); 36 | 37 | // Require config file downloaded from AWS console with endpoint and auth info 38 | const AppSyncConfig = require('./config').default; 39 | const AWSAppSyncClient = require('aws-appsync').default; 40 | // import MessageMutation from './graphql/messageMutation'; 41 | // import MeQuery from './graphql/meQuery'; 42 | import InboxSubscription from './graphql/inboxSubscription'; 43 | 44 | // Set up Apollo client 45 | const client = new AWSAppSyncClient({ 46 | url: AppSyncConfig.graphqlEndpoint, 47 | region: AppSyncConfig.region, 48 | auth: { 49 | type: AppSyncConfig.authenticationType, 50 | apiKey: AppSyncConfig.apiKey, 51 | } 52 | }); 53 | 54 | client.hydrated().then(function (client) { 55 | // const variables = { 56 | // to: 'Bobby', 57 | // body: 'Hello from browser' 58 | // }; 59 | // //Now run a query 60 | // client.mutate({ mutation: MessageMutation, variables: variables }) 61 | // .then(function logData(data) { 62 | // console.log('results of mutation: ', data); 63 | // }) 64 | // .catch(console.error); 65 | 66 | 67 | // TODO API 68 | // client.query({ query: MeQuery }) 69 | // .then(function logData(data) { 70 | // console.log('results of query: ', data); 71 | // }) 72 | // .catch(console.error); 73 | 74 | const observable = client.subscribe({ query: InboxSubscription, variables: { to: RECIPIENT } }); 75 | 76 | const realtimeResults = function realtimeResults(data) { 77 | console.log('realtime data: ', data); 78 | }; 79 | 80 | observable.subscribe({ 81 | next: realtimeResults, 82 | complete: console.log, 83 | error: console.log, 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/index.js', 5 | output: { 6 | filename: 'bundle.js', 7 | path: path.resolve(__dirname, 'dist') 8 | } 9 | }; 10 | --------------------------------------------------------------------------------