英文:
Why graphql-js executor stops resolving child fields that have their own resolvers when the parent resolver return null/undefined?
问题
在JavaScript中编写GraphQL库时,我遇到了一个奇怪的行为。我设法在一个非常简单的示例中隔离了它。让我们看看这个服务器片段:
const { ApolloServer, gql } = require("apollo-server")
const typeDefs = gql`
type Book {
resolveItSelf: String
}
type Query {
book: Book
}
`
const resolvers = {
Query: {
book: () => {
return null // 这里返回undefined也有相同的行为
}
},
Book: {
resolveItSelf: () => "resolveItSelf"
}
}
const server = new ApolloServer({ typeDefs, resolvers })
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`)
})
如果我们使用以下查询查询此服务器:
{
book {
resolveItSelf
}
}
我们会得到以下结果:
{
"data": {
"book": null
}
}
所以,我原本期望GraphQL执行器会尝试解析"resolveItSelf"字段(它有自己的解析器),即使book解析器返回null。
要获得我期望的行为,一种方法是稍微更改book的解析器:
const resolvers = {
Query: {
book: () => {
return {} // 返回一个空对象而不是null/undefined
}
},
Book: {
resolveItSelf: () => "resolveItSelf"
}
}
然后我们会得到以下结果:
{
"data": {
"book": {
"resolveItSelf": "resolveItSelf"
}
}
}
即使父级为空,字段也会被解析!
因此,我的问题是,为什么graphql-js执行器停止尝试解析字段,如果父级的解析器返回null/undefined,尽管请求的字段可以自行解析?(草案中是否有涵盖此问题的部分?)
英文:
While writing a lib for GraphQL in JavaScript I stumbled upon a curious behavior. I managed to isolate it in a very simple example. Let's take this server snippet:
const { ApolloServer, gql } = require("apollo-server")
const typeDefs = gql`
type Book {
resolveItSelf: String
}
type Query {
book: Book
}
`
const resolvers = {
Query: {
book: () => {
return null // same behavior with undefined here
}
},
Book: {
resolveItSelf: () => "resolveItSelf"
}
}
const server = new ApolloServer({ typeDefs, resolvers })
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`)
})
If we query this server with the following query:
{
book {
resolveItSelf
}
}
We get this result:
{
"data": {
"book": null
}
}
So, I was expecting the graphql executor to try to resolve the "resolveItSelf" field (which have its own resolver) even if the book resolver returned null.
A way to get the behavior I expect is to change the book's resolver a little bit:
const resolvers = {
Query: {
book: () => {
return {} // empty object instead of null/undefined
}
},
Book: {
resolveItSelf: () => "resolveItSelf"
}
}
Then we get this result:
{
"data": {
"book": {
"resolveItSelf": "resolveItSelf"
}
}
}
The field is resolved even if the parent is empty !
So my question is why the graphql-js executor stop trying to resolve fields if the parent's resolver return null/undefined, even though requested fields can be resolved on their own ? (Is there a section in the draft that cover this ?)
答案1
得分: 0
在GraphQL中,null表示缺少值。如果一个字段解析为null,那么调用它的“子”字段解析器是没有意义的,因为它们不会在响应中返回。
来自规范的"Value Completion"部分(重点在我这里):
- 如果fieldType是Non‐Null类型:
a. 让innerType成为fieldType的内部类型。
b. 让completedResult成为调用CompleteValue(innerType, fields, result, variableValues)的结果。
c. 如果completedResult为null,则抛出字段错误。
d. 返回completedResult。 - 如果result为null(或类似null的内部值,如undefined或NaN),则返回null。
- 如果fieldType是List类型:
a. 如果result不是值的集合,则抛出字段错误。
b. 让innerType成为fieldType的内部类型。
c. 返回一个列表,其中每个列表项都是调用CompleteValue(innerType, fields, resultItem, variableValues)的结果,其中resultItem是result中的每个项。 - 如果fieldType是Scalar或Enum类型:
a. 返回“强制转换”result的结果,确保它是fieldType的合法值,否则返回null。 - 如果fieldType是Object、Interface或Union类型:
a. 如果fieldType是Object类型。
i. 让objectType成为fieldType。
b. 否则,如果fieldType是Interface或Union类型。
i. 让objectType成为ResolveAbstractType(fieldType, result)的结果。
c. 让subSelectionSet成为调用MergeSelectionSets(fields)的结果。
d. 返回通常评估ExecuteSelectionSet(subSelectionSet, objectType, result, variableValues)的结果(允许并行执行)。
换句话说,即使一个字段的类型是Object(因此具有可能被解析的字段的选择集),如果它解析为null,那么沿该路径不会发生进一步的执行。
英文:
In GraphQL, null represents a lack of a value. If a field resolves to null, it doesn't make sense for its "child" field resolvers' to be called since they wouldn't be returned in the response anyway.
From the Value Completion section of the spec (emphasis mine):
> 1. If the fieldType is a Non‐Null type:
> a. Let innerType be the inner type of fieldType.
> b. Let completedResult be the result of calling CompleteValue(innerType, fields, result, variableValues).
> c. If completedResult is null, throw a field error.
> d. Return completedResult.
> 2. If result is null (or another internal value similar to null such as undefined or NaN), return null.
> 3. If fieldType is a List type:
> a. If result is not a collection of values, throw a field error.
> b. Let innerType be the inner type of fieldType.
> c. Return a list where each list item is the result of calling CompleteValue(innerType, fields, resultItem, variableValues), where resultItem is each item in result.
> 4. If fieldType is a Scalar or Enum type:
> a. Return the result of “coercing” result, ensuring it is a legal value of fieldType, otherwise null.
> 5. If fieldType is an Object, Interface, or Union type:
> a. If fieldType is an Object type.
> i. Let objectType be fieldType.
> b. Otherwise if fieldType is an Interface or Union type.
> i. Let objectType be ResolveAbstractType(fieldType, result).
> c. Let subSelectionSet be the result of calling MergeSelectionSets(fields).
> d. Return the result of evaluating ExecuteSelectionSet(subSelectionSet, objectType, result, variableValues) normally (allowing for parallelization).
In other words, even if a field's type is an Object (and therefore has a selection set of fields that may also be resolved), if it resolves to null, no further execution happens along that path.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论