Fermer

avril 5, 2024

Bases de GraphQL : résolveurs

Bases de GraphQL : résolveurs


Les résolveurs GraphQL permettent à un client de spécifier les données qu’il souhaite obtenir d’un serveur.

Dans un article précédent, nous avons discuté de certains éléments fondamentaux de GraphQL, tels que schéma, types d’objets et leurs relations. Aujourd’hui, nous allons approfondir un autre composant essentiel de tout serveur GraphQL :Résolveurs GraphQL.

Si nous nous souvenons, GraphQL est incroyablement puissant en permettant à un client de spécifier exactement les données qu’il souhaite obtenir d’un serveur. Cependant, comment le serveur sait-il comment récupérer ou dériver réellement ces données ? C’est là que les résolveurs entrent en jeu.

Champs racine

Dans un schéma GraphQL, les principaux points d’entrée des demandes des clients sont appelés champs racine. Ce sont des champs sur le principal Query, Mutation et Subscription les types. Chaque fois qu’une requête client arrive, ce sont les champs racine qui répondent initialement.

Par exemple, considérons user comme champ racine sur le Query taper.

type Query {
  user(id: ID!): User
}

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

La requête GraphQL à récupérer user les informations ressembleraient à ce qui suit :

{
  user(id: 1) {
    name
    email
  }
}

Résolveurs

Un résolveur GraphQL est essentiellement une fonction dont le travail consiste à remplir les données d’un seul champ de notre schéma. Chaque fois qu’un champ est exécuté dans une requête, la fonction liée à ce champ est appelée et fournit alors les données nécessaires.

Pour le user exemple que nous avons ci-dessus, pour récupérer le name et email pour un utilisateur particulier, nous aurions une fonction de résolution pour le user champ. Ce résolveur interagirait avec notre base de données ou toute autre source de données pour récupérer les informations requises.

const resolvers = {
  Query: {
    user: (obj, args, context, info) => {
      
      return database.getUserById(args.id);
    },
  },
};

Un résolveur GraphQL reçoit toujours quatre arguments de position.

  • obj: L’objet renvoyé par le résolveur sur le champ parent. Pour la racine Query et Mutation types d’objets, cet argument n’est souvent pas utilisé et n’est pas défini.
  • arguments: Les arguments fournis au champ. Pour notre user requête ci-dessus, le id l’argument serait disponible dans le args paramètre.
  • contexte: Une valeur fournie à chaque résolveur qui contient des informations contextuelles importantes. Ce contexte peut contenir des informations telles que l’utilisateur actuellement connecté, la connexion à la base de données, etc., et est particulièrement utile pour transmettre des données et des configurations partagées via des chaînes de résolution.
  • Info: Utilisé généralement uniquement dans les cas avancés mais contient des informations sur le exécution état de la requête, tel que le fieldName, schema, rootValueetc.

Résolveurs triviaux

Il convient de noter que chaque champ d’un schéma GraphQL est soutenu par un résolveur. Si aucun résolveur n’est explicitement défini pour un champ, les bibliothèques GraphQL utilisent souvent un résolveur par défaut. Pour expliquer cela davantage, considérons le User tapez notre exemple de schéma GraphQL.

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

Si nous avons déjà récupéré un user objet de notre base de données avec des propriétés qui correspondent à notre type d’objet GraphQL, et nous ne spécifions pas de résolveurs pour name et email, GraphQL utilisera les résolveurs par défaut. Cela signifie qu’il examinera l’objet parent (dans ce cas, notre user objet de la base de données) pour les propriétés portant le même nom et les renvoyer.


const userFromDatabase = {
  name: "John Doe",
  email: "john@example.com",
};


const resolvers = {
  User: {
    name: (user) => user.name,
    email: (user) => user.email,
  },
};

Cependant, dans les cas où les champs de notre schéma GraphQL et la source de données sous-jacente ne sont pas directement mappés ou lorsque certains champs nécessitent des calculs supplémentaires, des résolveurs personnalisés deviennent nécessaires.

Résolveurs asynchrones

Les résolveurs ont la capacité d’exécuter des opérations asynchrones, comme des requêtes de base de données ou des appels d’API. Dans ces situations, un résolveur peut produire une promesse qui finit par atteindre une valeur de retour acceptable. À titre d’exemple, supposons que nous souhaitions récupérer l’historique des achats récents d’un utilisateur à partir d’un microservice externe. Dans notre schéma, cela pourrait ressembler à :

type User {
  name: String!
  email: String!
  recentPurchases: [Purchase!]!
}

type Purchase {
  id: ID!
  item: String!
  price: Float!
}

Pour obtenir ces données d’achat, nous devrons peut-être effectuer un appel API asynchrone dans notre fonction de résolution :

const resolvers = {
  User: {
    recentPurchases: async (user, args, context) => {
      
      const purchases = await api.fetchPurchasesForUser(user.id);
      return purchases;
    },
  },
};

Notez que dans ce scénario, la fonction de résolution est asynchrone, marquée par le async mot-clé. La fonction utilise le await mot-clé pour attendre la fin de l’appel API. Ce modèle asynchrone garantit que pendant que nous attendons des données, d’autres opérations peuvent continuer, utilisant ainsi efficacement les ressources du serveur.

Autorisation au niveau du terrain

Une application fascinante des résolveurs consiste à effectuer une autorisation au niveau du champ. Dans un schéma GraphQL, nous pouvons avoir des champs qui ne doivent être accessibles qu’à certains utilisateurs en fonction de leurs rôles ou autorisations. Au lieu de gérer cette logique d’autorisation au niveau du contrôleur ou de la route (comme cela se fait couramment dans les API RESTful), GraphQL nous permet d’encapsuler cette logique directement dans le résolveur.

Prenons notre User tapez comme exemple qui a maintenant un nouveau privateNote champ:

type User {
  name: String!
  email: String!
  privateNote: String
}

Supposons que le privateNote Le champ ne doit être accessible que par l’utilisateur visualisant ses propres informations. Nous pouvons implémenter cela dans le résolveur comme ceci :

const resolvers = {
  User: {
    privateNote: (user, args, context) => {
      
      if (context.currentUser && context.currentUser.id === user.id) {
        return user.privateNote;
      }

      
      return null;
    },
  },
};

Dans le résolveur, nous utilisons le context argument pour déterminer l’utilisateur actuellement authentifié. Selon la nature de notre application, nous pouvons choisir de renvoyer une valeur par défaut (comme null), renvoie une erreur ou même renvoie une valeur masquée si la demande est faite par un utilisateur non authentifié.

Bien que l’approche ci-dessus fonctionne bien, pour les applications de production réelles, il est souvent préférable de déléguer la logique d’autorisation à la couche de logique métier. Cela garantit que le résolveur reste propre, concentré sur la récupération des données et adhère au principe de séparation des préoccupations.

Un exemple de ceci pourrait peut-être impliquer l’utilisation d’un hypothétique authorize() fonction middleware qui gère l’autorisation :

const authorize = (resolverFunction) => {
  return (user, args, context, info) => {
    if (!context.currentUser || context.currentUser.id !== user.id) {
      
      return null;
    }

    return resolverFunction(user, args, context, info);
  };
};

const resolvers = {
  User: {
    privateNote: authorize((user, args, context) => {
      return user.privateNote;
    }),
  },
};

Dans le code ci-dessus, le authorize() La fonction agit comme une fonction d’ordre supérieur qui enveloppe notre fonction de résolution d’origine. Il vérifie l’autorisation de l’utilisateur avant de procéder à l’appel de la fonction de résolution réelle. Si l’utilisateur ne dispose pas des autorisations requises, il court-circuite le processus et renvoie une valeur par défaut (dans ce cas, null).

Cette approche conduit à une meilleure modularité et maintenabilité de la base de code. En externalisant une logique telle que l’autorisation dans une fonction middleware, nous pouvons mettre à jour, étendre ou remplacer de manière plus transparente les mécanismes de sécurité sans avoir besoin d’approfondir la logique métier de base ou les routines de récupération de données.

Conclure

Les résolveurs jouent un rôle crucial dans l’architecture de GraphQL, comblant le fossé entre les demandes de données du client et les sources de données réelles. Que les fonctions du résolveur soient utilisées pour récupérer des données, effectuer des calculs ou transformer des réponses, les résolveurs garantissent que les clients obtiennent exactement ce qu’ils demandent.




Source link