From 16106ca5ebd0c43089c269927902c20fac665194 Mon Sep 17 00:00:00 2001 From: Ikuma Yamashita Date: Sat, 12 Jul 2025 04:42:22 +0900 Subject: [PATCH 1/5] feat(event): add AppSyncResolverEvent structure and example payload --- lambda-events/src/event/appsync/mod.rs | 94 +++++++++++++++++++ .../example-appsync-vtl-resolver.json | 64 +++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 lambda-events/src/fixtures/example-appsync-vtl-resolver.json diff --git a/lambda-events/src/event/appsync/mod.rs b/lambda-events/src/event/appsync/mod.rs index 32bf9f79..d1643502 100644 --- a/lambda-events/src/event/appsync/mod.rs +++ b/lambda-events/src/event/appsync/mod.rs @@ -61,6 +61,8 @@ where pub source_ip: Vec, #[serde(default)] pub default_auth_strategy: Option, + #[serde(default)] + pub groups: Option>, } pub type AppSyncOperation = String; @@ -117,6 +119,88 @@ where pub ttl_override: Option, } +/// `AppSyncResolverEvent` represents the default payload structure sent by AWS AppSync +/// when using **Direct Lambda Resolvers** (i.e., when both request and response mapping +/// templates are disabled). +/// +/// This structure includes the full AppSync **Context object**, as described in the +/// [AppSync Direct Lambda resolver reference](https://docs.aws.amazon.com/appsync/latest/devguide/direct-lambda-reference.html). +/// +/// It is recommended when working without VTL templates and relying on the standard +/// AppSync-to-Lambda event format. +/// +/// See also: +/// - [AppSync resolver mapping template context reference](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html) +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +pub struct AppSyncResolverEvent, TSource = Option> { + pub arguments: TArguments, + pub identity: Option, + pub source: TSource, + pub request: AppSyncRequest, + pub info: AppSyncInfo, + #[serde(default)] + pub prev: Option, + pub stash: HashMap, +} + +/// `AppSyncRequest` contains request-related metadata for a resolver invocation, +/// including client-sent headers and optional custom domain name. +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AppSyncRequest { + #[serde(default)] + pub headers: HashMap>, + #[serde(default)] + pub domain_name: Option, +} + +/// `AppSyncInfo` contains metadata about the current GraphQL field being resolved. +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AppSyncInfo { + #[serde(default)] + pub selection_set_list: Vec, + #[serde(rename = "selectionSetGraphQL")] + pub selection_set_graphql: String, + pub parent_type_name: String, + pub field_name: String, + #[serde(default)] + pub variables: HashMap, +} + +/// `AppSyncPrevResult` contains the result of the previous step in a pipeline resolver. +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +pub struct AppSyncPrevResult { + #[serde(default)] + pub result: HashMap, +} + +/// `AppSyncIdentity` represents the identity of the caller as determined by the +/// configured AppSync authorization mechanism (IAM, Cognito, OIDC, or Lambda). +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(untagged, rename_all = "camelCase")] +pub enum AppSyncIdentity { + IAM(AppSyncIamIdentity), + Cognito(AppSyncCognitoIdentity), + OIDC(AppSyncIdentityOIDC), + Lambda(AppSyncIdentityLambda), +} + +/// `AppSyncIdentityOIDC` represents identity information when using OIDC-based authorization. +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +pub struct AppSyncIdentityOIDC { + pub claims: Value, + pub issuer: String, + pub sub: String, +} + +/// `AppSyncIdentityLambda` represents identity information when using AWS Lambda +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AppSyncIdentityLambda { + pub resolver_context: Value, +} + #[cfg(test)] mod test { use super::*; @@ -160,4 +244,14 @@ mod test { let reparsed: AppSyncLambdaAuthorizerResponse = serde_json::from_slice(output.as_bytes()).unwrap(); assert_eq!(parsed, reparsed); } + + #[test] + #[cfg(feature = "appsync")] + fn example_appsync_vtl_resolver() { + let data = include_bytes!("../../fixtures/example-appsync-vtl-resolver.json"); + let parsed: AppSyncResolverEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: AppSyncResolverEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } } diff --git a/lambda-events/src/fixtures/example-appsync-vtl-resolver.json b/lambda-events/src/fixtures/example-appsync-vtl-resolver.json new file mode 100644 index 00000000..9a804876 --- /dev/null +++ b/lambda-events/src/fixtures/example-appsync-vtl-resolver.json @@ -0,0 +1,64 @@ +{ + "arguments": { + "input": "foo" + }, + "identity": { + "sourceIp": [ + "x.x.x.x" + ], + "userArn": "arn:aws:iam::123456789012:user/appsync", + "accountId": "666666666666", + "user": "AIDAAAAAAAAAAAAAAAAAA" + }, + "info": { + "fieldName": "greet", + "parentTypeName": "Query", + "selectionSetGraphQL": "", + "selectionSetList": [], + "variables": { + "inputVar": "foo" + } + }, + "prev": null, + "request": { + "domainName": null, + "headers": { + "accept": "application/json, text/plain, */*", + "accept-encoding": "gzip, deflate, br, zstd", + "accept-language": "en-US,en;q=0.9,ja;q=0.8,en-GB;q=0.7", + "cloudfront-forwarded-proto": "https", + "cloudfront-is-desktop-viewer": "true", + "cloudfront-is-mobile-viewer": "false", + "cloudfront-is-smarttv-viewer": "false", + "cloudfront-is-tablet-viewer": "false", + "cloudfront-viewer-asn": "17676", + "cloudfront-viewer-country": "JP", + "content-length": "40", + "content-type": "application/json", + "host": "2ojpkjk2ejb57l7stgad5o4qiq.appsync-api.ap-northeast-1.amazonaws.com", + "origin": "https://ap-northeast-1.console.aws.amazon.com", + "priority": "u=1, i", + "referer": "https://ap-northeast-1.console.aws.amazon.com/", + "sec-ch-ua": "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\", \"Microsoft Edge\";v=\"138\"", + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": "\"Windows\"", + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "cross-site", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0", + "via": "2.0 ee337d4db5c7ebfdc8ec0798a1ede776.cloudfront.net (CloudFront)", + "x-amz-cf-id": "O3ZflUCq6_TzxjouyYB3zg7-kl7Ze-gXbniM2jJ3hAOfDFpPMGRu3Q==", + "x-amz-user-agent": "AWS-Console-AppSync/", + "x-amzn-appsync-is-vpce-request": "false", + "x-amzn-remote-ip": "x.x.x.x", + "x-amzn-requestid": "7ada8740-bbf4-49e8-bf45-f10b3d67159b", + "x-amzn-trace-id": "Root=1-68713e21-7a03739120ad60703e794b22", + "x-api-key": "***", + "x-forwarded-for": "***", + "x-forwarded-port": "443", + "x-forwarded-proto": "https" + } + }, + "source": null, + "stash": {} +} \ No newline at end of file From 582ddecc04d109cb01c4fd3adb66b4160c35ea48 Mon Sep 17 00:00:00 2001 From: Ikuma Yamashita Date: Sat, 12 Jul 2025 05:14:10 +0900 Subject: [PATCH 2/5] fix: remove `groups` field from AppSyncCognitoIdentity to avoid semver break --- lambda-events/src/event/appsync/mod.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/lambda-events/src/event/appsync/mod.rs b/lambda-events/src/event/appsync/mod.rs index d1643502..c15616d5 100644 --- a/lambda-events/src/event/appsync/mod.rs +++ b/lambda-events/src/event/appsync/mod.rs @@ -61,8 +61,6 @@ where pub source_ip: Vec, #[serde(default)] pub default_auth_strategy: Option, - #[serde(default)] - pub groups: Option>, } pub type AppSyncOperation = String; From ee5290d8da75419d554128118fc3aa4aea6aba1e Mon Sep 17 00:00:00 2001 From: Ikuma Yamashita Date: Sat, 12 Jul 2025 06:17:43 +0900 Subject: [PATCH 3/5] fix: apply serde annotations and generic types to align with existing conventions --- lambda-events/src/event/appsync/mod.rs | 52 +++++++++++++++++++------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/lambda-events/src/event/appsync/mod.rs b/lambda-events/src/event/appsync/mod.rs index c15616d5..71e959c9 100644 --- a/lambda-events/src/event/appsync/mod.rs +++ b/lambda-events/src/event/appsync/mod.rs @@ -130,15 +130,23 @@ where /// See also: /// - [AppSync resolver mapping template context reference](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html) #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] -pub struct AppSyncResolverEvent, TSource = Option> { - pub arguments: TArguments, +pub struct AppSyncResolverEvent +where + TArguments: Serialize + DeserializeOwned, + TSource: Serialize + DeserializeOwned, + TStash: Serialize + DeserializeOwned, +{ + #[serde(bound = "")] + pub arguments: Option, pub identity: Option, - pub source: TSource, + #[serde(bound = "")] + pub source: Option, pub request: AppSyncRequest, pub info: AppSyncInfo, #[serde(default)] pub prev: Option, - pub stash: HashMap, + #[serde(bound = "")] + pub stash: TStash, } /// `AppSyncRequest` contains request-related metadata for a resolver invocation, @@ -146,7 +154,9 @@ pub struct AppSyncResolverEvent, TSource = Option>, #[serde(default)] pub domain_name: Option, @@ -155,22 +165,28 @@ pub struct AppSyncRequest { /// `AppSyncInfo` contains metadata about the current GraphQL field being resolved. #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -pub struct AppSyncInfo { +pub struct AppSyncInfo +where + T: Serialize + DeserializeOwned, +{ #[serde(default)] pub selection_set_list: Vec, #[serde(rename = "selectionSetGraphQL")] pub selection_set_graphql: String, pub parent_type_name: String, pub field_name: String, - #[serde(default)] - pub variables: HashMap, + #[serde(bound = "")] + pub variables: T, } /// `AppSyncPrevResult` contains the result of the previous step in a pipeline resolver. #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] -pub struct AppSyncPrevResult { - #[serde(default)] - pub result: HashMap, +pub struct AppSyncPrevResult +where + T: Serialize + DeserializeOwned, +{ + #[serde(bound = "")] + pub result: T, } /// `AppSyncIdentity` represents the identity of the caller as determined by the @@ -186,8 +202,12 @@ pub enum AppSyncIdentity { /// `AppSyncIdentityOIDC` represents identity information when using OIDC-based authorization. #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] -pub struct AppSyncIdentityOIDC { - pub claims: Value, +pub struct AppSyncIdentityOIDC +where + T: Serialize + DeserializeOwned, +{ + #[serde(bound = "")] + pub claims: T, pub issuer: String, pub sub: String, } @@ -195,8 +215,12 @@ pub struct AppSyncIdentityOIDC { /// `AppSyncIdentityLambda` represents identity information when using AWS Lambda #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -pub struct AppSyncIdentityLambda { - pub resolver_context: Value, +pub struct AppSyncIdentityLambda +where + T: Serialize + DeserializeOwned, +{ + #[serde(bound = "")] + pub resolver_context: T, } #[cfg(test)] From d3ec7e42adfe9b5af23a515b76cb3f79a3fbdc61 Mon Sep 17 00:00:00 2001 From: Ikuma Yamashita Date: Sat, 12 Jul 2025 06:19:35 +0900 Subject: [PATCH 4/5] refactor: rename AppSyncResolverEvent to AppSyncDirectResolverEvent --- lambda-events/src/event/appsync/mod.rs | 10 +++++----- ...olver.json => example-appsync-direct-resolver.json} | 0 2 files changed, 5 insertions(+), 5 deletions(-) rename lambda-events/src/fixtures/{example-appsync-vtl-resolver.json => example-appsync-direct-resolver.json} (100%) diff --git a/lambda-events/src/event/appsync/mod.rs b/lambda-events/src/event/appsync/mod.rs index 71e959c9..b1074540 100644 --- a/lambda-events/src/event/appsync/mod.rs +++ b/lambda-events/src/event/appsync/mod.rs @@ -130,7 +130,7 @@ where /// See also: /// - [AppSync resolver mapping template context reference](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html) #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] -pub struct AppSyncResolverEvent +pub struct AppSyncDirectResolverEvent where TArguments: Serialize + DeserializeOwned, TSource: Serialize + DeserializeOwned, @@ -269,11 +269,11 @@ mod test { #[test] #[cfg(feature = "appsync")] - fn example_appsync_vtl_resolver() { - let data = include_bytes!("../../fixtures/example-appsync-vtl-resolver.json"); - let parsed: AppSyncResolverEvent = serde_json::from_slice(data).unwrap(); + fn example_appsync_direct_resolver() { + let data = include_bytes!("../../fixtures/example-appsync-direct-resolver.json"); + let parsed: AppSyncDirectResolverEvent = serde_json::from_slice(data).unwrap(); let output: String = serde_json::to_string(&parsed).unwrap(); - let reparsed: AppSyncResolverEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + let reparsed: AppSyncDirectResolverEvent = serde_json::from_slice(output.as_bytes()).unwrap(); assert_eq!(parsed, reparsed); } } diff --git a/lambda-events/src/fixtures/example-appsync-vtl-resolver.json b/lambda-events/src/fixtures/example-appsync-direct-resolver.json similarity index 100% rename from lambda-events/src/fixtures/example-appsync-vtl-resolver.json rename to lambda-events/src/fixtures/example-appsync-direct-resolver.json From 6ce1e4fc59ac99dacedd9d15155c87a48e5a2b34 Mon Sep 17 00:00:00 2001 From: Ikuma Yamashita Date: Sat, 12 Jul 2025 06:36:46 +0900 Subject: [PATCH 5/5] fix: update doc comment to reflect AppSyncDirectResolverEvent rename --- lambda-events/src/event/appsync/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lambda-events/src/event/appsync/mod.rs b/lambda-events/src/event/appsync/mod.rs index b1074540..4aa62e5b 100644 --- a/lambda-events/src/event/appsync/mod.rs +++ b/lambda-events/src/event/appsync/mod.rs @@ -117,7 +117,7 @@ where pub ttl_override: Option, } -/// `AppSyncResolverEvent` represents the default payload structure sent by AWS AppSync +/// `AppSyncDirectResolverEvent` represents the default payload structure sent by AWS AppSync /// when using **Direct Lambda Resolvers** (i.e., when both request and response mapping /// templates are disabled). ///