Skip to content

Commit 6137896

Browse files
author
Michael Brewer
committed
feat(data-classes): Add AppSync resolver utilities
Changes: * Add helper functions to generate GraphQL scalar types * AppSyncResolver decorator which works with AppSyncResolverEvent
1 parent fa72167 commit 6137896

File tree

2 files changed

+245
-0
lines changed

2 files changed

+245
-0
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import datetime
2+
import time
3+
import uuid
4+
from typing import Any, Dict
5+
6+
from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent
7+
from aws_lambda_powertools.utilities.typing import LambdaContext
8+
9+
10+
def make_id():
11+
"""A unique identifier for an object. This scalar is serialized like a String but isn't meant to be
12+
human-readable."""
13+
return str(uuid.uuid4())
14+
15+
16+
def aws_date():
17+
"""AWSDate - An extended ISO 8601 date string in the format YYYY-MM-DD"""
18+
now = datetime.datetime.utcnow().date()
19+
return now.strftime("%Y-%m-%d")
20+
21+
22+
def aws_time():
23+
"""AWSTime - An extended ISO 8601 time string in the format hh:mm:ss.sss"""
24+
now = datetime.datetime.utcnow().time()
25+
return now.strftime("%H:%M:%S")
26+
27+
28+
def aws_datetime():
29+
"""AWSDateTime - An extended ISO 8601 date and time string in the format YYYY-MM-DDThh:mm:ss.sssZ."""
30+
now = datetime.datetime.utcnow()
31+
return now.strftime("%Y-%m-%dT%H:%M:%SZ")
32+
33+
34+
def aws_timestamp():
35+
"""AWSTimestamp - An integer value representing the number of seconds before or after 1970-01-01-T00:00Z."""
36+
return int(time.time())
37+
38+
39+
class AppSyncResolver:
40+
"""AppSync resolver decorator utility"""
41+
42+
def __init__(self):
43+
self._resolvers: dict = {}
44+
45+
def resolver(self, type_name: str = "*", field_name: str = None, **kwargs):
46+
"""Registers the resolver for field_name
47+
48+
Parameters
49+
----------
50+
type_name : str
51+
Type name
52+
field_name : str
53+
Field name
54+
kwargs :
55+
Keyword arguments
56+
"""
57+
58+
def register_resolver(func):
59+
self._resolvers[f"{type_name}.{field_name}"] = {
60+
"func": func,
61+
"config": kwargs,
62+
}
63+
return func
64+
65+
return register_resolver
66+
67+
def resolve(self, event: dict, context: LambdaContext) -> Any:
68+
"""Resolve field_name
69+
70+
Parameters
71+
----------
72+
event : dict
73+
Lambda event
74+
context : LambdaContext
75+
Lambda context
76+
77+
Returns
78+
-------
79+
Any
80+
Returns the result of the resolver
81+
82+
Raises
83+
-------
84+
ValueError
85+
If we could not find a field resolver
86+
"""
87+
event = AppSyncResolverEvent(event)
88+
resolver, config = self._resolver(event.type_name, event.field_name)
89+
kwargs = self._kwargs(event, context, config)
90+
return resolver(**kwargs)
91+
92+
def _resolver(self, type_name: str, field_name: str) -> tuple:
93+
"""Find resolver for field_name
94+
95+
Parameters
96+
----------
97+
type_name : str
98+
Type name
99+
field_name : str
100+
Field name
101+
102+
Returns
103+
-------
104+
tuple
105+
callable function and configuration
106+
"""
107+
full_name = f"{type_name}.{field_name}"
108+
resolver = self._resolvers.get(full_name, self._resolvers.get(f"*.{field_name}"))
109+
if not resolver:
110+
raise ValueError(f"No resolver found for '{full_name}'")
111+
return resolver["func"], resolver["config"]
112+
113+
@staticmethod
114+
def _kwargs(event: AppSyncResolverEvent, context: LambdaContext, config: dict) -> Dict[str, Any]:
115+
"""Get the keyword arguments
116+
117+
Parameters
118+
----------
119+
event : AppSyncResolverEvent
120+
Lambda event
121+
context : LambdaContext
122+
Lambda context
123+
config : dict
124+
Configuration settings
125+
126+
Returns
127+
-------
128+
dict
129+
Returns keyword arguments
130+
"""
131+
kwargs = {**event.arguments}
132+
if config.get("include_event", False):
133+
kwargs["event"] = event
134+
if config.get("include_context", False):
135+
kwargs["context"] = context
136+
return kwargs
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import datetime
2+
import json
3+
import os
4+
5+
import pytest
6+
7+
from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent
8+
from aws_lambda_powertools.utilities.data_classes.appsync_resolver_utils import (
9+
AppSyncResolver,
10+
aws_date,
11+
aws_datetime,
12+
aws_time,
13+
aws_timestamp,
14+
make_id,
15+
)
16+
17+
18+
def load_event(file_name: str) -> dict:
19+
full_file_name = os.path.dirname(os.path.realpath(__file__)) + "/../../events/" + file_name
20+
with open(full_file_name) as fp:
21+
return json.load(fp)
22+
23+
24+
def test_direct_resolver():
25+
_event = load_event("appSyncDirectResolver.json")
26+
27+
app = AppSyncResolver()
28+
29+
@app.resolver(field_name="createSomething", include_context=True)
30+
def create_something(context, id: str): # noqa AA03 VNE003
31+
assert context == {}
32+
return id
33+
34+
def handler(event, context):
35+
return app.resolve(event, context)
36+
37+
result = handler(_event, {})
38+
assert result == "my identifier"
39+
40+
41+
def test_amplify_resolver():
42+
_event = load_event("appSyncResolverEvent.json")
43+
44+
app = AppSyncResolver()
45+
46+
@app.resolver(type_name="Merchant", field_name="locations", include_event=True)
47+
def get_location(event: AppSyncResolverEvent, page: int, size: int, name: str):
48+
assert event is not None
49+
assert page == 2
50+
assert size == 1
51+
return name
52+
53+
def handler(event, context):
54+
return app.resolve(event, context)
55+
56+
result = handler(_event, {})
57+
assert result == "value"
58+
59+
60+
def test_resolver_no_params():
61+
app = AppSyncResolver()
62+
63+
@app.resolver(type_name="Query", field_name="noParams")
64+
def no_params():
65+
return "no_params has no params"
66+
67+
event = {"typeName": "Query", "fieldName": "noParams", "arguments": {}}
68+
result = app.resolve(event, None)
69+
70+
assert result == "no_params has no params"
71+
72+
73+
def test_resolver_value_error():
74+
app = AppSyncResolver()
75+
76+
with pytest.raises(ValueError) as exp:
77+
event = {"typeName": "type", "fieldName": "field", "arguments": {}}
78+
app.resolve(event, None)
79+
80+
assert exp.value.args[0] == "No resolver found for 'type.field'"
81+
82+
83+
def test_make_id():
84+
uuid: str = make_id()
85+
assert isinstance(uuid, str)
86+
assert len(uuid) == 36
87+
88+
89+
def test_aws_date():
90+
date_str = aws_date()
91+
assert isinstance(date_str, str)
92+
assert datetime.datetime.strptime(date_str, "%Y-%m-%d")
93+
94+
95+
def test_aws_time():
96+
time_str = aws_time()
97+
assert isinstance(time_str, str)
98+
assert datetime.datetime.strptime(time_str, "%H:%M:%S")
99+
100+
101+
def test_aws_datetime():
102+
datetime_str = aws_datetime()
103+
assert isinstance(datetime_str, str)
104+
assert datetime.datetime.strptime(datetime_str, "%Y-%m-%dT%H:%M:%SZ")
105+
106+
107+
def test_aws_timestamp():
108+
timestamp = aws_timestamp()
109+
assert isinstance(timestamp, int)

0 commit comments

Comments
 (0)