Appsync: Implement get_introspection_schema (#6337)

This commit is contained in:
Dave Pretty 2023-05-26 02:40:52 +10:00 committed by GitHub
parent 0144953273
commit 18b6bdf20a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 238 additions and 3 deletions

View File

@ -54,7 +54,7 @@ appsync
- [ ] get_domain_name - [ ] get_domain_name
- [ ] get_function - [ ] get_function
- [X] get_graphql_api - [X] get_graphql_api
- [ ] get_introspection_schema - [X] get_introspection_schema
- [ ] get_resolver - [ ] get_resolver
- [X] get_schema_creation_status - [X] get_schema_creation_status
- [X] get_type - [X] get_type

View File

@ -12,3 +12,16 @@ class GraphqlAPINotFound(AppSyncExceptions):
def __init__(self, api_id: str): def __init__(self, api_id: str):
super().__init__("NotFoundException", f"GraphQL API {api_id} not found.") super().__init__("NotFoundException", f"GraphQL API {api_id} not found.")
self.description = json.dumps({"message": self.message}) self.description = json.dumps({"message": self.message})
class GraphQLSchemaException(AppSyncExceptions):
code = 400
def __init__(self, message: str):
super().__init__("GraphQLSchemaException", message)
self.description = json.dumps({"message": self.message})
class BadRequestException(AppSyncExceptions):
def __init__(self, message: str):
super().__init__("BadRequestException", message)

View File

@ -1,4 +1,5 @@
import base64 import base64
import json
from datetime import timedelta, datetime, timezone from datetime import timedelta, datetime, timezone
from typing import Any, Dict, Iterable, List, Optional, Tuple from typing import Any, Dict, Iterable, List, Optional, Tuple
from moto.core import BaseBackend, BackendDict, BaseModel from moto.core import BaseBackend, BackendDict, BaseModel
@ -6,7 +7,34 @@ from moto.core.utils import unix_time
from moto.moto_api._internal import mock_random from moto.moto_api._internal import mock_random
from moto.utilities.tagging_service import TaggingService from moto.utilities.tagging_service import TaggingService
from .exceptions import GraphqlAPINotFound from .exceptions import GraphqlAPINotFound, GraphQLSchemaException, BadRequestException
# AWS custom scalars and directives
# https://github.com/dotansimha/graphql-code-generator/discussions/4311#discussioncomment-2921796
AWS_CUSTOM_GRAPHQL = """scalar AWSTime
scalar AWSDateTime
scalar AWSTimestamp
scalar AWSEmail
scalar AWSJSON
scalar AWSURL
scalar AWSPhone
scalar AWSIPAddress
scalar BigInt
scalar Double
directive @aws_subscribe(mutations: [String!]!) on FIELD_DEFINITION
# Allows transformer libraries to deprecate directive arguments.
directive @deprecated(reason: String!) on INPUT_FIELD_DEFINITION | ENUM
directive @aws_auth(cognito_groups: [String!]!) on FIELD_DEFINITION
directive @aws_api_key on FIELD_DEFINITION | OBJECT
directive @aws_iam on FIELD_DEFINITION | OBJECT
directive @aws_oidc on FIELD_DEFINITION | OBJECT
directive @aws_cognito_user_pools(
cognito_groups: [String!]
) on FIELD_DEFINITION | OBJECT
"""
class GraphqlSchema(BaseModel): class GraphqlSchema(BaseModel):
@ -49,6 +77,27 @@ class GraphqlSchema(BaseModel):
self.status = "FAILED" self.status = "FAILED"
self.parse_error = str(e) self.parse_error = str(e)
def get_introspection_schema(self, format_: str, include_directives: bool) -> str:
from graphql import (
print_schema,
build_client_schema,
introspection_from_schema,
build_schema,
)
schema = build_schema(self.definition + AWS_CUSTOM_GRAPHQL)
introspection_data = introspection_from_schema(schema, descriptions=False)
if not include_directives:
introspection_data["__schema"]["directives"] = []
if format_ == "SDL":
return print_schema(build_client_schema(introspection_data))
elif format_ == "JSON":
return json.dumps(introspection_data)
else:
raise BadRequestException(message=f"Invalid format {format_} given")
class GraphqlAPIKey(BaseModel): class GraphqlAPIKey(BaseModel):
def __init__(self, description: str, expires: Optional[int]): def __init__(self, description: str, expires: Optional[int]):
@ -254,6 +303,15 @@ class AppSyncBackend(BaseBackend):
raise GraphqlAPINotFound(api_id) raise GraphqlAPINotFound(api_id)
return self.graphql_apis[api_id] return self.graphql_apis[api_id]
def get_graphql_schema(self, api_id: str) -> GraphqlSchema:
graphql_api = self.get_graphql_api(api_id)
if not graphql_api.graphql_schema:
# When calling get_introspetion_schema without a graphql schema
# the response GraphQLSchemaException exception includes InvalidSyntaxError
# in the message. This might not be the case for other methods.
raise GraphQLSchemaException(message="InvalidSyntaxError")
return graphql_api.graphql_schema
def delete_graphql_api(self, api_id: str) -> None: def delete_graphql_api(self, api_id: str) -> None:
self.graphql_apis.pop(api_id) self.graphql_apis.pop(api_id)

View File

@ -69,6 +69,11 @@ class AppSyncResponse(BaseResponse):
if request.method == "GET": if request.method == "GET":
return self.get_type() return self.get_type()
def schema(self, request: Any, full_url: str, headers: Any) -> TYPE_RESPONSE: # type: ignore[return]
self.setup_class(request, full_url, headers)
if request.method == "GET":
return self.get_introspection_schema()
def create_graphql_api(self) -> TYPE_RESPONSE: def create_graphql_api(self) -> TYPE_RESPONSE:
params = json.loads(self.body) params = json.loads(self.body)
name = params.get("name") name = params.get("name")
@ -230,3 +235,19 @@ class AppSyncResponse(BaseResponse):
api_id=api_id, type_name=type_name, type_format=type_format api_id=api_id, type_name=type_name, type_format=type_format
) )
return 200, {}, json.dumps(dict(type=graphql_type)) return 200, {}, json.dumps(dict(type=graphql_type))
def get_introspection_schema(self) -> TYPE_RESPONSE: # type: ignore[return]
api_id = self.path.split("/")[-2]
format_ = self.querystring.get("format")[0] # type: ignore[index]
if self.querystring.get("includeDirectives"):
include_directives = (
self.querystring.get("includeDirectives")[0].lower() == "true" # type: ignore[index]
)
else:
include_directives = True
graphql_schema = self.appsync_backend.get_graphql_schema(api_id=api_id)
schema = graphql_schema.get_introspection_schema(
format_=format_, include_directives=include_directives
)
return 200, {}, schema

View File

@ -15,6 +15,7 @@ url_paths = {
"{0}/v1/apis/(?P<api_id>[^/]+)/apikeys$": response.api_key, "{0}/v1/apis/(?P<api_id>[^/]+)/apikeys$": response.api_key,
"{0}/v1/apis/(?P<api_id>[^/]+)/apikeys/(?P<api_key_id>[^/]+)$": response.api_key_individual, "{0}/v1/apis/(?P<api_id>[^/]+)/apikeys/(?P<api_key_id>[^/]+)$": response.api_key_individual,
"{0}/v1/apis/(?P<api_id>[^/]+)/schemacreation$": response.schemacreation, "{0}/v1/apis/(?P<api_id>[^/]+)/schemacreation$": response.schemacreation,
"{0}/v1/apis/(?P<api_id>[^/]+)/schema$": response.schema,
"{0}/v1/tags/(?P<resource_arn>.+)$": response.tags, "{0}/v1/tags/(?P<resource_arn>.+)$": response.tags,
"{0}/v1/tags/(?P<resource_arn_pt1>.+)/(?P<resource_arn_pt2>.+)$": response.tags, "{0}/v1/tags/(?P<resource_arn_pt1>.+)/(?P<resource_arn_pt2>.+)$": response.tags,
"{0}/v1/apis/(?P<api_id>[^/]+)/types/(?P<type_name>.+)$": response.types, "{0}/v1/apis/(?P<api_id>[^/]+)/types/(?P<type_name>.+)$": response.types,

View File

@ -1,6 +1,8 @@
import boto3 import boto3
import sure # noqa # pylint: disable=unused-import import sure # noqa # pylint: disable=unused-import
import json
from botocore.exceptions import ClientError
import pytest
from moto import mock_appsync from moto import mock_appsync
schema = """type Mutation { schema = """type Mutation {
@ -23,6 +25,33 @@ schema {
}""" }"""
schema_with_directives = """type Mutation {
putPost(id: ID!, title: String!): Post
}
"My custom post type"
type Post {
id: ID!
title: String!
createdAt: AWSDateTime!
}
type Query {
singlePost(id: ID!): Post
}
schema {
query: Query
mutation: Mutation
subscription: Subscription
}
type Subscription {
onPostCreated(id: ID!): Post @aws_subscribe(mutations: ["putPost"])
}
"""
@mock_appsync @mock_appsync
def test_start_schema_creation(): def test_start_schema_creation():
@ -86,3 +115,116 @@ def test_get_type_from_schema():
query_type = client.get_type(apiId=api_id, typeName="Query", format="SDL")["type"] query_type = client.get_type(apiId=api_id, typeName="Query", format="SDL")["type"]
query_type.should.have.key("name").equals("Query") query_type.should.have.key("name").equals("Query")
query_type.shouldnt.have.key("description") query_type.shouldnt.have.key("description")
@mock_appsync
def test_get_introspection_schema_raise_gql_schema_error_if_no_schema():
client = boto3.client("appsync", region_name="us-east-2")
api_id = client.create_graphql_api(name="api1", authenticationType="API_KEY")[
"graphqlApi"
]["apiId"]
with pytest.raises(ClientError) as exc:
client.get_introspection_schema(apiId=api_id, format="SDL")
err = exc.value.response["Error"]
err["Code"].should.equal("GraphQLSchemaException")
# AWS API appears to return InvalidSyntaxError if no schema exists
err["Message"].should.equal("InvalidSyntaxError")
@mock_appsync
def test_get_introspection_schema_sdl():
client = boto3.client("appsync", region_name="us-east-2")
api_id = client.create_graphql_api(name="api1", authenticationType="API_KEY")[
"graphqlApi"
]["apiId"]
client.start_schema_creation(apiId=api_id, definition=schema.encode("utf-8"))
resp = client.get_introspection_schema(apiId=api_id, format="SDL")
schema_sdl = resp["schema"].read().decode("utf-8")
schema_sdl.should.contain("putPost(")
schema_sdl.should.contain("singlePost(id: ID!): Post")
@mock_appsync
def test_get_introspection_schema_json():
client = boto3.client("appsync", region_name="us-east-2")
api_id = client.create_graphql_api(name="api1", authenticationType="API_KEY")[
"graphqlApi"
]["apiId"]
client.start_schema_creation(apiId=api_id, definition=schema.encode("utf-8"))
resp = client.get_introspection_schema(apiId=api_id, format="JSON")
schema_json = json.loads(resp["schema"].read().decode("utf-8"))
schema_json.should.have.key("__schema")
schema_json["__schema"].should.have.key("queryType")
schema_json["__schema"].should.have.key("mutationType")
schema_json["__schema"].should.have.key("subscriptionType")
schema_json["__schema"].should.have.key("types")
schema_json["__schema"].should.have.key("directives")
@mock_appsync
def test_get_introspection_schema_bad_format():
client = boto3.client("appsync", region_name="us-east-2")
api_id = client.create_graphql_api(name="api1", authenticationType="API_KEY")[
"graphqlApi"
]["apiId"]
client.start_schema_creation(apiId=api_id, definition=schema.encode("utf-8"))
with pytest.raises(ClientError) as exc:
client.get_introspection_schema(apiId=api_id, format="NotAFormat")
err = exc.value.response["Error"]
err["Code"].should.equal("BadRequestException")
err["Message"].should.equal("Invalid format NotAFormat given")
@mock_appsync
def test_get_introspection_schema_include_directives_true():
client = boto3.client("appsync", region_name="us-east-2")
api_id = client.create_graphql_api(name="api1", authenticationType="API_KEY")[
"graphqlApi"
]["apiId"]
client.start_schema_creation(
apiId=api_id, definition=schema_with_directives.encode("utf-8")
)
resp = client.get_introspection_schema(
apiId=api_id, format="SDL", includeDirectives=True
)
schema_sdl = resp["schema"].read().decode("utf-8")
schema_sdl.should.contain("@aws_subscribe")
@mock_appsync
def test_get_introspection_schema_include_directives_false():
client = boto3.client("appsync", region_name="us-east-2")
api_id = client.create_graphql_api(name="api1", authenticationType="API_KEY")[
"graphqlApi"
]["apiId"]
client.start_schema_creation(
apiId=api_id, definition=schema_with_directives.encode("utf-8")
)
resp = client.get_introspection_schema(
apiId=api_id, format="SDL", includeDirectives=False
)
schema_sdl = resp["schema"].read().decode("utf-8")
schema_sdl.shouldnt.contain("@aws_subscribe")