Skip to content

Commit 80d14a6

Browse files
feat(event-handler): Support AppSyncResolverEvent subclassing (#526)
Co-authored-by: heitorlessa <[email protected]>
1 parent 749a372 commit 80d14a6

File tree

3 files changed

+197
-6
lines changed

3 files changed

+197
-6
lines changed

Diff for: aws_lambda_powertools/event_handler/appsync.py

+62-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import logging
2-
from typing import Any, Callable, Optional
2+
from typing import Any, Callable, Optional, Type, TypeVar
33

44
from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent
55
from aws_lambda_powertools.utilities.typing import LambdaContext
66

77
logger = logging.getLogger(__name__)
88

9+
AppSyncResolverEventT = TypeVar("AppSyncResolverEventT", bound=AppSyncResolverEvent)
10+
911

1012
class AppSyncResolver:
1113
"""
@@ -38,7 +40,7 @@ def common_field() -> str:
3840
return str(uuid.uuid4())
3941
"""
4042

41-
current_event: AppSyncResolverEvent
43+
current_event: AppSyncResolverEventT # type: ignore[valid-type]
4244
lambda_context: LambdaContext
4345

4446
def __init__(self):
@@ -62,7 +64,9 @@ def register_resolver(func):
6264

6365
return register_resolver
6466

65-
def resolve(self, event: dict, context: LambdaContext) -> Any:
67+
def resolve(
68+
self, event: dict, context: LambdaContext, data_model: Type[AppSyncResolverEvent] = AppSyncResolverEvent
69+
) -> Any:
6670
"""Resolve field_name
6771
6872
Parameters
@@ -71,6 +75,56 @@ def resolve(self, event: dict, context: LambdaContext) -> Any:
7175
Lambda event
7276
context : LambdaContext
7377
Lambda context
78+
data_model:
79+
Your data data_model to decode AppSync event, by default AppSyncResolverEvent
80+
81+
Example
82+
-------
83+
84+
```python
85+
from aws_lambda_powertools.event_handler import AppSyncResolver
86+
from aws_lambda_powertools.utilities.typing import LambdaContext
87+
88+
@app.resolver(field_name="createSomething")
89+
def create_something(id: str): # noqa AA03 VNE003
90+
return id
91+
92+
def handler(event, context: LambdaContext):
93+
return app.resolve(event, context)
94+
```
95+
96+
**Bringing custom models**
97+
98+
```python
99+
from aws_lambda_powertools import Logger, Tracer
100+
101+
from aws_lambda_powertools.logging import correlation_paths
102+
from aws_lambda_powertools.event_handler import AppSyncResolver
103+
104+
tracer = Tracer(service="sample_resolver")
105+
logger = Logger(service="sample_resolver")
106+
app = AppSyncResolver()
107+
108+
109+
class MyCustomModel(AppSyncResolverEvent):
110+
@property
111+
def country_viewer(self) -> str:
112+
return self.request_headers.get("cloudfront-viewer-country")
113+
114+
115+
@app.resolver(field_name="listLocations")
116+
@app.resolver(field_name="locations")
117+
def get_locations(name: str, description: str = ""):
118+
if app.current_event.country_viewer == "US":
119+
...
120+
return name + description
121+
122+
123+
@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER)
124+
@tracer.capture_lambda_handler
125+
def lambda_handler(event, context):
126+
return app.resolve(event, context, data_model=MyCustomModel)
127+
```
74128
75129
Returns
76130
-------
@@ -82,7 +136,7 @@ def resolve(self, event: dict, context: LambdaContext) -> Any:
82136
ValueError
83137
If we could not find a field resolver
84138
"""
85-
self.current_event = AppSyncResolverEvent(event)
139+
self.current_event = data_model(event)
86140
self.lambda_context = context
87141
resolver = self._get_resolver(self.current_event.type_name, self.current_event.field_name)
88142
return resolver(**self.current_event.arguments)
@@ -108,6 +162,8 @@ def _get_resolver(self, type_name: str, field_name: str) -> Callable:
108162
raise ValueError(f"No resolver found for '{full_name}'")
109163
return resolver["func"]
110164

111-
def __call__(self, event, context) -> Any:
165+
def __call__(
166+
self, event: dict, context: LambdaContext, data_model: Type[AppSyncResolverEvent] = AppSyncResolverEvent
167+
) -> Any:
112168
"""Implicit lambda handler which internally calls `resolve`"""
113-
return self.resolve(event, context)
169+
return self.resolve(event, context, data_model)

Diff for: docs/core/event_handler/appsync.md

+112
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,118 @@ Use the following code for `merchantInfo` and `searchMerchant` functions respect
598598
}
599599
```
600600

601+
### Custom data models
602+
603+
You can subclass `AppSyncResolverEvent` to bring your own set of methods to handle incoming events, by using `data_model` param in the `resolve` method.
604+
605+
606+
=== "custom_model.py"
607+
608+
```python hl_lines="11-14 19 26"
609+
from aws_lambda_powertools import Logger, Tracer
610+
611+
from aws_lambda_powertools.logging import correlation_paths
612+
from aws_lambda_powertools.event_handler import AppSyncResolver
613+
614+
tracer = Tracer(service="sample_resolver")
615+
logger = Logger(service="sample_resolver")
616+
app = AppSyncResolver()
617+
618+
619+
class MyCustomModel(AppSyncResolverEvent):
620+
@property
621+
def country_viewer(self) -> str:
622+
return self.request_headers.get("cloudfront-viewer-country")
623+
624+
@app.resolver(field_name="listLocations")
625+
@app.resolver(field_name="locations")
626+
def get_locations(name: str, description: str = ""):
627+
if app.current_event.country_viewer == "US":
628+
...
629+
return name + description
630+
631+
@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER)
632+
@tracer.capture_lambda_handler
633+
def lambda_handler(event, context):
634+
return app.resolve(event, context, data_model=MyCustomModel)
635+
```
636+
637+
=== "schema.graphql"
638+
639+
```typescript hl_lines="6 20"
640+
schema {
641+
query: Query
642+
}
643+
644+
type Query {
645+
listLocations: [Location]
646+
}
647+
648+
type Location {
649+
id: ID!
650+
name: String!
651+
description: String
652+
address: String
653+
}
654+
655+
type Merchant {
656+
id: String!
657+
name: String!
658+
description: String
659+
locations: [Location]
660+
}
661+
```
662+
663+
=== "listLocations_event.json"
664+
665+
```json
666+
{
667+
"arguments": {},
668+
"identity": null,
669+
"source": null,
670+
"request": {
671+
"headers": {
672+
"x-forwarded-for": "1.2.3.4, 5.6.7.8",
673+
"accept-encoding": "gzip, deflate, br",
674+
"cloudfront-viewer-country": "NL",
675+
"cloudfront-is-tablet-viewer": "false",
676+
"referer": "https://eu-west-1.console.aws.amazon.com/appsync/home?region=eu-west-1",
677+
"via": "2.0 9fce949f3749407c8e6a75087e168b47.cloudfront.net (CloudFront)",
678+
"cloudfront-forwarded-proto": "https",
679+
"origin": "https://eu-west-1.console.aws.amazon.com",
680+
"x-api-key": "da1-c33ullkbkze3jg5hf5ddgcs4fq",
681+
"content-type": "application/json",
682+
"x-amzn-trace-id": "Root=1-606eb2f2-1babc433453a332c43fb4494",
683+
"x-amz-cf-id": "SJw16ZOPuMZMINx5Xcxa9pB84oMPSGCzNOfrbJLvd80sPa0waCXzYQ==",
684+
"content-length": "114",
685+
"x-amz-user-agent": "AWS-Console-AppSync/",
686+
"x-forwarded-proto": "https",
687+
"host": "ldcvmkdnd5az3lm3gnf5ixvcyy.appsync-api.eu-west-1.amazonaws.com",
688+
"accept-language": "en-US,en;q=0.5",
689+
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:78.0) Gecko/20100101 Firefox/78.0",
690+
"cloudfront-is-desktop-viewer": "true",
691+
"cloudfront-is-mobile-viewer": "false",
692+
"accept": "*/*",
693+
"x-forwarded-port": "443",
694+
"cloudfront-is-smarttv-viewer": "false"
695+
}
696+
},
697+
"prev": null,
698+
"info": {
699+
"parentTypeName": "Query",
700+
"selectionSetList": [
701+
"id",
702+
"name",
703+
"description"
704+
],
705+
"selectionSetGraphQL": "{\n id\n name\n description\n}",
706+
"fieldName": "listLocations",
707+
"variables": {}
708+
},
709+
"stash": {}
710+
}
711+
```
712+
601713
## Testing your code
602714

603715
You can test your resolvers by passing a mocked or actual AppSync Lambda event that you're expecting.

Diff for: tests/functional/event_handler/test_appsync.py

+23
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,26 @@ async def get_async():
138138

139139
# THEN
140140
assert asyncio.run(result) == "value"
141+
142+
143+
def test_resolve_custom_data_model():
144+
# Check whether we can handle an example appsync direct resolver
145+
mock_event = load_event("appSyncDirectResolver.json")
146+
147+
class MyCustomModel(AppSyncResolverEvent):
148+
@property
149+
def country_viewer(self):
150+
return self.request_headers.get("cloudfront-viewer-country")
151+
152+
app = AppSyncResolver()
153+
154+
@app.resolver(field_name="createSomething")
155+
def create_something(id: str): # noqa AA03 VNE003
156+
return id
157+
158+
# Call the implicit handler
159+
result = app(event=mock_event, context=LambdaContext(), data_model=MyCustomModel)
160+
161+
assert result == "my identifier"
162+
163+
assert app.current_event.country_viewer == "US"

0 commit comments

Comments
 (0)