2
2
3
3
import asyncio
4
4
import logging
5
+ import warnings
5
6
from typing import TYPE_CHECKING , Any , Callable
6
7
7
8
from aws_lambda_powertools .event_handler .events_appsync .router import Router
8
9
from aws_lambda_powertools .utilities .data_classes .appsync_resolver_events_event import AppSyncResolverEventsEvent
10
+ from aws_lambda_powertools .warnings import PowertoolsUserWarning
9
11
10
12
if TYPE_CHECKING :
11
13
from aws_lambda_powertools .utilities .typing .lambda_context import LambdaContext
@@ -43,7 +45,7 @@ def resolve(
43
45
self .current_event = Router .current_event
44
46
45
47
if self .current_event .info .operation == "PUBLISH" :
46
- response = self ._call_publish_events (payload = self .current_event .events )
48
+ return self ._call_publish_events (payload = self .current_event .events )
47
49
48
50
response = self ._call_subscribe_events ()
49
51
@@ -52,33 +54,165 @@ def resolve(
52
54
return response
53
55
54
56
def _call_subscribe_events (self ) -> Any :
55
- # PLACEHOLDER
57
+ logger .debug (f"Processing subscribe events for path { self .current_event .info .channel_path } " )
58
+
59
+ resolver = self ._subscribe_registry .find_resolver (self .current_event .info .channel_path )
60
+ if not resolver :
61
+ warnings .warn (
62
+ f"No resolvers were found for publish operations with path { self .current_event .info .channel_path } " ,
63
+ stacklevel = 2 ,
64
+ category = PowertoolsUserWarning ,
65
+ )
66
+ return
56
67
pass
57
68
58
69
def _call_publish_events (self , payload : list [dict [str , Any ]]) -> Any :
59
70
"""Call single event resolver
60
71
61
72
Parameters
62
73
----------
63
- event : dict
64
- Event
65
- data_model : type[AppSyncResolverEvent]
66
- Data_model to decode AppSync event, by default it is of AppSyncResolverEvent type or subclass of it
74
+ payload : list[dict[str, Any]]
75
+ the messages sent by AppSync
67
76
"""
68
77
69
- result = []
70
- logger .debug ("Processing direct resolver event" )
78
+ logger .debug (f"Processing publish events for path { self .current_event .info .channel_path } " )
71
79
72
- #self.current_event = data_model(event)
73
80
resolver = self ._publish_registry .find_resolver (self .current_event .info .channel_path )
74
- if not resolver :
75
- print (f"No resolver found for '{ self .current_event .info .channel_path } '" )
76
- print (resolver )
81
+ async_resolver = self ._async_publish_registry .find_resolver (self .current_event .info .channel_path )
82
+
83
+ if resolver and async_resolver :
84
+ warnings .warn (
85
+ f"Both synchronous and asynchronous resolvers found for the same event and field."
86
+ f"The synchronous resolver takes precedence. Executing: { resolver ['func' ].__name__ } " ,
87
+ stacklevel = 2 ,
88
+ category = PowertoolsUserWarning ,
89
+ )
90
+
91
+ if resolver :
92
+ logger .debug (f"Found sync resolver. { resolver } " )
93
+ return self ._call_publish_event_sync_resolver (
94
+ resolver = resolver ["func" ],
95
+ aggregate = resolver ["aggregate" ],
96
+ )
97
+
98
+ if async_resolver :
99
+ logger .debug (f"Found async resolver. { resolver } " )
100
+ return asyncio .run (
101
+ self ._call_publish_event_async_resolver (
102
+ resolver = async_resolver ["func" ],
103
+ aggregate = async_resolver ["aggregate" ],
104
+ ),
105
+ )
106
+
107
+ # No resolver found
108
+ # Warning and returning AS IS
109
+ warnings .warn (
110
+ f"No resolvers were found for publish operations with path { self .current_event .info .channel_path } " ,
111
+ stacklevel = 2 ,
112
+ category = PowertoolsUserWarning )
113
+
114
+ return {"events" : payload }
115
+
116
+ def _call_publish_event_sync_resolver (
117
+ self ,
118
+ resolver : Callable ,
119
+ aggregate : bool = True ,
120
+ ) -> list [Any ]:
121
+ """
122
+ Calls a synchronous batch resolver function for each event in the current batch.
123
+
124
+ Parameters
125
+ ----------
126
+ resolver: Callable
127
+ The callable function to resolve events.
128
+ raise_on_error: bool
129
+ A flag indicating whether to raise an error when processing batches
130
+ with failed items. Defaults to False, which means errors are handled without raising exceptions.
131
+ aggregate: bool
132
+ A flag indicating whether the batch items should be processed at once or individually.
133
+ If True (default), the batch resolver will process all items in the batch as a single event.
134
+ If False, the batch resolver will process each item in the batch individually.
135
+
136
+ Returns
137
+ -------
138
+ list[Any]
139
+ A list of results corresponding to the resolved events.
140
+ """
141
+
142
+ # Checks whether the entire batch should be processed at once
143
+ if aggregate :
144
+ # Process the entire batch
145
+ response = resolver (payload = self .current_event .events )
146
+
147
+ if not isinstance (response , list ):
148
+ warnings .warn (
149
+ "Response must be a list when using aggregate, AppSync will drop those events." ,
150
+ stacklevel = 2 ,
151
+ category = PowertoolsUserWarning )
152
+
153
+ return response
154
+
155
+
156
+ # By default, we gracefully append `None` for any records that failed processing
157
+ results = []
158
+ for idx , event in enumerate (self .current_event .events ):
159
+ try :
160
+ results .append (resolver (payload = event ))
161
+ except Exception :
162
+ logger .debug (f"Failed to process event number { idx } " )
163
+ results .append (None )
77
164
78
- if not resolver ["aggregate" ]:
79
- return resolver ["func" ](payload = self .current_event .events )
80
- else :
81
- for i in self .current_event .events :
82
- result .append (resolver ["func" ](payload = i ))
165
+ return results
83
166
84
- return result
167
+ async def _call_publish_event_async_resolver (
168
+ self ,
169
+ resolver : Callable ,
170
+ aggregate : bool = True ,
171
+ ) -> list [Any ]:
172
+ """
173
+ Asynchronously call a batch resolver for each event in the current batch.
174
+
175
+ Parameters
176
+ ----------
177
+ resolver: Callable
178
+ The asynchronous resolver function.
179
+ raise_on_error: bool
180
+ A flag indicating whether to raise an error when processing batches
181
+ with failed items. Defaults to False, which means errors are handled without raising exceptions.
182
+ aggregate: bool
183
+ A flag indicating whether the batch items should be processed at once or individually.
184
+ If True (default), the batch resolver will process all items in the batch as a single event.
185
+ If False, the batch resolver will process each item in the batch individually.
186
+
187
+ Returns
188
+ -------
189
+ list[Any]
190
+ A list of results corresponding to the resolved events.
191
+ """
192
+
193
+ # Checks whether the entire batch should be processed at once
194
+ if aggregate :
195
+ # Process the entire batch
196
+ response = await resolver (event = self .current_batch_event )
197
+ if not isinstance (response , list ):
198
+ warnings .warn (
199
+ "Response must be a list when using aggregate, AppSync will drop those events." ,
200
+ stacklevel = 2 ,
201
+ category = PowertoolsUserWarning )
202
+
203
+ return response
204
+
205
+ response : list = []
206
+
207
+ # Prime coroutines
208
+ tasks = [resolver (event = e , ** e .arguments ) for e in self .current_batch_event ]
209
+
210
+ # Aggregate results and exceptions, then filter them out
211
+ # Use `None` upon exception for graceful error handling at GraphQL engine level
212
+ #
213
+ # NOTE: asyncio.gather(return_exceptions=True) catches and includes exceptions in the results
214
+ # this will become useful when we support exception handling in AppSync resolver
215
+ results = await asyncio .gather (* tasks , return_exceptions = True )
216
+ response .extend (None if isinstance (ret , Exception ) else ret for ret in results )
217
+
218
+ return response
0 commit comments