Compare commits
No commits in common. "gh-pages" and "master" have entirely different histories.
2
.codespellrc
Normal file
|
@ -0,0 +1,2 @@
|
|||
[codespell]
|
||||
ignore-words-list = crate
|
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug, needs-triage
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
(code example preferred)
|
||||
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
10
.github/ISSUE_TEMPLATE/support-request.md
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
name: Support Request
|
||||
about: Create a support request if you have a problem with using Juniper.
|
||||
title: ''
|
||||
labels: question
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
16
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: cargo
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: /juniper/
|
||||
schedule:
|
||||
interval: daily
|
429
.github/workflows/ci.yml
vendored
Normal file
|
@ -0,0 +1,429 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["master"]
|
||||
tags: ["juniper*"]
|
||||
pull_request:
|
||||
branches: ["master"]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
jobs:
|
||||
|
||||
################
|
||||
# Pull Request #
|
||||
################
|
||||
|
||||
pr:
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
needs:
|
||||
- bench
|
||||
- codespell
|
||||
- clippy
|
||||
- feature
|
||||
- msrv
|
||||
- release-check
|
||||
- rustfmt
|
||||
- test
|
||||
- test-book
|
||||
- wasm
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: true
|
||||
|
||||
|
||||
|
||||
|
||||
##########################
|
||||
# Linting and formatting #
|
||||
##########################
|
||||
|
||||
clippy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
|
||||
- run: make cargo.lint
|
||||
|
||||
codespell:
|
||||
name: codespell (Book)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: codespell-project/actions-codespell@v2
|
||||
with:
|
||||
path: book/
|
||||
|
||||
rustfmt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: rustfmt
|
||||
|
||||
- run: make cargo.fmt check=yes
|
||||
|
||||
|
||||
|
||||
|
||||
###########
|
||||
# Testing #
|
||||
###########
|
||||
|
||||
bench:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- run: cargo clippy -p juniper_benchmarks --benches -- -D warnings
|
||||
- run: cargo bench -p juniper_benchmarks
|
||||
|
||||
feature:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { feature: <none>, crate: juniper }
|
||||
- { feature: anyhow, crate: juniper }
|
||||
- { feature: "anyhow,backtrace", crate: juniper }
|
||||
- { feature: bigdecimal, crate: juniper }
|
||||
- { feature: bson, crate: juniper }
|
||||
- { feature: chrono, crate: juniper }
|
||||
- { feature: chrono-clock, crate: juniper }
|
||||
- { feature: chrono-tz, crate: juniper }
|
||||
- { feature: expose-test-schema, crate: juniper }
|
||||
- { feature: jiff, crate: juniper }
|
||||
- { feature: rust_decimal, crate: juniper }
|
||||
- { feature: schema-language, crate: juniper }
|
||||
- { feature: time, crate: juniper }
|
||||
- { feature: url, crate: juniper }
|
||||
- { feature: uuid, crate: juniper }
|
||||
- { feature: graphql-transport-ws, crate: juniper_graphql_ws }
|
||||
- { feature: graphql-ws, crate: juniper_graphql_ws }
|
||||
- { feature: <none>, crate: juniper_actix }
|
||||
- { feature: subscriptions, crate: juniper_actix }
|
||||
- { feature: <none>, crate: juniper_axum }
|
||||
- { feature: subscriptions, crate: juniper_axum }
|
||||
- { feature: <none>, crate: juniper_warp }
|
||||
- { feature: subscriptions, crate: juniper_warp }
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
- uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- run: cargo +nightly update -Z minimal-versions
|
||||
|
||||
- run: cargo check -p ${{ matrix.crate }} --no-default-features
|
||||
${{ matrix.feature != '<none>'
|
||||
&& format('--features {0}', matrix.feature)
|
||||
|| '' }}
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
|
||||
msrv:
|
||||
name: MSRV
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
msrv: ["1.75.0"]
|
||||
crate:
|
||||
- juniper_codegen
|
||||
- juniper
|
||||
- juniper_subscriptions
|
||||
- juniper_graphql_ws
|
||||
- juniper_actix
|
||||
- juniper_axum
|
||||
#- juniper_hyper
|
||||
- juniper_rocket
|
||||
- juniper_warp
|
||||
os:
|
||||
- ubuntu
|
||||
- macOS
|
||||
- windows
|
||||
include:
|
||||
- { msrv: "1.79.0", crate: "juniper_hyper", os: "ubuntu" }
|
||||
- { msrv: "1.79.0", crate: "juniper_hyper", os: "macOS" }
|
||||
- { msrv: "1.79.0", crate: "juniper_hyper", os: "windows" }
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
- uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.msrv }}
|
||||
|
||||
- run: cargo +nightly update -Z minimal-versions
|
||||
|
||||
- run: make test.cargo crate=${{ matrix.crate }}
|
||||
|
||||
package:
|
||||
name: check (package)
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/juniper') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Parse crate name and version from Git tag
|
||||
id: tag
|
||||
uses: actions-ecosystem/action-regex-match@v2
|
||||
with:
|
||||
text: ${{ github.ref }}
|
||||
regex: '^refs/tags/(([a-z_]+)-v([0-9]+\.[0-9]+\.[0-9]+(-.+)?))$'
|
||||
|
||||
- run: cargo package -p ${{ steps.tag.outputs.group2 }} --all-features
|
||||
|
||||
test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
crate:
|
||||
- juniper_codegen
|
||||
- juniper
|
||||
- juniper_subscriptions
|
||||
- juniper_graphql_ws
|
||||
- juniper_integration_tests
|
||||
- juniper_codegen_tests
|
||||
- juniper_actix
|
||||
- juniper_axum
|
||||
- juniper_hyper
|
||||
- juniper_rocket
|
||||
- juniper_warp
|
||||
os:
|
||||
- ubuntu
|
||||
- macOS
|
||||
- windows
|
||||
toolchain:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
exclude:
|
||||
- crate: juniper_codegen_tests
|
||||
toolchain: beta
|
||||
- crate: juniper_codegen_tests
|
||||
toolchain: nightly
|
||||
- crate: juniper_codegen_tests
|
||||
os: macOS
|
||||
- crate: juniper_codegen_tests
|
||||
os: windows
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
components: rust-src
|
||||
|
||||
- run: cargo install cargo-careful
|
||||
if: ${{ matrix.toolchain == 'nightly' }}
|
||||
|
||||
- run: make test.cargo crate=${{ matrix.crate }}
|
||||
careful=${{ (matrix.toolchain == 'nightly' && 'yes')
|
||||
|| 'no' }}
|
||||
|
||||
test-book:
|
||||
name: test (Book)
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu
|
||||
- macOS
|
||||
- windows
|
||||
toolchain:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- uses: peaceiris/actions-mdbook@v2
|
||||
|
||||
- run: make test.book
|
||||
|
||||
wasm:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
crate:
|
||||
- juniper
|
||||
- juniper_axum
|
||||
target:
|
||||
- wasm32-unknown-unknown
|
||||
- wasm32-wasip1
|
||||
toolchain:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
target: ${{ matrix.target }}
|
||||
|
||||
- name: Switch Cargo workspace to `resolver = "2"`
|
||||
run: sed -i 's/resolver = "1"/resolver = "2"/' Cargo.toml
|
||||
|
||||
- run: cargo check --target ${{ matrix.target }} -p ${{ matrix.crate }}
|
||||
${{ (matrix.crate == 'juniper' && matrix.target == 'wasm32-unknown-unknown')
|
||||
&& '--features js'
|
||||
|| '' }}
|
||||
|
||||
|
||||
|
||||
|
||||
#############
|
||||
# Releasing #
|
||||
#############
|
||||
|
||||
publish:
|
||||
name: publish (crates.io)
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/juniper') }}
|
||||
needs: ["release-github"]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Parse crate name and version from Git tag
|
||||
id: tag
|
||||
uses: actions-ecosystem/action-regex-match@v2
|
||||
with:
|
||||
text: ${{ github.ref }}
|
||||
regex: '^refs/tags/(([a-z_]+)-v([0-9]+\.[0-9]+\.[0-9]+(-.+)?))$'
|
||||
|
||||
- run: cargo publish -p ${{ steps.tag.outputs.group2 }} --all-features
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CRATESIO_TOKEN }}
|
||||
|
||||
release-check:
|
||||
name: check (release)
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/juniper') }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
crate:
|
||||
- juniper_codegen
|
||||
- juniper
|
||||
- juniper_subscriptions
|
||||
- juniper_graphql_ws
|
||||
- juniper_actix
|
||||
- juniper_axum
|
||||
- juniper_hyper
|
||||
- juniper_rocket
|
||||
- juniper_warp
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- run: cargo install cargo-release
|
||||
|
||||
- run: make cargo.release crate=${{ matrix.crate }} ver=minor
|
||||
exec=no install=no
|
||||
|
||||
release-github:
|
||||
name: release (GitHub)
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/juniper') }}
|
||||
needs:
|
||||
- bench
|
||||
- clippy
|
||||
- codespell
|
||||
- feature
|
||||
- msrv
|
||||
- package
|
||||
- rustfmt
|
||||
- test
|
||||
- test-book
|
||||
- wasm
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Parse crate name and version from Git tag
|
||||
id: tag
|
||||
uses: actions-ecosystem/action-regex-match@v2
|
||||
with:
|
||||
text: ${{ github.ref }}
|
||||
regex: '^refs/tags/(([a-z_]+)-v([0-9]+\.[0-9]+\.[0-9]+(-.+)?))$'
|
||||
- name: Verify release version matches crate's Cargo manifest
|
||||
run: |
|
||||
test "${{ steps.tag.outputs.group3 }}" \
|
||||
== "$(grep -m1 'version = "' \
|
||||
${{ steps.tag.outputs.group2 }}/Cargo.toml \
|
||||
| cut -d '"' -f2)"
|
||||
|
||||
- name: Parse CHANGELOG link
|
||||
id: changelog
|
||||
run: echo "link=${{ github.server_url }}/${{ github.repository }}/blob/${{ steps.tag.outputs.group1 }}/${{ steps.tag.outputs.group2 }}/CHANGELOG.md#$(sed -n '/^## \[${{ steps.tag.outputs.group3 }}\]/{s/^## \[\(.*\)\][^0-9]*\([0-9].*\)/\1--\2/;s/[^0-9a-z-]*//g;p;}' ${{ steps.tag.outputs.group2 }}/CHANGELOG.md)"
|
||||
>> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: ${{ steps.tag.outputs.group2 }} ${{ steps.tag.outputs.group3 }}
|
||||
body: |
|
||||
[API docs](https://docs.rs/${{ steps.tag.outputs.group2 }}/${{ steps.tag.outputs.group3 }})
|
||||
[Changelog](${{ steps.changelog.outputs.link }})
|
||||
prerelease: ${{ contains(steps.tag.outputs.group3, '-') }}
|
||||
|
||||
|
||||
|
||||
|
||||
##########
|
||||
# Deploy #
|
||||
##########
|
||||
|
||||
deploy-book:
|
||||
name: deploy (Book)
|
||||
if: ${{ github.ref == 'refs/heads/master'
|
||||
|| startsWith(github.ref, 'refs/tags/juniper') }}
|
||||
needs: ["codespell", "test", "test-book"]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: peaceiris/actions-mdbook@v2
|
||||
|
||||
- run: make book.build out=gh-pages${{ (github.ref == 'refs/heads/master'
|
||||
&& '/master')
|
||||
|| '' }}
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
keep_files: true
|
||||
publish_dir: book/gh-pages
|
7
.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
/.idea/
|
||||
/.vscode/
|
||||
/*.iml
|
||||
.DS_Store
|
||||
|
||||
/Cargo.lock
|
||||
/target/
|
|
@ -1 +0,0 @@
|
|||
This file makes sure that Github Pages doesn't process mdBook's output.
|
2
.rustfmt.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
imports_granularity = "Crate"
|
||||
use_field_init_shorthand = true
|
218
404.html
|
@ -1,218 +0,0 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en" class="light" dir="ltr">
|
||||
<head>
|
||||
<!-- Book generated using mdBook -->
|
||||
<meta charset="UTF-8">
|
||||
<title>Page not found - Juniper Book</title>
|
||||
<base href="/">
|
||||
|
||||
|
||||
<!-- 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 -->
|
||||
|
||||
</head>
|
||||
<body class="sidebar-visible no-js">
|
||||
<div id="body-container">
|
||||
<!-- Provide site root to javascript -->
|
||||
<script>
|
||||
var path_to_root = "";
|
||||
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
|
||||
</script>
|
||||
|
||||
<!-- 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; }
|
||||
var html = document.querySelector('html');
|
||||
html.classList.remove('light')
|
||||
html.classList.add(theme);
|
||||
var body = document.querySelector('body');
|
||||
body.classList.remove('no-js')
|
||||
body.classList.add('js');
|
||||
</script>
|
||||
|
||||
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
|
||||
|
||||
<!-- Hide / unhide sidebar before it is displayed -->
|
||||
<script>
|
||||
var body = document.querySelector('body');
|
||||
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';
|
||||
body.classList.remove('sidebar-visible');
|
||||
body.classList.add("sidebar-" + sidebar);
|
||||
</script>
|
||||
|
||||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||
<div class="sidebar-scrollbox">
|
||||
<ol class="chapter"><li class="chapter-item expanded "><a href="introduction.html"><strong aria-hidden="true">1.</strong> Introduction</a></li><li class="chapter-item expanded "><a href="quickstart.html"><strong aria-hidden="true">2.</strong> Quickstart</a></li><li class="chapter-item expanded "><a href="types/index.html"><strong aria-hidden="true">3.</strong> Type system</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="types/objects/index.html"><strong aria-hidden="true">3.1.</strong> Objects</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="types/objects/complex_fields.html"><strong aria-hidden="true">3.1.1.</strong> Complex fields</a></li><li class="chapter-item expanded "><a href="types/objects/context.html"><strong aria-hidden="true">3.1.2.</strong> Context</a></li><li class="chapter-item expanded "><a href="types/objects/error/index.html"><strong aria-hidden="true">3.1.3.</strong> Error handling</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="types/objects/error/field.html"><strong aria-hidden="true">3.1.3.1.</strong> Field errors</a></li><li class="chapter-item expanded "><a href="types/objects/error/schema.html"><strong aria-hidden="true">3.1.3.2.</strong> Schema errors</a></li></ol></li><li class="chapter-item expanded "><a href="types/objects/generics.html"><strong aria-hidden="true">3.1.4.</strong> Generics</a></li></ol></li><li class="chapter-item expanded "><a href="types/interfaces.html"><strong aria-hidden="true">3.2.</strong> Interfaces</a></li><li class="chapter-item expanded "><a href="types/unions.html"><strong aria-hidden="true">3.3.</strong> Unions</a></li><li class="chapter-item expanded "><a href="types/enums.html"><strong aria-hidden="true">3.4.</strong> Enums</a></li><li class="chapter-item expanded "><a href="types/input_objects.html"><strong aria-hidden="true">3.5.</strong> Input objects</a></li><li class="chapter-item expanded "><a href="types/scalars.html"><strong aria-hidden="true">3.6.</strong> Scalars</a></li></ol></li><li class="chapter-item expanded "><a href="schema/index.html"><strong aria-hidden="true">4.</strong> Schema</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="schema/subscriptions.html"><strong aria-hidden="true">4.1.</strong> Subscriptions</a></li><li class="chapter-item expanded "><a href="schema/introspection.html"><strong aria-hidden="true">4.2.</strong> Introspection</a></li></ol></li><li class="chapter-item expanded "><a href="serve/index.html"><strong aria-hidden="true">5.</strong> Serving</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="serve/batching.html"><strong aria-hidden="true">5.1.</strong> Batching</a></li></ol></li><li class="chapter-item expanded "><a href="advanced/index.html"><strong aria-hidden="true">6.</strong> Advanced Topics</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="advanced/implicit_and_explicit_null.html"><strong aria-hidden="true">6.1.</strong> Implicit and explicit null</a></li><li class="chapter-item expanded "><a href="advanced/n_plus_1.html"><strong aria-hidden="true">6.2.</strong> N+1 problem</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="advanced/dataloader.html"><strong aria-hidden="true">6.2.1.</strong> DataLoader</a></li><li class="chapter-item expanded "><a href="advanced/lookahead.html"><strong aria-hidden="true">6.2.2.</strong> Look-ahead</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="advanced/eager_loading.html"><strong aria-hidden="true">6.2.2.1.</strong> Eager loading</a></li></ol></li></ol></li></ol></li></ol>
|
||||
</div>
|
||||
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
|
||||
<div class="sidebar-resize-indicator"></div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Track and set sidebar scroll position -->
|
||||
<script>
|
||||
var sidebarScrollbox = document.querySelector('#sidebar .sidebar-scrollbox');
|
||||
sidebarScrollbox.addEventListener('click', function(e) {
|
||||
if (e.target.tagName === 'A') {
|
||||
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
|
||||
}
|
||||
}, { passive: true });
|
||||
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
|
||||
sessionStorage.removeItem('sidebar-scroll');
|
||||
if (sidebarScrollTop) {
|
||||
// preserve sidebar scroll position when navigating via links within sidebar
|
||||
sidebarScrollbox.scrollTop = sidebarScrollTop;
|
||||
} else {
|
||||
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
|
||||
var activeSection = document.querySelector('#sidebar .active');
|
||||
if (activeSection) {
|
||||
activeSection.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<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="document-not-found-404"><a class="header" href="#document-not-found-404">Document not found (404)</a></h1>
|
||||
<p>This URL is invalid, sorry. Please use the navigation bar or search to continue.</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 -->
|
||||
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
36
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,36 @@
|
|||
# Juniper Contribution Guide
|
||||
|
||||
Juniper is always looking for new contributors, so don't be afraid to jump in and help!
|
||||
The maintainers are happy to provide guidance if required.
|
||||
|
||||
To get started, you can look for [issues with the "help wanted" label](https://github.com/graphql-rust/juniper/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22).
|
||||
|
||||
## PR Checklist
|
||||
|
||||
Before submitting a PR, you should follow these steps to prevent redundant churn or CI failures:
|
||||
|
||||
- [ ] Ensure proper formatting
|
||||
- [ ] Run all tests
|
||||
- [ ] Update the CHANGELOG
|
||||
|
||||
### Ensure proper formatting
|
||||
|
||||
Consistent formatting is enforced on the CI.
|
||||
|
||||
Before you submit your PR, you should run `cargo +nightly fmt --all` in the root directory (or use the `make fmt` shortcut).
|
||||
|
||||
Formatting should be run on the **nightly** compiler.
|
||||
|
||||
### Run all tests
|
||||
|
||||
To run all available tests, including verifying the code examples in the book:
|
||||
|
||||
1. Run `cargo test` in the root directory.
|
||||
2. Run `make test.book` in the root directory.
|
||||
|
||||
### Update the CHANGELOG
|
||||
|
||||
Add your changes to the relevant changelog if they affect users in any way.
|
||||
Each sub-crate has it's own CHANGELOG.md.
|
||||
|
||||
Your changes should be added to a `[master]` section on top of the file.
|
16
Cargo.toml
Normal file
|
@ -0,0 +1,16 @@
|
|||
[workspace]
|
||||
resolver = "1" # unifying Cargo features asap for Book tests
|
||||
members = [
|
||||
"benches",
|
||||
"juniper_codegen",
|
||||
"juniper",
|
||||
"juniper_hyper",
|
||||
"juniper_rocket",
|
||||
"juniper_subscriptions",
|
||||
"juniper_graphql_ws",
|
||||
"juniper_warp",
|
||||
"juniper_actix",
|
||||
"juniper_axum",
|
||||
"tests/codegen",
|
||||
"tests/integration",
|
||||
]
|
4
FontAwesome/css/font-awesome.css
vendored
Before Width: | Height: | Size: 434 KiB |
29
LICENSE
Normal file
|
@ -0,0 +1,29 @@
|
|||
BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2016-2025 Magnus Hallin <mhallin@fastmail.com>,
|
||||
Christoph Herzog <chris@theduke.at>,
|
||||
Christian Legnitto <christian@legnitto.com>,
|
||||
Ilya Solovyiov <ilya.solovyiov@gmail.com>,
|
||||
Kai Ren <tyranron@gmail.com>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
199
Makefile
Normal file
|
@ -0,0 +1,199 @@
|
|||
###############################
|
||||
# Common defaults/definitions #
|
||||
###############################
|
||||
|
||||
comma := ,
|
||||
|
||||
# Checks two given strings for equality.
|
||||
eq = $(if $(or $(1),$(2)),$(and $(findstring $(1),$(2)),\
|
||||
$(findstring $(2),$(1))),1)
|
||||
|
||||
|
||||
|
||||
|
||||
###########
|
||||
# Aliases #
|
||||
###########
|
||||
|
||||
book: book.build
|
||||
|
||||
|
||||
codespell: book.codespell
|
||||
|
||||
|
||||
fmt: cargo.fmt
|
||||
|
||||
|
||||
lint: cargo.lint
|
||||
|
||||
|
||||
test: test.cargo
|
||||
|
||||
|
||||
release: cargo.release
|
||||
|
||||
|
||||
|
||||
|
||||
##################
|
||||
# Cargo commands #
|
||||
##################
|
||||
|
||||
# Format Rust sources with rustfmt.
|
||||
#
|
||||
# Usage:
|
||||
# make cargo.fmt [check=(no|yes)]
|
||||
|
||||
cargo.fmt:
|
||||
cargo +nightly fmt --all $(if $(call eq,$(check),yes),-- --check,)
|
||||
|
||||
|
||||
# Lint Rust sources with Clippy.
|
||||
#
|
||||
# Usage:
|
||||
# make cargo.lint
|
||||
|
||||
cargo.lint:
|
||||
cargo clippy --workspace --all-features -- -D warnings
|
||||
cargo clippy -p juniper_integration_tests --tests --all-features -- -D warnings
|
||||
|
||||
|
||||
# Release Rust crate.
|
||||
#
|
||||
# Read more about bump levels here:
|
||||
# https://github.com/crate-ci/cargo-release/blob/master/docs/reference.md#bump-level
|
||||
#
|
||||
# Usage:
|
||||
# make cargo.release crate=<crate-name> [ver=(release|<bump-level>)]
|
||||
# ([exec=no]|exec=yes [push=(yes|no)])
|
||||
# [install=(yes|no)]
|
||||
|
||||
cargo.release:
|
||||
ifneq ($(install),no)
|
||||
cargo install cargo-release
|
||||
endif
|
||||
cargo release -p $(crate) --all-features \
|
||||
$(if $(call eq,$(exec),yes),\
|
||||
--no-publish $(if $(call eq,$(push),no),--no-push,) --execute,\
|
||||
-v $(if $(call eq,$(CI),),,--no-publish)) \
|
||||
$(or $(ver),release)
|
||||
|
||||
|
||||
cargo.test: test.cargo
|
||||
|
||||
|
||||
|
||||
|
||||
####################
|
||||
# Testing commands #
|
||||
####################
|
||||
|
||||
# Run Rust tests of Book.
|
||||
#
|
||||
# Usage:
|
||||
# make test.book [clean=(no|yes)]
|
||||
|
||||
test.book:
|
||||
ifeq ($(clean),yes)
|
||||
cargo clean
|
||||
endif
|
||||
$(eval target := $(strip $(shell cargo -vV | sed -n 's/host: //p')))
|
||||
cargo build --all-features
|
||||
mdbook test book -L target/debug/deps $(strip \
|
||||
$(if $(call eq,$(findstring windows,$(target)),),,\
|
||||
$(shell cargo metadata -q \
|
||||
| jq -r '.packages[] | select(.name == "windows_$(word 1,$(subst -, ,$(target)))_$(word 4,$(subst -, ,$(target)))") | .manifest_path' \
|
||||
| sed -e "s/^/-L '/" -e 's/Cargo.toml/lib/' -e "s/$$/'/" )))
|
||||
|
||||
|
||||
# Run Rust tests of project crates.
|
||||
#
|
||||
# Usage:
|
||||
# make test.cargo [crate=<crate-name>] [careful=(no|yes)]
|
||||
|
||||
test.cargo:
|
||||
ifeq ($(careful),yes)
|
||||
ifeq ($(shell cargo install --list | grep cargo-careful),)
|
||||
cargo install cargo-careful
|
||||
endif
|
||||
ifeq ($(shell rustup component list --toolchain=nightly \
|
||||
| grep 'rust-src (installed)'),)
|
||||
rustup component add --toolchain=nightly rust-src
|
||||
endif
|
||||
endif
|
||||
cargo $(if $(call eq,$(careful),yes),+nightly careful,) \
|
||||
test $(if $(call eq,$(crate),),--workspace,-p $(crate)) --all-features
|
||||
|
||||
|
||||
|
||||
|
||||
#################
|
||||
# Book commands #
|
||||
#################
|
||||
|
||||
# Build Book.
|
||||
#
|
||||
# Usage:
|
||||
# make book.build [out=<dir>]
|
||||
|
||||
book.build:
|
||||
mdbook build book/ $(if $(call eq,$(out),),,-d $(out))
|
||||
|
||||
|
||||
# Spellcheck Book.
|
||||
#
|
||||
# Usage:
|
||||
# make book.codespell [fix=(no|yes)]
|
||||
|
||||
book.codespell:
|
||||
codespell book/ $(if $(call eq,$(fix),yes),--write-changes,)
|
||||
|
||||
|
||||
# Serve Book on some port.
|
||||
#
|
||||
# Usage:
|
||||
# make book.serve [port=(3000|<port>)]
|
||||
|
||||
book.serve:
|
||||
mdbook serve book/ -p=$(or $(port),3000)
|
||||
|
||||
|
||||
|
||||
|
||||
######################
|
||||
# Forwarded commands #
|
||||
######################
|
||||
|
||||
# Download and prepare actual version of GraphiQL static files, used for
|
||||
# integrating it.
|
||||
#
|
||||
# Usage:
|
||||
# make graphiql
|
||||
|
||||
graphiql:
|
||||
@cd juniper/ && \
|
||||
make graphiql
|
||||
|
||||
|
||||
# Download and prepare actual version of GraphQL Playground static files, used
|
||||
# for integrating it.
|
||||
#
|
||||
# Usage:
|
||||
# make graphql-playground
|
||||
|
||||
graphql-playground:
|
||||
@cd juniper/ && \
|
||||
make graphql-playground
|
||||
|
||||
|
||||
|
||||
|
||||
##################
|
||||
# .PHONY section #
|
||||
##################
|
||||
|
||||
.PHONY: book codespell fmt lint release test \
|
||||
book.build book.codespell book.serve \
|
||||
cargo.fmt cargo.lint cargo.release cargo.test \
|
||||
graphiql graphql-playground \
|
||||
test.book test.cargo
|
126
README.md
Normal file
|
@ -0,0 +1,126 @@
|
|||
<img src="https://github.com/graphql-rust/juniper/raw/master/assets/logo/juniper-dark-word.png" alt="Juniper" width="500" />
|
||||
|
||||
> GraphQL server library for Rust
|
||||
|
||||
[![Build Status](https://dev.azure.com/graphql-rust/GraphQL%20Rust/_apis/build/status/graphql-rust.juniper)](https://dev.azure.com/graphql-rust/GraphQL%20Rust/_build/latest?definitionId=1)
|
||||
[![codecov](https://codecov.io/gh/graphql-rust/juniper/branch/master/graph/badge.svg)](https://codecov.io/gh/graphql-rust/juniper)
|
||||
[![Crates.io](https://img.shields.io/crates/v/juniper.svg?maxAge=2592000)](https://crates.io/crates/juniper)
|
||||
[![Gitter chat](https://badges.gitter.im/juniper-graphql/gitter.svg)](https://gitter.im/juniper-graphql/Lobby)
|
||||
|
||||
---
|
||||
|
||||
[GraphQL][graphql] is a data query language developed by Facebook intended to
|
||||
serve mobile and web application frontends.
|
||||
|
||||
_Juniper_ makes it possible to write GraphQL servers in Rust that are
|
||||
type-safe and blazingly fast. We also try to make declaring and resolving
|
||||
GraphQL schemas as convenient as Rust will allow.
|
||||
|
||||
Juniper does not include a web server - instead it provides building blocks to
|
||||
make integration with existing servers straightforward. It optionally provides a
|
||||
pre-built integration for the [Actix][actix], [Hyper][hyper], [Rocket], and [Warp][warp] frameworks, including
|
||||
embedded [Graphiql][graphiql] and [GraphQL Playground][playground] for easy debugging.
|
||||
|
||||
- [Cargo crate](https://crates.io/crates/juniper)
|
||||
- [API Reference][docsrs]
|
||||
- [Book][book]: Guides and Examples ([current][book] | [master][book_master])
|
||||
|
||||
The book is also available for the master branch and older versions published after 0.11.1. See the [book index][book_index].
|
||||
|
||||
|
||||
## Getting Started
|
||||
|
||||
The best place to get started is the [Juniper Book][book], which contains
|
||||
guides with plenty of examples, covering all features of Juniper. (very much WIP)
|
||||
|
||||
To get started quickly and get a feel for Juniper, check out the
|
||||
[Quickstart][book_quickstart] section.
|
||||
|
||||
For specific information about macros, types and the Juniper api, the
|
||||
[API Reference][docsrs] is the best place to look.
|
||||
|
||||
You can also check out the [Star Wars schema][test_schema_rs] to see a complex
|
||||
example including polymorphism with traits and interfaces.
|
||||
For an example of web framework integration,
|
||||
see the [actix][actix_examples], [axum][axum_examples], [hyper][hyper_examples], [rocket][rocket_examples], and [warp][warp_examples] examples folders.
|
||||
|
||||
## Features
|
||||
|
||||
Juniper supports the full GraphQL query language according to the
|
||||
[specification (October 2021)][graphql_spec], including interfaces, unions, schema
|
||||
introspection, and validations. It can also output the schema in the [GraphQL Schema Language][schema_language].
|
||||
|
||||
As an exception to other GraphQL libraries for other languages, Juniper builds
|
||||
non-null types by default. A field of type `Vec<Episode>` will be converted into
|
||||
`[Episode!]!`. The corresponding Rust type for e.g. `[Episode]` would be
|
||||
`Option<Vec<Option<Episode>>>`.
|
||||
|
||||
Juniper is agnostic to serialization format and network transport.
|
||||
|
||||
Juniper supports both asynchronous and synchronous execution using `execute()` and `execute_sync()` respectively. Asynchronous execution is runtime agnostic.
|
||||
|
||||
Juniper follows a [code-first approach][schema_approach] to defining GraphQL schemas. If you would like to use a [schema-first approach][schema_approach] instead, consider [juniper-from-schema][] for generating code from a schema file.
|
||||
|
||||
## Integrations
|
||||
|
||||
### Data types
|
||||
|
||||
Juniper has automatic integration with some very common Rust crates to make
|
||||
building schemas a breeze. The types from these crates will be usable in
|
||||
your Schemas automatically.
|
||||
|
||||
- [uuid][uuid]
|
||||
- [url][url]
|
||||
- [chrono][chrono]
|
||||
- [chrono-tz][chrono-tz]
|
||||
- [jiff][jiff]
|
||||
- [time][time]
|
||||
- [bson][bson]
|
||||
|
||||
### Web Frameworks
|
||||
|
||||
- [actix][actix]
|
||||
- [axum][axum]
|
||||
- [hyper][hyper]
|
||||
- [rocket][rocket]
|
||||
- [warp][warp]
|
||||
|
||||
## Guides & Examples
|
||||
|
||||
- [Juniper + actix-web example](https://github.com/actix/examples/tree/master/graphql/juniper)
|
||||
|
||||
## API Stability
|
||||
|
||||
Juniper has not reached 1.0 yet, thus some API instability should be expected.
|
||||
|
||||
[actix]: https://actix.rs/
|
||||
[axum]: https://docs.rs/axum
|
||||
[graphql]: http://graphql.org
|
||||
[graphiql]: https://github.com/graphql/graphiql
|
||||
[playground]: https://github.com/prisma/graphql-playground
|
||||
[graphql_spec]: https://spec.graphql.org/October2021
|
||||
[schema_language]: https://graphql.org/learn/schema/#type-language
|
||||
[schema_approach]: https://blog.logrocket.com/code-first-vs-schema-first-development-graphql/
|
||||
[test_schema_rs]: https://github.com/graphql-rust/juniper/blob/master/juniper/src/tests/fixtures/starwars/schema.rs
|
||||
[tokio]: https://github.com/tokio-rs/tokio
|
||||
[actix_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_actix/examples
|
||||
[axum_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_axum/examples
|
||||
[hyper_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_hyper/examples
|
||||
[rocket_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_rocket/examples
|
||||
[hyper]: https://hyper.rs
|
||||
[rocket]: https://rocket.rs
|
||||
[book]: https://graphql-rust.github.io/juniper
|
||||
[book_master]: https://graphql-rust.github.io/juniper/master
|
||||
[book_index]: https://graphql-rust.github.io/juniper
|
||||
[book_quickstart]: https://graphql-rust.github.io/juniper/quickstart.html
|
||||
[docsrs]: https://docs.rs/juniper
|
||||
[warp]: https://github.com/seanmonstar/warp
|
||||
[warp_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_warp/examples
|
||||
[uuid]: https://crates.io/crates/uuid
|
||||
[url]: https://crates.io/crates/url
|
||||
[chrono]: https://crates.io/crates/chrono
|
||||
[chrono-tz]: https://crates.io/crates/chrono-tz
|
||||
[jiff]: https://crates.io/crates/jiff
|
||||
[time]: https://crates.io/crates/time
|
||||
[bson]: https://crates.io/crates/bson
|
||||
[juniper-from-schema]: https://github.com/davidpdrsn/juniper-from-schema
|
59
RELEASING.md
Normal file
|
@ -0,0 +1,59 @@
|
|||
Releasing new crate versions
|
||||
============================
|
||||
|
||||
Releasing of [workspace] crates of this project is performed by pushing the Git release tag (having `<crate-name>@<version>` format), following by the [CI pipeline] creating a [GitHub release] and publishing the crate to [crates.io].
|
||||
|
||||
> __WARNING__: Only one [workspace] crate may be released at a time. So, if you need to release multiple [workspace] crates, do this sequentially.
|
||||
|
||||
|
||||
|
||||
|
||||
## Prerequisites
|
||||
|
||||
We use [`cargo-release`] to automate crate releases. You will need to install it locally:
|
||||
```bash
|
||||
cargo install cargo-release
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Preparing
|
||||
|
||||
To produce a new release a [workspace] crate, perform the following steps:
|
||||
|
||||
1. Check its `CHANGELOG.md` file to be complete and correctly formatted. The section for the new release __should start__ with `## master` header. Commit any changes you've made.
|
||||
|
||||
2. Determine a new release [bump level] (`patch`, `minor`, `major`, or default `release`).
|
||||
|
||||
3. Run the release process in dry-run mode and check the produced diffs to be made in the returned output.
|
||||
```bash
|
||||
make release crate=juniper ver=minor
|
||||
```
|
||||
|
||||
4. (Optional) Not everything may be captured in dry-run mode. It may be a good idea to run a local test, without pushing the created Git commit and tag.
|
||||
```bash
|
||||
make release crate=juniper ver=minor exec=yes push=no
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Executing
|
||||
|
||||
Once everything is prepared and checked, just execute the releasing process:
|
||||
```bash
|
||||
make release crate=juniper ver=minor exec=yes
|
||||
```
|
||||
|
||||
Once the [CI pipeline] for the pushed Git tag successfully finishes, the crate is fully released.
|
||||
|
||||
|
||||
|
||||
|
||||
[`cargo-release`]: https://crates.io/crates/cargo-release
|
||||
[CI pipeline]: /../../blob/master/.github/workflows/ci.yml
|
||||
[crates.io]: https://crates.io
|
||||
[GitHub release]: https://docs.github.com/repositories/releasing-projects-on-github/about-releases
|
||||
[release level]: https://github.com/crate-ci/cargo-release/blob/master/docs/reference.md#bump-level
|
||||
[workspace]: https://doc.rust-lang.org/cargo/reference/workspaces.html
|
|
@ -1,390 +0,0 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en" class="light" dir="ltr">
|
||||
<head>
|
||||
<!-- Book generated using mdBook -->
|
||||
<meta charset="UTF-8">
|
||||
<title>DataLoader - Juniper Book</title>
|
||||
|
||||
|
||||
<!-- 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 -->
|
||||
|
||||
</head>
|
||||
<body class="sidebar-visible no-js">
|
||||
<div id="body-container">
|
||||
<!-- Provide site root to javascript -->
|
||||
<script>
|
||||
var path_to_root = "../";
|
||||
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
|
||||
</script>
|
||||
|
||||
<!-- 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; }
|
||||
var html = document.querySelector('html');
|
||||
html.classList.remove('light')
|
||||
html.classList.add(theme);
|
||||
var body = document.querySelector('body');
|
||||
body.classList.remove('no-js')
|
||||
body.classList.add('js');
|
||||
</script>
|
||||
|
||||
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
|
||||
|
||||
<!-- Hide / unhide sidebar before it is displayed -->
|
||||
<script>
|
||||
var body = document.querySelector('body');
|
||||
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';
|
||||
body.classList.remove('sidebar-visible');
|
||||
body.classList.add("sidebar-" + sidebar);
|
||||
</script>
|
||||
|
||||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||
<div class="sidebar-scrollbox">
|
||||
<ol class="chapter"><li class="chapter-item expanded "><a href="../introduction.html"><strong aria-hidden="true">1.</strong> Introduction</a></li><li class="chapter-item expanded "><a href="../quickstart.html"><strong aria-hidden="true">2.</strong> Quickstart</a></li><li class="chapter-item expanded "><a href="../types/index.html"><strong aria-hidden="true">3.</strong> Type system</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../types/objects/index.html"><strong aria-hidden="true">3.1.</strong> Objects</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../types/objects/complex_fields.html"><strong aria-hidden="true">3.1.1.</strong> Complex fields</a></li><li class="chapter-item expanded "><a href="../types/objects/context.html"><strong aria-hidden="true">3.1.2.</strong> Context</a></li><li class="chapter-item expanded "><a href="../types/objects/error/index.html"><strong aria-hidden="true">3.1.3.</strong> Error handling</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../types/objects/error/field.html"><strong aria-hidden="true">3.1.3.1.</strong> Field errors</a></li><li class="chapter-item expanded "><a href="../types/objects/error/schema.html"><strong aria-hidden="true">3.1.3.2.</strong> Schema errors</a></li></ol></li><li class="chapter-item expanded "><a href="../types/objects/generics.html"><strong aria-hidden="true">3.1.4.</strong> Generics</a></li></ol></li><li class="chapter-item expanded "><a href="../types/interfaces.html"><strong aria-hidden="true">3.2.</strong> Interfaces</a></li><li class="chapter-item expanded "><a href="../types/unions.html"><strong aria-hidden="true">3.3.</strong> Unions</a></li><li class="chapter-item expanded "><a href="../types/enums.html"><strong aria-hidden="true">3.4.</strong> Enums</a></li><li class="chapter-item expanded "><a href="../types/input_objects.html"><strong aria-hidden="true">3.5.</strong> Input objects</a></li><li class="chapter-item expanded "><a href="../types/scalars.html"><strong aria-hidden="true">3.6.</strong> Scalars</a></li></ol></li><li class="chapter-item expanded "><a href="../schema/index.html"><strong aria-hidden="true">4.</strong> Schema</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../schema/subscriptions.html"><strong aria-hidden="true">4.1.</strong> Subscriptions</a></li><li class="chapter-item expanded "><a href="../schema/introspection.html"><strong aria-hidden="true">4.2.</strong> Introspection</a></li></ol></li><li class="chapter-item expanded "><a href="../serve/index.html"><strong aria-hidden="true">5.</strong> Serving</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../serve/batching.html"><strong aria-hidden="true">5.1.</strong> Batching</a></li></ol></li><li class="chapter-item expanded "><a href="../advanced/index.html"><strong aria-hidden="true">6.</strong> Advanced Topics</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../advanced/implicit_and_explicit_null.html"><strong aria-hidden="true">6.1.</strong> Implicit and explicit null</a></li><li class="chapter-item expanded "><a href="../advanced/n_plus_1.html"><strong aria-hidden="true">6.2.</strong> N+1 problem</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../advanced/dataloader.html" class="active"><strong aria-hidden="true">6.2.1.</strong> DataLoader</a></li><li class="chapter-item expanded "><a href="../advanced/lookahead.html"><strong aria-hidden="true">6.2.2.</strong> Look-ahead</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../advanced/eager_loading.html"><strong aria-hidden="true">6.2.2.1.</strong> Eager loading</a></li></ol></li></ol></li></ol></li></ol>
|
||||
</div>
|
||||
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
|
||||
<div class="sidebar-resize-indicator"></div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Track and set sidebar scroll position -->
|
||||
<script>
|
||||
var sidebarScrollbox = document.querySelector('#sidebar .sidebar-scrollbox');
|
||||
sidebarScrollbox.addEventListener('click', function(e) {
|
||||
if (e.target.tagName === 'A') {
|
||||
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
|
||||
}
|
||||
}, { passive: true });
|
||||
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
|
||||
sessionStorage.removeItem('sidebar-scroll');
|
||||
if (sidebarScrollTop) {
|
||||
// preserve sidebar scroll position when navigating via links within sidebar
|
||||
sidebarScrollbox.scrollTop = sidebarScrollTop;
|
||||
} else {
|
||||
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
|
||||
var activeSection = document.querySelector('#sidebar .active');
|
||||
if (activeSection) {
|
||||
activeSection.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<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="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="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="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="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>
|
||||
|
||||
</main>
|
||||
|
||||
<nav class="nav-wrapper" aria-label="Page navigation">
|
||||
<!-- Mobile navigation buttons -->
|
||||
<a rel="prev" href="../advanced/n_plus_1.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
|
||||
<a rel="next prefetch" href="../advanced/lookahead.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</a>
|
||||
|
||||
<div style="clear: both"></div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||
<a rel="prev" href="../advanced/n_plus_1.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
|
||||
<a rel="next prefetch" href="../advanced/lookahead.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</a>
|
||||
</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 -->
|
||||
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,463 +0,0 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en" class="light" dir="ltr">
|
||||
<head>
|
||||
<!-- Book generated using mdBook -->
|
||||
<meta charset="UTF-8">
|
||||
<title>Eager loading - Juniper Book</title>
|
||||
|
||||
|
||||
<!-- 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 -->
|
||||
|
||||
</head>
|
||||
<body class="sidebar-visible no-js">
|
||||
<div id="body-container">
|
||||
<!-- Provide site root to javascript -->
|
||||
<script>
|
||||
var path_to_root = "../";
|
||||
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
|
||||
</script>
|
||||
|
||||
<!-- 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; }
|
||||
var html = document.querySelector('html');
|
||||
html.classList.remove('light')
|
||||
html.classList.add(theme);
|
||||
var body = document.querySelector('body');
|
||||
body.classList.remove('no-js')
|
||||
body.classList.add('js');
|
||||
</script>
|
||||
|
||||
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
|
||||
|
||||
<!-- Hide / unhide sidebar before it is displayed -->
|
||||
<script>
|
||||
var body = document.querySelector('body');
|
||||
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';
|
||||
body.classList.remove('sidebar-visible');
|
||||
body.classList.add("sidebar-" + sidebar);
|
||||
</script>
|
||||
|
||||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||
<div class="sidebar-scrollbox">
|
||||
<ol class="chapter"><li class="chapter-item expanded "><a href="../introduction.html"><strong aria-hidden="true">1.</strong> Introduction</a></li><li class="chapter-item expanded "><a href="../quickstart.html"><strong aria-hidden="true">2.</strong> Quickstart</a></li><li class="chapter-item expanded "><a href="../types/index.html"><strong aria-hidden="true">3.</strong> Type system</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../types/objects/index.html"><strong aria-hidden="true">3.1.</strong> Objects</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../types/objects/complex_fields.html"><strong aria-hidden="true">3.1.1.</strong> Complex fields</a></li><li class="chapter-item expanded "><a href="../types/objects/context.html"><strong aria-hidden="true">3.1.2.</strong> Context</a></li><li class="chapter-item expanded "><a href="../types/objects/error/index.html"><strong aria-hidden="true">3.1.3.</strong> Error handling</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../types/objects/error/field.html"><strong aria-hidden="true">3.1.3.1.</strong> Field errors</a></li><li class="chapter-item expanded "><a href="../types/objects/error/schema.html"><strong aria-hidden="true">3.1.3.2.</strong> Schema errors</a></li></ol></li><li class="chapter-item expanded "><a href="../types/objects/generics.html"><strong aria-hidden="true">3.1.4.</strong> Generics</a></li></ol></li><li class="chapter-item expanded "><a href="../types/interfaces.html"><strong aria-hidden="true">3.2.</strong> Interfaces</a></li><li class="chapter-item expanded "><a href="../types/unions.html"><strong aria-hidden="true">3.3.</strong> Unions</a></li><li class="chapter-item expanded "><a href="../types/enums.html"><strong aria-hidden="true">3.4.</strong> Enums</a></li><li class="chapter-item expanded "><a href="../types/input_objects.html"><strong aria-hidden="true">3.5.</strong> Input objects</a></li><li class="chapter-item expanded "><a href="../types/scalars.html"><strong aria-hidden="true">3.6.</strong> Scalars</a></li></ol></li><li class="chapter-item expanded "><a href="../schema/index.html"><strong aria-hidden="true">4.</strong> Schema</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../schema/subscriptions.html"><strong aria-hidden="true">4.1.</strong> Subscriptions</a></li><li class="chapter-item expanded "><a href="../schema/introspection.html"><strong aria-hidden="true">4.2.</strong> Introspection</a></li></ol></li><li class="chapter-item expanded "><a href="../serve/index.html"><strong aria-hidden="true">5.</strong> Serving</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../serve/batching.html"><strong aria-hidden="true">5.1.</strong> Batching</a></li></ol></li><li class="chapter-item expanded "><a href="../advanced/index.html"><strong aria-hidden="true">6.</strong> Advanced Topics</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../advanced/implicit_and_explicit_null.html"><strong aria-hidden="true">6.1.</strong> Implicit and explicit null</a></li><li class="chapter-item expanded "><a href="../advanced/n_plus_1.html"><strong aria-hidden="true">6.2.</strong> N+1 problem</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../advanced/dataloader.html"><strong aria-hidden="true">6.2.1.</strong> DataLoader</a></li><li class="chapter-item expanded "><a href="../advanced/lookahead.html"><strong aria-hidden="true">6.2.2.</strong> Look-ahead</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../advanced/eager_loading.html" class="active"><strong aria-hidden="true">6.2.2.1.</strong> Eager loading</a></li></ol></li></ol></li></ol></li></ol>
|
||||
</div>
|
||||
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
|
||||
<div class="sidebar-resize-indicator"></div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Track and set sidebar scroll position -->
|
||||
<script>
|
||||
var sidebarScrollbox = document.querySelector('#sidebar .sidebar-scrollbox');
|
||||
sidebarScrollbox.addEventListener('click', function(e) {
|
||||
if (e.target.tagName === 'A') {
|
||||
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
|
||||
}
|
||||
}, { passive: true });
|
||||
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
|
||||
sessionStorage.removeItem('sidebar-scroll');
|
||||
if (sidebarScrollTop) {
|
||||
// preserve sidebar scroll position when navigating via links within sidebar
|
||||
sidebarScrollbox.scrollTop = sidebarScrollTop;
|
||||
} else {
|
||||
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
|
||||
var activeSection = document.querySelector('#sidebar .active');
|
||||
if (activeSection) {
|
||||
activeSection.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<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="eager-loading"><a class="header" href="#eager-loading">Eager loading</a></h1>
|
||||
<p>As a further evolution of the <a href="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"><a class="header" href="#full-example">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 -->
|
||||
<a rel="prev" href="../advanced/lookahead.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
|
||||
|
||||
<div style="clear: both"></div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||
<a rel="prev" href="../advanced/lookahead.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
|
||||
</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 -->
|
||||
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,320 +0,0 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en" class="light" dir="ltr">
|
||||
<head>
|
||||
<!-- Book generated using mdBook -->
|
||||
<meta charset="UTF-8">
|
||||
<title>Implicit and explicit null - Juniper Book</title>
|
||||
|
||||
|
||||
<!-- 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 -->
|
||||
|
||||
</head>
|
||||
<body class="sidebar-visible no-js">
|
||||
<div id="body-container">
|
||||
<!-- Provide site root to javascript -->
|
||||
<script>
|
||||
var path_to_root = "../";
|
||||
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
|
||||
</script>
|
||||
|
||||
<!-- 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; }
|
||||
var html = document.querySelector('html');
|
||||
html.classList.remove('light')
|
||||
html.classList.add(theme);
|
||||
var body = document.querySelector('body');
|
||||
body.classList.remove('no-js')
|
||||
body.classList.add('js');
|
||||
</script>
|
||||
|
||||
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
|
||||
|
||||
<!-- Hide / unhide sidebar before it is displayed -->
|
||||
<script>
|
||||
var body = document.querySelector('body');
|
||||
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';
|
||||
body.classList.remove('sidebar-visible');
|
||||
body.classList.add("sidebar-" + sidebar);
|
||||
</script>
|
||||
|
||||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||
<div class="sidebar-scrollbox">
|
||||
<ol class="chapter"><li class="chapter-item expanded "><a href="../introduction.html"><strong aria-hidden="true">1.</strong> Introduction</a></li><li class="chapter-item expanded "><a href="../quickstart.html"><strong aria-hidden="true">2.</strong> Quickstart</a></li><li class="chapter-item expanded "><a href="../types/index.html"><strong aria-hidden="true">3.</strong> Type system</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../types/objects/index.html"><strong aria-hidden="true">3.1.</strong> Objects</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../types/objects/complex_fields.html"><strong aria-hidden="true">3.1.1.</strong> Complex fields</a></li><li class="chapter-item expanded "><a href="../types/objects/context.html"><strong aria-hidden="true">3.1.2.</strong> Context</a></li><li class="chapter-item expanded "><a href="../types/objects/error/index.html"><strong aria-hidden="true">3.1.3.</strong> Error handling</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../types/objects/error/field.html"><strong aria-hidden="true">3.1.3.1.</strong> Field errors</a></li><li class="chapter-item expanded "><a href="../types/objects/error/schema.html"><strong aria-hidden="true">3.1.3.2.</strong> Schema errors</a></li></ol></li><li class="chapter-item expanded "><a href="../types/objects/generics.html"><strong aria-hidden="true">3.1.4.</strong> Generics</a></li></ol></li><li class="chapter-item expanded "><a href="../types/interfaces.html"><strong aria-hidden="true">3.2.</strong> Interfaces</a></li><li class="chapter-item expanded "><a href="../types/unions.html"><strong aria-hidden="true">3.3.</strong> Unions</a></li><li class="chapter-item expanded "><a href="../types/enums.html"><strong aria-hidden="true">3.4.</strong> Enums</a></li><li class="chapter-item expanded "><a href="../types/input_objects.html"><strong aria-hidden="true">3.5.</strong> Input objects</a></li><li class="chapter-item expanded "><a href="../types/scalars.html"><strong aria-hidden="true">3.6.</strong> Scalars</a></li></ol></li><li class="chapter-item expanded "><a href="../schema/index.html"><strong aria-hidden="true">4.</strong> Schema</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../schema/subscriptions.html"><strong aria-hidden="true">4.1.</strong> Subscriptions</a></li><li class="chapter-item expanded "><a href="../schema/introspection.html"><strong aria-hidden="true">4.2.</strong> Introspection</a></li></ol></li><li class="chapter-item expanded "><a href="../serve/index.html"><strong aria-hidden="true">5.</strong> Serving</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../serve/batching.html"><strong aria-hidden="true">5.1.</strong> Batching</a></li></ol></li><li class="chapter-item expanded "><a href="../advanced/index.html"><strong aria-hidden="true">6.</strong> Advanced Topics</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../advanced/implicit_and_explicit_null.html" class="active"><strong aria-hidden="true">6.1.</strong> Implicit and explicit null</a></li><li class="chapter-item expanded "><a href="../advanced/n_plus_1.html"><strong aria-hidden="true">6.2.</strong> N+1 problem</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../advanced/dataloader.html"><strong aria-hidden="true">6.2.1.</strong> DataLoader</a></li><li class="chapter-item expanded "><a href="../advanced/lookahead.html"><strong aria-hidden="true">6.2.2.</strong> Look-ahead</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../advanced/eager_loading.html"><strong aria-hidden="true">6.2.2.1.</strong> Eager loading</a></li></ol></li></ol></li></ol></li></ol>
|
||||
</div>
|
||||
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
|
||||
<div class="sidebar-resize-indicator"></div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Track and set sidebar scroll position -->
|
||||
<script>
|
||||
var sidebarScrollbox = document.querySelector('#sidebar .sidebar-scrollbox');
|
||||
sidebarScrollbox.addEventListener('click', function(e) {
|
||||
if (e.target.tagName === 'A') {
|
||||
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
|
||||
}
|
||||
}, { passive: true });
|
||||
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
|
||||
sessionStorage.removeItem('sidebar-scroll');
|
||||
if (sidebarScrollTop) {
|
||||
// preserve sidebar scroll position when navigating via links within sidebar
|
||||
sidebarScrollbox.scrollTop = sidebarScrollTop;
|
||||
} else {
|
||||
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
|
||||
var activeSection = document.querySelector('#sidebar .active');
|
||||
if (activeSection) {
|
||||
activeSection.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<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="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>
|
||||
|
||||
</main>
|
||||
|
||||
<nav class="nav-wrapper" aria-label="Page navigation">
|
||||
<!-- Mobile navigation buttons -->
|
||||
<a rel="prev" href="../advanced/index.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
|
||||
<a rel="next prefetch" href="../advanced/n_plus_1.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</a>
|
||||
|
||||
<div style="clear: both"></div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||
<a rel="prev" href="../advanced/index.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
|
||||
<a rel="next prefetch" href="../advanced/n_plus_1.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</a>
|
||||
</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 -->
|
||||
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,242 +0,0 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en" class="light" dir="ltr">
|
||||
<head>
|
||||
<!-- Book generated using mdBook -->
|
||||
<meta charset="UTF-8">
|
||||
<title>Advanced Topics - Juniper Book</title>
|
||||
|
||||
|
||||
<!-- 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 -->
|
||||
|
||||
</head>
|
||||
<body class="sidebar-visible no-js">
|
||||
<div id="body-container">
|
||||
<!-- Provide site root to javascript -->
|
||||
<script>
|
||||
var path_to_root = "../";
|
||||
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
|
||||
</script>
|
||||
|
||||
<!-- 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; }
|
||||
var html = document.querySelector('html');
|
||||
html.classList.remove('light')
|
||||
html.classList.add(theme);
|
||||
var body = document.querySelector('body');
|
||||
body.classList.remove('no-js')
|
||||
body.classList.add('js');
|
||||
</script>
|
||||
|
||||
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
|
||||
|
||||
<!-- Hide / unhide sidebar before it is displayed -->
|
||||
<script>
|
||||
var body = document.querySelector('body');
|
||||
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';
|
||||
body.classList.remove('sidebar-visible');
|
||||
body.classList.add("sidebar-" + sidebar);
|
||||
</script>
|
||||
|
||||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||
<div class="sidebar-scrollbox">
|
||||
<ol class="chapter"><li class="chapter-item expanded "><a href="../introduction.html"><strong aria-hidden="true">1.</strong> Introduction</a></li><li class="chapter-item expanded "><a href="../quickstart.html"><strong aria-hidden="true">2.</strong> Quickstart</a></li><li class="chapter-item expanded "><a href="../types/index.html"><strong aria-hidden="true">3.</strong> Type system</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../types/objects/index.html"><strong aria-hidden="true">3.1.</strong> Objects</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../types/objects/complex_fields.html"><strong aria-hidden="true">3.1.1.</strong> Complex fields</a></li><li class="chapter-item expanded "><a href="../types/objects/context.html"><strong aria-hidden="true">3.1.2.</strong> Context</a></li><li class="chapter-item expanded "><a href="../types/objects/error/index.html"><strong aria-hidden="true">3.1.3.</strong> Error handling</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../types/objects/error/field.html"><strong aria-hidden="true">3.1.3.1.</strong> Field errors</a></li><li class="chapter-item expanded "><a href="../types/objects/error/schema.html"><strong aria-hidden="true">3.1.3.2.</strong> Schema errors</a></li></ol></li><li class="chapter-item expanded "><a href="../types/objects/generics.html"><strong aria-hidden="true">3.1.4.</strong> Generics</a></li></ol></li><li class="chapter-item expanded "><a href="../types/interfaces.html"><strong aria-hidden="true">3.2.</strong> Interfaces</a></li><li class="chapter-item expanded "><a href="../types/unions.html"><strong aria-hidden="true">3.3.</strong> Unions</a></li><li class="chapter-item expanded "><a href="../types/enums.html"><strong aria-hidden="true">3.4.</strong> Enums</a></li><li class="chapter-item expanded "><a href="../types/input_objects.html"><strong aria-hidden="true">3.5.</strong> Input objects</a></li><li class="chapter-item expanded "><a href="../types/scalars.html"><strong aria-hidden="true">3.6.</strong> Scalars</a></li></ol></li><li class="chapter-item expanded "><a href="../schema/index.html"><strong aria-hidden="true">4.</strong> Schema</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../schema/subscriptions.html"><strong aria-hidden="true">4.1.</strong> Subscriptions</a></li><li class="chapter-item expanded "><a href="../schema/introspection.html"><strong aria-hidden="true">4.2.</strong> Introspection</a></li></ol></li><li class="chapter-item expanded "><a href="../serve/index.html"><strong aria-hidden="true">5.</strong> Serving</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../serve/batching.html"><strong aria-hidden="true">5.1.</strong> Batching</a></li></ol></li><li class="chapter-item expanded "><a href="../advanced/index.html" class="active"><strong aria-hidden="true">6.</strong> Advanced Topics</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../advanced/implicit_and_explicit_null.html"><strong aria-hidden="true">6.1.</strong> Implicit and explicit null</a></li><li class="chapter-item expanded "><a href="../advanced/n_plus_1.html"><strong aria-hidden="true">6.2.</strong> N+1 problem</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../advanced/dataloader.html"><strong aria-hidden="true">6.2.1.</strong> DataLoader</a></li><li class="chapter-item expanded "><a href="../advanced/lookahead.html"><strong aria-hidden="true">6.2.2.</strong> Look-ahead</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../advanced/eager_loading.html"><strong aria-hidden="true">6.2.2.1.</strong> Eager loading</a></li></ol></li></ol></li></ol></li></ol>
|
||||
</div>
|
||||
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
|
||||
<div class="sidebar-resize-indicator"></div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Track and set sidebar scroll position -->
|
||||
<script>
|
||||
var sidebarScrollbox = document.querySelector('#sidebar .sidebar-scrollbox');
|
||||
sidebarScrollbox.addEventListener('click', function(e) {
|
||||
if (e.target.tagName === 'A') {
|
||||
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
|
||||
}
|
||||
}, { passive: true });
|
||||
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
|
||||
sessionStorage.removeItem('sidebar-scroll');
|
||||
if (sidebarScrollTop) {
|
||||
// preserve sidebar scroll position when navigating via links within sidebar
|
||||
sidebarScrollbox.scrollTop = sidebarScrollTop;
|
||||
} else {
|
||||
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
|
||||
var activeSection = document.querySelector('#sidebar .active');
|
||||
if (activeSection) {
|
||||
activeSection.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<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="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="implicit_and_explicit_null.html">Implicit and explicit <code>null</code></a></li>
|
||||
<li><a href="n_plus_1.html">N+1 problem</a>
|
||||
<ul>
|
||||
<li><a href="dataloader.html">DataLoader</a></li>
|
||||
<li><a href="lookahead.html">Look-ahead</a>
|
||||
<ul>
|
||||
<li><a href="eager_loading.html">Eager loading</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</main>
|
||||
|
||||
<nav class="nav-wrapper" aria-label="Page navigation">
|
||||
<!-- Mobile navigation buttons -->
|
||||
<a rel="prev" href="../serve/batching.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
|
||||
<a rel="next prefetch" href="../advanced/implicit_and_explicit_null.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</a>
|
||||
|
||||
<div style="clear: both"></div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||
<a rel="prev" href="../serve/batching.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
|
||||
<a rel="next prefetch" href="../advanced/implicit_and_explicit_null.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</a>
|
||||
</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 -->
|
||||
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,429 +0,0 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en" class="light" dir="ltr">
|
||||
<head>
|
||||
<!-- Book generated using mdBook -->
|
||||
<meta charset="UTF-8">
|
||||
<title>Look-ahead - Juniper Book</title>
|
||||
|
||||
|
||||
<!-- 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 -->
|
||||
|
||||
</head>
|
||||
<body class="sidebar-visible no-js">
|
||||
<div id="body-container">
|
||||
<!-- Provide site root to javascript -->
|
||||
<script>
|
||||
var path_to_root = "../";
|
||||
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
|
||||
</script>
|
||||
|
||||
<!-- 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; }
|
||||
var html = document.querySelector('html');
|
||||
html.classList.remove('light')
|
||||
html.classList.add(theme);
|
||||
var body = document.querySelector('body');
|
||||
body.classList.remove('no-js')
|
||||
body.classList.add('js');
|
||||
</script>
|
||||
|
||||
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
|
||||
|
||||
<!-- Hide / unhide sidebar before it is displayed -->
|
||||
<script>
|
||||
var body = document.querySelector('body');
|
||||
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';
|
||||
body.classList.remove('sidebar-visible');
|
||||
body.classList.add("sidebar-" + sidebar);
|
||||
</script>
|
||||
|
||||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||
<div class="sidebar-scrollbox">
|
||||
<ol class="chapter"><li class="chapter-item expanded "><a href="../introduction.html"><strong aria-hidden="true">1.</strong> Introduction</a></li><li class="chapter-item expanded "><a href="../quickstart.html"><strong aria-hidden="true">2.</strong> Quickstart</a></li><li class="chapter-item expanded "><a href="../types/index.html"><strong aria-hidden="true">3.</strong> Type system</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../types/objects/index.html"><strong aria-hidden="true">3.1.</strong> Objects</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../types/objects/complex_fields.html"><strong aria-hidden="true">3.1.1.</strong> Complex fields</a></li><li class="chapter-item expanded "><a href="../types/objects/context.html"><strong aria-hidden="true">3.1.2.</strong> Context</a></li><li class="chapter-item expanded "><a href="../types/objects/error/index.html"><strong aria-hidden="true">3.1.3.</strong> Error handling</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../types/objects/error/field.html"><strong aria-hidden="true">3.1.3.1.</strong> Field errors</a></li><li class="chapter-item expanded "><a href="../types/objects/error/schema.html"><strong aria-hidden="true">3.1.3.2.</strong> Schema errors</a></li></ol></li><li class="chapter-item expanded "><a href="../types/objects/generics.html"><strong aria-hidden="true">3.1.4.</strong> Generics</a></li></ol></li><li class="chapter-item expanded "><a href="../types/interfaces.html"><strong aria-hidden="true">3.2.</strong> Interfaces</a></li><li class="chapter-item expanded "><a href="../types/unions.html"><strong aria-hidden="true">3.3.</strong> Unions</a></li><li class="chapter-item expanded "><a href="../types/enums.html"><strong aria-hidden="true">3.4.</strong> Enums</a></li><li class="chapter-item expanded "><a href="../types/input_objects.html"><strong aria-hidden="true">3.5.</strong> Input objects</a></li><li class="chapter-item expanded "><a href="../types/scalars.html"><strong aria-hidden="true">3.6.</strong> Scalars</a></li></ol></li><li class="chapter-item expanded "><a href="../schema/index.html"><strong aria-hidden="true">4.</strong> Schema</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../schema/subscriptions.html"><strong aria-hidden="true">4.1.</strong> Subscriptions</a></li><li class="chapter-item expanded "><a href="../schema/introspection.html"><strong aria-hidden="true">4.2.</strong> Introspection</a></li></ol></li><li class="chapter-item expanded "><a href="../serve/index.html"><strong aria-hidden="true">5.</strong> Serving</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../serve/batching.html"><strong aria-hidden="true">5.1.</strong> Batching</a></li></ol></li><li class="chapter-item expanded "><a href="../advanced/index.html"><strong aria-hidden="true">6.</strong> Advanced Topics</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../advanced/implicit_and_explicit_null.html"><strong aria-hidden="true">6.1.</strong> Implicit and explicit null</a></li><li class="chapter-item expanded "><a href="../advanced/n_plus_1.html"><strong aria-hidden="true">6.2.</strong> N+1 problem</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../advanced/dataloader.html"><strong aria-hidden="true">6.2.1.</strong> DataLoader</a></li><li class="chapter-item expanded "><a href="../advanced/lookahead.html" class="active"><strong aria-hidden="true">6.2.2.</strong> Look-ahead</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../advanced/eager_loading.html"><strong aria-hidden="true">6.2.2.1.</strong> Eager loading</a></li></ol></li></ol></li></ol></li></ol>
|
||||
</div>
|
||||
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
|
||||
<div class="sidebar-resize-indicator"></div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Track and set sidebar scroll position -->
|
||||
<script>
|
||||
var sidebarScrollbox = document.querySelector('#sidebar .sidebar-scrollbox');
|
||||
sidebarScrollbox.addEventListener('click', function(e) {
|
||||
if (e.target.tagName === 'A') {
|
||||
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
|
||||
}
|
||||
}, { passive: true });
|
||||
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
|
||||
sessionStorage.removeItem('sidebar-scroll');
|
||||
if (sidebarScrollTop) {
|
||||
// preserve sidebar scroll position when navigating via links within sidebar
|
||||
sidebarScrollbox.scrollTop = sidebarScrollTop;
|
||||
} else {
|
||||
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
|
||||
var activeSection = document.querySelector('#sidebar .active');
|
||||
if (activeSection) {
|
||||
activeSection.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<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="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"><a class="header" href="#n1-problem">N+1 problem</a></h2>
|
||||
<p>Naturally, look-ahead machinery allows us to solve <a href="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="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>
|
||||
|
||||
</main>
|
||||
|
||||
<nav class="nav-wrapper" aria-label="Page navigation">
|
||||
<!-- Mobile navigation buttons -->
|
||||
<a rel="prev" href="../advanced/dataloader.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
|
||||
<a rel="next prefetch" href="../advanced/eager_loading.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</a>
|
||||
|
||||
<div style="clear: both"></div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||
<a rel="prev" href="../advanced/dataloader.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
|
||||
<a rel="next prefetch" href="../advanced/eager_loading.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</a>
|
||||
</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 -->
|
||||
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,324 +0,0 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en" class="light" dir="ltr">
|
||||
<head>
|
||||
<!-- Book generated using mdBook -->
|
||||
<meta charset="UTF-8">
|
||||
<title>N+1 problem - Juniper Book</title>
|
||||
|
||||
|
||||
<!-- 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 -->
|
||||
|
||||
</head>
|
||||
<body class="sidebar-visible no-js">
|
||||
<div id="body-container">
|
||||
<!-- Provide site root to javascript -->
|
||||
<script>
|
||||
var path_to_root = "../";
|
||||
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
|
||||
</script>
|
||||
|
||||
<!-- 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; }
|
||||
var html = document.querySelector('html');
|
||||
html.classList.remove('light')
|
||||
html.classList.add(theme);
|
||||
var body = document.querySelector('body');
|
||||
body.classList.remove('no-js')
|
||||
body.classList.add('js');
|
||||
</script>
|
||||
|
||||
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
|
||||
|
||||
<!-- Hide / unhide sidebar before it is displayed -->
|
||||
<script>
|
||||
var body = document.querySelector('body');
|
||||
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';
|
||||
body.classList.remove('sidebar-visible');
|
||||
body.classList.add("sidebar-" + sidebar);
|
||||
</script>
|
||||
|
||||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||
<div class="sidebar-scrollbox">
|
||||
<ol class="chapter"><li class="chapter-item expanded "><a href="../introduction.html"><strong aria-hidden="true">1.</strong> Introduction</a></li><li class="chapter-item expanded "><a href="../quickstart.html"><strong aria-hidden="true">2.</strong> Quickstart</a></li><li class="chapter-item expanded "><a href="../types/index.html"><strong aria-hidden="true">3.</strong> Type system</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../types/objects/index.html"><strong aria-hidden="true">3.1.</strong> Objects</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../types/objects/complex_fields.html"><strong aria-hidden="true">3.1.1.</strong> Complex fields</a></li><li class="chapter-item expanded "><a href="../types/objects/context.html"><strong aria-hidden="true">3.1.2.</strong> Context</a></li><li class="chapter-item expanded "><a href="../types/objects/error/index.html"><strong aria-hidden="true">3.1.3.</strong> Error handling</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../types/objects/error/field.html"><strong aria-hidden="true">3.1.3.1.</strong> Field errors</a></li><li class="chapter-item expanded "><a href="../types/objects/error/schema.html"><strong aria-hidden="true">3.1.3.2.</strong> Schema errors</a></li></ol></li><li class="chapter-item expanded "><a href="../types/objects/generics.html"><strong aria-hidden="true">3.1.4.</strong> Generics</a></li></ol></li><li class="chapter-item expanded "><a href="../types/interfaces.html"><strong aria-hidden="true">3.2.</strong> Interfaces</a></li><li class="chapter-item expanded "><a href="../types/unions.html"><strong aria-hidden="true">3.3.</strong> Unions</a></li><li class="chapter-item expanded "><a href="../types/enums.html"><strong aria-hidden="true">3.4.</strong> Enums</a></li><li class="chapter-item expanded "><a href="../types/input_objects.html"><strong aria-hidden="true">3.5.</strong> Input objects</a></li><li class="chapter-item expanded "><a href="../types/scalars.html"><strong aria-hidden="true">3.6.</strong> Scalars</a></li></ol></li><li class="chapter-item expanded "><a href="../schema/index.html"><strong aria-hidden="true">4.</strong> Schema</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../schema/subscriptions.html"><strong aria-hidden="true">4.1.</strong> Subscriptions</a></li><li class="chapter-item expanded "><a href="../schema/introspection.html"><strong aria-hidden="true">4.2.</strong> Introspection</a></li></ol></li><li class="chapter-item expanded "><a href="../serve/index.html"><strong aria-hidden="true">5.</strong> Serving</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../serve/batching.html"><strong aria-hidden="true">5.1.</strong> Batching</a></li></ol></li><li class="chapter-item expanded "><a href="../advanced/index.html"><strong aria-hidden="true">6.</strong> Advanced Topics</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../advanced/implicit_and_explicit_null.html"><strong aria-hidden="true">6.1.</strong> Implicit and explicit null</a></li><li class="chapter-item expanded "><a href="../advanced/n_plus_1.html" class="active"><strong aria-hidden="true">6.2.</strong> N+1 problem</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../advanced/dataloader.html"><strong aria-hidden="true">6.2.1.</strong> DataLoader</a></li><li class="chapter-item expanded "><a href="../advanced/lookahead.html"><strong aria-hidden="true">6.2.2.</strong> Look-ahead</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../advanced/eager_loading.html"><strong aria-hidden="true">6.2.2.1.</strong> Eager loading</a></li></ol></li></ol></li></ol></li></ol>
|
||||
</div>
|
||||
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
|
||||
<div class="sidebar-resize-indicator"></div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Track and set sidebar scroll position -->
|
||||
<script>
|
||||
var sidebarScrollbox = document.querySelector('#sidebar .sidebar-scrollbox');
|
||||
sidebarScrollbox.addEventListener('click', function(e) {
|
||||
if (e.target.tagName === 'A') {
|
||||
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
|
||||
}
|
||||
}, { passive: true });
|
||||
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
|
||||
sessionStorage.removeItem('sidebar-scroll');
|
||||
if (sidebarScrollTop) {
|
||||
// preserve sidebar scroll position when navigating via links within sidebar
|
||||
sidebarScrollbox.scrollTop = sidebarScrollTop;
|
||||
} else {
|
||||
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
|
||||
var activeSection = document.querySelector('#sidebar .active');
|
||||
if (activeSection) {
|
||||
activeSection.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<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="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="dataloader.html">DataLoader</a></li>
|
||||
<li><a href="lookahead.html">Look-ahead machinery</a>
|
||||
<ul>
|
||||
<li><a href="eager_loading.html">Eager loading</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</main>
|
||||
|
||||
<nav class="nav-wrapper" aria-label="Page navigation">
|
||||
<!-- Mobile navigation buttons -->
|
||||
<a rel="prev" href="../advanced/implicit_and_explicit_null.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
|
||||
<a rel="next prefetch" href="../advanced/dataloader.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</a>
|
||||
|
||||
<div style="clear: both"></div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||
<a rel="prev" href="../advanced/implicit_and_explicit_null.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
|
||||
<a rel="next prefetch" href="../advanced/dataloader.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</a>
|
||||
</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 -->
|
||||
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
4
assets/logo/README.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
The rust-graphql logo is a derivative work based on
|
||||
|
||||
* The GraphQL Logo licensed under the terms of the BSD license
|
||||
* The Rust Logo licensed under the terms of the CC-BY license
|
BIN
assets/logo/juniper-dark-word.png
Normal file
After Width: | Height: | Size: 26 KiB |
31
assets/logo/juniper-dark-word.svg
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="1500" height="500" viewBox="0, 0, 1500, 500">
|
||||
<g id="Layer_1">
|
||||
<path d="M256.636,427.355 L256.194,428.121 L255.675,427.821 C256.485,427.479 256.18,427.662 256.636,427.355 z" fill="#E10098"/>
|
||||
<path d="M236.427,86.007 C236.247,86.893 236.347,86.293 236.269,87.826 C236.269,95.307 242.33,101.374 249.812,101.374 C257.29,101.374 263.354,95.307 263.354,87.826 C263.28,86.345 263.377,86.893 263.227,86.16 L386.485,157.354 L387.191,157.354 C381.489,159.387 378.145,164.245 377.843,170.222 C377.918,171.711 377.821,171.154 377.975,171.912 L272.961,111.256 L382.677,301.363 L382.677,180.581 C385.013,182.797 388.277,183.613 391.386,183.77 C394.739,183.741 396.829,182.819 399.536,180.991 L399.536,322.704 C397.18,320.801 394.351,320.072 391.384,319.922 L391.384,319.922 C383.902,319.922 377.838,325.989 377.838,333.47 C378.018,339.038 380.326,342.277 384.799,345.297 L384.36,345.297 L263.808,414.928 L263.479,415.498 C263.395,409.869 260.005,405.216 254.902,403.06 L361.419,341.538 L140.008,341.538 L245.87,402.684 C240.111,404.72 236.704,409.557 236.399,415.604 L236.456,416.72 L114.311,346.168 C119.379,343.968 121.978,338.858 122.069,333.53 C121.824,326.052 115.559,320.19 108.085,320.439 L108.085,320.439 C104.887,320.645 101.522,321.74 99.275,324.142 L99.959,322.956 L99.959,181.495 C102.428,183.517 105.326,184.353 108.46,184.512 C112.009,184.466 113.968,183.516 116.818,181.591 L116.818,301.423 L226.644,111.132 L121.971,171.591 L122.003,170.964 C122.003,163.479 115.938,157.416 108.46,157.416 L108.46,157.416 C104.808,157.464 102.884,158.456 99.959,160.431 L99.959,157.354 L112.905,157.354 L236.427,86.007 z M249.766,104.796 L122.863,324.673 L376.666,324.673 L249.766,104.796 z" fill="#E10098"/>
|
||||
<path d="M95.421,167.451 C94.898,170.017 95.024,168.855 94.92,170.92 L93.544,168.534 L95.421,167.451 z" fill="#E10098"/>
|
||||
<path d="M98.779,324.673 C96.1,327.263 95.062,330.795 95,334.43 C95.245,341.908 101.506,347.77 108.984,347.52 L109.714,347.459 L105.007,355.619 L90.406,347.188 L93.668,341.538 L87.063,341.538 L87.063,324.673 L98.779,324.673 z" fill="#E10098"/>
|
||||
<path d="M401.654,324.673 L412.33,324.673 L412.33,341.538 L405.865,341.538 L409.127,347.188 L394.526,355.623 L389.457,346.838 C390.452,347.041 389.815,346.939 391.384,347.018 C398.863,347.018 404.927,340.955 404.927,333.47 C404.811,330.367 403.925,326.945 401.654,324.673 z" fill="#E10098"/>
|
||||
<path d="M404.661,167.852 L405.955,168.599 L404.924,170.387 L404.932,170.222 C404.845,168.502 404.954,169.29 404.661,167.852 z" fill="#E10098"/>
|
||||
<path d="M395.582,157.354 L399.536,157.354 L399.536,159.453 C397.165,157.851 398.47,158.574 395.582,157.354 z" fill="#E10098"/>
|
||||
<path d="M248.427,74.344 C243.163,75.052 239.246,78.183 237.202,83.027 L231.895,73.827 L246.492,65.397 L249.766,71.066 L253.041,65.393 L267.638,73.824 L262.381,82.933 C260.36,77.556 255.397,74.556 249.812,74.274 L248.427,74.344 z" fill="#E10098"/>
|
||||
<path d="M217.062,75.033 L216.837,75.645 C216.962,75.242 216.888,75.447 217.062,75.033 z" fill="#4D4D4D"/>
|
||||
<path d="M249.455,9.704 L250.655,9.705 C254.228,11.502 252.973,10.367 254.836,12.58 L267.32,32.761 L273.195,33.332 L289.365,15.979 C292.644,12.474 299.264,13.775 300.942,18.291 L309.225,40.462 L314.938,42.194 L334.148,28.366 C338.05,25.556 344.284,28.147 345.056,32.886 L348.862,56.264 L354.11,59.078 L375.656,49.252 C380.039,47.259 385.657,51.011 385.471,55.814 L384.639,79.536 L389.195,83.284 L412.288,77.838 C416.98,76.738 421.74,81.503 420.64,86.197 L415.187,109.302 L418.934,113.849 L442.647,113.024 C447.479,112.839 451.214,118.456 449.203,122.847 L439.384,144.405 L442.193,149.655 L465.558,153.456 C470.307,154.239 472.897,160.469 470.076,164.372 L456.254,183.578 L457.989,189.297 L480.148,197.587 C484.655,199.271 485.974,205.886 482.447,209.176 L465.112,225.345 L465.686,231.226 L485.857,243.715 C489.944,246.242 489.94,253.009 485.853,255.524 L465.683,268.012 L465.108,273.89 L482.443,290.07 C485.97,293.35 484.651,299.961 480.144,301.652 L457.985,309.938 L456.25,315.661 L470.072,334.871 C472.893,338.786 470.311,345.023 465.555,345.783 L442.189,349.584 L439.38,354.841 L449.199,376.392 C451.211,380.776 447.43,386.412 442.643,386.219 L418.93,385.383 L415.195,389.94 L420.632,413.054 C421.736,417.736 416.976,422.494 412.284,421.394 L389.191,415.951 L384.639,419.688 L385.467,443.417 C385.642,448.229 380.035,451.977 375.653,449.976 L354.106,440.149 L348.859,442.967 L345.048,466.33 C344.281,471.081 338.046,473.66 334.144,470.862 L314.942,457.022 L309.222,458.762 L300.934,480.926 C299.256,485.427 292.64,486.754 289.362,483.226 L273.191,465.88 L267.316,466.47 L254.832,486.648 C252.307,490.726 245.547,490.718 243.029,486.649 L230.541,466.47 L224.662,465.88 L208.492,483.225 C205.215,486.754 198.594,485.427 196.916,480.926 L188.628,458.762 L182.908,457.022 L163.713,470.857 C159.812,473.66 153.574,471.088 152.802,466.334 L148.991,442.967 L143.743,440.149 L122.197,449.972 C117.82,451.977 112.201,448.221 112.383,443.418 L113.21,419.688 L108.655,415.951 L85.562,421.393 C80.871,422.494 76.107,417.736 77.207,413.054 L82.647,389.94 L78.9,385.383 L55.195,386.215 C50.417,386.412 46.636,380.776 48.64,376.393 L58.462,354.841 L55.645,349.584 L32.288,345.783 C27.54,345.023 24.958,338.786 27.767,334.871 L41.585,315.661 L39.853,309.938 L17.691,301.652 C13.192,299.961 11.876,293.35 15.392,290.071 L32.727,273.89 L32.152,268.012 L11.982,255.523 C7.899,253.009 7.899,246.242 11.986,243.716 L32.152,231.226 L32.727,225.345 L15.388,209.172 C11.876,205.886 13.192,199.271 17.691,197.588 L39.853,189.297 L41.585,183.578 L27.766,164.372 C24.965,160.469 27.536,154.228 32.285,153.456 L55.645,149.655 L58.462,144.405 L48.636,122.846 C46.648,118.456 50.417,112.888 55.2,113.024 L78.912,113.849 L82.647,109.302 L77.207,86.193 C76.107,81.503 80.871,76.745 85.562,77.838 L108.655,83.284 L113.21,79.536 L112.382,55.814 C112.201,51.011 117.82,47.251 122.198,49.252 L143.743,59.078 L148.991,56.264 L152.802,32.882 C153.574,28.147 159.812,25.549 163.714,28.367 L182.919,42.194 L188.632,40.462 L196.919,18.29 C198.602,13.775 205.218,12.474 208.496,15.984 L224.666,33.332 L230.549,32.761 L243.033,12.579 C244.754,10.079 246.649,9.849 249.455,9.704 L249.455,9.704 z M106.791,128.577 L103.787,128.754 C90.816,129.985 79.189,138.21 72.662,149.275 C63.481,165.488 65.934,187.734 80.197,200.361 C75.355,215.79 73.619,231.888 73.078,247.978 C73.522,266.185 75.582,284.757 81.707,302.023 C66.377,315.329 63.174,336.608 72.764,354.321 L72.764,354.323 C82.961,371.832 105.773,380.382 124.701,371.949 C147.497,395.467 177.646,410.322 208.848,418.985 C210.62,440.59 229.008,456.329 250.22,456.864 C272.564,456.568 289.438,440.191 291.667,418.236 C323.016,410.578 351.204,393.602 374.199,371.187 C394.578,380.145 416.103,372.827 427.661,354.346 L427.662,354.346 L427.669,354.333 L427.676,354.322 L427.676,354.321 C437.52,336.347 433.593,312.772 416.775,300.464 C422.339,283.529 424.596,265.748 424.821,247.978 C424.855,235.463 423.319,223.019 420.783,210.777 C420.163,207.785 419.233,204.866 418.459,201.911 C434.019,188.514 437.531,167.282 427.778,149.275 C418.995,135.808 409.184,130.496 393.509,128.553 L390.621,128.468 C388.612,128.666 386.59,128.766 384.593,129.063 C382.732,129.388 380.835,129.705 379.07,130.408 C355.621,103.96 324.367,86.778 290.611,77.372 C286.68,58.525 268.775,46.092 250.22,45.624 L250.22,45.623 C230.501,45.839 215.695,58.28 209.973,76.722 C174.553,83.963 143.111,103.253 118.926,129.834 C115.991,128.84 112.867,128.651 109.799,128.472 L106.791,128.577 z" fill="#4D4D4D"/>
|
||||
<path d="M392.399,304.221 C398.017,304.914 401.215,305.16 406.288,308.126 C420.393,316.274 425.181,334.142 417.035,348.17 L422.349,351.258 L417.021,348.194 C408.979,362.192 391.013,367.004 376.972,358.931 C362.977,350.883 358.168,332.91 366.239,318.864 C371.921,309.534 381.465,304.273 392.399,304.221 z M391.385,319.922 C383.902,319.922 377.838,325.989 377.838,333.47 C377.838,340.955 383.902,347.018 391.385,347.018 C398.863,347.018 404.927,340.955 404.927,333.47 C404.927,325.989 398.863,319.922 391.385,319.922 z" fill="#E10098"/>
|
||||
<path d="M109.479,140.76 C115.098,141.454 118.297,141.7 123.37,144.666 C137.422,153.315 141.95,170.163 134.102,184.73 C126.057,198.731 108.091,203.543 94.05,195.471 C80.059,187.423 75.249,169.449 83.318,155.402 C89.001,146.073 98.545,140.812 109.479,140.76 z M108.46,157.416 C100.978,157.416 94.918,163.479 94.918,170.964 C94.918,178.445 100.978,184.512 108.46,184.512 C115.938,184.512 122.003,178.445 122.003,170.964 C122.003,163.479 115.938,157.416 108.46,157.416 z" fill="#E10098"/>
|
||||
<path d="M108.029,304.217 C109.902,304.351 111.797,304.307 113.648,304.618 C122.26,306.069 129.821,311.389 134.219,318.888 C142.269,332.902 137.473,350.793 123.444,358.946 C120.272,360.554 119.279,361.225 115.207,362.146 C102.806,364.949 89.767,359.133 83.42,348.195 C81.809,345.034 81.138,344.046 80.216,339.982 C77.396,327.551 83.269,314.512 94.177,308.112 C98.542,305.887 103.044,304.262 108.029,304.217 z M108.085,320.439 C100.61,320.689 94.75,326.953 94.999,334.43 C95.245,341.908 101.506,347.77 108.984,347.52 C116.459,347.275 122.319,341.011 122.069,333.53 C121.824,326.052 115.559,320.19 108.085,320.439 z" fill="#E10098"/>
|
||||
<path d="M390.951,140.756 C392.824,140.89 394.718,140.846 396.57,141.158 C405.181,142.608 412.74,147.927 417.137,155.427 C425.19,169.441 420.396,187.332 406.366,195.485 C392.349,203.543 374.384,198.731 366.342,184.731 L361.014,187.794 L366.341,184.729 C358.275,170.695 363.069,152.805 377.099,144.652 C381.464,142.427 385.966,140.801 390.951,140.756 z M391.386,156.674 C383.907,156.674 377.843,162.741 377.843,170.222 C377.843,177.703 383.907,183.77 391.386,183.77 C398.868,183.77 404.932,177.703 404.932,170.222 C404.932,162.741 398.868,156.674 391.386,156.674 z" fill="#E10098"/>
|
||||
<path d="M250.22,385.95 C266.425,385.951 279.515,399.049 279.515,415.261 C278.711,431.162 268.828,441.848 253.213,444.416 L250.22,444.568 C234.015,444.568 220.925,431.473 220.925,415.261 C220.925,399.049 234.016,385.951 250.22,385.95 z M249.942,402.056 C242.46,402.056 236.4,408.123 236.399,415.604 C236.4,423.085 242.46,429.152 249.942,429.152 C257.42,429.152 263.485,423.085 263.485,415.604 C263.485,408.123 257.42,402.056 249.942,402.056 z" fill="#E10098"/>
|
||||
<path d="M250.22,58.247 C266.426,58.247 279.515,71.342 279.515,87.554 C278.714,103.482 268.873,114.157 253.219,116.713 L250.22,116.864 C234.016,116.865 220.925,103.767 220.925,87.554 C220.925,71.342 234.015,58.247 250.22,58.247 z M249.812,74.274 C242.33,74.274 236.269,80.341 236.269,87.826 C236.269,95.307 242.33,101.374 249.812,101.374 C257.29,101.374 263.354,95.307 263.354,87.826 C263.354,80.341 257.29,74.274 249.812,74.274 z" fill="#E10098"/>
|
||||
<g>
|
||||
<path d="M685.358,281.898 Q685.358,297.357 681.297,310.064 Q677.236,322.771 669.114,331.941 Q660.992,341.111 649.202,346.22 Q637.412,351.329 621.954,351.329 Q609.116,351.329 597.85,348.185 Q586.584,345.04 577.938,338.491 Q569.292,331.941 564.052,321.854 Q558.812,311.767 558.288,297.881 Q558.288,295.523 559.598,293.82 Q560.908,292.117 563.79,292.117 L569.03,292.117 Q571.65,292.117 573.091,293.689 Q574.532,295.26 574.794,297.618 Q575.58,307.837 579.641,315.042 Q583.702,322.247 590.121,326.832 Q596.54,331.417 604.662,333.513 Q612.784,335.609 621.954,335.609 Q633.482,335.609 642.259,331.678 Q651.036,327.749 656.931,320.544 Q662.826,313.339 665.839,303.383 Q668.852,293.427 668.852,281.637 L668.852,181.029 L572.174,181.029 Q566.41,181.029 566.41,175.265 L566.41,171.073 Q566.41,165.309 572.174,165.309 L679.332,165.309 Q685.358,165.309 685.358,171.335 z" fill="#E10098"/>
|
||||
<path d="M727.016,218.232 Q727.016,212.468 732.78,212.468 L737.234,212.468 Q742.998,212.468 742.998,218.232 L742.998,291.33 Q742.998,336.132 782.036,336.132 Q800.9,336.132 812.035,324.342 Q823.17,312.552 823.17,291.33 L823.17,218.232 Q823.17,212.468 828.934,212.468 L833.388,212.468 Q839.152,212.468 839.152,218.232 L839.152,342.944 Q839.152,348.708 833.388,348.708 L828.934,348.708 Q823.17,348.708 823.17,342.944 L823.17,330.63 Q816.096,339.8 806.14,345.564 Q796.184,351.328 779.416,351.328 Q766.054,351.328 756.229,346.874 Q746.404,342.421 739.854,334.56 Q733.304,326.701 730.16,315.958 Q727.016,305.216 727.016,292.641 z" fill="#E10098"/>
|
||||
<path d="M995.042,342.944 Q995.042,348.708 989.278,348.708 L984.824,348.708 Q979.06,348.708 979.06,342.944 L979.06,269.846 Q979.06,248.886 968.842,236.965 Q958.624,225.044 938.974,225.044 Q920.11,225.044 908.844,236.834 Q897.578,248.624 897.578,269.846 L897.578,342.944 Q897.578,348.708 891.814,348.708 L887.36,348.708 Q881.596,348.708 881.596,342.944 L881.596,218.232 Q881.596,212.468 887.36,212.468 L891.814,212.468 Q897.578,212.468 897.578,218.232 L897.578,230.546 Q904.652,221.376 914.608,215.612 Q924.564,209.848 941.594,209.848 Q954.956,209.848 964.912,214.302 Q974.868,218.756 981.549,226.616 Q988.23,234.476 991.636,245.218 Q995.042,255.96 995.042,268.536 z" fill="#E10098"/>
|
||||
<path d="M1055.04,178.671 Q1055.04,181.29 1053.337,182.863 Q1051.634,184.434 1049.014,184.434 L1039.058,184.434 Q1033.294,184.434 1033.294,178.671 L1033.294,168.715 Q1033.294,166.094 1034.866,164.391 Q1036.438,162.689 1039.058,162.689 L1049.014,162.689 Q1051.634,162.689 1053.337,164.391 Q1055.04,166.094 1055.04,168.715 z M1052.158,342.944 Q1052.158,348.708 1046.394,348.708 L1041.94,348.708 Q1036.176,348.708 1036.176,342.944 L1036.176,218.232 Q1036.176,212.469 1041.94,212.469 L1046.394,212.469 Q1052.158,212.469 1052.158,218.232 z" fill="#E10098"/>
|
||||
<path d="M1154.862,351.328 Q1138.356,351.328 1127.876,345.302 Q1117.396,339.276 1110.846,330.63 L1110.846,392.724 Q1110.846,398.488 1105.082,398.488 L1100.628,398.488 Q1094.864,398.488 1094.864,392.724 L1094.864,218.232 Q1094.864,212.469 1100.628,212.469 L1105.082,212.469 Q1110.846,212.469 1110.846,218.232 L1110.846,230.546 Q1117.396,221.639 1127.876,215.743 Q1138.356,209.848 1154.862,209.848 Q1170.058,209.848 1180.669,215.219 Q1191.28,220.59 1197.83,229.368 Q1204.38,238.145 1207.393,249.279 Q1210.406,260.414 1210.93,271.943 Q1211.192,275.872 1211.192,280.589 Q1211.192,285.305 1210.93,289.234 Q1210.406,300.763 1207.393,311.897 Q1204.38,323.033 1197.83,331.81 Q1191.28,340.586 1180.669,345.957 Q1170.058,351.328 1154.862,351.328 z M1110.846,271.156 Q1110.584,275.086 1110.584,281.375 Q1110.584,287.663 1110.846,291.592 Q1111.108,299.453 1113.597,307.444 Q1116.086,315.435 1121.326,321.723 Q1126.566,328.01 1134.426,332.072 Q1142.286,336.133 1153.29,336.133 Q1164.818,336.133 1172.547,332.203 Q1180.276,328.272 1185.123,321.591 Q1189.97,314.91 1192.197,306.134 Q1194.424,297.357 1194.948,287.924 Q1195.21,280.589 1194.948,273.253 Q1194.424,263.82 1192.197,255.174 Q1189.97,246.529 1185.123,239.848 Q1180.276,233.167 1172.547,229.105 Q1164.818,225.044 1153.29,225.044 Q1142.024,225.044 1134.164,229.236 Q1126.304,233.428 1121.195,240.109 Q1116.086,246.79 1113.597,254.913 Q1111.108,263.035 1110.846,271.156 z" fill="#E10098"/>
|
||||
<path d="M1240.012,270.108 Q1241.06,256.746 1245.383,245.611 Q1249.706,234.476 1256.911,226.616 Q1264.116,218.756 1274.334,214.302 Q1284.552,209.848 1297.39,209.848 Q1324.9,209.848 1340.096,227.795 Q1355.292,245.742 1355.292,277.444 L1355.292,281.374 Q1355.292,283.994 1353.589,285.566 Q1351.886,287.138 1349.266,287.138 L1255.994,287.138 L1255.994,289.758 Q1256.256,298.666 1259.269,307.05 Q1262.282,315.434 1267.522,321.853 Q1272.762,328.272 1280.36,332.202 Q1287.958,336.132 1297.39,336.132 Q1305.774,336.132 1312.062,334.036 Q1318.35,331.94 1322.804,329.189 Q1327.258,326.438 1330.009,323.556 Q1332.76,320.674 1333.808,319.102 Q1336.166,315.697 1337.476,315.041 Q1338.786,314.386 1341.668,314.386 L1345.86,314.386 Q1348.218,314.386 1349.921,315.827 Q1351.624,317.268 1351.362,319.626 Q1351.1,323.294 1347.301,328.796 Q1343.502,334.298 1336.559,339.276 Q1329.616,344.254 1319.66,347.791 Q1309.704,351.328 1297.39,351.328 Q1284.814,351.328 1274.596,347.005 Q1264.378,342.682 1257.042,334.691 Q1249.706,326.701 1245.383,315.565 Q1241.06,304.43 1240.012,291.068 Q1239.75,287.138 1239.75,280.589 Q1239.75,274.038 1240.012,270.108 z M1255.994,272.466 L1339.31,272.466 L1339.31,271.418 Q1339.31,261.724 1336.428,253.209 Q1333.546,244.694 1328.175,238.406 Q1322.804,232.118 1314.944,228.581 Q1307.084,225.044 1297.39,225.044 Q1287.434,225.044 1279.836,228.581 Q1272.238,232.118 1266.867,238.406 Q1261.496,244.694 1258.745,253.209 Q1255.994,261.724 1255.994,271.418 z" fill="#E10098"/>
|
||||
<path d="M1405.858,230.546 Q1415.29,212.468 1441.49,212.468 L1451.184,212.468 Q1456.948,212.468 1456.948,218.232 L1456.948,221.9 Q1456.948,227.664 1451.184,227.664 L1439.918,227.664 Q1424.198,227.664 1415.028,236.834 Q1405.858,246.004 1405.858,261.724 L1405.858,342.944 Q1405.858,345.564 1404.155,347.136 Q1402.452,348.708 1399.832,348.708 L1395.64,348.708 Q1389.876,348.708 1389.876,342.944 L1389.876,218.494 Q1389.876,215.874 1391.448,214.171 Q1393.02,212.468 1395.64,212.468 L1399.832,212.468 Q1402.452,212.468 1404.155,214.171 Q1405.858,215.874 1405.858,218.494 z" fill="#E10098"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 17 KiB |
BIN
assets/logo/juniper-dark.png
Normal file
After Width: | Height: | Size: 16 KiB |
22
assets/logo/juniper-dark.svg
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="500" height="500" viewBox="0, 0, 500, 500">
|
||||
<g id="Layer_1">
|
||||
<path d="M256.636,427.355 L256.194,428.121 L255.675,427.821 C256.485,427.479 256.18,427.662 256.636,427.355 z" fill="#E10098"/>
|
||||
<path d="M236.427,86.007 C236.247,86.893 236.347,86.293 236.269,87.826 C236.269,95.307 242.33,101.374 249.812,101.374 C257.29,101.374 263.354,95.307 263.354,87.826 C263.28,86.345 263.377,86.893 263.227,86.16 L386.485,157.354 L387.191,157.354 C381.489,159.387 378.145,164.245 377.843,170.222 C377.918,171.711 377.821,171.154 377.975,171.912 L272.961,111.256 L382.678,301.363 L382.678,180.581 C385.013,182.797 388.277,183.613 391.386,183.77 C394.739,183.741 396.829,182.819 399.536,180.991 L399.536,322.704 C397.18,320.801 394.351,320.072 391.384,319.922 L391.384,319.922 C383.902,319.922 377.838,325.989 377.838,333.47 C378.018,339.038 380.326,342.277 384.799,345.297 L384.36,345.297 L263.808,414.928 L263.479,415.498 C263.395,409.869 260.005,405.216 254.902,403.06 L361.419,341.538 L140.009,341.538 L245.87,402.684 C240.111,404.72 236.704,409.557 236.399,415.604 L236.456,416.72 L114.311,346.168 C119.379,343.968 121.978,338.858 122.069,333.53 C121.824,326.052 115.559,320.19 108.085,320.439 L108.085,320.439 C104.887,320.645 101.522,321.74 99.275,324.142 L99.959,322.956 L99.959,181.495 C102.428,183.517 105.326,184.353 108.46,184.512 C112.009,184.466 113.968,183.516 116.818,181.591 L116.818,301.423 L226.644,111.132 L121.971,171.591 L122.003,170.964 C122.003,163.479 115.938,157.416 108.46,157.416 L108.46,157.416 C104.808,157.464 102.884,158.456 99.959,160.431 L99.959,157.354 L112.905,157.354 L236.427,86.007 z M249.766,104.796 L122.863,324.673 L376.666,324.673 L249.766,104.796 z" fill="#E10098"/>
|
||||
<path d="M95.421,167.451 C94.898,170.017 95.024,168.855 94.92,170.92 L93.544,168.534 L95.421,167.451 z" fill="#E10098"/>
|
||||
<path d="M98.779,324.673 C96.1,327.263 95.062,330.795 95,334.43 C95.245,341.908 101.506,347.77 108.984,347.52 L109.714,347.459 L105.007,355.619 L90.406,347.188 L93.668,341.538 L87.063,341.538 L87.063,324.673 L98.779,324.673 z" fill="#E10098"/>
|
||||
<path d="M401.654,324.673 L412.33,324.673 L412.33,341.538 L405.865,341.538 L409.127,347.188 L394.526,355.623 L389.457,346.838 C390.452,347.041 389.815,346.939 391.384,347.018 C398.863,347.018 404.927,340.955 404.927,333.47 C404.811,330.367 403.925,326.945 401.654,324.673 z" fill="#E10098"/>
|
||||
<path d="M404.661,167.852 L405.955,168.599 L404.924,170.387 L404.932,170.222 C404.845,168.502 404.954,169.29 404.661,167.852 z" fill="#E10098"/>
|
||||
<path d="M395.582,157.354 L399.536,157.354 L399.536,159.453 C397.165,157.851 398.47,158.574 395.582,157.354 z" fill="#E10098"/>
|
||||
<path d="M248.427,74.344 C243.163,75.052 239.246,78.183 237.202,83.027 L231.895,73.827 L246.492,65.397 L249.766,71.066 L253.041,65.393 L267.638,73.824 L262.381,82.933 C260.36,77.556 255.397,74.556 249.812,74.274 L248.427,74.344 z" fill="#E10098"/>
|
||||
<path d="M217.062,75.033 L216.837,75.645 C216.962,75.242 216.888,75.447 217.062,75.033 z" fill="#4D4D4D"/>
|
||||
<path d="M249.455,9.704 L250.655,9.705 C254.228,11.502 252.973,10.367 254.836,12.58 L267.32,32.761 L273.195,33.332 L289.365,15.979 C292.644,12.474 299.264,13.775 300.942,18.291 L309.225,40.462 L314.938,42.194 L334.148,28.366 C338.05,25.556 344.284,28.147 345.056,32.886 L348.862,56.264 L354.11,59.078 L375.656,49.252 C380.039,47.259 385.657,51.011 385.471,55.814 L384.639,79.536 L389.195,83.284 L412.288,77.838 C416.98,76.738 421.74,81.503 420.64,86.197 L415.187,109.302 L418.934,113.849 L442.647,113.024 C447.479,112.839 451.214,118.456 449.203,122.847 L439.384,144.405 L442.193,149.655 L465.558,153.456 C470.307,154.239 472.897,160.469 470.076,164.372 L456.254,183.578 L457.989,189.297 L480.148,197.587 C484.655,199.271 485.974,205.886 482.447,209.176 L465.112,225.345 L465.686,231.226 L485.857,243.715 C489.944,246.242 489.94,253.009 485.853,255.524 L465.683,268.012 L465.108,273.89 L482.443,290.07 C485.97,293.35 484.651,299.961 480.144,301.652 L457.985,309.938 L456.25,315.661 L470.072,334.871 C472.893,338.786 470.311,345.023 465.555,345.783 L442.189,349.584 L439.38,354.841 L449.199,376.392 C451.211,380.776 447.43,386.412 442.643,386.219 L418.93,385.383 L415.195,389.94 L420.632,413.054 C421.736,417.736 416.976,422.494 412.284,421.394 L389.191,415.951 L384.639,419.688 L385.467,443.417 C385.642,448.229 380.035,451.977 375.653,449.976 L354.106,440.149 L348.859,442.967 L345.048,466.33 C344.281,471.081 338.046,473.66 334.144,470.862 L314.942,457.022 L309.222,458.762 L300.934,480.926 C299.256,485.427 292.64,486.754 289.362,483.226 L273.191,465.88 L267.316,466.47 L254.832,486.648 C252.307,490.726 245.547,490.718 243.029,486.649 L230.541,466.47 L224.662,465.88 L208.492,483.225 C205.215,486.754 198.595,485.427 196.916,480.926 L188.628,458.762 L182.908,457.022 L163.713,470.857 C159.812,473.66 153.574,471.088 152.802,466.334 L148.991,442.967 L143.743,440.149 L122.197,449.972 C117.82,451.977 112.201,448.221 112.383,443.418 L113.21,419.688 L108.655,415.951 L85.562,421.393 C80.871,422.494 76.107,417.736 77.207,413.054 L82.647,389.94 L78.9,385.383 L55.195,386.215 C50.417,386.412 46.636,380.776 48.64,376.393 L58.462,354.841 L55.645,349.584 L32.288,345.783 C27.54,345.023 24.958,338.786 27.767,334.871 L41.585,315.661 L39.853,309.938 L17.691,301.652 C13.192,299.961 11.876,293.35 15.392,290.071 L32.727,273.89 L32.152,268.012 L11.982,255.523 C7.899,253.009 7.899,246.242 11.986,243.716 L32.152,231.226 L32.727,225.345 L15.388,209.172 C11.876,205.886 13.192,199.271 17.691,197.588 L39.853,189.297 L41.585,183.578 L27.766,164.372 C24.965,160.469 27.536,154.228 32.285,153.456 L55.645,149.655 L58.462,144.405 L48.636,122.846 C46.648,118.456 50.417,112.888 55.2,113.024 L78.912,113.849 L82.647,109.302 L77.207,86.193 C76.107,81.503 80.871,76.745 85.562,77.838 L108.655,83.284 L113.21,79.536 L112.382,55.814 C112.201,51.011 117.82,47.251 122.198,49.252 L143.743,59.078 L148.991,56.264 L152.802,32.882 C153.574,28.147 159.812,25.549 163.714,28.367 L182.919,42.194 L188.632,40.462 L196.919,18.29 C198.602,13.775 205.218,12.474 208.496,15.984 L224.666,33.332 L230.549,32.761 L243.033,12.579 C244.754,10.079 246.649,9.849 249.455,9.704 L249.455,9.704 z M106.791,128.577 L103.787,128.754 C90.816,129.985 79.189,138.21 72.662,149.275 C63.481,165.488 65.934,187.734 80.197,200.361 C75.355,215.79 73.619,231.888 73.078,247.978 C73.522,266.185 75.582,284.757 81.707,302.023 C66.377,315.329 63.174,336.608 72.764,354.321 L72.765,354.323 C82.961,371.832 105.773,380.382 124.701,371.949 C147.497,395.467 177.646,410.322 208.848,418.985 C210.62,440.59 229.008,456.329 250.22,456.864 C272.564,456.568 289.438,440.191 291.667,418.236 C323.016,410.578 351.204,393.602 374.199,371.187 C394.578,380.145 416.103,372.827 427.661,354.346 L427.662,354.346 L427.669,354.333 L427.676,354.322 L427.676,354.321 C437.52,336.347 433.593,312.772 416.775,300.464 C422.339,283.529 424.596,265.748 424.821,247.978 C424.855,235.463 423.319,223.019 420.783,210.777 C420.163,207.785 419.233,204.866 418.459,201.911 C434.019,188.514 437.531,167.282 427.778,149.275 C418.995,135.808 409.184,130.496 393.509,128.553 L390.621,128.468 C388.612,128.666 386.59,128.766 384.593,129.063 C382.732,129.388 380.835,129.705 379.07,130.408 C355.621,103.96 324.367,86.778 290.611,77.372 C286.68,58.525 268.775,46.092 250.22,45.624 L250.22,45.623 C230.501,45.839 215.695,58.28 209.973,76.722 C174.553,83.963 143.111,103.253 118.926,129.834 C115.991,128.84 112.867,128.651 109.799,128.472 L106.791,128.577 z" fill="#4D4D4D"/>
|
||||
<path d="M392.399,304.221 C398.017,304.914 401.215,305.16 406.288,308.126 C420.393,316.274 425.181,334.142 417.035,348.17 L422.349,351.258 L417.021,348.194 C408.978,362.192 391.013,367.004 376.972,358.931 C362.977,350.883 358.168,332.91 366.239,318.864 C371.921,309.534 381.465,304.273 392.399,304.221 z M391.384,319.922 C383.902,319.922 377.838,325.989 377.838,333.47 C377.838,340.955 383.902,347.018 391.384,347.018 C398.863,347.018 404.927,340.955 404.927,333.47 C404.927,325.989 398.863,319.922 391.384,319.922 z" fill="#E10098"/>
|
||||
<path d="M109.479,140.76 C115.098,141.454 118.297,141.7 123.37,144.666 C137.422,153.315 141.95,170.163 134.102,184.73 C126.057,198.731 108.091,203.543 94.05,195.471 C80.059,187.422 75.249,169.449 83.318,155.402 C89.001,146.073 98.545,140.812 109.479,140.76 z M108.46,157.416 C100.978,157.416 94.918,163.479 94.918,170.964 C94.918,178.445 100.978,184.512 108.46,184.512 C115.938,184.512 122.003,178.445 122.003,170.964 C122.003,163.479 115.938,157.416 108.46,157.416 z" fill="#E10098"/>
|
||||
<path d="M108.029,304.217 C109.902,304.351 111.797,304.307 113.648,304.618 C122.26,306.069 129.821,311.389 134.219,318.888 C142.269,332.902 137.473,350.793 123.444,358.946 C120.272,360.554 119.279,361.225 115.207,362.145 C102.806,364.949 89.767,359.133 83.42,348.195 C81.809,345.034 81.138,344.046 80.216,339.982 C77.396,327.551 83.269,314.512 94.177,308.112 C98.542,305.887 103.044,304.262 108.029,304.217 z M108.085,320.439 C100.61,320.689 94.75,326.953 95,334.43 C95.245,341.908 101.506,347.77 108.984,347.52 C116.459,347.275 122.319,341.011 122.069,333.53 C121.824,326.052 115.559,320.19 108.085,320.439 z" fill="#E10098"/>
|
||||
<path d="M390.951,140.756 C392.824,140.89 394.718,140.846 396.57,141.158 C405.181,142.608 412.74,147.927 417.137,155.427 C425.19,169.441 420.396,187.332 406.366,195.485 C392.349,203.543 374.384,198.731 366.342,184.731 L361.014,187.794 L366.341,184.729 C358.275,170.695 363.069,152.805 377.099,144.652 C381.464,142.427 385.966,140.801 390.951,140.756 z M391.386,156.674 C383.907,156.674 377.843,162.741 377.843,170.222 C377.843,177.703 383.907,183.77 391.386,183.77 C398.868,183.77 404.932,177.703 404.932,170.222 C404.932,162.741 398.868,156.674 391.386,156.674 z" fill="#E10098"/>
|
||||
<path d="M250.22,385.95 C266.425,385.951 279.515,399.049 279.515,415.261 C278.711,431.162 268.828,441.848 253.213,444.416 L250.22,444.568 C234.015,444.568 220.925,431.473 220.925,415.261 C220.925,399.049 234.016,385.951 250.22,385.95 z M249.942,402.056 C242.46,402.056 236.399,408.123 236.399,415.604 C236.399,423.085 242.46,429.152 249.942,429.152 C257.42,429.152 263.485,423.085 263.484,415.604 C263.485,408.123 257.42,402.056 249.942,402.056 z" fill="#E10098"/>
|
||||
<path d="M250.22,58.247 C266.426,58.247 279.515,71.342 279.515,87.554 C278.714,103.482 268.873,114.157 253.219,116.713 L250.22,116.864 C234.016,116.865 220.925,103.767 220.925,87.554 C220.925,71.342 234.015,58.247 250.22,58.247 z M249.812,74.274 C242.33,74.274 236.269,80.341 236.269,87.826 C236.269,95.307 242.33,101.374 249.812,101.374 C257.29,101.374 263.354,95.307 263.354,87.826 C263.354,80.341 257.29,74.274 249.812,74.274 z" fill="#E10098"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/logo/juniper-light-word.png
Normal file
After Width: | Height: | Size: 14 KiB |
31
assets/logo/juniper-light-word.svg
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="1500" height="500" viewBox="0, 0, 1500, 500">
|
||||
<g id="Layer_1">
|
||||
<path d="M256.636,427.355 L256.194,428.121 L255.675,427.821 C256.485,427.479 256.18,427.662 256.636,427.355 z" fill="#FFFFFF"/>
|
||||
<path d="M236.427,86.007 C236.247,86.893 236.347,86.293 236.269,87.826 C236.269,95.307 242.33,101.374 249.812,101.374 C257.29,101.374 263.354,95.307 263.354,87.826 C263.28,86.345 263.377,86.893 263.227,86.16 L386.485,157.354 L387.191,157.354 C381.489,159.387 378.145,164.245 377.843,170.222 C377.918,171.711 377.821,171.154 377.975,171.912 L272.961,111.256 L382.677,301.363 L382.677,180.581 C385.013,182.797 388.277,183.613 391.386,183.77 C394.739,183.741 396.829,182.819 399.536,180.991 L399.536,322.704 C397.18,320.801 394.351,320.072 391.384,319.922 L391.384,319.922 C383.902,319.922 377.838,325.989 377.838,333.47 C378.018,339.038 380.326,342.277 384.799,345.297 L384.36,345.297 L263.808,414.928 L263.479,415.498 C263.395,409.869 260.005,405.216 254.902,403.06 L361.419,341.538 L140.009,341.538 L245.87,402.684 C240.111,404.72 236.704,409.557 236.399,415.604 L236.456,416.72 L114.311,346.168 C119.379,343.968 121.978,338.858 122.069,333.53 C121.824,326.052 115.559,320.19 108.085,320.439 L108.085,320.439 C104.887,320.645 101.522,321.74 99.275,324.142 L99.959,322.956 L99.959,181.495 C102.428,183.517 105.326,184.353 108.46,184.512 C112.009,184.466 113.968,183.516 116.818,181.591 L116.818,301.423 L226.644,111.132 L121.971,171.591 L122.003,170.964 C122.003,163.479 115.938,157.416 108.46,157.416 L108.46,157.416 C104.808,157.464 102.884,158.456 99.959,160.431 L99.959,157.354 L112.905,157.354 L236.427,86.007 z M249.766,104.796 L122.863,324.673 L376.666,324.673 L249.766,104.796 z" fill="#FFFFFF"/>
|
||||
<path d="M95.421,167.451 C94.898,170.017 95.024,168.855 94.92,170.92 L93.544,168.534 L95.421,167.451 z" fill="#FFFFFF"/>
|
||||
<path d="M98.779,324.673 C96.1,327.263 95.062,330.795 95,334.43 C95.245,341.908 101.506,347.77 108.984,347.52 L109.714,347.459 L105.007,355.619 L90.406,347.188 L93.668,341.538 L87.063,341.538 L87.063,324.673 L98.779,324.673 z" fill="#FFFFFF"/>
|
||||
<path d="M401.654,324.673 L412.33,324.673 L412.33,341.538 L405.865,341.538 L409.127,347.188 L394.526,355.623 L389.457,346.838 C390.452,347.041 389.815,346.939 391.384,347.018 C398.863,347.018 404.927,340.955 404.927,333.47 C404.811,330.367 403.925,326.945 401.654,324.673 z" fill="#FFFFFF"/>
|
||||
<path d="M404.661,167.852 L405.955,168.599 L404.924,170.387 L404.932,170.222 C404.845,168.502 404.954,169.29 404.661,167.852 z" fill="#FFFFFF"/>
|
||||
<path d="M395.582,157.354 L399.536,157.354 L399.536,159.453 C397.165,157.851 398.47,158.574 395.582,157.354 z" fill="#FFFFFF"/>
|
||||
<path d="M248.427,74.344 C243.163,75.052 239.246,78.183 237.202,83.027 L231.895,73.827 L246.492,65.397 L249.766,71.066 L253.041,65.393 L267.638,73.824 L262.381,82.933 C260.36,77.556 255.397,74.556 249.812,74.274 L248.427,74.344 z" fill="#FFFFFF"/>
|
||||
<path d="M217.062,75.033 L216.837,75.645 C216.962,75.242 216.888,75.447 217.062,75.033 z" fill="#FFFFFF"/>
|
||||
<path d="M249.455,9.704 L250.655,9.705 C254.228,11.502 252.973,10.367 254.836,12.58 L267.32,32.761 L273.195,33.332 L289.365,15.979 C292.644,12.474 299.264,13.775 300.942,18.291 L309.225,40.462 L314.938,42.194 L334.148,28.366 C338.05,25.556 344.284,28.147 345.056,32.886 L348.862,56.264 L354.11,59.078 L375.656,49.252 C380.039,47.259 385.657,51.011 385.471,55.814 L384.639,79.536 L389.195,83.284 L412.288,77.838 C416.98,76.738 421.74,81.503 420.64,86.197 L415.187,109.302 L418.934,113.849 L442.647,113.024 C447.479,112.839 451.214,118.456 449.203,122.847 L439.384,144.405 L442.193,149.655 L465.558,153.456 C470.307,154.239 472.897,160.469 470.076,164.372 L456.254,183.578 L457.989,189.297 L480.148,197.587 C484.655,199.271 485.974,205.886 482.447,209.176 L465.112,225.345 L465.686,231.226 L485.857,243.715 C489.944,246.242 489.94,253.009 485.853,255.524 L465.683,268.012 L465.108,273.89 L482.443,290.07 C485.97,293.35 484.651,299.961 480.144,301.652 L457.985,309.938 L456.25,315.661 L470.072,334.871 C472.893,338.786 470.311,345.023 465.555,345.783 L442.189,349.584 L439.38,354.841 L449.199,376.392 C451.211,380.776 447.43,386.412 442.643,386.219 L418.93,385.383 L415.195,389.94 L420.632,413.054 C421.736,417.736 416.976,422.494 412.284,421.394 L389.191,415.951 L384.639,419.688 L385.467,443.417 C385.642,448.229 380.035,451.977 375.653,449.976 L354.106,440.149 L348.859,442.967 L345.048,466.33 C344.281,471.081 338.046,473.66 334.144,470.862 L314.942,457.022 L309.222,458.762 L300.934,480.926 C299.256,485.427 292.64,486.754 289.362,483.226 L273.191,465.88 L267.316,466.47 L254.832,486.648 C252.307,490.726 245.547,490.718 243.029,486.649 L230.541,466.47 L224.662,465.88 L208.492,483.225 C205.215,486.754 198.594,485.427 196.916,480.926 L188.628,458.762 L182.908,457.022 L163.713,470.857 C159.812,473.66 153.574,471.088 152.802,466.334 L148.991,442.967 L143.743,440.149 L122.197,449.972 C117.82,451.977 112.201,448.221 112.383,443.418 L113.21,419.688 L108.655,415.951 L85.562,421.393 C80.871,422.494 76.107,417.736 77.207,413.054 L82.647,389.94 L78.9,385.383 L55.195,386.215 C50.417,386.412 46.636,380.776 48.64,376.393 L58.462,354.841 L55.645,349.584 L32.288,345.783 C27.54,345.023 24.958,338.786 27.767,334.871 L41.585,315.661 L39.853,309.938 L17.691,301.652 C13.192,299.961 11.876,293.35 15.392,290.071 L32.727,273.89 L32.152,268.012 L11.982,255.523 C7.899,253.009 7.899,246.242 11.986,243.716 L32.152,231.226 L32.727,225.345 L15.388,209.172 C11.876,205.886 13.192,199.271 17.691,197.588 L39.853,189.297 L41.585,183.578 L27.766,164.372 C24.965,160.469 27.536,154.228 32.285,153.456 L55.645,149.655 L58.462,144.405 L48.636,122.846 C46.648,118.456 50.417,112.888 55.2,113.024 L78.912,113.849 L82.647,109.302 L77.207,86.193 C76.107,81.503 80.871,76.745 85.562,77.838 L108.655,83.284 L113.21,79.536 L112.382,55.814 C112.201,51.011 117.82,47.251 122.198,49.252 L143.743,59.078 L148.991,56.264 L152.802,32.882 C153.574,28.147 159.812,25.549 163.714,28.367 L182.919,42.194 L188.632,40.462 L196.919,18.29 C198.602,13.775 205.218,12.474 208.496,15.984 L224.666,33.332 L230.549,32.761 L243.033,12.579 C244.754,10.079 246.649,9.849 249.455,9.704 L249.455,9.704 z M106.791,128.577 L103.787,128.754 C90.816,129.985 79.189,138.21 72.662,149.275 C63.481,165.488 65.934,187.734 80.197,200.361 C75.355,215.79 73.619,231.888 73.078,247.978 C73.522,266.185 75.582,284.757 81.707,302.023 C66.377,315.329 63.174,336.608 72.764,354.321 L72.764,354.323 C82.961,371.832 105.773,380.382 124.701,371.949 C147.497,395.467 177.646,410.322 208.848,418.985 C210.62,440.59 229.008,456.329 250.22,456.864 C272.564,456.568 289.438,440.191 291.667,418.236 C323.016,410.578 351.204,393.602 374.199,371.187 C394.578,380.145 416.103,372.827 427.661,354.346 L427.662,354.346 L427.669,354.333 L427.676,354.322 L427.676,354.321 C437.52,336.347 433.593,312.772 416.775,300.464 C422.339,283.529 424.596,265.748 424.821,247.978 C424.855,235.463 423.319,223.019 420.783,210.777 C420.163,207.785 419.233,204.866 418.459,201.911 C434.019,188.514 437.531,167.282 427.778,149.275 C418.995,135.808 409.184,130.496 393.509,128.553 L390.621,128.468 C388.612,128.666 386.59,128.766 384.593,129.063 C382.732,129.388 380.835,129.705 379.07,130.408 C355.621,103.96 324.367,86.778 290.611,77.372 C286.68,58.525 268.775,46.092 250.22,45.624 L250.22,45.623 C230.501,45.839 215.695,58.28 209.973,76.722 C174.553,83.963 143.111,103.253 118.926,129.834 C115.991,128.84 112.867,128.651 109.799,128.472 L106.791,128.577 z" fill="#FFFFFF"/>
|
||||
<path d="M392.399,304.221 C398.017,304.914 401.215,305.16 406.288,308.126 C420.393,316.274 425.181,334.142 417.035,348.17 L422.349,351.258 L417.021,348.194 C408.979,362.192 391.013,367.004 376.972,358.931 C362.977,350.883 358.168,332.91 366.239,318.864 C371.921,309.534 381.465,304.273 392.399,304.221 z M391.385,319.922 C383.902,319.922 377.838,325.989 377.838,333.47 C377.838,340.955 383.902,347.018 391.385,347.018 C398.863,347.018 404.927,340.955 404.927,333.47 C404.927,325.989 398.863,319.922 391.385,319.922 z" fill="#FFFFFF"/>
|
||||
<path d="M109.479,140.76 C115.098,141.454 118.297,141.7 123.37,144.666 C137.422,153.315 141.95,170.163 134.102,184.73 C126.057,198.731 108.091,203.543 94.05,195.471 C80.059,187.423 75.249,169.449 83.318,155.402 C89.001,146.073 98.545,140.812 109.479,140.76 z M108.46,157.416 C100.978,157.416 94.918,163.479 94.918,170.964 C94.918,178.445 100.978,184.512 108.46,184.512 C115.938,184.512 122.003,178.445 122.003,170.964 C122.003,163.479 115.938,157.416 108.46,157.416 z" fill="#FFFFFF"/>
|
||||
<path d="M108.029,304.217 C109.902,304.351 111.797,304.307 113.648,304.618 C122.26,306.069 129.821,311.389 134.219,318.888 C142.269,332.902 137.473,350.793 123.444,358.946 C120.272,360.554 119.279,361.225 115.207,362.146 C102.806,364.949 89.767,359.133 83.42,348.195 C81.809,345.034 81.138,344.046 80.216,339.982 C77.396,327.551 83.269,314.512 94.177,308.112 C98.542,305.887 103.044,304.262 108.029,304.217 z M108.085,320.439 C100.61,320.689 94.75,326.953 94.999,334.43 C95.245,341.908 101.506,347.77 108.984,347.52 C116.459,347.275 122.319,341.011 122.069,333.53 C121.824,326.052 115.559,320.19 108.085,320.439 z" fill="#FFFFFF"/>
|
||||
<path d="M390.951,140.756 C392.824,140.89 394.718,140.846 396.57,141.158 C405.181,142.608 412.74,147.927 417.137,155.427 C425.19,169.441 420.396,187.332 406.366,195.485 C392.349,203.543 374.384,198.731 366.342,184.731 L361.014,187.794 L366.341,184.729 C358.275,170.695 363.069,152.805 377.099,144.652 C381.464,142.427 385.966,140.801 390.951,140.756 z M391.386,156.674 C383.907,156.674 377.843,162.741 377.843,170.222 C377.843,177.703 383.907,183.77 391.386,183.77 C398.868,183.77 404.932,177.703 404.932,170.222 C404.932,162.741 398.868,156.674 391.386,156.674 z" fill="#FFFFFF"/>
|
||||
<path d="M250.22,385.95 C266.425,385.951 279.515,399.049 279.515,415.261 C278.711,431.162 268.828,441.848 253.213,444.416 L250.22,444.568 C234.015,444.568 220.925,431.473 220.925,415.261 C220.925,399.049 234.016,385.951 250.22,385.95 z M249.942,402.056 C242.46,402.056 236.4,408.123 236.399,415.604 C236.4,423.085 242.46,429.152 249.942,429.152 C257.42,429.152 263.485,423.085 263.485,415.604 C263.485,408.123 257.42,402.056 249.942,402.056 z" fill="#FFFFFF"/>
|
||||
<path d="M250.22,58.247 C266.426,58.247 279.515,71.342 279.515,87.554 C278.714,103.482 268.873,114.157 253.219,116.713 L250.22,116.864 C234.016,116.865 220.925,103.767 220.925,87.554 C220.925,71.342 234.015,58.247 250.22,58.247 z M249.812,74.274 C242.33,74.274 236.269,80.341 236.269,87.826 C236.269,95.307 242.33,101.374 249.812,101.374 C257.29,101.374 263.354,95.307 263.354,87.826 C263.354,80.341 257.29,74.274 249.812,74.274 z" fill="#FFFFFF"/>
|
||||
<g>
|
||||
<path d="M685.358,281.898 Q685.358,297.357 681.297,310.064 Q677.236,322.771 669.114,331.941 Q660.992,341.111 649.202,346.22 Q637.412,351.329 621.954,351.329 Q609.116,351.329 597.85,348.185 Q586.584,345.04 577.938,338.491 Q569.292,331.941 564.052,321.854 Q558.812,311.767 558.288,297.881 Q558.288,295.523 559.598,293.82 Q560.908,292.117 563.79,292.117 L569.03,292.117 Q571.65,292.117 573.091,293.689 Q574.532,295.26 574.794,297.618 Q575.58,307.837 579.641,315.042 Q583.702,322.247 590.121,326.832 Q596.54,331.417 604.662,333.513 Q612.784,335.609 621.954,335.609 Q633.482,335.609 642.259,331.678 Q651.036,327.749 656.931,320.544 Q662.826,313.339 665.839,303.383 Q668.852,293.427 668.852,281.637 L668.852,181.029 L572.174,181.029 Q566.41,181.029 566.41,175.265 L566.41,171.073 Q566.41,165.309 572.174,165.309 L679.332,165.309 Q685.358,165.309 685.358,171.335 z" fill="#FFFFFF"/>
|
||||
<path d="M727.016,218.232 Q727.016,212.468 732.78,212.468 L737.234,212.468 Q742.998,212.468 742.998,218.232 L742.998,291.33 Q742.998,336.132 782.036,336.132 Q800.9,336.132 812.035,324.342 Q823.17,312.552 823.17,291.33 L823.17,218.232 Q823.17,212.468 828.934,212.468 L833.388,212.468 Q839.152,212.468 839.152,218.232 L839.152,342.944 Q839.152,348.708 833.388,348.708 L828.934,348.708 Q823.17,348.708 823.17,342.944 L823.17,330.63 Q816.096,339.8 806.14,345.564 Q796.184,351.328 779.416,351.328 Q766.054,351.328 756.229,346.874 Q746.404,342.421 739.854,334.56 Q733.304,326.701 730.16,315.958 Q727.016,305.216 727.016,292.641 z" fill="#FFFFFF"/>
|
||||
<path d="M995.042,342.944 Q995.042,348.708 989.278,348.708 L984.824,348.708 Q979.06,348.708 979.06,342.944 L979.06,269.846 Q979.06,248.886 968.842,236.965 Q958.624,225.044 938.974,225.044 Q920.11,225.044 908.844,236.834 Q897.578,248.624 897.578,269.846 L897.578,342.944 Q897.578,348.708 891.814,348.708 L887.36,348.708 Q881.596,348.708 881.596,342.944 L881.596,218.232 Q881.596,212.468 887.36,212.468 L891.814,212.468 Q897.578,212.468 897.578,218.232 L897.578,230.546 Q904.652,221.376 914.608,215.612 Q924.564,209.848 941.594,209.848 Q954.956,209.848 964.912,214.302 Q974.868,218.756 981.549,226.616 Q988.23,234.476 991.636,245.218 Q995.042,255.96 995.042,268.536 z" fill="#FFFFFF"/>
|
||||
<path d="M1055.04,178.671 Q1055.04,181.29 1053.337,182.863 Q1051.634,184.434 1049.014,184.434 L1039.058,184.434 Q1033.294,184.434 1033.294,178.671 L1033.294,168.715 Q1033.294,166.094 1034.866,164.391 Q1036.438,162.689 1039.058,162.689 L1049.014,162.689 Q1051.634,162.689 1053.337,164.391 Q1055.04,166.094 1055.04,168.715 z M1052.158,342.944 Q1052.158,348.708 1046.394,348.708 L1041.94,348.708 Q1036.176,348.708 1036.176,342.944 L1036.176,218.232 Q1036.176,212.469 1041.94,212.469 L1046.394,212.469 Q1052.158,212.469 1052.158,218.232 z" fill="#FFFFFF"/>
|
||||
<path d="M1154.862,351.328 Q1138.356,351.328 1127.876,345.302 Q1117.396,339.276 1110.846,330.63 L1110.846,392.724 Q1110.846,398.488 1105.082,398.488 L1100.628,398.488 Q1094.864,398.488 1094.864,392.724 L1094.864,218.232 Q1094.864,212.469 1100.628,212.469 L1105.082,212.469 Q1110.846,212.469 1110.846,218.232 L1110.846,230.546 Q1117.396,221.639 1127.876,215.743 Q1138.356,209.848 1154.862,209.848 Q1170.058,209.848 1180.669,215.219 Q1191.28,220.59 1197.83,229.368 Q1204.38,238.145 1207.393,249.279 Q1210.406,260.414 1210.93,271.943 Q1211.192,275.872 1211.192,280.589 Q1211.192,285.305 1210.93,289.234 Q1210.406,300.763 1207.393,311.897 Q1204.38,323.033 1197.83,331.81 Q1191.28,340.586 1180.669,345.957 Q1170.058,351.328 1154.862,351.328 z M1110.846,271.156 Q1110.584,275.086 1110.584,281.375 Q1110.584,287.663 1110.846,291.592 Q1111.108,299.453 1113.597,307.444 Q1116.086,315.435 1121.326,321.723 Q1126.566,328.01 1134.426,332.072 Q1142.286,336.133 1153.29,336.133 Q1164.818,336.133 1172.547,332.203 Q1180.276,328.272 1185.123,321.591 Q1189.97,314.91 1192.197,306.134 Q1194.424,297.357 1194.948,287.924 Q1195.21,280.589 1194.948,273.253 Q1194.424,263.82 1192.197,255.174 Q1189.97,246.529 1185.123,239.848 Q1180.276,233.167 1172.547,229.105 Q1164.818,225.044 1153.29,225.044 Q1142.024,225.044 1134.164,229.236 Q1126.304,233.428 1121.195,240.109 Q1116.086,246.79 1113.597,254.913 Q1111.108,263.035 1110.846,271.156 z" fill="#FFFFFF"/>
|
||||
<path d="M1240.012,270.108 Q1241.06,256.746 1245.383,245.611 Q1249.706,234.476 1256.911,226.616 Q1264.116,218.756 1274.334,214.302 Q1284.552,209.848 1297.39,209.848 Q1324.9,209.848 1340.096,227.795 Q1355.292,245.742 1355.292,277.444 L1355.292,281.374 Q1355.292,283.994 1353.589,285.566 Q1351.886,287.138 1349.266,287.138 L1255.994,287.138 L1255.994,289.758 Q1256.256,298.666 1259.269,307.05 Q1262.282,315.434 1267.522,321.853 Q1272.762,328.272 1280.36,332.202 Q1287.958,336.132 1297.39,336.132 Q1305.774,336.132 1312.062,334.036 Q1318.35,331.94 1322.804,329.189 Q1327.258,326.438 1330.009,323.556 Q1332.76,320.674 1333.808,319.102 Q1336.166,315.697 1337.476,315.041 Q1338.786,314.386 1341.668,314.386 L1345.86,314.386 Q1348.218,314.386 1349.921,315.827 Q1351.624,317.268 1351.362,319.626 Q1351.1,323.294 1347.301,328.796 Q1343.502,334.298 1336.559,339.276 Q1329.616,344.254 1319.66,347.791 Q1309.704,351.328 1297.39,351.328 Q1284.814,351.328 1274.596,347.005 Q1264.378,342.682 1257.042,334.691 Q1249.706,326.701 1245.383,315.565 Q1241.06,304.43 1240.012,291.068 Q1239.75,287.138 1239.75,280.589 Q1239.75,274.038 1240.012,270.108 z M1255.994,272.466 L1339.31,272.466 L1339.31,271.418 Q1339.31,261.724 1336.428,253.209 Q1333.546,244.694 1328.175,238.406 Q1322.804,232.118 1314.944,228.581 Q1307.084,225.044 1297.39,225.044 Q1287.434,225.044 1279.836,228.581 Q1272.238,232.118 1266.867,238.406 Q1261.496,244.694 1258.745,253.209 Q1255.994,261.724 1255.994,271.418 z" fill="#FFFFFF"/>
|
||||
<path d="M1405.858,230.546 Q1415.29,212.468 1441.49,212.468 L1451.184,212.468 Q1456.948,212.468 1456.948,218.232 L1456.948,221.9 Q1456.948,227.664 1451.184,227.664 L1439.918,227.664 Q1424.198,227.664 1415.028,236.834 Q1405.858,246.004 1405.858,261.724 L1405.858,342.944 Q1405.858,345.564 1404.155,347.136 Q1402.452,348.708 1399.832,348.708 L1395.64,348.708 Q1389.876,348.708 1389.876,342.944 L1389.876,218.494 Q1389.876,215.874 1391.448,214.171 Q1393.02,212.468 1395.64,212.468 L1399.832,212.468 Q1402.452,212.468 1404.155,214.171 Q1405.858,215.874 1405.858,218.494 z" fill="#FFFFFF"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 17 KiB |
BIN
assets/logo/juniper-light.png
Normal file
After Width: | Height: | Size: 12 KiB |
22
assets/logo/juniper-light.svg
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="500" height="500" viewBox="0, 0, 500, 500">
|
||||
<g id="Layer_1">
|
||||
<path d="M256.636,427.355 L256.194,428.121 L255.675,427.821 C256.485,427.479 256.18,427.662 256.636,427.355 z" fill="#E10098"/>
|
||||
<path d="M236.427,86.007 C236.247,86.893 236.347,86.293 236.269,87.826 C236.269,95.307 242.33,101.374 249.812,101.374 C257.29,101.374 263.354,95.307 263.354,87.826 C263.28,86.345 263.377,86.893 263.227,86.16 L386.485,157.354 L387.191,157.354 C381.489,159.387 378.145,164.245 377.843,170.222 C377.918,171.711 377.821,171.154 377.975,171.912 L272.961,111.256 L382.678,301.363 L382.678,180.581 C385.013,182.797 388.277,183.613 391.386,183.77 C394.739,183.741 396.829,182.819 399.536,180.991 L399.536,322.704 C397.18,320.801 394.351,320.072 391.384,319.922 L391.384,319.922 C383.902,319.922 377.838,325.989 377.838,333.47 C378.018,339.038 380.326,342.277 384.799,345.297 L384.36,345.297 L263.808,414.928 L263.479,415.498 C263.395,409.869 260.005,405.216 254.902,403.06 L361.419,341.538 L140.009,341.538 L245.87,402.684 C240.111,404.72 236.704,409.557 236.399,415.604 L236.456,416.72 L114.311,346.168 C119.379,343.968 121.978,338.858 122.069,333.53 C121.824,326.052 115.559,320.19 108.085,320.439 L108.085,320.439 C104.887,320.645 101.522,321.74 99.275,324.142 L99.959,322.956 L99.959,181.495 C102.428,183.517 105.326,184.353 108.46,184.512 C112.009,184.466 113.968,183.516 116.818,181.591 L116.818,301.423 L226.644,111.132 L121.971,171.591 L122.003,170.964 C122.003,163.479 115.938,157.416 108.46,157.416 L108.46,157.416 C104.808,157.464 102.884,158.456 99.959,160.431 L99.959,157.354 L112.905,157.354 L236.427,86.007 z M249.766,104.796 L122.863,324.673 L376.666,324.673 L249.766,104.796 z" fill="#FFFFFF"/>
|
||||
<path d="M95.421,167.451 C94.898,170.017 95.024,168.855 94.92,170.92 L93.544,168.534 L95.421,167.451 z" fill="#E10098"/>
|
||||
<path d="M98.779,324.673 C96.1,327.263 95.062,330.795 95,334.43 C95.245,341.908 101.506,347.77 108.984,347.52 L109.714,347.459 L105.007,355.619 L90.406,347.188 L93.668,341.538 L87.063,341.538 L87.063,324.673 L98.779,324.673 z" fill="#E10098"/>
|
||||
<path d="M401.654,324.673 L412.33,324.673 L412.33,341.538 L405.865,341.538 L409.127,347.188 L394.526,355.623 L389.457,346.838 C390.452,347.041 389.815,346.939 391.384,347.018 C398.863,347.018 404.927,340.955 404.927,333.47 C404.811,330.367 403.925,326.945 401.654,324.673 z" fill="#E10098"/>
|
||||
<path d="M404.661,167.852 L405.955,168.599 L404.924,170.387 L404.932,170.222 C404.845,168.502 404.954,169.29 404.661,167.852 z" fill="#E10098"/>
|
||||
<path d="M395.582,157.354 L399.536,157.354 L399.536,159.453 C397.165,157.851 398.47,158.574 395.582,157.354 z" fill="#E10098"/>
|
||||
<path d="M248.427,74.344 C243.163,75.052 239.246,78.183 237.202,83.027 L231.895,73.827 L246.492,65.397 L249.766,71.066 L253.041,65.393 L267.638,73.824 L262.381,82.933 C260.36,77.556 255.397,74.556 249.812,74.274 L248.427,74.344 z" fill="#E10098"/>
|
||||
<path d="M217.062,75.033 L216.837,75.645 C216.962,75.242 216.888,75.447 217.062,75.033 z" fill="#4D4D4D"/>
|
||||
<path d="M249.455,9.704 L250.655,9.705 C254.228,11.502 252.973,10.367 254.836,12.58 L267.32,32.761 L273.195,33.332 L289.365,15.979 C292.644,12.474 299.264,13.775 300.942,18.291 L309.225,40.462 L314.938,42.194 L334.148,28.366 C338.05,25.556 344.284,28.147 345.056,32.886 L348.862,56.264 L354.11,59.078 L375.656,49.252 C380.039,47.259 385.657,51.011 385.471,55.814 L384.639,79.536 L389.195,83.284 L412.288,77.838 C416.98,76.738 421.74,81.503 420.64,86.197 L415.187,109.302 L418.934,113.849 L442.647,113.024 C447.479,112.839 451.214,118.456 449.203,122.847 L439.384,144.405 L442.193,149.655 L465.558,153.456 C470.307,154.239 472.897,160.469 470.076,164.372 L456.254,183.578 L457.989,189.297 L480.148,197.587 C484.655,199.271 485.974,205.886 482.447,209.176 L465.112,225.345 L465.686,231.226 L485.857,243.715 C489.944,246.242 489.94,253.009 485.853,255.524 L465.683,268.012 L465.108,273.89 L482.443,290.07 C485.97,293.35 484.651,299.961 480.144,301.652 L457.985,309.938 L456.25,315.661 L470.072,334.871 C472.893,338.786 470.311,345.023 465.555,345.783 L442.189,349.584 L439.38,354.841 L449.199,376.392 C451.211,380.776 447.43,386.412 442.643,386.219 L418.93,385.383 L415.195,389.94 L420.632,413.054 C421.736,417.736 416.976,422.494 412.284,421.394 L389.191,415.951 L384.639,419.688 L385.467,443.417 C385.642,448.229 380.035,451.977 375.653,449.976 L354.106,440.149 L348.859,442.967 L345.048,466.33 C344.281,471.081 338.046,473.66 334.144,470.862 L314.942,457.022 L309.222,458.762 L300.934,480.926 C299.256,485.427 292.64,486.754 289.362,483.226 L273.191,465.88 L267.316,466.47 L254.832,486.648 C252.307,490.726 245.547,490.718 243.029,486.649 L230.541,466.47 L224.662,465.88 L208.492,483.225 C205.215,486.754 198.595,485.427 196.916,480.926 L188.628,458.762 L182.908,457.022 L163.713,470.857 C159.812,473.66 153.574,471.088 152.802,466.334 L148.991,442.967 L143.743,440.149 L122.197,449.972 C117.82,451.977 112.201,448.221 112.383,443.418 L113.21,419.688 L108.655,415.951 L85.562,421.393 C80.871,422.494 76.107,417.736 77.207,413.054 L82.647,389.94 L78.9,385.383 L55.195,386.215 C50.417,386.412 46.636,380.776 48.64,376.393 L58.462,354.841 L55.645,349.584 L32.288,345.783 C27.54,345.023 24.958,338.786 27.767,334.871 L41.585,315.661 L39.853,309.938 L17.691,301.652 C13.192,299.961 11.876,293.35 15.392,290.071 L32.727,273.89 L32.152,268.012 L11.982,255.523 C7.899,253.009 7.899,246.242 11.986,243.716 L32.152,231.226 L32.727,225.345 L15.388,209.172 C11.876,205.886 13.192,199.271 17.691,197.588 L39.853,189.297 L41.585,183.578 L27.766,164.372 C24.965,160.469 27.536,154.228 32.285,153.456 L55.645,149.655 L58.462,144.405 L48.636,122.846 C46.648,118.456 50.417,112.888 55.2,113.024 L78.912,113.849 L82.647,109.302 L77.207,86.193 C76.107,81.503 80.871,76.745 85.562,77.838 L108.655,83.284 L113.21,79.536 L112.382,55.814 C112.201,51.011 117.82,47.251 122.198,49.252 L143.743,59.078 L148.991,56.264 L152.802,32.882 C153.574,28.147 159.812,25.549 163.714,28.367 L182.919,42.194 L188.632,40.462 L196.919,18.29 C198.602,13.775 205.218,12.474 208.496,15.984 L224.666,33.332 L230.549,32.761 L243.033,12.579 C244.754,10.079 246.649,9.849 249.455,9.704 L249.455,9.704 z M106.791,128.577 L103.787,128.754 C90.816,129.985 79.189,138.21 72.662,149.275 C63.481,165.488 65.934,187.734 80.197,200.361 C75.355,215.79 73.619,231.888 73.078,247.978 C73.522,266.185 75.582,284.757 81.707,302.023 C66.377,315.329 63.174,336.608 72.764,354.321 L72.765,354.323 C82.961,371.832 105.773,380.382 124.701,371.949 C147.497,395.467 177.646,410.322 208.848,418.985 C210.62,440.59 229.008,456.329 250.22,456.864 C272.564,456.568 289.438,440.191 291.667,418.236 C323.016,410.578 351.204,393.602 374.199,371.187 C394.578,380.145 416.103,372.827 427.661,354.346 L427.662,354.346 L427.669,354.333 L427.676,354.322 L427.676,354.321 C437.52,336.347 433.593,312.772 416.775,300.464 C422.339,283.529 424.596,265.748 424.821,247.978 C424.855,235.463 423.319,223.019 420.783,210.777 C420.163,207.785 419.233,204.866 418.459,201.911 C434.019,188.514 437.531,167.282 427.778,149.275 C418.995,135.808 409.184,130.496 393.509,128.553 L390.621,128.468 C388.612,128.666 386.59,128.766 384.593,129.063 C382.732,129.388 380.835,129.705 379.07,130.408 C355.621,103.96 324.367,86.778 290.611,77.372 C286.68,58.525 268.775,46.092 250.22,45.624 L250.22,45.623 C230.501,45.839 215.695,58.28 209.973,76.722 C174.553,83.963 143.111,103.253 118.926,129.834 C115.991,128.84 112.867,128.651 109.799,128.472 L106.791,128.577 z" fill="#FFFFFF"/>
|
||||
<path d="M392.399,304.221 C398.017,304.914 401.215,305.16 406.288,308.126 C420.393,316.274 425.181,334.142 417.035,348.17 L422.349,351.258 L417.021,348.194 C408.978,362.192 391.013,367.004 376.972,358.931 C362.977,350.883 358.168,332.91 366.239,318.864 C371.921,309.534 381.465,304.273 392.399,304.221 z M391.384,319.922 C383.902,319.922 377.838,325.989 377.838,333.47 C377.838,340.955 383.902,347.018 391.384,347.018 C398.863,347.018 404.927,340.955 404.927,333.47 C404.927,325.989 398.863,319.922 391.384,319.922 z" fill="#FFFFFF"/>
|
||||
<path d="M109.479,140.76 C115.098,141.454 118.297,141.7 123.37,144.666 C137.422,153.315 141.95,170.163 134.102,184.73 C126.057,198.731 108.091,203.543 94.05,195.471 C80.059,187.422 75.249,169.449 83.318,155.402 C89.001,146.073 98.545,140.812 109.479,140.76 z M108.46,157.416 C100.978,157.416 94.918,163.479 94.918,170.964 C94.918,178.445 100.978,184.512 108.46,184.512 C115.938,184.512 122.003,178.445 122.003,170.964 C122.003,163.479 115.938,157.416 108.46,157.416 z" fill="#FFFFFF"/>
|
||||
<path d="M108.029,304.217 C109.902,304.351 111.797,304.307 113.648,304.618 C122.26,306.069 129.821,311.389 134.219,318.888 C142.269,332.902 137.473,350.793 123.444,358.946 C120.272,360.554 119.279,361.225 115.207,362.145 C102.806,364.949 89.767,359.133 83.42,348.195 C81.809,345.034 81.138,344.046 80.216,339.982 C77.396,327.551 83.269,314.512 94.177,308.112 C98.542,305.887 103.044,304.262 108.029,304.217 z M108.085,320.439 C100.61,320.689 94.75,326.953 95,334.43 C95.245,341.908 101.506,347.77 108.984,347.52 C116.459,347.275 122.319,341.011 122.069,333.53 C121.824,326.052 115.559,320.19 108.085,320.439 z" fill="#FFFFFF"/>
|
||||
<path d="M390.951,140.756 C392.824,140.89 394.718,140.846 396.57,141.158 C405.181,142.608 412.74,147.927 417.137,155.427 C425.19,169.441 420.396,187.332 406.366,195.485 C392.349,203.543 374.384,198.731 366.342,184.731 L361.014,187.794 L366.341,184.729 C358.275,170.695 363.069,152.805 377.099,144.652 C381.464,142.427 385.966,140.801 390.951,140.756 z M391.386,156.674 C383.907,156.674 377.843,162.741 377.843,170.222 C377.843,177.703 383.907,183.77 391.386,183.77 C398.868,183.77 404.932,177.703 404.932,170.222 C404.932,162.741 398.868,156.674 391.386,156.674 z" fill="#FFFFFF"/>
|
||||
<path d="M250.22,385.95 C266.425,385.951 279.515,399.049 279.515,415.261 C278.711,431.162 268.828,441.848 253.213,444.416 L250.22,444.568 C234.015,444.568 220.925,431.473 220.925,415.261 C220.925,399.049 234.016,385.951 250.22,385.95 z M249.942,402.056 C242.46,402.056 236.399,408.123 236.399,415.604 C236.399,423.085 242.46,429.152 249.942,429.152 C257.42,429.152 263.485,423.085 263.484,415.604 C263.485,408.123 257.42,402.056 249.942,402.056 z" fill="#FFFFFF"/>
|
||||
<path d="M250.22,58.247 C266.426,58.247 279.515,71.342 279.515,87.554 C278.714,103.482 268.873,114.157 253.219,116.713 L250.22,116.864 C234.016,116.865 220.925,103.767 220.925,87.554 C220.925,71.342 234.015,58.247 250.22,58.247 z M249.812,74.274 C242.33,74.274 236.269,80.341 236.269,87.826 C236.269,95.307 242.33,101.374 249.812,101.374 C257.29,101.374 263.354,95.307 263.354,87.826 C263.354,80.341 257.29,74.274 249.812,74.274 z" fill="#FFFFFF"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/logo/juniper-mono-word.png
Normal file
After Width: | Height: | Size: 19 KiB |
31
assets/logo/juniper-mono-word.svg
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="1500" height="500" viewBox="0, 0, 1500, 500">
|
||||
<g id="Layer_1">
|
||||
<path d="M256.636,427.355 L256.194,428.121 L255.675,427.821 C256.485,427.479 256.18,427.662 256.636,427.355 z" fill="#4D4D4D"/>
|
||||
<path d="M236.427,86.007 C236.247,86.893 236.347,86.293 236.269,87.826 C236.269,95.307 242.33,101.374 249.812,101.374 C257.29,101.374 263.354,95.307 263.354,87.826 C263.28,86.345 263.377,86.893 263.227,86.16 L386.485,157.354 L387.191,157.354 C381.489,159.387 378.145,164.245 377.843,170.222 C377.918,171.711 377.821,171.154 377.975,171.912 L272.961,111.256 L382.677,301.363 L382.677,180.581 C385.013,182.797 388.277,183.613 391.386,183.77 C394.739,183.741 396.829,182.819 399.536,180.991 L399.536,322.704 C397.18,320.801 394.351,320.072 391.384,319.922 L391.384,319.922 C383.902,319.922 377.838,325.989 377.838,333.47 C378.018,339.038 380.326,342.277 384.799,345.297 L384.36,345.297 L263.808,414.928 L263.479,415.498 C263.395,409.869 260.005,405.216 254.902,403.06 L361.419,341.538 L140.009,341.538 L245.87,402.684 C240.111,404.72 236.704,409.557 236.399,415.604 L236.456,416.72 L114.311,346.168 C119.379,343.968 121.978,338.858 122.069,333.53 C121.824,326.052 115.559,320.19 108.085,320.439 L108.085,320.439 C104.887,320.645 101.522,321.74 99.275,324.142 L99.959,322.956 L99.959,181.495 C102.428,183.517 105.326,184.353 108.46,184.512 C112.009,184.466 113.968,183.516 116.818,181.591 L116.818,301.423 L226.644,111.132 L121.971,171.591 L122.003,170.964 C122.003,163.479 115.938,157.416 108.46,157.416 L108.46,157.416 C104.808,157.464 102.884,158.456 99.959,160.431 L99.959,157.354 L112.905,157.354 L236.427,86.007 z M249.766,104.796 L122.863,324.673 L376.666,324.673 L249.766,104.796 z" fill="#4D4D4D"/>
|
||||
<path d="M95.421,167.451 C94.898,170.017 95.024,168.855 94.92,170.92 L93.544,168.534 L95.421,167.451 z" fill="#4D4D4D"/>
|
||||
<path d="M98.779,324.673 C96.1,327.263 95.062,330.795 95,334.43 C95.245,341.908 101.506,347.77 108.984,347.52 L109.714,347.459 L105.007,355.619 L90.406,347.188 L93.668,341.538 L87.063,341.538 L87.063,324.673 L98.779,324.673 z" fill="#4D4D4D"/>
|
||||
<path d="M401.654,324.673 L412.33,324.673 L412.33,341.538 L405.865,341.538 L409.127,347.188 L394.526,355.623 L389.457,346.838 C390.452,347.041 389.815,346.939 391.384,347.018 C398.863,347.018 404.927,340.955 404.927,333.47 C404.811,330.367 403.925,326.945 401.654,324.673 z" fill="#4D4D4D"/>
|
||||
<path d="M404.661,167.852 L405.955,168.599 L404.924,170.387 L404.932,170.222 C404.845,168.502 404.954,169.29 404.661,167.852 z" fill="#4D4D4D"/>
|
||||
<path d="M395.582,157.354 L399.536,157.354 L399.536,159.453 C397.165,157.851 398.47,158.574 395.582,157.354 z" fill="#4D4D4D"/>
|
||||
<path d="M248.427,74.344 C243.163,75.052 239.246,78.183 237.202,83.027 L231.895,73.827 L246.492,65.397 L249.766,71.066 L253.041,65.393 L267.638,73.824 L262.381,82.933 C260.36,77.556 255.397,74.556 249.812,74.274 L248.427,74.344 z" fill="#4D4D4D"/>
|
||||
<path d="M217.062,75.033 L216.837,75.645 C216.962,75.242 216.888,75.447 217.062,75.033 z" fill="#4D4D4D"/>
|
||||
<path d="M249.455,9.704 L250.655,9.705 C254.228,11.502 252.973,10.367 254.836,12.58 L267.32,32.761 L273.195,33.332 L289.365,15.979 C292.644,12.474 299.264,13.775 300.942,18.291 L309.225,40.462 L314.938,42.194 L334.148,28.366 C338.05,25.556 344.284,28.147 345.056,32.886 L348.862,56.264 L354.11,59.078 L375.656,49.252 C380.039,47.259 385.657,51.011 385.471,55.814 L384.639,79.536 L389.195,83.284 L412.288,77.838 C416.98,76.738 421.74,81.503 420.64,86.197 L415.187,109.302 L418.934,113.849 L442.647,113.024 C447.479,112.839 451.214,118.456 449.203,122.847 L439.384,144.405 L442.193,149.655 L465.558,153.456 C470.307,154.239 472.897,160.469 470.076,164.372 L456.254,183.578 L457.989,189.297 L480.148,197.587 C484.655,199.271 485.974,205.886 482.447,209.176 L465.112,225.345 L465.686,231.226 L485.857,243.715 C489.944,246.242 489.94,253.009 485.853,255.524 L465.683,268.012 L465.108,273.89 L482.443,290.07 C485.97,293.35 484.651,299.961 480.144,301.652 L457.985,309.938 L456.25,315.661 L470.072,334.871 C472.893,338.786 470.311,345.023 465.555,345.783 L442.189,349.584 L439.38,354.841 L449.199,376.392 C451.211,380.776 447.43,386.412 442.643,386.219 L418.93,385.383 L415.195,389.94 L420.632,413.054 C421.736,417.736 416.976,422.494 412.284,421.394 L389.191,415.951 L384.639,419.688 L385.467,443.417 C385.642,448.229 380.035,451.977 375.653,449.976 L354.106,440.149 L348.859,442.967 L345.048,466.33 C344.281,471.081 338.046,473.66 334.144,470.862 L314.942,457.022 L309.222,458.762 L300.934,480.926 C299.256,485.427 292.64,486.754 289.362,483.226 L273.191,465.88 L267.316,466.47 L254.832,486.648 C252.307,490.726 245.547,490.718 243.029,486.649 L230.541,466.47 L224.662,465.88 L208.492,483.225 C205.215,486.754 198.594,485.427 196.916,480.926 L188.628,458.762 L182.908,457.022 L163.713,470.857 C159.812,473.66 153.574,471.088 152.802,466.334 L148.991,442.967 L143.743,440.149 L122.197,449.972 C117.82,451.977 112.201,448.221 112.383,443.418 L113.21,419.688 L108.655,415.951 L85.562,421.393 C80.871,422.494 76.107,417.736 77.207,413.054 L82.647,389.94 L78.9,385.383 L55.195,386.215 C50.417,386.412 46.636,380.776 48.64,376.393 L58.462,354.841 L55.645,349.584 L32.288,345.783 C27.54,345.023 24.958,338.786 27.767,334.871 L41.585,315.661 L39.853,309.938 L17.691,301.652 C13.192,299.961 11.876,293.35 15.392,290.071 L32.727,273.89 L32.152,268.012 L11.982,255.523 C7.899,253.009 7.899,246.242 11.986,243.716 L32.152,231.226 L32.727,225.345 L15.388,209.172 C11.876,205.886 13.192,199.271 17.691,197.588 L39.853,189.297 L41.585,183.578 L27.766,164.372 C24.965,160.469 27.536,154.228 32.285,153.456 L55.645,149.655 L58.462,144.405 L48.636,122.846 C46.648,118.456 50.417,112.888 55.2,113.024 L78.912,113.849 L82.647,109.302 L77.207,86.193 C76.107,81.503 80.871,76.745 85.562,77.838 L108.655,83.284 L113.21,79.536 L112.382,55.814 C112.201,51.011 117.82,47.251 122.198,49.252 L143.743,59.078 L148.991,56.264 L152.802,32.882 C153.574,28.147 159.812,25.549 163.714,28.367 L182.919,42.194 L188.632,40.462 L196.919,18.29 C198.602,13.775 205.218,12.474 208.496,15.984 L224.666,33.332 L230.549,32.761 L243.033,12.579 C244.754,10.079 246.649,9.849 249.455,9.704 L249.455,9.704 z M106.791,128.577 L103.787,128.754 C90.816,129.985 79.189,138.21 72.662,149.275 C63.481,165.488 65.934,187.734 80.197,200.361 C75.355,215.79 73.619,231.888 73.078,247.978 C73.522,266.185 75.582,284.757 81.707,302.023 C66.377,315.329 63.174,336.608 72.764,354.321 L72.764,354.323 C82.961,371.832 105.773,380.382 124.701,371.949 C147.497,395.467 177.646,410.322 208.848,418.985 C210.62,440.59 229.008,456.329 250.22,456.864 C272.564,456.568 289.438,440.191 291.667,418.236 C323.016,410.578 351.204,393.602 374.199,371.187 C394.578,380.145 416.103,372.827 427.661,354.346 L427.662,354.346 L427.669,354.333 L427.676,354.322 L427.676,354.321 C437.52,336.347 433.593,312.772 416.775,300.464 C422.339,283.529 424.596,265.748 424.821,247.978 C424.855,235.463 423.319,223.019 420.783,210.777 C420.163,207.785 419.233,204.866 418.459,201.911 C434.019,188.514 437.531,167.282 427.778,149.275 C418.995,135.808 409.184,130.496 393.509,128.553 L390.621,128.468 C388.612,128.666 386.59,128.766 384.593,129.063 C382.732,129.388 380.835,129.705 379.07,130.408 C355.621,103.96 324.367,86.778 290.611,77.372 C286.68,58.525 268.775,46.092 250.22,45.624 L250.22,45.623 C230.501,45.839 215.695,58.28 209.973,76.722 C174.553,83.963 143.111,103.253 118.926,129.834 C115.991,128.84 112.867,128.651 109.799,128.472 L106.791,128.577 z" fill="#4D4D4D"/>
|
||||
<path d="M392.399,304.221 C398.017,304.914 401.215,305.16 406.288,308.126 C420.393,316.274 425.181,334.142 417.035,348.17 L422.349,351.258 L417.021,348.194 C408.979,362.192 391.013,367.004 376.972,358.931 C362.977,350.883 358.168,332.91 366.239,318.864 C371.921,309.534 381.465,304.273 392.399,304.221 z M391.385,319.922 C383.902,319.922 377.838,325.989 377.838,333.47 C377.838,340.955 383.902,347.018 391.385,347.018 C398.863,347.018 404.927,340.955 404.927,333.47 C404.927,325.989 398.863,319.922 391.385,319.922 z" fill="#4D4D4D"/>
|
||||
<path d="M109.479,140.76 C115.098,141.454 118.297,141.7 123.37,144.666 C137.422,153.315 141.95,170.163 134.102,184.73 C126.057,198.731 108.091,203.543 94.05,195.471 C80.059,187.423 75.249,169.449 83.318,155.402 C89.001,146.073 98.545,140.812 109.479,140.76 z M108.46,157.416 C100.978,157.416 94.918,163.479 94.918,170.964 C94.918,178.445 100.978,184.512 108.46,184.512 C115.938,184.512 122.003,178.445 122.003,170.964 C122.003,163.479 115.938,157.416 108.46,157.416 z" fill="#4D4D4D"/>
|
||||
<path d="M108.029,304.217 C109.902,304.351 111.797,304.307 113.648,304.618 C122.26,306.069 129.821,311.389 134.219,318.888 C142.269,332.902 137.473,350.793 123.444,358.946 C120.272,360.554 119.279,361.225 115.207,362.146 C102.806,364.949 89.767,359.133 83.42,348.195 C81.809,345.034 81.138,344.046 80.216,339.982 C77.396,327.551 83.269,314.512 94.177,308.112 C98.542,305.887 103.044,304.262 108.029,304.217 z M108.085,320.439 C100.61,320.689 94.75,326.953 94.999,334.43 C95.245,341.908 101.506,347.77 108.984,347.52 C116.459,347.275 122.319,341.011 122.069,333.53 C121.824,326.052 115.559,320.19 108.085,320.439 z" fill="#4D4D4D"/>
|
||||
<path d="M390.951,140.756 C392.824,140.89 394.718,140.846 396.57,141.158 C405.181,142.608 412.74,147.927 417.137,155.427 C425.19,169.441 420.396,187.332 406.366,195.485 C392.349,203.543 374.384,198.731 366.342,184.731 L361.014,187.794 L366.341,184.729 C358.275,170.695 363.069,152.805 377.099,144.652 C381.464,142.427 385.966,140.801 390.951,140.756 z M391.386,156.674 C383.907,156.674 377.843,162.741 377.843,170.222 C377.843,177.703 383.907,183.77 391.386,183.77 C398.868,183.77 404.932,177.703 404.932,170.222 C404.932,162.741 398.868,156.674 391.386,156.674 z" fill="#4D4D4D"/>
|
||||
<path d="M250.22,385.95 C266.425,385.951 279.515,399.049 279.515,415.261 C278.711,431.162 268.828,441.848 253.213,444.416 L250.22,444.568 C234.015,444.568 220.925,431.473 220.925,415.261 C220.925,399.049 234.016,385.951 250.22,385.95 z M249.942,402.056 C242.46,402.056 236.4,408.123 236.399,415.604 C236.4,423.085 242.46,429.152 249.942,429.152 C257.42,429.152 263.485,423.085 263.485,415.604 C263.485,408.123 257.42,402.056 249.942,402.056 z" fill="#4D4D4D"/>
|
||||
<path d="M250.22,58.247 C266.426,58.247 279.515,71.342 279.515,87.554 C278.714,103.482 268.873,114.157 253.219,116.713 L250.22,116.864 C234.016,116.865 220.925,103.767 220.925,87.554 C220.925,71.342 234.015,58.247 250.22,58.247 z M249.812,74.274 C242.33,74.274 236.269,80.341 236.269,87.826 C236.269,95.307 242.33,101.374 249.812,101.374 C257.29,101.374 263.354,95.307 263.354,87.826 C263.354,80.341 257.29,74.274 249.812,74.274 z" fill="#4D4D4D"/>
|
||||
<g>
|
||||
<path d="M685.358,281.898 Q685.358,297.357 681.297,310.064 Q677.236,322.771 669.114,331.941 Q660.992,341.111 649.202,346.22 Q637.412,351.329 621.954,351.329 Q609.116,351.329 597.85,348.185 Q586.584,345.04 577.938,338.491 Q569.292,331.941 564.052,321.854 Q558.812,311.767 558.288,297.881 Q558.288,295.523 559.598,293.82 Q560.908,292.117 563.79,292.117 L569.03,292.117 Q571.65,292.117 573.091,293.689 Q574.532,295.26 574.794,297.618 Q575.58,307.837 579.641,315.042 Q583.702,322.247 590.121,326.832 Q596.54,331.417 604.662,333.513 Q612.784,335.609 621.954,335.609 Q633.482,335.609 642.259,331.678 Q651.036,327.749 656.931,320.544 Q662.826,313.339 665.839,303.383 Q668.852,293.427 668.852,281.637 L668.852,181.029 L572.174,181.029 Q566.41,181.029 566.41,175.265 L566.41,171.073 Q566.41,165.309 572.174,165.309 L679.332,165.309 Q685.358,165.309 685.358,171.335 z" fill="#4D4D4D"/>
|
||||
<path d="M727.016,218.232 Q727.016,212.468 732.78,212.468 L737.234,212.468 Q742.998,212.468 742.998,218.232 L742.998,291.33 Q742.998,336.132 782.036,336.132 Q800.9,336.132 812.035,324.342 Q823.17,312.552 823.17,291.33 L823.17,218.232 Q823.17,212.468 828.934,212.468 L833.388,212.468 Q839.152,212.468 839.152,218.232 L839.152,342.944 Q839.152,348.708 833.388,348.708 L828.934,348.708 Q823.17,348.708 823.17,342.944 L823.17,330.63 Q816.096,339.8 806.14,345.564 Q796.184,351.328 779.416,351.328 Q766.054,351.328 756.229,346.874 Q746.404,342.421 739.854,334.56 Q733.304,326.701 730.16,315.958 Q727.016,305.216 727.016,292.641 z" fill="#4D4D4D"/>
|
||||
<path d="M995.042,342.944 Q995.042,348.708 989.278,348.708 L984.824,348.708 Q979.06,348.708 979.06,342.944 L979.06,269.846 Q979.06,248.886 968.842,236.965 Q958.624,225.044 938.974,225.044 Q920.11,225.044 908.844,236.834 Q897.578,248.624 897.578,269.846 L897.578,342.944 Q897.578,348.708 891.814,348.708 L887.36,348.708 Q881.596,348.708 881.596,342.944 L881.596,218.232 Q881.596,212.468 887.36,212.468 L891.814,212.468 Q897.578,212.468 897.578,218.232 L897.578,230.546 Q904.652,221.376 914.608,215.612 Q924.564,209.848 941.594,209.848 Q954.956,209.848 964.912,214.302 Q974.868,218.756 981.549,226.616 Q988.23,234.476 991.636,245.218 Q995.042,255.96 995.042,268.536 z" fill="#4D4D4D"/>
|
||||
<path d="M1055.04,178.671 Q1055.04,181.29 1053.337,182.863 Q1051.634,184.434 1049.014,184.434 L1039.058,184.434 Q1033.294,184.434 1033.294,178.671 L1033.294,168.715 Q1033.294,166.094 1034.866,164.391 Q1036.438,162.689 1039.058,162.689 L1049.014,162.689 Q1051.634,162.689 1053.337,164.391 Q1055.04,166.094 1055.04,168.715 z M1052.158,342.944 Q1052.158,348.708 1046.394,348.708 L1041.94,348.708 Q1036.176,348.708 1036.176,342.944 L1036.176,218.232 Q1036.176,212.469 1041.94,212.469 L1046.394,212.469 Q1052.158,212.469 1052.158,218.232 z" fill="#4D4D4D"/>
|
||||
<path d="M1154.862,351.328 Q1138.356,351.328 1127.876,345.302 Q1117.396,339.276 1110.846,330.63 L1110.846,392.724 Q1110.846,398.488 1105.082,398.488 L1100.628,398.488 Q1094.864,398.488 1094.864,392.724 L1094.864,218.232 Q1094.864,212.469 1100.628,212.469 L1105.082,212.469 Q1110.846,212.469 1110.846,218.232 L1110.846,230.546 Q1117.396,221.639 1127.876,215.743 Q1138.356,209.848 1154.862,209.848 Q1170.058,209.848 1180.669,215.219 Q1191.28,220.59 1197.83,229.368 Q1204.38,238.145 1207.393,249.279 Q1210.406,260.414 1210.93,271.943 Q1211.192,275.872 1211.192,280.589 Q1211.192,285.305 1210.93,289.234 Q1210.406,300.763 1207.393,311.897 Q1204.38,323.033 1197.83,331.81 Q1191.28,340.586 1180.669,345.957 Q1170.058,351.328 1154.862,351.328 z M1110.846,271.156 Q1110.584,275.086 1110.584,281.375 Q1110.584,287.663 1110.846,291.592 Q1111.108,299.453 1113.597,307.444 Q1116.086,315.435 1121.326,321.723 Q1126.566,328.01 1134.426,332.072 Q1142.286,336.133 1153.29,336.133 Q1164.818,336.133 1172.547,332.203 Q1180.276,328.272 1185.123,321.591 Q1189.97,314.91 1192.197,306.134 Q1194.424,297.357 1194.948,287.924 Q1195.21,280.589 1194.948,273.253 Q1194.424,263.82 1192.197,255.174 Q1189.97,246.529 1185.123,239.848 Q1180.276,233.167 1172.547,229.105 Q1164.818,225.044 1153.29,225.044 Q1142.024,225.044 1134.164,229.236 Q1126.304,233.428 1121.195,240.109 Q1116.086,246.79 1113.597,254.913 Q1111.108,263.035 1110.846,271.156 z" fill="#4D4D4D"/>
|
||||
<path d="M1240.012,270.108 Q1241.06,256.746 1245.383,245.611 Q1249.706,234.476 1256.911,226.616 Q1264.116,218.756 1274.334,214.302 Q1284.552,209.848 1297.39,209.848 Q1324.9,209.848 1340.096,227.795 Q1355.292,245.742 1355.292,277.444 L1355.292,281.374 Q1355.292,283.994 1353.589,285.566 Q1351.886,287.138 1349.266,287.138 L1255.994,287.138 L1255.994,289.758 Q1256.256,298.666 1259.269,307.05 Q1262.282,315.434 1267.522,321.853 Q1272.762,328.272 1280.36,332.202 Q1287.958,336.132 1297.39,336.132 Q1305.774,336.132 1312.062,334.036 Q1318.35,331.94 1322.804,329.189 Q1327.258,326.438 1330.009,323.556 Q1332.76,320.674 1333.808,319.102 Q1336.166,315.697 1337.476,315.041 Q1338.786,314.386 1341.668,314.386 L1345.86,314.386 Q1348.218,314.386 1349.921,315.827 Q1351.624,317.268 1351.362,319.626 Q1351.1,323.294 1347.301,328.796 Q1343.502,334.298 1336.559,339.276 Q1329.616,344.254 1319.66,347.791 Q1309.704,351.328 1297.39,351.328 Q1284.814,351.328 1274.596,347.005 Q1264.378,342.682 1257.042,334.691 Q1249.706,326.701 1245.383,315.565 Q1241.06,304.43 1240.012,291.068 Q1239.75,287.138 1239.75,280.589 Q1239.75,274.038 1240.012,270.108 z M1255.994,272.466 L1339.31,272.466 L1339.31,271.418 Q1339.31,261.724 1336.428,253.209 Q1333.546,244.694 1328.175,238.406 Q1322.804,232.118 1314.944,228.581 Q1307.084,225.044 1297.39,225.044 Q1287.434,225.044 1279.836,228.581 Q1272.238,232.118 1266.867,238.406 Q1261.496,244.694 1258.745,253.209 Q1255.994,261.724 1255.994,271.418 z" fill="#4D4D4D"/>
|
||||
<path d="M1405.858,230.546 Q1415.29,212.468 1441.49,212.468 L1451.184,212.468 Q1456.948,212.468 1456.948,218.232 L1456.948,221.9 Q1456.948,227.664 1451.184,227.664 L1439.918,227.664 Q1424.198,227.664 1415.028,236.834 Q1405.858,246.004 1405.858,261.724 L1405.858,342.944 Q1405.858,345.564 1404.155,347.136 Q1402.452,348.708 1399.832,348.708 L1395.64,348.708 Q1389.876,348.708 1389.876,342.944 L1389.876,218.494 Q1389.876,215.874 1391.448,214.171 Q1393.02,212.468 1395.64,212.468 L1399.832,212.468 Q1402.452,212.468 1404.155,214.171 Q1405.858,215.874 1405.858,218.494 z" fill="#4D4D4D"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 17 KiB |
BIN
assets/logo/juniper-mono.png
Normal file
After Width: | Height: | Size: 12 KiB |
22
assets/logo/juniper-mono.svg
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="500" height="500" viewBox="0, 0, 500, 500">
|
||||
<g id="Layer_1">
|
||||
<path d="M256.636,427.355 L256.194,428.121 L255.675,427.821 C256.485,427.479 256.18,427.662 256.636,427.355 z" fill="#4D4D4D"/>
|
||||
<path d="M236.427,86.007 C236.247,86.893 236.347,86.293 236.269,87.826 C236.269,95.307 242.33,101.374 249.812,101.374 C257.29,101.374 263.354,95.307 263.354,87.826 C263.28,86.345 263.377,86.893 263.227,86.16 L386.485,157.354 L387.191,157.354 C381.489,159.387 378.145,164.245 377.843,170.222 C377.918,171.711 377.821,171.154 377.975,171.912 L272.961,111.256 L382.678,301.363 L382.678,180.581 C385.013,182.797 388.277,183.613 391.386,183.77 C394.739,183.741 396.829,182.819 399.536,180.991 L399.536,322.704 C397.18,320.801 394.351,320.072 391.384,319.922 L391.384,319.922 C383.902,319.922 377.838,325.989 377.838,333.47 C378.018,339.038 380.326,342.277 384.799,345.297 L384.36,345.297 L263.808,414.928 L263.479,415.498 C263.395,409.869 260.005,405.216 254.902,403.06 L361.419,341.538 L140.009,341.538 L245.87,402.684 C240.111,404.72 236.704,409.557 236.399,415.604 L236.456,416.72 L114.311,346.168 C119.379,343.968 121.978,338.858 122.069,333.53 C121.824,326.052 115.559,320.19 108.085,320.439 L108.085,320.439 C104.887,320.645 101.522,321.74 99.275,324.142 L99.959,322.956 L99.959,181.495 C102.428,183.517 105.326,184.353 108.46,184.512 C112.009,184.466 113.968,183.516 116.818,181.591 L116.818,301.423 L226.644,111.132 L121.971,171.591 L122.003,170.964 C122.003,163.479 115.938,157.416 108.46,157.416 L108.46,157.416 C104.808,157.464 102.884,158.456 99.959,160.431 L99.959,157.354 L112.905,157.354 L236.427,86.007 z M249.766,104.796 L122.863,324.673 L376.666,324.673 L249.766,104.796 z" fill="#4D4D4D"/>
|
||||
<path d="M95.421,167.451 C94.898,170.017 95.024,168.855 94.92,170.92 L93.544,168.534 L95.421,167.451 z" fill="#4D4D4D"/>
|
||||
<path d="M98.779,324.673 C96.1,327.263 95.062,330.795 95,334.43 C95.245,341.908 101.506,347.77 108.984,347.52 L109.714,347.459 L105.007,355.619 L90.406,347.188 L93.668,341.538 L87.063,341.538 L87.063,324.673 L98.779,324.673 z" fill="#4D4D4D"/>
|
||||
<path d="M401.654,324.673 L412.33,324.673 L412.33,341.538 L405.865,341.538 L409.127,347.188 L394.526,355.623 L389.457,346.838 C390.452,347.041 389.815,346.939 391.384,347.018 C398.863,347.018 404.927,340.955 404.927,333.47 C404.811,330.367 403.925,326.945 401.654,324.673 z" fill="#4D4D4D"/>
|
||||
<path d="M404.661,167.852 L405.955,168.599 L404.924,170.387 L404.932,170.222 C404.845,168.502 404.954,169.29 404.661,167.852 z" fill="#4D4D4D"/>
|
||||
<path d="M395.582,157.354 L399.536,157.354 L399.536,159.453 C397.165,157.851 398.47,158.574 395.582,157.354 z" fill="#4D4D4D"/>
|
||||
<path d="M248.427,74.344 C243.163,75.052 239.246,78.183 237.202,83.027 L231.895,73.827 L246.492,65.397 L249.766,71.066 L253.041,65.393 L267.638,73.824 L262.381,82.933 C260.36,77.556 255.397,74.556 249.812,74.274 L248.427,74.344 z" fill="#4D4D4D"/>
|
||||
<path d="M217.062,75.033 L216.837,75.645 C216.962,75.242 216.888,75.447 217.062,75.033 z" fill="#4D4D4D"/>
|
||||
<path d="M249.455,9.704 L250.655,9.705 C254.228,11.502 252.973,10.367 254.836,12.58 L267.32,32.761 L273.195,33.332 L289.365,15.979 C292.644,12.474 299.264,13.775 300.942,18.291 L309.225,40.462 L314.938,42.194 L334.148,28.366 C338.05,25.556 344.284,28.147 345.056,32.886 L348.862,56.264 L354.11,59.078 L375.656,49.252 C380.039,47.259 385.657,51.011 385.471,55.814 L384.639,79.536 L389.195,83.284 L412.288,77.838 C416.98,76.738 421.74,81.503 420.64,86.197 L415.187,109.302 L418.934,113.849 L442.647,113.024 C447.479,112.839 451.214,118.456 449.203,122.847 L439.384,144.405 L442.193,149.655 L465.558,153.456 C470.307,154.239 472.897,160.469 470.076,164.372 L456.254,183.578 L457.989,189.297 L480.148,197.587 C484.655,199.271 485.974,205.886 482.447,209.176 L465.112,225.345 L465.686,231.226 L485.857,243.715 C489.944,246.242 489.94,253.009 485.853,255.524 L465.683,268.012 L465.108,273.89 L482.443,290.07 C485.97,293.35 484.651,299.961 480.144,301.652 L457.985,309.938 L456.25,315.661 L470.072,334.871 C472.893,338.786 470.311,345.023 465.555,345.783 L442.189,349.584 L439.38,354.841 L449.199,376.392 C451.211,380.776 447.43,386.412 442.643,386.219 L418.93,385.383 L415.195,389.94 L420.632,413.054 C421.736,417.736 416.976,422.494 412.284,421.394 L389.191,415.951 L384.639,419.688 L385.467,443.417 C385.642,448.229 380.035,451.977 375.653,449.976 L354.106,440.149 L348.859,442.967 L345.048,466.33 C344.281,471.081 338.046,473.66 334.144,470.862 L314.942,457.022 L309.222,458.762 L300.934,480.926 C299.256,485.427 292.64,486.754 289.362,483.226 L273.191,465.88 L267.316,466.47 L254.832,486.648 C252.307,490.726 245.547,490.718 243.029,486.649 L230.541,466.47 L224.662,465.88 L208.492,483.225 C205.215,486.754 198.595,485.427 196.916,480.926 L188.628,458.762 L182.908,457.022 L163.713,470.857 C159.812,473.66 153.574,471.088 152.802,466.334 L148.991,442.967 L143.743,440.149 L122.197,449.972 C117.82,451.977 112.201,448.221 112.383,443.418 L113.21,419.688 L108.655,415.951 L85.562,421.393 C80.871,422.494 76.107,417.736 77.207,413.054 L82.647,389.94 L78.9,385.383 L55.195,386.215 C50.417,386.412 46.636,380.776 48.64,376.393 L58.462,354.841 L55.645,349.584 L32.288,345.783 C27.54,345.023 24.958,338.786 27.767,334.871 L41.585,315.661 L39.853,309.938 L17.691,301.652 C13.192,299.961 11.876,293.35 15.392,290.071 L32.727,273.89 L32.152,268.012 L11.982,255.523 C7.899,253.009 7.899,246.242 11.986,243.716 L32.152,231.226 L32.727,225.345 L15.388,209.172 C11.876,205.886 13.192,199.271 17.691,197.588 L39.853,189.297 L41.585,183.578 L27.766,164.372 C24.965,160.469 27.536,154.228 32.285,153.456 L55.645,149.655 L58.462,144.405 L48.636,122.846 C46.648,118.456 50.417,112.888 55.2,113.024 L78.912,113.849 L82.647,109.302 L77.207,86.193 C76.107,81.503 80.871,76.745 85.562,77.838 L108.655,83.284 L113.21,79.536 L112.382,55.814 C112.201,51.011 117.82,47.251 122.198,49.252 L143.743,59.078 L148.991,56.264 L152.802,32.882 C153.574,28.147 159.812,25.549 163.714,28.367 L182.919,42.194 L188.632,40.462 L196.919,18.29 C198.602,13.775 205.218,12.474 208.496,15.984 L224.666,33.332 L230.549,32.761 L243.033,12.579 C244.754,10.079 246.649,9.849 249.455,9.704 L249.455,9.704 z M106.791,128.577 L103.787,128.754 C90.816,129.985 79.189,138.21 72.662,149.275 C63.481,165.488 65.934,187.734 80.197,200.361 C75.355,215.79 73.619,231.888 73.078,247.978 C73.522,266.185 75.582,284.757 81.707,302.023 C66.377,315.329 63.174,336.608 72.764,354.321 L72.765,354.323 C82.961,371.832 105.773,380.382 124.701,371.949 C147.497,395.467 177.646,410.322 208.848,418.985 C210.62,440.59 229.008,456.329 250.22,456.864 C272.564,456.568 289.438,440.191 291.667,418.236 C323.016,410.578 351.204,393.602 374.199,371.187 C394.578,380.145 416.103,372.827 427.661,354.346 L427.662,354.346 L427.669,354.333 L427.676,354.322 L427.676,354.321 C437.52,336.347 433.593,312.772 416.775,300.464 C422.339,283.529 424.596,265.748 424.821,247.978 C424.855,235.463 423.319,223.019 420.783,210.777 C420.163,207.785 419.233,204.866 418.459,201.911 C434.019,188.514 437.531,167.282 427.778,149.275 C418.995,135.808 409.184,130.496 393.509,128.553 L390.621,128.468 C388.612,128.666 386.59,128.766 384.593,129.063 C382.732,129.388 380.835,129.705 379.07,130.408 C355.621,103.96 324.367,86.778 290.611,77.372 C286.68,58.525 268.775,46.092 250.22,45.624 L250.22,45.623 C230.501,45.839 215.695,58.28 209.973,76.722 C174.553,83.963 143.111,103.253 118.926,129.834 C115.991,128.84 112.867,128.651 109.799,128.472 L106.791,128.577 z" fill="#4D4D4D"/>
|
||||
<path d="M392.399,304.221 C398.017,304.914 401.215,305.16 406.288,308.126 C420.393,316.274 425.181,334.142 417.035,348.17 L422.349,351.258 L417.021,348.194 C408.978,362.192 391.013,367.004 376.972,358.931 C362.977,350.883 358.168,332.91 366.239,318.864 C371.921,309.534 381.465,304.273 392.399,304.221 z M391.384,319.922 C383.902,319.922 377.838,325.989 377.838,333.47 C377.838,340.955 383.902,347.018 391.384,347.018 C398.863,347.018 404.927,340.955 404.927,333.47 C404.927,325.989 398.863,319.922 391.384,319.922 z" fill="#4D4D4D"/>
|
||||
<path d="M109.479,140.76 C115.098,141.454 118.297,141.7 123.37,144.666 C137.422,153.315 141.95,170.163 134.102,184.73 C126.057,198.731 108.091,203.543 94.05,195.471 C80.059,187.422 75.249,169.449 83.318,155.402 C89.001,146.073 98.545,140.812 109.479,140.76 z M108.46,157.416 C100.978,157.416 94.918,163.479 94.918,170.964 C94.918,178.445 100.978,184.512 108.46,184.512 C115.938,184.512 122.003,178.445 122.003,170.964 C122.003,163.479 115.938,157.416 108.46,157.416 z" fill="#4D4D4D"/>
|
||||
<path d="M108.029,304.217 C109.902,304.351 111.797,304.307 113.648,304.618 C122.26,306.069 129.821,311.389 134.219,318.888 C142.269,332.902 137.473,350.793 123.444,358.946 C120.272,360.554 119.279,361.225 115.207,362.145 C102.806,364.949 89.767,359.133 83.42,348.195 C81.809,345.034 81.138,344.046 80.216,339.982 C77.396,327.551 83.269,314.512 94.177,308.112 C98.542,305.887 103.044,304.262 108.029,304.217 z M108.085,320.439 C100.61,320.689 94.75,326.953 95,334.43 C95.245,341.908 101.506,347.77 108.984,347.52 C116.459,347.275 122.319,341.011 122.069,333.53 C121.824,326.052 115.559,320.19 108.085,320.439 z" fill="#4D4D4D"/>
|
||||
<path d="M390.951,140.756 C392.824,140.89 394.718,140.846 396.57,141.158 C405.181,142.608 412.74,147.927 417.137,155.427 C425.19,169.441 420.396,187.332 406.366,195.485 C392.349,203.543 374.384,198.731 366.342,184.731 L361.014,187.794 L366.341,184.729 C358.275,170.695 363.069,152.805 377.099,144.652 C381.464,142.427 385.966,140.801 390.951,140.756 z M391.386,156.674 C383.907,156.674 377.843,162.741 377.843,170.222 C377.843,177.703 383.907,183.77 391.386,183.77 C398.868,183.77 404.932,177.703 404.932,170.222 C404.932,162.741 398.868,156.674 391.386,156.674 z" fill="#4D4D4D"/>
|
||||
<path d="M250.22,385.95 C266.425,385.951 279.515,399.049 279.515,415.261 C278.711,431.162 268.828,441.848 253.213,444.416 L250.22,444.568 C234.015,444.568 220.925,431.473 220.925,415.261 C220.925,399.049 234.016,385.951 250.22,385.95 z M249.942,402.056 C242.46,402.056 236.399,408.123 236.399,415.604 C236.399,423.085 242.46,429.152 249.942,429.152 C257.42,429.152 263.485,423.085 263.484,415.604 C263.485,408.123 257.42,402.056 249.942,402.056 z" fill="#4D4D4D"/>
|
||||
<path d="M250.22,58.247 C266.426,58.247 279.515,71.342 279.515,87.554 C278.714,103.482 268.873,114.157 253.219,116.713 L250.22,116.864 C234.016,116.865 220.925,103.767 220.925,87.554 C220.925,71.342 234.015,58.247 250.22,58.247 z M249.812,74.274 C242.33,74.274 236.269,80.341 236.269,87.826 C236.269,95.307 242.33,101.374 249.812,101.374 C257.29,101.374 263.354,95.307 263.354,87.826 C263.354,80.341 257.29,74.274 249.812,74.274 z" fill="#4D4D4D"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 11 KiB |
|
@ -1,78 +0,0 @@
|
|||
/*
|
||||
Based off of the Ayu theme
|
||||
Original by Dempfi (https://github.com/dempfi/ayu)
|
||||
*/
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
background: #191f26;
|
||||
color: #e6e1cf;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #5c6773;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-attribute,
|
||||
.hljs-attr,
|
||||
.hljs-regexp,
|
||||
.hljs-link,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class {
|
||||
color: #ff7733;
|
||||
}
|
||||
|
||||
.hljs-number,
|
||||
.hljs-meta,
|
||||
.hljs-builtin-name,
|
||||
.hljs-literal,
|
||||
.hljs-type,
|
||||
.hljs-params {
|
||||
color: #ffee99;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-bullet {
|
||||
color: #b8cc52;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-built_in,
|
||||
.hljs-section {
|
||||
color: #ffb454;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag,
|
||||
.hljs-symbol {
|
||||
color: #ff7733;
|
||||
}
|
||||
|
||||
.hljs-name {
|
||||
color: #36a3d9;
|
||||
}
|
||||
|
||||
.hljs-tag {
|
||||
color: #00568d;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
color: #91b362;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
color: #d96c75;
|
||||
}
|
19
benches/Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "juniper_benchmarks"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
authors = ["Christoph Herzog <chris@theduke.at>"]
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
dataloader = "0.18" # for Book only
|
||||
futures = "0.3"
|
||||
juniper = { path = "../juniper" }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.5"
|
||||
tokio = { version = "1.0", features = ["rt-multi-thread"] }
|
||||
|
||||
[[bench]]
|
||||
name = "benchmark"
|
||||
harness = false
|
80
benches/benches/benchmark.rs
Normal file
|
@ -0,0 +1,80 @@
|
|||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
|
||||
use juniper::InputValue;
|
||||
use juniper_benchmarks as j;
|
||||
|
||||
fn bench_sync_vs_async_users_flat_instant(c: &mut Criterion) {
|
||||
// language=GraphQL
|
||||
const ASYNC_QUERY: &str = r#"
|
||||
query Query($id: Int) {
|
||||
users_async_instant(ids: [$id]!) {
|
||||
id
|
||||
kind
|
||||
username
|
||||
email
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
// language=GraphQL
|
||||
const SYNC_QUERY: &str = r#"
|
||||
query Query($id: Int) {
|
||||
users_sync_instant(ids: [$id]!) {
|
||||
id
|
||||
kind
|
||||
username
|
||||
email
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
let mut group = c.benchmark_group("Sync vs Async - Users Flat - Instant");
|
||||
for count in [1, 10] {
|
||||
group.bench_function(BenchmarkId::new("Sync", count), |b| {
|
||||
let ids = (0..count).map(InputValue::scalar).collect::<Vec<_>>();
|
||||
let ids = InputValue::list(ids);
|
||||
b.iter(|| {
|
||||
j::execute_sync(
|
||||
SYNC_QUERY,
|
||||
vec![("ids".to_owned(), ids.clone())].into_iter().collect(),
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
group.bench_function(BenchmarkId::new("Async - Single Thread", count), |b| {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let ids = (0..count).map(InputValue::scalar).collect::<Vec<_>>();
|
||||
let ids = InputValue::list(ids);
|
||||
|
||||
b.iter(|| {
|
||||
let f = j::execute(
|
||||
ASYNC_QUERY,
|
||||
vec![("ids".to_owned(), ids.clone())].into_iter().collect(),
|
||||
);
|
||||
rt.block_on(f)
|
||||
})
|
||||
});
|
||||
|
||||
group.bench_function(BenchmarkId::new("Async - Threadpool", count), |b| {
|
||||
let rt = tokio::runtime::Builder::new_multi_thread().build().unwrap();
|
||||
|
||||
let ids = (0..count).map(InputValue::scalar).collect::<Vec<_>>();
|
||||
let ids = InputValue::list(ids);
|
||||
|
||||
b.iter(|| {
|
||||
let f = j::execute(
|
||||
ASYNC_QUERY,
|
||||
vec![("ids".to_owned(), ids.clone())].into_iter().collect(),
|
||||
);
|
||||
rt.block_on(f)
|
||||
})
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_sync_vs_async_users_flat_instant);
|
||||
criterion_main!(benches);
|
109
benches/src/lib.rs
Normal file
|
@ -0,0 +1,109 @@
|
|||
use juniper::{
|
||||
graphql_object, DefaultScalarValue, EmptyMutation, EmptySubscription, ExecutionError,
|
||||
FieldError, GraphQLEnum, GraphQLObject, RootNode, Value, Variables,
|
||||
};
|
||||
|
||||
pub type QueryResult = Result<
|
||||
(
|
||||
Value<DefaultScalarValue>,
|
||||
Vec<ExecutionError<DefaultScalarValue>>,
|
||||
),
|
||||
String,
|
||||
>;
|
||||
|
||||
pub struct Context;
|
||||
|
||||
impl Context {
|
||||
fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl juniper::Context for Context {}
|
||||
|
||||
#[derive(GraphQLEnum)]
|
||||
pub enum Gender {
|
||||
Male,
|
||||
Female,
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(GraphQLEnum)]
|
||||
pub enum UserKind {
|
||||
SuperAdmin,
|
||||
Admin,
|
||||
Moderator,
|
||||
User,
|
||||
Guest,
|
||||
}
|
||||
|
||||
#[derive(GraphQLObject)]
|
||||
pub struct User {
|
||||
pub id: i32,
|
||||
pub kind: UserKind,
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub gender: Option<Gender>,
|
||||
}
|
||||
|
||||
impl User {
|
||||
fn new(id: i32) -> Self {
|
||||
Self {
|
||||
id,
|
||||
kind: UserKind::Admin,
|
||||
username: "userx".into(),
|
||||
email: "userx@domain.com".into(),
|
||||
gender: Some(Gender::Female),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Query;
|
||||
|
||||
#[graphql_object(context = Context)]
|
||||
impl Query {
|
||||
fn user_sync_instant(id: i32) -> Result<User, FieldError> {
|
||||
Ok(User::new(id))
|
||||
}
|
||||
|
||||
fn users_sync_instant(ids: Option<Vec<i32>>) -> Result<Vec<User>, FieldError> {
|
||||
if let Some(ids) = ids {
|
||||
let users = ids.into_iter().map(User::new).collect();
|
||||
Ok(users)
|
||||
} else {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
async fn user_async_instant(id: i32) -> Result<User, FieldError> {
|
||||
Ok(User::new(id))
|
||||
}
|
||||
|
||||
async fn users_async_instant(ids: Option<Vec<i32>>) -> Result<Vec<User>, FieldError> {
|
||||
if let Some(ids) = ids {
|
||||
let users = ids.into_iter().map(User::new).collect();
|
||||
Ok(users)
|
||||
} else {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_schema() -> RootNode<'static, Query, EmptyMutation<Context>, EmptySubscription<Context>>
|
||||
{
|
||||
RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new())
|
||||
}
|
||||
|
||||
pub fn execute_sync(query: &str, vars: Variables) -> QueryResult {
|
||||
let root = new_schema();
|
||||
let ctx = Context::new();
|
||||
juniper::execute_sync(query, None, &root, &vars, &ctx).map_err(|e| format!("{e:?}"))
|
||||
}
|
||||
|
||||
pub async fn execute(query: &str, vars: Variables) -> QueryResult {
|
||||
let root = new_schema();
|
||||
let ctx = Context::new();
|
||||
juniper::execute(query, None, &root, &vars, &ctx)
|
||||
.await
|
||||
.map_err(|e| format!("{e:?}"))
|
||||
}
|
697
book.js
|
@ -1,697 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
// Fix back button cache problem
|
||||
window.onunload = function () { };
|
||||
|
||||
// Global variable, shared between modules
|
||||
function playground_text(playground, hidden = true) {
|
||||
let code_block = playground.querySelector("code");
|
||||
|
||||
if (window.ace && code_block.classList.contains("editable")) {
|
||||
let editor = window.ace.edit(code_block);
|
||||
return editor.getValue();
|
||||
} else if (hidden) {
|
||||
return code_block.textContent;
|
||||
} else {
|
||||
return code_block.innerText;
|
||||
}
|
||||
}
|
||||
|
||||
(function codeSnippets() {
|
||||
function fetch_with_timeout(url, options, timeout = 6000) {
|
||||
return Promise.race([
|
||||
fetch(url, options),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout))
|
||||
]);
|
||||
}
|
||||
|
||||
var playgrounds = Array.from(document.querySelectorAll(".playground"));
|
||||
if (playgrounds.length > 0) {
|
||||
fetch_with_timeout("https://play.rust-lang.org/meta/crates", {
|
||||
headers: {
|
||||
'Content-Type': "application/json",
|
||||
},
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
// get list of crates available in the rust playground
|
||||
let playground_crates = response.crates.map(item => item["id"]);
|
||||
playgrounds.forEach(block => handle_crate_list_update(block, playground_crates));
|
||||
});
|
||||
}
|
||||
|
||||
function handle_crate_list_update(playground_block, playground_crates) {
|
||||
// update the play buttons after receiving the response
|
||||
update_play_button(playground_block, playground_crates);
|
||||
|
||||
// and install on change listener to dynamically update ACE editors
|
||||
if (window.ace) {
|
||||
let code_block = playground_block.querySelector("code");
|
||||
if (code_block.classList.contains("editable")) {
|
||||
let editor = window.ace.edit(code_block);
|
||||
editor.addEventListener("change", function (e) {
|
||||
update_play_button(playground_block, playground_crates);
|
||||
});
|
||||
// add Ctrl-Enter command to execute rust code
|
||||
editor.commands.addCommand({
|
||||
name: "run",
|
||||
bindKey: {
|
||||
win: "Ctrl-Enter",
|
||||
mac: "Ctrl-Enter"
|
||||
},
|
||||
exec: _editor => run_rust_code(playground_block)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updates the visibility of play button based on `no_run` class and
|
||||
// used crates vs ones available on https://play.rust-lang.org
|
||||
function update_play_button(pre_block, playground_crates) {
|
||||
var play_button = pre_block.querySelector(".play-button");
|
||||
|
||||
// skip if code is `no_run`
|
||||
if (pre_block.querySelector('code').classList.contains("no_run")) {
|
||||
play_button.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
// get list of `extern crate`'s from snippet
|
||||
var txt = playground_text(pre_block);
|
||||
var re = /extern\s+crate\s+([a-zA-Z_0-9]+)\s*;/g;
|
||||
var snippet_crates = [];
|
||||
var item;
|
||||
while (item = re.exec(txt)) {
|
||||
snippet_crates.push(item[1]);
|
||||
}
|
||||
|
||||
// check if all used crates are available on play.rust-lang.org
|
||||
var all_available = snippet_crates.every(function (elem) {
|
||||
return playground_crates.indexOf(elem) > -1;
|
||||
});
|
||||
|
||||
if (all_available) {
|
||||
play_button.classList.remove("hidden");
|
||||
} else {
|
||||
play_button.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function run_rust_code(code_block) {
|
||||
var result_block = code_block.querySelector(".result");
|
||||
if (!result_block) {
|
||||
result_block = document.createElement('code');
|
||||
result_block.className = 'result hljs language-bash';
|
||||
|
||||
code_block.append(result_block);
|
||||
}
|
||||
|
||||
let text = playground_text(code_block);
|
||||
let classes = code_block.querySelector('code').classList;
|
||||
let edition = "2015";
|
||||
if(classes.contains("edition2018")) {
|
||||
edition = "2018";
|
||||
} else if(classes.contains("edition2021")) {
|
||||
edition = "2021";
|
||||
}
|
||||
var params = {
|
||||
version: "stable",
|
||||
optimize: "0",
|
||||
code: text,
|
||||
edition: edition
|
||||
};
|
||||
|
||||
if (text.indexOf("#![feature") !== -1) {
|
||||
params.version = "nightly";
|
||||
}
|
||||
|
||||
result_block.innerText = "Running...";
|
||||
|
||||
fetch_with_timeout("https://play.rust-lang.org/evaluate.json", {
|
||||
headers: {
|
||||
'Content-Type': "application/json",
|
||||
},
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
body: JSON.stringify(params)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
if (response.result.trim() === '') {
|
||||
result_block.innerText = "No output";
|
||||
result_block.classList.add("result-no-output");
|
||||
} else {
|
||||
result_block.innerText = response.result;
|
||||
result_block.classList.remove("result-no-output");
|
||||
}
|
||||
})
|
||||
.catch(error => result_block.innerText = "Playground Communication: " + error.message);
|
||||
}
|
||||
|
||||
// Syntax highlighting Configuration
|
||||
hljs.configure({
|
||||
tabReplace: ' ', // 4 spaces
|
||||
languages: [], // Languages used for auto-detection
|
||||
});
|
||||
|
||||
let code_nodes = Array
|
||||
.from(document.querySelectorAll('code'))
|
||||
// Don't highlight `inline code` blocks in headers.
|
||||
.filter(function (node) {return !node.parentElement.classList.contains("header"); });
|
||||
|
||||
if (window.ace) {
|
||||
// language-rust class needs to be removed for editable
|
||||
// blocks or highlightjs will capture events
|
||||
code_nodes
|
||||
.filter(function (node) {return node.classList.contains("editable"); })
|
||||
.forEach(function (block) { block.classList.remove('language-rust'); });
|
||||
|
||||
code_nodes
|
||||
.filter(function (node) {return !node.classList.contains("editable"); })
|
||||
.forEach(function (block) { hljs.highlightBlock(block); });
|
||||
} else {
|
||||
code_nodes.forEach(function (block) { hljs.highlightBlock(block); });
|
||||
}
|
||||
|
||||
// Adding the hljs class gives code blocks the color css
|
||||
// even if highlighting doesn't apply
|
||||
code_nodes.forEach(function (block) { block.classList.add('hljs'); });
|
||||
|
||||
Array.from(document.querySelectorAll("code.hljs")).forEach(function (block) {
|
||||
|
||||
var lines = Array.from(block.querySelectorAll('.boring'));
|
||||
// If no lines were hidden, return
|
||||
if (!lines.length) { return; }
|
||||
block.classList.add("hide-boring");
|
||||
|
||||
var buttons = document.createElement('div');
|
||||
buttons.className = 'buttons';
|
||||
buttons.innerHTML = "<button class=\"fa fa-eye\" title=\"Show hidden lines\" aria-label=\"Show hidden lines\"></button>";
|
||||
|
||||
// add expand button
|
||||
var pre_block = block.parentNode;
|
||||
pre_block.insertBefore(buttons, pre_block.firstChild);
|
||||
|
||||
pre_block.querySelector('.buttons').addEventListener('click', function (e) {
|
||||
if (e.target.classList.contains('fa-eye')) {
|
||||
e.target.classList.remove('fa-eye');
|
||||
e.target.classList.add('fa-eye-slash');
|
||||
e.target.title = 'Hide lines';
|
||||
e.target.setAttribute('aria-label', e.target.title);
|
||||
|
||||
block.classList.remove('hide-boring');
|
||||
} else if (e.target.classList.contains('fa-eye-slash')) {
|
||||
e.target.classList.remove('fa-eye-slash');
|
||||
e.target.classList.add('fa-eye');
|
||||
e.target.title = 'Show hidden lines';
|
||||
e.target.setAttribute('aria-label', e.target.title);
|
||||
|
||||
block.classList.add('hide-boring');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (window.playground_copyable) {
|
||||
Array.from(document.querySelectorAll('pre code')).forEach(function (block) {
|
||||
var pre_block = block.parentNode;
|
||||
if (!pre_block.classList.contains('playground')) {
|
||||
var buttons = pre_block.querySelector(".buttons");
|
||||
if (!buttons) {
|
||||
buttons = document.createElement('div');
|
||||
buttons.className = 'buttons';
|
||||
pre_block.insertBefore(buttons, pre_block.firstChild);
|
||||
}
|
||||
|
||||
var clipButton = document.createElement('button');
|
||||
clipButton.className = 'fa fa-copy clip-button';
|
||||
clipButton.title = 'Copy to clipboard';
|
||||
clipButton.setAttribute('aria-label', clipButton.title);
|
||||
clipButton.innerHTML = '<i class=\"tooltiptext\"></i>';
|
||||
|
||||
buttons.insertBefore(clipButton, buttons.firstChild);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Process playground code blocks
|
||||
Array.from(document.querySelectorAll(".playground")).forEach(function (pre_block) {
|
||||
// Add play button
|
||||
var buttons = pre_block.querySelector(".buttons");
|
||||
if (!buttons) {
|
||||
buttons = document.createElement('div');
|
||||
buttons.className = 'buttons';
|
||||
pre_block.insertBefore(buttons, pre_block.firstChild);
|
||||
}
|
||||
|
||||
var runCodeButton = document.createElement('button');
|
||||
runCodeButton.className = 'fa fa-play play-button';
|
||||
runCodeButton.hidden = true;
|
||||
runCodeButton.title = 'Run this code';
|
||||
runCodeButton.setAttribute('aria-label', runCodeButton.title);
|
||||
|
||||
buttons.insertBefore(runCodeButton, buttons.firstChild);
|
||||
runCodeButton.addEventListener('click', function (e) {
|
||||
run_rust_code(pre_block);
|
||||
});
|
||||
|
||||
if (window.playground_copyable) {
|
||||
var copyCodeClipboardButton = document.createElement('button');
|
||||
copyCodeClipboardButton.className = 'fa fa-copy clip-button';
|
||||
copyCodeClipboardButton.innerHTML = '<i class="tooltiptext"></i>';
|
||||
copyCodeClipboardButton.title = 'Copy to clipboard';
|
||||
copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title);
|
||||
|
||||
buttons.insertBefore(copyCodeClipboardButton, buttons.firstChild);
|
||||
}
|
||||
|
||||
let code_block = pre_block.querySelector("code");
|
||||
if (window.ace && code_block.classList.contains("editable")) {
|
||||
var undoChangesButton = document.createElement('button');
|
||||
undoChangesButton.className = 'fa fa-history reset-button';
|
||||
undoChangesButton.title = 'Undo changes';
|
||||
undoChangesButton.setAttribute('aria-label', undoChangesButton.title);
|
||||
|
||||
buttons.insertBefore(undoChangesButton, buttons.firstChild);
|
||||
|
||||
undoChangesButton.addEventListener('click', function () {
|
||||
let editor = window.ace.edit(code_block);
|
||||
editor.setValue(editor.originalCode);
|
||||
editor.clearSelection();
|
||||
});
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
(function themes() {
|
||||
var html = document.querySelector('html');
|
||||
var themeToggleButton = document.getElementById('theme-toggle');
|
||||
var themePopup = document.getElementById('theme-list');
|
||||
var themeColorMetaTag = document.querySelector('meta[name="theme-color"]');
|
||||
var stylesheets = {
|
||||
ayuHighlight: document.querySelector("[href$='ayu-highlight.css']"),
|
||||
tomorrowNight: document.querySelector("[href$='tomorrow-night.css']"),
|
||||
highlight: document.querySelector("[href$='highlight.css']"),
|
||||
};
|
||||
|
||||
function showThemes() {
|
||||
themePopup.style.display = 'block';
|
||||
themeToggleButton.setAttribute('aria-expanded', true);
|
||||
themePopup.querySelector("button#" + get_theme()).focus();
|
||||
}
|
||||
|
||||
function updateThemeSelected() {
|
||||
themePopup.querySelectorAll('.theme-selected').forEach(function (el) {
|
||||
el.classList.remove('theme-selected');
|
||||
});
|
||||
themePopup.querySelector("button#" + get_theme()).classList.add('theme-selected');
|
||||
}
|
||||
|
||||
function hideThemes() {
|
||||
themePopup.style.display = 'none';
|
||||
themeToggleButton.setAttribute('aria-expanded', false);
|
||||
themeToggleButton.focus();
|
||||
}
|
||||
|
||||
function get_theme() {
|
||||
var theme;
|
||||
try { theme = localStorage.getItem('mdbook-theme'); } catch (e) { }
|
||||
if (theme === null || theme === undefined) {
|
||||
return default_theme;
|
||||
} else {
|
||||
return theme;
|
||||
}
|
||||
}
|
||||
|
||||
function set_theme(theme, store = true) {
|
||||
let ace_theme;
|
||||
|
||||
if (theme == 'coal' || theme == 'navy') {
|
||||
stylesheets.ayuHighlight.disabled = true;
|
||||
stylesheets.tomorrowNight.disabled = false;
|
||||
stylesheets.highlight.disabled = true;
|
||||
|
||||
ace_theme = "ace/theme/tomorrow_night";
|
||||
} else if (theme == 'ayu') {
|
||||
stylesheets.ayuHighlight.disabled = false;
|
||||
stylesheets.tomorrowNight.disabled = true;
|
||||
stylesheets.highlight.disabled = true;
|
||||
ace_theme = "ace/theme/tomorrow_night";
|
||||
} else {
|
||||
stylesheets.ayuHighlight.disabled = true;
|
||||
stylesheets.tomorrowNight.disabled = true;
|
||||
stylesheets.highlight.disabled = false;
|
||||
ace_theme = "ace/theme/dawn";
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
themeColorMetaTag.content = getComputedStyle(document.documentElement).backgroundColor;
|
||||
}, 1);
|
||||
|
||||
if (window.ace && window.editors) {
|
||||
window.editors.forEach(function (editor) {
|
||||
editor.setTheme(ace_theme);
|
||||
});
|
||||
}
|
||||
|
||||
var previousTheme = get_theme();
|
||||
|
||||
if (store) {
|
||||
try { localStorage.setItem('mdbook-theme', theme); } catch (e) { }
|
||||
}
|
||||
|
||||
html.classList.remove(previousTheme);
|
||||
html.classList.add(theme);
|
||||
updateThemeSelected();
|
||||
}
|
||||
|
||||
// Set theme
|
||||
var theme = get_theme();
|
||||
|
||||
set_theme(theme, false);
|
||||
|
||||
themeToggleButton.addEventListener('click', function () {
|
||||
if (themePopup.style.display === 'block') {
|
||||
hideThemes();
|
||||
} else {
|
||||
showThemes();
|
||||
}
|
||||
});
|
||||
|
||||
themePopup.addEventListener('click', function (e) {
|
||||
var theme;
|
||||
if (e.target.className === "theme") {
|
||||
theme = e.target.id;
|
||||
} else if (e.target.parentElement.className === "theme") {
|
||||
theme = e.target.parentElement.id;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
set_theme(theme);
|
||||
});
|
||||
|
||||
themePopup.addEventListener('focusout', function(e) {
|
||||
// e.relatedTarget is null in Safari and Firefox on macOS (see workaround below)
|
||||
if (!!e.relatedTarget && !themeToggleButton.contains(e.relatedTarget) && !themePopup.contains(e.relatedTarget)) {
|
||||
hideThemes();
|
||||
}
|
||||
});
|
||||
|
||||
// Should not be needed, but it works around an issue on macOS & iOS: https://github.com/rust-lang/mdBook/issues/628
|
||||
document.addEventListener('click', function(e) {
|
||||
if (themePopup.style.display === 'block' && !themeToggleButton.contains(e.target) && !themePopup.contains(e.target)) {
|
||||
hideThemes();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
|
||||
if (!themePopup.contains(e.target)) { return; }
|
||||
|
||||
switch (e.key) {
|
||||
case 'Escape':
|
||||
e.preventDefault();
|
||||
hideThemes();
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
var li = document.activeElement.parentElement;
|
||||
if (li && li.previousElementSibling) {
|
||||
li.previousElementSibling.querySelector('button').focus();
|
||||
}
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
var li = document.activeElement.parentElement;
|
||||
if (li && li.nextElementSibling) {
|
||||
li.nextElementSibling.querySelector('button').focus();
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
e.preventDefault();
|
||||
themePopup.querySelector('li:first-child button').focus();
|
||||
break;
|
||||
case 'End':
|
||||
e.preventDefault();
|
||||
themePopup.querySelector('li:last-child button').focus();
|
||||
break;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
(function sidebar() {
|
||||
var body = document.querySelector("body");
|
||||
var sidebar = document.getElementById("sidebar");
|
||||
var sidebarLinks = document.querySelectorAll('#sidebar a');
|
||||
var sidebarToggleButton = document.getElementById("sidebar-toggle");
|
||||
var sidebarResizeHandle = document.getElementById("sidebar-resize-handle");
|
||||
var firstContact = null;
|
||||
|
||||
function showSidebar() {
|
||||
body.classList.remove('sidebar-hidden')
|
||||
body.classList.add('sidebar-visible');
|
||||
Array.from(sidebarLinks).forEach(function (link) {
|
||||
link.setAttribute('tabIndex', 0);
|
||||
});
|
||||
sidebarToggleButton.setAttribute('aria-expanded', true);
|
||||
sidebar.setAttribute('aria-hidden', false);
|
||||
try { localStorage.setItem('mdbook-sidebar', 'visible'); } catch (e) { }
|
||||
}
|
||||
|
||||
|
||||
var sidebarAnchorToggles = document.querySelectorAll('#sidebar a.toggle');
|
||||
|
||||
function toggleSection(ev) {
|
||||
ev.currentTarget.parentElement.classList.toggle('expanded');
|
||||
}
|
||||
|
||||
Array.from(sidebarAnchorToggles).forEach(function (el) {
|
||||
el.addEventListener('click', toggleSection);
|
||||
});
|
||||
|
||||
function hideSidebar() {
|
||||
body.classList.remove('sidebar-visible')
|
||||
body.classList.add('sidebar-hidden');
|
||||
Array.from(sidebarLinks).forEach(function (link) {
|
||||
link.setAttribute('tabIndex', -1);
|
||||
});
|
||||
sidebarToggleButton.setAttribute('aria-expanded', false);
|
||||
sidebar.setAttribute('aria-hidden', true);
|
||||
try { localStorage.setItem('mdbook-sidebar', 'hidden'); } catch (e) { }
|
||||
}
|
||||
|
||||
// Toggle sidebar
|
||||
sidebarToggleButton.addEventListener('click', function sidebarToggle() {
|
||||
if (body.classList.contains("sidebar-hidden")) {
|
||||
var current_width = parseInt(
|
||||
document.documentElement.style.getPropertyValue('--sidebar-width'), 10);
|
||||
if (current_width < 150) {
|
||||
document.documentElement.style.setProperty('--sidebar-width', '150px');
|
||||
}
|
||||
showSidebar();
|
||||
} else if (body.classList.contains("sidebar-visible")) {
|
||||
hideSidebar();
|
||||
} else {
|
||||
if (getComputedStyle(sidebar)['transform'] === 'none') {
|
||||
hideSidebar();
|
||||
} else {
|
||||
showSidebar();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
sidebarResizeHandle.addEventListener('mousedown', initResize, false);
|
||||
|
||||
function initResize(e) {
|
||||
window.addEventListener('mousemove', resize, false);
|
||||
window.addEventListener('mouseup', stopResize, false);
|
||||
body.classList.add('sidebar-resizing');
|
||||
}
|
||||
function resize(e) {
|
||||
var pos = (e.clientX - sidebar.offsetLeft);
|
||||
if (pos < 20) {
|
||||
hideSidebar();
|
||||
} else {
|
||||
if (body.classList.contains("sidebar-hidden")) {
|
||||
showSidebar();
|
||||
}
|
||||
pos = Math.min(pos, window.innerWidth - 100);
|
||||
document.documentElement.style.setProperty('--sidebar-width', pos + 'px');
|
||||
}
|
||||
}
|
||||
//on mouseup remove windows functions mousemove & mouseup
|
||||
function stopResize(e) {
|
||||
body.classList.remove('sidebar-resizing');
|
||||
window.removeEventListener('mousemove', resize, false);
|
||||
window.removeEventListener('mouseup', stopResize, false);
|
||||
}
|
||||
|
||||
document.addEventListener('touchstart', function (e) {
|
||||
firstContact = {
|
||||
x: e.touches[0].clientX,
|
||||
time: Date.now()
|
||||
};
|
||||
}, { passive: true });
|
||||
|
||||
document.addEventListener('touchmove', function (e) {
|
||||
if (!firstContact)
|
||||
return;
|
||||
|
||||
var curX = e.touches[0].clientX;
|
||||
var xDiff = curX - firstContact.x,
|
||||
tDiff = Date.now() - firstContact.time;
|
||||
|
||||
if (tDiff < 250 && Math.abs(xDiff) >= 150) {
|
||||
if (xDiff >= 0 && firstContact.x < Math.min(document.body.clientWidth * 0.25, 300))
|
||||
showSidebar();
|
||||
else if (xDiff < 0 && curX < 300)
|
||||
hideSidebar();
|
||||
|
||||
firstContact = null;
|
||||
}
|
||||
}, { passive: true });
|
||||
})();
|
||||
|
||||
(function chapterNavigation() {
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
|
||||
if (window.search && window.search.hasFocus()) { return; }
|
||||
var html = document.querySelector('html');
|
||||
|
||||
function next() {
|
||||
var nextButton = document.querySelector('.nav-chapters.next');
|
||||
if (nextButton) {
|
||||
window.location.href = nextButton.href;
|
||||
}
|
||||
}
|
||||
function prev() {
|
||||
var previousButton = document.querySelector('.nav-chapters.previous');
|
||||
if (previousButton) {
|
||||
window.location.href = previousButton.href;
|
||||
}
|
||||
}
|
||||
switch (e.key) {
|
||||
case 'ArrowRight':
|
||||
e.preventDefault();
|
||||
if (html.dir == 'rtl') {
|
||||
prev();
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault();
|
||||
if (html.dir == 'rtl') {
|
||||
next();
|
||||
} else {
|
||||
prev();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
(function clipboard() {
|
||||
var clipButtons = document.querySelectorAll('.clip-button');
|
||||
|
||||
function hideTooltip(elem) {
|
||||
elem.firstChild.innerText = "";
|
||||
elem.className = 'fa fa-copy clip-button';
|
||||
}
|
||||
|
||||
function showTooltip(elem, msg) {
|
||||
elem.firstChild.innerText = msg;
|
||||
elem.className = 'fa fa-copy tooltipped';
|
||||
}
|
||||
|
||||
var clipboardSnippets = new ClipboardJS('.clip-button', {
|
||||
text: function (trigger) {
|
||||
hideTooltip(trigger);
|
||||
let playground = trigger.closest("pre");
|
||||
return playground_text(playground, false);
|
||||
}
|
||||
});
|
||||
|
||||
Array.from(clipButtons).forEach(function (clipButton) {
|
||||
clipButton.addEventListener('mouseout', function (e) {
|
||||
hideTooltip(e.currentTarget);
|
||||
});
|
||||
});
|
||||
|
||||
clipboardSnippets.on('success', function (e) {
|
||||
e.clearSelection();
|
||||
showTooltip(e.trigger, "Copied!");
|
||||
});
|
||||
|
||||
clipboardSnippets.on('error', function (e) {
|
||||
showTooltip(e.trigger, "Clipboard error!");
|
||||
});
|
||||
})();
|
||||
|
||||
(function scrollToTop () {
|
||||
var menuTitle = document.querySelector('.menu-title');
|
||||
|
||||
menuTitle.addEventListener('click', function () {
|
||||
document.scrollingElement.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
})();
|
||||
|
||||
(function controllMenu() {
|
||||
var menu = document.getElementById('menu-bar');
|
||||
|
||||
(function controllPosition() {
|
||||
var scrollTop = document.scrollingElement.scrollTop;
|
||||
var prevScrollTop = scrollTop;
|
||||
var minMenuY = -menu.clientHeight - 50;
|
||||
// When the script loads, the page can be at any scroll (e.g. if you reforesh it).
|
||||
menu.style.top = scrollTop + 'px';
|
||||
// Same as parseInt(menu.style.top.slice(0, -2), but faster
|
||||
var topCache = menu.style.top.slice(0, -2);
|
||||
menu.classList.remove('sticky');
|
||||
var stickyCache = false; // Same as menu.classList.contains('sticky'), but faster
|
||||
document.addEventListener('scroll', function () {
|
||||
scrollTop = Math.max(document.scrollingElement.scrollTop, 0);
|
||||
// `null` means that it doesn't need to be updated
|
||||
var nextSticky = null;
|
||||
var nextTop = null;
|
||||
var scrollDown = scrollTop > prevScrollTop;
|
||||
var menuPosAbsoluteY = topCache - scrollTop;
|
||||
if (scrollDown) {
|
||||
nextSticky = false;
|
||||
if (menuPosAbsoluteY > 0) {
|
||||
nextTop = prevScrollTop;
|
||||
}
|
||||
} else {
|
||||
if (menuPosAbsoluteY > 0) {
|
||||
nextSticky = true;
|
||||
} else if (menuPosAbsoluteY < minMenuY) {
|
||||
nextTop = prevScrollTop + minMenuY;
|
||||
}
|
||||
}
|
||||
if (nextSticky === true && stickyCache === false) {
|
||||
menu.classList.add('sticky');
|
||||
stickyCache = true;
|
||||
} else if (nextSticky === false && stickyCache === true) {
|
||||
menu.classList.remove('sticky');
|
||||
stickyCache = false;
|
||||
}
|
||||
if (nextTop !== null) {
|
||||
menu.style.top = nextTop + 'px';
|
||||
topCache = nextTop;
|
||||
}
|
||||
prevScrollTop = scrollTop;
|
||||
}, { passive: true });
|
||||
})();
|
||||
(function controllBorder() {
|
||||
function updateBorder() {
|
||||
if (menu.offsetTop === 0) {
|
||||
menu.classList.remove('bordered');
|
||||
} else {
|
||||
menu.classList.add('bordered');
|
||||
}
|
||||
}
|
||||
updateBorder();
|
||||
document.addEventListener('scroll', updateBorder, { passive: true });
|
||||
})();
|
||||
})();
|
2
book/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/_rendered/
|
||||
/gh-pages/
|
55
book/README.md
Normal file
|
@ -0,0 +1,55 @@
|
|||
Juniper Book
|
||||
============
|
||||
|
||||
Book containing the [`juniper`](https://docs.rs/juniper) user guide.
|
||||
|
||||
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
### Requirements
|
||||
|
||||
The Book is built with [mdBook](https://github.com/rust-lang/mdBook).
|
||||
|
||||
You may install it with:
|
||||
```bash
|
||||
cargo install mdbook
|
||||
```
|
||||
|
||||
|
||||
### Local test server
|
||||
|
||||
To launch a local test server that continually re-builds the Book and auto-reloads the page, run:
|
||||
```bash
|
||||
mdbook serve
|
||||
|
||||
# or from project root dir:
|
||||
make book.serve
|
||||
```
|
||||
|
||||
|
||||
### Building
|
||||
|
||||
You may build the Book to rendered HTML with this command:
|
||||
```bash
|
||||
mdbook build
|
||||
|
||||
# or from project root dir:
|
||||
make book
|
||||
```
|
||||
|
||||
The output will be in the `_rendered/` directory.
|
||||
|
||||
|
||||
### Testing
|
||||
|
||||
To run the tests validating all code examples in the book, run (from project root dir):
|
||||
```bash
|
||||
cargo build
|
||||
mdbook test -L target/debug/deps
|
||||
|
||||
# or via shortcut:
|
||||
make test.book
|
||||
```
|
19
book/book.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[book]
|
||||
title = "Juniper Book"
|
||||
description = "User guide for Juniper (GraphQL server library for Rust)."
|
||||
language = "en"
|
||||
multilingual = false
|
||||
authors = [
|
||||
"Kai Ren (@tyranron)",
|
||||
]
|
||||
src = "src"
|
||||
|
||||
[build]
|
||||
build-dir = "_rendered"
|
||||
create-missing = false
|
||||
|
||||
[output.html]
|
||||
git_repository_url = "https://github.com/graphql-rust/juniper"
|
||||
|
||||
[rust]
|
||||
edition = "2021"
|
28
book/src/SUMMARY.md
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Summary
|
||||
|
||||
- [Introduction](introduction.md)
|
||||
- [Quickstart](quickstart.md)
|
||||
- [Type system](types/index.md)
|
||||
- [Objects](types/objects/index.md)
|
||||
- [Complex fields](types/objects/complex_fields.md)
|
||||
- [Context](types/objects/context.md)
|
||||
- [Error handling](types/objects/error/index.md)
|
||||
- [Field errors](types/objects/error/field.md)
|
||||
- [Schema errors](types/objects/error/schema.md)
|
||||
- [Generics](types/objects/generics.md)
|
||||
- [Interfaces](types/interfaces.md)
|
||||
- [Unions](types/unions.md)
|
||||
- [Enums](types/enums.md)
|
||||
- [Input objects](types/input_objects.md)
|
||||
- [Scalars](types/scalars.md)
|
||||
- [Schema](schema/index.md)
|
||||
- [Subscriptions](schema/subscriptions.md)
|
||||
- [Introspection](schema/introspection.md)
|
||||
- [Serving](serve/index.md)
|
||||
- [Batching](serve/batching.md)
|
||||
- [Advanced Topics](advanced/index.md)
|
||||
- [Implicit and explicit `null`](advanced/implicit_and_explicit_null.md)
|
||||
- [N+1 problem](advanced/n_plus_1.md)
|
||||
- [DataLoader](advanced/dataloader.md)
|
||||
- [Look-ahead](advanced/lookahead.md)
|
||||
- [Eager loading](advanced/eager_loading.md)
|
198
book/src/advanced/dataloader.md
Normal file
|
@ -0,0 +1,198 @@
|
|||
DataLoader
|
||||
==========
|
||||
|
||||
DataLoader pattern, named after the correspondent [`dataloader` NPM package][0], represents a mechanism of batching and caching data requests in a delayed manner for solving the [N+1 problem](n_plus_1.md).
|
||||
|
||||
> A port of the "Loader" API originally developed by [@schrockn] 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.
|
||||
|
||||
In [Rust] ecosystem, DataLoader pattern is introduced with the [`dataloader` crate][1], naturally usable with [Juniper].
|
||||
|
||||
Let's remake our [example of N+1 problem](n_plus_1.md), so it's solved by applying the DataLoader pattern:
|
||||
```rust
|
||||
# extern crate anyhow;
|
||||
# extern crate dataloader;
|
||||
# extern crate juniper;
|
||||
# use std::{collections::HashMap, sync::Arc};
|
||||
# use anyhow::anyhow;
|
||||
# use dataloader::non_cached::Loader;
|
||||
# use juniper::{graphql_object, GraphQLObject};
|
||||
#
|
||||
# type CultId = i32;
|
||||
# type UserId = i32;
|
||||
#
|
||||
# struct Repository;
|
||||
#
|
||||
# impl Repository {
|
||||
# async fn load_cults_by_ids(&self, cult_ids: &[CultId]) -> anyhow::Result<HashMap<CultId, Cult>> { unimplemented!() }
|
||||
# async fn load_all_persons(&self) -> anyhow::Result<Vec<Person>> { unimplemented!() }
|
||||
# }
|
||||
#
|
||||
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() {
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
And now, performing a [GraphQL query which lead to N+1 problem](n_plus_1.md)
|
||||
```graphql
|
||||
query {
|
||||
persons {
|
||||
id
|
||||
name
|
||||
cult {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
will lead to efficient [SQL] queries, just as expected:
|
||||
```sql
|
||||
SELECT id, name, cult_id FROM persons;
|
||||
SELECT id, name FROM cults WHERE id IN (1, 2, 3, 4);
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Caching
|
||||
|
||||
[`dataloader::cached`] provides a [memoization][2] cache: after `BatchFn::load()` is called once with given keys, the resulting values are cached to eliminate redundant loads.
|
||||
|
||||
DataLoader caching does not replace [Redis], [Memcached], or any other shared application-level cache. DataLoader is first and foremost a data loading mechanism, and its cache only serves the purpose of not repeatedly loading the same data [in the context of a single request][3].
|
||||
|
||||
> **WARNING**: 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.
|
||||
|
||||
|
||||
|
||||
|
||||
## Full example
|
||||
|
||||
For a full example using DataLoaders in [Juniper] check out the [`jayy-lmao/rust-graphql-docker` repository][4].
|
||||
|
||||
|
||||
|
||||
|
||||
[`dataloader::cached`]: https://docs.rs/dataloader/latest/dataloader/cached/index.html
|
||||
[@schrockn]: https://github.com/schrockn
|
||||
[Juniper]: https://docs.rs/juniper
|
||||
[Memcached]: https://memcached.org
|
||||
[Redis]: https://redis.io
|
||||
[Rust]: https://www.rust-lang.org
|
||||
[SQL]: https://en.wikipedia.org/wiki/SQL
|
||||
|
||||
[0]: https://github.com/graphql/dataloader
|
||||
[1]: https://docs.rs/crate/dataloader
|
||||
[2]: https://en.wikipedia.org/wiki/Memoization
|
||||
[3]: https://github.com/graphql/dataloader#caching
|
||||
[4]: https://github.com/jayy-lmao/rust-graphql-docker
|
280
book/src/advanced/eager_loading.md
Normal file
|
@ -0,0 +1,280 @@
|
|||
Eager loading
|
||||
=============
|
||||
|
||||
As a further evolution of the [dealing with the N+1 problem via look-ahead](lookahead.md#n1-problem), we may systematically remodel [Rust] types mapping to [GraphQL] ones in the way to encourage doing eager preloading of data for its [fields][0] and using the already preloaded data when resolving a particular [field][0].
|
||||
|
||||
At the moment, this approach is represented with the [`juniper-eager-loading`] crate for [Juniper].
|
||||
|
||||
> **NOTE**: Since this library requires [`juniper-from-schema`], it's best first to become familiar with it.
|
||||
|
||||
<!-- TODO: Provide example of solving the problem from "N+1 chapter" once `juniper-eager-loading` support the latest `juniper`. -->
|
||||
|
||||
From ["How this library works at a high level"][11] and ["A real example"][12] sections of [`juniper-eager-loading`] documentation:
|
||||
|
||||
> ### How this library works at a high level
|
||||
>
|
||||
> If you have a GraphQL type like this
|
||||
>
|
||||
> ```graphql
|
||||
> type User {
|
||||
> id: Int!
|
||||
> country: Country!
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> You might create the corresponding Rust model type like this:
|
||||
>
|
||||
> ```rust
|
||||
> struct User {
|
||||
> id: i32,
|
||||
> country_id: i32,
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> However this approach has one big issue. How are you going to resolve the field `User.country`
|
||||
> without doing a database query? All the resolver has access to is a `User` with a `country_id`
|
||||
> field. It can't get the country without loading it from the database...
|
||||
>
|
||||
> Fundamentally these kinds of model structs don't work for eager loading with GraphQL. So
|
||||
> this library takes a different approach.
|
||||
>
|
||||
> What if we created separate structs for the database models and the GraphQL models? Something
|
||||
> like this:
|
||||
>
|
||||
> ```rust
|
||||
> # fn main() {}
|
||||
> #
|
||||
> 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,
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> Now we're able to resolve the query with code like this:
|
||||
>
|
||||
> 1. Load all the users (first query).
|
||||
> 2. Map the users to a list of country ids.
|
||||
> 3. Load all the countries with those ids (second query).
|
||||
> 4. Pair up the users with the country with the correct id, so change `User.country` from
|
||||
> `HasOne::NotLoaded` to `HasOne::Loaded(matching_country)`.
|
||||
> 5. When resolving the GraphQL field `User.country` simply return the loaded country.
|
||||
>
|
||||
> ### A real example
|
||||
>
|
||||
> ```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> {
|
||||
> // ...
|
||||
> # unimplemented!()
|
||||
> }
|
||||
> }
|
||||
> }
|
||||
>
|
||||
> // 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> {
|
||||
> // ...
|
||||
> # unimplemented!()
|
||||
> }
|
||||
> }
|
||||
>
|
||||
> // 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)
|
||||
> }
|
||||
> }
|
||||
> #
|
||||
> # fn main() {}
|
||||
> ```
|
||||
|
||||
For more details, check out the [`juniper-eager-loading` documentation][`juniper-eager-loading`].
|
||||
|
||||
|
||||
|
||||
|
||||
## Full example
|
||||
|
||||
For a full example using eager loading in [Juniper] check out the [`davidpdrsn/graphql-app-example` repository][10].
|
||||
|
||||
|
||||
|
||||
|
||||
[`juniper-eager-loading`]: https://docs.rs/juniper-eager-loading
|
||||
[`juniper-from-schema`]: https://docs.rs/juniper-from-schema
|
||||
[GraphQL]: https://graphql.org
|
||||
[Juniper]: https://docs.rs/juniper
|
||||
[Redis]: https://redis.io
|
||||
[Rust]: https://www.rust-lang.org
|
||||
|
||||
[0]: https://spec.graphql.org/October2021#sec-Language.Fields
|
||||
[10]: https://github.com/davidpdrsn/graphql-app-example
|
||||
[11]: https://docs.rs/juniper-eager-loading/latest/juniper_eager_loading#how-this-library-works-at-a-high-level
|
||||
[12]: https://docs.rs/juniper-eager-loading/latest/juniper_eager_loading#a-real-example
|
117
book/src/advanced/implicit_and_explicit_null.md
Normal file
|
@ -0,0 +1,117 @@
|
|||
Implicit and explicit `null`
|
||||
============================
|
||||
|
||||
> [GraphQL] has two semantically different ways to represent the lack of a value:
|
||||
> - Explicitly providing the literal value: **null**.
|
||||
> - Implicitly not providing a value at all.
|
||||
|
||||
There are two ways that a client can submit a [`null` value][0] as an [argument][5] or a [field][4] in a [GraphQL] query:
|
||||
1. Either use an explicit `null` literal:
|
||||
```graphql
|
||||
{
|
||||
field(arg: null)
|
||||
}
|
||||
```
|
||||
2. Or simply omit the [argument][5], so the implicit default `null` value kicks in:
|
||||
```graphql
|
||||
{
|
||||
field
|
||||
}
|
||||
```
|
||||
|
||||
There are some situations where it's useful to know which one exactly has been provided.
|
||||
|
||||
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:
|
||||
```rust
|
||||
/// 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>>,
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
To set a user's favorite number to 7, we would set `favorite_number` to `Some(Some(7))`. In [GraphQL], that might look like this:
|
||||
```graphql
|
||||
mutation { patchUser(patch: { favoriteNumber: 7 }) }
|
||||
```
|
||||
|
||||
To unset the user's favorite number, we would set `favorite_number` to `Some(None)`. In [GraphQL], that might look like this:
|
||||
```graphql
|
||||
mutation { patchUser(patch: { favoriteNumber: null }) }
|
||||
```
|
||||
|
||||
And if we want to leave the user's favorite number alone, just set it to `None`. In [GraphQL], that might look like this:
|
||||
```graphql
|
||||
mutation { patchUser(patch: {}) }
|
||||
```
|
||||
|
||||
The last two cases rely on being able to distinguish between [explicit and implicit `null`][1].
|
||||
|
||||
Unfortunately, plain `Option` is not capable to distinguish them. That's why in [Juniper], this can be done using the [`Nullable`] type:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# struct UserPatch {
|
||||
# favorite_number: Option<Option<i32>>,
|
||||
# least_favorite_number: Option<Option<i32>>,
|
||||
# }
|
||||
#
|
||||
# struct Session;
|
||||
# impl Session {
|
||||
# fn patch_user(&self, _patch: UserPatch) -> FieldResult<()> { Ok(()) }
|
||||
# }
|
||||
#
|
||||
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)
|
||||
}
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
[`Nullable`]: https://docs.rs/juniper/0.16.1/juniper/enum.Nullable.html
|
||||
[GraphQL]: https://graphql.org
|
||||
[Juniper]: https://docs.rs/juniper
|
||||
[Rust]: https://www.rust-lang.org
|
||||
|
||||
[0]: https://spec.graphql.org/October2021#sec-Null-Value
|
||||
[1]: https://spec.graphql.org/October2021#sel-EAFdRDHAAEJDAoBxzT
|
||||
[4]: https://spec.graphql.org/October2021#sec-Language.Fields
|
||||
[5]: https://spec.graphql.org/October2021#sec-Language.Arguments
|
10
book/src/advanced/index.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
Advanced topics
|
||||
===============
|
||||
|
||||
The chapters below cover some more advanced topics.
|
||||
|
||||
- [Implicit and explicit `null`](implicit_and_explicit_null.md)
|
||||
- [N+1 problem](n_plus_1.md)
|
||||
- [DataLoader](dataloader.md)
|
||||
- [Look-ahead](lookahead.md)
|
||||
- [Eager loading](eager_loading.md)
|
229
book/src/advanced/lookahead.md
Normal file
|
@ -0,0 +1,229 @@
|
|||
Look-ahead
|
||||
==========
|
||||
|
||||
> In backtracking algorithms, **look ahead** 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.
|
||||
|
||||
In [GraphQL], look-ahead machinery allows us to introspect the currently [executed][1] [GraphQL operation][2] to see which [fields][3] has been actually selected by it.
|
||||
|
||||
In [Juniper], it's represented by the [`Executor::look_ahead()`][20] method.
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::{graphql_object, Executor, GraphQLObject, ScalarValue};
|
||||
#
|
||||
# type UserId = i32;
|
||||
#
|
||||
#[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);
|
||||
}
|
||||
// ...
|
||||
# unimplemented!()
|
||||
}
|
||||
}
|
||||
```
|
||||
> **TIP**: `S: ScalarValue` type parameter on the method is required here to keep the [`Executor`] being generic over [`ScalarValue`] types. We, instead, could have used the [`DefaultScalarValue`], which is the default [`ScalarValue`] type for the [`Executor`], and make our code more ergonomic, but less flexible and generic.
|
||||
> ```rust
|
||||
> # extern crate juniper;
|
||||
> # use juniper::{graphql_object, DefaultScalarValue, Executor, GraphQLObject};
|
||||
> #
|
||||
> # type UserId = i32;
|
||||
> #
|
||||
> # #[derive(GraphQLObject)]
|
||||
> # struct Person {
|
||||
> # id: UserId,
|
||||
> # name: String,
|
||||
> # }
|
||||
> #
|
||||
> # struct Query;
|
||||
> #
|
||||
> #[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);
|
||||
> }
|
||||
> // ...
|
||||
> # unimplemented!()
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
|
||||
|
||||
|
||||
|
||||
## N+1 problem
|
||||
|
||||
Naturally, look-ahead machinery allows us to solve [the N+1 problem](n_plus_1.md) by introspecting the requested fields and performing loading in batches eagerly, before actual resolving of those fields:
|
||||
```rust
|
||||
# extern crate anyhow;
|
||||
# extern crate juniper;
|
||||
# use std::collections::HashMap;
|
||||
# use anyhow::anyhow;
|
||||
# use juniper::{graphql_object, Executor, GraphQLObject, ScalarValue};
|
||||
#
|
||||
# type CultId = i32;
|
||||
# type UserId = i32;
|
||||
#
|
||||
# struct Repository;
|
||||
#
|
||||
# impl juniper::Context for Repository {}
|
||||
#
|
||||
# impl Repository {
|
||||
# async fn load_cult_by_id(&self, cult_id: CultId) -> anyhow::Result<Option<Cult>> { unimplemented!() }
|
||||
# async fn load_cults_by_ids(&self, cult_ids: &[CultId]) -> anyhow::Result<HashMap<CultId, Cult>> { unimplemented!() }
|
||||
# async fn load_all_persons(&self) -> anyhow::Result<Vec<Person>> { unimplemented!() }
|
||||
# }
|
||||
#
|
||||
# enum Either<L, R> {
|
||||
# Absent(L),
|
||||
# Loaded(R),
|
||||
# }
|
||||
#
|
||||
#[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)
|
||||
}
|
||||
}
|
||||
```
|
||||
And so, performing a [GraphQL query which lead to N+1 problem](n_plus_1.md)
|
||||
```graphql
|
||||
query {
|
||||
persons {
|
||||
id
|
||||
name
|
||||
cult {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
will lead to efficient [SQL] queries, just as expected:
|
||||
```sql
|
||||
SELECT id, name, cult_id FROM persons;
|
||||
SELECT id, name FROM cults WHERE id IN (1, 2, 3, 4);
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## More features
|
||||
|
||||
See more available look-ahead features in the API docs of the [`LookAheadSelection`][21] and the [`LookAheadChildren`][22].
|
||||
|
||||
|
||||
|
||||
|
||||
[`DefaultScalarValue`]: https://docs.rs/juniper/0.16.1/juniper/enum.DefaultScalarValue.html
|
||||
[`Executor`]: https://docs.rs/juniper/0.16.1/juniper/executor/struct.Executor.html
|
||||
[`ScalarValue`]: https://docs.rs/juniper/0.16.1/juniper/trait.ScalarValue.html
|
||||
[GraphQL]: https://graphql.org
|
||||
[Juniper]: https://docs.rs/juniper
|
||||
[Rust]: https://www.rust-lang.org
|
||||
[SQL]: https://en.wikipedia.org/wiki/SQL
|
||||
|
||||
[1]: https://spec.graphql.org/October2021#sec-Execution
|
||||
[2]: https://spec.graphql.org/October2021#sec-Language.Operations\
|
||||
[3]: https://spec.graphql.org/October2021#sec-Language.Fields
|
||||
[20]: https://docs.rs/juniper/0.16.1/juniper/executor/struct.Executor.html#method.look_ahead
|
||||
[21]: https://docs.rs/juniper/0.16.1/juniper/executor/struct.LookAheadSelection.html
|
||||
[22]: https://docs.rs/juniper/0.16.1/juniper/executor/struct.LookAheadChildren.html
|
111
book/src/advanced/n_plus_1.md
Normal file
|
@ -0,0 +1,111 @@
|
|||
N+1 problem
|
||||
===========
|
||||
|
||||
A common issue with [GraphQL] server implementations is how the [resolvers][2] 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 [HTTP] requests.
|
||||
|
||||
```rust
|
||||
# extern crate anyhow;
|
||||
# extern crate juniper;
|
||||
# use anyhow::anyhow;
|
||||
# use juniper::{graphql_object, GraphQLObject};
|
||||
#
|
||||
# type CultId = i32;
|
||||
# type UserId = i32;
|
||||
#
|
||||
# struct Repository;
|
||||
#
|
||||
# impl juniper::Context for Repository {}
|
||||
#
|
||||
# impl Repository {
|
||||
# async fn load_cult_by_id(&self, cult_id: CultId) -> anyhow::Result<Option<Cult>> { unimplemented!() }
|
||||
# async fn load_all_persons(&self) -> anyhow::Result<Vec<Person>> { unimplemented!() }
|
||||
# }
|
||||
#
|
||||
#[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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Let's say we want to list a bunch of `cult`s `persons` were in:
|
||||
```graphql
|
||||
query {
|
||||
persons {
|
||||
id
|
||||
name
|
||||
cult {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Once the `persons` [list][1] has been [resolved][2], a separate [SQL] query is run to find the `cult` of each `Person`. We can see how this could quickly become a problem.
|
||||
```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...
|
||||
```
|
||||
|
||||
There are several ways how this problem may be resolved in [Juniper]. The most common ones are:
|
||||
- [DataLoader](dataloader.md)
|
||||
- [Look-ahead machinery](lookahead.md)
|
||||
- [Eager loading](eager_loading.md)
|
||||
|
||||
|
||||
|
||||
|
||||
[GraphQL]: https://graphql.org
|
||||
[HTTP]: https://en.wikipedia.org/wiki/HTTP
|
||||
[Juniper]: https://docs.rs/juniper
|
||||
[Rust]: https://www.rust-lang.org
|
||||
[SQL]: https://en.wikipedia.org/wiki/SQL
|
||||
|
||||
[1]: https://spec.graphql.org/October2021#sec-List
|
||||
[2]: https://spec.graphql.org/October2021#sec-Executing-Fields
|
85
book/src/introduction.md
Normal file
|
@ -0,0 +1,85 @@
|
|||
Introduction
|
||||
============
|
||||
|
||||
> [GraphQL] is a query language for APIs and a runtime for fulfilling those queries with your existing data. [GraphQL] 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.
|
||||
|
||||
[Juniper] is a library for creating [GraphQL] servers in [Rust]. Build type-safe and fast API servers with minimal boilerplate and configuration (we do try to make declaring and resolving [GraphQL] schemas as convenient as [Rust] will allow).
|
||||
|
||||
[Juniper] 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 [Rust] ecosystem.
|
||||
|
||||
- [Cargo crate](https://crates.io/crates/juniper)
|
||||
- [API reference][`juniper`]
|
||||
|
||||
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
[Juniper] supports the full GraphQL query language according to the [specification (October 2021)][GraphQL spec].
|
||||
|
||||
> **NOTE**: As an exception to other [GraphQL] libraries for other languages, [Juniper] builds non-`null` types by default. A field of type `Vec<Episode>` will be converted into `[Episode!]!`. The corresponding Rust type for a `null`able `[Episode]` would be `Option<Vec<Option<Episode>>>` instead.
|
||||
|
||||
|
||||
|
||||
|
||||
## Integrations
|
||||
|
||||
|
||||
### Types
|
||||
|
||||
[Juniper] provides out-of-the-box integration for some very common [Rust] 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 [Cargo feature]:
|
||||
- [`bigdecimal`]
|
||||
- [`bson`]
|
||||
- [`chrono`], [`chrono-tz`]
|
||||
- [`jiff`]
|
||||
- [`rust_decimal`]
|
||||
- [`time`]
|
||||
- [`url`]
|
||||
- [`uuid`]
|
||||
|
||||
|
||||
|
||||
|
||||
### Web server frameworks
|
||||
|
||||
- [`actix-web`] ([`juniper_actix`] crate)
|
||||
- [`axum`] ([`juniper_axum`] crate)
|
||||
- [`hyper`] ([`juniper_hyper`] crate)
|
||||
- [`rocket`] ([`juniper_rocket`] crate)
|
||||
- [`warp`] ([`juniper_warp`] crate)
|
||||
|
||||
|
||||
|
||||
|
||||
## API stability
|
||||
|
||||
[Juniper] has not reached 1.0 yet, thus some API instability should be expected.
|
||||
|
||||
|
||||
|
||||
|
||||
[`actix-web`]: https://docs.rs/actix-web
|
||||
[`axum`]: https://docs.rs/axum
|
||||
[`bigdecimal`]: https://docs.rs/bigdecimal
|
||||
[`bson`]: https://docs.rs/bson
|
||||
[`chrono`]: https://docs.rs/chrono
|
||||
[`chrono-tz`]: https://docs.rs/chrono-tz
|
||||
[`jiff`]: https://docs.rs/jiff
|
||||
[`juniper`]: https://docs.rs/juniper
|
||||
[`juniper_actix`]: https://docs.rs/juniper_actix
|
||||
[`juniper_axum`]: https://docs.rs/juniper_axum
|
||||
[`juniper_hyper`]: https://docs.rs/juniper_hyper
|
||||
[`juniper_rocket`]: https://docs.rs/juniper_rocket
|
||||
[`juniper_warp`]: https://docs.rs/juniper_warp
|
||||
[`hyper`]: https://docs.rs/hyper
|
||||
[`rocket`]: https://docs.rs/rocket
|
||||
[`rust_decimal`]: https://docs.rs/rust_decimal
|
||||
[`time`]: https://docs.rs/time
|
||||
[`url`]: https://docs.rs/url
|
||||
[`uuid`]: https://docs.rs/uuid
|
||||
[`warp`]: https://docs.rs/warp
|
||||
[Cargo feature]: https://doc.rust-lang.org/cargo/reference/features.html
|
||||
[GraphQL]: https://graphql.org
|
||||
[GraphQL spec]: https://spec.graphql.org/October2021
|
||||
[Juniper]: https://docs.rs/juniper
|
||||
[Rust]: https://www.rust-lang.org
|
232
book/src/quickstart.md
Normal file
|
@ -0,0 +1,232 @@
|
|||
Quickstart
|
||||
==========
|
||||
|
||||
This page will give you a short introduction to the concepts in [Juniper].
|
||||
|
||||
**[Juniper] follows a [code-first] approach to define a [GraphQL] schema.**
|
||||
|
||||
> **TIP**: For a [schema-first] approach, consider using a [`juniper-from-schema`] crate for generating a [`juniper`]-based code from a [schema] file.
|
||||
|
||||
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
juniper = "0.16.1"
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Schema
|
||||
|
||||
Exposing simple enums and structs as [GraphQL] types is just a matter of adding a custom [derive attribute] to them. [Juniper] includes support for basic [Rust] types that naturally map to [GraphQL] features, such as `Option<T>`, `Vec<T>`, `Box<T>`, `Arc<T>`, `String`, `f64`, `i32`, references, slices and arrays.
|
||||
|
||||
For more advanced mappings, [Juniper] provides multiple macros to map your [Rust] types to a [GraphQL schema][schema]. The most important one is the [`#[graphql_object]` attribute][2] that is used for declaring a [GraphQL object] with resolvers (typically used for declaring [`Query` and `Mutation` roots][1]).
|
||||
|
||||
```rust
|
||||
# # ![allow(unused_variables)]
|
||||
# extern crate juniper;
|
||||
#
|
||||
# use std::fmt::Display;
|
||||
#
|
||||
use juniper::{
|
||||
graphql_object, EmptySubscription, FieldResult, GraphQLEnum,
|
||||
GraphQLInputObject, GraphQLObject, ScalarValue,
|
||||
};
|
||||
#
|
||||
# struct DatabasePool;
|
||||
# impl DatabasePool {
|
||||
# fn get_connection(&self) -> FieldResult<DatabasePool> { Ok(DatabasePool) }
|
||||
# fn find_human(&self, _id: &str) -> FieldResult<Human> { Err("")? }
|
||||
# fn insert_human(&self, _human: &NewHuman) -> FieldResult<Human> { Err("")? }
|
||||
# }
|
||||
|
||||
#[derive(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>>;
|
||||
#
|
||||
# fn main() {
|
||||
# _ = Schema::new(Query, Mutation, EmptySubscription::new());
|
||||
# }
|
||||
```
|
||||
|
||||
Now we have a very simple but functional schema for a [GraphQL] server!
|
||||
|
||||
To actually serve the [schema], see the guides for our various [server integrations](serve/index.md).
|
||||
|
||||
|
||||
|
||||
|
||||
## Execution
|
||||
|
||||
[Juniper] 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 `juniper::execute()` directly to get a result for a [GraphQL] query:
|
||||
|
||||
```rust
|
||||
# // Only needed due to 2018 edition because the macro is not accessible.
|
||||
# #[macro_use] extern crate juniper;
|
||||
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",
|
||||
}),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
[`juniper`]: https://docs.rs/juniper
|
||||
[`juniper-from-schema`]: https://docs.rs/juniper-from-schema
|
||||
[code-first]: https://www.apollographql.com/blog/backend/architecture/schema-first-vs-code-only-graphql#code-only
|
||||
[derive attribute]: https://doc.rust-lang.org/stable/reference/attributes/derive.html#derive
|
||||
[GraphQL]: https://graphql.org
|
||||
[GraphQL object]: https://spec.graphql.org/October2021#sec-Objects
|
||||
[Juniper]: https://docs.rs/juniper
|
||||
[Rust]: https://www.rust-lang.org
|
||||
[schema]: https://graphql.org/learn/schema
|
||||
[schema-first]: https://www.apollographql.com/blog/backend/architecture/schema-first-vs-code-only-graphql#schema-first
|
||||
|
||||
[1]: https://spec.graphql.org/October2021#sec-Root-Operation-Types
|
||||
[2]: https://docs.rs/juniper/0.16.1/juniper/macro.graphql_object.html
|
185
book/src/schema/index.md
Normal file
|
@ -0,0 +1,185 @@
|
|||
Schema
|
||||
======
|
||||
|
||||
**[Juniper] follows a [code-first] approach to define a [GraphQL] schema.**
|
||||
|
||||
> **TIP**: For a [schema-first] approach, consider using a [`juniper-from-schema`] crate for generating a [`juniper`]-based code from a [schema] file.
|
||||
|
||||
[GraphQL schema][0] consists of three [object types][4]: a [query root][1], a [mutation root][2], and a [subscription root][3].
|
||||
|
||||
> The **query** root operation type must be provided and must be an [Object][4] type.
|
||||
>
|
||||
> The **mutation** root operation type is optional; if it is not provided, the service does not support mutations. If it is provided, it must be an [Object][4] type.
|
||||
>
|
||||
> Similarly, the **subscription** 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 [Object][4] type.
|
||||
>
|
||||
> The **query**, **mutation**, and **subscription** root types must all be different types if provided.
|
||||
|
||||
In [Juniper], the [`RootNode`] type represents a [schema][0]. When the [schema][0] is first created, [Juniper] will traverse the entire object graph and register all types it can find. This means that if we [define a GraphQL object](../types/objects/index.md) somewhere but never use or reference it, it won't be exposed in a [GraphQL schema][0].
|
||||
|
||||
Both [query][1] and [mutation][2] objects are regular [GraphQL objects][4], defined like [any other object in Juniper](../types/objects/index.md). The [mutation][2] and [subscription][3] objects, however, are optional, since [schemas][0] can be read-only and do not require [subscriptions][3].
|
||||
|
||||
> **TIP**: If [mutation][2]/[subscription][3] functionality is not needed, consider using the predefined [`EmptyMutation`]/[`EmptySubscription`] types for stubbing them in a [`RootNode`].
|
||||
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::{
|
||||
# graphql_object, EmptySubscription, FieldResult, GraphQLObject, RootNode,
|
||||
# };
|
||||
#
|
||||
#[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...
|
||||
# unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
struct Mutation;
|
||||
|
||||
#[graphql_object]
|
||||
impl Mutation {
|
||||
fn sign_up_user(name: String, email: String) -> FieldResult<User> {
|
||||
// Validate inputs and save user in database...
|
||||
# unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
type Schema = RootNode<'static, Query, Mutation, EmptySubscription>;
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
> **NOTE**: It's considered a [good practice][5] to name [query][1], [mutation][2], and [subscription][3] root types as `Query`, `Mutation`, and `Subscription` respectively.
|
||||
|
||||
The usage of [subscriptions][3] is a little different from the [mutation][2] and [query][1] [objects][4], so they are discussed in the [separate chapter](subscriptions.md).
|
||||
|
||||
|
||||
|
||||
|
||||
## Export
|
||||
|
||||
Many tools in [GraphQL] ecosystem require a [schema] definition to operate on. With [Juniper] we can export our [GraphQL schema][0] defined in [Rust] code either represented in the [GraphQL schema language][6] or in [JSON].
|
||||
|
||||
|
||||
### SDL (schema definition language)
|
||||
|
||||
To generate an [SDL (schema definition language)][6] representation of a [GraphQL schema][0] defined in [Rust] code, the [`as_sdl()` method][20] should be used for the direct extraction (requires enabling the `schema-language` [Juniper] feature):
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::{
|
||||
# graphql_object, EmptyMutation, EmptySubscription, FieldResult, RootNode,
|
||||
# };
|
||||
#
|
||||
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!
|
||||
}
|
||||
";
|
||||
# #[cfg(not(target_os = "windows"))]
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### JSON
|
||||
|
||||
To export a [GraphQL schema][0] defined in [Rust] code as [JSON] (often referred to as `schema.json`), the specially crafted [introspection query][21] should be issued. [Juniper] provides a [convenience `introspect()` function][22] to [introspect](introspection.md) the entire [schema][0], which result can be serialized into [JSON]:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# extern crate serde_json;
|
||||
# use juniper::{
|
||||
# graphql_object, EmptyMutation, EmptySubscription, GraphQLObject,
|
||||
# IntrospectionFormat, RootNode,
|
||||
# };
|
||||
#
|
||||
#[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());
|
||||
}
|
||||
```
|
||||
|
||||
> **TIP**: We still can convert the generated [JSON] into a [GraphQL schema language][6] representation by using tools like [`graphql-json-to-sdl` command line utility][30].
|
||||
|
||||
|
||||
|
||||
|
||||
[`EmptyMutation`]: https://docs.rs/juniper/0.16.1/juniper/struct.EmptyMutation.html
|
||||
[`EmptySubscription`]: https://docs.rs/juniper/0.16.1/juniper/struct.EmptySubscription.html
|
||||
[`juniper`]: https://docs.rs/juniper
|
||||
[`juniper-from-schema`]: https://docs.rs/juniper-from-schema
|
||||
[`RootNode`]: https://docs.rs/juniper/0.16.1/juniper/struct.RootNode.html
|
||||
[code-first]: https://www.apollographql.com/blog/backend/architecture/schema-first-vs-code-only-graphql#code-only
|
||||
[schema-first]: https://www.apollographql.com/blog/backend/architecture/schema-first-vs-code-only-graphql#schema-first
|
||||
[GraphQL]: https://graphql.org
|
||||
[JSON]: https://www.json.org
|
||||
[Juniper]: https://docs.rs/juniper
|
||||
[Rust]: https://www.rust-lang.org
|
||||
[schema]: https://graphql.org/learn/schema
|
||||
|
||||
[0]: https://spec.graphql.org/October2021#sec-Schema
|
||||
[1]: https://spec.graphql.org/October2021#sel-FAHTRFCAACChCtpG
|
||||
[2]: https://spec.graphql.org/October2021#sel-FAHTRHCAACCuE9yD
|
||||
[3]: https://spec.graphql.org/October2021#sel-FAHTRJCAACC3EhsX
|
||||
[4]: https://spec.graphql.org/October2021#sec-Objects
|
||||
[5]: https://spec.graphql.org/October2021#sec-Root-Operation-Types.Default-Root-Operation-Type-Names
|
||||
[6]: https://graphql.org/learn/schema#type-language
|
||||
[20]: https://docs.rs/juniper/0.16.1/juniper/struct.RootNode.html#method.as_sdl
|
||||
[21]: https://docs.rs/crate/juniper/latest/source/src/introspection/query.graphql
|
||||
[22]: https://docs.rs/juniper/0.16.1/juniper/fn.introspect.html
|
||||
[30]: https://npmjs.com/package/graphql-json-to-sdl
|
83
book/src/schema/introspection.md
Normal file
|
@ -0,0 +1,83 @@
|
|||
Introspection
|
||||
=============
|
||||
|
||||
> The [schema introspection][1] system is accessible from the meta-fields `__schema` and `__type` which are accessible from the type of the root of a query operation.
|
||||
> ```graphql
|
||||
> __schema: __Schema!
|
||||
> __type(name: String!): __Type
|
||||
> ```
|
||||
> Like all meta-fields, these are implicit and do not appear in the fields list in the root type of the query operation.
|
||||
|
||||
[GraphQL] provides [introspection][0], allowing to see what [queries][2], [mutations][3] and [subscriptions][4] a [GraphQL] server supports at runtime.
|
||||
|
||||
Because [introspection][0] queries are just regular [GraphQL queries][2], [Juniper] supports them natively. For example, to get all the names of the types supported, we could [execute][5] the following [query][2] against [Juniper]:
|
||||
```graphql
|
||||
{
|
||||
__schema {
|
||||
types {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Disabling
|
||||
|
||||
> 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.
|
||||
|
||||
[Some security requirements and considerations][10] may mandate to disable [GraphQL schema introspection][1] in production environments. In [Juniper] this can be achieved by using the [`RootNode::disable_introspection()`][9] method:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::{
|
||||
# graphql_object, graphql_vars, EmptyMutation, EmptySubscription, GraphQLError,
|
||||
# RootNode,
|
||||
# };
|
||||
#
|
||||
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:#?}"),
|
||||
}
|
||||
}
|
||||
```
|
||||
> **NOTE**: Attempt to execute an [introspection query][1] results in [validation][11] error, rather than [execution][5] error.
|
||||
|
||||
|
||||
|
||||
|
||||
[GraphQL]: https://graphql.org
|
||||
[Juniper]: https://docs.rs/juniper
|
||||
|
||||
[0]: https://spec.graphql.org/October2021#sec-Introspection
|
||||
[1]: https://spec.graphql.org/October2021#sec-Schema-Introspection
|
||||
[2]: https://spec.graphql.org/October2021#sel-GAFRJBABABF_jB
|
||||
[3]: https://spec.graphql.org/October2021#sel-GAFRJDABABI5C
|
||||
[4]: https://spec.graphql.org/October2021#sel-GAFRJFABABMvpN
|
||||
[5]: https://spec.graphql.org/October2021#sec-Execution
|
||||
[9]: https://docs.rs/juniper/0.16.1/juniper/struct.RootNode.html#method.disable_introspection
|
||||
[10]: https://www.apollographql.com/blog/why-you-should-disable-graphql-introspection-in-production
|
||||
[11]: https://spec.graphql.org/October2021#sec-Validation
|
176
book/src/schema/subscriptions.md
Normal file
|
@ -0,0 +1,176 @@
|
|||
Subscriptions
|
||||
=============
|
||||
|
||||
[GraphQL subscriptions][9] are a way to push data from a server to clients requesting real-time messages from a server. [Subscriptions][9] are similar to [queries][7] 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.
|
||||
|
||||
In order to execute [subscriptions][9] in [Juniper], we need a coordinator (spawning long-lived connections) and a [GraphQL object][4] with [fields][5] resolving into a [`Stream`] of elements which will then be returned to a client. The [`juniper_subscriptions` crate][30] provides a default implementation of these abstractions.
|
||||
|
||||
The [subscription root][3] is just a [GraphQL object][4], similar to the [query root][1] and [mutations root][2] that we define for operations in our [GraphQL schema][0]. For [subscriptions][9] all fields should be `async` and return a [`Stream`] of some [GraphQL type][6] values, rather than direct values.
|
||||
|
||||
```rust
|
||||
# extern crate futures;
|
||||
# extern crate juniper;
|
||||
# use std::pin::Pin;
|
||||
# use futures::Stream;
|
||||
# use juniper::{graphql_object, graphql_subscription, FieldError};
|
||||
#
|
||||
# #[derive(Clone)]
|
||||
# pub struct Database;
|
||||
#
|
||||
# impl juniper::Context for Database {}
|
||||
#
|
||||
# pub struct Query;
|
||||
#
|
||||
# #[graphql_object]
|
||||
# #[graphql(context = Database)]
|
||||
# impl Query {
|
||||
# fn hello_world() -> &'static str {
|
||||
# "Hello World!"
|
||||
# }
|
||||
# }
|
||||
#
|
||||
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)
|
||||
}
|
||||
}
|
||||
#
|
||||
# fn main () {}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Coordinator
|
||||
|
||||
[GraphQL subscriptions][9] require a bit more resources than regular [queries][7] and provide a great vector for [DoS attacks][20]. This can can bring down a server easily if not handled correctly. The [`SubscriptionCoordinator` trait][`SubscriptionCoordinator`] provides coordination logic to enable functionality like [DoS attacks][20] mitigation and resource limits.
|
||||
|
||||
The [`SubscriptionCoordinator`] contains the [schema][0] and can keep track of opened connections, handle [subscription][9] start and end, and maintain a global ID for each [subscription][9]. Each time a connection is established, the [`SubscriptionCoordinator`] spawns a [32], which handles a single connection, providing resolver logic for a client stream as well as reconnection and shutdown logic.
|
||||
|
||||
While we can implement [`SubscriptionCoordinator`] ourselves, [Juniper] contains a simple and generic implementation called [`Coordinator`]. The `subscribe` method returns a [`Future`] resolving into a `Result<Connection, GraphQLError>`, where [`Connection`] is a [`Stream`] of [values][10] returned by the operation, and a [`GraphQLError`] is the error when the [subscription operation][9] fails.
|
||||
|
||||
```rust
|
||||
# extern crate futures;
|
||||
# extern crate juniper;
|
||||
# extern crate juniper_subscriptions;
|
||||
# extern crate serde_json;
|
||||
# use std::pin::Pin;
|
||||
# use futures::{Stream, StreamExt as _};
|
||||
# use juniper::{
|
||||
# http::GraphQLRequest,
|
||||
# graphql_object, graphql_subscription,
|
||||
# DefaultScalarValue, EmptyMutation, FieldError,
|
||||
# RootNode, SubscriptionCoordinator,
|
||||
# };
|
||||
# use juniper_subscriptions::Coordinator;
|
||||
#
|
||||
# #[derive(Clone)]
|
||||
# pub struct Database;
|
||||
#
|
||||
# impl juniper::Context for Database {}
|
||||
#
|
||||
# impl Database {
|
||||
# fn new() -> Self {
|
||||
# Self
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# pub struct Query;
|
||||
#
|
||||
# #[graphql_object]
|
||||
# #[graphql(context = Database)]
|
||||
# impl Query {
|
||||
# fn hello_world() -> &'static str {
|
||||
# "Hello World!"
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# type StringStream = Pin<Box<dyn Stream<Item = Result<String, FieldError>> + Send>>;
|
||||
#
|
||||
# pub struct Subscription;
|
||||
#
|
||||
# #[graphql_subscription]
|
||||
# #[graphql(context = Database)]
|
||||
# impl Subscription {
|
||||
# async fn hello_world() -> StringStream {
|
||||
# let stream = futures::stream::iter([
|
||||
# Ok(String::from("Hello")),
|
||||
# Ok(String::from("World!")),
|
||||
# ]);
|
||||
# Box::pin(stream)
|
||||
# }
|
||||
# }
|
||||
#
|
||||
type Schema = RootNode<'static, Query, EmptyMutation<Database>, Subscription>;
|
||||
|
||||
fn schema() -> Schema {
|
||||
Schema::new(Query, EmptyMutation::new(), Subscription)
|
||||
}
|
||||
|
||||
async fn run_subscription() {
|
||||
let schema = schema();
|
||||
let coordinator = Coordinator::new(schema);
|
||||
let 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());
|
||||
}
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## WebSocket
|
||||
|
||||
For information about serving [GraphQL subscriptions][9] over [WebSocket], see the ["Serving" chapter](../serve/index.md#websocket).
|
||||
|
||||
|
||||
|
||||
|
||||
[`Coordinator`]: https://docs.rs/juniper_subscriptions/0.17.0/juniper_subscriptions/struct.Coordinator.html
|
||||
[`Connection`]: https://docs.rs/juniper_subscriptions/0.17.0/juniper_subscriptions/struct.Connection.html
|
||||
[`Future`]: https://doc.rust-lang.org/stable/std/future/trait.Future.html
|
||||
[`GraphQLError`]: https://docs.rs/juniper/0.16.1/juniper/enum.GraphQLError.html
|
||||
[`Stream`]: https://docs.rs/futures/latest/futures/stream/trait.Stream.html
|
||||
[`SubscriptionCoordinator`]: https://docs.rs/juniper/0.16.1/juniper/trait.SubscriptionCoordinator.html
|
||||
[`SubscriptionConnection`]: https://docs.rs/juniper/0.16.1/juniper/trait.SubscriptionConnection.html
|
||||
[GraphQL]: https://graphql.org
|
||||
[Juniper]: https://docs.rs/juniper
|
||||
[Rust]: https://www.rust-lang.org
|
||||
[WebSocket]: https://en.wikipedia.org/wiki/WebSocket
|
||||
|
||||
[0]: https://spec.graphql.org/October2021#sec-Schema
|
||||
[1]: https://spec.graphql.org/October2021#sel-FAHTRFCAACChCtpG
|
||||
[2]: https://spec.graphql.org/October2021#sel-FAHTRHCAACCuE9yD
|
||||
[3]: https://spec.graphql.org/October2021#sel-FAHTRJCAACC3EhsX
|
||||
[4]: https://spec.graphql.org/October2021#sec-Objects
|
||||
[5]: https://spec.graphql.org/October2021#sec-Language.Fields
|
||||
[6]: https://spec.graphql.org/October2021#sec-Types
|
||||
[7]: https://spec.graphql.org/October2021#sec-Query
|
||||
[8]: https://spec.graphql.org/October2021#sec-Mutation
|
||||
[9]: https://spec.graphql.org/October2021#sec-Subscription
|
||||
[10]: https://spec.graphql.org/October2021#sec-Values
|
||||
[20]: https://en.wikipedia.org/wiki/Denial-of-service_attack
|
||||
[30]: https://docs.rs/juniper_subscriptions
|
81
book/src/serve/batching.md
Normal file
|
@ -0,0 +1,81 @@
|
|||
Batching
|
||||
========
|
||||
|
||||
The [GraphQL] 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.
|
||||
|
||||
Some client libraries (such as [`apollo-link-batch-http`][1]) have the ability to batch operations in a single [HTTP] request to save network round-trips and potentially increase performance. There are [some tradeoffs][3], though, that should be considered before [batching operations][2].
|
||||
|
||||
[Juniper]'s [server integration crates](index.md#officially-supported) support [batching multiple operations][2] in a single [HTTP] request out-of-the-box via [JSON] arrays. This makes them compatible with client libraries that support [batch operations][2] without any special configuration.
|
||||
|
||||
> **NOTE**: If you use a custom server integration, it's **not a hard requirement** to support [batching][2], as it's not a part of the [official GraphQL specification][0].
|
||||
|
||||
Assuming an integration supports [operations batching][2], for the following GraphQL query:
|
||||
```graphql
|
||||
{
|
||||
hero {
|
||||
name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The [JSON] `data` to [POST] for an individual request would be:
|
||||
```json
|
||||
{
|
||||
"query": "{hero{name}}"
|
||||
}
|
||||
```
|
||||
And the response would be in the form:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"hero": {
|
||||
"name": "R2-D2"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
However, if we want to run the same query twice in a single [HTTP] request, the batched [JSON] `data` to [POST] would be:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"query": "{hero{name}}"
|
||||
},
|
||||
{
|
||||
"query": "{hero{name}}"
|
||||
}
|
||||
]
|
||||
```
|
||||
And then, the response would be in the following array form:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"data": {
|
||||
"hero": {
|
||||
"name": "R2-D2"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"hero": {
|
||||
"name": "R2-D2"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
[GraphQL]: https://graphql.org
|
||||
[HTTP]: https://en.wikipedia.org/wiki/HTTP
|
||||
[JSON]: https://www.json.org
|
||||
[Juniper]: https://docs.rs/juniper
|
||||
[POST]: https://en.wikipedia.org/wiki/POST_(HTTP)
|
||||
|
||||
[0]: https://spec.graphql.org/October2021
|
||||
[1]: https://www.apollographql.com/docs/link/links/batch-http.html
|
||||
[2]: https://www.apollographql.com/blog/batching-client-graphql-queries
|
||||
[3]: https://www.apollographql.com/blog/batching-client-graphql-queries#what-are-the-tradeoffs-with-batching
|
69
book/src/serve/index.md
Normal file
|
@ -0,0 +1,69 @@
|
|||
Serving
|
||||
=======
|
||||
|
||||
Once we have built a [GraphQL schema][1], the next obvious step would be to serve it, so clients can interact with our [GraphQL] API. Usually, [GraphQL] APIs are served via [HTTP].
|
||||
|
||||
|
||||
|
||||
|
||||
## Web server frameworks
|
||||
|
||||
Though the [`juniper`] crate doesn't provide a built-in [HTTP] server, the surrounding ecosystem does.
|
||||
|
||||
|
||||
### Officially supported
|
||||
|
||||
[Juniper] officially supports the following widely used and adopted web server frameworks in [Rust] ecosystem:
|
||||
- [`actix-web`] ([`juniper_actix`] crate)
|
||||
- [`axum`] ([`juniper_axum`] crate)
|
||||
- [`hyper`] ([`juniper_hyper`] crate)
|
||||
- [`rocket`] ([`juniper_rocket`] crate)
|
||||
- [`warp`] ([`juniper_warp`] crate)
|
||||
|
||||
See their API docs and usage examples (accessible from API docs) for further details of how they should be used.
|
||||
|
||||
> **NOTE**: All the officially supported web server framework integrations provide a simple and convenient way for exposing [GraphiQL] and/or [GraphQL Playground] with the [GraphQL schema][1] along. These powerful tools ease the development process by enabling you to explore and send client requests to the [GraphQL] API under development.
|
||||
|
||||
|
||||
|
||||
|
||||
## WebSocket
|
||||
|
||||
> **NOTE**: [WebSocket] is a crucial part for serving [GraphQL subscriptions][2] over [HTTP].
|
||||
|
||||
There are two widely adopted protocols for serving [GraphQL] over [WebSocket]:
|
||||
1. [Legacy `graphql-ws` GraphQL over WebSocket Protocol][ws-old], formerly used by [Apollo] and the [`subscriptions-transport-ws` npm package], and now being deprecated.
|
||||
2. [New `graphql-transport-ws` GraphQL over WebSocket Protocol][ws-new], provided by the [`graphql-ws` npm package] and being used by [Apollo] as for now.
|
||||
|
||||
In the [Juniper] ecosystem, both implementations are provided by the [`juniper_graphql_ws`] crate. Most of the [officially supported web server framework integrations](#officially-supported) are able to serve a [GraphQL schema][1] over [WebSocket] (including [subscriptions][2]) and even support [auto-negotiation of the correct protocol based on the `Sec-Websocket-Protocol` HTTP header value][3]. See their API docs and usage examples (accessible from API docs) for further details of how to do so.
|
||||
|
||||
|
||||
|
||||
|
||||
[`actix-web`]: https://docs.rs/actix-web
|
||||
[`axum`]: https://docs.rs/axum
|
||||
[`graphql-ws` npm package]: https://npmjs.com/package/graphql-ws
|
||||
[`juniper`]: https://docs.rs/juniper
|
||||
[`juniper_actix`]: https://docs.rs/juniper_actix
|
||||
[`juniper_axum`]: https://docs.rs/juniper_axum
|
||||
[`juniper_graphql_ws`]: https://docs.rs/juniper_graphql_ws
|
||||
[`juniper_rocket`]: https://docs.rs/juniper_rocket
|
||||
[`juniper_warp`]: https://docs.rs/juniper_warp
|
||||
[`hyper`]: https://docs.rs/hyper
|
||||
[`rocket`]: https://docs.rs/rocket
|
||||
[`subscriptions-transport-ws` npm package]: https://npmjs.com/package/subscriptions-transport-ws
|
||||
[`warp`]: https://docs.rs/warp
|
||||
[Apollo]: https://www.apollographql.com
|
||||
[GraphiQL]: https://github.com/graphql/graphiql
|
||||
[GraphQL]: https://graphql.org
|
||||
[GraphQL Playground]: https://github.com/prisma/graphql-playground
|
||||
[HTTP]: https://en.wikipedia.org/wiki/HTTP
|
||||
[Juniper]: https://docs.rs/juniper
|
||||
[Rust]: https://www.rust-lang.org
|
||||
[WebSocket]: https://en.wikipedia.org/wiki/WebSocket
|
||||
[ws-new]: https://github.com/enisdenjo/graphql-ws/blob/v5.14.0/PROTOCOL.md
|
||||
[ws-old]: https://github.com/apollographql/subscriptions-transport-ws/blob/v0.11.0/PROTOCOL.md
|
||||
|
||||
[1]: ../schema/index.md
|
||||
[2]: ../schema/subscriptions.md
|
||||
[3]: https://developer.mozilla.org/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#subprotocols
|
151
book/src/types/enums.md
Normal file
|
@ -0,0 +1,151 @@
|
|||
Enums
|
||||
=====
|
||||
|
||||
> [GraphQL enum][0] types, like [scalar][1] types, also represent leaf values in a GraphQL type system. However [enum][0] types describe the set of possible values.
|
||||
>
|
||||
> [Enums][0] 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.
|
||||
|
||||
With [Juniper] a [GraphQL enum][0] may be defined by using the [`#[derive(GraphQLEnum)]`][2] attribute on a [Rust enum][3] as long as its variants do not have any fields:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::GraphQLEnum;
|
||||
#
|
||||
#[derive(GraphQLEnum)]
|
||||
enum Episode {
|
||||
NewHope,
|
||||
Empire,
|
||||
Jedi,
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
|
||||
### Renaming
|
||||
|
||||
By default, [enum][3] variants are converted from [Rust]'s standard `PascalCase` naming convention into [GraphQL]'s `SCREAMING_SNAKE_CASE` convention:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::GraphQLEnum;
|
||||
#
|
||||
#[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
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
We can override the name by using the `#[graphql(name = "...")]` attribute:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::GraphQLEnum;
|
||||
#
|
||||
#[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,
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
Or provide a different renaming policy for all the [enum][3] variants:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::GraphQLEnum;
|
||||
#
|
||||
#[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
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `camelCase` and `none` (disables any renaming).
|
||||
|
||||
|
||||
### Documentation and deprecation
|
||||
|
||||
Just like when [defining GraphQL objects](objects/index.md#documentation), the [GraphQL enum][0] type and its values could be [documented][4] and [deprecated][5] via `#[graphql(description = "...")]` and `#[graphql(deprecated = "...")]`/[`#[deprecated]`][13] attributes:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::GraphQLEnum;
|
||||
#
|
||||
/// 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
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
> **NOTE**: Only [GraphQL object][6]/[interface][7] fields and [GraphQL enum][0] values can be [deprecated][5].
|
||||
|
||||
|
||||
### Ignoring
|
||||
|
||||
By default, all [enum][3] variants are included in the generated [GraphQL enum][0] type as values. To prevent including a specific variant, annotate it with the `#[graphql(ignore)]` attribute:
|
||||
```rust
|
||||
# #![allow(dead_code)]
|
||||
# extern crate juniper;
|
||||
# use juniper::GraphQLEnum;
|
||||
#
|
||||
#[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
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
> **TIP**: See more available features in the API docs of the [`#[derive(GraphQLEnum)]`][2] attribute.
|
||||
|
||||
|
||||
|
||||
|
||||
[GraphQL]: https://graphql.org
|
||||
[Juniper]: https://docs.rs/juniper
|
||||
[Rust]: https://www.rust-lang.org
|
||||
|
||||
[0]: https://spec.graphql.org/October2021#sec-Enums
|
||||
[1]: https://spec.graphql.org/October2021#sec-Scalars
|
||||
[2]: https://docs.rs/juniper/0.16.1/juniper/derive.GraphQLEnum.html
|
||||
[3]: https://doc.rust-lang.org/reference/items/enumerations.html
|
||||
[4]: https://spec.graphql.org/October2021#sec-Descriptions
|
||||
[5]: https://spec.graphql.org/October2021#sec--deprecated
|
||||
[6]: https://spec.graphql.org/October2021#sec-Objects
|
||||
[7]: https://spec.graphql.org/October2021#sec-Interfaces
|
||||
[13]: https://doc.rust-lang.org/reference/attributes/diagnostics.html#the-deprecated-attribute
|
28
book/src/types/index.md
Normal file
|
@ -0,0 +1,28 @@
|
|||
Type system
|
||||
===========
|
||||
|
||||
Most of the work in working with [Juniper] consists of mapping the [GraphQL type system][0] to the [Rust] types our application uses.
|
||||
|
||||
[Juniper] provides some convenient abstractions making this process as painless as possible.
|
||||
|
||||
Find out more in the individual chapters below:
|
||||
- [Objects](objects/index.md)
|
||||
- [Complex fields](objects/complex_fields.md)
|
||||
- [Context](objects/Context.md)
|
||||
- [Error handling](objects/error/index.md)
|
||||
- [Field errors](objects/error/field.md)
|
||||
- [Schema errors](objects/error/schema.md)
|
||||
- [Generics](objects/generics.md)
|
||||
- [Interfaces](interfaces.md)
|
||||
- [Unions](unions.md)
|
||||
- [Enums](enums.md)
|
||||
- [Input objects](input_objects.md)
|
||||
- [Scalars](scalars.md)
|
||||
|
||||
|
||||
|
||||
|
||||
[Juniper]: https://docs.rs/juniper
|
||||
[Rust]: https://www.rust-lang.org
|
||||
|
||||
[0]: https://spec.graphql.org/October2021#sec-Type-System
|
163
book/src/types/input_objects.md
Normal file
|
@ -0,0 +1,163 @@
|
|||
Input objects
|
||||
=============
|
||||
|
||||
> [Fields][4] may accept [arguments][5] to configure their behavior. These inputs are often [scalars][12] or [enums][10], but they sometimes need to represent more complex values.
|
||||
>
|
||||
> A [GraphQL input object][0] defines a set of input fields; the input fields are either [scalars][12], [enums][10], or other [input objects][0]. This allows [arguments][5] to accept arbitrarily complex structs.
|
||||
|
||||
In [Juniper], defining a [GraphQL input object][0] is quite straightforward and similar to how [trivial GraphQL objects are defined](objects/index.md) - by using the [`#[derive(GraphQLInputObject)]` attribute][2] on a [Rust struct][struct]:
|
||||
```rust
|
||||
# #![allow(unused_variables)]
|
||||
# extern crate juniper;
|
||||
# use juniper::{graphql_object, GraphQLInputObject, GraphQLObject};
|
||||
#
|
||||
#[derive(GraphQLInputObject)]
|
||||
struct Coordinate {
|
||||
latitude: f64,
|
||||
longitude: f64
|
||||
}
|
||||
|
||||
struct Root;
|
||||
# #[derive(GraphQLObject)] struct User { name: String }
|
||||
|
||||
#[graphql_object]
|
||||
impl Root {
|
||||
fn users_at_location(coordinate: Coordinate, radius: f64) -> Vec<User> {
|
||||
// Send coordinate to database
|
||||
// ...
|
||||
# unimplemented!()
|
||||
}
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
|
||||
### Renaming
|
||||
|
||||
Just as with [defining GraphQL objects](objects/index.md#renaming), by default [struct] fields are converted from [Rust]'s standard `snake_case` naming convention into [GraphQL]'s `camelCase` convention:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::GraphQLInputObject;
|
||||
#
|
||||
#[derive(GraphQLInputObject)]
|
||||
struct Person {
|
||||
first_name: String, // exposed as `firstName` in GraphQL schema
|
||||
last_name: String, // exposed as `lastName` in GraphQL schema
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
We can override the name by using the `#[graphql(name = "...")]` attribute:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::GraphQLInputObject;
|
||||
#
|
||||
#[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
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
Or provide a different renaming policy for all the [struct] fields:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::GraphQLInputObject;
|
||||
#
|
||||
#[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
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `camelCase` and `none` (disables any renaming).
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
Similarly, [GraphQL descriptions][7] may be provided by either using [Rust doc comments][6] or with the `#[graphql(description = "...")]` attribute:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::GraphQLInputObject;
|
||||
#
|
||||
/// 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,
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
> **NOTE**: As of [October 2021 GraphQL specification][spec], [GraphQL input object][0]'s fields **cannot be** [deprecated][9].
|
||||
|
||||
|
||||
### Ignoring
|
||||
|
||||
By default, all [struct] fields are included into the generated [GraphQL input object][0] type. To prevent inclusion of a specific field annotate it with the `#[graphql(ignore)]` attribute:
|
||||
> **WARNING**: Ignored fields must either implement `Default` or be annotated with the `#[graphql(default = <expression>)]` argument.
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::GraphQLInputObject;
|
||||
#
|
||||
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,
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
> **TIP**: See more available features in the API docs of the [`#[derive(GraphQLInputObject)]`][2] attribute.
|
||||
|
||||
|
||||
|
||||
[GraphQL]: https://graphql.org
|
||||
[Juniper]: https://docs.rs/juniper
|
||||
[Rust]: https://www.rust-lang.org
|
||||
[struct]: https://doc.rust-lang.org/reference/items/structs.html
|
||||
[spec]: https://spec.graphql.org/October2021
|
||||
|
||||
[0]: https://spec.graphql.org/October2021#sec-Input-Objects
|
||||
[2]: https://docs.rs/juniper/0.16.1/juniper/derive.GraphQLInputObject.html
|
||||
[4]: https://spec.graphql.org/October2021#sec-Language.Fields
|
||||
[5]: https://spec.graphql.org/October2021#sec-Language.Arguments
|
||||
[6]: https://doc.rust-lang.org/reference/comments.html#doc-comments
|
||||
[7]: https://spec.graphql.org/October2021#sec-Descriptions
|
||||
[9]: https://spec.graphql.org/October2021#sec--deprecated
|
||||
[10]: https://spec.graphql.org/October2021#sec-Enums
|
||||
[12]: https://spec.graphql.org/October2021#sec-Scalars
|
416
book/src/types/interfaces.md
Normal file
|
@ -0,0 +1,416 @@
|
|||
Interfaces
|
||||
==========
|
||||
|
||||
> [GraphQL interfaces][0] represent a list of named [fields][4] and their [arguments][5]. [GraphQL objects][10] and [interfaces][0] can then implement these [interfaces][0] which requires that the implementing type will define all [fields][4] defined by those [interfaces][0].
|
||||
|
||||
[GraphQL interfaces][0] map well to interfaces known from common object-oriented languages such as Java or C#, but [Rust], unfortunately, has no concept that maps perfectly to them. The nearest analogue of [GraphQL interfaces][0] are [Rust traits][20], but the main difference is that in [GraphQL] an [interface type][0] serves both as an _abstraction_ and a _boxed value (dispatchable to concrete implementers)_, while in [Rust], a [trait][20] is an _abstraction only_, and _to represent such a boxed value a separate type is required_, like a [trait object][21] or an [enum][22] consisting of implementer types, because [Rust trait][20] doesn't represent a type itself, and so, can have no values.
|
||||
|
||||
Another notable difference is that [GraphQL interfaces][0] are more like [structurally-typed][30] contracts: they _only declare a list of [fields][4]_ a [GraphQL] type should already have. [Rust traits][20], on the other hand, are [type classes][31], which don't really care about existing methods, but, rather, _require to provide implementations for required methods_ despite the fact whether the type already has such methods or not. This difference makes the [trait implementation][23] not a good fit for expressing a [GraphQL interface][0] implementation, because _we don't really need to implement any [fields][4]_, the [GraphQL] type implementing a [GraphQL interface][0] has those [fields][4] already. _We only need to check that [fields'][4] signatures match_.
|
||||
|
||||
That's why [Juniper] takes the following approach to represent [GraphQL interfaces][0], which consists of two parts:
|
||||
1. Either a [struct][24], or a [trait][20] (in case [fields][4] have [arguments][5]), which acts only as a blueprint describing the required list of [fields][4], and is not used in runtime at all.
|
||||
2. An auto-generated [enum][22], representing a dispatchable value-type for the [GraphQL interfaces][0], which may be referred and returned by other [fields][4].
|
||||
|
||||
This may be done by using either the [`#[graphql_interface]` attribute][3] or the [`#[derive(GraphQLInterface)]`][2]:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::{graphql_interface, GraphQLInterface, GraphQLObject};
|
||||
#
|
||||
// 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
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
|
||||
### Interfaces implementing other interfaces
|
||||
|
||||
[GraphQL] allows implementing [interfaces][0] on other [interfaces][0] in addition to [objects][10]:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::{graphql_object, GraphQLInterface, ID};
|
||||
#
|
||||
#[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"
|
||||
}
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
> **NOTE**: Every [interface][0] has to specify all other [interfaces][0]/[objects][0] it implements or implemented for. Missing one of `for = ` or `impl = ` attribute arguments is a **compile-time error**.
|
||||
> ```rust,compile_fail
|
||||
> # extern crate juniper;
|
||||
> # use juniper::{GraphQLInterface, GraphQLObject};
|
||||
> #
|
||||
> #[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,
|
||||
> }
|
||||
> #
|
||||
> # fn main() {}
|
||||
> ```
|
||||
|
||||
|
||||
### Subtyping and additional `null`able arguments
|
||||
|
||||
[GraphQL] allows implementers (both [objects][10] and other [interfaces][0]) to return "subtypes" instead of an original value. Basically, this allows to impose additional bounds on the implementation.
|
||||
|
||||
Valid "subtypes" are:
|
||||
- [interface][0] implementer instead of an [interface][0] itself:
|
||||
- `I implements T` in place of a `T`;
|
||||
- `Vec<I implements T>` in place of a `Vec<T>`.
|
||||
- [non-`null`][6] value in place of a `null`able:
|
||||
- `T` in place of a `Option<T>`;
|
||||
- `Vec<T>` in place of a `Vec<Option<T>>`.
|
||||
|
||||
These rules are recursively applied, so `Vec<Vec<I implements T>>` is a valid "subtype" of a `Option<Vec<Option<Vec<Option<T>>>>>`.
|
||||
|
||||
Also, [GraphQL] allows implementers to add `null`able [field arguments][5], which aren't present on an original interface.
|
||||
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::{graphql_interface, graphql_object, GraphQLInterface, ID};
|
||||
#
|
||||
#[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!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
> **NOTE**: Violating [GraphQL] "subtyping" or additional `null`able [argument][5] rules is a **compile-time error**.
|
||||
>
|
||||
> ```rust,compile_fail
|
||||
> # extern crate juniper;
|
||||
> # use juniper::{graphql_object, GraphQLInterface};
|
||||
> #
|
||||
> 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,
|
||||
> }
|
||||
> #
|
||||
> # fn main() {}
|
||||
> ```
|
||||
>
|
||||
> ```rust,compile_fail
|
||||
> # extern crate juniper;
|
||||
> # use juniper::{GraphQLInterface, GraphQLObject};
|
||||
> #
|
||||
> #[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,
|
||||
> }
|
||||
> #
|
||||
> # fn main() {}
|
||||
> ```
|
||||
|
||||
|
||||
### Default arguments
|
||||
|
||||
[Similarly to GraphQL object fields](objects/complex_fields.md#default-arguments), [GraphQL arguments][4] of [interfaces][0] are able to have default values, though [Rust] doesn't have such notion:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::graphql_interface;
|
||||
#
|
||||
#[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;
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
|
||||
### Renaming
|
||||
|
||||
Just as with [defining GraphQL objects](objects/index.md#renaming), by default, [fields][4] are converted from [Rust]'s standard `snake_case` naming convention into [GraphQL]'s `camelCase` convention:
|
||||
|
||||
We can override the name by using the `#[graphql(name = "...")]` attribute:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::{graphql_interface, GraphQLInterface};
|
||||
#
|
||||
#[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;
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
Or provide a different renaming policy for all the defined [fields][4]:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::graphql_interface;
|
||||
#
|
||||
#[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;
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `camelCase` and `none` (disables any renaming).
|
||||
|
||||
|
||||
### Documentation and deprecation
|
||||
|
||||
Similarly, [GraphQL fields][4] of [interfaces][0] may also be [documented][7] and [deprecated][9] via `#[graphql(description = "...")]` and `#[graphql(deprecated = "...")]`/[`#[deprecated]`][13] attributes:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::graphql_interface;
|
||||
#
|
||||
/// 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
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
> **NOTE**: Only [GraphQL interface][0]/[object][10] fields and [GraphQL enum][11] values can be [deprecated][9].
|
||||
|
||||
|
||||
### Ignoring
|
||||
|
||||
By default, all [struct][24] fields or [trait][20] methods are considered as [GraphQL fields][4]. If a helper method is needed, or it should be ignored for some reason, then it should be marked with the `#[graphql(ignore)]` attribute:
|
||||
```rust
|
||||
# #![allow(dead_code)]
|
||||
# extern crate juniper;
|
||||
# use std::marker::PhantomPinned;
|
||||
# use juniper::{graphql_interface, GraphQLInterface};
|
||||
#
|
||||
#[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);
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
> **TIP**: See more available features in the API docs of the [`#[graphql_interface]`][3] attribute.
|
||||
|
||||
|
||||
|
||||
|
||||
[GraphQL]: https://graphql.org
|
||||
[Juniper]: https://docs.rs/juniper
|
||||
[Rust]: https://www.rust-lang.org
|
||||
|
||||
[0]: https://spec.graphql.org/October2021#sec-Interfaces
|
||||
[2]: https://docs.rs/juniper/0.16.1/juniper/derive.GraphQLInterface.html
|
||||
[3]: https://docs.rs/juniper/0.16.1/juniper/attr.graphql_interface.html
|
||||
[4]: https://spec.graphql.org/October2021#sec-Language.Fields
|
||||
[5]: https://spec.graphql.org/October2021#sec-Language.Arguments
|
||||
[6]: https://spec.graphql.org/October2021#sec-Non-Null
|
||||
[7]: https://spec.graphql.org/October2021#sec-Descriptions
|
||||
[9]: https://spec.graphql.org/October2021#sec--deprecated
|
||||
[10]: https://spec.graphql.org/October2021#sec-Objects
|
||||
[11]: https://spec.graphql.org/October2021#sec-Enums
|
||||
[13]: https://doc.rust-lang.org/reference/attributes/diagnostics.html#the-deprecated-attribute
|
||||
[20]: https://doc.rust-lang.org/reference/items/traits.html#traits
|
||||
[21]: https://doc.rust-lang.org/reference/types/trait-object.html#trait-objects
|
||||
[22]: https://doc.rust-lang.org/reference/items/enumerations.html#enumerations
|
||||
[23]: https://doc.rust-lang.org/reference/items/implementations.html#trait-implementations
|
||||
[24]: https://doc.rust-lang.org/reference/items/structs.html
|
||||
[30]: https://en.wikipedia.org/wiki/Structural_type_system
|
||||
[31]: https://en.wikipedia.org/wiki/Type_class
|
229
book/src/types/objects/complex_fields.md
Normal file
|
@ -0,0 +1,229 @@
|
|||
Complex fields
|
||||
==============
|
||||
|
||||
Using a plain [Rust struct][struct] for representing a [GraphQL object][0] is easy and trivial but does not cover every case. What if we need to express something non-trivial as a [GraphQL field][4], such as:
|
||||
- Calling non-trivial logic while [executing][1] the [field][4] (like querying database, etc.).
|
||||
- Accepting [field arguments][5].
|
||||
- Defining a circular [GraphQL object][0], where one of its [fields][4] returns the type itself.
|
||||
- Using some other (non-[struct]) [Rust] type to represent a [GraphQL object][0].
|
||||
|
||||
To support these more complicated use cases, we need a way to define a [GraphQL field][4] as a function. In [Juniper] this is achievable by placing the [`#[graphql_object]` attribute][3] on an [`impl` block][6], which turns its methods into [GraphQL fields][4]:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::{graphql_object, GraphQLObject};
|
||||
#
|
||||
#[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)
|
||||
}
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
> **NOTE**: To access global data such as database connections or authentication information, a _context_ is used. To learn more about this, see the ["Context" chapter](context.md).
|
||||
|
||||
|
||||
### Default arguments
|
||||
|
||||
Though [Rust] doesn't have the notion of default arguments, [GraphQL arguments][4] are able to have default values. These default values are used when a GraphQL operation doesn't specify the argument explicitly. In [Juniper], defining a default value for a [GraphQL argument][4] is enabled by the `#[graphql(default)]` attribute:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::graphql_object;
|
||||
#
|
||||
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}")
|
||||
}
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
|
||||
### Renaming
|
||||
|
||||
Like with the [`#[derive(GraphQLObject)]` attribute on structs](index.md#renaming), [field][4] names are converted from [Rust]'s standard `snake_case` naming convention into [GraphQL]'s `camelCase` convention.
|
||||
|
||||
We can override the name by using the `#[graphql(name = "...")]` attribute:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::graphql_object;
|
||||
#
|
||||
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
|
||||
}
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
Or provide a different renaming policy for all the defined [fields][4]:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::graphql_object;
|
||||
#
|
||||
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
|
||||
}
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `camelCase` and `none` (disables any renaming).
|
||||
|
||||
|
||||
### Documentation and deprecation
|
||||
|
||||
Similarly, [GraphQL fields][4] may also be [documented][7] and [deprecated][9] via `#[graphql(description = "...")]` and `#[graphql(deprecated = "...")]`/[`#[deprecated]`][13] attributes:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::graphql_object;
|
||||
#
|
||||
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
|
||||
}
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
> **NOTE**: Only [GraphQL object][0]/[interface][11] fields and [GraphQL enum][10] values can be [deprecated][9].
|
||||
|
||||
|
||||
### Ignoring
|
||||
|
||||
By default, all methods of an [`impl` block][6] are exposed as [GraphQL fields][4]. If a method should not be exposed as a [GraphQL field][4], it should be defined in a separate [`impl` block][6] or marked with the `#[graphql(ignore)]` attribute:
|
||||
```rust
|
||||
# #![allow(dead_code)]
|
||||
# extern crate juniper;
|
||||
# use juniper::graphql_object;
|
||||
#
|
||||
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...
|
||||
}
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
> **TIP**: See more available features in the API docs of the [`#[graphql_object]`][3] attribute.
|
||||
|
||||
|
||||
|
||||
|
||||
[GraphQL]: https://graphql.org
|
||||
[Juniper]: https://docs.rs/juniper
|
||||
[Rust]: https://www.rust-lang.org
|
||||
[struct]: https://doc.rust-lang.org/reference/items/structs.html
|
||||
|
||||
[0]: https://spec.graphql.org/October2021#sec-Objects
|
||||
[1]: https://spec.graphql.org/October2021#sec-Execution
|
||||
[2]: https://docs.rs/juniper/0.16.1/juniper/derive.GraphQLObject.html
|
||||
[3]: https://docs.rs/juniper/0.16.1/juniper/attr.graphql_object.html
|
||||
[4]: https://spec.graphql.org/October2021#sec-Language.Fields
|
||||
[5]: https://spec.graphql.org/October2021#sec-Language.Arguments
|
||||
[6]: https://doc.rust-lang.org/reference/items/implementations.html#inherent-implementations
|
||||
[7]: https://spec.graphql.org/October2021#sec-Descriptions
|
||||
[9]: https://spec.graphql.org/October2021#sec--deprecated
|
||||
[10]: https://spec.graphql.org/October2021#sec-Enums
|
||||
[11]: https://spec.graphql.org/October2021#sec-Interfaces
|
||||
[13]: https://doc.rust-lang.org/reference/attributes/diagnostics.html#the-deprecated-attribute
|
156
book/src/types/objects/context.md
Normal file
|
@ -0,0 +1,156 @@
|
|||
Context
|
||||
=======
|
||||
|
||||
_Context_ is a feature in [Juniper] that lets [field][4] resolvers access global data, most commonly database connections or authentication information.
|
||||
|
||||
Let's say that we have a simple `User`s database in a `HashMap`:
|
||||
```rust
|
||||
# #![allow(dead_code)]
|
||||
# use std::collections::HashMap;
|
||||
#
|
||||
struct Database {
|
||||
users: HashMap<i32, User>,
|
||||
}
|
||||
|
||||
struct User {
|
||||
id: i32,
|
||||
name: String,
|
||||
friend_ids: Vec<i32>,
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
We would like to define a `friends` [field][4] on `User` that returns a list of `User` [objects][0]. In order to write such a [field][4] we need to query a `Database`. To accomplish this we must first mark the `Database` as a valid context type and then assign it to the `User` [object][0]. To gain access to the context in the `friends` [field][4], we need to specify an argument with the same type as the specified context:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use std::collections::HashMap;
|
||||
# use juniper::graphql_object;
|
||||
#
|
||||
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
|
||||
}
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
|
||||
### Mutating and mutable references
|
||||
|
||||
Context cannot be a mutable reference as [fields][4] may be resolved concurrently. If something in the context requires a mutable reference, the context type should leverage the [_interior mutability_ pattern][5] (e.g. use `RwLock`, `RefCell` or similar).
|
||||
|
||||
For example, when using async runtime with [work stealing][6] (like [`tokio`]), which obviously requires thread safety in addition, we will need to use a corresponding async version of `RwLock`:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# extern crate tokio;
|
||||
# use std::collections::HashMap;
|
||||
# use juniper::graphql_object;
|
||||
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
|
||||
}
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
> **TIP**: Replace `tokio::sync::RwLock` with `std::sync::RwLock` (or similar) if you don't intend to use async resolving.
|
||||
|
||||
|
||||
|
||||
|
||||
[`tokio`]: https://docs.rs/tokio
|
||||
[GraphQL]: https://graphql.org
|
||||
[Juniper]: https://docs.rs/juniper
|
||||
[Rust]: https://www.rust-lang.org
|
||||
|
||||
[0]: https://spec.graphql.org/October2021#sec-Objects
|
||||
[4]: https://spec.graphql.org/October2021#sec-Language.Fields
|
||||
[5]: https://doc.rust-lang.org/reference/interior-mutability.html#interior-mutability
|
||||
[6]: https://en.wikipedia.org/wiki/Work_stealing
|
183
book/src/types/objects/error/field.md
Normal file
|
@ -0,0 +1,183 @@
|
|||
Field errors
|
||||
============
|
||||
|
||||
[Rust] provides [two ways of dealing with errors][11]:
|
||||
- [`Result<T, E>`][12] for recoverable errors;
|
||||
- [`panic!`][13] for unrecoverable errors.
|
||||
|
||||
[Juniper] does not do anything about panicking, it naturally bubbles up to the surrounding code/framework and can be dealt with there.
|
||||
|
||||
For recoverable errors, [Juniper] works well with the [built-in `Result` type][12]. You can use the [`?` operator][14] and things will work as you expect them to:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use std::{fs::File, io::Read, path::PathBuf, str};
|
||||
# use juniper::{graphql_object, FieldResult};
|
||||
#
|
||||
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()))
|
||||
}
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
[`FieldResult<T>`][21] is an alias for [`Result<T, FieldError>`][22], which is the [error type][1] all fallible [fields][6] must return. By using the [`?` operator][14], any type that implements the [`Display` trait][15] (which most of the error types out there do) can be automatically converted into a [`FieldError`][22].
|
||||
|
||||
> **TIP**: If a custom conversion into a [`FieldError`][22] is needed (to [fill up `extensions`][2], for example), the [`IntoFieldError` trait][23] should be implemented.
|
||||
|
||||
> **NOTE**: [`FieldError`][22]s are [GraphQL field errors][1] and are [not visible][9] in a [GraphQL schema][8] in any way.
|
||||
|
||||
|
||||
|
||||
|
||||
## Error payloads, `null`, and partial errors
|
||||
|
||||
[Juniper]'s error behavior conforms to the [GraphQL specification][0].
|
||||
|
||||
When a [field][6] returns an [error][11], the [field][6]'s result is replaced by `null`, and an additional `errors` object is created at the top level of the [response][7], and the [execution][5] is resumed.
|
||||
|
||||
Let's run the following query against the previous example:
|
||||
```graphql
|
||||
{
|
||||
example {
|
||||
contents
|
||||
foo
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If `str::from_utf8` results in a `std::str::Utf8Error`, then the following will be returned:
|
||||
```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}]
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
> Since [`Non-Null` type][4] [fields][5] cannot be **null**, [field errors][1] are propagated to be handled by the parent [field][5]. If the parent [field][5] may be **null** then it resolves to **null**, otherwise if it is a [`Non-Null` type][4], the [field error][1] is further propagated to its parent [field][5].
|
||||
|
||||
For example, with the following query:
|
||||
```graphql
|
||||
{
|
||||
example {
|
||||
contents
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If the `File::open()` above results in a `std::io::ErrorKind::PermissionDenied`, the following ill be returned:
|
||||
```json
|
||||
{
|
||||
"data": null,
|
||||
"errors": [{
|
||||
"message": "Permission denied (os error 13)",
|
||||
"locations": [{"line": 2, "column": 4}]
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Additional information
|
||||
|
||||
Sometimes it's desirable to return additional structured error information to clients. This can be accomplished by implementing the [`IntoFieldError` trait][23]:
|
||||
```rust
|
||||
# #[macro_use] extern crate juniper;
|
||||
# use juniper::{graphql_object, FieldError, IntoFieldError, ScalarValue};
|
||||
#
|
||||
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)
|
||||
}
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
And the specified structured error information will be included into the [error's `extensions`][2]:
|
||||
```json
|
||||
{
|
||||
"errors": [{
|
||||
"message": "Whatever does not exist",
|
||||
"locations": [{"line": 2, "column": 4}],
|
||||
"extensions": {
|
||||
"type": "NO_WHATEVER"
|
||||
}
|
||||
}]
|
||||
}
|
||||
```
|
||||
> **NOTE**: This pattern is particularly useful when it comes to instrumentation of returned [field errors][1] with custom error codes or additional diagnostics (like stack traces or tracing IDs).
|
||||
|
||||
|
||||
|
||||
|
||||
[GraphQL]: https://graphql.org
|
||||
[Juniper]: https://docs.rs/juniper
|
||||
[Rust]: https://www.rust-lang.org
|
||||
|
||||
[0]: https://spec.graphql.org/October2021#sec-Handling-Field-Errors
|
||||
[1]: https://spec.graphql.org/October2021#sec-Errors.Field-errors
|
||||
[2]: https://spec.graphql.org/October2021#sel-GAPHRPZCAACCC_7Q
|
||||
[4]: https://spec.graphql.org/October2021#sec-Non-Null
|
||||
[5]: https://spec.graphql.org/October2021#sec-Execution
|
||||
[6]: https://spec.graphql.org/October2021#sec-Language.Fields
|
||||
[7]: https://spec.graphql.org/October2021#sec-Response
|
||||
[8]: https://graphql.org/learn/schema
|
||||
[9]: https://spec.graphql.org/October2021#sec-Introspection
|
||||
[11]: https://doc.rust-lang.org/book/ch09-00-error-handling.html
|
||||
[12]: https://doc.rust-lang.org/stable/std/result/enum.Result.html
|
||||
[13]: https://doc.rust-lang.org/stable/std/macro.panic.html
|
||||
[14]: https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#a-shortcut-for-propagating-errors-the--operator
|
||||
[15]: https://doc.rust-lang.org/stable/std/fmt/trait.Display.html
|
||||
[21]: https://docs.rs/juniper/0.16.1/juniper/executor/type.FieldResult.html
|
||||
[22]: https://docs.rs/juniper/0.16.1/juniper/executor/struct.FieldError.html
|
||||
[23]: https://docs.rs/juniper/0.16.1/juniper/executor/trait.IntoFieldError.html
|
26
book/src/types/objects/error/index.md
Normal file
|
@ -0,0 +1,26 @@
|
|||
Error handling
|
||||
==============
|
||||
|
||||
Error handling in [GraphQL] can be done in multiple ways. We will cover the two different error handling models mostly used:
|
||||
1. [Implicit field results](field.md).
|
||||
2. [Explicit errors backend by GraphQL schema](schema.md).
|
||||
|
||||
Choosing the right error handling method depends on the requirements of the application and the concrete error happening. Investigating both approaches is beneficial.
|
||||
|
||||
|
||||
|
||||
|
||||
## Comparison
|
||||
|
||||
The [first approach](field.md) (where every error is a [field error][1]) is easier to implement. However, clients won't know what errors may occur and instead will have to infer what happens from the [error message][2]. 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.
|
||||
|
||||
[Encoding non-critical errors in a GraphQL schema](schema.md) 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 [GraphQL schema][8] requires additional code and up-front definition of non-critical errors.
|
||||
|
||||
|
||||
|
||||
|
||||
[GraphQL]: https://graphql.org
|
||||
|
||||
[1]: https://spec.graphql.org/October2021#sec-Errors.Field-errors
|
||||
[2]: https://spec.graphql.org/October2021/#sel-GAPHRPDCAACCyD57Z
|
||||
[8]: https://graphql.org/learn/schema
|
336
book/src/types/objects/error/schema.md
Normal file
|
@ -0,0 +1,336 @@
|
|||
Schema errors
|
||||
=============
|
||||
|
||||
[Rust]'s model of errors can be adapted for [GraphQL]. [Rust]'s panic is similar to a [field error][1] - the whole query is aborted and nothing can be extracted (except for error related information).
|
||||
|
||||
Not all errors require this strict handling. Recoverable or partial errors can be put into a [GraphQL schema][8], so the client can intelligently handle them.
|
||||
|
||||
To implement this approach, all errors must be partitioned into two classes:
|
||||
- _Critical_ errors that cannot be fixed by clients (e.g. a database error).
|
||||
- _Recoverable_ errors that can be fixed by clients (e.g. invalid input data).
|
||||
|
||||
Critical errors are returned from resolvers as [field errors][1] (from the [previous chapter](field.md)). Recoverable errors are part of a [GraphQL schema][8] and can be handled gracefully by clients. Similar to [Rust], [GraphQL] allows similar error models with [unions][9] (see ["Unions" chapter](../../unions.md)).
|
||||
|
||||
|
||||
### Example: Simple
|
||||
|
||||
In this example, basic input validation is implemented with [GraphQL types][7]. [Strings][5] are used to identify the problematic [field][6] name. Errors for a particular [field][6] are also returned as a [string][5].
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::{graphql_object, GraphQLObject, GraphQLUnion};
|
||||
#
|
||||
#[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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
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 `User` would require a new result type containing the variant `Ok(User)`instead of `Ok(Item)`.
|
||||
|
||||
> **NOTE**: In this example the returned [string][5] 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.
|
||||
|
||||
The client can send a mutation request and handle the resulting errors in the following manner:
|
||||
```graphql
|
||||
{
|
||||
mutation {
|
||||
addItem(name: "", quantity: 0) {
|
||||
... on Item {
|
||||
name
|
||||
}
|
||||
... on ValidationErrors {
|
||||
errors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **NOTE**: 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.
|
||||
|
||||
|
||||
### Example: Complex
|
||||
|
||||
Instead of using [strings][5] to propagate errors, it is possible to use [GraphQL type system][7] to describe the errors more precisely.
|
||||
|
||||
For each fallible [input argument][4] we create a [field][6] in a [GraphQL object][10]. The [field][6] is set if the validation for that particular [argument][4] fails.
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::{graphql_object, GraphQLObject, GraphQLUnion};
|
||||
#
|
||||
#[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)
|
||||
}
|
||||
}
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
> **NOTE**: 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 `ValidationResult` which contains only [fields][6] provided by the function.
|
||||
|
||||
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:
|
||||
```graphql
|
||||
{
|
||||
mutation {
|
||||
addItem {
|
||||
... on Item {
|
||||
name
|
||||
}
|
||||
... on ValidationErrorsItem {
|
||||
name
|
||||
quantity
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Example: Complex with critical errors
|
||||
|
||||
Our examples so far have only included non-critical errors. Providing errors inside a [GraphQL schema][8] still allows us to return unexpected critical errors when they occur.
|
||||
|
||||
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 [critical error][1]:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::{graphql_object, graphql_value, FieldError, GraphQLObject, GraphQLUnion, ScalarValue};
|
||||
#
|
||||
#[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))
|
||||
}
|
||||
}
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
|
||||
### Example: Shopify API
|
||||
|
||||
The [Shopify API] implements a similar approach. Their API is a good reference to explore this approach in a real world application.
|
||||
|
||||
|
||||
### Example: Non-struct [objects][10]
|
||||
|
||||
Up until now, we've only looked at mapping [structs][20] to [GraphQL objects][10]. However, any [Rust] type can be exposed a [GraphQL object][10].
|
||||
|
||||
Using `Result`-like [enums][1] can be a useful way of reporting validation errors from a mutation:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::{graphql_object, GraphQLObject};
|
||||
#
|
||||
#[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())
|
||||
}
|
||||
}
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
Here, we use an [enum][21] to decide whether a client's input data is valid or not, and it could be used as the result of e.g. a `signUp` mutation:
|
||||
```graphql
|
||||
{
|
||||
mutation {
|
||||
signUp(name: "wrong") {
|
||||
user {
|
||||
name
|
||||
}
|
||||
error {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
[GraphQL]: https://graphql.org
|
||||
[Juniper]: https://docs.rs/juniper
|
||||
[Rust]: https://www.rust-lang.org
|
||||
[Shopify API]: https://shopify.dev/docs/admin-api/graphql/reference
|
||||
|
||||
[1]: https://spec.graphql.org/October2021#sec-Errors.Field-errors
|
||||
[4]: https://spec.graphql.org/October2021#sec-Language.Arguments
|
||||
[5]: https://spec.graphql.org/October2021#sec-String
|
||||
[6]: https://spec.graphql.org/October2021#sec-Language.Fields
|
||||
[7]: https://spec.graphql.org/October2021#sec-Types
|
||||
[8]: https://graphql.org/learn/schema
|
||||
[9]: https://spec.graphql.org/October2021#sec-Unions
|
||||
[10]: https://spec.graphql.org/October2021#sec-Objects
|
||||
[20]: https://doc.rust-lang.org/reference/items/structs.html
|
||||
[21]: https://doc.rust-lang.org/reference/items/enumerations.html
|
78
book/src/types/objects/generics.md
Normal file
|
@ -0,0 +1,78 @@
|
|||
Generics
|
||||
========
|
||||
|
||||
Yet another point where [GraphQL] and [Rust] differs is in how generics work:
|
||||
- In [Rust], almost any type could be generic - that is, take type parameters.
|
||||
- In [GraphQL], there are only two generic types: [lists][1] and [non-`null`ables][2].
|
||||
|
||||
This poses a restriction on what we can expose in [GraphQL] from [Rust]: no generic structs can be exposed - all type parameters must be bound. For example, we cannot expose `Result<T, E>` as a [GraphQL type][0], but we _can_ expose `Result<User, String>` as a [GraphQL type][0].
|
||||
|
||||
Let's make a slightly more compact but generic implementation of [the last schema error example](error/schema.md#example-non-struct-objects):
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::{graphql_object, GraphQLObject};
|
||||
#
|
||||
#[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)
|
||||
}
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
Here, we've made a wrapper around a `Result` and exposed some concrete instantiations of `Result<T, E>` as distinct [GraphQL objects][3].
|
||||
|
||||
> **NOTE**: The reason we needed the wrapper is of [Rust]'s [orphan rules][10] (both the `Result` and [Juniper]'s internal traits are from third-party sources).
|
||||
|
||||
> **NOTE**: Because we're using generics, we also need to specify a `name` for our instantiated [GraphQL types][0]. Even if [Juniper] _could_ figure out the name, `MutationResult<User>` wouldn't be a [valid GraphQL type name][4]. And, also, two different [GraphQL types][0] cannot have the same `MutationResult` name, inferred by default.
|
||||
|
||||
|
||||
|
||||
|
||||
[GraphQL]: https://graphql.org
|
||||
[Juniper]: https://docs.rs/juniper
|
||||
[Rust]: https://www.rust-lang.org
|
||||
|
||||
[0]: https://spec.graphql.org/October2021#sec-Types
|
||||
[1]: https://spec.graphql.org/October2021#sec-List
|
||||
[2]: https://spec.graphql.org/October2021#sec-Non-Null
|
||||
[3]: https://spec.graphql.org/October2021#sec-Objects
|
||||
[4]: https://spec.graphql.org/October2021#sec-Names
|
||||
[10]: https://doc.rust-lang.org/reference/items/implementations.html#trait-implementation-coherence
|
228
book/src/types/objects/index.md
Normal file
|
@ -0,0 +1,228 @@
|
|||
Objects
|
||||
=======
|
||||
|
||||
> [GraphQL objects][0] represent a list of named fields, each of which yield a value of a specific type.
|
||||
|
||||
When declaring a [GraphQL schema][schema], most of the time we deal with [GraphQL objects][0], because they are the only place where we actually define the behavior once [schema] gets [executed][1].
|
||||
|
||||
There are two ways to define a [GraphQL object][0] in [Juniper]:
|
||||
1. The easiest way, suitable for trivial cases, is to use the [`#[derive(GraphQLObject)]` attribute][2] on a [struct], as described below.
|
||||
2. The other way, using the [`#[graphql_object]` attribute][3], is described in the ["Complex fields" chapter](complex_fields.md).
|
||||
|
||||
|
||||
|
||||
|
||||
## Trivial
|
||||
|
||||
While any type in [Rust] can be exposed as a [GraphQL object][0], the most common one is a [struct]:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::GraphQLObject;
|
||||
#
|
||||
#[derive(GraphQLObject)]
|
||||
struct Person {
|
||||
name: String,
|
||||
age: i32,
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
This creates a [GraphQL object][0] type called `Person`, with two fields: `name` of type `String!`, and `age` of type `Int!`. Because of [Rust]'s type system, everything is exported as [non-`null`][4] by default.
|
||||
|
||||
> **TIP**: If a `null`able field is required, the most obvious way is to use `Option`. Or [`Nullable`] for distinguishing between [explicit and implicit `null`s][14].
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
We should take advantage of the fact that [GraphQL] is [self-documenting][5] and add descriptions to the defined [GraphQL object][0] type and its fields. [Juniper] will automatically use associated [Rust doc comments][6] as [GraphQL descriptions][7]:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::GraphQLObject;
|
||||
#
|
||||
/// 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,
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
If using [Rust doc comments][6] is not desired (for example, when we want to keep [Rust] API docs and GraphQL schema descriptions different), the `#[graphql(description = "...")]` attribute can be used instead, which takes precedence over [Rust doc comments][6]:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::GraphQLObject;
|
||||
#
|
||||
/// 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,
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
|
||||
### Renaming
|
||||
|
||||
By default, [struct] fields are converted from [Rust]'s standard `snake_case` naming convention into [GraphQL]'s `camelCase` convention:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::GraphQLObject;
|
||||
#
|
||||
#[derive(GraphQLObject)]
|
||||
struct Person {
|
||||
first_name: String, // exposed as `firstName` in GraphQL schema
|
||||
last_name: String, // exposed as `lastName` in GraphQL schema
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
We can override the name by using the `#[graphql(name = "...")]` attribute:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::GraphQLObject;
|
||||
#
|
||||
#[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
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
Or provide a different renaming policy for all the [struct] fields:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::GraphQLObject;
|
||||
#
|
||||
#[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
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `camelCase` and `none` (disables any renaming).
|
||||
|
||||
|
||||
### Deprecation
|
||||
|
||||
To [deprecate][9] a [GraphQL object][0] field, either the `#[graphql(deprecated = "...")]` attribute, or [Rust's `#[deprecated]` attribute][13], should be used:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::GraphQLObject;
|
||||
#
|
||||
#[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,
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
> **NOTE**: Only [GraphQL object][0]/[interface][11] fields and [GraphQL enum][10] values can be [deprecated][9].
|
||||
|
||||
|
||||
### Ignoring
|
||||
|
||||
By default, all [struct] fields are included into the generated [GraphQL object][0] type. To prevent inclusion of a specific field annotate it with the `#[graphql(ignore)]` attribute:
|
||||
```rust
|
||||
# #![allow(dead_code)]
|
||||
# extern crate juniper;
|
||||
# use juniper::GraphQLObject;
|
||||
#
|
||||
#[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
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
> **TIP**: See more available features in the API docs of the [`#[derive(GraphQLObject)]`][2] attribute.
|
||||
|
||||
|
||||
|
||||
|
||||
## Relationships
|
||||
|
||||
[GraphQL object][0] fields can be of any [GraphQL] type, except [input objects][8].
|
||||
|
||||
Let's see what it means to build relationships between [objects][0]:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::GraphQLObject;
|
||||
#
|
||||
#[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!]!`
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
Because `Person` is a valid [GraphQL] type, we can have a `Vec<Person>` in a [struct], and it'll be automatically converted into a [list][12] of [non-`null`able][4] `Person` [objects][0].
|
||||
|
||||
|
||||
|
||||
|
||||
[`Nullable`]: https://docs.rs/juniper/0.16.1/juniper/enum.Nullable.html
|
||||
[GraphQL]: https://graphql.org
|
||||
[Juniper]: https://docs.rs/juniper
|
||||
[Rust]: https://www.rust-lang.org
|
||||
[schema]: https://graphql.org/learn/schema
|
||||
[struct]: https://doc.rust-lang.org/reference/items/structs.html
|
||||
|
||||
[0]: https://spec.graphql.org/October2021#sec-Objects
|
||||
[1]: https://spec.graphql.org/October2021#sec-Execution
|
||||
[2]: https://docs.rs/juniper/0.16.1/juniper/derive.GraphQLObject.html
|
||||
[3]: https://docs.rs/juniper/0.16.1/juniper/attr.graphql_object.html
|
||||
[4]: https://spec.graphql.org/October2021#sec-Non-Null
|
||||
[5]: https://spec.graphql.org/October2021#sec-Introspection
|
||||
[6]: https://doc.rust-lang.org/reference/comments.html#doc-comments
|
||||
[7]: https://spec.graphql.org/October2021#sec-Descriptions
|
||||
[8]: https://spec.graphql.org/October2021#sec-Input-Objects
|
||||
[9]: https://spec.graphql.org/October2021#sec--deprecated
|
||||
[10]: https://spec.graphql.org/October2021#sec-Enums
|
||||
[11]: https://spec.graphql.org/October2021#sec-Interfaces
|
||||
[12]: https://spec.graphql.org/October2021#sec-List
|
||||
[13]: https://doc.rust-lang.org/reference/attributes/diagnostics.html#the-deprecated-attribute
|
||||
[14]: https://spec.graphql.org/October2021#sel-EAFdRDHAAEJDAoBxzT
|
482
book/src/types/scalars.md
Normal file
|
@ -0,0 +1,482 @@
|
|||
Scalars
|
||||
=======
|
||||
|
||||
[GraphQL scalars][0] represent primitive leaf values in a GraphQL type system: numbers, strings, and booleans.
|
||||
|
||||
|
||||
|
||||
|
||||
## Built-in
|
||||
|
||||
[Juniper] provides support for all the [built-in scalars][5].
|
||||
|
||||
| [Rust] types | [GraphQL] scalar |
|
||||
|------------------|------------------|
|
||||
| `bool` | `Boolean` |
|
||||
| `i32` | `Int` |
|
||||
| `f64` | `Float` |
|
||||
| `String`, `&str` | `String` |
|
||||
| `juniper::ID` | [`ID`] |
|
||||
|
||||
> **NOTE**: [`ID`] scalar is [defined in the GraphQL spec][`ID`] as a type that is serialized as a string, but can be parsed from both a string and an integer.
|
||||
|
||||
> **TIP**: There is no built-in support for `i64`, `u64`, or other [Rust] integer types, as the [GraphQL spec doesn't define any built-in scalars for them][1] by default. Instead, to be supported, they should be defined as [custom scalars](#custom) in a [GraphQL schema][schema].
|
||||
|
||||
|
||||
|
||||
|
||||
## Custom
|
||||
|
||||
We can create [custom scalars][2] for other primitive values, but they are still [limited in the data types for representation][1], and only introduce additional semantic meaning. This, also, often requires coordination with the client library, intended to consume the API we're building.
|
||||
|
||||
[Custom scalars][2] can be defined in [Juniper] by using either [`#[derive(GraphQLScalar)]`][8] or [`#[graphql_scalar]`][9] attributes, which do work pretty much the same way (except, [`#[derive(GraphQLScalar)]`][8] cannot be used on [type aliases][4]).
|
||||
|
||||
|
||||
### Transparent delegation
|
||||
|
||||
Quite often, we want to create a [custom GraphQL scalar][2] type by just wrapping an existing one, inheriting all its behavior. In [Rust], this is often called as ["newtype pattern"][3]. This may be achieved by providing a `#[graphql(transparent)]` attribute to the definition:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::{graphql_scalar, GraphQLScalar};
|
||||
#
|
||||
#[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,
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
That's it, now the `UserId` and `MessageId` [scalars][0] can be used in [GraphQL schema][schema].
|
||||
|
||||
We may also customize the definition, to provide more information about our [custom scalar][2] in [GraphQL schema][schema]:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::GraphQLScalar;
|
||||
#
|
||||
/// 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);
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
|
||||
### Resolving
|
||||
|
||||
In case we need to customize [resolving][7] of a [custom GraphQL scalar][2] value (change the way it gets executed), the `#[graphql(to_output_with = <fn path>)]` attribute is the way to do so:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::{GraphQLScalar, ScalarValue, Value};
|
||||
#
|
||||
#[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)
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
|
||||
### Input value parsing
|
||||
|
||||
Customization of a [custom GraphQL scalar][2] value parsing is possible via `#[graphql(from_input_with = <fn path>)]` attribute:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::{GraphQLScalar, InputValue, ScalarValue};
|
||||
#
|
||||
#[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()))
|
||||
}
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
|
||||
### Token parsing
|
||||
|
||||
Customization of which tokens a [custom GraphQL scalar][0] type should be parsed from, is possible via `#[graphql(parse_token_with = <fn path>)]` or `#[graphql(parse_token(<types>)]` attributes:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::{
|
||||
# GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue,
|
||||
# ScalarValue, ScalarToken, Value,
|
||||
# };
|
||||
#
|
||||
#[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))
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
> **NOTE**: Once we provide all 3 custom functions, there is no sense to follow [newtype pattern][3] anymore, as nothing left to inherit.
|
||||
|
||||
|
||||
### Full behavior
|
||||
|
||||
Instead of providing all custom functions separately, it's possible to provide a module holding the appropriate `to_output()`, `from_input()` and `parse_token()` functions via `#[graphql(with = <module path>)]` attribute:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::{
|
||||
# GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue,
|
||||
# ScalarValue, ScalarToken, Value,
|
||||
# };
|
||||
#
|
||||
#[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))
|
||||
}
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
A regular `impl` block is also suitable for that:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::{
|
||||
# GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue,
|
||||
# ScalarValue, ScalarToken, Value,
|
||||
# };
|
||||
#
|
||||
#[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))
|
||||
}
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
At the same time, any custom function still may be specified separately, if required:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::{
|
||||
# GraphQLScalar, InputValue, ParseScalarResult, ScalarValue,
|
||||
# ScalarToken, Value
|
||||
# };
|
||||
#
|
||||
#[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.
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
> **TIP**: See more available features in the API docs of the [`#[derive(GraphQLScalar)]`][8] and [`#[graphql_scalar]`][9] attributes.
|
||||
|
||||
|
||||
|
||||
|
||||
## Foreign
|
||||
|
||||
For implementing [custom scalars][2] on foreign types there is [`#[graphql_scalar]`][9] attribute.
|
||||
|
||||
> **NOTE**: To satisfy [orphan rules], we should provide a local [`ScalarValue`] implementation.
|
||||
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# mod date {
|
||||
# pub struct Date;
|
||||
# impl std::str::FromStr for Date {
|
||||
# type Err = String;
|
||||
#
|
||||
# fn from_str(_value: &str) -> Result<Self, Self::Err> {
|
||||
# unimplemented!()
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# impl std::fmt::Display for Date {
|
||||
# fn fmt(&self, _f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
# unimplemented!()
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# use juniper::DefaultScalarValue as CustomScalarValue;
|
||||
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}")))
|
||||
}
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
|
||||
### Supported out-of-the-box
|
||||
|
||||
[Juniper] provides out-of-the-box [GraphQL scalar][0] implementations for some very common [Rust] crates. The types from these crates will be usable in your schemas automatically after enabling the correspondent self-titled [Cargo feature].
|
||||
|
||||
| [Rust] type | [GraphQL] scalar | [Cargo feature] |
|
||||
|-----------------------------|-----------------------|------------------|
|
||||
| [`bigdecimal::BigDecimal`] | `BigDecimal` | [`bigdecimal`] |
|
||||
| [`bson::oid::ObjectId`] | [`ObjectID`] | [`bson`] |
|
||||
| [`bson::DateTime`] | [`DateTime`] | [`bson`] |
|
||||
| [`chrono::NaiveDate`] | [`LocalDate`] | [`chrono`] |
|
||||
| [`chrono::NaiveTime`] | [`LocalTime`] | [`chrono`] |
|
||||
| [`chrono::NaiveDateTime`] | [`LocalDateTime`] | [`chrono`] |
|
||||
| [`chrono::DateTime`] | [`DateTime`] | [`chrono`] |
|
||||
| [`chrono_tz::Tz`] | [`TimeZone`] | [`chrono-tz`] |
|
||||
| [`rust_decimal::Decimal`] | `Decimal` | [`rust_decimal`] |
|
||||
| [`jiff::civil::Date`] | [`LocalDate`] | [`jiff`] |
|
||||
| [`jiff::civil::Time`] | [`LocalTime`] | [`jiff`] |
|
||||
| [`jiff::civil::DateTime`] | [`LocalDateTime`] | [`jiff`] |
|
||||
| [`jiff::Timestamp`] | [`DateTime`] | [`jiff`] |
|
||||
| [`jiff::Zoned`] | `ZonedDateTime` | [`jiff`] |
|
||||
| [`jiff::tz::TimeZone`] | `TimeZoneOrUtcOffset` | [`jiff`] |
|
||||
| [`jiff::tz::TimeZone`] via [`juniper::integrations::jiff::TimeZone`] | [`TimeZone`] | [`jiff`] |
|
||||
| [`jiff::tz::Offset`] | [`UtcOffset`] | [`jiff`] |
|
||||
| [`jiff::Span`] | [`Duration`] | [`jiff`] |
|
||||
| [`time::Date`] | [`LocalDate`] | [`time`] |
|
||||
| [`time::Time`] | [`LocalTime`] | [`time`] |
|
||||
| [`time::PrimitiveDateTime`] | [`LocalDateTime`] | [`time`] |
|
||||
| [`time::OffsetDateTime`] | [`DateTime`] | [`time`] |
|
||||
| [`time::UtcOffset`] | [`UtcOffset`] | [`time`] |
|
||||
| [`url::Url`] | [`URL`] | [`url`] |
|
||||
| [`uuid::Uuid`] | [`UUID`] | [`uuid`] |
|
||||
|
||||
|
||||
|
||||
|
||||
[`bigdecimal`]: https://docs.rs/bigdecimal
|
||||
[`bigdecimal::BigDecimal`]: https://docs.rs/bigdecimal/latest/bigdecimal/struct.BigDecimal.html
|
||||
[`bson`]: https://docs.rs/bson
|
||||
[`bson::DateTime`]: https://docs.rs/bson/latest/bson/struct.DateTime.html
|
||||
[`bson::oid::ObjectId`]: https://docs.rs/bson/latest/bson/oid/struct.ObjectId.html
|
||||
[`chrono`]: https://docs.rs/chrono
|
||||
[`chrono::DateTime`]: https://docs.rs/chrono/latest/chrono/struct.DateTime.html
|
||||
[`chrono::NaiveDate`]: https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDate.html
|
||||
[`chrono::NaiveDateTime`]: https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDateTime.html
|
||||
[`chrono::NaiveTime`]: https://docs.rs/chrono/latest/chrono/naive/struct.NaiveTime.html
|
||||
[`chrono-tz`]: https://docs.rs/chrono-tz
|
||||
[`chrono_tz::Tz`]: https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html
|
||||
[`DateTime`]: https://graphql-scalars.dev/docs/scalars/date-time
|
||||
[`Duration`]: https://graphql-scalars.dev/docs/scalars/duration
|
||||
[`ID`]: https://spec.graphql.org/October2021#sec-ID
|
||||
[`jiff`]: https://docs.rs/jiff
|
||||
[`jiff::civil::Date`]: https://docs.rs/jiff/latest/jiff/civil/struct.Date.html
|
||||
[`jiff::civil::DateTime`]: https://docs.rs/jiff/latest/jiff/civil/struct.DateTime.html
|
||||
[`jiff::civil::Time`]: https://docs.rs/jiff/latest/jiff/civil/struct.Time.html
|
||||
[`jiff::Span`]: https://docs.rs/jiff/latest/jiff/struct.Span.html
|
||||
[`jiff::Timestamp`]: https://docs.rs/jiff/latest/jiff/struct.Timestamp.html
|
||||
[`jiff::tz::Offset`]: https://docs.rs/jiff/latest/jiff/tz/struct.Offset.html
|
||||
[`jiff::tz::TimeZone`]: https://docs.rs/jiff/latest/jiff/tz/struct.TimeZone.html
|
||||
[`jiff::Zoned`]: https://docs.rs/jiff/latest/jiff/struct.Zoned.html
|
||||
[`juniper::integrations::jiff::TimeZone`]: https://docs.rs/juniper/0.16.1/juniper/integrations/jiff/struct.TimeZone.html
|
||||
[`LocalDate`]: https://graphql-scalars.dev/docs/scalars/local-date
|
||||
[`LocalDateTime`]: https://graphql-scalars.dev/docs/scalars/local-date-time
|
||||
[`LocalTime`]: https://graphql-scalars.dev/docs/scalars/local-time
|
||||
[`ObjectID`]: https://the-guild.dev/graphql/scalars/docs/scalars/object-id
|
||||
[`rust_decimal`]: https://docs.rs/rust_decimal
|
||||
[`rust_decimal::Decimal`]: https://docs.rs/rust_decimal/latest/rust_decimal/struct.Decimal.html
|
||||
[`ScalarValue`]: https://docs.rs/juniper/0.16.1/juniper/trait.ScalarValue.html
|
||||
[`serde`]: https://docs.rs/serde
|
||||
[`time`]: https://docs.rs/time
|
||||
[`time::Date`]: https://docs.rs/time/latest/time/struct.Date.html
|
||||
[`time::PrimitiveDateTime`]: https://docs.rs/time/latest/time/struct.PrimitiveDateTime.html
|
||||
[`time::Time`]: https://docs.rs/time/latest/time/struct.Time.html
|
||||
[`time::UtcOffset`]: https://docs.rs/time/latest/time/struct.UtcOffset.html
|
||||
[`time::OffsetDateTime`]: https://docs.rs/time/latest/time/struct.OffsetDateTime.html
|
||||
[`TimeZone`]: https://graphql-scalars.dev/docs/scalars/time-zone
|
||||
[`url`]: https://docs.rs/url
|
||||
[`url::Url`]: https://docs.rs/url/latest/url/struct.Url.html
|
||||
[`URL`]: https://graphql-scalars.dev/docs/scalars/url
|
||||
[`UtcOffset`]: https://graphql-scalars.dev/docs/scalars/utc-offset
|
||||
[`uuid`]: https://docs.rs/uuid
|
||||
[`uuid::Uuid`]: https://docs.rs/uuid/latest/uuid/struct.Uuid.html
|
||||
[`UUID`]: https://graphql-scalars.dev/docs/scalars/uuid
|
||||
[Cargo feature]: https://doc.rust-lang.org/cargo/reference/features.html
|
||||
[GraphQL]: https://graphql.org
|
||||
[Juniper]: https://docs.rs/juniper
|
||||
[orphan rules]: https://doc.rust-lang.org/reference/items/implementations.html#orphan-rules
|
||||
[Rust]: https://www.rust-lang.org
|
||||
[schema]: https://graphql.org/learn/schema
|
||||
|
||||
[0]: https://spec.graphql.org/October2021#sec-Scalars
|
||||
[1]: https://spec.graphql.org/October2021#sel-FAHXJDCAACKB1qb
|
||||
[2]: https://spec.graphql.org/October2021#sec-Scalars.Custom-Scalars
|
||||
[3]: https://rust-unofficial.github.io/patterns/patterns/behavioural/newtype.html
|
||||
[4]: https://doc.rust-lang.org/reference/items/type-aliases.html
|
||||
[5]: https://spec.graphql.org/October2021/#sec-Scalars.Built-in-Scalars
|
||||
[6]: https://serde.rs/container-attrs.html#transparent
|
||||
[7]: https://spec.graphql.org/October2021#sec-Value-Resolution
|
||||
[8]: https://docs.rs/juniper/0.16.1/juniper/derive.GraphQLScalar.html
|
||||
[9]: https://docs.rs/juniper/0.16.1/juniper/attr.graphql_scalar.html
|
168
book/src/types/unions.md
Normal file
|
@ -0,0 +1,168 @@
|
|||
Unions
|
||||
======
|
||||
|
||||
> [GraphQL unions][0] represent an object that could be one of a list of [GraphQL object][10] types, but provides for no guaranteed fields between those types. They also differ from [interfaces][12] in that [object][10] types declare what [interfaces][12] they implement, but are not aware of what [unions][0] contain them.
|
||||
|
||||
From the server's point of view, [GraphQL unions][0] are somewhat similar to [interfaces][12]: the main difference is that they don't contain fields on their own, and so, we only need to represent a value, _dispatchable_ into concrete [objects][10].
|
||||
|
||||
Obviously, the most straightforward approach to express [GraphQL unions][0] in [Rust] is to use [enums][22]. In [Juniper] this may be done by using [`#[derive(GraphQLInterface)]`][2] attribute on them:
|
||||
```rust
|
||||
# extern crate derive_more;
|
||||
# extern crate juniper;
|
||||
# use derive_more::From;
|
||||
# use juniper::{GraphQLObject, GraphQLUnion};
|
||||
#
|
||||
#[derive(GraphQLObject)]
|
||||
struct Human {
|
||||
id: String,
|
||||
home_planet: String,
|
||||
}
|
||||
|
||||
#[derive(GraphQLObject)]
|
||||
struct Droid {
|
||||
id: String,
|
||||
primary_function: String,
|
||||
}
|
||||
|
||||
#[derive(From, GraphQLUnion)]
|
||||
// ^^^^ only for convenience, and may be omitted
|
||||
enum Character {
|
||||
Human(Human),
|
||||
Droid(Droid),
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
|
||||
### Renaming
|
||||
|
||||
Just as with [renaming GraphQL objects](objects/index.md#renaming), we can override the default [union][0] name by using the `#[graphql(name = "...")]` attribute:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::{GraphQLObject, GraphQLUnion};
|
||||
#
|
||||
# #[derive(GraphQLObject)]
|
||||
# struct Human {
|
||||
# id: String,
|
||||
# home_planet: String,
|
||||
# }
|
||||
#
|
||||
# #[derive(GraphQLObject)]
|
||||
# struct Droid {
|
||||
# id: String,
|
||||
# primary_function: String,
|
||||
# }
|
||||
#
|
||||
#[derive(GraphQLUnion)]
|
||||
#[graphql(name = "CharacterUnion")]
|
||||
enum Character { // exposed as `CharacterUnion` in GraphQL schema
|
||||
Human(Human),
|
||||
Droid(Droid),
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
> **NOTE**: Unlike [Rust enum variants][22], [GraphQL union members][0] don't have any special names aside from the ones provided by [objects][10] themselves, and so, obviously, **cannot be renamed**.
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
Similarly to [documenting GraphQL objects](objects/index.md#documentation), we can [document][7] a [GraphQL union][0] via `#[graphql(description = "...")]` attribute or [Rust doc comments][6]:
|
||||
```rust
|
||||
# extern crate juniper;
|
||||
# use juniper::{GraphQLObject, GraphQLUnion};
|
||||
#
|
||||
# #[derive(GraphQLObject)]
|
||||
# struct Human {
|
||||
# id: String,
|
||||
# home_planet: String,
|
||||
# }
|
||||
#
|
||||
# #[derive(GraphQLObject)]
|
||||
# struct Droid {
|
||||
# id: String,
|
||||
# primary_function: String,
|
||||
# }
|
||||
#
|
||||
/// 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),
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
> **NOTE**: Unlike [Rust enum variants][22], [GraphQL union members][0] don't have any special constructors aside from the provided [objects][10] directly, and so, **cannot be [documented][7]**, but rather reuse [object descriptions][7] "as is".
|
||||
|
||||
|
||||
### Ignoring
|
||||
|
||||
In some rare situations we may want to omit exposing an [enum][22] variant in a [GraphQL schema][1]. [Similarly to GraphQL enums](enums.md#ignoring), we can just annotate the variant with the `#[graphql(ignore)]` attribute.
|
||||
|
||||
As an example, let's consider the situation where we need to bind some type parameter `T` for doing interesting type-level stuff in our resolvers. To achieve this we need to have `PhantomData<T>`, but we don't want it exposed in the GraphQL schema.
|
||||
|
||||
```rust
|
||||
# extern crate derive_more;
|
||||
# extern crate juniper;
|
||||
# use std::marker::PhantomData;
|
||||
# use derive_more::From;
|
||||
# use juniper::{GraphQLObject, GraphQLUnion};
|
||||
#
|
||||
#[derive(GraphQLObject)]
|
||||
struct Human {
|
||||
id: String,
|
||||
home_planet: String,
|
||||
}
|
||||
|
||||
#[derive(GraphQLObject)]
|
||||
struct Droid {
|
||||
id: String,
|
||||
primary_function: String,
|
||||
}
|
||||
|
||||
#[derive(From, GraphQLUnion)]
|
||||
enum Character<S> {
|
||||
Human(Human),
|
||||
Droid(Droid),
|
||||
#[from(ignore)]
|
||||
#[graphql(ignore)]
|
||||
// ^^^^^^ or `skip`, up to your preference
|
||||
_State(PhantomData<S>),
|
||||
}
|
||||
#
|
||||
# fn main() {}
|
||||
```
|
||||
> **WARNING**: It's the _library user's responsibility_ to ensure that ignored [enum][22] variant is **never** returned from resolvers, otherwise resolving the [GraphQL] query will **panic in runtime**.
|
||||
|
||||
> **TIP**: See more available features in the API docs of the [`#[derive(GraphQLUnion)]`][2] attribute.
|
||||
|
||||
|
||||
|
||||
|
||||
[GraphQL]: https://graphql.org
|
||||
[Juniper]: https://docs.rs/juniper
|
||||
[Rust]: https://www.rust-lang.org
|
||||
|
||||
[0]: https://spec.graphql.org/October2021#sec-Unions
|
||||
[1]: https://graphql.org/learn/schema
|
||||
[2]: https://docs.rs/juniper/0.16.1/juniper/derive.GraphQLUnion.html
|
||||
[6]: https://doc.rust-lang.org/reference/comments.html#doc-comments
|
||||
[7]: https://spec.graphql.org/October2021#sec-Descriptions
|
||||
[10]: https://spec.graphql.org/October2021#sec-Objects
|
||||
[11]: https://spec.graphql.org/October2021#sec-Enums
|
||||
[12]: https://spec.graphql.org/October2021#sec-Interfaces
|
||||
[22]: https://doc.rust-lang.org/reference/items/enumerations.html#enumerations
|
7
clipboard.min.js
vendored
604
css/chrome.css
|
@ -1,604 +0,0 @@
|
|||
/* CSS for UI elements (a.k.a. chrome) */
|
||||
|
||||
html {
|
||||
scrollbar-color: var(--scrollbar) var(--bg);
|
||||
}
|
||||
#searchresults a,
|
||||
.content a:link,
|
||||
a:visited,
|
||||
a > .hljs {
|
||||
color: var(--links);
|
||||
}
|
||||
|
||||
/*
|
||||
body-container is necessary because mobile browsers don't seem to like
|
||||
overflow-x on the body tag when there is a <meta name="viewport"> tag.
|
||||
*/
|
||||
#body-container {
|
||||
/*
|
||||
This is used when the sidebar pushes the body content off the side of
|
||||
the screen on small screens. Without it, dragging on mobile Safari
|
||||
will want to reposition the viewport in a weird way.
|
||||
*/
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
/* Menu Bar */
|
||||
|
||||
#menu-bar,
|
||||
#menu-bar-hover-placeholder {
|
||||
z-index: 101;
|
||||
margin: auto calc(0px - var(--page-padding));
|
||||
}
|
||||
#menu-bar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
background-color: var(--bg);
|
||||
border-block-end-color: var(--bg);
|
||||
border-block-end-width: 1px;
|
||||
border-block-end-style: solid;
|
||||
}
|
||||
#menu-bar.sticky,
|
||||
.js #menu-bar-hover-placeholder:hover + #menu-bar,
|
||||
.js #menu-bar:hover,
|
||||
.js.sidebar-visible #menu-bar {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 0 !important;
|
||||
}
|
||||
#menu-bar-hover-placeholder {
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
top: 0;
|
||||
height: var(--menu-bar-height);
|
||||
}
|
||||
#menu-bar.bordered {
|
||||
border-block-end-color: var(--table-border-color);
|
||||
}
|
||||
#menu-bar i, #menu-bar .icon-button {
|
||||
position: relative;
|
||||
padding: 0 8px;
|
||||
z-index: 10;
|
||||
line-height: var(--menu-bar-height);
|
||||
cursor: pointer;
|
||||
transition: color 0.5s;
|
||||
}
|
||||
@media only screen and (max-width: 420px) {
|
||||
#menu-bar i, #menu-bar .icon-button {
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
}
|
||||
.icon-button i {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.right-buttons {
|
||||
margin: 0 15px;
|
||||
}
|
||||
.right-buttons a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.left-buttons {
|
||||
display: flex;
|
||||
margin: 0 5px;
|
||||
}
|
||||
.no-js .left-buttons button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
display: inline-block;
|
||||
font-weight: 200;
|
||||
font-size: 2.4rem;
|
||||
line-height: var(--menu-bar-height);
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.js .menu-title {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu-bar,
|
||||
.menu-bar:visited,
|
||||
.nav-chapters,
|
||||
.nav-chapters:visited,
|
||||
.mobile-nav-chapters,
|
||||
.mobile-nav-chapters:visited,
|
||||
.menu-bar .icon-button,
|
||||
.menu-bar a i {
|
||||
color: var(--icons);
|
||||
}
|
||||
|
||||
.menu-bar i:hover,
|
||||
.menu-bar .icon-button:hover,
|
||||
.nav-chapters:hover,
|
||||
.mobile-nav-chapters i:hover {
|
||||
color: var(--icons-hover);
|
||||
}
|
||||
|
||||
/* Nav Icons */
|
||||
|
||||
.nav-chapters {
|
||||
font-size: 2.5em;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
max-width: 150px;
|
||||
min-width: 90px;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
transition: color 0.5s, background-color 0.5s;
|
||||
}
|
||||
|
||||
.nav-chapters:hover {
|
||||
text-decoration: none;
|
||||
background-color: var(--theme-hover);
|
||||
transition: background-color 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.nav-wrapper {
|
||||
margin-block-start: 50px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-nav-chapters {
|
||||
font-size: 2.5em;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
width: 90px;
|
||||
border-radius: 5px;
|
||||
background-color: var(--sidebar-bg);
|
||||
}
|
||||
|
||||
/* Only Firefox supports flow-relative values */
|
||||
.previous { float: left; }
|
||||
[dir=rtl] .previous { float: right; }
|
||||
|
||||
/* Only Firefox supports flow-relative values */
|
||||
.next {
|
||||
float: right;
|
||||
right: var(--page-padding);
|
||||
}
|
||||
[dir=rtl] .next {
|
||||
float: left;
|
||||
right: unset;
|
||||
left: var(--page-padding);
|
||||
}
|
||||
|
||||
/* Use the correct buttons for RTL layouts*/
|
||||
[dir=rtl] .previous i.fa-angle-left:before {content:"\f105";}
|
||||
[dir=rtl] .next i.fa-angle-right:before { content:"\f104"; }
|
||||
|
||||
@media only screen and (max-width: 1080px) {
|
||||
.nav-wide-wrapper { display: none; }
|
||||
.nav-wrapper { display: block; }
|
||||
}
|
||||
|
||||
/* sidebar-visible */
|
||||
@media only screen and (max-width: 1380px) {
|
||||
#sidebar-toggle-anchor:checked ~ .page-wrapper .nav-wide-wrapper { display: none; }
|
||||
#sidebar-toggle-anchor:checked ~ .page-wrapper .nav-wrapper { display: block; }
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
|
||||
:not(pre) > .hljs {
|
||||
display: inline;
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
:not(pre):not(a) > .hljs {
|
||||
color: var(--inline-code-color);
|
||||
overflow-x: initial;
|
||||
}
|
||||
|
||||
a:hover > .hljs {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
pre {
|
||||
position: relative;
|
||||
}
|
||||
pre > .buttons {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
right: 0px;
|
||||
top: 2px;
|
||||
margin: 0px;
|
||||
padding: 2px 0px;
|
||||
|
||||
color: var(--sidebar-fg);
|
||||
cursor: pointer;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: visibility 0.1s linear, opacity 0.1s linear;
|
||||
}
|
||||
pre:hover > .buttons {
|
||||
visibility: visible;
|
||||
opacity: 1
|
||||
}
|
||||
pre > .buttons :hover {
|
||||
color: var(--sidebar-active);
|
||||
border-color: var(--icons-hover);
|
||||
background-color: var(--theme-hover);
|
||||
}
|
||||
pre > .buttons i {
|
||||
margin-inline-start: 8px;
|
||||
}
|
||||
pre > .buttons button {
|
||||
cursor: inherit;
|
||||
margin: 0px 5px;
|
||||
padding: 3px 5px;
|
||||
font-size: 14px;
|
||||
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: 4px;
|
||||
border-color: var(--icons);
|
||||
background-color: var(--theme-popup-bg);
|
||||
transition: 100ms;
|
||||
transition-property: color,border-color,background-color;
|
||||
color: var(--icons);
|
||||
}
|
||||
@media (pointer: coarse) {
|
||||
pre > .buttons button {
|
||||
/* On mobile, make it easier to tap buttons. */
|
||||
padding: 0.3rem 1rem;
|
||||
}
|
||||
|
||||
.sidebar-resize-indicator {
|
||||
/* Hide resize indicator on devices with limited accuracy */
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
pre > code {
|
||||
display: block;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* FIXME: ACE editors overlap their buttons because ACE does absolute
|
||||
positioning within the code block which breaks padding. The only solution I
|
||||
can think of is to move the padding to the outer pre tag (or insert a div
|
||||
wrapper), but that would require fixing a whole bunch of CSS rules.
|
||||
*/
|
||||
.hljs.ace_editor {
|
||||
padding: 0rem 0rem;
|
||||
}
|
||||
|
||||
pre > .result {
|
||||
margin-block-start: 10px;
|
||||
}
|
||||
|
||||
/* Search */
|
||||
|
||||
#searchresults a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
mark {
|
||||
border-radius: 2px;
|
||||
padding-block-start: 0;
|
||||
padding-block-end: 1px;
|
||||
padding-inline-start: 3px;
|
||||
padding-inline-end: 3px;
|
||||
margin-block-start: 0;
|
||||
margin-block-end: -1px;
|
||||
margin-inline-start: -3px;
|
||||
margin-inline-end: -3px;
|
||||
background-color: var(--search-mark-bg);
|
||||
transition: background-color 300ms linear;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
mark.fade-out {
|
||||
background-color: rgba(0,0,0,0) !important;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.searchbar-outer {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
max-width: var(--content-max-width);
|
||||
}
|
||||
|
||||
#searchbar {
|
||||
width: 100%;
|
||||
margin-block-start: 5px;
|
||||
margin-block-end: 0;
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
padding: 10px 16px;
|
||||
transition: box-shadow 300ms ease-in-out;
|
||||
border: 1px solid var(--searchbar-border-color);
|
||||
border-radius: 3px;
|
||||
background-color: var(--searchbar-bg);
|
||||
color: var(--searchbar-fg);
|
||||
}
|
||||
#searchbar:focus,
|
||||
#searchbar.active {
|
||||
box-shadow: 0 0 3px var(--searchbar-shadow-color);
|
||||
}
|
||||
|
||||
.searchresults-header {
|
||||
font-weight: bold;
|
||||
font-size: 1em;
|
||||
padding-block-start: 18px;
|
||||
padding-block-end: 0;
|
||||
padding-inline-start: 5px;
|
||||
padding-inline-end: 0;
|
||||
color: var(--searchresults-header-fg);
|
||||
}
|
||||
|
||||
.searchresults-outer {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
max-width: var(--content-max-width);
|
||||
border-block-end: 1px dashed var(--searchresults-border-color);
|
||||
}
|
||||
|
||||
ul#searchresults {
|
||||
list-style: none;
|
||||
padding-inline-start: 20px;
|
||||
}
|
||||
ul#searchresults li {
|
||||
margin: 10px 0px;
|
||||
padding: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
ul#searchresults li.focus {
|
||||
background-color: var(--searchresults-li-bg);
|
||||
}
|
||||
ul#searchresults span.teaser {
|
||||
display: block;
|
||||
clear: both;
|
||||
margin-block-start: 5px;
|
||||
margin-block-end: 0;
|
||||
margin-inline-start: 20px;
|
||||
margin-inline-end: 0;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
ul#searchresults span.teaser em {
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: var(--sidebar-width);
|
||||
font-size: 0.875em;
|
||||
box-sizing: border-box;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior-y: contain;
|
||||
background-color: var(--sidebar-bg);
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
[dir=rtl] .sidebar { left: unset; right: 0; }
|
||||
.sidebar-resizing {
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.no-js .sidebar,
|
||||
.js:not(.sidebar-resizing) .sidebar {
|
||||
transition: transform 0.3s; /* Animation: slide away */
|
||||
}
|
||||
.sidebar code {
|
||||
line-height: 2em;
|
||||
}
|
||||
.sidebar .sidebar-scrollbox {
|
||||
overflow-y: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 10px 10px;
|
||||
}
|
||||
.sidebar .sidebar-resize-handle {
|
||||
position: absolute;
|
||||
cursor: col-resize;
|
||||
width: 0;
|
||||
right: calc(var(--sidebar-resize-indicator-width) * -1);
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sidebar-resize-handle .sidebar-resize-indicator {
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
background-color: var(--icons);
|
||||
margin-inline-start: var(--sidebar-resize-indicator-space);
|
||||
}
|
||||
|
||||
[dir=rtl] .sidebar .sidebar-resize-handle {
|
||||
left: calc(var(--sidebar-resize-indicator-width) * -1);
|
||||
right: unset;
|
||||
}
|
||||
.js .sidebar .sidebar-resize-handle {
|
||||
cursor: col-resize;
|
||||
width: calc(var(--sidebar-resize-indicator-width) - var(--sidebar-resize-indicator-space));
|
||||
}
|
||||
/* sidebar-hidden */
|
||||
#sidebar-toggle-anchor:not(:checked) ~ .sidebar {
|
||||
transform: translateX(calc(0px - var(--sidebar-width) - var(--sidebar-resize-indicator-width)));
|
||||
z-index: -1;
|
||||
}
|
||||
[dir=rtl] #sidebar-toggle-anchor:not(:checked) ~ .sidebar {
|
||||
transform: translateX(calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width)));
|
||||
}
|
||||
.sidebar::-webkit-scrollbar {
|
||||
background: var(--sidebar-bg);
|
||||
}
|
||||
.sidebar::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar);
|
||||
}
|
||||
|
||||
/* sidebar-visible */
|
||||
#sidebar-toggle-anchor:checked ~ .page-wrapper {
|
||||
transform: translateX(calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width)));
|
||||
}
|
||||
[dir=rtl] #sidebar-toggle-anchor:checked ~ .page-wrapper {
|
||||
transform: translateX(calc(0px - var(--sidebar-width) - var(--sidebar-resize-indicator-width)));
|
||||
}
|
||||
@media only screen and (min-width: 620px) {
|
||||
#sidebar-toggle-anchor:checked ~ .page-wrapper {
|
||||
transform: none;
|
||||
margin-inline-start: calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width));
|
||||
}
|
||||
[dir=rtl] #sidebar-toggle-anchor:checked ~ .page-wrapper {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.chapter {
|
||||
list-style: none outside none;
|
||||
padding-inline-start: 0;
|
||||
line-height: 2.2em;
|
||||
}
|
||||
|
||||
.chapter ol {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chapter li {
|
||||
display: flex;
|
||||
color: var(--sidebar-non-existant);
|
||||
}
|
||||
.chapter li a {
|
||||
display: block;
|
||||
padding: 0;
|
||||
text-decoration: none;
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
|
||||
.chapter li a:hover {
|
||||
color: var(--sidebar-active);
|
||||
}
|
||||
|
||||
.chapter li a.active {
|
||||
color: var(--sidebar-active);
|
||||
}
|
||||
|
||||
.chapter li > a.toggle {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
margin-inline-start: auto;
|
||||
padding: 0 10px;
|
||||
user-select: none;
|
||||
opacity: 0.68;
|
||||
}
|
||||
|
||||
.chapter li > a.toggle div {
|
||||
transition: transform 0.5s;
|
||||
}
|
||||
|
||||
/* collapse the section */
|
||||
.chapter li:not(.expanded) + li > ol {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.chapter li.chapter-item {
|
||||
line-height: 1.5em;
|
||||
margin-block-start: 0.6em;
|
||||
}
|
||||
|
||||
.chapter li.expanded > a.toggle div {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.spacer {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
margin: 5px 0px;
|
||||
}
|
||||
.chapter .spacer {
|
||||
background-color: var(--sidebar-spacer);
|
||||
}
|
||||
|
||||
@media (-moz-touch-enabled: 1), (pointer: coarse) {
|
||||
.chapter li a { padding: 5px 0; }
|
||||
.spacer { margin: 10px 0; }
|
||||
}
|
||||
|
||||
.section {
|
||||
list-style: none outside none;
|
||||
padding-inline-start: 20px;
|
||||
line-height: 1.9em;
|
||||
}
|
||||
|
||||
/* Theme Menu Popup */
|
||||
|
||||
.theme-popup {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: var(--menu-bar-height);
|
||||
z-index: 1000;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7em;
|
||||
color: var(--fg);
|
||||
background: var(--theme-popup-bg);
|
||||
border: 1px solid var(--theme-popup-border);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: none;
|
||||
/* Don't let the children's background extend past the rounded corners. */
|
||||
overflow: hidden;
|
||||
}
|
||||
[dir=rtl] .theme-popup { left: unset; right: 10px; }
|
||||
.theme-popup .default {
|
||||
color: var(--icons);
|
||||
}
|
||||
.theme-popup .theme {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 2px 20px;
|
||||
line-height: 25px;
|
||||
white-space: nowrap;
|
||||
text-align: start;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
background: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
.theme-popup .theme:hover {
|
||||
background-color: var(--theme-hover);
|
||||
}
|
||||
|
||||
.theme-selected::before {
|
||||
display: inline-block;
|
||||
content: "✓";
|
||||
margin-inline-start: -14px;
|
||||
width: 14px;
|
||||
}
|
232
css/general.css
|
@ -1,232 +0,0 @@
|
|||
/* Base styles and content styles */
|
||||
|
||||
:root {
|
||||
/* Browser default font-size is 16px, this way 1 rem = 10px */
|
||||
font-size: 62.5%;
|
||||
color-scheme: var(--color-scheme);
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
color: var(--fg);
|
||||
background-color: var(--bg);
|
||||
text-size-adjust: none;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-size: 1.6rem;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: var(--mono-font) !important;
|
||||
font-size: var(--code-font-size);
|
||||
direction: ltr !important;
|
||||
}
|
||||
|
||||
/* make long words/inline code not x overflow */
|
||||
main {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* make wide tables scroll if they overflow */
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* Don't change font size in headers. */
|
||||
h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
|
||||
font-size: unset;
|
||||
}
|
||||
|
||||
.left { float: left; }
|
||||
.right { float: right; }
|
||||
.boring { opacity: 0.6; }
|
||||
.hide-boring .boring { display: none; }
|
||||
.hidden { display: none !important; }
|
||||
|
||||
h2, h3 { margin-block-start: 2.5em; }
|
||||
h4, h5 { margin-block-start: 2em; }
|
||||
|
||||
.header + .header h3,
|
||||
.header + .header h4,
|
||||
.header + .header h5 {
|
||||
margin-block-start: 1em;
|
||||
}
|
||||
|
||||
h1:target::before,
|
||||
h2:target::before,
|
||||
h3:target::before,
|
||||
h4:target::before,
|
||||
h5:target::before,
|
||||
h6:target::before {
|
||||
display: inline-block;
|
||||
content: "»";
|
||||
margin-inline-start: -30px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
/* This is broken on Safari as of version 14, but is fixed
|
||||
in Safari Technology Preview 117 which I think will be Safari 14.2.
|
||||
https://bugs.webkit.org/show_bug.cgi?id=218076
|
||||
*/
|
||||
:target {
|
||||
/* Safari does not support logical properties */
|
||||
scroll-margin-top: calc(var(--menu-bar-height) + 0.5em);
|
||||
}
|
||||
|
||||
.page {
|
||||
outline: 0;
|
||||
padding: 0 var(--page-padding);
|
||||
margin-block-start: calc(0px - var(--menu-bar-height)); /* Compensate for the #menu-bar-hover-placeholder */
|
||||
}
|
||||
.page-wrapper {
|
||||
box-sizing: border-box;
|
||||
background-color: var(--bg);
|
||||
}
|
||||
.no-js .page-wrapper,
|
||||
.js:not(.sidebar-resizing) .page-wrapper {
|
||||
transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */
|
||||
}
|
||||
[dir=rtl] .js:not(.sidebar-resizing) .page-wrapper {
|
||||
transition: margin-right 0.3s ease, transform 0.3s ease; /* Animation: slide away */
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow-y: auto;
|
||||
padding: 0 5px 50px 5px;
|
||||
}
|
||||
.content main {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
max-width: var(--content-max-width);
|
||||
}
|
||||
.content p { line-height: 1.45em; }
|
||||
.content ol { line-height: 1.45em; }
|
||||
.content ul { line-height: 1.45em; }
|
||||
.content a { text-decoration: none; }
|
||||
.content a:hover { text-decoration: underline; }
|
||||
.content img, .content video { max-width: 100%; }
|
||||
.content .header:link,
|
||||
.content .header:visited {
|
||||
color: var(--fg);
|
||||
}
|
||||
.content .header:link,
|
||||
.content .header:visited:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
table {
|
||||
margin: 0 auto;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table td {
|
||||
padding: 3px 20px;
|
||||
border: 1px var(--table-border-color) solid;
|
||||
}
|
||||
table thead {
|
||||
background: var(--table-header-bg);
|
||||
}
|
||||
table thead td {
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
}
|
||||
table thead th {
|
||||
padding: 3px 20px;
|
||||
}
|
||||
table thead tr {
|
||||
border: 1px var(--table-header-bg) solid;
|
||||
}
|
||||
/* Alternate background colors for rows */
|
||||
table tbody tr:nth-child(2n) {
|
||||
background: var(--table-alternate-bg);
|
||||
}
|
||||
|
||||
|
||||
blockquote {
|
||||
margin: 20px 0;
|
||||
padding: 0 20px;
|
||||
color: var(--fg);
|
||||
background-color: var(--quote-bg);
|
||||
border-block-start: .1em solid var(--quote-border);
|
||||
border-block-end: .1em solid var(--quote-border);
|
||||
}
|
||||
|
||||
.warning {
|
||||
margin: 20px;
|
||||
padding: 0 20px;
|
||||
border-inline-start: 2px solid var(--warning-border);
|
||||
}
|
||||
|
||||
.warning:before {
|
||||
position: absolute;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
margin-inline-start: calc(-1.5rem - 21px);
|
||||
content: "ⓘ";
|
||||
text-align: center;
|
||||
background-color: var(--bg);
|
||||
color: var(--warning-border);
|
||||
font-weight: bold;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
blockquote .warning:before {
|
||||
background-color: var(--quote-bg);
|
||||
}
|
||||
|
||||
kbd {
|
||||
background-color: var(--table-border-color);
|
||||
border-radius: 4px;
|
||||
border: solid 1px var(--theme-popup-border);
|
||||
box-shadow: inset 0 -1px 0 var(--theme-hover);
|
||||
display: inline-block;
|
||||
font-size: var(--code-font-size);
|
||||
font-family: var(--mono-font);
|
||||
line-height: 10px;
|
||||
padding: 4px 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
:not(.footnote-definition) + .footnote-definition,
|
||||
.footnote-definition + :not(.footnote-definition) {
|
||||
margin-block-start: 2em;
|
||||
}
|
||||
.footnote-definition {
|
||||
font-size: 0.9em;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
.footnote-definition p {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.tooltiptext {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
color: #fff;
|
||||
background-color: #333;
|
||||
transform: translateX(-50%); /* Center by moving tooltip 50% of its width left */
|
||||
left: -8px; /* Half of the width of the icon */
|
||||
top: -35px;
|
||||
font-size: 0.8em;
|
||||
text-align: center;
|
||||
border-radius: 6px;
|
||||
padding: 5px 8px;
|
||||
margin: 5px;
|
||||
z-index: 1000;
|
||||
}
|
||||
.tooltipped .tooltiptext {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.chapter li.part-title {
|
||||
color: var(--sidebar-fg);
|
||||
margin: 5px 0px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.result-no-output {
|
||||
font-style: italic;
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
|
||||
#sidebar,
|
||||
#menu-bar,
|
||||
.nav-chapters,
|
||||
.mobile-nav-chapters {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#page-wrapper.page-wrapper {
|
||||
transform: none !important;
|
||||
margin-inline-start: 0px;
|
||||
overflow-y: initial;
|
||||
}
|
||||
|
||||
#content {
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page {
|
||||
overflow-y: initial;
|
||||
}
|
||||
|
||||
code {
|
||||
direction: ltr !important;
|
||||
}
|
||||
|
||||
pre > .buttons {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
a, a:visited, a:active, a:hover {
|
||||
color: #4183c4;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
page-break-inside: avoid;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
pre, code {
|
||||
page-break-inside: avoid;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.fa {
|
||||
display: none !important;
|
||||
}
|
|
@ -1,279 +0,0 @@
|
|||
|
||||
/* Globals */
|
||||
|
||||
:root {
|
||||
--sidebar-width: 300px;
|
||||
--sidebar-resize-indicator-width: 8px;
|
||||
--sidebar-resize-indicator-space: 2px;
|
||||
--page-padding: 15px;
|
||||
--content-max-width: 750px;
|
||||
--menu-bar-height: 50px;
|
||||
--mono-font: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace;
|
||||
--code-font-size: 0.875em /* please adjust the ace font size accordingly in editor.js */
|
||||
}
|
||||
|
||||
/* Themes */
|
||||
|
||||
.ayu {
|
||||
--bg: hsl(210, 25%, 8%);
|
||||
--fg: #c5c5c5;
|
||||
|
||||
--sidebar-bg: #14191f;
|
||||
--sidebar-fg: #c8c9db;
|
||||
--sidebar-non-existant: #5c6773;
|
||||
--sidebar-active: #ffb454;
|
||||
--sidebar-spacer: #2d334f;
|
||||
|
||||
--scrollbar: var(--sidebar-fg);
|
||||
|
||||
--icons: #737480;
|
||||
--icons-hover: #b7b9cc;
|
||||
|
||||
--links: #0096cf;
|
||||
|
||||
--inline-code-color: #ffb454;
|
||||
|
||||
--theme-popup-bg: #14191f;
|
||||
--theme-popup-border: #5c6773;
|
||||
--theme-hover: #191f26;
|
||||
|
||||
--quote-bg: hsl(226, 15%, 17%);
|
||||
--quote-border: hsl(226, 15%, 22%);
|
||||
|
||||
--warning-border: #ff8e00;
|
||||
|
||||
--table-border-color: hsl(210, 25%, 13%);
|
||||
--table-header-bg: hsl(210, 25%, 28%);
|
||||
--table-alternate-bg: hsl(210, 25%, 11%);
|
||||
|
||||
--searchbar-border-color: #848484;
|
||||
--searchbar-bg: #424242;
|
||||
--searchbar-fg: #fff;
|
||||
--searchbar-shadow-color: #d4c89f;
|
||||
--searchresults-header-fg: #666;
|
||||
--searchresults-border-color: #888;
|
||||
--searchresults-li-bg: #252932;
|
||||
--search-mark-bg: #e3b171;
|
||||
|
||||
--color-scheme: dark;
|
||||
}
|
||||
|
||||
.coal {
|
||||
--bg: hsl(200, 7%, 8%);
|
||||
--fg: #98a3ad;
|
||||
|
||||
--sidebar-bg: #292c2f;
|
||||
--sidebar-fg: #a1adb8;
|
||||
--sidebar-non-existant: #505254;
|
||||
--sidebar-active: #3473ad;
|
||||
--sidebar-spacer: #393939;
|
||||
|
||||
--scrollbar: var(--sidebar-fg);
|
||||
|
||||
--icons: #43484d;
|
||||
--icons-hover: #b3c0cc;
|
||||
|
||||
--links: #2b79a2;
|
||||
|
||||
--inline-code-color: #c5c8c6;
|
||||
|
||||
--theme-popup-bg: #141617;
|
||||
--theme-popup-border: #43484d;
|
||||
--theme-hover: #1f2124;
|
||||
|
||||
--quote-bg: hsl(234, 21%, 18%);
|
||||
--quote-border: hsl(234, 21%, 23%);
|
||||
|
||||
--warning-border: #ff8e00;
|
||||
|
||||
--table-border-color: hsl(200, 7%, 13%);
|
||||
--table-header-bg: hsl(200, 7%, 28%);
|
||||
--table-alternate-bg: hsl(200, 7%, 11%);
|
||||
|
||||
--searchbar-border-color: #aaa;
|
||||
--searchbar-bg: #b7b7b7;
|
||||
--searchbar-fg: #000;
|
||||
--searchbar-shadow-color: #aaa;
|
||||
--searchresults-header-fg: #666;
|
||||
--searchresults-border-color: #98a3ad;
|
||||
--searchresults-li-bg: #2b2b2f;
|
||||
--search-mark-bg: #355c7d;
|
||||
|
||||
--color-scheme: dark;
|
||||
}
|
||||
|
||||
.light {
|
||||
--bg: hsl(0, 0%, 100%);
|
||||
--fg: hsl(0, 0%, 0%);
|
||||
|
||||
--sidebar-bg: #fafafa;
|
||||
--sidebar-fg: hsl(0, 0%, 0%);
|
||||
--sidebar-non-existant: #aaaaaa;
|
||||
--sidebar-active: #1f1fff;
|
||||
--sidebar-spacer: #f4f4f4;
|
||||
|
||||
--scrollbar: #8F8F8F;
|
||||
|
||||
--icons: #747474;
|
||||
--icons-hover: #000000;
|
||||
|
||||
--links: #20609f;
|
||||
|
||||
--inline-code-color: #301900;
|
||||
|
||||
--theme-popup-bg: #fafafa;
|
||||
--theme-popup-border: #cccccc;
|
||||
--theme-hover: #e6e6e6;
|
||||
|
||||
--quote-bg: hsl(197, 37%, 96%);
|
||||
--quote-border: hsl(197, 37%, 91%);
|
||||
|
||||
--warning-border: #ff8e00;
|
||||
|
||||
--table-border-color: hsl(0, 0%, 95%);
|
||||
--table-header-bg: hsl(0, 0%, 80%);
|
||||
--table-alternate-bg: hsl(0, 0%, 97%);
|
||||
|
||||
--searchbar-border-color: #aaa;
|
||||
--searchbar-bg: #fafafa;
|
||||
--searchbar-fg: #000;
|
||||
--searchbar-shadow-color: #aaa;
|
||||
--searchresults-header-fg: #666;
|
||||
--searchresults-border-color: #888;
|
||||
--searchresults-li-bg: #e4f2fe;
|
||||
--search-mark-bg: #a2cff5;
|
||||
|
||||
--color-scheme: light;
|
||||
}
|
||||
|
||||
.navy {
|
||||
--bg: hsl(226, 23%, 11%);
|
||||
--fg: #bcbdd0;
|
||||
|
||||
--sidebar-bg: #282d3f;
|
||||
--sidebar-fg: #c8c9db;
|
||||
--sidebar-non-existant: #505274;
|
||||
--sidebar-active: #2b79a2;
|
||||
--sidebar-spacer: #2d334f;
|
||||
|
||||
--scrollbar: var(--sidebar-fg);
|
||||
|
||||
--icons: #737480;
|
||||
--icons-hover: #b7b9cc;
|
||||
|
||||
--links: #2b79a2;
|
||||
|
||||
--inline-code-color: #c5c8c6;
|
||||
|
||||
--theme-popup-bg: #161923;
|
||||
--theme-popup-border: #737480;
|
||||
--theme-hover: #282e40;
|
||||
|
||||
--quote-bg: hsl(226, 15%, 17%);
|
||||
--quote-border: hsl(226, 15%, 22%);
|
||||
|
||||
--warning-border: #ff8e00;
|
||||
|
||||
--table-border-color: hsl(226, 23%, 16%);
|
||||
--table-header-bg: hsl(226, 23%, 31%);
|
||||
--table-alternate-bg: hsl(226, 23%, 14%);
|
||||
|
||||
--searchbar-border-color: #aaa;
|
||||
--searchbar-bg: #aeaec6;
|
||||
--searchbar-fg: #000;
|
||||
--searchbar-shadow-color: #aaa;
|
||||
--searchresults-header-fg: #5f5f71;
|
||||
--searchresults-border-color: #5c5c68;
|
||||
--searchresults-li-bg: #242430;
|
||||
--search-mark-bg: #a2cff5;
|
||||
|
||||
--color-scheme: dark;
|
||||
}
|
||||
|
||||
.rust {
|
||||
--bg: hsl(60, 9%, 87%);
|
||||
--fg: #262625;
|
||||
|
||||
--sidebar-bg: #3b2e2a;
|
||||
--sidebar-fg: #c8c9db;
|
||||
--sidebar-non-existant: #505254;
|
||||
--sidebar-active: #e69f67;
|
||||
--sidebar-spacer: #45373a;
|
||||
|
||||
--scrollbar: var(--sidebar-fg);
|
||||
|
||||
--icons: #737480;
|
||||
--icons-hover: #262625;
|
||||
|
||||
--links: #2b79a2;
|
||||
|
||||
--inline-code-color: #6e6b5e;
|
||||
|
||||
--theme-popup-bg: #e1e1db;
|
||||
--theme-popup-border: #b38f6b;
|
||||
--theme-hover: #99908a;
|
||||
|
||||
--quote-bg: hsl(60, 5%, 75%);
|
||||
--quote-border: hsl(60, 5%, 70%);
|
||||
|
||||
--warning-border: #ff8e00;
|
||||
|
||||
--table-border-color: hsl(60, 9%, 82%);
|
||||
--table-header-bg: #b3a497;
|
||||
--table-alternate-bg: hsl(60, 9%, 84%);
|
||||
|
||||
--searchbar-border-color: #aaa;
|
||||
--searchbar-bg: #fafafa;
|
||||
--searchbar-fg: #000;
|
||||
--searchbar-shadow-color: #aaa;
|
||||
--searchresults-header-fg: #666;
|
||||
--searchresults-border-color: #888;
|
||||
--searchresults-li-bg: #dec2a2;
|
||||
--search-mark-bg: #e69f67;
|
||||
|
||||
--color-scheme: light;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.light.no-js {
|
||||
--bg: hsl(200, 7%, 8%);
|
||||
--fg: #98a3ad;
|
||||
|
||||
--sidebar-bg: #292c2f;
|
||||
--sidebar-fg: #a1adb8;
|
||||
--sidebar-non-existant: #505254;
|
||||
--sidebar-active: #3473ad;
|
||||
--sidebar-spacer: #393939;
|
||||
|
||||
--scrollbar: var(--sidebar-fg);
|
||||
|
||||
--icons: #43484d;
|
||||
--icons-hover: #b3c0cc;
|
||||
|
||||
--links: #2b79a2;
|
||||
|
||||
--inline-code-color: #c5c8c6;
|
||||
|
||||
--theme-popup-bg: #141617;
|
||||
--theme-popup-border: #43484d;
|
||||
--theme-hover: #1f2124;
|
||||
|
||||
--quote-bg: hsl(234, 21%, 18%);
|
||||
--quote-border: hsl(234, 21%, 23%);
|
||||
|
||||
--warning-border: #ff8e00;
|
||||
|
||||
--table-border-color: hsl(200, 7%, 13%);
|
||||
--table-header-bg: hsl(200, 7%, 28%);
|
||||
--table-alternate-bg: hsl(200, 7%, 11%);
|
||||
|
||||
--searchbar-border-color: #aaa;
|
||||
--searchbar-bg: #b7b7b7;
|
||||
--searchbar-fg: #000;
|
||||
--searchbar-shadow-color: #aaa;
|
||||
--searchresults-header-fg: #666;
|
||||
--searchresults-border-color: #98a3ad;
|
||||
--searchresults-li-bg: #2b2b2f;
|
||||
--search-mark-bg: #355c7d;
|
||||
}
|
||||
}
|
10
elasticlunr.min.js
vendored
BIN
favicon.png
Before Width: | Height: | Size: 5.5 KiB |
22
favicon.svg
|
@ -1,22 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 199.7 184.2">
|
||||
<style>
|
||||
@media (prefers-color-scheme: dark) {
|
||||
svg { fill: white; }
|
||||
}
|
||||
</style>
|
||||
<path d="M189.5,36.8c0.2,2.8,0,5.1-0.6,6.8L153,162c-0.6,2.1-2,3.7-4.2,5c-2.2,1.2-4.4,1.9-6.7,1.9H31.4c-9.6,0-15.3-2.8-17.3-8.4
|
||||
c-0.8-2.2-0.8-3.9,0.1-5.2c0.9-1.2,2.4-1.8,4.6-1.8H123c7.4,0,12.6-1.4,15.4-4.1s5.7-8.9,8.6-18.4l32.9-108.6
|
||||
c1.8-5.9,1-11.1-2.2-15.6S169.9,0,164,0H72.7c-1,0-3.1,0.4-6.1,1.1l0.1-0.4C64.5,0.2,62.6,0,61,0.1s-3,0.5-4.3,1.4
|
||||
c-1.3,0.9-2.4,1.8-3.2,2.8S52,6.5,51.2,8.1c-0.8,1.6-1.4,3-1.9,4.3s-1.1,2.7-1.8,4.2c-0.7,1.5-1.3,2.7-2,3.7c-0.5,0.6-1.2,1.5-2,2.5
|
||||
s-1.6,2-2.2,2.8s-0.9,1.5-1.1,2.2c-0.2,0.7-0.1,1.8,0.2,3.2c0.3,1.4,0.4,2.4,0.4,3.1c-0.3,3-1.4,6.9-3.3,11.6
|
||||
c-1.9,4.7-3.6,8.1-5.1,10.1c-0.3,0.4-1.2,1.3-2.6,2.7c-1.4,1.4-2.3,2.6-2.6,3.7c-0.3,0.4-0.3,1.5-0.1,3.4c0.3,1.8,0.4,3.1,0.3,3.8
|
||||
c-0.3,2.7-1.3,6.3-3,10.8c-1.7,4.5-3.4,8.2-5,11c-0.2,0.5-0.9,1.4-2,2.8c-1.1,1.4-1.8,2.5-2,3.4c-0.2,0.6-0.1,1.8,0.1,3.4
|
||||
c0.2,1.6,0.2,2.8-0.1,3.6c-0.6,3-1.8,6.7-3.6,11c-1.8,4.3-3.6,7.9-5.4,11c-0.5,0.8-1.1,1.7-2,2.8c-0.8,1.1-1.5,2-2,2.8
|
||||
s-0.8,1.6-1,2.5c-0.1,0.5,0,1.3,0.4,2.3c0.3,1.1,0.4,1.9,0.4,2.6c-0.1,1.1-0.2,2.6-0.5,4.4c-0.2,1.8-0.4,2.9-0.4,3.2
|
||||
c-1.8,4.8-1.7,9.9,0.2,15.2c2.2,6.2,6.2,11.5,11.9,15.8c5.7,4.3,11.7,6.4,17.8,6.4h110.7c5.2,0,10.1-1.7,14.7-5.2s7.7-7.8,9.2-12.9
|
||||
l33-108.6c1.8-5.8,1-10.9-2.2-15.5C194.9,39.7,192.6,38,189.5,36.8z M59.6,122.8L73.8,80c0,0,7,0,10.8,0s28.8-1.7,25.4,17.5
|
||||
c-3.4,19.2-18.8,25.2-36.8,25.4S59.6,122.8,59.6,122.8z M78.6,116.8c4.7-0.1,18.9-2.9,22.1-17.1S89.2,86.3,89.2,86.3l-8.9,0
|
||||
l-10.2,30.5C70.2,116.9,74,116.9,78.6,116.8z M75.3,68.7L89,26.2h9.8l0.8,34l23.6-34h9.9l-13.6,42.5h-7.1l12.5-35.4l-24.5,35.4h-6.8
|
||||
l-0.8-35L82,68.7H75.3z"/>
|
||||
</svg>
|
||||
<!-- Original image Copyright Dave Gandy — CC BY 4.0 License -->
|
Before Width: | Height: | Size: 1.8 KiB |
|
@ -1,202 +0,0 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -1,93 +0,0 @@
|
|||
Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
100
fonts/fonts.css
|
@ -1,100 +0,0 @@
|
|||
/* Open Sans is licensed under the Apache License, Version 2.0. See http://www.apache.org/licenses/LICENSE-2.0 */
|
||||
/* Source Code Pro is under the Open Font License. See https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL */
|
||||
|
||||
/* open-sans-300 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Open Sans Light'), local('OpenSans-Light'),
|
||||
url('open-sans-v17-all-charsets-300.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-300italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'),
|
||||
url('open-sans-v17-all-charsets-300italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-regular - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Open Sans Regular'), local('OpenSans-Regular'),
|
||||
url('open-sans-v17-all-charsets-regular.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: local('Open Sans Italic'), local('OpenSans-Italic'),
|
||||
url('open-sans-v17-all-charsets-italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-600 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'),
|
||||
url('open-sans-v17-all-charsets-600.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-600italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'),
|
||||
url('open-sans-v17-all-charsets-600italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-700 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Open Sans Bold'), local('OpenSans-Bold'),
|
||||
url('open-sans-v17-all-charsets-700.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-700italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'),
|
||||
url('open-sans-v17-all-charsets-700italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-800 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'),
|
||||
url('open-sans-v17-all-charsets-800.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-800italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'),
|
||||
url('open-sans-v17-all-charsets-800italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* source-code-pro-500 - latin_vietnamese_latin-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Source Code Pro';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: url('source-code-pro-v11-all-charsets-500.woff2') format('woff2');
|
||||
}
|