为什么GraphQL查询返回null?

Dan*_*den 5 graphql graphql-js apollo-server

我有一个graphql/ apollo-server/ graphql-yoga端点。该端点公开从数据库(或REST端点或其他服务)返回的数据。

我知道我的数据源正在返回正确的数据-如果将调用结果记录到解析器中的数据源,则可以看到正在返回的数据。但是,我的GraphQL字段始终解析为null。

如果我将字段设为非空,则errors在响应中的数组内部会看到以下错误:

无法为不可为空的字段返回null

为什么GraphQL不返回数据?

Dan*_*den 16

您的一个或多个字段解析为null的常见原因有两个:1)返回解析器内部形状错误的数据;和2)没有正确使用Promises。

注意:如果您看到以下错误:

无法为不可为空的字段返回null

根本的问题是您的字段返回null。您仍然可以按照以下概述的步骤尝试解决此错误。

以下示例将引用此简单模式:

type Query {
  post(id: ID): Post
  posts: [Post]
}

type Post {
  id: ID
  title: String
  body: String
}
Run Code Online (Sandbox Code Playgroud)

返回错误形状的数据

我们的架构与请求的查询一起定义data了端点返回的响应中对象的“形状” 。形状是指对象具有的属性,以及这些属性的值是标量值,其他对象还是对象或标量的数组。

In the same way a schema defines the shape of the total response, the type of an individual field defines the shape of that field's value. The shape of the data we return in our resolver must likewise match this expected shape. When it doesn't, we frequently end up with unexpected nulls in our response.

Before we dive into specific examples, though, it's important to grasp how GraphQL resolves fields.

Understanding default resolver behavior

While you certainly can write a resolver for every field in your schema, it's often not necessary because GraphQL.js uses a default resolver when you don't provide one.

At a high level, what the default resolver does is simple: it looks at the value the parent field resolved to and if that value is a JavaScript object, it looks for a property on that Object with the same name as the field being resolved. If it finds that property, it resolves to the value of that property. Otherwise, it resolves to null.

Let's say in our resolver for the post field, we return the value { title: 'My First Post', bod: 'Hello World!' }. If we don't write resolvers for any of the fields on the Post type, we can still request the post:

query {
  post {
    id
    title
    body
  }
}
Run Code Online (Sandbox Code Playgroud)

and our response will be

{
  "data": {
    "post" {
      "id": null,
      "title": "My First Post",
      "body": null,
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

The title field was resolved even though we didn't provide a resolver for it because the default resolver did the heavy lifting -- it saw there was a property named title on the Object the parent field (in this case post) resolved to and so it just resolved to that property's value. The id field resolved to null because the object we returned in our post resolver did not have an id property. The body field also resolved to null because of a typo -- we have a property called bod instead of body!

Pro tip: If bod is not a typo but what an API or database actually returns, we can always write a resolver for the body field to match our schema. For example: (parent) => parent.bod

One important thing to keep in mind is that in JavaScript, almost everything is an Object. So if the post field resolves to a String or a Number, the default resolver for each of the fields on the Post type will still try to find an appropriately named property on the parent object, inevitably fail and return null. If a field has an object type but you return something other than object in its resolver (like a String or an Array), you will not see any error about the type mismatch but the child fields for that field will inevitably resolve to null.

Common Scenario #1: Wrapped Responses

If we're writing the resolver for the post query, we might fetch our code from some other endpoint, like this:

function post (root, args) {
  // axios
  return axios.get(`http://SOME_URL/posts/${args.id}`)
    .then(res => res.data);

  // fetch
  return fetch(`http://SOME_URL/posts/${args.id}`)
    .then(res => res.json());

  // request-promise-native
  return request({
    uri: `http://SOME_URL/posts/${args.id}`,
    json: true
  });
}
Run Code Online (Sandbox Code Playgroud)

The post field has the type Post, so our resolver should return an object with properties like id, title and body. If this is what our API returns, we're all set. However, it's common for the response to actually be an object which contains additional metadata. So the object we actually get back from the endpoint might look something like this:

{
  "status": 200,
  "result": {
    "id": 1,
    "title": "My First Post",
    "body": "Hello world!"
  },
}
Run Code Online (Sandbox Code Playgroud)

In this case, we can't just return the response as-is and expect the default resolver to work correctly, since the object we're returning doesn't have the id , title and body properties we need. Our resolver isn't needs to do something like:

function post (root, args) {
  // axios
  return axios.get(`http://SOME_URL/posts/${args.id}`)
    .then(res => res.data.result);

  // fetch
  return fetch(`http://SOME_URL/posts/${args.id}`)
    .then(res => res.json())
    .then(data => data.result);

  // request-promise-native
  return request({
    uri: `http://SOME_URL/posts/${args.id}`,
    json: true
  })
    .then(res => res.result);
}
Run Code Online (Sandbox Code Playgroud)

Note: The above example fetches data from another endpoint; however, this sort of wrapped response is also very common when using a database driver directly (as opposed to using an ORM)! For example, if you're using node-postgres, you'll get a Result object that includes properties like rows, fields, rowCount and command. You'll need to extract the appropriate data from this response before returning it inside your resolver.

Common Scenario #2: Array Instead of Object

What if we fetch a post from the database, our resolver might look something like this:

function post(root, args, context) {
  return context.Post.find({ where: { id: args.id } })
}
Run Code Online (Sandbox Code Playgroud)

where Post is some model we're injecting through the context. If we're using sequelize, we might call findAll. mongoose and typeorm have find. What these methods have in common is that while they allow us to specify a WHERE condition, the Promises they return still resolve to an array instead of a single object. While there's probably only one post in your database with a particular ID, it's still wrapped in an array when you call one of these methods. Because an Array is still an Object, GraphQL will not resolve the post field as null. But it will resolve all of the child fields as null because it won't be able to find the appropriately named properties on the array.

You can easily fix this scenario by just grabbing the first item in the array and returning that in your resolver:

function post(root, args, context) {
  return context.Post.find({ where: { id: args.id } })
    .then(posts => posts[0])
}
Run Code Online (Sandbox Code Playgroud)

If you're fetching data from another API, this is frequently the only option. On the other hand, if you're using an ORM, there's often a different method that you can use (like findOne) that will explicitly return only a single row from the DB (or null if it doesn't exist).

function post(root, args, context) {
  return context.Post.findOne({ where: { id: args.id } })
}
Run Code Online (Sandbox Code Playgroud)

A special note on INSERT and UPDATE calls: We often expect methods that insert or update a row or model instance to return the inserted or updated row. Often they do, but some methods don't. For example, sequelize's upsert method resolves to a boolean, or tuple of the the upserted record and a boolean (if the returning option is set to true). mongoose's findOneAndUpdate resolves to an object with a value property that contains the modified row. Consult your ORM's documentation and parse the result appropriately before returning it inside your resolver.

Common Scenario #3: Object Instead of Array

In our schema, the posts field's type is a List of Posts, which means its resolver needs to return an Array of objects (or a Promise that resolves to one). We might fetch the posts like this:

function posts (root, args) {
  return fetch('http://SOME_URL/posts')
    .then(res => res.json())
}
Run Code Online (Sandbox Code Playgroud)

However, the actual response from our API might be an object that wraps the the array of posts:

{
  "count": 10,
  "next": "http://SOME_URL/posts/?page=2",
  "previous": null,
  "results": [
    {
      "id": 1,
      "title": "My First Post",
      "body" "Hello World!"
    },
    ...
  ]
}
Run Code Online (Sandbox Code Playgroud)

We can't return this object in our resolver because GraphQL is expecting an Array. If we do, the field will resolve to null and we'll see an error included in our response like:

Expected Iterable, but did not find one for field Query.posts.

Unlike the two scenarios above, in this case GraphQL is able to explicitly check the type of the value we return in our resolver and will throw if it's not an Iterable like an Array.

Like we discussed in the first scenario, in order to fix this error, we have to transform the response into the appropriate shape, for example:

function posts (root, args) {
  return fetch('http://SOME_URL/posts')
    .then(res => res.json())
    .then(data => data.results)
}
Run Code Online (Sandbox Code Playgroud)

Not Using Promises Correctly

GraphQL.js makes use of the Promise API under the hood. As such, a resolver can return some value (like { id: 1, title: 'Hello!' }) or it can return a Promise that will resolve to that value. For fields that have a List type, you may also return an array of Promises. If a Promise rejects, that field will return null and the appropriate error will be added to the errors array in the response. If a field has an Object type, the value the Promise resolves to is what will be passed down as the parent value to the resolvers of any child fields.

A Promise is an "object represents the eventual completion (or failure) of an asynchronous operation, and its resulting value." The next few scenarios outline some common pitfalls encountered when dealing with Promises inside resolvers. However, if you're not familiar with Promises and the newer async/await syntax, it's highly recommended you spend some time reading up on the fundamentals.

Note: the next few examples refer to a getPost function. The implementation details of this function are not important -- it's just a function that returns a Promise, which will resolve to a post object.

Common Scenario #4: Not Returning a Value

A working resolver for the post field might looks like this:

function post(root, args) {
  return getPost(args.id)
}
Run Code Online (Sandbox Code Playgroud)

getPosts returns a Promise and we're returning that Promise. Whatever that Promise resolves to will become the value our field resolves to. Looking good!

But what happens if we do this:

function post(root, args) {
  getPost(args.id)
}
Run Code Online (Sandbox Code Playgroud)

We're still creating a Promise that will resolve to a post. However, we're not returning the Promise, so GraphQL is not aware of it and it will not wait for it to resolve. In JavaScript functions without an explicit return statement implicitly return undefined. So our function creates a Promise and then immediately returns undefined, causing GraphQL to return null for the field.

If the Promise returned by getPost rejects, we won't see any error listed in our response either -- because we didn't return the Promise, the underlying code doesn't care about whether it resolves or rejects. In fact, if the Promise rejects, you'll see an UnhandledPromiseRejectionWarning in your server console.

Fixing this issue is simple -- just add the return.

Common Scenario #5: Not chaining Promises correctly

You decide to log the result of your call to getPost, so you change your resolver to look something like this:

function post(root, args) {
  return getPost(args.id)
    .then(post => {
      console.log(post)
    })
}
Run Code Online (Sandbox Code Playgroud)

When you run your query, you see the result logged in your console, but GraphQL resolves the field to null. Why?

When we call then on a Promise, we're effectively taking the value the Promise resolved to and returning a new Promise. You can think of it kind of like Array.map except for Promises. then can return a value, or another Promise. In either case, what's returned inside of then is "chained" onto the original Promise. Multiple Promises can be chained together like this by using multiple thens. Each Promise in the chain is resolved in sequence, and the final value is what's effectively resolved as the value of the original Promise.

In our example above, we returned nothing inside of the then, so the Promise resolved to undefined, which GraphQL converted to a null. To fix this, we have to return the posts:

function post(root, args) {
  return getPost(args.id)
    .then(post => {
      console.log(post)
      return post // <----
    })
}
Run Code Online (Sandbox Code Playgroud)

If you have multiple Promises you need to resolve inside your resolver, you have to chain them correctly by using then and returning the correct value. For example, if we need to call two other asynchronous functions (getFoo and getBar) before we can call getPost, we can do:

function post(root, args) {
  return getFoo()
    .then(foo => {
      // Do something with foo
      return getBar() // return next Promise in the chain
    })
    .then(bar => {
      // Do something with bar
      return getPost(args.id) // return next Promise in the chain
    })
Run Code Online (Sandbox Code Playgroud)

Pro tip: If you're struggling with correctly chaining Promises, you may find async/await syntax to be cleaner and easier to work with.

Common Scenario #6

Before Promises, the standard way to handle asynchronous code was to use callbacks, or functions that would be called once the asynchronous work was completed. We might, for example, call mongoose's findOne method like this:

function post(root, args) {
  return Post.findOne({ where: { id: args.id } }, function (err, post) {
    return post
  })
Run Code Online (Sandbox Code Playgroud)

The problem here is two-fold. One, a value that's returned inside a callback isn't used for anything (i.e. it's not passed to the underlying code in any way). Two, when we use a callback, Post.findOne doesn't return a Promise; it just returns undefined. In this example, our callback will be called, and if we log the value of post we'll see whatever was returned from the database. However, because we didn't use a Promise, GraphQL doesn't wait for this callback to complete -- it takes the return value (undefined) and uses that.

最受欢迎的库,包括mongoose开箱即用的支持Promises。那些不经常使用免费的“包装”库来添加此功能的库。使用GraphQL解析器时,应避免使用利用回调的方法,而应使用返回Promises的方法。

专家提示:同时支持回调和Promises的库经常以某种方式重载其功能,如果没有提供回调,则该函数将返回Promise。有关详细信息,请查阅库的文档。

如果绝对必须使用回调,也可以将回调包装在Promise中:

function post(root, args) {
  return new Promise((resolve, reject) => {
    Post.findOne({ where: { id: args.id } }, function (err, post) {
      if (err) {
        reject(err)
      } else {
        resolve(post)
      }
    })
  })
Run Code Online (Sandbox Code Playgroud)