mirror of
https://github.com/graphql-python/graphene-django.git
synced 2024-11-25 02:54:06 +03:00
Update GraphiQL, add GraphiQL subscription support (#1001)
This commit is contained in:
parent
1205e29bef
commit
6aa6aaaa8c
|
@ -28,6 +28,7 @@ For more advanced use, check out the Relay tutorial.
|
||||||
fields
|
fields
|
||||||
extra-types
|
extra-types
|
||||||
mutations
|
mutations
|
||||||
|
subscriptions
|
||||||
filtering
|
filtering
|
||||||
authorization
|
authorization
|
||||||
debug
|
debug
|
||||||
|
|
|
@ -104,7 +104,7 @@ Default: ``100``
|
||||||
|
|
||||||
|
|
||||||
``CAMELCASE_ERRORS``
|
``CAMELCASE_ERRORS``
|
||||||
------------------------------------
|
--------------------
|
||||||
|
|
||||||
When set to ``True`` field names in the ``errors`` object will be camel case.
|
When set to ``True`` field names in the ``errors`` object will be camel case.
|
||||||
By default they will be snake case.
|
By default they will be snake case.
|
||||||
|
@ -151,7 +151,7 @@ Default: ``False``
|
||||||
|
|
||||||
|
|
||||||
``DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME``
|
``DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME``
|
||||||
--------------------------------------
|
----------------------------------------
|
||||||
|
|
||||||
Define the path of a function that takes the Django choice field and returns a string to completely customise the naming for the Enum type.
|
Define the path of a function that takes the Django choice field and returns a string to completely customise the naming for the Enum type.
|
||||||
|
|
||||||
|
@ -170,3 +170,19 @@ Default: ``None``
|
||||||
GRAPHENE = {
|
GRAPHENE = {
|
||||||
'DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME': "myapp.utils.enum_naming"
|
'DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME': "myapp.utils.enum_naming"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
``SUBSCRIPTION_PATH``
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Define an alternative URL path where subscription operations should be routed.
|
||||||
|
|
||||||
|
The GraphiQL interface will use this setting to intelligently route subscription operations. This is useful if you have more advanced infrastructure requirements that prevent websockets from being handled at the same path (e.g., a WSGI server listening at ``/graphql`` and an ASGI server listening at ``/ws/graphql``).
|
||||||
|
|
||||||
|
Default: ``None``
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
GRAPHENE = {
|
||||||
|
'SUBSCRIPTION_PATH': "/ws/graphql"
|
||||||
|
}
|
||||||
|
|
42
docs/subscriptions.rst
Normal file
42
docs/subscriptions.rst
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
Subscriptions
|
||||||
|
=============
|
||||||
|
|
||||||
|
The ``graphene-django`` project does not currently support GraphQL subscriptions out of the box. However, there are
|
||||||
|
several community-driven modules for adding subscription support, and the provided GraphiQL interface supports
|
||||||
|
running subscription operations over a websocket.
|
||||||
|
|
||||||
|
To implement websocket-based support for GraphQL subscriptions, you’ll need to do the following:
|
||||||
|
|
||||||
|
1. Install and configure `django-channels <https://channels.readthedocs.io/en/latest/installation.html>`_.
|
||||||
|
2. Install and configure* a third-party module for adding subscription support over websockets. A few options include:
|
||||||
|
|
||||||
|
- `graphql-python/graphql-ws <https://github.com/graphql-python/graphql-ws>`_
|
||||||
|
- `datavance/django-channels-graphql-ws <https://github.com/datadvance/DjangoChannelsGraphqlWs>`_
|
||||||
|
- `jaydenwindle/graphene-subscriptions <https://github.com/jaydenwindle/graphene-subscriptions>`_
|
||||||
|
|
||||||
|
3. Ensure that your application (or at least your GraphQL endpoint) is being served via an ASGI protocol server like
|
||||||
|
daphne (built in to ``django-channels``), `uvicorn <https://www.uvicorn.org/>`_, or
|
||||||
|
`hypercorn <https://pgjones.gitlab.io/hypercorn/>`_.
|
||||||
|
|
||||||
|
..
|
||||||
|
|
||||||
|
*** Note:** By default, the GraphiQL interface that comes with
|
||||||
|
``graphene-django`` assumes that you are handling subscriptions at
|
||||||
|
the same path as any other operation (i.e., you configured both
|
||||||
|
``urls.py`` and ``routing.py`` to handle GraphQL operations at the
|
||||||
|
same path, like ``/graphql``).
|
||||||
|
|
||||||
|
If these URLs differ, GraphiQL will try to run your subscription over
|
||||||
|
HTTP, which will produce an error. If you need to use a different URL
|
||||||
|
for handling websocket connections, you can configure
|
||||||
|
``SUBSCRIPTION_PATH`` in your ``settings.py``:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
GRAPHENE = {
|
||||||
|
# ...
|
||||||
|
"SUBSCRIPTION_PATH": "/ws/graphql" # The path you configured in `routing.py`, including a leading slash.
|
||||||
|
}
|
||||||
|
|
||||||
|
Once your application is properly configured to handle subscriptions, you can use the GraphiQL interface to test
|
||||||
|
subscriptions like any other operation.
|
|
@ -39,6 +39,8 @@ DEFAULTS = {
|
||||||
# Set to True to enable v3 naming convention for choice field Enum's
|
# Set to True to enable v3 naming convention for choice field Enum's
|
||||||
"DJANGO_CHOICE_FIELD_ENUM_V3_NAMING": False,
|
"DJANGO_CHOICE_FIELD_ENUM_V3_NAMING": False,
|
||||||
"DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME": None,
|
"DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME": None,
|
||||||
|
# Use a separate path for handling subscriptions.
|
||||||
|
"SUBSCRIPTION_PATH": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
|
|
@ -1,5 +1,13 @@
|
||||||
(function() {
|
(function (
|
||||||
|
document,
|
||||||
|
GRAPHENE_SETTINGS,
|
||||||
|
GraphiQL,
|
||||||
|
React,
|
||||||
|
ReactDOM,
|
||||||
|
SubscriptionsTransportWs,
|
||||||
|
history,
|
||||||
|
location,
|
||||||
|
) {
|
||||||
// Parse the cookie value for a CSRF token
|
// Parse the cookie value for a CSRF token
|
||||||
var csrftoken;
|
var csrftoken;
|
||||||
var cookies = ('; ' + document.cookie).split('; csrftoken=');
|
var cookies = ('; ' + document.cookie).split('; csrftoken=');
|
||||||
|
@ -11,7 +19,7 @@
|
||||||
|
|
||||||
// Collect the URL parameters
|
// Collect the URL parameters
|
||||||
var parameters = {};
|
var parameters = {};
|
||||||
window.location.hash.substr(1).split('&').forEach(function (entry) {
|
location.hash.substr(1).split('&').forEach(function (entry) {
|
||||||
var eq = entry.indexOf('=');
|
var eq = entry.indexOf('=');
|
||||||
if (eq >= 0) {
|
if (eq >= 0) {
|
||||||
parameters[decodeURIComponent(entry.slice(0, eq))] =
|
parameters[decodeURIComponent(entry.slice(0, eq))] =
|
||||||
|
@ -41,7 +49,7 @@
|
||||||
var fetchURL = locationQuery(otherParams);
|
var fetchURL = locationQuery(otherParams);
|
||||||
|
|
||||||
// Defines a GraphQL fetcher using the fetch API.
|
// Defines a GraphQL fetcher using the fetch API.
|
||||||
function graphQLFetcher(graphQLParams) {
|
function httpClient(graphQLParams) {
|
||||||
var headers = {
|
var headers = {
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
|
@ -64,6 +72,68 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Derive the subscription URL. If the SUBSCRIPTION_URL setting is specified, uses that value. Otherwise
|
||||||
|
// assumes the current window location with an appropriate websocket protocol.
|
||||||
|
var subscribeURL =
|
||||||
|
location.origin.replace(/^http/, "ws") +
|
||||||
|
(GRAPHENE_SETTINGS.subscriptionPath || location.pathname);
|
||||||
|
|
||||||
|
// Create a subscription client.
|
||||||
|
var subscriptionClient = new SubscriptionsTransportWs.SubscriptionClient(
|
||||||
|
subscribeURL,
|
||||||
|
{
|
||||||
|
// Reconnect after any interruptions.
|
||||||
|
reconnect: true,
|
||||||
|
// Delay socket initialization until the first subscription is started.
|
||||||
|
lazy: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Keep a reference to the currently-active subscription, if available.
|
||||||
|
var activeSubscription = null;
|
||||||
|
|
||||||
|
// Define a GraphQL fetcher that can intelligently route queries based on the operation type.
|
||||||
|
function graphQLFetcher(graphQLParams) {
|
||||||
|
var operationType = getOperationType(graphQLParams);
|
||||||
|
|
||||||
|
// If we're about to execute a new operation, and we have an active subscription,
|
||||||
|
// unsubscribe before continuing.
|
||||||
|
if (activeSubscription) {
|
||||||
|
activeSubscription.unsubscribe();
|
||||||
|
activeSubscription = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operationType === "subscription") {
|
||||||
|
return {
|
||||||
|
subscribe: function (observer) {
|
||||||
|
subscriptionClient.request(graphQLParams).subscribe(observer);
|
||||||
|
activeSubscription = subscriptionClient;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return httpClient(graphQLParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the type of operation being executed for a given set of GraphQL parameters.
|
||||||
|
function getOperationType(graphQLParams) {
|
||||||
|
// Run a regex against the query to determine the operation type (query, mutation, subscription).
|
||||||
|
var operationRegex = new RegExp(
|
||||||
|
// Look for lines that start with an operation keyword, ignoring whitespace.
|
||||||
|
"^\\s*(query|mutation|subscription)\\s+" +
|
||||||
|
// The operation keyword should be followed by the operationName in the GraphQL parameters.
|
||||||
|
graphQLParams.operationName +
|
||||||
|
// The line should eventually encounter an opening curly brace.
|
||||||
|
"[^\\{]*\\{",
|
||||||
|
// Enable multiline matching.
|
||||||
|
"m",
|
||||||
|
);
|
||||||
|
var match = operationRegex.exec(graphQLParams.query);
|
||||||
|
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
|
||||||
// When the query and variables string is edited, update the URL bar so
|
// When the query and variables string is edited, update the URL bar so
|
||||||
// that it can be easily shared.
|
// that it can be easily shared.
|
||||||
function onEditQuery(newQuery) {
|
function onEditQuery(newQuery) {
|
||||||
|
@ -83,10 +153,10 @@
|
||||||
}
|
}
|
||||||
var options = {
|
var options = {
|
||||||
fetcher: graphQLFetcher,
|
fetcher: graphQLFetcher,
|
||||||
onEditQuery: onEditQuery,
|
onEditQuery: onEditQuery,
|
||||||
onEditVariables: onEditVariables,
|
onEditVariables: onEditVariables,
|
||||||
onEditOperationName: onEditOperationName,
|
onEditOperationName: onEditOperationName,
|
||||||
query: parameters.query,
|
query: parameters.query,
|
||||||
}
|
}
|
||||||
if (parameters.variables) {
|
if (parameters.variables) {
|
||||||
options.variables = parameters.variables;
|
options.variables = parameters.variables;
|
||||||
|
@ -99,4 +169,13 @@
|
||||||
React.createElement(GraphiQL, options),
|
React.createElement(GraphiQL, options),
|
||||||
document.getElementById("editor")
|
document.getElementById("editor")
|
||||||
);
|
);
|
||||||
})();
|
})(
|
||||||
|
document,
|
||||||
|
window.GRAPHENE_SETTINGS,
|
||||||
|
window.GraphiQL,
|
||||||
|
window.React,
|
||||||
|
window.ReactDOM,
|
||||||
|
window.SubscriptionsTransportWs,
|
||||||
|
window.history,
|
||||||
|
window.location,
|
||||||
|
);
|
||||||
|
|
|
@ -29,10 +29,19 @@ add "&raw" to the end of the URL within a browser.
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.min.js"
|
<script src="https://cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.min.js"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/subscriptions-transport-ws@{{subscriptions_transport_ws_version}}/browser/client.js"
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="editor"></div>
|
<div id="editor"></div>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
<script type="application/javascript">
|
||||||
|
window.GRAPHENE_SETTINGS = {
|
||||||
|
{% if subscription_path %}
|
||||||
|
subscriptionPath: "{{subscription_path}}",
|
||||||
|
{% endif %}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
<script src="{% static 'graphene_django/graphiql.js' %}"></script>
|
<script src="{% static 'graphene_django/graphiql.js' %}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -52,9 +52,10 @@ def instantiate_middleware(middlewares):
|
||||||
|
|
||||||
|
|
||||||
class GraphQLView(View):
|
class GraphQLView(View):
|
||||||
graphiql_version = "0.14.0"
|
graphiql_version = "1.0.3"
|
||||||
graphiql_template = "graphene/graphiql.html"
|
graphiql_template = "graphene/graphiql.html"
|
||||||
react_version = "16.8.6"
|
react_version = "16.13.1"
|
||||||
|
subscriptions_transport_ws_version = "0.9.16"
|
||||||
|
|
||||||
schema = None
|
schema = None
|
||||||
graphiql = False
|
graphiql = False
|
||||||
|
@ -64,6 +65,7 @@ class GraphQLView(View):
|
||||||
root_value = None
|
root_value = None
|
||||||
pretty = False
|
pretty = False
|
||||||
batch = False
|
batch = False
|
||||||
|
subscription_path = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -75,6 +77,7 @@ class GraphQLView(View):
|
||||||
pretty=False,
|
pretty=False,
|
||||||
batch=False,
|
batch=False,
|
||||||
backend=None,
|
backend=None,
|
||||||
|
subscription_path=None,
|
||||||
):
|
):
|
||||||
if not schema:
|
if not schema:
|
||||||
schema = graphene_settings.SCHEMA
|
schema = graphene_settings.SCHEMA
|
||||||
|
@ -97,6 +100,8 @@ class GraphQLView(View):
|
||||||
self.graphiql = self.graphiql or graphiql
|
self.graphiql = self.graphiql or graphiql
|
||||||
self.batch = self.batch or batch
|
self.batch = self.batch or batch
|
||||||
self.backend = backend
|
self.backend = backend
|
||||||
|
if subscription_path is None:
|
||||||
|
subscription_path = graphene_settings.SUBSCRIPTION_PATH
|
||||||
|
|
||||||
assert isinstance(
|
assert isinstance(
|
||||||
self.schema, GraphQLSchema
|
self.schema, GraphQLSchema
|
||||||
|
@ -134,6 +139,8 @@ class GraphQLView(View):
|
||||||
request,
|
request,
|
||||||
graphiql_version=self.graphiql_version,
|
graphiql_version=self.graphiql_version,
|
||||||
react_version=self.react_version,
|
react_version=self.react_version,
|
||||||
|
subscriptions_transport_ws_version=self.subscriptions_transport_ws_version,
|
||||||
|
subscription_path=self.subscription_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.batch:
|
if self.batch:
|
||||||
|
|
Loading…
Reference in New Issue
Block a user