web-dev-qa-db-fra.com

Pourquoi une requête GraphQL retourne null?

J'ai un graphql/apollo-server/graphql-yoga point final. Ce point de terminaison expose les données renvoyées par une base de données (ou un point de terminaison REST ou un autre service).

Je sais que ma source de données renvoie les données correctes - si j'enregistre le résultat de l'appel à la source de données dans mon résolveur, je peux voir les données renvoyées. Cependant, mes champs GraphQL résolvent toujours à null.

Si je rend le champ non nul, je vois l'erreur suivante dans le tableau errors dans la réponse:

Impossible de retourner null pour le champ non nullable

Pourquoi GraphQL ne retourne pas les données?

9
Daniel Rearden

Il y a deux raisons courantes pour lesquelles votre champ ou vos champs sont résolus à null: 1) renvoyer des données dans la mauvaise forme à l'intérieur de votre résolveur; et 2) ne pas utiliser correctement Promises.

Remarque: si vous voyez l'erreur suivante:

Impossible de retourner null pour le champ non nullable

le problème sous-jacent est que votre champ retourne null. Vous pouvez toujours suivre les étapes décrites ci-dessous pour essayer de résoudre cette erreur.

Les exemples suivants feront référence à ce schéma simple:

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

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

Renvoyer des données dans une mauvaise forme

Notre schéma, avec la requête demandée, définit la "forme" de l'objet data dans la réponse retournée par notre point de terminaison. Par forme, nous entendons les propriétés des objets et si ces "valeurs" de propriétés sont des valeurs scalaires, d'autres objets ou des tableaux d'objets ou de scalaires.

De la même manière qu'un schéma définit la forme de la réponse totale, le type d'un champ individuel définit la forme de la valeur de ce champ. La forme des données que nous renvoyons dans notre résolveur doit également correspondre à cette forme attendue. Lorsque ce n'est pas le cas, nous nous retrouvons fréquemment avec des valeurs nulles inattendues dans notre réponse.

Avant de nous plonger dans des exemples spécifiques, cependant, il est important de comprendre comment GraphQL résout les champs.

Comprendre le comportement du résolveur par défaut

Bien que vous pouvez écrivez un résolveur pour chaque champ de votre schéma, ce n'est souvent pas nécessaire car GraphQL.js utilise un résolveur par défaut lorsque vous n'en fournissez pas.

À un niveau élevé, ce que fait le résolveur par défaut est simple: il examine la valeur dans laquelle le champ parent est résolu et si cette valeur est un objet JavaScript, il recherche une propriété sur cet objet avec le même nom que le champ en cours de résolution. S'il trouve cette propriété, il se résout à la valeur de cette propriété. Sinon, il se résout à null.

Disons que dans notre résolveur pour le champ post, nous retournons la valeur { title: 'My First Post', bod: 'Hello World!' }. Si nous n'écrivons pas de résolveurs pour aucun des champs du type Post, nous pouvons toujours demander le post:

query {
  post {
    id
    title
    body
  }
}

et notre réponse sera

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

Le champ title a été résolu même si nous n'avons pas fourni de résolveur car le résolveur par défaut a fait le gros du travail - il a vu qu'il y avait une propriété nommée title sur le champ Objet le parent (dans ce cas post) résolu et donc il vient de se résoudre à la valeur de cette propriété. Le champ id s'est résolu en null car l'objet que nous avons renvoyé dans notre résolveur post n'avait pas de propriété id. Le champ body a également été résolu en null à cause d'une faute de frappe - nous avons une propriété appelée bod au lieu de body!

Astuce Pro : Si bod est pas une faute de frappe mais ce qu'une API ou une base de données retourne réellement, nous pouvons toujours écrire un résolveur pour le champ body pour correspondre à notre schéma. Par exemple: (parent) => parent.bod

Une chose importante à garder à l'esprit est qu'en JavaScript, presque tout est un objet. Donc, si le champ post se résout en une chaîne ou un nombre, le résolveur par défaut pour chacun des champs du type Post essaiera toujours de trouver une propriété nommée de manière appropriée sur l'objet parent, inévitablement échouer et retourner null. Si un champ a un type d'objet mais que vous renvoyez autre chose qu'un objet dans son résolveur (comme une chaîne ou un tableau), vous ne verrez aucune erreur concernant la non-concordance de type mais les champs enfants de ce champ seront inévitablement résolus à null.

Scénario commun n ° 1: réponses enveloppées

Si nous écrivons le résolveur pour la requête post, nous pouvons récupérer notre code à partir d'un autre point de terminaison, comme ceci:

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

Le champ post a le type Post, donc notre résolveur doit retourner un objet avec des propriétés comme id, title et body. Si c'est ce que notre API renvoie, nous sommes tous prêts. Cependant , il est courant que la réponse soit en fait un objet contenant des métadonnées supplémentaires. Ainsi, l'objet que nous récupérons réellement du point de terminaison pourrait ressembler à ceci:

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

Dans ce cas, nous ne pouvons pas simplement renvoyer la réponse telle quelle et nous attendre à ce que le résolveur par défaut fonctionne correctement, car l'objet que nous renvoyons n'a pas les id, title et body propriétés dont nous avons besoin. Notre résolveur n'a pas besoin de faire quelque chose comme:

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

Remarque : l'exemple ci-dessus récupère les données d'un autre point de terminaison; cependant, ce type de réponse encapsulée est également très courant lors de l'utilisation directe d'un pilote de base de données (par opposition à l'utilisation d'un ORM)! Par exemple, si vous utilisez node-postgres , vous obtiendrez un objet Result qui inclut des propriétés comme rows, fields, rowCount et command. Vous devrez extraire les données appropriées de cette réponse avant de les renvoyer dans votre résolveur.

Scénario commun n ° 2: un tableau au lieu d'un objet

Et si nous récupérons une publication dans la base de données, notre résolveur pourrait ressembler à ceci:

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

Post est un modèle que nous injectons à travers le contexte. Si nous utilisons sequelize, nous pouvons appeler findAll. mongoose et typeorm ont find. Ce que ces méthodes ont en commun, c'est que même si elles nous permettent de spécifier une condition WHERE, les promesses qu'elles renvoient sont toujours résolues en un tableau au lieu d'un seul objet . Bien qu'il n'y ait probablement qu'une seule publication dans votre base de données avec un ID particulier, elle est toujours enveloppée dans un tableau lorsque vous appelez l'une de ces méthodes. Parce qu'un tableau est toujours un objet, GraphQL ne résoudra pas le champ post comme nul. Mais cela va résoudre tous les champs enfants comme étant nuls car il ne pourra pas trouver les propriétés nommées de manière appropriée sur le tableau.

Vous pouvez facilement résoudre ce scénario en saisissant simplement le premier élément du tableau et en le renvoyant dans votre résolveur:

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

Si vous récupérez des données à partir d'une autre API, c'est souvent la seule option. D'un autre côté, si vous utilisez un ORM, il existe souvent une méthode différente que vous pouvez utiliser (comme findOne) qui ne renverra explicitement qu'une seule ligne de la base de données (ou null si ce n'est pas le cas) exister).

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

Une note spéciale sur les appels INSERT et UPDATE: nous attendons souvent des méthodes qui insèrent ou mettent à jour une ligne ou une instance de modèle pour renvoyer la ligne insérée ou mise à jour. Souvent, ils le font, mais certaines méthodes ne le font pas. Par exemple, la méthode sequelize de upsert se résout en un booléen, ou un tuple de l'enregistrement inversé et un booléen (si l'option returning est définie sur true). mongoose's findOneAndUpdate se résout en un objet avec une propriété value qui contient la ligne modifiée. Consultez la documentation de votre ORM et analysez le résultat de manière appropriée avant de le renvoyer dans votre résolveur.

Scénario commun n ° 3: objet au lieu d'un tableau

Dans notre schéma, le type du champ posts est un List de Posts, ce qui signifie que son résolveur doit retourner un tableau d'objets (ou une promesse qui se résout en un). Nous pourrions récupérer les messages comme ceci:

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

Cependant, la réponse réelle de notre API peut être un objet qui enveloppe le tableau des publications:

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

Nous ne pouvons pas renvoyer cet objet dans notre résolveur car GraphQL attend un tableau. Si nous le faisons, le champ se résoudra en null et nous verrons une erreur incluse dans notre réponse comme:

Iterable attendu, mais n'en a pas trouvé pour le champ Query.posts.

Contrairement aux deux scénarios ci-dessus, dans ce cas, GraphQL est capable de vérifier explicitement le type de la valeur que nous retournons dans notre résolveur et lancera si ce n'est pas un Iterable comme un tableau.

Comme nous l'avons vu dans le premier scénario, afin de corriger cette erreur, nous devons transformer la réponse en la forme appropriée, par exemple:

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

Ne pas utiliser correctement les promesses

GraphQL.js utilise l'API Promise sous le capot. En tant que tel, un résolveur peut renvoyer une valeur (comme { id: 1, title: 'Hello!' }) ou il peut renvoyer une promesse qui résoudra à cette valeur. Pour les champs qui ont un type List, vous pouvez également renvoyer un tableau de promesses. Si une promesse rejette, ce champ renverra null et l'erreur appropriée sera ajoutée au tableau errors dans la réponse. Si un champ a un type d'objet, la valeur à laquelle la promesse se résout est celle qui sera transmise en tant que valeur parent aux résolveurs des champs enfants.

Un Promesse est un "objet représente l'achèvement (ou l'échec) éventuel d'une opération asynchrone et sa valeur résultante." Les scénarios suivants décrivent certains pièges courants rencontrés lors de la gestion des promesses dans les résolveurs. Cependant, si vous n'êtes pas familier avec Promises et la nouvelle syntaxe asynchrone/wait, il est fortement recommandé de passer un peu de temps à lire les principes fondamentaux.

Remarque : les quelques exemples suivants font référence à une fonction getPost. Les détails d'implémentation de cette fonction ne sont pas importants - c'est juste une fonction qui renvoie une promesse, qui se résoudra en un objet de publication.

Scénario commun n ° 4: ne pas renvoyer de valeur

Un résolveur de travail pour le champ post pourrait ressembler à ceci:

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

getPosts renvoie une promesse et nous la retournons. Quelle que soit la résolution de cette promesse, elle deviendra la valeur de notre domaine. Vous cherchez bien!

Mais que se passe-t-il si nous faisons cela:

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

Nous créons toujours une promesse qui se résoudra en une publication. Cependant, nous ne renvoyons pas la promesse, donc GraphQL n'est pas au courant et il n'attendra pas sa résolution. Dans les fonctions JavaScript sans instruction return explicite, renvoie implicitement undefined. Ainsi, notre fonction crée une promesse et retourne immédiatement undefined, ce qui oblige GraphQL à retourner null pour le champ.

Si la promesse renvoyée par getPost rejette, nous ne verrons aucune erreur répertoriée dans notre réponse - car nous n'avons pas renvoyé la promesse, le code sous-jacent ne se soucie pas de savoir s'il résout ou rejette. En fait, si la promesse rejette, vous verrez un UnhandledPromiseRejectionWarning dans votre console de serveur.

La résolution de ce problème est simple - il suffit d'ajouter le return.

Scénario commun n ° 5: ne pas chaîner correctement les promesses

Vous décidez de consigner le résultat de votre appel dans getPost, vous changez donc votre résolveur pour qu'il ressemble à ceci:

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

Lorsque vous exécutez votre requête, vous voyez le résultat enregistré dans votre console, mais GraphQL résout le champ à null. Pourquoi?

Lorsque nous appelons then sur une promesse, nous prenons effectivement la valeur à laquelle la promesse a été résolue et retournons une nouvelle promesse. Vous pouvez y penser un peu comme Array.map sauf pour les promesses. then peut renvoyer une valeur ou une autre promesse. Dans les deux cas, ce qui est retourné à l'intérieur de then est "enchaîné" sur la promesse d'origine. Plusieurs promesses peuvent être chaînées comme ceci en utilisant plusieurs thens. Chaque promesse de la chaîne est résolue en séquence, et la valeur finale est ce qui est effectivement résolu comme la valeur de la promesse d'origine.

Dans notre exemple ci-dessus, nous n'avons retourné rien à l'intérieur du then, donc la promesse s'est résolue en undefined, que GraphQL a converti en null. Pour résoudre ce problème, nous devons renvoyer les messages:

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

Si vous avez plusieurs promesses que vous devez résoudre dans votre résolveur, vous devez les enchaîner correctement en utilisant then et en renvoyant la valeur correcte. Par exemple, si nous devons appeler deux autres fonctions asynchrones (getFoo et getBar) avant de pouvoir appeler getPost, nous pouvons faire:

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

Astuce de pro: Si vous avez du mal à enchaîner correctement les promesses, vous pouvez trouver la syntaxe async/wait pour être plus propre et plus facile à travailler.

Scénario commun n ° 6

Avant Promises, la façon standard de gérer le code asynchrone était d'utiliser des rappels ou des fonctions qui seraient appelées une fois le travail asynchrone terminé. Nous pourrions, par exemple, appeler la méthode mongoose de findOne comme ceci:

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

Le problème ici est double. Premièrement, une valeur renvoyée dans un rappel n'est utilisée pour rien (c'est-à-dire qu'elle n'est en aucun cas transmise au code sous-jacent). Deuxièmement, lorsque nous utilisons un rappel, Post.findOne ne renvoie pas de promesse; il renvoie juste indéfini. Dans cet exemple, notre rappel sera appelé, et si nous enregistrons la valeur de post, nous verrons tout ce qui a été renvoyé de la base de données. Cependant, comme nous n'avons pas utilisé de promesse, GraphQL n'attend pas que ce rappel se termine - il prend la valeur de retour (non définie) et l'utilise.

Les bibliothèques les plus populaires, y compris mongoose, prennent en charge les promesses prêtes à l'emploi. Ceux qui n'ont pas souvent de bibliothèques "wrapper" complémentaires qui ajoutent cette fonctionnalité. Lorsque vous travaillez avec des résolveurs GraphQL, vous devez éviter d'utiliser des méthodes qui utilisent un rappel, et plutôt utiliser celles qui renvoient des promesses.

Astuce Pro: Les bibliothèques qui prennent en charge les rappels et les promesses surchargent fréquemment leurs fonctions de telle sorte que si aucun rappel n'est fourni, la fonction renverra une promesse . Consultez la documentation de la bibliothèque pour plus de détails.

Si vous devez absolument utiliser un rappel, vous pouvez également envelopper le rappel dans une promesse:

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)
      }
    })
  })
28
Daniel Rearden

J'ai eu le même problème sur Nest.js.

Si vous souhaitez résoudre le problème. Vous pouvez ajouter l'option {nullable: true} à votre décorateur @Query.

Voici un exemple.

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

Ensuite, vous pouvez renvoyer un objet nul pour la requête.

enter image description here

0
Atsuhiro Teshima

Si rien de ce qui précède n'a aidé, et que vous avez un intercepteur global qui enveloppe toutes les réponses, par exemple à l'intérieur d'un champ "données", vous devez le désactiver pour graphql, les autres résolveurs de graphql judicieux convertis en null.

Voici ce que j'ai fait à l'intercepteur sur mon cas:

  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
        };
      }));
  }
0
Dima Grossman