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_function
- [X] get_graphql_api
- [ ] get_introspection_schema
- [X] get_introspection_schema
- [ ] get_resolver
- [X] get_schema_creation_status
- [X] get_type

View File

@ -12,3 +12,16 @@ class GraphqlAPINotFound(AppSyncExceptions):
def __init__(self, api_id: str):
super().__init__("NotFoundException", f"GraphQL API {api_id} not found.")
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 json
from datetime import timedelta, datetime, timezone
from typing import Any, Dict, Iterable, List, Optional, Tuple
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.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):
@ -49,6 +77,27 @@ class GraphqlSchema(BaseModel):
self.status = "FAILED"
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):
def __init__(self, description: str, expires: Optional[int]):
@ -254,6 +303,15 @@ class AppSyncBackend(BaseBackend):
raise GraphqlAPINotFound(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:
self.graphql_apis.pop(api_id)

View File

@ -69,6 +69,11 @@ class AppSyncResponse(BaseResponse):
if request.method == "GET":
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:
params = json.loads(self.body)
name = params.get("name")
@ -230,3 +235,19 @@ class AppSyncResponse(BaseResponse):
api_id=api_id, type_name=type_name, type_format=type_format
)
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/(?P<api_key_id>[^/]+)$": response.api_key_individual,
"{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_pt1>.+)/(?P<resource_arn_pt2>.+)$": response.tags,
"{0}/v1/apis/(?P<api_id>[^/]+)/types/(?P<type_name>.+)$": response.types,

View File

@ -1,6 +1,8 @@
import boto3
import sure # noqa # pylint: disable=unused-import
import json
from botocore.exceptions import ClientError
import pytest
from moto import mock_appsync
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
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.should.have.key("name").equals("Query")
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")