Skip to content

Add support for Direct Lambda Resolver event format #1015

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 11, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions lambda-events/src/event/appsync/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,112 @@ where
pub ttl_override: Option<i64>,
}

/// `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 AppSyncDirectResolverEvent<TArguments = Value, TSource = Value, TStash = Value>
where
TArguments: Serialize + DeserializeOwned,
TSource: Serialize + DeserializeOwned,
TStash: Serialize + DeserializeOwned,
{
#[serde(bound = "")]
pub arguments: Option<TArguments>,
pub identity: Option<AppSyncIdentity>,
#[serde(bound = "")]
pub source: Option<TSource>,
pub request: AppSyncRequest,
pub info: AppSyncInfo,
#[serde(default)]
pub prev: Option<AppSyncPrevResult>,
#[serde(bound = "")]
pub stash: TStash,
}

/// `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(deserialize_with = "deserialize_lambda_map")]
#[serde(default)]
#[serde(bound = "")]
pub headers: HashMap<String, Option<String>>,
#[serde(default)]
pub domain_name: Option<String>,
}

/// `AppSyncInfo` contains metadata about the current GraphQL field being resolved.
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AppSyncInfo<T = Value>
where
T: Serialize + DeserializeOwned,
{
#[serde(default)]
pub selection_set_list: Vec<String>,
#[serde(rename = "selectionSetGraphQL")]
pub selection_set_graphql: String,
pub parent_type_name: String,
pub field_name: String,
#[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<T = Value>
where
T: Serialize + DeserializeOwned,
{
#[serde(bound = "")]
pub result: T,
}

/// `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<T = Value>
where
T: Serialize + DeserializeOwned,
{
#[serde(bound = "")]
pub claims: T,
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<T = Value>
where
T: Serialize + DeserializeOwned,
{
#[serde(bound = "")]
pub resolver_context: T,
}

#[cfg(test)]
mod test {
use super::*;
Expand Down Expand Up @@ -160,4 +266,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_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: AppSyncDirectResolverEvent = serde_json::from_slice(output.as_bytes()).unwrap();
assert_eq!(parsed, reparsed);
}
}
64 changes: 64 additions & 0 deletions lambda-events/src/fixtures/example-appsync-direct-resolver.json
Original file line number Diff line number Diff line change
@@ -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": {}
}