- track GraphiQL new version via @dependabot - automate GraphiQL integration glue adapting for new versions - rework `example/warp_subscriptions` to support subscriptions in new GraphiQL
This commit is contained in:
parent
f172be5656
commit
f9d90277bf
14 changed files with 257 additions and 138 deletions
5
.github/dependabot.yml
vendored
5
.github/dependabot.yml
vendored
|
@ -9,3 +9,8 @@ updates:
|
|||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: /juniper/
|
||||
schedule:
|
||||
interval: daily
|
||||
|
|
18
Makefile
18
Makefile
|
@ -148,6 +148,23 @@ book.serve:
|
|||
|
||||
|
||||
|
||||
######################
|
||||
# Forwarded commands #
|
||||
######################
|
||||
|
||||
# Download and prepare actual version of GraphiQL static files, used for
|
||||
# integrating it.
|
||||
#
|
||||
# Usage:
|
||||
# make graphiql
|
||||
|
||||
graphiql:
|
||||
@cd juniper/ && \
|
||||
make graphiql
|
||||
|
||||
|
||||
|
||||
|
||||
##################
|
||||
# .PHONY section #
|
||||
##################
|
||||
|
@ -155,4 +172,5 @@ book.serve:
|
|||
.PHONY: book fmt lint release test \
|
||||
book.build book.serve \
|
||||
cargo.fmt cargo.lint cargo.release cargo.test \
|
||||
graphiql \
|
||||
test.book test.cargo
|
||||
|
|
|
@ -11,6 +11,7 @@ env_logger = "0.10"
|
|||
futures = "0.3"
|
||||
juniper = { path = "../../juniper" }
|
||||
juniper_graphql_ws = { path = "../../juniper_graphql_ws" }
|
||||
juniper_graphql_transport_ws = { path = "../../juniper_graphql_transport_ws" }
|
||||
juniper_warp = { path = "../../juniper_warp", features = ["subscriptions"] }
|
||||
log = "0.4.8"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
|
|
@ -7,8 +7,12 @@ use juniper::{
|
|||
graphql_object, graphql_subscription, graphql_value, EmptyMutation, FieldError, GraphQLEnum,
|
||||
RootNode,
|
||||
};
|
||||
use juniper_graphql_ws::ConnectionConfig;
|
||||
use juniper_warp::{playground_filter, subscriptions::serve_graphql_ws};
|
||||
use juniper_graphql_transport_ws::ConnectionConfig;
|
||||
use juniper_graphql_ws::ConnectionConfig as LegacyConnectionConfig;
|
||||
use juniper_warp::{
|
||||
graphiql_filter, playground_filter,
|
||||
subscriptions::{serve_graphql_transport_ws, serve_graphql_ws},
|
||||
};
|
||||
use warp::{http::Response, Filter};
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -108,13 +112,13 @@ struct Subscription;
|
|||
#[graphql_subscription(context = Context)]
|
||||
impl Subscription {
|
||||
async fn users() -> UsersStream {
|
||||
let mut counter = 0;
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(5));
|
||||
let stream = async_stream::stream! {
|
||||
counter += 1;
|
||||
let mut counter = 0;
|
||||
loop {
|
||||
counter += 1;
|
||||
interval.tick().await;
|
||||
if counter == 2 {
|
||||
if counter == 5 {
|
||||
yield Err(FieldError::new(
|
||||
"some field error from handler",
|
||||
graphql_value!("some additional string"),
|
||||
|
@ -156,36 +160,58 @@ async fn main() {
|
|||
let qm_state = warp::any().map(|| Context);
|
||||
let qm_graphql_filter = juniper_warp::make_graphql_filter(qm_schema, qm_state.boxed());
|
||||
|
||||
let root_node = Arc::new(schema());
|
||||
let ws_schema = Arc::new(schema());
|
||||
let transport_ws_schema = ws_schema.clone();
|
||||
|
||||
log::info!("Listening on 127.0.0.1:8080");
|
||||
|
||||
let routes = (warp::path("subscriptions")
|
||||
let routes = warp::path("subscriptions")
|
||||
.and(warp::ws())
|
||||
.map(move |ws: warp::ws::Ws| {
|
||||
let root_node = root_node.clone();
|
||||
let transport_ws_schema = transport_ws_schema.clone();
|
||||
ws.on_upgrade(move |websocket| async move {
|
||||
serve_graphql_ws(websocket, root_node, ConnectionConfig::new(Context))
|
||||
.map(|r| {
|
||||
if let Err(e) = r {
|
||||
println!("Websocket error: {e}");
|
||||
}
|
||||
})
|
||||
.await
|
||||
serve_graphql_transport_ws(
|
||||
websocket,
|
||||
transport_ws_schema,
|
||||
ConnectionConfig::new(Context),
|
||||
)
|
||||
.map(|r| {
|
||||
if let Err(e) = r {
|
||||
println!("Websocket error: {e}");
|
||||
}
|
||||
})
|
||||
.await
|
||||
})
|
||||
}))
|
||||
.map(|reply| {
|
||||
// TODO#584: remove this workaround
|
||||
warp::reply::with_header(reply, "Sec-WebSocket-Protocol", "graphql-ws")
|
||||
})
|
||||
.or(warp::post()
|
||||
.and(warp::path("graphql"))
|
||||
.and(qm_graphql_filter))
|
||||
.or(warp::get()
|
||||
.and(warp::path("playground"))
|
||||
.and(playground_filter("/graphql", Some("/subscriptions"))))
|
||||
.or(homepage)
|
||||
.with(log);
|
||||
})
|
||||
.or(warp::path("legacy-subscriptions")
|
||||
.and(warp::ws())
|
||||
.map(move |ws: warp::ws::Ws| {
|
||||
let ws_schema = ws_schema.clone();
|
||||
ws.on_upgrade(move |websocket| async move {
|
||||
serve_graphql_ws(websocket, ws_schema, LegacyConnectionConfig::new(Context))
|
||||
.map(|r| {
|
||||
if let Err(e) = r {
|
||||
println!("Websocket error: {e}");
|
||||
}
|
||||
})
|
||||
.await
|
||||
})
|
||||
})
|
||||
.map(|reply| {
|
||||
// TODO#584: remove this workaround
|
||||
warp::reply::with_header(reply, "Sec-WebSocket-Protocol", "graphql-ws")
|
||||
}))
|
||||
.or(warp::post()
|
||||
.and(warp::path("graphql"))
|
||||
.and(qm_graphql_filter))
|
||||
.or(warp::get()
|
||||
.and(warp::path("playground"))
|
||||
.and(playground_filter("/graphql", Some("/legacy-subscriptions"))))
|
||||
.or(warp::get()
|
||||
.and(warp::path("graphiql"))
|
||||
.and(graphiql_filter("/graphql", Some("/subscriptions"))))
|
||||
.or(homepage)
|
||||
.with(log);
|
||||
|
||||
warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
|
||||
}
|
||||
|
|
3
juniper/.gitignore
vendored
Normal file
3
juniper/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/node_modules/
|
||||
/package-lock.json
|
||||
/yarn.lock
|
|
@ -51,6 +51,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
|
|||
- Disabled `chrono` [Cargo feature] by default.
|
||||
- Removed `scalar-naivetime` [Cargo feature].
|
||||
- Removed lifetime parameter from `ParseError`, `GraphlQLError`, `GraphQLBatchRequest` and `GraphQLRequest`. ([#1081], [#528])
|
||||
- Upgraded [GraphiQL] to 3.0.5 version (requires new [`graphql-ws` GraphQL over WebSocket Protocol] integration on server, see `examples/warp_subscriptions`). ([#1188])
|
||||
|
||||
### Added
|
||||
|
||||
|
@ -121,6 +122,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
|
|||
[#1145]: /../../pull/1145
|
||||
[#1147]: /../../pull/1147
|
||||
[#1176]: /../../pull/1176
|
||||
[#1188]: /../../pull/1188
|
||||
[ba1ed85b]: /../../commit/ba1ed85b3c3dd77fbae7baf6bc4e693321a94083
|
||||
[CVE-2022-31173]: /../../security/advisories/GHSA-4rx6-g5vg-5f3j
|
||||
|
||||
|
@ -140,6 +142,8 @@ See [old CHANGELOG](/../../blob/juniper-v0.15.9/juniper/CHANGELOG.md).
|
|||
[`chrono-tz` crate]: https://docs.rs/chrono-tz
|
||||
[`time` crate]: https://docs.rs/time
|
||||
[Cargo feature]: https://doc.rust-lang.org/cargo/reference/features.html
|
||||
[`graphql-ws` GraphQL over WebSocket Protocol]: https://github.com/graphql/graphiql
|
||||
[GraphiQL]: https://github.com/enisdenjo/graphql-ws/master/PROTOCOL.md
|
||||
[graphql-scalars.dev]: https://graphql-scalars.dev
|
||||
[October 2021]: https://spec.graphql.org/October2021
|
||||
[object safety]: https://doc.rust-lang.org/reference/items/traits.html#object-safety
|
||||
|
|
|
@ -18,7 +18,7 @@ repository = "https://github.com/graphql-rust/juniper"
|
|||
readme = "README.md"
|
||||
categories = ["asynchronous", "web-programming", "web-programming::http-server"]
|
||||
keywords = ["apollo", "graphql", "server", "web"]
|
||||
exclude = ["/release.toml"]
|
||||
include = ["/src/", "/CHANGELOG.md", "/LICENSE", "/README.md"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
|
56
juniper/Makefile
Normal file
56
juniper/Makefile
Normal file
|
@ -0,0 +1,56 @@
|
|||
###############################
|
||||
# Common defaults/definitions #
|
||||
###############################
|
||||
|
||||
# Checks two given strings for equality.
|
||||
eq = $(if $(or $(1),$(2)),$(and $(findstring $(1),$(2)),\
|
||||
$(findstring $(2),$(1))),1)
|
||||
|
||||
# Multiplatform prefix of `sed -i` commands.
|
||||
sed-i = sed -i$(if $(call eq,$(shell uname -s),Darwin), '',)
|
||||
|
||||
|
||||
|
||||
|
||||
######################
|
||||
# Project parameters #
|
||||
######################
|
||||
|
||||
GRAPHIQL_VER ?= $(strip \
|
||||
$(shell grep -m1 '"graphiql": "' package.json | cut -d '"' -f4))
|
||||
|
||||
|
||||
|
||||
|
||||
############
|
||||
# Commands #
|
||||
############
|
||||
|
||||
# Download and prepare actual version of GraphiQL static files, used for
|
||||
# integrating it.
|
||||
#
|
||||
# Usage:
|
||||
# make graphiql
|
||||
|
||||
graphiql:
|
||||
curl -fL -o src/http/graphiql.html \
|
||||
https://raw.githubusercontent.com/graphql/graphiql/graphiql%40$(GRAPHIQL_VER)/examples/graphiql-cdn/index.html
|
||||
$(sed-i) 's|https://unpkg.com/graphiql/|https://unpkg.com/graphiql@$(GRAPHIQL_VER)/|g' \
|
||||
src/http/graphiql.html
|
||||
$(sed-i) "s|'https://swapi-graphql.netlify.app/.netlify/functions/index'|GRAPHQL_URL|g" \
|
||||
src/http/graphiql.html
|
||||
$(sed-i) "s|url: GRAPHQL_URL,|url: GRAPHQL_URL,\n subscriptionUrl: normalizeSubscriptionEndpoint(GRAPHQL_URL, GRAPHQL_SUBSCRIPTIONS_URL)|" \
|
||||
src/http/graphiql.html
|
||||
$(sed-i) 's|<script>|<script>\n<!-- inject -->|' \
|
||||
src/http/graphiql.html
|
||||
$(sed-i) '/X-Example-Header/d' \
|
||||
src/http/graphiql.html
|
||||
|
||||
|
||||
|
||||
|
||||
##################
|
||||
# .PHONY section #
|
||||
##################
|
||||
|
||||
.PHONY: graphiql
|
9
juniper/package.json
Normal file
9
juniper/package.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"postinstall": "make graphiql"
|
||||
},
|
||||
"dependencies": {
|
||||
"graphiql": "3.0.5"
|
||||
}
|
||||
}
|
75
juniper/src/http/graphiql.html
Normal file
75
juniper/src/http/graphiql.html
Normal file
|
@ -0,0 +1,75 @@
|
|||
<!--
|
||||
* Copyright (c) 2021 GraphQL Contributors
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
-->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>GraphiQL</title>
|
||||
<style>
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#graphiql {
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!--
|
||||
This GraphiQL example depends on Promise and fetch, which are available in
|
||||
modern browsers, but can be "polyfilled" for older browsers.
|
||||
GraphiQL itself depends on React DOM.
|
||||
If you do not want to rely on a CDN, you can host these files locally or
|
||||
include them directly in your favored resource bundler.
|
||||
-->
|
||||
<script
|
||||
crossorigin
|
||||
src="https://unpkg.com/react@18/umd/react.development.js"
|
||||
></script>
|
||||
<script
|
||||
crossorigin
|
||||
src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"
|
||||
></script>
|
||||
<script
|
||||
src="https://unpkg.com/graphiql@3.0.5/graphiql.min.js"
|
||||
type="application/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="https://unpkg.com/@graphiql/plugin-explorer/dist/index.umd.js"
|
||||
crossorigin
|
||||
></script>
|
||||
<!--
|
||||
These two files can be found in the npm module, however you may wish to
|
||||
copy them directly into your environment, or perhaps include them in your
|
||||
favored resource bundler.
|
||||
-->
|
||||
<link rel="stylesheet" href="https://unpkg.com/graphiql@3.0.5/graphiql.min.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="graphiql">Loading...</div>
|
||||
<script>
|
||||
<!-- inject -->
|
||||
const root = ReactDOM.createRoot(document.getElementById('graphiql'));
|
||||
const fetcher = GraphiQL.createFetcher({
|
||||
url: GRAPHQL_URL,
|
||||
subscriptionUrl: normalizeSubscriptionEndpoint(GRAPHQL_URL, GRAPHQL_SUBSCRIPTIONS_URL)
|
||||
});
|
||||
const explorerPlugin = GraphiQLPluginExplorer.explorerPlugin();
|
||||
root.render(
|
||||
React.createElement(GraphiQL, {
|
||||
fetcher,
|
||||
defaultEditorToolsVisibility: true,
|
||||
plugins: [explorerPlugin],
|
||||
}),
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
14
juniper/src/http/graphiql.js
Normal file
14
juniper/src/http/graphiql.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
function normalizeSubscriptionEndpoint(endpoint, subscriptionEndpoint) {
|
||||
if (subscriptionEndpoint) {
|
||||
if (subscriptionEndpoint.startsWith('/')) {
|
||||
const secure =
|
||||
endpoint.includes('https') || location.href.includes('https')
|
||||
? 's'
|
||||
: ''
|
||||
return `ws${secure}://${location.host}${subscriptionEndpoint}`
|
||||
} else {
|
||||
return subscriptionEndpoint.replace(/^http/, 'ws')
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
///
|
||||
/// ```
|
||||
/// # use juniper::http::graphiql::graphiql_source;
|
||||
/// let graphiql = graphiql_source("/graphql", Some("ws://localhost:8080/subscriptions"));
|
||||
/// let graphiql = graphiql_source("/graphql", Some("/subscriptions"));
|
||||
/// ```
|
||||
pub fn graphiql_source(
|
||||
graphql_endpoint_url: &str,
|
||||
|
@ -18,110 +18,20 @@ pub fn graphiql_source(
|
|||
""
|
||||
};
|
||||
|
||||
let stylesheet_source = r#"
|
||||
<style>
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
include_str!("graphiql.html").replace(
|
||||
"<!-- inject -->",
|
||||
// Language: JavaScript
|
||||
&format!(
|
||||
"
|
||||
var GRAPHQL_URL = '{graphql_url}';
|
||||
var GRAPHQL_SUBSCRIPTIONS_URL = '{graphql_subscriptions_url}';
|
||||
|
||||
#graphiql {
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
"#;
|
||||
let fetcher_source = r#"
|
||||
<script>
|
||||
if (usingSubscriptions) {
|
||||
var subscriptionEndpoint = normalizeSubscriptionEndpoint(GRAPHQL_URL, GRAPHQL_SUBSCRIPTIONS_URL);
|
||||
var subscriptionsClient = new window.SubscriptionsTransportWs.SubscriptionClient(subscriptionEndpoint, { reconnect: true });
|
||||
}
|
||||
{grahiql_js}
|
||||
|
||||
function normalizeSubscriptionEndpoint(endpoint, subscriptionEndpoint) {
|
||||
if (subscriptionEndpoint) {
|
||||
if (subscriptionEndpoint.startsWith('/')) {
|
||||
const secure =
|
||||
endpoint.includes('https') || location.href.includes('https')
|
||||
? 's'
|
||||
: ''
|
||||
return `ws${secure}://${location.host}${subscriptionEndpoint}`
|
||||
} else {
|
||||
return subscriptionEndpoint.replace(/^http/, 'ws')
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function graphQLFetcher(graphQLParams, opts) {
|
||||
const { headers = {} } = opts;
|
||||
|
||||
return fetch(
|
||||
GRAPHQL_URL,
|
||||
{
|
||||
method: 'post',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...headers,
|
||||
},
|
||||
body: JSON.stringify(graphQLParams),
|
||||
credentials: 'omit',
|
||||
},
|
||||
).then(function (response) {
|
||||
return response.json().catch(function () {
|
||||
return response.text();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var fetcher = usingSubscriptions ? window.GraphiQLSubscriptionsFetcher.graphQLFetcher(subscriptionsClient, graphQLFetcher) : graphQLFetcher;
|
||||
|
||||
ReactDOM.render(
|
||||
React.createElement(GraphiQL, {
|
||||
fetcher,
|
||||
defaultVariableEditorOpen: true,
|
||||
}),
|
||||
document.getElementById('graphiql'),
|
||||
);
|
||||
</script>
|
||||
"#;
|
||||
|
||||
format!(
|
||||
r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>GraphQL</title>
|
||||
{stylesheet_source}
|
||||
<script
|
||||
crossorigin
|
||||
src="https://unpkg.com/react@17/umd/react.development.js"
|
||||
></script>
|
||||
<script
|
||||
crossorigin
|
||||
src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"
|
||||
></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/graphiql/graphiql.min.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="graphiql">Loading...</div>
|
||||
<script
|
||||
src="https://unpkg.com/graphiql/graphiql.min.js"
|
||||
type="application/javascript"
|
||||
></script>
|
||||
<script>var GRAPHQL_URL = '{graphql_url}';</script>
|
||||
<script>var usingSubscriptions = {using_subscriptions};</script>
|
||||
<script>var GRAPHQL_SUBSCRIPTIONS_URL = '{graphql_subscriptions_url}';</script>
|
||||
{fetcher_source}
|
||||
</body>
|
||||
</html>
|
||||
"#,
|
||||
graphql_url = graphql_endpoint_url,
|
||||
stylesheet_source = stylesheet_source,
|
||||
fetcher_source = fetcher_source,
|
||||
graphql_subscriptions_url = subscriptions_endpoint,
|
||||
using_subscriptions = subscriptions_endpoint_url.is_some(),
|
||||
",
|
||||
graphql_url = graphql_endpoint_url,
|
||||
graphql_subscriptions_url = subscriptions_endpoint,
|
||||
grahiql_js = include_str!("graphiql.js"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -515,10 +515,8 @@ mod tests {
|
|||
"text/html; charset=utf-8"
|
||||
);
|
||||
let body = take_response_body_string(resp).await;
|
||||
assert!(body.contains("<script>var GRAPHQL_URL = '/dogs-api/graphql';</script>"));
|
||||
assert!(body.contains(
|
||||
"<script>var GRAPHQL_SUBSCRIPTIONS_URL = '/dogs-api/subscriptions';</script>"
|
||||
))
|
||||
assert!(body.contains("var GRAPHQL_URL = '/dogs-api/graphql';"));
|
||||
assert!(body.contains("var GRAPHQL_SUBSCRIPTIONS_URL = '/dogs-api/subscriptions';"))
|
||||
}
|
||||
|
||||
#[actix_web::rt::test]
|
||||
|
|
|
@ -556,7 +556,7 @@ mod tests {
|
|||
);
|
||||
let body = String::from_utf8(response.body().to_vec()).unwrap();
|
||||
|
||||
assert!(body.contains("<script>var GRAPHQL_URL = '/dogs-api/graphql';</script>"));
|
||||
assert!(body.contains("var GRAPHQL_URL = '/dogs-api/graphql';"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
Loading…
Reference in a new issue