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