2886 lines
119 KiB
HTML
2886 lines
119 KiB
HTML
<!DOCTYPE HTML>
|
|
<html lang="en" class="sidebar-visible no-js">
|
|
<head>
|
|
<!-- Book generated using mdBook -->
|
|
<meta charset="UTF-8">
|
|
<title>Juniper - GraphQL Server for Rust</title>
|
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
|
<meta name="description" content="Documentation for juniper, a GraphQL server library for Rust.">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="theme-color" content="#ffffff" />
|
|
|
|
<link rel="shortcut icon" href="favicon.png">
|
|
<link rel="stylesheet" href="css/variables.css">
|
|
<link rel="stylesheet" href="css/general.css">
|
|
<link rel="stylesheet" href="css/chrome.css">
|
|
<link rel="stylesheet" href="css/print.css" media="print">
|
|
|
|
<!-- Fonts -->
|
|
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
|
|
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800" rel="stylesheet" type="text/css">
|
|
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:500" rel="stylesheet" type="text/css">
|
|
|
|
<!-- Highlight.js Stylesheets -->
|
|
<link rel="stylesheet" href="highlight.css">
|
|
<link rel="stylesheet" href="tomorrow-night.css">
|
|
<link rel="stylesheet" href="ayu-highlight.css">
|
|
|
|
<!-- Custom theme stylesheets -->
|
|
|
|
|
|
|
|
</head>
|
|
<body class="light">
|
|
<!-- Provide site root to javascript -->
|
|
<script type="text/javascript">var path_to_root = "";</script>
|
|
|
|
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
|
<script type="text/javascript">
|
|
try {
|
|
var theme = localStorage.getItem('mdbook-theme');
|
|
var sidebar = localStorage.getItem('mdbook-sidebar');
|
|
|
|
if (theme.startsWith('"') && theme.endsWith('"')) {
|
|
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
|
}
|
|
|
|
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
|
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
|
}
|
|
} catch (e) { }
|
|
</script>
|
|
|
|
<!-- Set the theme before any content is loaded, prevents flash -->
|
|
<script type="text/javascript">
|
|
var theme;
|
|
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
|
if (theme === null || theme === undefined) { theme = 'light'; }
|
|
document.body.className = theme;
|
|
document.querySelector('html').className = theme + ' js';
|
|
</script>
|
|
|
|
<!-- Hide / unhide sidebar before it is displayed -->
|
|
<script type="text/javascript">
|
|
var html = document.querySelector('html');
|
|
var sidebar = 'hidden';
|
|
if (document.body.clientWidth >= 1080) {
|
|
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
|
sidebar = sidebar || 'visible';
|
|
}
|
|
html.classList.remove('sidebar-visible');
|
|
html.classList.add("sidebar-" + sidebar);
|
|
</script>
|
|
|
|
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
|
<ol class="chapter"><li class="affix"><a href="index.html">Introduction</a></li><li class="affix"><a href="quickstart.html">Quickstart</a></li><li class="affix"><a href="types/index.html">Type System</a></li><li><a href="types/objects/defining_objects.html"><strong aria-hidden="true">1.</strong> Defining objects</a></li><li><ol class="section"><li><a href="types/objects/complex_fields.html"><strong aria-hidden="true">1.1.</strong> Complex fields</a></li><li><a href="types/objects/using_contexts.html"><strong aria-hidden="true">1.2.</strong> Using contexts</a></li><li><a href="types/objects/error_handling.html"><strong aria-hidden="true">1.3.</strong> Error handling</a></li></ol></li><li><a href="types/other-index.html"><strong aria-hidden="true">2.</strong> Other types</a></li><li><ol class="section"><li><a href="types/enums.html"><strong aria-hidden="true">2.1.</strong> Enums</a></li><li><a href="types/interfaces.html"><strong aria-hidden="true">2.2.</strong> Interfaces</a></li><li><a href="types/input_objects.html"><strong aria-hidden="true">2.3.</strong> Input objects</a></li><li><a href="types/scalars.html"><strong aria-hidden="true">2.4.</strong> Scalars</a></li><li><a href="types/unions.html"><strong aria-hidden="true">2.5.</strong> Unions</a></li></ol></li><li><a href="schema/schemas_and_mutations.html"><strong aria-hidden="true">3.</strong> Schemas and mutations</a></li><li><a href="servers/index.html"><strong aria-hidden="true">4.</strong> Adding A Server</a></li><li><ol class="section"><li><a href="servers/official.html"><strong aria-hidden="true">4.1.</strong> Official Server Integrations</a></li><li><ol class="section"><li><a href="servers/warp.html"><strong aria-hidden="true">4.1.1.</strong> Warp</a></li><li><a href="servers/rocket.html"><strong aria-hidden="true">4.1.2.</strong> Rocket</a></li><li><a href="servers/iron.html"><strong aria-hidden="true">4.1.3.</strong> Iron</a></li><li><a href="servers/hyper.html"><strong aria-hidden="true">4.1.4.</strong> Hyper</a></li></ol></li><li><a href="servers/third-party.html"><strong aria-hidden="true">4.2.</strong> Third Party Integrations</a></li></ol></li><li><a href="advanced/index.html"><strong aria-hidden="true">5.</strong> Advanced Topics</a></li><li><ol class="section"><li><a href="advanced/introspection.html"><strong aria-hidden="true">5.1.</strong> Introspection</a></li><li><a href="advanced/non_struct_objects.html"><strong aria-hidden="true">5.2.</strong> Non-struct objects</a></li><li><a href="advanced/objects_and_generics.html"><strong aria-hidden="true">5.3.</strong> Objects and generics</a></li><li><a href="advanced/multiple_ops_per_request.html"><strong aria-hidden="true">5.4.</strong> Multiple operations per request</a></li><li><a href="advanced/dataloaders.html"><strong aria-hidden="true">5.5.</strong> Dataloaders</a></li><li><a href="advanced/subscriptions.html"><strong aria-hidden="true">5.6.</strong> Subscriptions</a></li></ol></li></ol>
|
|
</nav>
|
|
|
|
<div id="page-wrapper" class="page-wrapper">
|
|
|
|
<div class="page">
|
|
|
|
<div id="menu-bar" class="menu-bar">
|
|
<div id="menu-bar-sticky-container">
|
|
<div class="left-buttons">
|
|
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
|
<i class="fa fa-bars"></i>
|
|
</button>
|
|
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
|
<i class="fa fa-paint-brush"></i>
|
|
</button>
|
|
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
|
<li role="none"><button role="menuitem" class="theme" id="light">Light <span class="default">(default)</span></button></li>
|
|
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
|
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
|
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
|
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
|
|
</ul>
|
|
|
|
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
|
<i class="fa fa-search"></i>
|
|
</button>
|
|
|
|
</div>
|
|
|
|
<h1 class="menu-title">Juniper - GraphQL Server for Rust</h1>
|
|
|
|
<div class="right-buttons">
|
|
<a href="print.html" title="Print this book" aria-label="Print this book">
|
|
<i id="print-button" class="fa fa-print"></i>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div id="search-wrapper" class="hidden">
|
|
<form id="searchbar-outer" class="searchbar-outer">
|
|
<input type="search" name="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
|
</form>
|
|
<div id="searchresults-outer" class="searchresults-outer hidden">
|
|
<div id="searchresults-header" class="searchresults-header"></div>
|
|
<ul id="searchresults">
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
|
<script type="text/javascript">
|
|
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
|
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
|
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
|
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
|
});
|
|
</script>
|
|
|
|
<div id="content" class="content">
|
|
<main>
|
|
<a class="header" href="#juniper" id="juniper"><h1>Juniper</h1></a>
|
|
<p>Juniper is a <a href="http://graphql.org">GraphQL</a> server library for Rust. Build type-safe and fast API
|
|
servers with minimal boilerplate and configuration.</p>
|
|
<p><a href="http://graphql.org">GraphQL</a> is a data query language developed by Facebook intended to
|
|
serve mobile and web application frontends.</p>
|
|
<p><em>Juniper</em> makes it possible to write GraphQL servers in Rust that are
|
|
type-safe and blazingly fast. We also try to make declaring and resolving
|
|
GraphQL schemas as convenient as possible as Rust will allow.</p>
|
|
<p>Juniper does not include a web server - instead it provides building blocks to
|
|
make integration with existing servers straightforward. It optionally provides a
|
|
pre-built integration for the <a href="https://hyper.rs">Hyper</a>, <a href="http://ironframework.io">Iron</a>, <a href="https://rocket.rs">Rocket</a>, and <a href="https://github.com/seanmonstar/warp">Warp</a> frameworks, including
|
|
embedded <a href="https://github.com/graphql/graphiql">Graphiql</a> for easy debugging.</p>
|
|
<ul>
|
|
<li><a href="https://crates.io/crates/juniper">Cargo crate</a></li>
|
|
<li><a href="https://docs.rs/juniper">API Reference</a></li>
|
|
</ul>
|
|
<a class="header" href="#features" id="features"><h2>Features</h2></a>
|
|
<p>Juniper supports the full GraphQL query language according to the
|
|
<a href="http://facebook.github.io/graphql">specification</a>, including interfaces, unions, schema
|
|
introspection, and validations.
|
|
It does not, however, support the schema language.</p>
|
|
<p>As an exception to other GraphQL libraries for other languages, Juniper builds
|
|
non-null types by default. A field of type <code>Vec<Episode></code> will be converted into
|
|
<code>[Episode!]!</code>. The corresponding Rust type for e.g. <code>[Episode]</code> would be
|
|
<code>Option<Vec<Option<Episode>>></code>.</p>
|
|
<a class="header" href="#integrations" id="integrations"><h2>Integrations</h2></a>
|
|
<a class="header" href="#data-types" id="data-types"><h3>Data types</h3></a>
|
|
<p>Juniper has automatic integration with some very common Rust crates to make
|
|
building schemas a breeze. The types from these crates will be usable in
|
|
your Schemas automatically.</p>
|
|
<ul>
|
|
<li><a href="https://crates.io/crates/uuid">uuid</a></li>
|
|
<li><a href="https://crates.io/crates/url">url</a></li>
|
|
<li><a href="https://crates.io/crates/chrono">chrono</a></li>
|
|
<li><a href="https://crates.io/crates/bson">bson</a></li>
|
|
</ul>
|
|
<a class="header" href="#web-frameworks" id="web-frameworks"><h3>Web Frameworks</h3></a>
|
|
<ul>
|
|
<li><a href="https://hyper.rs">hyper</a></li>
|
|
<li><a href="https://rocket.rs">rocket</a></li>
|
|
<li><a href="http://ironframework.io">iron</a></li>
|
|
<li><a href="https://github.com/seanmonstar/warp">warp</a></li>
|
|
</ul>
|
|
<a class="header" href="#api-stability" id="api-stability"><h2>API Stability</h2></a>
|
|
<p>Juniper has not reached 1.0 yet, thus some API instability should be expected.</p>
|
|
<a class="header" href="#quickstart" id="quickstart"><h1>Quickstart</h1></a>
|
|
<p>This page will give you a short introduction to the concepts in Juniper.</p>
|
|
<p>Juniper follows a <a href="https://blog.logrocket.com/code-first-vs-schema-first-development-graphql/">code-first approach</a> to defining GraphQL schemas. If you would like to use a <a href="https://blog.logrocket.com/code-first-vs-schema-first-development-graphql/">schema-first approach</a> instead, consider <a href="https://github.com/davidpdrsn/juniper-from-schema">juniper-from-schema</a> for generating code from a schema file.</p>
|
|
<a class="header" href="#installation" id="installation"><h2>Installation</h2></a>
|
|
<p>!FILENAME Cargo.toml</p>
|
|
<pre><code class="language-toml">[dependencies]
|
|
juniper = "0.14.2"
|
|
</code></pre>
|
|
<a class="header" href="#schema-example" id="schema-example"><h2>Schema example</h2></a>
|
|
<p>Exposing simple enums and structs as GraphQL is just a matter of adding a custom
|
|
derive attribute to them. Juniper includes support for basic Rust types that
|
|
naturally map to GraphQL features, such as <code>Option<T></code>, <code>Vec<T></code>, <code>Box<T></code>,
|
|
<code>String</code>, <code>f64</code>, and <code>i32</code>, references, and slices.</p>
|
|
<p>For more advanced mappings, Juniper provides multiple macros to map your Rust
|
|
types to a GraphQL schema. The most important one is the
|
|
[object][jp_object] procedural macro that is used for declaring an object with
|
|
resolvers, which you will use for the <code>Query</code> and <code>Mutation</code> roots.</p>
|
|
<pre><pre class="playpen"><code class="language-rust">use juniper::{FieldResult, EmptySubscription};
|
|
|
|
# struct DatabasePool;
|
|
# impl DatabasePool {
|
|
# fn get_connection(&self) -> FieldResult<DatabasePool> { Ok(DatabasePool) }
|
|
# fn find_human(&self, _id: &str) -> FieldResult<Human> { Err("")? }
|
|
# fn insert_human(&self, _human: &NewHuman) -> FieldResult<Human> { Err("")? }
|
|
# }
|
|
|
|
#[derive(juniper::GraphQLEnum)]
|
|
enum Episode {
|
|
NewHope,
|
|
Empire,
|
|
Jedi,
|
|
}
|
|
|
|
#[derive(juniper::GraphQLObject)]
|
|
#[graphql(description="A humanoid creature in the Star Wars universe")]
|
|
struct Human {
|
|
id: String,
|
|
name: String,
|
|
appears_in: Vec<Episode>,
|
|
home_planet: String,
|
|
}
|
|
|
|
// There is also a custom derive for mapping GraphQL input objects.
|
|
|
|
#[derive(juniper::GraphQLInputObject)]
|
|
#[graphql(description="A humanoid creature in the Star Wars universe")]
|
|
struct NewHuman {
|
|
name: String,
|
|
appears_in: Vec<Episode>,
|
|
home_planet: String,
|
|
}
|
|
|
|
// Now, we create our root Query and Mutation types with resolvers by using the
|
|
// object macro.
|
|
// Objects can have contexts that allow accessing shared state like a database
|
|
// pool.
|
|
|
|
struct Context {
|
|
// Use your real database pool here.
|
|
pool: DatabasePool,
|
|
}
|
|
|
|
// To make our context usable by Juniper, we have to implement a marker trait.
|
|
impl juniper::Context for Context {}
|
|
|
|
struct Query;
|
|
|
|
#[juniper::graphql_object(
|
|
// Here we specify the context type for the object.
|
|
// We need to do this in every type that
|
|
// needs access to the context.
|
|
Context = Context,
|
|
)]
|
|
impl Query {
|
|
|
|
fn apiVersion() -> &str {
|
|
"1.0"
|
|
}
|
|
|
|
// Arguments to resolvers can either be simple types or input objects.
|
|
// To gain access to the context, we specify a argument
|
|
// that is a reference to the Context type.
|
|
// Juniper automatically injects the correct context here.
|
|
fn human(context: &Context, id: String) -> FieldResult<Human> {
|
|
// Get a db connection.
|
|
let connection = context.pool.get_connection()?;
|
|
// Execute a db query.
|
|
// Note the use of `?` to propagate errors.
|
|
let human = connection.find_human(&id)?;
|
|
// Return the result.
|
|
Ok(human)
|
|
}
|
|
}
|
|
|
|
// Now, we do the same for our Mutation type.
|
|
|
|
struct Mutation;
|
|
|
|
#[juniper::graphql_object(
|
|
Context = Context,
|
|
)]
|
|
impl Mutation {
|
|
|
|
fn createHuman(context: &Context, new_human: NewHuman) -> FieldResult<Human> {
|
|
let db = executor.context().pool.get_connection()?;
|
|
let human: Human = db.insert_human(&new_human)?;
|
|
Ok(human)
|
|
}
|
|
}
|
|
|
|
// A root schema consists of a query and a mutation.
|
|
// Request queries can be executed against a RootNode.
|
|
type Schema = juniper::RootNode<'static, Query, Mutation, EmptySubscription<Context>>;
|
|
|
|
# fn main() {
|
|
# let _ = Schema::new(Query, Mutation{}, EmptySubscription::new());
|
|
# }
|
|
</code></pre></pre>
|
|
<p>We now have a very simple but functional schema for a GraphQL server!</p>
|
|
<p>To actually serve the schema, see the guides for our various <a href="./servers/index.html">server integrations</a>.</p>
|
|
<p>You can also invoke the executor directly to get a result for a query:</p>
|
|
<a class="header" href="#executor" id="executor"><h2>Executor</h2></a>
|
|
<p>You can invoke <code>juniper::execute</code> directly to run a GraphQL query:</p>
|
|
<pre><pre class="playpen"><code class="language-rust"># // Only needed due to 2018 edition because the macro is not accessible.
|
|
# #[macro_use] extern crate juniper;
|
|
use juniper::{FieldResult, Variables, EmptyMutation, EmptySubscription};
|
|
|
|
|
|
#[derive(juniper::GraphQLEnum, Clone, Copy)]
|
|
enum Episode {
|
|
NewHope,
|
|
Empire,
|
|
Jedi,
|
|
}
|
|
|
|
// Arbitrary context data.
|
|
struct Ctx(Episode);
|
|
|
|
impl juniper::Context for Ctx {}
|
|
|
|
struct Query;
|
|
|
|
#[juniper::graphql_object(
|
|
Context = Ctx,
|
|
)]
|
|
impl Query {
|
|
fn favoriteEpisode(context: &Ctx) -> FieldResult<Episode> {
|
|
Ok(context.0)
|
|
}
|
|
}
|
|
|
|
|
|
// A root schema consists of a query and a mutation.
|
|
// Request queries can be executed against a RootNode.
|
|
type Schema = juniper::RootNode<'static, Query, EmptyMutation<Ctx>, EmptySubscription<Ctx>>;
|
|
|
|
fn main() {
|
|
// Create a context object.
|
|
let ctx = Ctx(Episode::NewHope);
|
|
|
|
// Run the executor.
|
|
let (res, _errors) = juniper::execute_sync(
|
|
"query { favoriteEpisode }",
|
|
None,
|
|
&Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()),
|
|
&Variables::new(),
|
|
&ctx,
|
|
).unwrap();
|
|
|
|
// Ensure the value matches.
|
|
assert_eq!(
|
|
res,
|
|
graphql_value!({
|
|
"favoriteEpisode": "NEW_HOPE",
|
|
})
|
|
);
|
|
}
|
|
</code></pre></pre>
|
|
<a class="header" href="#type-system" id="type-system"><h1>Type System</h1></a>
|
|
<p>Most of the work in working with juniper consists of mapping the
|
|
GraphQL type system to the Rust types your application uses.</p>
|
|
<p>Juniper provides some convenient abstractions that try to make this process
|
|
as painless as possible.</p>
|
|
<p>Find out more in the individual chapters below.</p>
|
|
<ul>
|
|
<li><a href="objects/defining_objects.html">Defining objects</a>
|
|
<ul>
|
|
<li><a href="objects/complex_fields.html">Complex fields</a></li>
|
|
<li><a href="objects/using_contexts.html">Using contexts</a></li>
|
|
<li><a href="objects/error_handling.html">Error handling</a></li>
|
|
</ul>
|
|
</li>
|
|
<li><a href="other-index.html">Other types</a>
|
|
<ul>
|
|
<li><a href="enums.html">Enums</a></li>
|
|
<li><a href="interfaces.html">Interfaces</a></li>
|
|
<li><a href="input_objects.html">Input objects</a></li>
|
|
<li><a href="scalars.html">Scalars</a></li>
|
|
<li><a href="unions.html">Unions</a></li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
<a class="header" href="#defining-objects" id="defining-objects"><h1>Defining objects</h1></a>
|
|
<p>While any type in Rust can be exposed as a GraphQL object, the most common one
|
|
is a struct.</p>
|
|
<p>There are two ways to create a GraphQL object in Juniper. If you've got a simple
|
|
struct you want to expose, the easiest way is to use the custom derive
|
|
attribute. The other way is described in the <a href="complex_fields.html">Complex fields</a>
|
|
chapter.</p>
|
|
<pre><pre class="playpen"><code class="language-rust">#[derive(juniper::GraphQLObject)]
|
|
struct Person {
|
|
name: String,
|
|
age: i32,
|
|
}
|
|
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<p>This will create a GraphQL object type called <code>Person</code>, with two fields: <code>name</code>
|
|
of type <code>String!</code>, and <code>age</code> of type <code>Int!</code>. Because of Rust's type system,
|
|
everything is exported as non-null by default. If you need a nullable field, you
|
|
can use <code>Option<T></code>.</p>
|
|
<p>We should take advantage of the
|
|
fact that GraphQL is self-documenting and add descriptions to the type and
|
|
fields. Juniper will automatically use associated doc comments as GraphQL
|
|
descriptions:</p>
|
|
<p>!FILENAME GraphQL descriptions via Rust doc comments</p>
|
|
<pre><pre class="playpen"><code class="language-rust">#[derive(juniper::GraphQLObject)]
|
|
/// Information about a person
|
|
struct Person {
|
|
/// The person's full name, including both first and last names
|
|
name: String,
|
|
/// The person's age in years, rounded down
|
|
age: i32,
|
|
}
|
|
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<p>Objects and fields without doc comments can instead set a <code>description</code>
|
|
via the <code>graphql</code> attribute. The following example is equivalent to the above:</p>
|
|
<p>!FILENAME GraphQL descriptions via attribute</p>
|
|
<pre><pre class="playpen"><code class="language-rust">#[derive(juniper::GraphQLObject)]
|
|
#[graphql(description="Information about a person")]
|
|
struct Person {
|
|
#[graphql(description="The person's full name, including both first and last names")]
|
|
name: String,
|
|
#[graphql(description="The person's age in years, rounded down")]
|
|
age: i32,
|
|
}
|
|
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<p>Descriptions set via the <code>graphql</code> attribute take precedence over Rust
|
|
doc comments. This enables internal Rust documentation and external GraphQL
|
|
documentation to differ:</p>
|
|
<pre><pre class="playpen"><code class="language-rust">#[derive(juniper::GraphQLObject)]
|
|
#[graphql(description="This description shows up in GraphQL")]
|
|
/// This description shows up in RustDoc
|
|
struct Person {
|
|
#[graphql(description="This description shows up in GraphQL")]
|
|
/// This description shows up in RustDoc
|
|
name: String,
|
|
/// This description shows up in both RustDoc and GraphQL
|
|
age: i32,
|
|
}
|
|
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<a class="header" href="#relationships" id="relationships"><h2>Relationships</h2></a>
|
|
<p>You can only use the custom derive attribute under these circumstances:</p>
|
|
<ul>
|
|
<li>The annotated type is a <code>struct</code>,</li>
|
|
<li>Every struct field is either
|
|
<ul>
|
|
<li>A primitive type (<code>i32</code>, <code>f64</code>, <code>bool</code>, <code>String</code>, <code>juniper::ID</code>), or</li>
|
|
<li>A valid custom GraphQL type, e.g. another struct marked with this attribute,
|
|
or</li>
|
|
<li>A container/reference containing any of the above, e.g. <code>Vec<T></code>, <code>Box<T></code>,
|
|
<code>Option<T></code></li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
<p>Let's see what that means for building relationships between objects:</p>
|
|
<pre><pre class="playpen"><code class="language-rust">#[derive(juniper::GraphQLObject)]
|
|
struct Person {
|
|
name: String,
|
|
age: i32,
|
|
}
|
|
|
|
#[derive(juniper::GraphQLObject)]
|
|
struct House {
|
|
address: Option<String>, // Converted into String (nullable)
|
|
inhabitants: Vec<Person>, // Converted into [Person!]!
|
|
}
|
|
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<p>Because <code>Person</code> is a valid GraphQL type, you can have a <code>Vec<Person></code> in a
|
|
struct and it'll be automatically converted into a list of non-nullable <code>Person</code>
|
|
objects.</p>
|
|
<a class="header" href="#renaming-fields" id="renaming-fields"><h2>Renaming fields</h2></a>
|
|
<p>By default, struct fields are converted from Rust's standard <code>snake_case</code> naming
|
|
convention into GraphQL's <code>camelCase</code> convention:</p>
|
|
<pre><pre class="playpen"><code class="language-rust">#[derive(juniper::GraphQLObject)]
|
|
struct Person {
|
|
first_name: String, // Would be exposed as firstName in the GraphQL schema
|
|
last_name: String, // Exposed as lastName
|
|
}
|
|
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<p>You can override the name by using the <code>graphql</code> attribute on individual struct
|
|
fields:</p>
|
|
<pre><pre class="playpen"><code class="language-rust">#[derive(juniper::GraphQLObject)]
|
|
struct Person {
|
|
name: String,
|
|
age: i32,
|
|
#[graphql(name="websiteURL")]
|
|
website_url: Option<String>, // Now exposed as websiteURL in the schema
|
|
}
|
|
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<a class="header" href="#deprecating-fields" id="deprecating-fields"><h2>Deprecating fields</h2></a>
|
|
<p>To deprecate a field, you specify a deprecation reason using the <code>graphql</code>
|
|
attribute:</p>
|
|
<pre><pre class="playpen"><code class="language-rust">#[derive(juniper::GraphQLObject)]
|
|
struct Person {
|
|
name: String,
|
|
age: i32,
|
|
#[graphql(deprecated = "Please use the name field instead")]
|
|
first_name: String,
|
|
}
|
|
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<p>The <code>name</code>, <code>description</code>, and <code>deprecation</code> arguments can of course be
|
|
combined. Some restrictions from the GraphQL spec still applies though; you can
|
|
only deprecate object fields and enum values.</p>
|
|
<a class="header" href="#skipping-fields" id="skipping-fields"><h2>Skipping fields</h2></a>
|
|
<p>By default all fields in a <code>GraphQLObject</code> are included in the generated GraphQL type. To prevent including a specific field, annotate the field with <code>#[graphql(skip)]</code>:</p>
|
|
<pre><pre class="playpen"><code class="language-rust">#[derive(juniper::GraphQLObject)]
|
|
struct Person {
|
|
name: String,
|
|
age: i32,
|
|
#[graphql(skip)]
|
|
# #[allow(dead_code)]
|
|
password_hash: String, // This cannot be queried or modified from GraphQL
|
|
}
|
|
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<a class="header" href="#complex-fields" id="complex-fields"><h1>Complex fields</h1></a>
|
|
<p>If you've got a struct that can't be mapped directly to GraphQL, that contains
|
|
computed fields or circular structures, you have to use a more powerful tool:
|
|
the <code>object</code> procedural macro. This macro lets you define GraphQL object
|
|
fields in a Rust <code>impl</code> block for a type. Note that only GraphQL fields
|
|
can be specified in this <code>impl</code> block. If you want to define normal methods on the struct,
|
|
you have to do so in a separate, normal <code>impl</code> block. Continuing with the
|
|
example from the last chapter, this is how you would define <code>Person</code> using the
|
|
macro:</p>
|
|
<pre><pre class="playpen"><code class="language-rust">
|
|
struct Person {
|
|
name: String,
|
|
age: i32,
|
|
}
|
|
|
|
#[juniper::graphql_object]
|
|
impl Person {
|
|
fn name(&self) -> &str {
|
|
self.name.as_str()
|
|
}
|
|
|
|
fn age(&self) -> i32 {
|
|
self.age
|
|
}
|
|
}
|
|
|
|
// Note that this syntax generates an implementation of the GraphQLType trait,
|
|
// the base impl of your struct can still be written like usual:
|
|
impl Person {
|
|
pub fn hidden_from_graphql(&self) {
|
|
// [...]
|
|
}
|
|
}
|
|
|
|
# fn main() { }
|
|
</code></pre></pre>
|
|
<p>While this is a bit more verbose, it lets you write any kind of function in the
|
|
field resolver. With this syntax, fields can also take arguments:</p>
|
|
<pre><pre class="playpen"><code class="language-rust">#[derive(juniper::GraphQLObject)]
|
|
struct Person {
|
|
name: String,
|
|
age: i32,
|
|
}
|
|
|
|
struct House {
|
|
inhabitants: Vec<Person>,
|
|
}
|
|
|
|
#[juniper::graphql_object]
|
|
impl House {
|
|
// Creates the field inhabitantWithName(name), returning a nullable person
|
|
fn inhabitant_with_name(&self, name: String) -> Option<&Person> {
|
|
self.inhabitants.iter().find(|p| p.name == name)
|
|
}
|
|
}
|
|
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<p>To access global data such as database connections or authentication
|
|
information, a <em>context</em> is used. To learn more about this, see the next
|
|
chapter: <a href="using_contexts.html">Using contexts</a>.</p>
|
|
<a class="header" href="#description-renaming-and-deprecation" id="description-renaming-and-deprecation"><h2>Description, renaming, and deprecation</h2></a>
|
|
<p>Like with the derive attribute, field names will be converted from <code>snake_case</code>
|
|
to <code>camelCase</code>. If you need to override the conversion, you can simply rename
|
|
the field. Also, the type name can be changed with an alias:</p>
|
|
<pre><pre class="playpen"><code class="language-rust">
|
|
struct Person {
|
|
}
|
|
|
|
/// Doc comments are used as descriptions for GraphQL.
|
|
#[juniper::graphql_object(
|
|
// With this attribtue you can change the public GraphQL name of the type.
|
|
name = "PersonObject",
|
|
// You can also specify a description here, which will overwrite
|
|
// a doc comment description.
|
|
description = "...",
|
|
)]
|
|
impl Person {
|
|
|
|
/// A doc comment on the field will also be used for GraphQL.
|
|
#[graphql(
|
|
// Or provide a description here.
|
|
description = "...",
|
|
)]
|
|
fn doc_comment(&self) -> &str {
|
|
""
|
|
}
|
|
|
|
// Fields can also be renamed if required.
|
|
#[graphql(
|
|
name = "myCustomFieldName",
|
|
)]
|
|
fn renamed_field() -> bool {
|
|
true
|
|
}
|
|
|
|
// Deprecations also work as you'd expect.
|
|
// Both the standard Rust syntax and a custom attribute is accepted.
|
|
#[deprecated(note = "...")]
|
|
fn deprecated_standard() -> bool {
|
|
false
|
|
}
|
|
|
|
#[graphql(deprecated = "...")]
|
|
fn deprecated_graphql() -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
# fn main() { }
|
|
</code></pre></pre>
|
|
<a class="header" href="#customizing-arguments" id="customizing-arguments"><h2>Customizing arguments</h2></a>
|
|
<p>Method field arguments can also be customized.</p>
|
|
<p>They can have custom descriptions and default values.</p>
|
|
<p><strong>Note</strong>: The syntax for this is currently a little awkward.
|
|
This will become better once the <a href="https://github.com/rust-lang/rust/issues/60406">Rust RFC 2565</a> is implemented.</p>
|
|
<pre><pre class="playpen"><code class="language-rust">
|
|
struct Person {}
|
|
|
|
#[juniper::graphql_object]
|
|
impl Person {
|
|
#[graphql(
|
|
arguments(
|
|
arg1(
|
|
// Set a default value which will be injected if not present.
|
|
// The default can be any valid Rust expression, including a function call, etc.
|
|
default = true,
|
|
// Set a description.
|
|
description = "The first argument..."
|
|
),
|
|
arg2(
|
|
default = 0,
|
|
)
|
|
)
|
|
)]
|
|
fn field1(&self, arg1: bool, arg2: i32) -> String {
|
|
format!("{} {}", arg1, arg2)
|
|
}
|
|
}
|
|
|
|
# fn main() { }
|
|
</code></pre></pre>
|
|
<a class="header" href="#more-features" id="more-features"><h2>More features</h2></a>
|
|
<p>GraphQL fields expose more features than Rust's standard method syntax gives us:</p>
|
|
<ul>
|
|
<li>Per-field description and deprecation messages</li>
|
|
<li>Per-argument default values</li>
|
|
<li>Per-argument descriptions</li>
|
|
</ul>
|
|
<p>These, and more features, are described more thorougly in <a href="https://docs.rs/juniper/latest/juniper/macro.object.html">the reference
|
|
documentation</a>.</p>
|
|
<a class="header" href="#using-contexts" id="using-contexts"><h1>Using contexts</h1></a>
|
|
<p>The context type is a feature in Juniper that lets field resolvers access global
|
|
data, most commonly database connections or authentication information. The
|
|
context is usually created from a <em>context factory</em>. How this is defined is
|
|
specific to the framework integration you're using, so check out the
|
|
documentation for either the <a href="../../servers/iron.html">Iron</a> or <a href="../../servers/rocket.html">Rocket</a>
|
|
integration.</p>
|
|
<p>In this chapter, we'll show you how to define a context type and use it in field
|
|
resolvers. Let's say that we have a simple user database in a <code>HashMap</code>:</p>
|
|
<pre><pre class="playpen"><code class="language-rust"># #![allow(dead_code)]
|
|
# use std::collections::HashMap;
|
|
|
|
struct Database {
|
|
users: HashMap<i32, User>,
|
|
}
|
|
|
|
struct User {
|
|
id: i32,
|
|
name: String,
|
|
friend_ids: Vec<i32>,
|
|
}
|
|
|
|
# fn main() { }
|
|
</code></pre></pre>
|
|
<p>We would like a <code>friends</code> field on <code>User</code> that returns a list of <code>User</code> objects.
|
|
In order to write such a field though, the database must be queried.</p>
|
|
<p>To solve this, we mark the <code>Database</code> as a valid context type and assign it to
|
|
the user object.</p>
|
|
<p>To gain access to the context, we need to specify an argument with the same
|
|
type as the specified <code>Context</code> for the type:</p>
|
|
<pre><pre class="playpen"><code class="language-rust"># use std::collections::HashMap;
|
|
extern crate juniper;
|
|
|
|
// This struct represents our context.
|
|
struct Database {
|
|
users: HashMap<i32, User>,
|
|
}
|
|
|
|
// Mark the Database as a valid context type for Juniper
|
|
impl juniper::Context for Database {}
|
|
|
|
struct User {
|
|
id: i32,
|
|
name: String,
|
|
friend_ids: Vec<i32>,
|
|
}
|
|
|
|
|
|
// Assign Database as the context type for User
|
|
#[juniper::graphql_object(
|
|
Context = Database,
|
|
)]
|
|
impl User {
|
|
// 3. Inject the context by specifying an argument
|
|
// with the context type.
|
|
// Note:
|
|
// - the type must be a reference
|
|
// - the name of the argument SHOULD be context
|
|
fn friends(&self, context: &Database) -> Vec<&User> {
|
|
|
|
// 5. Use the database to lookup users
|
|
self.friend_ids.iter()
|
|
.map(|id| context.users.get(id).expect("Could not find user with ID"))
|
|
.collect()
|
|
}
|
|
|
|
fn name(&self) -> &str {
|
|
self.name.as_str()
|
|
}
|
|
|
|
fn id(&self) -> i32 {
|
|
self.id
|
|
}
|
|
}
|
|
|
|
# fn main() { }
|
|
</code></pre></pre>
|
|
<p>You only get an immutable reference to the context, so if you want to affect
|
|
change to the execution, you'll need to use <a href="https://doc.rust-lang.org/book/first-edition/mutability.html#interior-vs-exterior-mutability">interior
|
|
mutability</a>
|
|
using e.g. <code>RwLock</code> or <code>RefCell</code>.</p>
|
|
<a class="header" href="#error-handling" id="error-handling"><h1>Error handling</h1></a>
|
|
<p>Error handling in GraphQL can be done in multiple ways. In the
|
|
following two different error handling models are discussed: field
|
|
results and GraphQL schema backed errors. Each approach has its
|
|
advantages. Choosing the right error handling method depends on the
|
|
requirements of the application--investigating both approaches is
|
|
beneficial.</p>
|
|
<a class="header" href="#field-results" id="field-results"><h2>Field Results</h2></a>
|
|
<p>Rust
|
|
<a href="https://doc.rust-lang.org/book/second-edition/ch09-00-error-handling.html">provides</a>
|
|
two ways of dealing with errors: <code>Result<T, E></code> for recoverable errors and
|
|
<code>panic!</code> for unrecoverable errors. Juniper does not do anything about panicking;
|
|
it will bubble up to the surrounding framework and hopefully be dealt with
|
|
there.</p>
|
|
<p>For recoverable errors, Juniper works well with the built-in <code>Result</code> type, you
|
|
can use the <code>?</code> operator or the <code>try!</code> macro and things will generally just work
|
|
as you expect them to:</p>
|
|
<pre><pre class="playpen"><code class="language-rust"># extern crate juniper;
|
|
use std::{
|
|
str,
|
|
path::PathBuf,
|
|
fs::{File},
|
|
io::{Read},
|
|
};
|
|
use juniper::FieldResult;
|
|
|
|
struct Example {
|
|
filename: PathBuf,
|
|
}
|
|
|
|
#[juniper::graphql_object]
|
|
impl Example {
|
|
fn contents() -> FieldResult<String> {
|
|
let mut file = File::open(&self.filename)?;
|
|
let mut contents = String::new();
|
|
file.read_to_string(&mut contents)?;
|
|
Ok(contents)
|
|
}
|
|
|
|
fn foo() -> FieldResult<Option<String>> {
|
|
// Some invalid bytes.
|
|
let invalid = vec![128, 223];
|
|
|
|
match str::from_utf8(&invalid) {
|
|
Ok(s) => Ok(Some(s.to_string())),
|
|
Err(e) => Err(e)?,
|
|
}
|
|
}
|
|
}
|
|
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<p><code>FieldResult<T></code> is an alias for <code>Result<T, FieldError></code>, which is the error
|
|
type all fields must return. By using the <code>?</code> operator or <code>try!</code> macro, any type
|
|
that implements the <code>Display</code> trait - which are most of the error types out
|
|
there - those errors are automatically converted into <code>FieldError</code>.</p>
|
|
<p>When a field returns an error, the field's result is replaced by <code>null</code>, an
|
|
additional <code>errors</code> object is created at the top level of the response, and the
|
|
execution is resumed. For example, with the previous example and the following
|
|
query:</p>
|
|
<pre><code class="language-graphql">{
|
|
example {
|
|
contents
|
|
foo
|
|
}
|
|
}
|
|
</code></pre>
|
|
<p>If <code>str::from_utf8</code> resulted in a <code>std::str::Utf8Error</code>, the following would be
|
|
returned:</p>
|
|
<p>!FILENAME Response for nullable field with error</p>
|
|
<pre><code class="language-js">{
|
|
"data": {
|
|
"example": {
|
|
contents: "<Contents of the file>",
|
|
foo: null,
|
|
}
|
|
},
|
|
"errors": [
|
|
"message": "invalid utf-8 sequence of 2 bytes from index 0",
|
|
"locations": [{ "line": 2, "column": 4 }])
|
|
]
|
|
}
|
|
</code></pre>
|
|
<p>If an error is returned from a non-null field, such as the
|
|
example above, the <code>null</code> value is propagated up to the first nullable parent
|
|
field, or the root <code>data</code> object if there are no nullable fields.</p>
|
|
<p>For example, with the following query:</p>
|
|
<pre><code class="language-graphql">{
|
|
example {
|
|
contents
|
|
}
|
|
}
|
|
</code></pre>
|
|
<p>If <code>File::open()</code> above resulted in <code>std::io::ErrorKind::PermissionDenied</code>, the
|
|
following would be returned:</p>
|
|
<p>!FILENAME Response for non-null field with error and no nullable parent</p>
|
|
<pre><code class="language-js">{
|
|
"errors": [
|
|
"message": "Permission denied (os error 13)",
|
|
"locations": [{ "line": 2, "column": 4 }])
|
|
]
|
|
}
|
|
</code></pre>
|
|
<a class="header" href="#structured-errors" id="structured-errors"><h3>Structured errors</h3></a>
|
|
<p>Sometimes it is desirable to return additional structured error information
|
|
to clients. This can be accomplished by implementing <a href="https://docs.rs/juniper/latest/juniper/trait.IntoFieldError.html"><code>IntoFieldError</code></a>:</p>
|
|
<pre><pre class="playpen"><code class="language-rust"># #[macro_use] extern crate juniper;
|
|
enum CustomError {
|
|
WhateverNotSet,
|
|
}
|
|
|
|
impl juniper::IntoFieldError for CustomError {
|
|
fn into_field_error(self) -> juniper::FieldError {
|
|
match self {
|
|
CustomError::WhateverNotSet => juniper::FieldError::new(
|
|
"Whatever does not exist",
|
|
graphql_value!({
|
|
"type": "NO_WHATEVER"
|
|
}),
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Example {
|
|
whatever: Option<bool>,
|
|
}
|
|
|
|
#[juniper::graphql_object]
|
|
impl Example {
|
|
fn whatever() -> Result<bool, CustomError> {
|
|
if let Some(value) = self.whatever {
|
|
return Ok(value);
|
|
}
|
|
Err(CustomError::WhateverNotSet)
|
|
}
|
|
}
|
|
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<p>The specified structured error information is included in the <a href="https://facebook.github.io/graphql/June2018/#sec-Errors"><code>extensions</code></a> key:</p>
|
|
<pre><code class="language-js">{
|
|
"errors": [
|
|
"message": "Whatever does not exist",
|
|
"locations": [{ "line": 2, "column": 4 }]),
|
|
"extensions": {
|
|
"type": "NO_WHATEVER"
|
|
}
|
|
]
|
|
}
|
|
</code></pre>
|
|
<a class="header" href="#errors-backed-by-graphqls-schema" id="errors-backed-by-graphqls-schema"><h2>Errors Backed by GraphQL's Schema</h2></a>
|
|
<p>Rust's model of errors can be adapted for GraphQL. Rust's panic is
|
|
similar to a <code>FieldError</code>--the whole query is aborted and nothing can
|
|
be extracted (except for error related information).</p>
|
|
<p>Not all errors require this strict handling. Recoverable or partial errors can be put
|
|
into the GraphQL schema so the client can intelligently handle them.</p>
|
|
<p>To implement this approach, all errors must be partitioned into two error classes:</p>
|
|
<ul>
|
|
<li>Critical errors that cannot be fixed by the user (e.g. a database error).</li>
|
|
<li>Recoverable errors that can be fixed by the user (e.g. invalid input data).</li>
|
|
</ul>
|
|
<p>Critical errors are returned from resolvers as <code>FieldErrors</code> (from the previous section). Non-critical errors are part of the GraphQL schema and can be handled gracefully by clients. Similar to Rust, GraphQL allows similar error models with unions (see Unions).</p>
|
|
<a class="header" href="#example-input-validation-simple" id="example-input-validation-simple"><h3>Example Input Validation (simple)</h3></a>
|
|
<p>In this example, basic input validation is implemented with GraphQL
|
|
types. Strings are used to identify the problematic field name. Errors
|
|
for a particular field are also returned as a string. In this example
|
|
the string contains a server-side localized error message. However, it is also
|
|
possible to return a unique string identifier and have the client present a localized string to the user.</p>
|
|
<pre><pre class="playpen"><code class="language-rust">#[derive(juniper::GraphQLObject)]
|
|
pub struct Item {
|
|
name: String,
|
|
quantity: i32,
|
|
}
|
|
|
|
#[derive(juniper::GraphQLObject)]
|
|
pub struct ValidationError {
|
|
field: String,
|
|
message: String,
|
|
}
|
|
|
|
#[derive(juniper::GraphQLObject)]
|
|
pub struct ValidationErrors {
|
|
errors: Vec<ValidationError>,
|
|
}
|
|
|
|
#[derive(juniper::GraphQLUnion)]
|
|
pub enum GraphQLResult {
|
|
Ok(Item),
|
|
Err(ValidationErrors),
|
|
}
|
|
|
|
pub struct Mutation;
|
|
|
|
#[juniper::graphql_object]
|
|
impl Mutation {
|
|
fn addItem(&self, name: String, quantity: i32) -> GraphQLResult {
|
|
let mut errors = Vec::new();
|
|
|
|
if !(10 <= name.len() && name.len() <= 100) {
|
|
errors.push(ValidationError {
|
|
field: "name".to_string(),
|
|
message: "between 10 and 100".to_string()
|
|
});
|
|
}
|
|
|
|
if !(1 <= quantity && quantity <= 10) {
|
|
errors.push(ValidationError {
|
|
field: "quantity".to_string(),
|
|
message: "between 1 and 10".to_string()
|
|
});
|
|
}
|
|
|
|
if errors.is_empty() {
|
|
GraphQLResult::Ok(Item { name, quantity })
|
|
} else {
|
|
GraphQLResult::Err(ValidationErrors { errors })
|
|
}
|
|
}
|
|
}
|
|
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<p>Each function may have a different return type and depending on the input
|
|
parameters a new result type is required. For example, adding a user
|
|
requires a new result type which contains the variant <code>Ok(User)</code>
|
|
instead of <code>Ok(Item)</code>.</p>
|
|
<p>The client can send a mutation request and handle the
|
|
resulting errors as shown in the following example:</p>
|
|
<pre><code class="language-graphql">{
|
|
mutation {
|
|
addItem(name: "", quantity: 0) {
|
|
... on Item {
|
|
name
|
|
}
|
|
... on ValidationErrors {
|
|
errors {
|
|
field
|
|
message
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</code></pre>
|
|
<p>A useful side effect of this approach is to have partially successful
|
|
queries or mutations. If one resolver fails, the results of the
|
|
successful resolvers are not discarded.</p>
|
|
<a class="header" href="#example-input-validation-complex" id="example-input-validation-complex"><h3>Example Input Validation (complex)</h3></a>
|
|
<p>Instead of using strings to propagate errors, it is possible to use
|
|
GraphQL's type system to describe the errors more precisely.</p>
|
|
<p>For each fallible input variable a field in a GraphQL object is created. The
|
|
field is set if the validation for that particular field fails. You will likely want some kind of code generation to reduce repetition as the number of types required is significantly larger than
|
|
before. Each resolver function has a custom <code>ValidationResult</code> which
|
|
contains only fields provided by the function.</p>
|
|
<pre><pre class="playpen"><code class="language-rust">#[derive(juniper::GraphQLObject)]
|
|
pub struct Item {
|
|
name: String,
|
|
quantity: i32,
|
|
}
|
|
|
|
#[derive(juniper::GraphQLObject)]
|
|
pub struct ValidationError {
|
|
name: Option<String>,
|
|
quantity: Option<String>,
|
|
}
|
|
|
|
#[derive(juniper::GraphQLUnion)]
|
|
pub enum GraphQLResult {
|
|
Ok(Item),
|
|
Err(ValidationError),
|
|
}
|
|
|
|
pub struct Mutation;
|
|
|
|
#[juniper::graphql_object]
|
|
impl Mutation {
|
|
fn addItem(&self, name: String, quantity: i32) -> GraphQLResult {
|
|
let mut error = ValidationError {
|
|
name: None,
|
|
quantity: None,
|
|
};
|
|
|
|
if !(10 <= name.len() && name.len() <= 100) {
|
|
error.name = Some("between 10 and 100".to_string());
|
|
}
|
|
|
|
if !(1 <= quantity && quantity <= 10) {
|
|
error.quantity = Some("between 1 and 10".to_string());
|
|
}
|
|
|
|
if error.name.is_none() && error.quantity.is_none() {
|
|
GraphQLResult::Ok(Item { name, quantity })
|
|
} else {
|
|
GraphQLResult::Err(error)
|
|
}
|
|
}
|
|
}
|
|
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<pre><code class="language-graphql">{
|
|
mutation {
|
|
addItem {
|
|
... on Item {
|
|
name
|
|
}
|
|
... on ValidationErrorsItem {
|
|
name
|
|
quantity
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</code></pre>
|
|
<p>Expected errors are handled directly inside the query. Additionally, all
|
|
non-critical errors are known in advance by both the server and the client.</p>
|
|
<a class="header" href="#example-input-validation-complex-with-critical-error" id="example-input-validation-complex-with-critical-error"><h3>Example Input Validation (complex with critical error)</h3></a>
|
|
<p>Our examples so far have only included non-critical errors. Providing
|
|
errors inside the GraphQL schema still allows you to return unexpected critical
|
|
errors when they occur.</p>
|
|
<p>In the following example, a theoretical database could fail
|
|
and would generate errors. Since it is not common for the database to
|
|
fail, the corresponding error is returned as a critical error:</p>
|
|
<pre><pre class="playpen"><code class="language-rust"># #[macro_use] extern crate juniper;
|
|
|
|
#[derive(juniper::GraphQLObject)]
|
|
pub struct Item {
|
|
name: String,
|
|
quantity: i32,
|
|
}
|
|
|
|
#[derive(juniper::GraphQLObject)]
|
|
pub struct ValidationErrorItem {
|
|
name: Option<String>,
|
|
quantity: Option<String>,
|
|
}
|
|
|
|
#[derive(juniper::GraphQLUnion)]
|
|
pub enum GraphQLResult {
|
|
Ok(Item),
|
|
Err(ValidationErrorItem),
|
|
}
|
|
|
|
pub enum ApiError {
|
|
Database,
|
|
}
|
|
|
|
impl juniper::IntoFieldError for ApiError {
|
|
fn into_field_error(self) -> juniper::FieldError {
|
|
match self {
|
|
ApiError::Database => juniper::FieldError::new(
|
|
"Internal database error",
|
|
graphql_value!({
|
|
"type": "DATABASE"
|
|
}),
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct Mutation;
|
|
|
|
#[juniper::graphql_object]
|
|
impl Mutation {
|
|
fn addItem(&self, name: String, quantity: i32) -> Result<GraphQLResult, ApiError> {
|
|
let mut error = ValidationErrorItem {
|
|
name: None,
|
|
quantity: None,
|
|
};
|
|
|
|
if !(10 <= name.len() && name.len() <= 100) {
|
|
error.name = Some("between 10 and 100".to_string());
|
|
}
|
|
|
|
if !(1 <= quantity && quantity <= 10) {
|
|
error.quantity = Some("between 1 and 10".to_string());
|
|
}
|
|
|
|
if error.name.is_none() && error.quantity.is_none() {
|
|
Ok(GraphQLResult::Ok(Item { name, quantity }))
|
|
} else {
|
|
Ok(GraphQLResult::Err(error))
|
|
}
|
|
}
|
|
}
|
|
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<a class="header" href="#additional-material" id="additional-material"><h2>Additional Material</h2></a>
|
|
<p>The <a href="https://shopify.dev/docs/admin-api/graphql/reference">Shopify API</a>
|
|
implements a similar approach. Their API is a good reference to
|
|
explore this approach in a real world application.</p>
|
|
<a class="header" href="#comparison" id="comparison"><h1>Comparison</h1></a>
|
|
<p>The first approach discussed above--where every error is a critical error defined by <code>FieldResult</code> --is easier to implement. However, the client does not know what errors may occur and must instead infer what happened from the error string. This is brittle and could change over time due to either the client or server changing. Therefore, extensive integration testing between the client and server is required to maintain the implicit contract between the two.</p>
|
|
<p>Encoding non-critical errors in the GraphQL schema makes the contract between the client and the server explicit. This allows the client to understand and handle these errors correctly and the server to know when changes are potentially breaking clients. However, encoding this error information into the GraphQL schema requires additional code and up-front definition of non-critical errors.</p>
|
|
<a class="header" href="#other-types" id="other-types"><h1>Other Types</h1></a>
|
|
<p>The GraphQL type system provides several types in additon to objects.</p>
|
|
<p>Find out more about each type below:</p>
|
|
<ul>
|
|
<li><a href="enums.html">Enums</a></li>
|
|
<li><a href="interfaces.html">Interfaces</a></li>
|
|
<li><a href="input_objects.html">Input objects</a></li>
|
|
<li><a href="scalars.html">Scalars</a></li>
|
|
<li><a href="unions.html">Unions</a></li>
|
|
</ul>
|
|
<a class="header" href="#enums" id="enums"><h1>Enums</h1></a>
|
|
<p>Enums in GraphQL are string constants grouped together to represent a set of
|
|
possible values. Simple Rust enums can be converted to GraphQL enums by using a
|
|
custom derive attribute:</p>
|
|
<pre><pre class="playpen"><code class="language-rust">#[derive(juniper::GraphQLEnum)]
|
|
enum Episode {
|
|
NewHope,
|
|
Empire,
|
|
Jedi,
|
|
}
|
|
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<p>Juniper converts all enum variants to uppercase, so the corresponding string
|
|
values for these variants are <code>NEWHOPE</code>, <code>EMPIRE</code>, and <code>JEDI</code>, respectively. If
|
|
you want to override this, you can use the <code>graphql</code> attribute, similar to how
|
|
it works when <a href="objects/defining_objects.html">defining objects</a>:</p>
|
|
<pre><pre class="playpen"><code class="language-rust">#[derive(juniper::GraphQLEnum)]
|
|
enum Episode {
|
|
#[graphql(name="NEW_HOPE")]
|
|
NewHope,
|
|
Empire,
|
|
Jedi,
|
|
}
|
|
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<a class="header" href="#documentation-and-deprecation" id="documentation-and-deprecation"><h2>Documentation and deprecation</h2></a>
|
|
<p>Just like when defining objects, the type itself can be renamed and documented,
|
|
while individual enum variants can be renamed, documented, and deprecated:</p>
|
|
<pre><pre class="playpen"><code class="language-rust">#[derive(juniper::GraphQLEnum)]
|
|
#[graphql(name="Episode", description="An episode of Star Wars")]
|
|
enum StarWarsEpisode {
|
|
#[graphql(deprecated="We don't really talk about this one")]
|
|
ThePhantomMenace,
|
|
|
|
#[graphql(name="NEW_HOPE")]
|
|
NewHope,
|
|
|
|
#[graphql(description="Arguably the best one in the trilogy")]
|
|
Empire,
|
|
Jedi,
|
|
}
|
|
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<a class="header" href="#supported-macro-attributes-derive" id="supported-macro-attributes-derive"><h2>Supported Macro Attributes (Derive)</h2></a>
|
|
<table><thead><tr><th> Name of Attribute </th><th align="center"> Container Support </th><th align="center"> Field Support </th></tr></thead><tbody>
|
|
<tr><td> context </td><td align="center"> ✔ </td><td align="center"> ? </td></tr>
|
|
<tr><td> deprecated </td><td align="center"> ✔ </td><td align="center"> ✔ </td></tr>
|
|
<tr><td> description </td><td align="center"> ✔ </td><td align="center"> ✔ </td></tr>
|
|
<tr><td> interfaces </td><td align="center"> ? </td><td align="center"> ✘ </td></tr>
|
|
<tr><td> name </td><td align="center"> ✔ </td><td align="center"> ✔ </td></tr>
|
|
<tr><td> noasync </td><td align="center"> ✔ </td><td align="center"> ? </td></tr>
|
|
<tr><td> scalar </td><td align="center"> ✘ </td><td align="center"> ? </td></tr>
|
|
<tr><td> skip </td><td align="center"> ? </td><td align="center"> ✘ </td></tr>
|
|
<tr><td> ✔: supported </td><td align="center"> ✘: not supported </td><td align="center"> ?: not available </td></tr>
|
|
</tbody></table>
|
|
<a class="header" href="#interfaces" id="interfaces"><h1>Interfaces</h1></a>
|
|
<p>GraphQL interfaces map well to interfaces known from common object-oriented
|
|
languages such as Java or C#, but Rust has unfortunately not a concept that maps
|
|
perfectly to them. Because of this, defining interfaces in Juniper can require a
|
|
little bit of boilerplate code, but on the other hand gives you full control
|
|
over which type is backing your interface.</p>
|
|
<p>To highlight a couple of different ways you can implement interfaces in Rust,
|
|
let's have a look at the same end-result from a few different implementations:</p>
|
|
<a class="header" href="#traits" id="traits"><h2>Traits</h2></a>
|
|
<p>Traits are maybe the most obvious concept you want to use when building
|
|
interfaces. But because GraphQL supports downcasting while Rust doesn't, you'll
|
|
have to manually specify how to convert a trait into a concrete type. This can
|
|
be done in a couple of different ways:</p>
|
|
<a class="header" href="#downcasting-via-accessor-methods" id="downcasting-via-accessor-methods"><h3>Downcasting via accessor methods</h3></a>
|
|
<pre><code class="language-rust ignore">#[derive(juniper::GraphQLObject)]
|
|
struct Human {
|
|
id: String,
|
|
home_planet: String,
|
|
}
|
|
|
|
#[derive(juniper::GraphQLObject)]
|
|
struct Droid {
|
|
id: String,
|
|
primary_function: String,
|
|
}
|
|
|
|
trait Character {
|
|
fn id(&self) -> &str;
|
|
|
|
// Downcast methods, each concrete class will need to implement one of these
|
|
fn as_human(&self) -> Option<&Human> { None }
|
|
fn as_droid(&self) -> Option<&Droid> { None }
|
|
}
|
|
|
|
impl Character for Human {
|
|
fn id(&self) -> &str { self.id.as_str() }
|
|
fn as_human(&self) -> Option<&Human> { Some(&self) }
|
|
}
|
|
|
|
impl Character for Droid {
|
|
fn id(&self) -> &str { self.id.as_str() }
|
|
fn as_droid(&self) -> Option<&Droid> { Some(&self) }
|
|
}
|
|
|
|
juniper::graphql_interface!(<'a> &'a dyn Character: () as "Character" where Scalar = <S> |&self| {
|
|
field id() -> &str { self.id() }
|
|
|
|
instance_resolvers: |_| {
|
|
// The left hand side indicates the concrete type T, the right hand
|
|
// side should be an expression returning Option<T>
|
|
&Human => self.as_human(),
|
|
&Droid => self.as_droid(),
|
|
}
|
|
});
|
|
|
|
# fn main() {}
|
|
</code></pre>
|
|
<p>The <code>instance_resolvers</code> declaration lists all the implementors of the given
|
|
interface and how to resolve them.</p>
|
|
<p>As you can see, you lose a bit of the point with using traits: you need to list
|
|
all the concrete types in the trait itself, and there's a bit of repetition
|
|
going on.</p>
|
|
<a class="header" href="#using-an-extra-database-lookup" id="using-an-extra-database-lookup"><h3>Using an extra database lookup</h3></a>
|
|
<p>If you can afford an extra database lookup when the concrete class is requested,
|
|
you can do away with the downcast methods and use the context instead. Here,
|
|
we'll use two hashmaps, but this could be two tables and some SQL calls instead:</p>
|
|
<pre><code class="language-rust ignore"># use std::collections::HashMap;
|
|
#[derive(juniper::GraphQLObject)]
|
|
#[graphql(Context = Database)]
|
|
struct Human {
|
|
id: String,
|
|
home_planet: String,
|
|
}
|
|
|
|
#[derive(juniper::GraphQLObject)]
|
|
#[graphql(Context = Database)]
|
|
struct Droid {
|
|
id: String,
|
|
primary_function: String,
|
|
}
|
|
|
|
struct Database {
|
|
humans: HashMap<String, Human>,
|
|
droids: HashMap<String, Droid>,
|
|
}
|
|
|
|
impl juniper::Context for Database {}
|
|
|
|
trait Character {
|
|
fn id(&self) -> &str;
|
|
}
|
|
|
|
impl Character for Human {
|
|
fn id(&self) -> &str { self.id.as_str() }
|
|
}
|
|
|
|
impl Character for Droid {
|
|
fn id(&self) -> &str { self.id.as_str() }
|
|
}
|
|
|
|
juniper::graphql_interface!(<'a> &'a dyn Character: Database as "Character" where Scalar = <S> |&self| {
|
|
field id() -> &str { self.id() }
|
|
|
|
instance_resolvers: |&context| {
|
|
&Human => context.humans.get(self.id()),
|
|
&Droid => context.droids.get(self.id()),
|
|
}
|
|
});
|
|
|
|
# fn main() {}
|
|
</code></pre>
|
|
<p>This removes the need of downcast methods, but still requires some repetition.</p>
|
|
<a class="header" href="#placeholder-objects" id="placeholder-objects"><h2>Placeholder objects</h2></a>
|
|
<p>Continuing on from the last example, the trait itself seems a bit unneccesary.
|
|
Maybe it can just be a struct containing the ID?</p>
|
|
<pre><code class="language-rust ignore"># use std::collections::HashMap;
|
|
#[derive(juniper::GraphQLObject)]
|
|
#[graphql(Context = "Database")]
|
|
struct Human {
|
|
id: String,
|
|
home_planet: String,
|
|
}
|
|
|
|
#[derive(juniper::GraphQLObject)]
|
|
#[graphql(Context = "Database")]
|
|
struct Droid {
|
|
id: String,
|
|
primary_function: String,
|
|
}
|
|
|
|
struct Database {
|
|
humans: HashMap<String, Human>,
|
|
droids: HashMap<String, Droid>,
|
|
}
|
|
|
|
impl juniper::Context for Database {}
|
|
|
|
struct Character {
|
|
id: String,
|
|
}
|
|
|
|
juniper::graphql_interface!(Character: Database where Scalar = <S> |&self| {
|
|
field id() -> &str { self.id.as_str() }
|
|
|
|
instance_resolvers: |&context| {
|
|
&Human => context.humans.get(&self.id),
|
|
&Droid => context.droids.get(&self.id),
|
|
}
|
|
});
|
|
|
|
# fn main() {}
|
|
</code></pre>
|
|
<p>This reduces repetition some more, but might be impractical if the interface's
|
|
surface area is large.</p>
|
|
<a class="header" href="#enums-1" id="enums-1"><h2>Enums</h2></a>
|
|
<p>Using enums and pattern matching lies half-way between using traits and using
|
|
placeholder objects. We don't need the extra database call in this case, so
|
|
we'll remove it.</p>
|
|
<pre><code class="language-rust ignore">#[derive(juniper::GraphQLObject)]
|
|
struct Human {
|
|
id: String,
|
|
home_planet: String,
|
|
}
|
|
|
|
#[derive(juniper::GraphQLObject)]
|
|
struct Droid {
|
|
id: String,
|
|
primary_function: String,
|
|
}
|
|
|
|
# #[allow(dead_code)]
|
|
enum Character {
|
|
Human(Human),
|
|
Droid(Droid),
|
|
}
|
|
|
|
juniper::graphql_interface!(Character: () where Scalar = <S> |&self| {
|
|
field id() -> &str {
|
|
match *self {
|
|
Character::Human(Human { ref id, .. }) |
|
|
Character::Droid(Droid { ref id, .. }) => id,
|
|
}
|
|
}
|
|
|
|
instance_resolvers: |_| {
|
|
&Human => match *self { Character::Human(ref h) => Some(h), _ => None },
|
|
&Droid => match *self { Character::Droid(ref d) => Some(d), _ => None },
|
|
}
|
|
});
|
|
|
|
# fn main() {}
|
|
</code></pre>
|
|
<a class="header" href="#input-objects" id="input-objects"><h1>Input objects</h1></a>
|
|
<p>Input objects are complex data structures that can be used as arguments to
|
|
GraphQL fields. In Juniper, you can define input objects using a custom derive
|
|
attribute, similar to simple objects and enums:</p>
|
|
<pre><pre class="playpen"><code class="language-rust">#[derive(juniper::GraphQLInputObject)]
|
|
struct Coordinate {
|
|
latitude: f64,
|
|
longitude: f64
|
|
}
|
|
|
|
struct Root;
|
|
# #[derive(juniper::GraphQLObject)] struct User { name: String }
|
|
|
|
#[juniper::graphql_object]
|
|
impl Root {
|
|
fn users_at_location(coordinate: Coordinate, radius: f64) -> Vec<User> {
|
|
// Send coordinate to database
|
|
// ...
|
|
# unimplemented!()
|
|
}
|
|
}
|
|
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<a class="header" href="#documentation-and-renaming" id="documentation-and-renaming"><h2>Documentation and renaming</h2></a>
|
|
<p>Just like the <a href="objects/defining_objects.html">other</a> <a href="enums.html">derives</a>, you can rename
|
|
and add documentation to both the type and the fields:</p>
|
|
<pre><pre class="playpen"><code class="language-rust">#[derive(juniper::GraphQLInputObject)]
|
|
#[graphql(name="Coordinate", description="A position on the globe")]
|
|
struct WorldCoordinate {
|
|
#[graphql(name="lat", description="The latitude")]
|
|
latitude: f64,
|
|
|
|
#[graphql(name="long", description="The longitude")]
|
|
longitude: f64
|
|
}
|
|
|
|
struct Root;
|
|
# #[derive(juniper::GraphQLObject)] struct User { name: String }
|
|
|
|
#[juniper::graphql_object]
|
|
impl Root {
|
|
fn users_at_location(coordinate: WorldCoordinate, radius: f64) -> Vec<User> {
|
|
// Send coordinate to database
|
|
// ...
|
|
# unimplemented!()
|
|
}
|
|
}
|
|
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<a class="header" href="#scalars" id="scalars"><h1>Scalars</h1></a>
|
|
<p>Scalars are the primitive types at the leaves of a GraphQL query: numbers,
|
|
strings, and booleans. You can create custom scalars to other primitive values,
|
|
but this often requires coordination with the client library intended to consume
|
|
the API you're building.</p>
|
|
<p>Since any value going over the wire is eventually transformed into JSON, you're
|
|
also limited in the data types you can use.</p>
|
|
<p>There are two ways to define custom scalars.</p>
|
|
<ul>
|
|
<li>For simple scalars that just wrap a primitive type, you can use the newtype pattern with
|
|
a custom derive.</li>
|
|
<li>For more advanced use cases with custom validation, you can use
|
|
the <code>graphql_scalar</code> proc macro.</li>
|
|
</ul>
|
|
<a class="header" href="#built-in-scalars" id="built-in-scalars"><h2>Built-in scalars</h2></a>
|
|
<p>Juniper has built-in support for:</p>
|
|
<ul>
|
|
<li><code>i32</code> as <code>Int</code></li>
|
|
<li><code>f64</code> as <code>Float</code></li>
|
|
<li><code>String</code> and <code>&str</code> as <code>String</code></li>
|
|
<li><code>bool</code> as <code>Boolean</code></li>
|
|
<li><code>juniper::ID</code> as <code>ID</code>. This type is defined <a href="http://facebook.github.io/graphql/#sec-ID">in the
|
|
spec</a> as a type that is serialized
|
|
as a string but can be parsed from both a string and an integer.</li>
|
|
</ul>
|
|
<p>Note that there is no built-in support for <code>i64</code>/<code>u64</code>, as the GraphQL spec <a href="https://spec.graphql.org/June2018/#sec-Int">doesn't define any built-in scalars for <code>i64</code>/<code>u64</code> by default</a>. You may wish to leverage a <a href="#custom-scalars">custom GraphQL scalar</a> in your schema to support them.</p>
|
|
<p><strong>Third party types</strong>:</p>
|
|
<p>Juniper has built-in support for a few additional types from common third party
|
|
crates. They are enabled via features that are on by default.</p>
|
|
<ul>
|
|
<li>uuid::Uuid</li>
|
|
<li>chrono::DateTime</li>
|
|
<li>url::Url</li>
|
|
<li>bson::oid::ObjectId</li>
|
|
</ul>
|
|
<a class="header" href="#newtype-pattern" id="newtype-pattern"><h2>newtype pattern</h2></a>
|
|
<p>Often, you might need a custom scalar that just wraps an existing type.</p>
|
|
<p>This can be done with the newtype pattern and a custom derive, similar to how
|
|
serde supports this pattern with <code>#[serde(transparent)]</code>.</p>
|
|
<pre><pre class="playpen"><code class="language-rust">#[derive(juniper::GraphQLScalarValue)]
|
|
pub struct UserId(i32);
|
|
|
|
#[derive(juniper::GraphQLObject)]
|
|
struct User {
|
|
id: UserId,
|
|
}
|
|
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<p>That's it, you can now user <code>UserId</code> in your schema.</p>
|
|
<p>The macro also allows for more customization:</p>
|
|
<pre><pre class="playpen"><code class="language-rust">/// You can use a doc comment to specify a description.
|
|
#[derive(juniper::GraphQLScalarValue)]
|
|
#[graphql(
|
|
transparent,
|
|
// Overwrite the GraphQL type name.
|
|
name = "MyUserId",
|
|
// Specify a custom description.
|
|
// A description in the attribute will overwrite a doc comment.
|
|
description = "My user id description",
|
|
)]
|
|
pub struct UserId(i32);
|
|
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<a class="header" href="#custom-scalars" id="custom-scalars"><h2>Custom scalars</h2></a>
|
|
<p>For more complex situations where you also need custom parsing or validation,
|
|
you can use the <code>graphql_scalar</code> proc macro.</p>
|
|
<p>Typically, you represent your custom scalars as strings.</p>
|
|
<p>The example below implements a custom scalar for a custom <code>Date</code> type.</p>
|
|
<p>Note: juniper already has built-in support for the <code>chrono::DateTime</code> type
|
|
via <code>chrono</code> feature, which is enabled by default and should be used for this
|
|
purpose.</p>
|
|
<p>The example below is used just for illustration.</p>
|
|
<p><strong>Note</strong>: the example assumes that the <code>Date</code> type implements
|
|
<code>std::fmt::Display</code> and <code>std::str::FromStr</code>.</p>
|
|
<pre><pre class="playpen"><code class="language-rust"># mod date {
|
|
# pub struct Date;
|
|
# impl std::str::FromStr for Date{
|
|
# type Err = String; fn from_str(_value: &str) -> Result<Self, Self::Err> { unimplemented!() }
|
|
# }
|
|
# // And we define how to represent date as a string.
|
|
# impl std::fmt::Display for Date {
|
|
# fn fmt(&self, _f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
# unimplemented!()
|
|
# }
|
|
# }
|
|
# }
|
|
|
|
use juniper::{Value, ParseScalarResult, ParseScalarValue};
|
|
use date::Date;
|
|
|
|
#[juniper::graphql_scalar(description = "Date")]
|
|
impl<S> GraphQLScalar for Date
|
|
where
|
|
S: ScalarValue
|
|
{
|
|
// Define how to convert your custom scalar into a primitive type.
|
|
fn resolve(&self) -> Value {
|
|
Value::scalar(self.to_string())
|
|
}
|
|
|
|
// Define how to parse a primitive type into your custom scalar.
|
|
fn from_input_value(v: &InputValue) -> Option<Date> {
|
|
v.as_scalar_value()
|
|
.and_then(|v| v.as_str())
|
|
.and_then(|s| s.parse().ok())
|
|
}
|
|
|
|
// Define how to parse a string value.
|
|
fn from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
|
|
<String as ParseScalarValue<S>>::from_str(value)
|
|
}
|
|
}
|
|
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<a class="header" href="#unions" id="unions"><h1>Unions</h1></a>
|
|
<p>From the server's point of view, <a href="https://spec.graphql.org/June2018/#sec-Unions">GraphQL unions</a> are similar to interfaces - the only exception is that they don't contain fields on their own.</p>
|
|
<p>For implementing <a href="https://spec.graphql.org/June2018/#sec-Unions">GraphQL unions</a> Juniper provides:</p>
|
|
<ul>
|
|
<li><code>#[derive(GraphQLUnion)]</code> macro for enums and structs.</li>
|
|
<li><code>#[graphql_union]</code> for traits.</li>
|
|
</ul>
|
|
<a class="header" href="#enums-2" id="enums-2"><h2>Enums</h2></a>
|
|
<p>Most of the time, we just need a trivial and straightforward Rust enum to represent a <a href="https://spec.graphql.org/June2018/#sec-Unions">GraphQL union</a>.</p>
|
|
<pre><pre class="playpen"><code class="language-rust"># #![allow(dead_code)]
|
|
use derive_more::From;
|
|
use juniper::{GraphQLObject, GraphQLUnion};
|
|
|
|
#[derive(GraphQLObject)]
|
|
struct Human {
|
|
id: String,
|
|
home_planet: String,
|
|
}
|
|
|
|
#[derive(GraphQLObject)]
|
|
struct Droid {
|
|
id: String,
|
|
primary_function: String,
|
|
}
|
|
|
|
#[derive(From, GraphQLUnion)]
|
|
enum Character {
|
|
Human(Human),
|
|
Droid(Droid),
|
|
}
|
|
#
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<a class="header" href="#ignoring-enum-variants" id="ignoring-enum-variants"><h3>Ignoring enum variants</h3></a>
|
|
<p>In some rare situations we may want to omit exposing an enum variant in the GraphQL schema.</p>
|
|
<p>As an example, let's consider the situation where we need to bind some type parameter <code>T</code> for doing interesting type-level stuff in our resolvers. To achieve this we need to have <code>PhantomData<T></code>, but we don't want it exposed in the GraphQL schema.</p>
|
|
<blockquote>
|
|
<p><strong>WARNING</strong>:<br />
|
|
It's the <em>library user's responsibility</em> to ensure that ignored enum variant is <em>never</em> returned from resolvers, otherwise resolving the GraphQL query will <strong>panic at runtime</strong>.</p>
|
|
</blockquote>
|
|
<pre><pre class="playpen"><code class="language-rust"># use std::marker::PhantomData;
|
|
use derive_more::From;
|
|
use juniper::{GraphQLObject, GraphQLUnion};
|
|
|
|
#[derive(GraphQLObject)]
|
|
struct Human {
|
|
id: String,
|
|
home_planet: String,
|
|
}
|
|
|
|
#[derive(GraphQLObject)]
|
|
struct Droid {
|
|
id: String,
|
|
primary_function: String,
|
|
}
|
|
|
|
#[derive(From, GraphQLUnion)]
|
|
enum Character<S> {
|
|
Human(Human),
|
|
Droid(Droid),
|
|
#[from(ignore)]
|
|
#[graphql(ignore)] // or `#[graphql(skip)]`, your choice
|
|
_State(PhantomData<S>),
|
|
}
|
|
#
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<a class="header" href="#external-resolver-functions" id="external-resolver-functions"><h3>External resolver functions</h3></a>
|
|
<p>If some custom logic is needed to resolve a <a href="https://spec.graphql.org/June2018/#sec-Unions">GraphQL union</a> variant, you may specify an external function to do so:</p>
|
|
<pre><pre class="playpen"><code class="language-rust"># #![allow(dead_code)]
|
|
use juniper::{GraphQLObject, GraphQLUnion};
|
|
|
|
#[derive(GraphQLObject)]
|
|
#[graphql(Context = CustomContext)]
|
|
struct Human {
|
|
id: String,
|
|
home_planet: String,
|
|
}
|
|
|
|
#[derive(GraphQLObject)]
|
|
#[graphql(Context = CustomContext)]
|
|
struct Droid {
|
|
id: String,
|
|
primary_function: String,
|
|
}
|
|
|
|
pub struct CustomContext {
|
|
droid: Droid,
|
|
}
|
|
impl juniper::Context for CustomContext {}
|
|
|
|
#[derive(GraphQLUnion)]
|
|
#[graphql(Context = CustomContext)]
|
|
enum Character {
|
|
Human(Human),
|
|
#[graphql(with = Character::droid_from_context)]
|
|
Droid(Droid),
|
|
}
|
|
|
|
impl Character {
|
|
// NOTICE: The function signature must contain `&self` and `&Context`,
|
|
// and return `Option<&VariantType>`.
|
|
fn droid_from_context<'c>(&self, ctx: &'c CustomContext) -> Option<&'c Droid> {
|
|
Some(&ctx.droid)
|
|
}
|
|
}
|
|
#
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<p>With an external resolver function we can even declare a new <a href="https://spec.graphql.org/June2018/#sec-Unions">GraphQL union</a> variant where the Rust type is absent in the initial enum definition. The attribute syntax <code>#[graphql(on VariantType = resolver_fn)]</code> follows the <a href="https://spec.graphql.org/June2018/#example-f8163">GraphQL syntax for dispatching union variants</a>.</p>
|
|
<pre><pre class="playpen"><code class="language-rust"># #![allow(dead_code)]
|
|
use juniper::{GraphQLObject, GraphQLUnion};
|
|
|
|
#[derive(GraphQLObject)]
|
|
#[graphql(Context = CustomContext)]
|
|
struct Human {
|
|
id: String,
|
|
home_planet: String,
|
|
}
|
|
|
|
#[derive(GraphQLObject)]
|
|
#[graphql(Context = CustomContext)]
|
|
struct Droid {
|
|
id: String,
|
|
primary_function: String,
|
|
}
|
|
|
|
#[derive(GraphQLObject)]
|
|
#[graphql(Context = CustomContext)]
|
|
struct Ewok {
|
|
id: String,
|
|
is_funny: bool,
|
|
}
|
|
|
|
pub struct CustomContext {
|
|
ewok: Ewok,
|
|
}
|
|
impl juniper::Context for CustomContext {}
|
|
|
|
#[derive(GraphQLUnion)]
|
|
#[graphql(Context = CustomContext)]
|
|
#[graphql(on Ewok = Character::ewok_from_context)]
|
|
enum Character {
|
|
Human(Human),
|
|
Droid(Droid),
|
|
#[graphql(ignore)] // or `#[graphql(skip)]`, your choice
|
|
Ewok,
|
|
}
|
|
|
|
impl Character {
|
|
fn ewok_from_context<'c>(&self, ctx: &'c CustomContext) -> Option<&'c Ewok> {
|
|
if let Self::Ewok = self {
|
|
Some(&ctx.ewok)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
#
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<a class="header" href="#structs" id="structs"><h2>Structs</h2></a>
|
|
<p>Using Rust structs as <a href="https://spec.graphql.org/June2018/#sec-Unions">GraphQL unions</a> is very similar to using enums, with the nuance that specifying an external resolver function is the only way to declare a <a href="https://spec.graphql.org/June2018/#sec-Unions">GraphQL union</a> variant.</p>
|
|
<pre><pre class="playpen"><code class="language-rust"># use std::collections::HashMap;
|
|
use juniper::{GraphQLObject, GraphQLUnion};
|
|
|
|
#[derive(GraphQLObject)]
|
|
#[graphql(Context = Database)]
|
|
struct Human {
|
|
id: String,
|
|
home_planet: String,
|
|
}
|
|
|
|
#[derive(GraphQLObject)]
|
|
#[graphql(Context = Database)]
|
|
struct Droid {
|
|
id: String,
|
|
primary_function: String,
|
|
}
|
|
|
|
struct Database {
|
|
humans: HashMap<String, Human>,
|
|
droids: HashMap<String, Droid>,
|
|
}
|
|
impl juniper::Context for Database {}
|
|
|
|
#[derive(GraphQLUnion)]
|
|
#[graphql(
|
|
Context = Database,
|
|
on Human = Character::get_human,
|
|
on Droid = Character::get_droid,
|
|
)]
|
|
struct Character {
|
|
id: String,
|
|
}
|
|
|
|
impl Character {
|
|
fn get_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human>{
|
|
ctx.humans.get(&self.id)
|
|
}
|
|
|
|
fn get_droid<'db>(&self, ctx: &'db Database) -> Option<&'db Droid>{
|
|
ctx.droids.get(&self.id)
|
|
}
|
|
}
|
|
#
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<a class="header" href="#traits-1" id="traits-1"><h2>Traits</h2></a>
|
|
<p>To use a Rust trait definition as a <a href="https://spec.graphql.org/June2018/#sec-Unions">GraphQL union</a> you need to use the <code>#[graphql_union]</code> macro. <a href="https://doc.rust-lang.org/stable/reference/procedural-macros.html#derive-macros">Rust doesn't allow derive macros on traits</a>, so using <code>#[derive(GraphQLUnion)]</code> on traits doesn't work.</p>
|
|
<blockquote>
|
|
<p><strong>NOTICE</strong>:<br />
|
|
A <strong>trait has to be <a href="https://doc.rust-lang.org/stable/reference/items/traits.html#object-safety">object safe</a></strong>, because schema resolvers will need to return a <a href="https://doc.rust-lang.org/stable/reference/types/trait-object.html">trait object</a> to specify a <a href="https://spec.graphql.org/June2018/#sec-Unions">GraphQL union</a> behind it.</p>
|
|
</blockquote>
|
|
<pre><pre class="playpen"><code class="language-rust">use juniper::{graphql_union, GraphQLObject};
|
|
|
|
#[derive(GraphQLObject)]
|
|
struct Human {
|
|
id: String,
|
|
home_planet: String,
|
|
}
|
|
|
|
#[derive(GraphQLObject)]
|
|
struct Droid {
|
|
id: String,
|
|
primary_function: String,
|
|
}
|
|
|
|
#[graphql_union]
|
|
trait Character {
|
|
// NOTICE: The method signature must contain `&self` and return `Option<&VariantType>`.
|
|
fn as_human(&self) -> Option<&Human> { None }
|
|
fn as_droid(&self) -> Option<&Droid> { None }
|
|
}
|
|
|
|
impl Character for Human {
|
|
fn as_human(&self) -> Option<&Human> { Some(&self) }
|
|
}
|
|
|
|
impl Character for Droid {
|
|
fn as_droid(&self) -> Option<&Droid> { Some(&self) }
|
|
}
|
|
#
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<a class="header" href="#custom-context" id="custom-context"><h3>Custom context</h3></a>
|
|
<p>If a context is required in a trait method to resolve a <a href="https://spec.graphql.org/June2018/#sec-Unions">GraphQL union</a> variant, specify it as an argument.</p>
|
|
<pre><pre class="playpen"><code class="language-rust"># #![allow(unused_variables)]
|
|
# use std::collections::HashMap;
|
|
use juniper::{graphql_union, GraphQLObject};
|
|
|
|
#[derive(GraphQLObject)]
|
|
#[graphql(Context = Database)]
|
|
struct Human {
|
|
id: String,
|
|
home_planet: String,
|
|
}
|
|
|
|
#[derive(GraphQLObject)]
|
|
#[graphql(Context = Database)]
|
|
struct Droid {
|
|
id: String,
|
|
primary_function: String,
|
|
}
|
|
|
|
struct Database {
|
|
humans: HashMap<String, Human>,
|
|
droids: HashMap<String, Droid>,
|
|
}
|
|
impl juniper::Context for Database {}
|
|
|
|
#[graphql_union(Context = Database)]
|
|
trait Character {
|
|
// NOTICE: The method signature may optionally contain `&Context`.
|
|
fn as_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human> { None }
|
|
fn as_droid<'db>(&self, ctx: &'db Database) -> Option<&'db Droid> { None }
|
|
}
|
|
|
|
impl Character for Human {
|
|
fn as_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human> {
|
|
ctx.humans.get(&self.id)
|
|
}
|
|
}
|
|
|
|
impl Character for Droid {
|
|
fn as_droid<'db>(&self, ctx: &'db Database) -> Option<&'db Droid> {
|
|
ctx.droids.get(&self.id)
|
|
}
|
|
}
|
|
#
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<a class="header" href="#ignoring-trait-methods" id="ignoring-trait-methods"><h3>Ignoring trait methods</h3></a>
|
|
<p>As with enums, we may want to omit some trait methods to be assumed as <a href="https://spec.graphql.org/June2018/#sec-Unions">GraphQL union</a> variants and ignore them.</p>
|
|
<pre><pre class="playpen"><code class="language-rust">use juniper::{graphql_union, GraphQLObject};
|
|
|
|
#[derive(GraphQLObject)]
|
|
struct Human {
|
|
id: String,
|
|
home_planet: String,
|
|
}
|
|
|
|
#[derive(GraphQLObject)]
|
|
struct Droid {
|
|
id: String,
|
|
primary_function: String,
|
|
}
|
|
|
|
#[graphql_union]
|
|
trait Character {
|
|
fn as_human(&self) -> Option<&Human> { None }
|
|
fn as_droid(&self) -> Option<&Droid> { None }
|
|
#[graphql_union(ignore)] // or `#[graphql_union(skip)]`, your choice
|
|
fn id(&self) -> &str;
|
|
}
|
|
|
|
impl Character for Human {
|
|
fn as_human(&self) -> Option<&Human> { Some(&self) }
|
|
fn id(&self) -> &str { self.id.as_str() }
|
|
}
|
|
|
|
impl Character for Droid {
|
|
fn as_droid(&self) -> Option<&Droid> { Some(&self) }
|
|
fn id(&self) -> &str { self.id.as_str() }
|
|
}
|
|
#
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<a class="header" href="#external-resolver-functions-1" id="external-resolver-functions-1"><h3>External resolver functions</h3></a>
|
|
<p>Similarly to enums and structs, it's not mandatory to use trait methods as <a href="https://spec.graphql.org/June2018/#sec-Unions">GraphQL union</a> variant resolvers. Instead, custom functions may be specified:</p>
|
|
<pre><pre class="playpen"><code class="language-rust"># use std::collections::HashMap;
|
|
use juniper::{graphql_union, GraphQLObject};
|
|
|
|
#[derive(GraphQLObject)]
|
|
#[graphql(Context = Database)]
|
|
struct Human {
|
|
id: String,
|
|
home_planet: String,
|
|
}
|
|
|
|
#[derive(GraphQLObject)]
|
|
#[graphql(Context = Database)]
|
|
struct Droid {
|
|
id: String,
|
|
primary_function: String,
|
|
}
|
|
|
|
struct Database {
|
|
humans: HashMap<String, Human>,
|
|
droids: HashMap<String, Droid>,
|
|
}
|
|
impl juniper::Context for Database {}
|
|
|
|
#[graphql_union(Context = Database)]
|
|
#[graphql_union(
|
|
on Human = DynCharacter::get_human,
|
|
on Droid = get_droid,
|
|
)]
|
|
trait Character {
|
|
#[graphql_union(ignore)] // or `#[graphql_union(skip)]`, your choice
|
|
fn id(&self) -> &str;
|
|
}
|
|
|
|
impl Character for Human {
|
|
fn id(&self) -> &str { self.id.as_str() }
|
|
}
|
|
|
|
impl Character for Droid {
|
|
fn id(&self) -> &str { self.id.as_str() }
|
|
}
|
|
|
|
// The trait object is always `Send` and `Sync`.
|
|
type DynCharacter<'a> = dyn Character + Send + Sync + 'a;
|
|
|
|
impl<'a> DynCharacter<'a> {
|
|
fn get_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human> {
|
|
ctx.humans.get(self.id())
|
|
}
|
|
}
|
|
|
|
// External resolver function doesn't have to be a method of a type.
|
|
// It's only a matter of the function signature to match the requirements.
|
|
fn get_droid<'db>(ch: &DynCharacter<'_>, ctx: &'db Database) -> Option<&'db Droid> {
|
|
ctx.droids.get(ch.id())
|
|
}
|
|
#
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<a class="header" href="#scalarvalue-considerations" id="scalarvalue-considerations"><h2><code>ScalarValue</code> considerations</h2></a>
|
|
<p>By default, <code>#[derive(GraphQLUnion)]</code> and <code>#[graphql_union]</code> macros generate code, which is generic over a <a href="https://docs.rs/juniper/latest/juniper/trait.ScalarValue.html"><code>ScalarValue</code></a> type. This may introduce a problem when at least one of <a href="https://spec.graphql.org/June2018/#sec-Unions">GraphQL union</a> variants is restricted to a concrete <a href="https://docs.rs/juniper/latest/juniper/trait.ScalarValue.html"><code>ScalarValue</code></a> type in its implementation. To resolve such problem, a concrete <a href="https://docs.rs/juniper/latest/juniper/trait.ScalarValue.html"><code>ScalarValue</code></a> type should be specified:</p>
|
|
<pre><pre class="playpen"><code class="language-rust"># #![allow(dead_code)]
|
|
use juniper::{DefaultScalarValue, GraphQLObject, GraphQLUnion};
|
|
|
|
#[derive(GraphQLObject)]
|
|
#[graphql(Scalar = DefaultScalarValue)]
|
|
struct Human {
|
|
id: String,
|
|
home_planet: String,
|
|
}
|
|
|
|
#[derive(GraphQLObject)]
|
|
struct Droid {
|
|
id: String,
|
|
primary_function: String,
|
|
}
|
|
|
|
#[derive(GraphQLUnion)]
|
|
#[graphql(Scalar = DefaultScalarValue)] // removing this line will fail compilation
|
|
enum Character {
|
|
Human(Human),
|
|
Droid(Droid),
|
|
}
|
|
#
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<a class="header" href="#schemas" id="schemas"><h1>Schemas</h1></a>
|
|
<p>Juniper follows a <a href="https://blog.logrocket.com/code-first-vs-schema-first-development-graphql/">code-first approach</a> to defining GraphQL schemas. If you would like to use a <a href="https://blog.logrocket.com/code-first-vs-schema-first-development-graphql/">schema-first approach</a> instead, consider <a href="https://github.com/davidpdrsn/juniper-from-schema">juniper-from-schema</a> for generating code from a schema file.</p>
|
|
<p>A schema consists of three types: a query object, a mutation object, and a subscription object.
|
|
These three define the root query fields, mutations and subscriptions of the schema, respectively.</p>
|
|
<p>The usage of subscriptions is a little different from the mutation and query objects, so there is a specific <a href="../advanced/subscriptions.html">section</a> that discusses them.</p>
|
|
<p>Both query and mutation objects are regular GraphQL objects, defined like any
|
|
other object in Juniper. The mutation and subscription object, however, is optional since schemas
|
|
can be read-only and without subscriptions as well. If mutations/subscriptions functionality is not needed, consider using <a href="https://docs.rs/juniper/0.14.2/juniper/struct.EmptyMutation.html">EmptyMutation</a>/[EmptySubscription][EmptySubscription].</p>
|
|
<p>In Juniper, the <code>RootNode</code> type represents a schema. You usually don't have to
|
|
create this object yourself: see the framework integrations for <a href="../servers/iron.html">Iron</a>
|
|
and <a href="../servers/rocket.html">Rocket</a> how schemas are created together with the handlers
|
|
themselves.</p>
|
|
<p>When the schema is first created, Juniper will traverse the entire object graph
|
|
and register all types it can find. This means that if you define a GraphQL
|
|
object somewhere but never references it, it will not be exposed in a schema.</p>
|
|
<a class="header" href="#the-query-root" id="the-query-root"><h2>The query root</h2></a>
|
|
<p>The query root is just a GraphQL object. You define it like any other GraphQL
|
|
object in Juniper, most commonly using the <code>object</code> proc macro:</p>
|
|
<pre><pre class="playpen"><code class="language-rust"># use juniper::FieldResult;
|
|
# #[derive(juniper::GraphQLObject)] struct User { name: String }
|
|
struct Root;
|
|
|
|
#[juniper::graphql_object]
|
|
impl Root {
|
|
fn userWithUsername(username: String) -> FieldResult<Option<User>> {
|
|
// Look up user in database...
|
|
# unimplemented!()
|
|
}
|
|
}
|
|
|
|
# fn main() { }
|
|
</code></pre></pre>
|
|
<a class="header" href="#mutations" id="mutations"><h2>Mutations</h2></a>
|
|
<p>Mutations are <em>also</em> just GraphQL objects. Each mutation is a single field that
|
|
usually performs some mutating side-effect, such as updating a database.</p>
|
|
<pre><pre class="playpen"><code class="language-rust"># use juniper::FieldResult;
|
|
# #[derive(juniper::GraphQLObject)] struct User { name: String }
|
|
struct Mutations;
|
|
|
|
#[juniper::graphql_object]
|
|
impl Mutations {
|
|
fn signUpUser(name: String, email: String) -> FieldResult<User> {
|
|
// Validate inputs and save user in database...
|
|
# unimplemented!()
|
|
}
|
|
}
|
|
|
|
# fn main() { }
|
|
</code></pre></pre>
|
|
<a class="header" href="#outputting-schemas-in-the-a-hrefhttpsgraphqlorglearnschematype-languagegraphql-schema-languagea" id="outputting-schemas-in-the-a-hrefhttpsgraphqlorglearnschematype-languagegraphql-schema-languagea"><h1>Outputting schemas in the <a href="https://graphql.org/learn/schema/#type-language">GraphQL Schema Language</a></h1></a>
|
|
<p>Many tools in the GraphQL ecosystem require the schema to be defined in the <a href="https://graphql.org/learn/schema/#type-language">GraphQL Schema Language</a>. You can generate a <a href="https://graphql.org/learn/schema/#type-language">GraphQL Schema Language</a> representation of your schema defined in Rust using the <code>schema-language</code> feature (on by default):</p>
|
|
<pre><pre class="playpen"><code class="language-rust"># // Only needed due to 2018 edition because the macro is not accessible.
|
|
# #[macro_use] extern crate juniper;
|
|
use juniper::{FieldResult, EmptyMutation, EmptySubscription, RootNode};
|
|
|
|
struct Query;
|
|
|
|
#[juniper::graphql_object]
|
|
impl Query {
|
|
fn hello(&self) -> FieldResult<&str> {
|
|
Ok("hello world")
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
// Define our schema in Rust.
|
|
let schema = RootNode::new(
|
|
Query,
|
|
EmptyMutation::<()>::new(),
|
|
EmptySubscription::<()>::new(),
|
|
);
|
|
|
|
// Convert the Rust schema into the GraphQL Schema Language.
|
|
let result = schema.as_schema_language();
|
|
|
|
let expected = "\
|
|
type Query {
|
|
hello: String!
|
|
}
|
|
|
|
schema {
|
|
query: Query
|
|
}
|
|
";
|
|
assert_eq!(result, expected);
|
|
}
|
|
</code></pre></pre>
|
|
<p>Note the <code>schema-language</code> feature may be turned off if you do not need this functionality to reduce dependencies and speed up
|
|
compile times.</p>
|
|
<!--TODO: Fix This URL when the EmptySubscription become available in the Documentation -->
|
|
[EmptySubscription]: https://docs.rs/juniper/0.14.2/juniper/struct.EmptySubscription.html
|
|
<a class="header" href="#adding-a-server" id="adding-a-server"><h1>Adding A Server</h1></a>
|
|
<p>To allow using Juniper with the HTTP server of your choice,
|
|
it does <strong>not</strong> come with a built in HTTP server.</p>
|
|
<p>To actually get a server up and running, there are multiple official and
|
|
third-party integration crates that will get you there.</p>
|
|
<ul>
|
|
<li><a href="official.html">Official Server Integrations</a>
|
|
<ul>
|
|
<li><a href="warp.html">Warp</a></li>
|
|
<li><a href="rocket.html">Rocket</a></li>
|
|
<li><a href="iron.html">Iron</a></li>
|
|
<li><a href="hyper.html">Hyper</a></li>
|
|
</ul>
|
|
</li>
|
|
<li><a href="third-party.html">Third Party Integrations</a>
|
|
<ul>
|
|
<li><a href="https://github.com/actix/examples/tree/master/juniper">Actix-Web</a></li>
|
|
<li><a href="https://github.com/finchers-rs/finchers-juniper">Finchers</a></li>
|
|
<li><a href="https://github.com/tsukuyomi-rs/tsukuyomi/tree/master/examples/juniper">Tsukuyomi</a></li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
<a class="header" href="#official-server-integrations" id="official-server-integrations"><h1>Official Server Integrations</h1></a>
|
|
<p>Juniper provides official integration crates for several popular Rust server
|
|
libraries.</p>
|
|
<ul>
|
|
<li><a href="warp.html">Warp</a></li>
|
|
<li><a href="rocket.html">Rocket</a></li>
|
|
<li><a href="iron.html">Iron</a></li>
|
|
<li><a href="hyper.html">Hyper</a></li>
|
|
</ul>
|
|
<a class="header" href="#integrating-with-warp" id="integrating-with-warp"><h1>Integrating with Warp</h1></a>
|
|
<p><a href="https://crates.io/crates/warp">Warp</a> is a super-easy, composable, web server framework for warp speeds.
|
|
The fundamental building block of warp is the Filter: they can be combined and composed to express rich requirements on requests. Warp is built on <a href="https://hyper.rs/">Hyper</a> and works on
|
|
Rust's stable channel.</p>
|
|
<p>Juniper's Warp integration is contained in the <a href="https://github.com/graphql-rust/juniper/tree/master/juniper_warp"><code>juniper_warp</code></a> crate:</p>
|
|
<p>!FILENAME Cargo.toml</p>
|
|
<pre><code class="language-toml">[dependencies]
|
|
juniper = "0.10"
|
|
juniper_warp = "0.1.0"
|
|
</code></pre>
|
|
<p>Included in the source is a <a href="https://github.com/graphql-rust/juniper/blob/master/juniper_warp/examples/warp_server.rs">small example</a> which sets up a basic GraphQL and <a href="https://github.com/graphql/graphiql">GraphiQL</a> handler.</p>
|
|
<a class="header" href="#integrating-with-rocket" id="integrating-with-rocket"><h1>Integrating with Rocket</h1></a>
|
|
<p><a href="https://rocket.rs/">Rocket</a> is a web framework for Rust that makes it simple to write fast web applications without sacrificing flexibility or type safety. All with minimal code. Rocket
|
|
does not work on Rust's stable channel and instead requires the nightly
|
|
channel.</p>
|
|
<p>Juniper's Rocket integration is contained in the <a href="https://github.com/graphql-rust/juniper/tree/master/juniper_rocket"><code>juniper_rocket</code></a> crate:</p>
|
|
<p>!FILENAME Cargo.toml</p>
|
|
<pre><code class="language-toml">[dependencies]
|
|
juniper = "0.10"
|
|
juniper_rocket = "0.2.0"
|
|
</code></pre>
|
|
<p>Included in the source is a <a href="https://github.com/graphql-rust/juniper/blob/master/juniper_rocket/examples/rocket_server.rs">small example</a> which sets up a basic GraphQL and <a href="https://github.com/graphql/graphiql">GraphiQL</a> handler.</p>
|
|
<a class="header" href="#integrating-with-iron" id="integrating-with-iron"><h1>Integrating with Iron</h1></a>
|
|
<p><a href="http://ironframework.io">Iron</a> is a library that's been around for a while in the Rust sphere but lately
|
|
hasn't seen much of development. Nevertheless, it's still a solid library with a
|
|
familiar request/response/middleware architecture that works on Rust's stable
|
|
channel.</p>
|
|
<p>Juniper's Iron integration is contained in the <code>juniper_iron</code> crate:</p>
|
|
<p>!FILENAME Cargo.toml</p>
|
|
<pre><code class="language-toml">[dependencies]
|
|
juniper = "0.10"
|
|
juniper_iron = "0.2.0"
|
|
</code></pre>
|
|
<p>Included in the source is a <a href="https://github.com/graphql-rust/juniper_iron/blob/master/examples/iron_server.rs">small
|
|
example</a>
|
|
which sets up a basic GraphQL and <a href="https://github.com/graphql/graphiql">GraphiQL</a> handler.</p>
|
|
<a class="header" href="#basic-integration" id="basic-integration"><h2>Basic integration</h2></a>
|
|
<p>Let's start with a minimal schema and just get a GraphQL endpoint up and
|
|
running. We use <a href="https://github.com/iron/mount">mount</a> to attach the GraphQL handler at <code>/graphql</code>.</p>
|
|
<p>The <code>context_factory</code> function will be executed on every request and can be used
|
|
to set up database connections, read session token information from cookies, and
|
|
set up other global data that the schema might require.</p>
|
|
<p>In this example, we won't use any global data so we just return an empty value.</p>
|
|
<pre><code class="language-rust ignore">extern crate juniper;
|
|
extern crate juniper_iron;
|
|
extern crate iron;
|
|
extern crate mount;
|
|
|
|
use mount::Mount;
|
|
use iron::prelude::*;
|
|
use juniper::EmptyMutation;
|
|
use juniper_iron::GraphQLHandler;
|
|
|
|
fn context_factory(_: &mut Request) -> IronResult<()> {
|
|
Ok(())
|
|
}
|
|
|
|
struct Root;
|
|
|
|
#[juniper::graphql_object]
|
|
impl Root {
|
|
fn foo() -> String {
|
|
"Bar".to_owned()
|
|
}
|
|
}
|
|
|
|
# #[allow(unreachable_code, unused_variables)]
|
|
fn main() {
|
|
let mut mount = Mount::new();
|
|
|
|
let graphql_endpoint = GraphQLHandler::new(
|
|
context_factory,
|
|
Root,
|
|
EmptyMutation::<()>::new(),
|
|
);
|
|
|
|
mount.mount("/graphql", graphql_endpoint);
|
|
|
|
let chain = Chain::new(mount);
|
|
|
|
# return;
|
|
Iron::new(chain).http("0.0.0.0:8080").unwrap();
|
|
}
|
|
</code></pre>
|
|
<a class="header" href="#accessing-data-from-the-request" id="accessing-data-from-the-request"><h2>Accessing data from the request</h2></a>
|
|
<p>If you want to access e.g. the source IP address of the request from a field
|
|
resolver, you need to pass this data using Juniper's <a href="../types/objects/using_contexts.html">context feature</a>.</p>
|
|
<pre><code class="language-rust ignore"># extern crate juniper;
|
|
# extern crate juniper_iron;
|
|
# extern crate iron;
|
|
# use iron::prelude::*;
|
|
use std::net::SocketAddr;
|
|
|
|
struct Context {
|
|
remote_addr: SocketAddr,
|
|
}
|
|
|
|
impl juniper::Context for Context {}
|
|
|
|
fn context_factory(req: &mut Request) -> IronResult<Context> {
|
|
Ok(Context {
|
|
remote_addr: req.remote_addr
|
|
})
|
|
}
|
|
|
|
struct Root;
|
|
|
|
#[juniper::graphql_object(
|
|
Context = Context,
|
|
)]
|
|
impl Root {
|
|
field my_addr(context: &Context) -> String {
|
|
format!("Hello, you're coming from {}", context.remote_addr)
|
|
}
|
|
}
|
|
|
|
# fn main() {
|
|
# let _graphql_endpoint = juniper_iron::GraphQLHandler::new(
|
|
# context_factory,
|
|
# Root,
|
|
# juniper::EmptyMutation::<Context>::new(),
|
|
# );
|
|
# }
|
|
</code></pre>
|
|
<a class="header" href="#integrating-with-hyper" id="integrating-with-hyper"><h1>Integrating with Hyper</h1></a>
|
|
<p><a href="https://hyper.rs/">Hyper</a> is a fast HTTP implementation that many other Rust web frameworks
|
|
leverage. It offers asynchronous I/O via the tokio runtime and works on
|
|
Rust's stable channel.</p>
|
|
<p>Hyper is not a higher-level web framework and accordingly
|
|
does not include ergonomic features such as simple endpoint routing,
|
|
baked-in HTTP responses, or reusable middleware. For GraphQL, those aren't
|
|
large downsides as all POSTs and GETs usually go through a single endpoint with
|
|
a few clearly-defined response payloads.</p>
|
|
<p>Juniper's Hyper integration is contained in the <a href="https://github.com/graphql-rust/juniper/tree/master/juniper_hyper"><code>juniper_hyper</code></a> crate:</p>
|
|
<p>!FILENAME Cargo.toml</p>
|
|
<pre><code class="language-toml">[dependencies]
|
|
juniper = "0.10"
|
|
juniper_hyper = "0.1.0"
|
|
</code></pre>
|
|
<p>Included in the source is a <a href="https://github.com/graphql-rust/juniper/blob/master/juniper_hyper/examples/hyper_server.rs">small example</a> which sets up a basic GraphQL and <a href="https://github.com/graphql/graphiql">GraphiQL</a> handler.</p>
|
|
<a class="header" href="#third-party-integrations" id="third-party-integrations"><h1>Third-Party Integrations</h1></a>
|
|
<p>There are several examples or third party integration crates that are not
|
|
officially maintained by Juniper developers.</p>
|
|
<ul>
|
|
<li><a href="https://github.com/actix/examples/tree/master/juniper">Actix-Web</a></li>
|
|
</ul>
|
|
<a class="header" href="#advanced-topics" id="advanced-topics"><h1>Advanced Topics</h1></a>
|
|
<p>The chapters below cover some more advanced scenarios.</p>
|
|
<ul>
|
|
<li><a href="introspection.html">Introspection</a></li>
|
|
<li><a href="non_struct_objects.html">Non-struct objects</a></li>
|
|
<li><a href="objects_and_generics.html">Objects and generics</a></li>
|
|
<li><a href="multiple_ops_per_request.html">Multiple operations per request</a></li>
|
|
<li><a href="dataloaders.html">Dataloaders</a></li>
|
|
<li><a href="subscriptions.html">Subscriptions</a></li>
|
|
</ul>
|
|
<a class="header" href="#introspection" id="introspection"><h1>Introspection</h1></a>
|
|
<p>GraphQL defines a special built-in top-level field called <code>__schema</code>. Querying
|
|
for this field allows one to <a href="https://graphql.org/learn/introspection/">introspect the schema</a>
|
|
at runtime to see what queries and mutations the GraphQL server supports.</p>
|
|
<p>Because introspection queries are just regular GraphQL queries, Juniper supports
|
|
them natively. For example, to get all the names of the types supported one
|
|
could execute the following query against Juniper:</p>
|
|
<pre><code class="language-graphql">{
|
|
__schema {
|
|
types {
|
|
name
|
|
}
|
|
}
|
|
}
|
|
</code></pre>
|
|
<a class="header" href="#schema-introspection-output-as-json" id="schema-introspection-output-as-json"><h2>Schema introspection output as JSON</h2></a>
|
|
<p>Many client libraries and tools in the GraphQL ecosystem require a complete
|
|
representation of the server schema. Often this representation is in JSON and
|
|
referred to as <code>schema.json</code>. A complete representation of the schema can be
|
|
produced by issuing a specially crafted introspection query.</p>
|
|
<p>Juniper provides a convenience function to introspect the entire schema. The
|
|
result can then be converted to JSON for use with tools and libraries such as
|
|
<a href="https://github.com/graphql-rust/graphql-client">graphql-client</a>:</p>
|
|
<pre><pre class="playpen"><code class="language-rust">use juniper::{EmptyMutation, EmptySubscription, FieldResult, IntrospectionFormat};
|
|
|
|
// Define our schema.
|
|
|
|
#[derive(juniper::GraphQLObject)]
|
|
struct Example {
|
|
id: String,
|
|
}
|
|
|
|
struct Context;
|
|
impl juniper::Context for Context {}
|
|
|
|
struct Query;
|
|
|
|
#[juniper::graphql_object(
|
|
Context = Context,
|
|
)]
|
|
impl Query {
|
|
fn example(id: String) -> FieldResult<Example> {
|
|
unimplemented!()
|
|
}
|
|
}
|
|
|
|
type Schema = juniper::RootNode<
|
|
'static,
|
|
Query,
|
|
EmptyMutation<Context>,
|
|
EmptySubscription<Context>
|
|
>;
|
|
|
|
fn main() {
|
|
// Create a context object.
|
|
let ctx = Context{};
|
|
|
|
// Run the built-in introspection query.
|
|
let (res, _errors) = juniper::introspect(
|
|
&Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()),
|
|
&ctx,
|
|
IntrospectionFormat::default(),
|
|
).unwrap();
|
|
|
|
// Convert introspection result to json.
|
|
let json_result = serde_json::to_string_pretty(&res);
|
|
assert!(json_result.is_ok());
|
|
}
|
|
</code></pre></pre>
|
|
<a class="header" href="#non-struct-objects" id="non-struct-objects"><h1>Non-struct objects</h1></a>
|
|
<p>Up until now, we've only looked at mapping structs to GraphQL objects. However,
|
|
any Rust type can be mapped into a GraphQL object. In this chapter, we'll look
|
|
at enums, but traits will work too - they don't <em>have</em> to be mapped into GraphQL
|
|
interfaces.</p>
|
|
<p>Using <code>Result</code>-like enums can be a useful way of reporting e.g. validation
|
|
errors from a mutation:</p>
|
|
<pre><pre class="playpen"><code class="language-rust"># #[derive(juniper::GraphQLObject)] struct User { name: String }
|
|
|
|
#[derive(juniper::GraphQLObject)]
|
|
struct ValidationError {
|
|
field: String,
|
|
message: String,
|
|
}
|
|
|
|
# #[allow(dead_code)]
|
|
enum SignUpResult {
|
|
Ok(User),
|
|
Error(Vec<ValidationError>),
|
|
}
|
|
|
|
#[juniper::graphql_object]
|
|
impl SignUpResult {
|
|
fn user(&self) -> Option<&User> {
|
|
match *self {
|
|
SignUpResult::Ok(ref user) => Some(user),
|
|
SignUpResult::Error(_) => None,
|
|
}
|
|
}
|
|
|
|
fn error(&self) -> Option<&Vec<ValidationError>> {
|
|
match *self {
|
|
SignUpResult::Ok(_) => None,
|
|
SignUpResult::Error(ref errors) => Some(errors)
|
|
}
|
|
}
|
|
}
|
|
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<p>Here, we use an enum to decide whether a user's input data was valid or not, and
|
|
it could be used as the result of e.g. a sign up mutation.</p>
|
|
<p>While this is an example of how you could use something other than a struct to
|
|
represent a GraphQL object, it's also an example on how you could implement
|
|
error handling for "expected" errors - errors like validation errors. There are
|
|
no hard rules on how to represent errors in GraphQL, but there are
|
|
<a href="https://github.com/facebook/graphql/issues/117#issuecomment-170180628">some</a>
|
|
<a href="https://github.com/graphql/graphql-js/issues/560#issuecomment-259508214">comments</a>
|
|
from one of the authors of GraphQL on how they intended "hard" field errors to
|
|
be used, and how to model expected errors.</p>
|
|
<a class="header" href="#objects-and-generics" id="objects-and-generics"><h1>Objects and generics</h1></a>
|
|
<p>Yet another point where GraphQL and Rust differs is in how generics work. In
|
|
Rust, almost any type could be generic - that is, take type parameters. In
|
|
GraphQL, there are only two generic types: lists and non-nullables.</p>
|
|
<p>This poses a restriction on what you can expose in GraphQL from Rust: no generic
|
|
structs can be exposed - all type parameters must be bound. For example, you can
|
|
not make e.g. <code>Result<T, E></code> into a GraphQL type, but you <em>can</em> make e.g.
|
|
<code>Result<User, String></code> into a GraphQL type.</p>
|
|
<p>Let's make a slightly more compact but generic implementation of <a href="non_struct_objects.html">the last
|
|
chapter</a>:</p>
|
|
<pre><pre class="playpen"><code class="language-rust"># #[derive(juniper::GraphQLObject)] struct User { name: String }
|
|
# #[derive(juniper::GraphQLObject)] struct ForumPost { title: String }
|
|
|
|
#[derive(juniper::GraphQLObject)]
|
|
struct ValidationError {
|
|
field: String,
|
|
message: String,
|
|
}
|
|
|
|
# #[allow(dead_code)]
|
|
struct MutationResult<T>(Result<T, Vec<ValidationError>>);
|
|
|
|
#[juniper::graphql_object(
|
|
name = "UserResult",
|
|
)]
|
|
impl MutationResult<User> {
|
|
fn user(&self) -> Option<&User> {
|
|
self.0.as_ref().ok()
|
|
}
|
|
|
|
fn error(&self) -> Option<&Vec<ValidationError>> {
|
|
self.0.as_ref().err()
|
|
}
|
|
}
|
|
|
|
#[juniper::graphql_object(
|
|
name = "ForumPostResult",
|
|
)]
|
|
impl MutationResult<ForumPost> {
|
|
fn forum_post(&self) -> Option<&ForumPost> {
|
|
self.0.as_ref().ok()
|
|
}
|
|
|
|
fn error(&self) -> Option<&Vec<ValidationError>> {
|
|
self.0.as_ref().err()
|
|
}
|
|
}
|
|
|
|
# fn main() {}
|
|
</code></pre></pre>
|
|
<p>Here, we've made a wrapper around <code>Result</code> and exposed some concrete
|
|
instantiations of <code>Result<T, E></code> as distinct GraphQL objects. The reason we
|
|
needed the wrapper is of Rust's rules for when you can derive a trait - in this
|
|
case, both <code>Result</code> and Juniper's internal GraphQL trait are from third-party
|
|
sources.</p>
|
|
<p>Because we're using generics, we also need to specify a name for our
|
|
instantiated types. Even if Juniper <em>could</em> figure out the name,
|
|
<code>MutationResult<User></code> wouldn't be a valid GraphQL type name.</p>
|
|
<a class="header" href="#multiple-operations-per-request" id="multiple-operations-per-request"><h1>Multiple operations per request</h1></a>
|
|
<p>The GraphQL standard generally assumes there will be one server request for each client operation you want to perform (such as a query or mutation). This is conceptually simple but has the potential to be inefficent.</p>
|
|
<p>Some client libraries such as <a href="https://www.apollographql.com/docs/link/links/batch-http.html">apollo-link-batch-http</a> have added the ability to batch operations in a single HTTP request to save network round-trips and potentially increase performance. There are some <a href="https://blog.apollographql.com/batching-client-graphql-queries-a685f5bcd41b">tradeoffs</a> that should be considered before batching requests.</p>
|
|
<p>Juniper's server integration crates support multiple operations in a single HTTP request using JSON arrays. This makes them compatible with client libraries that support batch operations without any special configuration.</p>
|
|
<p>Server integration crates maintained by others are <strong>not required</strong> to support batch requests. Batch requests aren't part of the official GraphQL specification.</p>
|
|
<p>Assuming an integration supports batch requests, for the following GraphQL query:</p>
|
|
<pre><code class="language-graphql">{
|
|
hero {
|
|
name
|
|
}
|
|
}
|
|
</code></pre>
|
|
<p>The json data to POST to the server for an individual request would be:</p>
|
|
<pre><code class="language-json">{
|
|
"query": "{hero{name}}"
|
|
}
|
|
</code></pre>
|
|
<p>And the response would be of the form:</p>
|
|
<pre><code class="language-json">{
|
|
"data": {
|
|
"hero": {
|
|
"name": "R2-D2"
|
|
}
|
|
}
|
|
}
|
|
</code></pre>
|
|
<p>If you wanted to run the same query twice in a single HTTP request, the batched json data to POST to the server would be:</p>
|
|
<pre><code class="language-json">[
|
|
{
|
|
"query": "{hero{name}}"
|
|
},
|
|
{
|
|
"query": "{hero{name}}"
|
|
}
|
|
]
|
|
</code></pre>
|
|
<p>And the response would be of the form:</p>
|
|
<pre><code class="language-json">[
|
|
{
|
|
"data": {
|
|
"hero": {
|
|
"name": "R2-D2"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"data": {
|
|
"hero": {
|
|
"name": "R2-D2"
|
|
}
|
|
}
|
|
}
|
|
]
|
|
</code></pre>
|
|
<a class="header" href="#avoiding-the-n1-problem-with-dataloaders" id="avoiding-the-n1-problem-with-dataloaders"><h1>Avoiding the N+1 Problem With Dataloaders</h1></a>
|
|
<p>A common issue with graphql servers is how the resolvers query their datasource.
|
|
This issue results in a large number of unneccessary database queries or http requests.
|
|
Say you were wanting to list a bunch of cults people were in</p>
|
|
<pre><code class="language-graphql">query {
|
|
persons {
|
|
id
|
|
name
|
|
cult {
|
|
id
|
|
name
|
|
}
|
|
}
|
|
}
|
|
</code></pre>
|
|
<p>What would be executed by a SQL database would be:</p>
|
|
<pre><code class="language-sql">SELECT id, name, cult_id FROM persons;
|
|
SELECT id, name FROM cults WHERE id = 1;
|
|
SELECT id, name FROM cults WHERE id = 1;
|
|
SELECT id, name FROM cults WHERE id = 1;
|
|
SELECT id, name FROM cults WHERE id = 1;
|
|
SELECT id, name FROM cults WHERE id = 2;
|
|
SELECT id, name FROM cults WHERE id = 2;
|
|
SELECT id, name FROM cults WHERE id = 2;
|
|
# ...
|
|
</code></pre>
|
|
<p>Once the list of users has been returned, a separate query is run to find the cult of each user.
|
|
You can see how this could quickly become a problem.</p>
|
|
<p>A common solution to this is to introduce a <strong>dataloader</strong>.
|
|
This can be done with Juniper using the crate <a href="https://github.com/cksac/dataloader-rs">cksac/dataloader-rs</a>, which has two types of dataloaders; cached and non-cached.</p>
|
|
<a class="header" href="#cached-loader" id="cached-loader"><h4>Cached Loader</h4></a>
|
|
<p>DataLoader provides a memoization cache, after .load() is called once with a given key, the resulting value is cached to eliminate redundant loads.</p>
|
|
<p>DataLoader caching does not replace Redis, Memcache, or any other shared application-level cache. DataLoader is first and foremost a data loading mechanism, and its cache only serves the purpose of not repeatedly loading the same data in the context of a single request to your Application. <a href="https://github.com/graphql/dataloader#caching">(read more)</a></p>
|
|
<a class="header" href="#what-does-it-look-like" id="what-does-it-look-like"><h3>What does it look like?</h3></a>
|
|
<p>!FILENAME Cargo.toml</p>
|
|
<pre><code class="language-toml">[dependencies]
|
|
actix-identity = "0.2"
|
|
actix-rt = "1.0"
|
|
actix-web = {version = "2.0", features = []}
|
|
juniper = { git = "https://github.com/graphql-rust/juniper" }
|
|
futures = "0.3"
|
|
postgres = "0.15.2"
|
|
dataloader = "0.12.0"
|
|
async-trait = "0.1.30"
|
|
</code></pre>
|
|
<pre><code class="language-rust ignore">// use dataloader::cached::Loader;
|
|
use dataloader::non_cached::Loader;
|
|
use dataloader::BatchFn;
|
|
use std::collections::HashMap;
|
|
use postgres::{Connection, TlsMode};
|
|
use std::env;
|
|
|
|
pub fn get_db_conn() -> Connection {
|
|
let pg_connection_string = env::var("DATABASE_URI").expect("need a db uri");
|
|
println!("Connecting to {}", pg_connection_string);
|
|
let conn = Connection::connect(&pg_connection_string[..], TlsMode::None).unwrap();
|
|
println!("Connection is fine");
|
|
conn
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Cult {
|
|
pub id: i32,
|
|
pub name: String,
|
|
}
|
|
|
|
pub fn get_cult_by_ids(hashmap: &mut HashMap<i32, Cult>, ids: Vec<i32>) {
|
|
let conn = get_db_conn();
|
|
for row in &conn
|
|
.query("SELECT id, name FROM cults WHERE id = ANY($1)", &[&ids])
|
|
.unwrap()
|
|
{
|
|
let cult = Cult {
|
|
id: row.get(0),
|
|
name: row.get(1),
|
|
};
|
|
hashmap.insert(cult.id, cult);
|
|
}
|
|
}
|
|
|
|
pub struct CultBatcher;
|
|
|
|
#[async_trait]
|
|
impl BatchFn<i32, Cult> for CultBatcher {
|
|
|
|
// A hashmap is used, as we need to return an array which maps each original key to a Cult.
|
|
async fn load(&self, keys: &[i32]) -> HashMap<i32, Cult> {
|
|
println!("load cult batch {:?}", keys);
|
|
let mut cult_hashmap = HashMap::new();
|
|
get_cult_by_ids(&mut cult_hashmap, keys.to_vec());
|
|
cult_hashmap
|
|
}
|
|
}
|
|
|
|
pub type CultLoader = Loader<i32, Cult, CultBatcher>;
|
|
|
|
// To create a new loader
|
|
pub fn get_loader() -> CultLoader {
|
|
Loader::new(CultBatcher)
|
|
// Usually a DataLoader will coalesce all individual loads which occur
|
|
// within a single frame of execution before calling your batch function with all requested keys.
|
|
// However sometimes this behavior is not desirable or optimal.
|
|
// Perhaps you expect requests to be spread out over a few subsequent ticks
|
|
// See: https://github.com/cksac/dataloader-rs/issues/12
|
|
// More info: https://github.com/graphql/dataloader#batch-scheduling
|
|
// A larger yield count will allow more requests to append to batch but will wait longer before actual load.
|
|
.with_yield_count(100)
|
|
}
|
|
|
|
#[juniper::graphql_object(Context = Context)]
|
|
impl Cult {
|
|
// your resolvers
|
|
|
|
// To call the dataloader
|
|
pub async fn cult_by_id(ctx: &Context, id: i32) -> Cult {
|
|
ctx.cult_loader.load(id).await
|
|
}
|
|
}
|
|
|
|
</code></pre>
|
|
<a class="header" href="#how-do-i-call-them" id="how-do-i-call-them"><h3>How do I call them?</h3></a>
|
|
<p>Once created, a dataloader has the async functions <code>.load()</code> and <code>.load_many()</code>.
|
|
In the above example <code>cult_loader.load(id: i32).await</code> returns <code>Cult</code>. If we had used <code>cult_loader.load_many(Vec<i32>).await</code> it would have returned <code>Vec<Cult></code>.</p>
|
|
<a class="header" href="#where-do-i-create-my-dataloaders" id="where-do-i-create-my-dataloaders"><h3>Where do I create my dataloaders?</h3></a>
|
|
<p><strong>Dataloaders</strong> should be created per-request to avoid risk of bugs where one user is able to load cached/batched data from another user/ outside of its authenticated scope.
|
|
Creating dataloaders within individual resolvers will prevent batching from occurring and will nullify the benefits of the dataloader.</p>
|
|
<p>For example:</p>
|
|
<p><em>When you declare your context</em></p>
|
|
<pre><code class="language-rust ignore">use juniper;
|
|
|
|
#[derive(Clone)]
|
|
pub struct Context {
|
|
pub cult_loader: CultLoader,
|
|
}
|
|
|
|
impl juniper::Context for Context {}
|
|
|
|
impl Context {
|
|
pub fn new(cult_loader: CultLoader) -> Self {
|
|
Self {
|
|
cult_loader
|
|
}
|
|
}
|
|
}
|
|
</code></pre>
|
|
<p><em>Your handler for GraphQL (Note: instantiating context here keeps it per-request)</em></p>
|
|
<pre><code class="language-rust ignore">pub async fn graphql(
|
|
st: web::Data<Arc<Schema>>,
|
|
data: web::Json<GraphQLRequest>,
|
|
) -> Result<HttpResponse, Error> {
|
|
|
|
// Context setup
|
|
let cult_loader = get_loader();
|
|
let ctx = Context::new(cult_loader);
|
|
|
|
// Execute
|
|
let res = data.execute(&st, &ctx).await;
|
|
let json = serde_json::to_string(&res).map_err(error::ErrorInternalServerError)?;
|
|
|
|
Ok(HttpResponse::Ok()
|
|
.content_type("application/json")
|
|
.body(json))
|
|
}
|
|
</code></pre>
|
|
<a class="header" href="#further-example" id="further-example"><h3>Further Example:</h3></a>
|
|
<p>For a full example using Dataloaders and Context check out <a href="https://github.com/jayy-lmao/rust-graphql-docker">jayy-lmao/rust-graphql-docker</a>.</p>
|
|
<a class="header" href="#subscriptions" id="subscriptions"><h1>Subscriptions</h1></a>
|
|
<a class="header" href="#how-to-achieve-realtime-data-with-graphql-subscriptions" id="how-to-achieve-realtime-data-with-graphql-subscriptions"><h3>How to achieve realtime data with GraphQL subscriptions</h3></a>
|
|
<p>GraphQL subscriptions are a way to push data from the server to clients requesting real-time messages
|
|
from the server. Subscriptions are similar to queries in that they specify a set of fields to be delivered to the client,
|
|
but instead of immediately returning a single answer, a result is sent every time a particular event happens on the
|
|
server.</p>
|
|
<p>In order to execute subscriptions you need a coordinator (that spawns connections)
|
|
and a GraphQL object that can be resolved into a stream--elements of which will then
|
|
be returned to the end user. The <a href="https://github.com/graphql-rust/juniper/tree/master/juniper_subscriptions">juniper_subscriptions</a> crate
|
|
provides a default connection implementation. Currently subscriptions are only supported on the <code>master</code> branch. Add the following to your <code>Cargo.toml</code>:</p>
|
|
<pre><code class="language-toml">[dependencies]
|
|
juniper = { git = "https://github.com/graphql-rust/juniper", branch = "master" }
|
|
juniper_subscriptions = { git = "https://github.com/graphql-rust/juniper", branch = "master" }
|
|
</code></pre>
|
|
<a class="header" href="#schema-definition" id="schema-definition"><h3>Schema Definition</h3></a>
|
|
<p>The Subscription is just a GraphQL object, similar to the Query root and Mutations object that you defined for the
|
|
operations in your [Schema][Schema], the difference is that all the operations defined there should be async and the return of it
|
|
should be a <a href="https://docs.rs/futures/0.3.4/futures/stream/trait.Stream.html">Stream</a>.</p>
|
|
<p>This example shows a subscription operation that returns two events, the strings <code>Hello</code> and <code>World!</code>
|
|
sequentially:</p>
|
|
<pre><pre class="playpen"><code class="language-rust"># use juniper::http::GraphQLRequest;
|
|
# use juniper::{DefaultScalarValue, FieldError, SubscriptionCoordinator};
|
|
# use juniper_subscriptions::Coordinator;
|
|
# use futures::{Stream, StreamExt};
|
|
# use std::pin::Pin;
|
|
# #[derive(Clone)]
|
|
# pub struct Database;
|
|
# impl juniper::Context for Database {}
|
|
# impl Database {
|
|
# fn new() -> Self {
|
|
# Self {}
|
|
# }
|
|
# }
|
|
# pub struct Query;
|
|
# #[juniper::graphql_object(Context = Database)]
|
|
# impl Query {
|
|
# fn hello_world() -> &str {
|
|
# "Hello World!"
|
|
# }
|
|
# }
|
|
pub struct Subscription;
|
|
|
|
type StringStream = Pin<Box<dyn Stream<Item = Result<String, FieldError>> + Send>>;
|
|
|
|
#[juniper::graphql_subscription(Context = Database)]
|
|
impl Subscription {
|
|
async fn hello_world() -> StringStream {
|
|
let stream = tokio::stream::iter(vec![
|
|
Ok(String::from("Hello")),
|
|
Ok(String::from("World!"))
|
|
]);
|
|
Box::pin(stream)
|
|
}
|
|
}
|
|
# fn main () {}
|
|
</code></pre></pre>
|
|
<a class="header" href="#coordinator" id="coordinator"><h3>Coordinator</h3></a>
|
|
<p>Subscriptions require a bit more resources than regular queries, since they can provide a great vector
|
|
for DOS attacks and can bring down a server easily if not handled right. [SubscriptionCoordinator][SubscriptionCoordinator] trait provides the coordination logic.
|
|
It contains the schema and can keep track of opened connections, handle subscription
|
|
start and maintains a global subscription id. Once connection is established, subscription
|
|
coordinator spawns a [SubscriptionConnection][SubscriptionConnection], which handles a
|
|
single connection, provides resolver logic for a client stream and can provide re-connection
|
|
and shutdown logic.</p>
|
|
<p>The [Coordinator][Coordinator] struct is a simple implementation of the trait [SubscriptionCoordinator][SubscriptionCoordinator]
|
|
that is responsible for handling the execution of subscription operation into your schema. The execution of the <code>subscribe</code>
|
|
operation returns a [Future][Future] with a Item value of a Result<[Connection][Connection], [GraphQLError][GraphQLError]>,
|
|
where the connection is the Stream of values returned by the operation and the GraphQLError is the error that occurred in the
|
|
resolution of this connection, which means that the subscription failed.</p>
|
|
<pre><pre class="playpen"><code class="language-rust"># use juniper::http::GraphQLRequest;
|
|
# use juniper::{DefaultScalarValue, EmptyMutation, FieldError, RootNode, SubscriptionCoordinator};
|
|
# use juniper_subscriptions::Coordinator;
|
|
# use futures::{Stream, StreamExt};
|
|
# use std::pin::Pin;
|
|
# use tokio::runtime::Runtime;
|
|
# use tokio::task;
|
|
#
|
|
# #[derive(Clone)]
|
|
# pub struct Database;
|
|
#
|
|
# impl juniper::Context for Database {}
|
|
#
|
|
# impl Database {
|
|
# fn new() -> Self {
|
|
# Self {}
|
|
# }
|
|
# }
|
|
#
|
|
# pub struct Query;
|
|
#
|
|
# #[juniper::graphql_object(Context = Database)]
|
|
# impl Query {
|
|
# fn hello_world() -> &str {
|
|
# "Hello World!"
|
|
# }
|
|
# }
|
|
#
|
|
# pub struct Subscription;
|
|
#
|
|
# type StringStream = Pin<Box<dyn Stream<Item = Result<String, FieldError>> + Send>>;
|
|
#
|
|
# #[juniper::graphql_subscription(Context = Database)]
|
|
# impl Subscription {
|
|
# async fn hello_world() -> StringStream {
|
|
# let stream =
|
|
# tokio::stream::iter(vec![Ok(String::from("Hello")), Ok(String::from("World!"))]);
|
|
# Box::pin(stream)
|
|
# }
|
|
# }
|
|
type Schema = RootNode<'static, Query, EmptyMutation<Database>, Subscription>;
|
|
|
|
fn schema() -> Schema {
|
|
Schema::new(Query {}, EmptyMutation::new(), Subscription {})
|
|
}
|
|
|
|
async fn run_subscription() {
|
|
let schema = schema();
|
|
let coordinator = Coordinator::new(schema);
|
|
let req: GraphQLRequest<DefaultScalarValue> = serde_json::from_str(
|
|
r#"
|
|
{
|
|
"query": "subscription { helloWorld }"
|
|
}
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
let ctx = Database::new();
|
|
let mut conn = coordinator.subscribe(&req, &ctx).await.unwrap();
|
|
while let Some(result) = conn.next().await {
|
|
println!("{}", serde_json::to_string(&result).unwrap());
|
|
}
|
|
}
|
|
|
|
# fn main() { }
|
|
</code></pre></pre>
|
|
<a class="header" href="#web-integration-and-examples" id="web-integration-and-examples"><h3>Web Integration and Examples</h3></a>
|
|
<p>Currently there is an example of subscriptions with [warp][warp], but it still in an alpha state.
|
|
GraphQL over [WS][WS] is not fully supported yet and is non-standard.</p>
|
|
<ul>
|
|
<li><a href="https://github.com/graphql-rust/juniper/tree/master/examples/warp_subscriptions">Warp Subscription Example</a></li>
|
|
<li><a href="https://github.com/graphql-rust/juniper/tree/master/examples/basic_subscriptions">Small Example</a></li>
|
|
</ul>
|
|
<!-- TODO: Fix these links when the documentation for the `juniper_subscriptions` are defined in the docs. --->
|
|
[Coordinator]: https://docs.rs/juniper_subscriptions/0.15.0/struct.Coordinator.html
|
|
[SubscriptionCoordinator]: https://docs.rs/juniper_subscriptions/0.15.0/trait.SubscriptionCoordinator.html
|
|
[Connection]: https://docs.rs/juniper_subscriptions/0.15.0/struct.Connection.html
|
|
[SubscriptionConnection]: https://docs.rs/juniper_subscriptions/0.15.0/trait.SubscriptionConnection.html
|
|
<!--- --->
|
|
[Future]: https://docs.rs/futures/0.3.4/futures/future/trait.Future.html
|
|
[warp]: https://github.com/graphql-rust/juniper/tree/master/juniper_warp
|
|
[WS]: https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md
|
|
[GraphQLError]: https://docs.rs/juniper/0.14.2/juniper/enum.GraphQLError.html
|
|
[Schema]: ../schema/schemas_and_mutations.md
|
|
|
|
</main>
|
|
|
|
<nav class="nav-wrapper" aria-label="Page navigation">
|
|
<!-- Mobile navigation buttons -->
|
|
|
|
|
|
|
|
|
|
<div style="clear: both"></div>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
|
|
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
|
|
|
|
|
|
|
</nav>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
|
|
<script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
|
|
<script src="searcher.js" type="text/javascript" charset="utf-8"></script>
|
|
|
|
|
|
<script src="clipboard.min.js" type="text/javascript" charset="utf-8"></script>
|
|
<script src="highlight.js" type="text/javascript" charset="utf-8"></script>
|
|
<script src="book.js" type="text/javascript" charset="utf-8"></script>
|
|
|
|
<!-- Custom JS scripts -->
|
|
|
|
|
|
|
|
|
|
<script type="text/javascript">
|
|
window.addEventListener('load', function() {
|
|
window.setTimeout(window.print, 100);
|
|
});
|
|
</script>
|
|
|
|
|
|
|
|
</body>
|
|
</html>
|