Расширение API-запросов в ShopModxBox

Всем добрый день!

Сегодня речь пойдет об API сборки. Напомню, что сердцем обновленной сборки является API на базе GraphQL. Если кто еще не видел, можно поиграться здесь: https://shopmodx.ru/db/

Как все это работает? В GraphQL есть две основные составляющие:

  1. Схема, то есть структура данных, которые в принципе могут быть запрошены. К примеру, вот схема заказа, а вот схема отдельной позиции в заказе.

Давайте разведем это на примере товарной позиции.

export const OrderProductFields = {
  id: {
    type: GraphQLInt,
    description: "ID",
  },
  order_id: {
    type: GraphQLInt,
    description: "ID заказа",
  },
  product_id: {
    type: GraphQLInt,
    description: "ID товара",
  },
  quantity: {
    type: GraphQLInt,
    description: "Количество",
  },
  price: {
    type: GraphQLFloat,
    description: "Стоимость",
  },
  Product: {
    type: MODXResourceType,
    description: "Товар",
    resolve: (source, args, context, info) => {

      const {
        product_id,
      } = source;

      // console.log("product_id", product_id);

      if(!product_id){
        return null;
      }

      const {
        rootResolver,
      } = context;

      Object.assign(args, {
        id: product_id,
        // _store: "remote",
      });

      return rootResolver(null, args, context, info);

    },
  },
};

const OrderProductType = new GraphQLObjectType({
  name: 'OrderProductType',
  description: 'Позиция заказа',
  fields: () => (OrderProductFields),
});

Большинство таких объектов - производные от класса GraphQLObjectType (хотя есть и другие классы, но мы их не будем сейчас разбирать здесь). В таких объектах два обязательных поля: name и fields. И вот тут сразу отмечу пару тонкостей. Во-первых, в рамках всей GraphQL-схемы имена объектов должны быть уникальными. Нельзя иметь в схеме два объекта с одним именем, даже если все поля в них разные. Это необходимо для правильного разбора структуры, так как граф позволяет формировать многоуровневые запросы с зависимостями и еще на уровне сбора схемы проверяет все ли везде в порядке. Плюс ко всему на основе общей структуры генерируется документация по сформированному АПИ.

Во-вторых, в качестве полей допустимо использовать не только простые поля типа GraphQLInt, GraphQLString и т.п., но и другие объекты. В данном примере это Product с типом MODXResourceType, экспортируемый из ../modResource, который, в свою очередь, экспортирует этот тип из компонента modx-react.

Да, все это довольно запутанно, но зато, как я убедился уже не раз в работе с различными проектами, потом не приходится бегать по всему проекту и искать ответы на вопросы что можно получить по АПИ, а что нет. Даже на тех проектах, где предоставлялась документация, на практике оказывалось, что документация часто не соответствует действительности (разработчик внес изменения в АПИ, а в документацию забыл). Здесь же документация генерируется на лету. А если где-то в схеме оказалась какая-то ошибка, то вы в принципе запроса не выполните, пока не устраните ошибку. И даже если у вас все хорошо, то есть валидация и по типам данных, то есть в АПИ прописан для поля тип данных число, а в ответе от источника пришла строка. Все, граф на это заругается, и правильно сделает. Так что на выходе потом имеющийся порядок компенсирует все мучения.

Здесь уточню, что хотя мы в поле прописали только одну сущность - Product, на выходе мы в этом месте получим всю структуру объекта Product.

Вторая часть графа - это уже непосредственно тело запроса. К примеру, если у вас есть аккаунт на гитхабе, вы можете поиграться с их конструктором запросов https://developer.github.com/v4/explorer/

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

query OwnOrder(
  $orderGetProducts:Boolean = false
  $orderProductGetProduct:Boolean = false
  $getImageFormats:Boolean = false
){
  
  order(
    ownOrder: true
  )
  @storage(store:remote)
  {
    ...Order
  }
  
}

fragment Order on OrderType{
  
  ...OrderFields
  
  Products
  @include(if:$orderGetProducts)
  @storage(store:remote)
  {
    ...OrderProduct
  }
  
}

query OwnOrder - это название отдельной операции.

$orderGetProducts:Boolean = false и т.п. - это входящие переменные (с указанием типа данных и значения по умолчанию).

Это непосредственно тело запроса:

  order(
    ownOrder: true
  )
  @storage(store:remote)
  {
    ...Order
  }

...Order - это конструкция передачи вывода в заранее определенный фрагмент (выше его код так же приведен).

@storage - директива, но директивы мы рассмотрим как-нибудь позже.

Так вот, все эти запросы описаны заранее и обрабатываются на стороне сервера, то есть передавая на сервер запрос OwnOrder, граф находит эту операцию в подготовленном листинге запросов и собирает указанные данные. Вот весь этот листинг в компоненте shopmodx-react: https://github.com/MODX-Club/shopmodx-react/blob/master/components/ORM/query.js

Но тут встает вопрос: а как же на конечном проекте расширять эти запросы, если они уже заранее прописаны в обновляемом компоненте?

Для решения этой задачи я выпустил сегодня новый компонент react-cms-graphql-utils, в составе которого имеется полезная функция mergeQuery

Передаем в нее базовый список запросов и кастомный, mergeQuery производит разбор одного и другого на отдельные сущности и мержит их, перетирая имеющиеся и дополняя новыми. Свои кастомные запросы можно писать сюда: https://github.com/MODX-Club/ShopModxBox/blob/5559089d68a1ae75b641492d4ef6395180e508fd/assets/components/modxsite/templates/shopmodx/v4.0/app/modules/Site/components/ORM/query.js#L6

Позже я покажу реальные кейсы с применением всего этого.