Tipos no GraphQL - AWS AppSync

Tipos no GraphQL

O GraphQL é compatível com muitos tipos diferentes. Como você viu na seção anterior, os tipos definem a forma ou o comportamento dos seus dados. Eles são os blocos de construção fundamentais de um esquema do GraphQL.

Os tipos podem ser categorizados em entradas e saídas. As entradas são tipos que podem ser transmitidos como argumento para os tipos de objetos especiais (Query, Mutation, etc.), enquanto os tipos de saída são usados estritamente para armazenar e retornar dados. Uma lista de tipos e suas categorizações estão listadas abaixo:

  • Objetos: um objeto contém campos que descrevem uma entidade. Por exemplo, um objeto pode ser algo como um book com campos descrevendo suas características, como authorName, publishingYear, etc. Eles são estritamente tipos de saída.

  • Escalares: esses são tipos primitivos como int, string, etc. Normalmente, eles são atribuídos a campos. Usando o campo authorName como exemplo, ele pode ser atribuído ao escalar String para armazenar um nome como “John Smith”. Escalares podem ser do tipo de entrada e saída.

  • Entradas: as entradas permitem que você transmita um grupo de campos como argumento. Elas são estruturadas de forma muito semelhante aos objetos, mas podem ser passadas como argumentos para objetos especiais. As entradas permitem que você defina escalares, enums e outras entradas em seu escopo. As entradas só podem ser tipos de entrada.

  • Objetos especiais: objetos especiais realizam operações de mudança de estado e fazem a maior parte do trabalho pesado do serviço. Há três tipos de objetos especiais: consulta, mutação e assinatura. As consultas normalmente buscam dados; as mutações manipulam dados; as assinaturas abrem e mantêm uma conexão bidirecional entre clientes e servidores para comunicação constante. Objetos especiais não são de entrada nem saída, dada sua funcionalidade.

  • Enums: enums são listas predefinidas de valores legais. Se você chamar um enum, seus valores só poderão ser definidos em seu escopo. Por exemplo, se você tivesse uma enumeração chamada trafficLights representando uma lista de sinais de trânsito, ela poderia ter valores como redLight e greenLight, mas não purpleLight. Um semáforo real terá um limite de sinais, então você pode usar o enum para defini-los e forçá-los a serem os únicos valores legais ao fazer referência a trafficLight. Escalares podem ser do tipo de entrada e saída.

  • Uniões/interfaces: as uniões permitem que você retorne uma ou mais coisas em uma solicitação, dependendo dos dados solicitados pelo cliente. Por exemplo, se você tivesse um tipo Book com um title campo e um tipo Author com um campo name, você poderia criar uma união entre os dois tipos. Se seu cliente quisesse consultar um banco de dados para a frase “Júlio César”, a união poderia retornar Júlio César (a peça de William Shakespeare) do title Book e Júlio César (o autor de Commentarii de Bello Gallico) do name Author. As uniões só podem ser tipos de saída.

    As interfaces são conjuntos de campos que os objetos devem implementar. Isso é um pouco semelhante às interfaces em linguagens de programação como Java, nas quais você precisa implementar os campos definidos na interface. Por exemplo, digamos que você tenha criado uma interface chamada Book que continha um campo title. Digamos que você tenha criado posteriormente um tipo chamado Novel que implementou Book. Seu Novel teria que incluir um campo title. No entanto, seu Novel também pode incluir outros campos que não estão na interface, como pageCount ou ISBN. As interfaces só podem ser tipos de saída.

As seções a seguir explicarão como cada tipo funciona no GraphQL.

Objetos

Os objetos do GraphQL são o tipo principal que você verá no código de produção. No GraphQL, você pode pensar em um objeto como um agrupamento de campos diferentes (semelhantes às variáveis em outras linguagens), e cada campo é definido por um tipo (normalmente um escalar ou outro objeto) que pode conter um valor. Os objetos representam uma unidade de dados que pode ser recuperada/manipulada a partir da implementação do serviço.

Os tipos de objetos são declarados usando a palavra-chave Type. Vamos modificar um pouco nosso exemplo de esquema:

type Person { id: ID! name: String age: Int occupation: Occupation } type Occupation { title: String }

Os tipos de objeto aqui são Person e Occupation. Cada objeto tem seus próprios campos com seus próprios tipos. Um atributo do GraphQL é a capacidade de definir campos como outros tipos. Você pode ver que o campo occupation em Person contém um tipo de objeto Occupation. Podemos fazer essa associação porque o GraphQL está apenas descrevendo os dados e não a implementação do serviço.

Escalares

Os escalares são tipos essencialmente primitivos que contêm valores. Em AWS AppSync, há dois tipos de escalares: os escalares padrão do GraphQL e escalares do AWS AppSync. Os escalares costumam ser usados para armazenar valores de campo em tipos de objetos. Os tipos padrão do GraphQL incluem Int, Float, String,Boolean e ID. Vamos usar o exemplo anterior novamente:

type Person { id: ID! name: String age: Int occupation: Occupation } type Occupation { title: String }

Destacando os campos name e title e, ambos possuem um escalar String, e Name poderia retornar um valor de string como "John Smith" e o título poderia retornar algo como "firefighter”. Algumas implementações do GraphQL também oferecem suporte a escalares personalizados usando a palavra-chaveScalar e implementando o comportamento do tipo. No entanto, atualmente o AWS AppSync não oferece suporte a escalares personalizados. Para obter uma lista de escalares, consulte Tipos de escalares no AWS AppSync.

Entradas

Devido ao conceito de tipos de entrada e saída, existem certas restrições ao transmitir argumentos. Os tipos que normalmente precisam ser transmitidos, principalmente objetos, são restritos. Você pode usar o tipo de entrada para ignorar essa regra. As entradas são tipos que contêm escalares, enums e outros tipos de entrada.

As entradas são definidas usando a palavra-chave 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 você pode ver, podemos ter entradas separadas que imitam o tipo original. Essas entradas geralmente serão usadas em suas operações de campo da seguinte forma:

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 como ainda estamos transmitindo occupationInput em vez de Occupation para criar uma Person.

Este é apenas um dos cenários para entradas. Elas não precisam copiar objetos 1:1 e, no código de produção, você provavelmente não as usará dessa forma. É uma prática recomendada aproveitar os esquemas do GraphQL definindo somente o que você precisa inserir como argumentos.

Além disso, as mesmas entradas podem ser usadas em várias operações, mas não recomendamos fazer isso. Preferencialmente, cada operação deve conter sua própria cópia exclusiva das entradas, caso os requisitos do esquema mudem.

Objetos especiais

O GraphQL reserva algumas palavras-chave para objetos especiais que definem parte da lógica de negócios de como seu esquema recuperará/manipulará dados. No máximo, pode haver uma de cada uma dessas palavras-chave em um esquema. Eles atuam como pontos de entrada para todos os dados solicitados que seus clientes executam no seu serviço GraphQL.

Objetos especiais também são definidos usando a palavra-chave type. Embora sejam usados de forma diferente dos tipos de objetos comuns, sua implementação é muito semelhante.

Queries

As consultas são muito semelhantes às operações GET, pois realizam uma busca somente para leitura para obter dados da sua fonte. No GraphQL, a Query define todos os pontos de entrada para clientes que fazem solicitações em seu servidor. Sempre haverá um Query em sua implementação do GraphQL.

Aqui estão os tipos Query de objetos modificados que usamos em nosso exemplo de esquema anterior:

type Person { id: ID! name: String age: Int occupation: Occupation } type Occupation { title: String } type Query { people: [Person] }

Nosso Query contém um campo chamado people que retorna uma lista de instâncias Person da fonte de dados. Digamos que precisemos mudar o comportamento do nosso aplicativo e agora precisamos retornar uma lista somente das instâncias de Occupation para algum propósito separado. Poderíamos simplesmente adicioná-lo à consulta:

type Query { people: [Person] occupations: [Occupation] }

No GraphQL, podemos tratar nossa consulta como a única fonte de solicitações. Como você pode ver, isso pode ser muito mais simples do que implementações RESTful que podem usar endpoints diferentes para alcançar a mesma coisa (.../api/1/people e .../api/1/occupations).

Supondo que tenhamos uma implementação de resolvedor para essa consulta, agora podemos realizar uma consulta real. Embora o tipo Query exista, precisamos chamá-lo explicitamente para que ele seja executado no código do aplicativo. Isso pode ser feito usando a palavra-chave query:

query getItems { people { name } occupations { title } }

Como você pode ver, essa consulta é chamada getItems e retorna people (uma lista de objetos Person) e occupations (uma lista de objetos Occupation). Em people, estamos retornando somente o campo name de cada Person, enquanto retornamos o campo title de cada Occupation. A resposta pode ser semelhante a:

{ "data": { "people": [ { "name": "John Smith" }, { "name": "Andrew Miller" }, . . . ], "occupations": [ { "title": "Firefighter" }, { "title": "Bookkeeper" }, . . . ] } }

O exemplo de resposta mostra como os dados seguem a forma da consulta. Cada entrada recuperada é listada dentro do escopo do campo. people e occupations estão retornando itens como listas separadas. Embora isso seja útil, talvez seja mais conveniente modificar a consulta para retornar uma lista dos nomes e ocupações das pessoas:

query getItems { people { name occupation { title } }

Essa é uma modificação legal porque nosso tipo Person contém um campo occupation de tipo Occupation. Quando listados no escopo de people, retornamos cada um name de Person junto com os Occupation associados por title. A resposta pode ser semelhante a:

} "data": { "people": [ { "name": "John Smith", "occupation": { "title": "Firefighter" } }, { "name": "Andrew Miller", "occupation": { "title": "Bookkeeper" } }, . . . ] } }
Mutations

As mutações são semelhantes às operações de alteração de estado, como ou PUT POST. Eles realizam uma operação de gravação para modificar os dados na fonte e, em seguida, buscam a resposta. Eles definem seus pontos de entrada para solicitações de modificação de dados. Diferentemente das consultas, uma mutação pode ou não ser incluída no esquema, dependendo das necessidades do projeto. Veja a mutação do exemplo do esquema:

type Mutation { addPerson(id: ID!, name: String, age: Int): Person }

O campo addPerson representa um ponto de entrada que adiciona uma Person à fonte de dados. addPerson é o nome do campo; id,name e age são os parâmetros; e Person é o tipo de retorno. Relembrando o tipo Person:

type Person { id: ID! name: String age: Int occupation: Occupation }

Adicionamos o campo occupation. No entanto, não podemos definir esse campo como Occupation diretamente porque os objetos não podem ser passados como argumentos; eles são estritamente tipos de saída. Em vez disso, devemos passar uma entrada com os mesmos campos de um argumento:

input occupationInput { title: String }

Também podemos atualizar facilmente nosso addPerson para incluir isso como um parâmetro ao criar novas instâncias de Person:

type Mutation { addPerson(id: ID!, name: String, age: Int, occupation: occupationInput): Person }

Veja o esquema atualizado:

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 que occupation passará o campo title de occupationInput para concluir a criação do objeto Person em vez do Occupation original. Supondo que tenhamos uma implementação de resolvedor para addPerson, agora podemos realizar uma mutação real. Embora o tipo Mutation exista, precisamos chamá-lo explicitamente para que ele seja executado no código do aplicativo. Isso pode ser feito usando a palavra-chave mutation:

mutation createPerson { addPerson(id: ID!, name: String, age: Int, occupation: occupationInput) { name age occupation { title } } }

Essa mutação é chamada de createPerson, e addPerson é a operação. Para criar uma nova Person, podemos inserir os argumentos para id, name, age e occupation. No escopo de addPerson, também podemos ver outros campos como name, age, etc. Esta é sua resposta; esses são os campos que serão retornados após a conclusão da operação addPerson. Aqui está a parte final do exemplo:

mutation createPerson { addPerson(id: "1", name: "Steve Powers", age: "50", occupation: "Miner") { id name age occupation { title } } }

Usando essa mutação, o resultado pode ser semelhante a:

{ "data": { "addPerson": { "id": "1", "name": "Steve Powers", "age": "50", "occupation": { "title": "Miner" } } } }

Como você pode ver, a resposta retornou os valores que solicitamos no mesmo formato definido em nossa mutação. A prática recomendada é retornar todos os valores que foram modificados para reduzir a confusão e a necessidade de mais consultas no futuro. As mutações permitem que você inclua várias operações em seu escopo. Eles serão executados sequencialmente na ordem listada na mutação. Por exemplo, se criarmos outra operação chamada addOccupation que adiciona cargos à fonte de dados, podemos chamar isso na mutação depois de addPerson. addPerson será tratado primeiro, seguido por addOccupation.

Subscriptions

As assinaturas usam WebSockets para abrir uma conexão bidirecional duradoura entre o servidor e seus clientes. Normalmente, um cliente assina ou escuta o servidor. Sempre que o servidor fizer uma alteração no servidor ou realizar um evento, o cliente assinante receberá as atualizações. Esse tipo de protocolo é útil quando vários clientes são assinantes e precisam ser notificados sobre alterações que estão acontecendo no servidor ou em outros clientes. Por exemplo, as assinaturas podem ser usadas para atualizar feeds de mídia social. Pode haver dois usuários, o usuário A e o usuário B, que são assinantes das atualizações automáticas de notificação sempre que recebem mensagens diretas. O usuário A no cliente A poderia enviar uma mensagem direta para o usuário B no cliente B. O cliente do usuário A enviaria a mensagem direta, que seria processada pelo servidor. O servidor então enviaria a mensagem direta para a conta do Usuário B enquanto enviaria uma notificação automática para o Cliente B.

Aqui está um exemplo de uma Subscription que poderíamos adicionar ao exemplo do esquema:

type Subscription { personAdded: Person }

O campo personAdded enviará uma mensagem aos clientes inscritos sempre que uma nova Person for adicionada à fonte de dados. Supondo que tenhamos uma implementação de resolvedor para personAdded, agora podemos usar a assinatura. Embora o tipo Subscription exista, precisamos chamá-lo explicitamente para que ele seja executado no código do aplicativo. Isso pode ser feito usando a palavra-chave subscription:

subscription personAddedOperation { personAdded { id name } }

A assinatura é chamada de personAddedOperation e a operação é personAdded. personAddedretornará os campos id e name de novas instâncias de Person. Analisando o exemplo de mutação, adicionamos uma Person usando esta operação:

addPerson(id: "1", name: "Steve Powers", age: "50", occupation: "Miner")

Se nossos clientes tiverem assinatura para receber atualizações da Person recém-adicionada, eles poderão ver isso após as execuções de addPerson:

{ "data": { "personAdded": { "id": "1", "name": "Steve Powers" } } }

Veja abaixo um resumo do que as assinaturas oferecem:

As assinaturas são canais bidirecionais que permitem que o cliente e o servidor recebam atualizações rápidas, mas constantes. Elas normalmente usam o protocolo WebSocket, que cria conexões padronizadas e seguras.

As assinaturas são ágeis, pois reduzem a sobrecarga de configuração da conexão. Depois de fazer uma assinatura, o cliente pode continuar executando essa assinatura por longos períodos. Eles geralmente usam recursos de computação de forma eficiente, permitindo que os desenvolvedores personalizem a vida útil da assinatura e configurem quais informações serão solicitadas.

No geral, o cliente pode fazer várias assinaturas ao mesmo tempo. No que diz respeito ao AWS AppSync, as assinaturas são usadas apenas para receber atualizações em tempo real do serviço AWS AppSync. Elas não podem ser usadas para realizar consultas ou mutações.

A principal alternativa às assinaturas é a sondagem, que envia consultas em intervalos definidos para solicitar dados. Esse processo geralmente é menos eficiente do que as assinaturas e sobrecarrega muito o cliente e o back-end.

Algo que não foi mencionado em nosso exemplo de esquema é o fato de que seus tipos de objetos especiais também devem ser definidos em um schema raiz. Dessa forma, quando você exporta um esquema no AWS AppSync, ele pode ter a seguinte aparência:

schema.graphql
schema { query: Query mutation: Mutation subscription: Subscription } . . . type Query { # code goes here } type Mutation { # code goes here } type Subscription { # code goes here }

Enumerações

Enumerações, ou enums, são escalares especiais que limitam os argumentos legais que um tipo ou campo pode ter. Isso significa que sempre que uma enum for definida no esquema, seu tipo ou campo associado será limitado aos valores da enum. As enums são serializadas como escalares de string. Observe que diferentes linguagens de programação podem lidar com enumerações do GraphQL de forma diferente. Por exemplo, o JavaScript não tem suporte nativo a enumeração, então os valores de enumeração podem ser mapeados para valores int em vez disso.

As enums são definidas usando a palavra-chave enum. Veja um exemplo abaixo:

enum trafficSignals { solidRed solidYellow solidGreen greenArrowLeft ... }

Ao chamar o trafficLights enum, os argumentos só podem ser solidRed, solidYellow, solidGreen, etc. É comum usar enums para descrever coisas que têm um número distinto, mas limitado, de opções.

Uniões/Interfaces

Consulte Interfaces e uniões no GraphQL.