Eliminación en cascada para GraphQL

eliminación en cascada

AWS Amplify: Eliminación en cascada para GraphQL en una API Lambda — Parte 2

Eliminación en cascada — En la Parte 1, construimos una capa Lambda con un cliente GraphQL para firmar solicitudes a nuestra API GraphQL con la identidad de la persona que llama a la función Lambda.

Ahora, construiremos las funciones de Lambda para usar este cliente, en este caso para implementar eliminaciones en cascada en tablas con relaciones de conexión entre ellas.


En la última publicación, configuramos un cliente GraphQL en una capa Lambda que firmaría solicitudes HTTP a nuestro punto final API GraphQL con un token de autorización proporcionado en su constructor. Pero, ¿de dónde viene este token de autorización?

La respuesta es que si usamos la autorización del grupo de usuarios de Cognito en una API REST y proporcionamos el encabezado Authorization: Bearer XXXX al punto final en nuestro cliente, ese token está disponible en la solicitud a la función Lambda invocada por el usuario autorizado.

Simplemente necesitamos extraerlo y luego pasarlo a nuestro cliente para que todas nuestras consultas funcionen, bien y con alcance.

Eliminación en cascada: esquema de ejemplo

Considere el siguiente esquema para una aplicación de vestuario, que se ha reducido a lo esencial:

type Outfit
  @model
  @auth(rules: [{ allow: owner }]) {
  components: [OutfitComponent!] @hasMany
}

type OutfitComponent
  @model
  @auth(rules: [{ allow: owner }]) {
  clotheImage: ClotheImage! @belongsTo
}

type Clothe
  @model
  @auth(rules: [{ allow: owner }]) {
  images: [ClotheImage!]! @hasMany
}

type ClotheImage
  @model
  @auth(rules: [{ allow: owner }]) {
  components: [OutfitComponent] @hasMany
  clothe: Clothe! @belongsTo
}

En el esquema anterior, definimos dos tipos principales: ClotheOutfits,  con tipos secundarios ClotheImage y OutfitComponent respectivamente: un  Clothe puede tener cualquier cantidad de ClotheImages, y un Outfit puede tener cualquier cantidad de OutfitComponents que hacen referencia a una sola ClotheImage, cada uno junto con otros metadatos no incluidos.

Cuando eliminamos una Clothe de la base de datos, también queremos eliminar todas sus ClotheImages correspondientes.

Sin embargo, también debemos eliminar todos los OutfitComponents correspondientes a las ClotheImages que se están eliminando, de lo contrario, nos quedarán referencias null donde no puede haberlas.

Entonces necesitamos una eliminación en cascada anidada dos veces en este caso. No hay que temer: Lambda está aquí.

Eliminación en cascada: Función Lambda

Construyamos una función Lambda de la clotheAPI que pueda manejar cualquier tipo de acción compleja que queramos realizar en una ropa, ya sea una eliminación en cascada o cualquier otra cosa en el futuro.

Cuando agreguemos nuestra API REST, colocaremos esta función Lambda detrás de una ruta como  /clothe/{clotheId} para poder usar métodos HTTP estándar como GETDELETE para realizar las acciones deseadas.

Agregue la función para amplificar:

$ amplify add function
? Provide an AWS Lambda function name: clotheAPI
? Choose the runtime that you want to use: NodeJS
? Choose the function template that you want to use: Serverless ExpressJS function (Integration with API Gateway)Available advanced settings:
- Resource access permissions
- Scheduled recurring invocation
- Lambda layers configuration
- Environment variables configuration
- Secret values configuration? Do you want to configure advanced settings? Yes
? Do you want to access other resources in this project from your Lambda function? Yes
? Select the categories you want this function to have access to. api
? Select the operations you want to permit on <my-wardrobe-api> Query, MutationYou can access the following resource attributes as environment variables from your Lambda function
 API_<MY-WARDROBE-API>_GRAPHQLAPIENDPOINTOUTPUT
 API_<MY-WARDROBE-API>_GRAPHQLAPIIDOUTPUT
 ENV
 REGION
? Do you want to invoke this function on a recurring schedule? No
? Do you want to enable Lambda layers for this function? Yes
? Provide existing layers or select layers in this project to access from this function (pick up to 5): <graphql-client-layer-from-part1>
? Do you want to configure environment variables for this function? No
? Do you want to configure secret values this function can access? No
? Do you want to edit the local lambda function now? No
Successfully added resource clotheAPI locally.

Bien, excelente. Ahora tenemos una función Lambda Express sin servidor incorporada en nuestro proyecto Amplify local, que tiene acceso al cliente GraphQL desde la capa Lambda en /opt/graphQLClient.js.

En este momento, solo nos importa implementar la funcionalidad de eliminación, así que agregue algo de código en app.js para registrar una ruta rápida en /clothe/:clotheId para el método DELETE:

const Clothe = require("./clothe.js");

\\ ...

app.delete("/clothe/:clotheId", function (req, res) {
  if (!req.headers["authorization"]) {
    res.json({ success: false, error: "Not authenticated!" });
  } else if (!req.params.clotheId) {
    res.json({ success: false, error: "No clothe ID parameter!" });
  } else {
    Clothe.deleteCascading(req.params.clotheId, req.headers["authorization"])
      .then((collaterals) => {
        res.json({ success: true, data: collaterals });
      })
      .catch((err) => {
        console.log(err);
        res.json({ success: false, error: err });
      });
  }
});

\\ ...

No te asustes por la importación de Clothe, lo haremos en solo un segundo. Por ahora, observe cómo leemos en el encabezado de authorization de la solicitud. ¡Ahí es donde encontraremos el Bearer XXX con el token Cognito JWT adentro! Solo tenemos que enviarlo a nuestro cliente GraphQL y luego podemos usarlo para autenticar las solicitudes.

Ahora, implementemos el código real para nuestra eliminación en cascada. Tenga en cuenta que he definido las siguientes consultas y mutaciones en /opt/graphql/queries.js/opt/graphql/mutations.js de Lambda Layer para facilitar la eliminación en cascada:

// /opt/graphql/queries.js

const listClotheImages = /* GraphQL */ `
  query listClotheImages(
    $filter: ModelClotheImageFilterInput
    $nextToken: String
  ) {
    listClotheImages(filter: $filter, nextToken: $nextToken) {
      items {
        id
      }
      nextToken
    }
  }
`;

const listOutfitComponents = /* GraphQL */ `
  query listOutfitComponents(
    $filter: ModelOutfitComponentFilterInput
    $nextToken: String
  ) {
    listOutfitComponents(filter: $filter, nextToken: $nextToken) {
      items {
        id
        outfitComponentsId
      }
      nextToken
    }
  }
`;

module.exports = {
  listClotheImages,
  listOutfitComponents,
};


// /opt/graphql/mutations.js

const deleteClothe = /* GraphQL */ `
  mutation deleteClothe($input: DeleteClotheInput!) {
    deleteClothe(input: $input) {
      id
    }
  }
`;

const deleteClotheImage = /* GraphQL */ `
  mutation deleteClotheImage($input: DeleteClotheImageInput!) {
    deleteClotheImage(input: $input) {
      id
    }
  }
`;

const deleteOutfitComponent = /* GraphQL */ `
  mutation deleteOutfitComponent($input: DeleteOutfitComponentInput!) {
    deleteOutfitComponent(input: $input) {
      id
    }
  }
`;

module.exports = {
  deleteClothe,
  deleteClotheImage,
  deleteOutfitComponent,
};

Ahora, solo creemos el código de eliminación en cascada en un archivo llamado clothe.js en el directorio src de nuestra función Lambda, en el mismo nivel que nuestra app.js:

const GraphQLClient = require("/opt/graphQLClient.js");
const Q = require("/opt/graphql/queries.js");
const M = require("/opt/graphql/mutations.js");

/* -------------------------------------------------------------------------- */
/*                                   EXPORTS                                  */
/* -------------------------------------------------------------------------- */

/**
 * Deletes a clothe, its clothe images, and any corresponding outfit components.
 * @param {*} clotheId The ID of the clothe to delete.
 * @param {*} authHeader The Bearer header from Lambda.
 */
const deleteClotheCascading = async (clotheId, authHeader) => {
  const collaterals = {
    clotheImages: new Set(),
    outfitComponents: new Set(),
    outfits: new Set(),
  };
  const GQLClient = new GraphQLClient(authHeader);
  // 1. Get all collateral clothe images and outfit components
  const clotheImages = await GQLClient.runWithPaginate(
    Q.listClotheImages,
    {
      filter: {
        clotheImagesId: {
          eq: clotheId,
        },
      },
    },
    "listClotheImages"
  );
  for (const clotheImage of clotheImages) {
    collaterals.clotheImages.add(clotheImage.id);
    const outfitComponents = await GQLClient.runWithPaginate(
      Q.listOutfitComponents,
      {
        filter: {
          clotheImageComponentsId: {
            eq: clotheImage.id,
          },
        },
      },
      "listOutfitComponents"
    );
    for (const outfitComponent of outfitComponents) {
      collaterals.outfitComponents.add(outfitComponent.id);
      collaterals.outfits.add(outfitComponent.outfitComponentsId);
    }
  }
  // 2. Delete all the collaterals (clothe, clotheImages, outfitComponents)
  await GQLClient.run(M.deleteClothe, {
    input: {
      id: clotheId,
    },
  });
  for (const clotheImageId of collaterals.clotheImages) {
    await GQLClient.run(M.deleteClotheImage, {
      input: {
        id: clotheImageId,
      },
    });
  }
  for (const outfitComponentId of collaterals.outfitComponents) {
    await GQLClient.run(M.deleteOutfitComponent, {
      input: {
        id: outfitComponentId,
      },
    });
  }
  // Now return the clothe id and the updated outfit ids
  return {
    clothe: clotheId,
    outfits: Array.from(collaterals.outfits),
  };
};

module.exports = {
  deleteCascading: deleteClotheCascading,
};

¡Ahí está el cliente GraphQL de nuestra capa Lambda en acción! Ejecuta las consultas de list y agrega los elementos colaterales que se destruirán en cada nivel anidado a un conjunto.

Una vez que se han encontrado todos los elementos colaterales, continúa, los recorre en bucle y ejecuta las mutaciones de eliminación. Esto probablemente podría optimizarse mediante el uso de la capacidad BatchWriteItem de DynamoDB, pero por ahora esto es suficiente.

Y luego que tengamos la API REST en funcionamiento, verá que el acceso a las tablas tiene un alcance basado en la propiedad del usuario que llama a la función Lambda.


Todo este código en conjunto define nuestra capa Lambda y la función Lambda para una ruta específica en una API REST hipotética, es decir, /clothe/{clotheId} con el método DELETE.

Pero aún no hemos creado esa API REST. Entonces, en la Parte 3, la última instalación de la serie, finalmente construiremos nuestra API con la autorización de Cognito y crearemos un punto de acceso para la función Lambda que creamos aquí.

Si tiene alguna pregunta o problema de implementación, no dude en contactarme.

Recent Post