Skip to content

added support for subscriptions #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@ coverage.xml

# Environments
env
.idea
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ gql_view(request) # <-- the instance is callable and expects a `aiohttp.web.Req
- `encoder`: the encoder to use for responses (sensibly defaults to `graphql_server.json_encode`)
- `error_formatter`: the error formatter to use for responses (sensibly defaults to `graphql_server.default_format_error`)
- `enable_async`: whether `async` mode will be enabled.
- `subscriptions`: The [GraphiQL] socket endpoint for using subscriptions in [graphql-ws].


## Testing
Expand Down Expand Up @@ -81,3 +82,4 @@ This project is licensed under the MIT License.
[Apollo-Client]: http://dev.apollodata.com/core/network.html#query-batching
[Devin Fee]: https://github.com/dfee
[aiohttp-graphql]: https://github.com/dfee/aiohttp-graphql
[graphql-ws]: https://github.com/graphql-python/graphql-ws
3 changes: 3 additions & 0 deletions aiohttp_graphql/graphqlview.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def __init__(
encoder=None,
error_formatter=None,
enable_async=True,
subscriptions=None,
):
# pylint: disable=too-many-arguments
# pylint: disable=too-many-locals
Expand All @@ -58,6 +59,7 @@ def __init__(
self.executor,
AsyncioExecutor,
)
self.subscriptions = subscriptions
assert isinstance(self.schema, GraphQLSchema), \
'A Schema is required to be provided to GraphQLView.'

Expand Down Expand Up @@ -98,6 +100,7 @@ def render_graphiql(self, params, result):
result=result,
graphiql_version=self.graphiql_version,
graphiql_template=self.graphiql_template,
subscriptions=self.subscriptions,
)

def is_graphiql(self, request):
Expand Down
131 changes: 10 additions & 121 deletions aiohttp_graphql/render_graphiql.py
Original file line number Diff line number Diff line change
@@ -1,130 +1,17 @@
import os
import json
import re

from aiohttp import web


GRAPHIQL_VERSION = '0.11.10'

TEMPLATE = '''<!--
The request to this GraphQL server provided the header "Accept: text/html"
and as a result has been presented GraphiQL - an in-browser IDE for
exploring GraphQL.
If you wish to receive JSON, provide the header "Accept: application/json" or
add "&raw" to the end of the URL within a browser.
-->
<!DOCTYPE html>
<html>
<head>
<style>
html, body {
height: 100%;
margin: 0;
overflow: hidden;
width: 100%;
}
</style>
<meta name="referrer" content="no-referrer">
<link href="//cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.css" rel="stylesheet" />
<script src="//cdn.jsdelivr.net/gh/github/fetch@2.0.3/fetch.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/react@16.2.0/umd/react.production.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/react-dom@16.2.0/umd/react-dom.production.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.min.js"></script>
</head>
<body>
<script>
// Collect the URL parameters
var parameters = {};
window.location.search.substr(1).split('&').forEach(function (entry) {
var eq = entry.indexOf('=');
if (eq >= 0) {
parameters[decodeURIComponent(entry.slice(0, eq))] =
decodeURIComponent(entry.slice(eq + 1));
}
});

// Produce a Location query string from a parameter object.
function locationQuery(params) {
return '?' + Object.keys(params).map(function (key) {
return encodeURIComponent(key) + '=' +
encodeURIComponent(params[key]);
}).join('&');
}

// Derive a fetch URL from the current URL, sans the GraphQL parameters.
var graphqlParamNames = {
query: true,
variables: true,
operationName: true
};

var otherParams = {};
for (var k in parameters) {
if (parameters.hasOwnProperty(k) && graphqlParamNames[k] !== true) {
otherParams[k] = parameters[k];
}
}
var fetchURL = locationQuery(otherParams);

// Defines a GraphQL fetcher using the fetch API.
function graphQLFetcher(graphQLParams) {
return fetch(fetchURL, {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(graphQLParams),
credentials: 'include',
}).then(function (response) {
return response.text();
}).then(function (responseBody) {
try {
return JSON.parse(responseBody);
} catch (error) {
return responseBody;
}
});
}

// When the query and variables string is edited, update the URL bar so
// that it can be easily shared.
function onEditQuery(newQuery) {
parameters.query = newQuery;
updateURL();
}

function onEditVariables(newVariables) {
parameters.variables = newVariables;
updateURL();
}
GRAPHIQL_VERSION = '0.11.11'
MAIN_DIR = os.path.dirname(os.path.abspath(__file__))
TEMPLATE_PATH = os.path.join(MAIN_DIR, 'static/index.jinja2')

function onEditOperationName(newOperationName) {
parameters.operationName = newOperationName;
updateURL();
}

function updateURL() {
history.replaceState(null, null, locationQuery(parameters));
}

// Render <GraphiQL /> into the body.
ReactDOM.render(
React.createElement(GraphiQL, {
fetcher: graphQLFetcher,
onEditQuery: onEditQuery,
onEditVariables: onEditVariables,
onEditOperationName: onEditOperationName,
query: {{query|tojson}},
response: {{result|tojson}},
variables: {{variables|tojson}},
operationName: {{operation_name|tojson}},
}),
document.body
);
</script>
</body>
</html>'''
with open(TEMPLATE_PATH) as template_file:
TEMPLATE = template_file.read()


def escape_js_value(value):
Expand All @@ -150,8 +37,8 @@ def process_var(template, name, value, jsonify=False):


def simple_renderer(template, **values):
replace = ['graphiql_version']
replace_jsonify = ['query', 'result', 'variables', 'operation_name']
replace = ['graphiql_version', 'subscriptions', ]
replace_jsonify = ['query', 'result', 'variables', 'operation_name', ]

for rep in replace:
template = process_var(template, rep, values.get(rep, ''))
Expand All @@ -168,6 +55,7 @@ async def render_graphiql(
graphiql_template=None,
params=None,
result=None,
subscriptions=None,
):
graphiql_version = graphiql_version or GRAPHIQL_VERSION
template = graphiql_template or TEMPLATE
Expand All @@ -177,6 +65,7 @@ async def render_graphiql(
'variables': params and params.variables,
'operation_name': params and params.operation_name,
'result': result,
'subscriptions': subscriptions or '',
}

if jinja_env:
Expand Down
134 changes: 134 additions & 0 deletions aiohttp_graphql/static/index.jinja2
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<!--
The request to this GraphQL server provided the header "Accept: text/html"
and as a result has been presented GraphiQL - an in-browser IDE for
exploring GraphQL.
If you wish to receive JSON, provide the header "Accept: application/json" or
add "&raw" to the end of the URL within a browser.
-->
<!DOCTYPE html>
<html>
<head>
<style>
html, body {
height: 100%;
margin: 0;
overflow: hidden;
width: 100%;
}
</style>
<meta name="referrer" content="no-referrer">
<link href="//cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.css" rel="stylesheet" />
<script src="//cdn.jsdelivr.net/gh/github/fetch@2.0.3/fetch.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/react@16.2.0/umd/react.production.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/react-dom@16.2.0/umd/react-dom.production.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.min.js"></script>
<script src="//unpkg.com/subscriptions-transport-ws@0.7.0/browser/client.js"></script>
<script src="//unpkg.com/graphiql-subscriptions-fetcher@0.0.2/browser/client.js"></script>
<body>
<script>
// Collect the URL parameters
var parameters = {};
window.location.search.substr(1).split('&').forEach(function (entry) {
var eq = entry.indexOf('=');
if (eq >= 0) {
parameters[decodeURIComponent(entry.slice(0, eq))] =
decodeURIComponent(entry.slice(eq + 1));
}
});

// Produce a Location query string from a parameter object.
function locationQuery(params) {
return '?' + Object.keys(params).map(function (key) {
return encodeURIComponent(key) + '=' +
encodeURIComponent(params[key]);
}).join('&');
}

// Derive a fetch URL from the current URL, sans the GraphQL parameters.
var graphqlParamNames = {
query: true,
variables: true,
operationName: true
};

var otherParams = {};
for (var k in parameters) {
if (parameters.hasOwnProperty(k) && graphqlParamNames[k] !== true) {
otherParams[k] = parameters[k];
}
}

var subscriptionsFetcher;
if ('{{subscriptions}}') {
const subscriptionsClient = new window.SubscriptionsTransportWs.SubscriptionClient(
'{{ subscriptions }}',
{reconnect: true}
);

subscriptionsFetcher = window.GraphiQLSubscriptionsFetcher.graphQLFetcher(
subscriptionsClient,
graphQLFetcher
);
}

let fetchURL = locationQuery(otherParams);

// Defines a GraphQL fetcher using the fetch API.
function graphQLFetcher(graphQLParams) {
return fetch(fetchURL, {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(graphQLParams),
credentials: 'include',
}).then(function (response) {
return response.text();
}).then(function (responseBody) {
try {
return JSON.parse(responseBody);
} catch (error) {
return responseBody;
}
});
}

// When the query and variables string is edited, update the URL bar so
// that it can be easily shared.
function onEditQuery(newQuery) {
parameters.query = newQuery;
updateURL();
}

function onEditVariables(newVariables) {
parameters.variables = newVariables;
updateURL();
}

function onEditOperationName(newOperationName) {
parameters.operationName = newOperationName;
updateURL();
}

function updateURL() {
history.replaceState(null, null, locationQuery(parameters));
}

// Render <GraphiQL /> into the body.
ReactDOM.render(
React.createElement(GraphiQL, {
fetcher: subscriptionsFetcher || graphQLFetcher,
onEditQuery: onEditQuery,
onEditVariables: onEditVariables,
onEditOperationName: onEditOperationName,
query: {{query|tojson}},
response: {{result|tojson}},
variables: {{variables|tojson}},
operationName: {{operation_name|tojson}}
}),
document.body
);
</script>
</body>
</html>
4 changes: 2 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from urllib.parse import urlencode

from aiohttp import web
import aiohttp.test_utils
from aiohttp.test_utils import TestClient, TestServer
from graphql.execution.executors.asyncio import AsyncioExecutor
import pytest

Expand Down Expand Up @@ -34,7 +34,7 @@ def app(event_loop, executor, view_kwargs):

@pytest.fixture
async def client(event_loop, app):
client = aiohttp.test_utils.TestClient(app, loop=event_loop)
client = TestClient(TestServer(app), loop=event_loop)
await client.start_server()
yield client
await client.close()
Expand Down
13 changes: 12 additions & 1 deletion tests/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,18 @@ def resolve_raises(*args):
)


Schema = GraphQLSchema(QueryRootType, MutationRootType)
SubscriptionsRootType = GraphQLObjectType(
name='SubscriptionsRoot',
fields={
'subscriptionsTest': GraphQLField(
type=QueryRootType,
resolver=lambda *args: QueryRootType
)
}
)


Schema = GraphQLSchema(QueryRootType, MutationRootType, SubscriptionsRootType)


# Schema with async methods
Expand Down
12 changes: 12 additions & 0 deletions tests/test_graphiqlview.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,18 @@ async def test_graphiql_get_mutation(client, url_builder):
assert 'response: null' in await response.text()


@pytest.mark.asyncio
async def test_graphiql_get_subscriptions(client, url_builder):
response = await client.get(
url_builder(query=(
'subscription TestSubscriptions { subscriptionsTest { test } }'
)),
headers={'Accept': 'text/html'},
)
assert response.status == 200
assert 'response: null' in await response.text()


class TestAsyncSchema:
@pytest.fixture
def executor(self, event_loop):
Expand Down
Loading