juniper
juniper copied to clipboard
Write docs for using Context with a lifetime
Right now my context has a lifetime
use juniper::Context as JuniperContext;
use postgres::transaction::Transaction;
pub struct Context<'a> {
pub tx: Transaction<'a>,
}
impl<'a> JuniperContext for Context<'a> {}
impl<'a> Context<'a> {
pub fn new(tx: Transaction<'a>) -> Context<'a> {
Context { tx: tx }
}
}
But when I try to specify the same lifetime in a resolver then it gives me this error,
error[E0207]: the lifetime parameter `'a` is not constrained by the impl trait, self type, or predicates
--> src/schema/query.rs:7:18
|
7 | graphql_object!(<'a> QueryRoot: Context<'a> as "Query" |&self| {
| ^^ unconstrained lifetime parameter
use models::user::User;
use schema::context::Context;
pub struct QueryRoot;
graphql_object!(<'a> QueryRoot: Context<'a> as "Query" |&self| {
field users(&executor) -> FieldResult<Vec<User>> {
Ok(vec![])
}
});
Did you ever figure this out? I'm struggling with the same issue.
Nope. Its go something to do with the macro implementation and the executor. Basically the lifetime parameter needs to be specified at the resolver level context also. But its not being done.
https://github.com/graphql-rust/juniper/blob/master/juniper_codegen/src/derive_object.rs#L174
I don't know how we would be able to do that.
One Other solution I can think of is trying to implement the impl ::juniper::GraphQLType for #ident
trait yourself for the type and seeing if it works.
I have decided to not go with this approach and create a transaction only when needed in the mutation. So my context doesn't require a lifetime parameter anymore.
Hello, and sorry for the late answer.
As far as I know, this is not possible in Rust since the context is an associated type on the underlying GraphQLType
trait. The graphql_object!
macro expands to something like this:
graphql_object!(<'a> QueryRoot: Context<'a> as "Query" |&self| {
field users(&executor) -> FieldResult<Vec<User>> {
Ok(vec![])
}
});
// Expands into:
impl<'a> GraphQLType for QueryRoot { // <- no constraint on 'a here
type Context = Context<'a>;
// ...
}
As you can see, the 'a
parameter is unconstrained in the impl
line, which is an error in Rust.
It might work with adding a lifetime parameter to QueryRoot
, by using e.g. PhantomData
, but I would advise against it since it's might cause trouble when we introduce asynchronous resolvers.
I doubt it does, but does the latest master with https://github.com/graphql-rust/juniper/pull/226 help here?
@LegNeato nah, those are for the derives, not the declarative macros.
I'm hitting the same issue. I'm using an embedded Graph DB, so context needs to be defined as: struct Context<'a> { graph: &'a Graph }
but I can't due to this issue. As a workaround I can something horrible like this:
type GraphPtr = *const Graph;
impl<'a> From<&'a Graph> for GraphPtr {
fn from(graph: &Graph) -> Self {
graph as GraphPtr
}
}
impl<'a> Into<&'a Graph> for GraphPtr {
fn into(self) -> &'a Graph {
unsafe { &*self }
}
}
struct Context {
graphPtr: GraphPtr,
}
I haven't touched this area of Juniper but I have some time tomorrow and will try to take a crack at it
Same issue, any "not-so-ugly" solution?
// context.rs
pub struct GraphQLContext<'a> {
pub language: Language,
pub db_pool: DbPool,
pub cookies: Cookies<'a>
}
impl <'a>Context for GraphQLContext<'a> {}
// mutation.rs is similar as query.rs
// query.rs
graphql_object!(<'a> &'a Query: GraphQLContext<'a> as "Query" |&self| {
field recipe(&executor, id: RecipeId) -> FieldResult<RecipeGql> {
get_recipe(&id, executor)
}
field recipePreparationStep(&executor, id: RecipePreparationStepId) -> FieldResult<RecipePreparationStepGql> {
get_rps(&id, executor)
}
});
//schema.rs
pub type Schema = RootNode<'static, Query, Mutation>;
// router.rs
#[post("/", data = "<request>")]
fn post_graphql_handler(
language_guard: LanguageGuard,
ctx: State<Context>,
request: juniper_rocket::GraphQLRequest,
schema: State<Schema>,
mut cookies: Cookies
) -> juniper_rocket::GraphQLResponse {
let gql_ctx = GraphQLContext::new(
language_guard.language,
ctx.db_pool.clone(),
cookies
);
request.execute(&schema, &gql_ctx)
}
gives me
--> src/router/graphql.rs:9:1
|
9 | / fn post_graphql_handler(
10 | | language_guard: LanguageGuard,
11 | | ctx: State<Context>,
12 | | request: juniper_rocket::GraphQLRequest,
... |
21 | | request.execute(&schema, &gql_ctx)
22 | | }
| |_^ the trait `juniper::GraphQLType` is not implemented for `graphql::query::Query`
|
= help: the following implementations were found:
<&'a graphql::query::Query as juniper::GraphQLType>
= note: required by `juniper::RootNode`
error[E0277]: the trait bound `graphql::mutation::Mutation: juniper::GraphQLType` is not satisfied
--> src/router/graphql.rs:9:1
|
9 | / fn post_graphql_handler(
10 | | language_guard: LanguageGuard,
11 | | ctx: State<Context>,
12 | | request: juniper_rocket::GraphQLRequest,
... |
21 | | request.execute(&schema, &gql_ctx)
22 | | }
| |_^ the trait `juniper::GraphQLType` is not implemented for `graphql::mutation::Mutation`
|
= help: the following implementations were found:
<&'a graphql::mutation::Mutation as juniper::GraphQLType>
= note: required by `juniper::RootNode`
I really need those cookies in context. Or is there better way how to pass them?
Don't mind me. I've just wanted to set cookies in login resolver, and that's probably not possible, dunno. Classic REST route will be better for this purpose.
@mrceperka Did you find a solution for this? I'm trying to do the exact same thing (pass cookies into the context with Rocket) and ran into the same issue.
Nope I did not. I've continued without cookies... (cos I just wanted to do something and I did not want to be stuck on this cookie thing :D) But one day, I will have to fix it anyways.
I'm doing something like this which compiles:
pub struct Query<'a> {
_marker: PhantomData<&'a ()>,
}
graphql_object!(<'a> Query<'a>: Context<'a> as "Query" |&self| {
field getUser(&executor, id: Uuid) -> FieldResult<User> {
Ok(User::read(&executor.context().db.connect()?, &id)?)
}
});
pub struct Mutation<'a> {
_marker: PhantomData<&'a ()>,
}
graphql_object!(<'a> Mutation<'a>: Context<'a> as "Mutation" |&self| {
field createUser(&executor, user: UserCreate) -> FieldResult<String> {
Ok(User::create(
&executor.context().db.connect()?,
executor.context().hasher,
executor.context().tokeniser,
&user
)?)
}
field updateUser(&executor, user: UserEdit) -> FieldResult<bool> {
Ok(User::update(
&executor.context().db.connect()?,
executor.context().hasher,
&user
)?)
}
field deleteUser(&executor, id: Uuid) -> FieldResult<bool> {
Ok(User::delete(&executor.context().db.connect()?, &id)?)
}
field login(&executor, user: UserLogin) -> FieldResult<String> {
Ok(User::login(
&executor.context().db.connect()?,
*executor.context().hash_verify,
executor.context().tokeniser,
&user
)?)
}
});
pub type Schema<'a> = RootNode<'static, Query<'a>, Mutation<'a>>;
pub fn new<'a>() -> Schema<'a> {
Schema::new(Query { _marker: PhantomData }, Mutation { _marker: PhantomData })
}
But crashes horribly when handling the request through Rocket (although it could be down to Rocket's State
for me, I've got a question here regarding: https://stackoverflow.com/questions/55884872/rockets-state-errors-with-attempted-to-retrieve-unmanaged-state-when-using-an)
This is an example that works with the new object macro.
THis should be added to the book and to the macro doc comment.
pub struct DataStore;
pub struct Context<'a> {
pub datastore: &'a DataStore
}
impl<'a> juniper::Context for Context<'a> {}
pub struct Query<'a> {
marker: std::marker::PhantomData<&'a ()>,
}
#[juniper::object(context = Context<'a>)]
impl<'a> Query<'a> {
fn datastore_version(ctx: &Context<'a>) -> String {
"lala".into()
}
}
fn main() {
let query = Query{ marker: std::marker::PhantomData };
}
Yeah, I've done something similar. This is the workaround I ended up creating:
#[derive(Debug)]
pub struct QueryRoot<'a>(PhantomData<&'a ()>);
#[derive(Debug)]
pub struct MutationRoot<'a>(PhantomData<&'a ()>);
#[derive(Debug)]
pub struct Schema(RootNode<'static, QueryRoot<'static>, MutationRoot<'static>>);
impl Schema {
// Workaround for https://github.com/graphql-rust/juniper/issues/143
//
// The only reason we have this lifetime on `QueryRoot`/`MutationRoot` is so we can use it on
// associated `Context` type so we can pass context items with lifetimes.
//
// At the same time, we need `Schema` to use `'static` lifetime internally since we create it
// once and store globally in `Arc`.
pub fn with_lifetime<'a>(&self) -> &RootNode<'a, QueryRoot<'a>, MutationRoot<'a>> {
let res: &RootNode<'static, QueryRoot<'static>, MutationRoot<'static>> = &self.0;
let res: &RootNode<'static, QueryRoot<'a>, MutationRoot<'a>> =
unsafe { std::mem::transmute(res) };
res
}
}
One constraint in my case is that building a new schema is quite expensive, so I have to create it once and store in Rocket State
(and hence it has to have a lifetime of 'static
).
Still, this is not a great solution as it uses unsafe to erase lifetimes (which seems to be fine in my particular case, but safe unsafe is no unsafe at all...).
which seems to be fine in my particular case
Yeah it's fine in this case because the context is guaranteed to be alive for the duration of execute
, but it's obviously unfortunate that this is needed.
Out of curiosity, is your schema so big that construction time is noticable?
Our schema is defined through metadata which we read from the database and/or files. And it's big, yes. Takes seconds to build a schema (in debug, in release it is fraction of a second, but still some significant amount of time).
Hey, I'm glad I found this issue, but I'm still struggling to get this to work...
use crate::{
models::recipe::Recipe as RecipeModel,
graphql::{
Context,
resolvers::{
RecipeIngredient,
}
}
};
pub struct Recipe<'a> {
marker: std::marker::PhantomData<&'a ()>,
record: RecipeModel,
}
#[juniper::graphql_object(Context = Context<'a>)]
impl<'a> Recipe<'a> {
fn id(&self) -> i32 {
self.record.id
}
fn name(&self) -> &String {
&self.record.name
}
async fn ingredients(&self, context: &Context<'a>) -> Vec<RecipeIngredient> {
let records = context.recipe_ingredient_loader.load(self.record.id).await;
records
.into_iter()
.map(|r| RecipeIngredient { marker: std::marker::PhantomData, record: r })
.collect()
}
}
This is one resolver that's not on the top level. When i compile this, i get the following error:
error[E0308]: mismatched types
--> src/graphql/resolvers/recipe.rs:16:1
|
16 | #[juniper::graphql_object(Context = Context<'a>)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ one type is more general than the other
|
= note: expected type `std::marker::Sync`
found type `std::marker::Sync`
= note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)
Any Idea what's happening here?
I actually solved this problem by removing the lifetime parameter from the Context object.
If you think about it, the context shouldn't need an annotated lifetime, because it's suppose to live as long as the program. In other words, the fields within the Context object should also live as long too.
Let's take a look at the OP's code:
use juniper::Context as JuniperContext;
use postgres::transaction::Transaction;
pub struct Context<'a> {
pub tx: Transaction<'a>,
}
Taking the explanation above, we can see that it's absurd that the tx
object live as long as the Context
, since tx
should only live as long as the lifetime of a client request, while Context
live until the program dies.
In my case, I initially have this following piece of code, the Context is Persistence
:
pub struct Persistence<'a, T>
where
T: Serialize + Deserialize<'a>,
{
db: mongodb::Database,
collection_name: String,
phantom: PhantomData<T>
}
And the implementation for this Persistence
struct contains function that deserialize JSON data. Then I realised something horribly wrong, if the lifetime of the deserialized object lives as long as the context, doesn't that means that memory consumption will be ever-increasing since the context lives until the program is terminated?
To solve this problem, where the Context and the deserialized object have different lifetime, I used Higher Kinded Trait Bounds, which allows you to introduce lifetime parameter from nowhere (kinda like creating a lamba).
pub struct Persistence<T>
where
T: Serialize + for<'a> Deserialize<'a>,
{
db: mongodb::Database,
collection_name: String,
phantom: PhantomData<T>
}
And voila the lifetime parameter is eliminated from the Persistence
struct (and so this issue become irrelevant).
P/S: I'm still very new to Rust, the explanation above is solely based on my speculations. I'll be more than happy if you could correct my mistakes.
I believe this can be fixed by using an Arc (or Rc, if you're not doing anything async) to wrap your data source, database, http client or anything else.
This way you can tell Rocket to manage this object, and on the FromRequest implementation you can always create a brand new Context, but instead of having to instantiate all it's dependencies, you would be cloning them from the Arc pointer, which is not an actual expensive clone, but only getting a reference to the initial instance, only adding to the Arc refs counter...
Something like this:
Here we create our UserAddressService wrapped by an Arc and tell Rocket to manage it, so we can ask for it later in the FromRequest trait implementation. This could be your database or anything else you might need in your context.
// Settings
let settings = rocket_builder.state::<Settings>().unwrap();
// Http Client
let http_client = Arc::new(
Client::builder()
.build(HttpsConnector::new())
);
let vtex_settings = settings.vtex_api.clone().unwrap();
let base_uri = vtex_settings.base_uri.clone().unwrap();
let app_key = vtex_settings.app_key.clone().unwrap();
let app_token = vtex_settings.app_token.clone().unwrap();
// Data Sources
let vtex_master_data_api = Arc::new(VtexMasterDataApi::new(
http_client.clone(),
base_uri.to_string(),
app_key.to_string(),
app_token.to_string(),
));
// Services
let user_address_service = Arc::new(UserAddressService::new(
vtex_master_data_api.clone(),
));
rocket_builder
.manage(user_address_service)
Here we ask Rocket for our reference and return a new context containing it.
pub struct RequestContext {
pub user_address_service: Arc<UserAddressService>,
}
impl<'a> juniper::Context for RequestContext {}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for RequestContext {
type Error = ();
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let user_address_service = request
.rocket()
.state::<Arc<UserAddressService>>()
.unwrap();
Outcome::Success(RequestContext {
// We don't need to worry about cloning here,
// since we're using Arc, we're only getting a shared reference!
user_address_service: user_address_service.clone(),
})
}
}
This way our dependency instance will be kept alive for the lifetime of our application (or at least while Rocket is running) and every time we need it inside a resolver we'll be getting a new reference without having to worry about managing lifetimes ourselves.
Another thing you may need to do if you intend to use mutable state inside your shared object, is using a Mutex or RwLock to make it thread-safe. Along the lines of:
Arc::new(RwLock::new(YourInMemoryDatabase::new()))
I'm pretty new to Rust, so if anyone have any suggestions on a better way to do this, i'm all ears! Anyways, I hope this helps anyone...