-
Notifications
You must be signed in to change notification settings - Fork 18
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
Alternative schema builder patterns for building Schemas which follow a template. #23
Comments
This is a variant of the approach I prototyped a while ago. The class below is used to create a relay compatible CRUD layer . The interface does seem to wire up correctly as far as introspection queries go but it's incorrect. I can't think of a way around this -- the approaches for making it lazy that I have tried lead to compiler errors. case class ModelTemplate[Ctx](
template: ObjectType[Ctx, Node],
/**
* Derive static relay compatible mutation fields from the OutputObjectType, make use of field tags for metadata --e.g., (create, update etc).
*/
deriveMutationFields: (ObjectType[Ctx,_]) => Seq[Field[Ctx,_]],
/**
* Derive static query fields --e.g., cursors.
*/
deriveQueryFields: (ObjectType[Ctx,_]) => Seq[Field[Ctx,_]]
)
class RelaySchemaGraftBuilder[Ctx](
templateModels: Seq[ModelTemplate[Ctx]],
resolvers: (GlobalId, Context[Ctx, Unit]) ⇒ LeafAction[Ctx, Option[Node]]
) {
private val NodeDefinition(nodeInterface, nodeField, nodesField) = Node.definition(
resolvers,
Node.possibleNodeTypes(templateModels.map( tm => PossibleNodeObject(tm.template)):_*)
)
private val modelTypes =
templateModels.map(mt => mt.copy(
template = mt.template.copy(interfaces = nodeInterface :: mt.template.interfaces)
))
/**
* fields to be plugged into the root query node of a schema.
*/
def queryFields: Seq[Field[Ctx,_]] =
Seq(nodeField, nodesField) ++ (modelTypes flatMap(mt => mt.deriveQueryFields(mt.template)))
/**
* fields to be plugged into the root mutation node of a schema.
*/
def mutationFields: Seq[Field[Ctx,_]] =
(modelTypes flatMap(mt => mt.deriveMutationFields(mt.template)))
} It would be nice to take this decoupling approach further for schema generation. The inversion above can only provide -- it would be useful to be able to inject fields defined elsewhere (for example generic cursors) and also template builders so that the derivation builders don't have to be in the "templates" . |
Ok so after a day of fighting the Scala type system I think the following is correct. Could you confirm @OlegIlyenko. package pylon.graphql.schema.dynamic
import cats.data.Reader
import sangria.relay._
import sangria.schema.{Context, Field, InterfaceType, LeafAction, ObjectType, PossibleType}
import scala.reflect.ClassTag
case class ModelTemplate[Ctx](
template: ObjectType[Ctx,_],
/**
* Derive static relay compatible mutation fields for a model by inspecting field tags --e.g., (create, update etc).
*/
deriveMutationFields: List[Reader[ObjectType[Ctx,_], Field[Ctx,Unit]]],
/**
* Derive static query fields --e.g., cursors.
*/
deriveQueryFields: List[Reader[ObjectType[Ctx,_], Field[Ctx,Unit]]]
){
def possibleNodeObject[T](implicit
ida: Identifiable[T],
ev: PossibleType[Node, T],
id: IdentifiableNode[Ctx, T]
) = PossibleNodeObject[Ctx, Node, T](template.asInstanceOf[ObjectType[Ctx,T]])
def init[T: ClassTag](face: InterfaceType[Ctx, _]): ModelTemplate[Ctx] = {
this.copy(
this.template.asInstanceOf[ObjectType[Ctx,T]].copy(
interfaces = face :: this.template.interfaces)
)
}
}
object ModelTemplate {
def apply[Ctx,T: ClassTag](
template: ObjectType[Ctx, T],
/**
* Derive static relay compatible mutation fields for a model by inspecting field tags --e.g., (create, update etc).
*/
deriveMutationFields: List[Reader[ObjectType[Ctx, T], Field[Ctx, Unit]]],
/**
* Derive static query fields --e.g., cursors.
*/
deriveQueryFields: List[Reader[ObjectType[Ctx, T], Field[Ctx, Unit]]]
) = new ModelTemplate[Ctx](
template,
deriveMutationFields.asInstanceOf[List[Reader[ObjectType[Ctx, _], Field[Ctx, Unit]]]],
deriveQueryFields.asInstanceOf[List[Reader[ObjectType[Ctx, _], Field[Ctx, Unit]]]])
}
class RelaySchemaGraftBuilder[Ctx, M](
resolvers: (GlobalId, Context[Ctx, Unit]) ⇒ LeafAction[Ctx, Option[M]],
templateModels: Seq[ModelTemplate[Ctx]]
)(implicit ev: Identifiable[M]) {
private val definition: NodeDefinition[Ctx,Unit,M] = Node.definition(
resolvers,
Node.possibleNodeTypes(modelTypes.map(_.possibleNodeObject):_*)
)
val modelTypes = templateModels.map(_.init(definition.interface.asInstanceOf[InterfaceType[Ctx,_]]))
/**
* fields to be plugged into the root query node of a schema.
*/
def queryFields(): List[Field[Ctx,Unit]] =
definition.nodeField ::
definition.nodeFields ::
(modelTypes flatMap (mt => mt.deriveQueryFields.map(mtf => mtf.run(mt.template)))).toList
/**
* fields to be plugged into the root mutation node of a schema.
*/
def mutationFields: List[Field[Ctx,Unit]] =
(modelTypes flatMap (mt => mt.deriveMutationFields.map(mtf => mtf.run(mt.template)))).toList
}
object RelaySchemaGraftBuilder {
def apply[Ctx, M: Identifiable](resolvers: (GlobalId, Context[Ctx, Unit]) ⇒ LeafAction[Ctx, Option[M]])(
templates: ModelTemplate[Ctx]*
) = new RelaySchemaGraftBuilder[Ctx,M](resolvers,templates)
} It addresses my initial concern. I'll continue to work on putting together a schema generator for my project in the meantime. Feel free to close this issue but I'd still like to leave a standing feature request for work towards a dynamic schema generator framework that is at a high level of abstraction compared to the AST builders. |
I updated the code above. it didn't work as I hoped. I am having to copy the interface into the model objects after initialising it. Moving on I have to define some of my own interfaces and I guess I'll have to use a similar pattern. |
@hsyed Were you able to accomplish what you wanted? |
The project in question in now on the backburner so feel free to close. |
Currently the relay support for schema building creates a circular dependency between
NodeInterface
and the objects that extend it. This is quite limiting for large scale schemas as decoupling is problematic or using DI of some form (injecting a list of types).I don't know what the solution is in general for the NodeInterface, maybe certain ObjectTypes could be Stateful references with a lifecycle --i.e., once the object type is finalised it can't be injected ?
Ultimately I need to generate most of my schema generically so that most of it uses a JSON AST (probably Map[String,Any]) as the Val. I'm currently looking at going the lowest possible level and constructing a schema from a generated AST but this is going to be hard to maintain.
A General purpose container for dynamic / incremental building of a schema would be most welcome. Perhaps Scaldi could underpin this ?
The text was updated successfully, but these errors were encountered: