- PVSM.RU - https://www.pvsm.ru -

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

Клиент может отправить на сервер запросы двух типов:
Первая задача, которую требовалось решить — это разбор полученного GraphQL запроса. Изначально я хотел найти внешнюю библиотеку, отправить в него запрос и получить AST. Но от этой идеи решил отказаться по ряду причин. Это еще одна черная коробка, да и долгие callback еще никто не отменял.
Так я пришел к тому, что нужно реализовать собственный парсер, но откуда взять его описание? Тут оказалось проще, GraphQL [5] — это open source проект, у Facebook он довольно хорошо описан, да и найти примеры парсеров на других языках не составило труда.
Описание AST можно найти здесь [6].
Давайте посмотрим на пример запроса и дерево:
{
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
}
]
}
}
]
}
}
]
}
После полученное дерево нужно проверить на существование классов, свойств, аргументов и их типов на сервере, то есть дерево нужно валидировать. Рекурсивно пробегаемся по дереву и проверяем на соответствие вышеперечисленного с тем, что на сервере. Вот как выглядит класс [7].
Схема — это документация по доступным классам, свойствам и описание типов свойств этих классов.
В реализации 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 [8], взять оттуда схему не составило труда. Но тут столкнулися с другой проблемой, там настолько большая серверная часть, что схема занимает аж 64 тыс. строк. Разбираться в этом не очень-то хотелось, стал искать другие способы получить схему.
Так как основой наших платформ является СУБД, то на следующем шаге решил самому собрать и запустить GraphQL для PostgreSQL и SQLite. С PostgreSQL получил схему всего в 22 тыс. строк, а SQLite 18 тыс. строк. Это уже лучше, но это тоже не мало, стал искать дальше.
Остановился на реализации для NodeJS, собрал [9], написал минимальный ресолвер и получил схему всего в 1800 строк — это уже намного лучше!
Разобравшись в схеме, я решил генерировать ее автоматически без предварительного создания ресолверов на сервере, так как получить метаинформацию о классах и их отношении друг к другу очень просто.
Для генерации своей схемы нужно понять несколько вещей:
{
"data": {
"__schema": {
"queryType": {
"name": "Query"
},
"mutationType": null,
"subscriptionType": null,
"types":[...
],
"directives":[...
]
}
}
}
{
"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
}
{
"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
}
Вот мы и добрались до самой сложной и интересной части. По запросу как-то нужно генерировать ответ. При этом, ответ должен быть в формате 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
}
}
Как этот процесс выглядит на схеме:

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

→ Ссылка [10] на репозиторий проекта
→ Ссылка [11] на демо сервер
Автор: Gevorg95
Источник [12]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ensemble/288458
Ссылки в тексте:
[1] GraphQL: http://graphql.org/
[2] статье: http://
[3] AST: https://ru.wikipedia.org/wiki/%D0%90%D0%B1%D1%81%D1%82%D1%80%D0%B0%D0%BA%D1%82%D0%BD%D0%BE%D0%B5_%D1%81%D0%B8%D0%BD%D1%82%D0%B0%D0%BA%D1%81%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5_%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE
[4] JSON: https://ru.wikipedia.org/wiki/JSON
[5] GraphQL: http://facebook.github.io/graphql/October2016/
[6] здесь: http://facebook.github.io/graphql/October2016/#Document
[7] класс: https://github.com/intersystems-ru/GraphQL/blob/master/cls/GraphQL/Query/Validation.cls
[8] GraphQL API: https://developer.github.com/v4/explorer/
[9] собрал: https://graphql.org/graphql-js/
[10] Ссылка: https://github.com/intersystems-community/GraphQL
[11] Ссылка: http://37.139.6.217:57773/graphiql/index.html
[12] Источник: https://habr.com/post/358720/?utm_campaign=358720
Нажмите здесь для печати.