Skip to content

Commit

Permalink
feat: add support for rendering GraphiQL with jinja (#103)
Browse files Browse the repository at this point in the history
  • Loading branch information
kiendang authored Feb 15, 2023
1 parent 36bbd47 commit 8b9639e
Show file tree
Hide file tree
Showing 31 changed files with 314 additions and 343 deletions.
2 changes: 1 addition & 1 deletion docs/aiohttp.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ gql_view(request) # <-- the instance is callable and expects a `aiohttp.web.Req
* `root_value`: The `root_value` you want to provide to graphql `execute`.
* `pretty`: Whether or not you want the response to be pretty printed JSON.
* `graphiql`: If `True`, may present [GraphiQL](https://github.com/graphql/graphiql) when loaded directly from a browser (a useful tool for debugging and exploration).
* `graphiql_version`: The graphiql version to load. Defaults to **"1.4.7"**.
* `graphiql_version`: The graphiql version to load. Defaults to **"2.2.0"**.
* `graphiql_template`: Inject a Jinja template string to customize GraphiQL.
* `graphiql_html_title`: The graphiql title to display. Defaults to **"GraphiQL"**.
* `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.
Expand Down
3 changes: 2 additions & 1 deletion docs/flask.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,10 @@ This will add `/graphql` endpoint to your app and enable the GraphiQL IDE.
* `root_value`: The `root_value` you want to provide to graphql `execute`.
* `pretty`: Whether or not you want the response to be pretty printed JSON.
* `graphiql`: If `True`, may present [GraphiQL](https://github.com/graphql/graphiql) when loaded directly from a browser (a useful tool for debugging and exploration).
* `graphiql_version`: The graphiql version to load. Defaults to **"1.4.7"**.
* `graphiql_version`: The graphiql version to load. Defaults to **"2.2.0"**.
* `graphiql_template`: Inject a Jinja template string to customize GraphiQL.
* `graphiql_html_title`: The graphiql title to display. Defaults to **"GraphiQL"**.
* `jinja_env`: Sets jinja environment to be used to process GraphiQL template. If environment is not set, fallbacks to simple regex-based renderer.
* `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))
* `middleware`: A list of graphql [middlewares](http://docs.graphene-python.org/en/latest/execution/middleware/).
* `validation_rules`: A list of graphql validation rules.
Expand Down
2 changes: 1 addition & 1 deletion docs/sanic.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ This will add `/graphql` endpoint to your app and enable the GraphiQL IDE.
* `root_value`: The `root_value` you want to provide to graphql `execute`.
* `pretty`: Whether or not you want the response to be pretty printed JSON.
* `graphiql`: If `True`, may present [GraphiQL](https://github.com/graphql/graphiql) when loaded directly from a browser (a useful tool for debugging and exploration).
* `graphiql_version`: The graphiql version to load. Defaults to **"1.4.7"**.
* `graphiql_version`: The graphiql version to load. Defaults to **"2.2.0"**.
* `graphiql_template`: Inject a Jinja template string to customize GraphiQL.
* `graphiql_html_title`: The graphiql title to display. Defaults to **"GraphiQL"**.
* `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.
Expand Down
3 changes: 2 additions & 1 deletion docs/webob.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@ This will add `/graphql` endpoint to your app and enable the GraphiQL IDE.
* `root_value`: The `root_value` you want to provide to graphql `execute`.
* `pretty`: Whether or not you want the response to be pretty printed JSON.
* `graphiql`: If `True`, may present [GraphiQL](https://github.com/graphql/graphiql) when loaded directly from a browser (a useful tool for debugging and exploration).
* `graphiql_version`: The graphiql version to load. Defaults to **"1.4.7"**.
* `graphiql_version`: The graphiql version to load. Defaults to **"2.2.0"**.
* `graphiql_template`: Inject a Jinja template string to customize GraphiQL.
* `graphiql_html_title`: The graphiql title to display. Defaults to **"GraphiQL"**.
* `jinja_env`: Sets jinja environment to be used to process GraphiQL template. If environment is not set, fallbacks to simple regex-based renderer.
* `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))
* `middleware`: A list of graphql [middlewares](http://docs.graphene-python.org/en/latest/execution/middleware/).
* `validation_rules`: A list of graphql validation rules.
Expand Down
14 changes: 14 additions & 0 deletions graphql_server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,3 +335,17 @@ def format_execution_result(
response = {"data": execution_result.data}

return FormattedResult(response, status_code)


def _check_jinja(jinja_env: Any) -> None:
try:
from jinja2 import Environment
except ImportError: # pragma: no cover
raise RuntimeError(
"Attempt to set 'jinja_env' to a value other than None while Jinja2 is not installed.\n"
"Please install Jinja2 to render GraphiQL with Jinja2.\n"
"Otherwise set 'jinja_env' to None to use the simple regex renderer."
)

if not isinstance(jinja_env, Environment): # pragma: no cover
raise TypeError("'jinja_env' has to be of type jinja2.Environment.")
4 changes: 4 additions & 0 deletions graphql_server/aiohttp/graphqlview.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from graphql_server import (
GraphQLParams,
HttpQueryError,
_check_jinja,
encode_execution_results,
format_error_default,
json_encode,
Expand Down Expand Up @@ -66,6 +67,9 @@ def __init__(self, **kwargs):
if not isinstance(self.schema, GraphQLSchema):
raise TypeError("A Schema is required to be provided to GraphQLView.")

if self.jinja_env is not None:
_check_jinja(self.jinja_env)

def get_root_value(self):
return self.root_value

Expand Down
7 changes: 6 additions & 1 deletion graphql_server/flask/graphqlview.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from graphql_server import (
GraphQLParams,
HttpQueryError,
_check_jinja,
encode_execution_results,
format_error_default,
json_encode,
Expand Down Expand Up @@ -39,6 +40,7 @@ class GraphQLView(View):
validation_rules = None
execution_context_class = None
batch = False
jinja_env = None
subscriptions = None
headers = None
default_query = None
Expand All @@ -62,6 +64,9 @@ def __init__(self, **kwargs):
if not isinstance(self.schema, GraphQLSchema):
raise TypeError("A Schema is required to be provided to GraphQLView.")

if self.jinja_env is not None:
_check_jinja(self.jinja_env)

def get_root_value(self):
return self.root_value

Expand Down Expand Up @@ -131,7 +136,7 @@ def dispatch_request(self):
graphiql_version=self.graphiql_version,
graphiql_template=self.graphiql_template,
graphiql_html_title=self.graphiql_html_title,
jinja_env=None,
jinja_env=self.jinja_env,
)
graphiql_options = GraphiQLOptions(
default_query=self.default_query,
Expand Down
7 changes: 6 additions & 1 deletion graphql_server/quart/graphqlview.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from graphql_server import (
GraphQLParams,
HttpQueryError,
_check_jinja,
encode_execution_results,
format_error_default,
json_encode,
Expand Down Expand Up @@ -42,6 +43,7 @@ class GraphQLView(View):
validation_rules = None
execution_context_class = None
batch = False
jinja_env = None
enable_async = False
subscriptions = None
headers = None
Expand All @@ -66,6 +68,9 @@ def __init__(self, **kwargs):
if not isinstance(self.schema, GraphQLSchema):
raise TypeError("A Schema is required to be provided to GraphQLView.")

if self.jinja_env is not None:
_check_jinja(self.jinja_env)

def get_root_value(self):
return self.root_value

Expand Down Expand Up @@ -147,7 +152,7 @@ async def dispatch_request(self):
graphiql_version=self.graphiql_version,
graphiql_template=self.graphiql_template,
graphiql_html_title=self.graphiql_html_title,
jinja_env=None,
jinja_env=self.jinja_env,
)
graphiql_options = GraphiQLOptions(
default_query=self.default_query,
Expand Down
92 changes: 37 additions & 55 deletions graphql_server/render_graphiql.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
"""Based on (express-graphql)[https://github.com/graphql/express-graphql/blob/main/src/renderGraphiQL.ts] and
(subscriptions-transport-ws)[https://github.com/apollographql/subscriptions-transport-ws]"""
(graphql-ws)[https://github.com/enisdenjo/graphql-ws]"""
import json
import re
from typing import Any, Dict, Optional, Tuple

from jinja2 import Environment
# This Environment import is only for type checking purpose,
# and only relevant if rendering GraphiQL with Jinja
try:
from jinja2 import Environment
except ImportError: # pragma: no cover
pass

from typing_extensions import TypedDict

GRAPHIQL_VERSION = "1.4.7"
GRAPHIQL_VERSION = "2.2.0"

GRAPHIQL_TEMPLATE = """<!--
The request to this GraphQL server provided the header "Accept: text/html"
Expand All @@ -34,13 +40,12 @@
}
</style>
<link href="//cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.css" rel="stylesheet" />
<script src="//cdn.jsdelivr.net/npm/[email protected].0/dist/polyfill.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/unfetch@4.2.0/dist/unfetch.umd.js"></script>
<script src="//cdn.jsdelivr.net/npm/react@16.14.0/umd/react.production.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/react-dom@16.14.0/umd/react-dom.production.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/[email protected].3/dist/polyfill.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/unfetch@5.0.0/dist/unfetch.umd.js"></script>
<script src="//cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.production.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/[email protected]/browser/client.js"></script>
<script src="//cdn.jsdelivr.net/npm/[email protected]/browser/client.js"></script>
<script src="//cdn.jsdelivr.net/npm/[email protected]/umd/graphql-ws.min.js"></script>
</head>
<body>
<div id="graphiql">Loading...</div>
Expand Down Expand Up @@ -75,35 +80,16 @@
otherParams[k] = parameters[k];
}
}
// Configure the subscription client
let subscriptionsFetcher = null;
if ('{{subscription_url}}') {
let subscriptionsClient = new SubscriptionsTransportWs.SubscriptionClient(
'{{ subscription_url }}',
{ reconnect: true }
);
subscriptionsFetcher = GraphiQLSubscriptionsFetcher.graphQLFetcher(
subscriptionsClient,
graphQLFetcher
);
}
var fetchURL = locationQuery(otherParams);
// Defines a GraphQL fetcher using the fetch API.
function graphQLFetcher(graphQLParams, opts) {
return fetch(fetchURL, {
method: 'post',
headers: Object.assign(
{
'Accept': 'application/json',
'Content-Type': 'application/json'
},
opts && opts.headers,
),
body: JSON.stringify(graphQLParams),
credentials: 'include',
}).then(function (response) {
return response.json();
// Defines a GraphQL fetcher.
var graphQLFetcher;
if ('{{subscription_url}}') {
graphQLFetcher = GraphiQL.createFetcher({
url: fetchURL,
subscription_url: '{{subscription_url}}'
});
} else {
graphQLFetcher = GraphiQL.createFetcher({ url: fetchURL });
}
// When the query and variables string is edited, update the URL bar so
// that it can be easily shared.
Expand All @@ -129,7 +115,7 @@
// Render <GraphiQL /> into the body.
ReactDOM.render(
React.createElement(GraphiQL, {
fetcher: subscriptionsFetcher || graphQLFetcher,
fetcher: graphQLFetcher,
onEditQuery: onEditQuery,
onEditVariables: onEditVariables,
onEditHeaders: onEditHeaders,
Expand All @@ -140,7 +126,7 @@
headers: {{headers|tojson}},
operationName: {{operation_name|tojson}},
defaultQuery: {{default_query|tojson}},
headerEditorEnabled: {{header_editor_enabled|tojson}},
isHeadersEditorEnabled: {{header_editor_enabled|tojson}},
shouldPersistHeaders: {{should_persist_headers|tojson}}
}),
document.getElementById('graphiql')
Expand Down Expand Up @@ -216,24 +202,12 @@ class GraphiQLOptions(TypedDict):
should_persist_headers: Optional[bool]


def escape_js_value(value: Any) -> Any:
quotation = False
if value.startswith('"') and value.endswith('"'):
quotation = True
value = value[1 : len(value) - 1]

value = value.replace("\\\\n", "\\\\\\n").replace("\\n", "\\\\n")
if quotation:
value = '"' + value.replace('\\\\"', '"').replace('"', '\\"') + '"'

return value


def process_var(template: str, name: str, value: Any, jsonify=False) -> str:
pattern = r"{{\s*" + name + r"(\s*|[^}]+)*\s*}}"
pattern = r"{{\s*" + name.replace("\\", r"\\") + r"(\s*|[^}]+)*\s*}}"
if jsonify and value not in ["null", "undefined"]:
value = json.dumps(value)
value = escape_js_value(value)

value = value.replace("\\", r"\\")

return re.sub(pattern, value, template)

Expand Down Expand Up @@ -296,6 +270,9 @@ def _render_graphiql(
or "false",
}

if template_vars["result"] in ("null"):
template_vars["result"] = None

return graphiql_template, template_vars


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

if jinja_env:
template = jinja_env.from_string(graphiql_template)
Expand All @@ -324,6 +301,11 @@ def render_graphiql_sync(
options: Optional[GraphiQLOptions] = None,
) -> str:
graphiql_template, template_vars = _render_graphiql(data, config, options)
jinja_env = config.get("jinja_env")

source = simple_renderer(graphiql_template, **template_vars)
if jinja_env:
template = jinja_env.from_string(graphiql_template)
source = template.render(**template_vars)
else:
source = simple_renderer(graphiql_template, **template_vars)
return source
4 changes: 4 additions & 0 deletions graphql_server/sanic/graphqlview.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from graphql_server import (
GraphQLParams,
HttpQueryError,
_check_jinja,
encode_execution_results,
format_error_default,
json_encode,
Expand Down Expand Up @@ -68,6 +69,9 @@ def __init__(self, **kwargs):
if not isinstance(self.schema, GraphQLSchema):
raise TypeError("A Schema is required to be provided to GraphQLView.")

if self.jinja_env is not None:
_check_jinja(self.jinja_env)

def get_root_value(self):
return self.root_value

Expand Down
2 changes: 1 addition & 1 deletion graphql_server/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

if sys.version_info >= (3, 10):
from typing import ParamSpec
else:
else: # pragma: no cover
from typing_extensions import ParamSpec


Expand Down
7 changes: 6 additions & 1 deletion graphql_server/webob/graphqlview.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from graphql_server import (
GraphQLParams,
HttpQueryError,
_check_jinja,
encode_execution_results,
format_error_default,
json_encode,
Expand Down Expand Up @@ -38,6 +39,7 @@ class GraphQLView:
validation_rules = None
execution_context_class = None
batch = False
jinja_env = None
enable_async = False
subscriptions = None
headers = None
Expand All @@ -61,6 +63,9 @@ def __init__(self, **kwargs):
if not isinstance(self.schema, GraphQLSchema):
raise TypeError("A Schema is required to be provided to GraphQLView.")

if self.jinja_env is not None:
_check_jinja(self.jinja_env)

def get_root_value(self):
return self.root_value

Expand Down Expand Up @@ -133,7 +138,7 @@ def dispatch_request(self, request):
graphiql_version=self.graphiql_version,
graphiql_template=self.graphiql_template,
graphiql_html_title=self.graphiql_html_title,
jinja_env=None,
jinja_env=self.jinja_env,
)
graphiql_options = GraphiQLOptions(
default_query=self.default_query,
Expand Down
12 changes: 4 additions & 8 deletions tests/aiohttp/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from aiohttp import web

from graphql_server.aiohttp import GraphQLView
from tests.aiohttp.schema import Schema

from .schema import Schema


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


def url_string(**url_params):
base_url = "/graphql"

if url_params:
return f"{base_url}?{urlencode(url_params)}"

return base_url
def url_string(url="/graphql", **url_params):
return f"{url}?{urlencode(url_params)}" if url_params else url
Loading

0 comments on commit 8b9639e

Please sign in to comment.