Skip to content
This repository was archived by the owner on Apr 14, 2022. It is now read-only.

Commit b91890a

Browse files
committed
Add HTTP server, which provide GraphiQL interface. Add usability
features Now it is possible to use commands like require('graphql').execute(...) or .start_server() with no manual creating of graphql instance; close #1; changes in gql_compile is related to #16 and #50.
1 parent 780c33f commit b91890a

16 files changed

+49208
-23
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ install:
4242
- git submodule update --recursive --init
4343
- tarantoolctl rocks install lulpeg
4444
- tarantoolctl rocks install lrexlib-pcre
45+
- tarantoolctl rocks install http
4546
- cd ..
4647
# lua (with dev headers) is necessary for luacheck
4748
- sudo apt-get install lua5.1

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,61 @@ The following example doesn't work:
8787
}
8888
```
8989

90+
## Usage
91+
92+
There are two ways to use the lib.
93+
1) Create an instance and use it (detailed examples may be found in /test):
94+
```
95+
local graphql = require('graphql').new({
96+
schemas = schemas,
97+
collections = collections,
98+
accessor = accessor,
99+
service_fields = service_fields,
100+
indexes = indexes
101+
})
102+
103+
local query = [[
104+
query user($user_id: String) {
105+
user_collection(user_id: $user_id) {
106+
user_id
107+
name
108+
}
109+
}
110+
]]
111+
112+
local compiled_query = graphql:compile(query)
113+
local variables = {user_id = 'user_id_1'}
114+
local result = compiled_query:execute(variables)
115+
```
116+
2) Use the lib itself (it will create a default instance underhood. As no
117+
avro-schema is given, GraphQL schemas will be generated from results
118+
box.space.some_space:format()):
119+
```
120+
local graphql_lib = require('graphql')
121+
-- considering the same query and variables
122+
123+
local compiled_query = graphql_lib.compile(query)
124+
local result = compiled_query:execute(variables)
125+
```
126+
127+
# GraphiQL
128+
```
129+
local graphql = require('graphql').new({
130+
schemas = schemas,
131+
collections = collections,
132+
accessor = accessor,
133+
service_fields = service_fields,
134+
indexes = indexes
135+
})
136+
137+
graphql:start_server()
138+
-- now you can use GraphiQL interface at http://127.0.0.1:8080
139+
graphql:stop_server()
140+
141+
-- as well you may do (with creating default instance underhood)
142+
require('graphql').start_server()
143+
```
144+
90145
## Run tests
91146

92147
```

graphql/core/introspection.lua

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ __Schema = types.object({
5858
end
5959
},
6060

61+
subscriptionType = {
62+
description = 'If this server supports mutation, the type that mutation operations will be rooted at.',
63+
kind = __Type,
64+
resolve = function(_)
65+
return nil
66+
end
67+
},
68+
6169
directives = {
6270
description = 'A list of all directives supported by this server.',
6371
kind = types.nonNull(types.list(types.nonNull(__Directive))),
@@ -230,7 +238,7 @@ __Type = types.object({
230238
kind = types.list(types.nonNull(__Type)),
231239
resolve = function(kind)
232240
if kind.__type == 'Object' then
233-
return kind.interfaces
241+
return kind.interfaces or {}
234242
end
235243
end
236244
},

graphql/core/validate.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ local visitors = {
220220

221221
fragmentDefinition = {
222222
enter = function(node, context)
223-
kind = context.schema:getType(node.typeCondition.name.value) or false
223+
local kind = context.schema:getType(node.typeCondition.name.value) or false
224224
table.insert(context.objects, kind)
225225
end,
226226

graphql/init.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ graphql.accessor_general = accessor_general
99
graphql.accessor_space = accessor_space
1010
graphql.accessor_shard = accessor_shard
1111
graphql.new = tarantool_graphql.new
12+
graphql.compile = tarantool_graphql.compile
13+
graphql.execute = tarantool_graphql.execute
14+
graphql.start_server = tarantool_graphql.start_server
15+
graphql.stop_server = tarantool_graphql.stop_server
1216

1317
graphql.TIMEOUT_INFINITY = accessor_general.TIMEOUT_INFINITY
1418

graphql/server/graphiql/index.html

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
<html>
2+
<head>
3+
<style>
4+
body {
5+
height: 100%;
6+
margin: 0;
7+
width: 100%;
8+
overflow: hidden;
9+
}
10+
#graphiql {
11+
height: 100vh;
12+
}
13+
</style>
14+
15+
<script src="https://unpkg.com/react@15/dist/react.js"></script>
16+
<script src="https://unpkg.com/react-dom@15/dist/react-dom.js"></script>
17+
18+
<!--
19+
These two files can be found in the npm module, however you may wish to
20+
copy them directly into your environment, or perhaps include them in your
21+
favored resource bundler.
22+
-->
23+
<link rel="stylesheet" href="/static/css/graphiql.css" />
24+
<script src="/static/js/graphiql.js"></script>
25+
26+
</head>
27+
<body>
28+
<div id="graphiql">Loading...</div>
29+
<script>
30+
/**
31+
* This GraphiQL example illustrates how to use some of GraphiQL's props
32+
* in order to enable reading and updating the URL parameters, making
33+
* link sharing of queries a little bit easier.
34+
*
35+
* This is only one example of this kind of feature, GraphiQL exposes
36+
* various React params to enable interesting integrations.
37+
*/
38+
// Parse the search string to get url parameters.
39+
var search = window.location.search;
40+
var parameters = {};
41+
search.substr(1).split('&').forEach(function (entry) {
42+
var eq = entry.indexOf('=');
43+
if (eq >= 0) {
44+
parameters[decodeURIComponent(entry.slice(0, eq))] =
45+
decodeURIComponent(entry.slice(eq + 1));
46+
}
47+
});
48+
// if variables was provided, try to format it.
49+
if (parameters.variables) {
50+
try {
51+
parameters.variables =
52+
JSON.stringify(JSON.parse(parameters.variables), null, 2);
53+
} catch (e) {
54+
// Do nothing, we want to display the invalid JSON as a string, rather
55+
// than present an error.
56+
}
57+
}
58+
// When the query and variables string is edited, update the URL bar so
59+
// that it can be easily shared
60+
function onEditQuery(newQuery) {
61+
parameters.query = newQuery;
62+
updateURL();
63+
}
64+
function onEditVariables(newVariables) {
65+
parameters.variables = newVariables;
66+
updateURL();
67+
}
68+
function onEditOperationName(newOperationName) {
69+
parameters.operationName = newOperationName;
70+
updateURL();
71+
}
72+
function updateURL() {
73+
var newSearch = '?' + Object.keys(parameters).filter(function (key) {
74+
return Boolean(parameters[key]);
75+
}).map(function (key) {
76+
return encodeURIComponent(key) + '=' +
77+
encodeURIComponent(parameters[key]);
78+
}).join('&');
79+
history.replaceState(null, null, newSearch);
80+
}
81+
// Defines a GraphQL fetcher using the fetch API. You're not required to
82+
// use fetch, and could instead implement graphQLFetcher however you like,
83+
// as long as it returns a Promise or Observable.
84+
function graphQLFetcher(graphQLParams) {
85+
// This example expects a GraphQL server at the path /graphql.
86+
// Change this to point wherever you host your GraphQL server.
87+
return fetch('/graphql', {
88+
method: 'post',
89+
headers: {
90+
'Accept': 'application/json',
91+
'Content-Type': 'application/json',
92+
},
93+
body: JSON.stringify(graphQLParams),
94+
credentials: 'include',
95+
}).then(function (response) {
96+
return response.text();
97+
}).then(function (responseBody) {
98+
try {
99+
return JSON.parse(responseBody);
100+
} catch (error) {
101+
return responseBody;
102+
}
103+
});
104+
}
105+
// Render <GraphiQL /> into the body.
106+
// See the README in the top level of this module to learn more about
107+
// how you can customize GraphiQL by providing different values or
108+
// additional child elements.
109+
ReactDOM.render(
110+
React.createElement(GraphiQL, {
111+
fetcher: graphQLFetcher,
112+
query: parameters.query,
113+
variables: parameters.variables,
114+
operationName: parameters.operationName,
115+
onEditQuery: onEditQuery,
116+
onEditVariables: onEditVariables,
117+
onEditOperationName: onEditOperationName
118+
}),
119+
document.getElementById('graphiql')
120+
);
121+
</script>
122+
</body>
123+
</html>

graphql/server/server.lua

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
local fio = require('fio')
2+
local utils = require('graphql.server.utils')
3+
local json = require('json')
4+
local check = require('graphql.utils').check
5+
6+
local server = {}
7+
8+
local default_charset = "utf-8"
9+
10+
local function file_mime_type(filename)
11+
if string.endswith(filename, ".css") then
12+
return string.format("text/css; charset=%s", default_charset)
13+
elseif string.endswith(filename, ".js") then
14+
return string.format("application/javascript; charset=%s", default_charset)
15+
elseif string.endswith(filename, ".html") then
16+
return string.format("text/html; charset=%s", default_charset)
17+
elseif string.endswith(filename, ".svg") then
18+
return string.format("image/svg+xml")
19+
end
20+
21+
return "application/octet-stream"
22+
end
23+
24+
local function static_handler(req)
25+
local path = req.path
26+
27+
if path == '/' then
28+
path = fio.pathjoin('graphiql', 'index.html')
29+
end
30+
31+
local lib_dir = utils.script_path()
32+
path = fio.pathjoin(lib_dir, path)
33+
local body = utils.read_file(path)
34+
35+
return {
36+
status = 200,
37+
headers = {
38+
['content-type'] = file_mime_type(path)
39+
},
40+
body = body
41+
}
42+
end
43+
44+
function server.init(graphql, host, port)
45+
local host = host or '127.0.0.1'
46+
local port = port or 8080
47+
local httpd = require('http.server').new(host, port)
48+
49+
local function api_handler(req)
50+
local body = req:read()
51+
52+
if body == nil or body == '' then
53+
return {
54+
status = 200,
55+
body = json.encode(
56+
{errors = {{message = "Expected a non-empty request body"}}}
57+
)
58+
}
59+
end
60+
61+
local parsed = json.decode(body)
62+
if parsed == nil then
63+
return {
64+
status = 200,
65+
body = json.encode(
66+
{errors = {{message = "Body should be a valid JSON"}}}
67+
)
68+
}
69+
end
70+
71+
if type(parsed) ~= 'table' then
72+
return {
73+
status = 200,
74+
body = json.encode(
75+
{errors = {message = "Body should be a dictionary"}}
76+
)
77+
}
78+
end
79+
80+
if parsed.query == nil or type(parsed.query) ~= "string" then
81+
return {
82+
status = 200,
83+
body = json.encode(
84+
{errors = {{message = "Body should have 'query' field"}}}
85+
)
86+
}
87+
end
88+
89+
local variables = parsed.variables
90+
if variables == nil then
91+
variables = {}
92+
end
93+
94+
if type(variables) == 'cdata' then
95+
if variables == nil then
96+
variables = {}
97+
end
98+
end
99+
100+
local query = parsed.query
101+
102+
local ok, compiled_query = pcall(graphql.compile, graphql, query)
103+
if not ok then
104+
return {
105+
status = 200,
106+
body = json.encode({errors = {{message = compiled_query}}})
107+
}
108+
end
109+
110+
local ok, result = pcall(compiled_query.execute, compiled_query, variables)
111+
if not ok then
112+
return {
113+
status = 200,
114+
body = json.encode({error = {{message = result}}})
115+
}
116+
end
117+
118+
result = {data = result}
119+
return {
120+
status = 200,
121+
headers = {
122+
[ 'content-type'] = "application/json; charset=utf-8"
123+
},
124+
body = json.encode(result)
125+
}
126+
end
127+
128+
httpd:route({ path = '/' }, static_handler)
129+
httpd:route({ path = '/static/css/graphiql.css' }, static_handler)
130+
httpd:route({ path = '/static/js/graphiql.js' }, static_handler)
131+
httpd:route({ path = '/graphql' }, api_handler)
132+
133+
return httpd
134+
end
135+
136+
return server

0 commit comments

Comments
 (0)