Skip to content

Commit 9ed003e

Browse files
Addressing Heitor's feedback
1 parent 857375c commit 9ed003e

File tree

2 files changed

+133
-24
lines changed

2 files changed

+133
-24
lines changed

aws_lambda_powertools/event_handler/appsync.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from aws_lambda_powertools.event_handler.graphql_appsync.router import Router
88
from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent
99
from aws_lambda_powertools.utilities.typing import LambdaContext
10+
from aws_lambda_powertools.warnings import PowertoolsUserWarning
1011

1112
logger = logging.getLogger(__name__)
1213

@@ -280,6 +281,7 @@ def _call_batch_resolver(self, event: List[dict], data_model: Type[AppSyncResolv
280281
f"Both synchronous and asynchronous resolvers found for the same event and field."
281282
f"The synchronous resolver takes precedence. Executing: {resolver['func'].__name__}",
282283
stacklevel=2,
284+
category=PowertoolsUserWarning,
283285
)
284286

285287
if resolver:

tests/functional/event_handler/required_dependencies/test_appsync.py

Lines changed: 131 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from aws_lambda_powertools.event_handler.graphql_appsync.router import Router
99
from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent
1010
from aws_lambda_powertools.utilities.typing import LambdaContext
11+
from aws_lambda_powertools.warnings import PowertoolsUserWarning
1112
from tests.functional.utils import load_event
1213

1314

@@ -303,8 +304,106 @@ def test_include_router_merges_context():
303304
assert app.context == router.context
304305

305306

307+
def test_resolve_batch_processing_with_related_events():
308+
# GIVEN An event with multiple requests to fetch related posts for different post IDs.
309+
event = [
310+
{
311+
"arguments": {},
312+
"identity": "None",
313+
"source": {
314+
"post_id": "3",
315+
"title": "Third book",
316+
},
317+
"info": {
318+
"selectionSetList": [
319+
"title",
320+
],
321+
"selectionSetGraphQL": "{\n title\n}",
322+
"fieldName": "relatedPosts",
323+
"parentTypeName": "Post",
324+
},
325+
},
326+
{
327+
"arguments": {},
328+
"identity": "None",
329+
"source": {
330+
"post_id": "4",
331+
"title": "Fifth book",
332+
},
333+
"info": {
334+
"selectionSetList": [
335+
"title",
336+
],
337+
"selectionSetGraphQL": "{\n title\n}",
338+
"fieldName": "relatedPosts",
339+
"parentTypeName": "Post",
340+
},
341+
},
342+
{
343+
"arguments": {},
344+
"identity": "None",
345+
"source": {
346+
"post_id": "1",
347+
"title": "First book",
348+
},
349+
"info": {
350+
"selectionSetList": [
351+
"title",
352+
],
353+
"selectionSetGraphQL": "{\n title\n}",
354+
"fieldName": "relatedPosts",
355+
"parentTypeName": "Post",
356+
},
357+
},
358+
]
359+
360+
# GIVEN A dictionary of posts and a dictionary of related posts.
361+
posts = {
362+
"1": {
363+
"post_id": "1",
364+
"title": "First book",
365+
},
366+
"2": {
367+
"post_id": "2",
368+
"title": "Second book",
369+
},
370+
"3": {
371+
"post_id": "3",
372+
"title": "Third book",
373+
},
374+
"4": {
375+
"post_id": "4",
376+
"title": "Fourth book",
377+
},
378+
}
379+
380+
posts_related = {
381+
"1": [posts["2"]],
382+
"2": [posts["3"], posts["4"], posts["1"]],
383+
"3": [posts["2"], posts["1"]],
384+
"4": [posts["3"], posts["1"]],
385+
}
386+
387+
app = AppSyncResolver()
388+
389+
@app.batch_resolver(type_name="Post", field_name="relatedPosts")
390+
def related_posts(event: AppSyncResolverEvent) -> Optional[list]:
391+
return posts_related[event.source["post_id"]]
392+
393+
# WHEN related_posts function, which is the batch resolver, is called with the event.
394+
result = app.resolve(event, LambdaContext())
395+
396+
# THEN the result must be a list of related posts
397+
assert result == [
398+
posts_related["3"],
399+
posts_related["4"],
400+
posts_related["1"],
401+
]
402+
403+
306404
# Batch resolver tests
307-
def test_resolve_batch_processing():
405+
def test_resolve_batch_processing_with_simple_queries():
406+
# GIVEN a list of events representing GraphQL queries for listing locations
308407
event = [
309408
{
310409
"typeName": "Query",
@@ -346,11 +445,12 @@ def test_resolve_batch_processing():
346445

347446
app = AppSyncResolver()
348447

448+
# WHEN the batch resolver for the listLocations field is defined
349449
@app.batch_resolver(field_name="listLocations")
350450
def create_something(event: AppSyncResolverEvent) -> Optional[list]: # noqa AA03 VNE003
351451
return event.source["id"] if event.source else None
352452

353-
# Call the implicit handler
453+
# THEN the resolver should correctly process the batch of queries
354454
result = app.resolve(event, LambdaContext())
355455
assert result == [appsync_event["source"]["id"] for appsync_event in event]
356456

@@ -359,6 +459,7 @@ def create_something(event: AppSyncResolverEvent) -> Optional[list]: # noqa AA0
359459

360460

361461
def test_resolve_batch_processing_with_raise_on_exception():
462+
# GIVEN a list of events representing GraphQL queries for listing locations
362463
event = [
363464
{
364465
"typeName": "Query",
@@ -400,16 +501,18 @@ def test_resolve_batch_processing_with_raise_on_exception():
400501

401502
app = AppSyncResolver()
402503

504+
# WHEN the sync batch resolver for the 'listLocations' field is defined with raise_on_error=True
403505
@app.batch_resolver(field_name="listLocations", raise_on_error=True)
404506
def create_something(event: AppSyncResolverEvent) -> Optional[list]: # noqa AA03 VNE003
405507
raise RuntimeError
406508

407-
# Call the implicit handler
509+
# THEN the resolver should raise a RuntimeError when processing the batch of queries
408510
with pytest.raises(RuntimeError):
409511
app.resolve(event, LambdaContext())
410512

411513

412514
def test_async_resolve_batch_processing_with_raise_on_exception():
515+
# GIVEN a list of events representing GraphQL queries for listing locations
413516
event = [
414517
{
415518
"typeName": "Query",
@@ -451,11 +554,12 @@ def test_async_resolve_batch_processing_with_raise_on_exception():
451554

452555
app = AppSyncResolver()
453556

557+
# WHEN the async batch resolver for the 'listLocations' field is defined with raise_on_error=True
454558
@app.async_batch_resolver(field_name="listLocations", raise_on_error=True)
455559
async def create_something(event: AppSyncResolverEvent) -> Optional[list]: # noqa AA03 VNE003
456560
raise RuntimeError
457561

458-
# Call the implicit handler
562+
# THEN the resolver should raise a RuntimeError when processing the batch of queries
459563
with pytest.raises(RuntimeError):
460564
app.resolve(event, LambdaContext())
461565

@@ -515,6 +619,7 @@ def create_something(event: AppSyncResolverEvent) -> Optional[list]: # noqa AA0
515619

516620

517621
def test_resolve_async_batch_processing_without_exception():
622+
# GIVEN a list of events representing GraphQL queries for listing locations
518623
event = [
519624
{
520625
"typeName": "Query",
@@ -556,16 +661,16 @@ def test_resolve_async_batch_processing_without_exception():
556661

557662
app = AppSyncResolver()
558663

664+
# WHEN the batch resolver for the 'listLocations' field is defined with raise_on_error=False
559665
@app.async_batch_resolver(field_name="listLocations", raise_on_error=False)
560666
async def create_something(event: AppSyncResolverEvent) -> Optional[list]: # noqa AA03 VNE003
561667
raise RuntimeError
562668

563-
# Call the implicit handler
564669
result = app.resolve(event, LambdaContext())
565-
assert result == [None, None, None]
566670

567-
assert app.current_batch_event and len(app.current_batch_event) == len(event)
568-
assert not app.current_event
671+
# THEN the resolver should return None for each event in the batch
672+
assert len(app.current_batch_event) == len(event)
673+
assert result == [None, None, None]
569674

570675

571676
def test_resolver_batch_with_resolver_not_found():
@@ -596,7 +701,7 @@ def get_locations(event: AppSyncResolverEvent, name: str) -> str:
596701

597702
app.include_router(router)
598703

599-
# THEN must fail with ValueError
704+
# THEN must fail with ResolverNotFoundError
600705
with pytest.raises(ResolverNotFoundError, match="No resolver found for.*"):
601706
app.resolve(mock_event1, LambdaContext())
602707

@@ -633,27 +738,27 @@ async def get_locations_async(event: AppSyncResolverEvent, name: str) -> str:
633738

634739
app.include_router(router)
635740

636-
# THEN must fail with ValueError
637-
with pytest.warns(UserWarning, match="Both synchronous and asynchronous resolvers*"):
741+
# THEN must raise a PowertoolsUserWarning
742+
with pytest.warns(PowertoolsUserWarning, match="Both synchronous and asynchronous resolvers*"):
638743
app.resolve(mock_event1, LambdaContext())
639744

640745

641-
def test_resolver_include_batch_resolver():
642-
# GIVEN
746+
def test_batch_resolver_with_router():
747+
# GIVEN an AppSyncResolver and a Router instance
643748
app = AppSyncResolver()
644749
router = Router()
645750

646751
@router.batch_resolver(type_name="Query", field_name="listLocations")
647752
def get_locations(event: AppSyncResolverEvent, name: str) -> str:
648-
return "get_locations#" + name + "#" + event.source["id"]
753+
return f"get_locations#{name}#" + event.source["id"]
649754

650-
@app.batch_resolver(field_name="listLocations2")
755+
@router.batch_resolver(field_name="listLocations2")
651756
def get_locations2(event: AppSyncResolverEvent, name: str) -> str:
652-
return "get_locations2#" + name + "#" + event.source["id"]
757+
return f"get_locations2#{name}#" + event.source["id"]
653758

759+
# WHEN we include the routes
654760
app.include_router(router)
655761

656-
# WHEN
657762
mock_event1 = [
658763
{
659764
"typeName": "Query",
@@ -685,12 +790,13 @@ def get_locations2(event: AppSyncResolverEvent, name: str) -> str:
685790
result1 = app.resolve(mock_event1, LambdaContext())
686791
result2 = app.resolve(mock_event2, LambdaContext())
687792

688-
# THEN
793+
# THEN the resolvers should return the expected results
689794
assert result1 == ["get_locations#value#1"]
690795
assert result2 == ["get_locations2#value#2"]
691796

692797

693798
def test_resolve_async_batch_processing():
799+
# GIVEN a list of events representing GraphQL queries for listing locations
694800
event = [
695801
{
696802
"typeName": "Query",
@@ -732,19 +838,20 @@ def test_resolve_async_batch_processing():
732838

733839
app = AppSyncResolver()
734840

841+
# WHEN the async batch resolver for the 'listLocations' field is defined
735842
@app.async_batch_resolver(field_name="listLocations")
736843
async def create_something(event: AppSyncResolverEvent) -> Optional[list]:
737844
return event.source["id"] if event.source else None
738845

739-
# Call the implicit handler
846+
# THEN the resolver should correctly process the batch of queries asynchronously
740847
result = app.resolve(event, LambdaContext())
741848
assert result == [appsync_event["source"]["id"] for appsync_event in event]
742849

743850
assert app.current_batch_event and len(app.current_batch_event) == len(event)
744851

745852

746853
def test_resolve_async_batch_and_sync_singular_processing():
747-
# GIVEN
854+
# GIVEN a router with an async batch resolver for 'listLocations' and a sync singular resolver for 'listLocation'
748855
app = AppSyncResolver()
749856
router = Router()
750857

@@ -758,7 +865,7 @@ def get_location(name: str) -> str:
758865

759866
app.include_router(router)
760867

761-
# WHEN
868+
# WHEN resolving a batch of events for async 'listLocations' and a singular event for 'listLocation'
762869
mock_event1 = [
763870
{
764871
"typeName": "Query",
@@ -778,7 +885,7 @@ def get_location(name: str) -> str:
778885
result1 = app.resolve(mock_event1, LambdaContext())
779886
result2 = app.resolve(mock_event2, LambdaContext())
780887

781-
# THEN
888+
# THEN the resolvers should return the expected results
782889
assert result1 == ["get_locations#value#1"]
783890
assert result2 == "get_location#value"
784891

@@ -790,11 +897,11 @@ def test_async_resolver_include_batch_resolver():
790897

791898
@router.async_batch_resolver(type_name="Query", field_name="listLocations")
792899
async def get_locations(event: AppSyncResolverEvent, name: str) -> str:
793-
return "get_locations#" + name + "#" + event.source["id"]
900+
return f"get_locations#{name}#" + event.source["id"]
794901

795902
@app.async_batch_resolver(field_name="listLocations2")
796903
async def get_locations2(event: AppSyncResolverEvent, name: str) -> str:
797-
return "get_locations2#" + name + "#" + event.source["id"]
904+
return f"get_locations2#{name}#" + event.source["id"]
798905

799906
app.include_router(router)
800907

0 commit comments

Comments
 (0)