Почему запрос GraphQL возвращает null?

avatar
Daniel Rearden
27 мая 2019 в 02:53
39302
6
26

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

.

Я знаю, что мой источник данных возвращает правильные данные — если я запишу результат обращения к источнику данных внутри своего преобразователя, я увижу возвращаемые данные. Однако мои поля GraphQL всегда разрешаются в null.

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

Невозможно вернуть значение NULL для поля, не допускающего значение NULL

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

Источник
Daniel Rearden
27 мая 2019 в 03:33
0

Примечание. Этот вопрос предназначен для использования в качестве справочного вопроса и потенциального обмана для подобных вопросов. Вот почему вопрос является широким и опускает какие-либо конкретные детали кода или схемы. Дополнительные сведения см. в этом метасообщении.

xadm
14 апреля 2020 в 06:14
1

Я думаю, вам следует изменить заголовок, так как его по-прежнему нелегко найти по «Невозможно вернуть значение null для поля, допускающего значение NULL» или даже «[graphql] Невозможно вернуть значение null для поля, допускающего значение NULL». поле, допускающее значение null - почему оно возвращает значение null?" ?

Ответы (6)

avatar
Daniel Rearden
27 мая 2019 в 02:53
68

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

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

Невозможно вернуть значение NULL для поля, не допускающего значение NULL

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

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

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

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

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

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

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

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

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

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

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

.

Скажем, в нашем распознавателе для поля 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 в родительском поле объекта (в этом case post) разрешается в и поэтому просто разрешается в значение этого свойства. Поле id разрешено как null, потому что объект, который мы вернули в нашем преобразователе post, не имел свойства id. Поле body также разрешено как null из-за опечатки — у нас есть свойство с именем bod вместо body!

Совет: если bod является не опечаткой, а тем, что на самом деле возвращает API или база данных, мы всегда можем написать сопоставитель для нашего поля <5727570>497 схема. Например: (parent) => parent.bod

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

Распространенный сценарий №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 и <572758750>. Вам нужно будет извлечь соответствующие данные из этого ответа, прежде чем возвращать его в ваш преобразователь.

Общий сценарий #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), который явно возвращает только одну строку из БД (или null, если она не существует).

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, что означает, что его сопоставитель должен возвращать массив объектов (или обещание, которое разрешается в единицу). Мы могли бы получить такие сообщения:

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 ожидает массив. Если мы это сделаем, поле будет иметь значение null, и мы увидим ошибку, включенную в наш ответ, например:

.

Ожидаемый объект Iterable, но не найден для поля Query.posts.

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

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

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!' }) или может возвращать обещание, которое будет resolve для этого значения. Для полей типа List вы также можете вернуть массив промисов. Если обещание отклонено, это поле вернет значение null, и соответствующая ошибка будет добавлена ​​в массив errors в ответе. Если поле имеет тип Object, то значение, в которое преобразуется Promise, будет передано как родительское значение преобразователям любых дочерних полей.

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

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

Распространенный сценарий №4: не возвращается значение

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

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

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

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

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

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

Если промис, возвращенный getPost, отклоняется, мы также не увидим никаких ошибок в нашем ответе — поскольку мы не вернули промис, базовый код не заботится о том, разрешается он или отклоняется. На самом деле, если обещание отклонено, вы увидите 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, поэтому промис разрешился в 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
    })

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

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

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

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

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

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

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

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

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)
      }
    })
  })
avatar
Ojonugwa Jude Ochalifu
26 октября 2021 в 16:57
2

Иду от Флаттера. Я не мог найти никакого решения, связанного с флаттером, поэтому, поскольку мой поиск всегда приводил меня сюда, давайте просто добавим его сюда.

Точная ошибка:

Не удалось выполнить запрос синхронизации к AppSync: [GraphQLResponse.Error{message='Невозможно вернуть значение null для ненулевого значения тип: 'AWSTimestamp' внутри parent

Итак, в моей схеме (в консоли AppSync) у меня было следующее:

type TypeName {
   id: ID!
   ...
   _version: Int!
   _deleted: Boolean
   _lastChangedAt: AWSTimestamp!
   createdAt: AWSDateTime!
   updatedAt: AWSDateTime!
 }

Я получил ошибку из поля _lastChangedAt, так как AWSTimestamp не может быть нулевым.

Все, что мне нужно было сделать, это remove the null-check (!) from the field, и проблема была решена.

Теперь я не знаю последствий этого в долгосрочной перспективе, но при необходимости обновлю этот ответ.

РЕДАКТИРОВАТЬ: Как я выяснил, все, что я делаю, означает, amplify.push, что изменение отменяется. Просто вернитесь в консоль appsync и снова измените ее во время тестирования. Так что это ненадежное решение, но болтовня, которую я нашел в Интернете, предполагает, что очень скоро появятся улучшения, чтобы усилить дрожание.

avatar
James L.
4 октября 2021 в 06:14
0

Я столкнулся с этой ошибкой при запросе вложенного объекта, но мой преобразователь не расширил объект. Это не произошло автоматически, как я ожидал. Как только я заставил его расшириться, ошибка исчезла

avatar
Abhishek Kumar
20 мая 2021 в 05:35
1

В случае, если кто-то использовал apollo-server-express и получил нулевое значение.

const typeDefs = require('./schema');
const resolvers = require('./resolver');

const server = new ApolloServer({typeDefs,resolvers});

Это вернет значение, как вы ожидаете.

const withDifferentVarNameSchema = require('./schema');
const withDifferentVarNameResolver= require('./resolver');

const server = new ApolloServer({withDifferentVarNameSchema,withDifferentVarNameResolver});

Это вернет null не так, как ожидалось.

Примечание. При создании экземпляра Apolloserver передайте только имя переменной typeDefs и преобразователя.

avatar
Dima Grossman
19 февраля 2020 в 15:04
0

Если ничего из вышеперечисленного не помогло, и у вас есть глобальный перехватчик, который упаковывает все ответы, например, в поле «данные», вы должны отключить это для graphql. Другие мудрые преобразователи graphql конвертируют в null.

Вот что я сделал с перехватчиком в моем случае:

  intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Observable<Response<T>> {
    if (context['contextType'] === 'graphql') return next.handle();

    return next
      .handle()
      .pipe(map(data => {
        return {
          data: isObject(data) ? this.transformResponse(data) : data
        };
      }));
  }
avatar
Atsuhiro Teshima
1 февраля 2020 в 08:14
4

У меня была такая же проблема с Nest.js.

Если вы хотите решить проблему. Вы можете добавить параметр {nullable: true} в декоратор @Query.

Вот пример.

@Resolver(of => Team)
export class TeamResolver {
  constructor(
    private readonly teamService: TeamService,
    private readonly memberService: MemberService,
  ) {}

  @Query(returns => Team, { name: 'team', nullable: true })
  @UseGuards(GqlAuthGuard)
  async get(@Args('id') id: string) {
    return this.teamService.findOne(id);
  }
}

Затем вы можете вернуть нулевой объект для запроса.

enter image description here

Atsuhiro Teshima
1 февраля 2020 в 08:18
0

Я разместил этот ответ здесь, потому что вопрос по этому URL-адресу (coderhelper.com/questions/58140891/…) помечен как дублирование этого вопроса.