Concepts, idées, stratégies
Concepts, idées, stratégiesStratégies pour le versionnage des champs et directives

Stratégies pour le versionnage des champs et directives

Veuillez d'abord lire le guide Faire évoluer le schéma via le versionnage de champs, qui explique la fonctionnalité de « field versioning » dans Gato GraphQL.

Gato GraphQL permet aux champs et aux directives de recevoir l'argument versionConstraint, pour choisir quelle version spécifique (c'est-à-dire l'implémentation) du champ/de la directive utiliser :

query GetPosts {
  posts(versionConstraint: "^1.0") {
    id
    title(versionConstraint: ">=2.1")
    excerpt @strUpperCase(versionConstraint: "~1.5.3")
  }
}

Que doit-il se passer lorsque nous ne spécifions pas l'argument versionConstraint ? Par exemple, vers quelle version le champ surname dans la requête ci-dessous doit-il se résoudre ?

query GetSurname {
  account(id: 1) {
    # Quelle version doit être utilisée ? 1.0.0 ? 2.0.0 ?
    surname
  }
}

Nous avons deux préoccupations ici :

  1. Décider quelle est la version par défaut à utiliser lorsqu'aucune n'est fournie
  2. Informer le client qu'il existe plusieurs versions parmi lesquelles choisir

Avant d'aborder ces préoccupations, nous devons découvrir dans quelle mesure GraphQL fournit un retour contextuel lors de l'exécution d'une requête.

Fournir un retour contextuel lors de l'exécution des requêtes

Nous devons souligner une circonstance moins qu'idéale avec GraphQL en ce moment : il n'offre pas de bonnes informations contextuelles lors de l'exécution des requêtes. Cela est évident concernant les dépréciations, où les données de dépréciation ne sont affichées que via l'introspection en interrogeant les champs isDeprecated et deprecationReason sur les types Field et Enum :

{
  __type(name: "Account") {
    name
    fields {
      name
      isDeprecated
      deprecationReason
    }
  }
}

La réponse sera :

{
  "data": {
    "__type": {
      "name": "Account",
      "fields": [
        {
          "name": "id",
          "isDeprecated": false,
          "deprecationReason": null
        },
        {
          "name": "name",
          "isDeprecated": false,
          "deprecationReason": null
        },
        {
          "name": "surname",
          "isDeprecated": true,
          "deprecationReason": "Use `personSurname`"
        },
        {
          "name": "personSurname",
          "isDeprecated": false,
          "deprecationReason": null
        }
      ]
    }
  }
}

Cependant, lors de l'exécution d'une requête impliquant un champ déprécié…

query GetSurname {
  account(id: 1) {
    surname
  }
}

...les informations de dépréciation n'apparaîtront pas dans la réponse :

{
  "data": {
    "account": {
      "surname": "Owens"
    }
  }
}

Cela signifie que le développeur exécutant la requête doit activement exécuter des requêtes d'introspection pour savoir si le schéma a été mis à jour et si un champ a été déprécié. Cela peut arriver… une fois de temps en temps ? Peut-être jamais ?

Ce serait une grande amélioration pour la révision des requêtes obsolètes si l'API GraphQL fournissait des informations de dépréciation lors de l'exécution de requêtes impliquant des champs dépréciés. Ces informations pourraient idéalement être données sous une nouvelle entrée de niveau supérieur deprecations, apparaissant après errors et avant data (suivant la suggestion de la spec pour le format de réponse).

Comme une entrée de niveau supérieur deprecations ne fait pas partie de la spec, la fonctionnalité « Proactive Feedback » de Gato GraphQL ajoute la prise en charge d'un meilleur retour dans la réponse à la requête en utilisant l'entrée de niveau supérieur générique extensions, qui permet d'étendre le protocole selon les besoins :

Informations de dépréciation dans la réponse à la requête

Publiciser les versions via des avertissements

Nous venons d'apprendre que le serveur GraphQL peut utiliser l'entrée de niveau supérieur extensions pour fournir des dépréciations. Nous pouvons utiliser cette même méthodologie pour ajouter une entrée warnings, dans laquelle nous informons le développeur qu'un champ a été versionné. Nous ne fournissons pas ces informations en permanence ; uniquement lorsque la requête implique un champ qui a été versionné et que l'argument versionConstraint est absent.

Définir la version par défaut d'un champ

Il existe plusieurs approches que nous pouvons employer, notamment :

  1. Rendre versionConstraint obligatoire
  2. Utiliser l'ancienne version par défaut jusqu'à une certaine date, à laquelle la nouvelle version devient la version par défaut
  3. Utiliser la dernière version par défaut et encourager les développeurs de requêtes à indiquer explicitement quelle version utiliser

Explorons chacune de ces stratégies et voyons leurs réponses lors de l'exécution de cette requête :

query GetSurname {
  account(id: 1) {
    surname
  }
}

1. Rendre versionConstraint obligatoire

C'est la plus évidente : interdire au client de ne pas spécifier la contrainte de version en rendant l'argument de champ obligatoire. Ainsi, chaque fois qu'il n'est pas fourni, la requête retournera une erreur.

L'exécution de la requête répondra avec :

{
  "errors": [
    {
      "message": "Argument 'versionConstraint' in field 'surname' cannot be empty"
    }
  ],
  "data": {
    "account": {
      "surname": null
    }
  }
}

2. Utiliser l'ancienne version par défaut jusqu'à une certaine date à laquelle la nouvelle version devient la version par défaut

Continuer à utiliser l'ancienne version jusqu'à une certaine date, où la nouvelle version deviendra la version par défaut. Pendant cette période de transition, demander aux développeurs de requêtes d'ajouter explicitement une contrainte de version à l'ancienne version avant cette date via la nouvelle entrée extensions.warnings dans la requête.

L'exécution de la requête pourrait répondre avec :

{
  "extensions": {
    "warnings": [
      {
        "message": "Field 'surname' has a new version: '2.0.0'. This version will become the default one on January 1st. We advise you to use this new version already and test that it works fine; if you find any problem, please report the issue in https://github.com/mycompany/myproject/issues. To do the switch, please add the 'versionConstraint' field argument to your query (using Composer's semver constraint rules; see https://getcomposer.org/doc/articles/versions.md#writing-version-constraints): surname(versionConstraint:\"^2.0\"). If you are unable to switch to the new version, please make sure to explicitly point to the current version '1.0.0' before January 1st: surname(versionConstraint:\"^1.0\"). In case of doubt, please contact us at name@company.com.",
    ]
  },
  "data": {
    "account": {
      "surname": "Owens"
    }
  }
}

3. Utiliser la dernière version et encourager les utilisateurs à indiquer explicitement quelle version utiliser

Utiliser la dernière version du champ lorsque versionConstraint n'est pas défini, et encourager les développeurs de requêtes à définir explicitement quelle version doit être utilisée, en affichant la liste de toutes les versions disponibles pour ce champ via une nouvelle entrée extensions.warnings :

L'exécution de la requête pourrait répondre avec :

{
  "extensions": {
    "warnings": [
      {
        "message": "Field 'surname' has more than 1 version. Please add the 'versionConstraint' field argument to your query to indicate which version to use (using Composer's semver constraint rules; see https://getcomposer.org/doc/articles/versions.md#writing-version-constraints). To use the latest version, use: surname(versionConstraint:\"^2.0\"). Available versions: '2.0.0', '1.0.0'.",
    ]
  },
  "data": {
    "account": {
      "surname": "Owens"
    }
  }
}

Versionnage des directives

Nous pouvons utiliser les mêmes stratégies pour versionner les directives. Par exemple, lors de l'exécution de la requête sans fournir la contrainte de version :

query {
  post(by: { id: 1 }) {
    title @strTitleCase
  }
}

Elle pourrait supposer une version par défaut à utiliser et produire un message d'avertissement pour que le développeur révise la requête :

Interroger une directive versionnée sans contraintes de version