🤔 Должен ли GraphQL быть разным для разных пользователей?
GraphQL — это интерфейс для получения данных из некоего источника, а спецификация GraphQL определяет требования к этому интерфейсу. Пока эти требования соблюдаются, GraphQL не заботит, каким образом они выполняются. Сервер GraphQL может быть реализован на JavaScript с использованием промисов, на основе конкурентной архитектуры на Golang, с маппингом на файл Excel или как угодно ещё — и всё это могут быть валидными реализациями спецификации GraphQL.

То, как реализован движок сервера, не важно для успешного выполнения GraphQL-запроса, поскольку взаимодействие между клиентом и сервером всегда одинаково: отправляется GraphQL-запрос с использованием определённого синтаксиса и получается соответствующий ответ в формате JSON.
Когда я говорю, что реализация не важна, я имею в виду это с точки зрения пользователя API, который просто намерен получить данные от сервера. Как были получены возвращённые данные — ему неинтересно.
Но ситуация меняется для серверного разработчика, работающего над API, для которого детали реализации действительно очень важны. Если я пишу свой GraphQL API на PHP, то я буду делать всё возможное, чтобы мой API решался как можно эффективнее и имел архитектурный дизайн как можно более элегантный — используя возможности, предоставляемые PHP.

Таким образом, возникает возможный конфликт интересов между необходимостью защиты API и ожидаемыми возможностями разработчиков, работающих над API, которые не хотят лишаться функций, поддерживаемых базовым языком (например, возможности выполнять рекурсивный код).
Этот конфликт стал очевидным в issue #929: Allow recursive references in fragments, которое утверждает, что GraphQL не должен запрещать рекурсии в fragments.
На одной из прошлых встреч рабочей группы GraphQL Роман, разработчик, поднявший этот вопрос, выразил, почему он не согласен с ограничением, наложенным спецификацией:
Я серверный разработчик, и мне кажется, что спецификация слишком много говорит о серверном выполнении, тогда как она должна сосредоточиться на том, что клиент хочет получить, — а не на том, как это делается
Правило, запрещающее рекурсии в fragments, обосновывалось необходимостью обеспечить безопасность публичного API. В конце концов, GraphQL был создан Facebook для доставки данных в их публичное приложение, и пользователи не должны иметь возможности воспользоваться уязвимостью в дизайне API, которая могла бы положить сервис.
Создатель GraphQL Ли Байрон выразил три основные опасения:
бесконечная рекурсия; ограничения были бы не только в спецификации — как и когда это должно останавливаться
валидация данных; возврат одного и того же значения несколько раз — как это представляется в данных. В идеале хочется обнаружить цикличность и сразу остановиться, но некоторые серверы не могут этого обнаружить и могут зациклиться много раз, прежде чем обнаружат, что что-то пошло не так, и остановятся
какова цена отсутствия этого; оправдывает ли это эти проблемы? Нет; всегда можно указать количество уровней вложенности в запросе — это фактически и есть развёрнутая версия того, что мы бы сделали, если бы решали это в GraphQL
Исходя из своих позиций, и Роман, и Ли правы. Ли Байрон обеспокоен безопасностью публичного GraphQL API. Запрет рекурсивных fragments оправдан тем, чтобы никакой злоумышленник не мог положить систему, выполняя бесконечный циклический цикл в запросе, и даже исключить вероятность «self-DDoS» со стороны команды, что могло бы произойти, если бы они непреднамеренно опубликовали запрос, который останавливает систему.
Роман, однако, обеспокоен ограничениями своих собственных возможностей при создании GraphQL API. Поскольку Роман может быть единственным потребителем своего API (то есть приватного API, не открытого для пользователей), или потому что его сервер может иметь возможность обнаруживать и останавливать рекурсивные циклы, он считает, что ограничение GraphQL вредоносно и неоправданно.
В основе дискуссии проблема не в том, следует ли разрешать рекурсивные fragments или нет, а в чём-то более фундаментальном: кто является целевой аудиторией GraphQL? Если не одна группа, может ли единая спецификация API удовлетворить требования всех различных заинтересованных сторон? И если конфликт нельзя предотвратить, можно ли его хотя бы как-то смягчить?
Давайте исследуем эти вопросы.
Кто является целевой аудиторией GraphQL?
GraphQL используется различными типами заинтересованных сторон, среди которых можно выделить:
1. Пользователи API: те, кто потребляет данные из какого-либо GraphQL endpoint, по любой причине. Например, все мы можем быть пользователями публичного GraphQL API GitHub для получения данных о наших репозиториях.
2. Фронтенд-разработчики: те, кто создаёт клиентские приложения, работающие на основе какого-либо GraphQL endpoint. Например, разработчики, создающие сайты с Gatsby, полагаются на GraphQL для получения контента сайта.
3. Бэкенд-разработчики: те, кто создаёт resolvers для GraphQL API.
Кроме того, необходимо отметить, что GraphQL API может быть публичным или приватным:
Публичный API: Поскольку любой имеет доступ к GraphQL endpoint, необходимо позаботиться о мерах безопасности для предотвращения атак со стороны злоумышленников.
Приватный API: Поскольку доступ к API имеют только предполагаемые участники, нет inherent рисков безопасности, а самодидосинг можно легко избежать с помощью хороших практик программирования.
Удовлетворяет ли единая спецификация API требованиям всех заинтересованных сторон?
Вопрос, поднятый Романом, можно интерпретировать так: «Если мой GraphQL API приватный, и я точно знаю, что делаю (имея 100% уверенность в том, что мой код будет работать как ожидается и не возникнет зависших выполнений), то почему я не могу использовать рекурсии в fragments?»

Пример такой ситуации возникает всякий раз, когда мы используем фреймворк на основе GraphQL для создания статических сайтов (например, Gatsby, Next.js или RedwoodJS), потому что GraphQL API часто будет приватным, и мы не можем случайно провести DDoS-атаку на наше собственное приложение и понести неблагоприятные последствия (в худшем случае оно упадёт при сборке статического сайта в среде разработки или staging).
Разработчики, использующие описанную конфигурацию, вполне могут задаться вопросом, почему спецификация GraphQL запрещает им использовать полезные функции, которые не имеют абсолютно никаких негативных последствий для их конфигурации.
В заключение: запрещая рекурсивные fragments, спецификация GraphQL навязывает меру безопасности, которая применима к определённой части всех потенциальных вариантов использования GraphQL — но не ко всем, — чтобы быть на безопасной стороне.
Могла бы спецификация GraphQL лучше удовлетворить всех заинтересованных сторон?
Если у разных заинтересованных сторон разные требования, как спецификация GraphQL может удовлетворить всех? (Идея состоит в том, чтобы избежать форка спецификации и создания кастомизированных версий для конкретных целей.)
Давайте рассмотрим пару идей, где первая потребовала бы прохождения через процесс вклада в спецификацию, а вторая — нет.
Feature-toggle на уровне спецификации GraphQL
Один возможный путь — это когда спецификация «предлагает», но не «навязывает» правила. В этом случае правило, запрещающее рекурсии в fragments, могло бы настоятельно рекомендоваться, но функция всё равно принималась бы.
Однако это решение изменило бы состояние рекурсивных fragments по умолчанию с «обязательного» на «необязательное», что привело бы к двум негативным последствиям:
- API по умолчанию был бы небезопасным (сценарий, которого хочет избежать Ли Байрон)
- Это привело бы к breaking change, поскольку ранее запрещённый запрос стал бы разрешённым
Тогда лучше было бы перевернуть опцию с ног на голову: рекурсии в fragments по-прежнему запрещены по умолчанию, но появляется возможность включить feature-flag, отключающий это поведение. Поскольку функция должна быть явно отключена, это сделают только администраторы, которые понимают, что делают.
Поскольку эта функция наиболее ценна в определённых конфигурациях, серверы и фреймворки GraphQL могут решать, предлагать ли/как/когда конфигурацию. Например, Gatsby мог бы наглядно отображать опцию через некоторый интерфейс при создании статических сайтов и скрывать её в иных случаях.
Общая идея заключается в том, чтобы спецификация GraphQL поддерживала «включённые, но необязательные функции», которые можно включать/отключать через конфигурацию, а их состояние по умолчанию — то, которое они уже имеют в спецификации.
Запрет рекурсивных fragments был бы одной из них, и могут быть и другие подобные функции, например тип Map, который не был принят в спецификацию Ли Байроном по следующей причине:
Существуют значительные компромиссы между типом Map и списком пар ключ/значение. Одна проблема — пагинация по коллекции. Списки значений могут иметь чёткие правила пагинации, тогда как Maps, которые часто имеют неупорядоченные пары ключ-значение, гораздо сложнее пагинировать.
Другая проблема — использование. Чаще всего Map используется внутри API, где индексируется одно поле значения, что, по моему мнению, является анти-паттерном API, поскольку индексирование — это вопрос хранения и кэширования на стороне клиента, но не транспорта. Этот анти-паттерн меня беспокоит. Хотя есть хорошие применения Maps в API, я опасаюсь, что общее использование будет именно для этих анти-паттернов, поэтому предлагаю действовать осторожно.
Ли Байрон выразил опасение, что функция будет использоваться как анти-паттерн. Однако он также признал, что для неё есть хорошие применения. Тогда, поскольку issue получило значительную поддержку сообщества (более 150 👍), разработчикам можно было бы дать возможность явно включить добавление типа Map в свои схемы и иметь дело с последствиями.
Feature-toggle на уровне серверов GraphQL
Если предложение выше не получит поддержки, поскольку оно слишком рискованно для спецификации GraphQL, альтернативой является реализация этого на уровне сервера GraphQL. Тогда серверы GraphQL могли бы предоставлять кастомную функцию, отключающую рекурсии в fragments.
Обобщая идею, серверы GraphQL могли бы предлагать отключать определённые функции спецификации и включать другие, отсутствующие в спецификации. Чтобы такое поведение не вызывало сюрпризов, серверы должны гарантировать, что состояние по умолчанию соответствует требованиям спецификации, а администратор API должен быть полностью осведомлён о последствиях переключения функции. (Это стратегия, которой следует Gato GraphQL для своих «innovative features".)
Подводя итог
По мере того как GraphQL становился всё более популярным, новые фреймворки, поддерживающие новые возможности, включили его в свой стек, и появились новые заинтересованные стороны (и новые их типы). Таким образом, спецификация, изначально созданная Facebook, чтобы определить, как его приложения будут получать данные с его серверов, должна всё активнее справляться со всё большим числом сценариев использования.
Неизбежно возникновение конфликтов, когда группа заинтересованных сторон нуждается в функции, которая контрпродуктивна или даже вредна для других заинтересованных сторон, как в случае с рекурсивными fragments. Что можно сделать для улучшения ситуации и предотвращения разочарования неудовлетворённых заинтересованных сторон в GraphQL?
Я утверждал, что спецификация могла бы предложить возможность «отключить» функцию, позволяя администраторам, знающим, что они делают, снимать некоторые ограничения для удовлетворения своих требований. Я сам не согласен с этим решением, но всё же выношу его на обсуждение, потому что этот разговор необходим. Поскольку эта идея спорна, лучшей альтернативой является то, чтобы серверы GraphQL предоставляли такое поведение через кастомные функции, которые должны быть явно включены.