Conceitos, Ideias, Estratégias
Conceitos, Ideias, EstratégiasEstratégias para versionamento de campos e diretivas

Estratégias para versionamento de campos e diretivas

Leia primeiro o guia Evoluindo o schema por meio do versionamento de campos, que explica o recurso de "field versioning" no Gato GraphQL.

Gato GraphQL permite que campos e diretivas recebam o argumento versionConstraint, para escolher qual versão específica (ou seja, a implementação) do campo/da diretiva utilizar:

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

O que deve acontecer quando não especificamos o argumento versionConstraint? Por exemplo, para qual versão o campo surname na query abaixo deve ser resolvido?

query GetSurname {
  account(id: 1) {
    # Qual versão deve ser usada? 1.0.0? 2.0.0?
    surname
  }
}

Temos duas preocupações aqui:

  1. Decidir qual é a versão padrão a ser usada quando nenhuma é fornecida
  2. Informar o cliente que há diversas versões disponíveis para escolha

Antes de abordar essas preocupações, precisamos descobrir o quão bem o GraphQL fornece feedback contextual ao executar uma query.

Fornecendo feedback contextual ao executar queries

Precisamos destacar uma circunstância menos do que ideal com o GraphQL atualmente: ele não oferece boas informações contextuais ao executar queries. Isso fica evidente em relação às depreciações, onde os dados de depreciação são exibidos apenas por meio de introspecção, consultando os campos isDeprecated e deprecationReason nos tipos Field e Enum:

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

A resposta será:

{
  "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
        }
      ]
    }
  }
}

Porém, ao executar uma query que envolve um campo depreciado…

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

...as informações de depreciação não aparecerão na resposta:

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

Isso significa que o desenvolvedor que executa a query deve ativamente executar queries de introspecção para descobrir se o schema foi atualizado e se algum campo foi depreciado. Isso pode acontecer… de vez em quando? Muito possivelmente nunca?

Seria uma grande melhoria para a revisão de queries desatualizadas se a API GraphQL fornecesse informações de depreciação ao executar queries que envolvem campos depreciados. Idealmente, essas informações seriam fornecidas sob uma nova entrada de nível superior deprecations, aparecendo após errors e antes de data (seguindo a sugestão da spec para o formato de resposta).

Como uma entrada de nível superior deprecations não faz parte da spec, o recurso "Proactive Feedback" do Gato GraphQL adiciona suporte a um feedback melhor na resposta à query, usando a entrada de nível superior genérica extensions, que permite estender o protocolo conforme necessário:

Informações de depreciação na resposta à query

Divulgando versões por meio de avisos

Acabamos de aprender que o servidor GraphQL pode usar a entrada de nível superior extensions para fornecer depreciações. Podemos usar essa mesma metodologia para adicionar uma entrada warnings, na qual informamos o desenvolvedor que um campo foi versionado. Não fornecemos essas informações sempre; apenas quando a query envolve um campo que foi versionado e o argumento versionConstraint está ausente.

Definindo a versão padrão de um campo

Existem várias abordagens que podemos adotar, incluindo:

  1. Tornar versionConstraint obrigatório
  2. Usar a versão antiga por padrão até uma determinada data, na qual a nova versão se torna a padrão
  3. Usar a versão mais recente por padrão e incentivar os desenvolvedores de queries a especificar explicitamente qual versão utilizar

Vamos explorar cada uma dessas estratégias e ver suas respostas ao executar esta query:

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

1. Tornar versionConstraint obrigatório

Esta é a mais óbvia: proibir o cliente de não especificar a restrição de versão tornando o argumento do campo obrigatório. Assim, sempre que não for fornecido, a query retornará um erro.

Executar a query responderá com:

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

2. Usar a versão antiga por padrão até uma determinada data na qual a nova versão se torna a padrão

Continuar usando a versão antiga até uma determinada data, quando a nova versão se tornará a padrão. Durante esse período de transição, pedir aos desenvolvedores de queries que adicionem explicitamente uma restrição de versão à versão antiga antes dessa data, por meio da nova entrada extensions.warnings na query.

Executar a query poderia responder com:

{
  "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. Usar a versão mais recente e incentivar os usuários a especificar explicitamente qual versão utilizar

Usar a versão mais recente do campo sempre que versionConstraint não estiver definido, e incentivar os desenvolvedores de queries a definir explicitamente qual versão deve ser usada, exibindo a lista de todas as versões disponíveis para aquele campo por meio de uma nova entrada extensions.warnings:

Executar a query poderia responder com:

{
  "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"
    }
  }
}

Versionamento de diretivas

Podemos usar as mesmas estratégias para versionar diretivas. Por exemplo, ao executar a query sem fornecer a restrição de versão:

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

Ela pode assumir uma versão padrão a ser usada e produzir uma mensagem de aviso para que o desenvolvedor revise a query:

Consultando uma diretiva versionada sem restrições de versão