Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
scott-wilson opened this issue Jul 10, 2021 · 8 comments · May be fixed by #975
Open

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

scott-wilson opened this issue Jul 10, 2021 · 8 comments · May be fixed by #975
Assignees
Labels

Comments

@scott-wilson
Copy link
Contributor

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`
@tyranron tyranron self-assigned this Jul 21, 2021
@tyranron
Copy link
Member

@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.

@chirino
Copy link

chirino commented Jul 21, 2021

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
Copy link

chirino commented Jul 22, 2021

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.

@LegNeato
Copy link
Member

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

@tyranron tyranron linked a pull request Aug 15, 2021 that will close this issue
11 tasks
@scott-wilson
Copy link
Contributor Author

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?

@itsezc
Copy link

itsezc commented Jan 22, 2022

@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.

@scott-wilson
Copy link
Contributor Author

scott-wilson commented Jan 22, 2022 via email

@KennethGomez
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants