diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/appsync.test.graphql b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/appsync.test.graphql new file mode 100644 index 0000000000000..9a2546e396abf --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/appsync.test.graphql @@ -0,0 +1,13 @@ +type Event { + message: String +} +type Query { + getTests: [Event]! +} +type Mutation { + publish(message: String!): Event +} + +type Subscription { + onPublish: Event @aws_subscribe(mutations: ["publish"]) +} diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/integ.appsync-events.js.snapshot/aws-cdk-aws-appsync-target-integ.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/integ.appsync-events.js.snapshot/aws-cdk-aws-appsync-target-integ.assets.json new file mode 100644 index 0000000000000..399b85c136688 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/integ.appsync-events.js.snapshot/aws-cdk-aws-appsync-target-integ.assets.json @@ -0,0 +1,19 @@ +{ + "version": "36.0.0", + "files": { + "fa005296bfe6cbad01df3cb800a324adbe7bcdb6273343c59488a74b1a9834a1": { + "source": { + "path": "aws-cdk-aws-appsync-target-integ.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "fa005296bfe6cbad01df3cb800a324adbe7bcdb6273343c59488a74b1a9834a1.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/integ.appsync-events.js.snapshot/aws-cdk-aws-appsync-target-integ.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/integ.appsync-events.js.snapshot/aws-cdk-aws-appsync-target-integ.template.json new file mode 100644 index 0000000000000..fef7e923e6aba --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/integ.appsync-events.js.snapshot/aws-cdk-aws-appsync-target-integ.template.json @@ -0,0 +1,238 @@ +{ + "Resources": { + "baseApiCDA4D43A": { + "Type": "AWS::AppSync::GraphQLApi", + "Properties": { + "AuthenticationType": "AWS_IAM", + "Name": "aws-cdk-aws-appsync-target-integ-api" + } + }, + "baseApiSchemaB12C7BB0": { + "Type": "AWS::AppSync::GraphQLSchema", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "baseApiCDA4D43A", + "ApiId" + ] + }, + "Definition": "type Event {\n message: String\n}\ntype Query {\n getTests: [Event]!\n}\ntype Mutation {\n publish(message: String!): Event\n}\n\ntype Subscription {\n onPublish: Event @aws_subscribe(mutations: [\"publish\"])\n}\n" + } + }, + "baseApinone7DDDEE3D": { + "Type": "AWS::AppSync::DataSource", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "baseApiCDA4D43A", + "ApiId" + ] + }, + "Name": "none", + "Type": "NONE" + } + }, + "baseApipublisherC3F47EA2": { + "Type": "AWS::AppSync::Resolver", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "baseApiCDA4D43A", + "ApiId" + ] + }, + "Code": "export const request = (ctx) => ({payload: null})\nexport const response = (ctx) => ctx.args.message", + "DataSourceName": "none", + "FieldName": "publish", + "Kind": "UNIT", + "Runtime": { + "Name": "APPSYNC_JS", + "RuntimeVersion": "1.0.0" + }, + "TypeName": "Mutation" + }, + "DependsOn": [ + "baseApinone7DDDEE3D", + "baseApiSchemaB12C7BB0" + ] + }, + "baseApiEventsRoleAC472BD7": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "baseApiEventsRoleDefaultPolicy94199357": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "appsync:GraphQL", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":appsync:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":apis/", + { + "Fn::GetAtt": [ + "baseApiCDA4D43A", + "ApiId" + ] + }, + "/types/Mutation/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "baseApiEventsRoleDefaultPolicy94199357", + "Roles": [ + { + "Ref": "baseApiEventsRoleAC472BD7" + } + ] + } + }, + "Queue4A7E3555": { + "Type": "AWS::SQS::Queue", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "QueuePolicy25439813": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Fn::GetAtt": [ + "TimerBF6F831F", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + }, + "Resource": { + "Fn::GetAtt": [ + "Queue4A7E3555", + "Arn" + ] + }, + "Sid": "AllowEventRuleawscdkawsappsynctargetintegTimer2A2187F8" + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "Queue4A7E3555" + } + ] + } + }, + "TimerBF6F831F": { + "Type": "AWS::Events::Rule", + "Properties": { + "ScheduleExpression": "rate(1 minute)", + "State": "ENABLED", + "Targets": [ + { + "AppSyncParameters": { + "GraphQLOperation": "mutation Publish($message: String!){ publish(message: $message) { message } }" + }, + "Arn": { + "Fn::GetAtt": [ + "baseApiCDA4D43A", + "GraphQLEndpointArn" + ] + }, + "DeadLetterConfig": { + "Arn": { + "Fn::GetAtt": [ + "Queue4A7E3555", + "Arn" + ] + } + }, + "Id": "Target0", + "Input": "{\"message\":\"hello world\"}", + "RoleArn": { + "Fn::GetAtt": [ + "baseApiEventsRoleAC472BD7", + "Arn" + ] + } + } + ] + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/integ.appsync-events.js.snapshot/awsappsyncintegDefaultTestDeployAssert7E101DC4.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/integ.appsync-events.js.snapshot/awsappsyncintegDefaultTestDeployAssert7E101DC4.assets.json new file mode 100644 index 0000000000000..7c6f6b0aa6d9f --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/integ.appsync-events.js.snapshot/awsappsyncintegDefaultTestDeployAssert7E101DC4.assets.json @@ -0,0 +1,19 @@ +{ + "version": "36.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "awsappsyncintegDefaultTestDeployAssert7E101DC4.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/integ.appsync-events.js.snapshot/awsappsyncintegDefaultTestDeployAssert7E101DC4.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/integ.appsync-events.js.snapshot/awsappsyncintegDefaultTestDeployAssert7E101DC4.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/integ.appsync-events.js.snapshot/awsappsyncintegDefaultTestDeployAssert7E101DC4.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/integ.appsync-events.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/integ.appsync-events.js.snapshot/cdk.out new file mode 100644 index 0000000000000..1f0068d32659a --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/integ.appsync-events.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"36.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/integ.appsync-events.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/integ.appsync-events.js.snapshot/integ.json new file mode 100644 index 0000000000000..3b6cc9115d7e0 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/integ.appsync-events.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "36.0.0", + "testCases": { + "aws-appsync-integ/DefaultTest": { + "stacks": [ + "aws-cdk-aws-appsync-target-integ" + ], + "assertionStack": "aws-appsync-integ/DefaultTest/DeployAssert", + "assertionStackName": "awsappsyncintegDefaultTestDeployAssert7E101DC4" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/integ.appsync-events.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/integ.appsync-events.js.snapshot/manifest.json new file mode 100644 index 0000000000000..5ec8f4c577f0c --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/integ.appsync-events.js.snapshot/manifest.json @@ -0,0 +1,161 @@ +{ + "version": "36.0.0", + "artifacts": { + "aws-cdk-aws-appsync-target-integ.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "aws-cdk-aws-appsync-target-integ.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "aws-cdk-aws-appsync-target-integ": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-cdk-aws-appsync-target-integ.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/fa005296bfe6cbad01df3cb800a324adbe7bcdb6273343c59488a74b1a9834a1.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "aws-cdk-aws-appsync-target-integ.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "aws-cdk-aws-appsync-target-integ.assets" + ], + "metadata": { + "/aws-cdk-aws-appsync-target-integ/baseApi/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "baseApiCDA4D43A" + } + ], + "/aws-cdk-aws-appsync-target-integ/baseApi/Schema": [ + { + "type": "aws:cdk:logicalId", + "data": "baseApiSchemaB12C7BB0" + } + ], + "/aws-cdk-aws-appsync-target-integ/baseApi/none/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "baseApinone7DDDEE3D" + } + ], + "/aws-cdk-aws-appsync-target-integ/baseApi/publisher/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "baseApipublisherC3F47EA2" + } + ], + "/aws-cdk-aws-appsync-target-integ/baseApi/EventsRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "baseApiEventsRoleAC472BD7" + } + ], + "/aws-cdk-aws-appsync-target-integ/baseApi/EventsRole/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "baseApiEventsRoleDefaultPolicy94199357" + } + ], + "/aws-cdk-aws-appsync-target-integ/Queue/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Queue4A7E3555" + } + ], + "/aws-cdk-aws-appsync-target-integ/Queue/Policy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "QueuePolicy25439813" + } + ], + "/aws-cdk-aws-appsync-target-integ/Timer/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "TimerBF6F831F" + } + ], + "/aws-cdk-aws-appsync-target-integ/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/aws-cdk-aws-appsync-target-integ/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "aws-cdk-aws-appsync-target-integ" + }, + "awsappsyncintegDefaultTestDeployAssert7E101DC4.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "awsappsyncintegDefaultTestDeployAssert7E101DC4.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "awsappsyncintegDefaultTestDeployAssert7E101DC4": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "awsappsyncintegDefaultTestDeployAssert7E101DC4.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "awsappsyncintegDefaultTestDeployAssert7E101DC4.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "awsappsyncintegDefaultTestDeployAssert7E101DC4.assets" + ], + "metadata": { + "/aws-appsync-integ/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/aws-appsync-integ/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "aws-appsync-integ/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/integ.appsync-events.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/integ.appsync-events.js.snapshot/tree.json new file mode 100644 index 0000000000000..e95bb0b5f8d49 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/integ.appsync-events.js.snapshot/tree.json @@ -0,0 +1,464 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "aws-cdk-aws-appsync-target-integ": { + "id": "aws-cdk-aws-appsync-target-integ", + "path": "aws-cdk-aws-appsync-target-integ", + "children": { + "baseApi": { + "id": "baseApi", + "path": "aws-cdk-aws-appsync-target-integ/baseApi", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-aws-appsync-target-integ/baseApi/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::AppSync::GraphQLApi", + "aws:cdk:cloudformation:props": { + "authenticationType": "AWS_IAM", + "name": "aws-cdk-aws-appsync-target-integ-api" + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_appsync.CfnGraphQLApi", + "version": "0.0.0" + } + }, + "Schema": { + "id": "Schema", + "path": "aws-cdk-aws-appsync-target-integ/baseApi/Schema", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::AppSync::GraphQLSchema", + "aws:cdk:cloudformation:props": { + "apiId": { + "Fn::GetAtt": [ + "baseApiCDA4D43A", + "ApiId" + ] + }, + "definition": "type Event {\n message: String\n}\ntype Query {\n getTests: [Event]!\n}\ntype Mutation {\n publish(message: String!): Event\n}\n\ntype Subscription {\n onPublish: Event @aws_subscribe(mutations: [\"publish\"])\n}\n" + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_appsync.CfnGraphQLSchema", + "version": "0.0.0" + } + }, + "LogGroup": { + "id": "LogGroup", + "path": "aws-cdk-aws-appsync-target-integ/baseApi/LogGroup", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "none": { + "id": "none", + "path": "aws-cdk-aws-appsync-target-integ/baseApi/none", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-aws-appsync-target-integ/baseApi/none/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::AppSync::DataSource", + "aws:cdk:cloudformation:props": { + "apiId": { + "Fn::GetAtt": [ + "baseApiCDA4D43A", + "ApiId" + ] + }, + "name": "none", + "type": "NONE" + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_appsync.CfnDataSource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_appsync.NoneDataSource", + "version": "0.0.0" + } + }, + "publisher": { + "id": "publisher", + "path": "aws-cdk-aws-appsync-target-integ/baseApi/publisher", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-aws-appsync-target-integ/baseApi/publisher/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::AppSync::Resolver", + "aws:cdk:cloudformation:props": { + "apiId": { + "Fn::GetAtt": [ + "baseApiCDA4D43A", + "ApiId" + ] + }, + "code": "export const request = (ctx) => ({payload: null})\nexport const response = (ctx) => ctx.args.message", + "dataSourceName": "none", + "fieldName": "publish", + "kind": "UNIT", + "runtime": { + "name": "APPSYNC_JS", + "runtimeVersion": "1.0.0" + }, + "typeName": "Mutation" + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_appsync.CfnResolver", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_appsync.Resolver", + "version": "0.0.0" + } + }, + "EventsRole": { + "id": "EventsRole", + "path": "aws-cdk-aws-appsync-target-integ/baseApi/EventsRole", + "children": { + "ImportEventsRole": { + "id": "ImportEventsRole", + "path": "aws-cdk-aws-appsync-target-integ/baseApi/EventsRole/ImportEventsRole", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-cdk-aws-appsync-target-integ/baseApi/EventsRole/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "aws-cdk-aws-appsync-target-integ/baseApi/EventsRole/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-aws-appsync-target-integ/baseApi/EventsRole/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": "appsync:GraphQL", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":appsync:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":apis/", + { + "Fn::GetAtt": [ + "baseApiCDA4D43A", + "ApiId" + ] + }, + "/types/Mutation/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "policyName": "baseApiEventsRoleDefaultPolicy94199357", + "roles": [ + { + "Ref": "baseApiEventsRoleAC472BD7" + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Role", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_appsync.GraphqlApi", + "version": "0.0.0" + } + }, + "Queue": { + "id": "Queue", + "path": "aws-cdk-aws-appsync-target-integ/Queue", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-aws-appsync-target-integ/Queue/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SQS::Queue", + "aws:cdk:cloudformation:props": {} + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_sqs.CfnQueue", + "version": "0.0.0" + } + }, + "Policy": { + "id": "Policy", + "path": "aws-cdk-aws-appsync-target-integ/Queue/Policy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-aws-appsync-target-integ/Queue/Policy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SQS::QueuePolicy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Fn::GetAtt": [ + "TimerBF6F831F", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + }, + "Resource": { + "Fn::GetAtt": [ + "Queue4A7E3555", + "Arn" + ] + }, + "Sid": "AllowEventRuleawscdkawsappsynctargetintegTimer2A2187F8" + } + ], + "Version": "2012-10-17" + }, + "queues": [ + { + "Ref": "Queue4A7E3555" + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_sqs.CfnQueuePolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_sqs.QueuePolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_sqs.Queue", + "version": "0.0.0" + } + }, + "Timer": { + "id": "Timer", + "path": "aws-cdk-aws-appsync-target-integ/Timer", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-aws-appsync-target-integ/Timer/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Events::Rule", + "aws:cdk:cloudformation:props": { + "scheduleExpression": "rate(1 minute)", + "state": "ENABLED", + "targets": [ + { + "id": "Target0", + "arn": { + "Fn::GetAtt": [ + "baseApiCDA4D43A", + "GraphQLEndpointArn" + ] + }, + "roleArn": { + "Fn::GetAtt": [ + "baseApiEventsRoleAC472BD7", + "Arn" + ] + }, + "deadLetterConfig": { + "arn": { + "Fn::GetAtt": [ + "Queue4A7E3555", + "Arn" + ] + } + }, + "appSyncParameters": { + "graphQlOperation": "mutation Publish($message: String!){ publish(message: $message) { message } }" + }, + "input": "{\"message\":\"hello world\"}" + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_events.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_events.Rule", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "aws-cdk-aws-appsync-target-integ/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "aws-cdk-aws-appsync-target-integ/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + }, + "aws-appsync-integ": { + "id": "aws-appsync-integ", + "path": "aws-appsync-integ", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "aws-appsync-integ/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "aws-appsync-integ/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "aws-appsync-integ/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "aws-appsync-integ/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "aws-appsync-integ/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/integ.appsync-events.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/integ.appsync-events.ts new file mode 100644 index 0000000000000..82622e771cc43 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/appsync/integ.appsync-events.ts @@ -0,0 +1,50 @@ +import * as events from 'aws-cdk-lib/aws-events'; +import * as appsync from 'aws-cdk-lib/aws-appsync'; +import * as sqs from 'aws-cdk-lib/aws-sqs'; +import * as cdk from 'aws-cdk-lib'; +import * as targets from 'aws-cdk-lib/aws-events-targets'; +import * as path from 'path'; +import { IntegTest } from '@aws-cdk/integ-tests-alpha'; + +const app = new cdk.App(); + +class AwsAppSyncEvent extends cdk.Stack { + + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const api = new appsync.GraphqlApi(this, 'baseApi', { + name: 'aws-cdk-aws-appsync-target-integ-api', + definition: appsync.Definition.fromFile(path.join(__dirname, 'appsync.test.graphql')), + authorizationConfig: { defaultAuthorization: { authorizationType: appsync.AuthorizationType.IAM } }, + }); + const none = api.addNoneDataSource('none'); + none.createResolver('publisher', { + typeName: 'Mutation', + fieldName: 'publish', + code: appsync.AssetCode.fromInline(` +export const request = (ctx) => ({payload: null}) +export const response = (ctx) => ctx.args.message +`.trim()), + runtime: appsync.FunctionRuntime.JS_1_0_0, + }); + + const graphQLOperation = 'mutation Publish($message: String!){ publish(message: $message) { message } }'; + const queue = new sqs.Queue(this, 'Queue'); + + const timer = new events.Rule(this, 'Timer', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), + }); + timer.addTarget(new targets.AppSync(api, { + graphQLOperation, + variables: events.RuleTargetInput.fromObject({ + message: 'hello world', + }), + deadLetterQueue: queue, + })); + + } +} + +const stack = new AwsAppSyncEvent(app, 'aws-cdk-aws-appsync-target-integ'); +new IntegTest(app, 'aws-appsync-integ', { testCases: [stack] }); \ No newline at end of file diff --git a/packages/aws-cdk-lib/aws-appsync/README.md b/packages/aws-cdk-lib/aws-appsync/README.md index dfb8fca7ce144..a239a481e057a 100644 --- a/packages/aws-cdk-lib/aws-appsync/README.md +++ b/packages/aws-cdk-lib/aws-appsync/README.md @@ -1,6 +1,5 @@ # AWS AppSync Construct Library - The `aws-cdk-lib/aws-appsync` package contains constructs for building flexible APIs that use GraphQL. @@ -86,8 +85,6 @@ demoDS.createResolver('QueryGetDemosConsistentResolver', { }); ``` - - ### Aurora Serverless AppSync provides a data source for executing SQL commands against Amazon Aurora @@ -303,6 +300,7 @@ httpDs.createResolver('MutationCallStepFunctionResolver', { ``` ### EventBridge + Integrating AppSync with EventBridge enables developers to use EventBridge rules to route commands for GraphQL mutations that need to perform any one of a variety of asynchronous tasks. More broadly, it enables teams to expose an event bus as a part of a GraphQL schema. @@ -437,6 +435,7 @@ ds.createResolver('QueryGetTestsResolver', { ``` ## Merged APIs + AppSync supports [Merged APIs](https://docs.aws.amazon.com/appsync/latest/devguide/merged-api.html) which can be used to merge multiple source APIs into a single API. ```ts @@ -602,9 +601,9 @@ sources and resolvers, an `apiId` is sufficient. ## Private APIs -By default all AppSync GraphQL APIs are public and can be accessed from the internet. -For customers that want to limit access to be from their VPC, the optional API `visibility` property can be set to `Visibility.PRIVATE` -at creation time. To explicitly create a public API, the `visibility` property should be set to `Visibility.GLOBAL`. +By default all AppSync GraphQL APIs are public and can be accessed from the internet. +For customers that want to limit access to be from their VPC, the optional API `visibility` property can be set to `Visibility.PRIVATE` +at creation time. To explicitly create a public API, the `visibility` property should be set to `Visibility.GLOBAL`. If visibility is not set, the service will default to `GLOBAL`. CDK stack file `app-stack.ts`: @@ -617,8 +616,8 @@ const api = new appsync.GraphqlApi(this, 'api', { }); ``` -See [documentation](https://docs.aws.amazon.com/appsync/latest/devguide/using-private-apis.html) -for more details about Private APIs +See [documentation](https://docs.aws.amazon.com/appsync/latest/devguide/using-private-apis.html) +for more details about Private APIs ## Authorization @@ -842,8 +841,8 @@ const api = new appsync.GraphqlApi(this, 'api', { ## Resolver Count Limits -You can control how many resolvers each query can process. -By default, each query can process up to 10000 resolvers. +You can control how many resolvers each query can process. +By default, each query can process up to 10000 resolvers. By setting a limit AppSync will not handle any resolvers past a certain number limit. ```ts @@ -870,3 +869,27 @@ const api = new appsync.GraphqlApi(this, 'api', { api.addEnvironmentVariable('EnvKey2', 'non-empty-2'); ``` + +## Configure an EventBridge target that invokes an AppSync GraphQL API + +Configuring the target relies on the `graphQLEndpointArn` property. + +Use the `AppSync` event target to trigger an AppSync GraphQL API. You need to +create an `AppSync.GraphqlApi` configured with `AWS_IAM` authorization mode. + +The code snippet below creates a AppSync GraphQL API target that is invoked, calling the `publish` mutation. + +```ts +import * as events from 'aws-cdk-lib/aws-events'; +import * as targets from 'aws-cdk-lib/aws-events-targets'; + +declare const rule: events.Rule; +declare const api: appsync.GraphqlApi; + +rule.addTarget(new targets.AppSync(api, { + graphQLOperation: 'mutation Publish($message: String!){ publish(message: $message) { message } }', + variables: events.RuleTargetInput.fromObject({ + message: 'hello world', + }), +})); +``` diff --git a/packages/aws-cdk-lib/aws-appsync/lib/graphqlapi-base.ts b/packages/aws-cdk-lib/aws-appsync/lib/graphqlapi-base.ts index 7f91d270c93ec..7b87718a52e5d 100644 --- a/packages/aws-cdk-lib/aws-appsync/lib/graphqlapi-base.ts +++ b/packages/aws-cdk-lib/aws-appsync/lib/graphqlapi-base.ts @@ -112,6 +112,47 @@ export class IamResource { } } +/** + * Visibility type for a GraphQL API + */ +export enum Visibility { + + /** + * Public, open to the internet + */ + GLOBAL = 'GLOBAL', + /** + * Only accessible through a VPC + */ + PRIVATE = 'PRIVATE', +} + +/** + * enum with all possible values for AppSync authorization type + */ +export enum AuthorizationType { + /** + * API Key authorization type + */ + API_KEY = 'API_KEY', + /** + * AWS IAM authorization type. Can be used with Cognito Identity Pool federated credentials + */ + IAM = 'AWS_IAM', + /** + * Cognito User Pool authorization type + */ + USER_POOL = 'AMAZON_COGNITO_USER_POOLS', + /** + * OpenID Connect authorization type + */ + OIDC = 'OPENID_CONNECT', + /** + * Lambda authorization type + */ + LAMBDA = 'AWS_LAMBDA', +} + /** * Interface for GraphQL */ @@ -132,6 +173,21 @@ export interface IGraphqlApi extends IResource { */ readonly arn: string; + /** + * The GraphQL endpoint ARN + */ + readonly graphQLEndpointArn: string; + + /** + * the visibility of the API + */ + readonly visibility: Visibility; + + /** + * The Authorization Types for this GraphQL Api + */ + readonly modes: AuthorizationType[]; + /** * add a new dummy data source to this API. Useful for pipeline resolvers * and for backend changes that don't require a data source. @@ -290,11 +346,26 @@ export abstract class GraphqlApiBase extends Resource implements IGraphqlApi { */ public abstract readonly apiId: string; + /** + * The GraphQL endpoint ARN + */ + public abstract readonly graphQLEndpointArn: string; + + /** + * The visibility of the API + */ + public abstract readonly visibility: Visibility; + /** * the ARN of the API */ public abstract readonly arn: string; + /** + * The Authorization Types for this GraphQL Api + */ + public abstract readonly modes: AuthorizationType[]; + /** * add a new dummy data source to this API. Useful for pipeline resolvers * and for backend changes that don't require a data source. diff --git a/packages/aws-cdk-lib/aws-appsync/lib/graphqlapi.ts b/packages/aws-cdk-lib/aws-appsync/lib/graphqlapi.ts index a301d90328d1d..4332257d5484c 100644 --- a/packages/aws-cdk-lib/aws-appsync/lib/graphqlapi.ts +++ b/packages/aws-cdk-lib/aws-appsync/lib/graphqlapi.ts @@ -1,6 +1,6 @@ import { Construct } from 'constructs'; import { CfnApiKey, CfnGraphQLApi, CfnGraphQLSchema, CfnDomainName, CfnDomainNameApiAssociation, CfnSourceApiAssociation } from './appsync.generated'; -import { IGraphqlApi, GraphqlApiBase } from './graphqlapi-base'; +import { IGraphqlApi, GraphqlApiBase, Visibility, AuthorizationType } from './graphqlapi-base'; import { ISchema, SchemaFile } from './schema'; import { MergeType, addSourceApiAutoMergePermission, addSourceGraphQLPermission } from './source-api-association'; import { ICertificate } from '../../aws-certificatemanager'; @@ -11,32 +11,6 @@ import { ILogGroup, LogGroup, LogRetention, RetentionDays } from '../../aws-logs import { CfnResource, Duration, Expiration, FeatureFlags, IResolvable, Lazy, Stack, Token } from '../../core'; import * as cxapi from '../../cx-api'; -/** - * enum with all possible values for AppSync authorization type - */ -export enum AuthorizationType { - /** - * API Key authorization type - */ - API_KEY = 'API_KEY', - /** - * AWS IAM authorization type. Can be used with Cognito Identity Pool federated credentials - */ - IAM = 'AWS_IAM', - /** - * Cognito User Pool authorization type - */ - USER_POOL = 'AMAZON_COGNITO_USER_POOLS', - /** - * OpenID Connect authorization type - */ - OIDC = 'OPENID_CONNECT', - /** - * Lambda authorization type - */ - LAMBDA = 'AWS_LAMBDA', -} - /** * Interface to specify default or additional authorization(s) */ @@ -260,21 +234,6 @@ export interface LogConfig { readonly retention?: RetentionDays; } -/** - * Visibility type for a GraphQL API - */ -export enum Visibility { - - /** - * Public, open to the internet - */ - GLOBAL = 'GLOBAL', - /** - * Only accessible through a VPC - */ - PRIVATE = 'PRIVATE', -} - /** * Domain name configuration for AppSync */ @@ -489,6 +448,27 @@ export interface GraphqlApiAttributes { * @default - autogenerated arn */ readonly graphqlApiArn?: string; + + /** + * The GraphQl endpoint arn for the GraphQL API + * + * @default - none, required to construct event rules from imported APIs + */ + readonly graphQLEndpointArn?: string; + + /** + * The GraphQl API visibility + * + * @default - GLOBAL + */ + readonly visibility?: Visibility; + + /** + * The Authorization Types for this GraphQL Api + * + * @default - none, required to construct event rules from imported APIs + */ + readonly modes?: AuthorizationType[]; } /** @@ -527,6 +507,13 @@ export class GraphqlApi extends GraphqlApiBase { class Import extends GraphqlApiBase { public readonly apiId = attrs.graphqlApiId; public readonly arn = arn; + + // the GraphQL endpoint ARN is not required to identify an AppSync GraphQL API + // this value is only needed to construct event rules. + public readonly graphQLEndpointArn = attrs.graphQLEndpointArn ?? ''; + public readonly visibility = attrs.visibility ?? Visibility.GLOBAL; + public readonly modes = attrs.modes ?? [] + constructor(s: Construct, i: string) { super(s, i); } @@ -545,6 +532,11 @@ export class GraphqlApi extends GraphqlApiBase { */ public readonly arn: string; + /** + * The GraphQL endpoint ARN + */ + public readonly graphQLEndpointArn: string; + /** * the URL of the endpoint created by AppSync * @@ -557,6 +549,11 @@ export class GraphqlApi extends GraphqlApiBase { */ public readonly name: string; + /** + * the visibility of the API + */ + public readonly visibility: Visibility; + /** * the schema attached to this api (only available for GraphQL APIs, not available for merged APIs) */ @@ -630,6 +627,8 @@ export class GraphqlApi extends GraphqlApiBase { } this.node.addValidation({ validate: () => this.validateEnvironmentVariables() }); + this.visibility = props.visibility ?? Visibility.GLOBAL; + this.api = new CfnGraphQLApi(this, 'Resource', { name: props.name, authenticationType: defaultMode.authorizationType, @@ -652,6 +651,7 @@ export class GraphqlApi extends GraphqlApiBase { this.arn = this.api.attrArn; this.graphqlUrl = this.api.attrGraphQlUrl; this.name = this.api.name; + this.graphQLEndpointArn = this.api.attrGraphQlEndpointArn; if (this.definition.schema) { this.schemaResource = new CfnGraphQLSchema(this, 'Schema', this.definition.schema.bind(this)); diff --git a/packages/aws-cdk-lib/aws-events-targets/README.md b/packages/aws-cdk-lib/aws-events-targets/README.md index 45e956687b750..310e7ea65e180 100644 --- a/packages/aws-cdk-lib/aws-events-targets/README.md +++ b/packages/aws-cdk-lib/aws-events-targets/README.md @@ -1,6 +1,5 @@ # Event Targets for Amazon EventBridge - This library contains integration classes to send Amazon EventBridge to any number of supported AWS Services. Instances of these classes should be passed to the `rule.addTarget()` method. @@ -17,6 +16,7 @@ Currently supported are: - [Queue a Batch job](#queue-a-batch-job) - [Invoke an API Gateway REST API](#invoke-an-api-gateway-rest-api) - [Invoke an API Destination](#invoke-an-api-destination) + - [Invoke an AppSync GraphQL API](#invoke-an-appsync-graphql-api) - [Put an event on an EventBridge bus](#put-an-event-on-an-eventbridge-bus) - [Run an ECS Task](#run-an-ecs-task) - [Tagging Tasks](#tagging-tasks) @@ -367,6 +367,65 @@ const rule = new events.Rule(this, 'OtherRule', { }); ``` +## Invoke an AppSync GraphQL API + +Use the `AppSync` target to trigger an AppSync GraphQL API. You need to +create an `AppSync.GraphqlApi` configured with `AWS_IAM` authorization mode. + +The code snippet below creates an AppSync GraphQL API target that is invoked every hour, calling the `publish` mutation. + +```ts +import * as appsync from 'aws-cdk-lib/aws-appsync'; + +const api = new appsync.GraphqlApi(this, 'api', { + name: 'api', + definition: appsync.Definition.fromFile('schema.graphql'), + authorizationConfig: { + defaultAuthorization: { authorizationType: appsync.AuthorizationType.IAM } + }, +}); + +const rule = new events.Rule(this, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.hours(1)), +}); + +rule.addTarget(new targets.AppSync(api, { + graphQLOperation: 'mutation Publish($message: String!){ publish(message: $message) { message } }', + variables: events.RuleTargetInput.fromObject({ + message: 'hello world', + }), +})); +``` + +You can pass an existing role with the proper permissions to be used for the target when the rule is triggered. The code snippet below uses an existing role and grants permissions to use the publish Mutation on the GraphQL API. + +```ts +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as appsync from 'aws-cdk-lib/aws-appsync'; + +const api = appsync.GraphqlApi.fromGraphqlApiAttributes(this, 'ImportedAPI', { + graphqlApiId: '', + graphqlApiArn: '', + graphQLEndpointArn: '', + visibility: appsync.Visibility.GLOBAL, + modes: [appsync.AuthorizationType.IAM], +}); + +const rule = new events.Rule(this, 'Rule', { schedule: events.Schedule.rate(cdk.Duration.minutes(1)), }); +const role = new iam.Role(this, 'Role', { assumedBy: new iam.ServicePrincipal('events.amazonaws.com') }); + +// allow EventBridge to use the `publish` mutation +api.grantMutation(role, 'publish'); + +rule.addTarget(new targets.AppSync(api, { + graphQLOperation: 'mutation Publish($message: String!){ publish(message: $message) { message } }', + variables: events.RuleTargetInput.fromObject({ + message: 'hello world', + }), + eventRole: role +})); +``` + ## Put an event on an EventBridge bus Use the `EventBus` target to route event to a different EventBus. diff --git a/packages/aws-cdk-lib/aws-events-targets/lib/appsync.ts b/packages/aws-cdk-lib/aws-events-targets/lib/appsync.ts new file mode 100644 index 0000000000000..13330066b974b --- /dev/null +++ b/packages/aws-cdk-lib/aws-events-targets/lib/appsync.ts @@ -0,0 +1,84 @@ +import { addToDeadLetterQueueResourcePolicy, bindBaseTargetConfig, singletonEventRole, TargetBaseProps } from './util'; +import * as appsync from '../../aws-appsync'; +import * as events from '../../aws-events'; +import * as iam from '../../aws-iam'; + +/** + * Customize the AppSync GraphQL API target + */ +export interface AppSyncGraphQLApiProps extends TargetBaseProps { + /** + * The GraphQL operation; that is, the query, mutation, or subscription + * to be parsed and executed by the GraphQL service. + */ + readonly graphQLOperation: string; + + /** + * The variables that are include in the GraphQL operation. + * + * @default - The entire event is used + */ + readonly variables?: events.RuleTargetInput; + + /** + * The role to assume before invoking the target + * (i.e., the pipeline) when the given rule is triggered. + * + * @default - a new role with permissions to access mutations will be created + */ + readonly eventRole?: iam.IRole; +} + +/** + * Use an AppSync GraphQL API as a target for Amazon EventBridge rules. + */ +export class AppSync implements events.IRuleTarget { + + constructor(private readonly appsyncApi: appsync.IGraphqlApi, private readonly props: AppSyncGraphQLApiProps) { + } + + /** + * Returns a RuleTarget that can be used to trigger this AppSync GraphQL API + * as a result from an EventBridge event. + */ + public bind(rule: events.IRule, _id?: string): events.RuleTargetConfig { + if (this.props.deadLetterQueue) { + addToDeadLetterQueueResourcePolicy(rule, this.props.deadLetterQueue); + } + + // make sure the API has AWS_IAM configured. + if (!this.appsyncApi.modes.includes(appsync.AuthorizationType.IAM)) { + throw new Error('You must have AWS_IAM authorization mode enabled on your API to configure an AppSync target'); + } + + // make sure this is a 'public' (i.e.: 'GLOBAL') API + if (this.appsyncApi.visibility !== appsync.Visibility.GLOBAL) { + throw new Error('Your API visibility must be "GLOBAL"'); + } + + // make sure the EndpointArn is not blank + if (this.appsyncApi.graphQLEndpointArn === '') { + throw new Error('You must have a valid `graphQLEndpointArn` set'); + } + + const role = this.props.eventRole || singletonEventRole(this.appsyncApi); + + // if a role was not provided, attach a permission + if (!this.props.eventRole) { + this.appsyncApi.grantMutation(role); + } + + return { + ...bindBaseTargetConfig(this.props), + arn: this.appsyncApi.graphQLEndpointArn, + role, + deadLetterConfig: this.props.deadLetterQueue && { arn: this.props.deadLetterQueue?.queueArn }, + input: this.props.variables, + targetResource: this.appsyncApi, + appSyncParameters: { + graphQlOperation: this.props.graphQLOperation, + }, + }; + } +} + diff --git a/packages/aws-cdk-lib/aws-events-targets/lib/index.ts b/packages/aws-cdk-lib/aws-events-targets/lib/index.ts index 6c91810ebca33..a7db6ab24a04f 100644 --- a/packages/aws-cdk-lib/aws-events-targets/lib/index.ts +++ b/packages/aws-cdk-lib/aws-events-targets/lib/index.ts @@ -14,4 +14,5 @@ export * from './log-group'; export * from './kinesis-firehose-stream'; export * from './api-gateway'; export * from './api-destination'; +export * from './appsync'; export * from './util'; diff --git a/packages/aws-cdk-lib/aws-events-targets/test/appsync/appsync.test.graphql b/packages/aws-cdk-lib/aws-events-targets/test/appsync/appsync.test.graphql new file mode 100644 index 0000000000000..251775e1e6902 --- /dev/null +++ b/packages/aws-cdk-lib/aws-events-targets/test/appsync/appsync.test.graphql @@ -0,0 +1,9 @@ +type Event { + message: String +} +type Query { + getTests: [Event]! +} +type Mutation { + publish(message: String!): Event +} diff --git a/packages/aws-cdk-lib/aws-events-targets/test/appsync/appsync.test.ts b/packages/aws-cdk-lib/aws-events-targets/test/appsync/appsync.test.ts new file mode 100644 index 0000000000000..c58eb2efb5332 --- /dev/null +++ b/packages/aws-cdk-lib/aws-events-targets/test/appsync/appsync.test.ts @@ -0,0 +1,395 @@ +import * as path from 'path'; +import { Template } from '../../../assertions'; +import * as appsync from '../../../aws-appsync'; +import * as events from '../../../aws-events'; +import * as iam from '../../../aws-iam'; +import * as sqs from '../../../aws-sqs'; +import * as cdk from '../../../core'; +import * as targets from '../../lib'; + +const graphQLOperation = 'mutation Publish($message: String!){ publish(message: $message) { event } }'; + +describe('AppSync GraphQL API target', () => { + let stack: cdk.Stack; + beforeEach(() => { + stack = new cdk.Stack(); + }); + + test('fails when AWS_IAM auth is not configured', () => { + const noiam_api = new appsync.GraphqlApi(stack, 'noiamApi', { + name: 'no_iam_api', + definition: appsync.Definition.fromFile(path.join(__dirname, 'appsync.test.graphql')), + }); + + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), + }); + + expect(() => { + rule.addTarget(new targets.AppSync(noiam_api, { + graphQLOperation, + variables: events.RuleTargetInput.fromObject({ + message: events.EventField.fromPath('$.detail'), + }), + })); + }).toThrow('You must have AWS_IAM authorization mode enabled on your API to configure an AppSync target'); + }); + + test('fails when graphQLEndpointArn is not configured', () => { + const noEndpointArnAPI = appsync.GraphqlApi.fromGraphqlApiAttributes(stack, 'ImportedAPI', { + graphqlApiId: 'MyApiId', + graphqlApiArn: 'MyApiArn', + modes: [appsync.AuthorizationType.IAM], + }); + + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), + }); + + expect(() => { + rule.addTarget(new targets.AppSync(noEndpointArnAPI, { + graphQLOperation, + variables: events.RuleTargetInput.fromObject({ + message: events.EventField.fromPath('$.detail'), + }), + })); + }).toThrow('You must have a valid `graphQLEndpointArn` set'); + }); + + test('Accepts API create with fromGraphqlApiAttributes', () => { + const api = appsync.GraphqlApi.fromGraphqlApiAttributes(stack, 'ImportedAPI', { + graphqlApiId: 'MyApiId', + graphqlApiArn: 'MyApiArn', + graphQLEndpointArn: 'arn:aws:appsync:us-east-2:000000000000:endpoints/graphql-api/00000000000000000000000000', + visibility: appsync.Visibility.GLOBAL, + modes: [appsync.AuthorizationType.IAM], + }); + + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), + }); + + // WHEN + rule.addTarget(new targets.AppSync(api, { + graphQLOperation, + variables: events.RuleTargetInput.fromObject({ + message: events.EventField.fromPath('$.detail'), + }), + })); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { + Targets: [ + { + Arn: api.graphQLEndpointArn, + AppSyncParameters: { GraphQLOperation: graphQLOperation }, + Id: 'Target0', + InputTransformer: { + InputPathsMap: { detail: '$.detail' }, + InputTemplate: '{"message":}', + }, + RoleArn: { + 'Fn::GetAtt': [ + 'ImportedAPIEventsRole9CE171B7', + 'Arn', + ], + }, + }, + ], + }); + }); + + test('allows secondary auth with AWS_IAM configured', () => { + const sec_api = new appsync.GraphqlApi(stack, 'sec_api', { + name: 'no_iam_api', + definition: appsync.Definition.fromFile(path.join(__dirname, 'appsync.test.graphql')), + authorizationConfig: { additionalAuthorizationModes: [{ authorizationType: appsync.AuthorizationType.IAM }] }, + }); + + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), + }); + + expect(() => { + rule.addTarget(new targets.AppSync(sec_api, { + graphQLOperation, + variables: events.RuleTargetInput.fromObject({ + message: events.EventField.fromPath('$.detail'), + }), + })); + }).not.toThrow('You must have AWS_IAM authorization mode enabled on your API to configure an AppSync target'); + }); + + test('fails when VISIBILITY is not "GLOBAL"', () => { + const sec_api = new appsync.GraphqlApi(stack, 'sec_api', { + name: 'no_iam_api', + visibility: appsync.Visibility.PRIVATE, + definition: appsync.Definition.fromFile(path.join(__dirname, 'appsync.test.graphql')), + authorizationConfig: { additionalAuthorizationModes: [{ authorizationType: appsync.AuthorizationType.IAM }] }, + }); + + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), + }); + + expect(() => { + rule.addTarget(new targets.AppSync(sec_api, { + graphQLOperation, + variables: events.RuleTargetInput.fromObject({ + message: events.EventField.fromPath('$.detail'), + }), + })); + }).toThrow('Your API visibility must be "GLOBAL"'); + }); + +}); + +describe('AppSync API with AWS_IAM auth', () => { + let api: appsync.GraphqlApi; + let stack: cdk.Stack; + beforeEach(() => { + stack = new cdk.Stack(); + api = new appsync.GraphqlApi(stack, 'baseApi', { + name: 'api', + definition: appsync.Definition.fromFile(path.join(__dirname, 'appsync.test.graphql')), + authorizationConfig: { defaultAuthorization: { authorizationType: appsync.AuthorizationType.IAM } }, + }); + }); + + test('use AppSync GraphQL API as an event rule target', () => { + + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), + }); + + // WHEN + rule.addTarget(new targets.AppSync(api, { + graphQLOperation, + variables: events.RuleTargetInput.fromObject({ + message: events.EventField.fromPath('$.detail'), + }), + })); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { + Targets: [ + { + Arn: { 'Fn::GetAtt': ['baseApiCDA4D43A', 'GraphQLEndpointArn'] }, + AppSyncParameters: { GraphQLOperation: graphQLOperation }, + Id: 'Target0', + InputTransformer: { + InputPathsMap: { detail: '$.detail' }, + InputTemplate: '{"message":}', + }, + RoleArn: { + 'Fn::GetAtt': [ + 'baseApiEventsRoleAC472BD7', + 'Arn', + ], + }, + }, + ], + }); + }); + + test('use a Dead Letter Queue', () => { + + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), + }); + + // WHEN + const queue = new sqs.Queue(stack, 'Queue'); + rule.addTarget(new targets.AppSync(api, { + graphQLOperation, + variables: events.RuleTargetInput.fromObject({ + message: events.EventField.fromPath('$.detail'), + }), + deadLetterQueue: queue, + })); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { + Targets: [ + { + DeadLetterConfig: { + Arn: { + 'Fn::GetAtt': [ + 'Queue4A7E3555', + 'Arn', + ], + }, + }, + Id: 'Target0', + }, + ], + }); + }); + + test('when no mutation fields provided, grant access to Mutations only', () => { + + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), + }); + + // WHEN + rule.addTarget(new targets.AppSync(api, { + graphQLOperation, + variables: events.RuleTargetInput.fromObject({ + message: events.EventField.fromPath('$.detail'), + }), + })); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { + Targets: [ + { + Arn: { 'Fn::GetAtt': ['baseApiCDA4D43A', 'GraphQLEndpointArn'] }, + AppSyncParameters: { GraphQLOperation: graphQLOperation }, + Id: 'Target0', + InputTransformer: { + InputPathsMap: { detail: '$.detail' }, + InputTemplate: '{"message":}', + }, + RoleArn: { + 'Fn::GetAtt': [ + 'baseApiEventsRoleAC472BD7', + 'Arn', + ], + }, + }, + ], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'events.amazonaws.com', + }, + }, + ], + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyName: 'baseApiEventsRoleDefaultPolicy94199357', + PolicyDocument: { + Statement: [{ + Action: 'appsync:GraphQL', + Effect: 'Allow', + Resource: + { + 'Fn::Join': [ + '', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':appsync:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':apis/', + { 'Fn::GetAtt': ['baseApiCDA4D43A', 'ApiId'] }, + '/types/Mutation/*', + ], + ], + }, + }], + Version: '2012-10-17', + }, + }); + }); + + test('a role is provided', () => { + + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), + }); + const eventRole = new iam.Role(stack, 'role', { + assumedBy: new iam.ServicePrincipal('events.amazonaws.com'), + }); + api.grantMutation(eventRole, 'publish'); + + // WHEN + rule.addTarget(new targets.AppSync(api, { + graphQLOperation, + variables: events.RuleTargetInput.fromObject({ + message: events.EventField.fromPath('$.detail'), + }), + eventRole, + })); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { + Targets: [ + { + Arn: { 'Fn::GetAtt': ['baseApiCDA4D43A', 'GraphQLEndpointArn'] }, + AppSyncParameters: { GraphQLOperation: graphQLOperation }, + Id: 'Target0', + InputTransformer: { + InputPathsMap: { detail: '$.detail' }, + InputTemplate: '{"message":}', + }, + RoleArn: { + 'Fn::GetAtt': [ + 'roleC7B7E775', + 'Arn', + ], + }, + }, + ], + }); + }); + + test('a role is not provided', () => { + + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), + }); + + // WHEN + rule.addTarget(new targets.AppSync(api, { + graphQLOperation, + variables: events.RuleTargetInput.fromObject({ + message: events.EventField.fromPath('$.detail'), + }), + })); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [{ + Action: 'appsync:GraphQL', + Effect: 'Allow', + Resource: + { + 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':appsync:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':apis/', { 'Fn::GetAtt': ['baseApiCDA4D43A', 'ApiId'] }, '/types/Mutation/*']], + }, + + }], + Version: '2012-10-17', + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'events.amazonaws.com', + }, + }, + ], + }, + }); + + }); + +}); \ No newline at end of file diff --git a/packages/aws-cdk-lib/aws-events/lib/rule.ts b/packages/aws-cdk-lib/aws-events/lib/rule.ts index faa7ea7c488f6..b96347e20ba2b 100644 --- a/packages/aws-cdk-lib/aws-events/lib/rule.ts +++ b/packages/aws-cdk-lib/aws-events/lib/rule.ts @@ -238,6 +238,7 @@ export class Rule extends Resource implements IRule { deadLetterConfig: targetProps.deadLetterConfig, retryPolicy: targetProps.retryPolicy, sqsParameters: targetProps.sqsParameters, + appSyncParameters: targetProps.appSyncParameters, input: inputProps && inputProps.input, inputPath: inputProps && inputProps.inputPath, inputTransformer: inputProps?.inputTemplate !== undefined ? { diff --git a/packages/aws-cdk-lib/aws-events/lib/target.ts b/packages/aws-cdk-lib/aws-events/lib/target.ts index e836bcf75e53d..711b20b416bdf 100644 --- a/packages/aws-cdk-lib/aws-events/lib/target.ts +++ b/packages/aws-cdk-lib/aws-events/lib/target.ts @@ -60,6 +60,12 @@ export interface RuleTargetConfig { */ readonly retryPolicy?: CfnRule.RetryPolicyProperty; + /** + * Contains the GraphQL operation to be parsed and executed, if the event target is an AWS AppSync API. + * @default - None + */ + readonly appSyncParameters?: CfnRule.AppSyncParametersProperty; + /** * The Amazon ECS task definition and task count to use, if the event target * is an Amazon ECS task.