Skip to content

Commit 9ab0b23

Browse files
Merge pull request #316 from minos-framework/issue-315-graphql-plugin-documentation
#315 - GraphQL documentation correction
2 parents 349a0af + 6ab4145 commit 9ab0b23

File tree

9 files changed

+116
-64
lines changed

9 files changed

+116
-64
lines changed

packages/core/minos-microservice-networks/minos/networks/http/requests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def from_response(cls, response: Optional[Response]) -> HttpResponse:
119119
if response is None:
120120
return cls()
121121

122-
return cls(response._data)
122+
return cls(response._data, status=response.status)
123123

124124

125125
class HttpResponseException(ResponseException):

packages/core/minos-microservice-networks/tests/test_networks/test_http/test_requests.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,13 @@ async def test_from_response(self):
112112
self.assertEqual("application/json", observed.content_type)
113113
self.assertEqual({"foo": "bar"}, await observed.content())
114114

115+
async def test_status_from_response(self):
116+
response = Response({"foo": "bar"}, status=401)
117+
observed = _HttpResponse.from_response(response)
118+
self.assertEqual("application/json", observed.content_type)
119+
self.assertEqual({"foo": "bar"}, await observed.content())
120+
self.assertEqual(401, observed.status)
121+
115122
def test_from_response_already(self):
116123
response = _HttpResponse()
117124
observed = _HttpResponse.from_response(response)

packages/plugins/minos-router-graphql/README.md

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,11 @@ class QueryService:
4747
```
4848

4949
### Execute query
50-
Send `post` request to `http://your_ip_address:port/graphql` endpoint:
51-
```javascript
52-
{ SimpleQuery }
50+
Send `post` request to `http://your_ip_address:port/service_name/graphql` endpoint:
51+
```json
52+
{
53+
"query": "{ SimpleQuery }"
54+
}
5355
```
5456

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

167-
If you POST `/graphql` endpoint passing the query and variables:
169+
If you POST `{service_name}/graphql` endpoint passing the query and variables:
168170

169171
```json
170172
{
171-
"query": "query ($userId: Int!) {
172-
order_query(request: $userId) {
173-
id
174-
firstName
175-
lastName
176-
tweets
177-
verified
178-
}
179-
}",
173+
"query": "query ($userId: Int!) { GetUser(request: $userId) {id firstName lastName tweets verified}}",
180174
"variables": {
181175
"userId": 3
182176
}
@@ -188,7 +182,7 @@ Yoy will receive:
188182
```json
189183
{
190184
"data": {
191-
"order_query": {
185+
"GetUser": {
192186
"id": "3",
193187
"firstName": "Jack",
194188
"lastName": "Johnson",
@@ -270,7 +264,7 @@ class User(NamedTuple):
270264

271265

272266
class CommandService:
273-
@enroute.graphql.command(name="GetUser", argument=GraphQLNonNull(user_input_type), output=user_type)
267+
@enroute.graphql.command(name="CreateUser", argument=GraphQLNonNull(user_input_type), output=user_type)
274268
def test_command(self, request: Request):
275269
params = await request.content()
276270
return Response(
@@ -284,21 +278,17 @@ class CommandService:
284278
)
285279
```
286280

287-
If you POST `/graphql` endpoint passing the query and variables:
281+
If you POST `{service_name}/graphql` endpoint passing the query and variables:
288282

289283
```json
290284
{
291-
"query": "mutation ($userData: UserInputType!) {
292-
createUser(request: $userData) {
293-
id, firstName, lastName, tweets, verified
294-
}
295-
}",
285+
"query": "mutation ($userData: UserInputType!) { CreateUser(request: $userData) {id, firstName, lastName, tweets, verified}}",
296286
"variables": {
297287
"userData": {
298-
"firstName": "John",
299-
"lastName":"Doe",
300-
"tweets": 42,
301-
"verified":true
288+
"firstName": "John",
289+
"lastName": "Doe",
290+
"tweets": 42,
291+
"verified": true
302292
}
303293
}
304294
}
@@ -309,7 +299,7 @@ Yoy will receive:
309299
```json
310300
{
311301
"data": {
312-
"createUser": {
302+
"CreateUser": {
313303
"id": "4kjjj43-l23k4l3-325kgaa2",
314304
"firstName": "John",
315305
"lastName": "Doe",
@@ -321,6 +311,12 @@ Yoy will receive:
321311
}
322312
```
323313

314+
### Get Schema
315+
By calling `{service_name}/graphql/schema` with `GET` method, you will receive the schema:
316+
```text
317+
"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}"
318+
```
319+
324320
## Documentation
325321

326322
The official API Reference is publicly available at the [GitHub Pages](https://minos-framework.github.io/minos-python).

packages/plugins/minos-router-graphql/minos/plugins/graphql/decorators.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ def __init__(self, name: str, output, argument: Optional = None):
2525
self.output = output
2626

2727
def __iter__(self) -> Iterable:
28-
yield from (self.name,)
28+
yield from (
29+
type(self),
30+
self.name,
31+
)
2932

3033

3134
class GraphQlCommandEnrouteDecorator(GraphQlEnrouteDecorator):

packages/plugins/minos-router-graphql/minos/plugins/graphql/handlers.py

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import logging
2+
import traceback
13
from typing import (
24
Any,
35
)
46

57
from graphql import (
68
ExecutionResult,
9+
GraphQLError,
710
GraphQLSchema,
811
graphql,
912
print_schema,
@@ -15,6 +18,8 @@
1518
ResponseException,
1619
)
1720

21+
logger = logging.getLogger(__name__)
22+
1823

1924
class GraphQlHandler:
2025
"""GraphQl Handler"""
@@ -28,7 +33,9 @@ async def execute_operation(self, request: Request) -> Response:
2833
:param request: The request containing the graphql operation.
2934
:return: A response containing the graphql result.
3035
"""
31-
result = await graphql(schema=self._schema, **(await self._build_graphql_arguments(request)))
36+
arguments = await self._build_graphql_arguments(request)
37+
result = await graphql(schema=self._schema, **arguments)
38+
3239
return self._build_response_from_graphql(result)
3340

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

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

50-
@staticmethod
51-
def _build_response_from_graphql(result: ExecutionResult) -> Response:
52-
errors = result.errors
53-
if errors is None:
54-
errors = list()
57+
def _build_response_from_graphql(self, result: ExecutionResult) -> Response:
58+
content = {"data": result.data}
59+
if result.errors is not None:
60+
content["errors"] = [err.message for err in result.errors]
61+
self._log_errors(result.errors)
5562

56-
status = 200
63+
status = self._get_status(result)
5764

58-
if len(errors):
59-
status = 500
60-
for error in errors:
61-
if isinstance(error.original_error, ResponseException):
62-
status = error.original_error.status
65+
return Response(content, status=status)
6366

64-
content = {"data": result.data, "errors": [err.message for err in errors]}
67+
@staticmethod
68+
def _get_status(result: ExecutionResult) -> int:
69+
status = 200
70+
for error in result.errors or []:
71+
if error.original_error is None:
72+
current = 400
73+
elif isinstance(error.original_error, ResponseException):
74+
current = error.original_error.status
75+
else:
76+
current = 500
77+
status = max(status, current)
78+
return status
6579

66-
return Response(content, status=status)
80+
@staticmethod
81+
def _log_errors(errors: list[GraphQLError]) -> None:
82+
for error in errors:
83+
if error.original_error is None:
84+
tb = repr(error)
85+
else:
86+
tb = "".join(traceback.format_tb(error.__traceback__))
87+
88+
if error.original_error is None or isinstance(error.original_error, ResponseException):
89+
logger.error(f"Raised an application exception:\n {tb}")
90+
else:
91+
logger.exception(f"Raised a system exception:\n {tb}")
6792

6893
async def get_schema(self, request: Request) -> Response:
6994
"""Get schema

packages/plugins/minos-router-graphql/minos/plugins/graphql/routers.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ def _filter_routes(self, routes: dict[EnrouteDecorator, Callable]) -> dict[Enrou
3434
}
3535
schema = GraphQLSchemaBuilder.build(routes)
3636
handler = GraphQlHandler(schema)
37+
service_name = self._config.get_name().lower()
3738
return {
38-
HttpEnrouteDecorator("/graphql", "POST"): handler.execute_operation,
39-
HttpEnrouteDecorator("/graphql/schema", "GET"): handler.get_schema,
39+
HttpEnrouteDecorator(f"/{service_name}/graphql", "POST"): handler.execute_operation,
40+
HttpEnrouteDecorator(f"/{service_name}/graphql/schema", "GET"): handler.get_schema,
4041
}

packages/plugins/minos-router-graphql/tests/test_config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
service:
2-
name: Order
2+
name: Foo
33
aggregate: tests.utils.Order
44
services:
55
- tests.services.commands.CommandService

packages/plugins/minos-router-graphql/tests/test_graphql/test_handlers.py

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ async def resolve_ticket_raises(request: Request):
4747
raise ResponseException("Some error.", status=403)
4848

4949

50+
async def resolve_ticket_raises_system(request: Request):
51+
raise ValueError()
52+
53+
5054
class TestGraphQlHandler(unittest.IsolatedAsyncioTestCase):
5155
CONFIG_FILE_PATH = BASE_PATH / "test_config.yml"
5256
_config = Config(CONFIG_FILE_PATH)
@@ -66,7 +70,8 @@ async def test_execute_operation(self):
6670
result = await handler.execute_operation(request)
6771

6872
self.assertEqual(200, result.status)
69-
self.assertDictEqual(await result.content(), {"data": {"order_query": "ticket #4"}, "errors": []})
73+
expected_content = {"data": {"order_query": "ticket #4"}}
74+
self.assertDictEqual(expected_content, await result.content())
7075

7176
async def test_execute_operation_raises(self):
7277
routes = {
@@ -83,6 +88,21 @@ async def test_execute_operation_raises(self):
8388

8489
self.assertEqual(403, result.status)
8590

91+
async def test_execute_operation_raises_system(self):
92+
routes = {
93+
GraphQlQueryEnrouteDecorator(name="ticket_query", output=GraphQLString): resolve_ticket_raises_system,
94+
}
95+
96+
schema = GraphQLSchemaBuilder.build(routes=routes)
97+
98+
handler = GraphQlHandler(schema)
99+
100+
request = InMemoryRequest(content="{ ticket_query }")
101+
102+
result = await handler.execute_operation(request)
103+
104+
self.assertEqual(500, result.status)
105+
86106
async def test_execute_wrong_operation(self):
87107
routes = {
88108
GraphQlQueryEnrouteDecorator(name="order_query", output=GraphQLString): callback_fn,
@@ -99,8 +119,8 @@ async def test_execute_wrong_operation(self):
99119

100120
content = await result.content()
101121

102-
self.assertEqual(500, result.status)
103-
self.assertNotEqual(content["errors"], [])
122+
self.assertEqual(400, result.status)
123+
self.assertEqual(1, len(content["errors"]))
104124

105125
async def test_schema(self):
106126
routes = {
@@ -148,8 +168,8 @@ async def test_query_with_variables(self):
148168
content = await result.content()
149169

150170
self.assertEqual(200, result.status)
151-
self.assertDictEqual({"order_query": 3}, content["data"])
152-
self.assertEqual([], content["errors"])
171+
expected_content = {"data": {"order_query": 3}}
172+
self.assertDictEqual(expected_content, content)
153173

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

173193
self.assertEqual(200, result.status)
174-
self.assertDictEqual({"SimpleQuery": "ABCD"}, content["data"])
175-
self.assertEqual([], content["errors"])
194+
expected_content = {"data": {"SimpleQuery": "ABCD"}}
195+
self.assertDictEqual(expected_content, content)
176196

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

206226
self.assertEqual(200, result.status)
207-
self.assertDictEqual(
208-
{"order_query": {"id": "3", "firstName": "Jack", "lastName": "Johnson", "tweets": 563, "verified": True}},
209-
content["data"],
210-
)
211-
self.assertEqual([], content["errors"])
227+
expected_content = {
228+
"data": {
229+
"order_query": {"id": "3", "firstName": "Jack", "lastName": "Johnson", "tweets": 563, "verified": True}
230+
}
231+
}
232+
self.assertDictEqual(expected_content, content)
212233

213234
async def test_mutation(self):
214235
routes = {
@@ -240,19 +261,18 @@ async def test_mutation(self):
240261
content = await result.content()
241262

242263
self.assertEqual(200, result.status)
243-
self.assertDictEqual(
244-
{
264+
expected_content = {
265+
"data": {
245266
"createUser": {
246267
"id": "4kjjj43-l23k4l3-325kgaa2",
247268
"firstName": "John",
248269
"lastName": "Doe",
249270
"tweets": 42,
250271
"verified": True,
251272
}
252-
},
253-
content["data"],
254-
)
255-
self.assertEqual([], content["errors"])
273+
}
274+
}
275+
self.assertDictEqual(expected_content, content)
256276

257277

258278
if __name__ == "__main__":

packages/plugins/minos-router-graphql/tests/test_graphql/test_routers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def test_from_config(self):
2323

2424
self.assertIsInstance(router, GraphQlHttpRouter)
2525
self.assertEqual(
26-
{HttpEnrouteDecorator("/graphql", "POST"), HttpEnrouteDecorator("/graphql/schema", "GET")},
26+
{HttpEnrouteDecorator("/foo/graphql", "POST"), HttpEnrouteDecorator("/foo/graphql/schema", "GET")},
2727
router.routes.keys(),
2828
)
2929

0 commit comments

Comments
 (0)