├── .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 |
--------------------------------------------------------------------------------