Как я реализовал 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
  • Лист из простых типов
  • Лист из объектов

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

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

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

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


ссылка на оригинал статьи https://habr.com/company/intersystems/blog/358720/


Комментарии

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

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