├── .github
└── dependabot.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── cdk
├── .gitignore
├── README.md
├── bin
│ └── multiregionsubs.ts
├── cdk.json
├── jest.config.js
├── lib
│ ├── globalSubs-ebXRegionRulesRegion1-stack.ts
│ ├── globalSubs-ebXRegionRulesRegion2-stack.ts
│ ├── globalSubs-region1-stack.ts
│ └── globalSubs-region2-stack.ts
├── package-lock.json
├── package.json
├── test
│ └── multiregionsubs.test.ts
└── tsconfig.json
├── client
├── .gitignore
├── .graphqlconfig.yml
├── README.md
├── curl_commands.txt
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── schema.graphql
└── src
│ ├── App.css
│ ├── App.js
│ ├── App.test.js
│ ├── graphql
│ ├── mutations.js
│ ├── queries.js
│ └── subscriptions.js
│ ├── index.css
│ ├── index.js
│ ├── logo.svg
│ ├── reportWebVitals.js
│ └── setupTests.js
└── images
├── client.png
├── globalWSAPI.png
└── invalidation.png
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "npm" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "daily"
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.d.ts
3 | node_modules
4 |
--------------------------------------------------------------------------------
/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, or recently closed, 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 *main* 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' 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](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution.
60 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7 | the Software, and to permit persons to whom the Software is furnished to do so.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15 |
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Global Serverless / Functionless WebSockets PubSub API
2 | ## with AWS AppSync and Amazon EventBridge
3 |
4 |
5 | 
6 |
7 | This is an implementation of a multi-region PubSub real-time API based on Serverless/Functionless WebSockets where clients are subscribed to a specific channel and messages are pushed automatically to clients listening/subscribed to the channel in both regions. Connections management, scalability, fan-out and broadcasting are all automatically handled by the regional AppSync APIs.
8 |
9 | For more details, check the related article: https://aws.amazon.com/blogs/mobile/multi-region-websocket-api/
10 |
11 |
12 |
13 | ## Requirements
14 |
15 | * [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources.
16 | * [Git installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
17 | * [Node and NPM](https://nodejs.org/en/download/) installed
18 | * [AWS Cloud Development Kit](https://docs.aws.amazon.com/cdk/latest/guide/cli.html) (AWS CDK) installed
19 | * [Amplify CLI](https://docs.amplify.aws/cli/start/install/), only required to generate code as the backend deployment is done via AWS CDK
20 |
21 |
22 |
23 | ## Deploy APIs and Event Buses to the cloud
24 |
25 | 1. Clone the project to your local working directory:
26 |
27 | ```sh
28 | git clone https://github.com/aws-samples/aws-global-pubsub-api
29 | ```
30 |
31 | 2. Change the working directory to:
32 |
33 | ```sh
34 | cd aws-global-pubsub-api/cdk
35 | ```
36 |
37 | 3. Install the project dependencies:
38 |
39 | ```sh
40 | npm install
41 | ```
42 |
43 | 4. Deploy the 4 CDK stacks with a single command to your default AWS account. Regions for each stack are defined [here](https://github.com/awsed/globalWSAPI/blob/main/cdk/bin/multiregionsubs.ts). After deployment, the output of the 2 first stacks display the GraphQL APIs endpoint, API IDs, and API keys. Take note of all the details as they are needed to setup clients later:
44 |
45 | ```sh
46 | cdk deploy --all
47 | ```
48 |
49 |
50 |
51 | ## Enhanced Filtering and Invalidation
52 |
53 |
54 |
55 |
56 |
57 |
58 | The APIs are configured to allow only 5 channels using backend [Enhanced Filtering](https://docs.aws.amazon.com/appsync/latest/devguide/aws-appsync-real-time-enhanced-filtering.html) logic:
59 |
60 | https://github.com/awsed/globalWSAPI/blob/76934587e8ca5c1dcc69d5cd8695d3d681566f00/cdk/lib/globalSubs-region1-stack.ts#L160
61 |
62 | A backend process or service can be used to [unsubscribe](https://docs.aws.amazon.com/appsync/latest/devguide/aws-appsync-real-time-invalidation.html) clients from a channel by calling an `unsubscribe` mutation and informing the channel name:
63 |
64 | ```
65 | mutation Unsubscribe {
66 | unsubscribe(name: "tech") {
67 | name
68 | }
69 | }
70 | ```
71 |
72 | This will forcibly close their WebSocket connection. Clients are authorized using API Keys however the invalidation mutation is configured so it can only be invoked with IAM authorization so clients cannot unsubscribe other clients. Either a backend service with the proper permissions or the [AWS Appsync Console](https://console.aws.amazon.com/appsync/home) can be used as an administrative tool to invoke the mutation to invalidate/unsubscribe clients in a given channel by selecting AWS IAM in the Queries section.
73 |
74 |
75 |
76 |
77 | ## Configure the React.js client
78 | 
79 |
80 | 1. Change the working directory to the `client` folder:
81 |
82 | ```sh
83 | cd ../client
84 | ```
85 |
86 | 2. Install the project dependencies:
87 |
88 | ```sh
89 | npm install
90 | ```
91 |
92 | 3. Open the file `src/App.js` and update the [AppSync API congifuration details](https://github.com/awsed/globalWSAPI/blob/064f6d3012e66a486185b56404a8fa92048589c2/client/src/App.js#L11) based on the output of the previous `cdk deploy`. You can connect the client to your API of choice (Oregon or Sydney). You could also duplicate the `client` folder and have a different instance of each client connecting to a different API in order to test multi-region subscriptions.
93 |
94 | 4. Generate the necessary code to interact with the API using the [Amplify CodeGen](https://docs.amplify.aws/cli/graphql-transformer/codegen/) with the API ID output of the previous `cdk deploy`. There's no need to create an Amplify CLI project, however you'll need to download the API schema from the [AWS Appsync Console](https://console.aws.amazon.com/appsync/home). Select the API `GlobalWS-API` in one of the regions your account and, in the Schema section, select **Export schema**. Download and copy the schema file to the root of the `/client` folder, where you need to execute the following command accepting all defaults:
95 |
96 | ```sh
97 | amplify add codegen --apiId xxxxxxxxxxxxxxxxxxxxxx
98 | ```
99 |
100 | 6. Execute the application and access it from multiple browser tabs/windows at :
101 |
102 | ```bash
103 | npm start
104 | ```
105 |
106 | 7. Select a channel from the drop-down, send messages from one client and get it broadcasted to all browser windows. You can also type a different channel name in the form and try to send a message, however no messages will be published as filtering in AppSync is blocking other channels. Since AWS AppSync automatically scales to demand, you can have thousands of clients broadcasting messages data.
107 |
--------------------------------------------------------------------------------
/cdk/.gitignore:
--------------------------------------------------------------------------------
1 | *.js
2 | !jest.config.js
3 | *.d.ts
4 | node_modules
5 |
6 | # CDK asset staging directory
7 | .cdk.staging
8 | cdk.out
9 |
--------------------------------------------------------------------------------
/cdk/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to your CDK TypeScript project
2 |
3 | This is a blank project for TypeScript development with CDK.
4 |
5 | The `cdk.json` file tells the CDK Toolkit how to execute your app.
6 |
7 | ## Useful commands
8 |
9 | * `npm run build` compile typescript to js
10 | * `npm run watch` watch for changes and compile
11 | * `npm run test` perform the jest unit tests
12 | * `cdk deploy` deploy this stack to your default AWS account/region
13 | * `cdk diff` compare deployed stack with current state
14 | * `cdk synth` emits the synthesized CloudFormation template
15 |
--------------------------------------------------------------------------------
/cdk/bin/multiregionsubs.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import 'source-map-support/register';
3 | import * as cdk from '@aws-cdk/core';
4 | import { GlobalSubsRegion1 } from '../lib/globalSubs-region1-stack';
5 | import { GlobalSubsRegion2 } from '../lib/globalSubs-region2-stack';
6 | import { GlobalSubsXRegionEBRulesRegion1 } from '../lib/globalSubs-ebXRegionRulesRegion1-stack';
7 | import { GlobalSubsXRegionEBRulesRegion2 } from '../lib/globalSubs-ebXRegionRulesRegion2-stack';
8 |
9 | const app = new cdk.App();
10 | const Stack1 = new GlobalSubsRegion1(app, 'GlobalSubsRegion1', {
11 | /* If you don't specify 'env', this stack will be environment-agnostic.
12 | * Account/Region-dependent features and context lookups will not work,
13 | * but a single synthesized template can be deployed anywhere. */
14 |
15 | /* Uncomment the next line to specialize this stack for the AWS Account
16 | * and Region that are implied by the current CLI configuration. */
17 | // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
18 |
19 | /* Uncomment the next line if you know exactly what Account and Region you
20 | * want to deploy the stack to. */
21 | env: { region: 'us-west-2' }
22 |
23 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
24 | });
25 | const Stack2 = new GlobalSubsRegion2(app, 'GlobalSubsRegion2', {
26 | /* If you don't specify 'env', this stack will be environment-agnostic.
27 | * Account/Region-dependent features and context lookups will not work,
28 | * but a single synthesized template can be deployed anywhere. */
29 |
30 | /* Uncomment the next line to specialize this stack for the AWS Account
31 | * and Region that are implied by the current CLI configuration. */
32 | // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
33 |
34 | /* Uncomment the next line if you know exactly what Account and Region you
35 | * want to deploy the stack to. */
36 | env: { region: 'ap-southeast-2' }
37 |
38 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
39 | });
40 |
41 | new GlobalSubsXRegionEBRulesRegion1(app, 'GlobalSubsXRegionEBRulesRegion1', {
42 | eventBus: Stack2.eventBus,
43 | env: { region: 'us-west-2' }
44 | });
45 |
46 | new GlobalSubsXRegionEBRulesRegion2(app, 'GlobalSubsXRegionEBRulesRegion2', {
47 | eventBus: Stack1.eventBus,
48 | env: { region: 'ap-southeast-2' }
49 | });
--------------------------------------------------------------------------------
/cdk/cdk.json:
--------------------------------------------------------------------------------
1 | {
2 | "app": "npx ts-node --prefer-ts-exts bin/multiregionsubs.ts",
3 | "watch": {
4 | "include": [
5 | "**"
6 | ],
7 | "exclude": [
8 | "README.md",
9 | "cdk*.json",
10 | "**/*.d.ts",
11 | "**/*.js",
12 | "tsconfig.json",
13 | "package*.json",
14 | "yarn.lock",
15 | "node_modules",
16 | "test"
17 | ]
18 | },
19 | "context": {
20 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true,
21 | "@aws-cdk/core:stackRelativeExports": true,
22 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true,
23 | "@aws-cdk/aws-lambda:recognizeVersionProps": true,
24 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true,
25 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
26 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
27 | "@aws-cdk/aws-iam:minimizePolicies": true,
28 | "@aws-cdk/core:target-partitions": [
29 | "aws",
30 | "aws-cn"
31 | ]
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/cdk/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: 'node',
3 | roots: ['/test'],
4 | testMatch: ['**/*.test.ts'],
5 | transform: {
6 | '^.+\\.tsx?$': 'ts-jest'
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/cdk/lib/globalSubs-ebXRegionRulesRegion1-stack.ts:
--------------------------------------------------------------------------------
1 | import * as cdk from '@aws-cdk/core';
2 | import * as events from '@aws-cdk/aws-events';
3 | import { Role, ServicePrincipal, PolicyDocument, PolicyStatement } from '@aws-cdk/aws-iam';
4 |
5 | interface GlobalSubsEBRulesRegion2Props extends cdk.StackProps {
6 | readonly eventBus: events.IEventBus;
7 | }
8 |
9 | export class GlobalSubsXRegionEBRulesRegion1 extends cdk.Stack {
10 | constructor(scope: cdk.Construct, id: string, props: GlobalSubsEBRulesRegion2Props) {
11 | super(scope, id, props);
12 |
13 | const eventBridgeRegion2Role = new Role(this, 'EventBridge2EventBridgeRole', {
14 | assumedBy: new ServicePrincipal('events.amazonaws.com'),
15 | inlinePolicies: {
16 | invokeAPI: new PolicyDocument({
17 | statements: [
18 | new PolicyStatement({
19 | resources: [props.eventBus.eventBusArn],
20 | actions: ['events:PutEvents'],
21 | }),
22 | ],
23 | }),
24 | },
25 | });
26 |
27 | const crossRegionrule = new events.CfnRule(this, 'toAppSyncRegion2', {
28 | name: 'toAppSyncRegion2',
29 | eventBusName: props.eventBus.eventBusName,
30 | eventPattern: {
31 | 'source': ['appsync'],
32 | 'detail-type': ['channel update'],
33 | },
34 | targets: [
35 | {
36 | id: 'toAppSyncRegion2',
37 | arn: props.eventBus.eventBusArn,
38 | roleArn: eventBridgeRegion2Role.roleArn
39 | },
40 | ],
41 | })
42 | }
43 | }
--------------------------------------------------------------------------------
/cdk/lib/globalSubs-ebXRegionRulesRegion2-stack.ts:
--------------------------------------------------------------------------------
1 | import * as cdk from '@aws-cdk/core';
2 | import * as events from '@aws-cdk/aws-events';
3 | import { Role, ServicePrincipal, PolicyDocument, PolicyStatement } from '@aws-cdk/aws-iam';
4 |
5 | interface GlobalSubsEBRulesRegion1Props extends cdk.StackProps {
6 | readonly eventBus: events.IEventBus;
7 | }
8 |
9 | export class GlobalSubsXRegionEBRulesRegion2 extends cdk.Stack {
10 | constructor(scope: cdk.Construct, id: string, props: GlobalSubsEBRulesRegion1Props) {
11 | super(scope, id, props);
12 |
13 | const eventBridgeRegion2Role = new Role(this, 'EventBridge2EventBridgeRole', {
14 | assumedBy: new ServicePrincipal('events.amazonaws.com'),
15 | inlinePolicies: {
16 | invokeAPI: new PolicyDocument({
17 | statements: [
18 | new PolicyStatement({
19 | resources: [props.eventBus.eventBusArn],
20 | actions: ['events:PutEvents'],
21 | }),
22 | ],
23 | }),
24 | },
25 | });
26 |
27 | const crossRegionrule = new events.CfnRule(this, 'toAppSyncRegion1', {
28 | name: 'toAppSyncRegion1',
29 | eventBusName: props.eventBus.eventBusName,
30 | eventPattern: {
31 | 'source': ['appsync'],
32 | 'detail-type': ['channel update'],
33 | },
34 | targets: [
35 | {
36 | id: 'toAppSyncRegion1',
37 | arn: props.eventBus.eventBusArn,
38 | roleArn: eventBridgeRegion2Role.roleArn
39 | },
40 | ],
41 | })
42 | }
43 | }
--------------------------------------------------------------------------------
/cdk/lib/globalSubs-region1-stack.ts:
--------------------------------------------------------------------------------
1 | import * as cdk from '@aws-cdk/core';
2 | import * as events from '@aws-cdk/aws-events';
3 | import { GraphqlApi, AuthorizationType, Directive, ObjectType, GraphqlType, ResolvableField, Field, MappingTemplate } from '@aws-cdk/aws-appsync';
4 | import { Role, ServicePrincipal, PolicyDocument, PolicyStatement } from '@aws-cdk/aws-iam';
5 |
6 | export class GlobalSubsRegion1 extends cdk.Stack {
7 |
8 | public readonly eventBus: events.IEventBus;
9 |
10 | constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
11 | super(scope, id, props);
12 |
13 | // Setting up GraphQL API
14 |
15 | const api = new GraphqlApi(this, 'Api', {
16 | name: 'GlobalWS-API',
17 | authorizationConfig: {
18 | defaultAuthorization: {
19 | authorizationType: AuthorizationType.API_KEY,
20 | },
21 | additionalAuthorizationModes: [{
22 | authorizationType: AuthorizationType.IAM
23 | }]
24 | }
25 | });
26 |
27 | // Defining data types (Code-first GraphQL) - Messages are sent to channels by name
28 |
29 | const channel = new ObjectType('Channel', {
30 | directives: [Directive.iam(),Directive.apiKey()],
31 | definition: {
32 | name: GraphqlType.string({ isRequired: true }),
33 | message: GraphqlType.string({ isRequired: true }),
34 | },
35 | });
36 |
37 | api.addType(channel);
38 |
39 | // Configuring Event Bridge as AppSync Data Source
40 |
41 | const endpoint = "https://events." + this.region + ".amazonaws.com/";
42 | const httpdatasource = api.addHttpDataSource('events', endpoint, {
43 | authorizationConfig: { signingRegion: this.region, signingServiceName: 'events' },
44 | });
45 |
46 | // Adding None/Local AppSync Data Source
47 |
48 | const pubsub = api.addNoneDataSource('pubsub');
49 |
50 | // Setting up AppSync IAM Role to add events to Event Bridge
51 |
52 | const appsyncEventBridgeRole = new Role(this, "AppSyncEventBridgeRole", {
53 | assumedBy: new ServicePrincipal("appsync.amazonaws.com")
54 | });
55 |
56 | appsyncEventBridgeRole.addToPolicy(
57 | new PolicyStatement({
58 | resources: ["*"],
59 | actions: ["events:PutEvents"]
60 | })
61 | );
62 |
63 | // Defining API Operations
64 |
65 | api.addQuery('getChannel', new Field({
66 | returnType: channel.attribute()
67 | }));
68 |
69 | // Clients publish messages to channels which are sent to Event Bridge - Frontend operation
70 |
71 | api.addMutation('publish', new ResolvableField({
72 | returnType: channel.attribute(),
73 | args: { name: GraphqlType.string({ isRequired: true }), message: GraphqlType.string({ isRequired: true }) },
74 | dataSource: httpdatasource,
75 | requestMappingTemplate: MappingTemplate.fromString(`
76 | {
77 | "version": "2018-05-29",
78 | "method": "POST",
79 | "resourcePath": "/",
80 | "params": {
81 | "headers": {
82 | "content-type": "application/x-amz-json-1.1",
83 | "x-amz-target": "AWSEvents.PutEvents"
84 | },
85 | "body": {
86 | "Entries":[
87 | {
88 | "Source": "appsync",
89 | "EventBusName": "AppSyncEventBus",
90 | "Detail": "{ \\\"name\\\": \\\"$ctx.arguments.name\\\",\\\"message\\\": \\\"$ctx.arguments.message\\\"}",
91 | "DetailType": "channel update"
92 | }
93 | ]
94 | }
95 | }
96 | }`
97 | ),
98 | responseMappingTemplate: MappingTemplate.fromString(`
99 | ## Raise a GraphQL field error in case of a datasource invocation error
100 | #if($ctx.error)
101 | $util.error($ctx.error.message, $ctx.error.type)
102 | #end
103 | ## if the response status code is not 200, then return an error. Else return the body **
104 | #if($ctx.result.statusCode == 200)
105 | ## If response is 200, return the body.
106 | {
107 | "name": "$ctx.args.name",
108 | "message": "$ctx.args.message"
109 | }
110 | #else
111 | ## If response is not 200, append the response to error block.
112 | $utils.appendError($ctx.result.body, "$ctx.result.statusCode")
113 | #end
114 | `)
115 | }))
116 |
117 | // Event Bridge publishes messages received in the Event Bus - Backend only operation
118 |
119 | api.addMutation('publishFromBus', new ResolvableField({
120 | returnType: channel.attribute(),
121 | args: { name: GraphqlType.string({ isRequired: true }), message: GraphqlType.string({ isRequired: true }) },
122 | dataSource: pubsub,
123 | requestMappingTemplate: MappingTemplate.fromString(`
124 | {
125 | "version": "2017-02-28",
126 | "payload": {
127 | "name": "$context.arguments.name",
128 | "message": "$context.arguments.message"
129 | }
130 | }`
131 | ),
132 | responseMappingTemplate: MappingTemplate.fromString(`$util.toJson($context.result)`)
133 | }))
134 |
135 |
136 | // Clients subscribe to channels by name and receive messages published to the channel
137 |
138 | api.addSubscription('subscribe', new ResolvableField({
139 | returnType: channel.attribute(),
140 | args: { name: GraphqlType.string({ isRequired: true }) },
141 | directives: [Directive.subscribe('publishFromBus')],
142 | dataSource: pubsub,
143 | requestMappingTemplate: MappingTemplate.fromString(`
144 | {
145 | "version": "2017-02-28",
146 | "payload": {
147 | "name": "demo",
148 | "message": "AppSync enhanced filtering and invalidation"
149 | }
150 | }`
151 | ),
152 | // Setting up filters
153 | responseMappingTemplate: MappingTemplate.fromString(`
154 | $extensions.setSubscriptionFilter({
155 | "filterGroup": [
156 | {
157 | "filters" : [
158 | {
159 | "fieldName" : "name",
160 | "operator" : "in",
161 | "value" : ["cars","robots","tech","music","media"]
162 | }
163 | ]
164 | }
165 | ]
166 | })
167 | $extensions.setSubscriptionInvalidationFilter({
168 | "filterGroup": [
169 | {
170 | "filters" : [
171 | {
172 | "fieldName" : "name",
173 | "operator" : "eq",
174 | "value" : $context.args.name
175 | }
176 | ]
177 | }
178 | ]
179 | })
180 | $util.toJson($context.result)
181 | `)
182 | }));
183 |
184 | // Operation to unsubscribe all clients in a given channel
185 |
186 | api.addMutation('unsubscribe', new ResolvableField({
187 | returnType: channel.attribute(),
188 | args: { name: GraphqlType.string({ isRequired: true }) },
189 | directives: [Directive.iam()],
190 | dataSource: pubsub,
191 | requestMappingTemplate: MappingTemplate.fromString(`
192 | {
193 | "version": "2017-02-28",
194 | "payload": {
195 | "name": "$context.arguments.name"
196 | }
197 | }`
198 | ),
199 | responseMappingTemplate: MappingTemplate.fromString(`
200 | $extensions.invalidateSubscriptions({
201 | "subscriptionField": "subscribe",
202 | "payload": {
203 | "name": $context.arguments.name
204 | }
205 | })
206 | $util.toJson($context.result)
207 | `)
208 | }))
209 |
210 |
211 | // Setting up Event Bus and granting access to AppSync
212 |
213 | this.eventBus = new events.EventBus(this, 'bus', {
214 | eventBusName: 'AppSyncEventBus'
215 | });
216 | this.eventBus.grantPutEventsTo(httpdatasource.grantPrincipal);
217 |
218 | // Configuring AppSync as Event Bridge API Destination
219 |
220 | const connection = new events.CfnConnection(this, 'AppSyncConnection', {
221 | authorizationType: 'API_KEY',
222 | authParameters: {
223 | apiKeyAuthParameters: {
224 | apiKeyName: 'x-api-key',
225 | apiKeyValue: api.apiKey!,
226 | },
227 | },
228 | })
229 |
230 | const destination = new events.CfnApiDestination(this, 'AppSyncDestination', {
231 | connectionArn: connection.attrArn,
232 | httpMethod: 'POST',
233 | invocationEndpoint: api.graphqlUrl,
234 | })
235 |
236 | const eventBridgeAppSyncRole = new Role(this, 'EventBridgeAppSyncRole', {
237 | assumedBy: new ServicePrincipal('events.amazonaws.com'),
238 | inlinePolicies: {
239 | invokeAPI: new PolicyDocument({
240 | statements: [
241 | new PolicyStatement({
242 | resources: [`arn:aws:events:${this.region}:${this.account}:api-destination/${destination.ref}/*`],
243 | actions: ['events:InvokeApiDestination'],
244 | }),
245 | ],
246 | }),
247 | },
248 | });
249 |
250 | const appSyncPublishrule = new events.CfnRule(this, 'AppSyncRule', {
251 | description: 'AppSync rule',
252 | name: 'appsync-rule',
253 | eventBusName: this.eventBus.eventBusName,
254 | eventPattern: {
255 | 'source': ['appsync'],
256 | 'detail-type': ['channel update'],
257 | },
258 | targets: [
259 | {
260 | id: 'default-target-appsync',
261 | arn: destination.attrArn,
262 | roleArn: eventBridgeAppSyncRole.roleArn,
263 | inputTransformer: {
264 | inputPathsMap: {
265 | name: '$.detail.name',
266 | message: '$.detail.message',
267 | },
268 | inputTemplate: `{
269 | "query": "mutation PublishFromBus($name:String!, $message:String!) {
270 | publishFromBus(name:$name, message:$message) { name message }
271 | }",
272 | "operationName": "PublishFromBus",
273 | "variables": {
274 | "name": "",
275 | "message": ""
276 | }
277 | }`.replace(/\n\s*/g, ' '),
278 | },
279 | },
280 | ],
281 | })
282 |
283 | // Outputs
284 |
285 | new cdk.CfnOutput(this, 'graphqlUrl', { value: api.graphqlUrl })
286 | new cdk.CfnOutput(this, 'apiKey', { value: api.apiKey! })
287 | new cdk.CfnOutput(this, 'apiId', { value: api.apiId })
288 | new cdk.CfnOutput(this, 'eventBus', { value: this.eventBus.eventBusArn })
289 | new cdk.CfnOutput(this, 'region', { value: this.region })
290 |
291 | }
292 | }
--------------------------------------------------------------------------------
/cdk/lib/globalSubs-region2-stack.ts:
--------------------------------------------------------------------------------
1 | import * as cdk from '@aws-cdk/core';
2 | import * as events from '@aws-cdk/aws-events';
3 | import { GraphqlApi, AuthorizationType, Directive, ObjectType, GraphqlType, ResolvableField, Field, MappingTemplate } from '@aws-cdk/aws-appsync';
4 | import { Role, ServicePrincipal, PolicyDocument, PolicyStatement } from '@aws-cdk/aws-iam';
5 |
6 | export class GlobalSubsRegion2 extends cdk.Stack {
7 |
8 | public readonly eventBus: events.IEventBus;
9 |
10 | constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
11 | super(scope, id, props);
12 |
13 | // Setting up GraphQL API
14 |
15 | const api = new GraphqlApi(this, 'Api', {
16 | name: 'GlobalWS-API',
17 | authorizationConfig: {
18 | defaultAuthorization: {
19 | authorizationType: AuthorizationType.API_KEY,
20 | },
21 | additionalAuthorizationModes: [{
22 | authorizationType: AuthorizationType.IAM
23 | }]
24 | }
25 | });
26 |
27 | // Defining data types (Code-first GraphQL) - Messages are sent to channels by name
28 |
29 | const channel = new ObjectType('Channel', {
30 | directives: [Directive.iam(), Directive.apiKey()],
31 | definition: {
32 | name: GraphqlType.string({ isRequired: true }),
33 | message: GraphqlType.string({ isRequired: true }),
34 | },
35 | });
36 |
37 | api.addType(channel);
38 |
39 | // Configuring Event Bridge as AppSync Data Source
40 |
41 | const endpoint = "https://events." + this.region + ".amazonaws.com/";
42 | const httpdatasource = api.addHttpDataSource('events', endpoint, {
43 | authorizationConfig: { signingRegion: this.region, signingServiceName: 'events' },
44 | });
45 |
46 | // Adding None/Local AppSync Data Source
47 |
48 | const pubsub = api.addNoneDataSource('pubsub');
49 |
50 | // Setting up AppSync IAM Role to add events to Event Bridge
51 |
52 | const appsyncEventBridgeRole = new Role(this, "AppSyncEventBridgeRole", {
53 | assumedBy: new ServicePrincipal("appsync.amazonaws.com")
54 | });
55 |
56 | appsyncEventBridgeRole.addToPolicy(
57 | new PolicyStatement({
58 | resources: ["*"],
59 | actions: ["events:PutEvents"]
60 | })
61 | );
62 |
63 | // Defining API Operations
64 |
65 | api.addQuery('getChannel', new Field({
66 | returnType: channel.attribute()
67 | }));
68 |
69 | // Clients publish messages to channels which are sent to Event Bridge - Frontend operation
70 |
71 | api.addMutation('publish', new ResolvableField({
72 | returnType: channel.attribute(),
73 | args: { name: GraphqlType.string({ isRequired: true }), message: GraphqlType.string({ isRequired: true }) },
74 | dataSource: httpdatasource,
75 | requestMappingTemplate: MappingTemplate.fromString(`
76 | {
77 | "version": "2018-05-29",
78 | "method": "POST",
79 | "resourcePath": "/",
80 | "params": {
81 | "headers": {
82 | "content-type": "application/x-amz-json-1.1",
83 | "x-amz-target": "AWSEvents.PutEvents"
84 | },
85 | "body": {
86 | "Entries":[
87 | {
88 | "Source": "appsync",
89 | "EventBusName": "AppSyncEventBus",
90 | "Detail": "{ \\\"name\\\": \\\"$ctx.arguments.name\\\",\\\"message\\\": \\\"$ctx.arguments.message\\\"}",
91 | "DetailType": "channel update"
92 | }
93 | ]
94 | }
95 | }
96 | }`
97 | ),
98 | responseMappingTemplate: MappingTemplate.fromString(`
99 | ## Raise a GraphQL field error in case of a datasource invocation error
100 | #if($ctx.error)
101 | $util.error($ctx.error.message, $ctx.error.type)
102 | #end
103 | ## if the response status code is not 200, then return an error. Else return the body **
104 | #if($ctx.result.statusCode == 200)
105 | ## If response is 200, return the body.
106 | {
107 | "name": "$ctx.args.name",
108 | "message": "$ctx.args.message"
109 | }
110 | #else
111 | ## If response is not 200, append the response to error block.
112 | $utils.appendError($ctx.result.body, "$ctx.result.statusCode")
113 | #end
114 | `)
115 | }))
116 |
117 | // Event Bridge publishes messages received in the Event Bus - Backend only operation
118 |
119 | api.addMutation('publishFromBus', new ResolvableField({
120 | returnType: channel.attribute(),
121 | args: { name: GraphqlType.string({ isRequired: true }), message: GraphqlType.string({ isRequired: true }) },
122 | dataSource: pubsub,
123 | requestMappingTemplate: MappingTemplate.fromString(`
124 | {
125 | "version": "2017-02-28",
126 | "payload": {
127 | "name": "$context.arguments.name",
128 | "message": "$context.arguments.message"
129 | }
130 | }`
131 | ),
132 | responseMappingTemplate: MappingTemplate.fromString(`$util.toJson($context.result)`)
133 | }))
134 |
135 |
136 | // Clients subscribe to channels by name and receive messages published to the channel
137 |
138 | api.addSubscription('subscribe', new ResolvableField({
139 | returnType: channel.attribute(),
140 | args: { name: GraphqlType.string({ isRequired: true }) },
141 | directives: [Directive.subscribe('publishFromBus')],
142 | dataSource: pubsub,
143 | requestMappingTemplate: MappingTemplate.fromString(`
144 | {
145 | "version": "2017-02-28",
146 | "payload": {
147 | "name": "demo",
148 | "message": "AppSync enhanced filtering and invalidation"
149 | }
150 | }`
151 | ),
152 | // Setting up filters
153 | responseMappingTemplate: MappingTemplate.fromString(`
154 | $extensions.setSubscriptionFilter({
155 | "filterGroup": [
156 | {
157 | "filters" : [
158 | {
159 | "fieldName" : "name",
160 | "operator" : "in",
161 | "value" : ["cars","robots","tech","music","media"]
162 | }
163 | ]
164 | }
165 | ]
166 | })
167 | $extensions.setSubscriptionInvalidationFilter({
168 | "filterGroup": [
169 | {
170 | "filters" : [
171 | {
172 | "fieldName" : "name",
173 | "operator" : "eq",
174 | "value" : $context.args.name
175 | }
176 | ]
177 | }
178 | ]
179 | })
180 | $util.toJson($context.result)
181 | `)
182 | }));
183 |
184 | // Operation to unsubscribe all clients in a given channel
185 |
186 | api.addMutation('unsubscribe', new ResolvableField({
187 | returnType: channel.attribute(),
188 | args: { name: GraphqlType.string({ isRequired: true }) },
189 | directives: [Directive.iam()],
190 | dataSource: pubsub,
191 | requestMappingTemplate: MappingTemplate.fromString(`
192 | {
193 | "version": "2017-02-28",
194 | "payload": {
195 | "name": "$context.arguments.name"
196 | }
197 | }`
198 | ),
199 | responseMappingTemplate: MappingTemplate.fromString(`
200 | $extensions.invalidateSubscriptions({
201 | "subscriptionField": "subscribe",
202 | "payload": {
203 | "name": $context.arguments.name
204 | }
205 | })
206 | $util.toJson($context.result)
207 | `)
208 | }))
209 |
210 |
211 | // Setting up Event Bus and granting access to AppSync
212 |
213 | this.eventBus = new events.EventBus(this, 'bus', {
214 | eventBusName: 'AppSyncEventBus'
215 | });
216 | this.eventBus.grantPutEventsTo(httpdatasource.grantPrincipal);
217 |
218 | // Configuring AppSync as Event Bridge API Destination
219 |
220 | const connection = new events.CfnConnection(this, 'AppSyncConnection', {
221 | authorizationType: 'API_KEY',
222 | authParameters: {
223 | apiKeyAuthParameters: {
224 | apiKeyName: 'x-api-key',
225 | apiKeyValue: api.apiKey!,
226 | },
227 | },
228 | })
229 |
230 | const destination = new events.CfnApiDestination(this, 'AppSyncDestination', {
231 | connectionArn: connection.attrArn,
232 | httpMethod: 'POST',
233 | invocationEndpoint: api.graphqlUrl,
234 | })
235 |
236 | const eventBridgeAppSyncRole = new Role(this, 'EventBridgeAppSyncRole', {
237 | assumedBy: new ServicePrincipal('events.amazonaws.com'),
238 | inlinePolicies: {
239 | invokeAPI: new PolicyDocument({
240 | statements: [
241 | new PolicyStatement({
242 | resources: [`arn:aws:events:${this.region}:${this.account}:api-destination/${destination.ref}/*`],
243 | actions: ['events:InvokeApiDestination'],
244 | }),
245 | ],
246 | }),
247 | },
248 | });
249 |
250 | const appSyncPublishrule = new events.CfnRule(this, 'AppSyncRule', {
251 | description: 'AppSync rule',
252 | name: 'appsync-rule',
253 | eventBusName: this.eventBus.eventBusName,
254 | eventPattern: {
255 | 'source': ['appsync'],
256 | 'detail-type': ['channel update'],
257 | },
258 | targets: [
259 | {
260 | id: 'default-target-appsync',
261 | arn: destination.attrArn,
262 | roleArn: eventBridgeAppSyncRole.roleArn,
263 | inputTransformer: {
264 | inputPathsMap: {
265 | name: '$.detail.name',
266 | message: '$.detail.message',
267 | },
268 | inputTemplate: `{
269 | "query": "mutation PublishFromBus($name:String!, $message:String!) {
270 | publishFromBus(name:$name, message:$message) { name message }
271 | }",
272 | "operationName": "PublishFromBus",
273 | "variables": {
274 | "name": "",
275 | "message": ""
276 | }
277 | }`.replace(/\n\s*/g, ' '),
278 | },
279 | },
280 | ],
281 | })
282 |
283 | // Outputs
284 |
285 | new cdk.CfnOutput(this, 'graphqlUrl', { value: api.graphqlUrl })
286 | new cdk.CfnOutput(this, 'apiKey', { value: api.apiKey! })
287 | new cdk.CfnOutput(this, 'apiId', { value: api.apiId })
288 | new cdk.CfnOutput(this, 'eventBus', { value: this.eventBus.eventBusArn })
289 | new cdk.CfnOutput(this, 'region', { value: this.region })
290 |
291 | }
292 | }
--------------------------------------------------------------------------------
/cdk/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "multiregionsubs",
3 | "version": "0.1.0",
4 | "bin": {
5 | "multiregionsubs": "bin/multiregionsubs.js"
6 | },
7 | "scripts": {
8 | "build": "tsc",
9 | "watch": "tsc -w",
10 | "test": "jest",
11 | "cdk": "cdk"
12 | },
13 | "devDependencies": {
14 | "@types/jest": "^26.0.10",
15 | "@types/node": "10.17.27",
16 | "aws-cdk": "2.20.0",
17 | "jest": "^26.4.2",
18 | "ts-jest": "^26.2.0",
19 | "ts-node": "^9.0.0",
20 | "typescript": "~3.9.7"
21 | },
22 | "dependencies": {
23 | "@aws-cdk/aws-appsync": "^1.152.0",
24 | "@aws-cdk/aws-events": "^1.152.0",
25 | "@aws-cdk/aws-iam": "^1.152.0",
26 | "@aws-cdk/core": "1.152.0",
27 | "constructs": "^10.0.0",
28 | "source-map-support": "^0.5.16"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/cdk/test/multiregionsubs.test.ts:
--------------------------------------------------------------------------------
1 | // import * as cdk from 'aws-cdk-lib';
2 | // import { Template } from 'aws-cdk-lib/assertions';
3 | // import * as Multiregionsubs from '../lib/multiregionsubs-stack';
4 |
5 | // example test. To run these tests, uncomment this file along with the
6 | // example resource in lib/multiregionsubs-stack.ts
7 | test('SQS Queue Created', () => {
8 | // const app = new cdk.App();
9 | // // WHEN
10 | // const stack = new Multiregionsubs.MultiregionsubsStack(app, 'MyTestStack');
11 | // // THEN
12 | // const template = Template.fromStack(stack);
13 |
14 | // template.hasResourceProperties('AWS::SQS::Queue', {
15 | // VisibilityTimeout: 300
16 | // });
17 | });
18 |
--------------------------------------------------------------------------------
/cdk/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2018",
4 | "module": "commonjs",
5 | "lib": [
6 | "es2018"
7 | ],
8 | "declaration": true,
9 | "strict": true,
10 | "noImplicitAny": true,
11 | "strictNullChecks": true,
12 | "noImplicitThis": true,
13 | "alwaysStrict": true,
14 | "noUnusedLocals": false,
15 | "noUnusedParameters": false,
16 | "noImplicitReturns": true,
17 | "noFallthroughCasesInSwitch": false,
18 | "inlineSourceMap": true,
19 | "inlineSources": true,
20 | "experimentalDecorators": true,
21 | "strictPropertyInitialization": false,
22 | "typeRoots": [
23 | "./node_modules/@types"
24 | ]
25 | },
26 | "exclude": [
27 | "node_modules",
28 | "cdk.out"
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/client/.graphqlconfig.yml:
--------------------------------------------------------------------------------
1 | projects:
2 | Codegen Project:
3 | schemaPath: schema.graphql
4 | includes:
5 | - src/graphql/**/*.js
6 | excludes:
7 | - ./amplify/**
8 | extensions:
9 | amplify:
10 | codeGenTarget: javascript
11 | generatedFileName: ''
12 | docsFilePath: src/graphql
13 | region: us-east-1
14 | apiId: m5aeup2tgrb57pec5v5ctvok5m
15 | frontend: javascript
16 | framework: react
17 | maxDepth: 2
18 |
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `npm start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
13 |
14 | The page will reload when you make changes.\
15 | You may also see any lint errors in the console.
16 |
17 | ### `npm test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `npm run build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `npm run eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!**
35 |
36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
39 |
40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
48 | ### Code Splitting
49 |
50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
51 |
52 | ### Analyzing the Bundle Size
53 |
54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
55 |
56 | ### Making a Progressive Web App
57 |
58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
59 |
60 | ### Advanced Configuration
61 |
62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
63 |
64 | ### Deployment
65 |
66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
67 |
68 | ### `npm run build` fails to minify
69 |
70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
71 |
--------------------------------------------------------------------------------
/client/curl_commands.txt:
--------------------------------------------------------------------------------
1 | curl 'https://xxxxxxxxxxx.appsync-api.us-west-2.amazonaws.com/graphql' \
2 | -H 'content-type: application/json' \
3 | -H 'x-api-key: da2-xxxxxxxxxxxx' \
4 | --data-raw $'{"query":"mutation publish($message: String\u0021, $name: String\u0021) {\\n publish(message: $message, name: $name) {\\n message\\n name\\n }\\n}\\n","variables":{"name":"tech","message":"Hello from Oregon!"}}' \
5 |
6 |
7 | curl 'https://xxxxxxxxxxxxx.appsync-api.ap-southeast-2.amazonaws.com/graphql' \
8 | -H 'content-type: application/json' \
9 | -H 'x-api-key: da2-xxxxxxxxxxxxx' \
10 | --data-raw $'{"query":"mutation publish($message: String\u0021, $name: String\u0021) {\\n publish(message: $message, name: $name) {\\n message\\n name\\n }\\n}\\n","variables":{"name":"tech","message":"Hello from Sydney!"}}' \
11 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@popperjs/core": "^2.11.5",
7 | "@testing-library/jest-dom": "^5.16.4",
8 | "@testing-library/react": "^13.0.1",
9 | "@testing-library/user-event": "^13.5.0",
10 | "aws-amplify": "^4.3.19",
11 | "bootstrap": "^5.1.3",
12 | "react": "^18.0.0",
13 | "react-dom": "^18.0.0",
14 | "react-scripts": "5.0.1",
15 | "react-scroll-to-bottom": "^4.2.0",
16 | "web-vitals": "^2.1.4"
17 | },
18 | "scripts": {
19 | "start": "react-scripts start",
20 | "build": "react-scripts build",
21 | "test": "react-scripts test",
22 | "eject": "react-scripts eject"
23 | },
24 | "eslintConfig": {
25 | "extends": [
26 | "react-app",
27 | "react-app/jest"
28 | ]
29 | },
30 | "browserslist": {
31 | "production": [
32 | ">0.2%",
33 | "not dead",
34 | "not op_mini all"
35 | ],
36 | "development": [
37 | "last 1 chrome version",
38 | "last 1 firefox version",
39 | "last 1 safari version"
40 | ]
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/aws-global-pubsub-api/7f1d2cb00ee13b47cb707301600e1f5b1118fdd8/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/client/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/aws-global-pubsub-api/7f1d2cb00ee13b47cb707301600e1f5b1118fdd8/client/public/logo192.png
--------------------------------------------------------------------------------
/client/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/aws-global-pubsub-api/7f1d2cb00ee13b47cb707301600e1f5b1118fdd8/client/public/logo512.png
--------------------------------------------------------------------------------
/client/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 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/client/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/client/schema.graphql:
--------------------------------------------------------------------------------
1 | schema {
2 | query: Query
3 | mutation: Mutation
4 | subscription: Subscription
5 | }
6 |
7 | type Channel {
8 | message: String!
9 | name: String!
10 | }
11 |
12 | type Mutation {
13 | publish(message: String!, name: String!): Channel
14 | publishFromBus(message: String!, name: String!): Channel
15 | unsubscribe(name: String!): Channel @aws_iam
16 | }
17 |
18 | type Query {
19 | getChannel: Channel
20 | }
21 |
22 | type Subscription {
23 | subscribe(name: String!): Channel @aws_subscribe(mutations : ["publishFromBus"])
24 | }
25 |
--------------------------------------------------------------------------------
/client/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | body {
6 | background-color: #282c34;
7 | }
8 |
9 | .App-logo {
10 | height: 10vmin;
11 | pointer-events: none;
12 | }
13 |
14 | @media (prefers-reduced-motion: no-preference) {
15 | .App-logo {
16 | animation: App-logo-spin infinite 20s linear;
17 | }
18 | }
19 |
20 | .App-header {
21 | background-color: #282c34;
22 | min-height: 100vh;
23 | display: flex;
24 | flex-direction: column;
25 | align-items: center;
26 | justify-content: center;
27 | font-size: calc(10px + 2vmin);
28 | color: white;
29 | }
30 |
31 | .App-link {
32 | color: #61dafb;
33 | }
34 |
35 | @keyframes App-logo-spin {
36 | from {
37 | transform: rotate(0deg);
38 | }
39 | to {
40 | transform: rotate(360deg);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/client/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import "bootstrap/dist/css/bootstrap.css";
3 | import "bootstrap/dist/css/bootstrap.min.css";
4 | import "@popperjs/core";
5 | import "bootstrap";
6 | import ScrollToBottom from "react-scroll-to-bottom";
7 | import logo from "./logo.svg";
8 | import "./App.css";
9 | import Amplify, { API, graphqlOperation } from "aws-amplify";
10 | import * as subscriptions from "./graphql/subscriptions"; //codegen generated code
11 | import * as mutations from "./graphql/mutations"; //codegen generated code
12 |
13 | //AppSync endpoint settings
14 | const myAppConfig = {
15 | aws_appsync_graphqlEndpoint:
16 | "https://xxxxxxxxxxxxxx.appsync-api.us-west-2.amazonaws.com/graphql",
17 | aws_appsync_region: "us-west-2",
18 | aws_appsync_authenticationType: "API_KEY",
19 | aws_appsync_apiKey: "da2-xxxxxxxxxxxxxxxxxx",
20 | };
21 |
22 | Amplify.configure(myAppConfig);
23 |
24 | function App() {
25 | const [channel, setChannel] = useState("");
26 | const [channelName, setChannelName] = useState("");
27 | const [message, setMessage] = useState("");
28 | const [received, setReceived] = useState([]);
29 | const [display, setDisplay] = useState(false);
30 | let messages = [];
31 |
32 | //Publish data to subscribed clients
33 | async function handleSubmit(evt) {
34 | evt.preventDefault();
35 | evt.stopPropagation();
36 | const publish = await API.graphql(
37 | graphqlOperation(mutations.publish, { name: channel, message: message })
38 | );
39 | setChannelName(channel);
40 | setChannel("");
41 | setMessage("");
42 | setDisplay(true);
43 | }
44 |
45 | useEffect(() => {
46 | //Subscribe via WebSockets
47 | const subscription = API.graphql(
48 | graphqlOperation(subscriptions.subscribe, { name: channelName })
49 | ).subscribe({
50 | next: ({ provider, value }) => {
51 | setReceived((prevArray) => [
52 | ...prevArray,
53 | {
54 | name: value.data.subscribe.name,
55 | message: value.data.subscribe.message
56 | },
57 | ]);
58 | },
59 | error: (error) => console.warn(error),
60 | });
61 | return () => subscription.unsubscribe();
62 | }, [channelName]);
63 |
64 | if (received) {
65 | //messages.push(received);
66 | messages = [].concat(received).map((msg, i) => (
67 |