Изменения схемы GraphQL: депрекация без номера версии
5 мин чтения
Changelog REST API существует, потому что вызывающий может отклонить ответ, которого не понимает, или хотя бы залогировать ошибку достаточно громко, чтобы кто-то заметил. GraphQL имеет одну схему на одном endpoint, и каждый клиент, мобильное приложение на прошлогодней сборке и внутренний дашборд, развёрнутый сегодня утром, запрашивает один и тот же граф. Нет URL, который можно было бы разветвить. Депрекация поля означает пометку его как депрекированного на месте, в схеме, от которой уже все зависят, что делает дисциплину другой, чем в REST, хотя базовая проблема, сказать вызывающим, что что-то исчезнет, та же самая, что в общем разбирает депрекация API.
Как GraphQL помечает поле как депрекированное, если нет версии для повышения?
Директивой @deprecated, применённой прямо к полю:
type Product {
price: Float @deprecated(reason: "Use priceV2 for multi-currency support.")
priceV2: Money
}
Поле остаётся доступным для запроса. Оно не исчезает, не возвращает 404, не меняет поведение; оно просто несёт машиночитаемую заметку, которую большинство инструментов GraphQL, GraphiQL, Apollo Studio, линтеры схем, покажут любому, кто просматривает схему или пишет запрос против неё. Это весь механизм. Нет отдельного endpoint для депрекации, никакого заголовка, никакого сопутствующего документа, требуемого спецификацией, что одновременно и привлекательность, и ловушка: директиву легко добавить и легко проигнорировать, потому что ничто не заставляет клиента на неё смотреть.
Видит ли кто-то вообще причину депрекации?
Только те, кто использует схему напрямую, через интроспекцию или редактор, знающий о схеме, и это
меньшая аудитория, чем обычные читатели changelog API. Мобильное приложение, построенное против
запроса шесть месяцев назад, уже впекло этот запрос в свой бинарник; оно продолжит запрашивать
price и продолжит получать ответ, депрекировано оно или нет, пока кто-то не пересоберёт
приложение с новым полем и не выпустит обновление. Директива говорит разработчице, пишущей новый
код, не использовать старое поле. Она ничего не делает для клиента, уже выпущенного и работающего.
| Механизм | Кого достигает |
|---|---|
Директива @deprecated | Разработчицы, просматривающие схему или пишущие новые запросы |
| Сбои CI линтера схемы | Команда, владеющая клиентской кодовой базой, если она такой запускает |
| Запись в changelog | Кто угодно, кто её читает, включая клиентскую команду без линтера |
| Ничего (поле просто работает) | Уже построенный клиент, использующий старое поле |
Должно ли депрекированное поле всё равно получить запись в changelog?
Да, и она делает больше работы, чем одна директива, потому что changelog достигает людей, которых директива достичь не может: команду-партнёра, которая потребляет граф, не просматривая его схему, клиент, построенный против закешированной месяцы назад копии схемы, любого, кто заметил бы это только прочитав прозу. Changelog API в общем разбирает, что запись должна вызывающему; запись GraphQL должна одну вещь, которую REST редко приходится проговаривать явно, потому что вызывающие REST выводят её из номера версии: работает ли старое поле сегодня всё ещё, работает ли ещё с предупреждением, или фактически перестало возвращать данные. Одна директива не отвечает ни на что из этого для читательницы, которая никогда не открывала схему.
Когда на самом деле безопасно удалять поле из схемы?
Только когда логи запросов показывают, что его больше никто не запрашивает, что вопрос
использования, а не календаря. Поле может нести @deprecated год и всё ещё быть несущим для
одного клиента, который никогда не пересобирался; удаление его по фиксированному расписанию, как
часто делает REST-овый Sunset, ломает этого клиента без какого-либо предупреждения, на которое
он мог бы среагировать, потому что GraphQL не даёт ему ничего, на что реагировать, кроме
директивы, которую он никогда не читал. Логируйте использование на уровне поля прежде чем
обязываться на дату удаления, и относитесь к любому ненулевому счётчику запросов как к паузе, а
не отсчёту.
Несёт ли добавление поля тот же риск, что и в API REST?
Меньше, для нового поля, потому что клиент GraphQL получает только те поля, о которых явно просит.
Добавление priceV2 рядом с price не может сломать существующий запрос так, как добавление поля
в JSON-ответ REST может сломать строгий десериализатор, потому что ничто не заставляет клиента
запрашивать новое поле. Добавление значения в существующий enum стоит назвать в том же дыхании как
исключение: клиент, исчерпывающе переключающийся по каждому значению enum, к чему поощряют строго
типизированные языки, ломается в момент появления нового значения, независимо от того, запрашивал
ли его какой-либо запрос. Эта безопасность держится только для полей и членов union, в которые
клиент сам решает войти; она не держится для закрытого множества, которое код клиента перечисляет
вручную.
Что нужно записи changelog GraphQL, чего не нужно записи REST?
Форма запроса, а не только имя поля, потому что «поле price депрекировано» не хватает именно той
части, которая на самом деле нужна вызывающему: какие типы и какие запросы его касаются. Полезная
запись называет тип, поле, поле-замену и, если можете это сгенерировать, реальные запросы в
продакшене, которые всё ещё запрашивают старую форму. Этот последний кусок, привязка уведомления о
депрекации к реальному использованию, это то, что вызывающие REST получают бесплатно из логов
сервера на URL, а вызывающие GraphQL нет, потому что каждый запрос попадает в один и тот же
endpoint независимо от того, что он запрашивает.
Может ли что-то, кроме поля, нести директиву @deprecated?
Значения enum, той же директивой на определении самого значения, а не поля:
enum ShippingMethod {
STANDARD
EXPRESS
OVERNIGHT @deprecated(reason: "Use EXPRESS with priority: true instead.")
}
Спецификация определяет @deprecated ровно для двух мест, определения поля или значения enum, и
больше ни для чего по состоянию на стабильный релиз; депрекация на уровне аргумента и input-поля
существует только в более поздних черновых формулировках, не в том, что реализует большинство
серверов сегодня. Значение enum, помеченное так, остаётся допустимым значением, которое сервер
всё ещё может вернуть или принять, то же самое не-ломающее обещание, что даёт депрекированное поле,
и именно это делает безопасным выпустить пометку до того, как значение будет удалено по-настоящему.
FAQ
Поддерживает ли GraphQL что-то вроде заголовка Sunset для целого endpoint?
Нет, потому что обычно есть только один endpoint. Тайминг депрекации живёт на уровне поля, в
тексте причины директивы @deprecated и в любом changelog или руководстве по миграции, которое
команда публикует рядом, а не в заголовке ответа, который клиент мог бы прочитать программно.
Можно ли удалить депрекированное поле и позже добавить снова с другим типом?
Только под новым именем поля. Повторное введение того же имени поля с изменённым типом — это
именно тот breaking change, который цикл депрекации существует, чтобы избежать; дайте замене
собственное имя, как делает priceV2, и дайте старому полностью угаснуть, прежде чем имя
освободится для повторного использования.
Должен ли текст причины @deprecated вести на запись changelog?
Да, когда инструменты схемы это поддерживают. Поле причины принимает обычную строку, и URL внутри
этой строки — кратчайший путь от разработчицы, смотрящей на вывод интроспекции, к более полному
объяснению, которое может дать запись changelog.
Бывает ли изменение схемы GraphQL когда-либо обратно совместимым так, как REST не бывает? Аддитивные изменения полей, да, по причине выше: клиенты получают только то, что запрашивают. Новые значения enum — исключение, потому что клиент, перечисляющий закрытое множество, может сломаться на значении, которого не ожидал. Удаления и изменения типов ровно так же ломающие, как их REST-эквиваленты.
Технические утверждения в этой статье не проходили независимую проверку. Если здесь что-то не так, сообщите нам, и мы исправим.