graphql graphql-js apollo-server

¿Por qué una consulta GraphQL devuelve nulo?



graphql-js apollo-server (1)

Tengo un punto graphql / apollo-server / graphql-yoga . Este punto final expone los datos devueltos desde una base de datos (o un punto final REST o algún otro servicio).

Sé que mi fuente de datos está devolviendo los datos correctos: si registro el resultado de la llamada a la fuente de datos dentro de mi sistema de resolución, puedo ver cómo se devuelven los datos. Sin embargo, mis campos GraphQL siempre se resuelven en nulos.

Si hago el campo no nulo, veo el siguiente error dentro de la matriz de errors en la respuesta:

No se puede devolver nulo para un campo no anulable

¿Por qué GraphQL no está devolviendo los datos?


Hay dos razones comunes por las que su campo o campos se están resolviendo como nulos: 1) devolver datos en la forma incorrecta dentro de su resolución; y 2) no usar Promesas correctamente.

Nota: si estás viendo el siguiente error:

No se puede devolver nulo para un campo no anulable

El problema subyacente es que su campo está volviendo nulo. Aún puede seguir los pasos que se describen a continuación para intentar resolver este error.

Los siguientes ejemplos se referirán a este simple esquema:

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

Devolviendo datos en la forma incorrecta

Nuestro esquema, junto con la consulta solicitada, define la "forma" del objeto de data en la respuesta devuelta por nuestro punto final. Por forma, nos referimos a las propiedades que tienen los objetos y si los valores de esas propiedades son valores escalares, otros objetos o matrices de objetos o escalares.

De la misma manera que un esquema define la forma de la respuesta total, el tipo de un campo individual define la forma del valor de ese campo. La forma de los datos que devolvemos en nuestro sistema de resolución también debe coincidir con esta forma esperada. Cuando no es así, con frecuencia terminamos con nulos inesperados en nuestra respuesta.

Sin embargo, antes de sumergirnos en ejemplos específicos, es importante comprender cómo GraphQL resuelve los campos.

Entender el comportamiento predeterminado de resolución

Si bien ciertamente puede escribir una resolución para cada campo en su esquema, a menudo no es necesario porque GraphQL.js usa una resolución predeterminada cuando no la proporciona.

En un nivel alto, lo que hace el resolutor predeterminado es simple: mira el valor que el campo principal resolvió y si ese valor es un objeto de JavaScript, busca una propiedad en ese Objeto con el mismo nombre que el campo que se está resolviendo. Si encuentra esa propiedad, se resuelve al valor de esa propiedad. De lo contrario, se resuelve en nulo.

Digamos que en nuestro resolver para el campo de post , devolvemos el valor { title: ''My First Post'', bod: ''Hello World!'' } { title: ''My First Post'', bod: ''Hello World!'' } . Si no escribimos resolutores para ninguno de los campos en el tipo de Post , aún podemos solicitar la post :

query { post { id title body } }

y nuestra respuesta será

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

El campo de title se resolvió a pesar de que no proporcionamos una resolución de resolución porque la resolución predeterminada hizo el trabajo pesado: vio que había una propiedad denominada title en el objeto al que se resolvió el campo principal (en este caso, post ) y así Solo se resolvió al valor de esa propiedad. El campo de id resolvió en nulo porque el objeto que devolvimos en nuestra resolución de resolución no tenía una propiedad de id . El campo del body también se resolvió en nulo debido a un error tipográfico: tenemos una propiedad llamada bod lugar de body .

Consejo profesional : si bod no es un error tipográfico, pero lo que realmente devuelve una API o una base de datos, siempre podemos escribir una resolución para que el campo del body coincida con nuestro esquema. Por ejemplo: (parent) => parent.bod

Una cosa importante a tener en cuenta es que en JavaScript, casi todo es un Objeto . Por lo tanto, si el campo de post resuelve en una cadena o un número, la resolución predeterminada para cada uno de los campos en el tipo de Post intentará encontrar una propiedad con el nombre apropiado en el objeto principal, inevitablemente fallará y devolverá el valor nulo. Si un campo tiene un tipo de objeto pero devuelve algo distinto al objeto en su resolución (como una Cadena o una Matriz), no verá ningún error sobre la falta de coincidencia de tipo, pero los campos secundarios para ese campo inevitablemente se resolverán en nulo.

Escenario común n. ° 1: respuestas envueltas

Si estamos escribiendo el resolutor para la consulta post , podríamos obtener nuestro código de algún otro punto final, como este:

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

El campo de post tiene el tipo Post , por lo que nuestra resolución debería devolver un objeto con propiedades como id , title y body . Si esto es lo que devuelve nuestra API, estamos listos. Sin embargo , es común que la respuesta sea realmente un objeto que contiene metadatos adicionales. Por lo tanto, el objeto que realmente recuperamos del punto final podría ser algo como esto:

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

En este caso, no podemos devolver la respuesta tal como está y esperar que la resolución predeterminada funcione correctamente, ya que el objeto que estamos devolviendo no tiene las propiedades de id , title y body que necesitamos. Nuestro resolutor no necesita hacer algo como:

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

Nota : el ejemplo anterior obtiene datos de otro punto final; sin embargo, este tipo de respuesta envuelta también es muy común cuando se usa un controlador de base de datos directamente (en lugar de usar un ORM). Por ejemplo, si está usando node-postgres , obtendrá un objeto Result que incluye propiedades como rows , fields , rowCount y command . Deberá extraer los datos apropiados de esta respuesta antes de devolverlos dentro de su resolutor.

Escenario común # 2: Array en lugar de objeto

¿Qué pasa si recuperamos una publicación de la base de datos, nuestro resolutor podría tener este aspecto?

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

donde Post es algún modelo que estamos inyectando a través del contexto. Si estamos utilizando la sequelize , podríamos llamar a findAll . mongoose y la typeorm tienen find . Lo que estos métodos tienen en común es que, si bien nos permiten especificar una condición WHERE , las promesas que devuelven aún se resuelven en una matriz en lugar de en un solo objeto . Si bien es probable que solo haya una publicación en su base de datos con un ID en particular, todavía se incluye en una matriz cuando llama a uno de estos métodos. Debido a que una matriz sigue siendo un objeto, GraphQL no resolverá el campo de post como nulo. Pero resolverá todos los campos secundarios como nulos porque no podrá encontrar las propiedades con el nombre adecuado en la matriz.

Puede arreglar este escenario fácilmente simplemente tomando el primer elemento de la matriz y devolviéndolo a su resolutor:

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

Si está obteniendo datos de otra API, esta es frecuentemente la única opción. Por otro lado, si está utilizando un ORM, a menudo hay un método diferente que puede usar (como findOne ) que devolverá explícitamente solo una fila del DB (o nulo si no existe).

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

Una nota especial sobre las llamadas INSERT y UPDATE : A menudo esperamos que los métodos que insertan o actualizan una fila o instancia de modelo devuelvan la fila insertada o actualizada. A menudo lo hacen, pero algunos métodos no lo hacen. Por ejemplo, el método upsert se resuelve en un valor booleano o tupla del registro mejorado y un valor booleano (si la opción de returning se establece en verdadero). findOneAndUpdate resuelve en un objeto con una propiedad de value que contiene la fila modificada. Consulte la documentación de su ORM y analice el resultado adecuadamente antes de devolverlo dentro de su resolución.

Escenario común # 3: Objeto en lugar de matriz

En nuestro esquema, el tipo de campo de las posts es una List de Post , lo que significa que su solucionador debe devolver una matriz de objetos (o una Promesa que se resuelve en una). Podríamos buscar las publicaciones de esta manera:

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

Sin embargo, la respuesta real de nuestra API podría ser un objeto que envuelve la matriz de publicaciones:

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

No podemos devolver este objeto en nuestra resolución porque GraphQL está esperando una matriz. Si lo hacemos, el campo se resolverá en nulo y veremos un error incluido en nuestra respuesta como:

Se esperaba que se pudiera iterar, pero no se encontró uno para el campo Query.posts.

A diferencia de los dos escenarios anteriores, en este caso, GraphQL puede verificar explícitamente el tipo de valor que devolvemos en nuestro sistema de resolución y arrojará si no es un Iterable como una matriz.

Como explicamos en el primer escenario, para corregir este error, tenemos que transformar la respuesta en la forma adecuada, por ejemplo:

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

No usar las promesas correctamente

GraphQL.js hace uso de la API Promise bajo el capó. Como tal, un resolutor puede devolver algún valor (como { id: 1, title: ''Hello!'' } ) O puede devolver una Promesa que se resolverá con ese valor. Para los campos que tienen un tipo de List , también puede devolver una matriz de Promesas. Si se rechaza una Promesa, ese campo devolverá un valor nulo y el error correspondiente se agregará a la matriz de errors en la respuesta. Si un campo tiene un tipo de Objeto, el valor al que se resuelve la Promesa es lo que se transmitirá como el valor primario a los resolutores de cualquier campo secundario.

Una Promise es un "objeto que representa la finalización (o falla) eventual de una operación asíncrona y su valor resultante". Los siguientes escenarios describen algunas de las dificultades comunes que surgen al tratar con las promesas dentro de los solucionadores. Sin embargo, si no está familiarizado con Promises y la nueva sintaxis de async / await, es muy recomendable que dedique un poco de tiempo a leer los fundamentos.

Nota : los siguientes ejemplos se refieren a una función getPost . Los detalles de implementación de esta función no son importantes, es solo una función que devuelve una Promesa, que se resolverá en un objeto de publicación.

Escenario común # 4: No devolver un valor

Una resolución de trabajo para el campo de post podría tener este aspecto:

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

getPosts devuelve una Promesa y nosotros la estamos devolviendo. Cualquier cosa que resuelva la Promesa se convertirá en el valor que nuestro campo resuelva. ¡Luciendo bien!

Pero que pasa si hacemos esto:

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

Todavía estamos creando una Promesa que se resolverá en una publicación. Sin embargo, no estamos devolviendo la Promesa, por lo que GraphQL no es consciente de ello y no esperará a que se resuelva. En las funciones de JavaScript sin una declaración de return explícita, return implícitamente undefined . Así que nuestra función crea una Promesa y luego devuelve inmediatamente lo undefined , lo que hace que GraphQL devuelva un valor nulo para el campo.

Si la Promesa devuelta por getPost rechaza, tampoco veremos ningún error en nuestra respuesta, ya que no getPost la Promesa, al código subyacente no le importa si se resuelve o rechaza. De hecho, si la Promesa se rechaza, verá una UnhandledPromiseRejectionWarning de UnhandledPromiseRejectionWarning Promisión UnhandledPromiseRejectionWarning en la consola de su servidor.

Resolver este problema es simple: solo agregue la return .

Escenario común n. ° 5: No encadenar las promesas correctamente

Decide registrar el resultado de su llamada a getPost , por lo que cambia su resolución para que se vea algo así:

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

Cuando ejecuta su consulta, ve el resultado registrado en su consola, pero GraphQL resuelve el campo en nulo. ¿Por qué?

Cuando invocamos una Promesa, efectivamente estamos tomando el valor que la Promesa resolvió y devolviendo una Nueva Promesa. Se puede pensar en algo así como Array.map excepción de Promises. then puede devolver un valor, u otra Promesa. En cualquier caso, lo que se devuelve dentro de then se "encadena" a la Promesa original. Múltiples promesas pueden ser encadenadas juntas de esta manera al usar múltiples y then s. Cada Promesa en la cadena se resuelve en secuencia, y el valor final es lo que efectivamente se resuelve como el valor de la Promesa original.

En nuestro ejemplo anterior, no devolvimos nada dentro de then , por lo que la Promesa se resolvió como undefined , lo que GraphQL convirtió a nulo. Para arreglar esto, tenemos que devolver las publicaciones:

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

Si tiene varias Promesas que necesita resolver dentro de su resolutor, debe encadenarlas correctamente usando then y devolviendo el valor correcto. Por ejemplo, si necesitamos llamar a otras dos funciones asíncronas ( getFoo y getBar ) antes de que podamos llamar a getPost , podemos hacerlo:

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

Sugerencia profesional: si está luchando con las promesas de encadenamiento correctamente, puede encontrar que la sintaxis asíncrona / espera es más limpia y más fácil de trabajar.

Escenario Común # 6

Antes de Promises, la forma estándar de manejar el código asíncrono era utilizar devoluciones de llamada o funciones que se llamarían una vez que se completara el trabajo asíncrono. Podríamos, por ejemplo, llamar findOne método findOne mongoose de la findOne manera:

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

El problema aquí es doble. Uno, un valor que se devuelve dentro de una devolución de llamada no se usa para nada (es decir, no se pasa de ninguna manera al código subyacente). Dos, cuando usamos una devolución de llamada, Post.findOne no devuelve una Promesa; sólo devuelve indefinido. En este ejemplo, se llamará a nuestra devolución de llamada y, si registramos el valor de la post , veremos lo que fue devuelto desde la base de datos. Sin embargo, debido a que no usamos una Promesa, GraphQL no espera a que se complete la devolución de llamada; toma el valor de retorno (no definido) y lo utiliza.

La mayoría de las bibliotecas más populares, incluido el soporte de mongoose Promesas fuera de la caja. Aquellos que con frecuencia no tienen bibliotecas complementarias complementarias que agregan esta funcionalidad. Cuando trabaje con resolutores GraphQL, debe evitar el uso de métodos que utilicen una devolución de llamada y, en su lugar, utilice métodos que devuelvan promesas.

Consejo profesional: las bibliotecas que admiten devoluciones de llamada y promesas suelen sobrecargar sus funciones de tal manera que si no se proporciona una devolución de llamada, la función devolverá una Promesa. Consulte la documentación de la biblioteca para más detalles.

Si absolutamente tiene que usar una devolución de llamada, también puede envolver la devolución de llamada en una promesa:

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