diff --git a/.autover/autover.json b/.autover/autover.json index b1fbf84da..afba7a20c 100644 --- a/.autover/autover.json +++ b/.autover/autover.json @@ -143,6 +143,10 @@ { "Name": "Amazon.Lambda.TestTool", "Path": "Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Amazon.Lambda.TestTool.csproj" + }, + { + "Name": "Amazon.Lambda.AppSyncEvents", + "Path": "Libraries/src/Amazon.Lambda.AppSyncEvents/Amazon.Lambda.AppSyncEvents.csproj" } ], "UseCommitsForChangelog": false, diff --git a/.autover/changes/78ee1265-f50d-434a-b25c-fcb8e9e7a26a.json b/.autover/changes/78ee1265-f50d-434a-b25c-fcb8e9e7a26a.json new file mode 100644 index 000000000..320fd3ada --- /dev/null +++ b/.autover/changes/78ee1265-f50d-434a-b25c-fcb8e9e7a26a.json @@ -0,0 +1,11 @@ +{ + "Projects": [ + { + "Name": "Amazon.Lambda.AppSyncEvents", + "Type": "Major", + "ChangelogMessages": [ + "Added AppSyncResolverEvent to support direct lambda resolver" + ] + } + ] +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e0660aeb5..31e288af2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -105,6 +105,7 @@ The available projects are: * Amazon.Lambda.SQSEvents * Amazon.Lambda.TestUtilities * Amazon.Lambda.TestTool.BlazorTester +* Amazon.Lambda.AppSyncEvents The possible increment types are: * Patch diff --git a/Libraries/Libraries.sln b/Libraries/Libraries.sln index bd3278a8f..03e0b7c79 100644 --- a/Libraries/Libraries.sln +++ b/Libraries/Libraries.sln @@ -123,17 +123,21 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestMinimalAPIApp", "test\T EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Amazon.Lambda.MQEvents", "src\Amazon.Lambda.MQEvents\Amazon.Lambda.MQEvents.csproj", "{BF85932E-2DFF-41CD-8090-A672468B8FBB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.LexV2Events", "src\Amazon.Lambda.LexV2Events\Amazon.Lambda.LexV2Events.csproj", "{3C6AABF5-0372-41E0-874F-DF18ECCC7FB6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Amazon.Lambda.LexV2Events", "src\Amazon.Lambda.LexV2Events\Amazon.Lambda.LexV2Events.csproj", "{3C6AABF5-0372-41E0-874F-DF18ECCC7FB6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest", "test\Amazon.Lambda.RuntimeSupport.Tests\CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest\CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest.csproj", "{0BD83939-458C-4EF5-8663-7098AD1200F2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest", "test\Amazon.Lambda.RuntimeSupport.Tests\CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest\CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest.csproj", "{0BD83939-458C-4EF5-8663-7098AD1200F2}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestExecutableServerlessApp", "test\TestExecutableServerlessApp\TestExecutableServerlessApp.csproj", "{DD378063-C54A-44C7-9A6F-32A6A1AE94B3}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestServerlessApp.NET8", "test\TestServerlessApp.NET8\TestServerlessApp.NET8.csproj", "{7300983D-8FCE-42EA-9B9E-B1C5347D15D8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SnapshotRestore.Registry", "src\SnapshotRestore.Registry\SnapshotRestore.Registry.csproj", "{7261A438-8C1D-47AD-98B0-7678F72E4382}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SnapshotRestore.Registry", "src\SnapshotRestore.Registry\SnapshotRestore.Registry.csproj", "{7261A438-8C1D-47AD-98B0-7678F72E4382}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SnapshotRestore.Registry.Tests", "test\SnapshotRestore.Registry.Tests\SnapshotRestore.Registry.Tests.csproj", "{A699E183-D0D4-4F26-A0A7-88DA5607F455}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SnapshotRestore.Registry.Tests", "test\SnapshotRestore.Registry.Tests\SnapshotRestore.Registry.Tests.csproj", "{A699E183-D0D4-4F26-A0A7-88DA5607F455}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Amazon.Lambda.AppSyncEvents", "src\Amazon.Lambda.AppSyncEvents\Amazon.Lambda.AppSyncEvents.csproj", "{99F39E49-1FD0-4EF5-BF4B-8F2473FB8198}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventsTests.NET8", "test\EventsTests.NET8\EventsTests.NET8.csproj", "{1FB22337-5D88-4CE7-ADFF-FFD89204F0E9}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.DynamoDBEvents.SDK.Convertor", "src\Amazon.Lambda.DynamoDBEvents.SDK.Convertor\Amazon.Lambda.DynamoDBEvents.SDK.Convertor.csproj", "{3400F4E9-BA12-4D3D-9BA1-2798AA8D0AFC}" EndProject @@ -387,6 +391,14 @@ Global {D61CBB71-17AB-4EC2-8C6A-70E9D7C60526}.Debug|Any CPU.Build.0 = Debug|Any CPU {D61CBB71-17AB-4EC2-8C6A-70E9D7C60526}.Release|Any CPU.ActiveCfg = Release|Any CPU {D61CBB71-17AB-4EC2-8C6A-70E9D7C60526}.Release|Any CPU.Build.0 = Release|Any CPU + {99F39E49-1FD0-4EF5-BF4B-8F2473FB8198}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {99F39E49-1FD0-4EF5-BF4B-8F2473FB8198}.Debug|Any CPU.Build.0 = Debug|Any CPU + {99F39E49-1FD0-4EF5-BF4B-8F2473FB8198}.Release|Any CPU.ActiveCfg = Release|Any CPU + {99F39E49-1FD0-4EF5-BF4B-8F2473FB8198}.Release|Any CPU.Build.0 = Release|Any CPU + {1FB22337-5D88-4CE7-ADFF-FFD89204F0E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1FB22337-5D88-4CE7-ADFF-FFD89204F0E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1FB22337-5D88-4CE7-ADFF-FFD89204F0E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1FB22337-5D88-4CE7-ADFF-FFD89204F0E9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -456,11 +468,14 @@ Global {3400F4E9-BA12-4D3D-9BA1-2798AA8D0AFC} = {AAB54E74-20B1-42ED-BC3D-CE9F7BC7FD12} {074DB940-82BA-47D4-B888-C213D4220A82} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69} {D61CBB71-17AB-4EC2-8C6A-70E9D7C60526} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69} + {99F39E49-1FD0-4EF5-BF4B-8F2473FB8198} = {AAB54E74-20B1-42ED-BC3D-CE9F7BC7FD12} + {1FB22337-5D88-4CE7-ADFF-FFD89204F0E9} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {503678A4-B8D1-4486-8915-405A3E9CF0EB} EndGlobalSection GlobalSection(SharedMSBuildProjectFiles) = preSolution + test\EventsTests.Shared\EventsTests.Shared.projitems*{1fb22337-5d88-4ce7-adff-ffd89204f0e9}*SharedItemsImports = 5 test\EventsTests.Shared\EventsTests.Shared.projitems*{44e9d925-b61d-4234-97b7-61424c963ba6}*SharedItemsImports = 5 test\EventsTests.Shared\EventsTests.Shared.projitems*{a2cb78bb-e54f-48ca-bbfb-9553d27ef23d}*SharedItemsImports = 13 test\EventsTests.Shared\EventsTests.Shared.projitems*{c1bb30d2-3237-4cfc-ba93-627471148ec2}*SharedItemsImports = 5 diff --git a/Libraries/src/Amazon.Lambda.AppSyncEvents/Amazon.Lambda.AppSyncEvents.csproj b/Libraries/src/Amazon.Lambda.AppSyncEvents/Amazon.Lambda.AppSyncEvents.csproj new file mode 100644 index 000000000..7b3352894 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.AppSyncEvents/Amazon.Lambda.AppSyncEvents.csproj @@ -0,0 +1,22 @@ + + + + + + Amazon Lambda .NET support - AWS AppSync package. + net8.0 + Amazon.Lambda.AppSyncEvents + 0.0.1 + Amazon.Lambda.AppSyncEvents + Amazon.Lambda.AppSyncEvents + AWS;Amazon;Lambda + enable + + + + IL2026,IL2067,IL2075 + true + true + + + diff --git a/Libraries/src/Amazon.Lambda.AppSyncEvents/AppSyncAuthorizerEvent.cs b/Libraries/src/Amazon.Lambda.AppSyncEvents/AppSyncAuthorizerEvent.cs new file mode 100644 index 000000000..cfb36d85d --- /dev/null +++ b/Libraries/src/Amazon.Lambda.AppSyncEvents/AppSyncAuthorizerEvent.cs @@ -0,0 +1,65 @@ +namespace Amazon.Lambda.AppSyncEvents; + +/// +/// Represents an AWS AppSync authorization event that is sent to a Lambda authorizer +/// for evaluating access permissions to the GraphQL API. +/// +public class AppSyncAuthorizerEvent +{ + /// + /// Gets or sets the authorization token received from the client request. + /// This token is used to make authorization decisions. + /// + public string AuthorizationToken { get; set; } + + /// + /// Gets or sets the headers from the client request. + /// Contains key-value pairs of HTTP header names and their values. + /// + public Dictionary RequestHeaders { get; set; } + + /// + /// Gets or sets the context information about the AppSync request. + /// Contains metadata about the API and the GraphQL operation being executed. + /// + public AppSyncRequestContext RequestContext { get; set; } +} + +/// +/// Contains contextual information about the AppSync request being authorized. +/// This class provides details about the API, account, and GraphQL operation. +/// +public class AppSyncRequestContext +{ + /// + /// Gets or sets the unique identifier of the AppSync API. + /// + public string ApiId { get; set; } + + /// + /// Gets or sets the AWS account ID where the AppSync API is deployed. + /// + public string AccountId { get; set; } + + /// + /// Gets or sets the unique identifier for this specific request. + /// + public string RequestId { get; set; } + + /// + /// Gets or sets the GraphQL query string containing the operation to be executed. + /// + public string QueryString { get; set; } + + /// + /// Gets or sets the name of the GraphQL operation to be executed. + /// This corresponds to the operation name in the GraphQL query. + /// + public string OperationName { get; set; } + + /// + /// Gets or sets the variables passed to the GraphQL operation. + /// Contains key-value pairs of variable names and their values. + /// + public Dictionary Variables { get; set; } +} diff --git a/Libraries/src/Amazon.Lambda.AppSyncEvents/AppSyncAuthorizerResult.cs b/Libraries/src/Amazon.Lambda.AppSyncEvents/AppSyncAuthorizerResult.cs new file mode 100644 index 000000000..356bc1895 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.AppSyncEvents/AppSyncAuthorizerResult.cs @@ -0,0 +1,33 @@ +using System.Text.Json.Serialization; +namespace Amazon.Lambda.AppSyncEvents; + +/// +/// Represents the authorization result returned by a Lambda authorizer to AWS AppSync +/// containing authorization decisions and optional context for the GraphQL API. +/// +public class AppSyncAuthorizerResult +{ + /// + /// Indicates if the request is authorized + /// + [JsonPropertyName("isAuthorized")] + public bool IsAuthorized { get; set; } + + /// + /// Custom context to pass to resolvers, only supports key-value pairs. + /// + [JsonPropertyName("resolverContext")] + public Dictionary ResolverContext { get; set; } + + /// + /// List of fields that are denied access + /// + [JsonPropertyName("deniedFields")] + public IEnumerable DeniedFields { get; set; } + + /// + /// The number of seconds that the response should be cached for + /// + [JsonPropertyName("ttlOverride")] + public int? TtlOverride { get; set; } +} diff --git a/Libraries/src/Amazon.Lambda.AppSyncEvents/AppSyncCognitoIdentity.cs b/Libraries/src/Amazon.Lambda.AppSyncEvents/AppSyncCognitoIdentity.cs new file mode 100644 index 000000000..2b048cc81 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.AppSyncEvents/AppSyncCognitoIdentity.cs @@ -0,0 +1,42 @@ +namespace Amazon.Lambda.AppSyncEvents; + +/// +/// Represents Amazon Cognito User Pools authorization identity for AppSync +/// +public class AppSyncCognitoIdentity +{ + /// + /// The source IP address of the caller received by AWS AppSync + /// + public List SourceIp { get; set; } + + /// + /// The username of the authenticated user + /// + public string Username { get; set; } + + /// + /// The UUID of the authenticated user + /// + public string Sub { get; set; } + + /// + /// The claims that the user has + /// + public Dictionary Claims { get; set; } + + /// + /// The default authorization strategy for this caller (ALLOW or DENY) + /// + public string DefaultAuthStrategy { get; set; } + + /// + /// List of OIDC groups + /// + public List Groups { get; set; } + + /// + /// The token issuer + /// + public string Issuer { get; set; } +} diff --git a/Libraries/src/Amazon.Lambda.AppSyncEvents/AppSyncIamIdentity.cs b/Libraries/src/Amazon.Lambda.AppSyncEvents/AppSyncIamIdentity.cs new file mode 100644 index 000000000..a22e82430 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.AppSyncEvents/AppSyncIamIdentity.cs @@ -0,0 +1,47 @@ +namespace Amazon.Lambda.AppSyncEvents; + +/// +/// Represents AWS IAM authorization identity for AppSync +/// +public class AppSyncIamIdentity +{ + /// + /// The source IP address of the caller received by AWS AppSync + /// + public List SourceIp { get; set; } + + /// + /// The username of the authenticated user (IAM user principal) + /// + public string Username { get; set; } + + /// + /// The AWS account ID of the caller + /// + public string AccountId { get; set; } + + /// + /// The Amazon Cognito identity pool ID associated with the caller + /// + public string CognitoIdentityPoolId { get; set; } + + /// + /// The Amazon Cognito identity ID of the caller + /// + public string CognitoIdentityId { get; set; } + + /// + /// The ARN of the IAM user + /// + public string UserArn { get; set; } + + /// + /// Either authenticated or unauthenticated based on the identity type + /// + public string CognitoIdentityAuthType { get; set; } + + /// + /// A comma separated list of external identity provider information used in obtaining the credentials used to sign the request + /// + public string CognitoIdentityAuthProvider { get; set; } +} diff --git a/Libraries/src/Amazon.Lambda.AppSyncEvents/AppSyncLambdaIdentity.cs b/Libraries/src/Amazon.Lambda.AppSyncEvents/AppSyncLambdaIdentity.cs new file mode 100644 index 000000000..b55a8645b --- /dev/null +++ b/Libraries/src/Amazon.Lambda.AppSyncEvents/AppSyncLambdaIdentity.cs @@ -0,0 +1,13 @@ +namespace Amazon.Lambda.AppSyncEvents; + +/// +/// Represents AWS Lambda authorization identity for AppSync +/// +public class AppSyncLambdaIdentity +{ + /// + /// Optional context information that will be passed to subsequent resolvers + /// Can contain user information, claims, or any other contextual data + /// + public Dictionary ResolverContext { get; set; } +} diff --git a/Libraries/src/Amazon.Lambda.AppSyncEvents/AppSyncOidcIdentity.cs b/Libraries/src/Amazon.Lambda.AppSyncEvents/AppSyncOidcIdentity.cs new file mode 100644 index 000000000..f1023c286 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.AppSyncEvents/AppSyncOidcIdentity.cs @@ -0,0 +1,22 @@ +namespace Amazon.Lambda.AppSyncEvents; + +/// +/// Represents OpenID Connect authorization identity for AppSync +/// +public class AppSyncOidcIdentity +{ + /// + /// Claims from the OIDC token as key-value pairs + /// + public Dictionary Claims { get; set; } + + /// + /// The issuer of the OIDC token + /// + public string Issuer { get; set; } + + /// + /// The UUID of the authenticated user + /// + public string Sub { get; set; } +} diff --git a/Libraries/src/Amazon.Lambda.AppSyncEvents/AppSyncResolverEvent.cs b/Libraries/src/Amazon.Lambda.AppSyncEvents/AppSyncResolverEvent.cs new file mode 100644 index 000000000..c650be02d --- /dev/null +++ b/Libraries/src/Amazon.Lambda.AppSyncEvents/AppSyncResolverEvent.cs @@ -0,0 +1,101 @@ +namespace Amazon.Lambda.AppSyncEvents; + +/// +/// Represents the event payload received from AWS AppSync. +/// +public class AppSyncResolverEvent +{ + /// + /// Gets or sets the input arguments for the GraphQL operation. + /// + public TArguments Arguments { get; set; } + + /// + /// An object that contains information about the caller. + /// Returns null for API_KEY authorization. + /// Returns AppSyncIamIdentity for AWS_IAM authorization. + /// Returns AppSyncCognitoIdentity for AMAZON_COGNITO_USER_POOLS authorization. + /// For AWS_LAMBDA authorization, returns the object returned by your Lambda authorizer function. + /// + /// + /// The Identity object type depends on the authorization mode: + /// - For API_KEY: null + /// - For AWS_IAM: + /// - For AMAZON_COGNITO_USER_POOLS: + /// - For AWS_LAMBDA: + /// - For OPENID_CONNECT: + /// + public object Identity { get; set; } + + /// + /// Gets or sets information about the data source that originated the event. + /// + public object Source { get; set; } + + /// + /// Gets or sets information about the HTTP request that triggered the event. + /// + public RequestContext Request { get; set; } + + /// + /// Gets or sets information about the previous state of the data before the operation was executed. + /// + public object Prev { get; set; } + + /// + /// Gets or sets information about the GraphQL operation being executed. + /// + public Information Info { get; set; } + + /// + /// Gets or sets additional information that can be passed between Lambda functions during an AppSync pipeline. + /// + public Dictionary Stash { get; set; } +} + +/// +/// Represents information about the HTTP request that triggered the event. +/// +public class RequestContext +{ + /// + /// Gets or sets the headers of the HTTP request. + /// + public Dictionary Headers { get; set; } + + /// + /// Gets or sets the domain name associated with the request. + /// + public string DomainName { get; set; } +} + +/// +/// Represents information about the GraphQL operation being executed. +/// +public class Information +{ + /// + /// Gets or sets the name of the GraphQL field being executed. + /// + public string FieldName { get; set; } + + /// + /// Gets or sets a list of fields being selected in the GraphQL operation. + /// + public List SelectionSetList { get; set; } + + /// + /// Gets or sets the GraphQL selection set for the operation. + /// + public string SelectionSetGraphQL { get; set; } + + /// + /// Gets or sets the variables passed to the GraphQL operation. + /// + public Dictionary Variables { get; set; } + + /// + /// Gets or sets the parent type name for the GraphQL operation. + /// + public string ParentTypeName { get; set; } +} diff --git a/Libraries/src/Amazon.Lambda.AppSyncEvents/README.md b/Libraries/src/Amazon.Lambda.AppSyncEvents/README.md new file mode 100644 index 000000000..c883e32cd --- /dev/null +++ b/Libraries/src/Amazon.Lambda.AppSyncEvents/README.md @@ -0,0 +1,72 @@ +# Amazon.Lambda.AppSyncEvents + +This package contains classes that can be used as input types for Lambda functions that process AppSync events. + +## Sample Function + +The following is a sample class and Lambda function that receives AppSync resolver event record data as an `appSyncResolverEvent` and logs some of the incoming event data. (Note that by default anything written to Console will be logged as CloudWatch Logs events.) + +```csharp +public void Handler(AppSyncResolverEvent> appSyncResolverEvent, ILambdaContext context) +{ + foreach (var item in appSyncResolverEvent.Arguments) + { + Console.WriteLine($"AppSync request key - {item.Key}."); + } + + if (appSyncResolverEvent.Identity != null) + { + // Create an instance of the serializer + var lambdaSerializer = new DefaultLambdaJsonSerializer(); + + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(appSyncResolverEvent.Identity.ToString()!))) + { + // When using AMAZON_COGNITO_USER_POOLS authorization + var cognitoIdentity = lambdaSerializer.Deserialize(stream); + + // When using AWS_IAM authorization + var iamIdentity = lambdaSerializer.Deserialize(stream); + + // When using AWS_LAMBDA authorization + var lambdaIdentity = lambdaSerializer.Deserialize(stream); + + // When using OPENID_CONNECT authorization + var oidcIdentity = lambdaSerializer.Deserialize(stream); + } + } +} +``` + +## Example of Custom Lambda Authorizer +This example demonstrates how to implement a custom Lambda authorizer for AppSync using the AppSync Events package. The authorizer function receives an `AppSyncAuthorizerEvent` containing the authorization token and request context. It returns an `AppSyncAuthorizerResult` that determines whether the request is authorized and includes additional context. + +The function provides contextual data through the `ResolverContext` property of the `AppSyncAuthorizerResult` instance. This information can be accessed via the `Identity` property of the `AppSyncResolverEvent` instance. Since the `Identity` property is of type `object`, you can deserialize it to `AppSyncLambdaIdentity` (as shown above) to get strong typing support. + + +```csharp +public async Task CustomLambdaAuthorizerHandler(AppSyncAuthorizerEvent appSyncAuthorizerEvent) +{ + var authorizationToken = appSyncAuthorizerEvent.AuthorizationToken; + var apiId = appSyncAuthorizerEvent.RequestContext.ApiId; + var accountId = appSyncAuthorizerEvent.RequestContext.AccountId; + + var response = new AppSyncAuthorizerResult + { + IsAuthorized = authorizationToken == "custom-authorized", + ResolverContext = new Dictionary + { + { "userid", "test-user-id" }, + { "info", "contextual information A" }, + { "more_info", "contextual information B" } + }, + DeniedFields = new List + { + $"arn:aws:appsync:{Environment.GetEnvironmentVariable("AWS_REGION")}:{accountId}:apis/{apiId}/types/Event/fields/comments", + "Mutation.createEvent" + }, + TtlOverride = 10 + }; + + return response; +} +``` \ No newline at end of file diff --git a/Libraries/test/EventsTests.NET6/EventsTests.NET6.csproj b/Libraries/test/EventsTests.NET6/EventsTests.NET6.csproj index af60ce665..5e64d7c09 100644 --- a/Libraries/test/EventsTests.NET6/EventsTests.NET6.csproj +++ b/Libraries/test/EventsTests.NET6/EventsTests.NET6.csproj @@ -1,7 +1,7 @@  - net6.0;net8.0 + net6.0 EventsTests31 true EventsTests.NET6 diff --git a/Libraries/test/EventsTests.NET8/AppSyncEventTests.cs b/Libraries/test/EventsTests.NET8/AppSyncEventTests.cs new file mode 100644 index 000000000..4c927f1fd --- /dev/null +++ b/Libraries/test/EventsTests.NET8/AppSyncEventTests.cs @@ -0,0 +1,287 @@ + +#pragma warning disable 618 +namespace Amazon.Lambda.Tests; + +using Amazon.Lambda.AppSyncEvents; +using Amazon.Lambda.Core; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Xunit; + +public class AppSyncEventTests +{ + // This utility method takes care of removing the BOM that System.Text.Json doesn't like. + public MemoryStream LoadJsonTestFile(string filename) + { + var json = File.ReadAllText(filename); + return new MemoryStream(UTF8Encoding.UTF8.GetBytes(json)); + } + + public string SerializeJson(ILambdaSerializer serializer, T response) + { + string serializedJson; + using (MemoryStream stream = new MemoryStream()) + { + serializer.Serialize(response, stream); + + stream.Position = 0; + serializedJson = Encoding.UTF8.GetString(stream.ToArray()); + } + return serializedJson; + } + + [Theory] + [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] + [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] + public void AppSyncTest(Type serializerType) + { + var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; + using (var fileStream = LoadJsonTestFile("appsync-event.json")) + { + var appSyncEvent = serializer.Deserialize>>(fileStream); + Assert.NotNull(appSyncEvent); + Assert.NotNull(appSyncEvent.Arguments); + Assert.NotNull(appSyncEvent.Arguments["input"]); + + Assert.NotNull(appSyncEvent.Request); + Assert.NotNull(appSyncEvent.Request.Headers); + var headers = appSyncEvent.Request.Headers; + Assert.Equal("value1", headers["key1"]); + Assert.Equal("value2", headers["key2"]); + + Assert.NotNull(appSyncEvent.Info); + Assert.Equal("openSupportTicket", appSyncEvent.Info.FieldName); + Assert.Equal("Mutation", appSyncEvent.Info.ParentTypeName); + + Assert.NotNull(appSyncEvent.Info.SelectionSetList); + Assert.Equal(6, appSyncEvent.Info.SelectionSetList.Count); + Assert.Contains("ticketId", appSyncEvent.Info.SelectionSetList); + Assert.Contains("status", appSyncEvent.Info.SelectionSetList); + Assert.Contains("title", appSyncEvent.Info.SelectionSetList); + Assert.Contains("description", appSyncEvent.Info.SelectionSetList); + Assert.Contains("createdAt", appSyncEvent.Info.SelectionSetList); + Assert.Contains("updatedAt", appSyncEvent.Info.SelectionSetList); + + Assert.NotNull(appSyncEvent.Info.SelectionSetGraphQL); + Assert.NotNull(appSyncEvent.Info.Variables); + Assert.NotNull(appSyncEvent.Info.Variables["input"]); + + Assert.NotNull(appSyncEvent.Stash); + Assert.Empty(appSyncEvent.Stash); + } + } + + [Theory] + [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] + [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] + public void AppSyncTestCognitoAuthorizer(Type serializerType) + { + var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; + using (var fileStream = LoadJsonTestFile("appsync-event-cognito-authorizer.json")) + { + var request = serializer.Deserialize>>(fileStream); + + Assert.NotNull(request.Identity); + + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(request.Identity.ToString()))) + { + var identity = serializer.Deserialize(stream); + Assert.NotNull(identity); + + // Claims + Assert.NotNull(identity.Claims); + Assert.True(identity.Claims.ContainsKey("client_id")); + Assert.True(identity.Claims.ContainsKey("scope")); + Assert.True(identity.Claims.ContainsKey("sub")); + Assert.True(identity.Claims.ContainsKey("token_use")); + + // DefaultAuthStrategy + Assert.NotEmpty(identity.DefaultAuthStrategy); + + // Groups + Assert.NotNull(identity.Groups); + Assert.NotEmpty(identity.Groups); + + // Issuer + Assert.NotEmpty(identity.Issuer); + + // SourceIp + Assert.NotNull(identity.SourceIp); + Assert.NotEmpty(identity.SourceIp); + + // Sub + Assert.NotEmpty(identity.Sub); + + // Username + Assert.NotEmpty(identity.Username); + } + } + } + + [Theory] + [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] + [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] + public void AppSyncTestIAMAuthorizer(Type serializerType) + { + var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; + using (var fileStream = LoadJsonTestFile("appsync-event-iam-authorizer.json")) + { + var request = serializer.Deserialize>>(fileStream); + + Assert.NotNull(request.Identity); + + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(request.Identity.ToString()))) + { + var identity = serializer.Deserialize(stream); + Assert.NotNull(identity); + + // AccountId + Assert.NotEmpty(identity.AccountId); + + // CognitoIdentityAuthProvider + Assert.NotEmpty(identity.CognitoIdentityAuthProvider); + + // CognitoIdentityAuthType + Assert.NotEmpty(identity.CognitoIdentityAuthType); + + // CognitoIdentityId + Assert.NotEmpty(identity.CognitoIdentityId); + + // CognitoIdentityPoolId + Assert.NotEmpty(identity.CognitoIdentityPoolId); + + // SourceIp + Assert.NotNull(identity.SourceIp); + Assert.NotEmpty(identity.SourceIp); + + // UserArn + Assert.NotEmpty(identity.UserArn); + + // Username + Assert.NotEmpty(identity.Username); + } + } + } + + [Theory] + [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] + [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] + public void AppSyncTestLambdaAuthorizer(Type serializerType) + { + var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; + using (var fileStream = LoadJsonTestFile("appsync-event-lambda-authorizer.json")) + { + var request = serializer.Deserialize>>(fileStream); + + Assert.NotNull(request.Identity); + + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(request.Identity.ToString()))) + { + var identity = serializer.Deserialize(stream); + Assert.NotNull(identity); + + // ResolverContext + Assert.NotNull(identity.ResolverContext); + Assert.NotEmpty(identity.ResolverContext["userid"]); + Assert.NotEmpty(identity.ResolverContext["info"]); + Assert.NotEmpty(identity.ResolverContext["more_info"]); + } + } + } + + [Theory] + [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] + [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] + public void AppSyncTestOidcAuthorizer(Type serializerType) + { + var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; + using (var fileStream = LoadJsonTestFile("appsync-event-oidc-authorizer.json")) + { + var request = serializer.Deserialize>>(fileStream); + + Assert.NotNull(request.Identity); + + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(request.Identity.ToString()))) + { + var identity = serializer.Deserialize(stream); + Assert.NotNull(identity); + + // Claims + Assert.NotNull(identity.Claims); + Assert.True(identity.Claims.ContainsKey("client_id")); + + // Issuer + Assert.NotEmpty(identity.Issuer); + + // Sub + Assert.NotEmpty(identity.Sub); + } + } + } + + [Theory] + [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] + [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] + public void AppSyncTestLambdaAuthorizerRequestEvent(Type serializerType) + { + var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; + using (var fileStream = LoadJsonTestFile("appsync-event-lambda-authorizer-request.json")) + { + var request = serializer.Deserialize(fileStream); + + // Assert Authorization Token + Assert.Equal("custom-token", request.AuthorizationToken); + + // Assert Request Context + Assert.NotNull(request.RequestContext); + Assert.Equal("xxxxxxxx", request.RequestContext.ApiId); + Assert.Equal("112233445566", request.RequestContext.AccountId); + Assert.Equal("36307622-97fe-4dfa-bd71-b15b1d03ce97", request.RequestContext.RequestId); + Assert.Equal("MyQuery", request.RequestContext.OperationName); + Assert.NotNull(request.RequestContext.Variables); + Assert.Empty(request.RequestContext.Variables); + Assert.Contains("listTodos", request.RequestContext.QueryString); + + // Assert Request Headers + Assert.NotNull(request.RequestHeaders); + Assert.Equal("This is test token", request.RequestHeaders["authorization"]); + Assert.Equal("application/json", request.RequestHeaders["content-type"]); + Assert.Equal("https://ap-south-1.console.aws.amazon.com", request.RequestHeaders["origin"]); + } + } + + [Theory] + [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] + [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] + public void AppSyncTestLambdaAuthorizerResponseEvent(Type serializerType) + { + var response = new AppSyncAuthorizerResult + { + IsAuthorized = true, + ResolverContext = new Dictionary + { + { "userid", "test-user-id" }, + { "info", "contextual information A" }, + { "more_info", "contextual information B" } + }, + DeniedFields = new List + { + "arn:aws:appsync:us-east-1:1234567890:apis/xxxxxx/types/Event/fields/comments", + "Mutation.createEvent" + }, + TtlOverride = 10 + }; + + var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; + var json = SerializeJson(serializer, response); + var actualObject = JObject.Parse(json); + var expectedJObject = JObject.Parse(File.ReadAllText("appsync-event-lambda-authorizer-response.json")); + + Assert.True(JToken.DeepEquals(actualObject, expectedJObject)); + } +} + +#pragma warning restore 618 diff --git a/Libraries/test/EventsTests.NET8/EventsTests.NET8.csproj b/Libraries/test/EventsTests.NET8/EventsTests.NET8.csproj new file mode 100644 index 000000000..db86179bf --- /dev/null +++ b/Libraries/test/EventsTests.NET8/EventsTests.NET8.csproj @@ -0,0 +1,66 @@ + + + + net8.0 + EventsTests31 + true + EventsTests.NET8 + latest + + + + + + + PreserveNewest + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/Libraries/test/EventsTests.Shared/EventsTests.Shared.projitems b/Libraries/test/EventsTests.Shared/EventsTests.Shared.projitems index 4fdc41123..b424aa506 100644 --- a/Libraries/test/EventsTests.Shared/EventsTests.Shared.projitems +++ b/Libraries/test/EventsTests.Shared/EventsTests.Shared.projitems @@ -12,6 +12,13 @@ + + + + + + + diff --git a/Libraries/test/EventsTests.Shared/appsync-event-cognito-authorizer.json b/Libraries/test/EventsTests.Shared/appsync-event-cognito-authorizer.json new file mode 100644 index 000000000..0c4805d02 --- /dev/null +++ b/Libraries/test/EventsTests.Shared/appsync-event-cognito-authorizer.json @@ -0,0 +1,68 @@ +{ + "arguments": { + "input": { + "title": "Support Ticket Test", + "description": "Support Ticket Test" + } + }, + "identity": { + "claims": { + "sub": "b662d2e4-d0a1-7098-4973-20a5d6ff5a12", + "cognito:groups": [ + "admin" + ], + "iss": "https://cognito-idp.eu-west-2.amazonaws.com/eu-west-2_xxxxxx", + "version": 2, + "client_id": "458vq6091kg88r4qurfnxxxxx", + "origin_jti": "6102131f-89b2-4909-80bc-200ccf675892", + "event_id": "373cdbc2-5460-4dff-bebb-3cba1de69777", + "token_use": "access", + "scope": "aws.cognito.signin.user.admin openid profile email", + "auth_time": 1737358577, + "exp": 1737362177, + "iat": 1737358577, + "jti": "821f10f1-9a6c-46bd-b1d6-62e266d6639b", + "username": "b662d2e4-d0a1-7098-4973-20a5d6ff5a12" + }, + "defaultAuthStrategy": "ALLOW", + "groups": [ + "admin" + ], + "issuer": "https://cognito-idp.eu-west-2.amazonaws.com/eu-west-2_xxxxxx", + "sourceIp": [ + "192.168.234.157" + ], + "sub": "b662d2e4-d0a1-7098-4973-20a5d6ff5a12", + "username": "b662d2e4-d0a1-7098-4973-20a5d6ff5a12" + }, + "source": null, + "request": { + "headers": { + "key1": "value1", + "key2": "value2" + }, + "domainName": null + }, + "prev": null, + "info": { + "fieldName": "openSupportTicket", + "selectionSetList": [ + "ticketId", + "status", + "title", + "description", + "createdAt", + "updatedAt", + "__typename" + ], + "selectionSetGraphQL": "{\n ticketId\n status\n title\n description\n createdAt\n updatedAt\n __typename\n}", + "variables": { + "input": { + "title": "Support Ticket Test", + "description": "Support Ticket Test" + } + }, + "parentTypeName": "Mutation" + }, + "stash": {} +} \ No newline at end of file diff --git a/Libraries/test/EventsTests.Shared/appsync-event-iam-authorizer.json b/Libraries/test/EventsTests.Shared/appsync-event-iam-authorizer.json new file mode 100644 index 000000000..f26e60b7c --- /dev/null +++ b/Libraries/test/EventsTests.Shared/appsync-event-iam-authorizer.json @@ -0,0 +1,48 @@ +{ + "arguments": { + "input": { + "title": "Support Ticket Test", + "description": "Support Ticket Test" + } + }, + "identity": { + "accountId": "123456789012", + "cognitoIdentityPoolId": "us-east-1:1234abcd-1234-5678-abcd-1234567890ab", + "cognitoIdentityId": "us-east-1:1234abcd-1234-5678-abcd-1234567890ab", + "sourceIp": [ "192.0.2.1" ], + "username": "IAMUser", + "userArn": "arn:aws:iam::123456789012:user/IAMUser", + "cognitoIdentityAuthType": "authenticated", + "cognitoIdentityAuthProvider": "cognito-identity.amazonaws.com" + }, + "source": null, + "request": { + "headers": { + "key1": "value1", + "key2": "value2" + }, + "domainName": null + }, + "prev": null, + "info": { + "fieldName": "openSupportTicket", + "selectionSetList": [ + "ticketId", + "status", + "title", + "description", + "createdAt", + "updatedAt", + "__typename" + ], + "selectionSetGraphQL": "{\n ticketId\n status\n title\n description\n createdAt\n updatedAt\n __typename\n}", + "variables": { + "input": { + "title": "Support Ticket Test", + "description": "Support Ticket Test" + } + }, + "parentTypeName": "Mutation" + }, + "stash": {} +} \ No newline at end of file diff --git a/Libraries/test/EventsTests.Shared/appsync-event-lambda-authorizer-request.json b/Libraries/test/EventsTests.Shared/appsync-event-lambda-authorizer-request.json new file mode 100644 index 000000000..47648175f --- /dev/null +++ b/Libraries/test/EventsTests.Shared/appsync-event-lambda-authorizer-request.json @@ -0,0 +1,35 @@ +{ + "authorizationToken": "custom-token", + "requestContext": { + "apiId": "xxxxxxxx", + "accountId": "112233445566", + "requestId": "36307622-97fe-4dfa-bd71-b15b1d03ce97", + "queryString": "query MyQuery {\n listTodos {\n completed\n createdAt\n description\n id\n title\n updatedAt\n }\n}\n", + "operationName": "MyQuery", + "variables": {} + }, + "requestHeaders": { + "x-forwarded-for": "182.70.233.9, 15.158.25.215", + "accept-encoding": "gzip, deflate, br, zstd", + "sec-ch-ua-mobile": "?0", + "referer": "https://ap-south-1.console.aws.amazon.com/", + "via": "2.0 30c122bd8d8efabc1fd1b3b11bfb53ea.cloudfront.net (CloudFront)", + "content-type": "application/json", + "origin": "https://ap-south-1.console.aws.amazon.com", + "sec-fetch-mode": "cors", + "authorization": "This is test token", + "sec-fetch-dest": "empty", + "content-length": "159", + "x-amz-user-agent": "AWS-Console-AppSync/", + "sec-ch-ua-platform": "\"macOS\"", + "x-forwarded-proto": "https", + "accept-language": "en-US,en;q=0.9", + "host": "cndjdbaxrfhzppcdhvfvx3a7zq.appsync-api.ap-south-1.amazonaws.com", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", + "sec-fetch-site": "cross-site", + "accept": "application/json, text/plain, */*", + "priority": "u=1, i", + "sec-ch-ua": "\"Chromium\";v=\"134\", \"Not:A-Brand\";v=\"24\", \"Google Chrome\";v=\"134\"", + "x-forwarded-port": "443" + } + } \ No newline at end of file diff --git a/Libraries/test/EventsTests.Shared/appsync-event-lambda-authorizer-response.json b/Libraries/test/EventsTests.Shared/appsync-event-lambda-authorizer-response.json new file mode 100644 index 000000000..6f406ad5f --- /dev/null +++ b/Libraries/test/EventsTests.Shared/appsync-event-lambda-authorizer-response.json @@ -0,0 +1,13 @@ +{ + "isAuthorized": true, + "resolverContext": { + "userid": "test-user-id", + "info": "contextual information A", + "more_info": "contextual information B" + }, + "deniedFields": [ + "arn:aws:appsync:us-east-1:1234567890:apis/xxxxxx/types/Event/fields/comments", + "Mutation.createEvent" + ], + "ttlOverride": 10 + } \ No newline at end of file diff --git a/Libraries/test/EventsTests.Shared/appsync-event-lambda-authorizer.json b/Libraries/test/EventsTests.Shared/appsync-event-lambda-authorizer.json new file mode 100644 index 000000000..d6fcf6a0c --- /dev/null +++ b/Libraries/test/EventsTests.Shared/appsync-event-lambda-authorizer.json @@ -0,0 +1,45 @@ +{ + "arguments": { + "input": { + "title": "Support Ticket Test", + "description": "Support Ticket Test" + } + }, + "identity": { + "resolverContext": { + "userid": "test-user-id", + "info": "contextual information A", + "more_info": "contextual information B" + } + }, + "source": null, + "request": { + "headers": { + "key1": "value1", + "key2": "value2" + }, + "domainName": null + }, + "prev": null, + "info": { + "fieldName": "openSupportTicket", + "selectionSetList": [ + "ticketId", + "status", + "title", + "description", + "createdAt", + "updatedAt", + "__typename" + ], + "selectionSetGraphQL": "{\n ticketId\n status\n title\n description\n createdAt\n updatedAt\n __typename\n}", + "variables": { + "input": { + "title": "Support Ticket Test", + "description": "Support Ticket Test" + } + }, + "parentTypeName": "Mutation" + }, + "stash": {} +} \ No newline at end of file diff --git a/Libraries/test/EventsTests.Shared/appsync-event-oidc-authorizer.json b/Libraries/test/EventsTests.Shared/appsync-event-oidc-authorizer.json new file mode 100644 index 000000000..c4d50868e --- /dev/null +++ b/Libraries/test/EventsTests.Shared/appsync-event-oidc-authorizer.json @@ -0,0 +1,51 @@ +{ + "arguments": { + "input": { + "title": "Support Ticket Test", + "description": "Support Ticket Test" + } + }, + "identity": { + "claims": { + "client_id": "458vq6091kg88r4qurfnxxxxx", + "event_id": "373cdbc2-5460-4dff-bebb-3cba1de69777", + "username": "b662d2e4-d0a1-7098-4973-20a5d6ff5a12" + }, + "defaultAuthStrategy": "ALLOW", + "groups": [ + "admin" + ], + "issuer": "https://cognito-idp.eu-west-2.amazonaws.com/eu-west-2_xxxxxx", + "sub": "b662d2e4-d0a1-7098-4973-20a5d6ff5a12" + }, + "source": null, + "request": { + "headers": { + "key1": "value1", + "key2": "value2" + }, + "domainName": null + }, + "prev": null, + "info": { + "fieldName": "openSupportTicket", + "selectionSetList": [ + "ticketId", + "status", + "title", + "description", + "createdAt", + "updatedAt", + "__typename" + ], + "selectionSetGraphQL": "{\n ticketId\n status\n title\n description\n createdAt\n updatedAt\n __typename\n}", + "variables": { + "input": { + "title": "Support Ticket Test", + "description": "Support Ticket Test" + } + }, + "parentTypeName": "Mutation" + }, + "stash": {} +} \ No newline at end of file diff --git a/Libraries/test/EventsTests.Shared/appsync-event.json b/Libraries/test/EventsTests.Shared/appsync-event.json new file mode 100644 index 000000000..224f5cb05 --- /dev/null +++ b/Libraries/test/EventsTests.Shared/appsync-event.json @@ -0,0 +1,38 @@ +{ + "arguments": { + "input": { + "title": "Support Ticket Test", + "description": "Support Ticket Test" + } + }, + "identity": null, + "source": null, + "request": { + "headers": { + "key1": "value1", + "key2": "value2" + }, + "domainName": null + }, + "prev": null, + "info": { + "fieldName": "openSupportTicket", + "selectionSetList": [ + "ticketId", + "status", + "title", + "description", + "createdAt", + "updatedAt" + ], + "selectionSetGraphQL": "{\n ticketId\n status\n title\n description\n createdAt\n updatedAt\n }", + "variables": { + "input": { + "title": "Support Ticket Test", + "description": "Support Ticket Test" + } + }, + "parentTypeName": "Mutation" + }, + "stash": {} +} \ No newline at end of file diff --git a/README.md b/README.md index f5ce22798..37871d167 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ These are the packages and their README.md files: * [Amazon.Lambda.SNSEvents](Libraries/src/Amazon.Lambda.SNSEvents) - [README.md](Libraries/src/Amazon.Lambda.SNSEvents/README.md) * [Amazon.Lambda.SQSEvents](Libraries/src/Amazon.Lambda.SQSEvents) - [README.md](Libraries/src/Amazon.Lambda.SQSEvents/README.md) * [Amazon.Lambda.KafkaEvents](Libraries/src/Amazon.Lambda.KafkaEvents) - [README.md](Libraries/src/Amazon.Lambda.KafkaEvents/README.md) +* [Amazon.Lambda.AppSyncEvents](Libraries/src/Amazon.Lambda.AppSyncEvents) - [README.md](Libraries/src/Amazon.Lambda.AppSyncEvents/README.md) ### Amazon.Lambda.Tools