mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-07-12 09:12:18 +03:00
Update GraphiQL, add GraphiQL subscription support
* Update the GraphiQL template to use the latest versions of react, react-dom, graphiql, and (new) subscriptions-transport-ws. * Add support for websocket connections and subscriptions to the GraphiQL template. * Add a `SUBSCRIPTION_URL` configuration option to allow GraphiQL to route subscriptions to a different path (allowing for more advanced infrastructure scenarios). * Update the README to include some starting points for implementing subscriptions and configuring `SUBSCRIPTION_URL`.
This commit is contained in:
parent
1205e29bef
commit
ab569ea2d6
36
README.md
36
README.md
|
@ -59,6 +59,42 @@ urlpatterns = [
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Subscription Support
|
||||||
|
|
||||||
|
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 GraphiQL interface provided by
|
||||||
|
`graphene-django` supports subscriptions over websockets.
|
||||||
|
|
||||||
|
To implement websocket-based support for GraphQL subscriptions, you'll need to:
|
||||||
|
|
||||||
|
1. Install and configure [`django-channels`](https://channels.readthedocs.io/en/latest/installation.html).
|
||||||
|
2. Install and configure<sup>1, 2</sup> 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/).
|
||||||
|
|
||||||
|
> **<sup>1</sup> 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`:
|
||||||
|
>
|
||||||
|
> ```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.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
Here is a simple Django model:
|
Here is a simple Django model:
|
||||||
|
|
|
@ -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) {
|
||||||
|
@ -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