Как я реализовал GraphQL для платформ компании InterSystems

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

О чем статья

  • Генерация AST по GraphQL запросу и его валидация
  • Генерация документации
  • Генерация ответа в формате JSON

Давайте рассмотрим весь цикл от отправки запроса до получения ответа на простой схеме:

image

Клиент может отправить на сервер запросы двух типов:
– Запрос на получение схемы.
На сервере генерируется схема и возвращается клиенту, об этом чуть позже.
– Запрос на получение/изменение определенного набора данных. В этом случаи происходит генерация AST, вальвация и генерация ответа.

Генерация AST

Первая задача, которую требовалось решить – это разбор полученного GraphQL запроса. Изначально я хотел найти внешнюю библиотеку, отправить в него запрос и получить AST. Но от этой идеи решил отказаться по ряду причин. Это еще одна черная коробка, да и долгие callback еще никто не отменял.

Так я пришел к тому, что нужно реализовать собственный парсер, но откуда взять его описание? Тут оказалось проще, GraphQL – это open source проект, у Facebook он довольно хорошо описан, да и найти примеры парсеров на других языках не составило труда.

Описание AST можно найти здесь.

Давайте посмотрим на пример запроса и дерево:

{
  Sample_Company(id: 15) {
    Name
  }
}

AST

{
  "Kind": "Document",
  "Location": {
    "Start": 1,
    "End": 45
  },
  "Definitions": [
    {
      "Kind": "OperationDefinition",
      "Location": {
        "Start": 1,
        "End": 45
      },
      "Directives": [],
      "VariableDefinitions": [],
      "Name": null,
      "Operation": "Query",
      "SelectionSet": {
        "Kind": "SelectionSet",
        "Location": {
          "Start": 1,
          "End": 45
        },
        "Selections": [
          {
            "Kind": "FieldSelection",
            "Location": {
              "Start": 5,
              "End": 44
            },
            "Name": {
              "Kind": "Name",
              "Location": {
                "Start": 5,
                "End": 20
              },
              "Value": "Sample_Company"
            },
            "Alias": null,
            "Arguments": [
              {
                "Kind": "Argument",
                "Location": {
                  "Start": 26,
                  "End": 27
                },
                "Name": {
                  "Kind": "Name",
                  "Location": {
                    "Start": 20,
                    "End": 23
                  },
                  "Value": "id"
                },
                "Value": {
                  "Kind": "ScalarValue",
                  "Location": {
                    "Start": 24,
                    "End": 27
                  },
                  "KindField": 11,
                  "Value": 15
                }
              }
            ],
            "Directives": [],
            "SelectionSet": {
              "Kind": "SelectionSet",
              "Location": {
                "Start": 28,
                "End": 44
              },
              "Selections": [
                {
                  "Kind": "FieldSelection",
                  "Location": {
                    "Start": 34,
                    "End": 42
                  },
                  "Name": {
                    "Kind": "Name",
                    "Location": {
                      "Start": 34,
                      "End": 42
                    },
                    "Value": "Name"
                  },
                  "Alias": null,
                  "Arguments": [],
                  "Directives": [],
                  "SelectionSet": null
                }
              ]
            }
          }
        ]
      }
    }
  ]
}

Валидация

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

Генерация схемы

Схема – это документация по доступным классам, свойствам и описание типов свойств этих классов.

В реализации GraphQL на других языках или технологиях схема генерируется по ресолверам. Ресолвер – это описание типов доступных данных на сервере.

Пример ресолверов, запроса и ответа

type Query {
  human(id: ID!): Human
}

type Human {
  name: String
  appearsIn: [Episode]
  starships: [Starship]
}

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

type Starship {
  name: String
}
{
  human(id: 1002) {
    name
    appearsIn
    starships {
      name
    }
  }
}
{
  "data": {
    "human": {
      "name": "Han Solo",
      "appearsIn": [
        "NEWHOPE",
        "EMPIRE",
        "JEDI"
      ],
      "starships": [
        {
          "name": "Millenium Falcon"
        },
        {
          "name": "Imperial shuttle"
        }
      ]
    }
  }
}

Но, чтобы сгенерировать схему нужно понять ее структуру, найти какое-то описание или лучше примеры. Первое, что я сделал, попробовал найти пример, который дал бы понять структуру схемы. Так как у GitHub есть свой GraphQL API, взять оттуда схему не составило труда. Но тут столкнулися с другой проблемой, там настолько большая серверная часть, что схема занимает аж 64 тыс. строк. Разбираться в этом не очень-то хотелось, стал искать другие способы получить схему.

Так как основой наших платформ является СУБД, то на следующем шаге решил самому собрать и запустить GraphQL для PostgreSQL и SQLite. С PostgreSQL получил схему всего в 22 тыс. строк, а SQLite 18 тыс. строк. Это уже лучше, но это тоже не мало, стал искать дальше.

Остановился на реализации для NodeJS, собрал, написал минимальный ресолвер и получил схему всего в 1800 строк – это уже намного лучше!

Разобравшись в схеме, я решил генерировать ее автоматически без предварительного создания ресолверов на сервере, так как получить метаинформацию о классах и их отношении друг к другу очень просто.

Для генерации своей схемы нужно понять несколько вещей:

  • Незачем генерировать ее с нуля, можно взять схему из NodeJS, убрать оттуда все лишнее и добавить все, что нужно мне.
  • В корне схемы есть тип queryType, его поле name нужно инициализировать каким-то значением. Остальные два типа нас не интересуют, так как на данный момент они находиться на стадии реализации.
  • Все доступные классы и их свойства необходимо добавить в массив types.
    {
        "data": {
            "__schema": {
                "queryType": {
                    "name": "Query"
                },
                "mutationType": null,
                "subscriptionType": null,
                "types":[...
                ],
                "directives":[...
                ]
            }
        }
    }
  • Во-первых, нужно описать корневой элемент Query, а в массив fields добавить все классы, их аргументы и типы этих класса. Таким образом они будут доступны из корневого элемента.

Рассмотрим на примере двух классов, Example_City и Example_Country

{
    "kind": "OBJECT",
    "name": "Query",
    "description": "The query root of InterSystems GraphQL interface.",
    "fields": [
        {
            "name": "Example_City",
            "description": null,
            "args": [
                {
                    "name": "id",
                    "description": "ID of the object",
                    "type": {
                        "kind": "SCALAR",
                        "name": "ID",
                        "ofType": null
                    },
                    "defaultValue": null
                },
                {
                    "name": "Name",
                    "description": "",
                    "type": {
                        "kind": "SCALAR",
                        "name": "String",
                        "ofType": null
                    },
                    "defaultValue": null
                }
            ],
            "type": {
                "kind": "LIST",
                "name": null,
                "ofType": {
                    "kind": "OBJECT",
                    "name": "Example_City",
                    "ofType": null
                }
            },
            "isDeprecated": false,
            "deprecationReason": null
        },
        {
            "name": "Example_Country",
            "description": null,
            "args": [
                {
                    "name": "id",
                    "description": "ID of the object",
                    "type": {
                        "kind": "SCALAR",
                        "name": "ID",
                        "ofType": null
                    },
                    "defaultValue": null
                },
                {
                    "name": "Name",
                    "description": "",
                    "type": {
                        "kind": "SCALAR",
                        "name": "String",
                        "ofType": null
                    },
                    "defaultValue": null
                }
            ],
            "type": {
                "kind": "LIST",
                "name": null,
                "ofType": {
                    "kind": "OBJECT",
                    "name": "Example_Country",
                    "ofType": null
                }
            },
            "isDeprecated": false,
            "deprecationReason": null
        }
    ],
    "inputFields": null,
    "interfaces": [],
    "enumValues": null,
    "possibleTypes": null
}
  • Во-вторых, поднимаемся на уровень выше и в types добавляем классы, которые уже описали в объекте Query уже со всеми свойствами, типами и отношением к другим классам.

Описание самих классов

{
"kind": "OBJECT",
"name": "Example_City",
"description": "",
"fields": [
    {
        "name": "id",
        "description": "ID of the object",
        "args": [],
        "type": {
            "kind": "SCALAR",
            "name": "ID",
            "ofType": null
        },
        "isDeprecated": false,
        "deprecationReason": null
    },
    {
        "name": "Country",
        "description": "",
        "args": [],
        "type": {
            "kind": "OBJECT",
            "name": "Example_Country",
            "ofType": null
        },
        "isDeprecated": false,
        "deprecationReason": null
    },
    {
        "name": "Name",
        "description": "",
        "args": [],
        "type": {
            "kind": "SCALAR",
            "name": "String",
            "ofType": null
        },
        "isDeprecated": false,
        "deprecationReason": null
    }
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Example_Country",
"description": "",
"fields": [
    {
        "name": "id",
        "description": "ID of the object",
        "args": [],
        "type": {
            "kind": "SCALAR",
            "name": "ID",
            "ofType": null
        },
        "isDeprecated": false,
        "deprecationReason": null
    },
    {
        "name": "City",
        "description": "",
        "args": [],
        "type": {
            "kind": "LIST",
            "name": null,
            "ofType": {
                "kind": "OBJECT",
                "name": "Example_City",
                "ofType": null
            }
        },
        "isDeprecated": false,
        "deprecationReason": null
    },
    {
        "name": "Name",
        "description": "",
        "args": [],
        "type": {
            "kind": "SCALAR",
            "name": "String",
            "ofType": null
        },
        "isDeprecated": false,
        "deprecationReason": null
    }
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
}
  • В-третьих, в types уже описаны все популярные скалярные типы, вроде int, string и т.д., свои скалярные типы добавляем туда же.

Генерация ответа

Вот мы и добрались до самой сложной и интересной части. По запросу как-то нужно генерировать ответ. При этом, ответ должен быть в формате js и соответствовать структуре запроса.

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

Пример запроса и сгенерированного класса

{
  Sample_Company(id: 15) {
    Name
  }
}
Class gqlcq.qsmytrXzYZmD4dvgwVIIA [ Not ProcedureBlock ]
{
ClassMethod Execute(arg1) As %DynamicObject
{
    set result = {"data":{}}
    set query1 = []

	#SQLCOMPILE SELECT=ODBC
&sql(DECLARE C1 CURSOR FOR
     SELECT  Name
     INTO :f1
     FROM Sample.Company
     WHERE id= :arg1
)   &sql(OPEN C1)
    &sql(FETCH C1)
    While (SQLCODE = 0) {
        do query1.%Push({"Name":(f1)})
        &sql(FETCH C1)
    }
    &sql(CLOSE C1)
    set result.data."Sample_Company" = query1
	
	quit result
}

ClassMethod IsUpToDate() As %Boolean
{
   quit:$$$comClassKeyGet("Sample.Company",$$$cCLASShash)'="3B5DBWmwgoE" $$$NO
   quit $$$YES
}
}

Как этот процесс выглядит на схеме:

image

На данный момент ответ генерируется по следующим запросам:

  • Базовые
  • Вложенные объекты
    • Только отношение many to one
  • Лист из простых типов
  • Лист из объектов

Ниже я привел схему, какие типы отношений еще необходимо реализовать:

Подведем итоги

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

Добавить комментарий

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