Почему запрос GraphQL возвращает ноль? - PullRequest
3 голосов
/ 27 мая 2019

У меня есть конечная точка graphql / apollo-server / graphql-yoga.Эта конечная точка предоставляет данные, возвращенные из базы данных (или конечной точки REST, или какой-либо другой службы).

Я знаю, что мой источник данных возвращает правильные данные - если я записываю результат вызова в источник данных внутримой распознаватель, я вижу возвращаемые данные.Тем не менее, мои поля (поля) GraphQL всегда принимают значение NULL.

Если я сделаю поле ненулевым, в ответе в массиве errors я вижу следующую ошибку:

Невозможно вернуть ноль для ненулевого поля

Почему GraphQL не возвращает данные?

1 Ответ

9 голосов
/ 27 мая 2019

Есть две распространенные причины, по которым ваше поле или поля имеют нулевое значение: 1) возвращение данных в неправильной форме внутри вашего резольвера;и 2) не правильно использует Обещания.

Примечание: , если вы видите следующую ошибку:

Невозможно возвратить ноль для поля, не обнуляемого

Основная проблема заключается в том, что ваше поле возвращает ноль.Вы все еще можете выполнить шаги, описанные ниже, чтобы попытаться устранить эту ошибку.

Следующие примеры будут ссылаться на эту простую схему:

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

type Post {
  id: ID
  title: String
  body: String
}

Возвращение данных в неправильной форме

Наша схема вместе с запрошенным запросом определяет «форму» объекта data в ответе, возвращаемом нашей конечной точкой.Под формой мы понимаем, какие свойства имеют объекты и являются ли значения этих свойств скалярными значениями, другими объектами или массивами объектов или скалярами.

Точно так же схема определяет форму полного откликатип отдельного поля определяет форму значения этого поля.Форма данных, которые мы возвращаем в нашем преобразователе, должна также соответствовать этой ожидаемой форме.Если этого не происходит, мы часто получаем неожиданные значения NULL в нашем ответе.

Прежде чем мы углубимся в конкретные примеры, важно понять, как GraphQL разрешает поля.

Понимание распознавателя по умолчаниюповедение

Хотя вы, конечно, можете написать преобразователь для каждого поля в вашей схеме, это часто не требуется, поскольку GraphQL.js использует преобразователь по умолчанию, когда вы его не предоставляете.

На высоком уровне распознаватель по умолчанию прост: он смотрит на значение, разрешенное в поле parent , и, если это значение является объектом JavaScript, он ищет свойство этого объекта с то же имя , что и разрешаемое поле.Если он находит это свойство, он разрешается до значения этого свойства.В противном случае он разрешается до нуля.

Допустим, в нашем преобразователе для поля post мы возвращаем значение { title: 'My First Post', bod: 'Hello World!' }.Если мы не напишем средства распознавания для каких-либо полей типа Post, мы все равно можем запросить post:

query {
  post {
    id
    title
    body
  }
}

, и наш ответ будет

{
  "data": {
    "post" {
      "id": null,
      "title": "My First Post",
      "body": null,
    }
  }
}

Поле title было разрешено, хотя мы не предоставили для него определитель, потому что решатель по умолчанию проделал большую работу - он увидел свойство с именем title в родительском поле Object (в данном случае *)1051 *) разрешено и поэтому просто разрешено до значения этого свойства.Поле id разрешено в ноль, потому что объект, который мы вернули в нашем преобразователе post, не имел свойства id.Поле body также обнулено из-за опечатки - у нас есть свойство с именем bod вместо body!

Pro tip : If bod is не опечатка, но то, что фактически возвращает API или база данных, мы всегда можем написать преобразователь для поля body, чтобы соответствовать нашей схеме.Например: (parent) => parent.bod

Важно помнить, что в JavaScript почти все является объектом .Поэтому, если поле post преобразуется в строку или число, распознаватель по умолчанию для каждого из полей типа Post все равно будет пытаться найти свойство с соответствующим именем в родительском объекте, что неизбежно приведет к сбою и возврату null.Если поле имеет тип объекта, но вы возвращаете что-то отличное от объекта в его преобразователе (например, String или Array), вы не увидите никакой ошибки о несоответствии типов, но дочерние поля для этого поля неизбежно будут иметь нулевое значение.

Общий сценарий № 1: Упакованные ответы

Если мы пишем распознаватель для запроса post, мы могли бы извлечь наш код из другой конечной точки, например:

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
  });
}

Поле post имеет тип Post, поэтому наш распознаватель должен вернуть объект со свойствами, такими как id, title и body. Если это то, что возвращает наш API, мы готовы. Однако , обычно ответом является объект, который содержит дополнительные метаданные. Таким образом, объект, который мы на самом деле возвращаем из конечной точки, может выглядеть примерно так:

{
  "status": 200,
  "result": {
    "id": 1,
    "title": "My First Post",
    "body": "Hello world!"
  },
}

В этом случае мы не можем просто вернуть ответ как есть и ожидать, что распознаватель по умолчанию будет работать правильно, так как возвращаемый объект не имеет id, title и body свойства нам нужны. Нашему распознавателю не нужно делать что-то вроде:

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);
}

Примечание : приведенный выше пример извлекает данные из другой конечной точки; однако этот вид упакованного ответа также очень распространен при непосредственном использовании драйвера базы данных (в отличие от использования ORM)! Например, если вы используете node-postgres , вы получите объект Result, который включает такие свойства, как rows, fields, rowCount и command. Вам нужно извлечь соответствующие данные из этого ответа, прежде чем возвращать их в свой распознаватель.

Общий сценарий № 2: Массив вместо объекта

Что если мы получим сообщение из базы данных, наш распознаватель может выглядеть примерно так:

function post(root, args, context) {
  return context.Post.find({ where: { id: args.id } })
}

где Post - это некоторая модель, которую мы вводим через контекст. Если мы используем sequelize, мы можем позвонить findAll. mongoose и typeorm имеют find. Общим для этих методов является то, что, хотя они позволяют нам задавать условие WHERE, обещания, которые они возвращают , по-прежнему разрешаются в массив вместо одного объекта . Хотя в вашей базе данных, вероятно, есть только одна запись с определенным идентификатором, она все равно помещается в массив при вызове одного из этих методов. Поскольку массив все еще является объектом, GraphQL не разрешит поле post как ноль. Но разрешит все дочерние поля как нулевые, поскольку не сможет найти свойства с соответствующим именем в массиве.

Вы можете легко исправить этот сценарий, просто взяв первый элемент в массиве и вернув его в свой преобразователь:

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

Если вы выбираете данные из другого API, это часто единственный вариант. С другой стороны, если вы используете ORM, часто есть другой метод, который вы можете использовать (например, findOne), который будет явно возвращать только одну строку из БД (или ноль, если он не существует).

function post(root, args, context) {
  return context.Post.findOne({ where: { id: args.id } })
}

Специальное примечание по INSERT и UPDATE вызывает : мы часто ожидаем, что методы, которые вставляют или обновляют строку или экземпляр модели, возвращают вставленную или обновленную строку. Часто они делают, но некоторые методы не делают. Например, метод sequelize upsert преобразуется в логическое значение или кортеж загруженной записи и логического значения (если для параметра returning установлено значение true). mongoose findOneAndUpdate разрешается в объект со свойством value, которое содержит измененную строку. Обратитесь к документации своего ORM и проанализируйте результат надлежащим образом, прежде чем возвращать его в свой распознаватель.

Общий сценарий № 3: объект вместо массива

В нашей схеме тип поля posts имеет значение List из Post s, что означает, что его распознаватель должен возвращать массив объектов (или Promise, который разрешается до одного). Мы могли бы получить сообщения как это:

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

Однако фактическим ответом нашего API может быть объект, который оборачивает массив сообщений:

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

Мы не можем вернуть этот объект в нашем преобразователе, потому что GraphQL ожидает массив. Если мы это сделаем, поле станет пустым, и мы увидим ошибку, включенную в наш ответ, например:

Ожидаемый итерируемый, но не найден для поля Query.posts.

В отличие от двух вышеописанных сценариев, в этом случае GraphQL может явно проверять тип значения, которое мы возвращаем в нашем преобразователе, и будет выдавать, если это не Итерируемый как массив.

Как мы обсуждали в первом сценарии, чтобы исправить эту ошибку, мы должны преобразовать ответ в соответствующую форму, например:

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

Правильное использование обещаний

GraphQL.js использует API Promise под капотом. Таким образом, распознаватель может вернуть какое-то значение (например, { id: 1, title: 'Hello!' }) или вернуть Обещание, которое разрешит к этому значению. Для полей с типом List вы также можете вернуть массив Promises. Если Promise отклоняется, это поле вернет ноль, и соответствующая ошибка будет добавлена ​​в массив errors в ответе. Если поле имеет тип объекта, то значение, которое разрешает Обещание, - это то, что будет передано в качестве родительского значения распознавателям любых дочерних полей.

A Обещание - это «объект, представляющий возможное завершение (или сбой) асинхронной операции и ее результирующее значение». Следующие несколько сценариев описывают некоторые распространенные ошибки, возникающие при работе с Promises внутри распознавателей. Однако, если вы не знакомы с Promises и более новым синтаксисом async / await, настоятельно рекомендуется потратить некоторое время на изучение основ.

Примечание : следующие несколько примеров относятся к функции getPost. Детали реализации этой функции не важны - это просто функция, которая возвращает Promise, который преобразуется в объект post.

Общий сценарий № 4: не возвращает значение

Рабочий распознаватель для поля post может выглядеть следующим образом:

function post(root, args) {
  return getPost(args.id)
}

getPosts возвращает обещание, и мы возвращаем это обещание. Все, что обещает Обещание, станет ценностью, к которой стремится наше поле. Хорошо выглядит!

Но что произойдет, если мы сделаем это:

function post(root, args) {
  getPost(args.id)
}

Мы все еще создаем Обещание, которое будет преобразовано в сообщение. Однако мы не возвращаем Обещание, поэтому GraphQL не знает об этом и не будет ждать его разрешения. В функциях JavaScript без явного оператора return неявно возвращается undefined. Таким образом, наша функция создает Promise, а затем немедленно возвращает undefined, в результате чего GraphQL возвращает null для поля.

Если обещание, возвращенное getPost, отклоняется, мы не увидим ни одной ошибки, указанной в нашем ответе - поскольку мы не возвращали обещание, базовый код не заботится о том, будет ли он разрешен или отклонен. Фактически, если Promise отклоняется, вы увидите UnhandledPromiseRejectionWarning в консоли вашего сервера.

Исправить эту проблему просто - просто добавьте return.

Общий сценарий № 5: Не правильная цепочка обещаний

Вы решили записать результат вашего звонка в getPost, поэтому вы меняете свой резольвер, чтобы он выглядел примерно так:

function post(root, args) {
  return getPost(args.id)
    .then(post => {
      console.log(post)
    })
}

Когда вы выполняете запрос, вы видите результат, записанный в вашей консоли, но GraphQL разрешает это поле как ноль. Почему?

Когда мы звоним then по Обещанию, мы фактически принимаем значение, которое Обещание решило, и возвращаем новое Обещание. Вы можете думать об этом вроде Array.map за исключением Обещаний. then может вернуть значение или другое обещание. В любом случае, то, что возвращается внутри then, «приковано» к исходному обещанию. Несколько обещаний можно связать вместе, используя несколько then с. Каждое Обещание в цепочке разрешается в последовательности, и окончательное значение - это то, что эффективно разрешается как значение исходного Обещания.

В нашем примере выше мы ничего не возвращали внутри then, поэтому Promise разрешился в undefined, что GraphQL преобразовало в ноль. Чтобы это исправить, мы должны вернуть сообщения:

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

Если у вас есть несколько Обещаний, которые вам нужно разрешить внутри вашего распознавателя, вы должны правильно их связать, используя then и возвращая правильное значение. Например, если нам нужно вызвать две другие асинхронные функции (getFoo и getBar), прежде чем мы сможем вызвать getPost, мы можем сделать:

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
    })

Совет Pro: Если вы боретесь с правильным построением цепочек Promises, вы можете найти синтаксис async / await более понятным и удобным для работы.

Общий сценарий #6

До Promises стандартным способом обработки асинхронного кода было использование обратных вызовов или функций, которые будут вызываться после завершения асинхронной работы.Мы могли бы, например, вызвать mongoose метод findOne следующим образом:

function post(root, args) {
  return Post.findOne({ where: { id: args.id } }, function (err, post) {
    return post
  })

Проблема здесь двоякая.Во-первых, значение, которое возвращается внутри обратного вызова, ни для чего не используется (то есть не передается в базовый код никоим образом).Во-вторых, когда мы используем обратный вызов, Post.findOne не возвращает обещание;он просто возвращает неопределенное.В этом примере будет вызван наш обратный вызов, и если мы зарегистрируем значение post, мы увидим все, что было возвращено из базы данных.Однако, поскольку мы не использовали Promise, GraphQL не ждет завершения этого обратного вызова - он принимает возвращаемое значение (не определено) и использует его.

Наиболее популярные библиотеки, включая mongooseПоддержка обещаний из коробки.Те, которые не часто имеют бесплатные библиотеки «оболочки», которые добавляют эту функциональность. При работе с распознавателями GraphQL следует избегать использования методов, использующих обратный вызов, и вместо этого использовать методы, которые возвращают Promises.

Совет Pro: Библиотеки, которыеподдерживают как обратные вызовы, так и обещания, часто перегружают их функции таким образом, что, если обратный вызов не предусмотрен, функция возвращает обещание.Подробности смотрите в документации библиотеки.

Если вам абсолютно необходимо использовать обратный вызов, вы также можете заключить его в Обещание:

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)
      }
    })
  })
...