Skip to content

Commit 8b9639e

Browse files
authored
feat: add support for rendering GraphiQL with jinja (#103)
1 parent 36bbd47 commit 8b9639e

31 files changed

+314
-343
lines changed

docs/aiohttp.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ gql_view(request) # <-- the instance is callable and expects a `aiohttp.web.Req
5252
* `root_value`: The `root_value` you want to provide to graphql `execute`.
5353
* `pretty`: Whether or not you want the response to be pretty printed JSON.
5454
* `graphiql`: If `True`, may present [GraphiQL](https://github.com/graphql/graphiql) when loaded directly from a browser (a useful tool for debugging and exploration).
55-
* `graphiql_version`: The graphiql version to load. Defaults to **"1.4.7"**.
55+
* `graphiql_version`: The graphiql version to load. Defaults to **"2.2.0"**.
5656
* `graphiql_template`: Inject a Jinja template string to customize GraphiQL.
5757
* `graphiql_html_title`: The graphiql title to display. Defaults to **"GraphiQL"**.
5858
* `jinja_env`: Sets jinja environment to be used to process GraphiQL template. If Jinja’s async mode is enabled (by `enable_async=True`), uses `Template.render_async` instead of `Template.render`. If environment is not set, fallbacks to simple regex-based renderer.

docs/flask.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,10 @@ This will add `/graphql` endpoint to your app and enable the GraphiQL IDE.
4646
* `root_value`: The `root_value` you want to provide to graphql `execute`.
4747
* `pretty`: Whether or not you want the response to be pretty printed JSON.
4848
* `graphiql`: If `True`, may present [GraphiQL](https://github.com/graphql/graphiql) when loaded directly from a browser (a useful tool for debugging and exploration).
49-
* `graphiql_version`: The graphiql version to load. Defaults to **"1.4.7"**.
49+
* `graphiql_version`: The graphiql version to load. Defaults to **"2.2.0"**.
5050
* `graphiql_template`: Inject a Jinja template string to customize GraphiQL.
5151
* `graphiql_html_title`: The graphiql title to display. Defaults to **"GraphiQL"**.
52+
* `jinja_env`: Sets jinja environment to be used to process GraphiQL template. If environment is not set, fallbacks to simple regex-based renderer.
5253
* `batch`: Set the GraphQL view as batch (for using in [Apollo-Client](http://dev.apollodata.com/core/network.html#query-batching) or [ReactRelayNetworkLayer](https://github.com/nodkz/react-relay-network-layer))
5354
* `middleware`: A list of graphql [middlewares](http://docs.graphene-python.org/en/latest/execution/middleware/).
5455
* `validation_rules`: A list of graphql validation rules.

docs/sanic.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ This will add `/graphql` endpoint to your app and enable the GraphiQL IDE.
4444
* `root_value`: The `root_value` you want to provide to graphql `execute`.
4545
* `pretty`: Whether or not you want the response to be pretty printed JSON.
4646
* `graphiql`: If `True`, may present [GraphiQL](https://github.com/graphql/graphiql) when loaded directly from a browser (a useful tool for debugging and exploration).
47-
* `graphiql_version`: The graphiql version to load. Defaults to **"1.4.7"**.
47+
* `graphiql_version`: The graphiql version to load. Defaults to **"2.2.0"**.
4848
* `graphiql_template`: Inject a Jinja template string to customize GraphiQL.
4949
* `graphiql_html_title`: The graphiql title to display. Defaults to **"GraphiQL"**.
5050
* `jinja_env`: Sets jinja environment to be used to process GraphiQL template. If Jinja’s async mode is enabled (by `enable_async=True`), uses `Template.render_async` instead of `Template.render`. If environment is not set, fallbacks to simple regex-based renderer.

docs/webob.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,10 @@ This will add `/graphql` endpoint to your app and enable the GraphiQL IDE.
4343
* `root_value`: The `root_value` you want to provide to graphql `execute`.
4444
* `pretty`: Whether or not you want the response to be pretty printed JSON.
4545
* `graphiql`: If `True`, may present [GraphiQL](https://github.com/graphql/graphiql) when loaded directly from a browser (a useful tool for debugging and exploration).
46-
* `graphiql_version`: The graphiql version to load. Defaults to **"1.4.7"**.
46+
* `graphiql_version`: The graphiql version to load. Defaults to **"2.2.0"**.
4747
* `graphiql_template`: Inject a Jinja template string to customize GraphiQL.
4848
* `graphiql_html_title`: The graphiql title to display. Defaults to **"GraphiQL"**.
49+
* `jinja_env`: Sets jinja environment to be used to process GraphiQL template. If environment is not set, fallbacks to simple regex-based renderer.
4950
* `batch`: Set the GraphQL view as batch (for using in [Apollo-Client](http://dev.apollodata.com/core/network.html#query-batching) or [ReactRelayNetworkLayer](https://github.com/nodkz/react-relay-network-layer))
5051
* `middleware`: A list of graphql [middlewares](http://docs.graphene-python.org/en/latest/execution/middleware/).
5152
* `validation_rules`: A list of graphql validation rules.

graphql_server/__init__.py

+14
Original file line numberDiff line numberDiff line change
@@ -335,3 +335,17 @@ def format_execution_result(
335335
response = {"data": execution_result.data}
336336

337337
return FormattedResult(response, status_code)
338+
339+
340+
def _check_jinja(jinja_env: Any) -> None:
341+
try:
342+
from jinja2 import Environment
343+
except ImportError: # pragma: no cover
344+
raise RuntimeError(
345+
"Attempt to set 'jinja_env' to a value other than None while Jinja2 is not installed.\n"
346+
"Please install Jinja2 to render GraphiQL with Jinja2.\n"
347+
"Otherwise set 'jinja_env' to None to use the simple regex renderer."
348+
)
349+
350+
if not isinstance(jinja_env, Environment): # pragma: no cover
351+
raise TypeError("'jinja_env' has to be of type jinja2.Environment.")

graphql_server/aiohttp/graphqlview.py

+4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from graphql_server import (
1313
GraphQLParams,
1414
HttpQueryError,
15+
_check_jinja,
1516
encode_execution_results,
1617
format_error_default,
1718
json_encode,
@@ -66,6 +67,9 @@ def __init__(self, **kwargs):
6667
if not isinstance(self.schema, GraphQLSchema):
6768
raise TypeError("A Schema is required to be provided to GraphQLView.")
6869

70+
if self.jinja_env is not None:
71+
_check_jinja(self.jinja_env)
72+
6973
def get_root_value(self):
7074
return self.root_value
7175

graphql_server/flask/graphqlview.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from graphql_server import (
1313
GraphQLParams,
1414
HttpQueryError,
15+
_check_jinja,
1516
encode_execution_results,
1617
format_error_default,
1718
json_encode,
@@ -39,6 +40,7 @@ class GraphQLView(View):
3940
validation_rules = None
4041
execution_context_class = None
4142
batch = False
43+
jinja_env = None
4244
subscriptions = None
4345
headers = None
4446
default_query = None
@@ -62,6 +64,9 @@ def __init__(self, **kwargs):
6264
if not isinstance(self.schema, GraphQLSchema):
6365
raise TypeError("A Schema is required to be provided to GraphQLView.")
6466

67+
if self.jinja_env is not None:
68+
_check_jinja(self.jinja_env)
69+
6570
def get_root_value(self):
6671
return self.root_value
6772

@@ -131,7 +136,7 @@ def dispatch_request(self):
131136
graphiql_version=self.graphiql_version,
132137
graphiql_template=self.graphiql_template,
133138
graphiql_html_title=self.graphiql_html_title,
134-
jinja_env=None,
139+
jinja_env=self.jinja_env,
135140
)
136141
graphiql_options = GraphiQLOptions(
137142
default_query=self.default_query,

graphql_server/quart/graphqlview.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from graphql_server import (
1515
GraphQLParams,
1616
HttpQueryError,
17+
_check_jinja,
1718
encode_execution_results,
1819
format_error_default,
1920
json_encode,
@@ -42,6 +43,7 @@ class GraphQLView(View):
4243
validation_rules = None
4344
execution_context_class = None
4445
batch = False
46+
jinja_env = None
4547
enable_async = False
4648
subscriptions = None
4749
headers = None
@@ -66,6 +68,9 @@ def __init__(self, **kwargs):
6668
if not isinstance(self.schema, GraphQLSchema):
6769
raise TypeError("A Schema is required to be provided to GraphQLView.")
6870

71+
if self.jinja_env is not None:
72+
_check_jinja(self.jinja_env)
73+
6974
def get_root_value(self):
7075
return self.root_value
7176

@@ -147,7 +152,7 @@ async def dispatch_request(self):
147152
graphiql_version=self.graphiql_version,
148153
graphiql_template=self.graphiql_template,
149154
graphiql_html_title=self.graphiql_html_title,
150-
jinja_env=None,
155+
jinja_env=self.jinja_env,
151156
)
152157
graphiql_options = GraphiQLOptions(
153158
default_query=self.default_query,

graphql_server/render_graphiql.py

+37-55
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
"""Based on (express-graphql)[https://github.com/graphql/express-graphql/blob/main/src/renderGraphiQL.ts] and
2-
(subscriptions-transport-ws)[https://github.com/apollographql/subscriptions-transport-ws]"""
2+
(graphql-ws)[https://github.com/enisdenjo/graphql-ws]"""
33
import json
44
import re
55
from typing import Any, Dict, Optional, Tuple
66

7-
from jinja2 import Environment
7+
# This Environment import is only for type checking purpose,
8+
# and only relevant if rendering GraphiQL with Jinja
9+
try:
10+
from jinja2 import Environment
11+
except ImportError: # pragma: no cover
12+
pass
13+
814
from typing_extensions import TypedDict
915

10-
GRAPHIQL_VERSION = "1.4.7"
16+
GRAPHIQL_VERSION = "2.2.0"
1117

1218
GRAPHIQL_TEMPLATE = """<!--
1319
The request to this GraphQL server provided the header "Accept: text/html"
@@ -34,13 +40,12 @@
3440
}
3541
</style>
3642
<link href="//cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.css" rel="stylesheet" />
37-
<script src="//cdn.jsdelivr.net/npm/[email protected].0/dist/polyfill.min.js"></script>
38-
<script src="//cdn.jsdelivr.net/npm/unfetch@4.2.0/dist/unfetch.umd.js"></script>
39-
<script src="//cdn.jsdelivr.net/npm/react@16.14.0/umd/react.production.min.js"></script>
40-
<script src="//cdn.jsdelivr.net/npm/react-dom@16.14.0/umd/react-dom.production.min.js"></script>
43+
<script src="//cdn.jsdelivr.net/npm/[email protected].3/dist/polyfill.min.js"></script>
44+
<script src="//cdn.jsdelivr.net/npm/unfetch@5.0.0/dist/unfetch.umd.js"></script>
45+
<script src="//cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js"></script>
46+
<script src="//cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.production.min.js"></script>
4147
<script src="//cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.min.js"></script>
42-
<script src="//cdn.jsdelivr.net/npm/[email protected]/browser/client.js"></script>
43-
<script src="//cdn.jsdelivr.net/npm/[email protected]/browser/client.js"></script>
48+
<script src="//cdn.jsdelivr.net/npm/[email protected]/umd/graphql-ws.min.js"></script>
4449
</head>
4550
<body>
4651
<div id="graphiql">Loading...</div>
@@ -75,35 +80,16 @@
7580
otherParams[k] = parameters[k];
7681
}
7782
}
78-
// Configure the subscription client
79-
let subscriptionsFetcher = null;
80-
if ('{{subscription_url}}') {
81-
let subscriptionsClient = new SubscriptionsTransportWs.SubscriptionClient(
82-
'{{ subscription_url }}',
83-
{ reconnect: true }
84-
);
85-
subscriptionsFetcher = GraphiQLSubscriptionsFetcher.graphQLFetcher(
86-
subscriptionsClient,
87-
graphQLFetcher
88-
);
89-
}
9083
var fetchURL = locationQuery(otherParams);
91-
// Defines a GraphQL fetcher using the fetch API.
92-
function graphQLFetcher(graphQLParams, opts) {
93-
return fetch(fetchURL, {
94-
method: 'post',
95-
headers: Object.assign(
96-
{
97-
'Accept': 'application/json',
98-
'Content-Type': 'application/json'
99-
},
100-
opts && opts.headers,
101-
),
102-
body: JSON.stringify(graphQLParams),
103-
credentials: 'include',
104-
}).then(function (response) {
105-
return response.json();
84+
// Defines a GraphQL fetcher.
85+
var graphQLFetcher;
86+
if ('{{subscription_url}}') {
87+
graphQLFetcher = GraphiQL.createFetcher({
88+
url: fetchURL,
89+
subscription_url: '{{subscription_url}}'
10690
});
91+
} else {
92+
graphQLFetcher = GraphiQL.createFetcher({ url: fetchURL });
10793
}
10894
// When the query and variables string is edited, update the URL bar so
10995
// that it can be easily shared.
@@ -129,7 +115,7 @@
129115
// Render <GraphiQL /> into the body.
130116
ReactDOM.render(
131117
React.createElement(GraphiQL, {
132-
fetcher: subscriptionsFetcher || graphQLFetcher,
118+
fetcher: graphQLFetcher,
133119
onEditQuery: onEditQuery,
134120
onEditVariables: onEditVariables,
135121
onEditHeaders: onEditHeaders,
@@ -140,7 +126,7 @@
140126
headers: {{headers|tojson}},
141127
operationName: {{operation_name|tojson}},
142128
defaultQuery: {{default_query|tojson}},
143-
headerEditorEnabled: {{header_editor_enabled|tojson}},
129+
isHeadersEditorEnabled: {{header_editor_enabled|tojson}},
144130
shouldPersistHeaders: {{should_persist_headers|tojson}}
145131
}),
146132
document.getElementById('graphiql')
@@ -216,24 +202,12 @@ class GraphiQLOptions(TypedDict):
216202
should_persist_headers: Optional[bool]
217203

218204

219-
def escape_js_value(value: Any) -> Any:
220-
quotation = False
221-
if value.startswith('"') and value.endswith('"'):
222-
quotation = True
223-
value = value[1 : len(value) - 1]
224-
225-
value = value.replace("\\\\n", "\\\\\\n").replace("\\n", "\\\\n")
226-
if quotation:
227-
value = '"' + value.replace('\\\\"', '"').replace('"', '\\"') + '"'
228-
229-
return value
230-
231-
232205
def process_var(template: str, name: str, value: Any, jsonify=False) -> str:
233-
pattern = r"{{\s*" + name + r"(\s*|[^}]+)*\s*}}"
206+
pattern = r"{{\s*" + name.replace("\\", r"\\") + r"(\s*|[^}]+)*\s*}}"
234207
if jsonify and value not in ["null", "undefined"]:
235208
value = json.dumps(value)
236-
value = escape_js_value(value)
209+
210+
value = value.replace("\\", r"\\")
237211

238212
return re.sub(pattern, value, template)
239213

@@ -296,6 +270,9 @@ def _render_graphiql(
296270
or "false",
297271
}
298272

273+
if template_vars["result"] in ("null"):
274+
template_vars["result"] = None
275+
299276
return graphiql_template, template_vars
300277

301278

@@ -305,7 +282,7 @@ async def render_graphiql_async(
305282
options: Optional[GraphiQLOptions] = None,
306283
) -> str:
307284
graphiql_template, template_vars = _render_graphiql(data, config, options)
308-
jinja_env: Optional[Environment] = config.get("jinja_env")
285+
jinja_env = config.get("jinja_env")
309286

310287
if jinja_env:
311288
template = jinja_env.from_string(graphiql_template)
@@ -324,6 +301,11 @@ def render_graphiql_sync(
324301
options: Optional[GraphiQLOptions] = None,
325302
) -> str:
326303
graphiql_template, template_vars = _render_graphiql(data, config, options)
304+
jinja_env = config.get("jinja_env")
327305

328-
source = simple_renderer(graphiql_template, **template_vars)
306+
if jinja_env:
307+
template = jinja_env.from_string(graphiql_template)
308+
source = template.render(**template_vars)
309+
else:
310+
source = simple_renderer(graphiql_template, **template_vars)
329311
return source

graphql_server/sanic/graphqlview.py

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from graphql_server import (
1515
GraphQLParams,
1616
HttpQueryError,
17+
_check_jinja,
1718
encode_execution_results,
1819
format_error_default,
1920
json_encode,
@@ -68,6 +69,9 @@ def __init__(self, **kwargs):
6869
if not isinstance(self.schema, GraphQLSchema):
6970
raise TypeError("A Schema is required to be provided to GraphQLView.")
7071

72+
if self.jinja_env is not None:
73+
_check_jinja(self.jinja_env)
74+
7175
def get_root_value(self):
7276
return self.root_value
7377

graphql_server/utils.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
if sys.version_info >= (3, 10):
55
from typing import ParamSpec
6-
else:
6+
else: # pragma: no cover
77
from typing_extensions import ParamSpec
88

99

graphql_server/webob/graphqlview.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from graphql_server import (
1212
GraphQLParams,
1313
HttpQueryError,
14+
_check_jinja,
1415
encode_execution_results,
1516
format_error_default,
1617
json_encode,
@@ -38,6 +39,7 @@ class GraphQLView:
3839
validation_rules = None
3940
execution_context_class = None
4041
batch = False
42+
jinja_env = None
4143
enable_async = False
4244
subscriptions = None
4345
headers = None
@@ -61,6 +63,9 @@ def __init__(self, **kwargs):
6163
if not isinstance(self.schema, GraphQLSchema):
6264
raise TypeError("A Schema is required to be provided to GraphQLView.")
6365

66+
if self.jinja_env is not None:
67+
_check_jinja(self.jinja_env)
68+
6469
def get_root_value(self):
6570
return self.root_value
6671

@@ -133,7 +138,7 @@ def dispatch_request(self, request):
133138
graphiql_version=self.graphiql_version,
134139
graphiql_template=self.graphiql_template,
135140
graphiql_html_title=self.graphiql_html_title,
136-
jinja_env=None,
141+
jinja_env=self.jinja_env,
137142
)
138143
graphiql_options = GraphiQLOptions(
139144
default_query=self.default_query,

tests/aiohttp/app.py

+4-8
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
from aiohttp import web
44

55
from graphql_server.aiohttp import GraphQLView
6-
from tests.aiohttp.schema import Schema
6+
7+
from .schema import Schema
78

89

910
def create_app(schema=Schema, **kwargs):
@@ -13,10 +14,5 @@ def create_app(schema=Schema, **kwargs):
1314
return app
1415

1516

16-
def url_string(**url_params):
17-
base_url = "/graphql"
18-
19-
if url_params:
20-
return f"{base_url}?{urlencode(url_params)}"
21-
22-
return base_url
17+
def url_string(url="/graphql", **url_params):
18+
return f"{url}?{urlencode(url_params)}" if url_params else url

0 commit comments

Comments
 (0)