juniper icon indicating copy to clipboard operation
juniper copied to clipboard

How to create an entity where (most) fields and the type are only known at runtime?

Open scott-wilson opened this issue 3 years ago • 8 comments

I have a project where I want to be able to have the database drive the entity types and (most) fields at runtime.

There's two questions I have. Firstly I get a compiler error when hooking up the entity type to the schema through the find_entity method in the query type (code and error below). I know I'm missing something, but is there an example of an entity where the type and the fields are known at runtime? Also, it looks like the meta method doesn't have any way to communicate with the database. The answer to this question will likely tie in with the main question, but is there a way for an entity to get its type from the context?

Thank you!

Entity type

use juniper::{
    marker::GraphQLObjectType, Arguments, DefaultScalarValue, ExecutionResult, Executor,
    GraphQLEnum, GraphQLInputObject, GraphQLObject, GraphQLType, GraphQLValue, GraphQLValueAsync,
    ScalarValue,
};
use std::collections::HashMap;

#[derive(Debug, Clone)]
pub enum Value {
    Int(i32),
    Float(f64),
    String(String),
    Boolean(bool),
    Entity(Box<Entity>),
    Entities(Vec<Entity>),
}

#[derive(Debug, Clone)]
pub enum Type {
    Int,
    Float,
    String,
    Boolean,
    Entity,
    Entities,
}

#[derive(Debug, Clone)]
pub struct TypeInfo {
    pub entity_type: String,
    pub fields: HashMap<String, Type>,
}

#[derive(Debug, Clone)]
pub struct Entity {
    pub id: u64,
    pub status: EntityStatus,
    pub fields: HashMap<String, Value>,
}

impl GraphQLType<DefaultScalarValue> for Entity {
    fn name(info: &Self::TypeInfo) -> Option<&str> {
        Some(&info.entity_type)
    }

    fn meta<'r>(
        info: &Self::TypeInfo,
        registry: &mut juniper::Registry<'r, DefaultScalarValue>,
    ) -> juniper::meta::MetaType<'r, DefaultScalarValue>
    where
        DefaultScalarValue: 'r,
    {
        let mut fields = Vec::with_capacity(info.fields.len());

        for (field_name, field_type) in &info.fields {
            fields.push(match field_type {
                Type::Int => registry.field::<&i32>(field_name, &()),
                Type::Float => registry.field::<&f64>(field_name, &()),
                Type::String => registry.field::<&String>(field_name, &()),
                Type::Boolean => registry.field::<&bool>(field_name, &()),
                Type::Entity => {
                    let field_info = TypeInfo {
                        entity_type: "how_to_get_entity_type".into(),
                        fields: HashMap::new(),
                    };
                    registry.field::<&Entity>(field_name, &field_info)
                }
                Type::Entities => {
                    let field_info = TypeInfo {
                        entity_type: "how_to_get_entity_type".into(),
                        fields: HashMap::new(),
                    };
                    registry.field::<&Vec<Entity>>(field_name, &field_info)
                }
            });
        }

        registry
            .build_object_type::<Entity>(info, &fields)
            .into_meta()
    }
}

impl GraphQLValue<DefaultScalarValue> for Entity {
    type Context = crate::Context;
    type TypeInfo = TypeInfo;

    fn type_name<'i>(&self, info: &'i Self::TypeInfo) -> Option<&'i str> {
        <Entity as GraphQLType>::name(info)
    }
}

impl<S> GraphQLObjectType<S> for Entity
where
    Self: GraphQLType<S>,
    S: ScalarValue,
{
}

impl<S> GraphQLValueAsync<S> for Entity
where
    Self: GraphQLValue<S> + Sync,
    Self::TypeInfo: Sync,
    Self::Context: Sync,
    S: ScalarValue + Send + Sync,
{
}

Schema

use crate::entities::Entity;

use super::context::Context;
use juniper::meta::ScalarMeta;
use juniper::{graphql_object, Executor, FieldResult, ScalarValue};
use std::{collections::HashMap, fmt::Display};

pub struct Query;

#[graphql_object(context = Context)]
impl Query {
    fn api_version() -> &'static str {
        "0.1.0"
    }

    fn find_entities(context: &Context, id: String) -> FieldResult<Entity> {
        let entity = Entity {
            id: "123".to_string().into(),
            status: EntityStatus::Active,
            fields: HashMap::new(),
        };
        Ok(entity)
    }

    fn with_executor(executor: &Executor) -> bool {
        let info = executor.look_ahead();

        true
    }
}

pub struct Mutation;

#[graphql_object(context = Context, Scalar = S)]
impl<S: ScalarValue + Display> Mutation {}

pub type Schema = juniper::RootNode<'static, Query, Mutation, juniper::EmptySubscription<Context>>;

Compiler Error

error[E0277]: the trait bound `Result<Entity, FieldError>: IntoResolvable<'_, __S, _, context::Context>` is not satisfied
  --> server/src/schema.rs:13:1
   |
13 | #[graphql_object(context = Context)]
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `IntoResolvable<'_, __S, _, context::Context>` is not implemented for `Result<Entity, FieldError>`
   |
   = help: the following implementations were found:
             <Result<(&'a <T as GraphQLValue<S2>>::Context, T), FieldError<S1>> as IntoResolvable<'a, S2, T, C>>
             <Result<T, E> as IntoResolvable<'a, S, T, C>>
             <Result<std::option::Option<(&'a <T as GraphQLValue<S2>>::Context, T)>, FieldError<S1>> as IntoResolvable<'a, S2, std::option::Option<T>, C>>
   = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0277]: the trait bound `Entity: GraphQLValue<__S>` is not satisfied
  --> server/src/schema.rs:13:1
   |
13 | #[graphql_object(context = Context)]
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `GraphQLValue<__S>` is not implemented for `Entity`
   |
   = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider extending the `where` bound, but there might be an alternative better way to express this requirement
   |
13 | #[graphql_object(context = Context)], Entity: GraphQLValue<__S>
   |                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^

error[E0308]: mismatched types
  --> server/src/schema.rs:13:1
   |
13 | #[graphql_object(context = Context)]
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   | |
   | expected enum `DefaultScalarValue`, found type parameter `__S`
   | help: try using a variant of the expected enum: `Ok(#[graphql_object(context = Context)])`
   |
   = note: expected enum `Result<_, FieldError<DefaultScalarValue>>`
              found enum `Result<juniper::Value<__S>, FieldError<__S>>`
   = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0308]: mismatched types
  --> server/src/schema.rs:13:1
   |
13 | #[graphql_object(context = Context)]
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   | |
   | expected type parameter `__S`, found enum `DefaultScalarValue`
   | expected `Result<juniper::Value<__S>, FieldError<__S>>` because of return type
   |
   = note: expected enum `Result<juniper::Value<__S>, FieldError<__S>>`
              found enum `Result<_, FieldError<DefaultScalarValue>>`
   = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0308]: `match` arms have incompatible types
  --> server/src/schema.rs:13:1
   |
13 | #[graphql_object(context = Context)]
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   | |
   | expected type parameter `__S`, found enum `DefaultScalarValue`
   | this is found to be of type `Result<juniper::Value<__S>, FieldError<__S>>`
   | this is found to be of type `Result<juniper::Value<__S>, FieldError<__S>>`
   | `match` arms have incompatible types
   |
   = note: expected enum `Result<juniper::Value<__S>, FieldError<__S>>`
              found enum `Result<_, FieldError<DefaultScalarValue>>`
   = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)

error: aborting due to 5 previous errors; 4 warnings emitted

Some errors have detailed explanations: E0277, E0308.
For more information about an error, try `rustc --explain E0277`.
error: could not compile `server`

scott-wilson avatar Jul 10 '21 20:07 scott-wilson

@scott-wilson you're going in the right direction by implementing GraphQLType and GraphQLValue manually, however more effort should be done.

First, to play well with other types, it's better to provide implementation not for DefaultScalarValue, but make the generic over S: ScalarValue. You may see the examples: here and here.

Second, don't forget about implementing required marker traits. You will definitely need marker::IsOutputType, at least.

tyranron avatar Jul 21 '21 16:07 tyranron

Would be nice if there was a more complex version of this test: https://github.com/graphql-rust/juniper/blob/master/juniper/src/tests/type_info_tests.rs

Where it shows how to make the meta function impl more dynamic. Maybe where the fields types added are based on data found in the TypeInfo. For example what if the TypeInfo contained type info parsed with https://github.com/graphql-rust/graphql-parser.

chirino avatar Jul 21 '21 17:07 chirino

I'm taking a stab a doing what I mentioned above.. see: https://github.com/chirino/dynamic-graphql/blob/main/src/lib.rs

Basically you can define a schema by parsing a graphql SDL, and the resolved data comes from a serde_json Value.

the basics seems to work. But It seems like it's going to get very complicated to deal with nested list types and such.

chirino avatar Jul 22 '21 18:07 chirino

There might be some ideas in https://github.com/davidpdrsn/juniper-from-schema as well

LegNeato avatar Jul 24 '21 23:07 LegNeato

First of all, thanks for the help so far. It looks like I'm a step closer to no compile errors.

The entity type looks done now, but I'm getting the following compile error on the Query type when I have the entity's type_info a simple i32 (just for testing).

error[E0308]: mismatched types
  --> src/schema.rs:10:1
   |
10 | #[graphql_object(context = Context)]
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `i32`, found `()`
   |
   = note: expected reference `&i32`
              found reference `&()`
   = note: this error originates in the attribute macro `graphql_object` (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0271]: type mismatch resolving `<Entity as GraphQLValue<__S>>::TypeInfo == ()`
  --> src/schema.rs:10:1
   |
10 | #[graphql_object(context = Context)]
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `i32`, found `()`
   |
   = note: this error originates in the attribute macro `graphql_object` (in Nightly builds, run with -Z macro-backtrace for more info)

How do I get the Query type to use the i32 info type while using the macro? Or would I have to implement the query by hand to get that?

scott-wilson avatar Jan 12 '22 06:01 scott-wilson

@scott-wilson by any chance are you intending on open sourcing your implementation? I have a similar problem and would love to see how you tackle it.

itsezc avatar Jan 22 '22 06:01 itsezc

Yeah, I'm slowly working on an interface to a graph DB that can be used in the VFX industry (stuff like shots, assets, etc). Hopefully it'll be open sourced once I get more done in it.

On Fri, Jan 21, 2022, 10:59 PM Chiru B @.***> wrote:

@scott-wilson https://github.com/scott-wilson by any chance are you intending on open sourcing your implementation? I have a similar problem and would love to see how you tackle it.

— Reply to this email directly, view it on GitHub https://github.com/graphql-rust/juniper/issues/957#issuecomment-1019079244, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAVFQOHGPUTA523AYAHOE4TUXJIV5ANCNFSM5AESHN3Q . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub.

You are receiving this because you were mentioned.Message ID: @.***>

scott-wilson avatar Jan 22 '22 08:01 scott-wilson

Here is our implementation: https://github.com/ForetagInc/Alchemy/blob/dev/engine/src/api/schema/mod.rs it's still a WIP, happy for people to comment/provide feedback on.

KennethGomez avatar Mar 11 '22 00:03 KennethGomez