Appsync: Implement get_introspection_schema (#6337)
This commit is contained in:
parent
0144953273
commit
18b6bdf20a
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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")
|
||||
|
Loading…
Reference in New Issue
Block a user