Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def from_response(cls, response: Optional[Response]) -> HttpResponse:
if response is None:
return cls()

return cls(response._data)
return cls(response._data, status=response.status)


class HttpResponseException(ResponseException):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,13 @@ async def test_from_response(self):
self.assertEqual("application/json", observed.content_type)
self.assertEqual({"foo": "bar"}, await observed.content())

async def test_status_from_response(self):
response = Response({"foo": "bar"}, status=401)
observed = _HttpResponse.from_response(response)
self.assertEqual("application/json", observed.content_type)
self.assertEqual({"foo": "bar"}, await observed.content())
self.assertEqual(401, observed.status)

def test_from_response_already(self):
response = _HttpResponse()
observed = _HttpResponse.from_response(response)
Expand Down
48 changes: 22 additions & 26 deletions packages/plugins/minos-router-graphql/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,11 @@ class QueryService:
```

### Execute query
Send `post` request to `http://your_ip_address:port/graphql` endpoint:
```javascript
{ SimpleQuery }
Send `post` request to `http://your_ip_address:port/service_name/graphql` endpoint:
```json
{
"query": "{ SimpleQuery }"
}
```

You will receive:
Expand Down Expand Up @@ -164,19 +166,11 @@ class QueryService:
return Response(User(firstName="Jack", lastName="Johnson", tweets=563, id=str(id), verified=True))
```

If you POST `/graphql` endpoint passing the query and variables:
If you POST `{service_name}/graphql` endpoint passing the query and variables:

```json
{
"query": "query ($userId: Int!) {
order_query(request: $userId) {
id
firstName
lastName
tweets
verified
}
}",
"query": "query ($userId: Int!) { GetUser(request: $userId) {id firstName lastName tweets verified}}",
"variables": {
"userId": 3
}
Expand All @@ -188,7 +182,7 @@ Yoy will receive:
```json
{
"data": {
"order_query": {
"GetUser": {
"id": "3",
"firstName": "Jack",
"lastName": "Johnson",
Expand Down Expand Up @@ -270,7 +264,7 @@ class User(NamedTuple):


class CommandService:
@enroute.graphql.command(name="GetUser", argument=GraphQLNonNull(user_input_type), output=user_type)
@enroute.graphql.command(name="CreateUser", argument=GraphQLNonNull(user_input_type), output=user_type)
def test_command(self, request: Request):
params = await request.content()
return Response(
Expand All @@ -284,21 +278,17 @@ class CommandService:
)
```

If you POST `/graphql` endpoint passing the query and variables:
If you POST `{service_name}/graphql` endpoint passing the query and variables:

```json
{
"query": "mutation ($userData: UserInputType!) {
createUser(request: $userData) {
id, firstName, lastName, tweets, verified
}
}",
"query": "mutation ($userData: UserInputType!) { CreateUser(request: $userData) {id, firstName, lastName, tweets, verified}}",
"variables": {
"userData": {
"firstName": "John",
"lastName":"Doe",
"tweets": 42,
"verified":true
"firstName": "John",
"lastName": "Doe",
"tweets": 42,
"verified": true
}
}
}
Expand All @@ -309,7 +299,7 @@ Yoy will receive:
```json
{
"data": {
"createUser": {
"CreateUser": {
"id": "4kjjj43-l23k4l3-325kgaa2",
"firstName": "John",
"lastName": "Doe",
Expand All @@ -321,6 +311,12 @@ Yoy will receive:
}
```

### Get Schema
By calling `{service_name}/graphql/schema` with `GET` method, you will receive the schema:
```text
"type Query {\n GetUser(request: Int): UserType\n}\n\ntype UserType {\n id: ID!\n firstName: String!\n lastName: String!\n tweets: Int\n verified: Boolean!\n}\n\ntype Mutation {\n CreateUser(request: UserInputType!): UserType\n}\n\ninput UserInputType {\n firstName: String!\n lastName: String!\n tweets: Int\n verified: Boolean\n}"
```

## Documentation

The official API Reference is publicly available at the [GitHub Pages](https://minos-framework.github.io/minos-python).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ def __init__(self, name: str, output, argument: Optional = None):
self.output = output

def __iter__(self) -> Iterable:
yield from (self.name,)
yield from (
type(self),
self.name,
)


class GraphQlCommandEnrouteDecorator(GraphQlEnrouteDecorator):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import logging
import traceback
from typing import (
Any,
)

from graphql import (
ExecutionResult,
GraphQLError,
GraphQLSchema,
graphql,
print_schema,
Expand All @@ -15,6 +18,8 @@
ResponseException,
)

logger = logging.getLogger(__name__)


class GraphQlHandler:
"""GraphQl Handler"""
Expand All @@ -28,7 +33,9 @@ async def execute_operation(self, request: Request) -> Response:
:param request: The request containing the graphql operation.
:return: A response containing the graphql result.
"""
result = await graphql(schema=self._schema, **(await self._build_graphql_arguments(request)))
arguments = await self._build_graphql_arguments(request)
result = await graphql(schema=self._schema, **arguments)

return self._build_response_from_graphql(result)

@staticmethod
Expand All @@ -47,23 +54,41 @@ async def _build_graphql_arguments(request: Request) -> dict[str, Any]:

return {"source": source, "variable_values": variables}

@staticmethod
def _build_response_from_graphql(result: ExecutionResult) -> Response:
errors = result.errors
if errors is None:
errors = list()
def _build_response_from_graphql(self, result: ExecutionResult) -> Response:
content = {"data": result.data}
if result.errors is not None:
content["errors"] = [err.message for err in result.errors]
self._log_errors(result.errors)

status = 200
status = self._get_status(result)

if len(errors):
status = 500
for error in errors:
if isinstance(error.original_error, ResponseException):
status = error.original_error.status
return Response(content, status=status)

content = {"data": result.data, "errors": [err.message for err in errors]}
@staticmethod
def _get_status(result: ExecutionResult) -> int:
status = 200
for error in result.errors or []:
if error.original_error is None:
current = 400
elif isinstance(error.original_error, ResponseException):
current = error.original_error.status
else:
current = 500
status = max(status, current)
return status

return Response(content, status=status)
@staticmethod
def _log_errors(errors: list[GraphQLError]) -> None:
for error in errors:
if error.original_error is None:
tb = repr(error)
else:
tb = "".join(traceback.format_tb(error.__traceback__))

if error.original_error is None or isinstance(error.original_error, ResponseException):
logger.error(f"Raised an application exception:\n {tb}")
else:
logger.exception(f"Raised a system exception:\n {tb}")

async def get_schema(self, request: Request) -> Response:
"""Get schema
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ def _filter_routes(self, routes: dict[EnrouteDecorator, Callable]) -> dict[Enrou
}
schema = GraphQLSchemaBuilder.build(routes)
handler = GraphQlHandler(schema)
service_name = self._config.get_name().lower()
return {
HttpEnrouteDecorator("/graphql", "POST"): handler.execute_operation,
HttpEnrouteDecorator("/graphql/schema", "GET"): handler.get_schema,
HttpEnrouteDecorator(f"/{service_name}/graphql", "POST"): handler.execute_operation,
HttpEnrouteDecorator(f"/{service_name}/graphql/schema", "GET"): handler.get_schema,
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
service:
name: Order
name: Foo
aggregate: tests.utils.Order
services:
- tests.services.commands.CommandService
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ async def resolve_ticket_raises(request: Request):
raise ResponseException("Some error.", status=403)


async def resolve_ticket_raises_system(request: Request):
raise ValueError()


class TestGraphQlHandler(unittest.IsolatedAsyncioTestCase):
CONFIG_FILE_PATH = BASE_PATH / "test_config.yml"
_config = Config(CONFIG_FILE_PATH)
Expand All @@ -66,7 +70,8 @@ async def test_execute_operation(self):
result = await handler.execute_operation(request)

self.assertEqual(200, result.status)
self.assertDictEqual(await result.content(), {"data": {"order_query": "ticket #4"}, "errors": []})
expected_content = {"data": {"order_query": "ticket #4"}}
self.assertDictEqual(expected_content, await result.content())

async def test_execute_operation_raises(self):
routes = {
Expand All @@ -83,6 +88,21 @@ async def test_execute_operation_raises(self):

self.assertEqual(403, result.status)

async def test_execute_operation_raises_system(self):
routes = {
GraphQlQueryEnrouteDecorator(name="ticket_query", output=GraphQLString): resolve_ticket_raises_system,
}

schema = GraphQLSchemaBuilder.build(routes=routes)

handler = GraphQlHandler(schema)

request = InMemoryRequest(content="{ ticket_query }")

result = await handler.execute_operation(request)

self.assertEqual(500, result.status)

async def test_execute_wrong_operation(self):
routes = {
GraphQlQueryEnrouteDecorator(name="order_query", output=GraphQLString): callback_fn,
Expand All @@ -99,8 +119,8 @@ async def test_execute_wrong_operation(self):

content = await result.content()

self.assertEqual(500, result.status)
self.assertNotEqual(content["errors"], [])
self.assertEqual(400, result.status)
self.assertEqual(1, len(content["errors"]))

async def test_schema(self):
routes = {
Expand Down Expand Up @@ -148,8 +168,8 @@ async def test_query_with_variables(self):
content = await result.content()

self.assertEqual(200, result.status)
self.assertDictEqual({"order_query": 3}, content["data"])
self.assertEqual([], content["errors"])
expected_content = {"data": {"order_query": 3}}
self.assertDictEqual(expected_content, content)

async def test_simple_query(self):
routes = {GraphQlQueryEnrouteDecorator(name="SimpleQuery", output=GraphQLString): resolve_simple_query}
Expand All @@ -171,8 +191,8 @@ async def test_simple_query(self):
content = await result.content()

self.assertEqual(200, result.status)
self.assertDictEqual({"SimpleQuery": "ABCD"}, content["data"])
self.assertEqual([], content["errors"])
expected_content = {"data": {"SimpleQuery": "ABCD"}}
self.assertDictEqual(expected_content, content)

async def test_query_with_variables_return_user(self):
routes = {GraphQlQueryEnrouteDecorator(name="order_query", argument=GraphQLInt, output=user_type): resolve_user}
Expand Down Expand Up @@ -204,11 +224,12 @@ async def test_query_with_variables_return_user(self):
content = await result.content()

self.assertEqual(200, result.status)
self.assertDictEqual(
{"order_query": {"id": "3", "firstName": "Jack", "lastName": "Johnson", "tweets": 563, "verified": True}},
content["data"],
)
self.assertEqual([], content["errors"])
expected_content = {
"data": {
"order_query": {"id": "3", "firstName": "Jack", "lastName": "Johnson", "tweets": 563, "verified": True}
}
}
self.assertDictEqual(expected_content, content)

async def test_mutation(self):
routes = {
Expand Down Expand Up @@ -240,19 +261,18 @@ async def test_mutation(self):
content = await result.content()

self.assertEqual(200, result.status)
self.assertDictEqual(
{
expected_content = {
"data": {
"createUser": {
"id": "4kjjj43-l23k4l3-325kgaa2",
"firstName": "John",
"lastName": "Doe",
"tweets": 42,
"verified": True,
}
},
content["data"],
)
self.assertEqual([], content["errors"])
}
}
self.assertDictEqual(expected_content, content)


if __name__ == "__main__":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def test_from_config(self):

self.assertIsInstance(router, GraphQlHttpRouter)
self.assertEqual(
{HttpEnrouteDecorator("/graphql", "POST"), HttpEnrouteDecorator("/graphql/schema", "GET")},
{HttpEnrouteDecorator("/foo/graphql", "POST"), HttpEnrouteDecorator("/foo/graphql/schema", "GET")},
router.routes.keys(),
)

Expand Down