🚀 Nuova versione beta disponibile! Feedback o problemi? Contattaci

Risolutori di Mutations in GraphQL: Come Gestire le Operazioni di Scrittura

Codegrind Team•Sep 03 2024

In GraphQL, le mutations sono utilizzate per eseguire operazioni di scrittura, come la creazione, l’aggiornamento o la cancellazione di dati. A differenza delle query, che si occupano di leggere i dati, le mutations modificano lo stato delle risorse nel server. I risolutori per le mutations gestiscono la logica necessaria per eseguire queste operazioni. In questo articolo esploreremo come implementare risolutori di mutations in GraphQL, come gestire argomenti complessi, validare dati in ingresso e migliorare le performance delle operazioni di scrittura.

Cos’è una Mutation in GraphQL?

Una mutation è una richiesta che altera i dati memorizzati nel backend. In un’applicazione tipica, potrebbe essere usata per:

  • Creare nuovi record (es. creare un nuovo utente o post).
  • Aggiornare dati esistenti (es. aggiornare il profilo di un utente).
  • Cancellare record (es. rimuovere un post o un commento).

Le mutations funzionano in modo simile alle query, ma ogni volta che una mutation viene eseguita, essa può alterare lo stato del sistema. Le mutations in GraphQL sono rappresentate come un tipo separato nello schema.

Esempio di Schema con Mutation

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
  deleteUser(id: ID!): Boolean
}

input CreateUserInput {
  name: String!
  email: String!
  password: String!
}

input UpdateUserInput {
  name: String
  email: String
  password: String
}

type User {
  id: ID!
  name: String!
  email: String!
}

In questo schema, abbiamo tre mutations principali: createUser, updateUser e deleteUser. Le mutations accettano input definiti da tipi personalizzati per gestire i dati in ingresso e restituire il tipo User o Boolean.

Come Funzionano i Risolutori di Mutations

Un risolutore di mutation è una funzione che gestisce la logica di backend per una mutation. Ogni mutation ha un risolutore associato che accetta gli argomenti forniti dal client e interagisce con il database o altre risorse per eseguire l’operazione richiesta.

Struttura di Base di un Risolutore di Mutation

I risolutori di mutation funzionano in modo simile ai risolutori di query, ma il loro obiettivo principale è modificare lo stato dei dati.

const resolvers = {
  Mutation: {
    createUser: async (parent, { input }, context) => {
      const newUser = await context.db.createUser(input);
      return newUser;
    },
    updateUser: async (parent, { id, input }, context) => {
      const updatedUser = await context.db.updateUser(id, input);
      return updatedUser;
    },
    deleteUser: async (parent, { id }, context) => {
      const result = await context.db.deleteUser(id);
      return result ? true : false;
    },
  },
};

In questo esempio, ci sono tre risolutori di mutation che eseguono operazioni asincrone (con async/await) per creare, aggiornare e cancellare un utente.

Esempi Pratici di Risolutori di Mutations

1. Creare una Risorsa

La mutation createUser crea un nuovo utente nel database.

Implementazione di createUser

const resolvers = {
  Mutation: {
    createUser: async (parent, { input }, context) => {
      const { name, email, password } = input;

      // Validazione dei dati
      if (!email.includes("@")) {
        throw new Error("Email non valida");
      }

      // Creazione di un nuovo utente nel database
      const newUser = await context.db.createUser({
        name,
        email,
        password: await hashPassword(password), // Esegui hash della password
      });

      return newUser;
    },
  },
};

In questo esempio:

  • Viene eseguita la validazione dell’email.
  • La password viene hashed prima di essere salvata nel database.
  • Viene restituito l’utente appena creato.

2. Aggiornare una Risorsa

La mutation updateUser consente di aggiornare i dettagli di un utente esistente.

Implementazione di updateUser

const resolvers = {
  Mutation: {
    updateUser: async (parent, { id, input }, context) => {
      // Verifica se l'utente esiste
      const user = await context.db.getUserById(id);
      if (!user) {
        throw new Error("Utente non trovato");
      }

      // Aggiorna i dati dell'utente nel database
      const updatedUser = await context.db.updateUser(id, input);
      return updatedUser;
    },
  },
};

Questo risolutore controlla prima se l’utente esiste e, se trovato, aggiorna i campi forniti nel database.

3. Cancellare una Risorsa

La mutation deleteUser consente di cancellare un utente dal database.

Implementazione di deleteUser

const resolvers = {
  Mutation: {
    deleteUser: async (parent, { id }, context) => {
      const user = await context.db.getUserById(id);
      if (!user) {
        throw new Error("Utente non trovato");
      }

      const result = await context.db.deleteUser(id);
      return result ? true : false;
    },
  },
};

In questo esempio:

  • Si verifica se l’utente esiste prima di tentare di cancellarlo.
  • Se l’utente viene cancellato con successo, viene restituito true, altrimenti false.

Best Practices per Risolutori di Mutations

1. Validazione degli Input

Prima di eseguire una mutation, è essenziale validare gli input forniti dal client. Le mutations che creano o aggiornano risorse dovrebbero assicurarsi che i dati siano nel formato corretto e rispettino le regole di business dell’applicazione.

Esempio di Validazione

const resolvers = {
  Mutation: {
    createUser: async (parent, { input }, context) => {
      const { email } = input;

      // Controllo formato email
      if (!email.includes("@")) {
        throw new Error("Email non valida");
      }

      // Altre validazioni...

      // Creazione dell'utente
      return await context.db.createUser(input);
    },
  },
};

2. Gestione degli Errori

La gestione degli errori è fondamentale nei risolutori di mutation. In caso di errore, come un dato non valido o un problema con il database, dovresti restituire messaggi di errore chiari e significativi al client.

Esempio di Gestione degli Errori

const resolvers = {
  Mutation: {
    updateUser: async (parent, { id, input }, context) => {
      try {
        const user = await context.db.getUserById(id);
        if (!user) {
          throw new Error("Utente non trovato");
        }
        return await context.db.updateUser(id, input);
      } catch (error) {
        throw new Error(
          `Errore durante l'aggiornamento dell'utente: ${error.message}`
        );
      }
    },
  },
};

3. Mutazioni Ottimistiche

Le mutazioni ottimistiche permettono di aggiornare immediatamente l’interfaccia utente prima che la risposta del server sia ricevuta. Questo migliora l’esperienza utente, specialmente in applicazioni con latenza elevata. Relay Modern e Apollo Client supportano le mutazioni ottimistiche.

Esempio con Apollo Client (Mutation Ottimistica)

import { gql, useMutation } from "@apollo/client";

const UPDATE_USER = gql`
  mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
    updateUser(id: $id, input: $input) {
      id
      name
      email
    }
  }
`;

const [updateUser] = useMutation(UPDATE_USER);

const handleUpdate = (id, input) => {
  updateUser({
    variables: { id, input },
    optimisticResponse: {
      updateUser: {
        id,
        name: input.name,
        email: input.email,
      },
    },
  });
};

In questo esempio, Apollo Client aggiorna l’interfaccia utente in modo ottimistico prima che la risposta dal server venga confermata.

4. Gestione delle Transazioni

Se una mutation esegue piĂą operazioni che devono essere eseguite

in modo atomico, è utile implementare transazioni per garantire che o tutte le operazioni vengano completate con successo o nessuna di esse venga applicata.

Esempio di Transazione

const resolvers = {
  Mutation: {
    createOrder: async (parent, { input }, context) => {
      const transaction = await context.db.startTransaction();

      try {
        const order = await context.db.createOrder(input.orderDetails, {
          transaction,
        });
        await context.db.updateInventory(input.products, { transaction });

        await transaction.commit(); // Conferma la transazione
        return order;
      } catch (error) {
        await transaction.rollback(); // Annulla la transazione in caso di errore
        throw new Error("Errore durante la creazione dell'ordine");
      }
    },
  },
};

In questo esempio, la creazione di un ordine e l’aggiornamento dell’inventario sono eseguiti in una transazione. Se uno dei due fallisce, l’intera operazione viene annullata.

Conclusione

I risolutori di mutations in GraphQL sono fondamentali per gestire le operazioni di scrittura, come la creazione, l’aggiornamento e la cancellazione di dati. Implementare risolutori di mutation efficienti e sicuri richiede la gestione accurata degli input, una solida gestione degli errori e l’uso di tecniche avanzate come le mutazioni ottimistiche e le transazioni. Seguendo le best practices per la validazione e la sicurezza, puoi garantire che le tue mutations siano robuste, scalabili e facili da mantenere, offrendo un’esperienza utente fluida e reattiva.