Sangria实现的GraphQL Schema

6
我正在研究使用Sangria库在Scala中编写GraphQL服务器。然而,感到奇怪的是,相同的类型系统必须实现两次:(1)作为GraphQL类型声明的一部分,(2)同时在服务器端作为Scala case类实现,并伴随有ObjectType、InterfaceType等值。
在Scala中硬编码类型系统非常令人恼火,因为我的目的是能够CRUD任意形状的聚合,其中每个形状都定义为GraphQL类型集合。例如,假设Shape类型的实例包含一个GraphQL文档作为字段;Entity类型的实例具有对其Shape的引用,并且还包含在该Shape中定义的Json对象。
case class Shape(id: String, name: String, doc: sangria.ast.Document)
case class Entity(id: String, name: String, shape: Shape, content: JsValue)

例如,如果形状文档如下所示:
type Person {
  firstName: String!
  lastName: String!
  age: Int
}

那么实体中的Json内容可能如下所示:
{
  "firstName": "John",
  "lastName": "Smith",
  "age": 30
}

(当然,真实的例子也会有嵌套类型等等。)

因此,我希望能够定义类型实体的实例,其形状由相应的形状定义。我不想硬编码相应的sangria.schema.Schema,而是希望直接从形状文档中派生它。

是否有一种现成的方法可以从包含类型声明的GraphQL文档中以编程方式生成GraphQL模式?


为什么将sangria.ast.Type映射到sangria.schema.OutputType这样的操作如此繁琐? - silverberry
1个回答

5
对于这种动态用例,Sangria 提供了一种从 GraphQL IDL 构建模式的方法。以下是如何实现它的方式(我稍微简化了您的示例,但在所有数据来自单独的类(如 Shape 和 Entity)时可以实现相同的结果):
import sangria.ast._
import sangria.schema._
import sangria.macros._
import sangria.marshalling.sprayJson._
import sangria.execution.Executor

import scala.concurrent.ExecutionContext.Implicits.global
import spray.json._

val schemaAst =
  gql"""
    type Person {
      firstName: String!
      lastName: String!
      age: Int
    }

    type Query {
      people: [Person!]
    }
  """

val schema = Schema.buildFromAst(schemaAst, builder)

val query =
  gql"""
    {
      people {
        firstName
        age
      }
    }
  """

val data =
  """
    {
      "people": [{
        "firstName": "John",
        "lastName": "Smith",
        "age": 30
      }]
    }
  """.parseJson

val result = Executor.execute(schema, query, data)

为了定义如何生成resolve函数,您需要创建一个自定义模式构建器,像这样,并仅覆盖resolveField方法:
val builder =
  new DefaultAstSchemaBuilder[JsValue] {
    override def resolveField(typeDefinition: TypeDefinition, definition: FieldDefinition) =
      typeDefinition.name match {
        case "Query" ⇒
          c ⇒ c.ctx.asJsObject.fields get c.field.name map fromJson
        case _ ⇒
          c ⇒ fromJson(c.value.asInstanceOf[JsObject].fields(c.field.name))
      }

    def fromJson(v: JsValue) = v match {
      case JsArray(l) ⇒ l
      case JsString(s) ⇒ s
      case JsNumber(n) ⇒ n.intValue()
      case other ⇒ other
    }
  }

当您执行此示例时,您将看到以下JSON result:
{
  "data": {
    "people": [{
      "firstName": "John",
      "age": 30
    }]
  }
}

如果您想看更复杂的例子,我建议您查看GrapohQL Toolbox "proxy"。该项目更进一步,甚至添加了自定义指令来控制解析函数生成。代码可在此处找到:

https://github.com/OlegIlyenko/graphql-toolbox


非常感谢,tenshi!这可能正是我正在寻找的。 - silverberry
然而,有一个问题。如果c.field.fieldType是OutputType[Int]类型,则JsNumber的情况应返回Int,如果是OutputType[Long],则返回Long等。不幸的是,我们只得到OutputType[_],其中TypeTag被擦除了(因此不能使用=:=比较typeOf[T])。这使得编写正确的fromJson函数非常麻烦,因为它不仅必须考虑ScalarLongType,还必须考虑OptionType(ScalarLongType),以及枚举和联合类型等。尽管如此,这只是一个小问题,希望在未来发布的Sangria中可以解决。 - silverberry
@silverberry 类型信息也可以在运行时获得。以下是一个函数的示例,它可以根据类型信息提取正确的Scala类型:https://github.com/OlegIlyenko/graphql-toolbox/blob/master/app/controllers/Materializer.scala#L129-L142 - tenshi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接