3732 lines
219 KiB
HTML
3732 lines
219 KiB
HTML
<!DOCTYPE HTML>
|
||
<html lang="en" class="light sidebar-visible" dir="ltr">
|
||
<head>
|
||
<!-- Book generated using mdBook -->
|
||
<meta charset="UTF-8">
|
||
<title>Juniper Book</title>
|
||
<meta name="robots" content="noindex">
|
||
|
||
|
||
<!-- Custom HTML head -->
|
||
|
||
<meta name="description" content="User guide for Juniper (GraphQL server library for Rust).">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta name="theme-color" content="#ffffff">
|
||
|
||
<link rel="icon" href="favicon.svg">
|
||
<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 rel="stylesheet" href="fonts/fonts.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 -->
|
||
|
||
|
||
<!-- Provide site root to javascript -->
|
||
<script>
|
||
var path_to_root = "";
|
||
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
|
||
</script>
|
||
<!-- Start loading toc.js asap -->
|
||
<script src="toc.js"></script>
|
||
</head>
|
||
<body>
|
||
<div id="body-container">
|
||
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||
<script>
|
||
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>
|
||
var theme;
|
||
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||
if (theme === null || theme === undefined) { theme = default_theme; }
|
||
const html = document.documentElement;
|
||
html.classList.remove('light')
|
||
html.classList.add(theme);
|
||
html.classList.add("js");
|
||
</script>
|
||
|
||
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
|
||
|
||
<!-- Hide / unhide sidebar before it is displayed -->
|
||
<script>
|
||
var sidebar = null;
|
||
var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
|
||
if (document.body.clientWidth >= 1080) {
|
||
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||
sidebar = sidebar || 'visible';
|
||
} else {
|
||
sidebar = 'hidden';
|
||
}
|
||
sidebar_toggle.checked = sidebar === 'visible';
|
||
html.classList.remove('sidebar-visible');
|
||
html.classList.add("sidebar-" + sidebar);
|
||
</script>
|
||
|
||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||
<!-- populated by js -->
|
||
<mdbook-sidebar-scrollbox class="sidebar-scrollbox"></mdbook-sidebar-scrollbox>
|
||
<noscript>
|
||
<iframe class="sidebar-iframe-outer" src="toc.html"></iframe>
|
||
</noscript>
|
||
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
|
||
<div class="sidebar-resize-indicator"></div>
|
||
</div>
|
||
</nav>
|
||
|
||
<div id="page-wrapper" class="page-wrapper">
|
||
|
||
<div class="page">
|
||
<div id="menu-bar-hover-placeholder"></div>
|
||
<div id="menu-bar" class="menu-bar sticky">
|
||
<div class="left-buttons">
|
||
<label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||
<i class="fa fa-bars"></i>
|
||
</label>
|
||
<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</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 Book</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 id="search-wrapper" class="hidden">
|
||
<form id="searchbar-outer" class="searchbar-outer">
|
||
<input type="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>
|
||
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>
|
||
<h1 id="introduction"><a class="header" href="#introduction">Introduction</a></h1>
|
||
<blockquote>
|
||
<p><a href="https://graphql.org">GraphQL</a> is a query language for APIs and a runtime for fulfilling those queries with your existing data. <a href="https://graphql.org">GraphQL</a> provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.</p>
|
||
</blockquote>
|
||
<p><a href="https://docs.rs/juniper">Juniper</a> is a library for creating <a href="https://graphql.org">GraphQL</a> servers in <a href="https://www.rust-lang.org">Rust</a>. Build type-safe and fast API servers with minimal boilerplate and configuration (we do try to make declaring and resolving <a href="https://graphql.org">GraphQL</a> schemas as convenient as <a href="https://www.rust-lang.org">Rust</a> will allow).</p>
|
||
<p><a href="https://docs.rs/juniper">Juniper</a> doesn't include a web server itself, instead, it provides building blocks to make integration with existing web servers straightforward. It optionally provides a pre-built integration for some widely used web server frameworks in <a href="https://www.rust-lang.org">Rust</a> ecosystem.</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>
|
||
<h2 id="features"><a class="header" href="#features">Features</a></h2>
|
||
<p><a href="https://docs.rs/juniper">Juniper</a> supports the full GraphQL query language according to the <a href="https://spec.graphql.org/October2021">specification (October 2021)</a>.</p>
|
||
<blockquote>
|
||
<p><strong>NOTE</strong>: As an exception to other <a href="https://graphql.org">GraphQL</a> libraries for other languages, <a href="https://docs.rs/juniper">Juniper</a> builds non-<code>null</code> types by default. A field of type <code>Vec<Episode></code> will be converted into <code>[Episode!]!</code>. The corresponding Rust type for a <code>null</code>able <code>[Episode]</code> would be <code>Option<Vec<Option<Episode>>></code> instead.</p>
|
||
</blockquote>
|
||
<h2 id="integrations"><a class="header" href="#integrations">Integrations</a></h2>
|
||
<h3 id="types"><a class="header" href="#types">Types</a></h3>
|
||
<p><a href="https://docs.rs/juniper">Juniper</a> provides out-of-the-box integration for some very common <a href="https://www.rust-lang.org">Rust</a> crates to make building schemas a breeze. The types from these crates will be usable in your schemas automatically after enabling the correspondent self-titled <a href="https://doc.rust-lang.org/cargo/reference/features.html">Cargo feature</a>:</p>
|
||
<ul>
|
||
<li><a href="https://docs.rs/bigdecimal"><code>bigdecimal</code></a></li>
|
||
<li><a href="https://docs.rs/bson"><code>bson</code></a></li>
|
||
<li><a href="https://docs.rs/chrono"><code>chrono</code></a>, <a href="https://docs.rs/chrono-tz"><code>chrono-tz</code></a></li>
|
||
<li><a href="https://docs.rs/jiff"><code>jiff</code></a></li>
|
||
<li><a href="https://docs.rs/rust_decimal"><code>rust_decimal</code></a></li>
|
||
<li><a href="https://docs.rs/time"><code>time</code></a></li>
|
||
<li><a href="https://docs.rs/url"><code>url</code></a></li>
|
||
<li><a href="https://docs.rs/uuid"><code>uuid</code></a></li>
|
||
</ul>
|
||
<h3 id="web-server-frameworks"><a class="header" href="#web-server-frameworks">Web server frameworks</a></h3>
|
||
<ul>
|
||
<li><a href="https://docs.rs/actix-web"><code>actix-web</code></a> (<a href="https://docs.rs/juniper_actix"><code>juniper_actix</code></a> crate)</li>
|
||
<li><a href="https://docs.rs/axum"><code>axum</code></a> (<a href="https://docs.rs/juniper_axum"><code>juniper_axum</code></a> crate)</li>
|
||
<li><a href="https://docs.rs/hyper"><code>hyper</code></a> (<a href="https://docs.rs/juniper_hyper"><code>juniper_hyper</code></a> crate)</li>
|
||
<li><a href="https://docs.rs/rocket"><code>rocket</code></a> (<a href="https://docs.rs/juniper_rocket"><code>juniper_rocket</code></a> crate)</li>
|
||
<li><a href="https://docs.rs/warp"><code>warp</code></a> (<a href="https://docs.rs/juniper_warp"><code>juniper_warp</code></a> crate)</li>
|
||
</ul>
|
||
<h2 id="api-stability"><a class="header" href="#api-stability">API stability</a></h2>
|
||
<p><a href="https://docs.rs/juniper">Juniper</a> has not reached 1.0 yet, thus some API instability should be expected.</p>
|
||
<div style="break-before: page; page-break-before: always;"></div><h1 id="quickstart"><a class="header" href="#quickstart">Quickstart</a></h1>
|
||
<p>This page will give you a short introduction to the concepts in <a href="https://docs.rs/juniper">Juniper</a>.</p>
|
||
<p><strong><a href="https://docs.rs/juniper">Juniper</a> follows a <a href="https://www.apollographql.com/blog/backend/architecture/schema-first-vs-code-only-graphql#code-only">code-first</a> approach to define a <a href="https://graphql.org">GraphQL</a> schema.</strong></p>
|
||
<blockquote>
|
||
<p><strong>TIP</strong>: For a <a href="https://www.apollographql.com/blog/backend/architecture/schema-first-vs-code-only-graphql#schema-first">schema-first</a> approach, consider using a <a href="https://docs.rs/juniper-from-schema"><code>juniper-from-schema</code></a> crate for generating a <a href="https://docs.rs/juniper"><code>juniper</code></a>-based code from a <a href="https://graphql.org/learn/schema">schema</a> file.</p>
|
||
</blockquote>
|
||
<h2 id="installation"><a class="header" href="#installation">Installation</a></h2>
|
||
<pre><code class="language-toml">[dependencies]
|
||
juniper = "0.16.1"
|
||
</code></pre>
|
||
<h2 id="schema"><a class="header" href="#schema">Schema</a></h2>
|
||
<p>Exposing simple enums and structs as <a href="https://graphql.org">GraphQL</a> types is just a matter of adding a custom <a href="https://doc.rust-lang.org/stable/reference/attributes/derive.html#derive">derive attribute</a> to them. <a href="https://docs.rs/juniper">Juniper</a> includes support for basic <a href="https://www.rust-lang.org">Rust</a> types that naturally map to <a href="https://graphql.org">GraphQL</a> features, such as <code>Option<T></code>, <code>Vec<T></code>, <code>Box<T></code>, <code>Arc<T></code>, <code>String</code>, <code>f64</code>, <code>i32</code>, references, slices and arrays.</p>
|
||
<p>For more advanced mappings, <a href="https://docs.rs/juniper">Juniper</a> provides multiple macros to map your <a href="https://www.rust-lang.org">Rust</a> types to a <a href="https://graphql.org/learn/schema">GraphQL schema</a>. The most important one is the <a href="https://docs.rs/juniper/0.16.1/juniper/macro.graphql_object.html"><code>#[graphql_object]</code> attribute</a> that is used for declaring a <a href="https://spec.graphql.org/October2021#sec-Objects">GraphQL object</a> with resolvers (typically used for declaring <a href="https://spec.graphql.org/October2021#sec-Root-Operation-Types"><code>Query</code> and <code>Mutation</code> roots</a>).</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring"># ![allow(unused_variables)]
|
||
</span><span class="boring">extern crate juniper;
|
||
</span><span class="boring">
|
||
</span><span class="boring">use std::fmt::Display;
|
||
</span><span class="boring">
|
||
</span>use juniper::{
|
||
graphql_object, EmptySubscription, FieldResult, GraphQLEnum,
|
||
GraphQLInputObject, GraphQLObject, ScalarValue,
|
||
};
|
||
<span class="boring">
|
||
</span><span class="boring">struct DatabasePool;
|
||
</span><span class="boring">impl DatabasePool {
|
||
</span><span class="boring"> fn get_connection(&self) -> FieldResult<DatabasePool> { Ok(DatabasePool) }
|
||
</span><span class="boring"> fn find_human(&self, _id: &str) -> FieldResult<Human> { Err("")? }
|
||
</span><span class="boring"> fn insert_human(&self, _human: &NewHuman) -> FieldResult<Human> { Err("")? }
|
||
</span><span class="boring">}
|
||
</span>
|
||
#[derive(GraphQLEnum)]
|
||
enum Episode {
|
||
NewHope,
|
||
Empire,
|
||
Jedi,
|
||
}
|
||
|
||
#[derive(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(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 `#[graphql_object]` attribute.
|
||
|
||
// Resolvers can have a context that allows accessing shared state like a
|
||
// database pool.
|
||
struct Context {
|
||
// Use your real database pool here.
|
||
db: DatabasePool,
|
||
}
|
||
|
||
// To make our `Context` usable by `juniper`, we have to implement a marker
|
||
// trait.
|
||
impl juniper::Context for Context {}
|
||
|
||
struct Query;
|
||
|
||
// Here we specify the context type for the object.
|
||
// We need to do this in every type that needs access to the `Context`.
|
||
#[graphql_object]
|
||
#[graphql(context = Context)]
|
||
impl Query {
|
||
// Note, that the field name will be automatically converted to the
|
||
// `camelCased` variant, just as GraphQL conventions imply.
|
||
fn api_version() -> &'static str {
|
||
"1.0"
|
||
}
|
||
|
||
fn human(
|
||
// Arguments to resolvers can either be simple scalar types, enums or
|
||
// input objects.
|
||
id: String,
|
||
// To gain access to the `Context`, we specify a `context`-named
|
||
// argument referring the correspondent `Context` type, and `juniper`
|
||
// will inject it automatically.
|
||
context: &Context,
|
||
) -> FieldResult<Human> {
|
||
// Get a `db` connection.
|
||
let conn = context.db.get_connection()?;
|
||
// Execute a `db` query.
|
||
// Note the use of `?` to propagate errors.
|
||
let human = conn.find_human(&id)?;
|
||
// Return the result.
|
||
Ok(human)
|
||
}
|
||
}
|
||
|
||
// Now, we do the same for our `Mutation` type.
|
||
|
||
struct Mutation;
|
||
|
||
#[graphql_object]
|
||
#[graphql(
|
||
context = Context,
|
||
// If we need to use `ScalarValue` parametrization explicitly somewhere
|
||
// in the object definition (like here in `FieldResult`), we could
|
||
// declare an explicit type parameter for that, and specify it.
|
||
scalar = S: ScalarValue + Display,
|
||
)]
|
||
impl Mutation {
|
||
fn create_human<S: ScalarValue + Display>(
|
||
new_human: NewHuman,
|
||
context: &Context,
|
||
) -> FieldResult<Human, S> {
|
||
let db = context.db.get_connection().map_err(|e| e.map_scalar_value())?;
|
||
let human: Human = db.insert_human(&new_human).map_err(|e| e.map_scalar_value())?;
|
||
Ok(human)
|
||
}
|
||
}
|
||
|
||
// Root schema consists of a query, a mutation, and a subscription.
|
||
// Request queries can be executed against a `RootNode`.
|
||
type Schema = juniper::RootNode<'static, Query, Mutation, EmptySubscription<Context>>;
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {
|
||
</span><span class="boring"> _ = Schema::new(Query, Mutation, EmptySubscription::new());
|
||
</span><span class="boring">}</span></code></pre></pre>
|
||
<p>Now we have a very simple but functional schema for a <a href="https://graphql.org">GraphQL</a> server!</p>
|
||
<p>To actually serve the <a href="https://graphql.org/learn/schema">schema</a>, see the guides for our various <a href="serve/index.html">server integrations</a>.</p>
|
||
<h2 id="execution"><a class="header" href="#execution">Execution</a></h2>
|
||
<p><a href="https://docs.rs/juniper">Juniper</a> is a library that can be used in many contexts: it doesn't require a server, nor it has a dependency on a particular transport or serialization format. You can invoke the <code>juniper::execute()</code> directly to get a result for a <a href="https://graphql.org">GraphQL</a> query:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">// Only needed due to 2018 edition because the macro is not accessible.
|
||
</span><span class="boring">#[macro_use] extern crate juniper;
|
||
</span>use juniper::{
|
||
graphql_object, graphql_value, EmptyMutation, EmptySubscription,
|
||
GraphQLEnum, Variables,
|
||
};
|
||
|
||
#[derive(GraphQLEnum, Clone, Copy)]
|
||
enum Episode {
|
||
// Note, that the enum value will be automatically converted to the
|
||
// `SCREAMING_SNAKE_CASE` variant, just as GraphQL conventions imply.
|
||
NewHope,
|
||
Empire,
|
||
Jedi,
|
||
}
|
||
|
||
// Arbitrary context data.
|
||
struct Ctx(Episode);
|
||
|
||
impl juniper::Context for Ctx {}
|
||
|
||
struct Query;
|
||
|
||
#[graphql_object]
|
||
#[graphql(context = Ctx)]
|
||
impl Query {
|
||
fn favorite_episode(context: &Ctx) -> Episode {
|
||
context.0
|
||
}
|
||
}
|
||
|
||
type Schema = juniper::RootNode<'static, Query, EmptyMutation<Ctx>, EmptySubscription<Ctx>>;
|
||
|
||
fn main() {
|
||
// Create a context.
|
||
let ctx = Ctx(Episode::NewHope);
|
||
|
||
// Run the execution.
|
||
let (res, _errors) = juniper::execute_sync(
|
||
"query { favoriteEpisode }",
|
||
None,
|
||
&Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()),
|
||
&Variables::new(),
|
||
&ctx,
|
||
).unwrap();
|
||
|
||
assert_eq!(
|
||
res,
|
||
graphql_value!({
|
||
"favoriteEpisode": "NEW_HOPE",
|
||
}),
|
||
);
|
||
}</code></pre></pre>
|
||
<div style="break-before: page; page-break-before: always;"></div><h1 id="type-system"><a class="header" href="#type-system">Type system</a></h1>
|
||
<p>Most of the work in working with <a href="https://docs.rs/juniper">Juniper</a> consists of mapping the <a href="https://spec.graphql.org/October2021#sec-Type-System">GraphQL type system</a> to the <a href="https://www.rust-lang.org">Rust</a> types our application uses.</p>
|
||
<p><a href="https://docs.rs/juniper">Juniper</a> provides some convenient abstractions making this process as painless as possible.</p>
|
||
<p>Find out more in the individual chapters below:</p>
|
||
<ul>
|
||
<li><a href="types/objects/index.html">Objects</a>
|
||
<ul>
|
||
<li><a href="types/objects/complex_fields.html">Complex fields</a></li>
|
||
<li><a href="types/objects/Context.html">Context</a></li>
|
||
<li><a href="types/objects/error/index.html">Error handling</a>
|
||
<ul>
|
||
<li><a href="types/objects/error/field.html">Field errors</a></li>
|
||
<li><a href="types/objects/error/schema.html">Schema errors</a></li>
|
||
</ul>
|
||
</li>
|
||
<li><a href="types/objects/generics.html">Generics</a></li>
|
||
</ul>
|
||
</li>
|
||
<li><a href="types/interfaces.html">Interfaces</a></li>
|
||
<li><a href="types/unions.html">Unions</a></li>
|
||
<li><a href="types/enums.html">Enums</a></li>
|
||
<li><a href="types/input_objects.html">Input objects</a></li>
|
||
<li><a href="types/scalars.html">Scalars</a></li>
|
||
</ul>
|
||
<div style="break-before: page; page-break-before: always;"></div><h1 id="objects"><a class="header" href="#objects">Objects</a></h1>
|
||
<blockquote>
|
||
<p><a href="https://spec.graphql.org/October2021#sec-Objects">GraphQL objects</a> represent a list of named fields, each of which yield a value of a specific type.</p>
|
||
</blockquote>
|
||
<p>When declaring a <a href="https://graphql.org/learn/schema">GraphQL schema</a>, most of the time we deal with <a href="https://spec.graphql.org/October2021#sec-Objects">GraphQL objects</a>, because they are the only place where we actually define the behavior once <a href="https://graphql.org/learn/schema">schema</a> gets <a href="https://spec.graphql.org/October2021#sec-Execution">executed</a>.</p>
|
||
<p>There are two ways to define a <a href="https://spec.graphql.org/October2021#sec-Objects">GraphQL object</a> in <a href="https://docs.rs/juniper">Juniper</a>:</p>
|
||
<ol>
|
||
<li>The easiest way, suitable for trivial cases, is to use the <a href="https://docs.rs/juniper/0.16.1/juniper/derive.GraphQLObject.html"><code>#[derive(GraphQLObject)]</code> attribute</a> on a <a href="https://doc.rust-lang.org/reference/items/structs.html">struct</a>, as described below.</li>
|
||
<li>The other way, using the <a href="https://docs.rs/juniper/0.16.1/juniper/attr.graphql_object.html"><code>#[graphql_object]</code> attribute</a>, is described in the <a href="types/objects/complex_fields.html">"Complex fields" chapter</a>.</li>
|
||
</ol>
|
||
<h2 id="trivial"><a class="header" href="#trivial">Trivial</a></h2>
|
||
<p>While any type in <a href="https://www.rust-lang.org">Rust</a> can be exposed as a <a href="https://spec.graphql.org/October2021#sec-Objects">GraphQL object</a>, the most common one is a <a href="https://doc.rust-lang.org/reference/items/structs.html">struct</a>:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::GraphQLObject;
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLObject)]
|
||
struct Person {
|
||
name: String,
|
||
age: i32,
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<p>This creates a <a href="https://spec.graphql.org/October2021#sec-Objects">GraphQL object</a> 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 <a href="https://www.rust-lang.org">Rust</a>'s type system, everything is exported as <a href="https://spec.graphql.org/October2021#sec-Non-Null">non-<code>null</code></a> by default.</p>
|
||
<blockquote>
|
||
<p><strong>TIP</strong>: If a <code>null</code>able field is required, the most obvious way is to use <code>Option</code>. Or <a href="https://docs.rs/juniper/0.16.1/juniper/enum.Nullable.html"><code>Nullable</code></a> for distinguishing between <a href="https://spec.graphql.org/October2021#sel-EAFdRDHAAEJDAoBxzT">explicit and implicit <code>null</code>s</a>.</p>
|
||
</blockquote>
|
||
<h3 id="documentation"><a class="header" href="#documentation">Documentation</a></h3>
|
||
<p>We should take advantage of the fact that <a href="https://graphql.org">GraphQL</a> is <a href="https://spec.graphql.org/October2021#sec-Introspection">self-documenting</a> and add descriptions to the defined <a href="https://spec.graphql.org/October2021#sec-Objects">GraphQL object</a> type and its fields. <a href="https://docs.rs/juniper">Juniper</a> will automatically use associated <a href="https://doc.rust-lang.org/reference/comments.html#doc-comments">Rust doc comments</a> as <a href="https://spec.graphql.org/October2021#sec-Descriptions">GraphQL descriptions</a>:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::GraphQLObject;
|
||
</span><span class="boring">
|
||
</span>/// Information about a person.
|
||
#[derive(GraphQLObject)]
|
||
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,
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<p>If using <a href="https://doc.rust-lang.org/reference/comments.html#doc-comments">Rust doc comments</a> is not desired (for example, when we want to keep <a href="https://www.rust-lang.org">Rust</a> API docs and GraphQL schema descriptions different), the <code>#[graphql(description = "...")]</code> attribute can be used instead, which takes precedence over <a href="https://doc.rust-lang.org/reference/comments.html#doc-comments">Rust doc comments</a>:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::GraphQLObject;
|
||
</span><span class="boring">
|
||
</span>/// This doc comment is visible only in Rust API docs.
|
||
#[derive(GraphQLObject)]
|
||
#[graphql(description = "This description is visible only in GraphQL schema.")]
|
||
struct Person {
|
||
/// This doc comment is visible only in Rust API docs.
|
||
#[graphql(desc = "This description is visible only in GraphQL schema.")]
|
||
// ^^^^ shortcut for a `description` argument
|
||
name: String,
|
||
|
||
/// This doc comment is visible in both Rust API docs and GraphQL schema
|
||
/// descriptions.
|
||
age: i32,
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<h3 id="renaming"><a class="header" href="#renaming">Renaming</a></h3>
|
||
<p>By default, <a href="https://doc.rust-lang.org/reference/items/structs.html">struct</a> fields are converted from <a href="https://www.rust-lang.org">Rust</a>'s standard <code>snake_case</code> naming convention into <a href="https://graphql.org">GraphQL</a>'s <code>camelCase</code> convention:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::GraphQLObject;
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLObject)]
|
||
struct Person {
|
||
first_name: String, // exposed as `firstName` in GraphQL schema
|
||
last_name: String, // exposed as `lastName` in GraphQL schema
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<p>We can override the name by using the <code>#[graphql(name = "...")]</code> attribute:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::GraphQLObject;
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLObject)]
|
||
#[graphql(name = "WebPerson")] // now exposed as `WebPerson` in GraphQL schema
|
||
struct Person {
|
||
name: String,
|
||
age: i32,
|
||
#[graphql(name = "websiteURL")]
|
||
website_url: Option<String>, // now exposed as `websiteURL` in GraphQL schema
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<p>Or provide a different renaming policy for all the <a href="https://doc.rust-lang.org/reference/items/structs.html">struct</a> fields:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::GraphQLObject;
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLObject)]
|
||
#[graphql(rename_all = "none")] // disables any renaming
|
||
struct Person {
|
||
name: String,
|
||
age: i32,
|
||
website_url: Option<String>, // exposed as `website_url` in GraphQL schema
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>TIP</strong>: Supported policies are: <code>SCREAMING_SNAKE_CASE</code>, <code>camelCase</code> and <code>none</code> (disables any renaming).</p>
|
||
</blockquote>
|
||
<h3 id="deprecation"><a class="header" href="#deprecation">Deprecation</a></h3>
|
||
<p>To <a href="https://spec.graphql.org/October2021#sec--deprecated">deprecate</a> a <a href="https://spec.graphql.org/October2021#sec-Objects">GraphQL object</a> field, either the <code>#[graphql(deprecated = "...")]</code> attribute, or <a href="https://doc.rust-lang.org/reference/attributes/diagnostics.html#the-deprecated-attribute">Rust's <code>#[deprecated]</code> attribute</a>, should be used:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::GraphQLObject;
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLObject)]
|
||
struct Person {
|
||
name: String,
|
||
age: i32,
|
||
#[graphql(deprecated = "Please use the `name` field instead.")]
|
||
first_name: String,
|
||
#[deprecated(note = "Please use the `name` field instead.")]
|
||
last_name: String,
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>NOTE</strong>: Only <a href="https://spec.graphql.org/October2021#sec-Objects">GraphQL object</a>/<a href="https://spec.graphql.org/October2021#sec-Interfaces">interface</a> fields and <a href="https://spec.graphql.org/October2021#sec-Enums">GraphQL enum</a> values can be <a href="https://spec.graphql.org/October2021#sec--deprecated">deprecated</a>.</p>
|
||
</blockquote>
|
||
<h3 id="ignoring"><a class="header" href="#ignoring">Ignoring</a></h3>
|
||
<p>By default, all <a href="https://doc.rust-lang.org/reference/items/structs.html">struct</a> fields are included into the generated <a href="https://spec.graphql.org/October2021#sec-Objects">GraphQL object</a> type. To prevent inclusion of a specific field annotate it with the <code>#[graphql(ignore)]</code> attribute:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">#![allow(dead_code)]
|
||
</span><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::GraphQLObject;
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLObject)]
|
||
struct Person {
|
||
name: String,
|
||
age: i32,
|
||
#[graphql(ignore)]
|
||
password_hash: String, // cannot be queried from GraphQL
|
||
#[graphql(skip)]
|
||
// ^^^^ alternative naming, up to your preference
|
||
is_banned: bool, // cannot be queried from GraphQL
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>TIP</strong>: See more available features in the API docs of the <a href="https://docs.rs/juniper/0.16.1/juniper/derive.GraphQLObject.html"><code>#[derive(GraphQLObject)]</code></a> attribute.</p>
|
||
</blockquote>
|
||
<h2 id="relationships"><a class="header" href="#relationships">Relationships</a></h2>
|
||
<p><a href="https://spec.graphql.org/October2021#sec-Objects">GraphQL object</a> fields can be of any <a href="https://graphql.org">GraphQL</a> type, except <a href="https://spec.graphql.org/October2021#sec-Input-Objects">input objects</a>.</p>
|
||
<p>Let's see what it means to build relationships between <a href="https://spec.graphql.org/October2021#sec-Objects">objects</a>:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::GraphQLObject;
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLObject)]
|
||
struct Person {
|
||
name: String,
|
||
age: i32,
|
||
}
|
||
|
||
#[derive(GraphQLObject)]
|
||
struct House {
|
||
address: Option<String>, // converted into `String` (`null`able)
|
||
inhabitants: Vec<Person>, // converted into `[Person!]!`
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<p>Because <code>Person</code> is a valid <a href="https://graphql.org">GraphQL</a> type, we can have a <code>Vec<Person></code> in a <a href="https://doc.rust-lang.org/reference/items/structs.html">struct</a>, and it'll be automatically converted into a <a href="https://spec.graphql.org/October2021#sec-List">list</a> of <a href="https://spec.graphql.org/October2021#sec-Non-Null">non-<code>null</code>able</a> <code>Person</code> <a href="https://spec.graphql.org/October2021#sec-Objects">objects</a>.</p>
|
||
<div style="break-before: page; page-break-before: always;"></div><h1 id="complex-fields"><a class="header" href="#complex-fields">Complex fields</a></h1>
|
||
<p>Using a plain <a href="https://doc.rust-lang.org/reference/items/structs.html">Rust struct</a> for representing a <a href="https://spec.graphql.org/October2021#sec-Objects">GraphQL object</a> is easy and trivial but does not cover every case. What if we need to express something non-trivial as a <a href="https://spec.graphql.org/October2021#sec-Language.Fields">GraphQL field</a>, such as:</p>
|
||
<ul>
|
||
<li>Calling non-trivial logic while <a href="https://spec.graphql.org/October2021#sec-Execution">executing</a> the <a href="https://spec.graphql.org/October2021#sec-Language.Fields">field</a> (like querying database, etc.).</li>
|
||
<li>Accepting <a href="https://spec.graphql.org/October2021#sec-Language.Arguments">field arguments</a>.</li>
|
||
<li>Defining a circular <a href="https://spec.graphql.org/October2021#sec-Objects">GraphQL object</a>, where one of its <a href="https://spec.graphql.org/October2021#sec-Language.Fields">fields</a> returns the type itself.</li>
|
||
<li>Using some other (non-<a href="https://doc.rust-lang.org/reference/items/structs.html">struct</a>) <a href="https://www.rust-lang.org">Rust</a> type to represent a <a href="https://spec.graphql.org/October2021#sec-Objects">GraphQL object</a>.</li>
|
||
</ul>
|
||
<p>To support these more complicated use cases, we need a way to define a <a href="https://spec.graphql.org/October2021#sec-Language.Fields">GraphQL field</a> as a function. In <a href="https://docs.rs/juniper">Juniper</a> this is achievable by placing the <a href="https://docs.rs/juniper/0.16.1/juniper/attr.graphql_object.html"><code>#[graphql_object]</code> attribute</a> on an <a href="https://doc.rust-lang.org/reference/items/implementations.html#inherent-implementations"><code>impl</code> block</a>, which turns its methods into <a href="https://spec.graphql.org/October2021#sec-Language.Fields">GraphQL fields</a>:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::{graphql_object, GraphQLObject};
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLObject)]
|
||
struct Person {
|
||
name: String,
|
||
age: i32,
|
||
}
|
||
|
||
struct House {
|
||
inhabitants: Vec<Person>,
|
||
}
|
||
|
||
// Defines the `House` GraphQL object.
|
||
#[graphql_object]
|
||
impl House {
|
||
// Creates the field `inhabitantWithName(name: String!)`,
|
||
// returning a `null`able `Person`.
|
||
fn inhabitant_with_name(&self, name: String) -> Option<&Person> {
|
||
self.inhabitants.iter().find(|p| p.name == name)
|
||
}
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>NOTE</strong>: To access global data such as database connections or authentication information, a <em>context</em> is used. To learn more about this, see the <a href="types/objects/context.html">"Context" chapter</a>.</p>
|
||
</blockquote>
|
||
<h3 id="default-arguments"><a class="header" href="#default-arguments">Default arguments</a></h3>
|
||
<p>Though <a href="https://www.rust-lang.org">Rust</a> doesn't have the notion of default arguments, <a href="https://spec.graphql.org/October2021#sec-Language.Fields">GraphQL arguments</a> are able to have default values. These default values are used when a GraphQL operation doesn't specify the argument explicitly. In <a href="https://docs.rs/juniper">Juniper</a>, defining a default value for a <a href="https://spec.graphql.org/October2021#sec-Language.Fields">GraphQL argument</a> is enabled by the <code>#[graphql(default)]</code> attribute:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::graphql_object;
|
||
</span><span class="boring">
|
||
</span>struct Person;
|
||
|
||
#[graphql_object]
|
||
impl Person {
|
||
fn field1(
|
||
// Default value can be any valid Rust expression, including a function
|
||
// call, etc.
|
||
#[graphql(default = true)]
|
||
arg1: bool,
|
||
// If default expression is not specified, then the `Default::default()`
|
||
// value is used.
|
||
#[graphql(default)]
|
||
arg2: i32,
|
||
) -> String {
|
||
format!("{arg1} {arg2}")
|
||
}
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<h3 id="renaming-1"><a class="header" href="#renaming-1">Renaming</a></h3>
|
||
<p>Like with the <a href="types/objects/index.html#renaming"><code>#[derive(GraphQLObject)]</code> attribute on structs</a>, <a href="https://spec.graphql.org/October2021#sec-Language.Fields">field</a> names are converted from <a href="https://www.rust-lang.org">Rust</a>'s standard <code>snake_case</code> naming convention into <a href="https://graphql.org">GraphQL</a>'s <code>camelCase</code> convention.</p>
|
||
<p>We can override the name by using the <code>#[graphql(name = "...")]</code> attribute:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::graphql_object;
|
||
</span><span class="boring">
|
||
</span>struct Person;
|
||
|
||
#[graphql_object]
|
||
#[graphql(name = "PersonObject")]
|
||
impl Person { // exposed as `PersonObject` in GraphQL schema
|
||
#[graphql(name = "myCustomFieldName")]
|
||
fn renamed_field( // exposed as `myCustomFieldName` in GraphQL schema
|
||
#[graphql(name = "myArgument")]
|
||
renamed_argument: bool, // exposed as `myArgument` in GraphQL schema
|
||
) -> bool {
|
||
renamed_argument
|
||
}
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<p>Or provide a different renaming policy for all the defined <a href="https://spec.graphql.org/October2021#sec-Language.Fields">fields</a>:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::graphql_object;
|
||
</span><span class="boring">
|
||
</span>struct Person;
|
||
|
||
#[graphql_object]
|
||
#[graphql(rename_all = "none")] // disables any renaming
|
||
impl Person {
|
||
fn renamed_field( // exposed as `renamed_field` in GraphQL schema
|
||
renamed_argument: bool, // exposed as `renamed_argument` in GraphQL schema
|
||
) -> bool {
|
||
renamed_argument
|
||
}
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>TIP</strong>: Supported policies are: <code>SCREAMING_SNAKE_CASE</code>, <code>camelCase</code> and <code>none</code> (disables any renaming).</p>
|
||
</blockquote>
|
||
<h3 id="documentation-and-deprecation"><a class="header" href="#documentation-and-deprecation">Documentation and deprecation</a></h3>
|
||
<p>Similarly, <a href="https://spec.graphql.org/October2021#sec-Language.Fields">GraphQL fields</a> may also be <a href="https://spec.graphql.org/October2021#sec-Descriptions">documented</a> and <a href="https://spec.graphql.org/October2021#sec--deprecated">deprecated</a> via <code>#[graphql(description = "...")]</code> and <code>#[graphql(deprecated = "...")]</code>/<a href="https://doc.rust-lang.org/reference/attributes/diagnostics.html#the-deprecated-attribute"><code>#[deprecated]</code></a> attributes:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::graphql_object;
|
||
</span><span class="boring">
|
||
</span>struct Person;
|
||
|
||
/// This doc comment is visible only in Rust API docs.
|
||
#[graphql_object]
|
||
#[graphql(description = "This description overwrites the one from doc comment.")]
|
||
impl Person {
|
||
/// This doc comment is visible only in Rust API docs.
|
||
#[graphql(description = "This description is visible only in GraphQL schema.")]
|
||
fn empty() -> &'static str {
|
||
""
|
||
}
|
||
|
||
#[graphql(desc = "This description is visible only in GraphQL schema.")]
|
||
// ^^^^ shortcut for a `description` argument
|
||
fn field(
|
||
#[graphql(desc = "This description is visible only in GraphQL schema.")]
|
||
arg: bool,
|
||
) -> bool {
|
||
arg
|
||
}
|
||
|
||
/// This doc comment is visible in both Rust API docs and GraphQL schema
|
||
/// descriptions.
|
||
#[graphql(deprecated = "Just because.")]
|
||
fn deprecated_graphql() -> bool {
|
||
true
|
||
}
|
||
|
||
// Standard Rust's `#[deprecated]` attribute works too!
|
||
#[deprecated(note = "Reason is optional, btw!")]
|
||
fn deprecated_standard() -> bool { // has no description in GraphQL schema
|
||
false
|
||
}
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>NOTE</strong>: Only <a href="https://spec.graphql.org/October2021#sec-Objects">GraphQL object</a>/<a href="https://spec.graphql.org/October2021#sec-Interfaces">interface</a> fields and <a href="https://spec.graphql.org/October2021#sec-Enums">GraphQL enum</a> values can be <a href="https://spec.graphql.org/October2021#sec--deprecated">deprecated</a>.</p>
|
||
</blockquote>
|
||
<h3 id="ignoring-1"><a class="header" href="#ignoring-1">Ignoring</a></h3>
|
||
<p>By default, all methods of an <a href="https://doc.rust-lang.org/reference/items/implementations.html#inherent-implementations"><code>impl</code> block</a> are exposed as <a href="https://spec.graphql.org/October2021#sec-Language.Fields">GraphQL fields</a>. If a method should not be exposed as a <a href="https://spec.graphql.org/October2021#sec-Language.Fields">GraphQL field</a>, it should be defined in a separate <a href="https://doc.rust-lang.org/reference/items/implementations.html#inherent-implementations"><code>impl</code> block</a> or marked with the <code>#[graphql(ignore)]</code> attribute:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">#![allow(dead_code)]
|
||
</span><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::graphql_object;
|
||
</span><span class="boring">
|
||
</span>struct Person {
|
||
name: String,
|
||
age: i32,
|
||
}
|
||
|
||
#[graphql_object]
|
||
impl Person {
|
||
fn name(&self) -> &str {
|
||
self.name.as_str()
|
||
}
|
||
|
||
fn age(&self) -> i32 {
|
||
self.age
|
||
}
|
||
|
||
#[graphql(ignore)]
|
||
pub fn hidden_from_graphql(&self) {
|
||
// whatever goes...
|
||
}
|
||
|
||
#[graphql(skip)]
|
||
// ^^^^ alternative naming, up to your preference
|
||
pub fn also_hidden_from_graphql(&self) {
|
||
// whatever goes...
|
||
}
|
||
}
|
||
|
||
impl Person {
|
||
pub fn not_even_considered_for_graphql(&self) {
|
||
// whatever goes...
|
||
}
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>TIP</strong>: See more available features in the API docs of the <a href="https://docs.rs/juniper/0.16.1/juniper/attr.graphql_object.html"><code>#[graphql_object]</code></a> attribute.</p>
|
||
</blockquote>
|
||
<div style="break-before: page; page-break-before: always;"></div><h1 id="context"><a class="header" href="#context">Context</a></h1>
|
||
<p><em>Context</em> is a feature in <a href="https://docs.rs/juniper">Juniper</a> that lets <a href="https://spec.graphql.org/October2021#sec-Language.Fields">field</a> resolvers access global data, most commonly database connections or authentication information.</p>
|
||
<p>Let's say that we have a simple <code>User</code>s database in a <code>HashMap</code>:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">#![allow(dead_code)]
|
||
</span><span class="boring">use std::collections::HashMap;
|
||
</span><span class="boring">
|
||
</span>struct Database {
|
||
users: HashMap<i32, User>,
|
||
}
|
||
|
||
struct User {
|
||
id: i32,
|
||
name: String,
|
||
friend_ids: Vec<i32>,
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<p>We would like to define a <code>friends</code> <a href="https://spec.graphql.org/October2021#sec-Language.Fields">field</a> on <code>User</code> that returns a list of <code>User</code> <a href="https://spec.graphql.org/October2021#sec-Objects">objects</a>. In order to write such a <a href="https://spec.graphql.org/October2021#sec-Language.Fields">field</a> we need to query a <code>Database</code>. To accomplish this we must first mark the <code>Database</code> as a valid context type and then assign it to the <code>User</code> <a href="https://spec.graphql.org/October2021#sec-Objects">object</a>. To gain access to the context in the <code>friends</code> <a href="https://spec.graphql.org/October2021#sec-Language.Fields">field</a>, we need to specify an argument with the same type as the specified context:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use std::collections::HashMap;
|
||
</span><span class="boring">use juniper::graphql_object;
|
||
</span><span class="boring">
|
||
</span>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>,
|
||
}
|
||
|
||
#[graphql_object]
|
||
#[graphql(context = Database)] // assign `Database` as the context type
|
||
impl User {
|
||
// Inject the `Database` context by specifying an argument with the
|
||
// context type:
|
||
// - the type must be a reference;
|
||
// - the name of the argument SHOULD be `context` (or `ctx`).
|
||
fn friends<'db>(&self, context: &'db Database) -> Vec<&'db User> {
|
||
// ^^^^^^^ or `ctx`, up to your preference
|
||
self.friend_ids.iter()
|
||
.map(|id| {
|
||
context.users.get(&id).expect("could not find `User` with ID")
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
fn friend<'db>(
|
||
&self,
|
||
id: i32,
|
||
// Alternatively, the context argument may be marked with an attribute,
|
||
// and thus, named arbitrary.
|
||
#[graphql(context)] db: &'db Database,
|
||
// ^^^^^^^ or `ctx`, up to your preference
|
||
) -> Option<&'db User> {
|
||
self.friend_ids.contains(&id).then(|| {
|
||
db.users.get(&id).expect("could not find `User` with ID")
|
||
})
|
||
}
|
||
|
||
fn name(&self) -> &str {
|
||
self.name.as_str()
|
||
}
|
||
|
||
fn id(&self) -> i32 {
|
||
self.id
|
||
}
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<h3 id="mutating-and-mutable-references"><a class="header" href="#mutating-and-mutable-references">Mutating and mutable references</a></h3>
|
||
<p>Context cannot be a mutable reference as <a href="https://spec.graphql.org/October2021#sec-Language.Fields">fields</a> may be resolved concurrently. If something in the context requires a mutable reference, the context type should leverage the <a href="https://doc.rust-lang.org/reference/interior-mutability.html#interior-mutability"><em>interior mutability</em> pattern</a> (e.g. use <code>RwLock</code>, <code>RefCell</code> or similar).</p>
|
||
<p>For example, when using async runtime with <a href="https://en.wikipedia.org/wiki/Work_stealing">work stealing</a> (like <a href="https://docs.rs/tokio"><code>tokio</code></a>), which obviously requires thread safety in addition, we will need to use a corresponding async version of <code>RwLock</code>:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">extern crate tokio;
|
||
</span><span class="boring">use std::collections::HashMap;
|
||
</span><span class="boring">use juniper::graphql_object;
|
||
</span>use tokio::sync::RwLock;
|
||
|
||
struct Database {
|
||
requested_count: HashMap<i32, i32>,
|
||
}
|
||
|
||
// Since we cannot directly implement `juniper::Context`
|
||
// for `RwLock`, we use the newtype idiom.
|
||
struct DatabaseContext(RwLock<Database>);
|
||
|
||
impl juniper::Context for DatabaseContext {}
|
||
|
||
struct User {
|
||
id: i32,
|
||
name: String
|
||
}
|
||
|
||
#[graphql_object]
|
||
#[graphql(context = DatabaseContext)]
|
||
impl User {
|
||
async fn times_requested<'db>(&self, ctx: &'db DatabaseContext) -> i32 {
|
||
// Acquire a mutable reference and `.await` if async `RwLock` is used,
|
||
// which is necessary if context consists of async operations like
|
||
// querying remote databases.
|
||
|
||
// Obtain base type.
|
||
let DatabaseContext(db) = ctx;
|
||
// If context is immutable use `.read()` on `RwLock` instead.
|
||
let mut db = db.write().await;
|
||
|
||
// Perform a mutable operation.
|
||
db.requested_count
|
||
.entry(self.id)
|
||
.and_modify(|e| *e += 1)
|
||
.or_insert(1)
|
||
.clone()
|
||
}
|
||
|
||
fn name(&self) -> &str {
|
||
self.name.as_str()
|
||
}
|
||
|
||
fn id(&self) -> i32 {
|
||
self.id
|
||
}
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>TIP</strong>: Replace <code>tokio::sync::RwLock</code> with <code>std::sync::RwLock</code> (or similar) if you don't intend to use async resolving.</p>
|
||
</blockquote>
|
||
<div style="break-before: page; page-break-before: always;"></div><h1 id="error-handling"><a class="header" href="#error-handling">Error handling</a></h1>
|
||
<p>Error handling in <a href="https://graphql.org">GraphQL</a> can be done in multiple ways. We will cover the two different error handling models mostly used:</p>
|
||
<ol>
|
||
<li><a href="types/objects/error/field.html">Implicit field results</a>.</li>
|
||
<li><a href="types/objects/error/schema.html">Explicit errors backend by GraphQL schema</a>.</li>
|
||
</ol>
|
||
<p>Choosing the right error handling method depends on the requirements of the application and the concrete error happening. Investigating both approaches is beneficial.</p>
|
||
<h2 id="comparison"><a class="header" href="#comparison">Comparison</a></h2>
|
||
<p>The <a href="types/objects/error/field.html">first approach</a> (where every error is a <a href="https://spec.graphql.org/October2021#sec-Errors.Field-errors">field error</a>) is easier to implement. However, clients won't know what errors may occur and instead will have to infer what happens from the <a href="https://spec.graphql.org/October2021/#sel-GAPHRPDCAACCyD57Z">error message</a>. This is brittle and could change over time due to either clients or server changing. Therefore, extensive integration testing between clients and server is required to maintain the implicit contract between the two.</p>
|
||
<p><a href="types/objects/error/schema.html">Encoding non-critical errors in a GraphQL schema</a> makes the contract between clients and the server explicit. This allows clients to understand and handle these errors correctly and the server to know when changes are potentially breaking clients. However, encoding this error information into a <a href="https://graphql.org/learn/schema">GraphQL schema</a> requires additional code and up-front definition of non-critical errors.</p>
|
||
<div style="break-before: page; page-break-before: always;"></div><h1 id="field-errors"><a class="header" href="#field-errors">Field errors</a></h1>
|
||
<p><a href="https://www.rust-lang.org">Rust</a> provides <a href="https://doc.rust-lang.org/book/ch09-00-error-handling.html">two ways of dealing with errors</a>:</p>
|
||
<ul>
|
||
<li><a href="https://doc.rust-lang.org/stable/std/result/enum.Result.html"><code>Result<T, E></code></a> for recoverable errors;</li>
|
||
<li><a href="https://doc.rust-lang.org/stable/std/macro.panic.html"><code>panic!</code></a> for unrecoverable errors.</li>
|
||
</ul>
|
||
<p><a href="https://docs.rs/juniper">Juniper</a> does not do anything about panicking, it naturally bubbles up to the surrounding code/framework and can be dealt with there.</p>
|
||
<p>For recoverable errors, <a href="https://docs.rs/juniper">Juniper</a> works well with the <a href="https://doc.rust-lang.org/stable/std/result/enum.Result.html">built-in <code>Result</code> type</a>. You can use the <a href="https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#a-shortcut-for-propagating-errors-the--operator"><code>?</code> operator</a> and things will work as you expect them to:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use std::{fs::File, io::Read, path::PathBuf, str};
|
||
</span><span class="boring">use juniper::{graphql_object, FieldResult};
|
||
</span><span class="boring">
|
||
</span>struct Example {
|
||
filename: PathBuf,
|
||
}
|
||
|
||
#[graphql_object]
|
||
impl Example {
|
||
fn contents(&self) -> 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];
|
||
|
||
Ok(Some(str::from_utf8(&invalid)?.to_string()))
|
||
}
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<p><a href="https://docs.rs/juniper/0.16.1/juniper/executor/type.FieldResult.html"><code>FieldResult<T></code></a> is an alias for <a href="https://docs.rs/juniper/0.16.1/juniper/executor/struct.FieldError.html"><code>Result<T, FieldError></code></a>, which is the <a href="https://spec.graphql.org/October2021#sec-Errors.Field-errors">error type</a> all fallible <a href="https://spec.graphql.org/October2021#sec-Language.Fields">fields</a> must return. By using the <a href="https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#a-shortcut-for-propagating-errors-the--operator"><code>?</code> operator</a>, any type that implements the <a href="https://doc.rust-lang.org/stable/std/fmt/trait.Display.html"><code>Display</code> trait</a> (which most of the error types out there do) can be automatically converted into a <a href="https://docs.rs/juniper/0.16.1/juniper/executor/struct.FieldError.html"><code>FieldError</code></a>.</p>
|
||
<blockquote>
|
||
<p><strong>TIP</strong>: If a custom conversion into a <a href="https://docs.rs/juniper/0.16.1/juniper/executor/struct.FieldError.html"><code>FieldError</code></a> is needed (to <a href="https://spec.graphql.org/October2021#sel-GAPHRPZCAACCC_7Q">fill up <code>extensions</code></a>, for example), the <a href="https://docs.rs/juniper/0.16.1/juniper/executor/trait.IntoFieldError.html"><code>IntoFieldError</code> trait</a> should be implemented.</p>
|
||
</blockquote>
|
||
<blockquote>
|
||
<p><strong>NOTE</strong>: <a href="https://docs.rs/juniper/0.16.1/juniper/executor/struct.FieldError.html"><code>FieldError</code></a>s are <a href="https://spec.graphql.org/October2021#sec-Errors.Field-errors">GraphQL field errors</a> and are <a href="https://spec.graphql.org/October2021#sec-Introspection">not visible</a> in a <a href="https://graphql.org/learn/schema">GraphQL schema</a> in any way.</p>
|
||
</blockquote>
|
||
<h2 id="error-payloads-null-and-partial-errors"><a class="header" href="#error-payloads-null-and-partial-errors">Error payloads, <code>null</code>, and partial errors</a></h2>
|
||
<p><a href="https://docs.rs/juniper">Juniper</a>'s error behavior conforms to the <a href="https://spec.graphql.org/October2021#sec-Handling-Field-Errors">GraphQL specification</a>.</p>
|
||
<p>When a <a href="https://spec.graphql.org/October2021#sec-Language.Fields">field</a> returns an <a href="https://doc.rust-lang.org/book/ch09-00-error-handling.html">error</a>, the <a href="https://spec.graphql.org/October2021#sec-Language.Fields">field</a>'s result is replaced by <code>null</code>, and an additional <code>errors</code> object is created at the top level of the <a href="https://spec.graphql.org/October2021#sec-Response">response</a>, and the <a href="https://spec.graphql.org/October2021#sec-Execution">execution</a> is resumed.</p>
|
||
<p>Let's run the following query against the previous example:</p>
|
||
<pre><code class="language-graphql">{
|
||
example {
|
||
contents
|
||
foo
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>If <code>str::from_utf8</code> results in a <code>std::str::Utf8Error</code>, then the following will be returned:</p>
|
||
<pre><code class="language-json">{
|
||
"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>
|
||
<blockquote>
|
||
<p>Since <a href="https://spec.graphql.org/October2021#sec-Non-Null"><code>Non-Null</code> type</a> <a href="https://spec.graphql.org/October2021#sec-Execution">fields</a> cannot be <strong>null</strong>, <a href="https://spec.graphql.org/October2021#sec-Errors.Field-errors">field errors</a> are propagated to be handled by the parent <a href="https://spec.graphql.org/October2021#sec-Execution">field</a>. If the parent <a href="https://spec.graphql.org/October2021#sec-Execution">field</a> may be <strong>null</strong> then it resolves to <strong>null</strong>, otherwise if it is a <a href="https://spec.graphql.org/October2021#sec-Non-Null"><code>Non-Null</code> type</a>, the <a href="https://spec.graphql.org/October2021#sec-Errors.Field-errors">field error</a> is further propagated to its parent <a href="https://spec.graphql.org/October2021#sec-Execution">field</a>.</p>
|
||
</blockquote>
|
||
<p>For example, with the following query:</p>
|
||
<pre><code class="language-graphql">{
|
||
example {
|
||
contents
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>If the <code>File::open()</code> above results in a <code>std::io::ErrorKind::PermissionDenied</code>, the following ill be returned:</p>
|
||
<pre><code class="language-json">{
|
||
"data": null,
|
||
"errors": [{
|
||
"message": "Permission denied (os error 13)",
|
||
"locations": [{"line": 2, "column": 4}]
|
||
}]
|
||
}
|
||
</code></pre>
|
||
<h2 id="additional-information"><a class="header" href="#additional-information">Additional information</a></h2>
|
||
<p>Sometimes it's desirable to return additional structured error information to clients. This can be accomplished by implementing the <a href="https://docs.rs/juniper/0.16.1/juniper/executor/trait.IntoFieldError.html"><code>IntoFieldError</code> trait</a>:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">#[macro_use] extern crate juniper;
|
||
</span><span class="boring">use juniper::{graphql_object, FieldError, IntoFieldError, ScalarValue};
|
||
</span><span class="boring">
|
||
</span>enum CustomError {
|
||
WhateverNotSet,
|
||
}
|
||
|
||
impl<S: ScalarValue> IntoFieldError<S> for CustomError {
|
||
fn into_field_error(self) -> FieldError<S> {
|
||
match self {
|
||
Self::WhateverNotSet => FieldError::new(
|
||
"Whatever does not exist",
|
||
graphql_value!({
|
||
"type": "NO_WHATEVER"
|
||
}),
|
||
),
|
||
}
|
||
}
|
||
}
|
||
|
||
struct Example {
|
||
whatever: Option<bool>,
|
||
}
|
||
|
||
#[graphql_object]
|
||
impl Example {
|
||
fn whatever(&self) -> Result<bool, CustomError> {
|
||
if let Some(value) = self.whatever {
|
||
return Ok(value);
|
||
}
|
||
Err(CustomError::WhateverNotSet)
|
||
}
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<p>And the specified structured error information will be included into the <a href="https://spec.graphql.org/October2021#sel-GAPHRPZCAACCC_7Q">error's <code>extensions</code></a>:</p>
|
||
<pre><code class="language-json">{
|
||
"errors": [{
|
||
"message": "Whatever does not exist",
|
||
"locations": [{"line": 2, "column": 4}],
|
||
"extensions": {
|
||
"type": "NO_WHATEVER"
|
||
}
|
||
}]
|
||
}
|
||
</code></pre>
|
||
<blockquote>
|
||
<p><strong>NOTE</strong>: This pattern is particularly useful when it comes to instrumentation of returned <a href="https://spec.graphql.org/October2021#sec-Errors.Field-errors">field errors</a> with custom error codes or additional diagnostics (like stack traces or tracing IDs).</p>
|
||
</blockquote>
|
||
<div style="break-before: page; page-break-before: always;"></div><h1 id="schema-errors"><a class="header" href="#schema-errors">Schema errors</a></h1>
|
||
<p><a href="https://www.rust-lang.org">Rust</a>'s model of errors can be adapted for <a href="https://graphql.org">GraphQL</a>. <a href="https://www.rust-lang.org">Rust</a>'s panic is similar to a <a href="https://spec.graphql.org/October2021#sec-Errors.Field-errors">field error</a> - 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 a <a href="https://graphql.org/learn/schema">GraphQL schema</a>, so the client can intelligently handle them.</p>
|
||
<p>To implement this approach, all errors must be partitioned into two classes:</p>
|
||
<ul>
|
||
<li><em>Critical</em> errors that cannot be fixed by clients (e.g. a database error).</li>
|
||
<li><em>Recoverable</em> errors that can be fixed by clients (e.g. invalid input data).</li>
|
||
</ul>
|
||
<p>Critical errors are returned from resolvers as <a href="https://spec.graphql.org/October2021#sec-Errors.Field-errors">field errors</a> (from the <a href="types/objects/error/field.html">previous chapter</a>). Recoverable errors are part of a <a href="https://graphql.org/learn/schema">GraphQL schema</a> and can be handled gracefully by clients. Similar to <a href="https://www.rust-lang.org">Rust</a>, <a href="https://graphql.org">GraphQL</a> allows similar error models with <a href="https://spec.graphql.org/October2021#sec-Unions">unions</a> (see <a href="types/objects/error/../../unions.html">"Unions" chapter</a>).</p>
|
||
<h3 id="example-simple"><a class="header" href="#example-simple">Example: Simple</a></h3>
|
||
<p>In this example, basic input validation is implemented with <a href="https://spec.graphql.org/October2021#sec-Types">GraphQL types</a>. <a href="https://spec.graphql.org/October2021#sec-String">Strings</a> are used to identify the problematic <a href="https://spec.graphql.org/October2021#sec-Language.Fields">field</a> name. Errors for a particular <a href="https://spec.graphql.org/October2021#sec-Language.Fields">field</a> are also returned as a <a href="https://spec.graphql.org/October2021#sec-String">string</a>.</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::{graphql_object, GraphQLObject, GraphQLUnion};
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLObject)]
|
||
pub struct Item {
|
||
name: String,
|
||
quantity: i32,
|
||
}
|
||
|
||
#[derive(GraphQLObject)]
|
||
pub struct ValidationError {
|
||
field: String,
|
||
message: String,
|
||
}
|
||
|
||
#[derive(GraphQLObject)]
|
||
pub struct ValidationErrors {
|
||
errors: Vec<ValidationError>,
|
||
}
|
||
|
||
#[derive(GraphQLUnion)]
|
||
pub enum GraphQLResult {
|
||
Ok(Item),
|
||
Err(ValidationErrors),
|
||
}
|
||
|
||
pub struct Mutation;
|
||
|
||
#[graphql_object]
|
||
impl Mutation {
|
||
fn add_item(&self, name: String, quantity: i32) -> GraphQLResult {
|
||
let mut errors = Vec::new();
|
||
|
||
if !(10 <= name.len() && name.len() <= 100) {
|
||
errors.push(ValidationError {
|
||
field: "name".into(),
|
||
message: "between 10 and 100".into(),
|
||
});
|
||
}
|
||
|
||
if !(1 <= quantity && quantity <= 10) {
|
||
errors.push(ValidationError {
|
||
field: "quantity".into(),
|
||
message: "between 1 and 10".into(),
|
||
});
|
||
}
|
||
|
||
if errors.is_empty() {
|
||
GraphQLResult::Ok(Item { name, quantity })
|
||
} else {
|
||
GraphQLResult::Err(ValidationErrors { errors })
|
||
}
|
||
}
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<p>Each function may have a different return type and depending on the input parameters a new result type may be required. For example, adding a <code>User</code> would require a new result type containing the variant <code>Ok(User)</code>instead of <code>Ok(Item)</code>.</p>
|
||
<blockquote>
|
||
<p><strong>NOTE</strong>: In this example the returned <a href="https://spec.graphql.org/October2021#sec-String">string</a> 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 its users.</p>
|
||
</blockquote>
|
||
<p>The client can send a mutation request and handle the resulting errors in the following manner:</p>
|
||
<pre><code class="language-graphql">{
|
||
mutation {
|
||
addItem(name: "", quantity: 0) {
|
||
... on Item {
|
||
name
|
||
}
|
||
... on ValidationErrors {
|
||
errors {
|
||
field
|
||
message
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<blockquote>
|
||
<p><strong>NOTE</strong>: 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>
|
||
</blockquote>
|
||
<h3 id="example-complex"><a class="header" href="#example-complex">Example: Complex</a></h3>
|
||
<p>Instead of using <a href="https://spec.graphql.org/October2021#sec-String">strings</a> to propagate errors, it is possible to use <a href="https://spec.graphql.org/October2021#sec-Types">GraphQL type system</a> to describe the errors more precisely.</p>
|
||
<p>For each fallible <a href="https://spec.graphql.org/October2021#sec-Language.Arguments">input argument</a> we create a <a href="https://spec.graphql.org/October2021#sec-Language.Fields">field</a> in a <a href="https://spec.graphql.org/October2021#sec-Objects">GraphQL object</a>. The <a href="https://spec.graphql.org/October2021#sec-Language.Fields">field</a> is set if the validation for that particular <a href="https://spec.graphql.org/October2021#sec-Language.Arguments">argument</a> fails.</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::{graphql_object, GraphQLObject, GraphQLUnion};
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLObject)]
|
||
pub struct Item {
|
||
name: String,
|
||
quantity: i32,
|
||
}
|
||
|
||
#[derive(GraphQLObject)]
|
||
pub struct ValidationError {
|
||
name: Option<String>,
|
||
quantity: Option<String>,
|
||
}
|
||
|
||
#[derive(GraphQLUnion)]
|
||
pub enum GraphQLResult {
|
||
Ok(Item),
|
||
Err(ValidationError),
|
||
}
|
||
|
||
pub struct Mutation;
|
||
|
||
#[graphql_object]
|
||
impl Mutation {
|
||
fn add_item(&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".into());
|
||
}
|
||
|
||
if !(1 <= quantity && quantity <= 10) {
|
||
error.quantity = Some("between 1 and 10".into());
|
||
}
|
||
|
||
if error.name.is_none() && error.quantity.is_none() {
|
||
GraphQLResult::Ok(Item { name, quantity })
|
||
} else {
|
||
GraphQLResult::Err(error)
|
||
}
|
||
}
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>NOTE</strong>: We 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 <a href="https://spec.graphql.org/October2021#sec-Language.Fields">fields</a> provided by the function.</p>
|
||
</blockquote>
|
||
<p>So, all the 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>
|
||
<pre><code class="language-graphql">{
|
||
mutation {
|
||
addItem {
|
||
... on Item {
|
||
name
|
||
}
|
||
... on ValidationErrorsItem {
|
||
name
|
||
quantity
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<h3 id="example-complex-with-critical-errors"><a class="header" href="#example-complex-with-critical-errors">Example: Complex with critical errors</a></h3>
|
||
<p>Our examples so far have only included non-critical errors. Providing errors inside a <a href="https://graphql.org/learn/schema">GraphQL schema</a> still allows us 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 a database to fail, the corresponding error is returned as a <a href="https://spec.graphql.org/October2021#sec-Errors.Field-errors">critical error</a>:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::{graphql_object, graphql_value, FieldError, GraphQLObject, GraphQLUnion, ScalarValue};
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLObject)]
|
||
pub struct Item {
|
||
name: String,
|
||
quantity: i32,
|
||
}
|
||
|
||
#[derive(GraphQLObject)]
|
||
pub struct ValidationErrorItem {
|
||
name: Option<String>,
|
||
quantity: Option<String>,
|
||
}
|
||
|
||
#[derive(GraphQLUnion)]
|
||
pub enum GraphQLResult {
|
||
Ok(Item),
|
||
Err(ValidationErrorItem),
|
||
}
|
||
|
||
pub enum ApiError {
|
||
Database,
|
||
}
|
||
|
||
impl<S: ScalarValue> juniper::IntoFieldError<S> for ApiError {
|
||
fn into_field_error(self) -> FieldError<S> {
|
||
match self {
|
||
Self::Database => FieldError::new(
|
||
"Internal database error",
|
||
graphql_value!({"type": "DATABASE"}),
|
||
),
|
||
}
|
||
}
|
||
}
|
||
|
||
pub struct Mutation;
|
||
|
||
#[graphql_object]
|
||
impl Mutation {
|
||
fn add_item(&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".into());
|
||
}
|
||
|
||
if !(1 <= quantity && quantity <= 10) {
|
||
error.quantity = Some("between 1 and 10".into());
|
||
}
|
||
|
||
if error.name.is_none() && error.quantity.is_none() {
|
||
Ok(GraphQLResult::Ok(Item { name, quantity }))
|
||
} else {
|
||
Ok(GraphQLResult::Err(error))
|
||
}
|
||
}
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<h3 id="example-shopify-api"><a class="header" href="#example-shopify-api">Example: Shopify API</a></h3>
|
||
<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>
|
||
<h3 id="example-non-struct-objects"><a class="header" href="#example-non-struct-objects">Example: Non-struct <a href="https://spec.graphql.org/October2021#sec-Objects">objects</a></a></h3>
|
||
<p>Up until now, we've only looked at mapping <a href="https://doc.rust-lang.org/reference/items/structs.html">structs</a> to <a href="https://spec.graphql.org/October2021#sec-Objects">GraphQL objects</a>. However, any <a href="https://www.rust-lang.org">Rust</a> type can be exposed a <a href="https://spec.graphql.org/October2021#sec-Objects">GraphQL object</a>.</p>
|
||
<p>Using <code>Result</code>-like <a href="https://spec.graphql.org/October2021#sec-Errors.Field-errors">enums</a> can be a useful way of reporting validation errors from a mutation:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::{graphql_object, GraphQLObject};
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLObject)]
|
||
struct User {
|
||
name: String,
|
||
}
|
||
|
||
#[derive(GraphQLObject)]
|
||
struct ValidationError {
|
||
field: String,
|
||
message: String,
|
||
}
|
||
|
||
enum SignUpResult {
|
||
Ok(User),
|
||
Error(Vec<ValidationError>),
|
||
}
|
||
|
||
#[graphql_object]
|
||
impl SignUpResult {
|
||
fn user(&self) -> Option<&User> {
|
||
match self {
|
||
Self::Ok(user) => Some(user),
|
||
Self::Error(_) => None,
|
||
}
|
||
}
|
||
|
||
fn error(&self) -> Option<&[ValidationError]> {
|
||
match self {
|
||
Self::Ok(_) => None,
|
||
Self::Error(errs) => Some(errs.as_slice())
|
||
}
|
||
}
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<p>Here, we use an <a href="https://doc.rust-lang.org/reference/items/enumerations.html">enum</a> to decide whether a client's input data is valid or not, and it could be used as the result of e.g. a <code>signUp</code> mutation:</p>
|
||
<pre><code class="language-graphql">{
|
||
mutation {
|
||
signUp(name: "wrong") {
|
||
user {
|
||
name
|
||
}
|
||
error {
|
||
field
|
||
message
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<div style="break-before: page; page-break-before: always;"></div><h1 id="generics"><a class="header" href="#generics">Generics</a></h1>
|
||
<p>Yet another point where <a href="https://graphql.org">GraphQL</a> and <a href="https://www.rust-lang.org">Rust</a> differs is in how generics work:</p>
|
||
<ul>
|
||
<li>In <a href="https://www.rust-lang.org">Rust</a>, almost any type could be generic - that is, take type parameters.</li>
|
||
<li>In <a href="https://graphql.org">GraphQL</a>, there are only two generic types: <a href="https://spec.graphql.org/October2021#sec-List">lists</a> and <a href="https://spec.graphql.org/October2021#sec-Non-Null">non-<code>null</code>ables</a>.</li>
|
||
</ul>
|
||
<p>This poses a restriction on what we can expose in <a href="https://graphql.org">GraphQL</a> from <a href="https://www.rust-lang.org">Rust</a>: no generic structs can be exposed - all type parameters must be bound. For example, we cannot expose <code>Result<T, E></code> as a <a href="https://spec.graphql.org/October2021#sec-Types">GraphQL type</a>, but we <em>can</em> expose <code>Result<User, String></code> as a <a href="https://spec.graphql.org/October2021#sec-Types">GraphQL type</a>.</p>
|
||
<p>Let's make a slightly more compact but generic implementation of <a href="types/objects/error/schema.html#example-non-struct-objects">the last schema error example</a>:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::{graphql_object, GraphQLObject};
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLObject)]
|
||
struct User {
|
||
name: String,
|
||
}
|
||
|
||
#[derive(GraphQLObject)]
|
||
struct ForumPost {
|
||
title: String,
|
||
}
|
||
|
||
#[derive(GraphQLObject)]
|
||
struct ValidationError {
|
||
field: String,
|
||
message: String,
|
||
}
|
||
|
||
struct MutationResult<T>(Result<T, Vec<ValidationError>>);
|
||
|
||
#[graphql_object]
|
||
#[graphql(name = "UserResult")]
|
||
impl MutationResult<User> {
|
||
fn user(&self) -> Option<&User> {
|
||
self.0.as_ref().ok()
|
||
}
|
||
|
||
fn error(&self) -> Option<&[ValidationError]> {
|
||
self.0.as_ref().err().map(Vec::as_slice)
|
||
}
|
||
}
|
||
|
||
#[graphql_object]
|
||
#[graphql(name = "ForumPostResult")]
|
||
impl MutationResult<ForumPost> {
|
||
fn forum_post(&self) -> Option<&ForumPost> {
|
||
self.0.as_ref().ok()
|
||
}
|
||
|
||
fn error(&self) -> Option<&[ValidationError]> {
|
||
self.0.as_ref().err().map(Vec::as_slice)
|
||
}
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<p>Here, we've made a wrapper around a <code>Result</code> and exposed some concrete instantiations of <code>Result<T, E></code> as distinct <a href="https://spec.graphql.org/October2021#sec-Objects">GraphQL objects</a>.</p>
|
||
<blockquote>
|
||
<p><strong>NOTE</strong>: The reason we needed the wrapper is of <a href="https://www.rust-lang.org">Rust</a>'s <a href="https://doc.rust-lang.org/reference/items/implementations.html#trait-implementation-coherence">orphan rules</a> (both the <code>Result</code> and <a href="https://docs.rs/juniper">Juniper</a>'s internal traits are from third-party sources).</p>
|
||
</blockquote>
|
||
<blockquote>
|
||
<p><strong>NOTE</strong>: Because we're using generics, we also need to specify a <code>name</code> for our instantiated <a href="https://spec.graphql.org/October2021#sec-Types">GraphQL types</a>. Even if <a href="https://docs.rs/juniper">Juniper</a> <em>could</em> figure out the name, <code>MutationResult<User></code> wouldn't be a <a href="https://spec.graphql.org/October2021#sec-Names">valid GraphQL type name</a>. And, also, two different <a href="https://spec.graphql.org/October2021#sec-Types">GraphQL types</a> cannot have the same <code>MutationResult</code> name, inferred by default.</p>
|
||
</blockquote>
|
||
<div style="break-before: page; page-break-before: always;"></div><h1 id="interfaces"><a class="header" href="#interfaces">Interfaces</a></h1>
|
||
<blockquote>
|
||
<p><a href="https://spec.graphql.org/October2021#sec-Interfaces">GraphQL interfaces</a> represent a list of named <a href="https://spec.graphql.org/October2021#sec-Language.Fields">fields</a> and their <a href="https://spec.graphql.org/October2021#sec-Language.Arguments">arguments</a>. <a href="https://spec.graphql.org/October2021#sec-Objects">GraphQL objects</a> and <a href="https://spec.graphql.org/October2021#sec-Interfaces">interfaces</a> can then implement these <a href="https://spec.graphql.org/October2021#sec-Interfaces">interfaces</a> which requires that the implementing type will define all <a href="https://spec.graphql.org/October2021#sec-Language.Fields">fields</a> defined by those <a href="https://spec.graphql.org/October2021#sec-Interfaces">interfaces</a>.</p>
|
||
</blockquote>
|
||
<p><a href="https://spec.graphql.org/October2021#sec-Interfaces">GraphQL interfaces</a> map well to interfaces known from common object-oriented languages such as Java or C#, but <a href="https://www.rust-lang.org">Rust</a>, unfortunately, has no concept that maps perfectly to them. The nearest analogue of <a href="https://spec.graphql.org/October2021#sec-Interfaces">GraphQL interfaces</a> are <a href="https://doc.rust-lang.org/reference/items/traits.html#traits">Rust traits</a>, but the main difference is that in <a href="https://graphql.org">GraphQL</a> an <a href="https://spec.graphql.org/October2021#sec-Interfaces">interface type</a> serves both as an <em>abstraction</em> and a <em>boxed value (dispatchable to concrete implementers)</em>, while in <a href="https://www.rust-lang.org">Rust</a>, a <a href="https://doc.rust-lang.org/reference/items/traits.html#traits">trait</a> is an <em>abstraction only</em>, and <em>to represent such a boxed value a separate type is required</em>, like a <a href="https://doc.rust-lang.org/reference/types/trait-object.html#trait-objects">trait object</a> or an <a href="https://doc.rust-lang.org/reference/items/enumerations.html#enumerations">enum</a> consisting of implementer types, because <a href="https://doc.rust-lang.org/reference/items/traits.html#traits">Rust trait</a> doesn't represent a type itself, and so, can have no values.</p>
|
||
<p>Another notable difference is that <a href="https://spec.graphql.org/October2021#sec-Interfaces">GraphQL interfaces</a> are more like <a href="https://en.wikipedia.org/wiki/Structural_type_system">structurally-typed</a> contracts: they <em>only declare a list of <a href="https://spec.graphql.org/October2021#sec-Language.Fields">fields</a></em> a <a href="https://graphql.org">GraphQL</a> type should already have. <a href="https://doc.rust-lang.org/reference/items/traits.html#traits">Rust traits</a>, on the other hand, are <a href="https://en.wikipedia.org/wiki/Type_class">type classes</a>, which don't really care about existing methods, but, rather, <em>require to provide implementations for required methods</em> despite the fact whether the type already has such methods or not. This difference makes the <a href="https://doc.rust-lang.org/reference/items/implementations.html#trait-implementations">trait implementation</a> not a good fit for expressing a <a href="https://spec.graphql.org/October2021#sec-Interfaces">GraphQL interface</a> implementation, because <em>we don't really need to implement any <a href="https://spec.graphql.org/October2021#sec-Language.Fields">fields</a></em>, the <a href="https://graphql.org">GraphQL</a> type implementing a <a href="https://spec.graphql.org/October2021#sec-Interfaces">GraphQL interface</a> has those <a href="https://spec.graphql.org/October2021#sec-Language.Fields">fields</a> already. <em>We only need to check that <a href="https://spec.graphql.org/October2021#sec-Language.Fields">fields'</a> signatures match</em>.</p>
|
||
<p>That's why <a href="https://docs.rs/juniper">Juniper</a> takes the following approach to represent <a href="https://spec.graphql.org/October2021#sec-Interfaces">GraphQL interfaces</a>, which consists of two parts:</p>
|
||
<ol>
|
||
<li>Either a <a href="https://doc.rust-lang.org/reference/items/structs.html">struct</a>, or a <a href="https://doc.rust-lang.org/reference/items/traits.html#traits">trait</a> (in case <a href="https://spec.graphql.org/October2021#sec-Language.Fields">fields</a> have <a href="https://spec.graphql.org/October2021#sec-Language.Arguments">arguments</a>), which acts only as a blueprint describing the required list of <a href="https://spec.graphql.org/October2021#sec-Language.Fields">fields</a>, and is not used in runtime at all.</li>
|
||
<li>An auto-generated <a href="https://doc.rust-lang.org/reference/items/enumerations.html#enumerations">enum</a>, representing a dispatchable value-type for the <a href="https://spec.graphql.org/October2021#sec-Interfaces">GraphQL interfaces</a>, which may be referred and returned by other <a href="https://spec.graphql.org/October2021#sec-Language.Fields">fields</a>.</li>
|
||
</ol>
|
||
<p>This may be done by using either the <a href="https://docs.rs/juniper/0.16.1/juniper/attr.graphql_interface.html"><code>#[graphql_interface]</code> attribute</a> or the <a href="https://docs.rs/juniper/0.16.1/juniper/derive.GraphQLInterface.html"><code>#[derive(GraphQLInterface)]</code></a>:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::{graphql_interface, GraphQLInterface, GraphQLObject};
|
||
</span><span class="boring">
|
||
</span>// By default a `CharacterValue` enum is generated by macro to represent
|
||
// values of this GraphQL interface.
|
||
#[derive(GraphQLInterface)]
|
||
#[graphql(for = Human)] // enumerating all implementers is mandatory
|
||
struct Character {
|
||
id: String,
|
||
}
|
||
|
||
// Using a trait to describe the required fields is fine too.
|
||
#[graphql_interface]
|
||
#[graphql(enum = HasHomeEnum, for = Human)]
|
||
// ^^^^ the generated value-type enum can be renamed, if required
|
||
trait HasHome {
|
||
fn home_planet(&self) -> &str;
|
||
}
|
||
|
||
#[derive(GraphQLObject)]
|
||
#[graphql(impl = [CharacterValue, HasHomeEnum])]
|
||
// ^^^^^^^^^^^^^^ ^^^^^^^^^^^
|
||
// Notice the enum type names, neither the trait name nor the struct name
|
||
// is used to refer the GraphQL interface.
|
||
struct Human {
|
||
id: String, // also resolves `Character.id` field
|
||
home_planet: String, // also resolves `HasHome.homePlanet` field
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<h3 id="interfaces-implementing-other-interfaces"><a class="header" href="#interfaces-implementing-other-interfaces">Interfaces implementing other interfaces</a></h3>
|
||
<p><a href="https://graphql.org">GraphQL</a> allows implementing <a href="https://spec.graphql.org/October2021#sec-Interfaces">interfaces</a> on other <a href="https://spec.graphql.org/October2021#sec-Interfaces">interfaces</a> in addition to <a href="https://spec.graphql.org/October2021#sec-Objects">objects</a>:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::{graphql_object, GraphQLInterface, ID};
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLInterface)]
|
||
#[graphql(for = [HumanValue, Luke])]
|
||
struct Node {
|
||
id: ID,
|
||
}
|
||
|
||
#[derive(GraphQLInterface)]
|
||
#[graphql(impl = NodeValue, for = Luke)]
|
||
struct Human {
|
||
id: ID,
|
||
home_planet: String,
|
||
}
|
||
|
||
struct Luke {
|
||
id: ID,
|
||
}
|
||
|
||
#[graphql_object]
|
||
#[graphql(impl = [HumanValue, NodeValue])]
|
||
impl Luke {
|
||
fn id(&self) -> &ID {
|
||
&self.id
|
||
}
|
||
|
||
// As `String` and `&str` aren't distinguished by GraphQL spec and
|
||
// represent the same `!String` GraphQL scalar, we can use them
|
||
// interchangeably. The same is applied for `Cow<'a, str>`.
|
||
// ⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄
|
||
fn home_planet() -> &'static str {
|
||
"Tatooine"
|
||
}
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>NOTE</strong>: Every <a href="https://spec.graphql.org/October2021#sec-Interfaces">interface</a> has to specify all other <a href="https://spec.graphql.org/October2021#sec-Interfaces">interfaces</a>/<a href="https://spec.graphql.org/October2021#sec-Interfaces">objects</a> it implements or implemented for. Missing one of <code>for = </code> or <code>impl = </code> attribute arguments is a <strong>compile-time error</strong>.</p>
|
||
<pre><pre class="playground"><code class="language-rust compile_fail edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::{GraphQLInterface, GraphQLObject};
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLObject)]
|
||
pub struct ObjA {
|
||
id: String,
|
||
}
|
||
|
||
#[derive(GraphQLInterface)]
|
||
#[graphql(for = ObjA)]
|
||
// ^^^^^^^^^^ the evaluated program panicked at
|
||
// 'Failed to implement interface `Character` on `ObjA`: missing interface
|
||
// reference in implementer's `impl` attribute.'
|
||
struct Character {
|
||
id: String,
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
</blockquote>
|
||
<h3 id="subtyping-and-additional-nullable-arguments"><a class="header" href="#subtyping-and-additional-nullable-arguments">Subtyping and additional <code>null</code>able arguments</a></h3>
|
||
<p><a href="https://graphql.org">GraphQL</a> allows implementers (both <a href="https://spec.graphql.org/October2021#sec-Objects">objects</a> and other <a href="https://spec.graphql.org/October2021#sec-Interfaces">interfaces</a>) to return "subtypes" instead of an original value. Basically, this allows to impose additional bounds on the implementation.</p>
|
||
<p>Valid "subtypes" are:</p>
|
||
<ul>
|
||
<li><a href="https://spec.graphql.org/October2021#sec-Interfaces">interface</a> implementer instead of an <a href="https://spec.graphql.org/October2021#sec-Interfaces">interface</a> itself:
|
||
<ul>
|
||
<li><code>I implements T</code> in place of a <code>T</code>;</li>
|
||
<li><code>Vec<I implements T></code> in place of a <code>Vec<T></code>.</li>
|
||
</ul>
|
||
</li>
|
||
<li><a href="https://spec.graphql.org/October2021#sec-Non-Null">non-<code>null</code></a> value in place of a <code>null</code>able:
|
||
<ul>
|
||
<li><code>T</code> in place of a <code>Option<T></code>;</li>
|
||
<li><code>Vec<T></code> in place of a <code>Vec<Option<T>></code>.</li>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<p>These rules are recursively applied, so <code>Vec<Vec<I implements T>></code> is a valid "subtype" of a <code>Option<Vec<Option<Vec<Option<T>>>>></code>.</p>
|
||
<p>Also, <a href="https://graphql.org">GraphQL</a> allows implementers to add <code>null</code>able <a href="https://spec.graphql.org/October2021#sec-Language.Arguments">field arguments</a>, which aren't present on an original interface.</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::{graphql_interface, graphql_object, GraphQLInterface, ID};
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLInterface)]
|
||
#[graphql(for = [HumanValue, Luke])]
|
||
struct Node {
|
||
id: ID,
|
||
}
|
||
|
||
#[derive(GraphQLInterface)]
|
||
#[graphql(for = HumanConnectionValue)]
|
||
struct Connection {
|
||
nodes: Vec<NodeValue>,
|
||
}
|
||
|
||
#[derive(GraphQLInterface)]
|
||
#[graphql(impl = NodeValue, for = Luke)]
|
||
struct Human {
|
||
id: ID,
|
||
home_planet: String,
|
||
}
|
||
|
||
#[derive(GraphQLInterface)]
|
||
#[graphql(impl = ConnectionValue)]
|
||
struct HumanConnection {
|
||
nodes: Vec<HumanValue>,
|
||
// ^^^^^^^^^^ notice not `NodeValue`
|
||
// This can happen, because every `Human` is a `Node` too, so we just
|
||
// impose additional bounds, which still can be resolved with
|
||
// `... on Connection { nodes }` syntax.
|
||
}
|
||
|
||
struct Luke {
|
||
id: ID,
|
||
}
|
||
|
||
#[graphql_object]
|
||
#[graphql(impl = [HumanValue, NodeValue])]
|
||
impl Luke {
|
||
fn id(&self) -> &ID {
|
||
&self.id
|
||
}
|
||
|
||
fn home_planet(language: Option<String>) -> &'static str {
|
||
// ^^^^^^^^^^^^^^
|
||
// Notice additional `null`able field argument, which is missing on
|
||
// `Human`. Resolving `...on Human { homePlanet }` will provide `None`
|
||
// for this argument (default argument value).
|
||
match language.as_deref() {
|
||
None | Some("en") => "Tatooine",
|
||
Some("ko") => "타투인",
|
||
_ => unimplemented!(),
|
||
}
|
||
}
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>NOTE</strong>: Violating <a href="https://graphql.org">GraphQL</a> "subtyping" or additional <code>null</code>able <a href="https://spec.graphql.org/October2021#sec-Language.Arguments">argument</a> rules is a <strong>compile-time error</strong>.</p>
|
||
<pre><pre class="playground"><code class="language-rust compile_fail edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::{graphql_object, GraphQLInterface};
|
||
</span><span class="boring">
|
||
</span>pub struct ObjA {
|
||
id: String,
|
||
}
|
||
|
||
#[graphql_object]
|
||
#[graphql(impl = CharacterValue)]
|
||
impl ObjA {
|
||
fn id(&self, is_present: bool) -> &str {
|
||
// ^^ the evaluated program panicked at
|
||
// 'Failed to implement interface `Character` on `ObjA`: Field `id`: Argument
|
||
// `isPresent` of type `Boolean!` isn't present on the interface and so has
|
||
// to be nullable.'
|
||
is_present.then_some(&self.id).unwrap_or("missing")
|
||
}
|
||
}
|
||
|
||
#[derive(GraphQLInterface)]
|
||
#[graphql(for = ObjA)]
|
||
struct Character {
|
||
id: String,
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<pre><pre class="playground"><code class="language-rust compile_fail edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::{GraphQLInterface, GraphQLObject};
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLObject)]
|
||
#[graphql(impl = CharacterValue)]
|
||
pub struct ObjA {
|
||
id: Vec<String>,
|
||
// ^^ the evaluated program panicked at
|
||
// 'Failed to implement interface `Character` on `ObjA`: Field `id`:
|
||
// implementer is expected to return a subtype of interface's return
|
||
// object: `[String!]!` is not a subtype of `String!`.'
|
||
}
|
||
|
||
#[derive(GraphQLInterface)]
|
||
#[graphql(for = ObjA)]
|
||
struct Character {
|
||
id: String,
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
</blockquote>
|
||
<h3 id="default-arguments-1"><a class="header" href="#default-arguments-1">Default arguments</a></h3>
|
||
<p><a href="types/objects/complex_fields.html#default-arguments">Similarly to GraphQL object fields</a>, <a href="https://spec.graphql.org/October2021#sec-Language.Fields">GraphQL arguments</a> of <a href="https://spec.graphql.org/October2021#sec-Interfaces">interfaces</a> are able to have default values, though <a href="https://www.rust-lang.org">Rust</a> doesn't have such notion:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::graphql_interface;
|
||
</span><span class="boring">
|
||
</span>#[graphql_interface]
|
||
trait Person {
|
||
fn field1(
|
||
// Default value can be any valid Rust expression, including a function
|
||
// call, etc.
|
||
#[graphql(default = true)]
|
||
arg1: bool,
|
||
// If default expression is not specified, then the `Default::default()`
|
||
// value is used.
|
||
#[graphql(default)]
|
||
arg2: i32,
|
||
) -> String;
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<h3 id="renaming-2"><a class="header" href="#renaming-2">Renaming</a></h3>
|
||
<p>Just as with <a href="types/objects/index.html#renaming">defining GraphQL objects</a>, by default, <a href="https://spec.graphql.org/October2021#sec-Language.Fields">fields</a> are converted from <a href="https://www.rust-lang.org">Rust</a>'s standard <code>snake_case</code> naming convention into <a href="https://graphql.org">GraphQL</a>'s <code>camelCase</code> convention:</p>
|
||
<p>We can override the name by using the <code>#[graphql(name = "...")]</code> attribute:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::{graphql_interface, GraphQLInterface};
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLInterface)]
|
||
#[graphql(name = "CharacterInterface")]
|
||
struct Character { // exposed as `CharacterInterface` in GraphQL schema
|
||
#[graphql(name = "myCustomFieldName")]
|
||
renamed_field: bool, // exposed as `myCustomFieldName` in GraphQL schema
|
||
}
|
||
|
||
#[graphql_interface]
|
||
#[graphql(name = "PersonInterface")]
|
||
trait Person { // exposed as `PersonInterface` in GraphQL schema
|
||
#[graphql(name = "myCustomFieldName")]
|
||
fn renamed_field( // exposed as `myCustomFieldName` in GraphQL schema
|
||
#[graphql(name = "myArgument")]
|
||
renamed_argument: bool, // exposed as `myArgument` in GraphQL schema
|
||
) -> bool;
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<p>Or provide a different renaming policy for all the defined <a href="https://spec.graphql.org/October2021#sec-Language.Fields">fields</a>:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::graphql_interface;
|
||
</span><span class="boring">
|
||
</span>#[graphql_interface]
|
||
#[graphql(rename_all = "none")] // disables any renaming
|
||
trait Person {
|
||
fn renamed_field( // exposed as `renamed_field` in GraphQL schema
|
||
renamed_argument: bool, // exposed as `renamed_argument` in GraphQL schema
|
||
) -> bool;
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>TIP</strong>: Supported policies are: <code>SCREAMING_SNAKE_CASE</code>, <code>camelCase</code> and <code>none</code> (disables any renaming).</p>
|
||
</blockquote>
|
||
<h3 id="documentation-and-deprecation-1"><a class="header" href="#documentation-and-deprecation-1">Documentation and deprecation</a></h3>
|
||
<p>Similarly, <a href="https://spec.graphql.org/October2021#sec-Language.Fields">GraphQL fields</a> of <a href="https://spec.graphql.org/October2021#sec-Interfaces">interfaces</a> may also be <a href="https://spec.graphql.org/October2021#sec-Descriptions">documented</a> and <a href="https://spec.graphql.org/October2021#sec--deprecated">deprecated</a> via <code>#[graphql(description = "...")]</code> and <code>#[graphql(deprecated = "...")]</code>/<a href="https://doc.rust-lang.org/reference/attributes/diagnostics.html#the-deprecated-attribute"><code>#[deprecated]</code></a> attributes:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::graphql_interface;
|
||
</span><span class="boring">
|
||
</span>/// This doc comment is visible only in Rust API docs.
|
||
#[graphql_interface]
|
||
#[graphql(description = "This description overwrites the one from doc comment.")]
|
||
trait Person {
|
||
/// This doc comment is visible only in Rust API docs.
|
||
#[graphql(description = "This description is visible only in GraphQL schema.")]
|
||
fn empty() -> &'static str;
|
||
|
||
#[graphql(desc = "This description is visible only in GraphQL schema.")]
|
||
// ^^^^ shortcut for a `description` argument
|
||
fn field(
|
||
#[graphql(desc = "This description is visible only in GraphQL schema.")]
|
||
arg: bool,
|
||
) -> bool;
|
||
|
||
/// This doc comment is visible in both Rust API docs and GraphQL schema
|
||
/// descriptions.
|
||
#[graphql(deprecated = "Just because.")]
|
||
fn deprecated_graphql() -> bool;
|
||
|
||
// Standard Rust's `#[deprecated]` attribute works too!
|
||
#[deprecated(note = "Reason is optional, btw!")]
|
||
fn deprecated_standard() -> bool; // has no description in GraphQL schema
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>NOTE</strong>: Only <a href="https://spec.graphql.org/October2021#sec-Interfaces">GraphQL interface</a>/<a href="https://spec.graphql.org/October2021#sec-Objects">object</a> fields and <a href="https://spec.graphql.org/October2021#sec-Enums">GraphQL enum</a> values can be <a href="https://spec.graphql.org/October2021#sec--deprecated">deprecated</a>.</p>
|
||
</blockquote>
|
||
<h3 id="ignoring-2"><a class="header" href="#ignoring-2">Ignoring</a></h3>
|
||
<p>By default, all <a href="https://doc.rust-lang.org/reference/items/structs.html">struct</a> fields or <a href="https://doc.rust-lang.org/reference/items/traits.html#traits">trait</a> methods are considered as <a href="https://spec.graphql.org/October2021#sec-Language.Fields">GraphQL fields</a>. If a helper method is needed, or it should be ignored for some reason, then it should be marked with the <code>#[graphql(ignore)]</code> attribute:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">#![allow(dead_code)]
|
||
</span><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use std::marker::PhantomPinned;
|
||
</span><span class="boring">use juniper::{graphql_interface, GraphQLInterface};
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLInterface)]
|
||
struct Character {
|
||
id: i32,
|
||
#[graphql(ignore)]
|
||
_pin: PhantomPinned,
|
||
}
|
||
|
||
#[graphql_interface]
|
||
trait Person {
|
||
fn name(&self) -> &str;
|
||
|
||
fn age(&self) -> i32;
|
||
|
||
#[graphql(ignore)]
|
||
fn hidden_from_graphql(&self) {
|
||
// Ignored methods are allowed to have a default implementation!
|
||
}
|
||
|
||
#[graphql(skip)]
|
||
// ^^^^ alternative naming, up to your preference
|
||
fn also_hidden_from_graphql(&self);
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>TIP</strong>: See more available features in the API docs of the <a href="https://docs.rs/juniper/0.16.1/juniper/attr.graphql_interface.html"><code>#[graphql_interface]</code></a> attribute.</p>
|
||
</blockquote>
|
||
<div style="break-before: page; page-break-before: always;"></div><h1 id="unions"><a class="header" href="#unions">Unions</a></h1>
|
||
<blockquote>
|
||
<p><a href="https://spec.graphql.org/October2021#sec-Unions">GraphQL unions</a> represent an object that could be one of a list of <a href="https://spec.graphql.org/October2021#sec-Objects">GraphQL object</a> types, but provides for no guaranteed fields between those types. They also differ from <a href="https://spec.graphql.org/October2021#sec-Interfaces">interfaces</a> in that <a href="https://spec.graphql.org/October2021#sec-Objects">object</a> types declare what <a href="https://spec.graphql.org/October2021#sec-Interfaces">interfaces</a> they implement, but are not aware of what <a href="https://spec.graphql.org/October2021#sec-Unions">unions</a> contain them.</p>
|
||
</blockquote>
|
||
<p>From the server's point of view, <a href="https://spec.graphql.org/October2021#sec-Unions">GraphQL unions</a> are somewhat similar to <a href="https://spec.graphql.org/October2021#sec-Interfaces">interfaces</a>: the main difference is that they don't contain fields on their own, and so, we only need to represent a value, <em>dispatchable</em> into concrete <a href="https://spec.graphql.org/October2021#sec-Objects">objects</a>.</p>
|
||
<p>Obviously, the most straightforward approach to express <a href="https://spec.graphql.org/October2021#sec-Unions">GraphQL unions</a> in <a href="https://www.rust-lang.org">Rust</a> is to use <a href="https://doc.rust-lang.org/reference/items/enumerations.html#enumerations">enums</a>. In <a href="https://docs.rs/juniper">Juniper</a> this may be done by using <a href="https://docs.rs/juniper/0.16.1/juniper/derive.GraphQLUnion.html"><code>#[derive(GraphQLInterface)]</code></a> attribute on them:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate derive_more;
|
||
</span><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use derive_more::From;
|
||
</span><span class="boring">use juniper::{GraphQLObject, GraphQLUnion};
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLObject)]
|
||
struct Human {
|
||
id: String,
|
||
home_planet: String,
|
||
}
|
||
|
||
#[derive(GraphQLObject)]
|
||
struct Droid {
|
||
id: String,
|
||
primary_function: String,
|
||
}
|
||
|
||
#[derive(From, GraphQLUnion)]
|
||
// ^^^^ only for convenience, and may be omitted
|
||
enum Character {
|
||
Human(Human),
|
||
Droid(Droid),
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<h3 id="renaming-3"><a class="header" href="#renaming-3">Renaming</a></h3>
|
||
<p>Just as with <a href="types/objects/index.html#renaming">renaming GraphQL objects</a>, we can override the default <a href="https://spec.graphql.org/October2021#sec-Unions">union</a> name by using the <code>#[graphql(name = "...")]</code> attribute:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::{GraphQLObject, GraphQLUnion};
|
||
</span><span class="boring">
|
||
</span><span class="boring">#[derive(GraphQLObject)]
|
||
</span><span class="boring">struct Human {
|
||
</span><span class="boring"> id: String,
|
||
</span><span class="boring"> home_planet: String,
|
||
</span><span class="boring">}
|
||
</span><span class="boring">
|
||
</span><span class="boring">#[derive(GraphQLObject)]
|
||
</span><span class="boring">struct Droid {
|
||
</span><span class="boring"> id: String,
|
||
</span><span class="boring"> primary_function: String,
|
||
</span><span class="boring">}
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLUnion)]
|
||
#[graphql(name = "CharacterUnion")]
|
||
enum Character { // exposed as `CharacterUnion` in GraphQL schema
|
||
Human(Human),
|
||
Droid(Droid),
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>NOTE</strong>: Unlike <a href="https://doc.rust-lang.org/reference/items/enumerations.html#enumerations">Rust enum variants</a>, <a href="https://spec.graphql.org/October2021#sec-Unions">GraphQL union members</a> don't have any special names aside from the ones provided by <a href="https://spec.graphql.org/October2021#sec-Objects">objects</a> themselves, and so, obviously, <strong>cannot be renamed</strong>.</p>
|
||
</blockquote>
|
||
<h3 id="documentation-1"><a class="header" href="#documentation-1">Documentation</a></h3>
|
||
<p>Similarly to <a href="types/objects/index.html#documentation">documenting GraphQL objects</a>, we can <a href="https://spec.graphql.org/October2021#sec-Descriptions">document</a> a <a href="https://spec.graphql.org/October2021#sec-Unions">GraphQL union</a> via <code>#[graphql(description = "...")]</code> attribute or <a href="https://doc.rust-lang.org/reference/comments.html#doc-comments">Rust doc comments</a>:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::{GraphQLObject, GraphQLUnion};
|
||
</span><span class="boring">
|
||
</span><span class="boring">#[derive(GraphQLObject)]
|
||
</span><span class="boring">struct Human {
|
||
</span><span class="boring"> id: String,
|
||
</span><span class="boring"> home_planet: String,
|
||
</span><span class="boring">}
|
||
</span><span class="boring">
|
||
</span><span class="boring">#[derive(GraphQLObject)]
|
||
</span><span class="boring">struct Droid {
|
||
</span><span class="boring"> id: String,
|
||
</span><span class="boring"> primary_function: String,
|
||
</span><span class="boring">}
|
||
</span><span class="boring">
|
||
</span>/// This doc comment is visible in both Rust API docs and GraphQL schema
|
||
/// descriptions.
|
||
#[derive(GraphQLUnion)]
|
||
enum Character {
|
||
/// This doc comment is visible only in Rust API docs.
|
||
Human(Human),
|
||
/// This doc comment is visible only in Rust API docs.
|
||
Droid(Droid),
|
||
}
|
||
|
||
/// This doc comment is visible only in Rust API docs.
|
||
#[derive(GraphQLUnion)]
|
||
#[graphql(description = "This description overwrites the one from doc comment.")]
|
||
// ^^^^^^^^^^^ or `desc` shortcut, up to your preference
|
||
enum Person {
|
||
/// This doc comment is visible only in Rust API docs.
|
||
Human(Human),
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>NOTE</strong>: Unlike <a href="https://doc.rust-lang.org/reference/items/enumerations.html#enumerations">Rust enum variants</a>, <a href="https://spec.graphql.org/October2021#sec-Unions">GraphQL union members</a> don't have any special constructors aside from the provided <a href="https://spec.graphql.org/October2021#sec-Objects">objects</a> directly, and so, <strong>cannot be <a href="https://spec.graphql.org/October2021#sec-Descriptions">documented</a></strong>, but rather reuse <a href="https://spec.graphql.org/October2021#sec-Descriptions">object descriptions</a> "as is".</p>
|
||
</blockquote>
|
||
<h3 id="ignoring-3"><a class="header" href="#ignoring-3">Ignoring</a></h3>
|
||
<p>In some rare situations we may want to omit exposing an <a href="https://doc.rust-lang.org/reference/items/enumerations.html#enumerations">enum</a> variant in a <a href="https://graphql.org/learn/schema">GraphQL schema</a>. <a href="types/enums.html#ignoring">Similarly to GraphQL enums</a>, we can just annotate the variant with the <code>#[graphql(ignore)]</code> attribute.</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>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate derive_more;
|
||
</span><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use std::marker::PhantomData;
|
||
</span><span class="boring">use derive_more::From;
|
||
</span><span class="boring">use juniper::{GraphQLObject, GraphQLUnion};
|
||
</span><span class="boring">
|
||
</span>#[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 `skip`, up to your preference
|
||
_State(PhantomData<S>),
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>WARNING</strong>: It's the <em>library user's responsibility</em> to ensure that ignored <a href="https://doc.rust-lang.org/reference/items/enumerations.html#enumerations">enum</a> variant is <strong>never</strong> returned from resolvers, otherwise resolving the <a href="https://graphql.org">GraphQL</a> query will <strong>panic in runtime</strong>.</p>
|
||
</blockquote>
|
||
<blockquote>
|
||
<p><strong>TIP</strong>: See more available features in the API docs of the <a href="https://docs.rs/juniper/0.16.1/juniper/derive.GraphQLUnion.html"><code>#[derive(GraphQLUnion)]</code></a> attribute.</p>
|
||
</blockquote>
|
||
<div style="break-before: page; page-break-before: always;"></div><h1 id="enums"><a class="header" href="#enums">Enums</a></h1>
|
||
<blockquote>
|
||
<p><a href="https://spec.graphql.org/October2021#sec-Enums">GraphQL enum</a> types, like <a href="https://spec.graphql.org/October2021#sec-Scalars">scalar</a> types, also represent leaf values in a GraphQL type system. However <a href="https://spec.graphql.org/October2021#sec-Enums">enum</a> types describe the set of possible values.</p>
|
||
<p><a href="https://spec.graphql.org/October2021#sec-Enums">Enums</a> are not references for a numeric value, but are unique values in their own right. They may serialize as a string: the name of the represented value.</p>
|
||
</blockquote>
|
||
<p>With <a href="https://docs.rs/juniper">Juniper</a> a <a href="https://spec.graphql.org/October2021#sec-Enums">GraphQL enum</a> may be defined by using the <a href="https://docs.rs/juniper/0.16.1/juniper/derive.GraphQLEnum.html"><code>#[derive(GraphQLEnum)]</code></a> attribute on a <a href="https://doc.rust-lang.org/reference/items/enumerations.html">Rust enum</a> as long as its variants do not have any fields:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::GraphQLEnum;
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLEnum)]
|
||
enum Episode {
|
||
NewHope,
|
||
Empire,
|
||
Jedi,
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<h3 id="renaming-4"><a class="header" href="#renaming-4">Renaming</a></h3>
|
||
<p>By default, <a href="https://doc.rust-lang.org/reference/items/enumerations.html">enum</a> variants are converted from <a href="https://www.rust-lang.org">Rust</a>'s standard <code>PascalCase</code> naming convention into <a href="https://graphql.org">GraphQL</a>'s <code>SCREAMING_SNAKE_CASE</code> convention:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::GraphQLEnum;
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLEnum)]
|
||
enum Episode {
|
||
NewHope, // exposed as `NEW_HOPE` in GraphQL schema
|
||
Empire, // exposed as `EMPIRE` in GraphQL schema
|
||
Jedi, // exposed as `JEDI` in GraphQL schema
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<p>We can override the name by using the <code>#[graphql(name = "...")]</code> attribute:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::GraphQLEnum;
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLEnum)]
|
||
#[graphql(name = "WrongEpisode")] // now exposed as `WrongEpisode` in GraphQL schema
|
||
enum Episode {
|
||
#[graphql(name = "LAST_HOPE")]
|
||
NewHope, // exposed as `LAST_HOPE` in GraphQL schema
|
||
Empire,
|
||
Jedi,
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<p>Or provide a different renaming policy for all the <a href="https://doc.rust-lang.org/reference/items/enumerations.html">enum</a> variants:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::GraphQLEnum;
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLEnum)]
|
||
#[graphql(rename_all = "none")] // disables any renaming
|
||
enum Episode {
|
||
NewHope, // exposed as `NewHope` in GraphQL schema
|
||
Empire, // exposed as `Empire` in GraphQL schema
|
||
Jedi, // exposed as `Jedi` in GraphQL schema
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>TIP</strong>: Supported policies are: <code>SCREAMING_SNAKE_CASE</code>, <code>camelCase</code> and <code>none</code> (disables any renaming).</p>
|
||
</blockquote>
|
||
<h3 id="documentation-and-deprecation-2"><a class="header" href="#documentation-and-deprecation-2">Documentation and deprecation</a></h3>
|
||
<p>Just like when <a href="types/objects/index.html#documentation">defining GraphQL objects</a>, the <a href="https://spec.graphql.org/October2021#sec-Enums">GraphQL enum</a> type and its values could be <a href="https://spec.graphql.org/October2021#sec-Descriptions">documented</a> and <a href="https://spec.graphql.org/October2021#sec--deprecated">deprecated</a> via <code>#[graphql(description = "...")]</code> and <code>#[graphql(deprecated = "...")]</code>/<a href="https://doc.rust-lang.org/reference/attributes/diagnostics.html#the-deprecated-attribute"><code>#[deprecated]</code></a> attributes:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::GraphQLEnum;
|
||
</span><span class="boring">
|
||
</span>/// This doc comment is visible only in Rust API docs.
|
||
#[derive(GraphQLEnum)]
|
||
#[graphql(description = "An episode of Star Wars")]
|
||
enum StarWarsEpisode {
|
||
/// This doc comment is visible only in Rust API docs.
|
||
#[graphql(description = "This description is visible only in GraphQL schema.")]
|
||
NewHope,
|
||
|
||
/// This doc comment is visible only in Rust API docs.
|
||
#[graphql(desc = "Arguably the best one in the trilogy.")]
|
||
// ^^^^ shortcut for a `description` argument
|
||
Empire,
|
||
|
||
/// This doc comment is visible in both Rust API docs and GraphQL schema
|
||
/// descriptions.
|
||
Jedi,
|
||
|
||
#[deprecated(note = "Only visible in Rust.")]
|
||
#[graphql(deprecated = "We don't really talk about this one.")]
|
||
// ^^^^^^^^^^ takes precedence over Rust's `#[deprecated]` attribute
|
||
ThePhantomMenace, // has no description in GraphQL schema
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>NOTE</strong>: Only <a href="https://spec.graphql.org/October2021#sec-Objects">GraphQL object</a>/<a href="https://spec.graphql.org/October2021#sec-Interfaces">interface</a> fields and <a href="https://spec.graphql.org/October2021#sec-Enums">GraphQL enum</a> values can be <a href="https://spec.graphql.org/October2021#sec--deprecated">deprecated</a>.</p>
|
||
</blockquote>
|
||
<h3 id="ignoring-4"><a class="header" href="#ignoring-4">Ignoring</a></h3>
|
||
<p>By default, all <a href="https://doc.rust-lang.org/reference/items/enumerations.html">enum</a> variants are included in the generated <a href="https://spec.graphql.org/October2021#sec-Enums">GraphQL enum</a> type as values. To prevent including a specific variant, annotate it with the <code>#[graphql(ignore)]</code> attribute:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">#![allow(dead_code)]
|
||
</span><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::GraphQLEnum;
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLEnum)]
|
||
enum Episode<T> {
|
||
NewHope,
|
||
Empire,
|
||
Jedi,
|
||
#[graphql(ignore)]
|
||
Legends(T), // cannot be queried from GraphQL
|
||
#[graphql(skip)]
|
||
// ^^^^ alternative naming, up to your preference
|
||
CloneWars(T), // cannot be queried from GraphQL
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>TIP</strong>: See more available features in the API docs of the <a href="https://docs.rs/juniper/0.16.1/juniper/derive.GraphQLEnum.html"><code>#[derive(GraphQLEnum)]</code></a> attribute.</p>
|
||
</blockquote>
|
||
<div style="break-before: page; page-break-before: always;"></div><h1 id="input-objects"><a class="header" href="#input-objects">Input objects</a></h1>
|
||
<blockquote>
|
||
<p><a href="https://spec.graphql.org/October2021#sec-Language.Fields">Fields</a> may accept <a href="https://spec.graphql.org/October2021#sec-Language.Arguments">arguments</a> to configure their behavior. These inputs are often <a href="https://spec.graphql.org/October2021#sec-Scalars">scalars</a> or <a href="https://spec.graphql.org/October2021#sec-Enums">enums</a>, but they sometimes need to represent more complex values.</p>
|
||
<p>A <a href="https://spec.graphql.org/October2021#sec-Input-Objects">GraphQL input object</a> defines a set of input fields; the input fields are either <a href="https://spec.graphql.org/October2021#sec-Scalars">scalars</a>, <a href="https://spec.graphql.org/October2021#sec-Enums">enums</a>, or other <a href="https://spec.graphql.org/October2021#sec-Input-Objects">input objects</a>. This allows <a href="https://spec.graphql.org/October2021#sec-Language.Arguments">arguments</a> to accept arbitrarily complex structs.</p>
|
||
</blockquote>
|
||
<p>In <a href="https://docs.rs/juniper">Juniper</a>, defining a <a href="https://spec.graphql.org/October2021#sec-Input-Objects">GraphQL input object</a> is quite straightforward and similar to how <a href="types/objects/index.html">trivial GraphQL objects are defined</a> - by using the <a href="https://docs.rs/juniper/0.16.1/juniper/derive.GraphQLInputObject.html"><code>#[derive(GraphQLInputObject)]</code> attribute</a> on a <a href="https://doc.rust-lang.org/reference/items/structs.html">Rust struct</a>:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">#![allow(unused_variables)]
|
||
</span><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::{graphql_object, GraphQLInputObject, GraphQLObject};
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLInputObject)]
|
||
struct Coordinate {
|
||
latitude: f64,
|
||
longitude: f64
|
||
}
|
||
|
||
struct Root;
|
||
<span class="boring">#[derive(GraphQLObject)] struct User { name: String }
|
||
</span>
|
||
#[graphql_object]
|
||
impl Root {
|
||
fn users_at_location(coordinate: Coordinate, radius: f64) -> Vec<User> {
|
||
// Send coordinate to database
|
||
// ...
|
||
<span class="boring"> unimplemented!()
|
||
</span> }
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<h3 id="renaming-5"><a class="header" href="#renaming-5">Renaming</a></h3>
|
||
<p>Just as with <a href="types/objects/index.html#renaming">defining GraphQL objects</a>, by default <a href="https://doc.rust-lang.org/reference/items/structs.html">struct</a> fields are converted from <a href="https://www.rust-lang.org">Rust</a>'s standard <code>snake_case</code> naming convention into <a href="https://graphql.org">GraphQL</a>'s <code>camelCase</code> convention:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::GraphQLInputObject;
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLInputObject)]
|
||
struct Person {
|
||
first_name: String, // exposed as `firstName` in GraphQL schema
|
||
last_name: String, // exposed as `lastName` in GraphQL schema
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<p>We can override the name by using the <code>#[graphql(name = "...")]</code> attribute:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::GraphQLInputObject;
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLInputObject)]
|
||
#[graphql(name = "WebPerson")] // now exposed as `WebPerson` in GraphQL schema
|
||
struct Person {
|
||
name: String,
|
||
age: i32,
|
||
#[graphql(name = "websiteURL")]
|
||
website_url: Option<String>, // now exposed as `websiteURL` in GraphQL schema
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<p>Or provide a different renaming policy for all the <a href="https://doc.rust-lang.org/reference/items/structs.html">struct</a> fields:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::GraphQLInputObject;
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLInputObject)]
|
||
#[graphql(rename_all = "none")] // disables any renaming
|
||
struct Person {
|
||
name: String,
|
||
age: i32,
|
||
website_url: Option<String>, // exposed as `website_url` in GraphQL schema
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>TIP</strong>: Supported policies are: <code>SCREAMING_SNAKE_CASE</code>, <code>camelCase</code> and <code>none</code> (disables any renaming).</p>
|
||
</blockquote>
|
||
<h3 id="documentation-2"><a class="header" href="#documentation-2">Documentation</a></h3>
|
||
<p>Similarly, <a href="https://spec.graphql.org/October2021#sec-Descriptions">GraphQL descriptions</a> may be provided by either using <a href="https://doc.rust-lang.org/reference/comments.html#doc-comments">Rust doc comments</a> or with the <code>#[graphql(description = "...")]</code> attribute:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::GraphQLInputObject;
|
||
</span><span class="boring">
|
||
</span>/// This doc comment is visible only in Rust API docs.
|
||
#[derive(GraphQLInputObject)]
|
||
#[graphql(description = "This description is visible only in GraphQL schema.")]
|
||
struct Person {
|
||
/// This doc comment is visible only in Rust API docs.
|
||
#[graphql(desc = "This description is visible only in GraphQL schema.")]
|
||
// ^^^^ shortcut for a `description` argument
|
||
name: String,
|
||
|
||
/// This doc comment is visible in both Rust API docs and GraphQL schema
|
||
/// descriptions.
|
||
age: i32,
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>NOTE</strong>: As of <a href="https://spec.graphql.org/October2021">October 2021 GraphQL specification</a>, <a href="https://spec.graphql.org/October2021#sec-Input-Objects">GraphQL input object</a>'s fields <strong>cannot be</strong> <a href="https://spec.graphql.org/October2021#sec--deprecated">deprecated</a>.</p>
|
||
</blockquote>
|
||
<h3 id="ignoring-5"><a class="header" href="#ignoring-5">Ignoring</a></h3>
|
||
<p>By default, all <a href="https://doc.rust-lang.org/reference/items/structs.html">struct</a> fields are included into the generated <a href="https://spec.graphql.org/October2021#sec-Input-Objects">GraphQL input object</a> type. To prevent inclusion of a specific field annotate it with the <code>#[graphql(ignore)]</code> attribute:</p>
|
||
<blockquote>
|
||
<p><strong>WARNING</strong>: Ignored fields must either implement <code>Default</code> or be annotated with the <code>#[graphql(default = <expression>)]</code> argument.</p>
|
||
</blockquote>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::GraphQLInputObject;
|
||
</span><span class="boring">
|
||
</span>enum System {
|
||
Cartesian,
|
||
}
|
||
|
||
#[derive(GraphQLInputObject)]
|
||
struct Point2D {
|
||
x: f64,
|
||
y: f64,
|
||
#[graphql(ignore, default = System::Cartesian)]
|
||
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
// This attribute is required, as we need to be able to construct
|
||
// a `Point2D` value from the `{ x: 0.0, y: 0.0 }` GraphQL input value,
|
||
// received from client-side.
|
||
system: System,
|
||
// `Default::default()` value is used, if no
|
||
// `#[graphql(default = <expression>)]` is specified.
|
||
#[graphql(skip)]
|
||
// ^^^^ alternative naming, up to your preference
|
||
shift: f64,
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>TIP</strong>: See more available features in the API docs of the <a href="https://docs.rs/juniper/0.16.1/juniper/derive.GraphQLInputObject.html"><code>#[derive(GraphQLInputObject)]</code></a> attribute.</p>
|
||
</blockquote>
|
||
<div style="break-before: page; page-break-before: always;"></div><h1 id="scalars"><a class="header" href="#scalars">Scalars</a></h1>
|
||
<p><a href="https://spec.graphql.org/October2021#sec-Scalars">GraphQL scalars</a> represent primitive leaf values in a GraphQL type system: numbers, strings, and booleans.</p>
|
||
<h2 id="built-in"><a class="header" href="#built-in">Built-in</a></h2>
|
||
<p><a href="https://docs.rs/juniper">Juniper</a> provides support for all the <a href="https://spec.graphql.org/October2021/#sec-Scalars.Built-in-Scalars">built-in scalars</a>.</p>
|
||
<div class="table-wrapper"><table><thead><tr><th><a href="https://www.rust-lang.org">Rust</a> types</th><th><a href="https://graphql.org">GraphQL</a> scalar</th></tr></thead><tbody>
|
||
<tr><td><code>bool</code></td><td><code>Boolean</code></td></tr>
|
||
<tr><td><code>i32</code></td><td><code>Int</code></td></tr>
|
||
<tr><td><code>f64</code></td><td><code>Float</code></td></tr>
|
||
<tr><td><code>String</code>, <code>&str</code></td><td><code>String</code></td></tr>
|
||
<tr><td><code>juniper::ID</code></td><td><a href="https://spec.graphql.org/October2021#sec-ID"><code>ID</code></a></td></tr>
|
||
</tbody></table>
|
||
</div>
|
||
<blockquote>
|
||
<p><strong>NOTE</strong>: <a href="https://spec.graphql.org/October2021#sec-ID"><code>ID</code></a> scalar is <a href="https://spec.graphql.org/October2021#sec-ID">defined in the GraphQL spec</a> as a type that is serialized as a string, but can be parsed from both a string and an integer.</p>
|
||
</blockquote>
|
||
<blockquote>
|
||
<p><strong>TIP</strong>: There is no built-in support for <code>i64</code>, <code>u64</code>, or other <a href="https://www.rust-lang.org">Rust</a> integer types, as the <a href="https://spec.graphql.org/October2021#sel-FAHXJDCAACKB1qb">GraphQL spec doesn't define any built-in scalars for them</a> by default. Instead, to be supported, they should be defined as <a href="types/scalars.html#custom">custom scalars</a> in a <a href="https://graphql.org/learn/schema">GraphQL schema</a>.</p>
|
||
</blockquote>
|
||
<h2 id="custom"><a class="header" href="#custom">Custom</a></h2>
|
||
<p>We can create <a href="https://spec.graphql.org/October2021#sec-Scalars.Custom-Scalars">custom scalars</a> for other primitive values, but they are still <a href="https://spec.graphql.org/October2021#sel-FAHXJDCAACKB1qb">limited in the data types for representation</a>, and only introduce additional semantic meaning. This, also, often requires coordination with the client library, intended to consume the API we're building.</p>
|
||
<p><a href="https://spec.graphql.org/October2021#sec-Scalars.Custom-Scalars">Custom scalars</a> can be defined in <a href="https://docs.rs/juniper">Juniper</a> by using either <a href="https://docs.rs/juniper/0.16.1/juniper/derive.GraphQLScalar.html"><code>#[derive(GraphQLScalar)]</code></a> or <a href="https://docs.rs/juniper/0.16.1/juniper/attr.graphql_scalar.html"><code>#[graphql_scalar]</code></a> attributes, which do work pretty much the same way (except, <a href="https://docs.rs/juniper/0.16.1/juniper/derive.GraphQLScalar.html"><code>#[derive(GraphQLScalar)]</code></a> cannot be used on <a href="https://doc.rust-lang.org/reference/items/type-aliases.html">type aliases</a>).</p>
|
||
<h3 id="transparent-delegation"><a class="header" href="#transparent-delegation">Transparent delegation</a></h3>
|
||
<p>Quite often, we want to create a <a href="https://spec.graphql.org/October2021#sec-Scalars.Custom-Scalars">custom GraphQL scalar</a> type by just wrapping an existing one, inheriting all its behavior. In <a href="https://www.rust-lang.org">Rust</a>, this is often called as <a href="https://rust-unofficial.github.io/patterns/patterns/behavioural/newtype.html">"newtype pattern"</a>. This may be achieved by providing a <code>#[graphql(transparent)]</code> attribute to the definition:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::{graphql_scalar, GraphQLScalar};
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLScalar)]
|
||
#[graphql(transparent)]
|
||
pub struct UserId(i32);
|
||
|
||
// Using `#[graphql_scalar]` attribute here makes no difference, and is fully
|
||
// interchangeable with `#[derive(GraphQLScalar)]`. It's only up to the
|
||
// personal preference - which one to use.
|
||
#[graphql_scalar]
|
||
#[graphql(transparent)]
|
||
pub struct MessageId {
|
||
value: i32,
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<p>That's it, now the <code>UserId</code> and <code>MessageId</code> <a href="https://spec.graphql.org/October2021#sec-Scalars">scalars</a> can be used in <a href="https://graphql.org/learn/schema">GraphQL schema</a>.</p>
|
||
<p>We may also customize the definition, to provide more information about our <a href="https://spec.graphql.org/October2021#sec-Scalars.Custom-Scalars">custom scalar</a> in <a href="https://graphql.org/learn/schema">GraphQL schema</a>:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::GraphQLScalar;
|
||
</span><span class="boring">
|
||
</span>/// You can use a Rust doc comment to specify a description in GraphQL schema.
|
||
#[derive(GraphQLScalar)]
|
||
#[graphql(
|
||
transparent,
|
||
// Overwrite the name of this type in the GraphQL schema.
|
||
name = "MyUserId",
|
||
// Specifying a type description via attribute takes precedence over the
|
||
// Rust doc comment, which allows to separate Rust API docs from GraphQL
|
||
// schema descriptions, if required.
|
||
description = "Actual description.",
|
||
// Optional specification URL.
|
||
specified_by_url = "https://tools.ietf.org/html/rfc4122",
|
||
)]
|
||
pub struct UserId(String);
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<h3 id="resolving"><a class="header" href="#resolving">Resolving</a></h3>
|
||
<p>In case we need to customize <a href="https://spec.graphql.org/October2021#sec-Value-Resolution">resolving</a> of a <a href="https://spec.graphql.org/October2021#sec-Scalars.Custom-Scalars">custom GraphQL scalar</a> value (change the way it gets executed), the <code>#[graphql(to_output_with = <fn path>)]</code> attribute is the way to do so:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::{GraphQLScalar, ScalarValue, Value};
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLScalar)]
|
||
#[graphql(to_output_with = to_output, transparent)]
|
||
struct Incremented(i32);
|
||
|
||
/// Increments [`Incremented`] before converting into a [`Value`].
|
||
fn to_output<S: ScalarValue>(v: &Incremented) -> Value<S> {
|
||
let inc = v.0 + 1;
|
||
Value::from(inc)
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<h3 id="input-value-parsing"><a class="header" href="#input-value-parsing">Input value parsing</a></h3>
|
||
<p>Customization of a <a href="https://spec.graphql.org/October2021#sec-Scalars.Custom-Scalars">custom GraphQL scalar</a> value parsing is possible via <code>#[graphql(from_input_with = <fn path>)]</code> attribute:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::{GraphQLScalar, InputValue, ScalarValue};
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLScalar)]
|
||
#[graphql(from_input_with = Self::from_input, transparent)]
|
||
struct UserId(String);
|
||
|
||
impl UserId {
|
||
/// Checks whether the [`InputValue`] is a [`String`] beginning with `id: `
|
||
/// and strips it.
|
||
fn from_input<S>(input: &InputValue<S>) -> Result<Self, String>
|
||
where
|
||
S: ScalarValue
|
||
{
|
||
input.as_string_value()
|
||
.ok_or_else(|| format!("Expected `String`, found: {input}"))
|
||
.and_then(|str| {
|
||
str.strip_prefix("id: ")
|
||
.ok_or_else(|| {
|
||
format!(
|
||
"Expected `UserId` to begin with `id: `, \
|
||
found: {input}",
|
||
)
|
||
})
|
||
})
|
||
.map(|id| Self(id.to_owned()))
|
||
}
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<h3 id="token-parsing"><a class="header" href="#token-parsing">Token parsing</a></h3>
|
||
<p>Customization of which tokens a <a href="https://spec.graphql.org/October2021#sec-Scalars">custom GraphQL scalar</a> type should be parsed from, is possible via <code>#[graphql(parse_token_with = <fn path>)]</code> or <code>#[graphql(parse_token(<types>)]</code> attributes:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::{
|
||
</span><span class="boring"> GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue,
|
||
</span><span class="boring"> ScalarValue, ScalarToken, Value,
|
||
</span><span class="boring">};
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLScalar)]
|
||
#[graphql(
|
||
to_output_with = to_output,
|
||
from_input_with = from_input,
|
||
parse_token_with = parse_token,
|
||
)]
|
||
// ^^^^^^^^^^^^^^^^ Can be replaced with `parse_token(String, i32)`, which
|
||
// tries to parse as `String` first, and then as `i32` if
|
||
// prior fails.
|
||
enum StringOrInt {
|
||
String(String),
|
||
Int(i32),
|
||
}
|
||
|
||
fn to_output<S: ScalarValue>(v: &StringOrInt) -> Value<S> {
|
||
match v {
|
||
StringOrInt::String(s) => Value::scalar(s.to_owned()),
|
||
StringOrInt::Int(i) => Value::scalar(*i),
|
||
}
|
||
}
|
||
|
||
fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<StringOrInt, String> {
|
||
v.as_string_value()
|
||
.map(|s| StringOrInt::String(s.into()))
|
||
.or_else(|| v.as_int_value().map(StringOrInt::Int))
|
||
.ok_or_else(|| format!("Expected `String` or `Int`, found: {v}"))
|
||
}
|
||
|
||
fn parse_token<S: ScalarValue>(value: ScalarToken<'_>) -> ParseScalarResult<S> {
|
||
<String as ParseScalarValue<S>>::from_str(value)
|
||
.or_else(|_| <i32 as ParseScalarValue<S>>::from_str(value))
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>NOTE</strong>: Once we provide all 3 custom functions, there is no sense to follow <a href="https://rust-unofficial.github.io/patterns/patterns/behavioural/newtype.html">newtype pattern</a> anymore, as nothing left to inherit.</p>
|
||
</blockquote>
|
||
<h3 id="full-behavior"><a class="header" href="#full-behavior">Full behavior</a></h3>
|
||
<p>Instead of providing all custom functions separately, it's possible to provide a module holding the appropriate <code>to_output()</code>, <code>from_input()</code> and <code>parse_token()</code> functions via <code>#[graphql(with = <module path>)]</code> attribute:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::{
|
||
</span><span class="boring"> GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue,
|
||
</span><span class="boring"> ScalarValue, ScalarToken, Value,
|
||
</span><span class="boring">};
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLScalar)]
|
||
#[graphql(with = string_or_int)]
|
||
enum StringOrInt {
|
||
String(String),
|
||
Int(i32),
|
||
}
|
||
|
||
mod string_or_int {
|
||
use super::*;
|
||
|
||
pub(super) fn to_output<S: ScalarValue>(v: &StringOrInt) -> Value<S> {
|
||
match v {
|
||
StringOrInt::String(s) => Value::scalar(s.to_owned()),
|
||
StringOrInt::Int(i) => Value::scalar(*i),
|
||
}
|
||
}
|
||
|
||
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<StringOrInt, String> {
|
||
v.as_string_value()
|
||
.map(|s| StringOrInt::String(s.into()))
|
||
.or_else(|| v.as_int_value().map(StringOrInt::Int))
|
||
.ok_or_else(|| format!("Expected `String` or `Int`, found: {v}"))
|
||
}
|
||
|
||
pub(super) fn parse_token<S: ScalarValue>(t: ScalarToken<'_>) -> ParseScalarResult<S> {
|
||
<String as ParseScalarValue<S>>::from_str(t)
|
||
.or_else(|_| <i32 as ParseScalarValue<S>>::from_str(t))
|
||
}
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<p>A regular <code>impl</code> block is also suitable for that:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::{
|
||
</span><span class="boring"> GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue,
|
||
</span><span class="boring"> ScalarValue, ScalarToken, Value,
|
||
</span><span class="boring">};
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLScalar)]
|
||
// #[graphql(with = Self)] <- default behaviour, so can be omitted
|
||
enum StringOrInt {
|
||
String(String),
|
||
Int(i32),
|
||
}
|
||
|
||
impl StringOrInt {
|
||
fn to_output<S: ScalarValue>(&self) -> Value<S> {
|
||
match self {
|
||
Self::String(s) => Value::scalar(s.to_owned()),
|
||
Self::Int(i) => Value::scalar(*i),
|
||
}
|
||
}
|
||
|
||
fn from_input<S>(v: &InputValue<S>) -> Result<Self, String>
|
||
where
|
||
S: ScalarValue
|
||
{
|
||
v.as_string_value()
|
||
.map(|s| Self::String(s.into()))
|
||
.or_else(|| v.as_int_value().map(Self::Int))
|
||
.ok_or_else(|| format!("Expected `String` or `Int`, found: {v}"))
|
||
}
|
||
|
||
fn parse_token<S>(value: ScalarToken<'_>) -> ParseScalarResult<S>
|
||
where
|
||
S: ScalarValue
|
||
{
|
||
<String as ParseScalarValue<S>>::from_str(value)
|
||
.or_else(|_| <i32 as ParseScalarValue<S>>::from_str(value))
|
||
}
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<p>At the same time, any custom function still may be specified separately, if required:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::{
|
||
</span><span class="boring"> GraphQLScalar, InputValue, ParseScalarResult, ScalarValue,
|
||
</span><span class="boring"> ScalarToken, Value
|
||
</span><span class="boring">};
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLScalar)]
|
||
#[graphql(
|
||
with = string_or_int,
|
||
parse_token(String, i32)
|
||
)]
|
||
enum StringOrInt {
|
||
String(String),
|
||
Int(i32),
|
||
}
|
||
|
||
mod string_or_int {
|
||
use super::*;
|
||
|
||
pub(super) fn to_output<S>(v: &StringOrInt) -> Value<S>
|
||
where
|
||
S: ScalarValue,
|
||
{
|
||
match v {
|
||
StringOrInt::String(s) => Value::scalar(s.to_owned()),
|
||
StringOrInt::Int(i) => Value::scalar(*i),
|
||
}
|
||
}
|
||
|
||
pub(super) fn from_input<S>(v: &InputValue<S>) -> Result<StringOrInt, String>
|
||
where
|
||
S: ScalarValue,
|
||
{
|
||
v.as_string_value()
|
||
.map(|s| StringOrInt::String(s.into()))
|
||
.or_else(|| v.as_int_value().map(StringOrInt::Int))
|
||
.ok_or_else(|| format!("Expected `String` or `Int`, found: {v}"))
|
||
}
|
||
|
||
// No need in `parse_token()` function.
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>TIP</strong>: See more available features in the API docs of the <a href="https://docs.rs/juniper/0.16.1/juniper/derive.GraphQLScalar.html"><code>#[derive(GraphQLScalar)]</code></a> and <a href="https://docs.rs/juniper/0.16.1/juniper/attr.graphql_scalar.html"><code>#[graphql_scalar]</code></a> attributes.</p>
|
||
</blockquote>
|
||
<h2 id="foreign"><a class="header" href="#foreign">Foreign</a></h2>
|
||
<p>For implementing <a href="https://spec.graphql.org/October2021#sec-Scalars.Custom-Scalars">custom scalars</a> on foreign types there is <a href="https://docs.rs/juniper/0.16.1/juniper/attr.graphql_scalar.html"><code>#[graphql_scalar]</code></a> attribute.</p>
|
||
<blockquote>
|
||
<p><strong>NOTE</strong>: To satisfy <a href="https://doc.rust-lang.org/reference/items/implementations.html#orphan-rules">orphan rules</a>, we should provide a local <a href="https://docs.rs/juniper/0.16.1/juniper/trait.ScalarValue.html"><code>ScalarValue</code></a> implementation.</p>
|
||
</blockquote>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">mod date {
|
||
</span><span class="boring"> pub struct Date;
|
||
</span><span class="boring"> impl std::str::FromStr for Date {
|
||
</span><span class="boring"> type Err = String;
|
||
</span><span class="boring">
|
||
</span><span class="boring"> fn from_str(_value: &str) -> Result<Self, Self::Err> {
|
||
</span><span class="boring"> unimplemented!()
|
||
</span><span class="boring"> }
|
||
</span><span class="boring"> }
|
||
</span><span class="boring">
|
||
</span><span class="boring"> impl std::fmt::Display for Date {
|
||
</span><span class="boring"> fn fmt(&self, _f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||
</span><span class="boring"> unimplemented!()
|
||
</span><span class="boring"> }
|
||
</span><span class="boring"> }
|
||
</span><span class="boring">}
|
||
</span><span class="boring">
|
||
</span><span class="boring">use juniper::DefaultScalarValue as CustomScalarValue;
|
||
</span>use juniper::{graphql_scalar, InputValue, ScalarValue, Value};
|
||
|
||
#[graphql_scalar(
|
||
with = date_scalar,
|
||
parse_token(String),
|
||
scalar = CustomScalarValue,
|
||
)]
|
||
// ^^^^^^^^^^^^^^^^^ local `ScalarValue` implementation
|
||
type Date = date::Date;
|
||
// ^^^^^^^^^^ type from another crate
|
||
|
||
mod date_scalar {
|
||
use super::*;
|
||
|
||
pub(super) fn to_output(v: &Date) -> Value<CustomScalarValue> {
|
||
Value::scalar(v.to_string())
|
||
}
|
||
|
||
pub(super) fn from_input(v: &InputValue<CustomScalarValue>) -> Result<Date, String> {
|
||
v.as_string_value()
|
||
.ok_or_else(|| format!("Expected `String`, found: {v}"))
|
||
.and_then(|s| s.parse().map_err(|e| format!("Failed to parse `Date`: {e}")))
|
||
}
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<h3 id="supported-out-of-the-box"><a class="header" href="#supported-out-of-the-box">Supported out-of-the-box</a></h3>
|
||
<p><a href="https://docs.rs/juniper">Juniper</a> provides out-of-the-box <a href="https://spec.graphql.org/October2021#sec-Scalars">GraphQL scalar</a> implementations for some very common <a href="https://www.rust-lang.org">Rust</a> crates. The types from these crates will be usable in your schemas automatically after enabling the correspondent self-titled <a href="https://doc.rust-lang.org/cargo/reference/features.html">Cargo feature</a>.</p>
|
||
<div class="table-wrapper"><table><thead><tr><th><a href="https://www.rust-lang.org">Rust</a> type</th><th><a href="https://graphql.org">GraphQL</a> scalar</th><th><a href="https://doc.rust-lang.org/cargo/reference/features.html">Cargo feature</a></th></tr></thead><tbody>
|
||
<tr><td><a href="https://docs.rs/bigdecimal/latest/bigdecimal/struct.BigDecimal.html"><code>bigdecimal::BigDecimal</code></a></td><td><code>BigDecimal</code></td><td><a href="https://docs.rs/bigdecimal"><code>bigdecimal</code></a></td></tr>
|
||
<tr><td><a href="https://docs.rs/bson/latest/bson/oid/struct.ObjectId.html"><code>bson::oid::ObjectId</code></a></td><td><a href="https://the-guild.dev/graphql/scalars/docs/scalars/object-id"><code>ObjectID</code></a></td><td><a href="https://docs.rs/bson"><code>bson</code></a></td></tr>
|
||
<tr><td><a href="https://docs.rs/bson/latest/bson/struct.DateTime.html"><code>bson::DateTime</code></a></td><td><a href="https://graphql-scalars.dev/docs/scalars/date-time"><code>DateTime</code></a></td><td><a href="https://docs.rs/bson"><code>bson</code></a></td></tr>
|
||
<tr><td><a href="https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDate.html"><code>chrono::NaiveDate</code></a></td><td><a href="https://graphql-scalars.dev/docs/scalars/local-date"><code>LocalDate</code></a></td><td><a href="https://docs.rs/chrono"><code>chrono</code></a></td></tr>
|
||
<tr><td><a href="https://docs.rs/chrono/latest/chrono/naive/struct.NaiveTime.html"><code>chrono::NaiveTime</code></a></td><td><a href="https://graphql-scalars.dev/docs/scalars/local-time"><code>LocalTime</code></a></td><td><a href="https://docs.rs/chrono"><code>chrono</code></a></td></tr>
|
||
<tr><td><a href="https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDateTime.html"><code>chrono::NaiveDateTime</code></a></td><td><a href="https://graphql-scalars.dev/docs/scalars/local-date-time"><code>LocalDateTime</code></a></td><td><a href="https://docs.rs/chrono"><code>chrono</code></a></td></tr>
|
||
<tr><td><a href="https://docs.rs/chrono/latest/chrono/struct.DateTime.html"><code>chrono::DateTime</code></a></td><td><a href="https://graphql-scalars.dev/docs/scalars/date-time"><code>DateTime</code></a></td><td><a href="https://docs.rs/chrono"><code>chrono</code></a></td></tr>
|
||
<tr><td><a href="https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html"><code>chrono_tz::Tz</code></a></td><td><a href="https://graphql-scalars.dev/docs/scalars/time-zone"><code>TimeZone</code></a></td><td><a href="https://docs.rs/chrono-tz"><code>chrono-tz</code></a></td></tr>
|
||
<tr><td><a href="https://docs.rs/rust_decimal/latest/rust_decimal/struct.Decimal.html"><code>rust_decimal::Decimal</code></a></td><td><code>Decimal</code></td><td><a href="https://docs.rs/rust_decimal"><code>rust_decimal</code></a></td></tr>
|
||
<tr><td><a href="https://docs.rs/jiff/latest/jiff/civil/struct.Date.html"><code>jiff::civil::Date</code></a></td><td><a href="https://graphql-scalars.dev/docs/scalars/local-date"><code>LocalDate</code></a></td><td><a href="https://docs.rs/jiff"><code>jiff</code></a></td></tr>
|
||
<tr><td><a href="https://docs.rs/jiff/latest/jiff/civil/struct.Time.html"><code>jiff::civil::Time</code></a></td><td><a href="https://graphql-scalars.dev/docs/scalars/local-time"><code>LocalTime</code></a></td><td><a href="https://docs.rs/jiff"><code>jiff</code></a></td></tr>
|
||
<tr><td><a href="https://docs.rs/jiff/latest/jiff/civil/struct.DateTime.html"><code>jiff::civil::DateTime</code></a></td><td><a href="https://graphql-scalars.dev/docs/scalars/local-date-time"><code>LocalDateTime</code></a></td><td><a href="https://docs.rs/jiff"><code>jiff</code></a></td></tr>
|
||
<tr><td><a href="https://docs.rs/jiff/latest/jiff/struct.Timestamp.html"><code>jiff::Timestamp</code></a></td><td><a href="https://graphql-scalars.dev/docs/scalars/date-time"><code>DateTime</code></a></td><td><a href="https://docs.rs/jiff"><code>jiff</code></a></td></tr>
|
||
<tr><td><a href="https://docs.rs/jiff/latest/jiff/struct.Zoned.html"><code>jiff::Zoned</code></a></td><td><code>ZonedDateTime</code></td><td><a href="https://docs.rs/jiff"><code>jiff</code></a></td></tr>
|
||
<tr><td><a href="https://docs.rs/jiff/latest/jiff/tz/struct.TimeZone.html"><code>jiff::tz::TimeZone</code></a></td><td><code>TimeZoneOrUtcOffset</code></td><td><a href="https://docs.rs/jiff"><code>jiff</code></a></td></tr>
|
||
<tr><td><a href="https://docs.rs/jiff/latest/jiff/tz/struct.TimeZone.html"><code>jiff::tz::TimeZone</code></a> via <a href="https://docs.rs/juniper/0.16.1/juniper/integrations/jiff/struct.TimeZone.html"><code>juniper::integrations::jiff::TimeZone</code></a></td><td><a href="https://graphql-scalars.dev/docs/scalars/time-zone"><code>TimeZone</code></a></td><td><a href="https://docs.rs/jiff"><code>jiff</code></a></td></tr>
|
||
<tr><td><a href="https://docs.rs/jiff/latest/jiff/tz/struct.Offset.html"><code>jiff::tz::Offset</code></a></td><td><a href="https://graphql-scalars.dev/docs/scalars/utc-offset"><code>UtcOffset</code></a></td><td><a href="https://docs.rs/jiff"><code>jiff</code></a></td></tr>
|
||
<tr><td><a href="https://docs.rs/jiff/latest/jiff/struct.Span.html"><code>jiff::Span</code></a></td><td><a href="https://graphql-scalars.dev/docs/scalars/duration"><code>Duration</code></a></td><td><a href="https://docs.rs/jiff"><code>jiff</code></a></td></tr>
|
||
<tr><td><a href="https://docs.rs/time/latest/time/struct.Date.html"><code>time::Date</code></a></td><td><a href="https://graphql-scalars.dev/docs/scalars/local-date"><code>LocalDate</code></a></td><td><a href="https://docs.rs/time"><code>time</code></a></td></tr>
|
||
<tr><td><a href="https://docs.rs/time/latest/time/struct.Time.html"><code>time::Time</code></a></td><td><a href="https://graphql-scalars.dev/docs/scalars/local-time"><code>LocalTime</code></a></td><td><a href="https://docs.rs/time"><code>time</code></a></td></tr>
|
||
<tr><td><a href="https://docs.rs/time/latest/time/struct.PrimitiveDateTime.html"><code>time::PrimitiveDateTime</code></a></td><td><a href="https://graphql-scalars.dev/docs/scalars/local-date-time"><code>LocalDateTime</code></a></td><td><a href="https://docs.rs/time"><code>time</code></a></td></tr>
|
||
<tr><td><a href="https://docs.rs/time/latest/time/struct.OffsetDateTime.html"><code>time::OffsetDateTime</code></a></td><td><a href="https://graphql-scalars.dev/docs/scalars/date-time"><code>DateTime</code></a></td><td><a href="https://docs.rs/time"><code>time</code></a></td></tr>
|
||
<tr><td><a href="https://docs.rs/time/latest/time/struct.UtcOffset.html"><code>time::UtcOffset</code></a></td><td><a href="https://graphql-scalars.dev/docs/scalars/utc-offset"><code>UtcOffset</code></a></td><td><a href="https://docs.rs/time"><code>time</code></a></td></tr>
|
||
<tr><td><a href="https://docs.rs/url/latest/url/struct.Url.html"><code>url::Url</code></a></td><td><a href="https://docs.rs/url"><code>URL</code></a></td><td><a href="https://docs.rs/url"><code>url</code></a></td></tr>
|
||
<tr><td><a href="https://docs.rs/uuid/latest/uuid/struct.Uuid.html"><code>uuid::Uuid</code></a></td><td><a href="https://docs.rs/uuid"><code>UUID</code></a></td><td><a href="https://docs.rs/uuid"><code>uuid</code></a></td></tr>
|
||
</tbody></table>
|
||
</div><div style="break-before: page; page-break-before: always;"></div><h1 id="schema-1"><a class="header" href="#schema-1">Schema</a></h1>
|
||
<p><strong><a href="https://docs.rs/juniper">Juniper</a> follows a <a href="https://www.apollographql.com/blog/backend/architecture/schema-first-vs-code-only-graphql#code-only">code-first</a> approach to define a <a href="https://graphql.org">GraphQL</a> schema.</strong></p>
|
||
<blockquote>
|
||
<p><strong>TIP</strong>: For a <a href="https://www.apollographql.com/blog/backend/architecture/schema-first-vs-code-only-graphql#schema-first">schema-first</a> approach, consider using a <a href="https://docs.rs/juniper-from-schema"><code>juniper-from-schema</code></a> crate for generating a <a href="https://docs.rs/juniper"><code>juniper</code></a>-based code from a <a href="https://graphql.org/learn/schema">schema</a> file.</p>
|
||
</blockquote>
|
||
<p><a href="https://spec.graphql.org/October2021#sec-Schema">GraphQL schema</a> consists of three <a href="https://spec.graphql.org/October2021#sec-Objects">object types</a>: a <a href="https://spec.graphql.org/October2021#sel-FAHTRFCAACChCtpG">query root</a>, a <a href="https://spec.graphql.org/October2021#sel-FAHTRHCAACCuE9yD">mutation root</a>, and a <a href="https://spec.graphql.org/October2021#sel-FAHTRJCAACC3EhsX">subscription root</a>.</p>
|
||
<blockquote>
|
||
<p>The <strong>query</strong> root operation type must be provided and must be an <a href="https://spec.graphql.org/October2021#sec-Objects">Object</a> type.</p>
|
||
<p>The <strong>mutation</strong> root operation type is optional; if it is not provided, the service does not support mutations. If it is provided, it must be an <a href="https://spec.graphql.org/October2021#sec-Objects">Object</a> type.</p>
|
||
<p>Similarly, the <strong>subscription</strong> root operation type is also optional; if it is not provided, the service does not support subscriptions. If it is provided, it must be an <a href="https://spec.graphql.org/October2021#sec-Objects">Object</a> type.</p>
|
||
<p>The <strong>query</strong>, <strong>mutation</strong>, and <strong>subscription</strong> root types must all be different types if provided.</p>
|
||
</blockquote>
|
||
<p>In <a href="https://docs.rs/juniper">Juniper</a>, the <a href="https://docs.rs/juniper/0.16.1/juniper/struct.RootNode.html"><code>RootNode</code></a> type represents a <a href="https://spec.graphql.org/October2021#sec-Schema">schema</a>. When the <a href="https://spec.graphql.org/October2021#sec-Schema">schema</a> is first created, <a href="https://docs.rs/juniper">Juniper</a> will traverse the entire object graph and register all types it can find. This means that if we <a href="schema/../types/objects/index.html">define a GraphQL object</a> somewhere but never use or reference it, it won't be exposed in a <a href="https://spec.graphql.org/October2021#sec-Schema">GraphQL schema</a>.</p>
|
||
<p>Both <a href="https://spec.graphql.org/October2021#sel-FAHTRFCAACChCtpG">query</a> and <a href="https://spec.graphql.org/October2021#sel-FAHTRHCAACCuE9yD">mutation</a> objects are regular <a href="https://spec.graphql.org/October2021#sec-Objects">GraphQL objects</a>, defined like <a href="schema/../types/objects/index.html">any other object in Juniper</a>. The <a href="https://spec.graphql.org/October2021#sel-FAHTRHCAACCuE9yD">mutation</a> and <a href="https://spec.graphql.org/October2021#sel-FAHTRJCAACC3EhsX">subscription</a> objects, however, are optional, since <a href="https://spec.graphql.org/October2021#sec-Schema">schemas</a> can be read-only and do not require <a href="https://spec.graphql.org/October2021#sel-FAHTRJCAACC3EhsX">subscriptions</a>.</p>
|
||
<blockquote>
|
||
<p><strong>TIP</strong>: If <a href="https://spec.graphql.org/October2021#sel-FAHTRHCAACCuE9yD">mutation</a>/<a href="https://spec.graphql.org/October2021#sel-FAHTRJCAACC3EhsX">subscription</a> functionality is not needed, consider using the predefined <a href="https://docs.rs/juniper/0.16.1/juniper/struct.EmptyMutation.html"><code>EmptyMutation</code></a>/<a href="https://docs.rs/juniper/0.16.1/juniper/struct.EmptySubscription.html"><code>EmptySubscription</code></a> types for stubbing them in a <a href="https://docs.rs/juniper/0.16.1/juniper/struct.RootNode.html"><code>RootNode</code></a>.</p>
|
||
</blockquote>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::{
|
||
</span><span class="boring"> graphql_object, EmptySubscription, FieldResult, GraphQLObject, RootNode,
|
||
</span><span class="boring">};
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLObject)]
|
||
struct User {
|
||
name: String,
|
||
}
|
||
|
||
struct Query;
|
||
|
||
#[graphql_object]
|
||
impl Query {
|
||
fn user_with_username(username: String) -> FieldResult<Option<User>> {
|
||
// Look up user in database...
|
||
<span class="boring"> unimplemented!()
|
||
</span> }
|
||
}
|
||
|
||
struct Mutation;
|
||
|
||
#[graphql_object]
|
||
impl Mutation {
|
||
fn sign_up_user(name: String, email: String) -> FieldResult<User> {
|
||
// Validate inputs and save user in database...
|
||
<span class="boring"> unimplemented!()
|
||
</span> }
|
||
}
|
||
|
||
type Schema = RootNode<'static, Query, Mutation, EmptySubscription>;
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>NOTE</strong>: It's considered a <a href="https://spec.graphql.org/October2021#sec-Root-Operation-Types.Default-Root-Operation-Type-Names">good practice</a> to name <a href="https://spec.graphql.org/October2021#sel-FAHTRFCAACChCtpG">query</a>, <a href="https://spec.graphql.org/October2021#sel-FAHTRHCAACCuE9yD">mutation</a>, and <a href="https://spec.graphql.org/October2021#sel-FAHTRJCAACC3EhsX">subscription</a> root types as <code>Query</code>, <code>Mutation</code>, and <code>Subscription</code> respectively.</p>
|
||
</blockquote>
|
||
<p>The usage of <a href="https://spec.graphql.org/October2021#sel-FAHTRJCAACC3EhsX">subscriptions</a> is a little different from the <a href="https://spec.graphql.org/October2021#sel-FAHTRHCAACCuE9yD">mutation</a> and <a href="https://spec.graphql.org/October2021#sel-FAHTRFCAACChCtpG">query</a> <a href="https://spec.graphql.org/October2021#sec-Objects">objects</a>, so they are discussed in the <a href="schema/subscriptions.html">separate chapter</a>.</p>
|
||
<h2 id="export"><a class="header" href="#export">Export</a></h2>
|
||
<p>Many tools in <a href="https://graphql.org">GraphQL</a> ecosystem require a <a href="https://graphql.org/learn/schema">schema</a> definition to operate on. With <a href="https://docs.rs/juniper">Juniper</a> we can export our <a href="https://spec.graphql.org/October2021#sec-Schema">GraphQL schema</a> defined in <a href="https://www.rust-lang.org">Rust</a> code either represented in the <a href="https://graphql.org/learn/schema#type-language">GraphQL schema language</a> or in <a href="https://www.json.org">JSON</a>.</p>
|
||
<h3 id="sdl-schema-definition-language"><a class="header" href="#sdl-schema-definition-language">SDL (schema definition language)</a></h3>
|
||
<p>To generate an <a href="https://graphql.org/learn/schema#type-language">SDL (schema definition language)</a> representation of a <a href="https://spec.graphql.org/October2021#sec-Schema">GraphQL schema</a> defined in <a href="https://www.rust-lang.org">Rust</a> code, the <a href="https://docs.rs/juniper/0.16.1/juniper/struct.RootNode.html#method.as_sdl"><code>as_sdl()</code> method</a> should be used for the direct extraction (requires enabling the <code>schema-language</code> <a href="https://docs.rs/juniper">Juniper</a> feature):</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::{
|
||
</span><span class="boring"> graphql_object, EmptyMutation, EmptySubscription, FieldResult, RootNode,
|
||
</span><span class="boring">};
|
||
</span><span class="boring">
|
||
</span>struct Query;
|
||
|
||
#[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 SDL schema.
|
||
let result = schema.as_sdl();
|
||
|
||
let expected = "\
|
||
schema {
|
||
query: Query
|
||
}
|
||
|
||
type Query {
|
||
hello: String!
|
||
}
|
||
";
|
||
<span class="boring"> #[cfg(not(target_os = "windows"))]
|
||
</span> assert_eq!(result, expected);
|
||
}</code></pre></pre>
|
||
<h3 id="json"><a class="header" href="#json">JSON</a></h3>
|
||
<p>To export a <a href="https://spec.graphql.org/October2021#sec-Schema">GraphQL schema</a> defined in <a href="https://www.rust-lang.org">Rust</a> code as <a href="https://www.json.org">JSON</a> (often referred to as <code>schema.json</code>), the specially crafted <a href="https://docs.rs/crate/juniper/latest/source/src/introspection/query.graphql">introspection query</a> should be issued. <a href="https://docs.rs/juniper">Juniper</a> provides a <a href="https://docs.rs/juniper/0.16.1/juniper/fn.introspect.html">convenience <code>introspect()</code> function</a> to <a href="schema/introspection.html">introspect</a> the entire <a href="https://spec.graphql.org/October2021#sec-Schema">schema</a>, which result can be serialized into <a href="https://www.json.org">JSON</a>:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">extern crate serde_json;
|
||
</span><span class="boring">use juniper::{
|
||
</span><span class="boring"> graphql_object, EmptyMutation, EmptySubscription, GraphQLObject,
|
||
</span><span class="boring"> IntrospectionFormat, RootNode,
|
||
</span><span class="boring">};
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLObject)]
|
||
struct Example {
|
||
id: String,
|
||
}
|
||
|
||
struct Query;
|
||
|
||
#[graphql_object]
|
||
impl Query {
|
||
fn example(id: String) -> Example {
|
||
unimplemented!()
|
||
}
|
||
}
|
||
|
||
type Schema = RootNode<'static, Query, EmptyMutation, EmptySubscription>;
|
||
|
||
fn main() {
|
||
// Run the built-in introspection query.
|
||
let (res, _errors) = juniper::introspect(
|
||
&Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()),
|
||
&(),
|
||
IntrospectionFormat::default(),
|
||
).unwrap();
|
||
|
||
// Serialize the introspection result into JSON.
|
||
let json_result = serde_json::to_string_pretty(&res);
|
||
assert!(json_result.is_ok());
|
||
}</code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>TIP</strong>: We still can convert the generated <a href="https://www.json.org">JSON</a> into a <a href="https://graphql.org/learn/schema#type-language">GraphQL schema language</a> representation by using tools like <a href="https://npmjs.com/package/graphql-json-to-sdl"><code>graphql-json-to-sdl</code> command line utility</a>.</p>
|
||
</blockquote>
|
||
<div style="break-before: page; page-break-before: always;"></div><h1 id="subscriptions"><a class="header" href="#subscriptions">Subscriptions</a></h1>
|
||
<p><a href="https://spec.graphql.org/October2021#sec-Subscription">GraphQL subscriptions</a> are a way to push data from a server to clients requesting real-time messages from a server. <a href="https://spec.graphql.org/October2021#sec-Subscription">Subscriptions</a> are similar to <a href="https://spec.graphql.org/October2021#sec-Query">queries</a> in that they specify a set of fields to be delivered to a client, but instead of immediately returning a single answer a result is sent every time a particular event happens on a server.</p>
|
||
<p>In order to execute <a href="https://spec.graphql.org/October2021#sec-Subscription">subscriptions</a> in <a href="https://docs.rs/juniper">Juniper</a>, we need a coordinator (spawning long-lived connections) and a <a href="https://spec.graphql.org/October2021#sec-Objects">GraphQL object</a> with <a href="https://spec.graphql.org/October2021#sec-Language.Fields">fields</a> resolving into a <a href="https://docs.rs/futures/latest/futures/stream/trait.Stream.html"><code>Stream</code></a> of elements which will then be returned to a client. The <a href="https://docs.rs/juniper_subscriptions"><code>juniper_subscriptions</code> crate</a> provides a default implementation of these abstractions.</p>
|
||
<p>The <a href="https://spec.graphql.org/October2021#sel-FAHTRJCAACC3EhsX">subscription root</a> is just a <a href="https://spec.graphql.org/October2021#sec-Objects">GraphQL object</a>, similar to the <a href="https://spec.graphql.org/October2021#sel-FAHTRFCAACChCtpG">query root</a> and <a href="https://spec.graphql.org/October2021#sel-FAHTRHCAACCuE9yD">mutations root</a> that we define for operations in our <a href="https://spec.graphql.org/October2021#sec-Schema">GraphQL schema</a>. For <a href="https://spec.graphql.org/October2021#sec-Subscription">subscriptions</a> all fields should be <code>async</code> and return a <a href="https://docs.rs/futures/latest/futures/stream/trait.Stream.html"><code>Stream</code></a> of some <a href="https://spec.graphql.org/October2021#sec-Types">GraphQL type</a> values, rather than direct values.</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate futures;
|
||
</span><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use std::pin::Pin;
|
||
</span><span class="boring">use futures::Stream;
|
||
</span><span class="boring">use juniper::{graphql_object, graphql_subscription, FieldError};
|
||
</span><span class="boring">
|
||
</span><span class="boring">#[derive(Clone)]
|
||
</span><span class="boring">pub struct Database;
|
||
</span><span class="boring">
|
||
</span><span class="boring">impl juniper::Context for Database {}
|
||
</span><span class="boring">
|
||
</span><span class="boring">pub struct Query;
|
||
</span><span class="boring">
|
||
</span><span class="boring">#[graphql_object]
|
||
</span><span class="boring">#[graphql(context = Database)]
|
||
</span><span class="boring">impl Query {
|
||
</span><span class="boring"> fn hello_world() -> &'static str {
|
||
</span><span class="boring"> "Hello World!"
|
||
</span><span class="boring"> }
|
||
</span><span class="boring">}
|
||
</span><span class="boring">
|
||
</span>type StringStream = Pin<Box<dyn Stream<Item = Result<String, FieldError>> + Send>>;
|
||
|
||
pub struct Subscription;
|
||
|
||
#[graphql_subscription]
|
||
#[graphql(context = Database)]
|
||
impl Subscription {
|
||
// This subscription operation emits two values sequentially:
|
||
// the `String`s "Hello" and "World!".
|
||
async fn hello_world() -> StringStream {
|
||
let stream = futures::stream::iter([
|
||
Ok(String::from("Hello")),
|
||
Ok(String::from("World!")),
|
||
]);
|
||
Box::pin(stream)
|
||
}
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main () {}</span></code></pre></pre>
|
||
<h2 id="coordinator"><a class="header" href="#coordinator">Coordinator</a></h2>
|
||
<p><a href="https://spec.graphql.org/October2021#sec-Subscription">GraphQL subscriptions</a> require a bit more resources than regular <a href="https://spec.graphql.org/October2021#sec-Query">queries</a> and provide a great vector for <a href="https://en.wikipedia.org/wiki/Denial-of-service_attack">DoS attacks</a>. This can can bring down a server easily if not handled correctly. The <a href="https://docs.rs/juniper/0.16.1/juniper/trait.SubscriptionCoordinator.html"><code>SubscriptionCoordinator</code> trait</a> provides coordination logic to enable functionality like <a href="https://en.wikipedia.org/wiki/Denial-of-service_attack">DoS attacks</a> mitigation and resource limits.</p>
|
||
<p>The <a href="https://docs.rs/juniper/0.16.1/juniper/trait.SubscriptionCoordinator.html"><code>SubscriptionCoordinator</code></a> contains the <a href="https://spec.graphql.org/October2021#sec-Schema">schema</a> and can keep track of opened connections, handle <a href="https://spec.graphql.org/October2021#sec-Subscription">subscription</a> start and end, and maintain a global ID for each <a href="https://spec.graphql.org/October2021#sec-Subscription">subscription</a>. Each time a connection is established, the <a href="https://docs.rs/juniper/0.16.1/juniper/trait.SubscriptionCoordinator.html"><code>SubscriptionCoordinator</code></a> spawns a [32], which handles a single connection, providing resolver logic for a client stream as well as reconnection and shutdown logic.</p>
|
||
<p>While we can implement <a href="https://docs.rs/juniper/0.16.1/juniper/trait.SubscriptionCoordinator.html"><code>SubscriptionCoordinator</code></a> ourselves, <a href="https://docs.rs/juniper">Juniper</a> contains a simple and generic implementation called <a href="https://docs.rs/juniper_subscriptions/0.17.0/juniper_subscriptions/struct.Coordinator.html"><code>Coordinator</code></a>. The <code>subscribe</code> method returns a <a href="https://doc.rust-lang.org/stable/std/future/trait.Future.html"><code>Future</code></a> resolving into a <code>Result<Connection, GraphQLError></code>, where <a href="https://docs.rs/juniper_subscriptions/0.17.0/juniper_subscriptions/struct.Connection.html"><code>Connection</code></a> is a <a href="https://docs.rs/futures/latest/futures/stream/trait.Stream.html"><code>Stream</code></a> of <a href="https://spec.graphql.org/October2021#sec-Values">values</a> returned by the operation, and a <a href="https://docs.rs/juniper/0.16.1/juniper/enum.GraphQLError.html"><code>GraphQLError</code></a> is the error when the <a href="https://spec.graphql.org/October2021#sec-Subscription">subscription operation</a> fails.</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate futures;
|
||
</span><span class="boring">extern crate juniper;
|
||
</span><span class="boring">extern crate juniper_subscriptions;
|
||
</span><span class="boring">extern crate serde_json;
|
||
</span><span class="boring">use std::pin::Pin;
|
||
</span><span class="boring">use futures::{Stream, StreamExt as _};
|
||
</span><span class="boring">use juniper::{
|
||
</span><span class="boring"> http::GraphQLRequest,
|
||
</span><span class="boring"> graphql_object, graphql_subscription,
|
||
</span><span class="boring"> DefaultScalarValue, EmptyMutation, FieldError,
|
||
</span><span class="boring"> RootNode, SubscriptionCoordinator,
|
||
</span><span class="boring">};
|
||
</span><span class="boring">use juniper_subscriptions::Coordinator;
|
||
</span><span class="boring">
|
||
</span><span class="boring">#[derive(Clone)]
|
||
</span><span class="boring">pub struct Database;
|
||
</span><span class="boring">
|
||
</span><span class="boring">impl juniper::Context for Database {}
|
||
</span><span class="boring">
|
||
</span><span class="boring">impl Database {
|
||
</span><span class="boring"> fn new() -> Self {
|
||
</span><span class="boring"> Self
|
||
</span><span class="boring"> }
|
||
</span><span class="boring">}
|
||
</span><span class="boring">
|
||
</span><span class="boring">pub struct Query;
|
||
</span><span class="boring">
|
||
</span><span class="boring">#[graphql_object]
|
||
</span><span class="boring">#[graphql(context = Database)]
|
||
</span><span class="boring">impl Query {
|
||
</span><span class="boring"> fn hello_world() -> &'static str {
|
||
</span><span class="boring"> "Hello World!"
|
||
</span><span class="boring"> }
|
||
</span><span class="boring">}
|
||
</span><span class="boring">
|
||
</span><span class="boring">type StringStream = Pin<Box<dyn Stream<Item = Result<String, FieldError>> + Send>>;
|
||
</span><span class="boring">
|
||
</span><span class="boring">pub struct Subscription;
|
||
</span><span class="boring">
|
||
</span><span class="boring">#[graphql_subscription]
|
||
</span><span class="boring">#[graphql(context = Database)]
|
||
</span><span class="boring">impl Subscription {
|
||
</span><span class="boring"> async fn hello_world() -> StringStream {
|
||
</span><span class="boring"> let stream = futures::stream::iter([
|
||
</span><span class="boring"> Ok(String::from("Hello")),
|
||
</span><span class="boring"> Ok(String::from("World!")),
|
||
</span><span class="boring"> ]);
|
||
</span><span class="boring"> Box::pin(stream)
|
||
</span><span class="boring"> }
|
||
</span><span class="boring">}
|
||
</span><span class="boring">
|
||
</span>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 db = Database::new();
|
||
|
||
let req: GraphQLRequest<DefaultScalarValue> = serde_json::from_str(
|
||
r#"{
|
||
"query": "subscription { helloWorld }"
|
||
}"#,
|
||
).unwrap();
|
||
|
||
let mut conn = coordinator.subscribe(&req, &db).await.unwrap();
|
||
while let Some(result) = conn.next().await {
|
||
println!("{}", serde_json::to_string(&result).unwrap());
|
||
}
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<h2 id="websocket"><a class="header" href="#websocket">WebSocket</a></h2>
|
||
<p>For information about serving <a href="https://spec.graphql.org/October2021#sec-Subscription">GraphQL subscriptions</a> over <a href="https://en.wikipedia.org/wiki/WebSocket">WebSocket</a>, see the <a href="schema/../serve/index.html#websocket">"Serving" chapter</a>.</p>
|
||
<div style="break-before: page; page-break-before: always;"></div><h1 id="introspection"><a class="header" href="#introspection">Introspection</a></h1>
|
||
<blockquote>
|
||
<p>The <a href="https://spec.graphql.org/October2021#sec-Schema-Introspection">schema introspection</a> system is accessible from the meta-fields <code>__schema</code> and <code>__type</code> which are accessible from the type of the root of a query operation.</p>
|
||
<pre><code class="language-graphql">__schema: __Schema!
|
||
__type(name: String!): __Type
|
||
</code></pre>
|
||
<p>Like all meta-fields, these are implicit and do not appear in the fields list in the root type of the query operation.</p>
|
||
</blockquote>
|
||
<p><a href="https://graphql.org">GraphQL</a> provides <a href="https://spec.graphql.org/October2021#sec-Introspection">introspection</a>, allowing to see what <a href="https://spec.graphql.org/October2021#sel-GAFRJBABABF_jB">queries</a>, <a href="https://spec.graphql.org/October2021#sel-GAFRJDABABI5C">mutations</a> and <a href="https://spec.graphql.org/October2021#sel-GAFRJFABABMvpN">subscriptions</a> a <a href="https://graphql.org">GraphQL</a> server supports at runtime.</p>
|
||
<p>Because <a href="https://spec.graphql.org/October2021#sec-Introspection">introspection</a> queries are just regular <a href="https://spec.graphql.org/October2021#sel-GAFRJBABABF_jB">GraphQL queries</a>, <a href="https://docs.rs/juniper">Juniper</a> supports them natively. For example, to get all the names of the types supported, we could <a href="https://spec.graphql.org/October2021#sec-Execution">execute</a> the following <a href="https://spec.graphql.org/October2021#sel-GAFRJBABABF_jB">query</a> against <a href="https://docs.rs/juniper">Juniper</a>:</p>
|
||
<pre><code class="language-graphql">{
|
||
__schema {
|
||
types {
|
||
name
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<h2 id="disabling"><a class="header" href="#disabling">Disabling</a></h2>
|
||
<blockquote>
|
||
<p>Disabling introspection in production is a widely debated topic, but we believe it’s one of the first things you can do to harden your GraphQL API in production.</p>
|
||
</blockquote>
|
||
<p><a href="https://www.apollographql.com/blog/why-you-should-disable-graphql-introspection-in-production">Some security requirements and considerations</a> may mandate to disable <a href="https://spec.graphql.org/October2021#sec-Schema-Introspection">GraphQL schema introspection</a> in production environments. In <a href="https://docs.rs/juniper">Juniper</a> this can be achieved by using the <a href="https://docs.rs/juniper/0.16.1/juniper/struct.RootNode.html#method.disable_introspection"><code>RootNode::disable_introspection()</code></a> method:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::{
|
||
</span><span class="boring"> graphql_object, graphql_vars, EmptyMutation, EmptySubscription, GraphQLError,
|
||
</span><span class="boring"> RootNode,
|
||
</span><span class="boring">};
|
||
</span><span class="boring">
|
||
</span>pub struct Query;
|
||
|
||
#[graphql_object]
|
||
impl Query {
|
||
fn some() -> bool {
|
||
true
|
||
}
|
||
}
|
||
|
||
type Schema = RootNode<'static, Query, EmptyMutation, EmptySubscription>;
|
||
|
||
fn main() {
|
||
let schema = Schema::new(Query, EmptyMutation::new(), EmptySubscription::new())
|
||
.disable_introspection();
|
||
|
||
let query = "query { __schema { queryType { name } } }";
|
||
|
||
match juniper::execute_sync(query, None, &schema, &graphql_vars! {}, &()) {
|
||
Err(GraphQLError::ValidationError(errs)) => {
|
||
assert_eq!(
|
||
errs.first().unwrap().message(),
|
||
"GraphQL introspection is not allowed, but the operation contained `__schema`",
|
||
);
|
||
}
|
||
res => panic!("expected `ValidationError`, returned: {res:#?}"),
|
||
}
|
||
}</code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>NOTE</strong>: Attempt to execute an <a href="https://spec.graphql.org/October2021#sec-Schema-Introspection">introspection query</a> results in <a href="https://spec.graphql.org/October2021#sec-Validation">validation</a> error, rather than <a href="https://spec.graphql.org/October2021#sec-Execution">execution</a> error.</p>
|
||
</blockquote>
|
||
<div style="break-before: page; page-break-before: always;"></div><h1 id="serving"><a class="header" href="#serving">Serving</a></h1>
|
||
<p>Once we have built a <a href="serve/../schema/index.html">GraphQL schema</a>, the next obvious step would be to serve it, so clients can interact with our <a href="https://graphql.org">GraphQL</a> API. Usually, <a href="https://graphql.org">GraphQL</a> APIs are served via <a href="https://en.wikipedia.org/wiki/HTTP">HTTP</a>.</p>
|
||
<h2 id="web-server-frameworks-1"><a class="header" href="#web-server-frameworks-1">Web server frameworks</a></h2>
|
||
<p>Though the <a href="https://docs.rs/juniper"><code>juniper</code></a> crate doesn't provide a built-in <a href="https://en.wikipedia.org/wiki/HTTP">HTTP</a> server, the surrounding ecosystem does.</p>
|
||
<h3 id="officially-supported"><a class="header" href="#officially-supported">Officially supported</a></h3>
|
||
<p><a href="https://docs.rs/juniper">Juniper</a> officially supports the following widely used and adopted web server frameworks in <a href="https://www.rust-lang.org">Rust</a> ecosystem:</p>
|
||
<ul>
|
||
<li><a href="https://docs.rs/actix-web"><code>actix-web</code></a> (<a href="https://docs.rs/juniper_actix"><code>juniper_actix</code></a> crate)</li>
|
||
<li><a href="https://docs.rs/axum"><code>axum</code></a> (<a href="https://docs.rs/juniper_axum"><code>juniper_axum</code></a> crate)</li>
|
||
<li><a href="https://docs.rs/hyper"><code>hyper</code></a> ([<code>juniper_hyper</code>] crate)</li>
|
||
<li><a href="https://docs.rs/rocket"><code>rocket</code></a> (<a href="https://docs.rs/juniper_rocket"><code>juniper_rocket</code></a> crate)</li>
|
||
<li><a href="https://docs.rs/warp"><code>warp</code></a> (<a href="https://docs.rs/juniper_warp"><code>juniper_warp</code></a> crate)</li>
|
||
</ul>
|
||
<p>See their API docs and usage examples (accessible from API docs) for further details of how they should be used.</p>
|
||
<blockquote>
|
||
<p><strong>NOTE</strong>: All the officially supported web server framework integrations provide a simple and convenient way for exposing <a href="https://github.com/graphql/graphiql">GraphiQL</a> and/or <a href="https://github.com/prisma/graphql-playground">GraphQL Playground</a> with the <a href="serve/../schema/index.html">GraphQL schema</a> along. These powerful tools ease the development process by enabling you to explore and send client requests to the <a href="https://graphql.org">GraphQL</a> API under development.</p>
|
||
</blockquote>
|
||
<h2 id="websocket-1"><a class="header" href="#websocket-1">WebSocket</a></h2>
|
||
<blockquote>
|
||
<p><strong>NOTE</strong>: <a href="https://en.wikipedia.org/wiki/WebSocket">WebSocket</a> is a crucial part for serving <a href="serve/../schema/subscriptions.html">GraphQL subscriptions</a> over <a href="https://en.wikipedia.org/wiki/HTTP">HTTP</a>.</p>
|
||
</blockquote>
|
||
<p>There are two widely adopted protocols for serving <a href="https://graphql.org">GraphQL</a> over <a href="https://en.wikipedia.org/wiki/WebSocket">WebSocket</a>:</p>
|
||
<ol>
|
||
<li><a href="https://github.com/apollographql/subscriptions-transport-ws/blob/v0.11.0/PROTOCOL.md">Legacy <code>graphql-ws</code> GraphQL over WebSocket Protocol</a>, formerly used by <a href="https://www.apollographql.com">Apollo</a> and the <a href="https://npmjs.com/package/subscriptions-transport-ws"><code>subscriptions-transport-ws</code> npm package</a>, and now being deprecated.</li>
|
||
<li><a href="https://github.com/enisdenjo/graphql-ws/blob/v5.14.0/PROTOCOL.md">New <code>graphql-transport-ws</code> GraphQL over WebSocket Protocol</a>, provided by the <a href="https://npmjs.com/package/graphql-ws"><code>graphql-ws</code> npm package</a> and being used by <a href="https://www.apollographql.com">Apollo</a> as for now.</li>
|
||
</ol>
|
||
<p>In the <a href="https://docs.rs/juniper">Juniper</a> ecosystem, both implementations are provided by the <a href="https://docs.rs/juniper_graphql_ws"><code>juniper_graphql_ws</code></a> crate. Most of the <a href="serve/index.html#officially-supported">officially supported web server framework integrations</a> are able to serve a <a href="serve/../schema/index.html">GraphQL schema</a> over <a href="https://en.wikipedia.org/wiki/WebSocket">WebSocket</a> (including <a href="serve/../schema/subscriptions.html">subscriptions</a>) and even support <a href="https://developer.mozilla.org/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#subprotocols">auto-negotiation of the correct protocol based on the <code>Sec-Websocket-Protocol</code> HTTP header value</a>. See their API docs and usage examples (accessible from API docs) for further details of how to do so.</p>
|
||
<div style="break-before: page; page-break-before: always;"></div><h1 id="batching"><a class="header" href="#batching">Batching</a></h1>
|
||
<p>The <a href="https://graphql.org">GraphQL</a> standard generally assumes that there will be one server request per each client operation to perform (such as a query or mutation). This is conceptually simple but potentially inefficient.</p>
|
||
<p>Some client libraries (such as <a href="https://www.apollographql.com/docs/link/links/batch-http.html"><code>apollo-link-batch-http</code></a>) have the ability to batch operations in a single <a href="https://en.wikipedia.org/wiki/HTTP">HTTP</a> request to save network round-trips and potentially increase performance. There are <a href="https://www.apollographql.com/blog/batching-client-graphql-queries#what-are-the-tradeoffs-with-batching">some tradeoffs</a>, though, that should be considered before <a href="https://www.apollographql.com/blog/batching-client-graphql-queries">batching operations</a>.</p>
|
||
<p><a href="https://docs.rs/juniper">Juniper</a>'s <a href="serve/index.html#officially-supported">server integration crates</a> support <a href="https://www.apollographql.com/blog/batching-client-graphql-queries">batching multiple operations</a> in a single <a href="https://en.wikipedia.org/wiki/HTTP">HTTP</a> request out-of-the-box via <a href="https://www.json.org">JSON</a> arrays. This makes them compatible with client libraries that support <a href="https://www.apollographql.com/blog/batching-client-graphql-queries">batch operations</a> without any special configuration.</p>
|
||
<blockquote>
|
||
<p><strong>NOTE</strong>: If you use a custom server integration, it's <strong>not a hard requirement</strong> to support <a href="https://www.apollographql.com/blog/batching-client-graphql-queries">batching</a>, as it's not a part of the <a href="https://spec.graphql.org/October2021">official GraphQL specification</a>.</p>
|
||
</blockquote>
|
||
<p>Assuming an integration supports <a href="https://www.apollographql.com/blog/batching-client-graphql-queries">operations batching</a>, for the following GraphQL query:</p>
|
||
<pre><code class="language-graphql">{
|
||
hero {
|
||
name
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>The <a href="https://www.json.org">JSON</a> <code>data</code> to <a href="https://en.wikipedia.org/wiki/POST_(HTTP)">POST</a> for an individual request would be:</p>
|
||
<pre><code class="language-json">{
|
||
"query": "{hero{name}}"
|
||
}
|
||
</code></pre>
|
||
<p>And the response would be in the form:</p>
|
||
<pre><code class="language-json">{
|
||
"data": {
|
||
"hero": {
|
||
"name": "R2-D2"
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>However, if we want to run the same query twice in a single <a href="https://en.wikipedia.org/wiki/HTTP">HTTP</a> request, the batched <a href="https://www.json.org">JSON</a> <code>data</code> to <a href="https://en.wikipedia.org/wiki/POST_(HTTP)">POST</a> would be:</p>
|
||
<pre><code class="language-json">[
|
||
{
|
||
"query": "{hero{name}}"
|
||
},
|
||
{
|
||
"query": "{hero{name}}"
|
||
}
|
||
]
|
||
</code></pre>
|
||
<p>And then, the response would be in the following array form:</p>
|
||
<pre><code class="language-json">[
|
||
{
|
||
"data": {
|
||
"hero": {
|
||
"name": "R2-D2"
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"data": {
|
||
"hero": {
|
||
"name": "R2-D2"
|
||
}
|
||
}
|
||
}
|
||
]
|
||
</code></pre>
|
||
<div style="break-before: page; page-break-before: always;"></div><h1 id="advanced-topics"><a class="header" href="#advanced-topics">Advanced topics</a></h1>
|
||
<p>The chapters below cover some more advanced topics.</p>
|
||
<ul>
|
||
<li><a href="advanced/implicit_and_explicit_null.html">Implicit and explicit <code>null</code></a></li>
|
||
<li><a href="advanced/n_plus_1.html">N+1 problem</a>
|
||
<ul>
|
||
<li><a href="advanced/dataloader.html">DataLoader</a></li>
|
||
<li><a href="advanced/lookahead.html">Look-ahead</a>
|
||
<ul>
|
||
<li><a href="advanced/eager_loading.html">Eager loading</a></li>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<div style="break-before: page; page-break-before: always;"></div><h1 id="implicit-and-explicit-null"><a class="header" href="#implicit-and-explicit-null">Implicit and explicit <code>null</code></a></h1>
|
||
<blockquote>
|
||
<p><a href="https://graphql.org">GraphQL</a> has two semantically different ways to represent the lack of a value:</p>
|
||
<ul>
|
||
<li>Explicitly providing the literal value: <strong>null</strong>.</li>
|
||
<li>Implicitly not providing a value at all.</li>
|
||
</ul>
|
||
</blockquote>
|
||
<p>There are two ways that a client can submit a <a href="https://spec.graphql.org/October2021#sec-Null-Value"><code>null</code> value</a> as an <a href="https://spec.graphql.org/October2021#sec-Language.Arguments">argument</a> or a <a href="https://spec.graphql.org/October2021#sec-Language.Fields">field</a> in a <a href="https://graphql.org">GraphQL</a> query:</p>
|
||
<ol>
|
||
<li>Either use an explicit <code>null</code> literal:
|
||
<pre><code class="language-graphql">{
|
||
field(arg: null)
|
||
}
|
||
</code></pre>
|
||
</li>
|
||
<li>Or simply omit the <a href="https://spec.graphql.org/October2021#sec-Language.Arguments">argument</a>, so the implicit default <code>null</code> value kicks in:
|
||
<pre><code class="language-graphql">{
|
||
field
|
||
}
|
||
</code></pre>
|
||
</li>
|
||
</ol>
|
||
<p>There are some situations where it's useful to know which one exactly has been provided.</p>
|
||
<p>For example, let's say we have a function that allows users to perform a "patch" operation on themselves. Let's say our users can optionally have favorite and least favorite numbers, and the input for that might look like this:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021">/// Updates user attributes. Fields that are [`None`] are left as-is.
|
||
struct UserPatch {
|
||
/// If [`Some`], updates the user's favorite number.
|
||
favorite_number: Option<Option<i32>>,
|
||
|
||
/// If [`Some`], updates the user's least favorite number.
|
||
least_favorite_number: Option<Option<i32>>,
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<p>To set a user's favorite number to 7, we would set <code>favorite_number</code> to <code>Some(Some(7))</code>. In <a href="https://graphql.org">GraphQL</a>, that might look like this:</p>
|
||
<pre><code class="language-graphql">mutation { patchUser(patch: { favoriteNumber: 7 }) }
|
||
</code></pre>
|
||
<p>To unset the user's favorite number, we would set <code>favorite_number</code> to <code>Some(None)</code>. In <a href="https://graphql.org">GraphQL</a>, that might look like this:</p>
|
||
<pre><code class="language-graphql">mutation { patchUser(patch: { favoriteNumber: null }) }
|
||
</code></pre>
|
||
<p>And if we want to leave the user's favorite number alone, just set it to <code>None</code>. In <a href="https://graphql.org">GraphQL</a>, that might look like this:</p>
|
||
<pre><code class="language-graphql">mutation { patchUser(patch: {}) }
|
||
</code></pre>
|
||
<p>The last two cases rely on being able to distinguish between <a href="https://spec.graphql.org/October2021#sel-EAFdRDHAAEJDAoBxzT">explicit and implicit <code>null</code></a>.</p>
|
||
<p>Unfortunately, plain <code>Option</code> is not capable to distinguish them. That's why in <a href="https://docs.rs/juniper">Juniper</a>, this can be done using the <a href="https://docs.rs/juniper/0.16.1/juniper/enum.Nullable.html"><code>Nullable</code></a> type:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate juniper;
|
||
</span>use juniper::{graphql_object, FieldResult, GraphQLInputObject, Nullable};
|
||
|
||
#[derive(GraphQLInputObject)]
|
||
struct UserPatchInput {
|
||
favorite_number: Nullable<i32>,
|
||
least_favorite_number: Nullable<i32>,
|
||
}
|
||
|
||
impl From<UserPatchInput> for UserPatch {
|
||
fn from(input: UserPatchInput) -> Self {
|
||
Self {
|
||
// The `explicit()` function transforms the `Nullable` into an
|
||
// `Option<Option<T>>` as expected by the business logic layer.
|
||
favorite_number: input.favorite_number.explicit(),
|
||
least_favorite_number: input.least_favorite_number.explicit(),
|
||
}
|
||
}
|
||
}
|
||
|
||
<span class="boring">struct UserPatch {
|
||
</span><span class="boring"> favorite_number: Option<Option<i32>>,
|
||
</span><span class="boring"> least_favorite_number: Option<Option<i32>>,
|
||
</span><span class="boring">}
|
||
</span><span class="boring">
|
||
</span><span class="boring">struct Session;
|
||
</span><span class="boring">impl Session {
|
||
</span><span class="boring"> fn patch_user(&self, _patch: UserPatch) -> FieldResult<()> { Ok(()) }
|
||
</span><span class="boring">}
|
||
</span><span class="boring">
|
||
</span>struct Context {
|
||
session: Session,
|
||
}
|
||
impl juniper::Context for Context {}
|
||
|
||
struct Mutation;
|
||
|
||
#[graphql_object]
|
||
#[graphql(context = Context)]
|
||
impl Mutation {
|
||
fn patch_user(patch: UserPatchInput, ctx: &Context) -> FieldResult<bool> {
|
||
ctx.session.patch_user(patch.into())?;
|
||
Ok(true)
|
||
}
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre></pre>
|
||
<div style="break-before: page; page-break-before: always;"></div><h1 id="n1-problem"><a class="header" href="#n1-problem">N+1 problem</a></h1>
|
||
<p>A common issue with <a href="https://graphql.org">GraphQL</a> server implementations is how the <a href="https://spec.graphql.org/October2021#sec-Executing-Fields">resolvers</a> query their datasource. With a naive and straightforward approach we quickly run into the N+1 problem, resulting in a large number of unnecessary database queries or <a href="https://en.wikipedia.org/wiki/HTTP">HTTP</a> requests.</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">#![allow(unused)]
|
||
</span><span class="boring">fn main() {
|
||
</span><span class="boring">extern crate anyhow;
|
||
</span><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use anyhow::anyhow;
|
||
</span><span class="boring">use juniper::{graphql_object, GraphQLObject};
|
||
</span><span class="boring">
|
||
</span><span class="boring">type CultId = i32;
|
||
</span><span class="boring">type UserId = i32;
|
||
</span><span class="boring">
|
||
</span><span class="boring">struct Repository;
|
||
</span><span class="boring">
|
||
</span><span class="boring">impl juniper::Context for Repository {}
|
||
</span><span class="boring">
|
||
</span><span class="boring">impl Repository {
|
||
</span><span class="boring"> async fn load_cult_by_id(&self, cult_id: CultId) -> anyhow::Result<Option<Cult>> { unimplemented!() }
|
||
</span><span class="boring"> async fn load_all_persons(&self) -> anyhow::Result<Vec<Person>> { unimplemented!() }
|
||
</span><span class="boring">}
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLObject)]
|
||
struct Cult {
|
||
id: CultId,
|
||
name: String,
|
||
}
|
||
|
||
struct Person {
|
||
id: UserId,
|
||
name: String,
|
||
cult_id: CultId,
|
||
}
|
||
|
||
#[graphql_object]
|
||
#[graphql(context = Repository)]
|
||
impl Person {
|
||
fn id(&self) -> CultId {
|
||
self.id
|
||
}
|
||
|
||
fn name(&self) -> &str {
|
||
self.name.as_str()
|
||
}
|
||
|
||
async fn cult(&self, #[graphql(ctx)] repo: &Repository) -> anyhow::Result<Cult> {
|
||
// Effectively performs the following SQL query:
|
||
// SELECT id, name FROM cults WHERE id = ${cult_id} LIMIT 1
|
||
repo.load_cult_by_id(self.cult_id)
|
||
.await?
|
||
.ok_or_else(|| anyhow!("No cult exists for ID `{}`", self.cult_id))
|
||
}
|
||
}
|
||
|
||
struct Query;
|
||
|
||
#[graphql_object]
|
||
#[graphql(context = Repository)]
|
||
impl Query {
|
||
async fn persons(#[graphql(ctx)] repo: &Repository) -> anyhow::Result<Vec<Person>> {
|
||
// Effectively performs the following SQL query:
|
||
// SELECT id, name, cult_id FROM persons
|
||
repo.load_all_persons().await
|
||
}
|
||
}
|
||
<span class="boring">}</span></code></pre></pre>
|
||
<p>Let's say we want to list a bunch of <code>cult</code>s <code>persons</code> were in:</p>
|
||
<pre><code class="language-graphql">query {
|
||
persons {
|
||
id
|
||
name
|
||
cult {
|
||
id
|
||
name
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>Once the <code>persons</code> <a href="https://spec.graphql.org/October2021#sec-List">list</a> has been <a href="https://spec.graphql.org/October2021#sec-Executing-Fields">resolved</a>, a separate <a href="https://en.wikipedia.org/wiki/SQL">SQL</a> query is run to find the <code>cult</code> of each <code>Person</code>. We can see how this could quickly become a problem.</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 = 2;
|
||
SELECT id, name FROM cults WHERE id = 1;
|
||
SELECT id, name FROM cults WHERE id = 3;
|
||
SELECT id, name FROM cults WHERE id = 4;
|
||
SELECT id, name FROM cults WHERE id = 1;
|
||
SELECT id, name FROM cults WHERE id = 2;
|
||
-- and so on...
|
||
</code></pre>
|
||
<p>There are several ways how this problem may be resolved in <a href="https://docs.rs/juniper">Juniper</a>. The most common ones are:</p>
|
||
<ul>
|
||
<li><a href="advanced/dataloader.html">DataLoader</a></li>
|
||
<li><a href="advanced/lookahead.html">Look-ahead machinery</a>
|
||
<ul>
|
||
<li><a href="advanced/eager_loading.html">Eager loading</a></li>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<div style="break-before: page; page-break-before: always;"></div><h1 id="dataloader"><a class="header" href="#dataloader">DataLoader</a></h1>
|
||
<p>DataLoader pattern, named after the correspondent <a href="https://github.com/graphql/dataloader"><code>dataloader</code> NPM package</a>, represents a mechanism of batching and caching data requests in a delayed manner for solving the <a href="advanced/n_plus_1.html">N+1 problem</a>.</p>
|
||
<blockquote>
|
||
<p>A port of the "Loader" API originally developed by <a href="https://github.com/schrockn">@schrockn</a> at Facebook in 2010 as a simplifying force to coalesce the sundry key-value store back-end APIs which existed at the time. At Facebook, "Loader" became one of the implementation details of the "Ent" framework, a privacy-aware data entity loading and caching layer within web server product code. This ultimately became the underpinning for Facebook's GraphQL server implementation and type definitions.</p>
|
||
</blockquote>
|
||
<p>In <a href="https://www.rust-lang.org">Rust</a> ecosystem, DataLoader pattern is introduced with the <a href="https://docs.rs/crate/dataloader"><code>dataloader</code> crate</a>, naturally usable with <a href="https://docs.rs/juniper">Juniper</a>.</p>
|
||
<p>Let's remake our <a href="advanced/n_plus_1.html">example of N+1 problem</a>, so it's solved by applying the DataLoader pattern:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">extern crate anyhow;
|
||
</span><span class="boring">extern crate dataloader;
|
||
</span><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use std::{collections::HashMap, sync::Arc};
|
||
</span><span class="boring">use anyhow::anyhow;
|
||
</span><span class="boring">use dataloader::non_cached::Loader;
|
||
</span><span class="boring">use juniper::{graphql_object, GraphQLObject};
|
||
</span><span class="boring">
|
||
</span><span class="boring">type CultId = i32;
|
||
</span><span class="boring">type UserId = i32;
|
||
</span><span class="boring">
|
||
</span><span class="boring">struct Repository;
|
||
</span><span class="boring">
|
||
</span><span class="boring">impl Repository {
|
||
</span><span class="boring"> async fn load_cults_by_ids(&self, cult_ids: &[CultId]) -> anyhow::Result<HashMap<CultId, Cult>> { unimplemented!() }
|
||
</span><span class="boring"> async fn load_all_persons(&self) -> anyhow::Result<Vec<Person>> { unimplemented!() }
|
||
</span><span class="boring">}
|
||
</span><span class="boring">
|
||
</span>struct Context {
|
||
repo: Repository,
|
||
cult_loader: CultLoader,
|
||
}
|
||
|
||
impl juniper::Context for Context {}
|
||
|
||
#[derive(Clone, GraphQLObject)]
|
||
struct Cult {
|
||
id: CultId,
|
||
name: String,
|
||
}
|
||
|
||
struct CultBatcher {
|
||
repo: Repository,
|
||
}
|
||
|
||
// Since `BatchFn` doesn't provide any notion of fallible loading, like
|
||
// `try_load()` returning `Result<HashMap<K, V>, E>`, we handle possible
|
||
// errors as loaded values and unpack them later in the resolver.
|
||
impl dataloader::BatchFn<CultId, Result<Cult, Arc<anyhow::Error>>> for CultBatcher {
|
||
async fn load(
|
||
&mut self,
|
||
cult_ids: &[CultId],
|
||
) -> HashMap<CultId, Result<Cult, Arc<anyhow::Error>>> {
|
||
// Effectively performs the following SQL query:
|
||
// SELECT id, name FROM cults WHERE id IN (${cult_id1}, ${cult_id2}, ...)
|
||
match self.repo.load_cults_by_ids(cult_ids).await {
|
||
Ok(found_cults) => {
|
||
found_cults.into_iter().map(|(id, cult)| (id, Ok(cult))).collect()
|
||
}
|
||
// One could choose a different strategy to deal with fallible loads,
|
||
// like consider values that failed to load as absent, or just panic.
|
||
// See cksac/dataloader-rs#35 for details:
|
||
// https://github.com/cksac/dataloader-rs/issues/35
|
||
Err(e) => {
|
||
// Since `anyhow::Error` doesn't implement `Clone`, we have to
|
||
// work around here.
|
||
let e = Arc::new(e);
|
||
cult_ids.iter().map(|k| (k.clone(), Err(e.clone()))).collect()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
type CultLoader = Loader<CultId, Result<Cult, Arc<anyhow::Error>>, CultBatcher>;
|
||
|
||
fn new_cult_loader(repo: Repository) -> CultLoader {
|
||
CultLoader::new(CultBatcher { repo })
|
||
// Usually a `Loader` will coalesce all individual loads which occur
|
||
// within a single frame of execution before calling a `BatchFn::load()`
|
||
// with all the collected keys. However, sometimes this behavior is not
|
||
// desirable or optimal (perhaps, a request is expected to be spread out
|
||
// over a few subsequent ticks).
|
||
// A larger yield count will allow more keys to be appended to the batch,
|
||
// but will wait longer before the actual load. For more details see:
|
||
// https://github.com/cksac/dataloader-rs/issues/12
|
||
// https://github.com/graphql/dataloader#batch-scheduling
|
||
.with_yield_count(100)
|
||
}
|
||
|
||
struct Person {
|
||
id: UserId,
|
||
name: String,
|
||
cult_id: CultId,
|
||
}
|
||
|
||
#[graphql_object]
|
||
#[graphql(context = Context)]
|
||
impl Person {
|
||
fn id(&self) -> CultId {
|
||
self.id
|
||
}
|
||
|
||
fn name(&self) -> &str {
|
||
self.name.as_str()
|
||
}
|
||
|
||
async fn cult(&self, ctx: &Context) -> anyhow::Result<Cult> {
|
||
ctx.cult_loader
|
||
// Here, we don't run the `CultBatcher::load()` eagerly, but rather
|
||
// only register the `self.cult_id` value in the `cult_loader` and
|
||
// wait for other concurrent resolvers to do the same.
|
||
// The actual batch loading happens once all the resolvers register
|
||
// their IDs and there is nothing more to execute.
|
||
.try_load(self.cult_id)
|
||
.await
|
||
// The outer error is the `io::Error` returned by `try_load()` if
|
||
// no value is present in the `HashMap` for the specified
|
||
// `self.cult_id`, meaning that there is no `Cult` with such ID
|
||
// in the `Repository`.
|
||
.map_err(|_| anyhow!("No cult exists for ID `{}`", self.cult_id))?
|
||
// The inner error is the one returned by the `CultBatcher::load()`
|
||
// if the `Repository::load_cults_by_ids()` fails, meaning that
|
||
// running the SQL query failed.
|
||
.map_err(|arc_err| anyhow!("{arc_err}"))
|
||
}
|
||
}
|
||
|
||
struct Query;
|
||
|
||
#[graphql_object]
|
||
#[graphql(context = Context)]
|
||
impl Query {
|
||
async fn persons(ctx: &Context) -> anyhow::Result<Vec<Person>> {
|
||
// Effectively performs the following SQL query:
|
||
// SELECT id, name, cult_id FROM persons
|
||
ctx.repo.load_all_persons().await
|
||
}
|
||
}
|
||
|
||
fn main() {
|
||
|
||
}</code></pre></pre>
|
||
<p>And now, performing a <a href="advanced/n_plus_1.html">GraphQL query which lead to N+1 problem</a></p>
|
||
<pre><code class="language-graphql">query {
|
||
persons {
|
||
id
|
||
name
|
||
cult {
|
||
id
|
||
name
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>will lead to efficient <a href="https://en.wikipedia.org/wiki/SQL">SQL</a> queries, just as expected:</p>
|
||
<pre><code class="language-sql">SELECT id, name, cult_id FROM persons;
|
||
SELECT id, name FROM cults WHERE id IN (1, 2, 3, 4);
|
||
</code></pre>
|
||
<h2 id="caching"><a class="header" href="#caching">Caching</a></h2>
|
||
<p><a href="https://docs.rs/dataloader/latest/dataloader/cached/index.html"><code>dataloader::cached</code></a> provides a <a href="https://en.wikipedia.org/wiki/Memoization">memoization</a> cache: after <code>BatchFn::load()</code> is called once with given keys, the resulting values are cached to eliminate redundant loads.</p>
|
||
<p>DataLoader caching does not replace <a href="https://redis.io">Redis</a>, <a href="https://memcached.org">Memcached</a>, 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 <a href="https://github.com/graphql/dataloader#caching">in the context of a single request</a>.</p>
|
||
<blockquote>
|
||
<p><strong>WARNING</strong>: A DataLoader should be created per-request to avoid risk of bugs where one client is able to load cached/batched data from another client outside its authenticated scope. Creating a DataLoader within an individual resolver will prevent batching from occurring and will nullify any benefits of it.</p>
|
||
</blockquote>
|
||
<h2 id="full-example"><a class="header" href="#full-example">Full example</a></h2>
|
||
<p>For a full example using DataLoaders in <a href="https://docs.rs/juniper">Juniper</a> check out the <a href="https://github.com/jayy-lmao/rust-graphql-docker"><code>jayy-lmao/rust-graphql-docker</code> repository</a>.</p>
|
||
<div style="break-before: page; page-break-before: always;"></div><h1 id="look-ahead"><a class="header" href="#look-ahead">Look-ahead</a></h1>
|
||
<blockquote>
|
||
<p>In backtracking algorithms, <strong>look ahead</strong> is the generic term for a subprocedure that attempts to foresee the effects of choosing a branching variable to evaluate one of its values. The two main aims of look-ahead are to choose a variable to evaluate next and to choose the order of values to assign to it.</p>
|
||
</blockquote>
|
||
<p>In <a href="https://graphql.org">GraphQL</a>, look-ahead machinery allows us to introspect the currently <a href="https://spec.graphql.org/October2021#sec-Execution">executed</a> <a href="https://spec.graphql.org/October2021#sec-Language.Operations%5C">GraphQL operation</a> to see which <a href="https://spec.graphql.org/October2021#sec-Language.Fields">fields</a> has been actually selected by it.</p>
|
||
<p>In <a href="https://docs.rs/juniper">Juniper</a>, it's represented by the <a href="https://docs.rs/juniper/0.16.1/juniper/executor/struct.Executor.html#method.look_ahead"><code>Executor::look_ahead()</code></a> method.</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">#![allow(unused)]
|
||
</span><span class="boring">fn main() {
|
||
</span><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::{graphql_object, Executor, GraphQLObject, ScalarValue};
|
||
</span><span class="boring">
|
||
</span><span class="boring">type UserId = i32;
|
||
</span><span class="boring">
|
||
</span>#[derive(GraphQLObject)]
|
||
struct Person {
|
||
id: UserId,
|
||
name: String,
|
||
}
|
||
|
||
struct Query;
|
||
|
||
#[graphql_object]
|
||
// NOTICE: Specifying `ScalarValue` as custom named type parameter,
|
||
// so its name is similar to the one used in methods.
|
||
#[graphql(scalar = S: ScalarValue)]
|
||
impl Query {
|
||
fn persons<S: ScalarValue>(executor: &Executor<'_, '_, (), S>) -> Vec<Person> {
|
||
// Let's see which `Person`'s fields were selected in the client query.
|
||
for field_name in executor.look_ahead().children().names() {
|
||
dbg!(field_name);
|
||
}
|
||
// ...
|
||
<span class="boring"> unimplemented!()
|
||
</span> }
|
||
}
|
||
<span class="boring">}</span></code></pre></pre>
|
||
<blockquote>
|
||
<p><strong>TIP</strong>: <code>S: ScalarValue</code> type parameter on the method is required here to keep the <a href="https://docs.rs/juniper/0.16.1/juniper/executor/struct.Executor.html"><code>Executor</code></a> being generic over <a href="https://docs.rs/juniper/0.16.1/juniper/trait.ScalarValue.html"><code>ScalarValue</code></a> types. We, instead, could have used the <a href="https://docs.rs/juniper/0.16.1/juniper/enum.DefaultScalarValue.html"><code>DefaultScalarValue</code></a>, which is the default <a href="https://docs.rs/juniper/0.16.1/juniper/trait.ScalarValue.html"><code>ScalarValue</code></a> type for the <a href="https://docs.rs/juniper/0.16.1/juniper/executor/struct.Executor.html"><code>Executor</code></a>, and make our code more ergonomic, but less flexible and generic.</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">#![allow(unused)]
|
||
</span><span class="boring">fn main() {
|
||
</span><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use juniper::{graphql_object, DefaultScalarValue, Executor, GraphQLObject};
|
||
</span><span class="boring">
|
||
</span><span class="boring">type UserId = i32;
|
||
</span><span class="boring">
|
||
</span><span class="boring">#[derive(GraphQLObject)]
|
||
</span><span class="boring">struct Person {
|
||
</span><span class="boring"> id: UserId,
|
||
</span><span class="boring"> name: String,
|
||
</span><span class="boring">}
|
||
</span><span class="boring">
|
||
</span><span class="boring">struct Query;
|
||
</span><span class="boring">
|
||
</span>#[graphql_object]
|
||
#[graphql(scalar = DefaultScalarValue)]
|
||
impl Query {
|
||
fn persons(executor: &Executor<'_, '_, ()>) -> Vec<Person> {
|
||
for field_name in executor.look_ahead().children().names() {
|
||
dbg!(field_name);
|
||
}
|
||
// ...
|
||
<span class="boring"> unimplemented!()
|
||
</span> }
|
||
}
|
||
<span class="boring">}</span></code></pre></pre>
|
||
</blockquote>
|
||
<h2 id="n1-problem-1"><a class="header" href="#n1-problem-1">N+1 problem</a></h2>
|
||
<p>Naturally, look-ahead machinery allows us to solve <a href="advanced/n_plus_1.html">the N+1 problem</a> by introspecting the requested fields and performing loading in batches eagerly, before actual resolving of those fields:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">#![allow(unused)]
|
||
</span><span class="boring">fn main() {
|
||
</span><span class="boring">extern crate anyhow;
|
||
</span><span class="boring">extern crate juniper;
|
||
</span><span class="boring">use std::collections::HashMap;
|
||
</span><span class="boring">use anyhow::anyhow;
|
||
</span><span class="boring">use juniper::{graphql_object, Executor, GraphQLObject, ScalarValue};
|
||
</span><span class="boring">
|
||
</span><span class="boring">type CultId = i32;
|
||
</span><span class="boring">type UserId = i32;
|
||
</span><span class="boring">
|
||
</span><span class="boring">struct Repository;
|
||
</span><span class="boring">
|
||
</span><span class="boring">impl juniper::Context for Repository {}
|
||
</span><span class="boring">
|
||
</span><span class="boring">impl Repository {
|
||
</span><span class="boring"> async fn load_cult_by_id(&self, cult_id: CultId) -> anyhow::Result<Option<Cult>> { unimplemented!() }
|
||
</span><span class="boring"> async fn load_cults_by_ids(&self, cult_ids: &[CultId]) -> anyhow::Result<HashMap<CultId, Cult>> { unimplemented!() }
|
||
</span><span class="boring"> async fn load_all_persons(&self) -> anyhow::Result<Vec<Person>> { unimplemented!() }
|
||
</span><span class="boring">}
|
||
</span><span class="boring">
|
||
</span><span class="boring">enum Either<L, R> {
|
||
</span><span class="boring"> Absent(L),
|
||
</span><span class="boring"> Loaded(R),
|
||
</span><span class="boring">}
|
||
</span><span class="boring">
|
||
</span>#[derive(Clone, GraphQLObject)]
|
||
struct Cult {
|
||
id: CultId,
|
||
name: String,
|
||
}
|
||
|
||
struct Person {
|
||
id: UserId,
|
||
name: String,
|
||
cult: Either<CultId, Cult>,
|
||
}
|
||
|
||
#[graphql_object]
|
||
#[graphql(context = Repository)]
|
||
impl Person {
|
||
fn id(&self) -> CultId {
|
||
self.id
|
||
}
|
||
|
||
fn name(&self) -> &str {
|
||
self.name.as_str()
|
||
}
|
||
|
||
async fn cult(&self, #[graphql(ctx)] repo: &Repository) -> anyhow::Result<Cult> {
|
||
match &self.cult {
|
||
Either::Loaded(cult) => Ok(cult.clone()),
|
||
Either::Absent(cult_id) => {
|
||
// Effectively performs the following SQL query:
|
||
// SELECT id, name FROM cults WHERE id = ${cult_id} LIMIT 1
|
||
repo.load_cult_by_id(*cult_id)
|
||
.await?
|
||
.ok_or_else(|| anyhow!("No cult exists for ID `{cult_id}`"))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
struct Query;
|
||
|
||
#[graphql_object]
|
||
#[graphql(context = Repository, scalar = S: ScalarValue)]
|
||
impl Query {
|
||
async fn persons<S: ScalarValue>(
|
||
#[graphql(ctx)] repo: &Repository,
|
||
executor: &Executor<'_, '_, Repository, S>,
|
||
) -> anyhow::Result<Vec<Person>> {
|
||
// Effectively performs the following SQL query:
|
||
// SELECT id, name, cult_id FROM persons
|
||
let mut persons = repo.load_all_persons().await?;
|
||
|
||
// If the `Person.cult` field has been requested.
|
||
if executor.look_ahead()
|
||
.children()
|
||
.iter()
|
||
.any(|sel| sel.field_original_name() == "cult")
|
||
{
|
||
// Gather `Cult.id`s to load eagerly.
|
||
let cult_ids = persons
|
||
.iter()
|
||
.filter_map(|p| {
|
||
match &p.cult {
|
||
Either::Absent(cult_id) => Some(*cult_id),
|
||
// If for some reason a `Cult` is already loaded,
|
||
// then just skip it.
|
||
Either::Loaded(_) => None,
|
||
}
|
||
})
|
||
.collect::<Vec<_>>();
|
||
|
||
// Load the necessary `Cult`s eagerly.
|
||
// Effectively performs the following SQL query:
|
||
// SELECT id, name FROM cults WHERE id IN (${cult_id1}, ${cult_id2}, ...)
|
||
let cults = repo.load_cults_by_ids(&cult_ids).await?;
|
||
|
||
// Populate `persons` with the loaded `Cult`s, so they do not perform
|
||
// any SQL queries on resolving.
|
||
for p in &mut persons {
|
||
let Either::Absent(cult_id) = &p.cult else { continue; };
|
||
p.cult = Either::Loaded(
|
||
cults.get(cult_id)
|
||
.ok_or_else(|| anyhow!("No cult exists for ID `{cult_id}`"))?
|
||
.clone(),
|
||
);
|
||
}
|
||
}
|
||
|
||
Ok(persons)
|
||
}
|
||
}
|
||
<span class="boring">}</span></code></pre></pre>
|
||
<p>And so, performing a <a href="advanced/n_plus_1.html">GraphQL query which lead to N+1 problem</a></p>
|
||
<pre><code class="language-graphql">query {
|
||
persons {
|
||
id
|
||
name
|
||
cult {
|
||
id
|
||
name
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>will lead to efficient <a href="https://en.wikipedia.org/wiki/SQL">SQL</a> queries, just as expected:</p>
|
||
<pre><code class="language-sql">SELECT id, name, cult_id FROM persons;
|
||
SELECT id, name FROM cults WHERE id IN (1, 2, 3, 4);
|
||
</code></pre>
|
||
<h2 id="more-features"><a class="header" href="#more-features">More features</a></h2>
|
||
<p>See more available look-ahead features in the API docs of the <a href="https://docs.rs/juniper/0.16.1/juniper/executor/struct.LookAheadSelection.html"><code>LookAheadSelection</code></a> and the <a href="https://docs.rs/juniper/0.16.1/juniper/executor/struct.LookAheadChildren.html"><code>LookAheadChildren</code></a>.</p>
|
||
<div style="break-before: page; page-break-before: always;"></div><h1 id="eager-loading"><a class="header" href="#eager-loading">Eager loading</a></h1>
|
||
<p>As a further evolution of the <a href="advanced/lookahead.html#n1-problem">dealing with the N+1 problem via look-ahead</a>, we may systematically remodel <a href="https://www.rust-lang.org">Rust</a> types mapping to <a href="https://graphql.org">GraphQL</a> ones in the way to encourage doing eager preloading of data for its <a href="https://spec.graphql.org/October2021#sec-Language.Fields">fields</a> and using the already preloaded data when resolving a particular <a href="https://spec.graphql.org/October2021#sec-Language.Fields">field</a>.</p>
|
||
<p>At the moment, this approach is represented with the <a href="https://docs.rs/juniper-eager-loading"><code>juniper-eager-loading</code></a> crate for <a href="https://docs.rs/juniper">Juniper</a>.</p>
|
||
<blockquote>
|
||
<p><strong>NOTE</strong>: Since this library requires <a href="https://docs.rs/juniper-from-schema"><code>juniper-from-schema</code></a>, it's best first to become familiar with it.</p>
|
||
</blockquote>
|
||
<!-- TODO: Provide example of solving the problem from "N+1 chapter" once `juniper-eager-loading` support the latest `juniper`. -->
|
||
<p>From <a href="https://docs.rs/juniper-eager-loading/latest/juniper_eager_loading#how-this-library-works-at-a-high-level">"How this library works at a high level"</a> and <a href="https://docs.rs/juniper-eager-loading/latest/juniper_eager_loading#a-real-example">"A real example"</a> sections of <a href="https://docs.rs/juniper-eager-loading"><code>juniper-eager-loading</code></a> documentation:</p>
|
||
<blockquote>
|
||
<h3 id="how-this-library-works-at-a-high-level"><a class="header" href="#how-this-library-works-at-a-high-level">How this library works at a high level</a></h3>
|
||
<p>If you have a GraphQL type like this</p>
|
||
<pre><code class="language-graphql">type User {
|
||
id: Int!
|
||
country: Country!
|
||
}
|
||
</code></pre>
|
||
<p>You might create the corresponding Rust model type like this:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">#![allow(unused)]
|
||
</span><span class="boring">fn main() {
|
||
</span>struct User {
|
||
id: i32,
|
||
country_id: i32,
|
||
}
|
||
<span class="boring">}</span></code></pre></pre>
|
||
<p>However this approach has one big issue. How are you going to resolve the field <code>User.country</code>
|
||
without doing a database query? All the resolver has access to is a <code>User</code> with a <code>country_id</code>
|
||
field. It can't get the country without loading it from the database...</p>
|
||
<p>Fundamentally these kinds of model structs don't work for eager loading with GraphQL. So
|
||
this library takes a different approach.</p>
|
||
<p>What if we created separate structs for the database models and the GraphQL models? Something
|
||
like this:</p>
|
||
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">fn main() {}
|
||
</span><span class="boring">
|
||
</span>mod models {
|
||
pub struct User {
|
||
id: i32,
|
||
country_id: i32
|
||
}
|
||
|
||
pub struct Country {
|
||
id: i32,
|
||
}
|
||
}
|
||
|
||
struct User {
|
||
user: models::User,
|
||
country: HasOne<Country>,
|
||
}
|
||
|
||
struct Country {
|
||
country: models::Country
|
||
}
|
||
|
||
enum HasOne<T> {
|
||
Loaded(T),
|
||
NotLoaded,
|
||
}</code></pre></pre>
|
||
<p>Now we're able to resolve the query with code like this:</p>
|
||
<ol>
|
||
<li>Load all the users (first query).</li>
|
||
<li>Map the users to a list of country ids.</li>
|
||
<li>Load all the countries with those ids (second query).</li>
|
||
<li>Pair up the users with the country with the correct id, so change <code>User.country</code> from
|
||
<code>HasOne::NotLoaded</code> to <code>HasOne::Loaded(matching_country)</code>.</li>
|
||
<li>When resolving the GraphQL field <code>User.country</code> simply return the loaded country.</li>
|
||
</ol>
|
||
<h3 id="a-real-example"><a class="header" href="#a-real-example">A real example</a></h3>
|
||
<pre><code class="language-rust ignore">use juniper::{Executor, FieldResult};
|
||
use juniper_eager_loading::{prelude::*, EagerLoading, HasOne};
|
||
use juniper_from_schema::graphql_schema;
|
||
use std::error::Error;
|
||
|
||
// Define our GraphQL schema.
|
||
graphql_schema! {
|
||
schema {
|
||
query: Query
|
||
}
|
||
|
||
type Query {
|
||
allUsers: [User!]! @juniper(ownership: "owned")
|
||
}
|
||
|
||
type User {
|
||
id: Int!
|
||
country: Country!
|
||
}
|
||
|
||
type Country {
|
||
id: Int!
|
||
}
|
||
}
|
||
|
||
// Our model types.
|
||
mod models {
|
||
use std::error::Error;
|
||
use juniper_eager_loading::LoadFrom;
|
||
|
||
#[derive(Clone)]
|
||
pub struct User {
|
||
pub id: i32,
|
||
pub country_id: i32
|
||
}
|
||
|
||
#[derive(Clone)]
|
||
pub struct Country {
|
||
pub id: i32,
|
||
}
|
||
|
||
// This trait is required for eager loading countries.
|
||
// It defines how to load a list of countries from a list of ids.
|
||
// Notice that `Context` is generic and can be whatever you want.
|
||
// It will normally be your Juniper context which would contain
|
||
// a database connection.
|
||
impl LoadFrom<i32> for Country {
|
||
type Error = Box<dyn Error>;
|
||
type Context = super::Context;
|
||
|
||
fn load(
|
||
employments: &[i32],
|
||
field_args: &(),
|
||
ctx: &Self::Context,
|
||
) -> Result<Vec<Self>, Self::Error> {
|
||
// ...
|
||
<span class="boring"> unimplemented!()
|
||
</span> }
|
||
}
|
||
}
|
||
|
||
// Our sample database connection type.
|
||
pub struct DbConnection;
|
||
|
||
impl DbConnection {
|
||
// Function that will load all the users.
|
||
fn load_all_users(&self) -> Vec<models::User> {
|
||
// ...
|
||
<span class="boring"> unimplemented!()
|
||
</span> }
|
||
}
|
||
|
||
// Our Juniper context type which contains a database connection.
|
||
pub struct Context {
|
||
db: DbConnection,
|
||
}
|
||
|
||
impl juniper::Context for Context {}
|
||
|
||
// Our GraphQL user type.
|
||
// `#[derive(EagerLoading)]` takes care of generating all the boilerplate code.
|
||
#[derive(Clone, EagerLoading)]
|
||
// You need to set the context and error type.
|
||
#[eager_loading(
|
||
context = Context,
|
||
error = Box<dyn Error>,
|
||
|
||
// These match the default so you wouldn't have to specify them
|
||
model = models::User,
|
||
id = i32,
|
||
root_model_field = user,
|
||
)]
|
||
pub struct User {
|
||
// This user model is used to resolve `User.id`
|
||
user: models::User,
|
||
|
||
// Setup a "has one" association between a user and a country.
|
||
//
|
||
// We could also have used `#[has_one(default)]` here.
|
||
#[has_one(
|
||
foreign_key_field = country_id,
|
||
root_model_field = country,
|
||
graphql_field = country,
|
||
)]
|
||
country: HasOne<Country>,
|
||
}
|
||
|
||
// And the GraphQL country type.
|
||
#[derive(Clone, EagerLoading)]
|
||
#[eager_loading(context = Context, error = Box<dyn Error>)]
|
||
pub struct Country {
|
||
country: models::Country,
|
||
}
|
||
|
||
// The root query GraphQL type.
|
||
pub struct Query;
|
||
|
||
impl QueryFields for Query {
|
||
// The resolver for `Query.allUsers`.
|
||
fn field_all_users(
|
||
&self,
|
||
executor: &Executor<'_, Context>,
|
||
trail: &QueryTrail<'_, User, Walked>,
|
||
) -> FieldResult<Vec<User>> {
|
||
let ctx = executor.context();
|
||
|
||
// Load the model users.
|
||
let user_models = ctx.db.load_all_users();
|
||
|
||
// Turn the model users into GraphQL users.
|
||
let mut users = User::from_db_models(&user_models);
|
||
|
||
// Perform the eager loading.
|
||
// `trail` is used to only eager load the fields that are requested. Because
|
||
// we're using `QueryTrail`s from "juniper_from_schema" it would be a compile
|
||
// error if we eager loaded associations that aren't requested in the query.
|
||
User::eager_load_all_children_for_each(&mut users, &user_models, ctx, trail)?;
|
||
|
||
Ok(users)
|
||
}
|
||
}
|
||
|
||
impl UserFields for User {
|
||
fn field_id(
|
||
&self,
|
||
executor: &Executor<'_, Context>,
|
||
) -> FieldResult<&i32> {
|
||
Ok(&self.user.id)
|
||
}
|
||
|
||
fn field_country(
|
||
&self,
|
||
executor: &Executor<'_, Context>,
|
||
trail: &QueryTrail<'_, Country, Walked>,
|
||
) -> FieldResult<&Country> {
|
||
// This will unwrap the country from the `HasOne` or return an error if the
|
||
// country wasn't loaded, or wasn't found in the database.
|
||
Ok(self.country.try_unwrap()?)
|
||
}
|
||
}
|
||
|
||
impl CountryFields for Country {
|
||
fn field_id(
|
||
&self,
|
||
executor: &Executor<'_, Context>,
|
||
) -> FieldResult<&i32> {
|
||
Ok(&self.country.id)
|
||
}
|
||
}
|
||
<span class="boring">
|
||
</span><span class="boring">fn main() {}</span></code></pre>
|
||
</blockquote>
|
||
<p>For more details, check out the <a href="https://docs.rs/juniper-eager-loading"><code>juniper-eager-loading</code> documentation</a>.</p>
|
||
<h2 id="full-example-1"><a class="header" href="#full-example-1">Full example</a></h2>
|
||
<p>For a full example using eager loading in <a href="https://docs.rs/juniper">Juniper</a> check out the <a href="https://github.com/davidpdrsn/graphql-app-example"><code>davidpdrsn/graphql-app-example</code> repository</a>.</p>
|
||
|
||
</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>
|
||
window.playground_copyable = true;
|
||
</script>
|
||
|
||
|
||
<script src="elasticlunr.min.js"></script>
|
||
<script src="mark.min.js"></script>
|
||
<script src="searcher.js"></script>
|
||
|
||
<script src="clipboard.min.js"></script>
|
||
<script src="highlight.js"></script>
|
||
<script src="book.js"></script>
|
||
|
||
<!-- Custom JS scripts -->
|
||
|
||
<script>
|
||
window.addEventListener('load', function() {
|
||
window.setTimeout(window.print, 100);
|
||
});
|
||
</script>
|
||
|
||
</div>
|
||
</body>
|
||
</html>
|