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

GraphiQL server (+ minor usability features) #112

Merged
merged 1 commit into from
Apr 20, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ install:
- git submodule update --recursive --init
- tarantoolctl rocks install lulpeg
- tarantoolctl rocks install lrexlib-pcre
- tarantoolctl rocks install http
- cd ..
# lua (with dev headers) is necessary for luacheck
- sudo apt-get install lua5.1
Expand Down
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,61 @@ The following example doesn't work:
}
```

## Usage

There are two ways to use the lib.
1) Create an instance and use it (detailed examples may be found in /test):
```
local graphql = require('graphql').new({
schemas = schemas,
collections = collections,
accessor = accessor,
service_fields = service_fields,
indexes = indexes
})

local query = [[
query user($user_id: String) {
user_collection(user_id: $user_id) {
user_id
name
}
}
]]

local compiled_query = graphql:compile(query)
local variables = {user_id = 'user_id_1'}
local result = compiled_query:execute(variables)
```
2) Use the lib itself (it will create a default instance underhood. As no
avro-schema is given, GraphQL schemas will be generated from results
box.space.some_space:format()):
```
local graphql_lib = require('graphql')
-- considering the same query and variables

local compiled_query = graphql_lib.compile(query)
local result = compiled_query:execute(variables)
```

# GraphiQL
```
local graphql = require('graphql').new({
schemas = schemas,
collections = collections,
accessor = accessor,
service_fields = service_fields,
indexes = indexes
})

graphql:start_server()
-- now you can use GraphiQL interface at http://127.0.0.1:8080
graphql:stop_server()

-- as well you may do (with creating default instance underhood)
require('graphql').start_server()
```

## Run tests

```
Expand Down
10 changes: 9 additions & 1 deletion graphql/core/introspection.lua
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ __Schema = types.object({
end
},

subscriptionType = {
description = 'If this server supports mutation, the type that mutation operations will be rooted at.',
kind = __Type,
resolve = function(_)
return nil
end
},

directives = {
description = 'A list of all directives supported by this server.',
kind = types.nonNull(types.list(types.nonNull(__Directive))),
Expand Down Expand Up @@ -230,7 +238,7 @@ __Type = types.object({
kind = types.list(types.nonNull(__Type)),
resolve = function(kind)
if kind.__type == 'Object' then
return kind.interfaces
return kind.interfaces or {}
end
end
},
Expand Down
2 changes: 1 addition & 1 deletion graphql/core/validate.lua
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ local visitors = {

fragmentDefinition = {
enter = function(node, context)
kind = context.schema:getType(node.typeCondition.name.value) or false
local kind = context.schema:getType(node.typeCondition.name.value) or false
table.insert(context.objects, kind)
end,

Expand Down
4 changes: 4 additions & 0 deletions graphql/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ graphql.accessor_general = accessor_general
graphql.accessor_space = accessor_space
graphql.accessor_shard = accessor_shard
graphql.new = tarantool_graphql.new
graphql.compile = tarantool_graphql.compile
graphql.execute = tarantool_graphql.execute
graphql.start_server = tarantool_graphql.start_server
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No space format at all, got:

{
  "errors": [
    {
      "message": "./graphql/core/validate.lua:223: assign to undeclared variable 'kind'"
    }
  ]
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And with format too.

Copy link
Contributor Author

@SudoBobo SudoBobo Apr 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was the case? I added a test with user-configured tarantool graphql instance + server usage and it passed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I need to try again.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The problem was with global variables in grahql/core

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to track such problems: found problem with empty variables (it is converted to box.NULL and box.NULL or {} is not {}).

graphql.stop_server = tarantool_graphql.stop_server

graphql.TIMEOUT_INFINITY = accessor_general.TIMEOUT_INFINITY

Expand Down
123 changes: 123 additions & 0 deletions graphql/server/graphiql/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<html>
<head>
<style>
body {
height: 100%;
margin: 0;
width: 100%;
overflow: hidden;
}
#graphiql {
height: 100vh;
}
</style>

<script src="https://unpkg.com/react@15/dist/react.js"></script>
<script src="https://unpkg.com/react-dom@15/dist/react-dom.js"></script>

<!--
These two files can be found in the npm module, however you may wish to
copy them directly into your environment, or perhaps include them in your
favored resource bundler.
-->
<link rel="stylesheet" href="/static/css/graphiql.css" />
<script src="/static/js/graphiql.js"></script>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you forget to add these files into the commit?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Sorry. Added them.


</head>
<body>
<div id="graphiql">Loading...</div>
<script>
/**
* This GraphiQL example illustrates how to use some of GraphiQL's props
* in order to enable reading and updating the URL parameters, making
* link sharing of queries a little bit easier.
*
* This is only one example of this kind of feature, GraphiQL exposes
* various React params to enable interesting integrations.
*/
// Parse the search string to get url parameters.
var search = window.location.search;
var parameters = {};
search.substr(1).split('&').forEach(function (entry) {
var eq = entry.indexOf('=');
if (eq >= 0) {
parameters[decodeURIComponent(entry.slice(0, eq))] =
decodeURIComponent(entry.slice(eq + 1));
}
});
// if variables was provided, try to format it.
if (parameters.variables) {
try {
parameters.variables =
JSON.stringify(JSON.parse(parameters.variables), null, 2);
} catch (e) {
// Do nothing, we want to display the invalid JSON as a string, rather
// than present an error.
}
}
// When the query and variables string is edited, update the URL bar so
// that it can be easily shared
function onEditQuery(newQuery) {
parameters.query = newQuery;
updateURL();
}
function onEditVariables(newVariables) {
parameters.variables = newVariables;
updateURL();
}
function onEditOperationName(newOperationName) {
parameters.operationName = newOperationName;
updateURL();
}
function updateURL() {
var newSearch = '?' + Object.keys(parameters).filter(function (key) {
return Boolean(parameters[key]);
}).map(function (key) {
return encodeURIComponent(key) + '=' +
encodeURIComponent(parameters[key]);
}).join('&');
history.replaceState(null, null, newSearch);
}
// Defines a GraphQL fetcher using the fetch API. You're not required to
// use fetch, and could instead implement graphQLFetcher however you like,
// as long as it returns a Promise or Observable.
function graphQLFetcher(graphQLParams) {
// This example expects a GraphQL server at the path /graphql.
// Change this to point wherever you host your GraphQL server.
return fetch('/graphql', {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(graphQLParams),
credentials: 'include',
}).then(function (response) {
return response.text();
}).then(function (responseBody) {
try {
return JSON.parse(responseBody);
} catch (error) {
return responseBody;
}
});
}
// Render <GraphiQL /> into the body.
// See the README in the top level of this module to learn more about
// how you can customize GraphiQL by providing different values or
// additional child elements.
ReactDOM.render(
React.createElement(GraphiQL, {
fetcher: graphQLFetcher,
query: parameters.query,
variables: parameters.variables,
operationName: parameters.operationName,
onEditQuery: onEditQuery,
onEditVariables: onEditVariables,
onEditOperationName: onEditOperationName
}),
document.getElementById('graphiql')
);
</script>
</body>
</html>
136 changes: 136 additions & 0 deletions graphql/server/server.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
local fio = require('fio')
local utils = require('graphql.server.utils')
local json = require('json')
local check = require('graphql.utils').check

local server = {}

local default_charset = "utf-8"

local function file_mime_type(filename)
if string.endswith(filename, ".css") then
return string.format("text/css; charset=%s", default_charset)
elseif string.endswith(filename, ".js") then
return string.format("application/javascript; charset=%s", default_charset)
elseif string.endswith(filename, ".html") then
return string.format("text/html; charset=%s", default_charset)
elseif string.endswith(filename, ".svg") then
return string.format("image/svg+xml")
end

return "application/octet-stream"
end

local function static_handler(req)
local path = req.path

if path == '/' then
path = fio.pathjoin('graphiql', 'index.html')
end

local lib_dir = utils.script_path()
path = fio.pathjoin(lib_dir, path)
local body = utils.read_file(path)

return {
status = 200,
headers = {
['content-type'] = file_mime_type(path)
},
body = body
}
end

function server.init(graphql, host, port)
local host = host or '127.0.0.1'
local port = port or 8080
local httpd = require('http.server').new(host, port)

local function api_handler(req)
local body = req:read()

if body == nil or body == '' then
return {
status = 200,
body = json.encode(
{errors = {{message = "Expected a non-empty request body"}}}
)
}
end

local parsed = json.decode(body)
if parsed == nil then
return {
status = 200,
body = json.encode(
{errors = {{message = "Body should be a valid JSON"}}}
)
}
end

if type(parsed) ~= 'table' then
return {
status = 200,
body = json.encode(
{errors = {message = "Body should be a dictionary"}}
)
}
end

if parsed.query == nil or type(parsed.query) ~= "string" then
return {
status = 200,
body = json.encode(
{errors = {{message = "Body should have 'query' field"}}}
)
}
end

local variables = parsed.variables
if variables == nil then
variables = {}
end

if type(variables) == 'cdata' then
if variables == nil then
variables = {}
end
end

local query = parsed.query

local ok, compiled_query = pcall(graphql.compile, graphql, query)
if not ok then
return {
status = 200,
body = json.encode({errors = {{message = compiled_query}}})
}
end

local ok, result = pcall(compiled_query.execute, compiled_query, variables)
if not ok then
return {
status = 200,
body = json.encode({error = {{message = result}}})
}
end

result = {data = result}
return {
status = 200,
headers = {
[ 'content-type'] = "application/json; charset=utf-8"
},
body = json.encode(result)
}
end

httpd:route({ path = '/' }, static_handler)
httpd:route({ path = '/static/css/graphiql.css' }, static_handler)
httpd:route({ path = '/static/js/graphiql.js' }, static_handler)
httpd:route({ path = '/graphql' }, api_handler)

return httpd
end

return server
Loading