Implementar mutações do Data Connect

Com o Firebase Data Connect, é possível criar conectores para suas instâncias do PostgreSQL gerenciadas com o Google Cloud SQL. Esses conectores são combinações de consultas e mutações para usar os dados do seu esquema.

O guia de início rápido apresentou um esquema de app de avaliação de filmes para PostgreSQL.

Esse guia também apresentou operações administrativas implantáveis e ad hoc, incluindo mutações.

  • As mutações implantáveis são aquelas que você implementa para chamar de apps clientes em um conector, com endpoints de API definidos por você. O Data Connect integra autenticação e autorização a essas mutações e gera SDKs de cliente com base na sua API.
  • As mutações administrativas ad hoc são executadas em ambientes privilegiados para preencher e gerenciar tabelas. É possível criar e executar essas consultas no console Firebase, em ambientes privilegiados usando o Firebase Admin SDK e em ambientes de desenvolvimento local usando a extensão do Data Connect para VS Code.

Este guia analisa mais detalhadamente as mutações implantáveis.

Recursos das mutações de Data Connect

Com o Data Connect, é possível fazer mutações básicas de todas as maneiras esperadas em um banco de dados PostgreSQL:

  • Realizar operações CRUD
  • Gerenciar operações de várias etapas com transações

Mas com as extensões do Data Connect para GraphQL, é possível implementar mutações avançadas para apps mais rápidos e eficientes:

  • Use escalares de chave retornados por muitas operações para simplificar operações repetidas em registros.
  • Use valores do servidor para preencher dados com operações fornecidas pelo servidor.
  • Realize consultas no decorrer de operações de mutação de várias etapas para pesquisar dados, economizando linhas de código e viagens de ida e volta ao servidor.

Usar campos gerados para implementar mutações

Suas operações Data Connect vão estender um conjunto de campos Data Connect gerados automaticamente com base nos tipos e nas relações de tipo no seu esquema. Esses campos são gerados por ferramentas locais sempre que você edita o esquema.

É possível usar campos gerados para implementar mutações, desde a criação, atualização e exclusão de registros individuais em tabelas únicas até atualizações mais complexas em várias tabelas.

Suponha que seu esquema contenha um tipo Movie e um tipo Actor associado. O Data Connect gera os campos movie_insert, movie_update, movie_delete e muito mais.

Mutação com o campo
movie_insert

O campo movie_insert representa uma mutação para criar um único registro na tabela Movie.

Use esse campo para criar um único filme.

mutation CreateMovie($data: Movie_Data!) {
  movie_insert(data: $data) { key }
}

Mutação com o campo
movie_update

O campo movie_update representa uma mutação para atualizar um único registro na tabela Movie.

Use este campo para atualizar um único filme pela chave.

mutation UpdateMovie($myKey: Movie_Key!, $data: Movie_Data!) {
  movie_update(key: $myKey, data: $data) { key }
}

Mutação com o campo
movie_delete

O campo movie_delete representa uma mutação para excluir um único registro na tabela Movie.

Use este campo para excluir um único filme pela chave.

  mutation DeleteMovie($myKey: Movie_Key!) {
    movie_delete(key: $myKey) { key }
  }

Elementos essenciais de uma mutação

As mutações do Data Connect são mutações GraphQL com extensões Data Connect. Assim como em uma mutação normal do GraphQL, você pode definir um nome de operação e uma lista de variáveis do GraphQL.

O Data Connect estende as consultas GraphQL com diretivas personalizadas, como @auth e @transaction.

Portanto, a seguinte mutação tem:

  • Uma definição de tipo mutation
  • Um nome de operação (mutação) SignUp
  • Um único argumento de operação de variável $username
  • Uma única diretiva, @auth
  • Um único campo user_insert.
mutation SignUp($username: String!) @auth(level: USER) {
  user_insert(data: {
    id_expr: "auth.uid"
    username: $username
  })
}

Todo argumento de mutação exige uma declaração de tipo, um tipo integrado como String ou um tipo personalizado definido pelo esquema, como Movie.

Escrever mutações básicas

Você pode começar a escrever mutações para criar, atualizar e excluir registros individuais do seu banco de dados.

Criar

Vamos fazer criações básicas.

# Create a movie based on user input
mutation CreateMovie($title: String!, $releaseYear: Int!, $genre: String!, $rating: Int!) {
  movie_insert(data: {
    title: $title
    releaseYear: $releaseYear
    genre: $genre
    rating: $rating
  })
}

# Create a movie with default values
mutation CreateMovie2 {
  movie_insert(data: {
    title: "Sherlock Holmes"
    releaseYear: 2009
    genre: "Mystery"
    rating: 5
  })
}

Ou um upsert.

# Movie upsert using combination of variables and literals
mutation UpsertMovie($title: String!) {
  movie_upsert(data: {
    title: $title
    releaseYear: 2009
    genre: "Mystery"
    rating: 5
    genre: "Mystery/Thriller"
  })
}

Fazer atualizações

Confira as atualizações. Produtores e diretores certamente esperam que essas classificações médias estejam na tendência.

O campo movie_update contém um argumento id esperado para identificar um registro e um campo data que pode ser usado para definir valores nessa atualização.

mutation UpdateMovie(
  $id: UUID!,
  $genre: String!,
  $rating: Int!,
  $description: String!
) {
  movie_update(id: $id,
    data: {
      genre: $genre
      rating: $rating
      description: $description
    })
}

Para fazer várias atualizações, use o campo movie_updateMany.

# Multiple updates (increase all ratings of a genre)
mutation IncreaseRatingForGenre($genre: String!, $rating: Int!) {
  movie_updateMany(
    where: { genre: { eq: $genre } },
    data:
      {
        rating: $rating
      })
}

Use operações de incremento, decremento, anexação e pré-anexação com _update

Embora nas mutações _update e _updateMany seja possível definir valores explicitamente em data:, geralmente é mais sensato aplicar um operador como incremento para atualizar valores.

Para modificar o exemplo de atualização anterior, suponha que você queira aumentar a classificação de um filme específico. É possível usar a sintaxe rating_update com o operador inc.

mutation UpdateMovie(
  $id: UUID!,
  $ratingIncrement: Int!
) {
  movie_update(id: $id, data: {
    rating_update: {
      inc: $ratingIncrement
    }
  })
}

O Data Connect aceita os seguintes operadores para atualizações de campo:

  • inc para incrementar os tipos de dados Int, Int64, Float, Date e Timestamp
  • dec para diminuir os tipos de dados Int, Int64, Float, Date e Timestamp

Para listas, também é possível atualizar com valores individuais ou listas de valores usando:

  • add para anexar itens se eles ainda não estiverem presentes nos tipos de lista, exceto listas de vetores.
  • remove para remover todos os itens, se houver, dos tipos de lista, exceto listas de vetores
  • append para anexar itens a tipos de lista, exceto listas de vetores
  • prepend para adicionar itens aos tipos de lista, exceto listas de vetores

Realizar exclusões

É claro que você pode excluir os dados de filmes. Os preservacionistas de filmes certamente vão querer que os filmes físicos sejam mantidos pelo maior tempo possível.

# Delete by key
mutation DeleteMovie($id: UUID!) {
  movie_delete(id: $id)
}

Aqui você pode usar o _deleteMany.

# Multiple deletes
mutation DeleteUnpopularMovies($minRating: Int!) {
  movie_deleteMany(where: { rating: { le: $minRating } })
}

Gravar mutações em relações

Observe como usar a mutação _upsert implícita em uma relação.

# Create or update a one to one relation
mutation MovieMetadataUpsert($movieId: UUID!, $director: String!) {
  movieMetadata_upsert(
    data: { movie: { id: $movieId }, director: $director }
  )
}

Projetar esquemas para mutações eficientes

O Data Connect oferece dois recursos importantes que permitem escrever mutações mais eficientes e economizar operações de ida e volta.

Escalares de chave são identificadores de objetos concisos que o Data Connect monta automaticamente com base em campos-chave nos seus esquemas. Os escalares principais são sobre eficiência, permitindo que você encontre em uma única chamada informações sobre a identidade e a estrutura dos seus dados. Elas são especialmente úteis quando você quer realizar ações sequenciais em novos registros e precisa de um identificador exclusivo para transmitir às próximas operações, e também quando quer acessar chaves relacionais para realizar outras operações mais complexas.

Usando valores do servidor, você pode permitir que o servidor preencha dinamicamente campos nas suas tabelas usando valores armazenados ou facilmente calculáveis de acordo com expressões CEL específicas do lado do servidor no argumento expr. Por exemplo, é possível definir um campo com um carimbo de data/hora aplicado quando ele é acessado usando o tempo armazenado em uma solicitação de operação, updatedAt: Timestamp! @default(expr: "request.time").

Escrever mutações avançadas: permitir que Data Connect forneça valores usando a sintaxe field_expr

Como discutido em escalares principais e valores do servidor, você pode projetar seu esquema para que o servidor preencha valores de campos comuns, como ids e datas, em resposta a solicitações do cliente.

Além disso, é possível usar dados, como IDs de usuário, enviados em objetos Data Connect request de apps cliente.

Ao implementar mutações, use a sintaxe field_expr para acionar atualizações geradas pelo servidor ou acessar dados de solicitações. Por exemplo, para transmitir a autorização uid armazenada em uma solicitação para uma operação _upsert, transmita "auth.uid" no campo userId_expr.

# Add a movie to the user's favorites list
mutation AddFavoritedMovie($movieId: UUID!) @auth(level: USER) {
  favorite_movie_upsert(data: { userId_expr: "auth.uid", movieId: $movieId })
}

# Remove a movie from the user's favorites list
mutation DeleteFavoritedMovie($movieId: UUID!) @auth(level: USER) {
  favorite_movie_delete(key: { userId_expr: "auth.uid", movieId: $movieId })
}

Ou, em um app de lista de tarefas conhecido, ao criar uma nova lista, você pode transmitir id_expr para instruir o servidor a gerar automaticamente um UUID para a lista.

mutation CreateTodoListWithFirstItem(
  $listName: String!
) @transaction {
  # Step 1
  todoList_insert(data: {
    id_expr: "uuidV4()", # <-- auto-generated. Or a column-level @default on `type TodoList` will also work
    name: $listName,
  })
}

Para mais informações, consulte os escalares _Expr na referência de escalares.

Escrever mutações avançadas: operações de várias etapas

Há muitas situações em que você pode querer incluir vários campos de gravação (como inserções) em uma mutação. Você também pode querer ler seu banco de dados durante a execução de uma mutação para pesquisar e verificar os dados existentes antes de realizar, por exemplo, inserções ou atualizações. Essas opções economizam operações de ida e volta e, portanto, custos.

O Data Connect permite realizar lógica de várias etapas nas mutações com suporte para:

  • Vários campos de gravação

  • Vários campos de leitura nas suas mutações (usando a palavra-chave do campo query).

  • A diretiva @transaction, que oferece suporte a transações conhecido dos bancos de dados relacionais.

  • A diretiva @check, que permite avaliar o conteúdo das leituras usando expressões CEL e, com base nos resultados dessa avaliação:

    • Continuar com criações, atualizações e exclusões definidas por uma mutação
    • Continuar para retornar os resultados de um campo de consulta
    • Use as mensagens retornadas para executar a lógica apropriada no código do cliente.
  • A diretiva @redact, que permite omitir resultados de campos de consulta dos resultados do protocolo de rede.

  • A vinculação response da CEL, que armazena os resultados acumulados de todas as mutações e consultas realizadas em uma operação complexa de várias etapas. É possível acessar a vinculação response:

    • Em diretivas @check, usando o argumento expr:
    • Com valores do servidor, usando a sintaxe field_expr

A diretiva @transaction

O suporte para mutações de várias etapas inclui o tratamento de erros usando transações.

A diretiva @transaction garante que uma mutação, com um único campo de gravação (por exemplo, _insert ou _update) ou com vários campos de gravação, sempre seja executada em uma transação de banco de dados.

  • Mutações sem @transaction executam cada campo raiz um após o outro em sequência. A operação mostra erros como erros de campo parciais, mas não os impactos das execuções subsequentes.

  • As mutações com @transaction têm garantia de sucesso ou falha total. Se algum dos campos na transação falhar, toda a transação será revertida.

As diretivas @check e @redact

A diretiva @check verifica se os campos especificados estão presentes nos resultados da consulta. Uma expressão Common Expression Language (CEL) é usada para testar valores de campo. O comportamento padrão da diretiva é verificar e rejeitar nós cujo valor é null ou [] (listas vazias).

A diretiva @redact encobre uma parte da resposta do cliente. Os campos omitidos ainda são avaliados quanto a efeitos colaterais (incluindo mudanças de dados e @check), e os resultados ainda estão disponíveis para etapas posteriores em expressões CEL.

Usar @check, @check(message:) e @redact

Um dos principais usos de @check e @redact é pesquisar dados relacionados para decidir se determinadas operações devem ser autorizadas, usando a pesquisa na lógica, mas ocultando-a dos clientes. Sua consulta pode retornar mensagens úteis para o processamento correto no código do cliente.

Para fins de ilustração, o campo de consulta a seguir verifica se um solicitante tem uma função de "administrador" adequada para ver os usuários que podem editar um filme.

query GetMovieEditors($movieId: UUID!) @auth(level: USER) {
  moviePermission(key: { movieId: $movieId, userId_expr: "auth.uid" }) @redact {
    role @check(expr: "this == 'admin'", message: "You must be an admin to view all editors of a movie.")
  }
  moviePermissions(where: { movieId: { eq: $movieId }, role: { eq: "editor" } }) {
    user {
      id
      username
    }
  }
}

Para saber mais sobre as diretivas @check e @redact em verificações de autorização, consulte a discussão sobre a pesquisa de dados de autorização.

Usar @check para validar chaves

Alguns campos de mutação, como _update, podem não fazer nada se um registro com uma chave especificada não existir. Da mesma forma, as pesquisas podem retornar nulo ou uma lista vazia. Esses não são considerados erros e, portanto, não acionam rollbacks.

Para evitar esse resultado, teste se as chaves podem ser encontradas usando a diretiva @check.

# Delete by key, error if not found
mutation MustDeleteMovie($id: UUID!) @transaction {
  movie_delete(id: $id) @check(expr: "this != null", message: "Movie not found, therefore nothing is deleted")
}

Usar a vinculação response para encadear mutações de várias etapas

A abordagem básica para criar registros relacionados, por exemplo, um novo Movie e uma entrada MovieMetadata associada, é:

  1. Chamar uma mutação de _insert para Movie
  2. Armazene a chave retornada do filme criado
  3. Em seguida, chame uma segunda mutação _insert para criar o registro MovieMetadata.

Mas com Data Connect, é possível processar esse caso comum em uma única operação de várias etapas acessando os resultados do primeiro _insert no segundo _insert.

Criar um app de avaliação de filmes de sucesso dá muito trabalho. Vamos acompanhar nossa lista de tarefas com um novo exemplo.

Use response para definir campos com valores do servidor

Na seguinte mutação da lista de tarefas:

  • A vinculação response representa o objeto de resposta parcial até o momento, que inclui todos os campos de mutação de nível superior antes do atual.
  • Os resultados da operação todoList_insert inicial, que retorna o campo id (chave), são acessados mais tarde em response.todoList_insert.id para que possamos inserir imediatamente um novo item de tarefa.
mutation CreateTodoListWithFirstItem(
  $listName: String!,
  $itemContent: String!
) @transaction {
  # Sub-step 1:
  todoList_insert(data: {
    id_expr: "uuidV4()", # <-- auto-generated. Or a column-level @default on `type TodoList` will also work
    name: $listName,
  })
  # Sub-step 2:
  todo_insert(data: {
    listId_expr: "response.todoList_insert.id" # <-- Grab the newly generated ID from the partial response so far.
    content: $itemContent,
  })
}

Use response para validar campos usando @check

O response também está disponível em @check(expr: "..."), para que você possa usá-lo e criar uma lógica do lado do servidor ainda mais complicada. Combinado com as etapas query { … } em mutações, é possível fazer muito mais sem viagens de ida e volta adicionais entre cliente e servidor.

No exemplo a seguir, observe que @check já tem acesso a response.query porque um @check sempre é executado após a etapa a que está anexado.

mutation CreateTodoInNamedList(
  $listName: String!,
  $itemContent: String!
) @transaction {
  # Sub-step 1: Look up List.id by its name
  query
  @check(expr: "response.query.todoLists.size() > 0", message: "No such TodoList with the name!")
  @check(expr: "response.query.todoLists.size() < 2", message: "Ambiguous listName!") {
    todoLists(where: { name: $listName }) {
      id
    }
  }
  # Sub-step 2:
  todo_insert(data: {
    listId_expr: "response.todoLists[0].id" # <-- Now we have the parent list ID to insert to
    content: $itemContent,
  })
}

Para mais informações sobre a vinculação response, consulte a referência da CEL.

Entenda as operações interrompidas com @transaction e query @check

Mutações de várias etapas podem encontrar erros:

  • As operações de banco de dados podem falhar.
  • A lógica de consulta @check pode encerrar operações.

O Data Connect recomenda que você use a diretiva @transaction com suas mutações de várias etapas. Isso resulta em um banco de dados mais consistente e em resultados de mutação mais fáceis de processar no código do cliente:

  • No primeiro erro ou falha de @check, a operação será encerrada. Portanto, não é necessário gerenciar a execução de campos subsequentes ou a avaliação da CEL.
  • As reversões são realizadas em resposta a erros de banco de dados ou lógica @check, resultando em um estado consistente do banco de dados.
  • Um erro de rollback sempre é retornado ao código do cliente.

Em alguns casos de uso, talvez você opte por não usar @transaction. Por exemplo, é possível escolher a consistência eventual se precisar de maior capacidade de processamento, escalonabilidade ou disponibilidade. No entanto, você precisa gerenciar seu banco de dados e seu código do cliente para permitir os resultados:

  • Se um campo falhar devido a operações de banco de dados, os campos subsequentes continuarão a ser executados. No entanto, @checks com falha ainda encerram toda a operação.
  • Os rollbacks não são realizados, o que significa um estado de banco de dados misto com algumas atualizações bem-sucedidas e outras com falha.
  • Suas operações com @check podem gerar resultados mais inconsistentes se a lógica de @check usar os resultados de leituras e/ou gravações em uma etapa anterior.
  • O resultado retornado ao código do cliente vai conter uma combinação mais complexa de respostas de sucesso e falha a serem processadas.

Diretivas para mutações de Data Connect

Além das diretivas usadas para definir tipos e tabelas, o Data Connect fornece as diretivas @auth, @check, @redact e @transaction para aumentar o comportamento das operações.

Diretiva Aplicável a Descrição
@auth Consultas e mutações Define a política de autorização para uma consulta ou mutação. Consulte o guia de autorização e comprovação.
@check Campos query em operações de várias etapas Verifica se os campos especificados estão presentes nos resultados da consulta. Uma expressão Common Expression Language (CEL) é usada para testar valores de campo. Consulte Operações de várias etapas.
@redact Consultas Reduz uma parte da resposta do cliente. Consulte Operações de várias etapas.
@transaction Mutações Garante que uma mutação sempre seja executada em uma transação de banco de dados. Consulte Operações de várias etapas.

Próximas etapas

Talvez você se interesse por: