Implementa mutaciones de Data Connect

Firebase Data Connect te permite crear conectores para tus instancias de PostgreSQL administradas con Google Cloud SQL. Estos conectores son combinaciones de consultas y mutaciones para usar tus datos desde tu esquema.

En la guía de inicio, se presentó un esquema de app de revisión de películas para PostgreSQL.

En esa guía, también se presentaron las operaciones administrativas implementables y ad hoc, incluidas las mutaciones.

  • Las mutaciones implementables son aquellas que implementas para llamar desde apps cliente en un conector, con los extremos de API que defines. Data Connect integra la autenticación y la autorización en estas mutaciones, y genera SDKs de cliente basados en tu API.
  • Las mutaciones administrativas ad hoc se ejecutan desde entornos privilegiados para completar y administrar tablas. Puedes crearlos y ejecutarlos en la consola de Firebase, desde entornos con privilegios con Firebase Admin SDK y en entornos de desarrollo locales con nuestra extensión de Data Connect para VS Code.

En esta guía, se analiza con mayor detalle el concepto de mutaciones implementables.

Características de las mutaciones de Data Connect

Data Connect te permite realizar mutaciones básicas de todas las formas que esperarías dada una base de datos de PostgreSQL:

  • Realizar operaciones CRUD
  • Administra operaciones de varios pasos con transacciones

Sin embargo, con las extensiones de Data Connect para GraphQL, puedes implementar mutaciones avanzadas para crear apps más rápidas y eficientes:

  • Usa escalares de clave que muestran muchas operaciones para simplificar las operaciones repetidas en los registros.
  • Usa valores del servidor para completar los datos con las operaciones que proporciona el servidor.
  • Realiza consultas en el transcurso de operaciones de mutación de varios pasos para buscar datos, lo que permite ahorrar líneas de código y viajes de ida y vuelta al servidor.

Cómo usar campos generados para implementar mutaciones

Tus operaciones de Data Connect extenderán un conjunto de campos Data Connect generados automáticamente según los tipos y las relaciones de tipos en tu esquema. Estos campos se generan con herramientas locales cada vez que editas tu esquema.

Puedes usar campos generados para implementar mutaciones, desde la creación, actualización y eliminación de registros individuales en tablas únicas hasta actualizaciones más complejas de varias tablas.

Supongamos que tu esquema contiene un tipo Movie y un tipo Actor asociado. Data Connect genera los campos movie_insert, movie_update, movie_delete y muchos más.

Mutación con el campo
movie_insert

El campo movie_insert representa una mutación para crear un solo registro en la tabla Movie.

Usa este campo para crear una sola película.

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

Mutación con el campo
movie_update

El campo movie_update representa una mutación para actualizar un solo registro en la tabla Movie.

Usa este campo para actualizar una sola película por su clave.

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

Mutación con el campo
movie_delete

El campo movie_delete representa una mutación para borrar un solo registro en la tabla Movie.

Usa este campo para borrar una sola película por su clave.

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

Elementos esenciales de una mutación

Las mutaciones de Data Connect son mutaciones de GraphQL con extensiones de Data Connect. Al igual que con una mutación de GraphQL normal, puedes definir un nombre de operación y una lista de variables de GraphQL.

Data Connect extiende las consultas de GraphQL con directivas personalizadas, como @auth y @transaction.

Por lo tanto, la siguiente mutación tiene lo siguiente:

  • Una definición de tipo mutation
  • Nombre de una operación SignUp (mutación)
  • Un solo argumento de operación $username
  • Una sola directiva, @auth
  • Un solo campo user_insert.
mutation SignUp($username: String!) @auth(level: USER) {
  user_insert(data: {
    id_expr: "auth.uid"
    username: $username
  })
}

Cada argumento de mutación requiere una declaración de tipo, ya sea un tipo integrado como String o un tipo personalizado definido por el esquema como Movie.

Escribe mutaciones básicas

Puedes comenzar a escribir mutaciones para crear, actualizar y borrar registros individuales de tu base de datos.

Crear

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

O una operación de 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"
  })
}

Realiza actualizaciones

Aquí tienes las actualizaciones. Los productores y directores ciertamente esperan que esas calificaciones promedio sigan la tendencia.

El campo movie_update contiene un argumento id esperado para identificar un registro y un campo data que puedes usar para establecer valores en esta actualización.

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

Para realizar varias actualizaciones, usa el 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
      })
}

Usa operaciones de incremento, disminución, anexión y anteposición con _update

Si bien en las mutaciones _update y _updateMany puedes establecer valores de forma explícita en data:, a menudo tiene más sentido aplicar un operador como el incremento para actualizar los valores.

Para modificar el ejemplo de actualización anterior, supongamos que deseas aumentar la calificación de una película en particular. Puedes usar la sintaxis de rating_update con el operador inc.

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

Data Connect admite los siguientes operadores para las actualizaciones de campos:

  • inc para incrementar los tipos de datos Int, Int64, Float, Date y Timestamp
  • dec para disminuir los tipos de datos Int, Int64, Float, Date y Timestamp

En el caso de las listas, también puedes actualizar con valores individuales o listas de valores con lo siguiente:

  • add para agregar elementos si aún no están presentes en los tipos de listas, excepto en las listas de vectores
  • remove para quitar todos los elementos, si están presentes, de los tipos de listas, excepto las listas de vectores
  • append para agregar elementos a tipos de listas, excepto a las listas de vectores
  • prepend para agregar elementos al principio de los tipos de listas, excepto las listas de vectores

Realizar eliminaciones

Por supuesto, puedes borrar los datos de las películas. Los conservacionistas de películas sin duda querrán que se mantengan las películas físicas durante el mayor tiempo posible.

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

Aquí puedes usar _deleteMany.

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

Escribe mutaciones en relaciones

Observa cómo usar la mutación _upsert implícita en una relación.

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

Diseña esquemas para mutaciones eficientes

Data Connect proporciona dos funciones importantes que te permiten escribir mutaciones más eficientes y ahorrar operaciones de ida y vuelta.

Los escalares clave son identificadores de objetos concisos que Data Connect ensambla automáticamente a partir de los campos clave de tus esquemas. Los escalares clave se relacionan con la eficiencia, ya que te permiten encontrar en una sola llamada información sobre la identidad y la estructura de tus datos. Son especialmente útiles cuando deseas realizar acciones secuenciales en registros nuevos y necesitas un identificador único para pasar a las próximas operaciones, y también cuando deseas acceder a claves relacionales para realizar operaciones adicionales más complejas.

Con los valores del servidor, puedes permitir que el servidor complete de forma dinámica los campos de tus tablas con valores almacenados o que se pueden calcular fácilmente según expresiones CEL particulares del servidor en el argumento expr. Por ejemplo, puedes definir un campo con una marca de tiempo aplicada cuando se accede al campo con la hora almacenada en una solicitud de operación, updatedAt: Timestamp! @default(expr: "request.time").

Escribe mutaciones avanzadas: Permite que Data Connect proporcione valores con la sintaxis de field_expr

Como se explicó en Valores clave escalares y del servidor, puedes diseñar tu esquema de modo que el servidor complete los valores de los campos comunes, como ids y fechas, en respuesta a las solicitudes del cliente.

Además, puedes usar datos, como los IDs de usuario, que se envían en objetos Data Connect request desde las apps para clientes.

Cuando implementes mutaciones, usa la sintaxis de field_expr para activar actualizaciones generadas por el servidor o acceder a datos de solicitudes. Por ejemplo, para pasar la autorización uid almacenada en una solicitud a una operación _upsert, pasa "auth.uid" en el 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 })
}

O bien, en una app de lista de tareas familiar, cuando crees una lista de tareas nueva, podrías pasar id_expr para indicarle al servidor que genere automáticamente un UUID para la 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 obtener más información, consulta los escalares de _Expr en la referencia de escalares.

Escribe mutaciones avanzadas: operaciones de varios pasos

Hay muchas situaciones en las que es posible que desees incluir varios campos de escritura (como inserciones) en una mutación. También es posible que desees leer tu base de datos durante la ejecución de una mutación para buscar y verificar los datos existentes antes de realizar, por ejemplo, inserciones o actualizaciones. Estas opciones ahorran operaciones de ida y vuelta y, por lo tanto, costos.

Data Connect te permite realizar lógica de varios pasos en tus mutaciones, ya que admite lo siguiente:

  • Varios campos de escritura

  • Varios campos de lectura en tus mutaciones (con la palabra clave del campo query)

  • La directiva @transaction, que proporciona compatibilidad con transacciones, como en las bases de datos relacionales.

  • La directiva @check, que te permite evaluar el contenido de las lecturas con expresiones de CEL y, según los resultados de esa evaluación, hacer lo siguiente:

    • Continuar con las operaciones de creación, actualización y eliminación definidas por una mutación
    • Cómo devolver los resultados de un campo de consulta
    • Usa los mensajes devueltos para realizar la lógica adecuada en tu código de cliente
  • La directiva @redact, que te permite omitir los resultados del campo de búsqueda de los resultados del protocolo de transferencia.

  • La vinculación response de CEL, que almacena los resultados acumulados de todas las mutaciones y consultas realizadas en una operación compleja de varios pasos. Puedes acceder a la vinculación response:

    • En las directivas @check, a través del argumento expr:
    • Con valores del servidor, usando la sintaxis de field_expr

La directiva @transaction

La compatibilidad con mutaciones de varios pasos incluye el control de errores con transacciones.

La directiva @transaction exige que una mutación, ya sea con un solo campo de escritura (por ejemplo, _insert o _update) o con varios campos de escritura, siempre se ejecute en una transacción de base de datos.

  • Las mutaciones sin @transaction ejecutan cada campo raíz uno después del otro en secuencia. La operación muestra los errores como errores de campos parciales, pero no los impactos de las ejecuciones posteriores.

  • Se garantiza que las mutaciones con @transaction se completarán correctamente o fallarán por completo. Si falla alguno de los campos dentro de la transacción, se revierte toda la transacción.

Las directivas @check y @redact

La directiva @check verifica que los campos especificados estén presentes en los resultados de la búsqueda. Se usa una expresión de Common Expression Language (CEL) para probar los valores de los campos. El comportamiento predeterminado de la directiva es verificar y rechazar los nodos cuyo valor sea null o [] (listas vacías).

La directiva @redact oculta una parte de la respuesta del cliente. Los campos censurados aún se evalúan para detectar efectos secundarios (incluidos los cambios en los datos y @check), y los resultados siguen disponibles para los pasos posteriores en las expresiones de CEL.

Usa @check, @check(message:) y @redact

Un uso importante de @check y @redact es buscar datos relacionados para decidir si se deben autorizar ciertas operaciones, usando la búsqueda en la lógica, pero ocultándola de los clientes. Tu búsqueda puede devolver mensajes útiles para el manejo correcto en el código del cliente.

A modo de ilustración, el siguiente campo de consulta verifica si un solicitante tiene un rol de "administrador" adecuado para ver a los usuarios que pueden editar una película.

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 obtener más información sobre las directivas @check y @redact en las verificaciones de autorización, consulta el análisis de la búsqueda de datos de autorización.

Usa @check para validar claves

Algunos campos de mutación, como _update, pueden no realizar ninguna operación si no existe un registro con una clave especificada. Del mismo modo, las búsquedas pueden devolver un valor nulo o una lista vacía. Estos no se consideran errores y, por lo tanto, no activarán reversiones.

Para evitar este resultado, prueba si se pueden encontrar claves con la directiva @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")
}

Usa la vinculación response para encadenar mutaciones de varios pasos

El enfoque básico para crear registros relacionados, por ejemplo, un nuevo Movie y una entrada MovieMetadata asociada, es el siguiente:

  1. Llama a una mutación _insert para Movie
  2. Almacena la clave devuelta de la película creada
  3. Luego, llama a una segunda mutación _insert para crear el registro MovieMetadata.

Sin embargo, con Data Connect, puedes controlar este caso común en una sola operación de varios pasos accediendo a los resultados del primer _insert en el segundo _insert.

Crear una app exitosa de opiniones de películas requiere mucho trabajo. Hagamos un seguimiento de nuestra lista de tareas pendientes con un nuevo ejemplo.

Usa response para establecer campos con valores del servidor

En la siguiente mutación de la lista de tareas pendientes:

  • La vinculación response representa el objeto de respuesta parcial hasta el momento, que incluye todos los campos de mutación de nivel superior antes del actual.
  • Se accede a los resultados de la operación todoList_insert inicial, que devuelve el campo id (clave), más adelante en response.todoList_insert.id para que podamos insertar de inmediato un nuevo elemento de la lista de tareas pendientes.
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,
  })
}

Usa response para validar campos con @check

response también está disponible en @check(expr: "..."), por lo que puedes usarlo para compilar una lógica del servidor aún más complicada. En combinación con los pasos de query { … } en las mutaciones, puedes lograr mucho más sin ningún viaje de ida y vuelta adicional entre el cliente y el servidor.

En el siguiente ejemplo, ten en cuenta que @check ya tiene acceso a response.query porque un @check siempre se ejecuta después del paso al que está adjunto.

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 obtener más información sobre la vinculación de response, consulta la referencia de CEL.

Comprende las operaciones interrumpidas con @transaction y query @check

Las mutaciones de varios pasos pueden generar errores:

  • Es posible que fallen las operaciones de la base de datos.
  • La lógica de la consulta @check puede finalizar las operaciones.

Data Connect recomienda que uses la directiva @transaction con tus mutaciones de varios pasos. Esto genera una base de datos más coherente y resultados de mutación más fáciles de controlar en el código del cliente:

  • La operación finalizará en el primer error o @check fallido, por lo que no es necesario administrar la ejecución de ningún campo posterior ni la evaluación de CEL.
  • Las reversiones se realizan en respuesta a errores de la base de datos o a la lógica de @check, lo que genera un estado coherente de la base de datos.
  • Siempre se devuelve un error de reversión al código del cliente.

Puede haber algunos casos de uso en los que elijas no usar @transaction: Por ejemplo, podrías optar por la coherencia eventual si necesitas una mayor capacidad de procesamiento, escalabilidad o disponibilidad. Sin embargo, debes administrar tu base de datos y tu código de cliente para permitir los resultados:

  • Si un campo falla debido a operaciones de la base de datos, los campos posteriores seguirán ejecutándose. Sin embargo, los @checks fallidos aún finalizan toda la operación.
  • No se realizan reversiones, lo que significa que la base de datos tiene un estado mixto con algunas actualizaciones exitosas y otras fallidas.
  • Tus operaciones con @check pueden arrojar resultados más incoherentes si tu lógica de @check usa los resultados de lecturas o escrituras en un paso anterior.
  • El resultado que se devuelve al código del cliente contendrá una combinación más compleja de respuestas de éxito y error que se deben controlar.

Directivas para mutaciones de Data Connect

Además de las directivas que usas para definir tipos y tablas, Data Connect proporciona las directivas @auth, @check, @redact y @transaction para aumentar el comportamiento de las operaciones.

Directiva Se aplica a Descripción
@auth Consultas y mutaciones Define la política de autorización para una consulta o mutación. Consulta la guía de autorización y certificación.
@check Campos query en operaciones de varios pasos Verifica que los campos especificados estén presentes en los resultados de la búsqueda. Se usa una expresión de Common Expression Language (CEL) para probar los valores de los campos. Consulta Operaciones de varios pasos.
@redact Consultas Oculta una parte de la respuesta del cliente. Consulta Operaciones de varios pasos.
@transaction Mutaciones Asegura que una mutación siempre se ejecute en una transacción de base de datos. Consulta Operaciones de varios pasos.

Próximos pasos

También te puede interesar: