Skip to content

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

Closed as duplicate

Description

@scott-wilson

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`

Activity

  1. self-assigned this
    on Jul 21, 2021
  2. tyranron commented on Jul 21, 2021

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

  3. chirino commented on Jul 21, 2021

    @chirino

    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.

  4. chirino commented on Jul 22, 2021

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

  5. LegNeato commented on Jul 24, 2021

    @LegNeato
    Member
  6. scott-wilson commented on Jan 12, 2022

    @scott-wilson
    ContributorAuthor

    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?

  7. itsezc commented on Jan 22, 2022

    @itsezc

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

  8. scott-wilson commented on Jan 22, 2022

    @scott-wilson
    ContributorAuthor
  9. KennethGomez commented on Mar 11, 2022

    @KennethGomez

    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.

  10. LegNeato commented on Oct 29, 2026

    @LegNeato
    Member

    Thanks for the detailed write-up! Mixing macro-generated types with types that need runtime TypeInfo depends on the core trait rework in #1072. #1117 tracks dynamic schemas, so I'm closing this in favor of it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions