Концепции, идеи, стратегии
Концепции, идеи, стратегииСтратегии версионирования полей и директив

Стратегии версионирования полей и директив

Пожалуйста, сначала прочитайте руководство Эволюция схемы с помощью версионирования полей, в котором объясняется функция «field versioning» в Gato GraphQL.

Gato GraphQL позволяет полям и директивам принимать аргумент versionConstraint, чтобы выбирать конкретную версию (то есть реализацию) поля/директивы:

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

Что должно происходить, когда мы не указываем аргумент versionConstraint? Например, к какой версии должно разрешаться поле surname в приведённом ниже запросе?

query GetSurname {
  account(id: 1) {
    # Какую версию использовать? 1.0.0? 2.0.0?
    surname
  }
}

У нас есть две задачи:

  1. Определить версию по умолчанию, используемую при отсутствии явного указания
  2. Сообщить клиенту о наличии нескольких версий на выбор

Прежде чем решать эти задачи, нужно разобраться, насколько хорошо GraphQL предоставляет контекстную обратную связь при выполнении запроса.

Предоставление контекстной обратной связи при выполнении queries

Следует отметить один недостаток GraphQL в настоящее время: он не предоставляет хорошей контекстной информации при выполнении queries. Это особенно заметно в отношении устаревших элементов (deprecations) — данные об устаревании отображаются только через интроспекцию путём запроса полей isDeprecated и deprecationReason в типах Field и Enum:

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

Ответ будет следующим:

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

Однако при выполнении запроса с устаревшим полем…

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

…информация об устаревании не появится в ответе:

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

Это означает, что разработчик, выполняющий запрос, должен самостоятельно запускать интроспекционные queries, чтобы выяснить, была ли обновлена схема и не устарело ли какое-либо поле. А это может происходить… раз в долгое время? А может, и никогда?

Большим улучшением для актуализации устаревших queries стало бы предоставление GraphQL API информации об устаревании при выполнении queries, затрагивающих устаревшие поля. В идеале эта информация могла бы передаваться в новом элементе верхнего уровня deprecations, расположенном после errors и перед data (в соответствии с предложением спецификации по формату ответа).

Поскольку элемент верхнего уровня deprecations не является частью спецификации, функция «Proactive Feedback» в Gato GraphQL добавляет поддержку улучшенной обратной связи в ответе на запрос, используя универсальный элемент верхнего уровня extensions, который позволяет расширять протокол по мере необходимости:

Информация об устаревании в ответе на запрос

Публикация версий через предупреждения

Мы только что узнали, что GraphQL-сервер может использовать элемент верхнего уровня extensions для передачи информации об устаревании. Мы можем использовать ту же методологию для добавления записи warnings, в которой будем информировать разработчика о том, что поле имеет версии. Эта информация предоставляется не всегда — только когда запрос затрагивает поле с версионированием, а аргумент versionConstraint отсутствует.

Определение версии по умолчанию для поля

Существует несколько подходов, включая:

  1. Сделать versionConstraint обязательным
  2. По умолчанию использовать старую версию до определённой даты, после которой новая версия станет версией по умолчанию
  3. По умолчанию использовать последнюю версию и призывать разработчиков queries явно указывать нужную версию

Рассмотрим каждую из этих стратегий и посмотрим на их ответы при выполнении следующего запроса:

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

1. Сделать versionConstraint обязательным

Это наиболее очевидный подход: запретить клиенту не указывать ограничение версии, сделав аргумент поля обязательным. Тогда при его отсутствии запрос будет возвращать ошибку.

Выполнение запроса вернёт:

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

2. По умолчанию использовать старую версию до определённой даты, после которой новая версия станет версией по умолчанию

Продолжать использовать старую версию вплоть до определённой даты, когда новая версия станет версией по умолчанию. В переходный период просить разработчиков queries явно добавить ограничение версии для старой версии до этой даты через новую запись extensions.warnings в запросе.

Выполнение запроса может вернуть:

{
  "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. Использовать последнюю версию и призывать пользователей явно указывать нужную версию

Использовать последнюю версию поля, когда versionConstraint не задан, и призывать разработчиков queries явно определять, какую версию следует использовать, отображая список всех доступных версий этого поля через новую запись extensions.warnings:

Выполнение запроса может вернуть:

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

Версионирование директив

Те же стратегии можно применять для версионирования директив. Например, при выполнении запроса без указания ограничения версии:

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

Сервер может предположить версию по умолчанию и сформировать предупреждающее сообщение для разработчика с просьбой пересмотреть запрос:

Запрос версионированной директивы без ограничений версии