Using contexts
The context type is a feature in Juniper that lets field resolvers access global data, most commonly database connections or authentication information. The context is usually created from a context factory. How this is defined is specific to the framework integration you're using, so check out the documentation for Rocket integration.
In this chapter, we'll show you how to define a context type and use it in field
resolvers. Let's say that we have a simple user database in a HashMap
:
#![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 a friends
field on User
that returns a list of User
objects.
In order to write such a field though, the database must be queried.
To solve this, we mark the Database
as a valid context type and assign it to
the user object.
To gain access to the context, we need to specify an argument with the same
type as the specified Context
for the type:
extern crate juniper; use std::collections::HashMap; use juniper::graphql_object; // This struct represents our context. struct Database { users: HashMap<i32, User>, } // Mark the Database as a valid context type for Juniper impl juniper::Context for Database {} struct User { id: i32, name: String, friend_ids: Vec<i32>, } // Assign Database as the context type for User #[graphql_object(context = Database)] impl User { // Inject the context by specifying an argument with the context type. // Note: // - the type must be a reference // - the name of the argument SHOULD be `context` fn friends<'db>(&self, context: &'db Database) -> Vec<&'db User> { // Use the database to lookup users self.friend_ids.iter() .map(|id| context.users.get(id).expect("Could not find user with ID")) .collect() } fn name(&self) -> &str { self.name.as_str() } fn id(&self) -> i32 { self.id } } fn main() { }
You only get an immutable reference to the context, so if you want to affect
change to the execution, you'll need to use interior
mutability
using e.g. RwLock
or RefCell
.
Dealing with mutable references
Context cannot be specified by a mutable reference, because concurrent fields resolving may be performed. If you have something in your context that requires access by mutable reference, then you need to leverage the interior mutability for that.
For example, when using async runtime with work stealing (like tokio
), which obviously requires thread safety in addition, you will need to use a corresponding async version of RwLock
:
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(context=DatabaseContext)] impl User { async fn times_requested<'db>(&self, context: &'db DatabaseContext) -> i32 { // Acquire a mutable reference and await if async RwLock is used, // which is necessary if context consists async operations like // querying remote databases. // Obtain base type let DatabaseContext(context) = context; // If context is immutable use .read() on RwLock. let mut context = context.write().await; // Perform a mutable operation. context.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() { }
Replace tokio::sync::RwLock
with std::sync::RwLock
(or similar) if you don't intend to use async resolving.