sangria-graphql / sangria

Scala GraphQL implementation

Home Page:https://sangria-graphql.github.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Sbt task to generate sangria schema stubs

markarasev opened this issue · comments

I have a problem in my team, the frontend (Javascript) and the backend (Scala) use a schema.graphql file to define API contracts. This file is edited and understood by both frontend and backend developers.

But by using Sangria, the backend team has to redefine the schema in Scala sources using the schema DSL. This ends up in schema definition duplication and sometimes the contract (the schema.graphql file) and the implementation (sangria schema DSL) differ which leads to bugs.

Other graphql libraries (java, javascript) allows you to first define your graphql types and queries (also by parsing a .graphql file) and then attach your resolvers to types and fields. But both the java and javascript implementation are quite fragile because when it comes to attaching resolvers to types and fields, developers have to rely on raw strings describing those types and fields rather than on a type system. Example using the java library :

val typeDefinitionRegistry = schemaParser.parse(schemaFile)
val runtimeWiring = RuntimeWiring
  .newRuntimeWiring()
  .`type`(
    newTypeWiring("Query")
      .dataFetcher("hero", StarWarsData.heroDataFetcher)
      .dataFetcher("human", StarWarsData.humanDataFetcher)
      .dataFetcher("droid", StarWarsData.droidDataFetcher)
      .build()
  )
  .`type`(
    newTypeWiring("Character")
      .typeResolver(StarWarsData.characterTypeResolver)
      .build()
  )
  .build()
val schemaGenerator = new SchemaGenerator
val graphQLSchema = schemaGenerator
  .makeExecutableSchema(typeDefinitionRegistry, runtimeWiring)

I think a fair tradeoff would be to have something like a SBT task that would parse a .graphql file and generate Sangria schema stubs (without the resolver part) to which developers would have to attach resolvers in a type-safe manner.

I managed to "attach" a resolver to a type after its definition in Sangria but in a hacky way and still relying on raw strings :

// to be generated by the SBT task
val articleType = ObjectType(...)
val idArg = Argument("id", StringType)
val queryType = ObjectType(
  "Query",
  fields[Repo, Unit](
    sangria.schema.Field(
      "article",
      OptionType(articleType),
      arguments = idArg :: Nil,
      resolve = c  ??? // should have been directly defined as c.ctx.loadArticle(c arg idArg) by a schema DSL user
    ))
)

// to be replaced by type-safe methods inferred from the previously generated types
val builder = AstSchemaBuilder.resolverBased[Repo](
  FieldResolver {
    case (TypeName("Query"), FieldName("article")) =>
      ctx =>
        val id = ctx.args.arg[String]("id")
        ctx.ctx.loadArticle(id)
  },
  FieldResolver.defaultInput[Repo, JsValue]
)

val staticSchema = Schema(queryType)
val schema = Schema.buildFromAst[Repo](staticSchema.toAst, builder)

The resolve = c ⇒ ??? is for sure not the best way to implement it but you got the idea and the last two lines should look more like :

val typeDefinitions = ... // generated
val resolvers = ... // something like the previous builder val
val schema = Schema.buildFromTypeDefinitionsAndResolvers(typeDefinitions, resolvers)

That would enable frontend and backend teams working on a unique schema.graphql file withoud duplicating it in the Scala sources.
What are your thoughts?

Sangria provides good support for SDL-based schema definition. I would suggest you to check this part of the docs (as well as "High-level SDL-based Schema Builder" and "Schema Materialization Example" sections):

https://sangria-graphql.org/learn/#based-on-sdl-definitions

You should use QueryParser.parse instead of of graphql macro since the string is dynamic.

Regarding the SBT task. I think it is an interesting idea. I'm not aware of my tool that does this kind of schema generation, so it's hard to tell in advance how well it will work. But I believe it is worth an experiment. I think it would be better to implement sbt plugin like this as a standalone library (similar to sbt-graphql).

The Document produced by QueryParser.parse would be quite a good start to generate the schema. Do you think you will have some time to work on that feature? I can try myself or help but I'm a beginner in graphql and I never developed a SBT task (it is a good opportunity to learn how to though). Any clues on what to use to do the code generation?

I see this more as a standalone project rather than a feature. I think is some respect project like this might take opinionated decisions about the schema definition DSL. I think it is actually a good thing since it might batter fit specific use-case and the team dynamic. If you are working on project that can take advantage of such codegen plugin, I think you would be the best person to drive this schema definition style since you have the vision for it and quick feedback loop within the team. I believe that the core sangria library provides all necessary building blocks to make this possible,

For some examples I would recommend you to check out sbt-graphql and sangria-codegen. Even though they are primarily focused on client-side codegen, I think they might provide a lot of guidance on how to build an Sbt plugin and do the code generation with it and sangria. Maybe it also might be an interesting idea to contribute this functionality in sbt-graphql?