Skip to content

Commit fcdad4c

Browse files
Adding documentation
1 parent 2eb5937 commit fcdad4c

8 files changed

+346
-123
lines changed

docs/core/event_handler/appsync_events.md

+84-122
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ stateDiagram-v2
5757

5858
You must have an existing AppSync Events API with real-time capabilities enabled and IAM permissions to invoke your Lambda function. That said, there are no additional permissions required to use Event Handler as routing requires no dependency (_standard library_).
5959

60+
=== "getting_started_with_appsync_events.yaml"
61+
62+
```python hl_lines="5 10 12"
63+
--8<-- "examples/event_handler_appsync_events/src/getting_started_with_appsync_events.yaml"
64+
```
65+
6066
### AppSync request and response format
6167

6268
AppSync Events uses a specific event format for Lambda requests and responses. In most scenarios, Powertools simplifies this interaction by automatically formatting resolver returns to match the expected AppSync response structure.
@@ -79,12 +85,13 @@ AppSync Events uses a specific event format for Lambda requests and responses. I
7985
--8<-- "examples/event_handler_appsync_events/src/appsync_payload_response_with_error.json"
8086
```
8187

82-
#### Events Response with Error
88+
#### Events response with error
8389

84-
When processing events with Lambda, you can return errors to AppSync in two ways:
90+
When processing events with Lambda, you can return errors to AppSync in three ways:
8591

8692
* **Error per item:** Return an `error` key within each individual item's response. AppSync Events expects this format for item-specific errors.
8793
* **Fail entire request:** Return a JSON object with a top-level `error` key. This signals a general failure, and AppSync treats the entire request as unsuccessful.
94+
* **Unauthorized exception**: Raise the **UnauthorizedException** exception to reject a subscribe or publish request with HTTP 403.
8895

8996
### Resolver decorator
9097

@@ -129,21 +136,6 @@ When multiple handlers could match the same event, the most specific pattern tak
129136

130137
More specific routes will always take precedence over less specific ones. For example, `/default/channel1` will take precedence over `/default/*`, which will take precedence over `/*`.
131138

132-
### Processing events with async resolvers
133-
134-
Use the `@app.async_on_publish()` decorator to process events asynchronously.
135-
136-
We use `asyncio` module to support async functions, and we ensure reliable execution by managing the event loop.
137-
138-
???+ note "Events order and AppSync Events"
139-
AppSync does not rely on event order. As long as each event includes the original `id`, AppSync processes them correctly regardless of the order in which they are received.
140-
141-
=== "working_with_async_resolvers.py"
142-
143-
```python hl_lines="5 6 13"
144-
--8<-- "examples/event_handler_appsync_events/src/working_with_async_resolvers.py"
145-
```
146-
147139
### Aggregated processing
148140

149141
???+ note "Aggregate Processing"
@@ -165,11 +157,27 @@ You can enable this with the `aggregate` parameter:
165157

166158
### Handling errors
167159

168-
You can filter or reject events by raising exceptions in your handlers. The event handler will catch these exceptions and include appropriate error information in the response.
160+
You can filter or reject events by throwing exceptions in your resolvers or by formatting the payload according to the expected response structure. This instructs AppSync not to propagate that specific message, so subscribers will not receive the corresponding message.
169161

170-
#### Error with individual itens
162+
#### Handling errors with individual items
171163

172-
**Note:** When using `aggregate=True`, you are responsible for formatting the error response according to the expected format.
164+
When processing items individually with `aggregate=False`, you can raise an exception to fail a specific item. When an exception is raised, the Event Handler will catch it and include the exception name and message in the response.
165+
166+
=== "working_with_error_handling.py"
167+
168+
```python hl_lines="5 6 13"
169+
--8<-- "examples/event_handler_appsync_events/src/working_with_error_handling.py"
170+
```
171+
172+
=== "working_with_error_handling_response.json"
173+
174+
```python hl_lines="5 6 13"
175+
--8<-- "examples/event_handler_appsync_events/src/working_with_error_handling_response.json"
176+
```
177+
178+
#### Handling errors with batch of items
179+
180+
When processing batch of items with `aggregate=False`, you can must format the payload according the expected response.
173181

174182
=== "working_with_error_handling.py"
175183

@@ -185,121 +193,75 @@ You can filter or reject events by raising exceptions in your handlers. The even
185193

186194
#### Rejecting the entire request
187195

196+
??? warning "Raising `UnauthorizedException` will cause the Lambda invocation to fail."
197+
198+
You can also reject the entire payload by raising an `UnauthorizedException`. This prevents Powertools from processing any messages and causes the Lambda invocation to fail, returning an error to AppSync.
199+
200+
=== "working_with_error_handling.py"
201+
202+
```python hl_lines="5 6 13"
203+
--8<-- "examples/event_handler_appsync_events/src/working_with_error_handling.py"
204+
```
205+
206+
=== "working_with_error_handling_response.json"
207+
208+
```python hl_lines="5 6 13"
209+
--8<-- "examples/event_handler_appsync_events/src/working_with_error_handling_response.json"
210+
```
211+
212+
### Processing events with async resolvers
213+
214+
Use the `@app.async_on_publish()` decorator to process events asynchronously.
215+
216+
We use `asyncio` module to support async functions, and we ensure reliable execution by managing the event loop.
217+
218+
???+ note "Events order and AppSync Events"
219+
AppSync does not rely on event order. As long as each event includes the original `id`, AppSync processes them correctly regardless of the order in which they are received.
220+
221+
=== "working_with_async_resolvers.py"
222+
223+
```python hl_lines="5 6 13"
224+
--8<-- "examples/event_handler_appsync_events/src/working_with_async_resolvers.py"
225+
```
188226

189227
### Accessing Lambda context and event
190228

191-
You might need access to the original Lambda event or context for additional information. These are accessible via the app instance:
192-
193-
=== "context_access.py"
194-
195-
```python
196-
from aws_lambda_powertools.event_handler.appsync_events import AppSyncEventsResolver
197-
198-
app = AppSyncEventsResolver()
199-
200-
@app.on_publish("/default/channel1")
201-
def handle_channel1_publish(payload):
202-
# Access the full event and context
203-
full_event = app.event
204-
lambda_context = app.context
205-
206-
# Access request headers
207-
headers = full_event.get("request", {}).get("headers", {})
208-
209-
# Check remaining time
210-
remaining_time = lambda_context.get_remaining_time_in_millis()
211-
212-
return {
213-
"payload": payload,
214-
"userAgent": headers.get("User-Agent"),
215-
"timeRemaining": remaining_time
216-
}
217-
218-
def lambda_handler(event, context):
219-
return app.resolve(event, context)
229+
You can access to the original Lambda event or context for additional information. These are accessible via the app instance:
230+
231+
=== "accessing_event_and_context.py"
232+
233+
```python hl_lines="5 6 13"
234+
--8<-- "examples/event_handler_appsync_events/src/accessing_event_and_context.py"
220235
```
221236

222237
## Testing your code
223238

224239
You can test your event handlers by passing a mocked or actual AppSync Events Lambda event.
225240

226-
=== "test_publish_handler.py"
227-
228-
```python
229-
import json
230-
from my_lambda_function import app
231-
232-
def test_publish_handler():
233-
# Load a sample event
234-
with open("sample_publish_event.json", "r") as f:
235-
event = json.load(f)
236-
237-
# Test the handler directly
238-
result = app.resolve(event, {})
239-
240-
# Verify the result
241-
assert len(result["events"]) == 2
242-
assert result["events"][0]["payload"]["processed"] is True
241+
### Testing publish events
242+
243+
=== "getting_started_with_testing_publish.py"
244+
245+
```python hl_lines="5 6 13"
246+
--8<-- "examples/event_handler_appsync_events/src/getting_started_with_testing_publish.py"
243247
```
244248

245-
=== "sample_publish_event.json"
246-
247-
```json
248-
{
249-
"events": [
250-
{
251-
"id": "event1",
252-
"payload": {"message": "Hello World"},
253-
"channel": {
254-
"path": "/default/channel1"
255-
}
256-
},
257-
{
258-
"id": "event2",
259-
"payload": {"message": "Test Message"},
260-
"channel": {
261-
"path": "/default/channel2"
262-
}
263-
}
264-
],
265-
"request": {
266-
"headers": {
267-
"x-api-key": "test-api-key"
268-
}
269-
}
270-
}
249+
=== "getting_started_with_testing_publish_event.json"
250+
251+
```python hl_lines="5 6 13"
252+
--8<-- "examples/event_handler_appsync_events/src/getting_started_with_testing_publish_event.json"
271253
```
272254

273-
For subscription events:
274-
275-
=== "test_subscribe_handler.py"
276-
277-
```python
278-
import json
279-
from my_lambda_function import app
280-
281-
def test_subscribe_handler():
282-
# Load a sample subscription event
283-
with open("sample_subscribe_event.json", "r") as f:
284-
event = json.load(f)
285-
286-
# Test the handler directly
287-
result = app.resolve(event, {})
288-
289-
# Verify the result
290-
assert result is True
255+
### Testing subscribe events
256+
257+
=== "getting_started_with_testing_subscribe.py"
258+
259+
```python hl_lines="5 6 13"
260+
--8<-- "examples/event_handler_appsync_events/src/getting_started_with_testing_subscribe.py"
291261
```
292262

293-
=== "sample_subscribe_event.json"
294-
295-
```json
296-
{
297-
"type": "SUBSCRIBE",
298-
"channel": {
299-
"path": "/default/channel1"
300-
},
301-
"identity": {
302-
"username": "testuser"
303-
}
304-
}
263+
=== "getting_started_with_testing_subscribe_event.json"
264+
265+
```python hl_lines="5 6 13"
266+
--8<-- "examples/event_handler_appsync_events/src/getting_started_with_testing_subscribe_event.json"
305267
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING, Any
4+
5+
from aws_lambda_powertools.event_handler import AppSyncEventsResolver
6+
from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEventsEvent
7+
8+
if TYPE_CHECKING:
9+
from aws_lambda_powertools.utilities.typing import LambdaContext
10+
11+
app = AppSyncEventsResolver()
12+
13+
class ValidationError(Exception):
14+
pass
15+
16+
@app.on_publish("/default/channel1")
17+
def handle_channel1_publish(payload: dict[str, Any]):
18+
# Access the full event and context
19+
lambda_event: AppSyncResolverEventsEvent = app.current_event
20+
lambda_context: LambdaContext = app.context
21+
22+
# Access request headers
23+
headers = lambda_event.get("request", {}).get("headers", {})
24+
25+
# Check remaining time
26+
remaining_time = lambda_context.get_remaining_time_in_millis()
27+
28+
return {
29+
"originalMessage": payload,
30+
"userAgent": headers.get("User-Agent"),
31+
"timeRemaining": remaining_time,
32+
}
33+
34+
def lambda_handler(event: dict, context: LambdaContext):
35+
return app.resolve(event, context)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
Resources:
2+
WebsocketAPI:
3+
Type: AWS::AppSync::Api
4+
Properties:
5+
EventConfig:
6+
AuthProviders:
7+
- AuthType: API_KEY
8+
ConnectionAuthModes:
9+
- AuthType: API_KEY
10+
DefaultPublishAuthModes:
11+
- AuthType: API_KEY
12+
DefaultSubscribeAuthModes:
13+
- AuthType: API_KEY
14+
Name: RealTimeEventAPI
15+
16+
WebasocketApiKey:
17+
Type: AWS::AppSync::ApiKey
18+
Properties:
19+
ApiId: !GetAtt WebsocketAPI.ApiId
20+
Description: "API KEY"
21+
Expires: 365
22+
23+
WebsocketAPINamespace:
24+
Type: AWS::AppSync::ChannelNamespace
25+
Properties:
26+
ApiId: !GetAtt WebsocketAPI.ApiId
27+
Name: powertools
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import json
2+
from pathlib import Path
3+
4+
from aws_lambda_powertools.event_handler import AppSyncEventsResolver
5+
6+
7+
class LambdaContext:
8+
def __init__(self):
9+
self.function_name = "test-func"
10+
self.memory_limit_in_mb = 128
11+
self.invoked_function_arn = "arn:aws:lambda:eu-west-1:809313241234:function:test-func"
12+
self.aws_request_id = "52fdfc07-2182-154f-163f-5f0f9a621d72"
13+
14+
def get_remaining_time_in_millis(self) -> int:
15+
return 1000
16+
17+
def test_publish_event_with_synchronous_resolver():
18+
"""Test handling a publish event with a synchronous resolver."""
19+
# GIVEN a sample publish event
20+
with Path.open("getting_started_with_testing_publish_event.json", "r") as f:
21+
event = json.load(f)
22+
23+
lambda_context = LambdaContext()
24+
25+
# GIVEN an AppSyncEventsResolver with a synchronous resolver
26+
app = AppSyncEventsResolver()
27+
28+
@app.on_publish(path="/default/*")
29+
def test_handler(payload):
30+
return {"processed": True, "data": payload["data"]}
31+
32+
# WHEN we resolve the event
33+
result = app.resolve(event, lambda_context)
34+
35+
# THEN we should get the correct response
36+
expected_result = {
37+
"events": [
38+
{"id": "123", "payload": {"processed": True, "data": "test data"}},
39+
],
40+
}
41+
assert result == expected_result

0 commit comments

Comments
 (0)