Repository: sid88in/serverless-appsync-plugin
Pull request #574 (2023-03-06 21:29:37)

feat(dataSources): Add Support for EventBridge DataSource
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appsync-datasource-eventbridgeconfig.html


Patch:
diff --git a/.eslintrc.js b/.eslintrc.js
index db003de..d95f58c 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -10,7 +10,7 @@ module.exports = {
     ecmaVersion: 2018,
   },
   rules: {
-    // used for tests until all erros aget fixed
+    // used for tests until all errors get fixed
     '@typescript-eslint/ban-ts-comment': 'off',
     // already handled by ts/no-unused
     'no-unused-vars': 'off',
diff --git a/README.md b/README.md
index 6debb23..f6bbba5 100644
--- a/README.md
+++ b/README.md
@@ -97,7 +97,7 @@ To upgrade form [v1](https://github.com/sid88in/serverless-appsync-plugin/tree/v
 
 If you have any questions, issue, feature request, please feel free to [open an issue](/issues/new).
 
-You are also very welcome to open a PR and we will gladely review it.
+You are also very welcome to open a PR and we will gladly review it.
 
 # Resources
 
diff --git a/doc/API-keys.md b/doc/API-keys.md
index e24f8ce..c75f685 100644
--- a/doc/API-keys.md
+++ b/doc/API-keys.md
@@ -19,7 +19,7 @@ It can either be string, which translates into the API key's name with default v
 
 - `name`: A unique name for this API key. Required.
 - `description`: An optional description for this API key.
-- `expiresAfter`: A time after which this API key will expire. [See below](#expiry) for more details about expiry. Defaults to `365y`.
+- `expiresAfter`: A time after which this API key will expire. [See below](#expiry) for more details about expiry. Defaults to `365d`.
 - `expiresAt`: A date-time at which this API key will expire. [See below](#expiry) for more details about expiry.
 - `wafRules`: an array of [WAF rules](WAF.md) that will apply to this API key only.
 
diff --git a/doc/WAF.md b/doc/WAF.md
index 1479de9..40d636f 100644
--- a/doc/WAF.md
+++ b/doc/WAF.md
@@ -8,6 +8,8 @@ You can configure WAF rules under the `appSync.waf` attribute.
 
 ## Quick start
 
+You can define a collection of rules for your web ACL and associate it:
+
 ```yaml
 appSync:
   name: my-api
@@ -19,17 +21,28 @@ appSync:
       - disableIntrospection
 ```
 
+Or directly associate an existing web ACL:
+
+```yaml
+appSync:
+  name: my-api
+  waf:
+    enabled: true
+    arn: 'arn:aws:wafv2:us-east-1:123456789012:regional/webacl/my-Waf/d7b694d2-4f7d-4dd6-a9a9-843dd1931330'
+```
+
 ## Configuration
 
 - `enabled`: Boolean. Enable or disable WAF. Defaults to `true` when `appSync.waf` is defined.
+- `arn`: Optional. The WAF's ARN to associate with your AppSync resource.
 - `name`: Optional. The name of this WAF instance. Defaults to the name of your API.
 - `defaultAction`: Optional. The default action if a request does not match a rule. `Allow` or `Block`. Defaults to `Allow`.
-- `description`: A description for this WAF instance.
+- `description`: Optional. A description for this WAF instance.
 - `visibilityConfig`: Optional. A [visibility config](https://docs.aws.amazon.com/waf/latest/APIReference/API_VisibilityConfig.html) for this WAF
   - `name`: Metric name
   - `cloudWatchMetricsEnabled`: A boolean indicating whether the associated resource sends metrics to Amazon CloudWatch
   - `sampledRequestsEnabled`: A boolean indicating whether AWS WAF should store a sampling of the web requests that match the rule
-- `rules`: An array of [rules](#rules).
+- `rules`: Required. An array of [rules](#rules). Optional when `arn` is present
 
 ## Rules
 
@@ -119,7 +132,7 @@ apiKeys:
       - disableIntrospection # disables introspection for this API key
 ```
 
-Adding a rule to an API key without any _statement_ will add a _match-all_ rule for that key (all requests will match that rule). This is usefull for example to exclude API keys from global rules. In that case, you need to make sure to attribute a higher priority to that rule.
+Adding a rule to an API key without any _statement_ will add a _match-all_ rule for that key (all requests will match that rule). This is useful for example to exclude API keys from global rules. In that case, you need to make sure to attribute a higher priority to that rule.
 
 Example:
 
diff --git a/doc/authentication.md b/doc/authentication.md
index b4773e5..0173097 100644
--- a/doc/authentication.md
+++ b/doc/authentication.md
@@ -1,6 +1,6 @@
 # Authentication
 
-[Authentication](https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html) definitions are found under the `appSync.authentication` (for the default authentication method) and `appSync.additionalAuthentication` (as an array) for additional ones
+[Authentication](https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html) definitions are found under the `appSync.authentication` (for the default authentication method) and `appSync.additionalAuthentications` (as an array) for additional ones
 
 ## Quick start
 
@@ -8,7 +8,7 @@
 appSync:
   authentication:
     type: 'API_KEY'
-  additionalAuthentication:
+  additionalAuthentications:
     - type: 'AMAZON_COGNITO_USER_POOLS'
       config:
         userPoolId: '123456789'
@@ -80,7 +80,7 @@ appSync:
 
 ### Lambda
 
-Allows custom authentication thoguht Lambda.
+Allows custom authentication through Lambda.
 
 ```yaml
 appSync:
diff --git a/doc/caching.md b/doc/caching.md
index 51b3bb6..f16aa60 100644
--- a/doc/caching.md
+++ b/doc/caching.md
@@ -22,11 +22,11 @@ appSync:
 - `ttl`: The default TTL of the cache in seconds. Defaults to `3600`. Maximum is `3600`
 - `enabled`: Boolean. Whether caching is enabled. Defaults to `true` when the `caching` definition is present.
 - `atRestEncryption`: Boolean. Whether to encrypt the data at rest. Defaults to `false`
-- `transitEncryption`: Boolean. Whether to encrypt the data in transit. Detaults to `false`
+- `transitEncryption`: Boolean. Whether to encrypt the data in transit. Defaults to `false`
 
 ## Per resolver caching
 
-See [Resolver cacing](resolvers.md#caching)
+See [Resolver caching](resolvers.md#caching)
 
 ## Flushing the cache
 
diff --git a/doc/commands.md b/doc/commands.md
index bf6353e..0b5f777 100644
--- a/doc/commands.md
+++ b/doc/commands.md
@@ -76,7 +76,7 @@ Before associating a domain to an API, you must first create it. You can do so u
 - `--stage`: The stage to use
 
 ```bash
-sls appsync-domain create
+sls appsync domain create
 ```
 
 ## Delete the domain
@@ -90,7 +90,7 @@ Deletes a domain from AppSync.
 - `--stage`: The stage to use
 
 ```bash
-sls appsync-domain delete
+sls appsync domain delete
 ```
 
 If an API is associated to it, you will need to [disassociate](#disassociate-the-api-from-the-domain) it first.
@@ -103,7 +103,7 @@ If you use Route53 for your hosted zone, you can also create the required CNAME
 - `--stage`: The stage to use
 
 ```bash
-sls appsync-domain create-record
+sls appsync domain create-record
 ```
 
 ## Delete the route53 record
@@ -113,7 +113,7 @@ sls appsync-domain create-record
 - `--stage`: The stage to use
 
 ```bash
-sls appsync-domain delete-record
+sls appsync domain delete-record
 ```
 
 ## Associate the API to the domain
@@ -124,7 +124,7 @@ Associate the API in this stack to the domain.
 - `--stage`: The stage to use
 
 ```bash
-sls appsync-domain assoc --stage dev
+sls appsync domain assoc --stage dev
 ```
 
 You can associate an API to a domain that already has another API attached to it. The old API will be replaced by the new one.
@@ -136,5 +136,5 @@ You can associate an API to a domain that already has another API attached to it
 - `--stage`: The stage to use
 
 ```bash
-sls appsync-domain disassoc --stage dev
+sls appsync domain disassoc --stage dev
 ```
diff --git a/doc/custom-domain.md b/doc/custom-domain.md
index f36bec4..c376b8a 100644
--- a/doc/custom-domain.md
+++ b/doc/custom-domain.md
@@ -16,9 +16,9 @@ appSync:
 
 ## Configuration
 
-- `name`: Required. The fully qualified domain name to assiciate this API to.
+- `name`: Required. The fully qualified domain name to associate this API to.
 - `certificateArn`: Optional. A valid certificate ARN for the domain name. See [Certificate](#certificate).
-- `useCloudFormation`: Boolean. Optional. Wheter to use CloudFormation or CLI commands to manage the domain. See [Using CloudFormation or CLI commands](#using-cloudformation-vs-the-cli-commands). Defaults to `true`.
+- `useCloudFormation`: Boolean. Optional. Whether to use CloudFormation or CLI commands to manage the domain. See [Using CloudFormation or CLI commands](#using-cloudformation-vs-the-cli-commands). Defaults to `true`.
 - `retain`: Boolean, optional. Whether to retain the domain and domain association when they are removed from CloudFormation. Defaults to `false`. See [Ejecting from CloudFormation](#ejecting-from-cloudformation)
 - `hostedZoneId`: Boolean, conditional. The Route53 hosted zone id where to create the certificate validation and/or AppSync Alias records. Required if `useCloudFormation` is `true` and `certificateArn` is not provided.
 - `hostedZoneName`: The hosted zone name where to create the route53 Alias record. If `certificateArn` is provided, it takes precedence over `hostedZoneName`.
diff --git a/doc/dataSources.md b/doc/dataSources.md
index 3a71f5f..f52add7 100644
--- a/doc/dataSources.md
+++ b/doc/dataSources.md
@@ -105,7 +105,7 @@ appSync:
 ```yaml
 appSync:
   dataSources:
-    api:
+    myDatabase:
       type: 'RELATIONAL_DATABASE'
       config:
         databaseName: myDatabase
@@ -117,13 +117,28 @@ appSync:
 ### config
 
 - `databaseName`: The name of the database
-- `region`: The region of the RDS HTTP ednpoint. Defaults to the region of the stack.
+- `region`: The region of the RDS HTTP endpoint. Defaults to the region of the stack.
 - `awsSecretStoreArn`: The ARN for database credentials stored in AWS Secrets Manager.
 - `dbClusterIdentifier`: Amazon RDS cluster Amazon Resource Name (ARN).
 - `schema`: Logical schema name.
 - `serviceRoleArn`: The service role ARN for this DataSource. If not provided, a new one will be created.
 - `iamRoleStatements`: Statements to use for the generated IAM Role. If not provided, default statements will be used.
 
+## EventBridge
+
+```yaml
+appSync:
+  dataSources:
+    myEventBus:
+      type: 'AMAZON_EVENTBRIDGE'
+      confing:
+        eventBusArn: !GetAtt MyEventBus.Arn
+```
+
+### config
+
+- `eventBusArn`: The ARN of the event bus
+
 ## NONE
 
 ```yaml
diff --git a/doc/general-config.md b/doc/general-config.md
index 494a311..84dee06 100644
--- a/doc/general-config.md
+++ b/doc/general-config.md
@@ -128,7 +128,7 @@ type Post {
 }
 ```
 
-This will result into the folowing schema:
+This will result into the following schema:
 
 ```graphql
 type Query {
@@ -164,11 +164,11 @@ This plugin intends to use modern schemas for future-proofing. Incompatibilities
 
 Old-style descriptions (using `#`) are supported by AppSync but will be removed by the [stitching procedure](#schema-stitching) which does not support them\*. Comments are also not supported on [enums](https://spec.graphql.org/October2021/#sec-Enums) by AppSync.
 
-\* If you want to retain `#` comments, the workwround is to skip schema stiching by putting your whole schema into one single file.
+\* If you want to retain `#` comments, the workaround is to skip schema stitching by putting your whole schema into one single file.
 
 **Multiple interfaces**
 
-Types can implement multiple [interfaces](https://spec.graphql.org/October2021/#sec-Interfaces) using an ampersand `&` in GraphQL, but AppSync uses the old comma (`,`) separator. `&` is the only separator suported by this plugin, but it will automatically be replaced with a `,`.
+Types can implement multiple [interfaces](https://spec.graphql.org/October2021/#sec-Interfaces) using an ampersand `&` in GraphQL, but AppSync uses the old comma (`,`) separator. `&` is the only separator supported by this plugin, but it will automatically be replaced with a `,`.
 
 ## Logging
 
diff --git a/doc/pipeline-functions.md b/doc/pipeline-functions.md
index 44e6893..abaacb8 100644
--- a/doc/pipeline-functions.md
+++ b/doc/pipeline-functions.md
@@ -14,14 +14,14 @@ appSync:
       code: myFunction.js
 ```
 
-## Configutation
+## Configuration
 
 - `dataSource`: The name of the dataSource to use.
 - `description`: An optional description for this pipeline function.
 - `code`: The path to the JS resolver handler file, relative to `serverless.yml`.
 - `request`: The path to the VTL request mapping template file, relative to `serverless.yml`.
 - `response`: The path to the VTL response mapping template file, relative to `serverless.yml`.
-- `maxBatchSize`: The maximum [batch size](https://aws.amazon.com/blogs/mobile/introducing-configurable-batching-size-for-aws-appsync-lambda-resolvers/) to use (only available for AWS Lambda DataAources)
+- `maxBatchSize`: The maximum [batch size](https://aws.amazon.com/blogs/mobile/introducing-configurable-batching-size-for-aws-appsync-lambda-resolvers/) to use (only available for AWS Lambda DataSources)
 - `substitutions`: See [VTL template substitutions](substitutions.md)
 - `sync`: [See SyncConfig](syncConfig.md)
 
diff --git a/doc/substitutions.md b/doc/substitutions.md
index 4738bd2..5d2a857 100644
--- a/doc/substitutions.md
+++ b/doc/substitutions.md
@@ -37,9 +37,9 @@ appSync:
 
 ## Usage
 
-Substutitions are defined as key-value pairs under the `appSync.substitutions`, `appSync.resolvers.[resolverName].substitutions` or `appSync.pipelineFunctions.[functionName].substitutions` attributes.
+Substitutions are defined as key-value pairs under the `appSync.substitutions`, `appSync.resolvers.[resolverName].substitutions` or `appSync.pipelineFunctions.[functionName].substitutions` attributes.
 
-Global substitutions are available globally to all mapping templates. Resovler and Pipeline function substitutions are only available where they are defined. Resolver and Pipeline function substitutions take precedence over global substitutions (values will be overwritten).
+Global substitutions are available globally to all mapping templates. Resolver and Pipeline function substitutions are only available where they are defined. Resolver and Pipeline function substitutions take precedence over global substitutions (values will be overwritten).
 
 Once defined, you can then use them within the mapping templates as if they were VTL variables. At deployment time, variables will be substituted with their corresponding value.
 
diff --git a/src/index.ts b/src/index.ts
index 1a251f1..6d93c2e 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -102,7 +102,7 @@ class ServerlessAppsyncPlugin {
     // We are using a newer version of AJV than Serverless Framework
     // and some customizations (eg: custom errors, $merge, filter irrelevant errors)
     // For SF, just validate the type of input to allow us to use a custom
-    // field (ie: `appSync`). Actual valiation will be handled by this plugin
+    // field (ie: `appSync`). Actual validation will be handled by this plugin
     // later in `validateConfig()`
     this.serverless.configSchemaHandler.defineTopLevelProperty('appSync', {
       type: 'object',
diff --git a/src/resources/DataSource.ts b/src/resources/DataSource.ts
index c31f363..5e3fd5c 100644
--- a/src/resources/DataSource.ts
+++ b/src/resources/DataSource.ts
@@ -11,6 +11,7 @@ import {
   DsHttpConfig,
   DsRelationalDbConfig,
   IamStatement,
+  DsEventBridgeConfig,
 } from '../types/plugin';
 import { Api } from './Api';
 
@@ -47,6 +48,10 @@ export class DataSource {
       );
     } else if (this.config.type === 'HTTP') {
       resource.Properties.HttpConfig = this.getHttpConfig(this.config);
+    } else if (this.config.type === 'AMAZON_EVENTBRIDGE') {
+      resource.Properties.EventBridgeConfig = this.getEventBridgeConfig(
+        this.config,
+      );
     }
 
     const logicalId = this.api.naming.getDataSourceLogicalId(this.config.name);
@@ -98,6 +103,14 @@ export class DataSource {
     }
   }
 
+  getEventBridgeConfig(
+    config: DsEventBridgeConfig,
+  ): CfnDataSource['Properties']['EventBridgeConfig'] {
+    return {
+      EventBusArn: config.config.eventBusArn,
+    };
+  }
+
   getOpenSearchConfig(
     config: DsOpenSearchConfig,
   ): CfnDataSource['Properties']['OpenSearchServiceConfig'] {
@@ -404,6 +417,16 @@ export class DataSource {
 
         return [defaultESStatement];
       }
+      case 'AMAZON_EVENTBRIDGE': {
+        // Allow PutEvents on the EventBridge bus
+        const defaultEventBridgeStatement: IamStatement = {
+          Action: ['events:PutEvents'],
+          Effect: 'Allow',
+          Resource: [this.config.config.eventBusArn],
+        };
+
+        return [defaultEventBridgeStatement];
+      }
     }
   }
 }
diff --git a/src/resources/Waf.ts b/src/resources/Waf.ts
index 9a78372..b9a6dfa 100644
--- a/src/resources/Waf.ts
+++ b/src/resources/Waf.ts
@@ -23,11 +23,23 @@ export class Waf {
     if (wafConfig.enabled === false) {
       return {};
     }
+    const apiLogicalId = this.api.naming.getApiLogicalId();
+    const wafAssocLogicalId = this.api.naming.getWafAssociationLogicalId();
+
+    if (wafConfig.arn) {
+      return {
+        [wafAssocLogicalId]: {
+          Type: 'AWS::WAFv2::WebACLAssociation',
+          Properties: {
+            ResourceArn: { 'Fn::GetAtt': [apiLogicalId, 'Arn'] },
+            WebACLArn: wafConfig.arn,
+          },
+        },
+      };
+    }
 
     const name = wafConfig.name || `${this.api.config.name}Waf`;
-    const apiLogicalId = this.api.naming.getApiLogicalId();
     const wafLogicalId = this.api.naming.getWafLogicalId();
-    const wafAssocLogicalId = this.api.naming.getWafAssociationLogicalId();
     const defaultActionSource = wafConfig.defaultAction || 'Allow';
     const defaultAction: CfnWafAction =
       typeof defaultActionSource === 'string'
diff --git a/src/types/cloudFormation.ts b/src/types/cloudFormation.ts
index 6db069a..6100d4e 100644
--- a/src/types/cloudFormation.ts
+++ b/src/types/cloudFormation.ts
@@ -39,7 +39,8 @@ export type CfnDataSource = {
       | 'AMAZON_OPENSEARCH_SERVICE'
       | 'NONE'
       | 'HTTP'
-      | 'RELATIONAL_DATABASE';
+      | 'RELATIONAL_DATABASE'
+      | 'AMAZON_EVENTBRIDGE';
     ServiceRoleArn?: string | IntrinsicFunction;
     LambdaConfig?: {
       LambdaFunctionArn: string | IntrinsicFunction;
@@ -75,6 +76,9 @@ export type CfnDataSource = {
         };
       };
     };
+    EventBridgeConfig?: {
+      EventBusArn: string | IntrinsicFunction;
+    };
   };
 };
 
diff --git a/src/types/plugin.ts b/src/types/plugin.ts
index 8f6138b..84d6deb 100644
--- a/src/types/plugin.ts
+++ b/src/types/plugin.ts
@@ -26,11 +26,12 @@ export type IamStatement = {
 
 export type WafConfig = {
   enabled?: boolean;
+  arn?: string;
   name?: string;
   defaultAction?: WafAction;
   description?: string;
   visibilityConfig?: VisibilityConfig;
-  rules: WafRule[];
+  rules?: WafRule[];
 };
 
 export type WafThrottleConfig =
@@ -204,6 +205,15 @@ export type DsDynamoDBConfig = {
   };
 };
 
+export type DsEventBridgeConfig = {
+  type: 'AMAZON_EVENTBRIDGE';
+  config: {
+    serviceRoleArn?: string | IntrinsicFunction;
+    iamRoleStatements?: IamStatement[];
+    eventBusArn: string | IntrinsicFunction;
+  };
+};
+
 export type DsRelationalDbConfig = {
   type: 'RELATIONAL_DATABASE';
   config: {
@@ -278,6 +288,7 @@ export type DataSourceConfig = {
   | DsRelationalDbConfig
   | DsOpenSearchConfig
   | DsLambdaConfig
+  | DsEventBridgeConfig
   | DsNone
 );
 
diff --git a/src/validation.ts b/src/validation.ts
index 7c12bb6..abbf8fb 100644
--- a/src/validation.ts
+++ b/src/validation.ts
@@ -19,6 +19,7 @@ const DATASOURCE_TYPES = [
   'HTTP',
   'NONE',
   'RELATIONAL_DATABASE',
+  'AMAZON_EVENTBRIDGE',
 ] as const;
 
 export const appSyncSchema = {
@@ -462,6 +463,17 @@ export const appSyncSchema = {
                 },
                 required: ['config'],
               },
+              else: {
+                if: { properties: { type: { const: 'AMAZON_EVENTBRIDGE' } } },
+                then: {
+                  properties: {
+                    config: {
+                      $ref: '#/definitions/datasourceEventBridgeConfig',
+                    },
+                  },
+                  required: ['config'],
+                },
+              },
             },
           },
         },
@@ -603,6 +615,13 @@ export const appSyncSchema = {
       },
       required: [],
     },
+    datasourceEventBridgeConfig: {
+      type: 'object',
+      properties: {
+        eventBusArn: { $ref: '#/definitions/stringOrIntrinsicFunction' },
+      },
+      required: ['eventBusArn'],
+    },
   },
   properties: {
     name: { type: 'string' },
@@ -664,19 +683,31 @@ export const appSyncSchema = {
       type: 'object',
       properties: {
         enabled: { type: 'boolean' },
-        name: { type: 'string' },
-        defaultAction: {
-          type: 'string',
-          enum: ['Allow', 'Block'],
-          errorMessage: "must be 'Allow' or 'Block'",
+      },
+      if: {
+        required: ['arn'],
+      },
+      then: {
+        properties: {
+          arn: { $ref: '#/definitions/stringOrIntrinsicFunction' },
         },
-        description: { type: 'string' },
-        rules: {
-          type: 'array',
-          items: { $ref: '#/definitions/wafRule' },
+      },
+      else: {
+        properties: {
+          name: { type: 'string' },
+          defaultAction: {
+            type: 'string',
+            enum: ['Allow', 'Block'],
+            errorMessage: "must be 'Allow' or 'Block'",
+          },
+          description: { type: 'string' },
+          rules: {
+            type: 'array',
+            items: { $ref: '#/definitions/wafRule' },
+          },
         },
+        required: ['rules'],
       },
-      required: ['rules'],
     },
     tags: {
       type: 'object',


Test patch:
diff --git a/src/__tests__/__snapshots__/dataSources.test.ts.snap b/src/__tests__/__snapshots__/dataSources.test.ts.snap
index 68023f5..c715fef 100644
--- a/src/__tests__/__snapshots__/dataSources.test.ts.snap
+++ b/src/__tests__/__snapshots__/dataSources.test.ts.snap
@@ -810,6 +810,198 @@ Object {
 }
 `;
 
+exports[`DataSource EventBridge should generate Resource with default role 1`] = `
+Object {
+  "GraphQlDseventBridge": Object {
+    "Properties": Object {
+      "ApiId": Object {
+        "Fn::GetAtt": Array [
+          "GraphQlApi",
+          "ApiId",
+        ],
+      },
+      "Description": "My eventBridge bus",
+      "EventBridgeConfig": Object {
+        "EventBusArn": "arn:aws:events:us-east-1:123456789012:event-bus/default",
+      },
+      "Name": "eventBridge",
+      "ServiceRoleArn": Object {
+        "Fn::GetAtt": Array [
+          "GraphQlDseventBridgeRole",
+          "Arn",
+        ],
+      },
+      "Type": "AMAZON_EVENTBRIDGE",
+    },
+    "Type": "AWS::AppSync::DataSource",
+  },
+  "GraphQlDseventBridgeRole": Object {
+    "Properties": Object {
+      "AssumeRolePolicyDocument": Object {
+        "Statement": Array [
+          Object {
+            "Action": Array [
+              "sts:AssumeRole",
+            ],
+            "Effect": "Allow",
+            "Principal": Object {
+              "Service": Array [
+                "appsync.amazonaws.com",
+              ],
+            },
+          },
+        ],
+        "Version": "2012-10-17",
+      },
+      "Policies": Array [
+        Object {
+          "PolicyDocument": Object {
+            "Statement": Array [
+              Object {
+                "Action": Array [
+                  "events:PutEvents",
+                ],
+                "Effect": "Allow",
+                "Resource": Array [
+                  "arn:aws:events:us-east-1:123456789012:event-bus/default",
+                ],
+              },
+            ],
+            "Version": "2012-10-17",
+          },
+          "PolicyName": "AppSync-Datasource-eventBridge",
+        },
+      ],
+    },
+    "Type": "AWS::IAM::Role",
+  },
+}
+`;
+
+exports[`DataSource EventBridge should generate default role with a Ref for the bus ARN 1`] = `
+Object {
+  "GraphQlDseventBridge": Object {
+    "Properties": Object {
+      "ApiId": Object {
+        "Fn::GetAtt": Array [
+          "GraphQlApi",
+          "ApiId",
+        ],
+      },
+      "Description": "My eventBridge bus",
+      "EventBridgeConfig": Object {
+        "EventBusArn": Object {
+          "Fn::GetAtt": Array [
+            "MyEventBus",
+            "Arn",
+          ],
+        },
+      },
+      "Name": "eventBridge",
+      "ServiceRoleArn": Object {
+        "Fn::GetAtt": Array [
+          "GraphQlDseventBridgeRole",
+          "Arn",
+        ],
+      },
+      "Type": "AMAZON_EVENTBRIDGE",
+    },
+    "Type": "AWS::AppSync::DataSource",
+  },
+  "GraphQlDseventBridgeRole": Object {
+    "Properties": Object {
+      "AssumeRolePolicyDocument": Object {
+        "Statement": Array [
+          Object {
+            "Action": Array [
+              "sts:AssumeRole",
+            ],
+            "Effect": "Allow",
+            "Principal": Object {
+              "Service": Array [
+                "appsync.amazonaws.com",
+              ],
+            },
+          },
+        ],
+        "Version": "2012-10-17",
+      },
+      "Policies": Array [
+        Object {
+          "PolicyDocument": Object {
+            "Statement": Array [
+              Object {
+                "Action": Array [
+                  "events:PutEvents",
+                ],
+                "Effect": "Allow",
+                "Resource": Array [
+                  Object {
+                    "Fn::GetAtt": Array [
+                      "MyEventBus",
+                      "Arn",
+                    ],
+                  },
+                ],
+              },
+            ],
+            "Version": "2012-10-17",
+          },
+          "PolicyName": "AppSync-Datasource-eventBridge",
+        },
+      ],
+    },
+    "Type": "AWS::IAM::Role",
+  },
+}
+`;
+
+exports[`DataSource EventBridge should generate default role with custom statement 1`] = `
+Object {
+  "GraphQlDseventBridgeRole": Object {
+    "Properties": Object {
+      "AssumeRolePolicyDocument": Object {
+        "Statement": Array [
+          Object {
+            "Action": Array [
+              "sts:AssumeRole",
+            ],
+            "Effect": "Allow",
+            "Principal": Object {
+              "Service": Array [
+                "appsync.amazonaws.com",
+              ],
+            },
+          },
+        ],
+        "Version": "2012-10-17",
+      },
+      "Policies": Array [
+        Object {
+          "PolicyDocument": Object {
+            "Statement": Array [
+              Object {
+                "Action": Array [
+                  "events:PutEvents",
+                ],
+                "Effect": "Allow",
+                "Resource": Array [
+                  "arn:aws:events:us-east-1:123456789012:event-bus/default",
+                  "arn:aws:events:us-east-1:123456789012:event-bus/other",
+                ],
+              },
+            ],
+            "Version": "2012-10-17",
+          },
+          "PolicyName": "AppSync-Datasource-eventBridge",
+        },
+      ],
+    },
+    "Type": "AWS::IAM::Role",
+  },
+}
+`;
+
 exports[`DataSource HTTP should generate Resource with IAM authorization config 1`] = `
 Object {
   "GraphQlDsmyEndpoint": Object {
diff --git a/src/__tests__/__snapshots__/waf.test.ts.snap b/src/__tests__/__snapshots__/waf.test.ts.snap
index d629f9d..4101842 100644
--- a/src/__tests__/__snapshots__/waf.test.ts.snap
+++ b/src/__tests__/__snapshots__/waf.test.ts.snap
@@ -287,6 +287,23 @@ Array [
 ]
 `;
 
+exports[`Waf Base Resources should generate only the waf association 1`] = `
+Object {
+  "GraphQlWafAssoc": Object {
+    "Properties": Object {
+      "ResourceArn": Object {
+        "Fn::GetAtt": Array [
+          "GraphQlApi",
+          "Arn",
+        ],
+      },
+      "WebACLArn": "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/my-Waf/d7b694d2-4f7d-4dd6-a9a9-843dd1931330",
+    },
+    "Type": "AWS::WAFv2::WebACLAssociation",
+  },
+}
+`;
+
 exports[`Waf Base Resources should generate waf Resources 1`] = `
 Object {
   "GraphQlWaf": Object {
diff --git a/src/__tests__/dataSources.test.ts b/src/__tests__/dataSources.test.ts
index db72665..2675fd9 100644
--- a/src/__tests__/dataSources.test.ts
+++ b/src/__tests__/dataSources.test.ts
@@ -106,6 +106,78 @@ describe('DataSource', () => {
     });
   });
 
+  describe('EventBridge', () => {
+    it('should generate Resource with default role', () => {
+      const api = new Api(given.appSyncConfig(), plugin);
+      const dataSource = new DataSource(api, {
+        type: 'AMAZON_EVENTBRIDGE',
+        name: 'eventBridge',
+        description: 'My eventBridge bus',
+        config: {
+          eventBusArn:
+            'arn:aws:events:us-east-1:123456789012:event-bus/default',
+        },
+      });
+
+      expect(dataSource.compile()).toMatchSnapshot();
+    });
+
+    it('should generate default role with a Ref for the bus ARN', () => {
+      const api = new Api(given.appSyncConfig(), plugin);
+      const dataSource = new DataSource(api, {
+        type: 'AMAZON_EVENTBRIDGE',
+        name: 'eventBridge',
+        description: 'My eventBridge bus',
+        config: {
+          eventBusArn: { 'Fn::GetAtt': ['MyEventBus', 'Arn'] },
+        },
+      });
+
+      expect(dataSource.compile()).toMatchSnapshot();
+    });
+
+    it('should generate default role with custom statement', () => {
+      const api = new Api(given.appSyncConfig(), plugin);
+      const dataSource = new DataSource(api, {
+        type: 'AMAZON_EVENTBRIDGE',
+        name: 'eventBridge',
+        description: 'My eventBridge bus',
+        config: {
+          eventBusArn:
+            'arn:aws:events:us-east-1:123456789012:event-bus/default',
+          iamRoleStatements: [
+            {
+              Effect: 'Allow',
+              Action: ['events:PutEvents'],
+              Resource: [
+                'arn:aws:events:us-east-1:123456789012:event-bus/default',
+                'arn:aws:events:us-east-1:123456789012:event-bus/other',
+              ],
+            },
+          ],
+        },
+      });
+
+      expect(dataSource.compileDataSourceIamRole()).toMatchSnapshot();
+    });
+
+    it('should not generate default role when a service role arn is passed', () => {
+      const api = new Api(given.appSyncConfig(), plugin);
+      const dataSource = new DataSource(api, {
+        type: 'AMAZON_EVENTBRIDGE',
+        name: 'eventBridge',
+        description: 'My eventBridge bus',
+        config: {
+          eventBusArn:
+            'arn:aws:events:us-east-1:123456789012:event-bus/default',
+          serviceRoleArn: 'arn:aws:iam:',
+        },
+      });
+
+      expect(dataSource.compileDataSourceIamRole()).toBeUndefined();
+    });
+  });
+
   describe('AWS Lambda', () => {
     it('should generate Resource with default role', () => {
       const api = new Api(given.appSyncConfig(), plugin);
diff --git a/src/__tests__/validation/__snapshots__/base.test.ts.snap b/src/__tests__/validation/__snapshots__/base.test.ts.snap
index 1d11788..3406190 100644
--- a/src/__tests__/validation/__snapshots__/base.test.ts.snap
+++ b/src/__tests__/validation/__snapshots__/base.test.ts.snap
@@ -36,14 +36,16 @@ exports[`Valdiation Log Invalid should validate a Invalid 1`] = `
 `;
 
 exports[`Valdiation Waf Invalid should validate a Invalid 1`] = `
-"/waf/enabled: must be boolean
-/waf/name: must be string
+"/waf/name: must be string
 /waf/defaultAction: must be 'Allow' or 'Block'
 /waf/rules/0: must be a valid WAF rule
 /waf/rules/1: must be a valid WAF rule
-/waf/rules/2: must be a valid WAF rule"
+/waf/rules/2: must be a valid WAF rule
+/waf/enabled: must be boolean"
 `;
 
+exports[`Valdiation Waf Invalid should validate a Invalid arn 1`] = `"/waf/arn: must be a string or a CloudFormation intrinsic function"`;
+
 exports[`Valdiation Waf Invalid should validate a Throttle limit 1`] = `
 "/waf/rules/0: must be a valid WAF rule
 /waf/rules/1: must be a valid WAF rule"
diff --git a/src/__tests__/validation/__snapshots__/datasources.test.ts.snap b/src/__tests__/validation/__snapshots__/datasources.test.ts.snap
index 818d4b5..303e25c 100644
--- a/src/__tests__/validation/__snapshots__/datasources.test.ts.snap
+++ b/src/__tests__/validation/__snapshots__/datasources.test.ts.snap
@@ -1,7 +1,7 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`Basic Invalid should validate: Invalid Datasource 1`] = `
-"/dataSources/myDynamoSource1/type: must be one of AMAZON_DYNAMODB, AMAZON_OPENSEARCH_SERVICE, AWS_LAMBDA, HTTP, NONE, RELATIONAL_DATABASE
+"/dataSources/myDynamoSource1/type: must be one of AMAZON_DYNAMODB, AMAZON_OPENSEARCH_SERVICE, AWS_LAMBDA, HTTP, NONE, RELATIONAL_DATABASE, AMAZON_EVENTBRIDGE
 /dataSources: contains invalid data source definitions"
 `;
 
@@ -28,6 +28,21 @@ exports[`DynamoDB Invalid should validate: Missing config 1`] = `
 /dataSources: contains invalid data source definitions"
 `;
 
+exports[`EventBridge Invalid should not validate: Empty config 1`] = `
+"/dataSources/myEventBridgeSource1/config: must have required property 'eventBusArn'
+/dataSources: contains invalid data source definitions"
+`;
+
+exports[`EventBridge Invalid should not validate: Invalid config 1`] = `
+"/dataSources/myEventBridgeSource1/config/eventBusArn: must be a string or a CloudFormation intrinsic function
+/dataSources: contains invalid data source definitions"
+`;
+
+exports[`EventBridge Invalid should not validate: Missing config 1`] = `
+"/dataSources/myEventBridgeSource1: must have required property 'config'
+/dataSources: contains invalid data source definitions"
+`;
+
 exports[`HTTP Invalid should validate: Empty config 1`] = `
 "/dataSources/http1/config: must have required property 'endpoint'
 /dataSources: contains invalid data source definitions"
diff --git a/src/__tests__/validation/base.test.ts b/src/__tests__/validation/base.test.ts
index 5aa1b4a..1d6ff8d 100644
--- a/src/__tests__/validation/base.test.ts
+++ b/src/__tests__/validation/base.test.ts
@@ -152,6 +152,16 @@ describe('Valdiation', () => {
             },
           } as AppSyncConfigInput,
         },
+        {
+          name: 'Using arn',
+          config: {
+            ...basicConfig,
+            waf: {
+              enabled: true,
+              arn: 'arn:aws:',
+            },
+          },
+        },
       ];
 
       assertions.forEach((config) => {
@@ -187,6 +197,15 @@ describe('Valdiation', () => {
             },
           },
         },
+        {
+          name: 'Invalid arn',
+          config: {
+            ...basicConfig,
+            waf: {
+              arn: 123,
+            },
+          },
+        },
         {
           name: 'Throttle limit',
           config: {
diff --git a/src/__tests__/validation/datasources.test.ts b/src/__tests__/validation/datasources.test.ts
index 1a92135..c582c4d 100644
--- a/src/__tests__/validation/datasources.test.ts
+++ b/src/__tests__/validation/datasources.test.ts
@@ -199,6 +199,113 @@ describe('DynamoDB', () => {
     });
   });
 });
+describe('EventBridge', () => {
+  describe('Valid', () => {
+    const assertions = [
+      {
+        name: 'Valid config',
+        config: {
+          dataSources: {
+            myDynamoSource1: {
+              type: 'AMAZON_EVENTBRIDGE',
+              config: {
+                eventBusArn:
+                  'arn:aws:events:us-east-1:123456789012:event-bus/my-event-bus',
+              },
+            },
+          },
+        },
+      },
+      {
+        name: 'EventBusArn as Ref',
+        config: {
+          dataSources: {
+            myDynamoSource1: {
+              type: 'AMAZON_EVENTBRIDGE',
+              config: {
+                eventBusArn: {
+                  'Fn::GetAtt': ['MyEventBus', 'Arn'],
+                },
+              },
+            },
+          },
+        },
+      },
+      {
+        name: 'Valid config, as array of maps',
+        config: {
+          dataSources: [
+            {
+              myDynamoSource1: {
+                type: 'AMAZON_EVENTBRIDGE',
+                config: {
+                  eventBusArn:
+                    'arn:aws:events:us-east-1:123456789012:event-bus/my-event-bus',
+                },
+              },
+            },
+          ],
+        },
+      },
+    ];
+
+    assertions.forEach((config) => {
+      it(`should validate a ${config.name}`, () => {
+        expect(validateConfig({ ...basicConfig, ...config.config })).toBe(true);
+      });
+    });
+  });
+
+  describe('Invalid', () => {
+    const assertions = [
+      {
+        name: 'Missing config',
+        config: {
+          dataSources: {
+            myEventBridgeSource1: {
+              type: 'AMAZON_EVENTBRIDGE',
+            },
+          },
+        },
+      },
+      {
+        name: 'Empty config',
+        config: {
+          dataSources: {
+            myEventBridgeSource1: {
+              type: 'AMAZON_EVENTBRIDGE',
+              config: {},
+            },
+          },
+        },
+      },
+      {
+        name: 'Invalid config',
+        config: {
+          dataSources: {
+            myEventBridgeSource1: {
+              type: 'AMAZON_EVENTBRIDGE',
+              config: {
+                eventBusArn: 1234,
+              },
+            },
+          },
+        },
+      },
+    ];
+
+    assertions.forEach((config) => {
+      it(`should not validate: ${config.name}`, () => {
+        expect(function () {
+          validateConfig({
+            ...basicConfig,
+            ...config.config,
+          });
+        }).toThrowErrorMatchingSnapshot();
+      });
+    });
+  });
+});
 
 describe('Lambda', () => {
   describe('Valid', () => {
diff --git a/src/__tests__/waf.test.ts b/src/__tests__/waf.test.ts
index 9de677d..9575f66 100644
--- a/src/__tests__/waf.test.ts
+++ b/src/__tests__/waf.test.ts
@@ -60,6 +60,15 @@ describe('Waf', () => {
       );
       expect(api.compileWafRules()).toEqual({});
     });
+
+    it('should generate only the waf association', () => {
+      const api = new Api(given.appSyncConfig(), plugin);
+      const waf = new Waf(api, {
+        enabled: true,
+        arn: 'arn:aws:wafv2:us-east-1:123456789012:regional/webacl/my-Waf/d7b694d2-4f7d-4dd6-a9a9-843dd1931330',
+      });
+      expect(waf.compile()).toMatchSnapshot();
+    });
   });
 
   describe('Throttle rules', () => {