Repository navigation
How to create an entity where (most) fields and the type are only known at runtime? #957
Copy link
Copy link
Closed as duplicate
Closed as duplicate
Copy link
Labels
Description
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
tyranron commentedon Jul 21, 2021
@scott-wilson you're going in the right direction by implementing
GraphQLTypeandGraphQLValuemanually, 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 overS: ScalarValue. You may see the examples: here and here.Second, don't forget about implementing required
markertraits. You will definitely needmarker::IsOutputType, at least.chirino commentedon 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
metafunction 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 commentedon 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 commentedon Jul 24, 2021
There might be some ideas in https://github.com/davidpdrsn/juniper-from-schema as well
scott-wilson commentedon Jan 12, 2022
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).
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 commentedon 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 commentedon Jan 22, 2022
KennethGomez commentedon Mar 11, 2022
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.
LegNeato commentedon Oct 29, 2026
Thanks for the detailed write-up! Mixing macro-generated types with types that need runtime
TypeInfodepends on the core trait rework in #1072. #1117 tracks dynamic schemas, so I'm closing this in favor of it.