Как я реализовал 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 и т.д., свои скалярные типы добавляем туда же.

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


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


    По каждому новому 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
    • Лист из простых типов
    • Лист из объектов

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



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


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

    Ссылка на репозиторий проекта
    Ссылка на демо сервер

    • +12
    • 2,3k
    • 2

    InterSystems

    95,42

    Вендор: СУБД Caché, OLAP DeepSee, шина Ensemble

    Поделиться публикацией

    Похожие публикации

    Комментарии 2
      0
      Ссылка на статью не работает, поправьте плз
        0
        Поправил, спасибо!

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

      Самое читаемое