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

Alternative schema builder patterns for building Schemas which follow a template. #23

Open
hsyed opened this issue Jun 20, 2017 · 5 comments
Labels

Comments

@hsyed
Copy link

hsyed commented Jun 20, 2017

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 ?

@hsyed
Copy link
Author

hsyed commented Jun 21, 2017

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

@hsyed
Copy link
Author

hsyed commented Jun 21, 2017

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.

@hsyed
Copy link
Author

hsyed commented Jun 22, 2017

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.

@wpK
Copy link

wpK commented Aug 22, 2017

@hsyed Were you able to accomplish what you wanted?

@hsyed
Copy link
Author

hsyed commented Sep 27, 2017

The project in question in now on the backburner so feel free to close.

yanns added a commit that referenced this issue Apr 28, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants