Резольверы на основе транков: Как построить мощный и гибкий шлюз GraphQL

В основе WunderGraph лежит очень мощный и гибкий API-шлюз GraphQL. Сегодня мы подробно рассмотрим паттерн, на котором он основан: Резольверы GraphQL на основе Thunk.

Изучение паттерна Thunk-based Resolver поможет вам понять, как можно создать свой собственный шлюз GraphQL. Если вы знакомы с Go, вам также не придется начинать с нуля, так как вы можете использовать наш Open Source Framework. Если вы хотите реализовать эту идею на другом языке, вы все равно сможете узнать много нового о паттернах, так как в этой заметке не будет уделено внимание реализации на Go.

Для начала давайте определим цели GraphQL API Gateway:

  • Он должен быть простым в настройке и расширении
  • Он должен иметь возможность посредничать между различными сервисами и протоколами
  • Должна быть возможность повторного развертывания без этапа генерации/компиляции кода
  • Он должен поддерживать сшивание схем
  • Он должен поддерживать федерацию Apollo
  • Должна поддерживать подписки
  • Должен поддерживать REST API

Создание GraphQL API-шлюза, способного поддерживать эти требования, является настоящей проблемой. Чтобы лучше понять проблему и то, как резолверы на основе Thunk помогают ее решить, давайте сначала рассмотрим различия между «обычным» резолвером и резолвером на основе Thunk.

Обычный GraphQL-резольвер

const userResolver = async (id) => {
    const user = await db.userByID(id);
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

Резольвер GraphQL на основе Thunk

С другой стороны, резолвер на основе thunk не возвращает данные немедленно. Вместо этого он возвращает функцию, которую вы можете выполнить позже.

Вот пример:

const userResolver = async () => {
    return async (root, args, context, info) => {
        const user = await db.userByID(args.id);
        return user;
    }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Это резолвер, основанный на технологии thunk. Он не возвращает данные немедленно. Вместо этого он возвращает функцию, которая может быть использована позже для загрузки пользователя.

Однако это резкое упрощение того, как резолверы на основе thunk выглядят на практике.

Чтобы добавить больше контекста, мы рассмотрим, как серверы GraphQL обычно разрешают запросы GraphQL.

Обычный сервер GraphQL

На обычном сервере GraphQL поток выполнения GraphQL-запроса выглядит следующим образом:

  • Разбор GraphQL-запроса
  • Нормализация
  • Валидация
  • Выполнение
  • Во время фазы Execution сервер GraphQL Server будет выполнять резольверы и собирать результат.

Рассмотрим простую схему GraphQL…

type Query {
    user(id: ID!): User
}
type User {
    id: ID!
    name: String!
    posts: [Post]
}
type Post {
    id: ID!
    title: String!
    body: String!
Вход в полноэкранный режим Выход из полноэкранного режима

…с этими двумя GraphQL-резольверами…

const userResolver = async (userID: string) => {
    const user = await db.userByID(userID);
    return user;
}
const postResolver = async (userID: string) => {
    const post = await db.postsByUserID(userID);
    return post;
}
Войти в полноэкранный режим Выход из полноэкранного режима

…и следующим запросом GraphQL:

query {
    user(id: "1") {
        id
        name
        posts {
            id
            title
            body
        }
    }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Выполнение этого запроса будет выглядеть следующим образом:

  • войти в поле user и выполнить userResolver с аргументом «1»
  • разрешить поля id и name объекта user
  • перейдите в поле posts и выполните postResolver с аргументом «1»
  • разрешить поля id, title и body объекта post.

После выполнения ответ может выглядеть следующим образом:

{
    "data": {
        "user": {
            "id": "1",
            "name": "John Doe",
            "posts": [
                {
                    "id": "1",
                    "title": "Hello World",
                    "body": "This is a post"
                }
            ]
        }
    }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Этого контекста должно быть достаточно для понимания проблемы создания API-шлюза. Предположим, что вы хотите построить API-шлюз GraphQL, который поддерживает приведенную выше схему GraphQL. Вы должны иметь возможность направить ваш GraphQL API-шлюз на ваш GraphQL Server, и он должен быть способен выполнить GraphQL-запрос.

Помните, что шлюз должен работать без генерации кода или каких-либо шагов компиляции. Это означает, что мы не можем создавать резольверы, как в предыдущем примере. Это потому, что шлюз не может заранее знать, какую схему вы собираетесь использовать. Он должен быть достаточно гибким, чтобы поддерживать любую схему, которую вы захотите использовать.

Как же решить эту проблему? Именно здесь в игру вступают резолверы на основе Thunk.

Сервер GraphQL на базе Thunk

Серверы GraphQL на базе Thunk отличаются от обычных серверов GraphQL тем, что они разделяют выполнение запроса GraphQL на две фазы — планирование и выполнение.

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

Чтобы максимально упростить понимание, давайте проделаем обратный путь от фазы выполнения к фазе планирования.

Фаза выполнения сервера GraphQL на базе Thunk

Фаза выполнения зависит от плана выполнения. Поскольку эти планы могут быть довольно сложными, мы используем упрощенную версию в целях иллюстрации.

На самом деле мы можем использовать ответ, приведенный выше, и превратить его в план выполнения.

{
    "data": {
        "__fetch": {
            "url": "http://localhost:8080/graphql",
            "body": "{"query":"query{user(id:\"1\"){id,name,posts{id,title,body}}}"}",
            "method": "POST"
        },
        "user": {
            "__type": "object",
            "__path": ["user"],
            "fields": [
              {
                "id": {
                  "__type": "string",
                  "__path": ["id"]
                },
                "name": {
                  "__type": "string",
                  "__path": ["name"]
                },
                "posts": {
                  "__type": "array",
                  "__path": ["posts"],
                  "__item": {
                    "__type": "object",
                    "__fields": [
                      {
                        "id": {
                          "__type": "string",
                          "__path": ["id"]
                        },
                        "title": {
                          "__type": "string",
                          "__path": ["title"]
                        },
                        "body": {
                          "__type": "string",
                          "__path": ["body"]
                        }
                      }
                    ]
                  }
                }
              }
            ]
        }
    }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Говоря простым языком, механизм выполнения создает ответ JSON. Он входит в поле данных и выполняет операцию выборки. После выполнения операции выборки он проходит по остальным полям и извлекает данные из ответа. Таким образом, формируется окончательный ответ.

Следует отметить несколько моментов:

  • Идентификатор пользователя жестко закодирован на «1». В реальности это будет динамическое значение. Поэтому в реальном мире вам придется превратить его в переменную и ввести в запрос.
  • Еще одно отличие реального мира от приведенного выше примера заключается в том, что вы обычно выполняете несколько вложенных операций выборки.
  • Также может потребоваться распараллеливание или пакетная обработка выборки.
  • Apollo Federation и Schema Stitching добавляют дополнительную сложность
  • Общение с базами данных означает, что это не только фетчи, но и более сложные запросы
  • Подписки требуют совершенно иного подхода, поскольку они открывают поток данных

Итак, теперь у нас есть план выполнения. Как мы уже упоминали ранее, мы работаем в обратном направлении. Теперь давайте перейдем к фазе планирования и посмотрим, как такой план выполнения может быть сгенерирован из GraphQL-запроса.

Фаза планирования GraphQL-сервера на основе Thunk

Первым компонентом для планировщика выполнения является конфигурация. Конфигурация содержит всю информацию о схеме GraphQL и о том, как разрешать запросы.

Вот упрощенная версия конфигурации:

{
  "dataSources": [
    {
      "type": "graphql",
      "url": "http://localhost:8080/graphql",
      "rootFields" : [
        {
          "type": "Query",
          "field": "user",
          "args": {
            "id": {
              "type": "string",
              "path": ["id"]
            }
          }
        }
      ],
      "childFields": [
          {
            "type": "User",
            "field": "id"
          },
          {
            "type": "User",
            "field": "name"
          },
          {
            "type": "User",
            "field": "posts"
          },
          {
            "type": "Post",
            "field": "id"
          },
          {
            "type": "Post",
            "field": "title"
          },
          {
            "type": "Post",
            "field": "body"
          }
        ]
    }
  ]
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Говоря простым языком, конфигурация содержит следующее: Если запрос начинается с корневого поля «user», он будет разрешен источником данных DataSource. Этот источник данных также отвечает за дочерние поля «id», «name» и «posts» для типа User, а также поля «id», «title» и «body» для типа Post.

Поскольку мы знаем, что это восходящий поток типа «graphql», мы знаем, что нам нужно создать GraphQL-запрос для получения данных.

Итак, у нас есть конфигурация. Давайте посмотрим на «резольверы».

Как преобразовать GraphQL-запрос в план выполнения, который мы видели выше?

Мы делаем это, посещая каждый узел GraphQL-запроса. Вот упрощенный пример:

const document = parse(graphQLOperation);
visit(document, {
    enterSelectionSet(node: SelectionSetNode) {
        // open json object
    },
    leaveSelectionSet(node: SelectionSetNode) {
        // close json object
    },
    enterField(node: FieldNode) {
        // add field to json object
        // if it's a scalar, add the type
        // if it's an object or array, created nested structure
        //
        // if it's a root field, create a fetch and add it to the field
    },
    leaveField(node: FieldNode) {
        // close objects and arrays
    },
});
Войти в полноэкранный режим Выход из полноэкранного режима

Как вы можете видеть, мы не разрешаем никаких данных. Вместо этого мы «проходим» по всем узлам разобранного запроса GraphQL Query. Мы используем шаблон посетителя, поскольку он позволяет нам посещать все поля и наборы выбора предсказуемым образом. Обходчик сначала проходит по всем узлам в глубину и вызывает «обратные вызовы» на посетителе, когда он входит или выходит из узла.

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

Итак, этот процесс делает две вещи. Во-первых, он создает структуру ответа, который мы вернем клиенту. Во-вторых, он настраивает фетчи, которые мы будем выполнять для получения данных.

Поскольку эти фетчи — просто функции, которые возвращают данные и могут быть выполнены позже, мы также можем называть их просто thunks. Именно так этот подход и получил свое название.

Трудности построения GraphQL Framework на основе Thunk

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

Вы не можете просто «прикрепить» источник данных к кортежу поля типа.

Это был один из первых дорогих выводов. Во время создания первой итерации «движка» я подумал, что было бы неплохо прикреплять источники данных непосредственно к комбинации типа и поля. Кортеж из типа и поля поначалу кажется уникальным.

Но проблема в том, что GraphQL допускает рекурсивные операции. Например, если у вас есть тип User, вы можете иметь поле «posts», которое возвращает массив объектов Post. С другой стороны, объект Post может иметь поле «user», которое возвращает объект User.

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

Как мы решили эту проблему самостоятельно? Мы «проходим» по Операции дважды.

Во время первого прохода мы определяем, сколько источников данных нам нужно, и инстанцируем их. Вторая прогулка используется для создания плана выполнения. Таким образом, «планировщику выполнения» не нужно беспокоиться о границах источников данных.

Именно поэтому мы решили использовать «корневые узлы» и «дочерние узлы» в конфигурации планировщика. Вместе они определяют границы каждого источника данных. Если вы входите в «корневой узел» во время первого прохода, вы инстанцируете источник данных. Затем, пока вы остаетесь в пределах «дочерних узлов», тот же самый источник данных остается ответственным. Как только вы попадаете в узел, который не является дочерним, ответственность за это поле передается следующему datasource.

Этой проблеме мы посвятим отдельный параграф в конце статьи.

Вы должны различать нисходящую и восходящую схемы

Еще одна проблема, с которой мы столкнулись, связана с масштабированием нашего подхода. Начать с одной схемы просто, но что если объединить множество схем?

Объединение нескольких схем GraphQL — это очень мощный инструмент, поскольку он позволяет запрашивать и объединять данные из нескольких источников данных с помощью одной операции GraphQL.

В то же время это сопряжено с некоторыми трудностями. Одной из основных проблем является «столкновение имен», которое происходит постоянно. Если не думать об этом слишком много, то в большинстве, если не во всех схемах, есть тип с именем «Пользователь». Очевидно, что пользователь Stripe — это не то же самое, что пользователь Salesforce.

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

Наш первый подход заключался в создании API, который позволил бы пользователю переименовывать типы. Это работает, но это не очень удобно для пользователя. Вы всегда будете сталкиваться с проблемами и вынуждены вручную переименовывать типы, прежде чем сможете объединить две схемы.

Другая проблема заключается в том, что у вас также возникнут коллизии в наименовании полей корневых типов Query, Mutation и Subscription. Сначала мы создали другой API, который позволял пользователям также переименовывать поля. Однако этот подход так же удобен для пользователя, как и предыдущий, это утомительный процесс, и он не очень масштабируем.

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

Мы вернулись к чертежной доске и занялись поиском решения, которое позволило бы нам объединить несколько схем автоматически, без какого-либо ручного вмешательства.

Решение, к которому мы пришли, — это «API namespacing». Позволив пользователю выбрать пространство имен для каждого API, мы можем автоматически снабжать все типы и поля префиксом этого пространства имен. Таким образом, все коллизии именования решаются автоматически.

При этом «пространство имен API» не является бесплатным.

Представьте себе следующую схему GraphQL:

type User {
  id: ID!
  name: String!
}
type Anonymous {
  name: String!
}
union Viewer = User | Anonymous
type Query {
    viewer: Viewer
}
Вход в полноэкранный режим Выход из полноэкранного режима

Если мы создадим пространство имен этой схемы с префиксом «identity», то в итоге получим эту схему:

type identity_User {
  id: ID!
  name: String!
}
type identity_Anonymous {
  name: String!
}
union identity_Viewer = identity_User | identity_Anonymous
type Query {
    identity_viewer: identity_User | identity_Anonymous
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь давайте представим «нисходящий» запрос, запрос, поступающий от клиента к «шлюзу».

query {
  identity_viewer {
    ... on identity_User {
      id
      name
    }
    ... on identity_Anonymous {
      name
    }
  }
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Мы не можем просто отправить этот запрос в API «идентификации». Мы должны «освободить пространство имен».

Вот как должен выглядеть восходящий запрос:

query {
  viewer {
    ... on User {
      id
      name
    }
    ... on Anonymous {
      name
    }
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Как видите, распределение имен в GraphQL не так просто. Но все может оказаться еще сложнее.

Как насчет определений переменных в нисходящем запросе, которые используются только в некоторых восходящих запросах? Как насчет директив с выравниванием имен, которые существуют только в некоторых восходящих схемах? API Namespacing — отличное решение проблемы, но реализовать его не так просто, как может показаться.

Преимущества подхода к GraphQL-резольверам на основе Thunk

Хватит о минусах и проблемах, давайте поговорим и о преимуществах!

Разрешение на основе Thunk, если оно выполнено правильно, означает, что вы можете перенести множество сложной вычислительной логики из ваших GraphQL-резольверов. Статический анализ позволяет убрать большую часть кода с горячего пути, делая выполнение ваших резолверов GraphQL более производительным.

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

Все, что мы делаем во время выполнения, это выполняем предварительно составленный план выполнения, который представляет собой просто структуру данных, определяющую форму ответа и время выполнения выборки. Однако нам этого было недостаточно. Мы сделали еще один шаг вперед и полностью отказались от GraphQL. Во время разработки вы по-прежнему пишете запросы и мутации GraphQL, но во время выполнения мы заменяем GraphQL на JSON RPC.

Мы генерируем безопасный клиент, который точно знает, какие операции GraphQL вы определили. Опыт разработчика остается прежним, но он гораздо более производительный и безопасный. Дело не только в том, что размер сгенерированного клиента составляет всего несколько килобайт, этот подход также устраняет большинство из 13 наиболее распространенных уязвимостей GraphQL.

На этом этапе должно быть понятно, почему мы ввели понятие виртуального Graph. Если у вас есть GraphQL API, состоящий из множества других API, но на самом деле вы используете только сгенерированный REST/JSON RPC, имеет смысл назвать его «виртуальным» Graph, поскольку составленная GraphQL Schema существует только виртуально.

Идея виртуального графа настолько мощная, что позволяет объединить разнородный набор API с помощью единой унифицированной схемы GraphQL и взаимодействовать с ними с помощью простых операций GraphQL. Объединять и сшивать данные из нескольких API, как если бы они были одним целым. Универсальный интерфейс ко всем вашим сервисам, API и базам данных.

Еще одним преимуществом подхода на основе thunk является то, что вы можете быть гораздо более эффективными, когда дело доходит до пакетной обработки. В традиционных резолверах GraphQL для пакетной обработки запросов приходится использовать шаблоны типа «DataLoader». DataLoader — это техника, которая ожидает в течение небольшого периода времени для пакетной обработки нескольких запросов.

С резолверами на основе thunk вам не нужно ждать, пока заполнится «окно пакетной обработки». Вы можете использовать статический анализ для вставки пакетной обработки в план выполнения. Это также означает, что вы можете выполнять пакетную обработку нескольких запросов с помощью одного «потока».

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

При использовании паттерна DataLoader 10 потоков будут блокироваться до тех пор, пока пакет не будет разрешен. С другой стороны, при статическом анализе один поток может разрешить все 10 полей синхронно.

Когда мы «заходим» в первое поле всех пакетных братьев и сестер, благодаря статическому анализу (во время компиляции) уже известно, что необходим пакетный запрос. Это означает, что мы можем немедленно создать пакетный запрос и выполнить его. После его выполнения мы можем продолжить синхронное разрешение полей всех полей-сестер.

Использование подхода на основе Thunk в данном случае означает, что мы можем использовать CPU гораздо лучше, поскольку мы не создаем ситуаций, когда потоки CPU должны ждать друг друга.

Резольверы на основе транков не заменяют «классический» подход к резольверам

Учитывая все преимущества подхода на основе thunk, вы можете подумать, что вам следует заменить все существующие резольверы GraphQL на подход на основе thunk.

Хотя это возможно, резолверы на основе thunk на самом деле не предназначены для замены «классического» подхода Resolver. Эта техника имеет смысл для создания API-шлюзов и прокси.

Если вы создаете промежуточное ПО, то имеет смысл использовать этот подход, поскольку у вас уже есть реализация API.

Однако построить GraphQL-сервер, используя только резолверы на основе thunk, очень сложно, и я бы не рекомендовал этого делать.

Как мы упростили настройку резольверов на основе thunk

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

Нашим решением проблемы стало создание TypeScript SDK, который позволяет «генерировать» конфигурацию для ваших резолверов на основе thunk всего в нескольких строках кода.

Давайте рассмотрим пример объединения трех API с помощью SDK:

const db = introspect.postgresql({
    apiNamespace: "db",
    databaseURL: "postgresql://admin:admin@localhost:54322/example?schema=public"
});

const countries = introspect.graphql({
    apiNamespace: "countries",
    url: "https://countries.trevorblades.com/",
});

const stripe = introspect.openApi({
    apiNamespace: 'stripe',
    statusCodeUnions: true,
    source: {
        kind: 'file',
        filePath: './stripe.yaml',
    },
    headers: builder => {
        return builder
            .addStaticHeader('Authorization', `Bearer ${process.env["STRIPE_SECRET_KEY"]}`)
    },
});
const myApplication = new Application({
    name: "app",
    apis: [
        db,
        countries,
        stripe,
    ],
});
Вход в полноэкранный режим Выход из полноэкранного режима

Здесь мы проверяем базу данных PostgreSQL, API Country GraphQL и спецификацию Stripe OpenAPI. Как вы можете видеть, API Namespaces — это простой параметр конфигурации, вся остальная конфигурация генерируется.

Представьте, что вам пришлось бы вручную настраивать и конфигурировать комбинацию из n API. Без такого фреймворка сложность конфигурации, вероятно, составила бы: O(n^2). Чем больше API вы добавляете, тем сложнее будет конфигурация. Если изменится хоть один API, вам придется заново пройти все шаги, чтобы заново настроить и протестировать все.

Сравните это с автоматизированным подходом с использованием SDK. Сложность конфигурации выглядит примерно так: O(n). Для каждого добавляемого API вам нужно настроить конфигурацию только один раз.

Если исходный API изменится, вы можете повторно запустить процесс интроспекции и обновить конфигурацию. Это можно сделать даже с помощью конвейера CI/CD.

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

Выполнили ли мы требования?

Давайте вернемся к требованиям, которые мы определили во введении, и посмотрим, выполнили ли мы их.

Он должен быть простым в настройке и расширении

Как мы показали, сочетание GraphQL Engine и Configuration SDK позволяет создать очень гибкое решение, при этом его легко настраивать и расширять.

Оно должно быть способно выступать посредником между различными сервисами и протоколами

Middleware говорит с клиентом на языке GraphQL. На самом деле, оно говорит на JSON RPC через HTTP, но давайте пока пропустим этот момент.

Резолверы на базе Thunk позволяют нам реализовать фазу планирования и фазу выполнения таким образом, что мы можем выступать посредником между GraphQL и любым другим протоколом, таким как REST, GraphQL, gRPC, Kafka или даже SQL.

Просто соберите всю необходимую информацию на «фазе планирования», например, таблицы и столбцы базы данных или темы и разделы сервиса Kafka, а затем выполните банкеты на «фазе исполнения», которые фактически общаются с базой данных или Kafka.

Должна быть возможность повторного развертывания без этапа генерации/компиляции кода.

Перекомпилировать и развертывать GraphQL Engine каждый раз, когда вы хотите изменить схему, было бы очень неприятно. Как мы описывали ранее, результаты фазы планирования, «планы выполнения», могут кэшироваться в памяти и даже сериализовываться/десериализовываться. Это означает, что мы можем изменять конфигурацию без шага компиляции и горизонтально масштабировать Engine вверх и вниз.

Он должен поддерживать сшивание схем

Если мы вспомним, как работает GraphQL Engine, то ранее мы описывали, что вы должны определить схему GraphQL Schema, а затем прикрепить DataSources к кортежам типов и полей. Это единственное требование для включения сшивки схем, что означает, что она работает из коробки.

Он должен поддерживать Apollo Federation

GraphQL DataSource (все с открытым исходным кодом) реализован таким образом, что он понимает спецификацию Apollo Federation Specification. Все, что вам нужно сделать, это передать правильные параметры конфигурации в DataSource, а остальное работает автоматически.

Вот пример его настройки:

const federatedApi = introspect.federation({
    apiNamespace: "federated",
    upstreams: [
        {
            url: "http://localhost:4001/graphql"
        },
        {
            url: "http://localhost:4002/graphql"
        },
    ]
});
Войти в полноэкранный режим Выход из полноэкранного режима

Источник данных GraphQL+Federation DataSource демонстрирует мощь подхода, основанного на технологии thunk. Он начинался как простой GraphQL DataSource, который затем был расширен для поддержки спецификации федерации Apollo. Поскольку фаза планирования и выполнения разделена на две части, его также очень легко тестировать.

Он должен поддерживать подписки

Подписки — это одна из тех функций, которых многие разработчики избегают при реализации GraphQL-шлюза. Особенно когда речь заходит о федерации, меня спросил кто-то из Apollo, как мы управляем всеми соединениями между клиентом и шлюзом (вниз по течению) и между шлюзом и подграфами (вверх по течению).

Ответ прост: Разделяй и властвуй. Подход, основанный на Thunk, позволяет нам разделить такие сложные проблемы, как подписки, на небольшие части.

Когда клиент подключается к шлюзу и хочет начать первую подписку, вы проходите через шаги планирования и выясняете, какую операцию GraphQL нужно отправить в источник. Если с источником еще нет активного WebSocket-соединения, вы инициируете его, используя протокол, о котором договорились все GraphQL-серверы. Получив ответное сообщение, вы пересылаете его клиенту. Если подключится второй клиент и захочет начать вторую подписку, вы можете повторно использовать то же WebSocket-соединение с источником.

Когда все клиенты отсоединятся от источника, вы можете закрыть WebSocket-соединение с источником.

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

Он должен поддерживать REST API

Тот же принцип применим и к REST API, что означает, что они также поддерживаются. REST API DataSource, вероятно, было проще всего реализовать. Это потому, что вам не нужно «обходить» дочерние поля, чтобы определить, нужно ли изменить вызов REST API. Если вы сравните REST DataSource с реализацией GraphQL DataSource, вы увидите, что первый намного проще.

Резюме

В этой статье мы раскрыли секреты создания GraphQL API Gateway: Резольверы на основе транков. Должно быть понятно, что классический подход к написанию резольверов имеет смысл для написания GraphQL API, в то время как подход на основе Thunk является правильным выбором для создания API-шлюзов, прокси и Middleware в целом.

В скором времени мы собираемся полностью открыть исходный код GraphQL Engine на основе Thunk и SDK, поэтому не забудьте подписаться, чтобы получить уведомление о релизе.

Outlook

Я мог бы написать больше о «закулисье» GraphQL-шлюзов, например, как мы реализовали Federated Subscriptions или как движок может автоматически пакетно обрабатывать запросы без необходимости полагаться на DataLoader. Пожалуйста, напишите нам в Twitter или Discord, если вас интересуют подобные темы.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *