8/7/2022

Магия декларативности и схемы. Часть 2: Выполняем GraphQL-запросы

GraphQL Engine: Чего же не хватает?

В прошлом посте мы обсудили парсинг GraphQL-запроса. Теперь разберемся, как его выполнять. Напомню, что мы делаем wiki для любителей функционального программирования. Каждая страничка wiki будет посвящена алгебраическому типу.


Его распарсил graphql-tag, так что с грамматической точки зрения все окей. Но мы все еще не знаем, способна ли наша среда выполнения обработать этот запрос.

Чтобы это понять, нам понадобится схема. Схема — это структура данных, которая описывает все, что может сделать наша система. Однако схема не описывает, как именно будет выполняться запрос.

Наличие схемы в сочетании с декларативными запросами открывает для нас очень интересные возможности. У нас еще нет схемы. Давайте это исправим.

А что, если схема уже спроектирована?

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

Проектируем схему

Перед началом чуток теории: GraphQL - схема описывается текстом, который можно распарсить (все тоже самое что мы делали в предыдущем посте). Сделать это можно при помощи graphql-tag. Текст, описывающий GraphQL-схему, также должен быть грамматически корректным.

Схему можно представить в виде графа. Узлы графа — это типы, описываемые схемой. У каждого типа может быть несколько полей другого типа. Типы могут быть простыми (числа, строки etc) или сложными. Сложные типы в свою очередь тоже состоят из полей. Все сложные типы описываются в схеме.

Ребро между двумя узлами возникает, если поле одного узла имеет тип другого. У GraphQL всегда есть один узел – тип Query, который соответствует операции query.

Подробнее про устройство GraphQL схемы смотрите в спеке

Схема это 🌳?

Нет :) В GraphQL схеме могут быть циклы, так что деревом граф схемы не является

Операции?

В GraphQL есть 3 операции: query - read-only запрос, mutation - запись + запрос, subscription - длинный запрос, который слушает стрим входящих событий и выдает ответ на каждое из них.

Давайте вернемся к нашей задаче. Для нашей wiki нам понадобится:

  • оглавление (список всех страниц),
  • глоссарий (список всех типов),
  • страничка,
  • описание типа.

Так и запишем :) Пока не обращаем внимание на поля типа AlgebraicDataType.


Оживляем engine

Схема есть, теперь нам нужно взять эту схему, добавить запрос и как-то получить данные в ответ. Чтобы это сделать, для каждого поля нашей схемы нужно определить функцию - resolver, который будет возвращать значение для каждого поля. К нашей большой радости для примитивных типов graphql engine сделал это за нас. Поэтому нам не нужно писать резолвер для каждого поля нашего сложного типа. В общем – проще показать, чем рассказать :)


Заглянув в консоль, вы увидите много интересного: описание самой схемы, ее директив и расширений. По шагам:

  • мы распарсили схему, получили schemaAST,
  • мы распарсили запрос, получили document,
  • написали resolver (очень сложный),
  • запихнули результаты в функцию execute и вуаля — получили нужный ответ.

Результат будет ожидаемым

{
  "data": {
    "allPages": []
}

Библиотека GraphQL.js позволяет в качестве резолвера указывать объект, а не функцию. Более правильно было бы вот так:

const rootResolver = {
  allPages: () => [],
}

В этой статье я хочу сконцентрироваться на тулинге и на общей концепции. Если вам интересно – вы всегда можете поэкспериментировать со снипетами выше, чтобы получить ответы повеселее. Все, что нам нужно понимать — схема резолвится таким образом, что большинство ошибок в процессе будут пойманы и отрепорчены в строго определенном месте. То, что схема зашита в сам движок, делает результат выполнения GraphQL операции по-настоящему предсказуемым. Кстати, swagger подобной предсказуемости не дает. Мы можем прикрутить OpenAPI спеку к нашим ручкам. Но это будет скорее обещание, чем гарантия.

Мы спроектировали схему. Но писать резолверы очень лень.

Нет ничего проще. Давайте их замокаем. Для этого нам понадобится еще одна библиотека graphql-tools У них есть несколько пакетов с очень полезными инструментами. Мы начнем с простых моков.

yarn add @graphql-tools/mock

И вот наш новый результат:


Получилось неплохо, но названия алгебраических типов данных не очень красивые :) Сделаем их посимпатичней, добавив кастомных моков. В graphql-tools это можно делать очень гибко


В нашем случае мы просто вернули объект из нескольких полей. Остальные поля замокались сами собой :)


Такая штука пригодится нам для тестов

Крутые фронтенды пишут тесты. Возможность быстро сгенерировать моки для каждого поля нам еще пригодится, когда мы начнем экспериментировать с тестами

Полезности из graphql-tools

Посмотрите на mock-server Воспользовавшись этой функцией, можно сделать примеры выше чуть короче. Мне показалось, что пример с явным использованием схемы будет более понятным, так что я не стала использовать mock-server

A теперь заведем нам настоящий-пренастоящий GraphQL сервер.

Раз у нас есть схема и автомоки, мы очень легко можем создать себе тестовую среду и пилить фронтенд-часть нужной нам фичи, не дожидаясь бэка. Алгоритм действий простой:

  • утаскиваем текущую схему с бэкенда,
  • расширяем нашу схему своими новыми типами (попозже посмотрим, как это сделать),
  • создаем схему с автомоками и запихиваем ее в наше приложение.

Последний пункт нужно немного раскрыть. Давайте подробнее рассмотрим варианты "запихивания".

Предположим, что вы используете apollo-client и react. Если нет – ничего страшного, идея та же самая. Чтобы ваши компонентики научились выполнять graphql-запросы, вам нужно создать специальный провайдер и обернуть им нужную часть вашего приложения. Я не буду здесь пересказывать доки. Подробности можно посмотреть в гайде от apollo

Давайте создадим игрушечный apollo-клиент и повыполняем при помощи него запросы.


Код автомоков нам уже знаком. Весь трюк в том, чтобы создать кастомный link для Apollo и закинуть в него функцию execute.

Что это за линки такие

Если вы действительно хотите разобраться, то проще всего почитать документацию

Если кратко: Operation — это AST вашего запроса + некоторое количество метаданных. Operation передается по цепочке links (по-русски это, видимо, ссылка :). У каждой link вызывается метод request, который возвращает Observable. Чтобы закинуть данные дальше по цепочке, используется метод forward

Если с этим возникают сложности, или вы не используете Apollo, можно пойти другим путем – взять нашу замоканую схему и сделать из нее сервер. Примеров как этого добиться — множество. Вот самый простой на основе graphql-js

Можно просто взять graphql-yoga или apollo-server

Запустили и замокали. Что дальше?

Теперь мы знаем, как парсится и как выполняется graphql. В следующем посте будем попробуем визуализировать нашу схему :)