Tipos de GraphQL
GraphQL admite muchos tipos diferentes. Como vio en la sección anterior, los tipos definen la forma o el comportamiento de los datos. Son los componentes fundamentales de un esquema de GraphQL.
Los tipos se pueden clasificar como entradas y salidas. Los de entrada son tipos que se pueden pasar como argumento para los tipos de objetos especiales (Query
, Mutation
, etc.), mientras que los tipos de salida se utilizan estrictamente para almacenar y devolver datos. A continuación, se muestra una lista de tipos y sus categorizaciones:
-
Objetos: un objeto contiene campos que describen una entidad. Por ejemplo, un objeto podría ser algo así como un book
con campos que describen sus características, como authorName
, publishingYear
, etc. Son estrictamente tipos de salida.
-
Escalares: son tipos primitivos como int, string, etc. Por lo general, se asignan a los campos. Usando el campo authorName
como ejemplo, se le podría asignar el escalar String
para almacenar un nombre como «John Smith». Los escalares pueden ser tanto de entrada como de salida.
-
Entradas: las entradas permiten transferir un grupo de campos como argumento. Están estructurados de forma muy similar a los objetos, pero se pueden transferir como argumentos a objetos especiales. Las entradas permiten definir escalares, enumeraciones y otras entradas incluidas en su ámbito. Las entradas solo pueden ser tipos de entrada.
-
Objetos especiales: los objetos especiales realizan operaciones que cambian de estado y se encargan de la mayor parte del trabajo pesado del servicio. Hay tres tipos de objetos especiales: consulta, mutación y suscripción. Las consultas suelen obtener datos; las mutaciones manipulan los datos; las suscripciones se abren y mantienen una conexión bidireccional entre los clientes y los servidores para una comunicación constante. Los objetos especiales no son ni entradas ni salidas debido a su funcionalidad.
-
Enumeraciones: listas predefinidas de valores legales. Si llama a una enumeración, sus valores solo pueden ser lo que esté definido en su ámbito. Por ejemplo, si tuviera una enumeración llamada trafficLights
que representara una lista de señales de tráfico, podría tener valores como redLight
y greenLight
, pero no purpleLight
. Un semáforo real solo tendrá un número determinado de señales, por lo que podría usar la enumeración para definirlas y hacer que sean los únicos valores legales para referirse a trafficLight
. Las enumeraciones pueden ser tanto de entrada como de salida.
-
Uniones/interfaces: las uniones permiten devolver uno o más elementos en una solicitud en función de los datos que haya solicitado el cliente. Por ejemplo, si tuviera un tipo Book
con un campo title
y un tipo Author
con un campo name
, podría crear una unión entre ambos tipos. Si su cliente quisiera consultar la frase “Julio César” en una base de datos, la unión podría devolver Julio César (la obra de William Shakespeare) del title
del Book
y Julio César (el autor de Commentarii de Bello Gallico) del name
de Author
. Las uniones solo pueden ser tipos de salida.
Las interfaces son conjuntos de campos que los objetos deben implementar. Es parecido a las interfaces de lenguajes de programación como Java, donde hay que implementar los campos definidos en la interfaz. Por ejemplo, supongamos que creó una interfaz llamada Book
que contenía un campo title
. Digamos que más tarde ha creado un tipo llamado Novel
que ha implementado Book
. Novel
tendría que incluir un campo title
. Sin embargo, Novel
también podría incluir otros campos que no estén en la interfaz, como pageCount
o ISBN
. Las interfaces solo pueden ser tipos de salida.
En las siguientes secciones se explicará cómo funciona cada tipo en GraphQL.
Objects
Los objetos de GraphQL son el tipo principal que verá en el código de producción. En GraphQL, puede pensar en un objeto como en una agrupación de campos diferentes (de forma aprecia a las variables en otros lenguajes) en la que cada campo se define por un tipo (normalmente un escalar u otro objeto) que puede contener un valor. Los objetos representan una unidad de datos que se puede recuperar o manipular a partir de la implementación del servicio.
Los tipos de objetos se declaran mediante la palabra clave Type
. Modifiquemos un poco nuestro ejemplo de esquema:
type Person {
id: ID!
name: String
age: Int
occupation: Occupation
}
type Occupation {
title: String
}
Los tipos de objetos que aparecen aquí son Person
y Occupation
. Cada objeto tiene sus propios campos con sus propios tipos. Una característica de GraphQL es la capacidad de establecer campos de otros tipos. Puede ver que el campo occupation
de Person
contiene un tipo de objeto Occupation
. Podemos hacer esta asociación porque GraphQL solo describe los datos y no la implementación del servicio.
Escalares
Los escalares son esencialmente tipos primitivos que contienen valores. En AWS AppSync, hay dos tipos de escalares: los escalares y los escalares predeterminados de GraphQL y los escalares de AWS AppSync. Los escalares se utilizan normalmente para almacenar valores de campo dentro de los tipos de objetos. Entre los tipos de GraphQL predeterminados se incluyen Int
, Float
, String
, Boolean
y ID
. Volvamos al ejemplo anterior:
type Person {
id: ID!
name: String
age: Int
occupation: Occupation
}
type Occupation {
title: String
}
Si elegimos los campos title
y name
, ambos contienen un escalar String
. Name
podría devolver un valor de cadena como "John Smith
" y el título podría devolver algo como "firefighter
". Algunas implementaciones de GraphQL también admiten escalares personalizados que utilizan la palabra clave Scalar
e implementan el comportamiento del tipo. Sin embargo, AWS AppSync actualmente no admite escalares personalizados. Para obtener una lista de escalares, consulte Tipos escalares en AWS AppSync.
Debido al concepto de tipos de entrada y salida, existen ciertas restricciones para la transferencia de argumentos. Los tipos que normalmente deben transferirse, especialmente los objetos, están restringidos. Puede usar el tipo de entrada para omitir esta regla. Las entradas son tipos que contienen escalares, enumeraciones y otros tipos de entrada.
Las entradas se definen mediante la palabra clave input
:
type Person {
id: ID!
name: String
age: Int
occupation: Occupation
}
type Occupation {
title: String
}
input personInput {
id: ID!
name: String
age: Int
occupation: occupationInput
}
input occupationInput {
title: String
}
Como ve, podemos tener entradas independientes que imitan el tipo original. Estas entradas se utilizarán a menudo en sus operaciones de campo de la siguiente manera:
type Person {
id: ID!
name: String
age: Int
occupation: Occupation
}
type Occupation {
title: String
}
input occupationInput {
title: String
}
type Mutation {
addPerson(id: ID!, name: String, age: Int, occupation: occupationInput): Person
}
Observe cómo seguimos transfiriendo occupationInput
en lugar de Occupation
para crear una Person
.
Este es solo uno de los escenarios sobre entradas. No es necesario que copien los objetos a escala 1:1 y, en el código de producción, lo más probable es que no lo utilice de esta manera. Una buena práctica es aprovechar los esquemas de GraphQL y definir solo lo que se necesite introducir como argumentos.
Además, se pueden usar las mismas entradas en varias operaciones, pero no recomendamos hacerlo. Lo ideal es que cada operación contenga su propia copia única de las entradas en caso de que cambien los requisitos del esquema.
Objetos especiales
GraphQL reserva algunas palabras clave para objetos especiales que definen parte de la lógica empresarial sobre la forma en que su esquema va a recuperar o manipular los datos. Como máximo, puede haber una de cada una de estas palabras clave en un esquema. Actúan como puntos de entrada para todos los datos solicitados que sus clientes ejecutan en el servicio de GraphQL.
Los objetos especiales también se definen con la palabra clave type
. Aunque se utilizan de forma diferente a los tipos de objetos normales, su implementación es muy parecida.
- Queries
-
Las consultas son muy similares a las operaciones GET
en el sentido de que realizan una búsqueda de solo lectura para obtener datos de su origen. En GraphQL, la Query
define todos los puntos de entrada para los clientes que realizan solicitudes a su servidor. Siempre habrá una Query
en tu implementación de GraphQL.
Aquí están la Query
y los tipos de objetos modificados que hemos utilizado en nuestro ejemplo de esquema anterior:
type Person {
id: ID!
name: String
age: Int
occupation: Occupation
}
type Occupation {
title: String
}
type Query {
people: [Person]
}
Nuestra Query
contiene un campo llamado people
que devuelve una lista de instancias de Person
del origen de datos. Supongamos que necesitamos cambiar el comportamiento de nuestra aplicación y que ahora necesitamos devolver una lista de solo las instancias de Occupation
para un propósito diferente. Simplemente podríamos añadirlo a la consulta:
type Query {
people: [Person]
occupations: [Occupation]
}
En GraphQL, podemos tratar nuestra consulta como el único origen de las solicitudes. Como puede ver, esto es potencialmente mucho más simple que las implementaciones RESTful, que pueden usar diferentes puntos de conexión para lograr lo mismo (.../api/1/people
y .../api/1/occupations
).
Suponiendo que tengamos una implementación de resolución para esta consulta, ahora podemos realizar una consulta real. Si bien el tipo de Query
existe, debemos llamarlo explícitamente para que se ejecute en el código de la aplicación. Esto se puede hacer con la palabra clave query
:
query getItems {
people {
name
}
occupations {
title
}
}
Como puede ver, esta consulta se llama getItems
y devuelve people
(una lista de objetos de Person
) y occupations
(una lista de objetos de Occupation
). En people
, devolvemos solo el campo name
de cada Person
, mientras que devolvemos el campo title
de cada Occupation
. La respuesta puede tener un aspecto similar al siguiente:
{
"data": {
"people": [
{
"name": "John Smith"
},
{
"name": "Andrew Miller"
},
.
.
.
],
"occupations": [
{
"title": "Firefighter"
},
{
"title": "Bookkeeper"
},
.
.
.
]
}
}
La respuesta de ejemplo muestra cómo los datos siguen la forma de la consulta. Cada entrada recuperada aparece dentro del ámbito del campo. people
y occupations
devuelven los elementos como listas separadas. Si bien es útil, puede ser más conveniente modificar la consulta para que devuelva una lista con los nombres y las ocupaciones de las personas:
query getItems {
people {
name
occupation {
title
}
}
Se trata de una modificación legal porque nuestro tipo Person
contiene un campo occupation
de tipo Occupation
. Cuando se incluye dentro del ámbito de people
, devolvemos el name
de cada Person
junto con la correspondiente Occupation
mediante title
. La respuesta puede tener un aspecto similar al siguiente:
}
"data": {
"people": [
{
"name": "John Smith",
"occupation": {
"title": "Firefighter"
}
},
{
"name": "Andrew Miller",
"occupation": {
"title": "Bookkeeper"
}
},
.
.
.
]
}
}
- Mutations
-
Las mutaciones son parecidas a las operaciones que cambian el estado, como PUT
o POST
. Realizan una operación de escritura para modificar los datos del origen y, a continuación, obtienen la respuesta. Definen los puntos de entrada para las solicitudes de modificación de datos. A diferencia de las consultas, una mutación puede incluirse en el esquema o no según las necesidades del proyecto. Esta es la mutación del ejemplo del esquema:
type Mutation {
addPerson(id: ID!, name: String, age: Int): Person
}
El campo addPerson
representa un punto de entrada que añade una Person
al origen de datos. addPerson
es el nombre del campo; id
, name
y age
son los parámetros; y Person
es el tipo de retorno. Volviendo al tipo de Person
:
type Person {
id: ID!
name: String
age: Int
occupation: Occupation
}
Hemos añadido el campo occupation
. Sin embargo, no podemos establecer este campo directamente en Occupation
porque los objetos no se pueden transferir como argumentos; son estrictamente tipos de salida. En lugar de ello, deberíamos pasar una entrada con los mismos campos que un argumento:
input occupationInput {
title: String
}
También podemos actualizar fácilmente nuestro addPerson
para incluirlo como parámetro al crear nuevas instancias de Person
:
type Mutation {
addPerson(id: ID!, name: String, age: Int, occupation: occupationInput): Person
}
Este es el esquema actualizado:
type Person {
id: ID!
name: String
age: Int
occupation: Occupation
}
type Occupation {
title: String
}
input occupationInput {
title: String
}
type Mutation {
addPerson(id: ID!, name: String, age: Int, occupation: occupationInput): Person
}
Tenga en cuenta que occupation
transferirá el campo title
desde occupationInput
para completar la creación de la Person
objeto en lugar del objeto Occupation
original. Suponiendo que tengamos una implementación de resolución para addPerson
, ahora podemos realizar una mutación real. Si bien el tipo de Mutation
existe, debemos llamarlo explícitamente para que se ejecute en el código de la aplicación. Esto se puede hacer con la palabra clave mutation
:
mutation createPerson {
addPerson(id: ID!, name: String, age: Int, occupation: occupationInput) {
name
age
occupation {
title
}
}
}
Esta mutación se llama createPerson
y addPerson
es la operación. Para crear una nueva Person
, podemos introducir los argumentos para id
, name
, age
y occupation
. En el ámbito de addPerson
, también podemos ver otros campos como name
, age
, etc. Esta es su respuesta; estos son los campos que se devolverán una vez finalizada la operación addPerson
. Esta es la parte final del ejemplo:
mutation createPerson {
addPerson(id: "1", name: "Steve Powers", age: "50", occupation: "Miner") {
id
name
age
occupation {
title
}
}
}
Si se usa esta mutación, el resultado podría ser parecido a este:
{
"data": {
"addPerson": {
"id": "1",
"name": "Steve Powers",
"age": "50",
"occupation": {
"title": "Miner"
}
}
}
}
Como puede ver, la respuesta ha devuelto los valores que solicitamos en el mismo formato que se definió en nuestra mutación. Se recomienda devolver todos los valores que se hayan modificado para reducir la confusión y la necesidad de realizar más consultas en el futuro. Las mutaciones permiten incluir varias operaciones dentro de su ámbito. Se ejecutarán secuencialmente en el orden indicado en la mutación. Por ejemplo, si creamos otra operación denominada addOccupation
que agregue títulos de trabajo al origen de datos, podemos llamarla así en la mutación después de addPerson
. addPerson
se gestionará primero y, a continuación, addOccupation
.
- Subscriptions
-
Las suscripciones utilizan WebSockets para abrir una conexión bidireccional duradera entre el servidor y sus clientes. Normalmente, un cliente se suscribe o escucha al servidor. Siempre que el servidor realice un cambio en el lado del servidor o realice un evento, el cliente suscrito recibirá las actualizaciones. Este tipo de protocolo resulta útil cuando hay varios clientes suscritos y es necesario notificarles los cambios que se produzcan en el servidor o en otros clientes. Por ejemplo, las suscripciones se pueden utilizar para actualizar los feeds de las redes sociales. Puede haber dos usuarios, el usuario A y el usuario B, que estén suscritos a las actualizaciones de notificaciones automáticas cada vez que reciban mensajes directos. El usuario A del cliente A podría enviar un mensaje directo al usuario B del cliente B. El cliente del usuario A enviaría el mensaje directo, que sería procesado por el servidor. A continuación, el servidor enviaría el mensaje directo a la cuenta del usuario B y, al mismo tiempo, enviaría una notificación automática al cliente B.
Este es un ejemplo de Subscription
que podríamos añadir al ejemplo del esquema:
type Subscription {
personAdded: Person
}
El campo personAdded
enviará un mensaje a los clientes suscritos cada vez que Person
se añada una nueva al nuevo origen de datos. Suponiendo que tengamos una implementación de solucionador para personAdded
, ahora podemos usar la suscripción. Si bien el tipo de Subscription
existe, debemos llamarlo explícitamente para que se ejecute en el código de la aplicación. Esto se puede hacer con la palabra clave subscription
:
subscription personAddedOperation {
personAdded {
id
name
}
}
La suscripción se llama personAddedOperation
y la operación es personAdded
. personAdded
devolverá los campos id
e name
de las nuevas instancias de Person
. Observando el ejemplo de la mutación, añadimos una operación Person
utilizando esta operación:
addPerson(id: "1", name: "Steve Powers", age: "50", occupation: "Miner")
Si nuestros clientes estaban suscritos a las actualizaciones de la Person
recién añadida, es posible que vean lo siguiente cuando se ejecute addPerson
:
{
"data": {
"personAdded": {
"id": "1",
"name": "Steve Powers"
}
}
}
A continuación se muestra un resumen de lo que ofrecen las suscripciones:
Las suscripciones son canales bidireccionales que permiten al cliente y al servidor recibir actualizaciones rápidas pero constantes. Por lo general, utilizan el protocolo WebSocket, que crea conexiones estandarizadas y seguras.
Las suscripciones son ágiles, ya que reducen la sobrecarga de configuración de la conexión. Una vez suscrito, un cliente puede seguir utilizando esa suscripción durante largos períodos de tiempo. Por lo general, utilizan los recursos informáticos de forma eficiente, ya que permiten a los desarrolladores personalizar la duración de la suscripción y configurar la información que se va a solicitar.
En general, las suscripciones permiten al cliente realizar varias suscripciones a la vez. Al pertenecer a AWS AppSync, las suscripciones solo se utilizan para recibir actualizaciones del servicio de AWS AppSync en tiempo real. No se pueden usar para realizar consultas o mutaciones.
La principal alternativa a las suscripciones es el sondeo, que envía consultas a intervalos establecidos para solicitar datos. Este proceso suele ser menos eficiente que las suscripciones y supone una gran carga tanto para el cliente como para el backend.
Lo que no se ha mencionado en nuestro ejemplo de esquema es que los tipos de objetos especiales también deben definirse en una raíz de schema
. Por lo tanto, si exporta un esquema AWS AppSync, este podría tener este aspecto:
- schema.graphql
-
schema {
query: Query
mutation: Mutation
subscription: Subscription
}
.
.
.
type Query {
# code goes here
}
type Mutation {
# code goes here
}
type Subscription {
# code goes here
}
Enumeraciones
Las enumeraciones son escalares especiales que limitan los argumentos legales que un tipo o campo puede tener. Esto significa que siempre que se defina una enumeración en el esquema, su tipo o campo asociado se limitará a los valores de la enumeración. Las enumeraciones se serializan como escalares de cadena. Tenga en cuenta que los diferentes lenguajes de programación pueden gestionar las enumeraciones de GraphQL de forma diferente. Por ejemplo, JavaScript no admite enumeraciones nativas, por lo que los valores de enumeración se pueden asignar a valores int.
Las enumeraciones se definen mediante la palabra clave enum
: A continuación se muestra un ejemplo:
enum trafficSignals {
solidRed
solidYellow
solidGreen
greenArrowLeft
...
}
Al llamar a la enumeración trafficLights
, los argumentos solo pueden ser solidRed
, solidYellow
, solidGreen
, etc. Es habitual usar enumeraciones para representar cosas que tienen un número distinto pero limitado de opciones.
Uniones/interfaces
Consulte Interfaces y uniones en GraphQL.