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

GraphQL - одна из технологий, которую я изучил в 2016 году, и она изменила мой взгляд на дизайн API.

Простота - это то, что мне нравится в GraphQL. Он позволяет вам определять пользовательские типы данных (узлы) и их отношения (ребра), и все. Об остальном позаботится GraphQL. Нет необходимости вдаваться в подробности о преимуществах GraphQL перед REST и прочим. Материала больше чем достаточно на тот предмет. Здесь я хочу представить проблему, с которой я столкнулся с GraphQL и моим решением.

Я решил отказаться от REST и использовать GraphQL в веб-фреймворке, который я сейчас разрабатываю с помощью JavaScript. Но возникла большая проблема. Моя структура использует модульную архитектуру. «Модуль» - это изолированный фрагмент кода, который может (1) добавлять новые функции в приложение или (2) расширять поведение существующих функций или модулей.

Проблема

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

У нас также есть модуль разрешений для управления разрешениями пользователей. Он создает две таблицы базы данных:

  1. permission, чтобы отслеживать все возможные разрешения и их метки,
  2. user_permission, который поддерживает отношения «многие ко многим» между нашими двумя сущностями User и Permission.

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

А теперь давайте снова поговорим о GraphQL! А именно GraphQL-JS (официальная эталонная реализация GraphQL для JavaScript).

  • Как мы создаем типы GraphQL для этих модулей?

У нас может быть UserType в модуле пользователя и PermissionType в модуле разрешений.

  • Как нам управлять связью между этими двумя? т.е. как мы можем получить разрешения пользователя за один раз?

Мы не можем изменить UserType в модуле пользователя и добавить поле permission. Потому что это делает модуль пользователя зависимым от модуля разрешений, что сводит на нет цель всего дизайна. Можем ли мы создать ссылку от пользователя к разрешению в модуле разрешений, добавив поле в наш UserType? Ну, нет!

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

Решение

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

Для достижения этой цели я написал небольшую оболочку вокруг GraphQL-JS, которая также действует как реестр. Используя эту оболочку, мы сначала определим пользовательские типы данных внутри реестра. Позже мы сможем вернуть их в любом месте кода. Вот пример создания и получения userType:

// defining the type
graph.type('userType', {
 name: 'User',
 description: 'User Object',
 fields: () => ({
  id: {type: graph.ql.GraphQLInt},
  name: {type: graph.ql.GraphQLString}
 })
});
// accessing the type
let userType = graph.type('userType');

Довольно знакомо, правда? Похоже, мы все еще используем GraphQL-JS. Но обратите внимание, что graph.type('userType') не является типом GraphQL, пока.
graph также предоставляет все встроенные типы GraphQL через свойство .ql, например graph.ql.GraphQLString (в основном graph.ql = require('graphql');.)

Также в нашем модуле разрешений:

graph.type('permissionType', {
 name: 'Permission',
 fields: () => ({
  id: {type: graph.ql.GraphQLInt},
  label: {type: graph.ql.GraphQLString}
 })
});

У каждого типа есть .extend(...) функция, с помощью которой мы можем расширять поля типа. Итак, мы можем получить userType внутри модуля разрешений и расширить его значением permissions:

graph.type('userType').extend(() => ({
 permissions: {
  type: new graph.ql.GraphQLList(graph.type('permissionType')),
  resolve: user => user.getPermissions()
 }
}));

.extend вводит функцию, возвращающую объект. graph расширит поля типа предоставленным объектом.
Но разве new graph.ql.GraphQLList не требует, чтобы ввод был типом GraphQL? Да! graph.type('...') изменит свое поведение и вернет твердый тип GraphQL-JS, как только нам понадобится завершить схему! Благодаря этому наша обертка очень проста и удобна в использовании.

Итак, теперь мы ожидаем, что userType также будет иметь поле permissions. И это здорово, поскольку модуль пользователя ничего об этом не знает. Все это в модуле разрешений!

Как это работает?

Мы храним все наши промежуточные типы во внутреннем объекте rawTypes. Но мы не храним все поля в объекте. Вместо этого мы храним их как массив объектов. Каждый элемент массива будет функцией, возвращающей дополнительный бит информации о том, что должно быть помещено в поля последнего типа! Итак, для расширения достаточно просто вставить функцию в этот массив:

extend: fields => rawTypes[name].fields.push(fields);
// Or to be more dev friendly, if the input is not a function,
// convert it to one.
extend: fields => rawTypes[name].fields.push(fields instanceof Function ? fields : () => fields)

При окончательной доработке схемы мы создаем GraphQLObjectType из наших необработанных типов. Но GraphQLObjectType ожидает, что поля будут всего лишь одной простой функцией, которая возвращает fields объект, а не массив. Нам нужно преобразовать все эти функции в одну функцию. Это однострочное решение для этой цели:

let fnFromArray = fns => () => fns.reduce((obj, fn) => Object.assign({}, obj, fn.call()), {});

fnFromArray вводит массив функций и возвращает другую функцию. «Возвращающая функция» перебирает все эти входные функции и вызывает их. Затем, используя Object.assign и reduce, он объединяет возвращаемые значения в один объект.

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

graph.type('query').extend(() => ({
 allUsers: {
  type: new graph.ql.GraphQLList(graph.type('userType')),
  resolve: () => User.findAll()
 }
}));

graph предоставляет .generate() функцию, которая блокирует наши промежуточные типы. Затем он создает из них типы GraphQL-JS. Он заменяет промежуточные типы реальными после их создания. Итак, graph.type('...') вернет тип GraphQL-JS, и строки, подобные new graph.ql.GraphQLList(graph.type('...')), станут действительными.

Этот обходной путь не является готовым к эксплуатации кодом. Это только идея о том, как мы можем добиться этого, используя немного взлома и кода!

Вы можете найти эту оболочку GraphQL на npm (graphql-dschema), а ее исходный код - на Github. Не стесняйтесь поднимать вопросы или предлагать улучшения по проблемам Github или в комментариях к этой публикации. Вы также можете найти небольшой рабочий пример по этому репо.