├── .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 | ![Screnshot](images/globalWSAPI.png) 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 | ![Screnshot](images/client.png) 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 |
68 |
69 |
70 | {msg.name} 71 |
72 |
73 | {msg.message} 74 |
75 |
76 |
77 | )); 78 | } 79 | 80 | //Display pushed data on browser 81 | return ( 82 |
83 |
84 |

Global PubSub App

85 |
86 |
87 | logo 88 |
89 |
90 | 98 |
    99 |
  • 100 |

    setChannel("cars")} 103 | > 104 | cars 105 |

    106 |
  • 107 |
  • 108 |

    setChannel("robots")} 111 | > 112 | robots 113 |

    114 |
  • 115 |
  • 116 |

    setChannel("tech")} 119 | > 120 | tech 121 |

    122 |
  • 123 |
  • 124 |

    setChannel("music")} 127 | > 128 | music 129 |

    130 |
  • 131 |
  • 132 |

    setChannel("media")} 135 | > 136 | media 137 |

    138 |
  • 139 |
140 | setChannel(e.target.value)} 146 | /> 147 |
148 |
149 | setMessage(e.target.value)} 157 | /> 158 | 166 |
167 |
168 |
169 | {display ? ( 170 |
171 |

Message Board

172 |
173 | {messages} 174 |
175 |
176 | ) : null} 177 |
178 |
179 | ); 180 | } 181 | 182 | export default App; -------------------------------------------------------------------------------- /client/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /client/src/graphql/mutations.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // this is an auto generated file. This will be overwritten 3 | 4 | export const publish = /* GraphQL */ ` 5 | mutation Publish($message: String!, $name: String!) { 6 | publish(message: $message, name: $name) { 7 | message 8 | name 9 | } 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /client/src/graphql/queries.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // this is an auto generated file. This will be overwritten 3 | 4 | export const getChannel = /* GraphQL */ ` 5 | query GetChannel { 6 | getChannel { 7 | message 8 | name 9 | } 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /client/src/graphql/subscriptions.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // this is an auto generated file. This will be overwritten 3 | 4 | export const subscribe = /* GraphQL */ ` 5 | subscription Subscribe($name: String!) { 6 | subscribe(name: $name) { 7 | message 8 | name 9 | } 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | root.render( 9 | 10 | 11 | 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /client/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /client/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /images/client.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-global-pubsub-api/7f1d2cb00ee13b47cb707301600e1f5b1118fdd8/images/client.png -------------------------------------------------------------------------------- /images/globalWSAPI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-global-pubsub-api/7f1d2cb00ee13b47cb707301600e1f5b1118fdd8/images/globalWSAPI.png -------------------------------------------------------------------------------- /images/invalidation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-global-pubsub-api/7f1d2cb00ee13b47cb707301600e1f5b1118fdd8/images/invalidation.png --------------------------------------------------------------------------------