📜 ⬆️ ⬇️

How I implemented graphQL for InterSystems platforms


About GraphQL and how to use it has already been discussed in this article . Here I will tell you about what tasks faced me, and about the results that were achieved in the implementation process of GraphQL for InterSystems platforms.


What is the article about?



Let's look at the whole cycle from sending a request to receiving a response on a simple scheme:


image

A client can send two types of requests to the server:



AST generation


The first task that needed to be solved is the parsing of the received GraphQL query. Initially, I wanted to find an external library, send a request to it and get an AST. But he decided to abandon this idea for several reasons. This is another black box, and no one has yet canceled a long callback.


So I came to the conclusion that you need to implement your own parser, but where can you get its description from? It turned out to be simpler here, GraphQL is an open source project, with Facebook it is pretty well described, and finding parsers in other languages ​​was not difficult.


Description AST can be found here .


Let's look at an example query and tree:


{ 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 } ] } } ] } } ] } 

Validation


After the resulting tree, you need to check for the existence of classes, properties, arguments and their types on the server, that is, the tree must be validated. Recursively run through the tree and check for compliance of the above with the fact that the server. This is what the class looks like.


Schema generation


A schema is a documentation of the available classes, properties, and a description of the types of properties of these classes.


In the implementation of GraphQL in other languages ​​or technologies, the scheme is generated by resolvers. Resolver is a description of the types of data available on the server.


An example of resolvers, request and response
 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" } ] } } } 

But in order to generate a schema, you need to understand its structure, find some description, or better examples. The first thing I did was try to find an example that would make the circuit structure clear. Since GitHub has its own GraphQL API , it’s easy to get a scheme from there. But here we faced another problem, there is such a large server part that the scheme takes up as much as 64 thousand lines. I didn’t really want to understand this, I began to look for other ways to get a scheme.


Since the basis of our platforms is a DBMS, then in the next step I decided to build and run GraphQL for PostgreSQL and SQLite myself. With PostgreSQL, I received a scheme of only 22 thousand lines, and SQLite 18 thousand lines. This is better, but it is also not enough, I began to look further.


I stopped at the implementation for NodeJS, assembled , wrote a minimal resolver and got a scheme of only 1800 lines - this is much better!


Having understood the scheme, I decided to generate it automatically without first creating resolvers on the server, since it is very easy to get meta-information about the classes and their relation to each other.


To generate your scheme you need to understand a few things:



Consider the example of two classes, Example_City and 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 } 


Description of the classes themselves
 { "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 } 


Answer generation


So we got to the most difficult and interesting part. On request, somehow you need to generate a response. At the same time, the answer must be in json format and correspond to the structure of the request.


For each new GraphQL query, a class should be generated on the server, in which the logic for obtaining the requested data will be described. In this case, the request is not considered new if the values ​​of the arguments have changed, i.e. if we get some data set in Moscow, and in the next query in London, the new class will not be generated, just the new values ​​will be substituted. Finally, in this class there will be a SQL query, after its execution the resulting data set will be saved in JSON format, the structure of which will correspond to the GraphQL query.


Sample query and generated class
 { 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 } } 

How this process looks on the diagram:


image

At the moment, the answer is generated by the following requests:



Below I give a diagram of what types of relationships still need to be implemented:



Let's sum up



→ Link to project repository
→ Link to demo server


')

Source: https://habr.com/ru/post/358720/


All Articles