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

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

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

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

Cannot return null for non-nullable field

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

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

Daniel Rearden 27.05.2019 05:33

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

xadm 14.04.2020 08:14
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Что такое Apollo Client и зачем он нужен?
Что такое Apollo Client и зачем он нужен?
Apollo Client - это полнофункциональный клиент GraphQL для JavaScript-приложений, который упрощает получение, управление и обновление данных в...
29
2
42 492
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

Ответ принят как подходящий

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

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

Cannot return null for non-nullable field

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

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

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

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

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

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

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

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

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

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

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

Следует помнить одну важную вещь: в 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)! Например, если вы используете узел-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), который будет явно возвращать только одну строку из БД (или null, если она не существует).

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

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

Распространенный сценарий № 3: объект вместо массива

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

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, и мы увидим ошибку, включенную в наш ответ, например:

Expected Iterable, but did not find one for field 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 вы также можете вернуть массив промисов. Если обещание отклонено, это поле вернет значение null, и соответствующая ошибка будет добавлена ​​в массив errors в ответе. Если поле имеет тип Object, то значение, в которое преобразуется Promise, будет передано как родительское значение преобразователям любых дочерних полей.

Обещать — это «объект, представляющий возможное завершение (или сбой) асинхронной операции и его результирующее значение». Следующие несколько сценариев описывают некоторые распространенные ловушки, возникающие при работе с промисами внутри преобразователей. Однако, если вы не знакомы с промисами и новым синтаксисом 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, отклонено, мы также не увидим никаких ошибок в нашем ответе — поскольку мы не вернули обещание, базовый код не заботится о том, разрешается оно или отклоняется. На самом деле, если Promise отклоняется, вы увидите UnhandledPromiseRejectionWarning в консоли сервера.

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

Распространенный сценарий № 5: Неправильная цепочка промисов

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

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

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

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

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

function post(root, args) {
  return getPost(args.id)
    .then(post => {
      console.info(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 tip: If you're struggling with correctly chaining Promises, you may find async/await syntax to be cleaner and easier to work with.

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

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

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

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

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

Pro tip: Libraries that support both callbacks and Promises frequently overload their functions in such a way that if a callback is not provided, the function will return a Promise. Check the library's documentation for details.

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

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

У меня была такая же проблема с 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);
  }
}

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

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

Atsuhiro Teshima 01.02.2020 09:18

Если ничего из вышеперечисленного не помогло, и у вас есть глобальный перехватчик, который охватывает все ответы, например, внутри поля «данные», вы должны отключить это для 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
        };
      }));
  }

В случае, если кто-то использовал 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 и разрешения.

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

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

Failure performing sync query to AppSync: [GraphQLResponse.Error{message='Cannot return null for non-nullable type: 'AWSTimestamp' within 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 и снова измените ее во время тестирования. Так что это не устойчивое решение, но болтовня, которую я нашел в Интернете, предполагает, что очень скоро появятся улучшения, чтобы усилить дрожание.

Другие вопросы по теме