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

Commit 97a22b8

Browse files
committed
Feature: convert graphql query to avro schema
Extend query object: add `avro_schema` method, which produces Avro schema which can be used to verify or flatten any `query_exequte` result. Closes #7
1 parent 002856d commit 97a22b8

File tree

4 files changed

+396
-0
lines changed

4 files changed

+396
-0
lines changed

graphql/query_to_avro.lua

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
--- Module for convertion GraphQl query to Avro schema.
2+
---
3+
--- Random notes:
4+
---
5+
--- * The best way to use this module is to just call `avro_schema` methon on
6+
--- compiled query object.
7+
local path = "graphql.core"
8+
local introspection = require(path .. '.introspection')
9+
local query_util = require(path .. '.query_util')
10+
11+
-- module functions
12+
local query_to_avro = {}
13+
14+
-- use long types, to avoid overflow problems
15+
local gql_scalar_to_avro_index = {
16+
String = "string",
17+
Int = "long",
18+
Long = "long",
19+
Float = "double",
20+
Boolean = "boolean"
21+
}
22+
local function gql_scalar_to_avro(fieldType)
23+
assert(fieldType.__type == "Scalar", "GraphQL scalar field expected")
24+
local result = gql_scalar_to_avro_index[fieldType.name]
25+
assert(result ~= nil, "Unexpected scalar type: " .. fieldType.name)
26+
return result
27+
end
28+
29+
local collection_to_avro
30+
31+
local function complete_field_to_avro(fieldType, result, subSelections, context)
32+
local fieldTypeName = fieldType.__type
33+
34+
if fieldTypeName == 'List' then
35+
local innerType = fieldType.ofType
36+
local type = {
37+
type = "array",
38+
items = complete_field_to_avro(innerType, {}, subSelections,
39+
context).type
40+
}
41+
result.type = type
42+
return result
43+
end
44+
45+
if fieldTypeName == 'Scalar' or fieldTypeName == 'Enum' then
46+
return fieldType.serialize(result)
47+
end
48+
49+
if fieldTypeName == 'Object' then
50+
--result.name = "lol"
51+
result.type = collection_to_avro(fieldType, result,
52+
subSelections, context)
53+
return result
54+
--return collection_to_avro(fieldType, result, subSelections, context)
55+
elseif fieldTypeName == 'Interface' or fieldTypeName == 'Union' then
56+
local objectType = fieldType.resolveType(result)
57+
result.type = collection_to_avro(objectType, result,
58+
subSelections, context)
59+
return result
60+
end
61+
error('Unknown type "' .. fieldTypeName .. '" for field "' ..
62+
field.name .. '"')
63+
end
64+
65+
local function field_to_avro(objectType, object, fields, context)
66+
local firstField = fields[1]
67+
local fieldName = firstField.name.value
68+
local fieldType = introspection.fieldMap[fieldName] or
69+
objectType.fields[fieldName]
70+
assert(fieldType ~= nil)
71+
local subSelections = query_util.mergeSelectionSets(fields)
72+
local fieldTypeName = fieldType.kind.__type
73+
local innerType = fieldType.kind
74+
local nullable = true
75+
-- In case the field is nullable, the real type is in ofType attibute.
76+
if fieldTypeName == 'NonNull' then
77+
innerType = innerType.ofType
78+
nullable = false
79+
end
80+
local result = {}
81+
result.name = fieldName
82+
if fieldType.resolve ~= nil then
83+
-- not scalar
84+
return complete_field_to_avro(innerType, result, subSelections, context)
85+
end
86+
-- scalar type
87+
result.type = gql_scalar_to_avro(innerType)
88+
result.type = nullable and result.type .. "*" or result.type
89+
return result
90+
end
91+
92+
collection_to_avro = function(objectType, object, selections, context)
93+
local groupedFieldSet = query_util.collectFields(objectType, selections,
94+
{}, {}, context)
95+
local result= {
96+
type = 'record',
97+
name = objectType.name,
98+
fields = {}
99+
}
100+
if #context.namespace_parts ~= 0 then
101+
result.namespace = table.concat(context.namespace_parts, ".")
102+
end
103+
table.insert(context.namespace_parts, result.name)
104+
for _, fields in pairs(groupedFieldSet) do
105+
table.insert(result.fields,
106+
field_to_avro(objectType, object, fields, context))
107+
end
108+
context.namespace_parts[#context.namespace_parts] = nil
109+
return result
110+
end
111+
112+
--- The function creates an Avro schema for a given query.
113+
---
114+
--- @tparam table query object which avro schema should be created for
115+
---
116+
--- @treturn table `avro_schema` avro schema for any `query:execute()` result.
117+
function query_to_avro.convert(query)
118+
local state = query.state
119+
local context = query_util.buildContext(state.schema, query.ast, {},
120+
{}, query.operation_name)
121+
-- The variable is necessary to avoid fullname interferention.
122+
-- Each nested Avro record creates it's namespace.
123+
context.namespace_parts = {}
124+
local rootType = state.schema[context.operation.operation]
125+
return collection_to_avro(rootType, {},
126+
context.operation.selectionSet.selections,
127+
context)
128+
end
129+
130+
return query_to_avro

graphql/tarantool_graphql.lua

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ local schema = require('graphql.core.schema')
1414
local types = require('graphql.core.types')
1515
local validate = require('graphql.core.validate')
1616
local execute = require('graphql.core.execute')
17+
local query_to_avro = require('graphql.query_to_avro')
1718

1819
local utils = require('graphql.utils')
1920

@@ -466,6 +467,7 @@ local function gql_compile(state, query)
466467
local gql_query = setmetatable(qstate, {
467468
__index = {
468469
execute = gql_execute,
470+
avro_schema = query_to_avro.convert
469471
}
470472
})
471473
return gql_query

test/extra/suite.ini

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[default]
2+
core = app
3+
description = tests on features which are not related to specific executor
4+
lua_libs = ../common/lua/test_data_user_order.lua

0 commit comments

Comments
 (0)