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

Клиент может отправить на сервер запросы двух типов:
- Запрос на получение схемы.
На сервере генерируется схема и возвращается клиенту, об этом чуть позже. - Запрос на получение/изменение определенного набора данных. В этом случаи происходит генерация AST, вальвация и генерация ответа.
Генерация AST
Первая задача, которую требовалось решить — это разбор полученного GraphQL запроса. Изначально я хотел найти внешнюю библиотеку, отправить в него запрос и получить AST. Но от этой идеи решил отказаться по ряду причин. Это еще одна черная коробка, да и долгие callback еще никто не отменял.
Так я пришел к тому, что нужно реализовать собственный парсер, но откуда взять его описание? Тут оказалось проще, GraphQL — это open source проект, у Facebook он довольно хорошо описан, да и найти примеры парсеров на других языках не составило труда.
Описание AST можно найти здесь.
Давайте посмотрим на пример запроса и дерево:
{ Sample_Company(id: 15) { Name } }
{ "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 добавить все классы, их аргументы и типы этих класса. Таким образом они будут доступны из корневого элемента.
{ "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 } }
Как этот процесс выглядит на схеме:

На данный момент ответ генерируется по следующим запросам:
- Базовые
- Вложенные объекты
- Только отношение many to one
- Лист из простых типов
- Лист из объектов
Ниже я привел схему, какие типы отношений еще необходимо реализовать:

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