Skip to content

Commit ee2e0d2

Browse files
authored
Add support for Direct Lambda Resolver event format (#1015)
* feat(event): add AppSyncResolverEvent structure and example payload * fix: remove `groups` field from AppSyncCognitoIdentity to avoid semver break * fix: apply serde annotations and generic types to align with existing conventions * refactor: rename AppSyncResolverEvent to AppSyncDirectResolverEvent * fix: update doc comment to reflect AppSyncDirectResolverEvent rename
1 parent fd0354e commit ee2e0d2

File tree

2 files changed

+180
-0
lines changed

2 files changed

+180
-0
lines changed

lambda-events/src/event/appsync/mod.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,112 @@ where
117117
pub ttl_override: Option<i64>,
118118
}
119119

120+
/// `AppSyncDirectResolverEvent` represents the default payload structure sent by AWS AppSync
121+
/// when using **Direct Lambda Resolvers** (i.e., when both request and response mapping
122+
/// templates are disabled).
123+
///
124+
/// This structure includes the full AppSync **Context object**, as described in the
125+
/// [AppSync Direct Lambda resolver reference](https://docs.aws.amazon.com/appsync/latest/devguide/direct-lambda-reference.html).
126+
///
127+
/// It is recommended when working without VTL templates and relying on the standard
128+
/// AppSync-to-Lambda event format.
129+
///
130+
/// See also:
131+
/// - [AppSync resolver mapping template context reference](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html)
132+
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
133+
pub struct AppSyncDirectResolverEvent<TArguments = Value, TSource = Value, TStash = Value>
134+
where
135+
TArguments: Serialize + DeserializeOwned,
136+
TSource: Serialize + DeserializeOwned,
137+
TStash: Serialize + DeserializeOwned,
138+
{
139+
#[serde(bound = "")]
140+
pub arguments: Option<TArguments>,
141+
pub identity: Option<AppSyncIdentity>,
142+
#[serde(bound = "")]
143+
pub source: Option<TSource>,
144+
pub request: AppSyncRequest,
145+
pub info: AppSyncInfo,
146+
#[serde(default)]
147+
pub prev: Option<AppSyncPrevResult>,
148+
#[serde(bound = "")]
149+
pub stash: TStash,
150+
}
151+
152+
/// `AppSyncRequest` contains request-related metadata for a resolver invocation,
153+
/// including client-sent headers and optional custom domain name.
154+
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
155+
#[serde(rename_all = "camelCase")]
156+
pub struct AppSyncRequest {
157+
#[serde(deserialize_with = "deserialize_lambda_map")]
158+
#[serde(default)]
159+
#[serde(bound = "")]
160+
pub headers: HashMap<String, Option<String>>,
161+
#[serde(default)]
162+
pub domain_name: Option<String>,
163+
}
164+
165+
/// `AppSyncInfo` contains metadata about the current GraphQL field being resolved.
166+
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
167+
#[serde(rename_all = "camelCase")]
168+
pub struct AppSyncInfo<T = Value>
169+
where
170+
T: Serialize + DeserializeOwned,
171+
{
172+
#[serde(default)]
173+
pub selection_set_list: Vec<String>,
174+
#[serde(rename = "selectionSetGraphQL")]
175+
pub selection_set_graphql: String,
176+
pub parent_type_name: String,
177+
pub field_name: String,
178+
#[serde(bound = "")]
179+
pub variables: T,
180+
}
181+
182+
/// `AppSyncPrevResult` contains the result of the previous step in a pipeline resolver.
183+
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
184+
pub struct AppSyncPrevResult<T = Value>
185+
where
186+
T: Serialize + DeserializeOwned,
187+
{
188+
#[serde(bound = "")]
189+
pub result: T,
190+
}
191+
192+
/// `AppSyncIdentity` represents the identity of the caller as determined by the
193+
/// configured AppSync authorization mechanism (IAM, Cognito, OIDC, or Lambda).
194+
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
195+
#[serde(untagged, rename_all = "camelCase")]
196+
pub enum AppSyncIdentity {
197+
IAM(AppSyncIamIdentity),
198+
Cognito(AppSyncCognitoIdentity),
199+
OIDC(AppSyncIdentityOIDC),
200+
Lambda(AppSyncIdentityLambda),
201+
}
202+
203+
/// `AppSyncIdentityOIDC` represents identity information when using OIDC-based authorization.
204+
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
205+
pub struct AppSyncIdentityOIDC<T = Value>
206+
where
207+
T: Serialize + DeserializeOwned,
208+
{
209+
#[serde(bound = "")]
210+
pub claims: T,
211+
pub issuer: String,
212+
pub sub: String,
213+
}
214+
215+
/// `AppSyncIdentityLambda` represents identity information when using AWS Lambda
216+
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
217+
#[serde(rename_all = "camelCase")]
218+
pub struct AppSyncIdentityLambda<T = Value>
219+
where
220+
T: Serialize + DeserializeOwned,
221+
{
222+
#[serde(bound = "")]
223+
pub resolver_context: T,
224+
}
225+
120226
#[cfg(test)]
121227
mod test {
122228
use super::*;
@@ -160,4 +266,14 @@ mod test {
160266
let reparsed: AppSyncLambdaAuthorizerResponse = serde_json::from_slice(output.as_bytes()).unwrap();
161267
assert_eq!(parsed, reparsed);
162268
}
269+
270+
#[test]
271+
#[cfg(feature = "appsync")]
272+
fn example_appsync_direct_resolver() {
273+
let data = include_bytes!("../../fixtures/example-appsync-direct-resolver.json");
274+
let parsed: AppSyncDirectResolverEvent = serde_json::from_slice(data).unwrap();
275+
let output: String = serde_json::to_string(&parsed).unwrap();
276+
let reparsed: AppSyncDirectResolverEvent = serde_json::from_slice(output.as_bytes()).unwrap();
277+
assert_eq!(parsed, reparsed);
278+
}
163279
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
"arguments": {
3+
"input": "foo"
4+
},
5+
"identity": {
6+
"sourceIp": [
7+
"x.x.x.x"
8+
],
9+
"userArn": "arn:aws:iam::123456789012:user/appsync",
10+
"accountId": "666666666666",
11+
"user": "AIDAAAAAAAAAAAAAAAAAA"
12+
},
13+
"info": {
14+
"fieldName": "greet",
15+
"parentTypeName": "Query",
16+
"selectionSetGraphQL": "",
17+
"selectionSetList": [],
18+
"variables": {
19+
"inputVar": "foo"
20+
}
21+
},
22+
"prev": null,
23+
"request": {
24+
"domainName": null,
25+
"headers": {
26+
"accept": "application/json, text/plain, */*",
27+
"accept-encoding": "gzip, deflate, br, zstd",
28+
"accept-language": "en-US,en;q=0.9,ja;q=0.8,en-GB;q=0.7",
29+
"cloudfront-forwarded-proto": "https",
30+
"cloudfront-is-desktop-viewer": "true",
31+
"cloudfront-is-mobile-viewer": "false",
32+
"cloudfront-is-smarttv-viewer": "false",
33+
"cloudfront-is-tablet-viewer": "false",
34+
"cloudfront-viewer-asn": "17676",
35+
"cloudfront-viewer-country": "JP",
36+
"content-length": "40",
37+
"content-type": "application/json",
38+
"host": "2ojpkjk2ejb57l7stgad5o4qiq.appsync-api.ap-northeast-1.amazonaws.com",
39+
"origin": "https://ap-northeast-1.console.aws.amazon.com",
40+
"priority": "u=1, i",
41+
"referer": "https://ap-northeast-1.console.aws.amazon.com/",
42+
"sec-ch-ua": "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\", \"Microsoft Edge\";v=\"138\"",
43+
"sec-ch-ua-mobile": "?0",
44+
"sec-ch-ua-platform": "\"Windows\"",
45+
"sec-fetch-dest": "empty",
46+
"sec-fetch-mode": "cors",
47+
"sec-fetch-site": "cross-site",
48+
"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",
49+
"via": "2.0 ee337d4db5c7ebfdc8ec0798a1ede776.cloudfront.net (CloudFront)",
50+
"x-amz-cf-id": "O3ZflUCq6_TzxjouyYB3zg7-kl7Ze-gXbniM2jJ3hAOfDFpPMGRu3Q==",
51+
"x-amz-user-agent": "AWS-Console-AppSync/",
52+
"x-amzn-appsync-is-vpce-request": "false",
53+
"x-amzn-remote-ip": "x.x.x.x",
54+
"x-amzn-requestid": "7ada8740-bbf4-49e8-bf45-f10b3d67159b",
55+
"x-amzn-trace-id": "Root=1-68713e21-7a03739120ad60703e794b22",
56+
"x-api-key": "***",
57+
"x-forwarded-for": "***",
58+
"x-forwarded-port": "443",
59+
"x-forwarded-proto": "https"
60+
}
61+
},
62+
"source": null,
63+
"stash": {}
64+
}

0 commit comments

Comments
 (0)