Skip to content

Commit 03a37fe

Browse files
vaibhav170jfuss
andauthored
feat: add introspection, query & resolver limits properties in appsync graphql api (#3668)
Co-authored-by: Jacob Fuss <[email protected]>
1 parent 51fec4b commit 03a37fe

13 files changed

+503
-0
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
[
2+
{
3+
"LogicalResourceId": "SuperCoolAPI",
4+
"ResourceType": "AWS::AppSync::GraphQLApi"
5+
},
6+
{
7+
"LogicalResourceId": "SuperCoolAPICloudWatchRole",
8+
"ResourceType": "AWS::IAM::Role"
9+
},
10+
{
11+
"LogicalResourceId": "SuperCoolAPISchema",
12+
"ResourceType": "AWS::AppSync::GraphQLSchema"
13+
},
14+
{
15+
"LogicalResourceId": "SuperCoolAPIQuerygetBook",
16+
"ResourceType": "AWS::AppSync::Resolver"
17+
},
18+
{
19+
"LogicalResourceId": "SuperCoolAPINoneDataSource",
20+
"ResourceType": "AWS::AppSync::DataSource"
21+
},
22+
{
23+
"LogicalResourceId": "SuperCoolAPIprocessQuery",
24+
"ResourceType": "AWS::AppSync::FunctionConfiguration"
25+
},
26+
{
27+
"LogicalResourceId": "SuperCoolAPIMyApiKey",
28+
"ResourceType": "AWS::AppSync::ApiKey"
29+
},
30+
{
31+
"LogicalResourceId": "IntrospectionDisableSuperCoolAPI",
32+
"ResourceType": "AWS::AppSync::GraphQLApi"
33+
},
34+
{
35+
"LogicalResourceId": "IntrospectionDisableSuperCoolAPICloudWatchRole",
36+
"ResourceType": "AWS::IAM::Role"
37+
},
38+
{
39+
"LogicalResourceId": "IntrospectionDisableSuperCoolAPISchema",
40+
"ResourceType": "AWS::AppSync::GraphQLSchema"
41+
},
42+
{
43+
"LogicalResourceId": "IntrospectionDisableSuperCoolAPIQuerygetBook",
44+
"ResourceType": "AWS::AppSync::Resolver"
45+
},
46+
{
47+
"LogicalResourceId": "IntrospectionDisableSuperCoolAPINoneDataSource",
48+
"ResourceType": "AWS::AppSync::DataSource"
49+
},
50+
{
51+
"LogicalResourceId": "IntrospectionDisableSuperCoolAPIprocessQuery",
52+
"ResourceType": "AWS::AppSync::FunctionConfiguration"
53+
},
54+
{
55+
"LogicalResourceId": "IntrospectionDisableSuperCoolAPIMyApiKey",
56+
"ResourceType": "AWS::AppSync::ApiKey"
57+
}
58+
]
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
Transform: AWS::Serverless-2016-10-31
2+
Resources:
3+
SuperCoolAPI:
4+
Type: AWS::Serverless::GraphQLApi
5+
Properties:
6+
SchemaInline: |
7+
type Book {
8+
bookName: String
9+
id: ID
10+
}
11+
type Query { getBook(bookName: String): Book }
12+
OwnerContact: blah-blah
13+
Auth:
14+
Type: API_KEY
15+
ApiKeys:
16+
MyApiKey: {}
17+
Functions:
18+
processQuery:
19+
Runtime:
20+
Name: APPSYNC_JS
21+
Version: 1.0.0
22+
DataSource: NONE
23+
InlineCode: |
24+
import { util } from '@aws-appsync/utils';
25+
26+
export function request(ctx) {
27+
const id = util.autoId();
28+
return { payload: { ...ctx.args, id } };
29+
}
30+
31+
export function response(ctx) {
32+
return ctx.result;
33+
}
34+
Resolvers:
35+
Query:
36+
getBook:
37+
Pipeline:
38+
- processQuery
39+
40+
IntrospectionDisableSuperCoolAPI:
41+
Type: AWS::Serverless::GraphQLApi
42+
Properties:
43+
SchemaInline: |
44+
type Book {
45+
bookName: String
46+
id: ID
47+
}
48+
type Query { getBook(bookName: String): Book }
49+
OwnerContact: blah-blah
50+
IntrospectionConfig: DISABLED
51+
QueryDepthLimit: 10
52+
ResolverCountLimit: 100
53+
Auth:
54+
Type: API_KEY
55+
ApiKeys:
56+
MyApiKey: {}
57+
Functions:
58+
processQuery:
59+
Runtime:
60+
Name: APPSYNC_JS
61+
Version: 1.0.0
62+
DataSource: NONE
63+
InlineCode: |
64+
import { util } from '@aws-appsync/utils';
65+
66+
export function request(ctx) {
67+
const id = util.autoId();
68+
return { payload: { ...ctx.args, id } };
69+
}
70+
71+
export function response(ctx) {
72+
return ctx.result;
73+
}
74+
Resolvers:
75+
Query:
76+
getBook:
77+
Pipeline:
78+
- processQuery
79+
Outputs:
80+
SuperCoolAPI:
81+
Description: AppSync API
82+
Value: !GetAtt SuperCoolAPI.GraphQLUrl
83+
MyApiKey:
84+
Description: API Id
85+
Value: !GetAtt SuperCoolAPIMyApiKey.ApiKey
86+
IntrospectionDisableSuperCoolAPI:
87+
Description: AppSync API
88+
Value: !GetAtt IntrospectionDisableSuperCoolAPI.GraphQLUrl
89+
IntrospectionDisableSuperCoolAPIMyApiKey:
90+
Description: API Id
91+
Value: !GetAtt IntrospectionDisableSuperCoolAPIMyApiKey.ApiKey
92+
93+
Metadata:
94+
SamTransformTest: true
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import json
2+
from unittest.case import skipIf
3+
4+
import pytest
5+
import requests
6+
7+
from integration.config.service_names import APP_SYNC
8+
from integration.helpers.base_test import BaseTest
9+
from integration.helpers.resource import current_region_does_not_support
10+
11+
12+
def execute_and_verify_appsync_query(url, api_key, query):
13+
"""
14+
Executes a query to an AppSync GraphQLApi.
15+
16+
Also checks that the response is 200 and does not contain errors before returning.
17+
"""
18+
headers = {
19+
"Content-Type": "application/json",
20+
"x-api-key": api_key,
21+
}
22+
payload = {"query": query}
23+
24+
response = requests.post(url, json=payload, headers=headers)
25+
response.raise_for_status()
26+
data = response.json()
27+
if "errors" in data:
28+
raise Exception(json.dumps(data["errors"]))
29+
30+
return data
31+
32+
33+
@skipIf(current_region_does_not_support([APP_SYNC]), "AppSync is not supported in this testing region")
34+
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
35+
class TestGraphQLApiConfiguration(BaseTest):
36+
def test_api(self):
37+
file_name = "single/graphqlapi-configuration"
38+
self.create_and_verify_stack(file_name)
39+
40+
outputs = self.get_stack_outputs()
41+
42+
url = outputs["SuperCoolAPI"]
43+
api_key = outputs["MyApiKey"]
44+
45+
introspection_disable_api_url = outputs["IntrospectionDisableSuperCoolAPI"]
46+
introspection_disable_api_key = outputs["IntrospectionDisableSuperCoolAPIMyApiKey"]
47+
48+
book_name = "GoodBook"
49+
query = f"""
50+
query MyQuery {{
51+
getBook(
52+
bookName: "{book_name}"
53+
) {{
54+
id
55+
bookName
56+
}}
57+
}}
58+
"""
59+
60+
response = execute_and_verify_appsync_query(url, api_key, query)
61+
self.assertEqual(response["data"]["getBook"]["bookName"], book_name)
62+
63+
introspection_disable_query_response = execute_and_verify_appsync_query(
64+
introspection_disable_api_url, introspection_disable_api_key, query
65+
)
66+
self.assertEqual(introspection_disable_query_response["data"]["getBook"]["bookName"], book_name)
67+
68+
query_introsepction = """
69+
query myQuery {
70+
__schema {
71+
types {
72+
name
73+
}
74+
}
75+
}
76+
"""
77+
78+
introspection_query_response = execute_and_verify_appsync_query(url, api_key, query_introsepction)
79+
self.assertIsNotNone(introspection_query_response["data"]["__schema"])
80+
81+
# sending introspection query and expecting error as introspection is DISABLED for this API using template file
82+
with self.assertRaises(Exception):
83+
execute_and_verify_appsync_query(
84+
introspection_disable_api_url, introspection_disable_api_key, query_introsepction
85+
)

pytest.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,5 @@ filterwarnings =
2020
ignore::pytest.PytestUnraisableExceptionWarning
2121
# https://github.com/urllib3/urllib3/blob/main/src/urllib3/poolmanager.py#L313
2222
ignore::DeprecationWarning:urllib3.*:
23+
# https://github.com/boto/boto3/issues/3889
24+
ignore:datetime.datetime.utcnow

samtranslator/internal/model/appsync.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ class GraphQLApi(Resource):
115115
"AdditionalAuthenticationProviders": GeneratedProperty(),
116116
"Visibility": GeneratedProperty(),
117117
"OwnerContact": GeneratedProperty(),
118+
"IntrospectionConfig": GeneratedProperty(),
119+
"QueryDepthLimit": GeneratedProperty(),
120+
"ResolverCountLimit": GeneratedProperty(),
118121
}
119122

120123
Name: str
@@ -128,6 +131,9 @@ class GraphQLApi(Resource):
128131
LogConfig: Optional[LogConfigType]
129132
Visibility: Optional[str]
130133
OwnerContact: Optional[str]
134+
IntrospectionConfig: Optional[str]
135+
QueryDepthLimit: Optional[int]
136+
ResolverCountLimit: Optional[int]
131137

132138
runtime_attrs = {"api_id": lambda self: fnGetAtt(self.logical_id, "ApiId")}
133139

samtranslator/internal/schema_source/aws_serverless_graphqlapi.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ class Properties(BaseModel):
164164
Cache: Optional[Cache]
165165
Visibility: Optional[PassThroughProp]
166166
OwnerContact: Optional[PassThroughProp]
167+
IntrospectionConfig: Optional[PassThroughProp]
168+
QueryDepthLimit: Optional[PassThroughProp]
169+
ResolverCountLimit: Optional[PassThroughProp]
167170

168171

169172
class Resource(BaseModel):

samtranslator/model/sam_resources.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2226,6 +2226,9 @@ class SamGraphQLApi(SamResourceMacro):
22262226
"Cache": Property(False, IS_DICT),
22272227
"Visibility": PassThroughProperty(False),
22282228
"OwnerContact": PassThroughProperty(False),
2229+
"IntrospectionConfig": PassThroughProperty(False),
2230+
"QueryDepthLimit": PassThroughProperty(False),
2231+
"ResolverCountLimit": PassThroughProperty(False),
22292232
}
22302233

22312234
Auth: List[Dict[str, Any]]
@@ -2243,6 +2246,9 @@ class SamGraphQLApi(SamResourceMacro):
22432246
Cache: Optional[Dict[str, Any]]
22442247
Visibility: Optional[PassThrough]
22452248
OwnerContact: Optional[PassThrough]
2249+
IntrospectionConfig: Optional[PassThrough]
2250+
QueryDepthLimit: Optional[PassThrough]
2251+
ResolverCountLimit: Optional[PassThrough]
22462252

22472253
# stop validation so we can use class variables for tracking state
22482254
validate_setattr = False
@@ -2322,6 +2328,13 @@ def _construct_appsync_api_resources(
23222328
api.OwnerContact = passthrough_value(model.OwnerContact)
23232329
api.XrayEnabled = model.XrayEnabled
23242330

2331+
if model.IntrospectionConfig:
2332+
api.IntrospectionConfig = passthrough_value(model.IntrospectionConfig)
2333+
if model.QueryDepthLimit:
2334+
api.QueryDepthLimit = passthrough_value(model.QueryDepthLimit)
2335+
if model.ResolverCountLimit:
2336+
api.ResolverCountLimit = passthrough_value(model.ResolverCountLimit)
2337+
23252338
lambda_auth_arns = self._parse_and_set_auth_properties(api, model.Auth)
23262339
auth_connectors = [
23272340
self._construct_lambda_auth_connector(api, arn, i) for i, arn in enumerate(lambda_auth_arns, 1)

samtranslator/schema/schema.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279729,6 +279729,9 @@
279729279729
"title": "Functions",
279730279730
"type": "object"
279731279731
},
279732+
"IntrospectionConfig": {
279733+
"$ref": "#/definitions/PassThroughProp"
279734+
},
279732279735
"Logging": {
279733279736
"anyOf": [
279734279737
{
@@ -279746,6 +279749,12 @@
279746279749
"OwnerContact": {
279747279750
"$ref": "#/definitions/PassThroughProp"
279748279751
},
279752+
"QueryDepthLimit": {
279753+
"$ref": "#/definitions/PassThroughProp"
279754+
},
279755+
"ResolverCountLimit": {
279756+
"$ref": "#/definitions/PassThroughProp"
279757+
},
279749279758
"Resolvers": {
279750279759
"additionalProperties": {
279751279760
"additionalProperties": {

schema_source/sam.schema.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6659,6 +6659,9 @@
66596659
"title": "Functions",
66606660
"type": "object"
66616661
},
6662+
"IntrospectionConfig": {
6663+
"$ref": "#/definitions/PassThroughProp"
6664+
},
66626665
"Logging": {
66636666
"anyOf": [
66646667
{
@@ -6676,6 +6679,12 @@
66766679
"OwnerContact": {
66776680
"$ref": "#/definitions/PassThroughProp"
66786681
},
6682+
"QueryDepthLimit": {
6683+
"$ref": "#/definitions/PassThroughProp"
6684+
},
6685+
"ResolverCountLimit": {
6686+
"$ref": "#/definitions/PassThroughProp"
6687+
},
66796688
"Resolvers": {
66806689
"additionalProperties": {
66816690
"additionalProperties": {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
Transform: AWS::Serverless-2016-10-31
2+
Resources:
3+
SuperCoolAPI:
4+
Type: AWS::Serverless::GraphQLApi
5+
Properties:
6+
SchemaInline: |
7+
type Book {
8+
bookName: String
9+
}
10+
type Query { getBook(bookName: String): Book }
11+
Visibility: PRIVATE
12+
OwnerContact: blah-blah
13+
IntrospectionConfig: DISABLED
14+
QueryDepthLimit: 10
15+
ResolverCountLimit: 100
16+
Auth:
17+
Type: AWS_IAM

0 commit comments

Comments
 (0)